即时通讯IM技术领域提高篇

6,245 阅读35分钟

[TOC]

即时通讯IM技术领域提高篇

即时通讯IM技术领域基础篇

接入层的服务器程序如何升级

对于当前特定Access长连接接入服务而言

我经历的xxx项目中的情况:

  1. Access接入层服务, tcp长连接的, 如果需要更新的话, 那不是客户端需要重新登录 ?

    • 是的,但是可以改造,access 再剥一层出来专门维护长连接
  2. access 分为连接层和 access,前者不涉及业务,所以预期不用重启,后者承载业务,更新重启对连接没有影响。后面还考虑把 push 合进 access

  3. 连接层和 access 通过共享内存来维护连接信息。

对于通用接入层而言

  1. 调整接入层有状态=>无状态, 接入层与逻辑层严格分离.

  2. 无状态的接入层,可以随时重启

  3. 逻辑服务层,可以通过etcd来做服务发现和注册,方便升级和扩容

单台服务器维持的TCP长连接数

  1. 操作系统包含最大打开文件数(Max Open Files)限制, 分为系统全局的, 和进程级的限制

    • fs.file-max
    • soft nofile/ hard nofile
  2. 每个tcp的socket连接都要占用一定内存

    • 通过测试验证和相关数据,只是保持connect,什么也不做,每个tcp连接,大致占用4k左右的内存,百万连接,OS就需要4G以上内存.
    • 这里注意还要修改net.ipv4.tcp_rmem/net.ipv4.tcp_wmem
  3. 网络限制

    • 假设百万连接中有 20% 是活跃的, 每个连接每秒传输 1KB 的数据, 那么需要的网络带宽是 0.2M x 1KB/s x 8 = 1.6Gbps, 要求服务器至少是万兆网卡(10Gbps).
  4. 一些基本常用的sysctl的修改:

    • net.ipv4.tcp_mem = 78643200 104857600 157286400
    • net.ipv4.tcp_rmem=4096 87380 16777216
    • net.ipv4.tcp_wmem=4096 87380 16777216
    • net.ipv4.ip_local_port_range = 1024 65535
    • net.ipv4.tcp_tw_recycle=1
    • net.ipv4.tcp_tw_reuse=1
    • fs.file-max = 1048576
    • net.ipv4.ip_conntrack_max = 1048576

    n = (mempages * (PAGE_SIZE / 1024)) / 10;

    PAGE_SIZE:typically 4096 in an x86_64

    files_stat.max_files = n;

  5. epoll机制,长连接数太多,会影响性能吗? <底层采用红黑树和链表来管理数据>

    • 这个不会影响tcp连接和性能, 哪怕epoll监控的事件再多,都OK
    • 内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。
  6. 实际应用中应该考虑哪些点呢?

    • 网卡多队列的支持, 查看网卡是否支持,要不然cpu不能很好处理网络数据, 这个需要好的网卡,也消耗cpu

    • 维护tcp长连接的节点管理, 这个需要消耗cpu, 需要有对应的数据结构来进行管理

    • 实际中,还应该考虑,每秒中能够建立连接的速度,因为百万连接并不是一下就建立的,如果重启了重连,那么连接速度如何呢 ?

    • 如果这个节点挂掉了,请求的分摊处理怎么弄?

    • 应用层对于每个连接的处理能力怎样? 服务端对协议包的解析处理能力如何 ?

    • tcp mem 问题,没有用到就不会分配内存, 但是不一定会马上回收.

  7. 关于长连接的另外考虑点:

    • 在稳定连接情况下,长连接数这个指标,在没有网络吞吐情况下对比,其实意义往往不大,维持连接消耗cpu资源很小,每条连接tcp协议栈会占约4k的内存开销,系统参数调整后,我们单机测试数据,最高也是可以达到单实例300w长连接。但做更高的测试,我个人感觉意义不大。

    • 实际网络环境下,单实例300w长连接,从理论上算压力就很大:实际弱网络环境下,移动客户端的断线率很高,假设每秒有1000分之一的用户断线重连。300w长连接,每秒新建连接达到3w,这同时连入的3w用户,要进行注册,加载离线存储等对内rpc调用,另外300w长连接的用户心跳需要维持,假设心跳300s一次,心跳包每秒需要1w tps。单播和多播数据的转发,广播数据的转发,本身也要响应内部的rpc调用,300w长连接情况下,gc带来的压力,内部接口的响应延迟能否稳定保障。这些集中在一个实例中,可用性是一个挑战。所以线上单实例不会hold很高的长连接,实际情况也要根据接入客户端网络状况来决定。

  8. 注意的一点就是close_wait 过多问题,由于网络不稳定经常会导致客户端断连,如果服务端没有能够及时关闭socket,就会导致处于close_wait状态的链路过多。

    • close_wait状态的链路并不释放句柄和内存等资源,如果积压过多可能会导致系统句柄耗尽,发生“Too many open files”异常,新的客户端无法接入,涉及创建或者打开句柄的操作都将失败。
  9. 考虑到不同地区不同网络运营商的情况下,用户可能因为网络限制,连接不上我们的服务或者比较慢。

    • 我们在实践中就发现,某些网络运营商将某些端口封禁了,导致部分用户连接不上服务。为了解决这个问题,可以提供多个ip和多个端口,客户端在连接某个ip比较慢的情况下,可以进行轮询,切换到一个更快的ip。
  10. TCP_NODELAY

    • 针对这个话题,Thompson认为很多在考虑微服务架构的人对TCP并没有充分的理解。在特定的场景中,有可能会遇到延迟的ACK,它会限制链路上所发送的数据包,每秒钟只会有2-5个数据包。这是因为TCP两个算法所引起的死锁:Nagle以及TCP Delayed Acknowledgement。在200-500ms的超时之后,会打破这个死锁,但是微服务之间的通信却会分别受到影响。推荐的方案是使用TCP_NODELAY,它会禁用Nagle的算法,多个更小的包可以依次发送。按照Thompson的说法,其中的差别在5到500 req/sec。

    • tcp_nodelay 告诉nginx不要缓存数据,而是一段一段的发送--当需要及时发送数据时,就应该给应用设置这个属性,这样发送一小块数据信息时就不能立即得到返回值。

    • 我们发现 gRPC 的同步调用与 Nagle's algorithm 会产生冲突,虽然 gRPC 在代码中加入了 TCP_NODELAY 这个 socketopt 但在 OS X 中是没有效果的。后来通过设定 net.inet.tcp.delayed_ack = 0 来解决,同样我们在 linux 下也设置了 net.ipv4.tcp_low_latency = 1,这样在 100M 带宽下一次同步调用的时间在 500us 以下。而且在实际应用中,我们通过 streaming 调用来解决大量重复数据传输的问题,而不是通过反复的同步调用来传相同的数据,这样一次写入可以在 5us 左右。其实批量写入一直都是一个很好的解决性能问题的方法S


