kubernetes 簡介:kube-proxy 和 service
簡介
在 kubernetes 集群中,網絡是非常基礎也非常重要的一部分。對于大規模的節點和容器來說,要保證網絡的連通性、網絡轉發的高效,同時能做的 ip 和 port 自動化分配和管理,并讓用戶用直觀簡單的方式來訪問需要的應用,這是需要復雜且細致設計的。
kubernetes 在這方面下了很大的功夫,它通過 service 、 dns 、 ingress 等概念,解決了服務發現、負載均衡的問題,也大大簡化了用戶的使用和配置。
這篇文章就講解如何配置 kubernetes 的網絡,最終從集群內部和集群外部都能訪問應用。
跨主機網絡配置:flannel
一直以來,kubernetes 并沒有專門的網絡模塊負責網絡配置,它需要用戶在主機上已經配置好網絡。kubernetes 對網絡的要求是:容器之間(包括同一臺主機上的容器,和不同主機的容器)可以互相通信,容器和集群中所有的節點也能直接通信。
至于具體的網絡方案,用戶可以自己選擇,目前使用比較多的是 flannel,因為它比較簡單,而且剛好滿足 kubernetes 對網絡的要求。我們會使用 flannel vxlan 模式,具體的配置我在博客之前有文章介紹過,這里不再贅述。
以后 kubernetes 網絡的發展方向是希望通過插件的方式來集成不同的網絡方案, CNI 就是這一努力的結果,flannel 也能夠通過 CNI 插件的形式使用。
kube-proxy 和 service
配置好網絡之后,集群是什么情況呢?我們可以創建 pod,也能通過 ReplicationController 來創建特定副本的 pod(這是更推薦也是生產上要使用的方法,即使某個 rc 中只有一個 pod 實例)。可以從集群中獲取每個 pod ip 地址,然后也能在集群內部直接通過 podIP:Port 來獲取對應的服務。
但是還有一個問題: pod 是經常變化的,每次更新 ip 地址都可能會發生變化 ,如果直接訪問容器 ip 的話,會有很大的問題。而且進行擴展的時候,rc 中會有新的 pod 創建出來,出現新的 ip 地址,我們需要一種更靈活的方式來訪問 pod 的服務。
Service 和 cluster IP
針對這個問題,kubernetes 的解決方案是“服務”(service),每個服務都一個固定的虛擬 ip(這個 ip 也被稱為 cluster IP),自動并且動態地綁定后面的 pod,所有的網絡請求直接訪問服務 ip,服務會自動向后端做轉發。Service 除了提供穩定的對外訪問方式之外,還能起到負載均衡(Load Balance)的功能,自動把請求流量分布到后端所有的服務上,服務可以做到對客戶透明地進行水平擴展(scale)。
而實現 service 這一功能的關鍵,就是 kube-proxy。kube-proxy 運行在每個節點上,監聽 API Server 中服務對象的變化,通過管理 iptables 來實現網絡的轉發。
NOTE: kube-proxy 要求 NODE 節點操作系統中要具備 /sys/module/br_netfilter 文件,而且還要設置 bridge-nf-call-iptables=1,如果不滿足要求,那么 kube-proxy 只是將檢查信息記錄到日志中,kube-proxy 仍然會正常運行,但是這樣通過 Kube-proxy 設置的某些 iptables 規則就不會工作。
kube-proxy 有兩種實現 service 的方案:userspace 和 iptables
- userspace 是在用戶空間監聽一個端口,所有的 service 都轉發到這個端口,然后 kube-proxy 在內部應用層對其進行轉發。因為是在用戶空間進行轉發,所以效率也不高
- iptables 完全實現 iptables 來實現 service,是目前默認的方式,也是推薦的方式,效率很高(只有內核中 netfilter 一些損耗)。
這篇文章通過 iptables 模式運行 kube-proxy,后面的分析也是針對這個模式的,userspace 只是舊版本支持的模式,以后可能會放棄維護和支持。
kube-proxy 參數介紹
kube-proxy 的功能相對簡單一些,也比較獨立,需要的配置并不是很多,比較常用的啟動參數包括:
參數 | 含義 | 默認值 |
---|---|---|
–alsologtostderr | 打印日志到標準輸出 | false |
–bind-address | HTTP 監聽地址 | 0.0.0.0 |
–cleanup-iptables | 如果設置為 true,會清理 proxy 設置的 iptables 選項并退出 | false |
–healthz-bind-address | 健康檢查 HTTP API 監聽端口 | 127.0.0.1 |
–healthz-port | 健康檢查端口 | 10249 |
–iptables-masquerade-bit | 使用 iptables 進行 SNAT 的掩碼長度 | 14 |
–iptables-sync-period | iptables 更新頻率 | 30s |
–kubeconfig | kubeconfig 配置文件地址 | |
–log-dir | 日志文件目錄/路徑 | |
–masquerade-all | 如果使用 iptables 模式,對所有流量進行 SNAT 處理 | false |
–master | kubernetes master API Server 地址 | |
–proxy-mode | 代理模式, userspace 或者 iptables , 目前默認是 iptables ,如果系統或者 iptables 版本不夠新,會 fallback 到 userspace 模式 | iptables |
–proxy-port-range | 代理使用的端口范圍, 格式為 beginPort-endPort ,如果沒有指定,會隨機選擇 | 0-0 |
–udp-timeout | UDP 空連接 timeout 時間,只對 userspace 模式有用 | 250ms |
–v | 日志級別 | 0 |
kube-proxy 的工作模式可以通過 --proxy-mode 進行配置,可以選擇 userspace 或者 iptables 。
實例啟動和測試
我們可以在終端上啟動 kube-proxy ,也可以使用諸如 systemd 這樣的工具來管理它,比如下面就是一個簡單的 kube-proxy.service 配置文件
[root@localhost]# cat /usr/lib/systemd/system/kube-proxy.service [Unit] Description=Kubernetes Proxy Service Documentation=http://kubernetes.com After=network.target Wants=network.target[Service] Type=simple EnvironmentFile=-/etc/sysconfig/kube-proxy ExecStart=/usr/bin/kube-proxy \ --master=http://172.17.8.100:8080 \ --v=4 \ --proxy-mode=iptables TimeoutStartSec=0 Restart=on-abnormal
[Install] WantedBy=multi-user.target</pre>
為了方便測試,我們創建一個 rc,里面有三個 pod。這個 pod 運行的是 cizixs/whoami 容器 ,它是一個簡單的 HTTP 服務器,監聽在 3000 端口,訪問它會返回容器的 hostname。
[root@localhost ~]# cat whoami-rc.yml apiVersion: v1 kind: ReplicationController metadata: name: whoami spec: replicas: 3 selector: app: whoami template: metadata: name: whoami labels: app: whoami env: dev spec: containers:- name: whoami image: cizixs/whoami:v0.5 ports: - containerPort: 3000 env: - name: MESSAGE value: viola</pre>
我們為每個 pod 設置了兩個 label: app=whoami 和 env=dev ,這兩個標簽很重要,也是后面服務進行綁定 pod 的關鍵。
為了使用 service,我們還要定義另外一個文件,并通過 kubectl create -f ./whoami-svc.yml 來創建出來對象:
apiVersion: v1 kind: Service metadata: labels: name: whoami name: whoami spec: ports: - port: 3000 targetPort: 3000 protocol: TCP selector: app: whoami env: dev其中 selector 告訴 kubernetes 這個 service 和后端哪些 pod 綁定在一起,這里包含的鍵值對會對所有 pod 的 labels 進行匹配,只要完全匹配,service 就會把 pod 作為后端。也就是說,service 和 rc 并不是對應的關系,一個 service 可能會使用多個 rc 管理的 pod 作為后端應用。
ports 字段指定服務的端口信息:
- port :虛擬 ip 要綁定的 port,每個 service 會創建出來一個虛擬 ip,通過訪問 vip:port 就能獲取服務的內容。這個 port 可以用戶隨機選取,因為每個服務都有自己的 vip,也不用擔心沖突的情況
- targetPort :pod 中暴露出來的 port,這是運行的容器中具體暴露出來的端口,一定不能寫錯
- protocol :提供服務的協議類型,可以是 TCP 或者 UDP
創建之后可以列出 service ,發現我們創建的 service 已經分配了一個虛擬 ip (10.10.10.28),這個虛擬 ip 地址是不會變化的(除非 service 被刪除)。查看 service 的詳情可以看到它的 endpoints 列出,對應了具體提供服務的 pod 地址和端口。
[root@localhost ~]# kubectl get svc NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes 10.10.10.1 <none> 443/TCP 19d whoami 10.10.10.28 <none> 3000/TCP 1d [root@localhost ~]# kubectl describe svc whoami Name: whoami Namespace: default Labels: name=whoami Selector: app=whoami Type: ClusterIP IP: 10.10.10.28 Port: <unset> 3000/TCP Endpoints: 10.11.32.6:3000,10.13.192.4:3000,10.16.192.3:3000 Session Affinity: None No events.
默認的 service 類型是 ClusterIP ,這個也可以從上面輸出看出來。在這種情況下,只能從集群內部訪問這個 IP,不能直接從集群外部訪問服務。如果想對外提供服務,我們后面會講解決方案。
測試一下,訪問 service 服務的時候可以看到它會隨機地訪問后端的 pod,給出不同的返回:
[root@localhost ~]# curl http://10.10.10.28:3000 viola from whoami-8fpqp [root@localhost ~]# curl http://10.10.10.28:3000 viola from whoami-c0x6h [root@localhost ~]# curl http://10.10.10.28:3000 viola from whoami-8fpqp [root@localhost ~]# curl http://10.10.10.28:3000 viola from whoami-dc9ds
默認情況下,服務會隨機轉發到可用的后端。如果希望保持會話(同一個 client 永遠都轉發到相同的 pod),可以把 service.spec.sessionAffinity 設置為 ClientIP 。
NOTE: 需要注意的是,服務分配的 cluster IP 是一個虛擬 ip,如果你嘗試 ping 這個 IP 會發現它沒有任何響應,這也是剛接觸 kubernetes service 的人經常會犯的錯誤。實際上,這個虛擬 IP 只有和它的 port 一起的時候才有作用,直接訪問它,或者想訪問該 IP 的其他端口都是徒勞。
外部能夠訪問的服務
上面創建的服務只能在集群內部訪問,這在生產環境中還不能直接使用。如果希望有一個能直接對外使用的服務,可以使用 NodePort 或者 LoadBalancer 類型的 Service。我們先說說 NodePort ,它的意思是在所有 worker 節點上暴露一個端口,這樣外部可以直接通過訪問 nodeIP:Port 來訪問應用。
我們先把剛才創建的服務刪除:
[root@localhost ~]# kubectl delete rc whoami replicationcontroller "whoami" deleted [root@localhost ~]# kubectl delete svc whoami service "whoami" deleted [root@localhost ~]# kubectl get pods,svc,rc NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes 10.10.10.1 <none> 443/TCP 14h
對我們原來的 Service 配置文件進行修改,把 spec.type 寫成 NodePort 類型:
[root@localhost ~]# cat whoami-svc.yml apiVersion: v1 kind: Service metadata: labels: name: whoami name: whoami spec: ports: - port: 3000 protocol: TCP # nodePort: 31000 selector: app: whoami type: NodePort
因為我們的應用比較簡單,只有一個端口。如果 pod 有多個端口,也可以在 spec.ports 中繼續添加,只有保證多個 port 之間不沖突就行。
重新創建 rc 和 svc:
[root@localhost ~]# kubectl create -f ./whoami-svc.yml service "whoami" created [root@localhost ~]# kubectl get rc,pods,svc NAME DESIRED CURRENT READY AGE rc/whoami 3 3 3 10s NAME READY STATUS RESTARTS AGE po/whoami-8zc3d 1/1 Running 0 10s po/whoami-mc2fg 1/1 Running 0 10s po/whoami-z6skj 1/1 Running 0 10s NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE svc/kubernetes 10.10.10.1 <none> 443/TCP 14h svc/whoami 10.10.10.163 <nodes> 3000:31647/TCP 7s
需要注意的是,因為我們沒有指定 nodePort 的值,kubernetes 會自動給我們分配一個,比如這里的 31647 (默認的取值范圍是 30000-32767)。當然我們也可以刪除配置中 # nodePort: 31000 的注釋,這樣會使用 31000 端口。
nodePort 類型的服務會在所有的 worker 節點(運行了 kube-proxy)上統一暴露出端口對外提供服務,也就是說外部可以任意選擇一個節點進行訪問。比如我本地集群有三個節點: 172.17.8.100 、 172.17.8.101 和 172.17.8.102 :
[root@localhost ~]# curl http://172.17.8.100:31647 viola from whoami-mc2fg [root@localhost ~]# curl http://172.17.8.101:31647 viola from whoami-8zc3d [root@localhost ~]# curl http://172.17.8.102:31647 viola from whoami-z6skj
有了 nodePort ,用戶可以通過外部的 Load Balance 或者路由器把流量轉發到任意的節點,對外提供服務的同時,也可以做到負載均衡的效果。
nodePort 類型的服務并不影響原來虛擬 IP 的訪問方式,內部節點依然可以通過 vip:port 的方式進行訪問。
LoadBalancer 類型的服務需要公有云支持,如果你的集群部署在公有云(GCE、AWS等)可以考慮這種方式。
service 原理解析
目前 kube-proxy 默認使用 iptables 模式,上述展現的 service 功能都是通過修改 iptables 實現的。
我們來看一下從主機上訪問 service:port 的時候發生了什么(通過 iptables-save 命令打印出來當前機器上的 iptables 規則)。
所有發送出去的報文會進入 KUBE-SERVICES 進行處理
*nat -A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
KUBE-SERVICES 每條規則對應了一個 service,它告訴繼續進入到某個具體的 service chain 進行處理,比如這里的 KUBE-SVC-OQCLJJ5GLLNFY3XB
-A KUBE-SERVICES -d 10.10.10.28/32 -p tcp -m comment --comment "default/whoami: cluster IP" -m tcp --dport 3000 -j KUBE-SVC-OQCLJJ5GLLNFY3XB
更具體的 chain 中定義了怎么轉發到對應 endpoint 的規則,比如我們的 rc 有三個 pods,這里也就會生成三個規則。這里利用了 iptables 隨機和概率轉發的功能
-A KUBE-SVC-OQCLJJ5GLLNFY3XB -m comment --comment "default/whoami:" -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-VN72UHNM6XOXLRPW -A KUBE-SVC-OQCLJJ5GLLNFY3XB -m comment --comment "default/whoami:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-YXCSPWPTUFI5WI5Y -A KUBE-SVC-OQCLJJ5GLLNFY3XB -m comment --comment "default/whoami:" -j KUBE-SEP-FN74S3YUBFMWHBLF
我們來看第一個 chain,這個 chain 有兩個規則,第一個表示給報文打上 mark;第二個是進行 DNAT(修改報文的目的地址),轉發到某個 pod 地址和端口。
-A KUBE-SEP-VN72UHNM6XOXLRPW -s 10.11.32.6/32 -m comment --comment "default/whoami:" -j KUBE-MARK-MASQ -A KUBE-SEP-VN72UHNM6XOXLRPW -p tcp -m comment --comment "default/whoami:" -m tcp -j DNAT --to-destination 10.11.32.6:3000
因為地址是發送出去的,報文會根據路由規則進行處理,后續的報文就是通過 flannel 的網絡路徑發送出去的。
nodePort 類型的 service 原理也是類似的,在 KUBE-SERVICES chain 的最后,如果目標地址不是 VIP 則會通過 KUBE-NODEPORTS :
Chain KUBE-SERVICES (2 references) pkts bytes target prot opt in out source destination 0 0 KUBE-NODEPORTS all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL
而 KUBE-NODEPORTS chain 和 KUBE-SERVICES chain 其他規則一樣,都是轉發到更具體的 service chain,然后轉發到某個 pod 上面。
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/whoami:" -m tcp --dport 31647 -j KUBE-MARK-MASQ -A KUBE-NODEPORTS -p tcp -m comment --comment "default/whoami:" -m tcp --dport 31647 -j KUBE-SVC-OQCLJJ5GLLNFY3XB
不足之處
看起來 service 是個完美的方案,可以解決服務訪問的所有問題,但是 service 這個方案(iptables 模式)也有自己的缺點。
首先,如果轉發的 pod 不能正常提供服務,它不會自動嘗試另一個 pod,當然這個可以通過 readiness probes 來解決。每個 pod 都有一個健康檢查的機制,當有 pod 健康狀況有問題時,kube-proxy 會刪除對應的轉發規則。
另外, nodePort 類型的服務也無法添加 TLS 或者更復雜的報文路由機制。
參考資料
- Kubernetes 1.2 如何使用 iptables
- Kubernetes User Guide: Service
- Kubernetes User Guide: Debugging Services
- Kubernetes Services and Ingress Under X-ray
- CoreOS documentation: Overview of a Service
來自:http://cizixs.com/2017/03/30/kubernetes-introduction-service-and-kube-proxy