深入理解 IM 系统

第 11 章

会话同步

会话在服务端,手机去同步。Ana 有 2,000 个会话,每次打开都要下载 2,000 个最新 seq,其中只有十来个变了。怎么只把变了的发下去?

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

第 10 章定了:会话存在服务端,手机照抄。可同步还是第 6 章那样:每次回来,问一遍每个会话的最新 seq(会话里最新一条消息的序号),和本地比,大的就是有新消息。

Ana 有 2,000 个会话,每次就下载 2,000 个 seq,连会话 ID 每个 12 字节,共 24 KB。一天打开 20 次是 480 KB,是她一天收到的文字(267 条,约 53 KB)的 9 倍。可两次打开之间,变了的会话只有十几个。服务端也白干:每次都要读 2,000 个 seq。

服务端全发,是因为它不知道这台手机上次拿到了哪。这一章让手机只拿变了的。

1. 想法:给每个会话的那一行贴版本号

这一章有两个数,先分清:

  • seq:每个会话自己的消息序号。徒步群第 4,823 条消息,seq 就是 4,823,群里人人一样。
  • 版本号:每个人自己的计数器(第 6 章的记录版本记录版本record version每人一个递增的号,会话记录每改一次加一;取号时锁住这个人在 users 里那一行,所以版本的先后就是提交的先后。手机记着自己拿到的版本,回来时问“这之后改了哪些”,被拉进的新群、来了消息的会话都在这里出现。在术语表里查看)。Ana 的哪个会话变了,她的版本号就加一。
  1. 服务端 Ana 的每个会话一行(会话记录会话记录conversation record每人每个会话一行:置顶、免打扰、这个会话的最新 seq,以后还有读到哪。进群、退群、改设置时写;从第 11 章起,来了消息也改(写上最新 seq)。每次改动给这个人的记录版本加一。消息本身不在这里。在术语表里查看),多记一个最新 seq。
  2. 每来一条消息,给这个会话的每个成员改一下自己的那一行:写上新 seq,贴上这个人的新版本号。
  3. 手机记着上次拿到的最大版本号,回来只问“比它大的行给我”。

图里 Ana 的手机上次同步到版本 9,000。徒步群(9,013)和工作群(9,004)都比它大,发回来;读书群(8,571)没变,不发。

下面是 Ana 的一天,每格是她的一个会话:

一天 20 次打开,每次下载多少(点一根看那一次)

24 KB123丢4567891011121314丢15161718丢1920

第 1 次打开:服务器回了 24 KB,全部 2,000 个会话。

变了,发了没变,也发了手机不知道最新的一条

一天:共下载 480 KB;最后一次打开后,每个会话的最新一条手机都知道。

  • 每次全发:每次 24 KB,2,000 格全亮,真正变了的只有十来格。
  • 只拿版本号更大的:每次三四百字节。回复丢了(标着“丢”的那次),手机的版本号就不动,下一次照样问,那几个会话再发一遍(图上橙框的格子),一个不漏。

2. 扩散又回来了一点

第 10 章说,服务端建,消息不用给成员写任何东西。这一章又写回来了:每条消息,给每个成员改一行。第 6 章“其他答案”里列过这个做法,当时会话不多,不值得。

和信箱比,拿 500 人的徒步群算:

  • 写的次数一样。 信箱追加 500 条,这里改 500 行;每次改还更贵一点,要锁这个人、涨版本号。
  • 行数不涨。 信箱一天追加 50 万条,越攒越多;这里改来改去还是那 500 行。热闹时,同一行的好几次改动能合成一次。
  • 读省了。 手机只拿变了的十几行,服务端也只读这十几行。

3. 两件要做对的事

版本号不能先领后写。 要是先领号、再写那一行,就会漏:

  1. 消息甲给 Ana 领到 101,还没写完;
  2. 消息乙领到 102,先写完了;
  3. 手机这时来同步,拿到 102,记下“到 102 了”;
  4. 甲这才写进去,101 比手机记下的小,以后不会再发。

