- N +

京东618:商城分布式智能容器DNS实践

  欧洲通用数据保护条例(PR)即将开始实施,PR重点强调风险意识,我们需要做好隐私风险控制工作,这将会在很大程度上影响软件开发者。

  在几年前,组合使用Akka和Java 8来创建基于微服务的分布式系统还是一个美梦。基于Actor的系统能够让开发人员创建快速演化的微服务架构,这种架构能够弹性扩展以应对大量的数据。

  在microXchg 2017大会上,Uwe Friedrichsen发表演讲,讨论了有弹性的功能服务设计的核心概念以及如何创建可观测的系统。Friedrichsen认为,微服务开发者必须:了解容错设计模式和缓存;理解领域驱动设计和模块化;将组件的可替代性作为设计目标而不是复用性。

  亲爱的读者:我们最近添加了一些个人消息定制功能,您只需选择感兴趣的技术主题,即可获取重要资讯的邮件和网页通知。

  InfoQ报道京东大促技术已有三年的时间,这三年的陪伴见证了京东技术的成长。2015年京东开启容器技术,拥有数千实例;2016年,该数字上升至15万;2017年,容器实例规模继续稳步上升并开源数个项目。

  ArchSummit全球架构师峰会深圳站将于2017年7月7日~8日在深圳华侨城洲际酒店召开,大会设置了相关专题来深入解读电商大促背后的技术故事,大会还邀请了eBay、WalmartLabs等国外顶尖技术专家,分享AI促销、搜索引擎、异地多活、库存物流等核心架构实践。

  随着京东业务的高速增长,以及JDOS2.0的线上大规模运营,进而容器集群的编排成为常态,Pod失效也成为常态,RS(Replication Set)在处理失效Pod时候会带来IP的变化。这样容器之间基于IP相互访问就有可能存在问题。所以一个强大的能支持百万级hostname域名解析服务,可以很好地解决这个问题。

  本文介绍的DNS命名为ContainerDNS,作为京东商城软件定义数据中心的关键基础服务之一,具有以下特点:

  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地址自动过滤掉。

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

  如上图所示,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 容器的f 中。

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

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

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

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

  Service to DNS需要监控JDOS集群的Service的变化,以这种格式生成相关的域名。由于系统对用户创建的服务会自动的创建load-balance的服务,所以域名的IP对应的是这个服务关联的lb的IP,而lb的后端才是对应着的是真正提供服务的POD。

  增量同步调用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的访问配置信息,包括认证信息等。

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

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

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

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

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

  本文主要介绍了ContainerDNS在实际环境中的实践、应用和一些设计的思路。全部的代码已经开源在GitHub上(详见)。我们也正在做一些后续的优化和持续的改进。

  鲍永成,京东商城 基础平台部技术总监。2013年加入京东,负责京东容器集群平台(JDOS)研发,带领团队完成京东容器大规模落地战略项目,有效承载京东全部业务系统和80%数据库,特别在大促期间 scale up 秒级弹性应对高峰流量。目前聚焦在京东容器集群 JDOS 2.0 以及京东敏捷智能数据中心研发。服务过土豆网(,思科(CRDC)等,在分布式、虚拟化、容器、数据中心建设有丰富的实践经验。

  陈书刚,京东商城基础平台部软件工程师,有着多年从事数通产品的开发、协议报文解析的工作的经验,目前主要从事基础网络功能的开发与维护。

  我们理解您使用ad blocker的初衷,但为了保证InfoQ能够继续以免费方式为您服务,我们需要您的支持。InfoQ绝不会在未经您许可的情况下将您的数据提供给第三方。我们仅将其用于向读者发送相关广告内容。请您将InfoQ添加至白名单,感谢您的理解与支持。

返回列表
上一篇:
下一篇:
评论列表 (暂无评论,共535人参与)

还没有评论,来说两句吧...

发表评论

验证码