开发即时通讯系统时如何优化系统的并发处理能力

开发即时通讯系统时如何优化系统的并发处理能力

即时通讯系统开发的同学都知道,这个领域最让人头疼的问题之一就是并发处理。你想啊,一个社交APP可能有几百万甚至上千万人同时在线,大家发消息、加好友、看朋友圈,这些请求密密麻麻地涌过来,系统要是扛不住,那用户体验直接崩了。我之前参与过几个IM项目的开发,在这个过程中踩过不少坑,也总结了一些实战经验,今天就想着跟大家聊聊,怎么从架构设计到代码实现,把系统的并发处理能力给提上去。

不过说实在的,优化并发能力这件事,没有那种"用了就见效"的万能药方,你得从多个维度去考虑问题。消息的接收、存储、推送、通知,每个环节都可能成为瓶颈。我会把我知道的一些方法论和实践经验都分享出来,希望能给正在做这方面工作的朋友们一些参考。

先理解问题:并发到底难在哪

在聊具体的优化方案之前,我觉得有必要先理清楚,即时通讯系统里的并发处理到底复杂在哪里。这个理解清楚了,后面的方案你才能吃得透。

即时通讯系统的并发挑战,首先体现在它是一个读写密集型的场景。想象一下,一个热门的微信群里有几千人同时聊天,每发一条消息,系统要处理的事情可不少——消息要写入存储、要推送给除发送者之外的所有人、每个人的客户端要收到通知、已读状态要同步更新。这还不算完,要是这时候有人疯狂发消息,或者群里有人发了个大文件,那负载就更高了。

另一个难点在于实时性要求。IM系统对延迟是非常敏感的,用户发消息恨不得对方瞬间就能收到。你要是用个传统的关系数据库死命写,那延迟肯定感人。但你要是为了追求速度把什么数据都放内存里,机器一重启数据全丢,这也不行。所以你得在可靠性、实时性、性能之间找一个平衡点,这个平衡还挺难把握的。

还有一点容易被忽视,就是流量峰值的波动。社交类产品经常会出现一些意料之外的流量高峰,比如春晚的时候抢红包、元旦跨年倒计时、某个明星官宣恋情微博服务器崩了这种。这种瞬时流量可能是平时的几十倍甚至上百倍,系统必须要有弹性扩展的能力,不然关键时刻准掉链子。

架构层面的优化:从根上解决问题

我觉得优化并发处理能力这件事,首先要搞定的是架构设计。架构要是没做好,后面再怎么调优都是修修补补,治标不治本。

无状态服务与水平扩展

最基础的架构优化思路就是把服务设计成无状态的。无状态是什么意思呢?就是每个服务实例不保存用户的会话信息,所有的用户数据都存在外部的存储系统里。这样一来,当系统负载增加的时候,你只需要简单地增加服务实例就行,负载均衡器把请求往新实例上一分发,齐活。

举个实际的例子,消息收发服务就应该是无状态的。张三给李四发消息,不管这个消息是被服务节点A处理还是节点B处理,结果都一样——消息写入存储、李四收到推送。服务节点本身不保存任何与用户相关的数据,这样扩容的时候你不用考虑数据迁移的问题。

不过无状态服务说起来简单,做起来还是有不少细节要注意的。比如session信息怎么处理?总不能每次请求都让用户重新登录吧。这时候你可以把session信息存在Redis这样的缓存系统里,所有服务节点都能访问到。还有就是一些状态性的配置,比如某个用户当前连接的是哪个网关节点,这个信息也是需要共享的。

读写分离与分库分表

IM系统的读请求和写请求比例通常是严重不对称的。你想啊,用户大部分时间是在刷新消息列表、查看历史聊天记录,真正发消息的频率相对低一些。所以把读请求和写请求分开,让读请求走从库,写请求走主库,这个分离能大大减轻主库的压力。

具体怎么实现呢?写操作走主库,这个没问题。读操作的话,你可以让业务代码先读从库,如果从库没有(比如刚写入的数据),再升级到主库去读。这种方案在大部分场景下都能work得很好。

当用户量进一步增长,单库单表肯定扛不住的时候,你就得上分库分表了。分库分表的设计要点在于选择一个合理的分片键,也就是你按照什么维度来拆分数据。对于即时通讯系统来说,最常见的分片策略有两种:一种是按用户ID分片,另一种是按会话(也就是聊天关系)分片。