心跳相关处理

  1. 心跳其实有两个作用

    • 心跳保证客户端和服务端的连接保活功能,服务端以此来判断客户端是否还在线
    • 心跳还需要维持移动网络的GGSN.
  2. 最常见的就是每隔固定时间(如4分半)发送心跳,但是这样不够智能.

    • 心跳时间太短,消耗流量/电量,增加服务器压力.
    • 心跳时间太长,可能会被因为运营商的策略淘汰NAT表中的对应项而被动断开连接
  3. 心跳算法 (参考Android微信智能心跳策略)

    • 维护移动网GGSN(网关GPRS支持节点)
      • 大部分移动无线网络运营商都在链路一段时间没有数据通讯时,会淘汰 NAT 表中的对应项,造成链路中断。NAT超时是影响TCP连接寿命的一个重要因素(尤其是国内),所以客户端自动测算NAT超时时间,来动态调整心跳间隔,是一个很重要的优化点。
    • 参考微信的一套自适应心跳算法:
      • 为了保证收消息及时性的体验,当app处于前台活跃状态时,使用固定心跳。
      • app进入后台(或者前台关屏)时,先用几次最小心跳维持长链接。然后进入后台自适应心跳计算。这样做的目的是尽量选择用户不活跃的时间段,来减少心跳计算可能产生的消息不及时收取影响。
  4. 精简心跳包,保证一个心跳包大小在10字节之内, 根据APP前后台状态调整心跳包间隔 (主要是安卓)


弱网环境下的相关处理

  1. 网络加速 cdn

    • 包括信令加速点和图片CDN网络
  2. 协议精简和压缩

    • 使用压缩算法,对数据包进行压缩
  3. TCP第一次通过域名连接上后,缓存IP,下次进行IP直连;若下次IP连接失败,则重新走域名连接

  4. 对于大文件和图片等, 使用断点上传和分段上传

  5. 平衡网络延迟和带宽的影响

    • 在包大小小于1500字节时, 尽量合并请求包. 减少请求
  6. ip就近接入

    • ip 直连(域名转ip)
    • 域名解析(ip库), 域名解析的耗时在移动网络中尤其慢
    • 计算距离用户地理位置最近的同一运营商的接入点

