TT Lab
ブログ

한국어 English 日本語 中文

はじめる
ブログ

ブログ

閉鎖網 k0s マルチノード実測 2: ノード2台、コントローラーが90秒停止している間もワーカーは応答し続けた

状態: 実測 (2026-10-31)。 TT Labの仮想マシン(VM)環境にk0s v1.37.1+k0s.0をk0sctl v0.33.1で構築して測定した値です。2台のシナリオはVMを新規作成して 最初から2回実行し、どちらの結果も表に記載しています。閉鎖網の構築方法は 第1部と同様です。ノードのアドレスは例文用の 帯域(192.0.2.0/24)に書き換えています。作成ルール: 実測データを基本とし、測定できなかったものは「未測定」、原因を確認できなかった解釈は「推定」と表記します。

目次

1. 結論から

2. なぜ2台構成は「コントローラー1台+ワーカー1台」なのか

k0sのマルチコントローラー構成ではetcdを使用します。etcdはメンバーの過半数が同意しなければ書き込みを受け付けません。 etcd FAQの表を まとると以下のようになります。

メンバー数 過半数 耐障害性
1 1 0
2 2 0
3 2 1

メンバー2個は1個の時と耐障害性が変わらず、障害に耐えられるサーバーが1台増えるだけです。そのため「どちらもコントローラー」にする構成は、1台のコントローラーよりもむしろ劣ります。点検のために1台を停止させると、残る1台のAPIまで書き込みが停止します。2台構成で実際に選択すべきなのは コントローラー1台+ワーカー1台 であり、残る疑問はコントローラーがワーカーを兼任するかどうかです。

今回は兼任させませんでした。兼任はノードが3台以上の場合に用います(第3部)。同様の問題をデータベースと検索エンジンの観点から測定した記事として、 サーバー2台でHAが実現できない理由 シリーズがあります。

3. k0sctlによる構築

1台目は手動で構築しましたが、2台目以降は k0sctlを使用します。持ち込み用サーバーからSSH経由で各ノードにバイナリやバンドルを転送し、設定を書き込み、参加トークンを作成してワーカーを紐付ける作業を1つのファイルで宣言します。閉鎖網で使用した設定は以下の通りです。

apiVersion: k0sctl.k0sproject.io/v1beta1
kind: Cluster
metadata: {name: k0s-two}
spec:
  hosts:
  - role: controller
    ssh: {address: 192.0.2.3, user: root, keyPath: /root/.ssh/id_ed25519}
    hostname: ctrl01
    privateInterface: eth1
    privateAddress: 192.0.2.3
    uploadBinary: true
    k0sBinaryPath: /opt/airgap/bin/k0s-v1.37.1+k0s.0-amd64
  - role: worker
    ssh: {address: 192.0.2.4, user: root, keyPath: /root/.ssh/id_ed25519}
    hostname: worker01
    privateInterface: eth1
    privateAddress: 192.0.2.4
    uploadBinary: true
    k0sBinaryPath: /opt/airgap/bin/k0s-v1.37.1+k0s.0-amd64
    installFlags: [--kubelet-extra-args=--node-ip=192.0.2.4]
    files:
    - {name: k0s-airgap-bundle, src: /opt/airgap/images/k0s-airgap-bundle-v1.37.1+k0s.0-linux-amd64.tar, dstDir: /var/lib/k0s/images/, perm: "0644"}
    - {name: apps-bundle, src: /opt/airgap/images/apps-bundle.tar, dstDir: /var/lib/k0s/images/, perm: "0644"}
  k0s:
    version: v1.37.1+k0s.0
    config:
      apiVersion: k0s.k0sproject.io/v1beta1
      kind: ClusterConfig
      metadata: {name: k0s}
      spec:
        api: {onlyBindToAddress: true}     # NIC 가 둘이면 필요하다(3.1)
        network: {provider: kuberouter, podCIDR: 172.20.0.0/16, serviceCIDR: 172.21.0.0/16}
        images: {default_pull_policy: Never}
        telemetry: {enabled: false}

閉鎖網において意味のある部分に絞って解説します。

持ち込み用サーバーから k0sctl apply を実行したところ、2回の実行でそれぞれ 1分58秒、2分44秒(k0sctlのレポート)で完了しました。同じ設定であっても実行ごとにこれだけのばらつきが生じましたが、その原因については追調査していません。純粋なワーカーの場合、k0sctlが「waiting for node to become ready」としてReadyになるまで待機してくれるため、applyが完了した時点でワーカーはすでにReadyになっていました。この待機処理がコントローラー兼ワーカーには存在しないという点については、第3部で改めて取り上げます。

3.1 罠: 参加トークンが予期せぬアドレスを指していた

最初は onlyBindToAddress なしで privateInterface·privateAddress のみを記述しました。社内網のアドレスを使うよう明記しているため問題なく動作するはずでしたが、ワーカーの参加処理が172秒間フリーズした末に失敗しました。

