(共556篇)
全部分类

zabbix系统监控系统前端简单实现读写分离的思路及解决办法
[ PHP ] 

环境

1
2
3
OS : Linux mysqldb3 3.10.0-957.el7.x86_64 #1 SMP Thu Nov 8 23:39:32 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
Mysql: Ver 8.0.23 for Linux on x86_64 (MySQL Community Server - GPL)
MySQL Router:  Ver 8.0.23 for Linux on x86_64 (MySQL Community - GPL)

zabbix的前端项目是用php写的,默认会根据配置文件连接默认的数据库,如果监控的设备多了,一个数据库肯定是不够的,容灾性也比较差,公司的项目使用了数据库集群,并利用mysqlrouter进行读写管理,具体的实现方式这里不提,只说如何修改zabbix项目,让项目能够实现读写分离.

先说说数据库的配置

1
2
3
4
10.10.20.70  mysqlrouter 所在的服务器  监听6446端口转发给主库, 监听6447端口轮询转发给从库
10.10.10.13  read-write  主库
10.10.10.12  read-only  从库1
10.10.10.15     read-only    从库2

zabbix的配置文件中,默认使用了6446的接口,也就是说,默认情况下,所有sql都会在主库中执行.

要想对sql的执行读写分离,就得在某些地方,对查询类的sql创建单独的连接.

zabbix对于sql的处理,有一个单独的文件/include/db.inc.php, 这个文件中封装了所有操作数据库的基础方法,比如

1
2
3
4
5
6
7
8
DBconnect
DBstart
DBend
DBclose
DBcommit
DBrollback
DBselect
DBexecute

这些函数从名字上基本上就能看出来它们的作用,简单又明了.

这里有两个函数DBselect,DBexecute,从代码上看,这两个函数内都接受了变量$query作为参数,且执行了类似mysqli_query()的操作(我这了以mysql为例, 实际上zabbix中支持了多种数据库).

根据经验,这两个函数将会是执行sql语句的主要函数, 所以我在上面哪些主要函数的最开始,添加了日志记录语句, 想要知道这些函数的执行顺序 (本地没有部署项目,服务器上也没来的及开启远程调试功能,只能用这种本方法了).

并在DBselect,DBexecute个函数中,分别把$query打印在自定义的日志文件中, 这是为了确认它们最终将会执行哪些类型的函数,

在查看日志的过程中发现:

  1. 每个接口的调用都会连接一次数据库
  2. 事务内的sql语句,有通过DBexecute执行的,也有通过DBselect执行的
  3. DBselect函数中执行的都是查询类sql,比如show, select等,非事务类的sql语句,大部分都通过它了

这就好办了,事务内的sql语句,不管读写肯定要保持是在同一个数据库上操作,主要修改的就是非事务类的sql语句执行问题了, 主要修改的就是DBselect函数.

思路如下:

  1. DBselect函数既能执行事务类的sql,又能执行非事务类的sql, 就对非事务类的sql语句单独挑出来处理(建立单独的新连接),
  2. DBstart函数中,在每次事务开始时,设置了DB['TRANSACTIONS']的值为1, 可以利用这个值判断DBselect函数执行的是事务类sql,还是非事务类sql
1
2
3
4
5
6
7
8
function DBselect($query){
 if(DB['TRANSACTIONS'] == 0){
    //    建立一个新的数据库连接
//    数据库的连接,zabbix提供了基础类,可以参考DBconnect中的方法, 但我们的项目用的是mysql,没必要和DBselect一样判断数据库类型,直接连接就可以
 }else{
    // 老的代码放这里
 }
}

这么一改就报错了, 因为非事务类的sql语句实在太多了,而且zabbix的项目结构导致了可能存在多个接口异步调用的情况, 结果只有一个: Too many connections to mysql!!!

转一下思路, 既然每个接口在调用时会创建一个接口内公用的数据库连接, 我能不能也在DBconnect中为非事务类的sql执行,创建一个单独的数据库连接呢?