断线重连策略

掉线后,根据不同的状态需要选择不同的重连间隔。如果是本地网络出错,并不需要定时去重连,这时只需要监听网络状态,等到网络恢复后重连即可。如果网络变化非常频繁,特别是 App 处在后台运行时,对于重连也可以加上一定的频率控制,在保证一定消息实时性的同时,避免造成过多的电量消耗。

  1. 断线重连的最短间隔时间按单位秒(s)以4、8、16...(最大不超过30)数列执行,以避免频繁的断线重连,从而减轻服务器负担。当服务端收到正确的包时,此策略重置

  2. 有网络但连接失败的情况下,按单位秒(s)以间隔时间为2、2、4、4、8、8、16、16...(最大不超过120)的数列不断重试

  3. 重连成功后的策略机制

    • 合并部分请求,以减少一次不必要的网络请求来回的时间
    • 简化登录后的同步请求,部分同步请求可以推迟到UI操作时进行,如群成员信息刷新。
  4. 在重连Timer中,为了防止雪崩效应的出现,我们在检测到socket失效(服务器异常),并不是立马进行重连,而是让客户端随机Sleep一段时间(或者上述其他策略)再去连接服务端,这样就可以使不同的客户端在服务端重启的时候不会同时去连接,从而造成雪崩效应。


网络切换怎么处理? 是否需要重连,是否重新登录?

  1. 一般的话,有网络切换(3g->4g->wifi->4g)就重连,重新走一遍整体流程

  2. 最好APP能以尽量少的通讯量来重新注册服务器, 比如不再从服务器获取配置信息,从上一次拉取的服务器配置的缓存数据直接读取(如果服务器改变,最好能够发一条通知给app更新)

  3. 如从wifi 切换到4G、处于地铁、WIFI边缘地带等,为避免造成重连风暴(因为网络不稳定,会频繁发起重连请求), 可以采用稍加延迟重连策略


服务端程序怎么扩容/缩容? 水平扩展方案?

  1. 采用业界常用的分布式服务发现,配置方案. 如通过etcd来进行服务发现和注册.

  2. 设计的各个模块要能独立化部署,设计为无状态,例如所谓的微服务, 这样才能够很好的做服务的升级、扩容, 保证无单点故障, 也方便灰度发布更新

  3. 动态配置


群消息相关

  1. 消息是写扩散,还是读扩散: 群里面每个人都写一次相同的消息,还是群里面都从同一个地方读取这条相同消息?

    • 写扩散: 简单,但是群里面每个人都要写一遍缓存.数据量有点大,而且增加网络消耗(比如写redis的时候).

    • 读扩算: 只写一份到缓存,拉取的时候,从这个群缓存消息里面拉,需要增加一点逻辑处理,方便在所有群成员都拉取完后删掉缓存数据(或者过期)

  2. 发送方式

    • 遍历群成员,如果在线就依次发送, 但是群成员多,群活跃的时候,可能会增大压力.

    • 遍历群成员, 在线人员, 服务内部流转(rpc)的时候是否可以批量发送?

  3. 群方式

    • 在线的,msg只有一份到db中, index还是写扩散到cache和db中.
    • 离线的,缓存中,写扩散(msg和index),如果缓存失效,则穿透到db中拉取.
  4. 对于群消息,每条消息都需要拉取群成员的在线状态.如果存放在redis,拉取会太过频繁.连接数会暴增,并发过高. 这样可以增加一级本地缓存,把连接信息放到本地缓存(通过消耗内存来减少网络连接和请求)


客户端减小电量消耗策略

  1. 不能影响手机休眠,采用alarm manager触发心跳包

  2. 尽量减少网络请求,最好能够合并(或者一次发送多个请求). 批量、合并数据请求/发送

  3. 移动网络下载速度大于上传速度,2G一次发送数据包不要太大,3G/4G一次发送多更省电.


消息是如何保证可达(不丢)/唯一/保序?

  1. 消息头包含字段dup, 如果是重复递送的消息,置位此字段,用来判定重复递送

  2. 服务端缓存对应的msgid列表, 客户端下发已收到的最大msgid, 服务端根据客户端收到的最大msgid来判断小于此id的消息已经全部被接收.这样保证消息不丢.

  3. 服务端确保msgid生成器的极度高的可用性,并且递增, 通过msgid的大小,来保证消息的顺序

详细说明消息防丢失机制

