需求分析
功能
- 添加好友
- 聊天会话列表
- 单聊 A<->B
- 群聊
- 多设备登录
- 消息漫游
- 消息已读,查看已读/未读列表
视频通话(学音视频技术时补上)语音通话
约束
- DAU(Daily Active Users,日活跃用户数量) 10亿
- 假设每人平均每天发100条消息,1000Mli*100/86400 = 12Mli QPS(Queries per seconds,每秒请求数)
- 假设一条消息存储消耗1kb,1000Mli*100 * 1KB = 1P 每天
- 峰值预估12Mli *1.5 = 18Mli QPS
- 可靠性要求5个9
- 收发消息延迟在10ms以下
- 消息时序一致性(发送与接收端的消息顺序一致,不重不漏)
- 万人群聊
- 可运维性
可行解
更优解
高性能
接入层优化
- 轮询拉模式,无法满足实时性要求,消息不能及时触达用户
- 使用TCP长连接push消息给用户
- 客户端如何与服务端建立长链接?
- 客户端只能通过公网IP,Socket TCP编程直接连接服务端
- 通过一个IPConf服务下发公网IP给到客户端,灵活扩展并可智能调度长连接
- 建立长连接后 IM Server 业务层拿到FD与uid的映射
- IPconf 通过协调服务与IM Server做服务发现,根据机器的负载状态进行负载均衡
- 长连接服务占用大量内存资源,IO密集度高,而IM Server操作数据库逻辑较重整体性能受限
- 分离接入层与IM Server 业务层,长链接负责维护状态收发消息,IM Server负责业务
- 长连接服务为维持最优的消息通路,常需要以连接维度存储大量状态信息,用于做各种调度策略优化网络通信,同时会频繁迭代一些消息信令支持各种业务开发,这将导致由于服务的频繁迭代,对于一个有状态服务重启是缓慢的,并需要大量的重建连接影响消息收发延迟,该怎么办?
- 将变与不变的业务逻辑在服务上做到物理隔离,变化的服务底层使用DB,使其做到无状态
- 不变的消息通路(长连接网关) 保证其不会频繁迭代
- 长连接服务同步更新state server, IpConf 服务读取状态信息用于旁路调度决策
- State 通过状态变化控制长连接的断开,通过MQ发送close信令用于长连接的调度
- 如何知道客户端B的长连接在哪个接入层服务器上,进行消息路由并保证消息可靠送达呢?
- 广播
- 一致性哈希
- 路由服务层
存储层优化
- 读写比 1:9999,超大群每人发送一条消息要1亿分发量,应该如何控制超大群将机器资源耗尽?
- 并发控制,做到按群维度限制最大并发数,保证超大群收发消息不至于导致系统崩溃
- 限制并发数量后必然会导致消息挤压,应该如何处理收发消息延迟增大的问题?
- 达到并发限制后,消息如果积压则将一段窗口内的消息合并压缩后下发
- 使用推拉结合的方式本质上就是通过信令对消息进行了压缩,但是多了一步拉取的网络调用
- 如果用推拉结合的模式可以在一段时间窗口内聚合对同一用户的拉取信令减少网络调用次数
- 存储系统上存在的严重的写放大(扩散)问题?如何减少写入数据量,降低存储成本?
- 对超大群降级为读放大模式进行存储,消息仅同步写入收件箱
- 群状态信息,如回执消息等写入Hbase等存储中
- 那么回执消息应该如何处理?如何保证万人群中的已读/未读列表与数量的一致性?
- 实时流处理,对于接收者已读消息的下发要同步进行,而接收者的消息已读可以异步落库
- 对于超大群,群状态变更服务进行降级,不再发送信令通知(可自动识别群的聊天热度)
- 异步写入通过重试保证最终一致性
- 从DB直接取数无法满足延迟10ms以下,应该如何优化?
计算机各种操作耗时
- 分级存储
- 多元DB存储
- 图数据库
- 存储层代理服务
消息时序一致性
- 消息如何不丢失?
- 上行消息客户端重试
- 下行消息服务端重试
- 接收消息方需要回复ACK,避免无限重试
- 如何防止重复?
- 使用UUID去重复是否可以行?
- 上行消息
- 客户端在一个会话内生成一个消息的递增cid,服务器存储当前会话接收到的max cid
- 如果发送的消息不是max_cid+1,则将其丢弃
- 下行消息
- 服务端为每个发送的消息分配seqid,客户的维护接收到的最大max_seq_id
- 如果发送的消息不是max_seq_id+1,则将其丢弃
- 如何保证消息是有序的?
- 按某个递增的消息ID排序
- 上行消息按cid为每个消息分配seqID
- 下行消息客户端按seqID进行排序
- 如何生成递增的消息ID?
-
纯拉模式 不存在一致性问题,只要保证上行消息一致性即可
- 缺点:
- 优点
-
单调递增ID生成器
保证会话内seqID的‘万有一失’的单调递增性+拉模式消息补洞- 缺点
- 优点
-
双ID链方法
- 接收方为每个会话保留preID(上一次接收到消息的id)
- 发送方发送当前消息的id以及上一次发送消息的id(preID)
- 形成消息链条,接收方对比preID判断是否一致来识别是否存在消息漏洞
- 如果preID不匹配,则将接收方的preID返回
- 发送方回退到接收方的preID的位置,重置所有消息心跳后重新发送
- 缺点
- 优点
-
推拉结合:
- 下行消息给客户端发送拉取信令,通知客户端根据自身seqID拉取消息
- pull 请求本身可作为上一次请求的ACK
- 缺点
- 优点
-
上行消息 PreID+下行消息 推拉结合
- 缺点
- 优点
高可用
- 跨越公网的长连接如果断了怎么办?
-
心跳保活
- 为什么应用层还需要心跳? tcp的心跳不好用吗?
- 客户端周期性发送心跳给IM Gateway,IM Gateway重置内部定时器
- 控制心跳包不要过大,通常在0.5kb以下
- 心跳过长,服务端感知到已经断线的客户端的效率越低,资源利用率也就越低
- 心跳过短,则会造成心跳潮汐,给网关造成流量压力(通过随机打撒来解决问题)
- 发送心跳会经过很多运营商网络,不同地区运营商对于长链接的资源回收策略不同GGSN
- 自适应心跳包(前台状态固定心跳,后台状态自适应)测算NAT淘汰时间
-
断线重连
- 客户端由于网络原因断线(频繁切换网络,做地铁),如何能快速稳定的重新建立长链接?
1. 断线后快速重连几次到原来的服务端
2. 服务端发现连接断开后不会立即删除用户的状态信息,等待一个超时时间
3. 在这段时间内,客户端快速建立新连接,服务端复用原有状态信息实现重连接 - 服务端如果崩溃导致大规模客户端重新连接造成雪崩如何处理?
1. IPConf服务通过服务发现机制快速识别服务端节点故障,进行调度
2. 客户端断线后,通过一定的随机策略打散重连请求的时间
3. Get ipconf服务拿到新的ip调度列表重新调度,如果原有服务器还在其中则优先选
- 客户端由于网络原因断线(频繁切换网络,做地铁),如何能快速稳定的重新建立长链接?
-
心跳风暴
-
弱网场景下如何保证消息可靠触达?
- 快链路 优化TCP连接
1. 限定传输数据包的大小1400字节,避免超过MSS造成IP数据分片
2. 放大TCP拥塞控制窗口,为有线网络设计的拥塞控制算法,并不适用于无线网络
3. 调整socket读写缓冲区,避免数据包溢出
4. 调整RTO初始值设置为3s,避免重试造成的堵塞
5. 禁用Nagle算法延迟算法,避免小数据包被协议栈缓存 - 调度策略优化
1. 赛马机制,多个ip后台测速选择最快连接线路(服务端要对请求进行识别,避免资源浪费)
2. 调度策略基于客户端上报数据进行计算,ipconf分发最佳ip进行调度
3. 基于网络环境设置不同的超时时间,超时参数由Ipconf服务动态下发,加入策略计算
4. 短链退化, 当长链接断线次数太多,重连过于频繁的极端弱网环境例如地铁,则退化为轮询模式收发消息 - 协议优化
1. 弱网场景下切换QUIC协议
2. 精简协议包,采用自定义二进制协议
- 快链路 优化TCP连接
-
异地多活方案如何设计?
- 核心思想:尽量减少广域请求
- 基于跨数据中心的服务发现系统
- 通过IPconf进行旁路调度,IPConf跨数据中心感知到IM Gateway
- IPConf基于客户端ip位置信息进行IDC的流量分片
- 基于位置策略调度最优IDC中的最优IM Gateway Server
- 如果IDC 不可用,IPConf服务自动切流量到可用的IDC上
- 每个IDC存储全量数据以应对机房级故障,基于底层同步组件同步数据
- IM Server 通过跨IDC 的Route Server服务发现IM Gateway并通过跨IDC的MQ发送消息
- Route Server 将自己IDC创建的kv对广播给其他IDC,以便于进行路由转发
- IM Server转发消息给到接收者所在的IDC的IM Gateway上,消息内携带此消息写入的IDC
- 接收者根据push通知中携带的多个IDC信息,并行的去多个IDC拉取消息
- 为应对转发失败的情况,客户端可以在未收到任何push通知的一段时间后主动pull消息