Design Facebook message / Whatsapp

1. 理解需求

1.1 商业目的

让用户通过手机网络互相联系。

1.2 功能性需求

  • 核心

    • 一对一聊天

    • 上次在线时间 (Presence)

    • 消息状态 (Sent / Delivered / Seen)

  • 非核心

    • 群聊

    • 推送

    • 图片和视频消息

1.3 非功能性需求

  • 消息第一时间送达

  • 隐私 服务器不存聊天信息

  • 消息不丢失

  • 消息顺序不出错

2. 资源估算

2.1 假设

  • 1B DAU

  • 50 messages sent daily

  • 100 messages received daily

2.2 估算

  • Ongoing Connections - 500M

  • Message Delivery Rate 100 * 1,000,000,000 / (24 * 60 * 60) = 1.2 Million

3. High-level Diagram

4. 核心子系统设计

这一部分我们的讨论会限制在一对一聊天的核心三点功能性需求上,以免内容组织过于复杂。其他需求会在Deep Dive中点到。

4.1 一对一聊天

在市面上的Messaging Service中,有一类服务是在服务器上存储所有历史记录的(比如Facebook Messenger, Google Chat),另一类只作为消息传递的中间人(比如Whatsapp,微信)。这些服务在聊天机制上会有重大区别,同学们在面试过程中务必要了解面试官在考哪一种。

根据非功能性需求的第二条 - 隐私,下面讨论中我们会设计上面提到的第二类服务。

因为我们的聊天服务只做中间人,一旦消息被传递到了收信人的终端,服务器就不需要存储任何消息的内容和状态了。这样的不仅保护了隐私,而且帮助服务节省大量的存储成本。这也是Whatsapp可以使用几百台机器服务数亿人的原因之一。

下面我们具体说说消息传递的流程。这里我们把消息分成两类,收发双方都在线的消息,以及收消息方不在线的情况。

  • 双方在线 - 消息通过收件人的队列 (Queue) 直接递送

  • 发件方在线,收件人离线, 消息暂时存储在write-back cache中, Write-back cache异步更新Staging Message DB, 一旦收件方上线,收件方的队列被建立,从Cache和DB中把相关信息读出,递送,删除Cache和DB entry

4.2 上次在线时间

  • 因为Gateway和用户终端有双向连接,当用户正在使用App的时候,这个Heartbeat可以比较有效率地间隔数秒发送一次。

  • 单独的Presence Service接收Gateway发出的Heartbeat,并更新Presence DB和对应Cache。

  • Presence DB只需存储每个用户最后一次使用App的时间。

4.3 消息状态

消息状态有三种Sent / Delivered / Seen,对应着每条消息是否被服务器接收,被传递给接收人以及接收人是否阅读了。大多数聊天服务都会在这三种状态中实现一种或多种。在群聊中一般Seen状态因为成本太高而不会被实现。

这些消息状态的信息在服务高峰期可以考虑降低优先级发送或者不发送。

4.3.1 Sent

服务接收到消息后可以给用户终端发送一个Ack的信号。

4.3.2 Delivered / Seen

每一条送达或者被看到的消息会产生一条反向的消息告诉发件人这个消息的状态。传递消息状态的机制跟发送一条新消息是一致的。

4.4 及时送达

对于Messaging Service,最重要的使命是把消息及时送到。当消息的时效性成为最终的非功能需求的时候,我们就要考虑使用 Push 的方法而不是 Pull。一旦服务器接到消息,我们就将其推送到用户的终端。

我们可以选用 Websocket 去保持服务器跟用户终端尽量始终保持连接。这样选择的额外好处有以下:

  • 在高QPS环境下,省去反复handshake的时间

  • 服务器需要存储尽可能少的消息

  • 方便支持上次在线时间服务 - 需要每隔数秒传递用户终端状态

为了能进一步优化服务器和用户终端的沟通,避免在App关闭后消息无法传递,我们可以保持一个后台程序长期运行,一个例子是Android中可以使用一个MQTT后台程序。这样可以保持大多数设备的长期在线。

当然,这样做也是有成本的,Gateway需要消耗内存跟大量的设备长期双向联系。Whatsapp使用高度优化的Gateway,单台服务器可以跟1M-2M的设备保持联系,令人惊叹。

5. 数据结构与存储

以下DB可以配合使用 write-back cache 来保证服务的最高时效性。

5.1 Staging Message DB

RECIPIENT ID (SHARDING KEY)

TIMESTAMP (SECONDARY INDEX)

SENDER ID

ENCRYPTED MESSAGE

5.2 Presence DB

RECIPIENT ID (SHARDING KEY)

LAST ACTIVE TIMESTAMP

6. 接口设计

  • Websocket APIs with JSON payload

  • Websocket handshake contains info on “sender_id” and “timestamp”

