通常情況下,直接訪問 Pod 會有如下幾個問題:
- Pod 會隨時被 Deployment 這樣的控制器刪除重建,那訪問 Pod 的結(jié)果就會變得不可預(yù)知。
- Pod 的 IP 地址是在 Pod 啟動后才被分配,在啟動前并不知道 Pod 的 IP 地址。
- 應(yīng)用往往都是由多個運(yùn)行相同鏡像的一組 Pod 組成,一個個 Pod 的訪問也變得不現(xiàn)實(shí)。
Kubernetes 中的 Service 對象就是用來解決上述 Pod 訪問問題的。Service 有一個固定 IP 地址,這個地址在集群內(nèi)部是固定不變的,不同集群中會不一樣,它將訪問該地址的流量轉(zhuǎn)發(fā)給 Pod,具體轉(zhuǎn)發(fā)給哪些 Pod 可以通過 label selectors 確定,而且 Service 可以給這些 Pod 做負(fù)載均衡。
Service 是由 kube-proxy 組件加上 iptables 來共同實(shí)現(xiàn)的(還有一種是 IPVS 模式實(shí)現(xiàn)),在 Kubernetes 集群中,每個 Node 運(yùn)行一個 kube-proxy 進(jìn)程,負(fù)責(zé)維護(hù)節(jié)點(diǎn)上的網(wǎng)絡(luò)規(guī)則,這些網(wǎng)絡(luò)規(guī)則允許從集群內(nèi)部或外部與 Pod 進(jìn)行網(wǎng)絡(luò)通信。
如上圖所示,當(dāng) Kubernetes 監(jiān)測到 Service、Pod 對象的創(chuàng)建和銷毀時,kube-proxy 會配置相應(yīng)的 iptables 規(guī)則,以攔截到達(dá)該 Service 的 IP 和 Port 的請求,并且會將請求重定向到它所代理的 Endpoint。
下面是一個 Service(NodePort) 和它所代理的 Endpoint 的 iptables 信息:

