Kubernetes是Google開源的容器集群管理系統,其提供應用部署、維護、 擴展機制等功能,利用Kubernetes能方便地管理跨機器運行容器化的應用,是Docker分布式系統的解決方案。k8s里所有的資源都可以用yaml或Json定義。
1 K8s基本概念
1.1 Master
Master節點負責整個集群的控制和管理,所有的控制命令都是發給它,上面運行著一組關鍵進程:
- kube-apiserver:提供了HTTP REST接口,是k8s所有資源增刪改查等操作的唯一入口,也是集群控制的入口。
- kube-controller-manager:所有資源的自動化控制中心。當集群狀態與期望不同時,kcm會努力讓集群恢復期望狀態,比如:當一個pod死掉,kcm會努力新建一個pod來恢復對應replicas set期望的狀態。
- kube-scheduler:負責Pod的調度。
實際上,Master只是一個名義上的概念,三個關鍵的服務不一定需要運行在一個節點上。
1.1.1 API Server的原理
集群中的各個功能模塊通過 apiserver將信息存儲在Etcd,當需要修改這些信息的時候通過其REST接口來實現。
1.1.2 Controller Manager的原理
內部包含:
- Replication Controller
- Node Controller
- ResourceQuota Controller
- Namespace Controller
- ServiceAccount Controller
- Token Controller
- Service Controller
- Endpoint Controller等
這些Controller通過API Server實時監控各個資源的狀態,當有資源因為故障導致狀態變化,Controller就會嘗試將系統由“現有狀態”恢復到“期待狀態”。
1.1.3 Scheduler的原理
作用是將apiserver或controller manager創建的Pod調度和綁定到具體的Node上,一旦綁定,就由Node上的kubelet接手Pod的接下來的生命周期管理。
1.2 Node
Node是工作負載節點,運行著Master分配的負載(Pod),但一個Node宕機時,其上的負載會被自動轉移到其他Node上。其上運行的關鍵組件是:
- kubelet:負責Pod的生命周期管理,同時與Master密切協作,實現集群管理的基本功能。
- kube-proxy:實現Service的通信與負載均衡機制的重要組件,老版本主要通過設置iptables規則實現,新版1.9基于kube-proxy-lvs 實現。
- Docker Engine:Docker引擎,負責Docker的生命周期管理。
1.2.1 kube-proxy的原理
每個Node上都運行著一個kube-proxy進程,它在本地建立一個SocketServer接收和轉發請求,可以看作是Service的透明代理和負載均衡器,負載均衡策略模式是Round Robin
。也可以設置會話保持,策略使用的是“ClientIP”,將同一個ClientIP的請求轉發同一個Endpoint上。
Service的Cluster IP和NodePort等概念都是kube-proxy服務通過Iptables的NAT轉換實現,Iptables機制針對的是kube-proxy監聽的端口,所以每個Node上都要有kube-proxy。
1.2.2 kubelet原理
每個Node都會啟動一個kubelet,主要作用有:
(1)Node管理
- 注冊節點信息;
- 通過
cAdvisor
監控容器和節點的資源; - 定期向
Master(實際上是apiserver)
匯報本節點資源消耗情況
(2)Pod管理
所以非通過apiserver方式創建的Pod叫Static Pod,這里我們討論的都是通過apiserver創建的普通Pod。kubelet通過apiserver監聽etcd,所有針對Pod的操作都會被監聽到,如果其中有涉及到本節點的Pod,則按照要求進行創建、修改、刪除等操作。
(3)容器健康檢查
kubelet通過兩類探針檢查容器的狀態:
LivenessProbe:判斷一個容器是否健康,如果不健康則會刪除這個容器,并按照restartPolicy看是否重啟這個容器。實現的方式有ExecAction(在容器內部執行一個命令)、TCPSocketAction(如果端口可以被訪問,則健康)、HttpGetAction(如果返回200則健康)。
ReadinessProbe:用于判斷容器是否啟動完全。如果返回的是失敗,則Endpoint Controller會將這個Pod的Endpoint從Service的Endpoint列表中刪除。也就是,不會有請求轉發給它。
1.3 Pod
Pod是k8s進行資源調度的最小單位,每個Pod中運行著一個或多個密切相關的業務容器
,這些業務容器共享這個Pause容器的IP和Volume,我們以這個不易死亡的Pause容器作為Pod的根容器,以它的狀態表示整個容器組的狀態。一個Pod一旦被創建就會放到Etcd中存儲,然后由Master調度到一個Node綁定,由這個Node上的Kubelet進行實例化。
每個Pod會被分配一個單獨的Pod IP,Pod IP + ContainerPort 組成了一個Endpoint
。
1.4 Service
K8s中一個Service相當于一個微服務的概念,一個Service對應后端多個Pod計算實例,使用LabelSelector
將一類Pod都綁定到自己上來。一般還會需要一個Deployment或者RC來幫助這個Service來保證這個Service的服務能力和質量。
1.4.1 kube-proxy負載均衡
運行在每個Node上的kube-proxy其實就是一個智能的軟件負載均衡器,它負載將發給Service的請求轉發到后端對應的Pod,也就是說它負責會話保持和負責均衡。
1.4.2 Cluster IP
負載均衡的基礎是負載均衡器要維護一個后端Endpoint列表,但是Pod的Endpoint會隨著Pod的銷毀和重建而改變,k8s使這個問題透明化。一旦Service被創建,就會立刻分配給它一個Cluster IP,在Service的整個生命周期內,這個Cluster IP不會改變。于是,服務發現的問題也解決了:只要用Service Name和Service Cluster IP做一個DNS域名映射就可以了。
1.4.3 DNS
從Kubernetes 1.3開始,DNS通過使用插件管理系統cluster add-on,成為了一個內建的自啟動服務。Kubernetes DNS在Kubernetes集群上調度了一個DNS Pod和Service,并配置kubelet,使其告訴每個容器使用DNS Service的IP來解析DNS名稱。
(1)Service
集群中定義的每個Service(包括DNS Service它自己)都被分配了一個DNS名稱。默認的,Pod的DNS搜索列表中會包含Pod自己的命名空間和集群的默認域,下面我們用示例來解釋以下。 假設有一個名為foo
的Service,位于命名空間bar中。運行在bar
命名空間中的Pod可以通過DNS查找foo
關鍵字來查找到這個服務,而運行在命名空間quux
中的Pod可以通過關鍵字foo.bar
來查找到這個服務。
普通(非headless)的Service都被分配了一個DNS記錄,該記錄的名稱格式為my-svc.my-namespace.svc.cluster.local
,通過該記錄可以解析出服務的集群IP。 Headless(沒有集群IP)的Service也被分配了一個DNS記錄,名稱格式為my-svc.my-namespace.svc.cluster.local
。與普通Service不同的是,它會解析出Service選擇的Pod的IP列表。
(2)Pod
Pod也可以使用DNS服務。pod會被分配一個DNS記錄,名稱格式為pod-ip-address.my-namespace.pod.cluster.local
。 比如,一個pod,它的IP地址為1.2.3.4
,命名空間為default
,DNS名稱為cluster.local,那么它的記錄就是:1-2-3-4.default.pod.cluster.local
。 當pod被創建時,它的hostname設置在Pod的metadata.name
中。
在v1.2版本中,用戶可以指定一個Pod注解,pod.beta.kubernetes.io/hostname
,用于指定Pod的hostname。這個Pod注解,一旦被指定,就將優先于Pod的名稱,成為pod的hostname。比如,一個Pod,其注解為pod.beta.kubernetes.io/hostname: my-pod-name
,那么該Pod的hostname會被設置為my-pod-name。 v1.2中還引入了一個beta特性,用戶指定Pod注解,pod.beta.kubernetes.io/subdomain
,來指定Pod的subdomain。比如,一個Pod,其hostname注解設置為“foo”
,subdomain注解為“bar”
,命名空間為“my-namespace”
,那么它最終的FQDN就是“foo.bar.my-namespace.svc.cluster.local”
。 在v1.3版本中,PodSpec有了hostname
和subdomain
字段,用于指定Pod的hostname和subdomain。它的優先級則高于上面提到的pod.beta.kubernetes.io/hostname
和pod.beta.kubernetes.io/subdomain
。
1.4.4 外部訪問Service的問題
先明確這樣幾個IP:
-
Node IP
:Node主機的IP,與它是否屬于K8s無關。 -
Pod IP
:是Dokcer Engine通過docker0網橋的IP地址段進行分配的,通常是一個虛擬的二層網絡。k8s中一個Pod訪問另一個Pod就是通過Pod IP。 -
Cluster IP
:僅用于Service對象,屬于k8s的內部IP,外界無法直接訪問。
(1)NodePort
在Service的yaml中定義NodePort,k8s為集群中每個Node都增加對這個端口的監聽,使用這種方式往往需要一個獨立與k8s之外的負載均衡器作為流量的入口。
(2)使用External IP
- 運行Hello World應用程序的五個實例。
- 創建一個暴露外部IP地址的Service對象。
- 使用Service對象訪問正在運行的應用程序。
使用deployment創建暴露的Service對象:
~ kubectl expose deployment hello-world --type=LoadBalancer --name=my-service
顯示關于Service的信息:
~ kubectl get services my-service
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-service 10.3.245.137 104.198.205.71 8080/TCP 54s
~ kubectl describe services my-service
Name: my-service
Namespace: default
Labels: run=load-balancer-example
Selector: run=load-balancer-example
Type: LoadBalancer
IP: 10.3.245.137
LoadBalancer Ingress: 104.198.205.71
Port: <unset> 8080/TCP
NodePort: <unset> 32377/TCP
Endpoints: 10.0.0.6:8080,10.0.1.6:8080,10.0.1.7:8080 + 2 more...
Session Affinity: None
Events:
在此例子中,外部IP地址為104.198.205.71。還要注意Port的值。在這個例子中,端口是8080。在上面的輸出中,您可以看到該服務有多個端點:10.0.0.6:8080,10.0.1.6:8080,10.0.1.7:8080 + 2 more…。這些是運行Hello World應用程序的pod的內部地址。
使用外部IP地址訪問Hello World應用程序:
~ curl http://<external-ip>:<port>
Hello Kubernetes!
刪除服務
~ kubectl delete services my-service
~ kubectl delete deployment hello-world
1.5 Ingress
通常情況下,service和pod僅可在集群內部網絡中通過IP地址訪問。所有到達邊界路由器的流量或被丟棄或被轉發到其他地方。Ingress是授權入站連接到達集群服務的規則集合。你可以給Ingress配置提供外部可訪問的URL、負載均衡、SSL、基于名稱的虛擬主機等。用戶通過POST Ingress資源到API server的方式來請求ingress。 Ingress controller負責實現Ingress,通常使用負載平衡器,它還可以配置邊界路由和其他前端,這有助于以HA方式處理流量。
最簡化的Ingress配置:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test-ingress
spec:
rules:
- http:
paths:
- path: /testpath
backend:
serviceName: test
servicePort: 80
- path: /bar
backend:
serviceName: s2
servicePort: 80
1-4行:跟Kubernetes的其他配置一樣,ingress的配置也需要apiVersion,kind和metadata字段。配置文件的詳細說明請查看部署應用, 配置容器和 使用resources.
5-7行: Ingress spec 中包含配置一個loadbalancer或proxy server的所有信息。最重要的是,它包含了一個匹配所有入站請求的規則列表。目前ingress只支持http規則。
8-9行:每條http規則包含以下信息:一個
host
配置項(比如for.bar.com,在這個例子中默認是*),path
列表(比如:/testpath),每個path
都關聯一個backend
(比如test:80)。在loadbalancer將流量轉發到backend之前,所有的入站請求都要先匹配host和path。10-12行:backend是一個
service:port
的組合。Ingress的流量被轉發到它所匹配的backend。
配置TLS證書
你可以通過指定包含TLS私鑰和證書的secret來加密Ingress。 目前,Ingress僅支持單個TLS端口443,并假定TLS termination。 如果Ingress中的TLS配置部分指定了不同的主機,則它們將根據通過SNI TLS擴展指定的主機名(假如Ingress controller支持SNI)在多個相同端口上進行復用。 TLS secret中必須包含名為tls.crt和tls.key的密鑰,這里面包含了用于TLS的證書和私鑰,例如:
(1)創建Secret
apiVersion: v1
data:
tls.crt: base64 encoded cert
tls.key: base64 encoded key
kind: Secret
metadata:
name: testsecret
namespace: default
type: Opaque
(2)創建Ingress:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: no-rules-map
spec:
tls:
- secretName: testsecret
backend:
serviceName: s1
servicePort: 80
2 高可用
Kubernetes服務本身的穩定運行對集群管理至關重要,影響服務穩定的因素一般來說分為兩種,一種是服務本身異?;蛘叻账跈C器宕機,另一種是因為網絡問題導致的服務不可用。本文將從存儲層、管理層、接入層三個方面介紹高可用Kubernetes集群的原理。
2.1 Etcd高可用方案
Kubernetes的存儲層使用的是Etcd。Etcd是CoreOS開源的一個高可用強一致性的分布式存儲服務,Kubernetes使用Etcd作為數據存儲后端,把需要記錄的pod、rc、service等資源信息存儲在Etcd中。
Etcd使用raft算法將一組主機組成集群,raft 集群中的每個節點都可以根據集群運行的情況在三種狀態間切換:follower, candidate 與 leader。leader 和 follower 之間保持心跳。如果follower在一段時間內沒有收到來自leader的心跳,就會轉為candidate,發出新的選主請求。
集群初始化的時候內部的節點都是follower節點,之后會有一個節點因為沒有收到leader的心跳轉為candidate節點,發起選主請求。當這個節點獲得了大于一半節點的投票后會轉為leader節點。當leader節點服務異常后,其中的某個follower節點因為沒有收到leader的心跳轉為candidate節點,發起選主請求。只要集群中剩余的正常節點數目大于集群內主機數目的一半,Etcd集群就可以正常對外提供服務。
當集群內部的網絡出現故障集群可能會出現“腦裂”問題,這個時候集群會分為一大一小兩個集群(奇數節點的集群),較小的集群會處于異常狀態,較大的集群可以正常對外提供服務。
2.2 Master高可用方案
Master上有三個關鍵的服務:apiserver、controller-manager和scheduler,這三個不一定要運行在一臺主機上。
2.2.1 controller-manager和scheduler的選舉配置
Kubernetes的管理層服務包括kube-scheduler和kube-controller-manager。kube-scheduer和kube-controller-manager使用一主多從的高可用方案,在同一時刻只允許一個服務處以具體的任務。Kubernetes中實現了一套簡單的選主邏輯,依賴Etcd實現scheduler和controller-manager的選主功能。
如果scheduler和controller-manager在啟動的時候設置了leader-elect
參數,它們在啟動后會先嘗試獲取leader節點身份,只有在獲取leader節點身份后才可以執行具體的業務邏輯。它們分別會在Etcd中創建kube-scheduler和kube-controller-manager的endpoint,endpoint的信息中記錄了當前的leader節點信息,以及記錄的上次更新時間。leader節點會定期更新endpoint的信息,維護自己的leader身份。每個從節點的服務都會定期檢查endpoint的信息,如果endpoint的信息在時間范圍內沒有更新,它們會嘗試更新自己為leader節點。
scheduler服務以及controller-manager服務之間不會進行通信,利用Etcd的強一致性,能夠保證在分布式高并發情況下leader節點的全局唯一性。整體方案如下圖所示:
當集群中的leader節點服務異常后,其它節點的服務會嘗試更新自身為leader節點,當有多個節點同時更新endpoint時,由Etcd保證只有一個服務的更新請求能夠成功。通過這種機制sheduler和controller-manager可以保證在leader節點宕機后其它的節點可以順利選主,保證服務故障后快速恢復。當集群中的網絡出現故障時對服務的選主影響不是很大,因為scheduler和controller-manager是依賴Etcd進行選主的,在網絡故障后,可以和Etcd通信的主機依然可以按照之前的邏輯進行選主,就算集群被切分,Etcd也可以保證同一時刻只有一個節點的服務處于leader狀態。
2.2.2 apiserver的高可用
Kubernetes的接入層服務主要是kube-apiserver。apiserver本身是無狀態的服務,它的主要任務職責是把資源數據存儲到Etcd中,后續具體的業務邏輯是由scheduler和controller-manager執行的。所以可以同時起多個apiserver服務,使用nginx把客戶端的流量轉發到不同的后端apiserver上實現接入層的高可用。具體的實現如下圖所示:
接入層的高可用分為兩個部分,一個部分是多活的apiserver服務,另一個部分是一主一備的nginx服務。
2.3 Keepalived簡介
Keepalived
軟件起初是專為LVS
負載均衡軟件設計的,用來管理并監控LVS集群系統中各個服務節點的狀態,后來又加入了可以實現高可用的VRRP
功能。因此,Keepalived除了能夠管理LVS軟件外,還可以作為其他服務(例如:Nginx
、Haproxy
、MySQL
等)的高可用解決方案軟件。Keepalived軟件主要是通過VRRP協議實現高可用功能的。VRRP是Virtual Router RedundancyProtocol(虛擬路由器冗余協議)
的縮寫,VRRP出現的目的就是為了解決靜態路由單點故障
問題的,它能夠保證當個別節點宕機時,整個網絡可以不間斷地運行。所以,Keepalived 一方面具有配置管理LVS的功能,同時還具有對LVS下面節點進行健康檢查的功能,另一方面也可實現系統網絡服務的高可用功能。
<u style="box-sizing: border-box;">故障切換轉移原理</u>
Keepalived高可用服務對之間的故障切換轉移,是通過 VRRP (Virtual Router Redundancy Protocol ,虛擬路由器冗余協議)來實現的。在 Keepalived服務正常工作時,主 Master節點會不斷地向備節點發送(多播的方式)心跳消息,用以告訴備Backup節點自己還活看,當主 Master節點發生故障時,就無法發送心跳消息,備節點也就因此無法繼續檢測到來自主 Master節點的心跳了,于是調用自身的接管程序,接管主Master節點的 IP資源及服務。而當主 Master節點恢復時,備Backup節點又會釋放主節點故障時自身接管的IP資源及服務,恢復到原來的備用角色。
3 容器網絡
3.1 docker默認容器網絡
在默認情況下會看到三個網絡,它們是Docker Deamon進程創建的。它們實際上分別對應了Docker過去的三種『網絡模式』,可以使用docker network ls來查看:
master@ubuntu:~$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
18d934794c74 bridge bridge local
f7a7b763f013 host host local
697354257ae3 none null local
這 3 個網絡包含在 Docker 實現中。運行一個容器時,可以使用 the –net標志指定您希望在哪個網絡上運行該容器。您仍然可以使用這 3 個網絡。
- bridge 網絡表示所有 Docker 安裝中都存在的 docker0 網絡。除非使用 docker run –net=選項另行指定,否則 Docker 守護進程默認情況下會將容器連接到此網絡。在主機上使用 ifconfig命令,可以看到此網橋是主機的網絡堆棧的一部分。
- none 網絡在一個特定于容器的網絡堆棧上添加了一個容器。該容器缺少網絡接口。
- host 網絡在主機網絡堆棧上添加一個容器。您可以發現,容器中的網絡配置與主機相同。
3.2 跨主機通信的方案
和host共享network namespace
這種接入模式下,不會為容器創建網絡協議棧,即容器沒有獨立于host的network namespace,但是容器的其他namespace(如IPC、PID、Mount等)還是和host的namespace獨立的。容器中的進程處于host的網絡環境中,與host共用L2-L4的網絡資源。該方式的優點是,容器能夠直接使用host的網絡資源與外界進行通信,沒有額外的開銷(如NAT),缺點是網絡的隔離性差,容器和host所使用的端口號經常會發生沖突。
和host共享物理網卡
2與1的區別在于,容器和host共享物理網卡,但容器擁有獨立于host的network namespace,容器有自己的MAC地址、IP地址、端口號。這種接入方式主要使用SR-IOV技術,每個容器被分配一個VF,直接通過PCIe網卡與外界通信,優點是旁路了host kernel不占任何計算資源,而且IO速度較快,缺點是VF數量有限且對容器遷移的支持不足。
Behind the POD
這種方式是Google在Kubernetes中的設計中提出來的。Kubernetes中,POD是指一個可以被創建、銷毀、調度、管理的最小的部署單元,一個POD有一個基礎容器以及一個或一組應用容器,基礎容器對應一個獨立的network namespace并擁有一個其它POD可見的IP地址(以IP A.B.C.D指代),應用容器間則共享基礎容器的network namespace(包括MAC、IP以及端口號等),還可以共享基礎容器的其它的namespace(如IPC、PID、Mount等)。POD作為一個整體連接在host的vbridge/vswitch上,使用IP地址A.B.C.D與其它POD進行通信,不同host中的POD處于不同的subnet中,同一host中的不同POD處于同一subnet中。這種方式的優點是一些業務上密切相關的容器可以共享POD的全部資源(它們一般不會產生資源上的沖突),而這些容器間的通信高效便利。
3.3 Flannel
在k8s的網絡設計中,服務以POD為單位,每個POD的IP地址,容器通過Behind the POD方式接入網絡(見“容器的網絡模型”),一個POD中可包含多個容器,這些容器共享該POD的IP地址。另外,k8s要求容器的IP地址都是全網可路由的,那么顯然docker0+iptables的NAT方案是不可行的。
實現上述要求其實有很多種組網方法,Flat L3是一種(如Calico),Hierarchy L3(如Romana)是一種,另外L3 Overlay也是可以的,CoreOS就采用L3 Overlay的方式設計了flannel, 并規定每個host下各個POD屬于同一個subnet,不同的host/VM下的POD屬于不同subnet。我們來看flannel的架構,控制平面上host本地的flanneld負責從遠端的ETCD集群同步本地和其它host上的subnet信息,并為POD分配IP地址。數據平面flannel通過UDP封裝來實現L3 Overlay,既可以選擇一般的TUN設備又可以選擇VxLAN設備(注意,由于圖來源不同,請忽略具體的IP地址)。
flannel是CoreOS提供用于解決Dokcer集群跨主機通訊的覆蓋網絡工具。它的主要思路是:預先留出一個網段,每個主機使用其中一部分,然后每個容器被分配不同的ip;讓所有的容器認為大家在同一個直連的網絡,底層通過UDP/VxLAN
等進行報文的封裝和轉發。
flannel默認使用8285端口作為UDP
封裝報文的端口,VxLan使用8472端口。那么一條網絡報文是怎么從一個容器發送到另外一個容器的呢?
- 容器直接使用目標容器的ip訪問,默認通過容器內部的eth0發送出去。
- 報文通過
veth pair
被發送到vethXXX
。 -
vethXXX
是直接連接到虛擬交換機docker0
的,報文通過虛擬bridge docker0
發送出去。 - 查找路由表,外部容器ip的報文都會轉發到
flannel0
虛擬網卡,這是一個P2P
的虛擬網卡,然后報文就被轉發到監聽在另一端的flanneld
。 -
flanneld
通過etcd
維護了各個節點之間的路由表,把原來的報文UDP
封裝一層,通過配置的iface
發送出去。 - 報文通過主機之間的網絡找到目標主機。
- 報文繼續往上,到傳輸層,交給監聽在8285端口的
flanneld
程序處理。 - 數據被解包,然后發送給
flannel0
虛擬網卡。 - 查找路由表,發現對應容器的報文要交給
docker0
。 -
docker0
找到連到自己的容器,把報文發送過去。
應用部署工具Helm