京东原来你运用的这玩意,不错,我也要!! ContainerDNS

转自社区git

ContainerDNSgithub

本文介绍的 DNS 命名为 ContainerDNS,做为京东商城软件定义数据中心的关键基础服务之一,具备如下特色:golang

  • 分布式,高可用
  • 自动发现服务域名
  • 后端探活
  • 易于维护、易于动态扩展llllllll,* 容器化部署

ContainerDNS

图一 ContainerDNS 架构图数据库

ContainerDNS 包括四大组件 DNS Server、Service to DNS 、User API 、IP status check。这四个组件经过 etcd 集群结合在一块儿,彼此独立,彻底解耦,每一个模块能够单独部署和横向扩展。    DNS Server 用于提供 DNS 查询服务的主体,目前支持了大部分经常使用的查询类型(A、AAAA、SRV、NS、TXT、MX、CNAME 等)。    Service to DNS 组件是 JDOS 集群与 DNS Server 的中间环节,会实时监控 JDOS 集群的服务的建立,将服务转化为域名信息,存入 etcd 数据库中。    User API 组件提供 restful API,用户能够建立本身的域名信息,数据一样保持到 etcd 数据库中。    IP status check 模块用于对系统中域名所对应的 IP 作探活处理,数据状态也会存入到 etcd 数据库中。若是某一个域名对应的某一个 IP 地址不能对外提供服务,DNS Server 会在查询这个域名的时候,将这个不能提供服务的 IP 地址自动过滤掉。后端

2、系统设计与实现

(1)DNS Server  

DNS Server 是提供 DNS 的主体模块,系统中是挂载在项目 ContainerLB(一种基于 DPDK 平台实现的快速可靠的软件网络负载均衡系统)以后,经过 VIP 对外提供服务。结构以下:缓存

ContainerLB

图二 DNS Server 与 ContainerLB安全

如上图所示,DNS Server 经过 VIP 对外提供服务,经过这层 LB 能够对 DNS Server 作负载均衡,DNS Server 的高可用、动态扩展都变得很容易。同时 DNS Server 的数据源依赖于 etcd 数据库,因此对 DNS Server 的扩展部署十分简单。因为 etcd 是一种强一致性的数据库,这也有效保障挂在 LB 后面的 DNS Server 对外提供的数据一致性。性能优化

DNS Server 做为 JDOS 集群的 DNS 服务,因此须要把服务器的地址传给容器。咱们知道 JDOS 的 POD 都是由 JDOS Node 节点建立的,而 POD 指定 DNS 服务的地址和域名后缀。最终体现为 Docker 容器的 /etc/resolv.conf 中。服务器

DNS Server

DNS Server 的启动过程restful

DNS Server 首先根据用户的配置,连接 etcd 数据库,并读取对应的域名信息放在程序的缓存中。而后启动 watch 监听 etcd 的变化,同步数据库与缓存中的数据。新的 DNS 请求不用在查询 etcd 数据库直接使用缓存中的数据,从而提升响应的速度。启动后监听用户配置的端口(默认 53 号),对收到的数据包进行处理。同时查出过得结果会缓存的 DNS-Server 的内存缓存中,对于缓存的数据不老化删除,就是说查询过的域名会一直在缓存中以提升查询的速度,从而达到很高的响应性能。若是域名信息发生变化,DNS Server 经过监听 etcd 随时感知这种变化,从而更新缓存中的数据,从而提供很好的实时性。测试发现,从发生变化到能查出变动预期的结果通常在 20ms 之内,坏的状况不超过 50-60ms。

DNS Server

上图是 DNS Server 响应一次查询的过程。首先根据域名和查询的类型生成一个数据缓存的索引,而后查询 DNS 数据缓存若是命中,简单处理返回给用户。没有命中从数据库查询结果,并将返回的结果插入到数据缓存中,下次查询直接从缓存中取得,提升响应速度。为了进一步提升性能,缓存的数据不会老化删除,只有到了缓存的数量限制才会随机删除一些释放空间。不删除缓存,缓存中的数据和实际的域名数据的一致性就是一个关键的问题。咱们采用 etcd 监控功能实时抓取变动,从而更新缓存的数据,通过几个星期的不停地循环,增、删、改、查域名,近 10 亿次测试,未出现数据不一致的状况。下面是 DNS Server 监控到域名信息变化的处理流程。

下面是 DNS Server 的配置文件:

DNS Server

其中 DNS 域主要是对 DNS 的配置,DNS-domains 提供可查询的域名的 zone,支持多组用 % 分隔。ex-nameServers 若是不是配置的域名,DNS Server 会将请求转发到这个地址进行解析。解析的结果再经过 DNS Server 转给用户。inDomainServers 选择作已知域名 zone 的转发功能。首先若是访问的域名匹配到 inDomainServers, 则交给 inDomainServers 指定的服务器处理,其次若是匹配到 DNS-domains 则查询本地数据,最后若是都不匹配则交给 ex-nameServers 配置的 DNS 服务器处理。IP-monitor-path 是用于和探活模块作数据交互的,系统中的 IP 状态会存在 etcd 此目录下。DNS Server 读取其中的数据,并监控数据的变化,从而更新本身缓存中的数据。

