KubernetesロードバランサーのMetalLBを導入した話(Necoプロジェクト体験入部)

こんにちは。kintoneメンテチームの石井です。

サイボウズでは、cybozu.comのアーキテクチャ刷新プロジェクト「Neco」を実施しています。 今回、サイボウズの「大人の体験入部」の制度を利用し、Necoプロジェクトの体験入部をしてきたので、その内容をレポートします。

本記事では、体験入部中に行なったタスクの紹介をします。 「Neco」プロジェクトや「大人の体験入部」について知りたい方は、以下の記事を参照ください。

こんなことしてました

Necoでの体験入部中、いくつかのタスクを体験(勉強?)させてもらいました。 今回はその中の一つ、ロードバランサー(MetalLB)の導入について説明します。

Kubernetesとネットワーク

はじめに、Kubernetesとネットワークについて少しだけ説明しておきます。

Kubernetesとは、分散システム上で、コンテナ化したアプリケーションの管理をしてくれるシステムです。 システムのあるべき姿を宣言(設定)しておくと、Kubernetesが自律的にコンテナのデプロイやスケール、障害の復旧もしてくれる優れもので、 これにより、大規模なデータセンターでも、少人数で管理ができるようにしようというものです。 とても便利なシステムではありますが、Kubenetesクラスタ(Kubenetesが管理するサーバのまとまりのこと)を構築するには、 いろいろとやらなければいけないことがあり、その1つにネットワークの設定があります。

Kubernetesは、データセンターのような多くのサーバからなる分散システム上での動作を想定しているのですが、 サーバ間の通信方法はデータセンター内のネットワーク構成に依存するため、Kubernetesとしての標準実装を提供できません。

例えば、Pod(Kubernetesでのアプリケーションの動作単位)を作成する場合、 PodへのIPアドレスの割り当てや、経路情報の広報が必要になるのですが、 IPアドレスの体系や、通信経路の冗長度、ルーティングプロトコル等は、データセンター毎に異なるので、 利用者(Kubernetesクラスタを構築する人)が環境に合わせた設定をしなければいけません。

そのため、利用者側で具体的な通信方式をプライグインとして追加できるように、 Kubernetesでは、通信に必要な要件とインターフェースだけを定義しています。

GCPやAWSといったIaaSでは、クラウドベンダーがプラグインを作成してくれているので、 サービス利用者側がこういったことを意識することはあまりありません。 しかし、Necoでは、自社データセンター内にKubernetesクラスタを構築するため、プラグインの選定や実装をする必要がありました。

Necoのネットワーク

Necoのネットワークについては、このブログ(Cybozu Inside Out)でもいくつか記載しています。

詳細は、上記記事を参照いただければと思いますが、これまで以下のような事を実施してきました。

  • CLOSアーキテクチャを採用し、East-Westトラフィック(同クラスタ内のサーバ間の通信)が増大してもスケールしやすいネットワークの設計
  • BGPを使用して、ネットワーク機器ベンダの独自機能に依存しない、高信頼なネットワークの実現
  • 大規模クラスタでの経路情報の増大を防ぐために、ネットワークプラグインcoilの実装

これらにより、Kubernetesクラスタ内での通信は問題なく動作するようになりました。 簡単に表現すると、以下の図のようなイメージです。ただし、まだ外部からの通信の対応はできていませんでした。

内部通信
内部通信

Kubernetesのロードバランサー

Kubernetesでは、負荷分散や冗長化のため、Serviceと呼ばれる単位で複数のPodをひとまとめに扱うことができます。

ServiceにはClusterIPLoadBalancerなどのタイプがあり、うちLoadBalancerタイプが外部通信の負荷分散用とされています。 例えば、レプリカ数3でnginxのコンテナを動かす、といったLoadBalancerタイプのServiceを設定した場合、Kubernetesが3つのPodでうまいこと負荷分散してくれます。

クラスタ外部からアプリケーションにアクセスする場合は、LoadBalancerタイプのServiceに割り当てられた公開用IPアドレス(External IP)に対してアクセスします。 すると以下の図のように、ロードバランサーによって動作中のPodにアクセスが振り分けられます。

k8sロードバランサー概要
k8sロードバランサー概要

ただし前の節で説明したように、Kubernetesではネットワークに関する標準実装は提供していません。 ロードバランサーについても、外部からExternal IPへのルーティングや、各Podへのアクセスの振り分け方法は、利用者(Kubernetesクラスタを構築する人)側でプラグインの選定や実装をする必要がありました。

そこで今回のタスクでは、社内(現行のcybozu.com)から接続する際のロードバランサーの実現性検証と各種設定を行いました。

MetalLB

Necoでは、以下の要件から社内向けロードバランサーとして、MetalLBというロードバランサーを使うことを計画しており、 今回のタスクで、MetalLBをNecoに導入する場合ための実現性検証と各種設定を行いました。

  • 自社データセンターのベアメタル環境で利用する
  • 経路制御は BGP で行う