DBconnectreturn true前面插入如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 处理DBR开始
    golbal $DBR;
    $db2 = new MysqlDbBackend();
    if ($DB['ENCRYPTION']) {
        $db2->setConnectionSecurity($DB['KEY_FILE'], $DB['CERT_FILE'], $DB['CA_FILE'], $DB['VERIFY_HOST'],
            $DB['CIPHER_LIST']
        );
    }
    $DBR = $db2->connect($DB['SERVER'], '6447', $DB['USER'], $DB['PASSWORD'], $DB['DATABASE'], $DB['SCHEMA']);
    $db2->init();
    // 处理DBR结束

DBselect也调整一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if ($DB['TRANSACTIONS'] == 0) {
        pp('不在事务中的DBselect');
        pp($query);
        $result = false;

        if (!$query = DBaddLimit($query, $limit, $offset)) {
            return false;
        }
        pp('b-------');
        if (!$DBR) {
            pp('没找到dbr');
              return false;
        } else {
            pp('找到了dbr');
        }
        // pp($DBR);

        if (!$result = mysqli_query($DBR, $query)) {
            pp('Error in query [' . $query . '] [' . mysqli_error($DBR) . ']', 'sql');
        }
        pp('e------');

        return $result;

    }

问题来了, 原本DBconnect中的这段代码报错了

1
2
3
4
5
6
if ($db->getError() || ($DB['ENCRYPTION'] && !$db->isConnectionSecure()) || !$db->checkDbVersion()
        || !$db->checkConfig()) {
        $error = $db->getError();
        pp('getError--' . $error); // 这段是我加上用来调试的,
        return false;
    }

这是因为DBconnect中创建默认连接时,有一个$db->init()函数执行了,这个函数执行过程中会调用DBselect,且是一个非事务内的sql语句.

而非事务内的sql语句,使用的是我们自己创建的第二个数据库连接, 执行$db->init()的时候,第二个数据库连接还没创建呢,导致了上面那段代码报错, 这就好办了, 把创建第二个数据库连接放到 $db->init()的前面执行,就可以了.

最终代码

下面是修改的文件及其改动内容,其他函数都不需要改动,理论上,直接复制代码覆盖原有函数即可.

哦,对了pp是自己封装的一个日志记录函数,用于调试的, 调试好了把函数内容注释掉即可.

/etc/zabbix/web/zabbix.conf.php

1
2
3
4
5
6
7
// 从库配置项
$DBR['SERVER'] = '127.0.0.1';
$DBR['PORT'] = '6447';
$DBR['DATABASE'] = 'zabbix';
$DBR['USER'] = 'zabbix';
$DBR['PASSWORD'] = 'password';
// 从库配置项 end

