Leadership Management

본 문서는 기 준비한 슬라이드를 기반으로 AI가 정리했습니다.

Overview

Juju의 Leadership Management는 lease 일정 기간 동안 리소스에 대한 독점적 소유권을 부여하는 메커니즘. 만료 시간이 있어 자동으로 해제되며, 소유자가 주기적으로 갱신해야 유지된다. 기반으로 동작하며, 각 MySQL 유닛이 controller에 리더십을 요청하고 갱신하는 구조이다.


Unit Side

각 유닛(예: Mysql/0, Mysql/1, Mysql/2)은 내부에 worker/leadership/tracker를 가지고 있다.

Leader Unit

Leader Unit - renewLease

리더 유닛은 lease를 **60초(2 * duration) 동안 claim lease를 최초 획득하는 동작. 이미 다른 유닛이 claim한 lease는 획득할 수 없으며, controller의 lease Manager가 중재한다. **하고, 30초(duration) 후에 갱신을 시도한다:

참고: 30초는 하드코딩이 아니라 LeadershipGuarantee 리더십이 보장되는 최소 시간. 기본 30초이며, model migration 시에는 안정성을 위해 60초로 증가한다. 설정값이다. container agent와 unit deployer에서 기본 30초, model migration 시 60초.

renewLease (duration=30s, leaseDuration=2*duration=60s)
  -> refresh()
  -> ClaimLeadership()
  -> setLeader()

Non-Leader (Minion) Units

Minion Units

비리더 유닛들은 리더십 획득을 시도하지만, 이미 리더가 있으면 minion으로 설정되고 리더십이 해제될 때까지 블로킹된다:

-> refresh()
-> ClaimLeadership()
-> setMinion()
-> BlockUntilLeadershipReleased()

BlockUntilLeadershipReleased() 현재 리더의 lease가 만료되거나 명시적으로 해제될 때까지 goroutine을 블로킹하는 함수. lease 해제 시 모든 대기 중인 minion이 깨어나 경쟁한다. 는 현재 리더의 lease가 만료되거나 해제될 때까지 대기하며, 해제 시점에 다시 ClaimLeadership()을 시도한다.

Leadership Failover

Leadership Failover

기존 리더(예: Mysql/0)가 lease 갱신에 실패하면(연결 끊김, 유닛 다운 등), lease가 만료되고 대기 중이던 minion 유닛이 리더를 인계받는다:

[Mysql/0 - 기존 Leader] ── X (연결 끊김/lease 만료)

[Mysql/1 - Minion -> New Leader]
  BlockUntilLeadershipReleased()  -- lease 만료 감지
  -> loop()
    -> refresh()
    -> ClaimLeadership()
    -> setLeader()                -- 새 리더로 승격

모든 유닛이 동시에 ClaimLeadership()을 시도할 수 있으며, controller의 lease Manager controller 내에서 모든 lease의 생명주기를 관리하는 중앙 컴포넌트. claim/extend/revoke 요청을 처리하고 dqlite에 영속화한다. 가 하나의 유닛에게만 lease를 부여한다.


Controller Side

Controller Machine 구성

Controller machine 내부에는 다음 컴포넌트가 존재한다:

ComponentRole
controller전체 관리
lease Manager리스 관리 및 저장

Lease Manager Store

Lease Manager는 다음 경로의 데이터를 관리한다:

  • domain/lease
  • /service
  • /state

Controller Bootstrap과 dqlite 리더십

Controller Bootstrap

controller/0에서 jujud Juju의 메인 에이전트 바이너리. controller와 machine agent 모두 이 바이너리로 실행되며, 내부에 수십 개의 worker를 관리한다. 프로세스가 부트스트랩될 때의 내부 구조:

jujud 내부 워커:

WorkerRole비고
bootstrap초기화 및 DB 생성
worker/provisioner머신 프로비저닝
worker/peergrouperHA 피어 그룹 관리3.6까지 존재, 4.0에서 제거
worker/dbaccessordqlite 클러스터 관리4.0에서 peergrouper 역할 대체

Bootstrap 과정:

bootstrap
  -> app.New() - Follower        // dqlite 노드를 Follower로 시작
  -> dqlite.Ready() - Leader     // dqlite 클러스터에서 Leader로 승격
  -> create 'controller' db      // controller 데이터베이스 생성
  -> create tables               // 테이블 생성
     - lease, cloud, ..

