ホームページ  >  記事  >  バックエンド開発  >  Kubernetes を利用したリーダー選挙を Go アプリに追加する方法

Kubernetes を利用したリーダー選挙を Go アプリに追加する方法

WBOY
WBOYオリジナル
2024-07-20 09:15:39937ブラウズ

How to add Kubernetes-powered leader election to your Go apps

元々はブログで公開されました

Kubernetes 標準ライブラリには、エコシステムの一部であるさまざまなサブパッケージの多くに隠された宝石がたくさんあります。そのような例の 1 つは、私が最近発見した k8s.io/client-go/tools/leaderelection です。これは、Kubernetes クラスター内で実行されているアプリケーションにリーダー選出プロトコルを追加するために使用できます。この記事では、リーダー選挙とは何か、それがこの Kubernetes パッケージでどのように実装されるかについて説明し、独自のアプリケーションでこのライブラリを使用する方法の例を示します。

リーダー選挙

リーダー選挙は、高可用性ソフトウェアの中核となる構成要素である分散システムの概念です。これにより、複数の同時プロセスが相互に調整し、単一の「リーダー」プロセスを選択できるようになり、そのプロセスがデータ ストアへの書き込みなどの同期アクションの実行を担当します。

これは、ハードウェアやネットワーク障害に対する冗長性を確保するために複数のプロセスが実行されているが、データの一貫性を確保するためにストレージに同時に書き込むことができない分散データベースやキャッシュなどのシステムで役立ちます。将来のある時点でリーダー プロセスが応答しなくなった場合、残りのプロセスによって新しいリーダーの選挙が開始され、最終的にリーダーとして機能する新しいプロセスが選択されます。

この概念を使用すると、単一のリーダーと複数のスタンバイ レプリカを備えた高可用性ソフトウェアを作成できます。

Kubernetes では、コントローラー ランタイム パッケージはリーダー選出を使用してコントローラーの高可用性を実現します。コントローラーのデプロイメントでは、リソースの調整は、プロセスがリーダーであり、他のレプリカがスタンバイで待機している場合にのみ発生します。リーダー ポッドが応答しなくなった場合、残りのレプリカは新しいリーダーを選択して後続の調整を実行し、通常の操作を再開します。

Kubernetes リース

このライブラリは、プロセスによって取得できる Kubernetes リース (分散ロック) を使用します。リースは、単一の ID によって一定期間保持されるネイティブ Kubernetes リソースであり、更新オプションも付いています。 以下はドキュメントの仕様例です:

apiVersion: coordination.k8s.io/v1
kind: Lease
metadata:
  labels:
    apiserver.kubernetes.io/identity: kube-apiserver
    kubernetes.io/hostname: master-1
  name: apiserver-07a5ea9b9b072c4a5f3d1c3702
  namespace: kube-system
spec:
  holderIdentity: apiserver-07a5ea9b9b072c4a5f3d1c3702_0c8914f7-0f35-440e-8676-7844977d3a05
  leaseDurationSeconds: 3600
  renewTime: "2023-07-04T21:58:48.065888Z"

リースは、k8s エコシステムによって次の 3 つの方法で使用されます。

  1. ノード ハートビート: すべてのノードには対応するリース リソースがあり、その renewTime フィールドを継続的に更新します。リースの renewTime がしばらく更新されなかった場合、ノードは利用不可として汚染され、それ以上のポッドはスケジュールされなくなります。
  2. リーダー選挙: この場合、リースは、リーダーにリースのholderIdentityを更新させることで、複数のプロセス間を調整するために使用されます。異なる ID を持つスタンバイ レプリカは、リースの期限が切れるのを待ったままになります。リースの有効期限が切れ、リーダーによって更新されない場合は、新しい選挙が行われ、残りのレプリカが、そのholderIdentity を自分のもので更新することによって、リースの所有権を取得しようとします。 Kubernetes API サーバーは古いオブジェクトの更新を禁止しているため、単一のスタンバイ ノードのみがリースを正常に更新でき、その時点で新しいリーダーとして実行を継続します。
  3. API サーバー ID: v1.26 以降、ベータ機能として、各 kube-apiserver レプリカは専用のリースを作成することでその ID を公開します。これは比較的スリムな新機能であるため、実行されている API サーバーの数以外に Lease オブジェクトから得られるものはあまりありません。ただし、これにより、将来の k8s バージョンでこれらのリースにさらにメタデータを追加する余地が残ります。

次に、リーダー選出シナリオでリースを使用する方法を示すサンプル プログラムを作成して、リースの 2 番目の使用例を調べてみましょう。

サンプルプログラム

このコード例では、リーダー選挙とリース操作の詳細を処理するためにリーダー選挙パッケージを使用しています。

package main

import (
    "context"
    "fmt"
    "os"
    "time"

    "k8s.io/client-go/tools/leaderelection"
    rl "k8s.io/client-go/tools/leaderelection/resourcelock"
    ctrl "sigs.k8s.io/controller-runtime"
)

var (
    // lockName and lockNamespace need to be shared across all running instances
    lockName      = "my-lock"
    lockNamespace = "default"

    // identity is unique to the individual process. This will not work for anything,
    // outside of a toy example, since processes running in different containers or
    // computers can share the same pid.
    identity      = fmt.Sprintf("%d", os.Getpid())
)

