概述

项目使用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 - 推送项目上线一年后的总结和复盘相关推荐

  1. 使用HTTPS方式向git托管网站推送项目时输错用户名密码

    如果在使用HTTPS方式向git托管网站推送项目时输错用户名密码,那么后面不会再弹出输入用户名密码的界面,直接报错误. 解决方法是 1.打开控制面板(快捷打开win+R,输入control): 2.点 ...

  2. 新建远程仓库并推送项目

    有时可能有人会想要自己写好了个项目,然后想上传至github上作为远程仓库.然后不是通过as自带的git来,而是想用SourceTree,这个时候,就会去创建仓库. 创建好后,发现提交本地是提交成功了 ...

  3. git推送项目到码云(gitee)

    git推送项目到码云(gitee) git推送项目到码云(gitee) 创建账号 创建一个Gitee账号,我使用的是Gitee因为国内速度快~ 本地安装Git 前往 Git 根据操作系统下载Git到本 ...

  4. git创建仓库之推送项目到远程仓库流程以及svn提交代码流程

    1.git推送项目到远程仓库 自己在gitlab上面建立仓库会得到一个git仓库的地址,如下:https://xxx.com/xxx.git 2.在本地先克隆仓库下来 git clone https: ...

  5. 使用git新建分支推送项目

    前言: 作者:神的孩子都在唱歌 一个还在努力的编程小白 转载请标注来源 使用git新建分支推送项目 一. 新建自己的分支 二. 推送项目到仓库 三. 错误 四. 参考 一. 新建自己的分支 如果单纯的 ...

  6. GitLab-使用SSH的方式拉取和推送项目

    场景 Docker Compose部署GitLab服务,搭建自己的代码托管平台(图文教程): https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/det ...

  7. git同时推送项目到GitHub和Gitee

    前言 今天准备给GitHub新建一个分支用于家里面的Mac电脑提交文件,同时练习一下Git的branch相关命令,然后呢,第一步进行的很顺利,分支创建无任何问题并把项目push到了新创建的分支下.然后 ...

  8. Push rejected: Push to origin/master was rejected--git推送项目到远程服务器

    翻译:推到原点/母版被拒绝 Push rejected: Push to origin/master was rejected 直接是解决办法:直接打开你要上传代码的文件夹位置鼠标右键git Bash ...

  9. vs2019推送项目上自己的github账户报错

    1.报错信息: could not read Username for 'https://github.com': terminal prompts disabled 解决方法: 打开项目所在的目录下 ...

最新文章

  1. CollectionView侧滑刷新
  2. mybatis中mysql流式读取_MyBatis读取大量数据(流式读取)
  3. literature review and methodology
  4. webservie报文格式
  5. c++猜数字_用Excel玩数字炸弹,猜0-100你需要几次?
  6. NAT、远程访问和站点间***集成
  7. OpenCV-图像处理(15、自定义线性滤波)
  8. 在Windows系统下,手把手教你制作属于自己的星际译王词典
  9. 破解Windows7开机密码
  10. 系统集成项目管理工程师高频考点(第二章)
  11. Python 取模运算(取余)%误区及详解
  12. AutoJs7打包薅羊毛时间版
  13. 友点 CMS V9.1 后台登录绕过 GetShell
  14. 佳佳GIS学习笔记2
  15. 云集群搭建-创建阿里云实例
  16. 进击吧!Pythonista(6/100)
  17. 几种常见的传统汽车总线传输通信技术
  18. oracle 存储过程 exception,oracle存储过程中exception问题
  19. 使用Docker部署MySQL(数据持久化),将mysql的数据映射到本机磁盘
  20. 一个ICESat-2数据下载的保姆教程(downthemall)

热门文章

  1. java正常运行但javac报错
  2. 谷歌查排名php,百度权重、pagerank、alexa及百度和谷歌收录情况查询接口
  3. python throw_python 之 异常处理
  4. 南怀瑾:如何静坐(附视频)
  5. 一行代码解决IE6~IE8以及IE兼容模式下的兼容问题
  6. 形式化、半形式化和非形式
  7. Python3相对路径符号斜杠 (/),点斜杠(./),点点斜杠(../)的意思
  8. 基于JAVA的停车场管理系统
  9. 财阀还是民主?DeFi协议大战,暗潮汹涌
  10. CTF-日常密码泄露分析溯源