90

NSQ 最佳实践

 5 years ago
source link: https://mp.weixin.qq.com/s/W_-Pj-0gHGpdqAQByym1tA?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

目前,全新的异步任务服务每天高效稳定的为唱吧提供数亿次的调用。服务器团队用全新的方式重新定义了异步任务实现方式,以为云计算而生的NSQ、成熟的PHP执行者PHP-FPM、自主开发的中间件NSQProxy以及admin管理后台共同组成了异步任务的队列服务。

唱吧异步任务的前世

打开唱吧服务器代码,一股历史的厚重感扑面而来,“这块代码是历史原因”成了同学们的口头禅。

为什么要用异步任务

为了提高响应,减少用户等待,和线上用户无直接关系的代码挪到异步后台执行,这样可以让前台业务逻辑代码更加简洁,执行速度更快。比如:用户购买礼物时,余额检查、付款、礼物进入用户背包等逻辑放在同步执行,统计等放在异步执行。

流程

  1. 同步执行的代码将数据插入队列(MemcacheQ)

  2. 以crontab的方式在后台启进程,进程开头代码就是while(true)

  3. 从队列中去取出数据消费,然后sleep,一直死循环下去。

3MBRvar.jpg!web

弊端

  • MemcacheQ远不如Nginx等足够成熟稳定,偶尔会莫名其妙的卡主自己好几秒。

  • 消费者自由散漫,分散在各个机器上,开几个进程也都是随心所欲。

  • 不支持订阅发布(推送),只能死循环里去get,get不到就sleep。出现了1次set对应136次get(135次get到空),白白浪费服务器资源。

  • 消费者以死循环的方式常住内存,导致代码更新不及时,生成端的数据消费端不认识,必须上服务器手动kill进程,这期间会造成消费失败的数据丢失。

  • 每一个环节都是单点,无法避免单点故障,坏一个洞全船都要沉。

鸟哥: 不要拿PHP做常驻内存的事情,因为我没给PHP写优雅的GC

唱吧异步任务的今身

NSQ替换MemcacheQ

Golang编写,云计算时代的产物,MQ领域的新星,为分布式消息队列而生,性能强劲,部署及其方便,二次开发难度低,有Go、Python、PHP等多语言客户端。NSQ相关不是重点,不在赘述。

NSQ优势

  • 消息可靠性高:有ack/requeue机制。

  • 磁盘落地:积压的消息可以磁盘落地。

  • 扩展性:优势明显,新增节点极其方便。

  • 易用性:部署简单,甚至安装不需要编译。

  • 分布式:为分布式而生,去中心化,官方推荐的拓扑结构没有单点故障。

  • 延迟投递。

  • 支持订阅发布。

  • bitly、有赞、docker、digg等大规模部署,并且有赞开源了二次开发版本。

唱吧村特殊村情及主要矛盾

历史上,我们的异步任务入队的数据是:PHP的类名(string)、PHP方法名和该方法的参数拼接成一个字符串。消费者需要new一个类,然后以执行字符串的方式(eval())来执行,由于方法和参数拼接成字符串,带来转义风险,最主要的是,这样就决定了我们的最终消费者,必须是PHP。

全新的架构

  • 该拉取为推送。有数据就执行,没数据就阻塞,避免空轮询。

  • 引入PHP-FPM。PHP-FPM作为非常成熟的PHP执行者,有完善的进程管理、垃圾回收、性能优化,并且常驻内存,非常适合作为最终消费者。并且PHP-FPM常驻内存并监听9000端口,也非常适合承担订阅者的角色,代码实时更新。

  • 将上述两点融合,开发一个中间件,从NSQ订阅数据,再以FAST-CGI协议推送给PHP-FPM。

  • 开发一个管理页面,可以方便的配置和管理。

  • 旧版是线上请求直接写入单点的MemcacheQ机器上,先在是以HTTP写入Nginx,Nginx做负载均衡,转发到NSQ节点上。

VjQRZbR.jpg!web

中间件NSQProxy

NSQProxy是唱吧服务器部门自住开发的轻量级中间件,Golang编写,性能强劲。

实现方式

qYNFf2e.jpg!web

  1. 启动第一步主备检测:

  • 如果是主机,则一个监听4140端口,此时新启两个协程,该协程等待备机发PING并回PONG,一直阻塞等待accept。另一个协程(主协程)继续向下走。

  • 如果是备机,则死循环向主机发PING,如果收到PONG,则sleep,程序会一直阻塞在这里。如果主机未回复,否则备机转为主机启动。

  • 主备角色等信息由配置文件决定。

信号监听:

  • 新启二个协程,一个是走主流程的(下面第3点),一个是走动态消费流程(下面第4点)。

  • 主协程监听SIGINT(2), SIGTERM(15), SIGTRAP(5),如果是SIGINT(2)和SIGTERM(15)则程序退出,如果是SIGTRAP(5)忽略不管。主协程阻塞在这里。