为了达到任意一条消息都不丢的状态,最简单的方案是手机端对收到的每条消息都给服务器进行一次ack确认,但该方案在手机端和服务器之间的交互过多,并且也会遇到在弱网络情况下ack丢失等问题。因此,引入sequence机制

  1. 每个用户都有42亿的sequnence空间(从1到UINT_MAX),从小到大连续分配
  2. 每个用户的每条消息都需要分配一个sequence
  3. 服务器存储有每个用户已经分配到的最大sequence
  4. 手机端存储有已收取消息的最大sequence
    image.png

** 方案优点 **

  1. 根据服务器和手机端之间sequence的差异,可以很轻松的实现增量下发手机端未收取下去的消息

  2. 对于在弱网络环境差的情况,丢包情况发生概率是比较高的,此时经常会出现服务器的回包不能到达手机端的现象。由于手机端只会在确切的收取到消息后才会更新本地的sequence,所以即使服务器的回包丢了,手机端等待超时后重新拿旧的sequence上服务器收取消息,同样是可以正确的收取未下发的消息。

  3. 由于手机端存储的sequence是确认收到消息的最大sequence,所以对于手机端每次到服务器来收取消息也可以认为是对上一次收取消息的确认。一个帐号在多个手机端轮流登录的情况下,只要服务器存储手机端已确认的sequence,那就可以简单的实现已确认下发的消息不会重复下发,不同手机端之间轮流登录不会收到其他手机端已经收取到的消息。


通信方式(TCP/UDP/HTTP)同时使用tcp和http.

  1. IM系统的主要需求:包括账号、关系链、在线状态显示、消息交互(文本、图片、语音)、实时音视频

  2. http模式(short链接)和 tcp 模式(long 链接),分别应对状态协议和数据传输协议

  3. 保持长连接的时候,用TCP. 因为需要随时接受信息. 要维持长连接就只能选TCP,而非UDP

  4. 获取其他非及时性的资源的时候,采用http短连接. 为啥不全部用TCP协议呢? 用http协议有什么好处?

    • 目前大部分功能可以通过TCP来实现.
    • 文件上传下载的话,就非http莫属了
      • 支持断点续传和分片上传.
    • 离线消息用拉模式,避免 tcp 通道压力过大,影响即时消息下发效率
    • 大涂鸦、文件采用存储服务上传,避免 tcp 通道压力过大
  5. IM到底该用UDP还是TCP协议

    • UDP和TCP各有各的应用场景,作为IM来说,早期的IM因为服务端资源(服务器硬件、网络带宽等)比较昂贵且没有更好的办法来分担性能负载,所以很多时候会考虑使用UDP,这其中主要是早期的QQ为代表。

    • TCP的服务端负载已经有了很好的解决方案,加之服务器资源成本的下降,目前很多IM、消息推送解决方案也都在使用TCP作为传输层协议。不过,UDP也并未排除在IM、消息推送的解决方案之外,比如:弱网络通信(包括跨国的高延迟网络环境)、物联网通信、IM中的实时音视频通信等等场景下,UDP依然是首选项。

    • 关于IM到底该选择UDP还是TCP,这是个仁者见仁智者见智的问题,没有必要过于纠结,请从您的IM整体应用场景、开发代价、部署和运营成本等方面综合考虑,相信能找到你要的答案。


服务器和客户端的通信协议选择

  1. 常用IM协议:IM协议选择原则一般是:易于拓展,方便覆盖各种业务逻辑,同时又比较节约流量。后一点的需求在移动端IM上尤其重要?

    • xmpp: 协议开源,可拓展性强,在各个端(包括服务器)有各种语言的实现,开发者接入方便。但是缺点也是不少:XML表现力弱,有太多冗余信息,流量大,实际使用时有大量天坑。

    • MQTT: 协议简单,流量少,但是它并不是一个专门为IM设计的协议,多使用于推送. 需要自己在业务上实现群,好友相关等等. 适合推送业务,适合直播IM场景。

    • SIP: 多用于VOIP相关的模块,是一种文本协议. sip信令控制比较复杂

    • 私有协议: 自己实现协议.大部分主流IM APP都是是使用私有协议,一个被良好设计的私有协议一般有如下优点:高效,节约流量(一般使用二进制协议),安全性高,难以破解。

  2. 协议设计的考量:

    • 网络数据大小——占用带宽,传输效率:虽然对单个用户来说,数据量传输很小,但是对于服务器端要承受众多的高并发数据传输,必须要考虑到数据占用带宽,尽量不要有冗余数据,这样才能够少占用带宽,少占用资源,少网络IO,提高传输效率;

    • 网络数据安全性——敏感数据的网络安全:对于相关业务的部分数据传输都是敏感数据,所以必须考虑对部分传输数据进行加密

    • 编码复杂度——序列化和反序列化复杂度,效率,数据结构的可扩展性

    • 协议通用性——大众规范:数据类型必须是跨平台,数据格式是通用的

  3. 常用序列化协议比较

    • 提供序列化和反序列化库的开源协议: pb,Thrift. 扩展相当方便,序列化和反序列化方便

    • 文本化协议: xml,json. 序列化,反序列化容易,但是占用体积大.

  4. 定义协议考量

    • 包数据可以考虑压缩,减小数据包大小

    • 包数据考虑加密,保证数据安全

    • 协议里面有些字段uint64,可以适当调整为uint32.减小包头大小

    • 协议头里面最好包含seq_num

      • 这个是为了异步化的支持。这种消息通道最重要的是解决通道问题,所有消息处理不能是同步的,必须是异步的,你发一个消息出去,ABC三个包,你收到XYZ三个包之后,你怎么知道它是对应的,就是对应关系的话我们怎么处理,就是加一个ID

IM系统架构设计的重点考量点

  1. 编码角度:采用高效的网络模型,线程模型,I/O处理模型,合理的数据库设计和操作语句的优化;

  2. 垂直扩展:通过提高单服务器的硬件资源或者网络资源来提高性能;

  3. 水平扩展:通过合理的架构设计和运维方面的负载均衡策略将负载分担,有效提高性能;后期甚至可以考虑加入数据缓存层,突破IO瓶颈;

  4. 系统的高可用性:防止单点故障;

  5. 在架构设计时做到业务处理和数据的分离,从而依赖分布式的部署使得在单点故障时能保证系统可用。

  6. 对于关键独立节点可以采用双机热备技术进行切

  7. 数据库数据的安全性可以通过磁盘阵列的冗余配置和主备数据库来解决。


TCP 拥堵解决方案

TCP的拥塞控制由4个核心算法组成:“慢启动”(Slow Start)、“拥塞避免”(Congestion voidance)、“快速重传 ”(Fast Retransmit)、“快速恢复”(Fast Recovery)。


怎么判断kafka队列是否滞后了?

kafka队列,没有满的概念, 只有消费滞后/堆积的概念

  1. 通过offset monitor 监控对kafka进行实时监控

  2. 对于kafka

    • 本身就是一个分布式,本身就能给支持这种线性的扩展,所以不会面临这种问题。

    • 你会写数据不消费么。


操作缓存和数据库的方案

  1. 写: 先写数据库,成功后,更新缓存

  2. 读: 先读缓存, 没有数据则穿透到db.

但是, 假如我写数据库成功,更新缓存失败了. 那下次读的时候,就会读到脏数据(数据不一致),这种情况怎么处理?

方案:

  1. 先淘汰缓存,再写数据库. 但是如果在并发的时候,也可能出现不一致的问题,就是假如淘汰掉缓存后,还没有及时写入db, 这个时候来了读请求,就会直接从db里面读取旧数据.

    • 因此,需要严格保证针对同一个数据的操作都是串行的.
  2. 由于数据库层面的读写并发,引发的数据库与缓存数据不一致的问题(本质是后发生的读请求先返回了),可能通过两个小的改动解决:

    • 修改服务Service连接池,id取模选取服务连接,能够保证同一个数据的读写都落在同一个后端服务上

    • 修改数据库DB连接池,id取模选取DB连接,能够保证同一个数据的读写在数据库层面是串行的


数据库分库分表

数据库为什么要分库分表? 什么情况下分库分表 ?

  1. 解决磁盘系统最大文件限制

  2. 减少增量数据写入时的锁 对查询的影响,减少长时间查询造成的表锁,影响写入操作等锁竞争的情况. (表锁和行锁) . 避免单张表间产生的锁竞争,节省排队的时间开支,增加呑吐量

  3. 由于单表数量下降,常见的查询操作由于减少了需要扫描的记录,使得单表单次查询所需的检索行数变少,减少了磁盘IO,时延变短

  4. 一台服务器的资源(CPU、磁盘、内存、IO等)是有限的,最终数据库所能承载的数据量、数据处理能力都将遭遇瓶颈。分库的目的是降低单台服务器负载,切分原则是根据业务紧密程度拆分,缺点是跨数据库无法联表查询

  5. 当数据量超大的时候,B-Tree索引的作用就没那么明显了。如果数据量巨大,将产生大量随机I/O,同时数据库的响应时间将大到不可接受的程度。

    • 数据量超大的时候,B-TREE的树深度会变深,从根节点到叶子节点要经过的IO次数也会增大。当IO层数超过4层之后,就会变得很慢,其实4层IO,存储的数据都是TB级别的了,除非你的数据类型都是INT等小类型的。也不能说BTREE不起作用,只是说作用没那么明显了。
    • 数据量巨大,就一定是随机IO吗?这不一定的,如果都是主键查询,10E条记录都可以很快返回结果。当用二级索引来查询的时候,就变成随机IO了,响应时间是会变慢,这也要看数据的分布。另外他也没说存储介质,如果用SSD盘,随机IO比SAS的强100倍,性能也是不错的

