싱글턴 배포 구성하기
싱글턴 배포 구성하기 (Configure singleton deployments)
싱글턴 배포는 클러스터에서 주어진 할당의 인스턴스가 동시에 최대 하나만 실행되는 배포예요. 워크로드가 데이터 저장소 같은 원격 리소스에 대한 독점 접근이 필요할 때 이 기능이 필요할 수 있어요. Nomad는 싱글턴 배포를 기본 제공 기능으로 지원하지 않아요. 워크로드는 Nomad 클라이언트 에이전트가 충돌하더라도 계속 실행되므로, 주어진 워크로드에 대해 최대 하나의 할당만 있는지 보장하려면 작업의 협력이 필요해요. 이 문서는 싱글턴 배포를 구현하는 방법을 설명해요.
출처: 문서
본문
설계 목표 (Design Goals)
여기서 설명하는 구성은 다음의 주요 설계 목표를 충족해요:
- 이 설계는 태스크가 포함된 특정 프로세스가 Nomad 클러스터의 다른 곳에서 이미 실행 중인 동일한 태스크 인스턴스가 있을 때 실행되지 않도록 방지해요.
- Nomad는 최소한의 다운타임으로 태스크 또는 태스크가 실행되는 노드의 실패로부터 복구할 수 있어야 해요. 여기서 "복구"는 Nomad가 원래 태스크를 중지하고 대체 태스크를 스케줄해야 한다는 것을 의미해요.
- Nomad는 전환(cutover) 중 불필요한 다운타임을 피하기 위해 실패의 오탐지(false positive)를 최소화해야 해요.
복구 속도와 오탐지 사이에는 트레이드오프가 있어요. Nomad가 실패에서 복구를 시도하는 속도를 빠르게 할수록 일시적인 실패로 인해 Nomad가 대체 태스크를 스케줄하고 이후 다운타임이 발생할 가능성이 높아져요.
분산 시스템에서 완벽하게 제로 다운타임인 싱글턴 할당을 설계하는 것은 불가능하다는 점에 유의하세요. 이 설계는 정확성에 무게를 둬요: 하나 또는 두 개의 할당이 실행되는 잘못된 상태보다는 0개 또는 1개의 할당만 실행되도록 해요.
개요 (Overview)
구현의 일부 세부 사항에는 몇 가지 선택 사항이 있지만, 모두 다음을 포함해요:
- 할당에서 갱신되는 TTL이 있는 분산 잠금이 있어야 해요. 잠금을 설정하고 갱신하는 프로세스의 수명 주기는 메인 태스크에 묶여 있어야 해요. 인프로세스, 감독이 있는 인태스크, 또는 사이드카로 실행될 수 있어요. 할당이 잠금을 얻지 못하면 싱글턴으로 의도한 프로세스나 작업을 시작하지 않아야 해요. 구성 가능한 시간 창 안에 잠금을 얻지 못하면 할당은 실패해야 해요.
group.disconnect.stop_on_client_after필드를 설정해야 해요. 이 필드는 서버에서 연결이 끊긴 Nomad 클라이언트가 싱글턴 할당을 중지하도록 강제하며, 이는 결과적으로 잠금을 해제하거나 TTL이 만료되도록 해요.
잠금 TTL, 할당이 포기하는 데 걸리는 시간, stop_on_client_after 지속 시간 타이머 값을 조정해 애플리케이션이 가질 수 있는 최대 다운타임을 줄일 수 있어요.
Nomad Locks API가 필요한 작업을 지원할 수 있어요. 의사 코드로 이 작업들은 다음과 같아요:
- 잠금을 획득하려면
PUT /v1/var/:path?lock-acquire- 성공 시: TTL의 1/2마다 하트비트 시작
- 충돌 또는 실패 시: 백오프와 타임아웃으로 재시도
- 시도가 소진되면 오류 코드와 함께 프로세스 종료
- 하트비트를 보내려면
PUT /v1/var/:path?lock-renew- 성공 시: 계속
- 충돌 시: 오류 코드와 함께 프로세스 종료
- 실패 시: TTL까지 백오프로 재시도
- TTL이 만료되면 잠금 해제(revoke)를 시도한 다음 오류 코드와 함께 프로세스 종료
할당은 서버와 직접 통신하는 대신 Nomad Task API 소켓을 사용해 Locks API에 안전하게 쓸 수 있어요. 이렇게 하면 서버의 부하가 줄고 실패한 클라이언트 노드의 감지가 빨라져요. 연결이 끊긴 클라이언트는 Task API 요청을 리더에게 전달할 수 없기 때문이에요.
nomad var lock 명령이 이 로직을 구현하므로, 이를 사용해 잠글 대상 프로세스를 shim할 수 있어요.
ACLs
할당은 기본적으로 Nomad 변수에 쓸 수 없어요. namespace.variables 블록에서 쓰기 접근을 허용하는 워크로드 연결 ACL 정책을 구성해야 해요. 예를 들어 다음 ACL 정책은 prod 네임스페이스의 nomad/jobs/example/lock 경로에 잠금을 쓰는 접근을 허용해요:
namespace "prod" {
variables {
path "nomad/jobs/example/lock" {
capabilities = ["write", "read", "list"]
}
}
}
이 정책을 nomad acl policy apply -namespace prod -job example example-lock ./policy.hcl로 작업에 설정해요.
구현 (Implementation)
nomad var lock 사용하기 (Use nomad var lock)
잠금 로직을 작업의 shim으로 nomad var lock으로 구현하는 것을 권장해요. 이 예제 jobspec은 컨테이너 이미지에 Nomad 바이너리가 있다고 가정해요.
job "example" {
group "group" {
disconnect {
stop_on_client_after = "1m"
}
task "primary" {
config {
driver = "docker"
image = "example/app:1"
command = "nomad"
args = [
"var", "lock", "nomad/jobs/example/lock", # lock
"busybox", "httpd", # application
"-vv", "-f", "-p", "8001", "-h", "/local" # application args
]
}
identity {
env = true
}
}
}
}
컨테이너 이미지에 Nomad 바이너리를 포함하고 싶지 않다면 호스트 볼륨에서 바이너리를 읽기 전용으로 마운트해요. 이것은 Nomad 바이너리가 정적으로 링크되었거나 컨테이너 이미지에 glibc가 있는 경우에만 작동해요.
job "example" {
group "group" {
disconnect {
stop_on_client_after = "1m"
}
volume "binaries" {
type = "host"
source = "binaries"
read_only = true
}
task "primary" {
config {
driver = "docker"
image = "example/app:1"
command = "/opt/bin/nomad"
args = [
"var", "lock", "nomad/jobs/example/lock", # lock
"busybox", "httpd", # application
"-vv", "-f", "-p", "8001", "-h", "/local" # application args
]
}
identity {
env = true # make NOMAD_TOKEN available to lock command
}
volume_mount {
volume = "binaries"
destination = "/opt/bin"
}
}
}
}
사이드카 잠금 (Sidecar lock)
애플리케이션이나 nomad var lock 같은 shim으로 잠금 로직을 구현할 수 없다면, task.leader=true가 설정된 잠금 태스크의 사이드카로 잠글 대상 태스크가 실행되도록 구현해야 해요.
job "example" {
group "group" {
disconnect {
stop_on_client_after = "1m"
}
task "lock" {
leader = true
config {
driver = "raw_exec"
command = "/opt/lock-script.sh"
pid_mode = "host"
}
identity {
env = true # make NOMAD_TOKEN available to lock command
}
}
task "application" {
lifecycle {
hook = "poststart"
sidecar = true
}
config {
driver = "docker"
image = "example/app:1"
}
}
}
}
잠금 태스크에는 다음 요구 사항이 있어요:
- 잠글 대상 태스크와 같은 그룹에 있어야 해요.
- Nomad 클라이언트가 켜져 있지 않아도 잠글 대상 태스크를 종료할 수 있어야 해요. 예를 들어 동일한 PID 네임스페이스를 공유하거나 잠금 태스크가 권한(privileged)을 가져야 해요.
- 잠글 대상 태스크에 시작해도 안전하다는 신호를 보내는 방법이 있어야 해요. 예를 들어 잠금 태스크가
/alloc디렉토리에 Sentinel 파일을 쓸 수 있고, 잠긴 태스크는 시작 시 이 파일을 읽으려 시도하고 존재할 때까지 차단해요.
세 번째 요구 사항을 충족할 수 없다면 잠금 획득과 잠금 하트비트를 별도의 태스크로 분리해야 해요.
job "example" {
group "group" {
disconnect {
stop_on_client_after = "1m"
}
task "acquire" {
lifecycle {
hook = "prestart"
sidecar = false
}
config {
driver = "raw_exec"
command = "/opt/lock-acquire-script.sh"
}
identity {
env = true # make NOMAD_TOKEN available to lock command
}
}
task "heartbeat" {
leader = true
config {
driver = "raw_exec"
command = "/opt/lock-heartbeat-script.sh"
pid_mode = "host"
}
identity {
env = true # make NOMAD_TOKEN available to lock command
}
}
task "application" {
lifecycle {
hook = "poststart"
sidecar = true
}
config {
driver = "docker"
image = "example/app:1"
}
}
}
}