探探长连接项目的Go语言实践


长连接服务处于网络接入层,这个领域很是适合用Go语言发挥其多协程并行,异步IO特色。探探自长连接项目上线之后,对服务进行了屡次优化:GC从5ms降到100微秒(Go版本均为1.9以上),主要Grpc接口调用延时p999从300ms降低到5ms。在业内大多把目光聚焦于单机链接数的时候,咱们则更聚焦于服务的SLA。前端


张凯宏|探探高级技术专家 golang


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



01算法

项目背景编程



缘起
咱们这个项目是2018年下半年开始,据今天大概1年半时间。当时探探遇到几个问题,首先是比较严重依赖第三方Push,好比说第三方有一些故障的话,对聊天的KPS有比较大的影响。当时经过push推送消息,应用内的push延时比较高,平均延时五六百毫秒,这个时间咱们不能接受。   
当时没有一个 Ping Pland 机制,没法知道用户是否在线。当时产品和技术同窗都以为是机会搞一个长连接了。


项目经历
项目大概持续了一个季度时间,首先是拿IM业务落地,咱们以为长连接跟IM绑定比较紧密一些。IM落地以后,后续长连接上线以后,各个业务比较依赖于长连接服务。这中间有一个小插曲,主要是取名字那一块。项目之初给项目起名字叫Socket,看到socket比较亲切,以为它就是一个长连接,这个感受比较莫名,不知道为何。运维提出了异议,以为UDP也是Socket,我以为UDP其实也能够作长连接。运维提议叫Keppcom,这个是出自于Keep Alive实现的,这个提议仍是挺不错的,最后咱们也是用了这个名字。客户端给的建议是Longlink,另一个是Longconn,一个是IOS版,一个是安卓版。最后咱们都败了,运维同窗胜了,运维同窗以为,若是名字定不下来就别上线的,最后咱们妥协了。


作长连接的缘由
为何作长连接?看一下对比挺明显,左边是长连接,右边是短长连接。对于长连接来讲,不须要从新进入连接,或者是释放连接,一个X包只须要一个RTT就完事。右边对于一个短连接须要三次握手发送一个push包,最后作挥手。
结论,若是发送N条消息的数据包,对于长连接是2+N次的RTT,对于短连接是3N次RTT,最后开启Keep Alive,N是连接的个数。



长连接的四大优点
第一,实时性方面,长连接是双向的通道,对消息的推送也是比较实时。
第二,长连接自己维护用户的状态,经过KeepAlive方式,肯定用户是否在线。
第三,长连接比较省流量,能够作一些用户自定义的数据压缩,自己也能够省很多的归属包和链接包,因此说比较省流量。在这个前提下,客户端就比较省量了。
第四,减小网络流量以后,可以进一步下降客户端的耗电。


02后端

设计细节缓存



想说一些设计细节,在项目开始以前作了比较多的考量,最终定位一个详细设计的报告。首先咱们看一下对于移动端的长连接来讲,TCP协议是否是可以Work?在传统的长连接来讲,Web端的长连接TCP能够胜任,在移动端来讲TCP可否胜任?取决于TCP几个特性,首先TCP有慢启动和滑动窗口的特性,TCP经过这种方式控制PU包,避免网络阻塞。
TCP连接以后走一个慢启动流程,这个流程从初始窗大小作2个N次方的扩张,最后到必定的域值,好比域值是16包,从16包开始逐步往上递增,最后到24个数据包,这样达到窗口最大值。一旦遇到丢包的状况,固然两种状况。一种是快速重传,窗口简单了,至关因而12个包的窗口。若是启动一个RTO相似于状态连接,窗口一下跌到初始的窗口大小。
若是启动RTO重传的话,对于后续包的阻塞蛮严重,一个包阻塞其余包的发送。


03安全