- 是 Service 通過 NodePort 方式向集群外部暴露訪問入口。
- 是從集群內(nèi)訪問服務(wù) Service,10.98.76.27 這個 IP 只是一條 iptables 規(guī)則上的配置,并沒有真正的網(wǎng)絡(luò) 設(shè)備,所以 ping 這個地址,是不會有任何響應(yīng)的。
- 是一組隨機(jī)模式下(–mode random)的 iptables 規(guī)則鏈,也就是 Service 實(shí)現(xiàn)負(fù)載均衡的位置。
- 是這個 Service 代理的三個 Endpoint,通過 DNAT 做目標(biāo)地址轉(zhuǎn)換,訪問具體的 Pod 服務(wù)。
下面進(jìn)入正題,看下 Kubernetes 支持的兩種基本的服務(wù)發(fā)現(xiàn)模式 —— 環(huán)境變量 和 DNS。
- 環(huán)境變量
在每個 Pod 啟動的時候,Kubernetes 會把當(dāng)前命名空間下面的所有 Service 的 IP 和端口通過環(huán)境變量的形式注入到 Pod 中去,這樣 Pod 中的應(yīng)用可以通過讀取環(huán)境變量,來獲取依賴服務(wù)的地址信息。
這種方法使用起來相對簡單,但是有一個很大的問題,就是依賴的服務(wù)必須在 Pod 啟動之前就存在,不然是不會被注入到環(huán)境變量中的。
下圖展示的就是一個 Pod 中的環(huán)境變量,它記錄了其它 Service 服務(wù)的 IP、Port。
/ # env | sort
......
ORDER_SERVER_PORT=tcp://10.103.88.0:9099
ORDER_SERVER_PORT_9099_TCP=tcp://10.103.88.0:9099
ORDER_SERVER_PORT_9099_TCP_ADDR=10.103.88.0
ORDER_SERVER_PORT_9099_TCP_PORT=9099
ORDER_SERVER_PORT_9099_TCP_PROTO=tcp
ORDER_SERVER_SERVICE_HOST=10.103.88.0
ORDER_SERVER_SERVICE_PORT=9099
ORDER_SERVER_SERVICE_PORT_TCP=9099
......
USER_SERVER_PORT=tcp://10.110.228.7:9097
USER_SERVER_PORT_9097_TCP=tcp://10.110.228.7:9097
USER_SERVER_PORT_9097_TCP_ADDR=10.110.228.7
USER_SERVER_PORT_9097_TCP_PORT=9097
USER_SERVER_PORT_9097_TCP_PROTO=tcp
USER_SERVER_SERVICE_HOST=10.110.228.7
USER_SERVER_SERVICE_PORT=9097
USER_SERVER_SERVICE_PORT_TCP=9097
......
- DNS
這種方式我們不需要去關(guān)心分配的 ClusterIP 的地址,因?yàn)?VIP 地址并不是固定不變的,雖然在一個集群中是不變的,但是我們?nèi)绻嬖?DEV、TEST、UAT 等環(huán)境,是不能保證在每個集群中的 IP 是相同的。
如果能夠直接使用 Service 的名稱(Service 的名稱一般使用微服務(wù)名稱,而且一旦使用基本上不會變化),其對應(yīng)的 ClusterIP 地址的解析能夠自動完成就好了。
KubeDNS 和 CoreDNS 是兩個已建立的 DNS 解決方案,用于定義 DNS 命名規(guī)則,并將 Pod 和 Service 的 DNS 解析到它們相應(yīng)的集群 IP。
通過 CoreDNS,Kubernetes 服務(wù)之間可以僅僅通過服務(wù)名稱來相互訪問。
Kubernetes 使用 DNS 作為服務(wù)注冊表,每個 Service 都會自動注冊到集群 DNS 之中,要使用服務(wù)發(fā)現(xiàn)功能,每個 Pod 都需要知道集群 DNS 的位置才能使用它。因此每個 Pod 中的 /etc/resolv.conf 文件都被配置為使用集群 DNS 進(jìn)行解析,這也是默認(rèn)的 DNS 策略:ClusterFirst。
集群中 kubelet 的啟動參數(shù)有 --cluster-dns=<dns-service-ip style="margin-bottom: 0px; margin-top: 0px;"> 和 --cluster-domain=<default-local-domain style="margin-bottom: 0px; margin-top: 0px;"> ,這兩個參數(shù)分別被用來設(shè)置集群 DNS 服務(wù)器的 IP 地址和主域名后綴。</default-local-domain></dns-service-ip>
Pod 的 dnsPolicy 配置支持四種策略:
- ClusterFirst:通過 CoreDNS 來做域名解析,Pod 內(nèi) /etc/resolv.conf 配置的 DNS 服務(wù)地址是集群 DNS 服務(wù)的 kube-dns 地址。該策略是集群工作負(fù)載的默認(rèn)策略。
- None:忽略集群 DNS 策略,需要提供 dnsConfig 字段來指定 DNS 配置信息。
- Default:Pod 直接繼承集群節(jié)點(diǎn)的域名解析配置。使用宿主機(jī)的 /etc/resolv.conf 文件。
- ClusterFirstWithHostNetwork:強(qiáng)制在 hostNetWork 網(wǎng)絡(luò)模式下使用 ClusterFirst 策略(默認(rèn)使用 Default 策略)。
我的演示環(huán)境是用一鍵安裝利器 Kubeadm 安裝的,它會默認(rèn)安裝 CoreDNS 插件。