主流程:

  • 协程从Mysql获取管理后台的配置,每个队列都与NSQ建立一个链接,并根据配置的并发量(N个),启动N个协程,以FAST-CGI协议向PHP-FPM发送消费数据。

  • 在刚进入主流程时,上一小步执行之前,会启一个新协程,以定时器的方式,定时更新系统配置建和管理后台的配置数据。

动态消费:

  • 有些运营活动、push推送等突发性的入队,造成原本的并发量消费能力不足,这时候需要新增协程来帮助消费。

  • 以定时器的方式,定时扫描NSQ中积压的队列,若某队列达到积压阈值,则会启动与NSQ建立新的连接,启动新的协程来增加消费能力。再以定时器的方式,达到设定时间(如300秒),就会关闭链接并协程协程。

性能测试

入队

借助apache-ab工具,消息长度:356(短信验证码的标准长度)。

  1. Golang TCP协议:10个进程,每个进程入队10000,总入队100000

  • 运行时间:8s

  • 单次平均时间:8s / 10w = 0.08ms

  • 单次真实时间: 0.08ms * 10 = 0.8ms

  • QPS: 10w/8s = 12500 个

  • CPU * 20核 ≈ 30%

Golang HTTP协议:10个进程,每个进程入队10000,总入队100000

  • 运行时间:2.9s

  • 单次平均时间:2.9s / 10w = 0.029ms

  • 单次真实时间: 0.029ms * 10 = 0.29ms

  • QPS: 10w/2.9s = 34482 个

  • CPU * 20核 ≈ 30%

PHP HTTP协议:10个进程,每个进程入队10000,总入队100000

  • 运行时间:3.4s

  • 单次平均时间:3.4s / 10w = 0.034ms

  • 单次真实时间: 0.034ms * c = 0.34ms

  • QPS: 10w/3.4s = 29412 个

  • CPU * 4核 ≈ 30%,16个核是0%

出队

消息长度:356(短信验证码的标准长度),消费操作就是打日志。速度是根据打日志的时间戳来统计。

  1. 20并发,最多推送40

  • QPS:2177

  • PHP-FPM:起初CPU * 20核 > 90%, 后来就长期维持100%。

  • NSQD:CPU会有个别一两个核,在个别时刻闪现到1%~2%,然后恢复0%,网络读写210k/960k

  • NSQProxy:CPU一个核30%,一个核15%,其余都是4%。网络读写2400k/3320k,磁盘每30秒写30M(每条消费log)

10并发,最多推送20

  • QPS:同上

  • 起初CPU * 20核 > 60%, 后来就所有核长期维持100%。

  • NSQD:CPU会有个别一两个核,最多2个核闪现到1%,然后恢复0%,网络读写210k/960k

  • NSQProxy:CPU一个核30%,一个核13%,其余都是4%。网络读写2400k/3320k,磁盘每30秒写30M(每条消费log)

谁还不是写BUG的咋滴

PHP-FPM的特性决定了它的瓶颈

PHP-FPM就像HTTP协议一样,每次请求相互独立,不保存上次请求的状态和上下文。抛开优化点不谈,一次请求结束,删除变量,删除引用,释放内存,一切成空。下次个请求到来时,从头再来!

这就造成了一个问题:如敏感词检测的类,在new Class()时,会加载十几万行词库文件并解析,这一步大约耗时700ms,那么PHP-FPM每次消费前,都要重复这一步骤,执行完成后再销毁,这就会使消费速度大幅下降,队列积压严重。如果是Golang、JAVA等语言,则可以一次加载解析,永久使用。

PHP-FPM进程数限制

我们的异步任务几乎都是IO密集型,没有CPU密集型。所以可以开数百个进程跑,反正都在Sleep等待网络返回。而PHP-FPM却不可以,

在PHP7下,PHP-FPM开到200以上性能CPU的占用就开始飙升,同时每个PHP-FPM是同步执行,进程不可复用。那么在PHP-FPM进程数固定的大前提下,如果有一个任务执行特别慢,那么就会占用PHP-FPM进程不释放,这样的任务多来几个,很快就会把服务器上PHP-FPM的进程全部占光,导致其他消费快的队列却无可用的PHP-FPM。

二期畅想

一期工程所暴露的问题,在二期中会逐一解决。

针对以上的坑,列出几点解决方案,待探讨论证。

方式一

维持现状,NSQ和MemcacheQ共存,大多数队列在NSQ + PHP-FPM的方式,个别特殊队列仍然使用MemcacheQ + 死循环的方式。

方式二

安装NSQ-PHP客户端扩展,取代PHP-FPM。

方式三

简单粗暴执行:消费快的队列在PHP-FPM中执行,消费慢的队列仍然在while(true)中执行。NSQProxy不仅仅提供一个消息转发到PHP-FPM的功能(订阅发布的推送模式),同时还要监听一个端口可供消费者主动获取(拉取数据模式)。但是这样,既不优雅,也不统一和规范,尽管它可以快速解决当前问题。

方式四

PHP实现一个常驻内存的,监听某端口的功能,取代PHP-FPM。我小时候写的PHP-Socket项目有了用户之地:MeepoPS是Nginx + PHP-FPM的结合体,即对外监听端口,也可以直接运行PHP代码,压测数据表明比较稳定。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK