Kubernetes架構學習筆記
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服務本身的穩定執行對叢集管理至關重要,影響服務穩定的因素一般來說分為兩種,一種是服務本身異常或者服務所在機器宕機,另一種是因為網路問題導致的服務不可用。本文將從儲存層、管理層、接入層三個方面介紹高可用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下面節點進行健康檢查的功能,另一方面也可實現系統網路服務的高可用功能。
故障切換轉移原理
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來檢視:
[email protected]:~$ 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