failed to connect to kubernetes api using the join token ...
The connection to the server <NAT 주소>:6443 was refused
조인 토큰이 엉뚱한 주소를 가리킨 이유 k0sctl v0.33.1, 노드 주소는 예시 노드 VM 마다 NIC 가 둘 enp1s0(기본 경로) NAT 주소, 모든 VM 이 같다 eth1(사내망) 192.0.2.3 privateAddress 만 줌 k0sctl: etcd 피어·node-ip 에만 spec.api.address 는 비워 둠 k0s: 첫 비루프백 주소를 고름 → NAT 주소 조인 토큰도 NAT 주소:6443 → 172초 뒤 connection refused onlyBindToAddress: true k0sctl 이 호스트마다 address 를 privateAddress 로 → 192.0.2.3 API 는 eth1 주소에만 bind 조인 토큰도 사내망 주소 → 2대 설치 1분58초, rc=0
図: privateAddressのみを指定した場合、APIアドレスはk0sがデフォルトルートのNICから選択し、onlyBindToAddressを指定するとk0sctlがホストごとに社内網のアドレスを設定する

<NAT 주소> はこれらテスト用VMのデフォルトルートNICのアドレスです。KubeVirtのマスカレードネットワークはすべてのVMに同一のNATアドレスを割り当てるため、ワーカーがそのアドレスの6443番ポートにアクセスすると、 自分自身に通信することになります。そこにはAPIが存在しないため、拒否されます。

なぜトークンにそのアドレスが含まれていたのかは、k0sctlのソースコードで確認しました。 configure_k0s.goは privateAddress をetcdピアアドレス(spec.storage.etcd.peerAddress)に使用しますが、 spec.api.address は onlyBindToAddress がtrueの場合のみ 設定します。空欄の場合、k0sが決定しますが、 設定ドキュメントによるとデフォルト値は「ノードで最初に検出された非ループバックアドレス」であり、このVMの場合はデフォルトルートNICのNATアドレスになっていました。参加トークンにはこのアドレスが格納されます。

spec.api.onlyBindToAddress: true を指定すると、2つの要素が同時に変更されます。k0sctlがホストごとに address をそのホストの privateAddress で埋め、APIサーバーはすべてのインターフェースではなくそのアドレスのみにバインドします。k0sの設定ドキュメントでも、このオプションは「multi-homed control plane nodes」において必要になる場合があると記載されています。修正後、2台のインストールは1分58秒、rc=0で完了しました。

第3部の3台のコントローラー構成では、この方法が実質唯一の手段となります。k0sctlの config はクラスター内で1つしか存在しないため、コントローラーごとに異なる address を指定する箇所がありません。同じ問題を扱ったk0sctlのissuesが3件オープンされています。 #1026(HAコントローラーごとに異なるアドレスを指定できない)、 #1035(ワーカーがAPIにプライベートアドレスで接続できない)、 #760(privateInterfaceが適切なアドレスを選択しない)。

4. コントローラーを90秒間停止させると

4.1 測定方法

ワーカー上でnginxの2つのインスタンス(Deployment web)とNodePort Service(30080)を起動した後、コントローラーの k0scontroller サービスを停止しました。持ち込み用サーバーから約5秒間隔で以下の2点を記録しました。

経過時間20秒ほどで、ワーカー上のnginxの最も古いプロセス、すなわちマスタープロセスを強制終了しました(pkill -o -x nginx)。APIが存在しない状態でコンテナが終了した場合、誰が再起動させるのかを確認するためです。90秒経過したらコントローラーを再起動し、APIが復帰するまでの秒数を測定しました。

4.2 結果

컨트롤러가 90초 멈춘 동안 컨트롤러 1 + 워커 1, 약 5초 간격 18번(최종 회차, 실측) 0초 30초 60초 90초 22초: 워커의 nginx 마스터 kill API 18/18 실패 nginx 18/18 = 200 다시 켜자 API 는 3초(첫 회차 4초) 만에 돌아왔다 계속 된 것 이미 뜬 파드의 응답 워커의 kube-proxy 규칙 kubelet 의 로컬 재시작 (첫 회차에서 관측) 멈춘 것 배포 · 스케일 · 설정 바꾸기 워커가 죽었을 때 재배치 상태 보고(재시작 횟수 등) API 가 돌아온 뒤 반영
図: コントローラー停止中の90秒間に18回測定したAPIはすべて失敗、ワーカーのnginxはすべて200。22秒目にnginxのマスタープロセスを強制終了した
実行回数 API ワーカーnginx nginxマスター強制終了後の再起動回数 コントローラー再起動 → API復帰
1 18/18 失敗 18/18 = 200 0 → 1(API復帰後に報告) 4秒
2(最終) 18/18 失敗 18/18 = 200 0 → 0 3秒

ワーカーの k0sworker サービスは終始activeでした。

