5. 设计即时消息服务

需求

  • 有类似功能的软件:
    1. Facebook Messenger
    2. 微博
    3. 微信

功能需求:

  1. 用户在线、离线; 扩展:状态变更通知
  2. 1对1对话; 扩展:群聊
  3. 对话历史保存

非功能需求:

  1. 聊天延迟低
  2. 聊天记录一致
  3. 为保证一致性,可容忍较低的可用性

规模预估

假设

  1. 每天有5亿活跃用户,
  2. 平均每个用户每天发送40条消息
  3. 一条消息平均为100 B

每天消息 = 5亿 * 40条 = 20G条

流量

= 20G * 100 B = 2 TB

带宽

传入 = 2 TB/86400秒 ~= 25 MB/s
下载 = 传入

存储

200亿条消息100字节=>2 TB/天
五年 = 2 TB
365天*5年~=3.6 PB

缓存

28原则,0.4TB

数据模型

Client
+int ID
+String Name
Message
+发送方 sourceID
+接收方 targetID
+时间戳 time
+内容 contex
+状态 status

概要设计

上传/下载
上传/下载
client1
消息server
client2
数据库

详细设计

功能用例:

  1. client 接收传入消息,传递消息
  2. 数据库存储消息,检索消息
  3. 记录用户状态,变更是通知其他用户

消息推拉模式

1.拉模式

  • 流程
    client:用户定期拉取,询问服务器是否有任何新消息。
    server:服务器需要跟踪记录所有未拉取的消息,用户拉取时全部返回
  • 缺点:容易浪费网络、IO资源
    为了实时性,要频繁检索数据库里未拉取的消息
    如果没有消息会空拉

2.推送模式

  • 流程
    client:保持与服务器的连接打开
    server:跟踪client连接,有新消息就通知用户

  • 缺点:保持连接难
    轮询的效率低,非常浪费资源。
    HTTP要始终打开,有超时、断开连接情况

推送模式连接问题 比起拉模式网络、io资源问题 容易处理。
综上,选取推送模式

处理推送模式连接问题

  • client连接保持方法

    1. http长轮询
    2. webSocket
  • 服务端跟踪client连接方法
    维护一个hash表(k=clientId,v=连接对象)

  • 接收方client断开情况

    1. 通知发送方发送失败,由发送方自动重试发送
    2. 服务端缓存一段时间消息,由服务端重试发送

服务器

  • 数量
    5亿活跃用户,要保证5亿连接
    每台服务器50K 并发连接。需要10k个服务器

  • 流程

    1. 将消息存储在数据库中
    2. 将消息发送给接收者
    3. 向发送者发送确认

消息顺序问题

Mon 06 12:00 Tue 07 12:00 Wed 08 12:00 Thu 09 12:00 Fri 10 发送 M1 接收 M2 发送 M2 接收M1 client1 client2 不能以客户端时间排序,会出现以下情况

client1 顺序为:M1 - M2
client2 顺序为:M2 - M1

需要为每个客户端的每条消息保留一个序列号。此序列号将确定每个用户消息的确切顺序

消息落库

服务器收到新消息时,有两种落库选择

  1. 向数据库发送异步请求以存储消息。
  2. 启动一个单独的线程,该线程将与数据库一起存储消息。
  • 失败重试机制

服务器推送消息

自动分页。不同的客户端,页面大小可能不同,

  • 手机屏幕较小,因此我们需要在视口中减少消息/对话的数量。
  • 电脑屏幕大,适量多展示数量

更新用户状态

5亿用户,广播给所有相关的活动用户,将消耗大量资源

所以不能状态变更就立刻广播状态,要不频繁地更新状态

更新状态情况如下:

  1. 【client启动时】:提取其好友列表 中所有用户的当前状态。
  2. 【client刷新其他用户状态】:这不应该是一个频繁的操作,因为服务器正在广播联机状态,我们可以暂时忍受用户陈旧的脱机状态。
  3. 【每当client联机时】:服务器总是可以以几秒钟的延迟广播该状态秒,以查看用户是否没有立即脱机。
  4. 【每当client开始与另一个用户进行新的聊天时】,都获取当时client的状态。
  5. 【每当client1向另一个已脱机的client2发送消息时】:通知client1发送失败,并更新client2的状态。

存储

数据库选择

  1. 支持更新率高
  2. 存储海量数据

MySQL 不能满足高io更新需求
redis、MongoDB不满足海量存储需求

用宽列数据库 HBase

原因:

  1. 面向列的键值NoSQL数据库
  2. 运行在Hadoop分布式文件系统(HDFS)、快速存储大量小数据
  3. 可以通过键或扫描行范围获取行

数据分区

5年3.6PB,我们需要将其分发到多个数据库服务器上。

2个分区方案:

  1. 基于ClientID的hash进行分区
    一个用户消息落在一个分区
    如果一个DB分区是4TB,五年将拥有 “3.6PB/4TB~=900” 个分区。为了简单起见取整,我们保留1000个分区。
    程序将通过“hash(UserID)%1000”找到分区号,然后从中存储/检索数据。此分区方案还可以非常快速地获取任何用户的聊天历史记录。

  2. 基于MessageID的分区:
    一个用户的消息落在 多个分区上
    则获取用户的一系列聊天消息需要对多个分区io,将非常缓慢,因此我们不应采用此方案。

性能、可用

缓存

缓存最近聊天的client最新的一些消息
如:最近1万个发消息的client,每个client缓存100条message

由于我们决定将用户的所有消息存储在一个碎片上,因此用户的缓存也应该完全驻留在一台机器上。

负载平衡

  1. server负载均衡
  2. cache负载均衡

容错和副本

情况分析:

服务器宕机

  1. 将client连接转移到其他服务器(难实现)
  2. client重连

数据库宕机

  1. 备份数据,不同的服务器上存储数据的多个副本
  2. 使用Reed-Solomon encoding(纠错码)等技术来分发和复制数据。

扩展需求实现

群聊

创建群组聊天对象GroupChat,存储在聊天服务器上

GroupChat
-群id GroupChatID
-群成员 ClientID列表
-群消息 messageID列表

基于GroupChatID分区的单独表

提醒推送

  • 背景
    目前client只能向活动用户发送消息,如果向脱机状态用户发消息将会返回失败。

  • 提醒推送 功能:
    使我们的系统能够向脱机用户发送消息。

  • 设计

    1. client选择开启 提醒推送功能
    2. 单独创建一个通知server,在server为离线client创建一个暂存消息的对象。
    3. 离线client收到消息后,记录在通知server
    4. client上线后通知server将这些通知推送到client。

猜你喜欢

转载自blog.csdn.net/xyc1211/article/details/127969108