微信终端跨平台组件 Mars 系列(三)连接超时与IP&Port排序

前言

Mars 是微信官方的终端基础组件,是一个使用 C++ 编写的业务无关、跨平台的基础组件。目前在微信 Android、iOS、Windows、Mac、WP 等多个平台中使用。Mars 主要包括以下几个独立的部分:

  • COMM:基础库,包括socket、线程、消息队列、协程等基础工具;
  • XLOG:通用日志模块,充分考虑移动终端的特点,提供高性能、高可用、安全性、容错性的日志功能;(详情点击:高性能日志模块xlog)
  • SDT:网络诊断模块;
  • STN:信令传输网络模块,负责终端与服务器的小数据信令通道。包含了微信终端在移动网络上的大量优化经验与成果,经历了微信海量用户的考验。

Mars 系列开始,将为大家介绍 STN(信令传输网络模块)。由于 STN 的复杂性,该模块将被分解为多个篇章进行介绍。本文主要介绍微信中关于 socket 连接及 IP&Port 选择的思考与设计。

你需要知道的TCP连接

TCP 协议应该是目前使用的最广泛的传输层协议,它提供了可靠的端到端的传输,为应用的设计节省了大量的工作。TCP 建立连接的”三次握手”与连接终止的“四次挥手”也广为人知。在这简单的 connect 调用中,还能做怎样的思考与设计呢?

1
int connect(int sockfd, const struct *addr, socklen_t addrlen)

连接的超时重传

超时与重传是 TCP 协议最核心的部分,在不稳定的移动网络中,超时重传的设计尤为重要。在连接建立的过程中,由于网络本身的不可靠特性,不可避免的需要重传的机制来保障可靠服务。在《TCP/IP详解 卷1》的描述中,在大多数 BSD 实现中,若主动 connect 方没有收到 SYN 的回应,会在第6秒发送第2个 SYN 进行重试,第3个 SYN 则是与第2个间隔24秒。在第75秒还没有收到回应,则 connect 调用返回 ETIMEOUT。

这就意味着,在不能立刻确认失败(例如 unreachable 等)的情况下,需要75秒的时间,才能获得结果。如果真相并不是用户的网络不可用,而是某台服务器故障、繁忙、网络不稳定等因素,那75秒的时间只能尝试1个 IP&Port 资源,对于大多数移动应用而言,是不可接受的。我们需要更积极的超时重传机制!!!

然而,我们并不能修改 TCP 的协议栈,我们只能在应用层进行干预,设计应用层的超时机制。说干就干,这个时候你是否已经在构思新的、应用层的连接超时重传机制了呢?应用层的超时重传,典型做法就是提前结束 connect 的阻塞调用,使用新的 IP&Port 资源进行 connect 重试。但是,我们应该选择怎样的连接超时值呢?4秒?10秒?20秒?30秒?不同的应用场景会有不同的选择。我们来看一下常见的几种场景:

  • 连不通 or 网络不可用等
  • 服务器繁忙 or 中间路由故障等
  • 基站繁忙 or 连接信号弱 or 丢包率高等

在第一种场景中,连接超时设置不会带来什么区别。在第二种场景中,部分服务器资源或路由不可用,我们希望连接超时能稍微短一些,使得我们能尽快的发现故障,并且通过更换 IP&Port 的方式获得可用资源或路由路径。而第三种场景则是在移动网络中经常遇到的弱网络的场景。在这种场景中,我们更换 IP&Port 资源也是无效的,因此希望连接超时能相对长一些,进行更多的TCP层的重传。(当然,也不是超时越长越好,后面的分析可以看到很多等待时长是效果低微的)

不同的场景对连接超时有不同的需求,然而,我们在程序中并没有很好的方法来区分这些场景。在进行连接超时这个阈值的选择前,我们先来看看,当前主流的 android、iOS 操作系统的连接设计。android 的 TCP 层连接超时重传如下图所示(测试机型为 nexus5,android 4.4)。超时间隔依次为(1,2,4,8,16),第5次重试后32秒返回 ETIMEOUT,总用时63秒。超时设置符合 Linux 的常规设置。 tips-net-mars-ip-sort-2019312162610

但在不同的机型中,偶尔会出现差异性。如下图 android 抓包(三星 android 4.4)。 tips-net-mars-ip-sort-2019312162636

iOS 的 connect 超时重传如下图所示。超时间隔依次为(1,1,1,1,1,2,4,8,16,32),总共是67s。 tips-net-mars-ip-sort-2019312162711