TCP实现长连接的四个问题服务器


   一、移动端的消息量仍是比较稀疏,用户每次拿到手机以后,发的消息总数比较少,每条消息的间隔比较长。这种状况下TCP的间连和保持长连接的优点比较明显一些。
    二、弱网条件下丢包率比较高,丢包后Block后续数据发送容易阻塞。
    三、TCP链接超时时间过长,默认1秒钟,这个因为TCP诞生的年代比较早,那会儿网络状态没有如今好,当时定是1s的超时,如今能够设的更短一点。
    四、在没有快速重传的状况下,RTO重传等待时间较长,默认15分钟,每次是N次方的递减。


    为什么最终仍是选择TCP呢?由于咱们以为UDP更严重一点。首先UDP没有滑动窗口,无流量控制,也没有慢启动的过程,很容易致使丢包,也很容易致使在网络中间状态下丢包和超时。
    UDP一旦丢包以后没有重传机制的,因此咱们须要在应用层去实现一个重传机制,这个开发量不是那么大,可是我以为由于比较偏底层,容易出故障,因此最终选择了TCP。


04微信

选择TCP的三个理由




一、目前在移动端、安卓、IOS来讲,初始窗口大小比较大默认是10,综合TCP慢启动的劣势来看。
二、在普通的文本传输状况下,对于丢包的严重不是很敏感,并非说传多媒体的数据流,只是传一些文本数据,这一块对于丢包的反作用TCP不是特别严重。
三、咱们以为TCP在应用层用的比较多,这里有三个考量点。第一个考量点:基本如今应用程序走HTP协议或者是push方式基本都是TCP,咱们以为TCP通常不会出大的问题。一旦抛弃TCP用UDP或者是Q协议的话,保不齐会出现比较大的问题,短期解决不了,因此最终用了TCP。第二个考量点:咱们的服务在基础层上用哪一种方式作LB,当时有两种选择,一种是传统的LVS,另外一种是HttpDNS。最后咱们选择了HttpDNS,首先咱们仍是须要跨机房的LB支持,这一点HttpDNS彻底胜出。其次,若是须要跨网端的话,LVS作不到,须要其余的部署方式。再者,在扩容方面,LVS算是略胜一筹。最后,对于通常的LB算法,LVS支持并很差,须要根据用户ID的LB算法,另外须要一致性哈希的LB算法,还须要根据地理位置的定位信息,在这些方面HttpDNS都可以完美的胜出,可是LVS都作不到。


第三个考量点,咱们在作TCP的饱和机制时经过什么样的方式? Ping包的方式,间隔时间怎么肯定,Ping包的时间细节怎么样肯定? 当时比较纠结是客户端主动发ping仍是服务端主动发Ping? 对于客户端保活的机制支持更好一些,由于客户端可能会被唤醒,可是客户端进入后台以后可能发不了包,其次,APP先后台对于不一样的Ping包间隔来保活,由于在后台自己处于一种弱在线的状态,并不须要去频繁的发Ping包肯定在线状态。 因此,在后台的Ping包的时间间隔能够长一些,前端能够短一些。 再者,须要Ping指数增加的间隔支持,在故障的时候仍是比较救命的。 好比说服务端一旦故障以后,客户端若是拼命Ping的话,可能把服务端完全搞瘫痪了。 若是有一个指数级增加的Ping包间隔,基本服务端还能缓一缓,这个在故障时比较重要。 最后,Ping包重试是否须要Backoff,Ping包从新发Ping,若是没有收到Bang包的话,须要等到Backoff发Ping。


05

动态Ping包时间间隔算法



咱们还设计了一个动态的Ping包时间间隔算法,国内的网络运营商对于NIT设备有一个保活机制,目前基本在5分钟以上,5分钟若是不发包的话,会把你的缓存给删掉。基本运营商都在5分钟以上,只不过移动4G阻碍了。基本能够在4到10分钟以内发一个Ping包就行,能够维持网络运营商设备里的缓存,一直保持着,这样就没有问题,使长连接一直保活着。能够减小网络流量,可以进一步下降客户端的耗电,这一块的受益仍是比较大的。


