概述
项目使用Swoole+Redis,更新迭代了4版,从最初的消息提醒、到后来的客服系统,它终于慢慢的长大了,具备了可持续推送的能力和动力,我要总结一下其中的酸甜苦辣。
PHP的运行模式的生命周期来讲一种有2种,一种是fpm的,这种模式下,主要编写Web服务,一种是Cli模式的,Swoole是php7版本之后的神作,可以说它开启了php也可以提供更多的网络服务的可能。
学习了源码后,发现php7真的是划时代的一个产品,重构了数组(从物理Hash链路调整成逻辑链结构)、字符串(二进制安全)、新增了Ast(抽象语法树)还有很多,主要提升了性能,说的有点多,回到项目本身。
项目我愿意把它定义成PMQ,主要的功能就是推送+拉取,尽量的精简以免后期维护上的麻烦,选取EasySwoole这个框架,因为当时慕课网上有视频教程,而且文档丰富,加入群有问题可以及时反馈给开发作者。
使用WebSocket服务来推送消息,WebSocket是一个建立在Http协议上的全双工通信协议,掌握以后发现还是挺简单,在这之前做了大量的学习和积累,对我的成长帮助非常大,做一个东西往往要深挖背后的原理,掌握好原理在去指导实践事半功倍,其实我当时也是瑟瑟发抖,没有心情考虑那么多,第一个想法就是实现功能。
失败和解决问题
第一次主要的功能是心跳、登陆、拉取未读消息数、评论和通知,消息确认几个功能。
第一次上线就,直接失败了,并发量太大,只上线了半个小时,Mysql没有扛住直接就挂了,服务紧急叫停,查找原因,当时正在年前,正好春节因为疫情也没有回家,就查找原因,进行下一次尝试。
1.添加缓存,更改缓存失效算法
从垮掉的Mysql开始修复,首先判断是这个原因,在登陆验证用户身份时请求主站的地方,做了一层缓存,主站过期的策略是7300s,为了防止同一时刻回收缓存引起雪崩,过期策略采用固定时长+随机过期的方法。
public function getUserId(string $token) :int
{
$uid = \EasySwoole\RedisPool\RedisPool::invoke(function (\EasySwoole\Redis\Redis $redis) use ($token) {
$uid = $redis->get($token);
if (!isset($uid) || empty($uid)) {
//远程验证token
$uid = OAuth::getUserInfo($token);
if (isset($uid) && !empty($uid) && intval($uid) > 0) {
//存入缓存时间,过期时间小于 7300s
$expireTime = 3650 + rand(1, 3000);
$redis->setEx($token, $expireTime, $uid);
}
}
return $uid;
}, self::REDIS_CONN_NAME);
return intval($uid);
}
2.函数内实现功能
当时对Swoole的新特性不太清楚,好像也报了几个跨协程调用的错误,为了保证服务的稳定和可靠,都把改成自己函数处理,当时有点惊弓之鸟。
还把所有的Redis链接池的defer模式改成了invoke模式,invoke模式的特点就是使用完成后立刻回收资源,defer模式是等执行完成后统一回收,区别是这点。
后来V1不那么完美,但是成功上线了。
计数
V2的功能只是在基础通信的基础上添加了计数功能,这版本平平无奇,只是加了个计数器的功能,很平静。
设计方案使用字符串为每个使用的人单独做key,在接收到主站的http请求时,未读数+1,读取消息数后清零。
在使用Crontab里的计划任务执行队列里的消息 1000/分,因为主站的消息没有事实请求我的服务,所以只能采用折中的方案,有一定的延迟。
public function commentsCounter(int $toUid, int $commentUid)
{
if ($toUid == $commentUid || empty($toUid) || empty($commentUid)) return false;
\EasySwoole\RedisPool\RedisPool::invoke(function (\EasySwoole\Redis\Redis $redis) use ($toUid, $commentUid) {
//收到评论数 +1
$redis->incr(Category::_getUnReadFromCommentsKeyName($toUid));
//更新消息未读数
$redis->lPush(self::PUSH_UNREAD_NUMBER_All, $toUid);
$redis->lPush(self::PUSH_UNREAD_NUMBER_All, $commentUid);
}, self::REDIS_CONN_NAME);
}
客服IM
客服功能是一个绝对优化和升华的精品项目,进行了大刀阔斧的改革,重构、设计方案、优化方案都得到了质的飞跃。
1.链接到WebSocket服务,进行用户验证(第一版功能)
2.进行链接客服管理员 【建立 - 通信 - 结束】 ,建立一次通信的生命周期过程和流程。
客服的user_id,使用的是虚拟ID,66666666做为起始值(理论上当公司用户发展到6千万或者客服发展到3千人的时候,会出现问题)。
3.分配方法:上次聊天的客服管理员优先分配,第一次咨询的用户随机分配。
4.支持离线消息/消息确认/聊天记录/发送图片等等功能
5.离线消息按用户和管理员关系进行,1对1分配。
6.重构和优化,重构了第一版中函数的代码冗余,优化了初始化的路由层。
下面是代码细细说部分:
1.关于服务的优化
Swoole的高效在于预加载和常驻内存的特点,所以Swoole服务的热启动只支持Controller部分,所以对路由层做了一个大的整改,提高可用性。
把所有对路由的参数交给ForwardRoute类,去处理和控制参数,是路由变的灵活,可自动重启。
/**
* 解析器避免高耦合,从解析器开始分发请求,使控制器分离
*/
public function decode($raw, $client): ?Caller
{
$caller = new Caller();
$this->data = json_decode($raw, true);
$toolRoute = new ForwardRoute($this->data);
$controllerRoute = $toolRoute->_getRouter();
$this->action = $toolRoute->_getAction();
$this->body = $toolRoute->_formatBody();
$caller->setControllerClass($controllerRoute);
$caller->setAction($this->action);
$caller->setArgs($this->body);
return $caller;
}
2.停止服务时,添加onShutdown方法,完善清理fd旧关系数据
$register->set(EventRegister::onShutdown,function (\swoole_server $server ) use ($websocketEvent) {
$websocketEvent->onShutdown($server);
});
3.分离Server层
新建Server层的原因有两个,减少代码冗余,将函数分离出来还有一个好处,函数在执行完以后,回收内存,尽可能的减少内存开销。
├── ChangPeiServer
│ └── UserServer.php
├── MysqlServer
│ └── PushMsg.php
├── RedisServer
│ ├── CountServer.php
│ ├── FdServer.php
│ └── QueueServer.php
├── Server.php
└── WebSocketServer
└── PushServer.php
4.消费消息的双队列(这是第一版内容)
消费队列设计了快慢两个队列进行消费,快速队列6分支为一组,设置超时时间,如果用户超过1天不上线,放入延迟队列,延迟队列最多保持15天,15天后系统进行回收。
在使用Nosql缓存时,一定要注意的是内存的回收,设置超时时间。
$redis = \EasySwoole\RedisPool\RedisPool::defer('redis');
$server = ServerManager::getInstance()->getSwooleServer();
//每分钟消费limit条
$data = $redis->lRange(self::PUSH_MSG_COMMENT_LISTS, 0, $this->limit );
if (!empty($data) && is_array($data)){
$pushLists = [];
$lRemList = [];
foreach ($data as $json){
$msgPushInfo = json_decode($json,true);
if(isset($msgPushInfo['to_uid']) && !empty($msgPushInfo['to_uid'])){
$delayList = [];
//用户超过1天不上线,放入延迟队列
$diff_unix_times = time() - $msgPushInfo['create_time'];
if( $diff_unix_times > $this->timeOut ){
$delayList[] = $json;
} else {
$lRemList[$msgPushInfo['to_uid']][] = $json;
$pushLists[$msgPushInfo['to_uid']]['uid'] = $msgPushInfo['to_uid'];
$pushLists[$msgPushInfo['to_uid']]['noce_ack'] = $msgPushInfo['noce_ack'];
}
}else{
//对错误数据及时清理
$redis->lRem(self::PUSH_MSG_COMMENT_LISTS, 1, $json);
}
}
还有问题?
万万没想到,竟然还有问题,链接数变化正常,但是内存好像没有得到很好的释放,而且进程里也出现了很多野进程?
野进程多可能存在的原因是这样的,你没有守护启动,然后主进程挂了,后面的进程找不到父进程,变成了僵尸进程或者是孤儿进程。
内存也不对劲,大概率是我执行脚本里出了问题,去掉了修改配置的语句,在Base类里加入了unset,及时释放掉内存。
ini_set('memory_limit', '1024M');
set_time_limit(0);
第二天查看了日志,错误大小只有9.1K了,有明显改善,放心了。
未来
php的未来多半是扩展开发,集成Swoole的服务功能,集成Swoole想对来说还比较简单,扩展开发就有难度了,所有的方向都是殊途同归的,就是让性能达到最优,但我觉得性能不能单纯的寄托给语言,也要和Redis、Nginx、Linux参数配合使用,把思路融入在项目生产中,会创造更大的价值吧。