func main() {
    // Get the active kubernetes context
    cfg, err := ctrl.GetConfig()
    if err != nil {
        panic(err.Error())
    }

    // Create a new lock. This will be used to create a Lease resource in the cluster.
    l, err := rl.NewFromKubeconfig(
        rl.LeasesResourceLock,
        lockNamespace,
        lockName,
        rl.ResourceLockConfig{
            Identity: identity,
        },
        cfg,
        time.Second*10,
    )
    if err != nil {
        panic(err)
    }

    // Create a new leader election configuration with a 15 second lease duration.
    // Visit https://pkg.go.dev/k8s.io/client-go/tools/leaderelection#LeaderElectionConfig
    // for more information on the LeaderElectionConfig struct fields
    el, err := leaderelection.NewLeaderElector(leaderelection.LeaderElectionConfig{
        Lock:          l,
        LeaseDuration: time.Second * 15,
        RenewDeadline: time.Second * 10,
        RetryPeriod:   time.Second * 2,
        Name:          lockName,
        Callbacks: leaderelection.LeaderCallbacks{
            OnStartedLeading: func(ctx context.Context) { println("I am the leader!") },
            OnStoppedLeading: func() { println("I am not the leader anymore!") },
            OnNewLeader:      func(identity string) { fmt.Printf("the leader is %s\n", identity) },
        },
    })
    if err != nil {
        panic(err)
    }

    // Begin the leader election process. This will block.
    el.Run(context.Background())

}

leaderelection パッケージの優れた点は、リーダー選挙を処理するためのコールバックベースのフレームワークを提供していることです。こうすることで、特定の状態の変化にきめ細かく対応し、新しいリーダーが選出されたときにリソースを適切に解放できます。これらのコールバックを個別のゴルーチンで実行することにより、パッケージは Go の強力な同時実行サポートを利用して、マシン リソースを効率的に利用します。

テストしてみる

これをテストするには、kind を使用してテスト クラスターをスピンアップします。

$ kind create cluster

サンプル コードを main.go にコピーし、新しいモジュールを作成し (go mod init leaderelectiontest)、整理して (go mod tiny) 依存関係をインストールします。 go run main.go を実行すると、次のような出力が表示されるはずです。

$ go run main.go
I0716 11:43:50.337947     138 leaderelection.go:250] attempting to acquire leader lease default/my-lock...
I0716 11:43:50.351264     138 leaderelection.go:260] successfully acquired lease default/my-lock
the leader is 138
I am the leader!

The exact leader identity will be different from what's in the example (138), since this is just the PID of the process that was running on my computer at the time of writing.

And here's the Lease that was created in the test cluster:

$ kubectl describe lease/my-lock
Name:         my-lock
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  coordination.k8s.io/v1
Kind:         Lease
Metadata:
  Creation Timestamp:  2024-07-16T15:43:50Z
  Resource Version:    613
  UID:                 1d978362-69c5-43e9-af13-7b319dd452a6
Spec:
  Acquire Time:            2024-07-16T15:43:50.338049Z
  Holder Identity:         138
  Lease Duration Seconds:  15
  Lease Transitions:       0
  Renew Time:              2024-07-16T15:45:31.122956Z
Events:                    <none>

See that the "Holder Identity" is the same as the process's PID, 138.

Now, let's open up another terminal and run the same main.go file in a separate process:

$ go run main.go
I0716 11:48:34.489953     604 leaderelection.go:250] attempting to acquire leader lease default/my-lock...
the leader is 138

This second process will wait forever, until the first one is not responsive. Let's kill the first process and wait around 15 seconds. Now that the first process is not renewing its claim on the Lease, the .spec.renewTime field won't be updated anymore. This will eventually cause the second process to trigger a new leader election, since the Lease's renew time is older than its duration. Because this process is the only one now running, it will elect itself as the new leader.

the leader is 604
I0716 11:48:51.904732     604 leaderelection.go:260] successfully acquired lease default/my-lock
I am the leader!

If there were multiple processes still running after the initial leader exited, the first process to acquire the Lease would be the new leader, and the rest would continue to be on standby.

No single-leader guarantees

This package is not foolproof, in that it "does not guarantee that only one client is acting as a leader (a.k.a. fencing)". For example, if a leader is paused and lets its Lease expire, another standby replica will acquire the Lease. Then, once the original leader resumes execution, it will think that it's still the leader and continue doing work alongside the newly-elected leader. In this way, you can end up with two leaders running simultaneously.

To fix this, a fencing token which references the Lease needs to be included in each request to the server. A fencing token is effectively an integer that increases by 1 every time a Lease changes hands. So a client with an old fencing token will have its requests rejected by the server. In this scenario, if an old leader wakes up from sleep and a new leader has already incremented the fencing token, all of the old leader's requests would be rejected because it is sending an older (smaller) token than what the server has seen from the newer leader.

Implementing fencing in Kubernetes would be difficult without modifying the core API server to account for corresponding fencing tokens for each Lease. However, the risk of having multiple leader controllers is somewhat mitigated by the k8s API server itself. Because updates to stale objects are rejected, only controllers with the most up-to-date version of an object can modify it. So while we could have multiple controller leaders running, a resource's state would never regress to older versions if a controller misses a change made by another leader. Instead, reconciliation time would increase as both leaders need to refresh their own internal states of resources to ensure that they are acting on the most recent versions.

Still, if you're using this package to implement leader election using a different data store, this is an important caveat to be aware of.

Conclusion

Leader election and distributed locking are critical building blocks of distributed systems. When trying to build fault-tolerant and highly-available applications, having tools like these at your disposal is critical. The Kubernetes standard library gives us a battle-tested wrapper around its primitives to allow application developers to easily build leader election into their own applications.

While use of this particular library does limit you to deploying your application on Kubernetes, that seems to be the way the world is going recently. If in fact that is a dealbreaker, you can of course fork the library and modify it to work against any ACID-compliant and highly-available datastore.

Stay tuned for more k8s source deep dives!

以上がKubernetes を利用したリーダー選挙を Go アプリに追加する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。