zabbix目录/include/class/core/CConfig.php

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
public function load()
    {
        if (!file_exists($this->configFile)) {
            self::exception('Config file does not exist.', self::CONFIG_NOT_FOUND);
        }
        if (!is_readable($this->configFile)) {
            self::exception('Permission denied.');
        }

        ob_start();
        include $this->configFile;
        ob_end_clean();

        if (!isset($DB['TYPE'])) {
            self::exception('DB type is not set.');
        }

        if (!array_key_exists($DB['TYPE'], self::$supported_db_types)) {
            self::exception(
                'Incorrect value "' . $DB['TYPE'] . '" for DB type. Possible values ' .
                implode(', ', array_keys(self::$supported_db_types)) . '.'
            );
        }

        $php_supported_db = array_keys(CFrontendSetup::getSupportedDatabases());

        if (!in_array($DB['TYPE'], $php_supported_db)) {
            self::exception('DB type "' . $DB['TYPE'] . '" is not supported by current setup.' .
                ($php_supported_db ? ' Possible values ' . implode(', ', $php_supported_db) . '.' : '')
            );
        }

        if (!isset($DB['DATABASE'])) {
            self::exception('DB database is not set.');
        }

        $this->setDefaults();

        // 从库配置保存到config
        if (!isset($DBR)) {
            self::exception('Slave database config not found');
        }
        $this->config['DBR'] = $DBR;

        // 从库配置保存到config end

        $this->config['DB']['TYPE'] = $DB['TYPE'];
        $this->config['DB']['DATABASE'] = $DB['DATABASE'];

        if (isset($DB['SERVER'])) {
            $this->config['DB']['SERVER'] = $DB['SERVER'];
        }

        if (isset($DB['PORT'])) {
            $this->config['DB']['PORT'] = $DB['PORT'];
        }

        if (isset($DB['USER'])) {
            $this->config['DB']['USER'] = $DB['USER'];
        }

        if (isset($DB['PASSWORD'])) {
            $this->config['DB']['PASSWORD'] = $DB['PASSWORD'];
        }

        if (isset($DB['SCHEMA'])) {
            $this->config['DB']['SCHEMA'] = $DB['SCHEMA'];
        }

        if (isset($DB['ENCRYPTION'])) {
            $this->config['DB']['ENCRYPTION'] = $DB['ENCRYPTION'];
        }

        if (isset($DB['VERIFY_HOST'])) {
            $this->config['DB']['VERIFY_HOST'] = $DB['VERIFY_HOST'];
        }

        if (isset($DB['KEY_FILE'])) {
            $this->config['DB']['KEY_FILE'] = $DB['KEY_FILE'];
        }

        if (isset($DB['CERT_FILE'])) {
            $this->config['DB']['CERT_FILE'] = $DB['CERT_FILE'];
        }

        if (isset($DB['CA_FILE'])) {
            $this->config['DB']['CA_FILE'] = $DB['CA_FILE'];
        }

        if (isset($DB['CIPHER_LIST'])) {
            $this->config['DB']['CIPHER_LIST'] = $DB['CIPHER_LIST'];
        }

        if (isset($DB['DOUBLE_IEEE754'])) {
            $this->config['DB']['DOUBLE_IEEE754'] = $DB['DOUBLE_IEEE754'];
        }

        if (isset($DB['VAULT_URL'])) {
            $this->config['DB']['VAULT_URL'] = $DB['VAULT_URL'];
        }

        if (isset($DB['VAULT_DB_PATH'])) {
            $this->config['DB']['VAULT_DB_PATH'] = $DB['VAULT_DB_PATH'];
        }

        if (isset($DB['VAULT_TOKEN'])) {
            $this->config['DB']['VAULT_TOKEN'] = $DB['VAULT_TOKEN'];
        }

        if (isset($ZBX_SERVER)) {
            $this->config['ZBX_SERVER'] = $ZBX_SERVER;
        }
        if (isset($ZBX_SERVER_PORT)) {
            $this->config['ZBX_SERVER_PORT'] = $ZBX_SERVER_PORT;
        }
        if (isset($ZBX_SERVER_NAME)) {
            $this->config['ZBX_SERVER_NAME'] = $ZBX_SERVER_NAME;
        }

        if (isset($IMAGE_FORMAT_DEFAULT)) {
            $this->config['IMAGE_FORMAT_DEFAULT'] = $IMAGE_FORMAT_DEFAULT;
        }

        if (isset($HISTORY)) {
            $this->config['HISTORY'] = $HISTORY;
        }

        if (isset($SSO)) {
            $this->config['SSO'] = $SSO;
        }

        if ($this->config['DB']['VAULT_URL'] !== ''
            && $this->config['DB']['VAULT_DB_PATH'] !== ''
            && $this->config['DB']['VAULT_TOKEN'] !== '') {
            list($this->config['DB']['USER'], $this->config['DB']['PASSWORD']) = $this->getCredentialsFromVault();

            if ($this->config['DB']['USER'] === '' || $this->config['DB']['PASSWORD'] === '') {
                self::exception(_('Unable to load database credentials from Vault.'), self::CONFIG_VAULT_ERROR);
            }
        }

        $this->makeGlobal();

        return $this->config;
    }

