本文来自OPPO互联网技术团队,转载请注名作者。同时欢迎关注我们的公众号:OPPO_tech,与你分享OPPO前沿互联网技术及活动。

1. 背景

IPv6推广已久,基础体系建设也日趋完善。加之工信部的响应号召,我们做应用体系的v6地址迁移适配势在必行。但整个迁移过程也不是一蹴而就,期间也面临着不少疑难问题亟待解决。

从协议层看,IPv4和IPv6的兼容性并不好,部署改造困难。其次,由于国内数量庞大的IPv4用户和设备,以及广泛的NAT技术应用,也使得网络结构更加复杂,对IPv6升级带来不少挑战。

最重要的是,当前并不算完备的IPv6基础环境,意味着基于v6网络而建设的服务体系,在用户使用体验层面相比v4有着较为明显可感知的劣势,这对各大应用服务商来说,是不能接受的,这也使得IPv6推进改造的动力严重不足。

所以我们从以下几个点了引入和讲解OPPO迁移适配历程:

  • IPv6是什么,为什么要升级IPv6
  • 软硬件支持程度如何,该怎样去迁移适配IPv6
  • 在IPv6网络不友好的情况下,如何降级和改善使用体验

在阐述升级IPv6的必要性之前,我们先了解下IPv6

1.1 IPv6 概念

IPv6(Internet Protocol version 6)是用于数据包交换互联网络的网络层协议,由互联网工程任务小组(IETF)于1998年设计的用来替代 IPv4 协议的互联网协议版本。相较于IPv4使用的32位,IPv6采用128位构成,其所拥有的地址容量是 IPv4 的约 8×10^28 倍,能够极大的满足网络地址资源数量问题。

那么除了量级方面,IPv6还有哪些优点呢?

1.2 IPV6 优势

IPv6 在解决了 Pv4 的地址匮乏问题的同时,还在许多方面提出了改进。与 传统的 IPv4 相比,IPv6 具有以下几方面特点及优势:

  • 充足的地址空间和层次化的结构
  • 增强的组播支持和对流控制
  • 更小的路由表,更灵活的路由机制
  • 动态配置状态地址
  • 更有效的认证安全机制
  • 省流高效
  • 扩展灵活

既然IPv6有如此大的优势和普及需求,那么推广和部署目标该如何达成呢?

当前阶段来看,在v6与v4共存的大环境下,IPv6的迁移适配将会是一个繁杂而又漫长的过程,我们需要一个平稳的过渡和转换步骤,以最低影响来完成升级改造。

2. 过度技术及设备支持情况

为了向IPv6网络逐步演进,IETF提出了三种转换机制来过渡IPv4到IPv6。

2.1 IPv6应用迁移技术

a. 双栈技术

双栈技术是指涉及到业务交互的从用户侧到网络侧的所有软硬件设备同时支持IPv4和IPv6两个协议栈,即双栈节点使用IPv4协议栈与IPv4节点进行通讯,使用IPv6协议栈与IPv6节点进行通讯。

双栈技术的优点是改造彻底,适用性广、用户互通性好,缺点是投资大、周期长。

b. 隧道技术

隧道技术通过对报文的封装、解封装,使得两个同构网络能够在一个异构网络的两边桥接起来从而实现相互通信。简单来说,隧道机制就是在必要时将IPv6数据包作为数据封装在IPv4数据包里,使IPv6数据包能在已有的IPv4基础设施(主要是指IPv4路由器)上传输的机制。

隧道技术的优点是App应用只需要新增一个IPv6隧道服务器,应用系统本身基本不影响,方便快速部署,缺点是需用户安装相应IPv6隧道软件,普适性和方便性都有局限。

c. 网络地址转换技术

翻译类技术有NAT64、SPACE6等。翻译技术是在IPv6用户和IPv4移动端App应用之间部署协议转换设备,建立IPv6/IPv4之间地址和端口的映射关系,以实现透明的IPv6和IPv4互访。翻译技术具有改动小、部署快、投资小的优点。