Golang的goroutine

  1. goroutine都是用户态的调度, 协程切换只是简单地改变执行函数栈,不涉及内核态与用户态转化, 上下文切换的开销比较小.

  2. 创建一个goroutine需要大概2k(V1.4)左右的栈空间.

  3. go有抢占式调度:如果一个Goroutine一直占用CPU,长时间没有被调度过,就会被runtime抢占掉

是不是表示,在内存够用的条件下, 创建一定量(比如,30w,50w)的goroutine, 不会因为cpu调度原因导致性能下降太多?

  1. 如果系统里面goroutine太多, 可能原因之一就是因为每个goroutine处理时间过长,那么就需要查看为啥处理耗时较长.

  2. 给出大概数据,24核,64G的服务器上,在QoS为message at least,纯粹推,消息体256B~1kB情况下,单个实例100w实际用户(200w+)协程,峰值可以达到2~5w的QPS...内存可以稳定在25G左右,gc时间在200~800ms左右(还有优化空间)。 (来自360消息系统分享)

长连接接入层的net连接管理

长连接接入层的net连接很多,一般单台服务器可以有几十万、甚至上百万,那么怎么管理这些连接 ? 后端数据来了, 怎么快速找到这个请求对应的连接呢 ? 连接和用户如何对应

管理tcp长连接

  1. 一个连接结构. 包含tcp连接信息,上次通信时间, 加解密sharekey, clientaddr. 还包含一个用户结构

    • 用户结构里面包含uid, deviceid. name ,version ...., 还包含上面的这个连接, 两者一一对应.

    • 不用map来管理, 而是把tcp连接信息和user信息来进行一一对应,如果map的话,几百万可能查找起来比较慢.

    • 登录请求的时候,可以根据这个tcp连接信息,获取user信息,但是此时user信息基本没有填充什么数据,所以就需要根据登录来填充user信息结构. 关键是: 在当前Access接入服务里面,会有一个useMap,会把uid和user信息对应起来,可以用来判断此uid,是否在本实例上登录过

    • 返回数据的时候, 可以根据这个uid,来获取对应的user结构,然后通过这个结构可以获取对应的tcp 连接信息, 可以进行发送信息.

  2. 另外,登录登出的时候,会有另外的连接信息(uid/topic/protoType/addr...) 添加删除到用户中心

    • 登录成功:UseAddConn
    • 登出下线:UserDelConn
    • 这里的连接信息,供其他远程服务调用,如Oracle.
  3. 如果有多个Access接入层, 每个接入层都会有一个useMap结构.

    • 如果多个终端登录同一个账号,而且在不同的Access,那么就不能通过useMap来踢出,就需要上步说的用户中心来管理踢出
    • 多个Access,意味着多个useMap,那么就需要保证,从某个Access下发的请求,一定会回到当前Access. 怎么保证呢? 把当前Access的ip:addr一直下发下去,然后返回的时候,根据下发的Access的ip:addr来回到对应的Access.
    • 然后根据uid,来获取当前uid对应的user结构和tcp连接结构.

数据结构: map/hash(红黑树)

管理收发异常,请求回应ack, 超时

  1. 利用map数据结构, 发送(publish)完消息后,立即通过msgid和uid,把对应的消息体添加到map结构.

  2. 收到回应后,删除对应的map结构.

  3. 超时后,重新提交OfflineDeliver. 然后删除对应的map结构.

