在自己学习各种各样软件系统,特别是分布式系统的过程中,我做了一些笔记,有许多常见的、经典的系统,是非常值得学习和总结的。它们数量不算多,但具有典型意义,可能这样的系统也就十几个。
我在回顾这些笔记的时候发现,有时候一张简单的图,包含最核心的几个设计,就可以很大程度地帮助理解和记忆。所以我想把这些笔记和图解的结合通过文章的形式发出来,预计每篇文章都很短,基本上一张图,加上一点说明性的文字。
Disclaimer:这些都来自我自己的阅读和理解,肯定有着相当的改变和简化,因此它并不代表任何系统实际的样子。
今天是第一篇,即时消息系统,但是基本上好多即时通讯软件都属于这一类,比如微信,比如 QQ,比如 Facebook Messenger,比如 WhatsApp。
- 用户发送消息,直接发给 Chat Service,它会做少量的处理并持久化,然后放到基于 channel 的队列中,每一个对话(thread)都会有一个 channel,这个过程中,它并不关心这个对话有多少人参与(支持群聊)。
- 队列有两类消费者,一类是 Message Service 用来服务在线用户。由于单台机器和客户端的连接数量有限(比如小于 2^16=65536),因此 Message Service 需要有很多机器,根据用户的 id 来 sharding,它们去订阅自己感兴趣的频道,有新的消息就发送给用户。
- Message Service 获取客户端的心跳,保持来自客户端的连接(long polling 或者 socket)为了实时性肯定要用 push 模型。因此它知道用户的当前在线状态,也知道最后一条发送成功消息的时间戳(状态)。这个状态可以用于决定用户离线时消息是否要通过其它方式通知用户。
- 还有一类是 Notification Service 用来发离线通知。
- Chat Service 有两个职责,一个是处理发消息的请求,一个是接纳读取历史消息的请求,这两个功能可以分成两个组件,也可以一个组件,我放在一起了。
- 右侧的消息数据库,RDB 往往不太适合,因为消息数量太大,对于一组对话(thread)的展示,需要找到该对话 N 条最近的记录,行数据库效率较低,可以考虑列数据库,比如 HBase。这种方式下,同一 thread 下的消息都是按时序存放在一起的,读的效率非常高,写因为基本是 append,也很方便。
- 用户数据的存储,可以使用 RDB,也可以使用 KV 数据库。 这里面存放的数据库表包括:用户表;对话表;用户对话关联表:二者是 M:N 的关系,并且每个用户都可以有对于特定对话的设置,例如设置对话中的昵称,是否屏蔽消息通知等等。
- 对于图中基于 Channel 的队列,把数据 fanout 给下游,它有几个作用,一个是解耦,把消息发送和消息接收分离开,消息发送者可能只管把消息发到群里,但是并不关心这个群应该有几个用户得到通知;第二个是缓冲,无论是为离线用户服务的 Notification Service,还是为在线用户服务的 Message Service,它们消费数据的速率是无法确定的。
- 对于用户上线、下线的实现,其实也类似,上线、下线的事件可以推送到一个特定的 Channel 里面。用户的好友,也就是感兴趣的 Notification Service 的个体去订阅消息;还有一种思路是把状态更新到用户表里面,这样所有人都可以查询得到,这后一种方式适合非好友也要查看用户状态的情况。上、下线需要保留缓冲时间,容许一定状态的延迟,没必要,也不应过于实时。
这是《常见分布式系统设计图解》系列文章中的一篇,如果你感兴趣,请参阅汇总(目录)寻找你其它感兴趣的内容。
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》
第一条有错误,单个机器连接限制不是 2^16, 连接标识是一个五元组,(协议,收发 ip 和端口)
谢谢您的总结。真的总结的太好了。我在这里有个问题:
为什么 send message 是直接 send 给 chat service 而不走 socket 呢?messaging 应该是一个读写平衡的 service。感觉没有特别大的理由需要把 read 和 write 的逻辑分开?
谢谢!
好问题,我觉得都可以吧。我这样画只有一些不太强的理由,比如我觉得这个消息发送并不算是那种典型的小数据大量发送的情形,因此发送不需要长连接(但在另外的一些系统中,比如 https://www.raychase.net/6429 就如你所愿了)。我不知道有没有更好的想法。
Channel/Queue 这里可以深入一点, 是类似 kafka 一样的 queue 吗?比如在用户离线的情况下如何处理 queue 里积压的信息?
我的理解是,和 Kafka 这样的比起来,还是明显有区别的,因为 ChatService 负责所有消息的持久化了,要是这里再做一遍就重复了,这里其实主要还是一个 channel 的功能,消息应该是暂存(缓存)给 PushService 即时读取之用的,因为数据 push 过程对于速率无法保证,那么数据消费的速率也就无法保证,同时数据也可能被多台 PushService 机器使用(比如群聊的情况),因此使用这个 channel 来共享、缓存待推送新鲜数据。为此我给文中增加了一点描述。
因此离线情况下,消息可以通过其它方式推送,上面的 channel 里面不会积压信息,即便丢失也没有关系,因为消息是持久化在右侧的数据库里面的。等到用户再上线的时候,会发送给 ChatService 一个时间戳(这个戳可以是客户端保存,也可以存放在服务端的用户系统里),这样 ChatService 就可以推送相应的该时间戳之后的积压的消息。
Notification Service 获取客户端的心跳,保持来自客户端的连接(long polling 或者 socket)为了实时性肯定要用 pull 模型,不能用 push 模型。 这里有点不理解,为了实时性不是更应该用 push 模型吗?
你说的对,我写错了。这就修改
Update:做了修改,现在的图示应该更加合理了。