深入理解 IM 系统

第 0 章

全景

一个 IM 系统由哪些部分组成,一条消息怎样从 Ana 的手机走到 Ben 的每一台设备?

这一章是初稿,会在写完前八章后再修订。

发一条消息,看起来再简单不过:Ana 把消息发给服务器,服务器转给 Ben。一个下午就能写出能跑的版本,在办公室的 Wi-Fi 下,它也确实能跑。

问题出在办公室之外:

  • Ana 在电梯里点了发送,信号断了一秒。消息到底发出去没有?她的 App 不知道。
  • App 只好再发一次,结果 Ben 收到两条一模一样的消息。
  • Ana 和 Ben 几乎同时说话,两个人看到的先后顺序不一样。
  • Ben 电脑上的网页版一整天没打开。打开时它要知道自己缺了哪些消息,又不能把几百个会话挨个问一遍。
  • 群里有 500 人,一句话要送到大约 750 台设备;直播间里有 100 万人,一句话要推 100 万次。
  • 100 万条长连接长连接long-lived connection设备和服务端之间一直开着的一条 TCP 或 WebSocket 连接。有了它,服务端可以随时主动把消息推下去,不用等客户端来问。在术语表里查看,大部分时候一个字节都不传,却每一条都占着内存。运营商的路由器会悄悄忘掉太久没说话的连接;一次发布重启,十万台手机会在同一秒冲回来。

而用户对聊天的要求很高:不能丢,不能重复,不能乱序,还要马上到。一个网页慢半秒,很少有人察觉;一条消息丢了,对方马上会问“你收到了吗?”。

IM 难,不是因为其中哪一个问题特别难,而是它们同时出现,并且互相牵扯:为了不丢要重传,重传带来重复;为了实时要保持长连接,长连接带来百万连接和重连风暴;为了省电少发心跳,少发心跳又发现不了断线。每解决一个问题,都会引出下一个。

这个系列叫“深入理解 IM 系统”。它一步步讲,一个 IM 系统怎样从“一个下午的版本”长成能扛住这些的样子。开始之前,先把整个系统看一遍。

1. 一个 IM 系统要做到什么

大多数后端是“客户端问,服务端答”:你刷新一下,它返回一页。IM 反过来:Ben 什么也没做,服务端就要把 Ana 的消息主动送到他的手机上。这种能力来自长连接,严格说属于连接层,而不是 IM 本身:同一个长连接层也可以拿来做直播弹幕、推送、设备控制(第 19 章)。IM 是在这个能力之上,再加上一整套保证。

用一句话说:Ana 发出的每一条消息,都要出现在 Ben 的每一台设备上,只出现一次,顺序和所有人看到的一样,哪怕 Ben 的某台设备离线了一整天。

这句话里每个词都有代价:

  • “每一条”:网络会丢包,所以要确认ACK(确认)接收方回给发送方的“收到了”。在本书里,服务端的 ACK 表示消息已经写进消息日志,不表示对方已经收到。在术语表里查看和重传。
  • “每一台设备”:Ben 有手机,也有电脑上的网页版,它们的状态要对得上。
  • “只出现一次”:网络只能做到“至少送到一次”,重传会带来重复,所以要按消息 ID 去重去重dedup同一个消息 ID 再来一次时,返回第一次的结果,不再存一份。网络只能做到“至少送到一次”,去重让每条消息只显示一次。在术语表里查看,让它只显示一次。
  • “顺序一样”:每个人的时钟都不一样准,所以不能按时间戳排。
  • “离线一整天”:回来时要知道自己缺了什么,并且高效地补上。

再把“Ben”换成“一个 500 人的群”,或者“一个 100 万人的直播间”,同样的要求就变成了完全不同的工程问题。

2. 贯穿全书的例子

这个 App:一个普通的社交聊天 App,有单聊,也有最多 500 人的群;手机、电脑、网页都能用。它没有名字,免得像任何一个真实的产品。

人物:Ana 和 Ben 互相聊天,也都在“徒步群”里(500 人)。Ben 有一部手机,还有电脑上的网页版。

数字:全书共用一套假设,每一章的估算都从这里来。比如:高峰时 10% 的用户在线;每人每天发 40 条消息;每人平均 1.5 台设备;每条消息约 200 字节;丢包率 10%(故意定得很高,好让故障看得见,好的网络远低于这个数)。所以徒步群里的一条消息,要写进 500 个人的信箱信箱inbox每人一份的收件索引,记着“有哪些新消息要给你”,有自己按人递增的序号。写扩散时写它,设备按同步游标从它这里拉。在术语表里查看,推到大约 500 × 1.5 = 750 台设备。到 v3,这个 App 有 1,000 万日活用户,高峰时 100 万人同时在线。

