轉自社區git
ContainerDNSgithub
本文介紹的 DNS 命名爲 ContainerDNS,做爲京東商城軟件定義數據中心的關鍵基礎服務之一,具備如下特色:golang
圖一 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 的啓動過程restful
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 )。咱們也正在作一些後續的優化和持續的改進。