异步,并发的时候,rpc 框架,怎么知道哪个请求是哪个的呢 ?

  1. client线程每次通过socket调用一次远程接口前,生成一个唯一的ID,即requestID(requestID必需保证在一个Socket连接里面是唯一的),一般常常使用AtomicLong从0开始累计数字生成唯一ID,或者利用时间戳来生成唯一ID.

  2. grpc 也需要服务发现. grpc服务可能有一个实例. 2个, 甚至多个? 可能某个服务会挂掉/宕机. 可以利用zookeeper来管理.

  3. 同步 RPC 调用一直会阻塞直到从服务端获得一个应答,这与 RPC 希望的抽象最为接近。另一方面网络内部是异步的,并且在许多场景下能够在不阻塞当前线程的情况下启动 RPC 是非常有用的。 在多数语言里,gRPC 编程接口同时支持同步和异步的特点。

  4. gRPC 允许客户端在调用一个远程方法前指定一个最后期限值。这个值指定了在客户端可以等待服务端多长时间来应答,超过这个时间值 RPC 将结束并返回DEADLINE_EXCEEDED错误。在服务端可以查询这个期限值来看是否一个特定的方法已经过期,或者还剩多长时间来完成这个方法。 各语言来指定一个截止时间的方式是不同的

服务性能方面的考虑点

  1. 编码角度:

    • 采用高效的网络模型,线程模型,I/O处理模型,合理的数据库设计和操作语句的优化;
  2. 垂直扩展:

    • 通过提高单服务器的硬件资源或者网络资源来提高性能;
  3. 水平扩展:

    • 通过合理的架构设计和运维方面的负载均衡策略将负载分担,有效提高性能;后期甚至可以考虑加入数据缓存层,突破IO瓶颈;
  4. 系统的高可用性:

    • 防止单点故障;
  5. 在架构设计时做到业务处理和数据的分离,从而依赖分布式的部署使得在单点故障时能保证系统可用。

  6. 对于关键独立节点可以采用双机热备技术进行切换。

  7. 数据库数据的安全性可以通过磁盘阵列的冗余配置和主备数据库来解决。


服务器的瓶颈分析

通过压测得知gRPC是瓶颈影响因素之一,为啥是grpc? 为啥消耗cpu? 怎么解决? 网络一定不会影响吞吐.

  1. 采用uarmy 方式. 可以考虑采用streaming方式. 批量发送,提高效率

    • uarmy方式一对一,并发增大的时候,连接数会增大

    • streaming方式的话,就是合并多个请求(批量打包请求/响应), 减少网络交互, 减少连接

    • 做过streaming 的压测,性能说比 unary 高一倍还多

  2. 一般服务器都会有个抛物线规律, 随着并发数的增大,会逐渐消耗并跑满(cpu/内存/网络带宽/磁盘io), 随之带来的就是响应时间变慢(时延Latency变成长),而qps/吞吐量也上不去.

  3. 对于grpc 而言, 并发数增多后,能看到实际效果就是延迟增大,有部分请求的一次请求响应时间达到了5s左右(ACCESS/PUSH), 这样说明时延太长, qps/吞吐量 = 并发数/响应时间. 响应时间太长,吞吐当然上不去.

    • 为啥响应时间这么长了? 是因为cpu跑满了么?

    • 还有一个原因倒是响应慢,那就是最终请求会到Oracle服务, 而oracle会请求数据资源(cache/db), oracle的设计中请求资源的并发增多(连接数也增多),导致请求资源的时延增长,因此返回到上级grpc的调用也会增大时延.

    • 因此关键最终又回到了 cpu/内存/网络带宽/磁盘io这里了

  4. rpc 而言, 连接数增多了,会导致:

    • 类似tcp长连接一样, 每个连接肯定要分配一定的内存

    • 要同时处理这么多连接,每个连接都有相应的事务, cpu的处理能力要强

  5. 后来经过调查我们发现 gRPC 的同步调用与 Nagle's algorithm 会产生冲突,虽然 gRPC 在代码中加入了 TCP_NODELAY 这个 socketopt 但在 OS X 中是没有效果的。后来通过设定 net.inet.tcp.delayed_ack = 0 来解决,同样我们在 linux 下也设置了 net.ipv4.tcp_low_latency = 1,这样在 100M 带宽下一次同步调用的时间在 500us 以下。而且在实际应用中,我们通过 streaming 调用来解决大量重复数据传输的问题,而不是通过反复的同步调用来传相同的数据,这样一次写入可以在 5us 左右。其实批量写入一直都是一个很好的解决性能问题的方法


如何快速接入服务端的接入层

如果服务器在北京, 客户端在广州, 如何能够快速接入? 除了走cdn还有其他方式没 ?

  1. 如果只有一个数据中心, 暂时除了cdn加速, 没有其他方法.

  2. 如果有两个数据中心, 可以采取就近原则,但是需要两个数据中心的数据进行同步

  3. 就近接入:就是利用DNS服务找到离用户最近的机器,从而达到最短路径提供服务

