系统的要求和目标
功能需求
支持1对1聊天
支持离线在线状态
支持永久存储聊天记录
非功能需求
实时聊天体验,延迟越小越好
高度一致,相同的聊天信息在USER的DEVICE上
可以容忍稍微低一点的可用为了一致性
拓展需求
群聊
推送通知
容量估计
500M DAU, 一个用户一天发40条微信。
20 B 消息每天。
QPS 大概在 20 B/100K = 0.2m = 200K
假设每条消息100B。 2T 每天
五年需要 3.6 P的存储
服务
用户A通过聊天服务器向用户B发送消息。
服务器接收消息并向用户A发送确认。
服务器将消息存储在其数据库中,并将消息发送给User-B。
用户B接收消息并将确认发送到服务器。
服务器通知用户A该消息已成功传递给用户B。
存储
自然我们会需要一个MESSAGE TABLE,用来存储用户发送的每一条信息。随后我们为了构建一个1对1的会话,我们需要一个THREAD TABLE。
Message 表有,message_id, from_user,content,created_at,thread_id
Thread 表有, Thread_id, participants, created_at,updated_at,owner_id,is_muted
这里MESSAGE TABLE,像LOG一样,可以用NOSQL直接来存。
使用像HBase这样的宽列数据库解决方案可以轻松满足我们的两个要求。 HBase是一个面向列的键值NoSQL数据库,可以将一个键的多个值存储到多个列中。 HBase以Google的BigTable为模型,运行在Hadoop分布式文件系统(HDFS)之上。 HBase将数据组合在一起以将新数据存储在内存缓冲区中,一旦缓冲区已满,它就会将数据转储到磁盘。这种存储方式不仅可以帮助快速存储大量小数据,还可以通过密钥或扫描范围获取行的行。 HBase也是一个存储可变大小数据的高效数据库,这也是我们服务所需要的。
THREAD TABLE,需要按照更新时间(updated_at)排序,同时需要对OWNER_ID+THREAD_ID来做PK。所以可以用SQL。
如何重试失败的请求?
比如用户发送一条消息,判断有没有收到服务器的ACK。因此,我们可以告诉用户这条消息未能发送过去,让他们选择是否重试。
只有当ACK的MESSAGE 才会被存入DB。
管理用户状态
每当客户启动应用程序时,它都可以将朋友列表中所有用户的当前状态拉出。(每当用户向另一个离线用户发送消息时,我们都可以向发件人发送失败消息并更新客户端上的状态。)
每当用户上线时,服务器总是可以延迟几秒钟来广播该状态,以查看用户是否没有立即下线。
客户端可以从服务器获取有关用户视口中显示的用户的状态。这不应该是一个频繁的操作,因为服务器正在广播用户的在线状态,我们可以暂时处理用户的陈旧离线状态。
每当客户端与另一个用户开始新的聊天时,我们就可以在那时提取状态。
用户定期去拿新的好友在线离线状态。
数据分区
Thread_id 基于USERID 分区
基于UserID的分区:假设我们基于UserID的哈希进行分区,这样我们就可以将用户的所有消息保存在同一个数据库中。
MESSAGE 可以基于THREAD_ID来分区。
实时方案
用WEB SOCKET
有人一发送消息,服务器就可以进行推送。
断开连接后,用户如何接受消息
可以通过下次上线的PULL 或者IOS NOTIFICATION
这里要引进一个PUSH SERVER。
当用户上线时,首先问WEB SERVER 要一个PUSH SERVER的IP。随后和PUSH SERVER注册一个WEB SOCKET的双向连接。
之后用户B发给A消息,会先发给WEB SERVER,WEBSERVER知道A在哪个PUSH SERVER,把消息转发过去。
Q: WebSocket 和Socket 是什么联系和区别?
A: Socket 是很早就有的技术,Web Socket 是在H5 之后才诞生的技术,专门用于让浏览器支持被服务器推送信息所用。Socket 是更通用和强大的可以在任何地方使用的。WebSocket 只在浏览器上使用。
Q: 如果Push Server 宕机了怎么办?用户还收得到信息么?
A: 因为Socket 是一个双向连接,如果Push Server 宕机了,Client 端是知道链接已经断开了的,因此Client 端上只需要有一个backup 的逻辑,让client fallback 到每隔10s 拉一次数据的poll 机制就可以了。
群聊支持
群聊的问题主要是,有一些人没上线。如果所有人都在线,其实就是你发了一条消息,我就再向这个群的另外所有人都发送。
如果有很多人不在线的时候,WEB SERVER就会往PUSH SERVER空发很多信息。
让费了资源。
这里我们再引入一个CHANNEL SERVICE。当用户上线了,就订阅到每一个THREAD_ID(群聊的) 对应的CHANNEL SERVICE。 当用户下线,PUSH SERVER 会把这个用户从那些CHANEL 上移除。
这样MESSAGE SERVICE(web server)就只要向CHANEL SERVICE发信息。
CHANNEL SERVICE向当前在线的用户发消息给PUSH SERVICE。
Q: Channel Service 中的数据是什么结构?
A: key-value 的结构。key 为channel name,可以是一个字符串比如“#personal::user_1”。value是一个
set 代表哪些人订阅到了这个channel 下。
Q: Channel Service 用什么数据存储?
A: 根据上面所提到的key-value 结构以及value 需要是一个set,Redis 是一个很好的选择。
Q: 如何知道一个用户该订阅到哪些Channels?
A: 首先用户需要订阅自己的personal channel,如#personal::user_1,与该用户有关的私聊信息都在这个channel 里发送。小于一定人数的群聊可以依然通过personal channel 推送,超过一定人数的群聊,
可以采用lazy subscribe 的方式,在用户打开APP 且群处于比较靠前的位置的时候才订阅,用户没有主动订阅的群聊靠Poll 的模式获取最新消息。
Q: 用户关闭APP 以后还能收到提醒么?
A: 如果真的关闭了APP 是不行的。所以很多APP 会常驻后台,保证至少Poll 模式还能工作即可。
容错
聊天服务器出现故障会发生什么? 我们的聊天服务器与用户保持着联系。 如果服务器出现故障,我们是否应该设计一种机制将这些连接转移到其他服务器? 将TCP连接故障转移到其他服务器非常困难; 更简单的方法是让客户端在连接丢失时自动重新连接。
我们应该存储多个用户消息副本吗? 我们不能只有一个用户数据副本,因为如果持有数据的服务器崩溃或永久停机,我们没有任何恢复该数据的机制。 为此,我们必须在不同的服务器上存储多个数据副本,或者使用Reed-Solomon编码等技术来分发和复制它。