在低端安卓设备的状况下,有一些DHCP租期的问题,这个问题集中在安卓端的低版本上,安卓不会去续租过时的IP。 解决问题也比较简单,在DHCP租期到一半的时候,去及时向DHCP服务器续租一下就能解决了。
下一个设计的细节对于长连接来讲是比较关键的,通常分为两个部分,一个是Header,还有Payload。Header通常是硬程,Payload是编程,固然Header也是有编程的,Header协议基本组成里面丢包括Header里面,Header会包括一些比较重要的控制字段和Flack字段,这些字段首先会按数据包的功能是哪几个,而且会作出一些功能的取舍。对于协议来讲,还须要考虑扩展性和安全性。


咱们如今用的是Websocket,Websocket协议跟着Web1.1诞生,时间比较早,有些特性用不着了。 Websocket协议首先是Finolway,这个Way表示数据包是否是一个结束包,刚才三个RCEWay主要是扩展,后面四个Way主要是表示这个包是作什么用的,好比说是链接包、调开包或者是push的消息包,后面包是Mask包,MaskWay就是表示这个包是否是通过数据的Mask操做,后面是7个位的Payload长度,若是不够用的话,后面还有10个位的Payload长度,若是Payload长度小于127位的话。


MOTT协议稍微复杂一些,咱们看到MOTT自己支持变程的Header,咱们这边是定程的Header。 左边是Header详细协议,左边有四位是指定Context包的信息,它是一个间连的包仍是断连的包,右边是四个Falway,目前基本的MOTT协议就用到四个,后面跟着两个是QS,QS等级是多少,MOTT通常三个等级,最少一次,最多一次,正好一次的三个语义,最后一个V是保留V,这个消息是否是保持在服务端,以便下一次再链接的时候,传给客户端。


对于MQTT协议来讲,自己涉及比较地道的地方,通常看到长链接会实现正好一次传输的语义,对这个语义作了简化。 由于MQTT这种实现更复杂一些,很难理解,咱们作了简化。
比方说咱们认为一个语义这么实现,客户端反复重试,直到服务端收到为止。服务端经过消息的ID去作屈从,服务端保证AT Mose Once,客户端保证至少一次,这样就可以输出正好一次的语义,这样算是比较简单。咱们的消息分为两种类型,一个是上位消息,从发送方和客户端推到业务端去,这种方式客户端就是发送方,由客户端保证AT Least Once的语义。经过传输链路最低端,业务微服务保证AT LeastOnce的语义。对于下行消息,通常业务方发送的时候也会保证最少一次的语义,这样实如今传输链路上各个端都保证至少一次语义的话,须要在接收方去保证最多一次就好了。下面是服务架构。


服务架构比较简单,大概是四个模块。
首先是HttpDNS,一个是Connector接入层,接入层提供IP,而后是Router,相似于代理转发消息,根据IP选择接入层的服务器,最后推到用户。
最后还有认证的模块Account,咱们目前只是探探APP,这个在用户中心实现。


部署上至关于三个模块,一个是Dispatcher,一个是Redis,一个是Cluser。   


客户端在链接的时候,须要拿到一个协议,第二步经过HttpDNS拿到ConnectorIP,经过IP连长连接,下一步发送Auth消息认证,链接成功,后面发送Ping包保活,以后断开链接。


后面是消息转发的流程,分为两个部分。
首先是消息上行,服务端发起一个消息包,经过Connector接入服务,客户端经过Connector发送消息,再经过Connector把消息发到微服务上,若是不须要微服务的话直接去转发到Vetor就行的,这种状况下Connector更像一个Gateway。
对于下行,业务方都须要请求Router,找到具体的Connector,根据Connector部署消息。


各个公司都是微服务的架构,长连接跟微服务的交互基本两块,一块是消息上行时,更像是Gateway,下行经过Router接入,经过Connector发送消息。