데이터베이스 구성:

DBEngine용도비고
State DBMongoDB기존 상태 저장소2.9 시절. 3.6+에서는 dqlite로 전환
Controller DBdqlite (/var/lib/juju/dqlite)lease, cloud 등 controller 데이터3.6+ 기본 백엔드

주의: 슬라이드는 2.9->3.6 전환기를 설명하며, 3.6 이후에는 MongoDB가 완전히 제거되고 모든 데이터가 dqlite에 저장된다.

영속화 (Persistence)

코드 검증 결과: 슬라이드에서 “save 60s”로 표현된 부분은 실제 코드에서 MaxSleep = time.Minute lease expiry를 체크하는 최대 대기 시간. 1분으로 설정되어 있으며, 이벤트가 없어도 최소 1분마다 만료된 lease를 정리한다. 에 해당한다. 이는 lease expiry 체크 최대 간격이지, 60초마다 배치 저장하는 것이 아니다. Lease 데이터는 ClaimLease(), ExtendLease(), RevokeLease()이벤트 발생 시 즉시 dqlite에 영속화된다.


Controller HA (juju enable-ha 3)

버전 참고: 아래 HA 구성 설명은 슬라이드 기준(2.9~3.6 전환기)이다.

  • 3.6: worker/peergrouper가 MongoDB replica-set + dqlite 클러스터를 모두 관리
  • 4.0: worker/peergrouper 제거, worker/dbaccessor dqlite 클러스터의 노드 가입/탈퇴를 관리하는 워커. 4.0에서 peergrouper를 대체하여 HA 구성의 핵심 역할을 한다. 가 dqlite 네이티브 클러스터링으로 HA 담당, MongoDB 완전 제거

Step 1: 새 controller 프로비저닝

HA Provisioning

controller/0에서 controller/1, controller/2를 프로비저닝한다.

controller/0 (bootstrap node)
  jujud
    ├── bootstrap
    ├── worker/provisioner ──────────> controller/1, controller/2 머신 프로비저닝
    ├── worker/peergrouper (3.6) ───> MongoDB replica-set 멤버 관리
    └── worker/dbaccessor  (4.0) ───> dqlite 클러스터 조인 관리

Step 2: 새 controller 워커 구성

Controller Workers

프로비저닝된 controller/1, controller/2에는 다음 워커가 실행된다:

ControllerWorkers (3.6)Workers (4.0)
controller/0bootstrap, provisioner, peergrouperbootstrap, provisioner, dbaccessor
controller/1peergrouper, dbaccessordbaccessor
controller/2peergrouper, dbaccessordbaccessor

4.0에서 worker/dbaccessor는 새 controller 노드가 부트될 때 빈 설정 파일을 감지하여 자동으로 dqlite 클러스터에 조인한다.

Step 3: DB Replica 구성

DB Replica

3.6 (MongoDB + dqlite 공존):

worker/peergrouper가 MongoDB replica-set을 구성한다:

ControllerMongoDB Role
controller/0Primary
controller/1Secondary
controller/2Secondary

4.0 (dqlite only): MongoDB 완전 제거. dqlite만 사용.

Step 4: dqlite 클러스터 구성

dqlite Cluster

worker/dbaccessor가 각 controller의 dqlite 노드를 클러스터로 연결한다:

Controllerdqlite Role
controller/0Primary (Leader)
controller/1Secondary (Follower)
controller/2Secondary (Follower)

Step 5: dqlite Write Forwarding

dqlite Write Forwarding

dqlite는 single-leader write 모든 쓰기가 단일 리더 노드에서만 처리되는 모델. 읽기는 모든 노드에서 가능하지만, 쓰기는 리더로 포워딩된다. 강한 일관성을 보장한다. 모델이다. Secondary 노드에서 쓰기 요청이 오면:

controller/1 (Secondary)                    controller/0 (Primary)
  worker/dbaccessor                           Controller DB
    |                                           dqlite
    +-- write request -----> NOT_LEADER         ^
    |                        (rejected)         |
    +-- forward write --------------------------+
    |
    +-- go-dqlite cache  <-- read (로컬 캐시에서 읽기 가능)

