本文導讀:html
閱讀本文建議先了解:git
咱們的微服務目前都是在服務器上部署的,也是基於 Docker 來部署的。github
運維部門基於 K8s 自研了一套容器雲管理平臺,平臺名稱叫作 Ares,咱們也開始準備將微服務遷移到這平臺上,下降虛擬機或實體機服務器運維成本,提升服務器資源利用效率。express
Ares:阿瑞斯(戰神)後端
希臘神話中爲戰爭而生的神,奧林匹斯十二神之一,被視爲尚武精神的化身。看起來很牛逼的樣子!bash
微服務框架使用了流行的 Spring Cloud 框架。服務器
框架技術組件以下:網絡
網關 Zuul 層使用了 Ribbon 作負載均衡、Hystrix 作限流熔斷。後端微服務使用了阿里巴巴開源的 Sentinel 作限流熔斷。數據結構
因爲當時服務器的配置不一樣,好比有低配置的虛擬機,還有高配置的物理機服務器。架構
因此呢,咱們基於當時的服務器配置現狀,基於 Ribbon 自行擴展了按照權重的負載均衡策略,對 Eureka 註冊中心管理界面作了一點改造,可以支持動態對每臺服務器變動權重。
由於本文的問題跟 Eureka 註冊中心有關,對 Eureka 架構作個介紹下。
Eureka 註冊中心簡易架構圖:
上圖簡要描述了 Eureka 的基本架構,由3個角色組成:
1)Eureka Server
提供服務註冊、發現、健康檢查。
2)Service Provider
服務提供方,將自身服務註冊到 Eureka,從而使服務消費方可以找到,咱們將容器能夠做爲服務提供者,會註冊到 Eureka。
3)Service Consumer
服務消費方,從Eureka獲取註冊服務列表,從而可以消費服務咱們能夠將 Zuul 網關做爲服務消費者。
考慮到使用的 Spring Cloud 框架,結合運維提供的容器平臺。
制定容器化部署架構以下:
從容器建立到可訪問流程:
1)建立容器
選擇鏡像及版本、CPU、內存配置、配置健康檢查、日誌收集、Pod 副本數量。提交建立容器。
2)服務註冊
容器啓動時,申請 SLB VIP,做爲服務註冊 IP,向 Eureka 上發起註冊。
3)網關發起請求
域名請求,DNS 解析通過 GSLB(全局軟負載均衡)負載到 Zuul 網關,Zuul 網關從 Eureka 註冊中心拉取服務註冊表,經過 Ribbon 負載均衡,從本地服務註冊列表中,選擇其中一臺 Server,發起 Http 調用。
4)容器提供服務
容器內註冊的是 SLB VIP(軟負載均衡),這個 SLB 經過內部的 Nginx 負載均衡機制,輪詢到後端的容器的多個 Pod IP 上,Pod IP 正是咱們部署的微服務業務。
爲何要使用SLB VIP呢?
當時咱們對接口壓測時,發現使用 K8s 內部的 Service IP 存在性能瓶頸,該問題還在研究中。後來運維內部商榷,使用 SLB 來達到負載均衡的效果。
另外說明一點:運維基於 K8s 自研的這套容器平臺,網絡層面作了從新架設和優化,打通了各個機房的網絡。
這樣作給咱們的架構部署帶來了好處:前期目標僅爲了遷移微服務業務,考慮到穩定性等因素,正式上線的Zuul網關和Eureka 註冊中心部署在 K8s 集羣外,微服務業務部署在容器內,因網絡可通,容器啓動後申請的 VIP,能夠直接註冊到 Eureka 上。
仿真環境(預上線環境)是直接將Eureka註冊中心,也部署在了容器平臺中,接下來會說下,所以致使的一些問題,以及解決該問題的方式。
容器測試階段結束,因爲運維調整爲了 SLB VIP,將之前的應用(一個應用下包含多個 Pod 容器)都刪除掉,咱們從新搭建一套仿真環境用於,上線前的性能測試環境。
可是當咱們部署完 Eureka 後,發現之前刪除掉的應用VIP 也註冊上來了,並且這個 VIP 網絡是不通的,沒法訪問的。
Eureka 管理控制檯示意圖:
telnet 命令測試:
telnet 10.11.195.197 80
Trying 10.11.195.197...
telnet: connect to address 10.11.195.197: Network is unreachable
telnet: Unable to connect to remote host複製代碼
結果提示 10.11.195.197 這個 VIP,網絡是不可達的。
那爲何這個服務能註冊上來呢?
起初,跟運維老哥請教,通過容器內排查後,也暫時沒有太多眉目,肯定是這個 VIP,已經下線了,網絡也不通。
按照這個推測,是不太可能註冊到 Eureka 上來的。
開始考慮到覺得是 Eureka 機制是否是有問題,但仔細用「屁股」猜測論思考一下,結合 Eureka 框架底層原理來看,是不該該出現這個狀況。
根據 Eureka 續約機制,必定是有哪一個「哥們」在默默給這個服務 IP 發續約(向註冊中心發送心跳
)。
咱們在 Eureka Server 服務端,也有監聽各個動做的機制,如註冊服務、續約服務、下線服務,根據日誌看,也的確是有這個服務 IP 一直在發送續約動做。
續約監聽代碼:
@EventListener
public void listen(EurekaInstanceRenewedEvent event) {
InstanceInfo instanceInfo = event.getInstanceInfo();
if (instanceInfo != null) {
logger.info("renew ...." + instanceInfo.getInstanceId());
} else {
logger.info("renew ....instanceInfo is null");
}
}複製代碼
既然引出了上述問題,固然不能聽任無論,必定要一探究竟。這種問題你若不理他,遲早會搞出點別的事情來的。
Eureka 服務端已經收到了註冊和一直續約的請求,說明必定是有哪一個服務一直在偷偷發送心跳。
究竟是誰幹的啊?
到底如何找到這個上報的服務呢?
運維老哥暫時比較忙,看來只能先查找網絡鏈路,抓取網絡數據包看看究竟是怎麼回事了。
網絡工具通常經常使用的就是 tcpdump、Wireshark。
Wireshark 小故事:
大概發生在 10 幾年前,主導 Ethereal(應該據說過吧)的大佬跳槽了,而後這個商標就不能繼續使用了,可是這個工具在當時來講人氣很旺,後來大佬就將項目改名爲 Wireshark 了。
服務器上命令行的抓包程序 tethereal 改名爲了 tshark。
容器鏡像中默認是不會自帶這些工具的。鏡像中 Linux 操做系統使用的是 CentOS,經過自帶的 yum 源安裝網絡工具包,比較方便。
安裝 wireshark:
`yum install -y wireshark`
安裝 tcpdump:
`yum install -y tcpdump`
這裏咱們使用的是 Wireshark 工具,簡單介紹下這個工具:
若是你要看到所有網絡數據包,直接執行tshark命令便可。
1)得到 tshark 命令幫助
`tshark --help`
2)tshark 抓包模式參數一覽
3)tshark 命令實戰使用
打印源目標 Host 及 Http 協議信息:
`tshark -s 512 -i eth0 -n -f 'tcp dst port 80' -t ad -R 'http.host and http.request.uri' -T fields -e "frame.time" -e "ip.src" -e "http.host" -e "http.request.method" -e "http.request.uri" | tr -d ' '`
參數解釋:
-i 捕獲 eth0 網卡;
-n 禁止全部地址名字解析(默認爲容許全部)
-t 設置解碼結果的時間格式。
"ad"表示帶日期的絕對時間,"a"表示不帶日期的絕對時間,"r"表示從第一個包到如今的相對時間,「d」表示兩個相鄰包之間的增量時間(delta)
-R 設置讀取(顯示)過濾表達式(read filter expression)
-T -e 輸出指定的字段
執行結果:
來段文本:
[root@mas-manager-eureka-es1-66cb79bfb7-snmxm manager]# tshark -n -t a -R http.request -T fields -e "frame.time" -e "ip.src" -e "http.host" -e "http.request.method" -e "http.request.uri" | grep 10.11
Running as user "root" and group "root". This could be dangerous.
Capturing on eth0
Sep 27, 2019 00:22:05.174770971 10.124.12.169 10.124.14.4 PUT /eureka/apps/LETV-MAS-CALLER-TVPROXY-USER/10.11.195.197:80?status=UP&lastDirtyTimestamp=1569490783397
Sep 27, 2019 00:22:13.814821143 10.124.11.125 10.124.14.4 PUT /eureka/apps/LETV-MAS-CALLER-TVPROXY-USER/10.11.195.197:80?status=UP&lastDirtyTimestamp=1569407741389
Sep 27, 2019 00:22:15.180243816 10.124.11.123 10.124.14.4 PUT /eureka/apps/LETV-MAS-CALLER-TVPROXY-USER/10.11.195.197:80?status=UP&lastDirtyTimestamp=1569490783397複製代碼
經過抓包,根據問題 IP 過濾獲得的結果,咱們看到了/eureka/apps/LETV-MAS-CALLER-TVPROXY-USER/10.11.195.197:80?status=UP&lastDirtyTimestamp=1569490783397
說明是有 IP 在上報,上報來源 IP 就是第二列的 IP。
將這個信息提供給運維同窗,根據來源 IP 去繼續查找線索。
可是,發現第二列 IP 並非實際的服務器節點 IP,查不到。由於這些 IP 都是主機上的虛擬 IP,每次上報來源 IP 不一樣,因此還要反向查找實際歸屬的主機節點。
爲何要有虛擬 IP?
實際是 SLB 節點上虛擬 IP,由於會負載到它所屬主機節點,這臺主機上默認只能支撐最大 65535 個 TCP 鏈接,因此爲了單機能支撐更高的 TCP 鏈接數,會虛擬出來不少個 IP。假設有 10 個虛擬 IP,每一個虛擬 IP 支撐 65535 個 TCP 鏈接,這臺主機總共能夠支撐 10 * 65535 = 60萬以上的鏈接數了。
K8s 有多套集羣,每一個集羣中有不少臺主機節點,茫茫主機池中怎麼去查找這些虛擬 IP 呢,
其實咱們的目的是爲了找到,誰往註冊中心發送請求了,仍是能夠繼續經過抓取網絡數據包來定位這個問題。
這些網絡數據包必定會通過 K8s 集羣裏的某一臺節點,一臺一臺去找,很麻煩,編寫簡單腳本抓包查找
:
#!/bin/bash
tcpdump -i any host 10.124.14.4 -n -s 0 -X -l | grep 10.11.195>/tmp/1.txt &
sleep 20s
kill -2 %1
cat /tmp/1.txt複製代碼
網絡抓包結果:
截取了關鍵的抓包信息:
11:41:56.598204 IP 10.110.157.81.54078 > 10.124.14.4.http: Flags [P.], seq 273:622, ack 218, win 245, options [nop,nop,TS val 3348483954 ecr 1420800289], length 349: HTTP: PUT /eureka/apps/LETV-MAS-CALLER-TVPROXY-USER/10.11.195.197:80?status=UP&lastDirtyTimestamp=1569394834392 HTTP/1.1複製代碼
這裏的 10.110.157.81.54078
就是主機節點 IP,目標地址 10.124.14.4
就是容器內的 Eureka 註冊中心地址。
發送的請求是 /eureka/apps/LETV-MAS-CALLER-TVPROXY-USER/10.11.195.197:80?status=UP&lastDirtyTimestamp=1569394834392
這個請求地址上就帶了 10.11.195.197 這個網絡不可達的 IP 地址。
到這裏,其實咱們已經基本定位到了,必定是從 K8s 集羣容器內發出來了。根據這個有價值的節點信息,鏈接到 K8s 集羣內,查找該節點上部署的容器。
查找 K8s 集羣內 Pod 命令行:`kubectl get pod --all-namespaces -o wide |grep 10.110.157.81`
部署在改節點上的容器:
運維根據 Eureka 上名稱大概猜想一下,終於找到這個「罪魁禍首」的容器了。進入容器內,查看配置 SVIP (Eureka上的註冊IP)就是 10.11.195.197 這個IP。
將這個問題容器完全關閉後,沒有再繼續發送續約請求,Eureka 註冊中心上過了一段時間摘掉了 IP。
你們可能有疑問,這麼繁瑣,爲啥不直接到 K8s 集羣內去找,由於 K8s 集羣內目前已有業務在運行着,集羣內有幾百個容器在跑着。當時運維一塊兒測試時,容器名稱都是自定義的,因此不是很好查找。
我們通過這個過程的排查,確認了這個 Eureka 註冊中心上的地址雖然不通,可是一直是有容器在上報,而上報的「ServerId」指向的 10.11.195.197:80 地址。
咱們也能夠結合底層源碼瞭解下。
Eureka 續約時序圖:
接口實現方式跟註冊服務相似,更新自身狀態後,會同步到其餘集羣節點。
PeerAwareInstanceRegistryImpl 類的 renew 方法會調用到 AbstractInstaceRegistry 抽象實例註冊類的 renew 方法。
AbstractInstaceRegistry#renew 方法源碼:
會根據服務 id 從註冊表中獲取 Lease 對象,若是不爲空,則完成續約,更新 lastUpdateTimestamp 字段。
Eureka 註冊表的數據結構:
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
= new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();複製代碼
是一個 ConcurrentHashMap 結構,Key 就是應用名稱,Value 也是一個 Map 結構。Map 結構中的 Key 是註冊ID(IP + 端口),Value 是 Lease 服務續約對象,裏面包含了動做類型,最後上報(心跳)更新時間戳等等信息。
容器服務做爲 Eureka Client,每隔必定時間間隔(默認60秒)向註冊中心發起一次續約。
Eureka Server 會定時檢測服務實例心跳是否正常,若是間隔必定時間(90秒),尚未來續約,就會將這個服務從註冊中心摘除掉。
最後總結:
總結上述分析過程,一圖勝千言:
重要的不是結果,而是這個過程,但願你也能享受這個過程。
參考資料:
Wireshark 使用文檔:https://www.wireshark.org/docs/man-pages/tshark.html
Netflix Eureka 源代碼:https://github.com/netflix/eureka
歡迎關注個人公衆號,掃二維碼關注得到更多精彩文章,與你一同成長~