按用户ID分片的话,同一个用户的所有数据都在一个库里,查询这个用户的消息很方便,但如果你要查询某个群里的所有消息,就得跨库查询了。按会话分片的话,群消息的查询会更容易一些,但用户维度的统计查询又麻烦一些。具体选哪种,要看你的业务特点是什么。

我见过一些团队在分库分表之后,遇到跨库查询的问题就头疼得不行。比如要查询两个用户之间的聊天记录,结果这两个用户被分到了不同的库里,查询效率就变得很低。对于这种情况,我的建议是在设计之初就尽量避免跨库查询,或者引入一些中间件来做透明的跨库查询支持。

引入消息队列削峰填谷

即时通讯系统中,消息的写入速度可能是不均匀的。有时候用户疯狂发消息,瞬时写入量非常大;有时候系统又比较空闲。如果让这些请求直接打到数据库上,数据库很容易被打挂。

这时候消息队列就派上用场了。典型的做法是这样的:消息先写入消息队列,然后由专门的消费者服务去处理写入数据库的操作。消息队列起到了一个缓冲的作用,把瞬时的高流量平滑成稳定的处理速率。

用消息队列还有一个好处,就是可以让系统更有韧性。比如数据库临时维护的时候,消息积压在队列里,等数据库恢复之后再慢慢处理,不会丢失数据。当然队列积压太多也会有问题,所以你需要做好监控和告警。

不过引入消息队列也会带来一些复杂性,最大的问题就是延迟。消息从写入队列到被消费写入数据库,这中间有个时间差,用户可能会感觉到消息发出去之后,对方稍微过了一会儿才收到。对于一些对实时性要求极高的场景,这个延迟可能无法接受。你需要根据自己的业务场景来权衡利弊。

关键环节的优化细节

架构定下来之后,还有一些具体的优化点值得说说。这些细节做得好与不好,对系统的整体性能影响还是蛮大的。

消息推送机制的优化

消息推送给接收者这个环节,其实是IM系统并发压力最大的地方之一。假设一个群里有10000人,发一条消息要同时推送给9999个人,如果处理不当,这个推送请求量是巨大的。

传统做法是每个在线的用户都建立一个长连接,消息来了之后服务器主动推送给客户端。但面对大规模的场景,这种点对点推送的方式效率不太高。我了解到像声网这样的专业服务商,他们在处理海量并发消息推送的时候,会采用一些更聪明的策略。

首先是推拉结合的策略。服务器只推送一个通知告诉客户端"你有新消息了",然后客户端再主动来拉取具体的消息内容。这样服务器推送的只是一个小通知,数据量很小,网络开销也低。拉取消息的请求可以做到更细粒度的限流和负载均衡,整体的扩展性更好。

然后是离线消息的处理。对于不在线的用户,消息会存储在离线消息库里,等用户上线之后再拉取。这个离线消息库的设计也很讲究,你要考虑按用户ID分片、消息的过期清理、未读计数的维护等等。

连接管理的高效实现

IM系统里,每个在线用户都会保持一个长连接,这个连接的维护和管理也是需要精心设计的。早期的IM系统用单台服务器来维护所有用户的连接,后来用户量大了,单台服务器肯定扛不住连接数,就演进成了多服务器分布式管理连接的方案。

分布式连接管理的核心问题在于:如何快速知道某个用户当前连接的是哪台服务器。比如A要给B发消息,消息路由服务需要快速查询到B所在的网关服务器地址,然后把消息路由过去。

常见的解决方案是用Redis来存储用户和服务器的映射关系。用户上线的时候,把用户ID和服务器ID写入Redis;用户下线的时候,从Redis里删除。查询的时候直接Redis里读,速度很快。当然Redis本身也需要做高可用部署,不然Redis挂了,整个系统都找不到用户了。

还有一个需要注意的点是连接的心跳检测。用户跟服务器保持长连接,但TCP连接可能会因为网络波动而断开,如果不及时检测到断线,服务器还会傻傻地把消息往这个无效连接上发。所以需要设计合理的心跳机制,定期检测连接的存活状态,及时清理无效连接。

数据缓存策略

合理使用缓存是提升系统性能的一个重要手段。对于IM系统来说,哪些数据适合缓存呢?我觉得主要有这么几类:用户信息、会话列表、群组信息、最近的聊天记录。

用户信息和群组信息这种相对稳定的数据,非常适合缓存。而且这类数据在读操作中会被频繁访问,缓存命中率会很高。缓存的更新策略要注意,如果是用户修改了头像或者群主变更了群名称,缓存要及时更新,不然用户看到的就是旧数据。

