K3s のロードバランサー (Klipper LB) の負荷分散方法

このページには 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 は省略)。

Klipper LB hehavior

この絵からわかるように Pod への負荷分散はされますが、External-IP として選出されたノードにトラフィックが収集してしまいます。 External-IP に選出されるノードの候補を指定できるわけではなさそうなので、クラスターのいずれかノードが選出されたときにそのノードにトラフィックが集中してもボトルネックにならないようなノード構成にする必要があります。

おおよその Klipper LB の挙動は以上のとおりになっています。