经过 tcpdump 的调研分析后,我们发现:

  1. 在 iOS 系统中对 connect 的超时重传进行了一定的修改,在 connect 初期使用更积极的策略,以适应移动网络的不稳定特征。而在 android 系统中,connect 超时重传则使用了较为“懒惰”、适用于有线网络的超时重传间隔;
  2. 不管什么平台,连接总超时时长都需要1分钟左右,这个时长在大多数移动应用中,都是不符合用户体验要求的;
  3. 连接的初始阶段,TCP 超时重传会更积极一些,越到后面,重传间隔越大。

因此,在实际的连接超时设置上,我们根据不同的系统特征,结合应用能接受的“用户体验”范围,可以设置不同的连接超时间隔。例如在 iOS 系统中,由于采用了较为积极的超时间隔,我们可以将 connect 调用的超时设置为10s。在10s内,iOS 会自动进行6次的重发。在 android 系统中,系统会在第7秒发起第3次重发,之后需要在第15秒才会重发。在不同的用户体验要求下,应用可以将 connect 的调用超时设置为不同的值。例如也可以设置为10s(意味着给第3次重发3s的等待时间),从而避免无效的等待时长。同时通过更换 IP&Port 后,重新调用 connect 操作的方式,来获得更积极的重发策略,更快的查找到可用的 IP&Port 组合。

连接的终止

“四次挥手”的连接终止协议已经口熟能详。过程如下图所示。需要关注的是,图中主动关闭的一方会进入 TIME_WAIT 状态,在此状态中通常将停留2倍的 MSL 时长。MSL 时长在不同的操作系统中有不同的设置,通常在30秒到60秒。TIME_WAIT 的数量太多会导致耗尽主动关闭方的 socket 端口和句柄,导致无法再发起新的连接,进而严重影响主动关闭方的并发性能。虽然在实际的使用中,可以通过 tcp_tw_recycle,tcp_tw_reuse,tcp_max_tw_buckets 等方式缓解该问题,但也会带来一些副作用。最好的解决方案是在协议的设计上,尽量的由终端来发起关闭的操作,避免服务器的大量 TIME_WAIT 状态。例如,使用长连接避免频繁的关闭;在短连接的协议设计上,务必加上终止标记(例如 http 头部加上 content-length )使得可以由终端来发起关闭的操作。 tips-net-mars-ip-sort-2019312162922

串行连接 VS 并发连接 VS 复合连接

在上述的连接超时策略中,我们选择10秒的连接超时。这就意味着我们需要10秒的时间来确认一个 IP&Port 组合的 connect 超时。当我们有多个 IP&Port 资源时,遍历的效率偏低。那我们是否能设置 connect 的超时为更短呢?例如4秒。我们知道移动互联网具有不稳定的特征,超时时间设置过短,会导致在弱网络的情况下,connect 总是失败,导致不可用。串行连接的策略在超时选择上,由于需要兼顾高性能与高可用的设计目标,使得该策略是一个相对“慢”的连接策略。

与此相应,我们会想到并发连接的策略。并发连接,同时发起对N个 IP&Port 的连接调用,可以让我们第一时间发现可用的连接,并且还顺带发现了 connect 最快的 IP&Port 配置。并发连接可以一举解决了“高性能”、“高可用”的设计目标,看起来很完美。然而,这个时候,服务端的同学“跳”起来了。在并发连接的策略下,服务器需要提供的连接能力是串行连接的N倍,对服务器连接资源是极大的浪费。同时,并发连接是否会引起连接资源的竞争,从而影响网络正常用户的常规体验,也是个未知的因素。

让我们来回顾串行连接与并行连接的优缺点。

串行连接

  • 资源占用少
  • 无服务器负载问题
  • 超时选择困难
  • 最慢可用

并行连接

  • 网络资源竞争
  • 服务器负载高
  • 最快可用

那么,有没有一种策略,能同时满足高性能、高可用、低负载的目标呢?在微信的连接设计中,我们使用了”复合连接“的策略。如下图所示。 tips-net-mars-ip-sort-2019312163133

初始阶段,应用发起对 IP1 &Port1 的 connect 调用。在第4秒的时候,如果第一个 connect 还没有返回,则发起对 IP2 &Port2 的 connect 调用。以此类推,直至发起了5组 IP&Port 的 connect 调用。

  • 对比串行连接与并行连接,复合连接有以下特点:
  • 常规情况下,服务器负载与串行连接策略相同,实现了低负载的目标;
  • 异常情况下,每4s发起新(IP,Port)组合的 connect 调用,使得应用可以快速的查找可用 IP&Port,实现高性能的目标;
  • 在超时时间的选择上,复合方式的“并发”已经实现了高性能、低负载的目标,因此在超时时间的选择上可以相对宽松,以保障高可用为重。