这些都是为了举例选的整数,不是任何真实公司的数据。

3. 整个系统,和一条消息的旅程

下面这张图是这个系列最后会搭出来的系统(主干的 v3 版本),按职责分层画出:

  • 最上面是两台设备:发出消息的 Ana,收消息的 Ben。
  • 跨过公网,是 IM 服务端的四层:连接层(为每台设备保持一条长连接)、消息层(收下消息)、分发层(送到每一台设备)、存储层,最底下是各层共用的基础设施。
  • 左边一列是业务层:账号、好友和群、审核这些规则。它在旁边,因为消息并不从它这里流过,而是各层需要时来问它;它也是改得最频繁的部分,放在旁边,其他几层才能保持稳定。
  • 虚线框里的业务系统(你的 App 后端)和系统推送系统推送OS push通过苹果 APNs、谷歌 FCM 或手机厂商的通道,发给没有在线连接的手机的通知。它属于第三方,尽力而为,不保证送达。在术语表里查看(苹果、谷歌、厂商)不属于 IM。

消息层和分发层之间有一条点线:上面是“收下”,发送方等着它回 ACK;下面是“送达”,从这里起是异步的。消息日志消息日志message log持久化、只追加的队列,收和发之间的交接点。消息写进去才算收下,服务端这时回 ACK。通常建在消息队列上,比如一个按会话分区的主题。在术语表里查看就是两边唯一的交接点。业内常说的“接入层、逻辑层、存储层”里,连接层就是接入层,消息层和分发层合起来就是逻辑层。

先看全图,点任意一层或卡片看它做什么、难在哪。然后点“跟着这条消息走一遍”:Ana 在徒步群里问了一句“周六还去吗?”,图会把它走过的 8 步一步步点亮。首页简版的 4 步,对应这里的 1–2、3–4、5–7 和 8。

一条消息怎样穿过系统:先可靠地收下(步骤 1–4),再异步地送到每台设备(5–7);离线的设备回来时自己拉取(8)。

发送方:Ana 的手机周六还去吗?IM SDK:发件箱、本地数据库、同步接收方:Ben同步游标:每台设备一个手机在线网页离线公网:TCP 或 WebSocket,TLSIM 服务端业务层(旁路)被调用,不在路径上账号与鉴权关系与权限黑名单、禁言、审核发送前同步检查群与成员群的成员表开放接口业务消息、回调连接层接入层,持有连接有状态,随在线设备增长接入调度为设备选网关长连接网关每台在线设备一条连接:鉴权、心跳、收发消息层(收)不丢、不重、有序随消息量增长消息服务按会话分片:去重、分配 seq分发层(发)送到每一台设备随 消息 × 接收人 增长接收方单聊、群扩散写扩散 / 读扩散路由查在线路由表在线推送离线推送同步服务存储层消息和状态有状态,随保留期增长消息存储会话时间线已读游标读到哪在线路由表网关登记,路由查询元数据与缓存会话列表、文件信箱每人一条基础设施消息队列服务发现与配置限流与过载保护监控与链路追踪↑ 同步↓ 异步消息日志持久化的队列IM 之外业务系统App 后端、客服、运营IM 之外系统推送APNs、FCM、厂商通道账号、关系、业务消息唤醒后台的手机12345678
  1. 发送:消息从 Ana 的长连接进入网关
  2. 网关看会话 ID,转给负责这个会话的消息服务
  3. 先看消息 ID 最近见没见过(重发就原样回上次的序号和 ACK;更晚的重发由存储里的唯一键挡住),再检查能否发言、分配会话内的序号,写入消息日志(持久化的队列)
  4. 写入成功后,消息服务回 ACK:发送方只等到这一步
  5. 分发层从日志读取,写入消息存储,再去分发(从这里起是异步的)
  6. 确定接收方,扩散(写扩散时写入每人的信箱),查在线路由表
  7. 经 Ben 手机所在的网关推送。没有在线连接的手机改走系统推送,它只是提醒;网页这里不用系统推送,下次打开时补齐
  8. 离线的设备回来后带着同步游标来拉(游标指向它在自己信箱里读到的位置),同步服务从信箱(写扩散)或群的时间线(读扩散)返回缺的消息;在线的设备发现会话序号断档也会去拉,推送和拉取重叠时按序号去重

蓝色实线:消息的路径蓝色虚线:拉取 灰线:调用、写入、通知虚线框:IM 之外点线:同步与异步的分界

业内常说的“接入层、逻辑层、存储层”:连接层就是接入层,消息层和分发层合起来就是逻辑层。消息日志通常用基础设施里的消息队列实现,比如一个按会话分区的主题。