controller/2 (Secondary)
  worker/dbaccessor
    |
    +-- write request -----> NOT_LEADER
    +-- forward write -----> controller/0 Controller DB
    +-- go-dqlite cache  <-- read
  • 쓰기(Write): Secondary에서 시도 시 NOT_LEADER 응답 -> Primary로 포워딩
  • 읽기(Read): 로컬 go-dqlite 캐시에서 처리 가능

참고: NOT_LEADER 처리와 write forwarding은 go-dqlite dqlite의 Go 클라이언트 라이브러리. NOT_LEADER 에러 감지와 리더 노드로의 자동 포워딩을 투명하게 처리한다. 라이브러리 내부에서 투명하게 처리된다. Juju 코드에 명시적 NOT_LEADER 핸들링은 없다.


How Juju CLI Works

How Juju CLI Works

juju status 등의 CLI 명령은 ~/.local/share/juju/controllers.yaml에서 api-endpoints를 읽어 랜덤 액세스로 controller에 접속한다.

# ~/.local/share/juju/controllers.yaml
controllers:
  juju3:
    uuid: 0694eaf3-45e6-4169-884a-9929110c267a
    api-endpoints: ['192.168.10.10:17070', '10.0.0.91:17070']
juju status (random access)
    |
    +---> controller/0: jujud -> worker/apiserver
    +---> controller/1: jujud -> worker/apiserver
    +---> controller/2: jujud -> worker/apiserver

버전별 Lease 백엔드 변화

VersionLease BackendState DBHA 관리
2.9 Raft 합의 알고리즘. 분산 시스템에서 노드 간 상태 일관성을 보장한다. 2.9에서는 lease 관리에 Raft FSM을 사용했으나, 3.6부터 dqlite로 대체되었다. FSM + MongoDBMongoDBworker/peergrouper
3.6dqlite (domain/lease/state)dqlite (MongoDB 잔존)worker/peergrouper + worker/dbaccessor
4.0dqlite (domain/lease/state)dqlite onlyworker/dbaccessor (peergrouper 제거)

리더 사망 시 Hook 처리

리더 유닛이 hook 실행 중 사망하면:

  1. lease 갱신이 중단되어 60초 후 lease 만료
  2. 대기 중이던 minion이 새 리더로 승격
  3. 새 리더 유닛에서 leader-elected 리더십이 변경되었을 때 실행되는 charm hook. 새 리더가 DB 마스터 승격 등 리더 전용 작업을 수행할 수 있게 한다. hook이 실행됨
  4. 기존 리더의 미완료 hook은 실행 결과가 무시됨 — 에이전트가 재시작되면 새 리더가 아니므로 리더 전용 작업은 건너뜀

주의: 이 60초 간격 동안 리더가 없는 상태가 발생할 수 있다. 이 기간에 leader-set이 필요한 hook은 실패한다.


핵심 포인트

  1. 리더 선출은 lease 기반: lease를 60초간 claim, 30초 후 갱신 시도 (LeadershipGuarantee 설정)
  2. Minion은 블로킹 대기: 비리더 유닛은 BlockUntilLeadershipReleased()로 리더 해제를 대기
  3. Lease Manager: controller 내에서 lease 상태를 중앙 관리 (domain/lease/service, domain/lease/state)
  4. 백엔드 진화: MongoDB+Raft(2.9) -> dqlite(3.6) -> dqlite only(4.0)
  5. 이벤트 기반 영속화: lease 변경 시 즉시 dqlite에 저장 (슬라이드의 “save 60s”는 MaxSleep expiry 체크 간격)
  6. Controller HA: juju enable-ha 3으로 3-node 구성. 3.6: peergrouper, 4.0: dbaccessor
  7. dqlite single-leader write: go-dqlite 라이브러리가 NOT_LEADER 포워딩을 투명하게 처리
  8. Juju CLI: controllers.yaml의 api-endpoints에 랜덤 접속, worker/apiserver가 처리

관련 문서

  • Model Migration — 마이그레이션 중 Fortress lockdown으로 lease 갱신이 중단되는 이유와 리더십 보존 메커니즘
  • Juju Statusjuju status에서 리더 유닛에 * 표시가 붙는 원리
  • Change Stream — lease 변경이 다른 워커에게 전파되는 이벤트 인프라