內部服務發現
前面我們給大家講解了 Service 的用法,我們可以通過 Service 生成的 ClusterIP(VIP)來訪問 Pod 提供的服務,但是在使用的時候還有一個問題:我們怎么知道某個應用的 VIP 呢?比如我們有兩個應用,一個是 api 應用,一個是 db 應用,兩個應用都是通過 Deployment 進行管理的,並且都通過 Service 暴露出了端口提供服務。api 需要連接到 db 這個應用,我們只知道 db 應用的名稱和 db 對應的 Service 的名稱,但是並不知道它的 VIP 地址,我們前面的 Service 課程中是不是學習到我們通過 ClusterIP 就可以訪問到后面的 Pod 服務,如果我們知道了 VIP 的地址是不是就行了?
apiserver
我們知道可以從 apiserver 中直接查詢獲取到對應 service 的后端 Endpoints信息,所以最簡單的辦法是從 apiserver 中直接查詢,如果偶爾一個特殊的應用,我們通過 apiserver 去查詢到 Service 后面的 Endpoints 直接使用是沒問題的,但是如果每個應用都在啟動的時候去查詢依賴的服務,這不但增加了應用的復雜度,這也導致了我們的應用需要依賴 Kubernetes 了,耦合度太高了,不具有通用性。
環境變量
為了解決上面的問題,在之前的版本中,Kubernetes 采用了環境變量的方法,每個 Pod 啟動的時候,會通過環境變量設置所有服務的 IP 和 port 信息,這樣 Pod 中的應用可以通過讀取環境變量來獲取依賴服務的地址信息,這種方法使用起來相對簡單,但是有一個很大的問題就是依賴的服務必須在 Pod 啟動之前就存在,不然是不會被注入到環境變量中的。比如我們首先創建一個 Nginx 服務:(test-nginx.yaml)
apiVersion: apps/v1beta1 kind: Deployment metadata: name: nginx-deploy labels: k8s-app: nginx-demo spec: replicas: 2 template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.7.9 ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: nginx-service labels: name: nginx-service spec: ports: - port: 5000 targetPort: 80 selector: app: nginx
創建上面的服務:
$ kubectl create -f test-nginx.yaml
deployment.apps "nginx-deploy" created service "nginx-service" created $ kubectl get pods NAME READY STATUS RESTARTS AGE ... nginx-deploy-75675f5897-47h4t 1/1 Running 0 53s nginx-deploy-75675f5897-mmm8w 1/1 Running 0 53s ... $ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE ... nginx-service ClusterIP 10.107.225.42 <none> 5000/TCP 1m ...
我們可以看到兩個 Pod 和一個名為 nginx-service 的服務創建成功了,該 Service 監聽的端口是 5000,同時它會把流量轉發給它代理的所有 Pod(我們這里就是擁有 app: nginx 標簽的兩個 Pod)。
現在我們再來創建一個普通的 Pod,觀察下該 Pod 中的環境變量是否包含上面的 nginx-service 的服務信息:(test-pod.yaml)
apiVersion: v1 kind: Pod metadata: name: test-pod spec: containers: - name: test-service-pod image: busybox command: ["/bin/sh", "-c", "env"]
然后創建該測試的 Pod:
$ kubectl create -f test-pod.yaml pod "test-pod" created
等 Pod 創建完成后,我們查看日志信息:
$ kubectl logs test-pod
... KUBERNETES_PORT=tcp://10.96.0.1:443 KUBERNETES_SERVICE_PORT=443 HOSTNAME=test-pod HOME=/root NGINX_SERVICE_PORT_5000_TCP_ADDR=10.107.225.42 NGINX_SERVICE_PORT_5000_TCP_PORT=5000 NGINX_SERVICE_PORT_5000_TCP_PROTO=tcp KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin NGINX_SERVICE_SERVICE_HOST=10.107.225.42 NGINX_SERVICE_PORT_5000_TCP=tcp://10.107.225.42:5000 KUBERNETES_PORT_443_TCP_PORT=443 KUBERNETES_PORT_443_TCP_PROTO=tcp NGINX_SERVICE_SERVICE_PORT=5000 NGINX_SERVICE_PORT=tcp://10.107.225.42:5000 KUBERNETES_SERVICE_PORT_HTTPS=443 KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443 KUBERNETES_SERVICE_HOST=10.96.0.1 PWD=/ ...
我們可以看到打印了很多環境變量處理,其中就包括我們剛剛創建的 nginx-service 這個服務,有 HOST、PORT、PROTO、ADDR 等,也包括其他已經存在的 Service 的環境變量,現在如果我們需要在這個 Pod 里面訪問 nginx-service 的服務,我們是不是可以直接通過 NGINX_SERVICE_SERVICE_HOST 和 NGINX_SERVICE_SERVICE_PORT 就可以了,但是我們也知道如果這個 Pod 啟動起來的時候如果 nginx-service 服務還沒啟動起來,在環境變量中我們是無法獲取到這些信息的,當然我們可以通過 initContainer 之類的方法來確保 nginx-service 啟動后再啟動 Pod,但是這種方法畢竟增加了 Pod 啟動的復雜性,所以這不是最優的方法。
KubeDNS
由於上面環境變量這種方式的局限性,我們需要一種更加智能的方案,其實我們可以自己想學一種比較理想的方案:那就是可以直接使用 Service 的名稱,因為 Service 的名稱不會變化,我們不需要去關心分配的 ClusterIP 的地址,因為這個地址並不是固定不變的,所以如果我們直接使用 Service 的名字,然后對應的 ClusterIP 地址的轉換能夠自動完成就很好了。我們知道名字和 IP 直接的轉換是不是和我們平時訪問的網站非常類似啊?他們之間的轉換功能通過 DNS 就可以解決了,同樣的,Kubernetes 也提供了 DNS 的方案來解決上面的服務發現的問題。
kubedns 介紹
DNS 服務不是一個獨立的系統服務,而是作為一種 addon 插件而存在,也就是說不是 Kubernetes 集群必須安裝的,當然我們強烈推薦安裝,可以將這個插件看成是一種運行在 Kubernetes 集群上的一直比較特殊的應用,現在比較推薦的兩個插件:kube-dns 和 CoreDNS。我們在前面使用 kubeadm 搭建集群的時候直接安裝的 kube-dns 插件,如果不記得了可以回頭去看一看。當然如果我們想使用 CoreDNS 的話也很方便,只需要執行下面的命令即可:
$ kubeadm init --feature-gates=CoreDNS=true
Kubernetes DNS pod 中包括 3 個容器,可以通過 kubectl 工具查看:
$ kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
... kube-dns-5868f69869-zp5kz 3/3 Running 0 19d ...
READY 一欄可以看到是 3/3,用如下命令可以很清楚的看到 kube-dns 包含的3個容器:
$ kubectl describe pod kube-dns-5868f69869-zp5kz -n kube-system
kube-dns、dnsmasq-nanny、sidecar 這3個容器分別實現了什么功能?
- kubedns: kubedns 基於 SkyDNS 庫,通過 apiserver 監聽 Service 和 Endpoints 的變更事件同時也同步到本地 Cache,實現了一個實時的 Kubernetes 集群內 Service 和 Pod 的 DNS服務發現
- dnsmasq: dsnmasq 容器則實現了 DNS 的緩存功能(在內存中預留一塊默認大小為 1G 的地方,保存當前最常用的 DNS 查詢記錄,如果緩存中沒有要查找的記錄,它會到 kubedns 中查詢,並把結果緩存起來),通過監聽 ConfigMap 來動態生成配置
- sider: sidecar 容器實現了可配置的 DNS 探測,並采集對應的監控指標暴露出來供 prometheus 使用
kube dns
對 Pod 的影響
DNS Pod 具有靜態 IP 並作為 Kubernetes 服務暴露出來。該靜態 IP 被分配后,kubelet 會將使用 --cluster-dns = <dns-service-ip>
參數配置的 DNS 傳遞給每個容器。DNS 名稱也需要域名,本地域可以使用參數--cluster-domain = <default-local-domain>
在 kubelet 中配置。
我們說 dnsmasq 容器通過監聽 ConfigMap 來動態生成配置,可以自定義存根域和上下游域名服務器。
例如,下面的 ConfigMap 建立了一個 DNS 配置,它具有一個單獨的存根域和兩個上游域名服務器:
apiVersion: v1 kind: ConfigMap metadata: name: kube-dns namespace: kube-system data: stubDomains: | {"acme.local": ["1.2.3.4"]} upstreamNameservers: | ["8.8.8.8", "8.8.4.4"]
按如上說明,具有.acme.local后綴的 DNS 請求被轉發到 DNS 1.2.3.4。Google 公共 DNS 服務器 為上游查詢提供服務。下表描述了具有特定域名的查詢如何映射到它們的目標 DNS 服務器:
域名 | 響應查詢的服務器 |
---|---|
kubernetes.default.svc.cluster.local | kube-dns |
foo.acme.local | 自定義 DNS (1.2.3.4) |
widget.com | 上游 DNS (8.8.8.8, 8.8.4.4,其中之一) |
另外我們還可以為每個 Pod 設置 DNS 策略。 當前 Kubernetes 支持兩種 Pod 特定的 DNS 策略:“Default” 和 “ClusterFirst”。 可以通過 dnsPolicy 標志來指定這些策略。
注意:Default 不是默認的 DNS 策略。如果沒有顯式地指定dnsPolicy,將會使用 ClusterFirst
- 如果 dnsPolicy 被設置為 “Default”,則名字解析配置會繼承自 Pod 運行所在的節點。自定義上游域名服務器和存根域不能夠與這個策略一起使用
- 如果 dnsPolicy 被設置為 “ClusterFirst”,這就要依賴於是否配置了存根域和上游 DNS 服務器
- 未進行自定義配置:沒有匹配上配置的集群域名后綴的任何請求,例如 “www.kubernetes.io”,將會被轉發到繼承自節點的上游域名服務器。
- 進行自定義配置:如果配置了存根域和上游 DNS 服務器(類似於 前面示例 配置的內容),DNS 查詢將基於下面的流程對請求進行路由:
- 查詢首先被發送到 kube-dns 中的 DNS 緩存層。
- 從緩存層,檢查請求的后綴,並根據下面的情況轉發到對應的 DNS 上:
- 具有集群后綴的名字(例如 “.cluster.local”):請求被發送到 kubedns。
- 具有存根域后綴的名字(例如 “.acme.local”):請求被發送到配置的自定義 DNS 解析器(例如:監聽在 1.2.3.4)。
- 未能匹配上后綴的名字(例如 “widget.com”):請求被轉發到上游 DNS(例如:Google 公共 DNS 服務器,8.8.8.8 和 8.8.4.4)。
域名格式
我們前面說了如果我們建立的 Service 如果支持域名形式進行解析,就可以解決我們的服務發現的功能,那么利用 kubedns 可以將 Service 生成怎樣的 DNS 記錄呢?
- 普通的 Service:會生成 servicename.namespace.svc.cluster.local 的域名,會解析到 Service 對應的 ClusterIP 上,在 Pod 之間的調用可以簡寫成 servicename.namespace,如果處於同一個命名空間下面,甚至可以只寫成 servicename 即可訪問
- Headless Service:無頭服務,就是把 clusterIP 設置為 None 的,會被解析為指定 Pod 的 IP 列表,同樣還可以通過 podname.servicename.namespace.svc.cluster.local 訪問到具體的某一個 Pod。
CoreDNS 實現的功能和 KubeDNS 是一致的,不過 CoreDNS 的所有功能都集成在了同一個容器中,在最新版的1.11.0版本中官方已經推薦使用 CoreDNS了,大家也可以安裝 CoreDNS 來代替 KubeDNS,其他使用方法都是一致的:https://coredns.io/
測試
現在我們來使用一個簡單 Pod 來測試下 Service 的域名訪問:
$ kubectl run --rm -i --tty test-dns --image=busybox /bin/sh If you don't see a command prompt, try pressing enter. / # cat /etc/resolv.conf nameserver 10.96.0.10 search default.svc.cluster.local svc.cluster.local cluster.local options ndots:5 / #
我們進入到 Pod 中,查看/etc/resolv.conf中的內容,可以看到 nameserver 的地址10.96.0.10,該 IP 地址即是在安裝 kubedns 插件的時候集群分配的一個固定的靜態 IP 地址,我們可以通過下面的命令進行查看:
$ kubectl get svc kube-dns -n kube-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP 62d
也就是說我們這個 Pod 現在默認的 nameserver 就是 kubedns 的地址,現在我們來訪問下前面我們創建的 nginx-service 服務:
/ # wget -q -O- nginx-service.default.svc.cluster.local
可以看到上面我們使用 wget 命令去訪問 nginx-service 服務的域名的時候被 hang 住了,沒有得到期望的結果,這是因為上面我們建立 Service 的時候暴露的端口是 5000:
/ # wget -q -O- nginx-service.default.svc.cluster.local:5000 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> <p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/> Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p> <p><em>Thank you for using nginx.</em></p> </body> </html>
加上 5000 端口,就正常訪問到服務,再試一試訪問:nginx-service.default.svc、nginx-service.default、nginx-service,不出意外這些域名都可以正常訪問到期望的結果。
到這里我們是不是就實現了在集群內部通過 Service 的域名形式進行互相通信了,大家下去試着看看訪問不同 namespace 下面的服務呢?下節課我們來給大家講解使用 ingress 來實現集群外部的服務發現功能。