Xiuno BBS 审计之问题:无读写分离,读压主库
贰先生 5小时前

此文章为XIUNOX版本重构审计时发现问题,XIUNOX版本已优化修复此问题。分享出来方便后续想基于xiuno bbs4.0.4版本制作维护版本或插件模板等需求的开发者和站长参考。

现象

Xiuno BBS 4.0.4 在 db_mysql 类中提供了主从连接能力(master + slaves),但默认配置 'slaves' => array() 为空,从库连接回退到主库连接($this->rlink = $this->wlink)。业务层调用 db_find_one/db_find/db_count/db_maxid 等读操作时,虽然 db_mysql::find_one 内部会调用 connect_slave() 获取读连接,但由于 slaves 为空,所有读请求最终都打到主库。系统未强制要求生产环境配置 slaves,也未提供读请求路由到从库的明确策略,导致"支持主从"形同虚设。

源码证据

1. conf.default.php slaves 默认为空数组

文件:xiunobbs_4.0.4/conf/conf.default.php 第 12-37 行

'db' => array (
    'type' => 'mysql',
    'mysql' => array (
        'master' => array (
            'host' => 'localhost',
            'user' => 'root',
            'password' => 'root',
            'name' => 'test',
            'tablepre' => 'bbs_',
            'charset' => 'utf8',
            'engine' => 'innodb',
        ),
        'slaves' => array (),                          // 默认空,无从库
    ),
    'pdo_mysql' => array (
        'master' => array (
            // ...
        ),
        'slaves' => array (),                          // 同样为空
    ),
),

2. db_mysql::connect_slave 在 slaves 为空时回退到 master 连接

文件:xiunobbs_4.0.4/xiunophp/db_mysql.class.php 第 36-50 行

// 连接从服务器,如果有多台,则随机挑选一台,如果为空,则与主服务器一致。
public function connect_slave() {
    if($this->rlink) return $this->rlink;
    if(empty($this->conf['slaves'])) {
        if($this->wlink === NULL) $this->wlink = $this->connect_master();
        $this->rlink = $this->wlink;                  // 回退到主连接
        $this->rconf = $this->conf['master'];
    } else {
        $n = array_rand($this->conf['slaves']);
        $conf = $this->conf['slaves'][$n];
        $this->rconf = $conf;
        $this->rlink = $this->real_connect($conf['host'], $conf['user'], $conf['password'], $conf['name'], $conf['charset'], $conf['engine']);
    }
    return $this->rlink;
}

3. db.func.php 读函数虽传 d参数,但默认NULL回退到_SERVER['db'] 主连接

文件:xiunobbs_4.0.4/xiunophp/db.func.php 第 60-99 行

function db_sql_find($sql, $key = NULL, $d = NULL) {
    $db = $_SERVER['db'];                              // 主连接
    $d = $d ? $d : $db;                                // 默认用主
    if(!$d) return FALSE;
    $arr = $d->sql_find($sql, $key);
    db_errno_errstr($arr, $d, $sql);
    return $arr;
}

function db_count($table, $cond = array(), $d = NULL) {
    $db = $_SERVER['db'];
    $d = $d ? $d : $db;
    if(!$d) return FALSE;
    $r = $d->count($d->tablepre.$table, $cond);
    db_errno_errstr($r, $d);
    return $r;
}

业务调用几乎从不传 $d 参数:

文件:xiunobbs_4.0.4/model/user.func.php 第 27 行

function user__read($uid) {
    $user = db_find_one('user', array('uid'=>$uid));   // 不传 $d,用主连接
    return $user;
}

全站 db_find_onedb_finddb_countdb_maxid 调用均不显式传从库连接,依赖 db_mysql::find_one 内部调用 connect_slave()。但 connect_slave() 在 slaves 为空时返回主连接,读请求实际打到主库。

4. db_mysql 类内部 find_one 调用 connect_slave 但被 slaves 空配置抵消

文件:xiunobbs_4.0.4/xiunophp/db_mysql.class.php(类内部 find_one 实现,Grep 命中第 1 行 trigger_error

读方法虽调用 $this->connect_slave() 获取读连接,但 slaves 配置为空时 connect_slave() 返回 $this->wlink(主连接),读写分离失效。

5. 业务层无强制读写分离策略

Grep 全站无 connect_slave 显式调用,无 db_find_one(..., $slave_db) 传从库连接的代码模式。读写分离完全依赖 db_mysql 类内部隐式行为,业务无感知。

6. 写后立即读可能读到从库延迟数据

即使配置了 slaves,db_find_one 读从库但 db_insert/db_update 写主库,写后立即读(如注册后登录)可能因主从延迟读到旧数据,系统未提供"读主"开关。

风险等级与结论

架构缺陷(中)

危害:

  • 默认配置 slaves 为空,所有读请求压主库,主库成为性能瓶颈
  • 业务层无读写分离意识,无法显式指定从库读或强制读主
  • "支持主从"为伪能力,需手工修改 conf.php 才能启用,文档未强制要求
  • 写后立即读无"读主"开关,主从延迟下数据不一致
  • 从库连接随机挑选(array_rand),无健康检查、无故障转移
  • 从库不可用时无降级策略(连接失败直接报错)

修复建议:

  • 默认配置示例改为提供 slaves 占位项(注释说明),引导生产部署配置从库
  • 业务层引入 db_read_one()(强制从库)/ db_read_one_master()(强制主库)双接口
  • 写后读场景(注册后登录)自动路由到主库(基于 cookie/标记)
  • 从库连接增加健康检查与故障转移,从库不可用时降级到主库并告警
  • 配置层支持多从库权重(weight 字段),按权重负载均衡
最新回复 (0)
全部楼主
返回