MetalLBとはベアメタル環境で使用できるKubernetesのExternal Load Balancerの実装の一種です。 Googleによって開発されたシンプルなロードバランサーで、LoadBalancerタイプのServiceに対する公開用IPアドレス(External IP)の割り当てと、 External IPに対する経路情報の広報、といったの2つの機能を持ちます。

少し実装に踏み込んだ話をしますと、MetalLBはControllerSpeakerの2種類のモジュールで構成されており、 External IPの割り当てと、経路情報の広報は次の図のように実施されます。

MetalLBの経路情報の広報
MetalLBの経路情報の広報

そして、外部からのアクセスがあった場合、設定された経路情報をもとに、ルータやipvsが接続先を調整してくれます。 (そのため、MetalLB自体がロードバランスをしている感じはないですね。)

MetalLBのロードバランス
MetalLBのロードバランス

Necoに入れたらこうなった

Necoでは、各Node上でBIRDというルーティングソフトウェアが動作しており、BGPで経路の交換を行なっています。 そこで、MetalLBからの経路の広報先を自NodeのBIRDに設定し、BIRDから経路の広報を行うようにしました。

下の図は、MetalLBから外部までどのように経路が広報されるか(≒通信経路)を示した図になります。 各Nodeで動作するMetalLBから、BIRD、各種Switchと、経路情報が広報されていき、 外部からKubernetes上で動作するPodにアクセスできるようになりました。

Neco + MetalLB
Neco + MetalLB

外部からアクセスされた場合、以下の図のようにルーティングされます。 各種Switchから各NodeまではECMP(Equal-Cost Multi-Path routing)でルーティングされます。 そして、各Nodeに到達したパケットは、ipvsにより各Podに振り分けられるようになりました。

Necoルーティング
Necoルーティング

MetalLB導入時の問題点

このように書くとすんなりできたように見えますが、以下のような問題がありました。

externalTrafficPolicy=Localを指定するとパケットがロストする(kube-proxy v1.13の不具合)

LoadBalancerタイプのServiceにはexternalTrafficPolicyというオプションがあり、kube-proxyでアクセス先のPodを振り分ける際に、 クラスタワイドに(Nodeをまたいで)アクセス先を振り分けるか、それともノードローカルのPodにだけ振り分けるか、を設定することができます。

NecoではNode間の通信を少なくするためexternalTrafficPolicy=LocalとしてノードローカルのPodにだけアクセスを振り分ける設定にしたのですが、 この時外部からPodに対してアクセスすると一定の割合でパケットがロストするといった事象が発生しました。

Necoでは、Kubernetes v1.13を使用しているのですが、このバージョンのkube-proxyには 以下の条件でパケットがロストしてしまうという不具合があり、それが原因でした。

  • kube-proxyをipvsモードで動作させる
  • Serviceの設定でexternalTrafficPolicy:Localを指定する

この不具合はKubernetes v1.14で改修されていたため、修正パッチをあてて対応しました。

https://github.com/kubernetes/kubernetes/issues/71596

kube-proxyのオプション指定誤り(kubeletとkube-proxyの両方で--hostname-overrideを指定する)

もう一つが、kube-proxyの--hostname-overrideオプション指定の誤りです。

kubelet(Podを管理するモジュール)は、デフォルトでは/etc/hostnameの値をNode名として扱うのですが、 Node名を別に設定したい場合は--hostname-overrideオプションでNode名を指定することができます。

いままで、kubeletだけがNode名を使用していると認識していたのですが、 externalTrafficPolicy=Localを指定したLoadBalancerタイプのServiceで、ノードローカルなPodがあるかどうかを判定する際に、 kube-proxyがNode名を参照していることが分かりました。

Necoでは、kubeletだけ--hostname-overideオプションを指定していたため、kubeletとkube-proxyでNode名の判定で不整合が出てしまい動作しませんでした。

こちらはCKEを改修し、kube-proxyの起動時に--hostname-overideオプションを指定するようにしました。

最終的にどうなった?

MetalLBを導入することで、外部からNecoのKubernetesクラスタにアクセスできるようになりました。

Kubernetesクラスタ上で動作するServiceにグローバルIPアドレスを割当て、 現行のcybozu.com(社内データセンター)及びInternet(社外)からアクセスできることを確認しました。

おわりに

今回の記事では、「大人の体験入部」として「Neco」プロジェクトで行ったタスクの一部を紹介しました。

Necoプロジェクトでは、まだまだやらなければいけないことが沢山あり、一緒に働いてくれる方を募集しています。 今回の内容に興味がある、やってみたい、そもそも俺だったら一瞬で終わらせてやるぜ!っといった方がおりましたら、 ぜひご応募ください。We are hiring!