58集团面向亿级用户IM长连接服务设计与实践

  1. 1 长连接服务简介
  2. 2 架构设计
  3. 3 线程管理
  4. 4 内存管理
  5. 5 TCP拆包流程
  6. 6 长连接保活
  7. 7 容量控制
  8. 8 总结

1 长连接服务简介

微聊,是58一款聊天工具,目前已经接入58的大部分产品,及时准确数据传输,是对一款聊天工具最基本的要求。长连接服务就是在客户端到服务端之间建立一条全双工的数据通路,实现客户端和服务端之间逻辑收发数据,在线离线等功能。

角色
(1) 长连接服务在整个微聊系统中,位于客户端与后台逻辑之间;
(2) 整个后台服务最重要的直接对外接口之一(另一个是短连接请求nginx);
(3) 长连接服务对外和对内均采用TCP连接;
长连接

系统瓶颈
(1) 长连接服务主要功能是收发数据,保持在线,使用的系统资源主要包括:CPU,内存,网卡;
(2) 当连接活跃时,大量数据接收与发送,会用到更多的CPU和网卡,大量用户在线的时候,需要维持这些连接,保持会话,需要用到大量内存;
(3) 考虑到微聊的实际场景,APP端占有很大比例,由于手机的网络环境相对PC来说不太稳定,需要处理大量连接的新建与断开,所以对CPU使用率比较高;

设计难点
(1) 设计单台物理连接数100W的处理能力;
(2) CPU资源充分利用,线程的分配;
(3) 内存合理分配,数据结构选择;
(4) 异步化,剥离业务逻辑和网络IO之间的相互依赖;

2 架构设计

架构设计
架构的设计需要考虑TCP连接管理与应用层协议解析和处理分离,下面是主要功能模块:
(1) TCP连接层(黑色虚线框)连接保持,session管理等,实现了TCP和TLS层;
(2) BlayServer,逻辑层服务,逻辑层节点管理,协议解析等;
(3) ClientServer,客户端服务,客户端协议解析;
(4) Protocol,http,websocket,protocol协议封装;
(5) Tools,json,log,config,crypto;

3 线程管理

为了充分利用CPU,需要合理的进行线程规划。整个长连接服务使用事件驱动,包括:定时器事件和IO事件(listen fd,socket fd, pipe fd), 所以线程规划就是合理给这些事件分配线程。

线程优化
(1) 大量连接socket需要平均分配到各个线程;
(2) 新的连接请求量比较大,listen线程压力大,需要考虑多线程处理;
线程分配
主要涉及到3种fd的线程分配:
(1) 监听fd,包括监听逻辑层的连接请求和监听客户端的连接请求,并且支持启动多个监听端口,每个端口多个线程同时工作,整体会按顺序分配到线程;
(2) 连接fd,包括逻辑层的连接和客户端的连接,采用fd取余线程数,保证平均分配到所有线程;
(3) pipe fd,负责本线程管道中数据的读取,每个线程一个;
这个样是的每个线程的CPU使用率相当。

线程间通信
客户端fd和listen fd,采用不同的规则分配到同一组线程中,当客户端的数据需要发给逻辑层,或是逻辑层数据需要发给客户端,就存在客户端连接和逻辑层的连接存在同一线程或不同线程两种情况,不同线程之间通信主要考虑了以下几种方式:
(1) 加锁,比如session读写锁,从而让session可以在多线程中操作,会导致多个线程竞争资源,阻塞线程;
(2) 共享内存,需要加锁保证原子性,需要线程设置定时器做可读性检测,实现复杂;
(3) 我们采用操作系统提供的管道--pipe解决线程间通信的问题;
pipe线程通信
pipe通信:
(1) close session,一个线程需要关闭另一个线程上的连接,比如收到逻辑层需要把某个用户踢下线;
(2) send to session,一个线程给另一个线程发送数据,比如给某个用户推送消息;
(3) 应用层事件,应用层事件跨线程转发,一种通用的跨线程调用;

4 内存管理

在长连接服务工作过程中,会有大量会话不停创建和销毁,在会话过程中,又会有大量长度不等的数据通信,长时间稳定的服务需要合理高效的内存使用,包括以下几个方面:操作系统TCP协议栈内存使用,服务中session管理,读写数据缓存(Buffer)等。
内存使用
说明:
(1) 最主要的内存使用是TCP协议栈,包括TCP和读写缓存,其它内存使用体现在session存储和session读写buffer;
(2) 由于在TCP上层实现了更加逻辑友好的Buffer(后面会详细讲到),实际部署中可以把TCP协议栈中的读写Buffer设置成比较小的空间,比如1k;
session静态模型
当一个客户端在线,服务端会生成一个session保存该连接的状态、逻辑信息和socket信息等,大量的会话保持,需要服务端合理的管理这些session,考虑以下几种存储结构:
(1) Hash:能够实现快速存取,需要自己实现Hash算法,经常分配释放内存会导致内存碎片;
(2) 固定数组:需要一开始分配大块内存,不支持动态扩展,但存取快速,不会有内存碎片;
(3) 动态数据:动态扩展,存取快速,无内存碎片,但扩展的时候会有大块内存分配与拷贝;
长连接服务采用数组存储session:
(1) 预先分配100W session全量的空间,相当于100W session数组,内存耗费418M,且有容量使用监控,防止空间满了建立连接失败;
(2) 直接使用session对应fd作为下标,实现存取$O(1)$;
(3) 由于fd分配策略是从小到大,故可保证数组在当前连接数下的空间利用率比较高;
session存储
Buffer
在socket读写过程中,会遇到以下几个问题:
(1) 当向socket中写数据的时候,如果当前不足容纳要写的所有数据,则会写入部分数据,剩下的数据需要在socket可写的时候继续写入,以保证数据的连贯性,这里需要有一个保存并记录需要写入数据的数据结构,且需要保证写入数据的先后顺序;