办法:取号和写那一行放在同一个事务里,取号时锁住这个人(UPDATE users SET record_version = record_version + 1,第 6 章就这么取号)。前一个没写完,后一个拿不到下一个号,版本号小的一定先写完。服务端按版本号从小到大回,手机记回复里最大的那个,不另去问服务端“现在到几号了”。

发消息的人不用等。 徒步群一条消息要改 500 行,不能让发消息的人等着。发消息时,同一个事务里只记一行待办:“徒步群 4,823,改所有成员的会话”;回 ACK、推送照常。之后分发层按待办,一人一个小事务慢慢改,挂了重启接着改。改两次也没关系:seq 只往大里改,版本号多涨一次,不过让这个会话多发一次。

退群、删会话也一样:改这一行(标成删除,不真删)并涨版本号,手机照样收到。

4. 算一下

  • 手机: 每个变了的会话一行 28 字节(会话记录 24 字节,加最新 seq 4 字节),十几个约 360 字节一次,一天约 7 KB。
  • 服务端: v2 高峰每秒 139 条消息,平均每条 6.67 个收件人(全书的数),约 927 次改行,约占一台存储写入能力的一成。

5. 代价

  • 写跟着群的人数涨。 500 人的群每秒说 10 条,就是每秒 5,000 次改行,一台存储的一半。
  • 会话那一行比消息晚一点。 待办没改完时来同步,看不到最新那条;推送会先到,不然就等下一次同步。
  • 每人取号的那一行是热点。 一个人在好几个热闹的群里,这一行每秒被锁好几次,谁在它上面开个长事务,他的会话更新全得排队。

6. 其他答案

  • 用更新时间当版本号。 会话那一行记一个 updated_at,手机拿本地最大的那个往后一页页拉,直到拉完,不用另外的计数器,也不用锁每个人。小心两处:同一时刻会有好几行,翻页要带上会话 ID 一起排;时间是写的时候取的,不等于提交的先后,早取时间、晚提交的那一行会落到手机已经翻过去的地方,和第 3 节是同一个洞。常见的补法是每次往回多拉几秒;比这更长的事务或时钟偏差仍会漏,还得有个兜底的全量对账。
  • 服务端替每个连接记一份。 Matrix 的简化同步(MSC4186)这样做:记住上次给这个连接发过什么,下次只发变了的。消息来了不用改成员的行;代价是每个连接一份状态(v2 约 360 MB),还要确认上次的回复真的到了。
  • 大群不改,回来时扫。 小群走这一章的做法,大群像第 6 章那样问最新 seq。两条路要一起维护。
  • 每次全发。 第 6 章的做法。会话不多的人(平均 200 个,2.4 KB)其实不疼。

7. 这一章的决定

第 11 章:会话记录跟着消息变。消息提交时记一行待办,分发层再给每个成员改他那行会话记录:写上最新 seq,锁住他在 users 里那一行取下一个记录版本。手机只记一个版本号,回来只拿它之后变了的记录,不再问遍所有会话的最新 seq。
Ana发件箱(本地库)Ana 的笔记本同步会话记录Ben记录版本消息 → ← ACK推送带 seq;回来带记录版本,只拿变了的一个程序连接层持有长连接;记着 用户 → 连接消息层(收)取 seq,写消息和一行待办,回 ACK分发层(发)提交后改每个成员的会话记录;同步只回变了的业务层(旁路)Ana 在这个会话里吗存储层唯一键与会话记录发送者+消息ID会话.last_seq会话记录+版本待办:改成员记录照片分片上传(经上传服务存进对象存储)上传地址对象存储 + CDN按文件 id 存整份文件Ben 先看缩略图,点开才下载原图

会话同步只拿变了的。可“群里有谁”到现在还是一张随手查的名单:Ben 退了群,下一条消息还推给了他;新进来的人,看得到他进群以前的消息。下一章:入群与退群。