最近的聊天记录缓存也是一个重要的优化点。用户打开聊天界面的时候,首先看到的肯定是最近的聊天内容。如果这些内容能从缓存里拿到,速度会比从数据库查询快得多。可以设计一个LRU的缓存结构,把最近活跃的会话的聊天记录放在缓存里。

不过缓存用得不好的话,也会带来一些问题。最常见的就是缓存穿透、缓存击穿和缓存雪崩。举例来说,如果有人恶意查询一个不存在的用户的所有消息,每次查询都会穿透缓存打到数据库上,这就是缓存穿透。解决方案是可以把这个不存在的用户ID也缓存起来,缓存一个空值或者标记。

高可用与容灾设计

优化并发处理能力,不能只盯着性能指标,高可用性同样重要。系统在高负载下依然能稳定运行,这才是真正的能力。

多活与异地部署

对于用户分布在全国甚至全球的IM系统来说,单机房部署是存在风险的。万一机房出了什么故障,整个系统就不可用了。所以多机房多活部署是必须的。

多活部署的关键在于数据的同步。不同机房之间需要实时同步用户数据、消息数据,保证任一机房都能服务所有用户。这个同步的技术方案有多种,比如基于消息队列的异步同步、基于数据库主从复制的同步等等。

我了解到声网作为全球领先的实时音视频云服务商,他们在全球化部署方面有比较成熟的经验。声网的解决方案覆盖了全球多个区域,能够为出海企业提供本地化的技术支持和最佳实践,这对于需要全球化部署的IM系统来说还是挺有帮助的。

限流与熔断机制

系统负载过高的时候,如果你什么都不做,所有请求都在排队等待,延迟越来越高,最终可能导致整个系统崩溃。更合理的做法是主动限流——当系统负载超过某个阈值的时候,拒绝一部分请求,保证剩下的请求能够正常处理。

限流的策略有多种,最简单的比如限制每秒处理的请求数。复杂一点的可以基于用户的维度限流,比如限制每个用户每秒只能发送多少条消息,防止某个用户恶意刷消息。还可以基于资源维度限流,比如限制数据库连接池的使用数量。

熔断机制跟限流有点类似,但出发点不同。熔断是指当检测到某个下游服务不可用的时候,自动切断对这个服务的调用,避免大量请求堆积导致雪崩。比如数据库响应变慢的时候,继续往数据库发请求只会让情况更糟,这时候就应该触发熔断,返回一个友好的错误提示,等数据库恢复了再恢复正常调用。

优雅降级方案

系统压力特别大的时候,除了限流和熔断,你还可以考虑优雅降级——关闭部分非核心功能,保证核心功能可用。

比如IM系统的核心功能是发消息和收消息,那么像消息已读状态同步、对方正在输入提示、消息撤回这些功能,在极端情况下可以暂时关闭。先保证用户能正常收发消息,其他的慢慢再补。

降级方案需要你在设计系统的时候就考虑好,哪些功能是核心的,哪些是非核心的,非核心功能在什么情况下可以被降级。这些都是要提前规划好的,不能临时抱佛脚。

写在最后

优化IM系统的并发处理能力,是一个需要持续投入的事情。不是说你按我上面说的做了一遍就万事大吉了,业务在增长,用户在变化,你得持续监控系统的运行状况,发现新的瓶颈,解决新的问题。

另外我觉得很重要的一点是,不要盲目追求技术上的"先进",而要选择适合自己业务阶段的方案。小公司用不着上来就搞微服务架构,大公司也有可能用一些看起来"土"但实际很有效的方案。技术选型这件事,归根结底要服务于业务需求。

如果你正在开发IM系统,或者准备开发IM系统,我觉得可以先评估一下自己的技术能力和业务需求,然后从最基础的架构设计开始,一步一步地把系统搭建好。中间遇到问题了就去解决问题,不断迭代优化。这个过程虽然辛苦,但当你看到系统能够稳定服务几百万用户的时候,那种成就感是无法替代的。

上一篇企业即时通讯方案的移动端消息推送的预览
下一篇 实时通讯系统的多端同步延迟时间是多少

为您推荐

联系我们

联系我们

在线咨询: QQ交谈

邮箱:

工作时间:周一至周五,9:00-17:30,节假日休息
关注微信
微信扫一扫关注我们

微信扫一扫关注我们

手机访问
手机扫一扫打开网站

手机扫一扫打开网站

返回顶部