Vault 복제의 최종 일관성(Eventual consistency)
Vault 복제의 최종 일관성(Eventual consistency)
Enterprise
적절한 Vault Enterprise 라이선스 또는 HCP Vault Dedicated 클러스터가 필요합니다.
클러스터에서 실행할 때 Vault는 최종 일관성 모델을 가집니다. 백엔드 스토리지에 쓸 수 있는 노드는 오직 하나(리더)뿐입니다. 사용자는 일반적으로 읽기-후-쓰기 일관성(read-after-write consistency)을 기대합니다. 즉, foo=1을 쓴 후에 foo를 읽으면 1이 반환되어야 합니다. 그러나 Vault 구성에 따라 항상 그런 것은 아닙니다. 통합 스토리지와 함께 성능 대기(performance standby)를 사용하거나 성능 복제(performance replication)를 사용할 때는, 항상 읽기-후-쓰기 일관성을 보장하지 않는 작업 순서들이 있습니다.
성능 대기 노드
성능 대기 없이 통합 스토리지 백엔드를 사용할 때는 단일 Vault 노드(활성 노드)만 요청을 처리합니다. 일반 대기에 보내진 요청은 활성 노드로 전달되어 처리됩니다. 이 Vault 구성은 기본 Consul 일관성 모델과 같은 동작을 Vault에 제공합니다.
성능 대기와 함께 통합 스토리지 백엔드를 사용할 때는 활성 노드와 성능 대기 모두 요청을 처리할 수 있습니다. 성능 대기가 로그인 요청이나 동적 시크릿을 생성하는 요청을 처리하면, 성능 대기는 활성 노드에 원격 프로시저 호출(RPC)을 발행해 토큰 및/또는 임대를 저장합니다. 성능 대기가 스토리지 쓰기를 초래하는 다른 요청을 처리하면, 일반 대기가 모든 요청을 전달하는 것과 같은 방식으로 그 요청을 활성 노드로 전달합니다.
통합 스토리지를 사용할 때 모든 쓰기는 활성 노드에서 일어나며, 활성 노드는 다른 모든 노드의 로컬 스토리지를 갱신하는 RPC를 발행합니다. 활성 노드가 데이터를 로컬 디스크에 쓰는 시점과, 다른 노드들이 그 RPC를 처리해 데이터를 로컬 디스크에 쓰는 시점 사이에서는, 그 노드들이 데이터의 오래된(stale) 뷰를 제시합니다.
결과적으로 항상 같은 성능 대기와 통신하더라도 읽기-후-쓰기 의미론을 얻지 못할 수 있습니다. 쓰기가 활성 노드로 보내지고, 그 후의 읽기 요청이 새 데이터가 읽기 요청을 처리하는 노드로 보내지기 전에 발생하면, 그 노드에 새 데이터가 아직 없기 때문에 읽기 요청이 쓰기를 반영할 수 없습니다.
성능 복제
성능 복제를 사용할 때도 비슷한 현상이 일어납니다. 이것이 나타나는 한 예는 공유 마운트를 사용할 때입니다. KV 시크릿 엔진이 local=false로 기본(primary)에 마운트되면 보조(secondary) 클러스터에도 존재합니다. 보조 클러스터는 그 마운트에 대한 요청을 처리할 수 있지만, 성능 대기와 마찬가지로 쓰기 요청은 전달되어야 합니다. — 이 경우에는 기본 활성 노드로 전달됩니다. 데이터가 기본 클러스터에 쓰여지면, 데이터가 기본에서 복제될 때까지 보조 클러스터에서는 보이지 않습니다. 따라서 보조 클러스터에서는 처음에 데이터 쓰기가 일어나지 않은 것처럼 보입니다.
보조 클러스터가 통합 스토리지를 사용하고 읽기 요청이 그 성능 대기 중 하나에서 처리된다면, 먼저 기본 활성 노드에서 보조 활성 노드로, 그다음 거기서 보조 성능 대기로 보내져야 하기 때문에 각 단계가 자체적인 지연을 만들 수 있어 문제가 더 커집니다.
공유 시크릿 엔진이 없어도 성능 복제에서 오래된 읽기가 발생할 수 있습니다. Identity 하위 시스템은 클러스터에 걸쳐 있는 엔티티와 그룹의 뷰를 제공하는 것을 목표로 합니다. 따라서 공유 마운트를 사용해 보조 클러스터에 로그인할 때, Vault는 엔티티와 앨리어스가 아직 없으면 생성하려 하며, 이는 RPC를 사용해 기본에 저장해야 합니다. 그룹에서도 비슷한 일이 일어납니다.
시계 편차(Clock skew)와 복제 지연
위에서 보았듯이 성능 대기와 복제 보조 모두 활성 노드 또는 기본 뒤로 지연될 수 있습니다. Vault 1.17부터 sys/health, sys/ha-status, 그리고 복제 상태 엔드포인트를 사용해 그 지연에 대한 통찰을 얻을 수 있습니다.
보조와 대기는 정기적으로 업스트림 소스에 "에코(echo)" 하트비트 RPC를 발행합니다. 이 하트비트는 여러 목적을 수행하며, 그중 하나는 클라이언트와 서버의 시계가 동기화되어 있는지 대략적으로 파악하는 것입니다. 하트비트 RPC에 대한 서버 응답은 서버의 로컬 시계 시간을 포함하며, 클라이언트는 그 시간과 클라이언트 로컬 시계 시간 사이의 밀리초 차이를 계산해 clock_skew_ms 필드를 만듭니다. 실제로 RPC를 수행하는 데 걸린 시간을 그 필드에 반영하려는 노력은 없지만, 그 정보는 last_heartbeat_duration_ms 필드로 제공됩니다. 즉, 보고된 시계 편차에는 최대 last_heartbeat_duration_ms만큼의 불확실성이 있습니다.
Vault는 클러스터의 모든 노드에 걸쳐 시계가 동기화되어 있다고 가정하며, 그렇지 않으면 문제가 발생할 수 있습니다. 예를 들어 한 노드는 임대가 만료되었다고 생각하는데 다른 노드는 아직 그러지 않을 수 있습니다. 일부 커뮤니티 지원 스토리지 백엔드는 HA 모드와 관련해 추가 문제가 있을 수 있습니다.
복제 기본과 보조 사이에 시계 편차가 있을 때는 문제가 더 적을 것으로 예상됩니다. 그러나 알려진 문제 하나는, 클러스터 간에 시계가 동기화되지 않으면 다음에 논의할 복제 지연 카나리(canary)가 예상 밖의 값을 만들어 낸다는 것입니다.
비(非)-보조 활성 노드는 주기적으로 해당 노드의 로컬 시계 시간을 담은 작은 레코드를 스토리지에 기록합니다. 복제 보조는 그 레코드를 읽고 로컬 시계 시간과 비교해 그 차이를 replication_primary_canary_age_ms라 부르며, 이는 복제 상태 엔드포인트에 노출됩니다. 성능 대기도 같은 계산을 수행해 sys/health와 sys/ha-status 엔드포인트에 replication_primary_canary_age_ms를 노출합니다. 성능 대기와 복제 보조는 앞서 언급한 "에코" 하트비트 RPC 페이로드의 일부로 현재 replication_primary_canary_age_ms 값을 포함해, 활성 노드나 기본 클러스터가 다운스트림 클라이언트가 보는 지연을 보고할 수 있게 합니다.
완화(Mitigations)
위 문제들에 대한 부분적 완화가 오랫동안 있었습니다. RPC를 통해 데이터를 쓸 때(예: 성능 대기가 로그인이나 동적 시크릿 생성 후 활성 노드에 토큰과 임대를 등록할 때), 응답의 일부에는 "WAL index"(Write-Ahead Log index, 미리 쓰기 로그 인덱스)로 알려진 숫자가 포함됩니다.
이에 대한 전체 설명은 이 문서의 범위를 벗어나지만, 간단히 말하면 성능 복제와 성능 대기 모두 로그 배송(log shipping)을 사용해 쓰기 소스의 업스트림과 동기화를 유지합니다. RPC를 통해 쓰기를 수행하는 노드들이 역사적으로 사용해 온 완화는 응답의 WAL index를 보고, 업스트림에서 배송되는 로그에 그 WAL index가 나타나는지 최대 2초 동안 기다리는 것입니다. WAL index가 보이면, RPC를 초래한 요청을 처리하는 Vault 노드는 클라이언트에 자체 응답을 반환할 수 있습니다. 즉 이후의 읽기는 방금 쓰인 값을 볼 수 있음을 알 수 있습니다. 그 2초 안에 WAL index가 보이지 않으면 Vault 노드는 어쨌든 요청을 완료하고 응답에 경고를 반환합니다.
이 완화 옵션은 Vault 1.7에도 여전히 존재하지만, 이제 대기 시간을 조정하는 구성 옵션이 있습니다. best_effort_wal_wait_duration.
Vault 1.7 완화
이제 다양한 다른 완화 방법을 사용할 수 있습니다.
- 요청별로 항상 요청을 활성 노드로 전달하는 옵션
- 요청별로 오래된 읽기가 될 경우에만 조건부로 활성 노드로 요청을 전달하는 옵션
- 요청별로 오래된 읽기가 될 수 있다면 요청을 실패시키는 옵션
- 프록시된 요청에 대해 위 작업을 수행하는 Vault 프록시 구성
이 문서의 나머지는 이러한 완화 방법의 트레이드오프와 사용 방법을 설명합니다.
전달을 요청하는 모든 헤더는 기본적으로 비활성화되며, allow_forwarding_via_header를 사용해 활성화해야 합니다.
무조건 전달(조건 전달) — 성능 대기 전용
성능 대기에서 오래된 읽기를 절대 겪지 않는 가장 간단한 해결책은 요청에 다음 HTTP 헤더를 제공하는 것입니다.
X-Vault-Forward: active-node
여기서의 단점은 모든 요청을 활성 노드로 전달한다면 성능 대기를 쓰지 않는 것과 같다는 것입니다. 따라서 이 완화는 선택적으로만 사용하는 것이 합리적입니다.
이 완화는 성능 복제와 관련된 오래된 읽기에는 도움이 되지 않습니다.
조건부 전달 — 성능 대기 전용
Vault Enterprise 1.7부터 스토리지를 수정하는 모든 요청은 새 HTTP 응답 헤더를 반환합니다.
X-Vault-Index: <base64 value>
그 쓰기 요청에서 비롯된 상태가 후속 요청에 보이도록 하려면 그 두 번째 요청에 다음 헤더를 추가하세요.
X-Vault-Index: <base64 value taken from previous response>
X-Vault-Inconsistent: forward-active-node
효과는 요청을 처리하는 노드가 로컬로 가진 상태를 보고, X-Vault-Index 헤더가 설명하는 상태를 포함하지 않으면 요청을 활성 노드로 전달하는 것입니다.
여기서의 단점은 요청이 활성 노드로 전달될 때 성능 대기가 제공하는 가치가 줄어든다는 것입니다. 이런 일이 자주 발생하면 활성 노드가 병목 지점이 되어, 성능 대기가 제공하려는 수평적 읽기 확장성을 제한할 수 있습니다.
오래된 요청 재시도하기
Vault Enterprise 1.7부터 스토리지를 수정하는 모든 요청은 새 HTTP 응답 헤더를 반환합니다.
X-Vault-Index: <base64 value>
그 쓰기 요청에서 비롯된 상태가 후속 요청에 보이도록 하려면 그 두 번째 요청에 이 헤더를 추가하세요.
X-Vault-Index: <base64 value taken from previous response>
원하는 상태가 없으면 Vault는 HTTP 상태 코드 412의 실패 응답을 반환합니다. 이것은 클라이언트에게 요청을 재시도해야 한다고 알려 줍니다. 위의 조건부 전달 솔루션에 비해 두 가지 장점이 있습니다. 첫째, 활성 노드에 추가 부하가 없습니다. 둘째, 이 솔루션은 성능 대기뿐 아니라 성능 복제에도 적용할 수 있습니다.
Vault Go API는 이제 412를 자동으로 재시도하며, X-Vault-Index 응답 헤더를 후속 요청의 요청 헤더로 전파하는 편의 메서드를 제공합니다. Vault Go API를 사용하지 않는 사용자는 자신의 클라이언트 라이브러리에 동등한 기능을 구축해야 합니다.
Vault 프록시와 일관성 헤더
구성되면 Vault API 프록시가 들어오는 요청을 Vault로 프록시합니다. api_proxy 스탠자에서 클라이언트를 수정하지 않고도 위 완화 중 일부를 활용할 수 있는 프록시 구성이 있습니다.
enforce_consistency="always"를 설정하면 프록시는 항상 X-Vault-Index 일관성 헤더를 제공합니다. 헤더에 사용할 값은 이전에 프록시를 통과한 응답을 기반으로 합니다.
when_inconsistent 옵션은 오래된 읽기가 어떻게 방지되는지 제어합니다.
"fail"—412응답이 보이면 클라이언트에 반환됩니다."retry"—412응답이 프록시에 의해 자동으로 재시도되어 클라이언트가 처리할 필요가 없습니다."forward"— 위의 조건부 전달 아래에 설명된 대로 프록시가X-Vault-Inconsistent: forward-active-node헤더를 제공합니다.
Vault 1.10 완화
Vault 1.10에서는 토큰 형식이 변경되어, 서비스 토큰이 이제 서버 측 일관성을 사용합니다. 즉, 기본적으로 Vault 토큰을 로컬에서 확인하는 데 필요한 WAL index가 없어 읽기-후-쓰기 일관성을 지원할 수 없는 노드로 만들어진 요청은 412 상태 코드를 출력합니다. Vault Go API는 412를 받으면 자동으로 재시도하므로, 상당한 복제 지연이 없는 한 사용자는 읽기-후-쓰기 일관성을 경험하게 됩니다.
복제 옵션 allow_forwarding_via_token을 사용하면 앞서 언급한 방식으로 412를 반환했을 요청이 대신 활성 노드로 전달되도록 강제할 수 있습니다.
클라이언트 API 도우미
새 헤더와 함께 사용할 api 패키지의 도우미 몇 개가 있습니다. WithRequestCallbacks와 WithResponseCallbacks는 클라이언트의 얕은 복사본을 만들어 주어진 콜백으로 채웁니다. RecordState와 RequireState는 한 요청의 응답 헤더를 저장하고 후속 요청에 제공하는 데 사용됩니다. 예를 들어:
client := api.NewClient(api.DefaultConfig)
var state string
_, err := client.WithResponseCallbacks(api.RecordState(&state)).Write(path, data)
secret, err := client.WithRequestCallbacks(api.RequireState(state)).Read(path)
이것은 Write가 저장한 데이터가 존재할 때까지 Read를 재시도합니다. 전달을 위한 콜백 ForwardInconsistent와 ForwardAlways도 있습니다.
출처: 문서