サービスが継続した理由。 リクエストが通過する経路はすべてワーカー内部にあります。NodePortをポッドに転送するkube-proxyのルール、コンテナを実行するcontainerd、ポッドを管理するkubeletのいずれもワーカー上で動作しており、コントローラーは「何をどこに配置するか」を決定する時のみ必要となります。そのため、すでに起動しているポッドはコントローラーが存在しなくても応答します。

停止したコンテナの再起動。 第1回実行では、nginxのマスタープロセスを終了させた後、再起動回数が0から1に増加しました。kubeletはすでに受け取っているポッドの定義と再起動ポリシーを保持しているため、APIなしでもコンテナを再起動します。ただし、その事実をAPIに報告できなかったため、数値はAPIが復帰した後に初めて確認できるようになりました。最終実行では同様の操作を行ったにもかかわらず、再起動回数が0のままでした。その間、nginxの応答は18回すべて200でした。なぜ今回は再起動としてカウントされなかったのかは確認できていません(原因不明)。そのため、「kubeletはAPIなしで再起動を行う」ということは、2回中1回観測された事実としてのみ記載します。

停止するもの。 コントローラーが存在しない間は、以下の操作がすべて不可能になります。

2台構成の危険性は「サービスが停止すること」ではなく、 「停止している間は何も変更できず、その間にワーカーが停止した場合に復旧させる主体が存在しないこと」 です。そのため、2台構成においてはコントローラーの迅速な復旧、すなわちバックアップ(第6節)を高可用性よりも優先すべきです。

5. ワーカーの追加と削除

追加。 k0sctl.yamlの hosts にworker02を追加してapplyを実行しました。k0sctlは宣言型であるため、既存のノードには手をつけず「Install workers」のステップのみが実行され、 1分42秒・2分31秒 ほどで worker02 が Ready になりました。これが第3部で比較する「コントローラー 1 + ワーカー 2」構成です。

削除。 順番が重要です。

  1. kubectl drain worker01 --ignore-daemonsets --delete-emptydir-data でポッドを worker02 に移動させます。移動先が必要なため、追加を先に行いました。
  2. k0sctl.yaml の worker01 ホストに reset: true を記述して apply します。k0sctl が「Reset workers」の段階でそのノードの k0s を削除し、ノード一覧からも worker01 が消えました。 24秒 · 39秒 かかりました。

nginx の2つは worker02 で引き続き利用可能でした。リセット時に failed to remove existing configuration /etc/k0s/k0s.yaml: file does not exist の警告が出ますが、ワーカーにはもともと k0s.yaml がないため無害です。

6. バックアップ

k0sctl backup --config k0sctl.yaml
回 所要時間 ファイル
1 16秒 k0s_backup_<epoch>.tar.gz 416KB
2 32秒 440KB

k0sctl はコントローラーで k0s backup を実行し、結果を取り込みサーバーに取得します。 k0s バックアップドキュメントによると、中には etcd のスナップショットと証明書・キーが入っています。クラスター CA の秘密鍵も入っているため、 バックアップファイルはシークレットのように保管しなければなりません。 閉域網だからといって、このファイルを適当な共有フォルダーに置いてはなりません。

復元(k0sctl apply --restore-from)は今回は測定しませんでした(未実測)。同じドキュメントには、復元はコントローラーで行う必要があり、異なる IP のノードに復元する際はアドレスを合わせる手順が別にあると記載されています。2台構成においてバックアップが高可用性の代わりになると言うには、復元時間を測定しておくことが次の課題となります。

7. まとめ

チェック項目 理由(この記事の実測)
2台の場合コントローラーは1つ etcd 2メンバーが耐えられる障害は0、1つ落としただけで書き込みが停止する
NIC が2つなら onlyBindToAddress: true がないと参加トークンがデフォルト経路のアドレスを含み、172秒後に失敗
k0sBinaryPath とローカル files 経路がないか、URL の場合、取り込みサーバーが外部を探す
コントローラーの障害は「変更不可」で備える API 18/18失敗の間もサービスは18/18正常、復帰3〜4秒
削除は drain の次に行う reset: true 手順を守れば web の2つが途切れない
バックアップファイルはシークレットのように etcd スナップショットと CA キーが含まれている
復元時間は別途測定する 今回は未実測、2台構成ではこれが復旧計画のすべてである

この記事の実測は TT Lab の実習コース「閉域網 k0s:ノード1・2・3台とその上の Ceph・PostgreSQL」(サイトパス /course/k0s-airgap-multinode)の第2モジュール「ノード2台」にて、同じバージョン、同じ取り込みリストで実際に試すことができます。コントローラーの停止、ワーカーの追加・削除、バックアップを実際の VM 上で行い、採点機が段階ごとに結果を確認します。

TT の一言。 学級委員(コントローラー)がちょっと席を外しても授業(サービス)は続きますよ。ただしその間は席を替えたり新しいお友達を入れたりすることはできません。そして学級委員を2人選んだらもっと心強そうに思えますが、2人とも賛成しなければならないルールだと、1人が体調を崩しただけで何も決められなくなってしまいます!

ログインするといいねできます

コメント

まだコメントはありません。

ログインするとコメントできます