前言
这里不讨论服务器分布式部署好处,服务并发处理的首要问题是:处理用户登录状态的一致性。
服务器环境,注意其中时间(手动编译的已经改到+8时区):
[]:~/tmp/dk# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b8dbf2f865e3 cffycls/php7:1.7 "php-fpm" 14 hours ago Up 14 hours 9000/tcp p3
de7a1b775409 cffycls/php7:1.7 "php-fpm" 14 hours ago Up 14 hours 9000/tcp p2
8cf40c35c24f cffycls/php7:1.7 "php-fpm" 14 hours ago Up 14 hours 9000/tcp p1
18363de84ddc cffycls/redis5:1.7 "redis-server /etc/r…" 22 hours ago Up 22 hours 0.0.0.0:6384->6379/tcp, 0.0.0.0:16384->16379/tcp cls5
12c37d446d94 cffycls/redis5:1.7 "redis-server /etc/r…" 22 hours ago Up 10 hours 0.0.0.0:6383->6379/tcp, 0.0.0.0:16383->16379/tcp cls4
530c32c6b60f cffycls/redis5:1.7 "redis-server /etc/r…" 22 hours ago Up 22 hours 0.0.0.0:6382->6379/tcp, 0.0.0.0:16382->16379/tcp clm5
ac9ccc6a15f3 cffycls/redis5:1.7 "redis-server /etc/r…" 22 hours ago Up 21 hours 0.0.0.0:6381->6379/tcp, 0.0.0.0:16381->16379/tcp clm4
347fd5658f36 cffycls/redis5:1.7 "redis-server /etc/r…" 22 hours ago Up 22 hours 0.0.0.0:6396->6379/tcp, 0.0.0.0:16396->16379/tcp cls3
cedc6ac4f33c cffycls/redis5:1.7 "redis-server /etc/r…" 22 hours ago Up 22 hours 0.0.0.0:6395->6379/tcp, 0.0.0.0:16395->16379/tcp cls2
182fc5a8594c cffycls/redis5:1.7 "redis-server /etc/r…" 22 hours ago Up 12 seconds 0.0.0.0:6394->6379/tcp, 0.0.0.0:16394->16379/tcp cls1
bdaca4f0f5cf cffycls/redis5:1.7 "redis-server /etc/r…" 22 hours ago Up 22 hours 0.0.0.0:6393->6379/tcp, 0.0.0.0:16393->16379/tcp clm3
52bff91c6993 cffycls/redis5:1.7 "redis-server /etc/r…" 22 hours ago Up 22 hours 0.0.0.0:6392->6379/tcp, 0.0.0.0:16392->16379/tcp clm2
a511272b5c7f cffycls/redis5:1.7 "redis-server /etc/r…" 22 hours ago Up 11 seconds 0.0.0.0:6391->6379/tcp, 0.0.0.0:16391->16379/tcp clm1
ed661d418d50 cffycls/redis5:1.7 "redis-server /etc/r…" 4 days ago Up 4 days 0.0.0.0:6380->6379/tcp rs
ffab7734ce36 cffycls/redis5:1.7 "redis-server /etc/r…" 4 days ago Up 4 days 0.0.0.0:6379->6379/tcp rm
5fe47b34bb7f openresty/openresty "/usr/bin/openresty …" 4 days ago Up 4 days 0.0.0.0:8082->80/tcp n2
a05f33ce3e8f openresty/openresty "/usr/bin/openresty …" 4 days ago Up 4 days 0.0.0.0:8080->80/tcp n1
2b4c57614d22 mysql:8.0 "docker-entrypoint.s…" 8 days ago Up 4 days 33060/tcp, 0.0.0.0:3308->3306/tcp ms
2635980cf576 mysql:8.0 "docker-entrypoint.s…" 8 days ago Up 4 days 0.0.0.0:3306->3306/tcp, 33060/tcp mm
即:1mysql主从+4redis主从集群+2nginx+3php。
1.提出问题
用户在浏览器登录记住密码7天,第一台机器的登录信息保存,期间可能会访问到多台机器,保持登录状态。
用户登录状态的需要保存,session如何同步?
2.方案选择
网上大神的实践方案很多,这里作选择测试。session是存储在服务器本地的,认为浏览器端cookie开启,不开起可以对环境设置session的url传参模式,但对于普通用户看来安全性降低了,原理是区别不大。
参考《分布式系统session一致性的问题》,主要有:
其一,session入库:把session信息添加到mysql表、redis中,如果本地不存在、可以通过cookie可以查询获取这些用户状态信息,但这会增加后端设计开发的复杂度;
第二,代理分发:主动映射服务器ip,用户只会访问到指定的服务器,但部署起来有些麻烦。
这里选择代理分发模式,服务器扩容的问题优先在部署方解决,整体逻辑上也比较简洁清晰。
3.session存入redis部署测试
这里2个nginx容器部署着相同的1对3-php的负载均衡,首先存入系统默认缓存。
a.存入系统缓存
#remote_host是公网地址(由于mysql未加入到之前mybridge网段,内网不通)
session_start();
$pdo = new \PDO("mysql:host=remote_host;dbname=test", "root", "123456");
$result = $pdo->query("SELECT username,last_login FROM `user` limit 0,1");
$user = $result->fetch(PDO::FETCH_ASSOC);
while(empty($user)){
$sql = sprintf("INSERT INTO `user` (`username`,`passwd`,`last_login`) VALUES ('%s', '%s', '%s')",
'cffycls', sha1(md5('123456')), date('Y-m-d H:i:s'));
$result = $pdo->query($sql);
$result = $pdo->query("SELECT username,last_login FROM `user` limit 0,1");
$user = $result->fetch(PDO::FETCH_ASSOC);
}
$serverInfo = [
'gethostbyname'=>gethostbyname(gethostname().'.'),
'REQUEST_TIME'=>date("Y-m-d H:i:s", $_SERVER['REQUEST_TIME']),
'HTTP_COOKIE'=>$_SERVER['HTTP_COOKIE'],
'HTTP_HOST'=>$_SERVER['HTTP_HOST'],
'SERVER_SOFTWARE'=>$_SERVER['SERVER_SOFTWARE'],
'SERVER_ADDR'=>$_SERVER['SERVER_ADDR'],
'SERVER_PORT'=>$_SERVER['SERVER_PORT'],
'REMOTE_ADDR'=>$_SERVER['REMOTE_ADDR'],
'REMOTE_PORT'=>$_SERVER['REMOTE_PORT'],
//'$_SERVER'=>$_SERVER,
];
$data = array_merge_recursive(['session_id'=>session_id()], $user, $serverInfo);
echo '';
print_r($data);
注:这里参考获取本机ip的方法,获取本机真实IP地址实例代码,方法二使用exec("ipconfig/ifconfig", $out, $stats),需要系统函数支持。
不断刷新页面,可以看到主机ip是变换的:
Array
(
[session_id] => kk324lubhgp35a7j8tbtpe5vj9
[username] => cffycls
[last_login] => 2019-07-07 08:10:18
[gethostbyname] => 172.1.1.13
[REQUEST_TIME] => 2019-07-07 08:32:42
[HTTP_COOKIE] => PHPSESSID=kk324lubhgp35a7j8tbtpe5vj9
[HTTP_HOST] => remote_host:8080
[SERVER_SOFTWARE] => nginx/1.15.8
[SERVER_ADDR] => 172.1.0.2
[SERVER_PORT] => 80
[REMOTE_ADDR] => xx.xx
[REMOTE_PORT] => 11026
)
b.主动存入redis数据库
为了后续方便,重构mysql容器加入mybridge网络并固定ip:172.1.11.11/172.1.12.12,并重新设置主从。
docker run --name mm --restart=always -p 3306:3306 \
--network=mybridge --ip=172.1.11.11 \
-v /root/tmp/dk/mysql/data:/var/lib/mysql \
-v /root/tmp/dk/mysql/config:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-d mysql:8.0
docker run --name ms --restart=always -p 3308:3306 \
--network=mybridge --ip=172.1.12.12 \
-v /root/tmp/dk/mysql_slave/data:/var/lib/mysql \
-v /root/tmp/dk/mysql_slave/config:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-d mysql:8.0
#登录从服务器容器,重置复制状态:
mysql> reset slave all;
mysql> change master to master_host='172.1.11.11',master_port=3306,master_user='repl',\
master_password='Ron_master_1',master_log_file='mysql-bin.000012',master_log_pos=621;
由于是采用集群的查询方式,需要单独建表存储,存取处理:判断session_id是否存在本地,然后确定查询redis,部分方法:
$servers = ['172.1.50.11:6379', '172.1.50.12:6379', '172.1.50.13:6379', '172.1.50.21:6379'];
//查出所有分片与槽的分布情况
$rs = [];
$slotNodes = [];
foreach ($servers as $addr){
//随机
$r = new Redis();
$server=explode(':',$addr);
$r->connect($server[0], (int) $server[1]);
$r->auth('123456');
$rs[$addr] = $r;
if(empty($slotInfo)){
//单一节点可以看到所有存在槽的节点
$slotInfo = $r->rawCommand('cluster','slots');
foreach ($slotInfo as $ix => $value){
$slotNodes[$value[2][0].':'.$value[2][1].' '.($ix+1)]=[$value[0], $value[1]];
}
}
}
//redis列表的存取方法
$crc = new \Predis\Cluster\Hash\CRC16();
$lRange = function ($key, $start, $end) use (&$slotNodes, &$crc, &$rs) {
$code = $crc->hash($key) % 16384;
foreach ($slotNodes as $addr => $boundry){
if( $code>=$boundry[0] && $code<=$boundry[1] ){
$host =explode(' ', $addr)[0];
return $rs[$host]->lRange($key, $start, $end);
}
}
};
$lPush = function ($key, $value) use (&$slotNodes, &$crc, &$rs) {
$code = $crc->hash($key) % 16384;
foreach ($slotNodes as $addr => $boundry){
if( $code>=$boundry[0] && $code<=$boundry[1] ){
$host =explode(' ', $addr)[0];
return $rs[$host]->lPush($key, $value);
}
}
};
目前mysql和redis未做同步。
c.被动Session.handler方式
参考官方《PHP Session handler》的被动方式介绍。
session.save_handler = redis
session.save_path = "tcp://172.1.50.11:6379?auth=123456&weight=1,tcp://172.1.50.12:6379?auth=123456&weight=1&timeout=2.5,\
tcp://172.1.50.13:6379?auth=123456&weight=1&timeout=2.5,tcp://172.1.50.21:6379?auth=123456&weight=2&timeout=2.5"
修改后cp -f复制同步,docker restart p1 p2 p3。
遇到问题:
Warning: session_start(): Failed to read session data: redis ...
Fatal error: Uncaught RedisException: MOVED 15832 172.1.50.13:6379 in ...
对于使用集群的问题,难以解决,于是使用172.1.12.11/12的主从作为专门的session服务器。
修改配置文件:
session.save_handler = redis
session.save_path = "tcp://172.1.12.11:6379?auth=123456"
刷新页面,登录172.1.12.11/rm容器的redis,查看
172.1.12.11:6379> keys *
1) "PHPREDIS_SESSION:kk324lubhgp35a7j8tbtpe5vj9"
进入php容器p1,清空session目录/tmp,刷新页面直到出现当前容器ip:172.1.1.11,重复刷新未看到新的session文件生成,说明页面session已取自专用的redis服务器了。
添加数组测试结果返回:
session_start();
echo '';
if(empty($_SESSION['testArray'])){
echo print_r('设置session数组
');
$_SESSION['testArray'] = ['name' => 'cffycls', 'setTime' => date("Y-m-d H:i:s")];
}
//连接redis
$redis = new redis();
$redis->connect('172.1.12.11', 6379);
$redis->auth('123456');
print_r($_SESSION);
$redis_save = $redis->get('PHPREDIS_SESSION:' . session_id());
print_r($redis_save);
print_r(json_decode($redis_save));
$serverInfo = [
'gethostbyname'=>gethostbyname(gethostname().'.'),
'REQUEST_TIME'=>date("Y-m-d H:i:s", $_SERVER['REQUEST_TIME']),
'HTTP_COOKIE'=>$_SERVER['HTTP_COOKIE'],
'HTTP_HOST'=>$_SERVER['HTTP_HOST'],
'SERVER_SOFTWARE'=>$_SERVER['SERVER_SOFTWARE'],
'SERVER_ADDR'=>$_SERVER['SERVER_ADDR'],
'SERVER_PORT'=>$_SERVER['SERVER_PORT'],
'REMOTE_ADDR'=>$_SERVER['REMOTE_ADDR'],
'REMOTE_PORT'=>$_SERVER['REMOTE_PORT'],
//'$_SERVER'=>$_SERVER,
];
$data = array_merge_recursive(['session_id'=>session_id()], $serverInfo);
print_r($data);
结果:
Array
(
[testArray] => Array
(
[name] => cffycls
[setTime] => 2019-07-07 15:33:00
)
)
testArray|a:2:{s:4:"name";s:7:"cffycls";s:7:"setTime";s:19:"2019-07-07 15:33:00";}Array
(
[session_id] => kk324lubhgp35a7j8tbtpe5vj9
[gethostbyname] => 172.1.1.11
[REQUEST_TIME] => 2019-07-07 16:06:55
[HTTP_COOKIE] => PHPSESSID=kk324lubhgp35a7j8tbtpe5vj9
[HTTP_HOST] => remote_host:8080
[SERVER_SOFTWARE] => nginx/1.15.8
[SERVER_ADDR] => 172.1.0.2
[SERVER_PORT] => 80
[REMOTE_ADDR] => xx.xx
[REMOTE_PORT] => 21060
)
这里redis-session存储由php自动处理序列化、反序列化操作,无需额外代码。
小结
后面的session.save_handeler=redis的被动处理方式,对于集群操作需要配置专用的主从服务器,使用比较方便。