PMQ - 推送项目上线一年后的总结和复盘
概述
项目使用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参数配合使用,把思路融入在项目生产中,会创造更大的价值吧。
PMQ - 推送项目上线一年后的总结和复盘相关推荐
- 使用HTTPS方式向git托管网站推送项目时输错用户名密码
如果在使用HTTPS方式向git托管网站推送项目时输错用户名密码,那么后面不会再弹出输入用户名密码的界面,直接报错误. 解决方法是 1.打开控制面板(快捷打开win+R,输入control): 2.点 ...
- 新建远程仓库并推送项目
有时可能有人会想要自己写好了个项目,然后想上传至github上作为远程仓库.然后不是通过as自带的git来,而是想用SourceTree,这个时候,就会去创建仓库. 创建好后,发现提交本地是提交成功了 ...
- git推送项目到码云(gitee)
git推送项目到码云(gitee) git推送项目到码云(gitee) 创建账号 创建一个Gitee账号,我使用的是Gitee因为国内速度快~ 本地安装Git 前往 Git 根据操作系统下载Git到本 ...
- git创建仓库之推送项目到远程仓库流程以及svn提交代码流程
1.git推送项目到远程仓库 自己在gitlab上面建立仓库会得到一个git仓库的地址,如下:https://xxx.com/xxx.git 2.在本地先克隆仓库下来 git clone https: ...
- 使用git新建分支推送项目
前言: 作者:神的孩子都在唱歌 一个还在努力的编程小白 转载请标注来源 使用git新建分支推送项目 一. 新建自己的分支 二. 推送项目到仓库 三. 错误 四. 参考 一. 新建自己的分支 如果单纯的 ...
- GitLab-使用SSH的方式拉取和推送项目
场景 Docker Compose部署GitLab服务,搭建自己的代码托管平台(图文教程): https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/det ...
- git同时推送项目到GitHub和Gitee
前言 今天准备给GitHub新建一个分支用于家里面的Mac电脑提交文件,同时练习一下Git的branch相关命令,然后呢,第一步进行的很顺利,分支创建无任何问题并把项目push到了新创建的分支下.然后 ...
- Push rejected: Push to origin/master was rejected--git推送项目到远程服务器
翻译:推到原点/母版被拒绝 Push rejected: Push to origin/master was rejected 直接是解决办法:直接打开你要上传代码的文件夹位置鼠标右键git Bash ...
- vs2019推送项目上自己的github账户报错
1.报错信息: could not read Username for 'https://github.com': terminal prompts disabled 解决方法: 打开项目所在的目录下 ...
最新文章
- CollectionView侧滑刷新
- mybatis中mysql流式读取_MyBatis读取大量数据(流式读取)
- literature review and methodology
- webservie报文格式
- c++猜数字_用Excel玩数字炸弹,猜0-100你需要几次?
- NAT、远程访问和站点间***集成
- OpenCV-图像处理(15、自定义线性滤波)
- 在Windows系统下,手把手教你制作属于自己的星际译王词典
- 破解Windows7开机密码
- 系统集成项目管理工程师高频考点(第二章)
- Python 取模运算(取余)%误区及详解
- AutoJs7打包薅羊毛时间版
- 友点 CMS V9.1 后台登录绕过 GetShell
- 佳佳GIS学习笔记2
- 云集群搭建-创建阿里云实例
- 进击吧!Pythonista(6/100)
- 几种常见的传统汽车总线传输通信技术
- oracle 存储过程 exception,oracle存储过程中exception问题
- 使用Docker部署MySQL(数据持久化),将mysql的数据映射到本机磁盘
- 一个ICESat-2数据下载的保姆教程(downthemall)
热门文章
- java正常运行但javac报错
- 谷歌查排名php,百度权重、pagerank、alexa及百度和谷歌收录情况查询接口
- python throw_python 之 异常处理
- 南怀瑾:如何静坐(附视频)
- 一行代码解决IE6~IE8以及IE兼容模式下的兼容问题
- 形式化、半形式化和非形式
- Python3相对路径符号斜杠 (/),点斜杠(./),点点斜杠(../)的意思
- 基于JAVA的停车场管理系统
- 财阀还是民主?DeFi协议大战,暗潮汹涌
- CTF-日常密码泄露分析溯源