京東618如何支援容器百萬級域名解析服務?
作者:鮑永成、陳書剛
編輯:木環
InfoQ 報道京東大促技術已有三年的時間,這三年的陪伴見證了京東技術的成長。2015 年京東開啟容器技術,擁有數千例項;2016 年,該數字上升至 15 萬;2017 年,容器例項規模繼續穩步上升並開源數個專案。
隨著京東業務的高速增長,以及 JDOS2.0 的線上大規模運營,進而容器叢集的編排成為常態,Pod 失效也成為常態,RS(Replication Set)在處理失效 Pod 時候會帶來 IP 的變化。這樣容器之間基於 IP 相互訪問就有可能存在問題。所以一個強大的能支援百萬級 hostname 域名解析服務,可以很好地解決這個問題。
一、系統概述
本文介紹的 DNS 命名為 ContainerDNS,作為京東商城軟體定義資料中心的關鍵基礎服務之一,具有以下特點:
- 分散式,高可用
- 自動發現服務域名
- 後端探活
- 易於維護、易於動態擴充套件llllllll,* 容器化部署
圖一 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 地址自動過濾掉。
二、系統設計與實現
(1)DNS Server
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 統計資訊,方便做叢集的資料統計。效果如下:
(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 的快取中。從而使得使用者域名資料被查詢。簡單的配置如下:
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 狀態的結果,並將結果及時同步到自己的快取中。
三、功能驗證
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 )。我們也正在做一些後續的優化和持續的改進。
作者介紹
陳書剛,京東商城基礎平臺部軟體工程師,有著多年從事數通產品的開發、協議報文解析的工作的經驗,目前主要從事基礎網路功能的開發與維護。
文章來自微信公眾號:高效運維開發