点任意一层或卡片,看它做什么、难在哪、在哪几章深入讲。

几个词先说清楚,后面每一章都会用到(全部术语见术语表):

  • 消息日志是收和发之间的交接点,消息写进去才算收下;消息存储是长期保存的历史(每个会话一条时间线);信箱是每个人一份的收件索引,记着“有哪些新消息要给你”。
  • 写扩散:发的时候给每个成员的信箱各写一条,读的时候只看自己的信箱。读扩散:消息只在群的时间线里存一份,每个人读的时候自己去取。500 人的徒步群用写扩散;更大的群写不起,改用读扩散(第 10 章)。
  • 两种序号:群里的序号(4,811、4,812……)决定这个群的顺序,用来排序和去重;信箱有自己按人递增的序号,每台设备的同步游标就记在它上面。

这里只是走一遍,看每件事发生在哪里。从第 1 章开始,每一章会拿掉其中一个机制,让你在模拟器里亲眼看到会出什么问题。

4. 群和直播间:分发层在这里分岔

同样是“一条消息发给很多人”,群和直播间走的是两条不同的路,差别只在分发这一段。注意 100 万观众的直播间已经是另一种产品了:它相当于这个 App 高峰时的全部在线人数。图里假设一个热门房间每秒 100 条消息,观众分布在 20 台网关上,每台 5 万个连接。

同一条消息,两种送法。群聊要找到每一位成员的每一台设备,并保证都能补齐;直播间只找到网关,由网关把消息推给自己身上属于这个房间的连接,允许丢。
群:500 人分发在线路由表成员 → 设备 → 网关每个成员查一次信箱每人一份,写 500 份写扩散按网关打包,每台设备一份网关 3网关 7网关 12…500 次路由查询(每个成员一次)500 次信箱写入约 750 台设备,每台推一份离线的设备,之后从信箱补齐直播间:100 万观众房间路由观众进房时,网关来登记没有每人的信箱,不保证补齐房间内可以有序号,只用来去重和排序只发给这 20 台网关网关 15 万个连接网关 25 万个连接网关 205 万个连接…20 次发给网关不按人查路由、不写信箱:每台网关自己记着房间里的连接每台网关推给自己身上的 5 万个连接

每秒 100 条 × 100 万观众 = 每秒 1 亿条。所以网关把一秒内的消息打成一个包(约 2 KB),每个连接每秒只推一次:每台网关每秒 5 万次。再多就采样、分优先级(礼物优先)。

群里每个人都重要,每一条都要送到、都能补齐,所以要逐个成员去找设备、逐个写信箱。直播间里一百万人同时在看,屏幕一次只显示十几条,晚到的消息已经没有意义,所以只找网关网关gateway连接层的服务器。每台持有大量长连接,负责鉴权、心跳和收发,不看消息内容。它是服务端里少数有状态的部分:重启会断开身上所有的连接。在术语表里查看、不找人,允许丢。第 40 章会从数字一步步推出这个设计。

直播间只是其中一个例子。最后几章(第 38–44 章)还会换成别的产品,看同样的决策怎样反过来。

5. 这个系统是怎样长大的

上面的图是终点。这个系列从起点开始:跟着一个聊天 App 从 1,000 个用户长到 100 万人同时在线。四层从一开始就都在,只是一开始挤在一个程序里;每一次增长,都会逼着其中一部分被拆出来、做得更深。切换下面的版本看看:

1,000 用户“App 里要加个聊天。”先做出能用的:一个程序加一张表,客户端先轮询,后来改成长连接推送。

业务层(旁路)几行权限判断客户端发请求、显示结果连接层轮询 → 长连接消息层(收)收到就存分发层(发)推给在线的人存储层一张 messages 表一个程序一个数据库
本版本新增 最小的聊天 … 从轮询到推送 (第 1–2 章)

6. 怎么读一章

从第 1 章开始,每一章只做一个设计决策,并且都按首页写的那 5 步走:问题,看它坏掉,估算,修好它,代价和其他答案。每章最后,架构图上会多出一块,配一张决策卡。模拟器是一个沙盒,不连真的服务器,可以放心把它弄坏:把丢包率调高,把某个机制关掉,看看会怎样。

7. 从哪里开始

  • 想跟着故事走:从第 1 章“最小的聊天”开始(计划中,下一篇就是它)。
  • 只关心某一层:在上面的图里点它,看哪几章在讲它;或者打开架构图。
  • 想先看全部章节:回到首页的章节地图。
  • 遇到不认识的词:查术语表。正文里带点线下划线的词,指上去或点一下就能看到解释。