Preface
本文章是屬於 kubernetes
service 系列文之一,該系列文希望能夠與大家討論下量兩個觀念
- 什麼是
Kubernetes Service
, 為什麼我們需要它? 它能夠幫忙解決什麼問題 Kubernetes Service
是怎麼實現的?, 讓我們用 iptables 來徹徹底底的理解他
相關文章: [Kubernetes] What is Service [Kubernetes] How To Implement Kubernetes Service - ClusterIP [Kubernetes] How To Implement Kubernetes Service - NodePort
本文銜接前續文章,繼續透過對 iptables
的分析來研究 kubernetes service
中 SessionAffinity
的實作原理。
What Is Connection
在我們開始討論 SessionAffinity
之前,我們要先來討論一下,什麼叫做 Connection
, 並且透過對 Connection
的瞭解,我們會更容易的理解到底 SessionAffinity
想要解決的問題。
這邊我們使用下列的圖示來說明到底什麼是 Connection
這個範例中,我們有一個 kubernetes
叢集,其透過 kubernetes service(NodePort)
的方式將內部的 Nginx
服務讓外界能夠存取。
此時,在外部我們有兩台機器想要存取這些 Nginx
服務,其中第一台機器 Host1 上面有一個 Client Application
, 透過 kubernetes service
的功用,其最後存取到了Nginx1
這個容器。
而第二台機器 Host2 上面則有兩個 Client Application
, 其分別對應到的是 Nginx2
以及 Nginx3
這兩個容器。
首先,本文中所有提到的 Connection
代表的就是如上圖所示的 ClientApp
到 Nginx
的連線,所以上述的範例總共有三個 Connection
.
這邊我們先思考一個問題,針對圖中 ConnectionA
來討論一下,該 ConnectionA
中所有的來回請求回應,都會走到 Nginx1
嘛?
這些所謂的請求回應,實際是都是網際網路世界中的封包,每個封包一旦到達 Kubernetes
叢集內就會遇到 iptaables
規則的處理。
在這個情況下,我們如何保證該 ConnectionA
裡面的所有封包都可以送達到同一個 Nginx1
也就是所謂的 EndPoints
.
這個問題其實已經解決了,是透過 iptables
解決的。
這個原理沒有辦法一言兩語解決,我之後若有時間會寫相關的文章介紹這邊的原理與實現。
這邊只要知道其方法是倚賴 Linux Kernel
裡面相關的技術去提供類似 Cache
的機制,確保相同的 Connection
內所有的來回封包都會執行相同的 SNAT/DNAT
.
有興趣的讀者可以使用下列關鍵字去搜尋相關文章,不然就是等我哪天有時間在來仔細介紹這邊的概念lol
- netfilter
- conntrack
- DNAT/SNAT
What Is SessionAffinity
假如相同 Connection
內的封包都已經可以保證連接到相同的 Endpoints
了,那到底什麼是 SessionAffinity
?
換個角度來說,我們有沒有辦法讓建立的新 Connection
都連接到相同的 Endpoints
?
這個問題就是 SessionAffinity
想要解決並處理的,透過一種機制,讓不同的 Client Application
所建立新的 Connection
最後都會連接到相同的 EndPoints
.
以上圖的範例來說,有沒有可能讓 Connection A,B,C
都連接到相同的 EndPoints
?
Configuration
首先,我們先來看一下 Kubernetes
裡面定義的 SessionAffinity
要怎麼使用
目前總共有兩種類型可以選擇
- None
- ClientIP
第一種 None
其實就是什麼都不做,針對每一條新的Connection
都不去採去任何動作,基本上就是每條Connection
都看運氣來選擇最後選擇到哪一個 Endpoints
.
第二種 ClientIP
則是真的有事情要做了,就如同字面上的意思,Client IP
.
目的很簡單,對於每一條新建立的 Connection
, 若其 Client IP
地址相同,就導到相同的 Endpoints
去使用。
所以回到剛剛上述的問題
以上圖的範例來說,有沒有可能讓 Connection A,B,C
都連接到相同的 EndPoints
?
按照目前 kubernetes
的設計,上述的答案是在最簡單的網路架構下,只能夠確保 Host2 上面所建立的所有 Connection 都可以連接到相同的 Endpoints.
這邊假設這些 Host 都有自己的公開 IP
地址,不考慮任何 SNAT
的效果。
How It Works
Setup
接下來,我們繼續使用kubeDemo來進行相關的服務部屬以及iptables
規則研究。
vortex-dev:01:37:28 [~/go/src/github.com/hwchiu/kubeDemo/services](master)vagrant
$kubectl apply -f service/nginx-
nginx-affinity.yml nginx-cluster.yml nginx-node.yml
vortex-dev:01:37:28 [~/go/src/github.com/hwchiu/kubeDemo/services](master)vagrant
$kubectl apply -f service/nginx-affinity.yml
service/k8s-nginx-affinity created
我們用下列指令確認一下剛剛部屬的 kubernetes service
是否真的有設定 sessionAffinity
vortex-dev:01:40:58 [~/go/src/github.com/hwchiu/kubeDemo/services](master)vagrant
$kubectl get service k8s-nginx-affinity -o jsonpath='{.spec.sessionAffinity}'
ClientIP
IPTABLES
按照慣例,最簡單的觀察方式就是直接觀察 iptables
的規則,這邊直接透過 k8s-ngins-affinity
這個關鍵字來查詢所有相關的規則
$sudo iptables-save | grep k8s-nginx-affinity
-A KUBE-SEP-HDMJEKA4BFKBU6OK -s 10.244.0.145/32 -m comment --comment "default/k8s-nginx-affinity:" -j KUBE-MARK-MASQ
-A KUBE-SEP-HDMJEKA4BFKBU6OK -p tcp -m comment --comment "default/k8s-nginx-affinity:" -m recent --set --name KUBE-SEP-HDMJEKA4BFKBU6OK --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 10.244.0.145:80
-A KUBE-SEP-Q5HAFBJX4HVXF6EM -s 10.244.0.144/32 -m comment --comment "default/k8s-nginx-affinity:" -j KUBE-MARK-MASQ
-A KUBE-SEP-Q5HAFBJX4HVXF6EM -p tcp -m comment --comment "default/k8s-nginx-affinity:" -m recent --set --name KUBE-SEP-Q5HAFBJX4HVXF6EM --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 10.244.0.144:80
-A KUBE-SEP-YFKOY7G33LWKGTLC -s 10.244.0.143/32 -m comment --comment "default/k8s-nginx-affinity:" -j KUBE-MARK-MASQ
-A KUBE-SEP-YFKOY7G33LWKGTLC -p tcp -m comment --comment "default/k8s-nginx-affinity:" -m recent --set --name KUBE-SEP-YFKOY7G33LWKGTLC --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 10.244.0.143:80
-A KUBE-SERVICES ! -s 10.244.0.0/16 -d 10.109.59.245/32 -p tcp -m comment --comment "default/k8s-nginx-affinity: cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.109.59.245/32 -p tcp -m comment --comment "default/k8s-nginx-affinity: cluster IP" -m tcp --dport 80 -j KUBE-SVC-UBXGHWUUHMMRNNE6
-A KUBE-SVC-UBXGHWUUHMMRNNE6 -m comment --comment "default/k8s-nginx-affinity:" -m recent --rcheck --seconds 10800 --reap --name KUBE-SEP-YFKOY7G33LWKGTLC --mask 255.255.255.255 --rsource -j KUBE-SEP-YFKOY7G33LWKGTLC
-A KUBE-SVC-UBXGHWUUHMMRNNE6 -m comment --comment "default/k8s-nginx-affinity:" -m recent --rcheck --seconds 10800 --reap --name KUBE-SEP-Q5HAFBJX4HVXF6EM --mask 255.255.255.255 --rsource -j KUBE-SEP-Q5HAFBJX4HVXF6EM
-A KUBE-SVC-UBXGHWUUHMMRNNE6 -m comment --comment "default/k8s-nginx-affinity:" -m recent --rcheck --seconds 10800 --reap --name KUBE-SEP-HDMJEKA4BFKBU6OK --mask 255.255.255.255 --rsource -j KUBE-SEP-HDMJEKA4BFKBU6OK
-A KUBE-SVC-UBXGHWUUHMMRNNE6 -m comment --comment "default/k8s-nginx-affinity:" -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-YFKOY7G33LWKGTLC
-A KUBE-SVC-UBXGHWUUHMMRNNE6 -m comment --comment "default/k8s-nginx-affinity:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-Q5HAFBJX4HVXF6EM
-A KUBE-SVC-UBXGHWUUHMMRNNE6 -m comment --comment "default/k8s-nginx-affinity:" -j KUBE-SEP-HDMJEKA4BFKBU6OK
稍微看了一下可以發現規則數量變多了,每個 Endpoints
本身多出兩條的規則出來,所以此範例中因為有三個Endpoints
,所以總共會多出六條新的規則。
-A KUBE-SEP-HDMJEKA4BFKBU6OK -p tcp -m comment --comment "default/k8s-nginx-affinity:" -m recent --set --name KUBE-SEP-HDMJEKA4BFKBU6OK --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 10.244.0.145:80
-A KUBE-SEP-Q5HAFBJX4HVXF6EM -p tcp -m comment --comment "default/k8s-nginx-affinity:" -m recent --set --name KUBE-SEP-Q5HAFBJX4HVXF6EM --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 10.244.0.144:80
-A KUBE-SEP-YFKOY7G33LWKGTLC -p tcp -m comment --comment "default/k8s-nginx-affinity:" -m recent --set --name KUBE-SEP-YFKOY7G33LWKGTLC --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 10.244.0.143:80
-A KUBE-SVC-UBXGHWUUHMMRNNE6 -m comment --comment "default/k8s-nginx-affinity:" -m recent --rcheck --seconds 10800 --reap --name KUBE-SEP-YFKOY7G33LWKGTLC --mask 255.255.255.255 --rsource -j KUBE-SEP-YFKOY7G33LWKGTLC
-A KUBE-SVC-UBXGHWUUHMMRNNE6 -m comment --comment "default/k8s-nginx-affinity:" -m recent --rcheck --seconds 10800 --reap --name KUBE-SEP-Q5HAFBJX4HVXF6EM --mask 255.255.255.255 --rsource -j KUBE-SEP-Q5HAFBJX4HVXF6EM
-A KUBE-SVC-UBXGHWUUHMMRNNE6 -m comment --comment "default/k8s-nginx-affinity:" -m recent --rcheck --seconds 10800 --reap --name KUBE-SEP-HDMJEKA4BFKBU6OK --mask 255.255.255.255 --rsource -j KUBE-SEP-HDMJEKA4BFKBU6OK
在我們開始研究這些規則之前,我們還是要先來問自己一句話 如果是我們自己來實作這個功能,我們會怎麼實作?
假設需求就是 ClientIP
,相同來源IP
地址所建立的新連線都要分配到相同的 EndPoints
來使用
直覺下,我們可以用類似 Cache
的概念來完成這個功能,其流程如下
- 收到新的連線請求, 檢查該來源
IP
地址是否存在Cache
中 - 若存在,直接使用該
Cache
內關於的目標Endpoints
來使用 - 若不存在,則嘗試從
EndPoints
內挑選出一個目標,並且將結果記錄到Cache
之中.
所以可以將該 cache
分成 Read/Wrtie
兩個功能面向來看待,以下圖來表示
上述的流程看起來滿直觀且合理的,但是這些流程在 iptables
的規則中到底要怎麼完成?
recent modules
我們將前面6條新規則縮減到兩條來單獨觀察就好(因為每個EndPoints會有兩條)
-A KUBE-SEP-HDMJEKA4BFKBU6OK -p tcp -m comment --comment "default/k8s-nginx-affinity:" -m recent --set --name KUBE-SEP-HDMJEKA4BFKBU6OK --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 10.244.0.145:80
-A KUBE-SVC-UBXGHWUUHMMRNNE6 -m comment --comment "default/k8s-nginx-affinity:" -m recent --rcheck --seconds 10800 --reap --name KUBE-SEP-HDMJEKA4BFKBU6OK --mask 255.255.255.255 --rsource -j KUBE-SEP-HDMJEKA4BFKBU6OK
為了加深各位的印象並且能夠順利的解讀 ClusterIP
的原理,需要再次複習一下這張圖片,並且確保知道下圖中各個項目的含意。
Save
首先我們觀察第一條規則,其位於 KUBE-SEP
這個位置,這個其實就是真正執行 DNAT
的 custom chain
.
這邊做的事情與我們假想的流程完全一致, 當選出欲使用的 Endoints
並進行 DNAT
轉換之時,順便將該結果記錄到 Cache
內。
若不存在,則嘗試從 EndPoints
內挑選出一個目標,並且將結果記錄到 Cache
之中.
我們來仔細看一下這條規則
-A KUBE-SEP-HDMJEKA4BFKBU6OK -p tcp -m comment --comment "default/k8s-nginx-affinity:" -m recent --set --name KUBE-SEP-HDMJEKA4BFKBU6OK --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 10.244.0.145:80
裡面新增加的部份則是
-m recent --set --name KUBE-SEP-HDMJEKA4BFKBU6OK --mask 255.255.255.255 --rsource
這邊我們要介紹一個新的 iptables
的擴充模組 recent. 但是礙於篇幅沒有辦法詳細介紹其所有用法以及原理。
我們可以將 recent
想成他提供一個簡單的類似 key/value
的 cache
機制,同時支援 Read/Write 等操作來存取該 Cache
.
這邊就針對這參數進行一個簡單的介紹
- -m recent: 使用擴充模組
recent
- --set: 這次的行為想要進行儲存的動作,將某些
key/value
寫進到recent cache
內 - --name KUBE-SEP-XXXXXXXX: 這邊對應的就是存到
cache
內的Value
. - --mask 255.255.255.255: 這個搭配下一個參數使用
- --rsource: 這邊代表是的我要用什麼當做
key
, 這邊使用的是souruce
就是所謂的封包來源IP
地址,既然有IP
地址,就可以搭配前面的mask
來調整IP
位址的範圍,這個範例中就是/32的設定,意味IP
要完全一樣才行。
所以歸納一下,若今天已經選定了一個Endpoints
要來使用,首先會先跳到屬於該 Endpoints
專屬於的 custom chani
KUBE-SEP-HDMJEKA4BFKBU6OK.
在進行 DNAT
之前,會先透過 recent cache
的方式去紀錄下列的對映關係
[來源IP地址] => KUBE-SEP-HDMJEKA4BFKBU6OK
將上述的概念重新整理,目前的已知拼圖如下
另外,之前有提到過 iptables
的每個指令都是符合特定規則
,執行特定行為
.
所以其實 recent
模組內關於 Set/Write
相關的操作永遠都是回傳符合
,讓上層的規則可以繼續往下執行。
畢竟針對 Set/Write
這類型操作本身就沒有要比對任何東西,只是被拿來進行其他的操作而已。
Read
看完了第一題規則後,接下來來看一下最後一條,其實也就是第二條規則
-A KUBE-SVC-UBXGHWUUHMMRNNE6 -m comment --comment "default/k8s-nginx-affinity:" -m recent --rcheck --seconds 10800 --reap --name KUBE-SEP-HDMJEKA4BFKBU6OK --mask 255.255.255.255 --rsource -j KUBE-SEP-HDMJEKA4BFKBU6OK
這條規則其實就是 Cache
裡面關於 Read
的操作,但是這邊有一個點要注意,因為 iptables
的規則就是一條一條根據比對條件
來判斷要不要執行特定行為
.
所以這邊沒有辦法用程式化的方式去從 Cache
裡面取得對應的 EndPoints
名稱。
我們先記住,該 recent
提供的方式是詢問請問該Key
有沒有資料,有的話是不是這個Value
。
在這種情況下,我們可以想像一下其運作原理。
針對每一條 KUBE-SVC-XXX
裡面的規則,依序每個 EndPoints
執行下列操作
- 請問
Cachue
裡面有沒有來源IP位址
=>當前EndPoints
的紀錄? 有的話就直接跳到對應的Endooints
的custom chain
去執行DNAT
. - 如果沒有的話,嘗試第二個
Endpoints
- 所有的
Endpoints
嘗試後都沒有結果,那就透過機率的方式選擇一個可用的Endpoints
有了這些概念後,我們從參數的部分來直接看一下 iptables
實際上的下法
- -m:recent 使用擴充模組
recent
- --rcheck: 這邊我們執行
READ
的指令,要檢查cache
內是否有對應的key/value
- --name:
value
, 就是Endpoints
對應到的custom-chain name
- --rsouce/--mask:
key
, 封包的來源IP
- --seconds: 每個
cache
內的記錄都會有一個過期的時間,這個時間的意思是只有上次設定該ket/value
的時間距離現在N
秒內的才算數,已這個範例來說就是10800秒
內的cache
記錄才算數,如果是超過10800秒
前記錄的,就當失效。 - --reap: 這個是指每次查詢的時候,會將已經超過
有效時間
的規則一併清除。
把這整個流程全部重新組合後,我們用下面的這張圖來描述關於整個 SessionAffinity
的概念與實作
seconds
相關的概念我就沒有加入到該圖片中了,因為篇幅有限,針對主要觀念去描述即可。
Summary
看到這邊,我們大概瞭解如何透過 iptables
的功能來達成 SessionAffinity:ClientIP
的功能,透過 iptables
的擴充模組 recent
提供類似 key/value
的 cache
機制來紀錄 來源IP地址
與 Endpoints
的關係。
如果對於 iptables
擴充模組有興趣的讀者,之後我會撰寫一些文章介紹 iptables
的架構以及如何自己撰寫一個 iptables
的擴充模組。
最後我們將本篇文章所學的概念與一直以來使用的關係圖給整合起來,當設定 SessionAffinity
時,我們會在KUBE-SVC
嘗試透過 Recent/Cache
的方式找到是否有使用過的 Endpoints
之後再真正執行 DNAT
的 KUBE-SEP-XXXX
時會不停的更新 Recent/Cache
內的資料以及時間,避免該筆資料過期。