Lease
공유 리소스를 lock하고, 연결된 노드들 간의 활동을 조정하기 위한, 특정 pod가 일정 기간 동안 리더임을 선언하는 객체
여러개의 pod 중에서 하나의 pod만 주어진 시간 동안 대표가 되어서 활동할 수 있도록 하는 객체이다.
lease API는 `coordination.k8s.io` API 그룹에 존재하며, 대표적으로 아래와 같은 feature를 갖는다.
- holderIdentity : 현재 lease의 대표 id
- leaseDurationSeconds : lease의 지속시간
- renewTime : 마지막 lease 갱신 시각
쿠버네티스 클러스터에는 기본적으로 `kube-node-lease`라는 네임스페이스가 기본적으로 존재하는데, 이 네임스페이스에는 클러스터에 연결된 모든 노드들의 상태(`.status`)를 업데이트하는 lease가 있다.
이 lease는 각 노드의 kubelet 하트비트를 마스터 노드의 API 서버로 전송하는 역할을 수행한다.
Leader Election
동일한 작업을 수행하는 여러개의 pod 중에서 대표 하나를 선정하는 작업
아래 예시를 보면, 3개의 pod가 하나의 lease를 통해 묶여있는 것을 볼 수 있다.
이 상태에서 대표인 Pod A는 leaseDurationSeconds로 지정된 시간안에 주기적으로 lease를 갱신한다. 그리고 나머지 pod들은 이 lease를 주기적으로 감시하며, Pod A가 이를 갱신하지 못한 경우(Pod의 장애, 네트워크 이슈 등)에 나머지 pod들이 알고리즘을 기반으로 새로운 leader를 선출하게 된다.
예전에 한창 인기를 끌었던 드라마 [Designated Survivor]가 있었다. 미국 드라마로, 한국 버전으로 각색되어서 리메이크까지 된 드라마였다.
미국 국회에서의 폭발 사고로 인해 대통령을 포함한 모든 정치인들이 사망하게 되어 "지정생존자"로 임명되어있던 주인공이 사고의 배후를 밝혀내는 것이 작품의 전반적인 내용이다.
실제로 미국 정부에서는 대통령이 갑작스럽게 직무를 수행할 수 없을 경우를 대비해 특정 인물을 "지정생존자(Designated Survivor)"로 정한다고 한다. 만약 대통령을 포함한 주요 정부 인사들이 불의의 사고 등으로 인해 모두 사망하면, 지정생존자가 새로운 리더가 되어 국가를 이끌도록하는 구조인 것이다.
이러한 개념이 동일하게 Kubernetes에서도 적용되는 것이다. Lease를 통해 여러 개의 파드 중 하나가 리더 역할을 수행하며, 만약 리더가 Lease를 갱신하지 못하면, 남은 pod 중에서 새로운 리더가 자동으로 선출되는 것이다.
예시
이러한 Lease와 leader election을 직접 확인할 수 있는 간단한 예시를 들어보자.
먼저 비어있는 lease 객체를 생성한다.
apiVersion: coordination.k8s.io/v1
kind: Lease
metadata:
name: leader-election-lease
namespace: default
spec:
holderIdentity: "" # 현재 리더가 지정되지 않음
leaseDurationSeconds: 10
그리고 kubernetes API를 이용해 lease 객체를 조회하고, 갱신할 수 있는 Application pod를 생성한다.
여기에서는 아래와 같은 python App을 작성해서 테스트를 진행했다.
import time
import socket
from datetime import datetime, timezone, timedelta
from kubernetes import client, config
LEASE_NAME = "leader-election-lease"
NAMESPACE = "default"
LEASE_DURATION_SECONDS = 10 # 리더 유지 시간
RENEW_INTERVAL = 5 # 리더가 Lease 갱신하는 간격
# Kubernetes 클라이언트 설정
config.load_incluster_config()
v1 = client.CoordinationV1Api()
def get_current_time():
return datetime.now(timezone.utc).isoformat()
def acquire_or_renew_lease():
try:
lease = v1.read_namespaced_lease(LEASE_NAME, NAMESPACE)
holder_identity = lease.spec.holder_identity
renew_time = lease.spec.renew_time
if renew_time:
# `renew_time`이 존재하면 datetime으로 변환
renew_time = datetime.fromisoformat(str(renew_time).rstrip("Z")).replace(tzinfo=timezone.utc)
else:
renew_time = datetime.min.replace(tzinfo=timezone.utc) # 기본값 설정
current_time = datetime.now(timezone.utc)
# 현재 리더가 없거나 Lease 시간이 만료되었으면 새로운 리더가 된다.
if not holder_identity or (renew_time + timedelta(seconds=LEASE_DURATION_SECONDS) < current_time):
lease.spec.holder_identity = socket.gethostname()
lease.spec.renew_time = get_current_time()
v1.replace_namespaced_lease(LEASE_NAME, NAMESPACE, lease)
print(f"[{get_current_time()}] I am the new leader: {lease.spec.holder_identity}")
elif holder_identity == socket.gethostname():
# 현재 리더가 본인이라면 Lease를 갱신
lease.spec.renew_time = get_current_time()
v1.replace_namespaced_lease(LEASE_NAME, NAMESPACE, lease)
print(f"[{get_current_time()}] Renewing lease as leader.")
else:
print(f"[{get_current_time()}] Current leader: {holder_identity}")
except client.exceptions.ApiException:
print(f"[{get_current_time()}] Lease object not found. Creating lease...")
lease = client.V1Lease(
metadata=client.V1ObjectMeta(name=LEASE_NAME),
spec=client.V1LeaseSpec(
holder_identity=socket.gethostname(),
lease_duration_seconds=LEASE_DURATION_SECONDS,
renew_time=get_current_time()
)
)
v1.create_namespaced_lease(NAMESPACE, lease)
if __name__ == "__main__":
while True:
acquire_or_renew_lease()
time.sleep(RENEW_INTERVAL)
이 python App에서 Lease 객체에 접근하기 위해서는 파드에 권한을 주어야한다. 따라서 아래와 같이 lease API에 접근할 수 있는 Role과 SA(service account)를 설정해준다.
apiVersion: v1
kind: ServiceAccount
metadata:
name: leader-election-sa
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: leader-election-role
rules:
- apiGroups: ["coordination.k8s.io"] # lease API의 apigroup
resources: ["leases"]
verbs: ["get", "watch", "list", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: leader-election-rolebinding
subjects:
- kind: ServiceAccount
name: leader-election-sa
namespace: default
roleRef:
kind: Role
name: leader-election-role
apiGroup: rbac.authorization.k8s.io
마지막으로 python App을 구동하는 pod를 3개 생성해준다.
apiVersion: v1
kind: Pod
metadata:
name: leader-election-1
labels:
app: leader-election
spec:
serviceAccountName: leader-election-sa
containers:
- name: leader-election-container
image: leader-election:latest
imagePullPolicy: Never
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
---
apiVersion: v1
kind: Pod
metadata:
name: leader-election-2
labels:
app: leader-election
spec:
serviceAccountName: leader-election-sa
containers:
- name: leader-election-container
image: leader-election:latest
imagePullPolicy: Never
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
---
apiVersion: v1
kind: Pod
metadata:
name: leader-election-3
labels:
app: leader-election
spec:
serviceAccountName: leader-election-sa
containers:
- name: leader-election-container
image: leader-election:latest
imagePullPolicy: Never
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
pod를 구동한 뒤, 앞서 생성한 lease를 확인하면 holderIdentity가 지정된 것을 볼 수 있다.
이제 이 상황에서 holderIdentity로 지정된 pod를 삭제해본다.
kubectl delete pod leader-election-3
이러면 기존의 holderIdentity로 지정된 pod가 lease를 갱신하지 못하게 된다.
그리고 나머지 2개의 pod들에서 leader election이 발생하게 되고, 새롭게 holderIdentity가 지정되는 것을 볼 수 있다.
이렇게 여러 개의 pod와 하나의 lease 객체를 두고, pod들의 상태를 변경하며 lease의 leader(holder identity)가 변경되는 것을 볼 수 있었다.