在 IM 方面,弱网络一直是横亘在应用开发者面前的一大问题,微信终端跨平台网络基础组件 Mars 团队基于微信业务需求,针对网络层进行了大量的优化工作,以解决国内在复杂移动网络情况下的网络连接问题,并经历了微信 5 亿用户的检验。本文作者重点介绍了针对移动网络,Mars 做了哪些事情,解决了哪些问题,希望能够给正在探索网络优化的开发者带来启发,也可以通过了解 Mars 来看其是否适合自己的业务。
移动网络概述
对于 TCP 网络请求来说,最重要的莫过于延迟和成功率。在两者之中我们更为关心成功率,但其实可以认为当延迟高到一定程度也就导致了失败。而影响 TCP 延迟的最主要的两点是 IP 层以下的丢包和误码,相比有线以太网络和光纤,移动网络在这两方面更为严重,可以先来看两组数据,如图 1 所示。
从图 1 很容易看出,移动网络的丢包率是高于有线网络的,同时从时间分布上也能看出,接入网络的设备越多,丢包越严重。
如果说丢包率方面移动网络虽然高于有线网络,但也没有非常大的差距,那么误码率(Bit Error Rate)的差距就比较明显了,如图 2 所示。
如果想对上面的差异性追根溯源的话,就需要来看核心网络的架构,以 LTE 为例,见图 3。
移动网络整个传输过程只有手机至 RAN(无线接入网络)是无线的,这个过程极不稳定,会受到空气微尘、温度、湿度、障碍物、基站拥挤、信号盲点等客观因素影响。还有用户高速移动等主观因素也会导致较高的丢包率和误码率。同时,核心网络的设计也将直接影响到网络延迟,在图 3 中:
- ①的耗时称为控制面延迟,耗时<100ms;
- ②的耗时称为用户面延迟,耗时<5ms;
- ③的耗时称为核心网络延迟,耗时 30-100ms;
- ④的耗时称为互联网路由延迟,时间不定。
这里需要特别注意的是控制面延迟,高时可达 100ms,低时可能为 0。至于为什么浮动如此之大,这就又和通信协议的 RRC 状态有关。简单描述下即为移动设备为了省电,在使用手机网络的情况下,如果持续一段时间内没有收发数据的话,网络模块会进入休眠状态,此时只传输控制信令。如果在休眠态下需要收发数据,就必须先通过控制信令到活跃态下,如图 4 所示。
排除丢包误码以及控制面延迟,美国最大的移动运营商 AT&T 为不同的网络核心网络延迟给出了期望值(如图 5 所示),这些值在很大程度上也代表了行业水平。
Mars
限于篇幅原因,如果将 Mars 的每一部分做具体描述,几乎不大可能,但是我们这里可以只看网络最核心的部分。
如图 6 所示,将一个网络模块只保留 socket 的逻辑。
根据 AT&T 的数据可以估算下总耗时:100ms(DNS) + 100ms(连接) + 50ms(发送) +50ms(接收) = 300ms。但是再加上丢包误码以及控制面延迟,可能有时候能到 400ms+。
针对这个最简单的逻辑,我们一个阶段一个阶段地进行优化。
RRC
首先是否有办法将 RRC 切换的时间尽量避免掉?既然长时间不收发数据会进入 IDLE 状态,那么如果可以预知用户将要使用网络前,主动先发下数据使 RRC 进入 Active 状态,真正用网络时也就可以避免掉控制面延迟了。这里需要注意:
- 干扰 RRC 是把双刃剑,不鼓励用;
- 精准预测到需要使用网络时再用;
- 实现使用 UDP 可以减小服务器压力。
RRC 如果可以优化,那么在连接之前最后一步准备工作——DNS 呢?
DNS
但凡使用域名来给用户提供服务的业务,都无法避免在互联网环境中遭遇到各种域名劫持、用户跨网访问慢等问题。事实上当前 DNS 的一些缺点(如域名劫持、解析转发、更新缓慢等)也一直被业界诟病。抛开这些问题不谈,在耗时方面,如果不对解析到的地址进行缓存,每次使用时都要再次解析,而且每次只能解析单个域名。
2013 年前后,HTTPDNS 概念开始兴起,基本克服了现有 DNS 的缺点,且支持批量解析,极大地提高了网络访问速度。微信的 NewDNS 和 HTTPDNS 的实现原理类似,是从 2012 年中就开始建设的一个服务。从 NewDNS 的回包中截取一段:
<domain name="your.domain1" timeout="1800">
<ip>111.111.11.111</ip>
<ip>111.111.11.112</ip>
</domain>
<domain name="your.domain2" timeout="1800">
<ip>111.111.11.113</ip>
<ip>111.111.11.114</ip>
</domain>
在安全上,通过时间戳和签名机制,可以做到防重放防篡改。但考虑到 NewDNS 和微信的业务结合过于紧密,且当前的 HTTPDNS 机制已经很成熟,Mars 开源并没有将 NewDNS 的实现包括在内,不过也预留了回调接口以供大家使用第三方的 HTTPDNS 服务。
连接
如果说 RRC 和 DNS 都可以把耗时优化到 0,接下来的流程在 TCP 层可控制的就不多了。在连接方式上,如果只用一个 IP 连接失败就认为彻底失败,大概是属于最原始的方案了。一般会使用并发连接或串行连接,进而提高连通率,但两者都有不容忽视的缺点:
- 并发连接——网络资源竞争、服务器负载、最快可用;
- 串行连接——资源占用少、无服务器负载问题、超时选择困难、最慢可用。
为了实现同时满足高性能、高可用、低负载,在并发连接和串行连接的基础上,Mars 提出了复合连接,可见图 7。
对比串行连接与并行连接,复合连接有以下特点:
- 常规情况下,服务器负载与串行连接策略相同,实现了低负载的目标;
- 异常情况下,每 4s 发起新(IP,Port)组合的 connect 调用,使得应用可以快速地查找可用 IP&Port,实现高性能的目标;
- 在超时时间的选择上,复合方式的“并发”已经实现了高性能、低负载的目标,因此可以相对宽松,以保障高可用为重。
TCP 的大多数实现中,若主动 connect 方没有收到 SYN 的回应,后面的重试间隔会以“类指数退避”的方式增加。实测显示,Android 超时间隔依次为(1,2,4,8,16,32),iOS 超时间隔依次为(1,1,1,1,1,2,4,8,16,32)。因此,期望通过 TCP 的自有超时机制来发现连接失败,时间之长是不能忍受的。在综合了几个平台的超时间隔之后选择了 10s。
发送
连接上肯定是用来收发数据的,但发送也并不只是把数据放到系统 Buffer 里这么简单。
我们知道 TCP/IP 网络协议栈分为应用层、传输层、网络层和链路层。在通信过程中,应用层协议把我们真正关心的数据放进去,其他协议层的也都会加上一个数据头部,最后发出的数据包结构如图 8 所示。
当发送方产生的数据很慢,或接收端处理数据很慢,或二者兼有,就会使单次发送数据的有效载荷很小。极端情况甚至只有 1 字节的有效数据,称之为糊涂窗口综合症。针对发送端的解决办法是 Nagle 算法,针对接收端的解决办法是 Clark 和延迟 ACK。因为我们是发送端,这里只关注 Nagle 算法:
- 如果包长度达到 MSS,则允许发送;
- 如果该包含有 FIN,则允许发送;
- 设置了 TCP_NODELAY 选项,则允许发送;
- 未设置 TCP_CORK 选项时,若所有发出去的小数据包(包长度小于 MSS)均被确认,则允许发送;
- 上述条件都未满足,但发生了超时(一般为 200ms),则立即发送。
本来 Nagle 算法是防止糊涂窗口综合症产生的,但当我们的应用场景主要是发送小数据时,极端情况下会被延迟 200ms,这几乎是不能忍受的,所以设置 TCP_NODELAY 选项很重要。
把数据发出去了,是不是只需要等回包和(或)等失败就行了?前面有提到 TCP 的自有连接超时失败时间很长,发送超时是不是也类似?传统 Unix 的实现是(1、3、6、12、24、48、64、64……),实测 Android 手机各个厂商的实现各异,但也基本符合“指数退避”的原则,其中一个厂商的实现是(0.42、0.9、1.8、3.7、7.5、15、30、 60、120……),相比这两个系统,iOS 的实现就比较激进了,为(1、1、1、2、4.5、9、13.5、26、26……)。了解了具体实现后,很明显应用层在发送数据阶段仍然需要超时机制。
在 Mars 中有四个超时概念,分别为首包超时、包包超时、读写超时、任务超时。首包超时为从请求发出去到收到第一个包最大等待时长,读写超时则是单次请求从发送请求到收到完整回包的最大等待时长,计算公式分别为:
- 首包超时 = 发包大小/最低网速+服务器约定最大耗时+并发数*常量;
- 包包超时 = 常量;
- 读写超时 = 首包超时+最大回包大小/最低网速;
- 任务超时 = (读写超时 + 常量) * 重试次数。
需要特别注意的是,读写超时的计算公式中有一个最大回包大小,这个数值只能预估。目前在 Mars 中预估为 64K,这也是为什么不建议用 Mars 传输大数据的原因之一。
在上述的方案中,读写超时、首包超时都使用了一些估值,使得这两个超时是比较大的值。假如我们能获得实时的动态网络信息,也就能得到更好的超时机制。基于这个想法,我们引入了动态超时机制,基本思想是:根据最近的历史任务完成情况把网络分为优良、评估、恶劣,由此来变动估值的大小。
接收
接收没有太多需要注意的地方。只需保证循环接收的 Buffer 不要太小,以防产生太多的系统调用,且注意将网络线程和业务处理线程分离就行了。
连接的维持
如果需要频繁发送数据或需即时收到服务器的消息,维持一个长连接会是不错的选择:
- 消息及时;
- 省电省流量;
- 提高发送速度。
但运营商会因为网络资源的原因,当一个连接长时间不发送数据时会断掉该连接,所以要想保持连接,就需要用心跳维持。太长的心跳会导致起不到相应的功能,太短的心跳因为频繁唤醒手机,频繁让 RRC 状态机进入 Active 状态,会非常耗电。Mars 针对这个问题也有智能心跳的方案,不过一般建议心跳间隔最短 4.5 min(实际测试到某个地区移动联通 NAT 超时时间 5min,电信的大于 28min)。
技术方案
通过上面对几个过程针对性地优化之后,我们有了整体的优化方案。有方案就需要通过代码实现,但怎么去写代码也是需要仔细思考,首先我们来看一下移动网络应用的特点:
- 随时启动与中止——用户退出或更改账户、手机休眠与唤醒……
- 并发少状态多——主要功能收发、网络的有无、用户的活跃状态……
- 尽量少的资源、尽量快的网络——省电、省流量、网络要敏感……
基于这些特点,在方案选择上可能也需要再三斟酌。线程模型方面,消息队列比多线程更合适,I/O 模型上,事件驱动的 I/O 复用模型比阻塞式的更为灵活。
不过,无论使用哪种技术方案,代码都不大可能写得一点问题都没有。Crash 方面就需要依赖各个平台自己的实现进行捕捉堆栈了,不过捕捉到的堆栈最好包括所有线程的。Bug 方面,一般是通过记下的 Xlog 日志进行推断,疑难杂症可通过 TCPDump 抓包进行分析。
作者:闫国跃,微信高级工程师,目前主要负责 Mars 开源工作。先后参与了微信终端基础组件的开发、微信终端日志系统的建设、微信终端运维门户的开发。
责编:唐小引(@唐门教主),欢迎技术投稿、约稿、给文章纠错,请发送邮件至tangxy@csdn.net。
版权声明:本文为 CSDN 原创文章,未经允许,请勿转载。
了解最新移动开发、VR/AR 干货技术分享,请关注 mobilehub 微信公众号(ID: mobilehub)。