下面我們進(jìn)入到一個 Pod 中查看下它的 dns 配置文件:
/ # cat /etc/resolv.conf
nameserver 10.96.0.10
search cloud.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
可以看到 DNS 的服務(wù)器地址既是 coredns 的 svc ip,所有域名的解析,其實(shí)都要經(jīng)過 coredns 的虛擬 IP 10.96.0.10 進(jìn)行解析。
- cloud.svc.cluster.local
- svc.cluster.local
- cluster.local
域名的搜索域有如上三個,在解析的時候會依次帶入 /etc/resolve.conf 中的 search 域進(jìn)行 DNS 查找,所以集群中同一 namespace 中的 pod 之間相互訪問是可以直接使用 svc name,不同 namespace 下的服務(wù)訪問需要帶上 namespace。
例如訪問同一 namespace 下的服務(wù) user-server,只需要匹配一次 search 域就可以解析到正確的 IP 地址。
user-server.cloud.svc.cluster.local
下面通過 nslookup 驗(yàn)證下 DNS 查找是否將服務(wù)的 DNS 解析為正確的 IP(A 記錄),user-server 的 ClusterIP 就是 10.110.228.7,與 DNS 查找解析到的 IP 相同的。auth-server 與 user-server 在同一個 namespace:cloud 下,可直接通過服務(wù)名稱訪問。
/ # nslookup user-server
Name: user-server
Address 1: 10.110.228.7 user-server.cloud.svc.cluster.local
不同 namespace 下的服務(wù)訪問,則需要攜帶名稱空間,例如要訪問 istio-system 命名空間下的istio-egressgateway 服務(wù),就必須攜帶命名空間:
/ # nslookup istio-egressgateway
nslookup: can't resolve 'istio-egressgateway': Name does not resolve
/ # nslookup istio-egressgateway.istio-system
Name: istio-egressgateway.istio-system
Address 1: 10.100.190.21 istio-egressgateway.istio-system.svc.cluster.local
Kubernetes 通過其內(nèi)置的 DNS 附加組件實(shí)現(xiàn)了高效的服務(wù)發(fā)現(xiàn)?;?DNS 的服務(wù)發(fā)現(xiàn)功能非常強(qiáng)大,因?yàn)椴恍枰獙?IP 和端口等網(wǎng)絡(luò)參數(shù)硬編碼到應(yīng)用程序中。一旦 Service 管理了一組 pod,就可以使用服務(wù)的 DNS 輕松地訪問它們,并且通過 iptables 實(shí)現(xiàn)了 Pod 的 Load Balancer。
具體哪些 Pod 才可以成為 Service 的 Endpoint 接收流量,Kubernetes 給我們提供了三種探針,來實(shí)現(xiàn)對 Pod 的健康檢查,如下表:
| 探針 | 作用 | 詳細(xì)說明 |
|---|---|---|
| livenessProbe | 何時需要重啟容器 | Liveness 指針是存活探針,它用來判斷容器是否存活、判斷 pod 是否 running。如果 Liveness 指針判斷 容器不健康,此時會通過 kubelet 殺掉相應(yīng)的 pod,并根據(jù)重啟策略來判斷是否重啟這個容器。如果默認(rèn)不配置 Liveness 指針,則默認(rèn)情況下認(rèn)為它這個探測默認(rèn)返回是成功的。 |
| startupProbe | 啟動探針 | Kubelet 使用啟動探針來了解容器何時啟動。如果配置了這樣的探測,它將禁用存活探針和準(zhǔn)備狀態(tài)檢查,直到成功為止,確保這些探測不會干擾應(yīng)用程序啟動。這可以用于對緩慢啟動的容器進(jìn)行活性檢查,避免它們在啟動和運(yùn)行之前被 kubelet 殺死。 |
| readinessProbe | 何時可以開始接收流量 | Readiness 指針用來判斷這個容器是否啟動完成,即 pod 的 condition 是否 ready。如果探測的一個結(jié)果是不成功,那么此時它會從 pod 上 Endpoint 上移除,也就是說從 Service 接入層上面會把前一個 pod 進(jìn)行摘除,直到下一次判斷成功,這個 pod 才會再次掛到相應(yīng)的 endpoint 之上。 |
我們來看下在 Kubernetes 平臺中滾動更新服務(wù)時 Eureka 注冊中心存在的問題。
在我們微服務(wù)示例中,服務(wù)滾動更新的策略是,“一上一下,先上后下”的原則,具體參數(shù)配置如下,即 1 個新版本 pod ready(結(jié)合 readiness 探針)之后,才會去銷毀舊版本的 pod,這個保證了 Service 至少會存在一個 ready 狀態(tài)的站點(diǎn),是最平穩(wěn)的更新方式。
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
Kubernetes 提供了兩個參數(shù),來控制 rollingUpdate 滾動更新的速度:
- maxUnavailable:和期望 ready 的副本數(shù)比,不可用副本數(shù)最大比例(或最大值),這個值越小,越能保證服務(wù)穩(wěn)定,更新越平滑。
- maxSurge:和期望 ready 的副本數(shù)比,超過期望副本數(shù)最大比例(或最大值),這個值調(diào)的越大,副本更新速度越快。
如果由于一些特殊原因,你的容器不是以單進(jìn)程模式運(yùn)行的,也就是應(yīng)用進(jìn)程并不是容器內(nèi)的 1 號進(jìn)程,也沒有使用 Tini 作為 init 進(jìn)程,這就導(dǎo)致容器內(nèi)的應(yīng)用進(jìn)程接收的是 SIGKILL 信號,以非 graceful shutdown 的方式終止。
在滾動更新完成后,雖然這時舊版本的 Pod 已經(jīng)被從 Service 的 Endpoint 站點(diǎn)中移除,但是這種服務(wù)停止的方式不會調(diào)用 Eureka-Server 的 API 主動下線服務(wù),而是只能等默認(rèn)定時 60s 去清理 90s 內(nèi)沒有續(xù)約的服務(wù)。
在這期間 Eureka Server 中會有兩個 instance 在線,盡管其中舊版本的 Pod 已經(jīng)被銷毀了。