DNS Server 另外提供两个附加的功能,能够根据访问端的 IP 地址作不一样的处理。Hold-one 若是使能,同一个客户端访问同一个域名会返回一个固定的 IP。而 random-one 相反,每次访问返回一个不一样的 IP。固然这两个功能在一个域名对应多个 IP 的时候才能体现出来。为了提升查询速度,查询的域名会放在缓存中,cacheSize 用于控制缓存的大小,以防止内存的无限之扩张。DNS Server 因为采用的是 Go 语言,cache 被设计为普通的字典,字典的 key 就是域名和访问类型的组合生成的结果。

DNS Server 提供统计数据的监控,经过 restful API 用户能够读取 DNS 的历史数据,访问采用了简单的认证,密码经过配置文件配置。用户能够访问获得 DNS Server 启动后查询域名的总的次数、成功的次数、查询不到次数等信息。用户一样能够获得某一个域名的查询次数和最后一次访问的时间等有效信息。经过 DNS Server 统计信息,方便作集群的数据统计。效果以下:

(2)Service to DNS  

这个组件的主要功能是经过 JDOS 的 JDOS-APIServer 的 watch-list 接口监控用户建立的 Service 和以及 endpoint 的变化,从而生成一条域名记录,并将域名记录导入到 etcd 数据库中。简单的结构以下图。Service to DNS 进程,支持多点冗余,防止单点故障。

Service to DNS 生成的域名主要目的是给 Docker 容器内部访问,域名的格式是 ServiceName.nameSpace.svc. clusterDomain。这个格式的要求和 JDOS 有密切的关系,咱们知道 JDOS 建立 POD 的时候,传递数据生成容器的 resolv.conf 文件。下面是 JDOS 的代码片断及 Docker 容器的 resolv.conf 文件的内容。

能够看到域名采用的是 ServiceName.NameSpace.svc.clusterDomain 的命名格式,故而Service to DNS 须要监控 JDOS 集群的 Service 的变化,以这种格式生成相关的域名。因为系统对用户建立的服务会自动的建立 load-balance 的服务,因此域名的 IP 对应的是这个服务关联的 lb 的 IP,而 lb 的后端才是对应着的是真正提供服务的 POD。

Service to DNS 进程有两种任务:分别作数据增量同步和数据全量同步。

增量同步调用 JDOS-API 提供的 watch 接口,实时监控 JDOS 集群 Service 和 endpoint 数据的变化,将变化的结果同步到 etcd 数据库中,从而获得域名的信息。因为各类缘由,增量同步有可能失败,好比操做 etcd 数据库,因为网络缘由发生失败。正如此全量同步才显得有必要。全量同步是个周期性的任务,这个任务首先同步 JDOS-API 的 list 接口获得,集群中的 Service 信息,而后调用 etcd 的 get 接口获得 etcd 中存储域名数据信息,而后将两边的数据左匹配,从而保证 JDOS 集群中的 Service 数据和 etcd 的域名数据彻底匹配起来。

另外,Service to DNS 支持多点部署的特性,因此有可能同时多个 Service to DNS 服务监听到 JDOS 集群数据的变化,从而引发了同时操做 etcd 的问题。这样不利于数据的一致性,同时对相同的数据,屡次操做 etcd,会屡次触发 etcd 的变动通知,从而使得 DNS Server 监听到一些无心义的变动。为此 etcd 的读写接口采用了 Golang 的 Context 库管理上下文,能够有效地实现多个任务对 etcd 的同步操做。好比插入一条数据,会首先判断数据是否存在,对于已经存在的数据,插入操做失败。同时支持对过个数据的插入操做,其中有一个失败,本次操做失败。配置文件以下:

其中 etcd-Server 为 etcd 集群信息,这个要与 DNS Server 的配置文件要一致。Host 字段用于区别 Service to DNS 的运行环境的地址,此数据会写到 etcd 数据库中,能够很方便看到系统运行了多少个冗余服务。IP-monitor-path 写入原始的 IP 数据供探活模块使用。JDOS-domain 域名信息,这个要和 DNS Server 保持一致,同时要和 JDOS 启动的 –cluster-domain 选项保持一致,数据才能被 Docker 容器正常的访问。JDOS-config-file 文件是 JDOS-API 的访问配置信息,包括认证信息等。

(3)User API  

User API 提供 restful API,用户能够配置本身域名信息。用户能够对本身的域名信息进行增、删、改、查。数据结果会同步到 etcd 数据库中,DNS Server 会经过监听 etcd 的变化将用户的域名信息及时同步到 DNS Server 的缓存中。从而使得用户域名数据被查询。简单的配置以下:

 

User API

API-domains 支持多个域名后缀的操做,API-auth 用于 API 认证信息。其余信息 IP-monitor-path 等和 Service to DNS 模块的功能相同。具体的 API 的使用见

(4)IP status check  

