>  기사  >  백엔드 개발  >  Go 앱에 Kubernetes 기반 리더 선택을 추가하는 방법

Go 앱에 Kubernetes 기반 리더 선택을 추가하는 방법

WBOY
WBOY원래의
2024-07-20 09:15:39939검색

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

원래 블로그에 게시됨

Kubernetes 표준 라이브러리는 생태계의 일부인 다양한 하위 패키지에 숨겨져 있는 보석으로 가득 차 있습니다. 최근 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 생태계에서 세 가지 방식으로 사용됩니다.

  1. 노드 하트비트: 모든 노드에는 해당 임대 리소스가 있으며 지속적으로 renewTime 필드를 업데이트합니다. 임대의 renewTime이 한동안 업데이트되지 않으면 노드는 사용할 수 없는 것으로 오염되고 더 이상 포드가 예약되지 않습니다.
  2. 리더 선택: 이 경우 Lease는 리더가 Lease의holderIdentity를 업데이트하도록 하여 여러 프로세스를 조정하는 데 사용됩니다. 다른 ID를 가진 대기 복제본이 임대가 만료되기를 기다리고 있습니다. 임대가 만료되고 리더가 갱신하지 않으면 남은 복제본이 해당holderIdentity를 자신의 것으로 업데이트하여 임대의 소유권을 얻으려고 시도하는 새로운 선택이 발생합니다. Kubernetes API 서버는 오래된 객체에 대한 업데이트를 허용하지 않으므로 단일 대기 노드만 임대를 성공적으로 업데이트할 수 있으며, 이 시점에서 임대는 새로운 리더로 계속 실행됩니다.
  3. API 서버 ID: v1.26부터 베타 기능으로 각 kube-apiserver 복제본은 전용 임대를 생성하여 ID를 게시합니다. 이는 비교적 슬림하고 새로운 기능이므로 실행 중인 API 서버 수 외에 Lease 개체에서 파생할 수 있는 내용이 많지 않습니다. 그러나 이는 향후 k8s 버전에서 이러한 임대에 더 많은 메타데이터를 추가할 여지를 남겨둡니다.

이제 리더 선택 시나리오에서 Lease를 어떻게 사용할 수 있는지 보여주는 샘플 프로그램을 작성하여 Lease의 두 번째 사용 사례를 살펴보겠습니다.

예제 프로그램

이 코드 예제에서는 리더 선택 패키지를 사용하여 리더 선택 및 임대 조작 세부 사항을 처리합니다.

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())

}

리더 선택 패키지의 좋은 점은 리더 선택을 처리하기 위한 콜백 기반 프레임워크를 제공한다는 것입니다. 이런 방식으로 특정 상태 변경에 대해 세부적인 방식으로 조치를 취하고 새 리더가 선출되면 리소스를 적절하게 해제할 수 있습니다. 별도의 고루틴에서 이러한 콜백을 실행함으로써 패키지는 Go의 강력한 동시성 지원을 활용하여 머신 리소스를 효율적으로 활용합니다.

테스트해 보세요

이를 테스트하려면 종류를 사용하여 테스트 클러스터를 가동해 보겠습니다.

$ kind create cluster

샘플 코드를 main.go에 복사하고, 새 모듈을 생성하고(go mod init Leaderelectiontest) 정리하여(go mod tidy) 종속성을 설치합니다. 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!

위 내용은 Go 앱에 Kubernetes 기반 리더 선택을 추가하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.