公司最近需要一个扩展性良好的Mysql主从分离方案,老大要求对业务层的代码改动要小。
业务层代码改动小,意味着要在连接层直接把读/写请求分发,而不是到业务层通过”SQL识别”的来分离。
脑子里有两套方案, Mysqlnd_ms 和 Mysql-Proxy。两个方案各有优/缺点
Mysqlnd_ms
优点:
1. PHP官方推荐的扩展,PECL安装方便
- 基于PHP扩展对SQL进行解析,进而分离请求,对业务层代码改动小,性能损失也小
- 扩展本身已经在社区打滚有些年头了,较稳定,口碑好
- 入门级文档清晰,易配置易上手
- 有连接池
- 功能强大,支持SQL Hint, 事务,最终/Session/强一致性等场景
缺点:
1. 基于扩展的封装,扩展的实现对上层透明。扩展本身缺乏debug工具, 所以真遇到问题可能比较难查
- Failover 策略较基础
- 缺乏中文文档
- 强一致性的场景会有些坑
Mysql-Proxy
优点:
1. 来自Mysql官方的解决方案
- 性能好,资源占用少
- 有连接池
- 配置简单/文档众多
- 应有广泛,口碑好
缺点:
1. 需要第三方脚本(Lua)支持
鉴于”对业务层代码改动要小”这个需求,以及笔者对Mysql-Proxy 使用较少,所以在简单比较之后,果断选择了Mysqlnd_ms。
安装
1. wget http://pecl.php.net/get/mysqlnd_ms-1.5.2.tgz
2. tar xzvf mysqlnd_ms-1.5.2.tgz
3. cd mysqlnd_ms-1.5.2
4. /path/to/phpize
5. ./configure --enable-mysqlnd-ms --with-php-config=/usr/local/php/bin/php-config
6. make
7. make install
8. sudo /etc/init.d/php-fpm restart
9. /path/to/php -m | grep mysql #看到"mysqlnd_ms"扩展表示安装成功
配置
1. 创建配置文件 /usr/local/etc/php/php.d/mysqlnd_ms.ini
extension="/usr/local/php-5.5.9/lib/php/extensions/no-debug-non-zts-20121212/mysqlnd_ms.so"
; 启动mysqlnd_ms
mysqlnd_ms.enable=1
; 加载的主从配置文件
mysqlnd_ms.config_file="/usr/local/etc/php/php.d/mysqlnd_ms.conf"
; 是否禁用读写分离
mysqlnd_ms.disable_rw_split=0
; mysqlnd 日志输出格式(线上环境可选)
mysqlnd.debug="d:t:x:A,/tmp/mysqlnd.trace"
;SERVER_QUERY_NO_GOOD_INDEX_USED=16
;SERVER_QUERY_NO_INDEX_USED=32
;SERVER_QUERY_WAS_SLOW=1024
; 日志掩码
mysqlnd.log_mask=1072
; 网络读缓存大小
; 64K
mysqlnd.net_read_buffer_size=65536
; 网络读等待时间
mysqlnd.net_read_timeout=600
2. 创建主从策略文件 /usr/local/etc/php/php.d/mysqlnd_ms.conf (位置由mysqlnd_ms.ini指定)
Demo 主从写成了同一个库
{
"master-slave": { // section 对应一组服务
"master": { // 主库配置(默认主库只包含一项)
"master_0": {
"host": "192.168.100.100",//host必选 其他可为空。 扩展优先使用本文件中的配置,如果为空,再去使用php连接mysql时使用的参数
"port": "3306",
"socket": "\/var\/data\/mysql\/mysql.sock",
"db": "master-slave",
"user": "root",
"password": "",
"connect_flags": 0
}
},
"slave": { // 从库配置(可配置多项,slave为空会在php处报warning)
"slave_0": {
"host": "192.168.100.101",
"port": "3306",
"socket": "\/var\/data\/mysql\/mysql.sock",
"db": "master-slave",
"user": "root",
"password": "",
"connect_flags": 0
},
"slave_1": {
"host": "192.168.100.102",
"port": "3306",
"socket": "\/var\/data\/mysql\/mysql.sock",
"db": "master-slave",
"user": "root",
"password": "",
"connect_flags": 0
},
},
"lazy_connections": 1, //只在执行sql之前才连接数据库
"server_charset" : "utf8" //服务端编码
}
}
/etc/init.d/php-fpm restart #重启fpm生效
3. 一些基本的参数说明
| 参数名称 | 描述 |
|---|---|
| master | 主库配置 |
| host | ip or hostname |
| port | 端口 |
| socket | 连接套接字 |
| db | 库名字 |
| user | 用户名 |
| password | 用户密码 |
| connect_flags | 连接参数 |
| slave | 从库配置 |
| global_transaction_id_injection | GTID的配置 详见 http://cn2.php.net/manual/zh/mysqlnd-ms.plugin-ini-json.php |
| filter | 选择从库的策略 random/roundrobin/user/user_multi/node_groups 策略详情详见http://cn2.php.net/manual/zh/mysqlnd-ms.plugin-ini-json.php |
| failover | 故障转移 disabled (默认), master(读从失败就连主库),loop_before_master(读从失败轮寻其他从库再读主库) |
| lazy_connections | 被动连接 |
| server_charset | 服务端字符集 |
| trx_stickiness | 事务控制策略 |
使用
- 通常情况下,使用mysqlnd_ms 不需要改动业务端的代码,只需要在连接数据库时,使用配置中的host,即可实现主从分离,如下:
/* Master */ foreach( $dbh -> query ( 'show tables' ) as $row ) { print_r ( $row ); }/* Slave */ foreach( $dbh -> query ( ‘SELECT * from test LIMIT 1’ ) as $row ) { print_r ( $row ); }
$dbh = null ; } catch ( PDOException $e ) { print “Error!: ” . $e -> getMessage () . ”
” ; die(); } catch (Exception $e) { var_dump($e); }- 采取主从分离,就一定存在着同步延迟的问题。 在一个繁忙的服务中,主/从同步会存在延迟。mysqlnd_ms 将这种场景细分为:
- 最终一致性:用户可能无法立即看到自己写入的数据
- session一致性:用户自己可以立即看到自己写入的数据
- 强一致性:所有的用户都可以立即看到其他用户的写入的数据
mysqlnd_ms 针对不同的场景,提供不同的策略。如果对延迟比较敏感(如:用户提交表单后,页面立即跳转展示用户提交的数据),在编码的过程中还需要参考下述文档
- SQL Hints: http://cn2.php.net/manual/zh/mysqlnd-ms.quickstart.sqlhints.php
- 事务: http://cn2.php.net/manual/zh/mysqlnd-ms.quickstart.transactions.php
- 服务级别和一致性: http://cn2.php.net/manual/zh/mysqlnd-ms.quickstart.qos-consistency.php
- Global transaction IDs (GTID):http://cn2.php.net/manual/zh/mysqlnd-ms.quickstart.gtid.php
- Cache integration: http://cn2.php.net/manual/zh/mysqlnd-ms.quickstart.cache.php
一些陷阱
- Mysqlnd_ms 的行为是这样的。 所有的Select 操作会访问从库, 除Select 其他的所有操作都访问主库,包括(SHOW, SET等)。
所以习惯使用mysql变量来编程的同学要注意了,如:set @id=2;(主库) select * from table where id:=@id(从库);
上面的第二条语句会因为在从库找不到id这个变量而报错, 这种情况要SQL Hint 给Mysql一些提示
- Mysqlnd_ms 并不支持 mysqli 的multi_statements。 如使用mysqli_multi_query() 来批量执行SQL
$sql = "SELECT * FROM {$table} WHERE .."; $sql .= "INSERT INTO {$table}"; $mysqli -> multi_query ($sql);上面的第一条SQL会被识别为访问从库,Mysqlnd_ms 无法拆分一个批量操作,所以第二条Insert 也会进从库而造成数据不一致。
- 事务的默认行为也和上述第二点一样,无法拆分一个事务,所以”读/写”请求尽量不要写进同一个事务中。如果一定要写,要使用SQL Hint 给Mysqlnd_ms 一些提示。