针对移动设备来说,双栈技术是最适合推广和改造的,因为客户端设备硬件迭代快,随着设备的更替和系统升级,APN双栈的支持会很快推广普及开。

2.2 移动端双栈支持情况

a. iOS生态

苹果在2015年即宣布IOS9开始支持IPv6,从16年6月开始对AppStore提交规范做了限制,至2017年6月,要求App使用IPv6才能上架。自IOS12.1开始,苹果默认开启APN双栈支持。

b. Android客户端

相对开放的Android来说,APN协议设置都是支持编辑的。并且5.0以后默认缺省设置为双栈模式。其他场景下,也可以在移动网络访问网络时,通过修改接入点(APN)中“APN协议/APN漫游协议”属性设置为“IPv4/IPv6”双栈网络使设备支持双栈。

注:是否支持编辑 APN 协议,具体需要根据不同机型来侦测判断。

关于其他设备软硬件对IPv6的支持度情况,可以参见互联网国家中心2018年软硬件关于IPv6支持度的调查报告[1]。

2.3 移动端应用IPv6协议适配改造

对于已支持双栈的手机设备,需要在应用层面也适配IPv6地址升级,通过v6 协议来完成和服务端的数据交互。

移动端App应用IPv6升级,是指经过软件层面适配IPv6, 使得在原来仅支持用户通过IPv4协议访问并获取服务的应用,同样能够通过IPv6协议来访问并获取服务。改造过程主要包括:

  • 检测移动设备是否支持IPv4/IPv6双协议栈
  • DNS服务能正确返回IPv4/IPv6地址
  • 建立v6链接,与服务器通信

由于Linux内核的TCP/IP协议栈在2.2版本就已经支持IPv6了, Android是基于2.6之后的,所以默认是能够使用IPv6地址与后台交互的。

应用后台的IPv6的演进升级不做赘述,目前常见采用双栈云主机、DNS64+NAT64、SPACE6翻译等多种技术完成IPv6的升级改造,在不改变现有应用服务架构的情况下,实现移动端App应用服务全面支持IPv6。

那如何知道DNS能否返回IPv4/IPv6地址呢?

IPv6地址获取流程

当客户端是双栈环境时,客户端在向DNS服务器请求地址解析时,会发起域名A记录和AAAA记录的解析请求,如果后台支持双栈,就拿到对应的两条解析地址结果。

如图,DNS查询基本是同时发起的 A 和 AAAA 记录的查询,然后先后获得地址解析结果。

名词解释:

A记录: 一个域名指向 IPv4 地址的解析结果,即最常见的记录类型

AAAA记录:是一个域名指向 IPv6 地址的解析结果。如果想要一个域名解析到 IPv6 地址,则需要设置此种类型的解析结果。同一个域名可以同时有 A 与 AAAA 两种记录类型

DNS 服务器:是进行域名(domain name)和与之相对应的IP地址(IP address)转换的服务器。将域名解析为ip

至此,我们解决了开头提出的前两个问题,即升级IPv6的必要性和如何在软硬件层面兼容适配IPv6。

那么,能够解析到并处理IPv6地址建链是否就意味我们完成了整个v6的升级改造工作吗?

由于当前IPv6基础建设尚未完善,连通性问题和可靠性问题不能得到有效保证,链接超时或失败会导致出现加载等待、页面错误等情况,造成用户可感知的负面体验,所以还是需要使用IPv4协议适当的做ip降级或者兜底策略。

针对 IPv6 的回退和降级策略,IETF于12年和17年分别发布了两版RFC算法来描述了关于在域名解析、地址排序和连接尝试阶段v4配合v6升级适配的详细方案,该方案成为 HappyEyeball。

3. HappyEyeball算法

HappyEyeballs 分为两版,分别是 Cisco于2012年提出的 RFC6555 版和 Apple于2017年提出的 RFC8305 版。这里参考v2版详细解读一下 从域名解析到地址排序和连接建立的过程。

先从Apple在IETF上关于 HappyEyeballs v2的简要介绍来了解一下算法概貌[2]:

