循证与决策路径
在前文中提过,循证大概是我们读技术干货文章的一个原始诉求,通过分析别人走过的路径,来拨开自己技术道路探索上的迷雾。
关于 IM 类消息应用最重要的一个技术决策就是关于消息模型,微信采用了存储转发模型,其具体描述如下(参考[1]):
消息被发出后,会先在后台临时存储;为使接收者能更快接收到消息,会推送消息通知给接收者;最后客户端主动到服务器收取消息。
简单描述这个模型就是三个步骤:
消息接收后在服务端临时存储,并通知发送端已发送成功。
通知接收端有消息,请来拉取。
接收端收到通知后,再来拉取真正的消息。
初一看这个模型多了一层通知再拉取的冗余,为什么不直接把消息推下去?对,最早期我们自己做 IM 就设计的先尝试直接推消息下去,若接收端没有确认收到,再临时存储的模型。后者减少了临时存储的量和时间,也没有一个多余的通知。
但后面这个模型增加了另一层复杂度,在早期的 PC 互联网时期,推送并确认效率还算挺高的,但在移动环境下,就不太行了。而且引入了移动端,实际就导致了另一层复杂性,多终端在线,多终端确认,多终端已读和未读,都需要在服务端记录各个端的状态。所以,之后我们也就慢慢演变成同时存储和推送消息的并行模型,存储是为了方便各终端拉取各自的离线消息,但推送因为需要考虑旧终端版本的支持,还得直接推消息本身而并不容易简化成消息通知来取消掉消息的接收确认过程。
循证,即便你看到了一个更好的方式,但也要结合自身的实际情况去思考实践的路径。所以,如今我们的模型相比微信是一个更妥协的版本,若是五年多前要改成微信这样的模型,也许只需要一两个程序员一周的时间。但如今也许需要好几个不同的开发团队(各终端和后端)配合弄上一两个季度也未必能将所有用户切换干净了。
切磋与思考方式
IM 中还有个大家特别常用和熟悉的功能 —— 群消息。关于群消息模型,微信采用的是写扩散模型,也就是说发到群里的一条消息会给群里的每个人都存一份(消息索引,参考[1])。这个模型的最大缺点就是要把消息重复很多份,通过牺牲空间来换取了每个人拉取群消息的效率。
好多年前我们刚开始做群时,也是采用了的写扩散模型,后来因为存储压力太大,一度又改成了读扩散模型。在读扩散模型下,群消息只存一份,记录每个群成员读取群消息的偏移量(消息索引号,单调增长)。之所以存储压力大在于当时公司还没有一个统一的存储组件,我们直接采用的 Redis 的内存存储,当时原生的 Redis 在横向和纵向上的扩展性上都比较受限。这在当时属于两害相权取其轻,选择了一个对我们研发团队来说成本更低的方案。
再后来公司有了扩展性和性能都比较好的统一存储组件后,实际再换回写扩散模型则更好。毕竟读扩散模型逻辑比较复杂,考虑自己不知道加了多少个群了,每次打开应用都要检查每个群是否有消息,性能开销是呈线程递增的。唯一的问题是,写好、测好、上线运行稳定几年的程序,谁也不想再去换了对吧,每一次的技术升级和架构优化其实是需要一个契机的。
另外一个是所有分布式后台系统都有的共性问题 —— 性能问题。只要你的用户量到了一定规模,比如 100 万,以后每上一个量级,对技术支撑的挑战实际上并不是呈线性的。微信春晚红包的案例(参考[2])给出了一个很好的参考和启发,因为市面上几乎很少有系统能到这个量级了。
微信 2015 年春节的红包峰值请求是 1400 万每秒,而微信后台其实也采用了微服务的架构,其拆分原则如下(参考[1]):
实现不同业务功能的 CGI 被拆到不同 Logicsrv,同一功能但是重要程度不一样的也进行拆分。例如,作为核心功能的消息收发逻辑,就被拆为 3 个服务模块:消息同步、发文本和语音消息、发图片和视频消息。
服务拆散了,在自动化基础设施的辅助下,运维效率下降不大,而开发协作效率会提升很多,但性能会下降。那么在面对微信春晚红包这样的极端性能场景下,该如何应对?在电商里,正常下单和秒杀下单多是分离的两套系统来支撑,秒杀专为性能优化,简化了很多正常流程,而且秒杀本身需要支持的 sku 不多,所以它具备简化的基础。而微信给出的方案中实际也是类似的思路,但它有个特殊点在于,能把拆散的服务为了性能又合并回去。
在如此海量请求之下,在这个分布式系统中,任何一点网络或服务的波动都可能是灾难性的。最终我们选择把摇一摇服务去掉,把一千万每秒的请求干掉了,把这个服务挪入到接入服务。但这样做,有一个前提:不能降低接入服务的稳定性。因为不单是春晚摇一摇请求,微信消息收发等基础功能的请求也需要通过接入服务来中转,假如嵌入的摇一摇逻辑把接入服务拖垮,将得不偿失。
这里面的黑科技在于 C++ 技术栈的优势,同一台接入服务器上实际由不同的进程来处理不同的功能,达到了隔离的效果。而不同进程间又可以通过共享内存来通信,这比用 Socket 网络通信高效多了,又有效的规避了网络层带来的波动性影响,这是我们用 Java 做后台没法做到的事。
切磋,你不能看见别人的功夫套路好,破解难题手到擒来,就轻易决定改练别人的功夫。表面的招式相同,内功可能完全不同,就像金庸小说里的鸠摩智非要用小无相功催动少林七十二绝技,最后弄的自废武功的结局。切磋主要是带给你不同的思维方式,用自己的功夫寻求破解之道。
连结与有效提取
如何选择干货,我在前文《技术干货的选择性问题》中最后给出的结论是,给自己结一张网,形成知识体系。暂时离你的网太远的技术潮流性的东西,可以暂不考虑,结合功利性和兴趣原则去不断编织和扩大自己的技术之网。在编织了一些新结点入网后,就需要进一步有效提取这些结点的价值。
刚做 IM 时,曾经有个疑惑,就是 IM 的长连接接入系统,到底单机接入多少长连接算合适的?很早时运维对于长连接有个报警指标是单机 1 万,但当时我用 Java NIO 开 2G 最大堆内存,在可接受的 GC 停顿下,在一台 4 核物理机上测试极限支撑 10 万长连接是可用的。那么平时保守点,使用测试容量的一半 5 万应该是可以的。
之后一次机会去拜访了当时阿里旺旺的后端负责人,我们也讨论到了这个长连接的数量问题。当时淘宝有 600 万卖家同时在线,另外大概还有 600 万买家实时在线。所以同时大概有 1200 万用户在线,而当时他们后端的接入服务器有 400 台,也就是每台保持 3 万连接。他说,这不是一个技术限制,而是业务限制。因为单机故障率高,一旦机器挂了,上面的所有用户会短暂掉线并重连。若一次性掉线用户数太多,恢复时间会加长,这会对淘宝的订单交易成交产生明显的影响。
他还说了一次事故,整个机房故障,导致单机房 600 万用户同时掉线。整个故障和自动切换恢复时间持续了数十分钟,在此期间淘宝交易额也同比下降了约 40% 左右。因为这种旺旺在线和交易的高度相关性,所以才限制了单机长连接的数量,而当时已经有百万级的单机长连接实验证明是可行的。
在微信春晚红包的那篇文章里提到:
在上海跟深圳两地建立了十八个接入集群,每个城市有三网的接入,总共部署了 638 台接入服务器,可以支持同时 14.6 亿的在线。
简单算一下,大概就是 228.8 万单机长连接的容量规划,14.6 亿怕是以当时全国人口作为预估上限了。实际当然没有那么多,但估计单机百万长连接左右应该是有的。这是一个相当不错的数量了,而采用 Java 技术栈要实现这个单机数量,恐怕也需要多进程,不然大堆的 GC 停顿就是一个不可接受和需要单独调优的工作了。
连结,便是这样一个针对同一个问题或场景,将你已知的部分连结上新的知识和实践,形成更大的网,再去探索更多的未知。