IP status check 组件对域名的 IP 进行探活,包括 DNS-scheduler 和 DNS-scanner 两个模块。DNS-scheduler 模块监控 Service to DNS 和 uer API 组件输入的域名 IP 的信息,并将相关的 IP 探活合理地分配给不用的 DNS-scanner 任务;DNS-scanner 模块负责对 IP 的具体的周期探活工做,并将实际的结果写到指定的 etcd 数据库指定的目录。DNS Server 组件会监听 etcd IP 状态的结果,并将结果及时同步到本身的缓存中。

3、功能验证

Docker 容器中验证

Docker

服务器验证:typeA

SRV 格式:

API 验证:

API

IP status check  验证:

能够当 192.168.10.1 的状态变成 DOWN 后,查询 DNS Server,192.168.10.1 的地址不会再出如今返回结果中。

性能优化ContainerDNS 的组件的交互依赖于 etcd,etcd 是由 Go 语言开发了。ContainerDNS 也采用 Go 语言。

测试环境:CPU: Intel(R) Xeon(R) CPU E5-2640 v3 @ 2.60GHzNIC:  Intel Corporation 82599ES 10-Gigabit SFI/SFP+ Network Connection (rev 01)测试工具:queryperf域名数据:1000W 条域名记录

性能数据:

数据

从上面三个表中能够清晰地看出,走 etcd 查询速度最慢,走缓存查询速度提高不少。一样,不存在缓存老化。因此程序优化的第一步,就是采用了全缓存,不老化的实现机制。就是说 DNS Server 启动的时候,将 etcd 中的数据全量读取到内存中,后期 watch 到 etcd 数据的变动,实时更新内存中的数据。全缓存一个最大的挑战就是 etcd 的数据要和缓存中的数据的一致性。为此代码中增长了不少对域名变动时,对缓存的处理流程。同时为了防止有 watch 不到的变动(一周稳定性测试 10 亿次变动,出现过一次异常),增长了周期性全量同步数据的过程,这个同步粒度很细,是基于域名的,程序中会记录每次域名变动的时间,若是发现同步的过程当中这个域名的数据发生变化,这个域名本次不会同步,从而保证了缓存数据的实时性,不会由于同步致使新的变动丢失。

同时咱们采集了每一秒的响应状况,发现抖动很大。并且全缓存状况下 queryperf 测试虽然平均能达到 10W TPS,可是抖动从 2W-14W 区间较大。

queryperf

经过实验测试进程 CPU 损耗,咱们发现 golang GC 对 CPU 的占用很大。

 CPU

同时咱们采集了 10 分钟内存的状况,以下

能够发现,系统动态申请了好多内存大概 200 多个 G,而 golang GC 会动态回收内存。

gc 18 @460.002s 0%: 0.030+44+0.21 ms clock, 0.97+1.8/307/503+6.9 ms cpu, 477->482->260 MB, 489 MB goal, 32 P

gc 19 @462.801s 0%: 0.046+50+0.19 ms clock, 1.4+25/352/471+6.3 ms cpu, 508->512->275 MB, 521 MB goal, 32 P

gc 20 @465.164s 0%: 0.067+50+0.41 ms clock, 2.1+64/351/539+13 ms cpu, 536->541->287 MB, 550 MB goal, 32 P

gc 21 @467.624s 0%: 0.10+54+0.20 ms clock, 3.2+65/388/568+6.2 ms cpu, 560->566->302 MB, 574 MB goal, 32 P

gc 22 @470.277s 0%: 0.050+57+0.23 ms clock, 1.6+73/401/633+7.3 ms cpu, 590->596->313 MB, 605 MB goal, 32 P

因为 golang GC 会 STW(Stop The World),致使 GC 处理的时候有一段时间全部的协程中止响应。这也会引发程序的抖动。高级语言都带有 GC 功能,只要是有内存的动态使用,最终会触发 GC,而咱们能够作的事是想办法减小内存的动态申请。为此基于 pprof 工具采集的内存使用的结果,将一些占用大的固定 size 的内存放入缓存队列中,申请内存首先从缓存重申请,若是缓存中没有才动态申请内存,当这块内存使用完后,主动放在缓存中,这样后续的申请就能够从缓存中取得。从而大大减小对内存动态申请的需求。因为各个协程均可能会操做这个数据缓存,从而这个缓存队列的设计就要求其安全和高效。为此咱们实现了一个无锁队列的设计,下面是入队的代码片断。

目前对 512 字节的 msg 数据结构作了缓存。用 pprof 采集内存使用状况以下:

能够看到内存由原来的 200G 减小到 120G,动态申请内存的数量大大减少。

同时性能也有所提高:

性能

10 分钟内的采集结果能够看出,抖动从原来的 2-10W 变成如今的 10-16W,抖动相对变小。同时 queryperf 测试每秒大概 14W TPS,比原来提升了 4W。

写在最后  

本文主要介绍了 ContainerDNS 在实际环境中的实践、应用和一些设计的思路。所有的代码已经开源在 GitHub 上(详见 https://github.com/ipdcode/skydns )。咱们也正在作一些后续的优化和持续的改进。

相关文章
相关标签/搜索