Kubernetes能夠方便的爲容器應用提供了一個持續運行且方便擴展的環境,可是,應用最終是要被用戶或其餘應用訪問、調用的。要訪問應用pod,就會有如下兩個問題:node
service主要就是用來解決這兩個問題的。簡單來講,它是一個抽象的api對象,用來表示一組提供相同服務的pod及對這組pod的訪問方式。web
service做爲一個相似中介的角色,對內,它要能代理訪問到不斷變換的一組後端Pod;對外,它要能暴露本身給集羣內部或外部的其餘資源訪問。咱們分別來看下具體是怎麼實現的。segmentfault
以前的文章kubeadm部署最後的測試部分,建立了一組pod及服務來驗證業務,繼續以這個例子來講明:後端
集羣中已經有以下一組pod:api
NAME READY STATUS IP NODE APP goweb-55c487ccd7-5t2l2 1/1 Running 10.244.1.15 node-1 goweb goweb-55c487ccd7-cp6l8 1/1 Running 10.244.3.9 node-2 goweb goweb-55c487ccd7-gcs5x 1/1 Running 10.244.1.17 node-1 goweb goweb-55c487ccd7-pp6t6 1/1 Running 10.244.3.10 node-2 goweb
pod都帶有app:goweb標籤,對外暴露8000端口,訪問/info路徑會返回主機名。架構
建立一個servcie有兩種方式app
$ kubectl expose deployment goweb --name=gowebsvc --port=80 --target-port=8000
# 定義服務配置文件 # svc-goweb.yaml apiVersion: v1 kind: Service metadata: name: gowebsvc spec: selector: app: goweb ports: - name: default protocol: TCP port: 80 targetPort: 8000 type: ClusterIP # 建立服務 $ kubectl apply -f svc-goweb.yaml
咱們來看下配置文件中幾個重點字段:負載均衡
經過apply建立服務後,來查看一下服務狀態less
$ kubectl get svc gowebsvc -o wide NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR gowebsvc ClusterIP 10.106.202.0 <none> 80/TCP 3d app=goweb
能夠看到,Kubernetes自動爲服務分配了一個CLUSTER-IP。經過這個訪問這個IP的80端口,就能夠訪問到"app: goweb"這組pod的8000端口,而且能夠在這組pod中負載均衡。dom
[root@master-1 ~]# curl http://10.106.202.0/info Hostname: goweb-55c487ccd7-gcs5x [root@master-1 ~]# curl http://10.106.202.0/info Hostname: goweb-55c487ccd7-cp6l8 [root@master-1 ~]# curl http://10.106.202.0/info Hostname: goweb-55c487ccd7-pp6t6
cluster-ip是一個虛擬的ip地址,並非某張網卡的真實地址。那具體的請求代理轉發過程是怎麼實現的呢? 答案是iptables。咱們來看下iptables中與cluster-ip相關的規則
[root@master-1 ~]# iptables-save | grep 10.106.202.0 -A KUBE-SERVICES ! -s 10.244.0.0/16 -d 10.106.202.0/32 -p tcp -m comment --comment "default/gowebsvc:default cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ -A KUBE-SERVICES -d 10.106.202.0/32 -p tcp -m comment --comment "default/gowebsvc:default cluster IP" -m tcp --dport 80 -j KUBE-SVC-SEG6BTF25PWEPDFT
能夠看到,目的地址爲CLUSTER-IP、目的端口爲80的數據包,會被轉發到KUBE-MARK-MASQ與KUBE-SVC-SEG6BTF25PWEPDFT鏈上。其中,KUBE-MARK-MASQ鏈的做用是給數據包打上特定的標記(待驗證),重點來看下KUBE-SVC-SEG6BTF25PWEPDFT鏈:
-A KUBE-SVC-SEG6BTF25PWEPDFT -m statistic --mode random --probability 0.25000000000 -j KUBE-SEP-5ZXTVLEM4DKNW7T2 -A KUBE-SVC-SEG6BTF25PWEPDFT -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-EBFXI7VOCPDT2QU5 -A KUBE-SVC-SEG6BTF25PWEPDFT -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-C3PKSXKMO2M43SPF -A KUBE-SVC-SEG6BTF25PWEPDFT -j KUBE-SEP-2GQCCNJGO65Z5MFS
能夠看到,KUBE-SVC-SEG6BTF25PWEPDFT鏈經過設置--probability,將請求等機率轉發到4條鏈上,查看其中一條轉發鏈:
[root@master-1 ~]# iptables-save | grep "A KUBE-SEP-5ZXTVLEM4DKNW7T2" -A KUBE-SEP-5ZXTVLEM4DKNW7T2 -s 10.244.1.15/32 -j KUBE-MARK-MASQ -A KUBE-SEP-5ZXTVLEM4DKNW7T2 -p tcp -m tcp -j DNAT --to-destination 10.244.1.15:8000
發現KUBE-SEP-5ZXTVLEM4DKNW7T2這條規則對請求的目的地址做了DNAT到10.244.1.15:8000,這正是goweb組中goweb-55c487ccd7-5t2l2這個pod的ip地址。這樣,對svc的CLUSTER-IP的請求,就會經過iptables規則轉發到相應的pod。
可是,還有個問題,svc是怎麼跟蹤pod的ip變化的?
注意到前面的nat規則,第一次轉發的鏈名稱是KUBE-SVC-xxx,第二次轉發給具體pod的鏈名稱是KUBE-SEP-xxx,這裏的SEP實際指的是kubernetes另外一個對象endpoint,咱們能夠經過vkubectl get ep命令來查看:
[root@master-1 ~]# kubectl get ep gowebsvc NAME ENDPOINTS gowebsvc 10.244.1.15:8000,10.244.1.17:8000,10.244.3.10:8000 + 1 more... 35d
在svc建立的時候,kube-proxy組件會自動建立同名的endpoint對象,動態地跟蹤匹配selector的一組pod當前ip及端口,並生成相應的iptables KUBE-SVC-xxx規則。
上面說的請求代理轉發的方式,是kubernetes目前版本的默認方式,實際上,service的代理方式一共有三種:
在這種模式下,kube-proxy爲每一個服務都打開一個隨機的端口,全部訪問這個端口的請求都會被轉發到服務對應endpoints指定的後端。最後,kube-proxy還會生成一條iptables規則,把訪問cluster-ip的請求重定向到上面說的隨機端口,最終轉發到後端pod。整個過程以下圖所示:
Userspace模式的代理轉發主要依靠kube-proxy實現,工做在用戶態。因此,轉發效率不高。較爲不推薦用該種模式。
iptables模式是目前版本的默認服務代理轉發模式,上兩小節作過詳細說明的就是這種模式,來看下請求轉發的示意圖:
與userspace模式最大的不一樣點在於,kube-proxy只動態地維護iptables,而轉發徹底靠iptables實現。因爲iptables工做在內核態,不用在用戶態與內核態切換,因此相比userspace模式更高效也更可靠。可是每一個服務都會生成若干條iptables規則,大型集羣iptables規則數會很是多,形成性能降低也不易排查問題。
在v1.9版本之後,服務新增了ipvs轉發方式。kube-proxy一樣只動態跟蹤後端endpoints的狀況,而後調用netlink接口來生成ipvs規則。經過ipvs來轉發請求:
ipvs一樣工做在內核態,並且底層轉發是依靠hash表實現,因此性能比iptables還要好的多,同步新規則也比iptables快。同時,負載均衡的方式除了簡單rr還有多種選擇,因此很適合在大型集羣使用。而缺點就是帶來了額外的配置維護操做。
在集羣內部對一個服務的訪問,主要有2種方式,環境變量與DNS。
當一個pod建立時,集羣中屬於同個namespace下的全部service對象信息都會被做爲環境變量添加到pod中。隨便找一個pod查看一下:
$ kubectl exec goweb-55c487ccd7-5t2l2 'env' | grep GOWEBSVC GOWEBSVC_PORT_80_TCP_ADDR=10.106.202.0 GOWEBSVC_SERVICE_PORT=80 GOWEBSVC_SERVICE_PORT_DEFAULT=80 GOWEBSVC_PORT_80_TCP=tcp://10.106.202.0:80 GOWEBSVC_PORT_80_TCP_PROTO=tcp GOWEBSVC_PORT_80_TCP_PORT=80 GOWEBSVC_PORT=tcp://10.106.202.0:80 GOWEBSVC_SERVICE_HOST=10.106.202.0
能夠看到,pod經過{SVCNAME}_SERVICE_HOST/PORT就能夠方便的訪問到某個服務。這種訪問方式簡單易用,能夠用來快速測試服務。但最大的問題就是,服務必須先於pod建立,後建立的服務是不會添加到現有pod的環境變量中的。
DNS組件是k8s集羣的可選組件,它會不停監控k8s API,在有新服務建立時,自動建立相應的DNS記錄。。以gowebsvc爲例,在服務建立時,會建立一條gowebsvc.default.svc.cluster.local的dns記錄指向服務。並且dns記錄做用域是整個集羣,不侷限在namespace。
雖然是可選組件,但DNS生產環境能夠說是必備的組件了。這裏先簡單說明,後面打算專門開篇文章來詳細介紹。
服務發現解決了集羣內部訪問pod問題,但不少時候,pod提供的服務也是要對集羣外部來暴露訪問的,最典型的就是web服務。k8s中的service有多種對外暴露的方式,能夠在部署Service時經過ServiceType字段來指定。默認狀況下,ServiceType配置是隻能內部訪問的ClusterIP方式,前面的例子都是這種模式,除此以外,還能夠配置成下面三種方式:
該方式把服務暴露在每一個Node主機IP的特定端口上,同一個服務在全部Node上端口是相同的,並自動生成相應的路由轉發到ClusterIP。這樣,集羣外部經過<NodeIP>:<NodePort>就能夠訪問到對應的服務。舉個例子:
## 建立svc,經過Nodeport方式暴露服務 $ kubectl expose deployment goweb --name=gowebsvc-nodeport --port=80 --target-port=8000 --type=NodePort ## 查看svc,能夠看到NodePort隨機分配的端口爲32538 $ kubectl get svc gowebsvc-nodeport NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE gowebsvc-nodeport NodePort 10.101.166.252 <none> 80:32538/TCP 86s ## 隨便訪問一個nodeip的32538端口,均可以訪問到gowebsvc-nodeport服務對應的pod $ curl 172.16.201.108:32538/info Hostname: goweb-55c487ccd7-pp6t6 $ curl 172.16.201.109:32538/info Hostname: goweb-55c487ccd7-5t2l2
LoadBalance方式主要是給公有云服務使用的,經過配置LoadBalance,能夠觸發公有云建立負載均衡器,並把node節點做爲負載的後端節點。每一個公有云的配置方式不一樣,具體能夠參考各公有云的相關文檔。
當ServiceType被配置爲這種方式時,該服務的目的就不是爲了外部訪問了,而是爲了方便集羣內部訪問外部資源。舉個例子,假如目前集羣的pod要訪問一組DB資源,而DB是部署在集羣外部的物理機,尚未容器化,能夠配置這麼一個服務:
apiVersion: v1 kind: Service metadata: name: dbserver namespace: default spec: type: ExternalName externalName: database.abc.com
這樣,集羣內部的pod經過dbserver.default.svc.cluster.local這個域名訪問這個服務時,請求會被cname到database.abc.com來。事後,假如db容器化了,不須要修改業務代碼,直接修改service,加上相應selector就能夠了。
除了上面這些一般的service配置,還有幾種特殊狀況:
service能夠配置不止一個端口,好比官方文檔的例子:
apiVersion: v1 kind: Service metadata: name: my-service spec: selector: app: MyApp ports: - name: http protocol: TCP port: 80 targetPort: 9376 - name: https protocol: TCP port: 443 targetPort: 9377
這個service保留了80與443端口,分別對應pod的9376與9377端口。這裏須要注意的是,pod的每一個端口必定指定name字段(默認是default)。
Headless services是指一個服務沒有配置了clusterIP=None的服務。這種狀況下,kube-proxy不會爲這個服務作負載均衡的工做,而是交予DNS完成。具體又分爲2種狀況:
若是有個非node本地的IP地址,能夠經過好比外部負載均衡的vip等方式被路由到任意一臺node節點,那就能夠經過配置service的externalIPs字段,經過這個IP地址訪問到服務。集羣以這個IP爲目的IP的請求時,會把請求轉發到對應服務。參考官方文檔的例子:
apiVersion: v1 kind: Service metadata: name: my-service spec: selector: app: MyApp ports: - name: http protocol: TCP port: 80 targetPort: 9376 externalIPs: - 80.11.12.10
這裏的80.11.12.10就是一個不禁kubernetes維護的公網IP地址,經過80.11.12.10:80就能夠訪問到服務對應的pod。
簡單總結下,service對象實際上解決的就是一個分佈式系統的服務發現問題,把相同功能的pod作成一個服務集也能很好的對應微服務的架構。在目前的kubernetes版本中,service還只能實現4層的代理轉發,而且要搭配好DNS服務才能真正知足生產環境的需求。