下面是一些是细节,咱们用了GO语言1.13.4,内部消息传输上是gRPC,传输协议是Http2,咱们在内部经过ETCD作LB的方式,提供服务注册和发现的服务。


这是Connector的一个内室图,Connector就是状态,它从用户ID到链接的一个状态信息,咱们看右边这张图它实际上是存在一个比较大的MAP,为了防止MAP的锁竞争过于严重,把MAP拆到2到56个子MAP,经过这种方式去实现高读写的MAP。 对于每个MAP从一个ID到链接状态的映射关系,每个链接是一个Go Ping,实现细节读写是4KB,这个没改过。


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


咱们发现两个状态,一个是Connector,一个是Router。 首先以Connector状态为主,Router是状态一致的保证,这个里面分为两种状况。 若是链接在同一个Connector上的话,Connector须要保证向Router复制的顺序是正确的,若是顺序不一致,致使Router和Connector状态不一致。 经过统一Connector的窗口实现消息一致性,若是跨Connector的话,经过在Redis Lua脚本实现Compare And Update方式,去保证只有本身Connector写的状态才能被本身更新,若是是别的Connector的话,更新不了其余人的信心。 咱们保证跨Connector和同一Connector都可以去按照顺序经过一致的方式更新Router里面链接的状态。


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


目前经过无单点的忙是实现一个高可用,首先是Http DNS和Router,这两个是无障碍的服务,只须要经过LB保证,对于Connector来讲,经过Http DNS的客户端主动漂移实现链接层的Ordfrev,经过这种方式保证一旦一个Connector出问题了,客户端能够立马漂到下一个Connector,去实现自动的工做转移,目前是没有单点的。         


06

后续优化



第一,网络优化;这一块拉着客户端一块儿作,首先客户端须要重传包的时候发三个嗅探包,经过这种方式作一个快速重传的机制,经过这种机制提升快速重传的比例。
第二个是经过动态的Ping包间隔时间,减小Ping包的数量,这个还在开发中。
第三个是经过客户端使用IP直连方式,回避域名劫持的操做。
第四个是经过HttpDNS每次返回多个IP的方式,来请求客户端的HttpDNS。


对于接入层来讲,其实Connector的链接数比较多,而且Connector的负载也是比较高。 咱们对于Connector作了比较大的优化,首先看Connector最先的GC时间到了四、5毫秒,惨不忍睹的。 咱们看一下这张图是优化后的结果,大概平均100微秒,这算是比较好。 第二张图是第二次优化的结果,大概是29微秒,第三张图大概是20几微秒。


看一下消息延迟,探探对消息的延迟要求比较高,特别注重用户的体验。 这一块刚开始大概到200ms,若是对于一个操做的话,200ms仍是比较严重的。 第一次优化以后上一张图的状态大概1点几毫秒,第二次优化以后如今降到最低点差很少100微秒,跟通常的Net操做时间维度上比较接近。


优化过程是这样的:
首先须要关键路径上的Info日志,经过采样实现Access Log,info日志是接入层比较重的操做;
第二经过Sync.Poll缓存对象;
第三经过Escape Analysis对象尽量在线上分配。
   

         
后面还实现了Connector的无损发版,这一块比较有价值。长连接刚上线发版比较多,每次发版对于用户来讲都有感,经过这种方式让用户尽可能无感。首先实现了Connector的Graceful Shutdown的方式,经过这种方式优化连接。
首先,在HttpDNS上下线该机器,下线以后缓慢断开用户链接,直到链接数小于必定阈值。后面是重启服务,发版二进制。最后是HttpDNS上线该机器,经过这种方式实现用户发版,时间比较长,当时测了挺长时间,去衡量每秒钟断开多少个链接,最后阈值是多少。


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


Goroutine数量跟链接数同样,差很少15万个。


看一下内存使用状态,上面图是GO的内存总量,大概是2: 3,剩下五分之一是属于未占用,内存总量是7.3个G。


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


