赢博体育

赢博体育

你的当前位置: 赢博体育 > 赢博动态

IM电竞APP探探的IM长连接技术实践:技术选型、架构设计、性能优化

发布日期:2022-09-05 10:49:57 点击次数:

  本文由探探服务端高级技术专家张凯宏分享,原题“探探长链接项目的Go语言实践”,因原文内容有较多错误,有修订和改动。

  即时通信长连接服务处于网络接入层,这个领域非常适合用Go语言发挥其多协程并行、异步IO的特点。

  探探自长连接项目上线以后,对服务进行了多次优化:GC从5ms降到100微秒(Go版本均为1.9以上),主要gRPC接口调用延时p999从300ms下降到5ms。在业内大多把目光聚焦于单机连接数的时候,我们则更聚焦于服务的SLA(服务可用性)。

  本文将要分享的是陌生人社交应用探探的IM长连接模块从技术选型到架构设计,再到性能优化的整个技术实践过程和经验总结。

  - 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》- 开源IM框架源码:

  6年Go语言开发经验,曾用Go语言构建多个大型Web项目,其中涉及网络库、存储服务、长连接服务等。专注于Go语言实践、存储服务研发及大数据场景下的Go语言深度优化。

  当时探探遇到一些技术痛点,最严重的就是严重依赖第三方Push,比如说第三方有一些故障的话,对实时IM聊天的KPS有比较大的影响。

  当时通过push推送消息,应用内的push延时比较高,平均延时五六百毫秒,这个时间我们不能接受。

  而且也没有一个 Ping Pland 机制(心跳检查机制?),无法知道用户是否在线。

  项目大概持续了一个季度时间,首先是拿IM业务落地,我们觉得长连接跟IM绑定比较紧密一些。

  项目之初给项目起名字叫Socket,看到socket比较亲切,觉得它就是一个长连接,这个感觉比较莫名,不知道为什么。但运维提出了异议,觉得UDP也是Socket,我觉得UDP其实也可以做长连接。

  运维提议叫Keepcom,这个是出自于Keep Alive实现的,这个提议还是挺不错的,最后我们也是用了这个名字。

  客户端给的建议是Longlink,另外一个是Longconn,一个是IOS端技术同事取的、一个是安卓端技术同事取的。

  最后我们都败了,运维同学胜了,运维同学觉得,如果名字定不下来就别上线的,最后我们妥协了。

  对于长连接来说,不需要重新进入连接,或者是释放连接,一个X包只需要一个RTT就完事。右边对于一个短连接需要三次握手发送一个push包,最后做挥手。

  结论:如果发送N条消息的数据包,对于长连接是2+N次的RTT,对于短连接是3N次RTT,最后开启Keep Alive,N是连接的个数。

  对于传统的长连接来说,Web端的长连接TCP可以胜任,在移动端来说TCP能否胜任?这取决于TCP的几个特性。

  首先TCP有慢启动和滑动窗口的特性,TCP通过这种方式控制PU包,避免网络阻塞。

  TCP连接之后走一个慢启动流程,这个流程从初始窗大小做2个N次方的扩张,最后到一定的域值,比如域值是16包,从16包开始逐步往上递增,最后到24个数据包,这样达到窗口最大值。

  一旦遇到丢包的情况,当然两种情况。一种是快速重传,窗口简单了,相当于是12个包的窗口。如果启动一个RTO类似于状态连接,窗口一下跌到初始的窗口大小。

  如果启动RTO重传的话,对于后续包的阻塞蛮严重,一个包阻塞其他包的发送。

  (▲ 上图引用自《迈向高阶:优秀程序员必知必会的网络基础》)

  (▲ 上图引用自《移动端IM/推送系统的协议选型:UDP还是TCP?》)

  1)移动端的消息量还是比较稀疏,用户每次拿到手机之后,发的消息总数比较少,每条消息的间隔比较长。这种情况下TCP的间连和保持长链接的优势比较明显一些;

  3)TCP连接超时时间过长,默认1秒钟,这个由于TCP诞生的年代比较早,那会儿网络状态没有现在好,当时定是1s的超时,现在可以设的更短一点;

  4)在没有快速重传的情况下,RTO重传等待时间较长,默认15分钟,每次是N次方的递减。

  因为我们觉得UDP更严重一点。首先UDP没有滑动窗口,无流量控制,也没有慢启动的过程,很容易导致丢包,也很容易导致在网络中间状态下丢包和超时。

  UDP一旦丢包之后没有重传机制的,所以我们需要在应用层去实现一个重传机制,这个开发量不是那么大,但是我觉得因为比较偏底层,容易出故障,所以最终选择了TCP。

  基本现在应用程序走HTP协议或者是push方式基本都是TCP,我们觉得TCP一般不会出大的问题。

  一旦抛弃TCP用UDP或者是QUIC协议的话,保不齐会出现比较大的问题,短时间解决不了,所以最终用了TCP。

  我们的服务在基础层上用哪种方式做LB,当时有两种选择,一种是传统的LVS,另一种是HttpDNS(关于HttpDNS请见《全面了解移动端DNS域名劫持等杂症:原理、根源、HttpDNS解决方案等》)。

  最后我们选择了HttpDNS,首先我们还是需要跨机房的LB支持,这一点HttpDNS完全胜出。其次,如果需要跨网端的话,LVS做不到,需要其他的部署方式。再者,在扩容方面,LVS算是略胜一筹。最后,对于一般的LB算法,LVS支持并不好,需要根据用户ID的LB算法,另外需要一致性哈希的LB算法,还需要根据地理位置的定位信息,在这些方面HttpDNS都能够完美的胜出,但是LVS都做不到。

  我们在做TCP的饱和机制时通过什么样的方式?Ping包的方式,间隔时间怎么确定,Ping包的时间细节怎么样确定?

  对于客户端保活的机制支持更好一些,因为客户端可能会被唤醒,但是客户端进入后台之后可能发不了包。

  APP前后台对于不同的Ping包间隔来保活,因为在后台本身处于一种弱在线的状态,并不需要去频繁的发Ping包确定在线状态。

  服务端一旦故障之后,客户端如果拼命Ping的话,可能把服务端彻底搞瘫痪了。如果有一个指数级增长的Ping包间隔,基本服务端还能缓一缓,这个在故障时比较重要。

  在IM里这其实有个更专业的叫法——“智能心跳算法”。我们还设计了一个动态的Ping包时间间隔算法。

  因为国内的网络运营商对于NIT设备有一个保活机制,目前基本在5分钟以上,5分钟如果不发包的话,会把你的缓存给删掉。基本上各运营商都在5分钟以上,只不过移动4G阻碍了。基本可以在4到10分钟之内发一个Ping包就行,可以维持网络运营商设备里的缓存,一直保持着,这样就没有问题,使长连接一直保活着。

  增加Ping包间隔可以减少网络流量,能够进一步降低客户端的耗电,这一块的受益还是比较大的。

  在低端安卓设备的情况下,有一些DHCP租期的问题。这个问题集中在安卓端的低版本上,安卓不会去续租过期的IP。

  解决问题也比较简单,在DHCP租期到一半的时候,去及时向DHCP服务器续租一下就能解决了。

  2)另一个是Connector接入层,接入层提供IP,3)然后是Router,类似于代理转发消息,根据IP选择接入层的服务器,最后推到用户;

  2)第二步通过HttpDNS拿到ConnectorIP;3)通过IP连长连接,下一步发送Auth消息认证;

  首先是消息上行:服务端发起一个消息包,通过Connector接入服务,客户端通过Connector发送消息,再通过Connector把消息发到微服务上,如果不需要微服务的话直接去转发到Vetor就行的,这种情况下Connector更像一个Gateway。

  我们看下图的右边:它其实是存在一个比较大的MAP,为了防止MAP的锁竞争过于严重,把MAP拆到2到56个子MAP,通过这种方式去实现高读写的MAP。对于每一个MAP从一个ID到连接状态的映射关系,每一个连接是一个Go Ping,实现细节读写是4KB,这个没改过。

  我们看一下Router:它是一个无状态的CommonGRPC服务,它比较容易扩容,现在状态信息都存在Redis里面,Redis大概一组一层,目前峰值是3000。

  如果连接在同一个Connector上的话,Connector需要保证向Router复制的顺序是正确的,如果顺序不一致,会导致Router和Connector状态不一致。通过统一Connector的窗口实现消息一致性,如果跨Connector的话,通过在Redis Lua脚本实现Compare And Update方式,去保证只有自己Connector写的状态才能被自己更新IM电竞APP,如果是别的Connector的话IM电竞APP,更新不了其他人的信心。我们保证跨Connector和同一Connector都能够去按照顺序通过一致的方式更新Router里面连接的状态。

  是一个纯粹的Common Http API服务,它提供Http API,目前延时比较低大概20微秒,4个CPU就可以支撑10万个并发。

  目前通过无单点的结构实现一个高可用:首先是Http DNS和Router,这两个是无障碍的服务,只需要通过LB保证。对于Connector来说,通过Http DNS的客户端主动漂移实现连接层的Ordfrev,通过这种方式保证一旦一个Connector出问题了,客户端可以立马漂到下一个Connector,去实现自动的工作转移,目前是没有单点的。

  1)网络优化:这一块拉着客户端一起做,首先客户端需要重传包的时候发三个嗅探包,通过这种方式做一个快速重传的机制,通过这种机制提高快速重传的比例;

  2)心跳优化:通过动态的Ping包间隔时间,减少Ping包的数量,这个还在开发中;3)防止劫持:是通过客户端使用IP直连方式,回避域名劫持的操作;

  4)DNS优化:是通过HttpDNS每次返回多个IP的方式,来请求客户端的HttpDNS。

  对于接入层来说,其实Connector的连接数比较多,并且Connector的负载也是比较高。

  我们对于Connector做了比较大的优化,首先看Connector最早的GC时间到了4、5毫秒,惨不忍睹的。

  我们看一下下面这张图(图上)是优化后的结果,大概平均100微秒,这算是比较好。第二张图(图下)是第二次优化的结果,大概是29微秒,第三张图大概是20几微秒。

  第一次优化之后(下图-上)的状态大概1点几毫秒,第二次优化之后(下图-下)现在降到最低点差不多100微秒,跟一般的Net操作时间维度上比较接近。

  1)首先需要关键路径上的Info日志,通过采样实现Access Log,info日志是接入层比较重的操作;

  后面还实现了Connector的无损发版:这一块比较有价值。长连接刚上线发版比较多,每次发版对于用户来说都有感,通过这种方式让用户尽量无感。

  实现了Connector的Graceful Shutdown的方式,通过这种方式优化连接。

  在HttpDNS上下线该机器,下线之后缓慢断开用户连接,直到连接数小于一定阈值。后面是重启服务,发版二进制。

  是HttpDNS上线该机器,通过这种方式实现用户发版,时间比较长,当时测了挺长时间,去衡量每秒钟断开多少个连接,最后阈值是多少。

  后面是一些数据:刚才GC也是一部分,目前连接数都属于比较关键的数据。首先看连接数单机连接数比较少,不敢放太开,最多是15万的单机连接数,大约100微秒。

  下图是GC状态,GC比较健康,红线是GC每次活跃内存数,红线远远高于绿线。

  看到GC目前的状况大概是20几微秒,感觉目前跟GO的官方时间比较能对得上,我们感觉GC目前都已经优化到位了。

  对系统上还是需要更多优化Connector层,更多去减少内存的分配,尽量把内存分配到堆上而不是站上,通过这种方式减少GC压力,我们看到GO是非Generational Collection GE,堆的内存越多的话,扫的内存也会越多,这样它不是一个线性的增长。

  在内部更多去用Sync Pool做短暂的内存分配,比如说Context或者是临时的Dbyle。

  协议也要做优化:目前用的是WebSocket协议,后面会加一些功能标志,把一些重要信息传给服务端。比如说一些重传标志,如果客户端加入重传标志的话,我们可以先校验这个包是不是重传包,如果是重传包的话会去判断这个包是不是重复,是不是之前发过,如果发过的话就不需要去解包,这样可以少做很多的服务端操作。

  另外:可以去把Websocket目前的Mask机制去掉,因为Mask机制防止Web端的改包操作,但是基本是客户端的传包,所以并不需要Mask机制。

  业务上:目前规划后面需要做比较多的事情。我们觉得长连接因为是一个接入层,是一个非常好的地方去统计一些客户端的分布。比如说客户端的安卓、IOS的分布状况。

  进一步:可以做用户画像的统计,男的女的,年龄是多少,地理位置是多少。大概是这些,谢谢!

  * 提问:刚才说连接层对话重启,间接的过程中那些断掉的用户就飘到其他的,是这样做的吗?

  * 提问:现在是1千万日活,如果服IM电竞APP务端往客户端一下推100万,这种场景怎么做的?

  张凯宏:目前我们没有那么大的消息推送量,有时候会发一些业务相关的推送,目前做了一个限流,通过客户端限流实现的,大概三四千。

  * 提问:如果做到后端,意味着会存在安全隐患,攻击者会不停的建立连接,导致很难去做防御,会有这个问题吗?因为恶意的攻击,如果攻击的话建立连接就可以了,不需要认证的机制。

  张凯宏:明白你的意思,这一块不只是长连接,短连接也有这个问题。客户端一直在伪造访问结果,流量还是比较大的,这一块靠防火墙和IP层防火墙实现。

  张凯宏:目前接着如下层直接暴露在外网层,前面过一层IP的防DNSFre的防火墙。除此之外没有别的网络设备了。

  张凯宏:目前没有这个计划,后面会在Websofte接入层前面加个LS层可以方便扩容IM电竞APP,这个收益不是特别大,所以现在没有去计划。

  张凯宏:我们想更多的去触发快速重传,这样对于TCP的重传间隔更短一些,服务端根据三个循环包判断是否快速重传,我们会发三个循环包避免一个RTO重传的开启。

  张凯宏:如果极光有一些故障的话,对我们影响还是蛮大。之前极光的故障频率挺高,我们想是不是自己能把服务做起来。第二点,极光本身能提供一个用户是否在线的判断,但是它那个判断要走通道,延时比较高,本身判断是连接把延时降低一些。

  * 提问:比如说一个新用户上线连接过来,有一些用户发给他消息,他是怎么把一线消息拿到的?

  张凯宏:我们通过业务端保证的,未发出来的消息会存一个ID号,当用户重新连的时候,业务端再拉一下。

  [1] 移动端IM/推送系统的协议选型:UDP还是TCP?[2] 5G时代已经到来,TCP/IP老矣,尚能饭否?

  [3] 为何基于TCP协议的移动端IM仍然需要心跳保活机制?[4] 一文读懂即时通讯应用中的网络心跳包机制:作用、原理、实现思路等

  [5] 微信团队原创分享:版微信后台保活实战分享(网络保活篇)

  [8] 全面了解移动端DNS域名劫持等杂症:原理、根源、HttpDNS解决方案等

  [9] 技术扫盲:新一代基于UDP的低延时网络传输层协议——QUIC详解

  [11] 长连接网关技术专题(二):知乎千万级并发的高性能长连接网关技术实践

  [12] 长连接网关技术专题(三):手淘亿级移动端接入层网关的技术演进之路

  [15] 一套亿级用户的IM架构技术干货(下篇):可靠性、有序性、弱网优化等