怎么提高在IM领域的能力 ?

  1. 要能在不压测的情况下,就能够预估出系统能够支持的qps. 要能够粗略估算出一次db的请求耗时多久, 一次redis的请求耗时多少, 一次rpc调用的请求耗时多少?

    • 系统中有哪些是比较耗时,比较消耗cpu的.
  2. 所有系统, 一定都是分为几层, 从上层到底层, 每一步的请求是如何的? 在每个层耗时咋样?

    • 系统有没有引入其他资源

    • 性能瓶颈无法是cpu/io.

    • db查询慢,是为啥慢? 慢一定有原因的?

      • 查询一条sql语句的时间大致在0.2-0.5ms(在表数据量不大的情况下, 是否根据索引id来查询,区别不大.)
    • 单台机, qps为8k, 是比较少的. qps: 8k, 那么平均请求响应时间: 1/8ms=0.125ms, qps为8k, 那么5台机器, qps就是4w, 同时10w人在线, 收发算一个qps的话,那么qps减半, 那就是2w qps, 10w同时在线, 每个人3-4s发一次消息, 需要qps到3w.

    • 之前测试redis的时候, 有测试过,如果并发太高,会导致拉取redis耗时较长,超过3s左右.

    • 正常情况下,一个人发送一条消息需要耗时至少5s左右(6-8个字).

  3. 要深入提高IM技术, 就必须要能够学会分析性能, 找到性能瓶颈, 并解决掉.

    • 还要看别人如微信的一些做法
  4. 架构都是逐步改造的, 每个阶段有每个阶段的架构, 一般架构,初始都是三层/四层架构. 然后开始改造, 改造第一阶段都是拆分服务,按逻辑拆分,按业务拆分, 合并资源请求,减少并发数,减少连接数.

  5. 要经常关注一些大数据, 比如注册用户数, 日活, 月活, 留存. 要对数据敏感, 为什么一直不变, 为什么突然增高, 峰值是多少? 目前能抗住多少 ?

  6. 关注系统性能指标,cpu,内存,网络,磁盘等数据, 经常观测, 看看有没有异常, 做到提前发现问题,而不是等到问题出现了再进行解决, 就是是出现问题了再进行解决, 也要保证解决时间是分钟级别的.

    • 完全理解系统底层工具的含义,如sar,iosta,dstat,vmstat,top等,这些数据要经常观察,经常看

    • 保证整套系统中所涉及的各个部分都是白盒的

      • 依赖的其他服务是谁负责,部署情况,在哪个机房

      • 使用的资源情况,redis内存多大 ? mysql 数据库多少? 表多少? 主从怎么分布 ? 对于消息: 一主两从,32库,32表. 对于好友数据:一主一从,128表. 对于朋友圈,按月分表.

总结&如何思考问题和提升自我能力

  1. 如果没有自己思考,现有的东西都是合理的, 这显然是不行的.

    • 每看一个东西,都要思考, 这个东西合不合理? 是否可以优化? 有哪些类似的? 要想如果怎么样

    • 例如:刚开始接触xxx项目的时候,觉得这个架构不错,觉得不用优化了,但是后面需要大规模推广后,xxx就提出了一些优化点, 通过量级的提高,暴露了一些问题

      • 并发大后,mysql慢请求问题

      • 并发大后,请求资源并发太多,连接数太多问题,因此需要合并资源请求

      • Access接入层长连接的问题, Access接入层服务升级不方便, 因此需要拆分Access长连接,提升稳定性.方便服务升级.

  2. 除了熟悉代码框架外, 一定还要深入到细节, 比如golang的底层优化, 系统级别的优化.

  3. 围绕im领域思考问题和量级, 当前的量级是什么级别,然后需要考虑更高级别要做的事情.

    • 当前级别为w级别的时候, 就要考虑十万级别该做的事,十万级别后,就要考虑百万, 不一定要马上做,但是一定要先想,先考虑,目前性能如何,怎么扩展? 怎么重构?
  4. 了解业界相关技术方案, 了解别人踩过的坑. 用来后续量大了后,可以提供更好的技术方法和架构, 往资深im/im高级方向发展, 不仅仅限于xxx项目. 要能够围绕整个IM 领域方向思考

    • 业界的架构, 技术方案, 选型, 都需要先了解.

【"欢迎关注我的微信公众号:Linux 服务端系统研发,后面会大力通过微信公众号发送优质文章"】

我的微信公众号