Leadership Management
본 문서는 기 준비한 슬라이드를 기반으로 AI가 정리했습니다.
Overview
Juju의 Leadership Management는 lease 일정 기간 동안 리소스에 대한 독점적 소유권을 부여하는 메커니즘. 만료 시간이 있어 자동으로 해제되며, 소유자가 주기적으로 갱신해야 유지된다. 기반으로 동작하며, 각 MySQL 유닛이 controller에 리더십을 요청하고 갱신하는 구조이다.
Unit Side
각 유닛(예: Mysql/0, Mysql/1, Mysql/2)은 내부에 worker/leadership/tracker를 가지고 있다.
Leader Unit

리더 유닛은 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으로 설정되고 리더십이 해제될 때까지 블로킹된다:
-> refresh()
-> ClaimLeadership()
-> setMinion()
-> BlockUntilLeadershipReleased()
BlockUntilLeadershipReleased() 현재 리더의 lease가 만료되거나 명시적으로 해제될 때까지 goroutine을 블로킹하는 함수. lease 해제 시 모든 대기 중인 minion이 깨어나 경쟁한다. 는 현재 리더의 lease가 만료되거나 해제될 때까지 대기하며, 해제 시점에 다시 ClaimLeadership()을 시도한다.
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 내부에는 다음 컴포넌트가 존재한다:
| Component | Role |
|---|---|
| controller | 전체 관리 |
| lease Manager | 리스 관리 및 저장 |
Lease Manager Store
Lease Manager는 다음 경로의 데이터를 관리한다:
domain/lease/service/state
Controller Bootstrap과 dqlite 리더십

controller/0에서 jujud Juju의 메인 에이전트 바이너리. controller와 machine agent 모두 이 바이너리로 실행되며, 내부에 수십 개의 worker를 관리한다. 프로세스가 부트스트랩될 때의 내부 구조:
jujud 내부 워커:
| Worker | Role | 비고 |
|---|---|---|
| bootstrap | 초기화 및 DB 생성 | |
| worker/provisioner | 머신 프로비저닝 | |
| worker/peergrouper | HA 피어 그룹 관리 | 3.6까지 존재, 4.0에서 제거 |
| worker/dbaccessor | dqlite 클러스터 관리 | 4.0에서 peergrouper 역할 대체 |
Bootstrap 과정:
bootstrap
-> app.New() - Follower // dqlite 노드를 Follower로 시작
-> dqlite.Ready() - Leader // dqlite 클러스터에서 Leader로 승격
-> create 'controller' db // controller 데이터베이스 생성
-> create tables // 테이블 생성
- lease, cloud, ..
데이터베이스 구성:
| DB | Engine | 용도 | 비고 |
|---|---|---|---|
| State DB | MongoDB | 기존 상태 저장소 | 2.9 시절. 3.6+에서는 dqlite로 전환 |
| Controller DB | dqlite (/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 프로비저닝

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/1, controller/2에는 다음 워커가 실행된다:
| Controller | Workers (3.6) | Workers (4.0) |
|---|---|---|
| controller/0 | bootstrap, provisioner, peergrouper | bootstrap, provisioner, dbaccessor |
| controller/1 | peergrouper, dbaccessor | dbaccessor |
| controller/2 | peergrouper, dbaccessor | dbaccessor |
4.0에서
worker/dbaccessor는 새 controller 노드가 부트될 때 빈 설정 파일을 감지하여 자동으로 dqlite 클러스터에 조인한다.
Step 3: DB Replica 구성

3.6 (MongoDB + dqlite 공존):
worker/peergrouper가 MongoDB replica-set을 구성한다:
| Controller | MongoDB Role |
|---|---|
| controller/0 | Primary |
| controller/1 | Secondary |
| controller/2 | Secondary |
4.0 (dqlite only): MongoDB 완전 제거. dqlite만 사용.
Step 4: dqlite 클러스터 구성

worker/dbaccessor가 각 controller의 dqlite 노드를 클러스터로 연결한다:
| Controller | dqlite Role |
|---|---|
| controller/0 | Primary (Leader) |
| controller/1 | Secondary (Follower) |
| controller/2 | Secondary (Follower) |
Step 5: 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

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 백엔드 변화
| Version | Lease Backend | State DB | HA 관리 |
|---|---|---|---|
| 2.9 | Raft 합의 알고리즘. 분산 시스템에서 노드 간 상태 일관성을 보장한다. 2.9에서는 lease 관리에 Raft FSM을 사용했으나, 3.6부터 dqlite로 대체되었다. FSM + MongoDB | MongoDB | worker/peergrouper |
| 3.6 | dqlite (domain/lease/state) | dqlite (MongoDB 잔존) | worker/peergrouper + worker/dbaccessor |
| 4.0 | dqlite (domain/lease/state) | dqlite only | worker/dbaccessor (peergrouper 제거) |
리더 사망 시 Hook 처리
리더 유닛이 hook 실행 중 사망하면:
- lease 갱신이 중단되어 60초 후 lease 만료
- 대기 중이던 minion이 새 리더로 승격
- 새 리더 유닛에서 leader-elected 리더십이 변경되었을 때 실행되는 charm hook. 새 리더가 DB 마스터 승격 등 리더 전용 작업을 수행할 수 있게 한다. hook이 실행됨
- 기존 리더의 미완료 hook은 실행 결과가 무시됨 — 에이전트가 재시작되면 새 리더가 아니므로 리더 전용 작업은 건너뜀
주의: 이 60초 간격 동안 리더가 없는 상태가 발생할 수 있다. 이 기간에
leader-set이 필요한 hook은 실패한다.
핵심 포인트
- 리더 선출은 lease 기반: lease를 60초간 claim, 30초 후 갱신 시도 (
LeadershipGuarantee설정) - Minion은 블로킹 대기: 비리더 유닛은
BlockUntilLeadershipReleased()로 리더 해제를 대기 - Lease Manager: controller 내에서 lease 상태를 중앙 관리 (
domain/lease/service,domain/lease/state) - 백엔드 진화: MongoDB+Raft(2.9) -> dqlite(3.6) -> dqlite only(4.0)
- 이벤트 기반 영속화: lease 변경 시 즉시 dqlite에 저장 (슬라이드의 “save 60s”는
MaxSleepexpiry 체크 간격) - Controller HA:
juju enable-ha 3으로 3-node 구성. 3.6: peergrouper, 4.0: dbaccessor - dqlite single-leader write: go-dqlite 라이브러리가 NOT_LEADER 포워딩을 투명하게 처리
- Juju CLI: controllers.yaml의 api-endpoints에 랜덤 접속, worker/apiserver가 처리
관련 문서
- Model Migration — 마이그레이션 중 Fortress lockdown으로 lease 갱신이 중단되는 이유와 리더십 보존 메커니즘
- Juju Status —
juju status에서 리더 유닛에*표시가 붙는 원리 - Change Stream — lease 변경이 다른 워커에게 전파되는 이벤트 인프라