The updated implementation performs the following:

  • Query the DNS resolver for A and AAAA.
    If the DNS records are not in the cache, the requests are sent back to back on the wire, AAAA first.
  • If the first reply we get is AAAA, we send out the v6 SYN immediately
  • If the first reply we get is A and we're expecting a AAAA, we start a 25ms timer

    • If the timer fires, we send out the v4 SYN
    • If we get the AAAA during that 25ms window, we move on to address selection
  • When we have a list of IP addresses (either from the DNS cache or by receiving them close together with v4 before v6), we perform our own address selection algorithm to sort them. This algorithm uses historical RTT data to prefer addresses that have lower latency
  • but has a 25ms leeway: if the historical RTT of two compared address are within 25ms of each other, we use RFC3484 to pick the best one.
  • Once the list is sorted, we send out the SYN for the first address and start timers based on average and variance of the historical TCP RTT. Roughly speaking, we start the second address around the same time we send out a SYN retransmission for the first address.
  • The first address to reply with a SYN-ACK wins the race, we then cancel the other TCP connection attempts.

整个过程如下:

  • 从DNS服务器同时获取AAAA记录和A记录解析地址
  • 如果v6地址先返回就直接开始握手建链,如果v4地址先返回,则有25ms解析时延,在收到v6地址后会做地址选择
  • 如果是有v4和v6地址列表,会通过算法对地址排序,排序依据历史RTT数据
  • 排序完成后,会依次有序的取地址发送握手建链请求,并启动定时任务,该任务在250ms后检查若未完成连接建立,则触发第二个地址开始连接尝试
  • 只有有一个握手确认成功(建立了连接),就会取消所有其他的连接尝试

我们从 RFC8305 中摘选几个关键步骤【地址解析排序和建链】的过程来深入探究一下:

3.1 域名解析

  • 在解析域名的时候,需要同时发送A记录和AAAA记录地址查询
  • 如果DNS服务器不支持异步API来同时获取解析两种记录的IP,可以尝试异步请求去分别获取v4和v6地址
  • 业务不应该等待所有的解析地址返回之后,才开始连接建立。如果AAAA记录响应了,就应该立即执行IPv6连接尝试
  • 相反,如A记录先响应,客户端应该等待一小段时间(解析时延建议为50ms<文档新修>),确保能获取到v6地址
  • 在等待延时内如有AAAA记录返回,则进入地址排序过程
  • 若在延时外才有v6地址返回,则将地址加入候选队列,即建链过程中也会尝试请求

3.2 地址排序

  • 如果客户端是有状态的且每个地址都有历史RTT路由记录,则应根据规则选择较低RTT的
  • 如果客户端有某个地址历史记录,也应该将其优先级提高到其他没有用过的地址之前(跨网络或者切换网络情况不在此范围)
  • v4地址和v6地址应该以组(family)为单位交错排列,一组可能有多个相同类别(同为v4或同v6)的地址,避免以为地址过去而导致建链时间过长

3.3 连接尝试

  • 为避免无意义的网络连接,建链过程不应该并行,而是依次有序的单个启动
  • 在一定的连接尝试延时(推荐250ms)过后,再次使用列表中的后续ip地址开始建链,逐次尝试。
  • 一旦出现首个Ip建链握手成功后,即取消其他未完成的连接尝试。另外,前面的DNS解析响应过程不会中断(推荐1s)
  • 建链时延推荐为250ms,可根据相同域名的历史RTT数据采集来动态调整延时,但时长区间应限制在100ms-2s

4. 最佳实践

目前已经使用上述算法的项目包括:

  • Chrome
  • Openra 12.10
  • FireFox 13
  • CURL

这里选取CURL对整个HappyEyeball做个功能验证。

4.1 验证CURL建链过程

a. 域名解析

对 google做一下域名解析,可以看到有 IPv4 和 IPv6地址返回

nslookup -type=AAAA google.com

b. 尝试连接访问

curl "https://google.com" -v 

如图,双栈情况下,同时发起A记录和AAAA记录解析域名,即使A记录先返回,建链也是优先v6地址。

4.2 实践客户端网络库HappyEyeballs

基于网络库Okhttp,制定一套策略来适配ip地址竞速过程,即网络层实现HappyEyeballs。

