一、 引入
随着 TIG 阿基米德平台全面应用。组成京东容器生态技术栈的分布式域名解析服务 ContainerDNS(go 版 https://github.com/tiglabs/containerdns )全量生产环境应用,承载着每天百亿的访问量,单实例峰值每秒请求达到 15W QPS,已经接近 ContainerDNS 的性能极限(17W QPS)。为了更好的提高系统的并发服务,对 ContainerDNS 的优化也势在必行。
本文对 ContainerDNS 性能优化思考和技术实践历程,希望对业内在容器领域和域名解析方向技术实践一些启迪。
ContainerDNS 的 DNS Server 代码用 Go 语言实现,我们在后期的优化中,通过各种尝试,并通过 pprof 采集数据分析,发现性能损耗最大的是收发包的地方。同时我们对 bind9 进行了压测、采集分析发现同样是收发包函数最为耗时。
数据平面开发套件DPDK(Data Plane Development Kit) 可以极大提高数据处理性能和吞吐量,提高数据平面应用程序的工作效率。灵感来自TIG 容器生态技术栈另外一个重要的服务高性能负载均衡服务Jupiter ( https://github.com/tiglabs/jupiter )实现单实例 200 万 QPS 的高性能。因此 TIG 团队工程师想到用 DPDK 来实现 DNS 数据报文的收发是一个有挑战和前景的尝试。
二、系统概述
让我们先来回顾下 ContainerDNS 设计之初的愿景和初心。ContainerDNS 作为京东商城新一代软件定义数据中心的关键基础服务之一,具有以下特点:
- 分布式设计 多数据中心数据同步复制 避免维护多个域名解析元数据副本
- 支持自动发现服务域名 支持容器 Service
- 支持后端探活
- 易于维护、易于动态扩展
图一 ContainerDNS 架构图
ContainerDNS 包括四大组件 DNS server、service to DNS 、user API 、IP status check。这四个组件通过 etcd 数据库集群结合在一起,彼此独立,降低了耦合性,每个模块可以单独部署。DNS server 用于提供 DNS 查询服务的主体。service to DNS 组件是 k8s 集群与 DNS server 的中间环节,会实时监控 k8s 集群的服务的创建,将服务转化为域名信息,存入 etcd 数据库中。user API 组件提供 restful api,用户可以创建自己的域名信息,数据同样保持到 etcd 数据库中。IP status check 模块用于对系统中域名所对应的 ip 做探活处理,数据状态也会存入到 etcd 数据库中。如果某一个域名对应的某一个 ip 地址不能对外提供服务,DNS server 会在查询这个域名的时候,将这个不能提供服务的 ip 地址自动过滤掉。
KDNS(DPDK DNS 简称) 不是 ContainerDNS 的否定而是其延续,我们只是替换了 DNS server 模块,采用 C 语言,并且是基于 DPDK 全新实现。ContainerDNS 探活模块、API 模块、k8s 监控模块都是可以复用的,对于工程来说具有很好的延续性。如上图所示 KDNS Server 进程部署物理机上,同时部署 quagga 进程,对外发布一个 VIP。这样可以实现 KDNS Server 的多活,同时 Client 端经过路由器访问 DNS Server 的 VIP,路由器对 BGP 协议的支持,不同的访问可能会访问到不同的 KDNS Server 上,可以实现一定的负载均衡。这样设计方便后期的升级扩展,关闭 quagga 程序数据就不会打到对应的 KDNS Server,然后版本升级,开启 quagga 发布 VIP,新的 Server 对外提供服务。可以很方便在服务不中断的条件下进行 KDNS Server 版本的动态回滚、升级。同时可以方便的监控 KDNS,如果不能提供服务可以将此物理机上的 quagga 杀死,这样这台服务器就不对外提供服务。这种设计方式的实现可以很方便的实现热升级、高可用。下面将主要介绍 DNS server 基于 DPDK 的实现。
三、 KDNS 系统设计与实现
DNS server 是提供 DNS 的主体模块实现架构如下图:
如上图所示,系统中用到的CPU 核有两种角色:主核和从核。主核相当于控制通道,域名数据、交给协议栈的数据都是主核处理。从核主要处理数据通道的数据,通过DPDK 网口收包,报文解析处理,解析结果发包给用户。
Data Plane Development Kit(DPDK):是运行在 Linux 用户态,实现 X86 通用平台网络报文快速处理的库和驱动的集合,其主要特点:
- 多核编程框架及 CPU 亲和性
- 巨页(HugePage),减少页表项数目,降低 TLB miss
- 无锁队列
- UIO 支持,用户态驱动,减少报文 copy
- poll-mode 网卡模式,无中断轮询收包
为了充分利用 DPDK 的特性,我们根据功能将 KDNS 划分为:数据收发模块、协议解析处理模块、转发报文处理模块、域名信息数据处理模块、ARP/BGP 协议报文处理模块等。数据收包、协议解析都是跑在从核中,转发报文处理模块、域名信息数据处理模块为单独的处理线程处理,ARP/BGP 协议等协议的支持由主核通过 DPDK 的 KNI 接口发送给内核协议栈,发送数据由主核发出。每个包数据、域名数据在各个模块中的交互,通过 DPDK 的无锁队列实现。并且对外提供 RESTful API 接口,用户可以对域名数据的更新。为了结合 ContainerDNS 其他组件的使用,我们这里开发了 DNS-agent 进程,此进程 go 语言实现会监控 etcd 的数据变化,实时将域名信息通过 RESTful API 接口更新到 KDNS Server 中。当然 KDNS 完全可以脱离 ContainerDNS 的生态环境运行,只要通过 RESTful API 接口就可以更新域名数据,这样 KDNS 就可以正常工作起来。
下面是主核的处理流程:
首先创建和Restful API 数据交互的domain-msg-ring,域名变更处理线程检测到域名变更数据,会将数据放入domain-msg-ring 中,主核后面会进入一个死循环,首选处理域名变更,并将变更的数据分发到所有的从核,从核同样在循环处理的时候回首先更新域名数据到本地缓存中。然后主核会检测是否有从核发过来的数据(例如ARP 请求),主核将数据转给DPDK 的KNI 接口交由Linux 协议栈处理。然后主核调用接口从KNI 收取数据,如果有协议栈发过来的数据,调用DPDK 的发包函数rte_eth_tx_burst 将数据发送出去。然后主核判断是否有转发域名数据,如果有同样调用rte_eth_tx_burst 将转发域名请求的结果发送出去。这里主核只负责发包,转发查询上级DNS Server 全部由后台的转发处理线程处理,不会影响主核的处理速度。
从核的处理逻辑较为简单,首先调用处理主核发过来的域名数据,从而及时改变自己的域名数据缓存信息。然后调用DPDK 收包函数rte_eth_rx_burst 收取报文,如果有数据则进行数据包解析处理,参考下面的协议解析处理模块。如果是本地zone 的域名查询,将查询的结果直接发送给客户端。如果是ARP 请求,则将数据包放入kni-pkt-ring 中交由主核处理。如果是转发域名,则交由转发报文处理线程处理。
数据收发模块:采用DPDK 的收发包接口,开DPDK 启RSS (Receive Side Scaling,多 CPU 之间高效分发的网卡驱动技术),由于DNS 访问基本是UDP 包端口是53,RSS 采用了对IP 和端口进行Hash 的方式,当客户端很多的时候可以有效地实现多核的均衡。为了更好地提高性能,系统中每一个从核只处理一个收包队列上面的数据。发包也是只发送对应队列上的数据。所以收发包数据间,所有的核都是独立的,没有任何耦合不需要任何锁机制,更加快速。
协议解析处理模块:每一个从核通过DPDK 接口收到数据包,进行数据包解析。并将结果返回给客户端。流程图如下:
转发报文处理模块:系统所有的域名数据都是基于某一个或者多个zone 进行的,如果域名不在本地支持的zone 内就要将请求发送给上级的DNS Server, 比如我们的DNS Server 支持本地的zone 是 tst.local,也就是说所有访问*.tst.local 的域名本地的DNS Server 都会查询解析,如果用户访问的不是*.tst.local 的域名就得转发给上级的DNS 进行解析,这个转发的处理流程较慢,我们采用了一组后台的线程进行处理,这些线程不会绑定到CPU 上,完全与主核、从核的功能分离开来,这样主、从核都不会处理这样的慢速数据请求。
首先数据处理从核收到数据包,发现如果请求的域名不是本地的zone 配置的域名后缀,走慢速流程即转发流程。从核会将本请求入放在和转发处理线程共享的数据队列中,这样做的好处是把从核解放出来,只处理快速的查询请求,不会block 住,从而能全速的处理本地数据。转发报文处理线程是一个死循环,首先从队列中读取数据,没有数据则休眠。如果有数据则将预处理数据并调用转发接口转发给上级DNS Server,并将上级DNS Server 的回包处理后放入与主核共享的rte_mbuf 无锁队列中。主核会及时的出队转发报文处理线程放入的数据,调用DPDK 的网卡发送接口,将数据从主核的发送队列发送出去。需要说明的是这期间数据都是公用一个rte_mbuf 内存,没有任何的数据复制的过程,从而更好地提高性能。
域名信息数据处理模块: 这个是一个后台的线程提供Restful API,可以和agent 进行数据交互,从而获取系统的域名数据。
首先注册支持url 和方法,目前支持GET、POST、DELETE 分别对应着域名的查询、增加、删除三个接口。同时提供状态的查询接口,目前只有两种状态Init 和Runing,当agent 检测到进程是刚启动(Init 状态)时,会将所有的域名信息下发到DPDK DNS Server,之后将DPDK DNS Server 的状态设置为Runing 状态。后面如果有域名信息变更,agent 会调用POST 或者DELETE 接口将域名数据同步到DNS Server。为了提高数据的安全性,DNS Server 的API 支持ssl 证书,这样可以有效的防止域名数据被恶意的窃取、修改。同时域名数据插入删除操作的时候,采用了Hash 表的设计,每一个域名先计算出Hash 值,如果发生冲突先比较Hash 值,如果相同再进行字符串匹配。由于Hash 值远远大于Hash 桶的长度,当发生Hash 冲突的时候先匹配Hash 值会增加大大提高匹配的效率。
ARP/BGP 报文处理:这模块较为简单,从核解析数据包如果发现是 ARP 协议报文,将数据传送给主核,主核在将数据通过 DPDK 的 KNI 将数据报文发给 Linux 协议栈,主核后面再通过 KNI 读取 Linux 协议栈处理的结果,然后调用 DPDK 的发送接口,将数据发送出去。BGP 协议的处理模式类似,也是交与 Linux 内核协议栈处理。
四、性能优化及测试
- 恰当地使用 rte_prefetch0(),可以减少 cache-miss 次数
- likely() 和 unlikely() 的使用,可以减少分支预测失败的次数
- 数据处理全程无锁,交互部分利用无锁队列实现
- 整个处理过程无包 copy,发送包复用收包的 mbuf 内存,减少 mbuf 的申请
性能测试:
1)测试环境
CPU:Intel® Xeon® CPU E5-2698 v4 @ 2.20GHz
NIC:intel 82599ES 10-Gigabit SFI/SFP+ Network Connection
2)测试配置
KDNS(八个数据核一个控制核),bind9 (16 个核,版本 bind-9.10.6-P1)
3)测试结果
bind9 五分钟单域名测试结果(47W QPS):
上图是bind9 稳定运行5 分钟的数据采集结果,平均47W QPS。
KDNS 20 分钟单个域名测试结果 (1000W QPS):
如上图所示,优化后的KDNS 性能达到1000 万QPS,而且二十分钟运行很平稳,并发性能是bind9 20 多倍、ContainerDNS(go 版)的50 倍。
KDNS 多个域名测试结果,其中系统域名记录大概 20 亿条。
相同网络环境下响应时间对比(queryperf 测试):
Queryperf 数量
最慢响应 us(bind9/ KDNS)
最块响应 us(bind9/ KDNS)
平均响应 us(bind9/ KDNS)
1
1140/226
15/16
102/68
3
1138/654
18/17
172/83
如上图所示,可以看到在相同的测试服务器、客户端主机、网络的环境下测试,利用 DPDK 收发包实现的 DNS,对于访问的响应更加优秀和稳定,最慢响应由 bind9 的 1140 微妙提高到 226 微妙,平均响应时间也提高了一倍。
五、总结
本文主要介绍了 KDNS,一种基于 Intel DPDK 平台开发的 DNS Server。其接近网卡线速处理能力,灵活的部署,多样的监控以及可靠的稳定性, 同时兼顾 ContainerDNS 的所有的分布式模块功能。作为 TIG 阿基米德平台京东数据中心操作系统(JDOS)的一个重要的组成部分,在京东数据中心发挥至关重要的作用。
评论