public function makeGlobal()
    {
        global $DB, $DBR, $ZBX_SERVER, $ZBX_SERVER_PORT, $ZBX_SERVER_NAME, $IMAGE_FORMAT_DEFAULT, $HISTORY, $SSO;

        $DB = $this->config['DB'];

        // 第二个数据库连接保存到全局变量
        $DBR = $this->config['DBR'];
        // 第二个数据库连接保存到全局变量 end

        $ZBX_SERVER = $this->config['ZBX_SERVER'];
        $ZBX_SERVER_PORT = $this->config['ZBX_SERVER_PORT'];
        $ZBX_SERVER_NAME = $this->config['ZBX_SERVER_NAME'];
        $IMAGE_FORMAT_DEFAULT = $this->config['IMAGE_FORMAT_DEFAULT'];
        $HISTORY = $this->config['HISTORY'];
        $SSO = $this->config['SSO'];
    }

zabbix目录/include/db.inc.php

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
function pp($content)
{
    file_put_contents('/usr/share/zabbix/dev.log', 
        json_encode(
            $content, 
            JSON_UNESCAPED_UNICODE
          ).PHP_EOL.PHP_EOL, 
        FILE_APPEND
      );
}

function DBconnect(&$error)
{
    global $DB, $DBR;
    pp('DBconnect');

    if (isset($DB['DB'])) {
        $error = _('Cannot create another database connection.');
        return false;
    }

    $DB['DB'] = null; // global db handler
    $DB['TRANSACTIONS'] = 0; // level of a nested transaction
    $DB['TRANSACTION_NO_FAILED_SQLS'] = true; // true - if no statements failed in transaction, false - there are failed statements
    $DB['SELECT_COUNT'] = 0; // stats
    $DB['EXECUTE_COUNT'] = 0;

    if (!isset($DB['TYPE'])) {
        $error = 'Unknown database type.';
        return false;
    }

    $db_types = [
        ZBX_DB_MYSQL => MysqlDbBackend::class,
        ZBX_DB_POSTGRESQL => PostgresqlDbBackend::class,
        ZBX_DB_ORACLE => OracleDbBackend::class,
    ];

    if (!array_key_exists($DB['TYPE'], $db_types)) {
        $error = 'Unsupported database';
        return false;
    }

    $db = new $db_types[$DB['TYPE']];

    if ($DB['ENCRYPTION']) {
        $db->setConnectionSecurity($DB['KEY_FILE'], $DB['CERT_FILE'], $DB['CA_FILE'], $DB['VERIFY_HOST'],
            $DB['CIPHER_LIST']
        );
    }

    $DB['DB'] = $db->connect($DB['SERVER'], $DB['PORT'], $DB['USER'], $DB['PASSWORD'], $DB['DATABASE'], $DB['SCHEMA']);

    // 建立从库连接
    if (isset($DBR['DB'])) {
        $error = _('Cannot create another slave database connection.');
        return false;
    }
    $DBR['DB'] = null;
    $DBR['SELECT_COUNT'] = 0; // stats
    $DBR['EXECUTE_COUNT'] = 0;

    $dbr = new MysqlDbBackend();
    if ($DB['ENCRYPTION']) {
        $dbr->setConnectionSecurity($DB['KEY_FILE'], $DB['CERT_FILE'], $DB['CA_FILE'], $DB['VERIFY_HOST'],
            $DB['CIPHER_LIST']
        );
    }
    $DBR['DB'] = $dbr->connect($DBR['SERVER'], $DBR['PORT'], $DBR['USER'], $DBR['PASSWORD'], $DBR['DATABASE'], $DB['SCHEMA']);
    if ($DBR['DB']) {
        $dbr->init();
    }

    if ($dbr->getError() || ($DB['ENCRYPTION'] && !$dbr->isConnectionSecure()) || !$dbr->checkDbVersion()
        || !$dbr->checkConfig()) {
        $error = $dbr->getError();
        pp('dbr connect error--' . $error);
        return false;
    }
    // 建立从库连接 end

    if ($DB['DB']) {
        $db->init();
    }

    if ($db->getError() || ($DB['ENCRYPTION'] && !$db->isConnectionSecure()) || !$db->checkDbVersion()
        || !$db->checkConfig()) {
        $error = $db->getError();
        pp('db connect error--' . $error);
        return false;
    }

    return true;
}


