《京東618如何支持容器百萬級域名解析服務?》要點:
本文介紹了京東618如何支持容器百萬級域名解析服務?,希望對您有用。如果有疑問,可以聯系我們。
作者:鮑永成、陳書剛
編輯:木環
InfoQ 報道京東大促技術已有三年的時間,這三年的陪伴見證了京東技術的成長.2015 年京東開啟容器技術,擁有數千實例;2016 年,該數字上升至 15 萬;2017 年,容器實例規模繼續穩步上升并開源數個項目.
隨著京東業務的高速增長,以及 JDOS2.0 的線上大規模運營,進而容器集群的編排成為常態,Pod 失效也成為常態,RS(Replication Set)在處理失效 Pod 時候會帶來 IP 的變化.這樣容器之間基于 IP 相互訪問就有可能存在問題.所以一個強大的能支持百萬級 hostname 域名解析服務,可以很好地解決這個問題.
本文介紹的 DNS 命名為 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 地址自動過濾掉.
DNS Server 是提供 DNS 的主體模塊,系統中是掛載在項目 ContainerLB(一種基于 DPDK 平臺實現的快速可靠的軟件網絡負載均衡系統)之后,通過 VIP 對外提供服務.結構如下:
圖二 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 首先根據用戶的配置,鏈接 etcd 數據庫,并讀取對應的域名信息放在程序的緩存中.然后啟動 watch 監聽 etcd 的變化,同步數據庫與緩存中的數據.新的 DNS 請求不用在查詢 etcd 數據庫直接使用緩存中的數據,從而提高響應的速度.啟動后監聽用戶配置的端口(默認 53 號),對收到的數據包進行處理.同時查出過得結果會緩存的 DNS-Server 的內存緩存中,對于緩存的數據不老化刪除,就是說查詢過的域名會一直在緩存中以提高查詢的速度,從而達到很高的響應性能.如果域名信息發生變化,DNS Server 通過監聽 etcd 隨時感知這種變化,從而更新緩存中的數據,從而提供很好的實時性.測試發現,從發生變化到能查出變更預期的結果一般在 20ms 以內,壞的情況不超過 50-60ms.
上圖是 DNS Server 響應一次查詢的過程.首先根據域名和查詢的類型生成一個數據緩存的索引,然后查詢 DNS 數據緩存如果命中,簡單處理返回給用戶.沒有命中從數據庫查詢結果,并將返回的結果插入到數據緩存中,下次查詢直接從緩存中取得,提高響應速度.為了進一步提高性能,緩存的數據不會老化刪除,只有到了緩存的數量限制才會隨機刪除一些釋放空間.不刪除緩存,緩存中的數據和實際的域名數據的一致性就是一個關鍵的問題.我們采用 etcd 監控功能實時抓取變更,從而更新緩存的數據,經過幾個星期的不停地循環,增、刪、改、查域名,近 10 億次測試,未出現數據不一致的情況.下面是 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 統計信息,方便做集群的數據統計.效果如下:
這個組件的主要功能是通過 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 的訪問配置信息,包括認證信息等.
User API 提供 restful API,用戶可以配置自己域名信息.用戶可以對自己的域名信息進行增、刪、改、查.數據結果會同步到 etcd 數據庫中,DNS Server 會通過監聽 etcd 的變化將用戶的域名信息及時同步到 DNS Server 的緩存中.從而使得用戶域名數據被查詢.簡單的配置如下:
API-domains 支持多個域名后綴的操作,API-auth 用于 API 認證信息.其他信息 IP-monitor-path 等和 Service to DNS 模塊的功能相同.具體的 API 的使用見
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 狀態的結果,并將結果及時同步到自己的緩存中.
Docker 容器中驗證
服務器驗證:typeA
SRV 格式:
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 區間較大.
通過實驗測試進程 CPU 損耗,我們發現 golang GC 對 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 ).我們也正在做一些后續的優化和持續的改進.
陳書剛,京東商城基礎平臺部軟件工程師,有著多年從事數通產品的開發、協議報文解析的工作的經驗,目前主要從事基礎網絡功能的開發與維護.
文章來自微信公眾號:高效運維開發