GO千万级消息推送服务
公司此前有一个简单的文章订阅业务,但是采用的是定时拉取的模式,周期比较长,时效性不佳。
于是考虑做一个长连接服务,主动把新产生的文章推送下去。
因为是web场景,所以优先考虑成熟的websocket协议,很多编程语言都有成熟的服务端开发框架。
技术核心难点
系统调用的瓶颈
假设有100万人在线,那么1篇文章会导致100万次推送,10篇文章就是1000万次推送。
根据经验值,linux系统在处理TCP网络系统调用的时候,大概每秒只能处理100万左右个包。
这么看的话,推送1篇文章就已经到达了单机的处理能力极限,这是第一个难点。
锁瓶颈
我们在推送时,需要遍历所有的在线连接,通常这些连接被放在一个集合里。
遍历100万个连接去发送消息,肯定需要花费一个可观的时间,而在推送期间客户端仍旧在不停的上线与下线,所以这个集合是需要上锁做并发保护的。
可见,遍历期间上锁的时间会非常长,而且只能有一个线程顺序遍历集合,这个耗时是无法接受的。
CPU瓶颈
一般客户端与服务端之间基于JSON协议通讯,给每个客户端推送消息前需要对消息做json encode编码。
当在线连接比较少(比如1万)而推送消息比较频繁(每秒10万条)的情况下,我们可以计算得到每秒要json encode编码的次数是:10000 * 100000 = 10^9次。
即便我们提前对10万条消息做json encode后,再向1万个连接做分发,那么每秒也需要10万次的编码。
JSON编码是一个纯CPU计算行为,非常耗费CPU,我们仍旧面临不小的优化压力。
解决技术难点
系统调用瓶颈
仍旧假设100万人在线,那么单机极限就是每秒推送1篇文章,这会带来每秒100万次的网络系统调用。
如果我们想推送100篇文章,仍旧使用单机处理,优化的思路是什么呢?
很简单,我们把100篇文章作为一条消息推送,那么仍旧是每秒100万次系统调用。
无论是10篇,50篇,80篇,我们都合并成1条消息推送,那么100万人在线的推送频次就是恒定的每秒100万次,不随着文章数量的变化而变化。
当然,合并消息不可能无限大,当超过一定的阈值之后,TCP/IP层会进行大包拆分,此时底层实际包频就会超过每秒100万次,再次到达系统调用的极限。
锁瓶颈
在做海量服务架构设计的时候,一个很有用的思路就是:大拆小。
既然100万连接放在一个集合里导致锁粒度太大,那么我们就可以把连接通过哈希的方式散列到多个集合中,每个集合有自己的锁。
当我们推送的时候,可以通过多个线程,分别负责若干个集合的推送任务。
因为推送的集合不同,所以线程之间没有锁竞争关系。而对于同一个集合并发推送多条不同的消息,我们可以把互斥锁换成读写锁,从而支持多线程并发遍历同一个集合发送不同的消息。
其实操作系统管理CPU也是分时的,就像我们的推送任务被拆分成若干小集合一样,每个集合只需要占用一点点的时间片快速完成,而多个集合则尽可能的利用多核的优势实现真并行。
CPU瓶颈
其实当我们通过消息合并的方式减少网络系统调用的时候,我们已经完成了对sys cpu的优化,操作系统用来处理网络系统调用的CPU时间大幅减少。
但是user CPU需要我们继续做优化,我们如果在每个连接级别做json encode,那么1篇文章就会带来100万次encode,是完全无法接受的性能。
因为业务上消息推送分2类,一种是按客户端关注的主题做推送,一种是推送给所有客户端。
基于上述特点,我们可以把消息合并动作提前到消息入口层,即把近一段时间所有要推往某个主题、推给所有在线的消息做消息合并成batch,每个batch可能包含100条消息。当1个batch塞满后或者超时后,经过对其进行一次json encode编码后,即可直接向目标客户端做遍历分发。
经过消息合并前置,编码的CPU消耗不再与在线的连接数有关,也不再直接与要推送的消息条数有关,而是与打包后的batch个数有关,具有量级上的锐减效果。
架构考量
集群化gateway
经过上述的设计后,我们可以用GO来实现一个高并发的websocket长连接网关(gateway)。
gateway可以横向部署构成集群,前端采用LVS/HA/DNS负载均衡。
当我们采用gateway集群化部署之后,当我们想要推送一条消息的时候,需要将消息分发给所有的gateway进程。
逻辑服务logic
因此,我实现了一个Logic服务,它本身是无状态的,负责2个核心功能:
1,为业务提供了HTTP接口提交推送消息,因为作为推送系统的推送频次不会太高。而且业务方在推送前会有很多业务逻辑判定,最终通过HTTP完成推送,相信是一个比较易于接入的方式。
2,负责将推送消息向各gateway进程做分发,在这里采用了HTTP/2作为RPC协议(GRPC就是HTTP/2)保障了单连接的高并发能力,同时保障了不同gateway之间的故障隔离,互不影响。
认证服务
目前尚未引入websocket连接的登录认证,今后存在向特定用户推送的需求时,需要实现认证服务。
认证服务独立于gateway与logic,可以称作Passport。
客户端首先基于公司帐号体系向passport完成登录,得到一个自验证的Login token(例如JWT),然后再发起gateway连接。
gateway验证token后完成uid的识别,整个过程不需要与其他业务系统额外交互,当然也可以增加额外的调用服务验证。
那么当logic希望向特定uid推送消息的时候,当前架构下仍旧必须将消息分发给所有gateway,由gateway找到uid对应的连接。但是这无疑造成了浪费,因为uid可能只连在某一个gateway上,对其他gateway毫无意义。
session会话层
未来可以考虑增加会话层,记录uid与gateway之间的连接关系,这样logic经过session层反查找到uid所在的gateway,完成定向推送即可。
会话层可以做一层单独的服务,采用纯内存的方式保存uid与gateway的关系。
因为gateway宕机等原因,可能导致我们无法及时剔除掉线的会话,所以gateway与session之间应该定时传输健康客户端的心跳信息。
当然,也可以简单粗暴的将会话层用redis集群取代,仅仅提供单一的uid->gateway的反查能力。
源代码
项目代码我开源到了github,代码量非常少,所以感兴趣的话不如读一下源码喽。
我的慕课网课程
我近期在慕课网录制了《GO实现千万级WebSocket消息推送服务》 ,是一门免费课,大家可以花1小时快速了解一下相关技术。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~
是阈值。
关于登陆后的连接是在哪个实例上的问题,可以考虑实现一个用户表(或K-V)放在redis中,在连接建立(登陆)后在redis中找到此用户,并将所在实例的标识(或IP)写入.
你的这个想法有业务耦合,我建议PUSH和业务尽量解耦。
PUSH是一个消息通道,业务是围绕在PUSH周边的卫星系统。
包括认证登录应该独立成一个passport服务,认证后携带JWT等登录token连接IM系统,并在REDIS建立会话关系。
你提到的用户表我认为不属于PUSH的必要部分,是有办法解耦开的。
最终的PUSH应该通过开放协议的方式(包括长连接开放协议),向其他业务线开放消息的推/拉通道,提供更加复杂的业务能力。
我觉得所有关于高性能的系统都可以考虑如下的模型:1、单线程进行任务请求处理和任务分发 2、多工作线程进行实际工作。针对你的并发瓶颈,可以考虑使用单线程拷贝数据到任务线程,这样就不会有锁的问题,即使上下线是针对单个信息上锁,不会有那么大的竞争。但是拷贝数据又会带来内存的使用,可能需要实际评测。
通过副本减少共享内存,需要考虑内存拷贝的cpu成本。另外,大量内存拷分配,最好引入内存池来缓解碎片问题。
阈值
已修正,谢谢提醒。
支持多队列的网卡 PPS 会更高
maybe。
有一个想法, logic和gateway是不是可以解耦,将推送消息向各gateway进程做分发的时候, 往logic push不直接http2推到gateway而是写道消息队列里面, 所有的gateway读取消息队列然后推到client
想法很好,真实情况的确是这样的。
Indraprastha Apollo Hospital, New Delhi is one of the finest multispeciality institutes for issues concerning the heart and its valves. The multispecialty heart institute at Indraprastha Apollo Hospitals in New Delhi has a reputation for providing patients with state-of-the-art treatment and effectively meeting all of the public’s healthcare needs. The department at Apollo Hospital Delhi works with our cardiologists and cardiothoracic surgeons to prevent and cure heart disease, which has improved outcomes and quality of life for the thousands of heart patients who visit us annually. It has around 710 beds with top-notch facilities.
Синдром Ремхельда, или Ремхельда-Тодда, это редкое генетическое заболевание, которое обычно проявляется при рождении или в раннем детстве. Это врожденный порок сердца, при котором артериальная пульсация в области локтевых сгибов изменена из-за аномалии артериальной системы. Этот синдром обычно проявляется следующими признаками