a. IP协议栈探测

在手机端,我们如果想提前判断当前设备是否支持双栈,由此来感知是否支持IPv4/IPv6协议栈,是一个很棘手的难题。因为这个判定结果,直接影响到后续dns解析和竞速建链过程的算法抉择。

能否直接先建立连接然后来确认地址和端口呢? 答案是肯定的。

但是,如果使用TCP,就会面临连接connect过程前必须先进行三次握手建链,造成无用的网络损耗。而UDP的 connect方法就给我们的IP协议栈确认提供了可能。 因为在UDP发生真实网络数据请求和数据响应之前,他的connect方法本身只是用于检测端口是否可用,地址是否准确,并记录这些信息,返回给调用者。

//UDP接口
{
    connect(InetAddress, port) //尝试bind地址和端口后,并赋值。 可以通过连接本地 ipv6地址FE80::xxxx来验证协议支持
    send(DatagramPacket)
    // c
    sendto() //请求数据
    recvfrom() //响应数据
}

详细实现参见 IPv6可用协议栈检测 [3]

所以我们可以通过上述方式,来提前感知设备所支持的IP协议栈,做后续DNS请求和连接竞速优化。当然,由于设备APN配置变化频率不高,我们可以将整个探测结果做本地缓存,避免重复的协议栈检测。

b. DNS获取和排序

OKHttp的域名解析提供了dns()方法配置,有默认解析和自定义解析两种:

  • 默认DNS: 网路库DNS域名解析过程默认实现调用的是 InetAddress.getAllByName(host), 根据运营商配置会返回指定域名对应的IPv4和IPv6地址。我们可以在lookup阶段对结果做一次干预和过滤,配合网络库EventListener接口收集来的IP连接成功率和协议类型对IPv4和IPv6地址做交错排序。
  • 自定义HttpDNS:自定义Dns有诸多优势,比如流量调度和防劫持。这里不一一列举。此方案我们可以直接向自己的DNS服务器同时请求AAAA记录和A记录用于获取可用IPv6/IPv4地址。同时可以配合后台下发权重和历史连接成功率实时调整排序,做交错列表用于算法竞速。

c. 地址竞速

通过修改okhttp的请求并发控制,在StreamAllocation.findConnection()过程增加IP建链并发竞速过程即可。

具体为,在连接的查找和尝试阶段,从连接池或地址列表首位,取v6地址发起握手建链,同时,开启一个延迟250ms后发起v4的建连的任务,如果在该时间段内v6建连成功,延迟任务直接终止,流程结束。若以外会将列表进行交换调整,使v4排在首位,发起v4连接,此时将处于竞争模式,当哪一个连接先完成建立,就将那个作为目标地址,并取消其他握手建链过程,调整列表顺序,并执行后续数据交换操作。否则,顺序遍历列表其他v6/v4组合,重复上述步骤。

其他网络连接可参照上述过程。此处附上Github上的一套HappyEyeballs C实现方案[4]。

5. 总结

IPv6的普及推广是一个繁重而又漫长却十分值得的过程,为保证过渡的顺利和体验层面的提升,HappyEyeballs作为一种平滑策略是非常值得肯定的。这里从IPv6说起到客户端适配的过程,尽量按照笔者的认知去尽力做详尽描述和总结提炼,但也难免存在谬误或者认知不足之处。欢迎交流沟通或者提出优化意见或建议。

6. 参考资料

[1]. 互联网国家中心2018年软硬件关于IPv6支持度的调查报告 www.ipv6ready.org.cn/public/download/ipv6.pdf

[2]. Apple关于HappyEyeballs讲解
https://mailarchive.ietf.org/...

[3]. IPv6可用协议栈检测
https://cloud.tencent.com/dev...

[4]. GitHub上的HappyEyeballs C实现方案
https://github.com/shtrom/hap...

[5]. HappyEyeballs最佳实践
https://segmentfault.com/a/11...

[6]. 谈一谈IPv6和HappyEyeballs
https://fukun.org/post/201901...


OPPO数智技术
612 声望952 粉丝