看到GC目前的情况大概是20几微秒,感受目前跟GO的官方时间比较能对得上,咱们感受GC目前都已经优化到位了。
最后是后续要作的事情,规划后续还要作优化,首先对系统上仍是须要更多优化Connector层,更多去减小内存的分配,尽可能把内存分配到堆上而不是站上,经过这种方式减小GC压力,咱们看到GO是非Generational Collection GE,堆的内存越多的话,扫的内存也会越多,这样它不是一个线性的增加。
第二,在内部更多去用Sync Pool作短暂的内存分配,好比说Context或者是临时的Dbyle。
协议也要作优化,目前用的是Websocket协议,后面会加一些功能标志,把一些重要信息传给服务端。好比说一些重传标志,若是客户端加入重传标志的话,咱们能够先校验这个包是否是重传包,若是是重传包的话会去判断这个包是否是重复,是否是以前发过,若是发过的话就不须要去解包,这样能够少作不少的服务端操做。
第二点,能够去把Websocket目前的Mask机制去掉,由于Mask机制防止Web端的改包操做,可是基本是客户端的传包,因此并不须要Mask机制。


业务上,目前规划后面须要作比较多的事情。 咱们以为长链接由于是一个接入层,是一个很是好的地方去统计一些客户端的分布。 好比说客户端的安卓、IOS的分布情况。
第二步能够作用户画像的统计,男的女的,年龄是多少,地理位置是多少。大概是这些,谢谢!



Q & A



提问:刚才说链接层对话重启,间接的过程当中那些断掉的用户就飘到其余的,是这样作的吗?
张凯宏: 目前是这样的,客户端作自动飘移。
提问: 如今是1千万日活,若是服务端往客户端一下推100万,这种场景怎么作的?
张凯宏: 目前咱们没有那么大的消息推送量,有时候会发一些业务相关的推送,目前作了一个限流,经过客户端限流实现的,大概三四千。
提问: 若是作到后端,意味着会存在安全隐患,攻击者会不停的创建链接,致使很难去作防护,会有这个问题吗? 由于恶意的攻击,若是攻击的话创建链接就能够了,不须要认证的机制。
张凯宏:明白你的意思,这一块不仅是长连接,短连接也有这个问题。客户端一直在伪造访问结果,流量仍是比较大的,这一块靠防火墙和IP层防火墙实现。
提问: 长连接服务器是挂在最外方,中间有没有一层?
张凯宏:目前接着以下层直接暴露在外网层,前面过一层IP的防DNSFre的防火墙。除此以外没有别的网络设备了。
提问: 基于什么样的考虑中间没有加一层,由于前面还加了一层的状况。
张凯宏:目前没有这个计划,后面会在Websofte接入层前面加个LS层能够方便扩容,这个收益不是特别大,因此如今没有去计划。
提问: 刚刚说的断开重传的三次嗅探那个是什么意思?
张凯宏:咱们想更多的去触发快速重传,这样对于TCP的重传间隔更短一些,服务端根据三个循环包判断是否快速重传,咱们会发三个循环包避免一个RTO重传的开启。
提问: 探探最开始安卓服务器是使用第三方的吗?
张凯宏:对的,刚开始是极光推送的。
提问: 从第三方的安卓服务器到自研。
张凯宏:若是极光有一些故障的话,对咱们影响仍是蛮大。以前极光的故障频率挺高,咱们想是否是本身能把服务作起来。第二点,极光自己能提供一个用户是否在线的判断,可是它那个判断要走通道,延时比较高,自己判断是经过用户的Ping包来作权衡,咱们以为偏高一些,咱们想本身实现长连接把延时下降一些。
提问: 好比说一个新用户上线链接过来,有一些用户发给他消息,他是怎么把一线消息拿到的?
张凯宏:咱们经过业务端保证的,未发出来的消息会存一个ID号,当用户从新连的时候,业务端再拉一下。

本文分享自微信公众号 - GoCN(golangchina)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索