簡介
在 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(這是更推荐也是production上要使用的方法,即使某個 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
為了方便測試,我們創建一個 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
我們為每個 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 的其他端口都是徒劳。
外部能够訪問的服務
上面創建的服務只能在集群内部訪問,這在production環境中還不能直接使用。如果希望有一個能直接對外使用的服務,可以使用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 或者更複雜的報文路由機制。