综合对比,复合连接能够维持低资源消耗的情况下,能同时实现低负载、高性能、高可用的目标。

微信 IP&Port 排序算法的演进

在建立连接的调用中,除了超时时间的设置外,IP&Port是连接的最重要参数。IP&Port 的排序、选择对于 connect 的性能也是有着重大的影响。本节主要讨论在已知 IP 列表、Port 列表的情况下,如何排序、组合的问题,而不讨论如何获得就近接入等问题。

IP&Port 的组成

在微信中,IP有多种来源类型。优先级从上而下分别为:

  • WXDNS IP 自建的DNS服务获得的IP列表
  • DNS IP 通过常规的 DNS 解析获得的 IP 列表
  • Auth IP 动态下发的保底列表
  • Hardcode IP 最终保底IP列表

WXDNS IP 是通过微信自建的 DNS 服务获得的IP列表,自建 DNS 对防劫持、有效期控制等有重要作用。DNS IP 则是通过常规的 DNS 解析获得的 IP 列表。Auth IP 是微信动态下发的保底IP列表。而Hardcode IP 则是最终的保底IP列表。总体而言,分为常规IP列表、保底IP列表两个类别。WXDNS IP、DNS IP 为常规列表,Auth IP,Hardcode IP 为保底列表。同时,在组成实际使用的 IP&Port 列表时,由于 WXDNS 与 DNS 的功能近似,因此通常只出现其中一种类型的IP列表。Auth IP 与Hardcode IP 的功能近似,也是同时只能出现两者中的一种类型。 在 Port 的选择上,微信服务在常规情况下提供2个端口,预防端口被封锁的情况。特别情况下,可以通过配置下发进行端口更新。

IP&Port排序算法(一):随机组合排序算法

每个TCP连接都是以 IP&Port 的组合为唯一标识。在 IP&Port 的选择上,我们初步归纳为2个目标:

  • 高可用:尽快的找到可用的 IP&Port 资源
  • 高性能:优先使用质量好的 IP&Port
  • 负载均衡:IP的排序算法不带任何偏向因子,避免造成人为的负载不均衡

在微信早期的排序选择上,我们使用了一种随机组合的排序算法。即将 WXDNS or DNS IP 列表与 Port 列表进行组合,组合后的结果进行随机排序。在随机排序的结果列表中,使用下述步骤进行排序:

  1. 选取IP1+Port1;
  2. 选取IP2+Port2,尽量使得IP1与IP2不相等,Port1与Port2不相等;
  3. 选取IP3+Port3,尽量使得IP3与IP1、IP2都不相等,Port3与Port1、Port2都不相等;
  4. 以此类推,形成常规列表。

同理,使用 Auth IP or Hardcode IP 列表与 Port 列表的组合,我们按照相同算法生成另外一份保底列表,并将保底列表排序在常规列表的后面,从而组成完整的 IP&Port 列表。随机组合排序的算法有着以下的特点:

  • 高性能:每一次尝试都尽量使用完全不同的资源,使得能最快的发现可用资源;
  • 初始随机,从而避免列表顺序的固化;
  • 保底列表在最后,形成最后的保护屏障;
  • 在不同的网络下,维护着不同的资源列表。

在使用中,如果发现 IP&Port 访问失败,则在列表中 ban 掉该资源。这里有个小优化,即当 IP1&Port1 的上一次访问成功时,需要连续失败2次才 ban 该资源。目的是为了减小偶然的网络抖动造成的影响。

随机组合排序算法的设计初衷,是为了以最快的速度尝试不同的资源组合,从而快速寻找到可用的资源。然而,在微信的实际使用中,却发现这种算法存在着诸多的问题。例如:

  • 网络不可用或网络较大波动情况下,列表被ban的速度较快;
  • Auth IP or Hardcode IP 列表太容易被访问到:随着常规资源陆续被ban,保底资源总是会被访问到,造成对保底资源的访问量大。保底资源是为了微信服务这不符合保底资源的设计初衷。
  • 当引入复合连接策略后,IP资源不足。这是因为 ban 的策略简单粗暴的丢弃失败的 IP,导致 IP 资源越来越少;
  • 每次缓存超时或列表轮空后,对于新列表没有经验信息可用

在随机组合排序算法的基础上,为了解决遇到的新问题,微信使用了新的“以史为鉴”的算法。

IP&Port 排序算法(二):以史为鉴

