簡介

在 kubernetes 集群中,網路是非常基礎也非常重要的一部分。對於大規模的節點和容器來說,要保證網路的連通性、網路轉發的高效,同時能做的 ip 和 port 自動化分配和管理,並讓用戶用直觀簡單的方式来訪問需要的應用,這是需要複雜且细緻設計的。

kubernetes 在這方面下了很大的功夫,它通過servicednsingress等概念,解决了服務發現、負載均衡的問題,也大大简化了用戶的使用和配置。

這篇文章就講解如何配置 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=whoamienv=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.100172.17.8.101172.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-SERVICESchain 的最後,如果目標地址不是 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-NODEPORTSchain 和KUBE-SERVICESchain 其他規則一樣,都是轉發到更具體的servicechain,然後轉發到某個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 或者更複雜的報文路由機制。

results matching ""

    No results matching ""