這個銷毀的舊版本的 Pod 最大可能會在注冊列表中存在 180s,如果再加上三級緩存同步周期 30s,消費(fèi)端拉取服務(wù)列表的周期 30s,以及 Ribbon 的緩存周期 30s,消費(fèi)端拉取的服務(wù)列表對 Pod 銷毀下線的感知就會有 4 分鐘左右的延遲,這 4 分鐘內(nèi)流入這個已經(jīng)銷毀的 Pod 的請求都會調(diào)用失敗。
因?yàn)榕f Pod 的 IP 在集群中已經(jīng)不存在了,所以會提示 Host unreachable
No route to host (Host unreachable)
如果我們使用 svc 域名(即服務(wù)名稱)注冊到 Eureka, 微服務(wù)內(nèi)部之間相互調(diào)用不再依賴于各自的服務(wù) IP(Pod IP),直接通過 Service 的域名訪問,域名解析和負(fù)載均衡都交給 Kubernetes 平臺,利用 Kubernetes 中 Pod 的探針監(jiān)測機(jī)制,可以自動過濾已下線或有問題的服務(wù)。
kubernetes:
namespace: cloud
eureka:
client:
serviceUrl:
defaultZone: ${eureka}
instance:
instance-id: ${spring.cloud.client.ip-address}:${server.port}
hostname: ${spring.application.name}.${kubernetes.namespace}
這種方式使用了 Kubernetes 的服務(wù)發(fā)現(xiàn)功能,當(dāng) Pod 不可用時就會從 Service 的 Endpoint 列表中移除,流量就不會在負(fù)載到該 Pod,可以避免 Eureka 緩存導(dǎo)致的一段時間內(nèi)服務(wù)調(diào)用失敗問題。
此時 Eureka 的服務(wù)發(fā)現(xiàn)、Ribbon 的負(fù)載均衡其實(shí)也就失去作用了,服務(wù)發(fā)現(xiàn)使用了 Kubernetes 基于 DNS 的方式,負(fù)載均衡則是 kube-proxy 組件維護(hù)的 iptables 規(guī)則來隨機(jī)訪問一個 Pod。
微服務(wù)之間的相互訪問如下圖所示:

這里完全可以廢除掉 Eureka,完全融入 Kubernetes 的服務(wù)發(fā)現(xiàn)機(jī)制,但是因?yàn)榇罅康奈⒎?wù)都是用的 Spring Cloud Eureka,底層走的 Feign 調(diào)用,徹底替換改造有點(diǎn)大,所以選擇了向 Eureka 注冊 SVC 域名這種方案。
~ END ~。