Send "message" {"receipient_id": "123", "message": "abc"}
Send "heartbeat"{}

7. 扩展性,容错性,延迟要求

总结一下之前提到的一些设计要素。

7.1 扩展性

  • 由于服务器端只长期存储很少信息,服务几乎可以无限横向扩展

7.2 容错性

  • 服务复杂度低

  • 异步(Async)处理消息 - 底层服务错误不会造成上层服务错误

7.3 延迟要求

  • Websocket Connection

  • Heavily Cached

8. 监控和警报

8.1 常规

  • 服务 QPS

  • 服务延迟

  • 服务可用性 (Availability)

  • Messaging Queue Health

9. 专题深挖

9.1 群聊

  • 群聊中消息需要被分发到大量接收人的终端 (fanout)。接收人如果过多,服务器负载会很大,因此群聊一般被限制在一定人数范围内,不能无限向上加。

  • 群聊发送机制上跟一对一聊天一样,消息被放进接收人的队列中,按顺序发出。

  • 如果需要存入 DB,考虑把信息本身存在单独的表中,把其ID存在多个用户名下,避免反复存储消息本身。

9.2 推送 (Push)

推送需要借助 Apple APNS 以及 Android GCM 或自建后台服务(如 MQTT Service)。当消息需要被发送给接收人时,找到对应设备的 Push Token,通过 API发送推送。

9.3 图片视频消息

图片视频消息因为存储空间要求高,不适合存入 DB,而是使用 File Storage 来存储(如 Amazon S3)。用户之间传递的消息只包括链接,如果用户点开图片或者视频,才会到 File Storage 中加载内容给用户。

对于大量观看的图片和视频,可以保留在 Memory 中或进一步部署到 CDN。

  • 老师你好,如果消息先存在 write-back cache 再异步写到DB中,会不会有消息丢失的风险? 谢谢

    回复

    分享 ›

      纯的内存 Cache 会有信息丢失的风险。Redis 带有 RDB, AOF 等恢复机制,可以降低这个风险。

      回复

      分享 ›

    感谢讲解。有一个关于Gateway的问题。

    正文中使用的Gateway似乎需要处理很多通讯的逻辑,比如user和connection之间的对应关系,以及发送message到正确的WebSocket connection。一般来说Gateway的职责只是简单的分流,所以我想知道,有没有一个具体的方案来实现这种复杂的逻辑。比如说,用Nginx可不可以实现?

    另外,当在线人数很多时,concurrent connection也会有很多,这就牵涉到Gateway里面多台server nodes的协作,也会涉及到一些Availability的问题。如果Gateway这里某一个node下线了,另一个node是不是可以接替处理原本的WebSocket connection?这样是不是需要一个Distributed key/value store来管理user和connection?比如使用Redis和Memcached。在实际中,这种设计可以跟Gateway一起使用吗?

    回复

    分享 ›

    • 提的很好,原图确实有说的不清楚的地方,其实我在直播课里已经更新了设计图,我也刚刚更新到了文章里。Gateway 跟 Websocket server 应该分开,这里的 Gateway 就是用 Nginx 实现,Stateless。 Websocket server 负责维持 connection,Stateful。Websocket 的连接如果断了,客户端可以试图重连。Websocket 会协议层面在内存中处理信息存储,不一定要跟 Redis Memcached 一起使用。

      回复

      分享 ›

    Messaging Service中的消息队列是怎么实现的可以详细说明下吗?谢谢

    回复

    分享 ›

      每个消息的 Recipient 会有属于自己的 Queue,这不同于 Kafka,是非常简单的编程语言意义上的 Queue,起到按顺序处理消息的作用。一旦这个 Queue 被清空,内存就可以被释放。

十分感谢您的文章,作为一个刚入门的小白,我从中收获良多!

在High-level Diagram中,有一个基础的问题希望向您请教:在service和clients之间,我们经常看到Load Balancer或者API gateway或者Load Balancer + API gateway。就我现在所查阅的资料,Load Balancer是负责分流到不同的node,而API gateway是负责分流到不同的service。请问在系统设计中,我们是不是一般两个都需要呢?如果两个都需要,他们是否有前后的顺序关系呢(例如clients的请求会先到API gateway然后再到load balancer)?还是说一般在系统设计中,Nginx就能把两个的功能都统一在一起了?

回复

分享 ›

    API gateway 可以提供 load balancing 的功能。在分布式系统设计中,一般我们是需要 Load balancing 的,因为牵涉到多台机器的分流。即使不是大系统,API gateway 也是需要的,因为有多个 API Server。逻辑上两者的功能不同,但本质都是分流。实际操作中 Nginx 可以同时提供两种功能。

    回复

    分享 ›

    • 十分感谢您的回答!

Last updated