function DBclose()
{
    global $DB, $DBR;
    pp('DBclose');

    // 关闭DBR
    if (isset($DBR['DB'])) {
        mysqli_close($DB['DB']);
    }
    unset($DBR['DB']);
    // 关闭DBR end

    $result = false;

    if (isset($DB['DB']) && !empty($DB['DB'])) {
        switch ($DB['TYPE']) {
            case ZBX_DB_MYSQL:
                $result = mysqli_close($DB['DB']);
                break;
            case ZBX_DB_POSTGRESQL:
                $result = pg_close($DB['DB']);
                break;
            case ZBX_DB_ORACLE:
                $result = oci_close($DB['DB']);
                break;
        }
    }
    unset($DB['DB']);
    return $result;
}


function DBselect($query, $limit = null, $offset = 0)
{
    global $DB, $DBR;

    // 处理非事务内的sql语句,使用从库连接池
    if ($DB['TRANSACTIONS'] == 0) {
        $result = false;

        if (!$query = DBaddLimit($query, $limit, $offset)) {
            return false;
        }
        if (!$DBR) {
            pp('没找到dbr');
            return false;
        } else {
            pp('找到了dbr');
        }
        // pp($DBR);

        if (!$result = mysqli_query($DBR['DB'], $query)) {
            pp('Error in query [' . $query . '] [' . mysqli_error($DBR['DB']) . ']', 'sql');
        }

        return $result;

    }
    // 处理非事务内的sql语句,使用从库连接池 end

    $result = false;

    if (!isset($DB['DB']) || empty($DB['DB'])) {
        return $result;
    }

    // add the LIMIT clause
    if (!$query = DBaddLimit($query, $limit, $offset)) {
        return false;
    }

    $time_start = microtime(true);
    $DB['SELECT_COUNT']++;

    switch ($DB['TYPE']) {
        case ZBX_DB_MYSQL:
            if (!$result = mysqli_query($DB['DB'], $query)) {
                error('Error in query [' . $query . '] [' . mysqli_error($DB['DB']) . ']', 'sql');
            }
            break;
        case ZBX_DB_POSTGRESQL:
            if (!$result = pg_query($DB['DB'], $query)) {
                error('Error in query [' . $query . '] [' . pg_last_error() . ']', 'sql');
            }
            break;
        case ZBX_DB_ORACLE:
            if (!$result = oci_parse($DB['DB'], $query)) {
                $e = @oci_error();
                error('SQL error [' . $e['message'] . '] in [' . $e['sqltext'] . ']', 'sql');
            } elseif (!@oci_execute($result, ($DB['TRANSACTIONS'] ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS))) {
                $e = oci_error($result);
                error('SQL error [' . $e['message'] . '] in [' . $e['sqltext'] . ']', 'sql');
            }
            break;
    }

    // $result is false only if an error occurred
    if ($DB['TRANSACTION_NO_FAILED_SQLS'] && !$result) {
        $DB['TRANSACTION_NO_FAILED_SQLS'] = false;
    }

    if (CApiService::$userData !== null && array_key_exists('debug_mode', CApiService::$userData)
        && CApiService::$userData['debug_mode'] == GROUP_DEBUG_MODE_ENABLED) {
        CProfiler::getInstance()->profileSql(microtime(true) - $time_start, $query);
    }
    // }

    return $result;
}