このページには K3s が提供しているロードバランサー Klipper LB (K3s v1.20.2+k3s1 版) の負荷分散の方法について書かれています。
Klipper Load Balancer #
K3s には Klipper Load Balancer というロードバランサーが組み込まれており、オンプレ環境でも簡単に LoadBalancer Service を作成することができます。
公式ドキュメントでは次のように説明しています。
K3s creates a controller that creates a Pod for the service load balancer, which is a Kubernetes object of kind Service.
For each service load balancer, a DaemonSet is created. The DaemonSet creates a pod with the svc prefix on each node.
The Service LB controller listens for other Kubernetes Services. After it finds a Service, it creates a proxy Pod for the service using a DaemonSet on all of the nodes. This Pod becomes a proxy to the other Service, so that for example, requests coming to port 8000 on a node could be routed to your workload on port 8888.
If the Service LB runs on a node that has an external IP, it uses the external IP.
If multiple Services are created, a separate DaemonSet is created for each Service.
It is possible to run multiple Services on the same node, as long as they use different ports.
If you try to create a Service LB that listens on port 80, the Service LB will try to find a free host in the cluster for port 80. If no host with that port is available, the LB will stay in Pending.
挙動確認 #
Klipper LB の挙動について確認していきます。
動作確認環境 #
K3s サーバー3台、K3s エージェント2台、それぞれ v1.20.2 の環境です。
$ kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k3s-agent1 Ready <none> 182d v1.20.2+k3s1 192.168.0.81 <none> Ubuntu 20.04.1 LTS 5.4.0-1035-raspi containerd://1.4.3-k3s1
k3s-agent2 Ready <none> 182d v1.20.2+k3s1 192.168.0.82 <none> Ubuntu 20.04.1 LTS 5.4.0-1035-raspi containerd://1.4.3-k3s1
k3s-server1 Ready control-plane,etcd,master 183d v1.20.2+k3s1 192.168.0.71 <none> Raspbian GNU/Linux 10 (buster) 5.4.83-v7+ containerd://1.4.3-k3s1
k3s-server2 Ready control-plane,etcd,master 183d v1.20.2+k3s1 192.168.0.72 <none> Raspbian GNU/Linux 10 (buster) 5.4.83-v7+ containerd://1.4.3-k3s1
k3s-server3 Ready control-plane,etcd,master 183d v1.20.2+k3s1 192.168.0.73 <none> Raspbian GNU/Linux 10 (buster) 5.4.83-v7+ containerd://1.4.3-k3s1
実行例 #
例として、Nginx の Deployment リソースとそれに接続するための LoadBalancer Service を作成します。
マニフェストのサンプルは次のとおりです。
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-lb-deployment
spec:
replicas: 3
selector:
matchLabels:
app: test-lb
template:
metadata:
labels:
app: test-lb
spec:
containers:
- name: nginx-container
image: nginx:1.12
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: test-lb
spec:
type: LoadBalancer
ports:
- name: "http-port"
protocol: "TCP"
port: 18080
targetPort: 80
selector:
app: test-lb
このマニフェストを apply して出来上がったリソースを見てみてみます。
$ kubectl get deployment,svc,daemonset,po -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
deployment.apps/test-lb-deployment 3/3 3 3 44m nginx-container nginx:1.12 app=test-lb-replica
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 183d <none>
service/test-lb LoadBalancer 10.43.16.40 192.168.0.73 18080:32651/TCP 44m app=test-lb-replica
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE CONTAINERS IMAGES SELECTOR
daemonset.apps/svclb-test-lb 5 5 5 5 5 <none> 44m lb-port-18080 rancher/klipper-lb:v0.1.2 app=svclb-test-lb
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/svclb-test-lb-52crh 1/1 Running 0 44m 10.42.4.21 k3s-agent2 <none> <none>
pod/svclb-test-lb-bchk8 1/1 Running 0 44m 10.42.0.46 k3s-server1 <none> <none>
pod/svclb-test-lb-czlf5 1/1 Running 0 44m 10.42.2.16 k3s-server3 <none> <none>
pod/svclb-test-lb-q22lq 1/1 Running 0 44m 10.42.1.17 k3s-server2 <none> <none>
pod/svclb-test-lb-vvflr 1/1 Running 0 44m 10.42.3.16 k3s-agent1 <none> <none>
pod/test-lb-deployment-7dd99947b9-2qggg 1/1 Running 0 44m 10.42.3.15 k3s-agent1 <none> <none>
pod/test-lb-deployment-7dd99947b9-5k89s 1/1 Running 0 44m 10.42.4.20 k3s-agent2 <none> <none>
pod/test-lb-deployment-7dd99947b9-wvpbk 1/1 Running 0 44m 10.42.3.14 k3s-agent1 <none> <none>
マニフェストで定義した Nginx の Deployment と LoadBalancer Service が出来上がっています。
それ以外に、公式の説明のとおり、svc
から始まるロードバランサー用の DaemonSet リソースもできています。
ただここで注意しなければならないことは、Pod を見てみるとわかりますが、ロードバランサー用の DaemonSet リソースが K3s Server のノードにもできていることです。
その結果、この例のように LoadBalancer Service が指す External-IP は K3s Server の IP アドレスになることがあります。
K3s のソースコードまで確認していないため正確なアルゴリズムはわかりませんが、何度かマニフェストを delete 、apply を繰り返してみると External-IP には毎回異なるノードの IP アドレスに割り振られていました。 また、ロードバランサー用の DaemonSet リソースを作成するノードから K3s Server を外す方法は公式ドキュメント等を探しても見当たらりませんでした。
ネットワークの挙動 #
Klipper LB の DaemonSet の Pod が行っていることは、External-IP に指定されたノードで、負荷分散させる Pod へトラフィックを転送するために iptables を書き換えることです。
External-IP に指定されたノードの iptables を確認してみます。 下記の実行結果は Klipper LB によって追加されたところのみ抽出しています。
$ sudo iptables -L -t nat
// PREROUTING として KUBE-SERVICES を適用する
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
KUBE-SERVICES all -- anywhere anywhere /* kubernetes service portals */
DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL
CNI-HOSTPORT-DNAT all -- anywhere anywhere ADDRTYPE match dst-type LOCAL
// 192.168.0.73:18080 宛に来た TCP トラフィックに KUBE-FW-CF5PVMP3SUQAMXIP を適用する
Chain KUBE-SERVICES (2 references)
target prot opt source destination
KUBE-MARK-MASQ tcp -- !10.42.0.0/16 10.43.16.40 tcp dpt:18080 /* default/test-lb:http-port cluster IP */
KUBE-SVC-CF5PVMP3SUQAMXIP tcp -- anywhere 10.43.16.40 tcp dpt:18080 /* default/test-lb:http-port cluster IP */
KUBE-FW-CF5PVMP3SUQAMXIP tcp -- anywhere 192.168.0.73 tcp dpt:18080 /* default/test-lb:http-port loadbalancer IP */
// すべてのトラフィックに KUBE-SVC-CF5PVMP3SUQAMXIP を適用する
Chain KUBE-FW-CF5PVMP3SUQAMXIP (1 references)
target prot opt source destination
KUBE-MARK-MASQ all -- anywhere anywhere /* default/test-lb:http-port loadbalancer IP */
KUBE-SVC-CF5PVMP3SUQAMXIP all -- anywhere anywhere /* default/test-lb:http-port loadbalancer IP */
KUBE-MARK-DROP all -- anywhere anywhere /* default/test-lb:http-port loadbalancer IP */
// それぞれ 1/3 の確率で下記のいずれかのルールを適用する
Chain KUBE-SVC-CF5PVMP3SUQAMXIP (3 references)
target prot opt source destination
KUBE-SEP-CPGO6MPRYS5TUAR7 all -- anywhere anywhere statistic mode random probability 0.33333333349 /* default/test-lb:http-port */
KUBE-SEP-XXXSUQ6SEQOMRGB4 all -- anywhere anywhere statistic mode random probability 0.50000000000 /* default/test-lb:http-port */
KUBE-SEP-3ES3K6FV5B2OLAYR all -- anywhere anywhere /* default/test-lb:http-port */
// NGINX の3つの Pod のうちの1つ(10.42.3.14:80)に転送する
Chain KUBE-SEP-CPGO6MPRYS5TUAR7 (1 references)
target prot opt source destination
KUBE-MARK-MASQ all -- 10.42.3.14 anywhere /* default/test-lb:http-port */
DNAT tcp -- anywhere anywhere tcp /* default/test-lb:http-port */ to:10.42.3.14:80
// NGINX の3つの Pod のうちの1つ(10.42.3.15:80)に転送する
Chain KUBE-SEP-XXXSUQ6SEQOMRGB4 (1 references)
target prot opt source destination
KUBE-MARK-MASQ all -- 10.42.3.15 anywhere /* default/test-lb:http-port */
DNAT tcp -- anywhere anywhere tcp /* default/test-lb:http-port */ to:10.42.3.15:80
// NGINX の3つの Pod のうちの1つ(10.42.4.20:80)に転送する
Chain KUBE-SEP-3ES3K6FV5B2OLAYR (1 references)
target prot opt source destination
KUBE-MARK-MASQ all -- 10.42.4.20 anywhere /* default/test-lb:http-port */
DNAT tcp -- anywhere anywhere tcp /* default/test-lb:http-port */ to:10.42.4.20:80
External-IP に指定されているノードにトラフィックが入ってくると Nginx の Deployment リソースとして生成された Pod に等確率で分散されるように iptables に追加されていることがわかります。
これを簡単に絵で表すと次のようになります(Cluster IP などに関わる iptables での NAPT は省略)。
この絵からわかるように Pod への負荷分散はされますが、External-IP として選出されたノードにトラフィックが収集してしまいます。 External-IP に選出されるノードの候補を指定できるわけではなさそうなので、クラスターのいずれかノードが選出されたときにそのノードにトラフィックが集中してもボトルネックにならないようなノード構成にする必要があります。
おおよその Klipper LB の挙動は以上のとおりになっています。