(高并发探测)二、分布式场景常见问题之session同步

前言

这里不讨论服务器分布式部署好处,服务并发处理的首要问题是:处理用户登录状态的一致性。
服务器环境,注意其中时间(手动编译的已经改到+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的被动处理方式,对于集群操作需要配置专用的主从服务器,使用比较方便。

你可能感兴趣的:(php,session)