(2) 当从socket中读数据的时候,TCP只保证数据流的顺序性,并不知道应用层协议包大小,所以需要从TCP流中分离出应用层数据包,在解决拆包和粘包问题的过程中,遇到数据包不够的情况下,需要等待后续数据的读入,直到读到的数据构成一个完整的应用层数据包,然后整体返回给上层;

(3) 在发送和接收的过程中,需要支持大小不一样的应用层数据包,所以这里需要一个可以发送拆解大数据包的缓存队列;

为了满足以上功能和要求,我们设计了Buffer:
(1) 分配与释放采用固定存储单元防止产生内存碎片;
(2) 动态扩展的双向循环链表(队列);
(3) 对外提供了连续数据存取接口;
Buffer

5 TCP拆包流程

TCP是面向字节流的传输,TCP保证了传输数据的顺序性和可靠性,当接收到字节流的时候,如何分离出上层协议,是TCP拆包应该考虑的问题。
![拆包]](/pic/tech9_pc8.png)
如上图所示,逻辑层需要实现getProtocolSize和receiveCallback两个接口,前者通过参数中传入的数据判断出当前应用层数据包大小,后者是返回应用层数据包的回调。 当socket读事件发生时,首先从socket中读取数据,写入buffer中,然后调用Buffer的预读接口(只是返回队列头部的只读指针,并不拷贝数据),调用getProtocolSize接口,由逻辑层返回应用层数据包大小(只有应用层才知道自己该识别哪种协议),再根据该大小从Buffer队列中读出协议包,最后通过receiveCallback返回给上层处理。

6 长连接保活

长连接保活由于TCP自身的断开确认机制,如果一条TCP连接中间网络断开,此时客户端和服务端物理网络断开导致客户端和服务端都没有办法通知对方连接断开,这样服务端和客户端就会存在死连接,造成假在线,占用的资源得不到有效的回收,长连接保活主要有下面两种方式:
(1) TCP keepalive,通过设置Keepalive参数,TCP协议栈会在超过一定时间没有数据交互的时候,发送Keepalive探测包,如果连接几次都没有收到回包,则断开连接,优点是TCP协议栈提供的功能,稳定占用带宽较少;
(2) 采用应用层心跳包的方式,客户端定时向服务端发送心跳包,服务端收到心跳包后立即进行回包,客户端如果没有检测到回包,则断开连接;服务端检测超时还没有收到心跳包,则断开连接,优点主要是应用层有感知,可控并且可以带些业务数据,比如时间戳;

微聊长连接,考虑到多端支持(Android, iOS, Web等),兼容老版本实现方式,应用层协议主要使用http,提供了http-chunk和 http-long-polling两种Http接入方式,这样就限制了客户端上发数据,所以我们采用了服务端下发心跳,和Keepalive结合的方式,实现服务端和客户端的保活, 如下图:
![长连接保活]](/pic/tech9_pc9.png)
(1) 服务端通过开启TCP Keepalive进行保活,当一条连接超过一定时间没有活动, 服务端会发送Keepalive包,如果连续3次都没收到回包,服务端就会认为这是一个死连接,关闭;
(2) 对于客户端,服务端会定时下发心跳包,客户端通过监测心跳包来判断当前连接是否正常,如果不能正常收到心跳包,则会重新建立新的连接;

7 容量控制

在TCP连接建立到通信的流程中,为了防止一些恶意连接与攻击,长连接服务做了容量控制,体现在下面几个方面:
(1) 客户端建立TCP连接到TLS握手,再到发出登录请求返回登录结果,整个过程是连贯的,如果客户端停在中间的某一步骤而不往下进行,就会一直占用服务端资源,针对这种情况,服务端增加了定时控制,在TCP 连接之后30s,如果没有收到登录请求,服务端会主动断开连接;
(2) 服务统计了正在进行TLS握手,正在进行登录校验的连接数量,分别设置上限,防止同时出现大量请求的时候,对后端服务的冲击;
(3) 增加了会话通信过程中Buffer内存使用量的上限,防止对端不接收数据,导致服务端数据积压;
(4) 增加了IP统计服务,建立连接后,会将新连接的IP等信息发送到IP统计服务,对整个服务的客户端IP 情况作统计监控,增加IP黑名单功能;

8 总结

(1) 长连接服务是微聊的基础服务,稳定性尤其重要;
(2) 在稳定性的基础上,通过TLS握手优化等,不断提高建立连接的速度,更好的应对断网,弱网等复杂的外网环境;
(3) 通过支持更多的应用层协议,提高多端多设备的接入能力;
(4) 通过监控,不断挖掘潜在在安全威胁,同时预防常见的网络安全问题;


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至yj.mapple@gmail.com

文章标题:58集团面向亿级用户IM长连接服务设计与实践

文章字数:3.1k

本文作者:melonshell

发布时间:2020-01-30, 12:11:17

最后更新:2020-10-19, 11:27:19

原始链接:http://melonshell.github.io/2020/01/30/tech9_58pc/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏

相册