由于复合连接的引入,在每次复合连接的尝试中,微信可以伪“并发”的对N个 IP&Port 进行 connect(微信中目前N=5)。简单的ban丢弃的策略会使得 IP 资源越来越少。 针对这个特点,我们对IP&Port算法进行了以下修改:

  • 初始资源列表分为两类列表:常规列表,保底列表,分别使用方案(一)随机组合排序算法生成初始顺序;
  • 对每次复合连接使用的列表,规定5个资源的组成是4个常规资源+1个保底资源,并且保底资源在最后(完全无法获取常规资源的情况除外)。这种资源组成方式一方面解决了“保底资源太容易被访问到”的问题,一方面也保障了保底资源的作用;
  • 在不同网络中,分别记录每个 IP&Port 的使用情况,并根据使用记录进行评分、排序;
  • 区分连续记录:对每个 IP&Port 的更新,10秒内的连续成功或失败,不进行使用情况的记录。这种处理方式一方面是为了避免网络不可用或网络出现较大波动时,IP资源被过快的错误标记;一方面也避免失败历史被快速的覆盖;
  • 最近的8条使用记录中,如果有超过3条失败记录,且最新一次失败记录时间为10分钟内,则本次排序ban该记录。这种处理方式的目的是避免历史分数较高的 IP&Port 在突然出现故障时很难被排序算法排除的问题;
  • 无历史的记录使用随机评分排序。

通过上述方法,我们保证了保底资源不会被轻易访问到,解决了列表被快速标记的问题,同时也保证了历史记录好的资源在出现故障时也能被快速替换。

IP&Port 排序算法(三):遗忘历史

“以史为鉴”的方案在微信中使用了一段时间,看起来运行良好。直至某一天,微信的部分服务集群出现了故障。虽然微信客户端快速的切换到可用的服务器资源,但当故障服务器恢复后,微信客户端却迟迟没有分流到已恢复服务的集群,导致部分微信服务器负载过高,而部分微信服务器却负载较低的情况。通过分析,发现“以史为鉴”的排序方案存在着一些问题:

  • 初始阶段排在前面的资源容易获得较多的成功记录,从而分数始终维持在较高的水平;
  • 出灾情况下,故障机器由于有失败记录,使得很难获得“被原谅”的机会,从而也很难更新使用历史;
  • 采用了无历史记录随机评分,破坏了原有的“相邻记录尽量不相同”的随机性设计; 因此,好的 IP&Port 排序算法,不仅应该快速的发现可用的资源,使得在出灾情况下能快速的响应,同时,也应该具备一定的“遗忘性”、“容灾性”,使得灾情恢复后能较快的发现“灾情恢复”这一事实,并且进行重排序,使得服务器资源得到更合理的使用。在综合考虑“以史为鉴”和“遗忘历史”后,新的 方案具有以下特征:
  • 内存历史、文件历史双层记录历史:反映资源使用的近期情况及历史情况;
  • 初始化状态:每次进程重启或网络切换后,从文件历史中“压缩”出内存历史作为初始状态;
  • 旁路检测:额外更新历史的渠道,更有助于挑选高性能的资源,并且帮助“灾情恢复”的资源获得使用的机会;
  • 文件历史的遗忘性:文件历史每24小时强制刷新,避免高分数的记录长期“占有”队列;
  • 无历史、有历史的混合排序。

具体实现查看 Mars 源代码中的 simple_ipport_sort。

总结

连接是信令传输的前提,一个简单的连接操作蕴含着不少的优化空间。在连接超时的选择上,我们要兼顾性能与可用性,过短的连接超时可能导致弱网络下的低可用性,但过长的连接超时又影响用户体验。在 STN 中,我们结合系统本身的 TCP 连接重传特性,进行了相应的设计考量。即使如此,串行的连接方案仍然不能满足高性能的需求。并发连接的方案获得高性能的同时,也带来了服务器负载剧增的损失。综合考虑下,STN 使用了“复合连接”的方案,获得高性能的同时,也保证通常情况下的服务器低负载。

IP&Port 是连接的最重要资源,IP&Port 的排序选择是连接过程的重要部分。在微信的实际使用中,我们依次使用了“随机组合”、“以史为鉴”、“遗忘历史”三种方案,综合的考虑了查找性能、移动互联网的不稳定性、容灾及容灾恢复等。

连接超时、连接策略及 IP&Port 排序是连接的是三个重要组成部分,相关的方案也随着微信实践在不断的发展中。相信在不同的应用场景中,我们可能会遇到更多的不同问题及需求。随着Mars的开源,也能有机会参考、吸收其他应用中的实战经验,使得网络优化持续的深入。

转自微信终端跨平台组件 Mars 系列(三)连接超时与IP&Port排序

坚持原创技术分享,您的支持将鼓励我继续创作!