4P by GN⁺ | ★ favorite | 댓글 1개
  • Redis 기반 Redlock은 장애 허용 분산 락을 목표로 하지만, 정확성이 걸린 작업에는 안전성이 부족하고 효율 최적화 용도로는 지나치게 복잡함
  • 분산 락은 중복 작업을 줄이는 효율성 목적과 공유 상태를 보호하는 정확성 목적을 먼저 구분해야 하며, 실패 시 비용 증가인지 데이터 손상인지가 판단 기준임
  • 완벽한 락 서비스가 있어도 긴 GC 정지, 프로세스 일시 중단, 네트워크 지연 때문에 lease 만료 뒤 오래된 쓰기가 실행될 수 있어 fencing token이 필요함
  • Redlock은 락 획득마다 단조 증가하는 토큰을 만들 수 없고, Redis 키 만료가 gettimeofday 기반 시스템 시계에 의존해 시계 점프나 지연 상황에서 안전성이 깨질 수 있음
  • 정확성이 필요한 락에는 ZooKeeper 같은 합의 시스템과 fencing token 검사를 쓰고, Redis 단일 노드 락은 근사적·비핵심 용도로 제한해야 함

Redlock을 검토한 출발점

  • Redlock은 Redis 위에서 장애 허용 분산 락, 더 정확히는 lease를 구현하는 알고리듬임
  • 이미 10개가 넘는 독립 구현이 존재하고 누가 이 알고리듬에 의존하는지 알 수 없어 공개 검토할 가치가 있음
  • Redis 자체는 서버 간 일시적이고 근사적이며 빠르게 변하는 데이터를 공유하는 용도에 잘 맞음
    • 예: IP 주소별 요청 카운터, 사용자 ID별 고유 IP 집합
  • 우려되는 지점은 Redis가 더 강한 일관성내구성을 기대하는 데이터 관리 영역으로 점차 쓰이는 흐름이며, 분산 락도 그런 영역 중 하나임

락의 목적: 효율성인가 정확성인가

  • 분산 애플리케이션에서 락은 여러 노드가 같은 작업을 시도할 때 한 번에 하나만 수행하도록 하는 장치임
  • 락 사용 이유는 크게 두 가지로 나뉨
    • 효율성: 같은 비싼 계산을 두 번 하지 않기 위한 최적화이며, 실패해도 AWS 비용이 조금 늘거나 같은 이메일 알림이 두 번 가는 정도에 그침
    • 정확성: 동시 프로세스가 같은 상태를 망가뜨리지 않게 막기 위한 장치이며, 실패하면 파일 손상, 데이터 손실, 영구 불일치, 잘못된 약물 투여 같은 심각한 문제가 생길 수 있음
  • 효율성 목적의 락에는 5개 Redis 서버와 다수결 확인을 사용하는 Redlock의 비용과 복잡성이 불필요함
    • 단일 Redis 인스턴스와 필요 시 비동기 복제를 쓰는 편이 더 적합함
    • 이 경우 전원 장애나 Redis 노드 문제로 일부 락을 잃을 수 있지만, 비핵심 최적화라면 감수 가능한 실패임
  • Redlock은 5개 복제본과 다수결 때문에 정확성이 중요한 락에 맞아 보이지만, 실제로는 그 목적에 부적합함

lease만으로는 리소스를 안전하게 보호할 수 없음

  • 분산 시스템의 락은 멀티스레드 애플리케이션의 mutex와 다르며, 노드와 네트워크가 서로 독립적으로 실패할 수 있어 더 복잡함
  • 공유 스토리지의 파일을 갱신하는 전형적인 흐름은 락 획득, 파일 읽기, 수정, 다시 쓰기, 락 해제임
    • 락은 두 클라이언트가 read-modify-write를 동시에 수행해 업데이트를 잃는 일을 막기 위한 것임
  • 클라이언트가 락을 가진 채 오래 멈추면 lease가 만료될 수 있음
    • GC가 개입해 클라이언트가 긴 시간 정지할 수 있음
    • lease는 크래시한 클라이언트가 락을 영원히 잡지 않도록 하는 좋은 설계지만, 정지 시간이 만료 시간보다 길면 클라이언트는 만료 사실을 모른 채 위험한 쓰기를 실행할 수 있음
  • 이 문제는 이론적 사례가 아니며 HBase에도 과거 유사 문제가 있었음
    • “stop-the-world” GC 정지는 몇 분 동안 지속된 사례가 있음
    • HotSpot JVM의 CMS 같은 “concurrent” GC도 때때로 애플리케이션을 멈춰야 함
  • 쓰기 직전에 락 만료 여부를 확인하는 방식으로는 해결되지 않음
    • GC는 마지막 확인과 쓰기 작업 사이를 포함해 어떤 지점에서도 실행 중인 스레드를 멈출 수 있음

프로세스 정지와 네트워크 지연은 일반적인 위협 모델임

  • 긴 GC 정지가 없는 런타임을 쓰더라도 프로세스는 여러 이유로 멈출 수 있음
    • 메모리에 없는 주소를 읽어 page fault가 발생할 수 있음
    • 디스크가 EBS라면 변수 읽기가 Amazon 네트워크를 통한 동기 요청으로 바뀔 수 있음
    • CPU 경합, 스케줄러 지연, 실수로 보낸 SIGSTOP도 프로세스를 멈출 수 있음
  • 네트워크 지연도 같은 문제를 만듦
    • 애플리케이션이 쓰기 요청을 보냈지만, 패킷이 지연되어 lease 만료 뒤 스토리지 서버에 도착할 수 있음
    • GitHub의 한 장애에서는 네트워크 패킷이 약 90초 지연됨
  • Ethernet과 IP 같은 패킷 네트워크는 패킷을 임의로 지연시킬 수 있고 실제로 그런 일이 발생함
  • 따라서 잘 관리된 네트워크에서도 타이밍을 가정할 수 없으며, 단순한 lease 기반 코드는 어떤 락 서비스를 쓰더라도 근본적으로 안전하지 않음

fencing token으로 오래된 쓰기를 차단해야 함

  • 해결책은 모든 스토리지 쓰기 요청에 fencing token을 포함하는 것임
  • fencing token은 클라이언트가 락을 획득할 때마다 증가하는 숫자임
    • 예: 클라이언트 1이 token 33으로 lease를 얻은 뒤 오래 멈추고 lease가 만료됨
    • 클라이언트 2가 token 34로 새 lease를 얻고 스토리지에 쓰기 요청을 보냄
    • 나중에 클라이언트 1이 깨어나 token 33으로 쓰기를 보내면, 스토리지는 이미 더 높은 token 34를 처리했으므로 token 33 요청을 거부함
  • 스토리지 서버가 토큰을 적극적으로 검사하고, 토큰 값이 뒤로 간 쓰기를 거부해야 안전함
  • 락 서비스가 엄격하게 단조 증가하는 토큰을 생성하면 락을 안전하게 만들 수 있음
    • ZooKeeper를 락 서비스로 쓰는 경우 zxid나 znode 버전 번호를 fencing token으로 사용할 수 있음
  • Redlock의 큰 문제는 fencing token 생성 기능이 없음
    • Redlock의 고유한 난수 값은 필요한 단조 증가성을 제공하지 않음
    • 단일 Redis 노드의 카운터는 그 노드가 실패할 수 있어 충분하지 않음
    • 여러 노드의 카운터는 서로 어긋날 수 있음
    • fencing token 생성을 위해서도 합의 알고리듬이 필요할 가능성이 있음

Redlock은 시간 가정에 안전성을 의존함

  • 분산 알고리듬에서 실용적인 모델은 비동기 모델과 불신뢰 실패 감지기
    • 프로세스는 임의 길이로 멈출 수 있음
    • 패킷은 네트워크에서 임의로 지연될 수 있음
    • 시계는 임의로 틀릴 수 있음
    • 그래도 알고리듬은 올바른 결정을 해야 함
  • 시계는 노드가 다운됐을 때 영원히 기다리지 않기 위한 timeout 생성에만 쓰일 수 있음
    • timeout은 정확할 필요가 없으며, 요청이 timeout됐다고 상대 노드가 반드시 다운된 것은 아님
    • 네트워크 지연이나 로컬 시계 오류일 수도 있음
  • Redis는 키 만료를 결정할 때 monotonic clock이 아니라 gettimeofday를 사용함
    • gettimeofday는 시스템 시간이 불연속적으로 점프할 수 있음
    • NTP가 시계를 조정하거나 관리자가 수동으로 시간을 바꾸면 Redis 키 만료가 예상보다 훨씬 빠르거나 느려질 수 있음
  • 비동기 모델의 알고리듬은 일반적으로 타이밍 가정 없이 안전성을 유지하고, timeout 같은 실패 감지기는 활성에만 영향을 줌
    • 타이밍이 엉망이면 성능은 나빠질 수 있지만 잘못된 결정을 내리지는 않아야 함
  • Redlock은 이와 다르게 안전성이 여러 타이밍 가정에 의존함
    • 모든 Redis 노드가 대략 올바른 시간 동안 키를 유지해야 함
    • 네트워크 지연이 만료 시간보다 충분히 작아야 함
    • 프로세스 정지가 만료 시간보다 훨씬 짧아야 함

나쁜 타이밍에서 Redlock이 깨지는 사례

  • 5개 Redis 노드 A, B, C, D, E와 클라이언트 1, 2가 있을 때, 한 노드의 시계가 앞으로 점프하면 두 클라이언트가 모두 락을 가졌다고 믿을 수 있음
    • 클라이언트 1이 A, B, C에서 락을 얻고 네트워크 문제로 D, E에는 닿지 못함
    • C의 시계가 앞으로 점프해 락이 만료됨
    • 클라이언트 2가 C, D, E에서 락을 얻고 네트워크 문제로 A, B에는 닿지 못함
    • 결과적으로 클라이언트 1과 2가 모두 락 보유자라고 판단함
  • C가 락을 디스크에 지속화하기 전에 크래시하고 즉시 재시작해도 비슷한 문제가 생길 수 있음
    • Redlock 문서는 크래시한 노드의 재시작을 가장 긴 락 TTL 이상 지연하라고 권장함
    • 이 재시작 지연도 합리적으로 정확한 시간 측정에 의존하며, 시계가 점프하면 실패할 수 있음
  • 클라이언트 프로세스 정지도 Redlock을 깨뜨릴 수 있음
    • 클라이언트 1이 A, B, C, D, E에 락을 요청함
    • 응답이 이동 중일 때 클라이언트 1이 stop-the-world GC에 들어감
    • 모든 Redis 노드의 락이 만료됨
    • 클라이언트 2가 A, B, C, D, E에서 락을 얻음
    • 클라이언트 1이 GC를 끝내고 커널 네트워크 버퍼에 머물던 성공 응답을 받음
    • 두 클라이언트가 모두 락을 보유한다고 믿음
  • Redis가 C로 작성되어 GC가 없다는 점은 도움이 되지 않음
    • 문제는 클라이언트가 GC 정지를 겪을 수 있는 시스템에서 발생함
    • fencing token 같은 방식으로 클라이언트 2가 락을 얻은 뒤 클라이언트 1의 작업을 막아야 안전함
  • 긴 네트워크 지연도 프로세스 정지와 같은 효과를 낼 수 있음
    • TCP user timeout을 Redis TTL보다 훨씬 짧게 설정하면 지연 패킷이 무시될 가능성이 있지만, 구체적인 TCP 구현을 봐야 확신할 수 있음
    • 이 경우에도 다시 시간 측정의 정확성 문제로 돌아감

Redlock이 요구하는 동기 시스템 가정

  • Redlock은 다음 속성을 가진 동기 시스템 모델에서만 올바르게 동작함
    • 네트워크 지연의 상한이 보장됨
    • 프로세스 정지 시간이 제한됨
    • 시계 오차가 제한됨
  • 동기 모델은 시계가 정확히 동기화된다는 뜻이 아니라, 네트워크 지연·정지·시계 drift에 대해 알려진 고정 상한이 있다는 뜻임
  • Redlock은 지연, 정지, drift가 모두 락 TTL에 비해 작다고 가정함
    • 타이밍 문제가 TTL만큼 커지면 알고리듬이 실패함
  • 일반적인 데이터센터 환경에서는 이런 타이밍 가정이 대부분의 시간에 만족될 수 있으며, 이를 부분 동기 시스템이라고 부름
  • 정확성이 락에 의존한다면 “대부분의 시간”은 충분하지 않음
    • 타이밍 가정이 깨지는 순간 Redlock은 한 클라이언트의 lease가 만료되기 전에 다른 클라이언트에 lease를 부여하는 등 안전성을 위반할 수 있음
    • GitHub의 90초 패킷 지연 사례는 실제 환경에서 동기 시스템 모델을 가정하기 어렵다는 근거임
  • Raft, Viewstamped Replication, Zab, Paxos는 부분 동기 시스템 모델 또는 실패 감지기가 있는 비동기 모델을 위해 설계된 합의 알고리듬 범주에 속함
    • 이런 알고리듬은 타이밍 가정을 버려야 하며, 분산 시스템의 네트워크·프로세스·시계가 실제보다 신뢰할 만하다고 가정하지 않도록 주의해야 함

결론과 권장 선택지

  • Redlock은 효율 최적화용 락에는 불필요하게 무겁고 비싸며, 정확성이 걸린 락에는 충분히 안전하지 않음
  • 특히 네트워크 지연과 연산 실행 시간에 상한이 있는 동기 시스템을 사실상 가정하고, 그 가정이 깨지면 안전성을 위반할 수 있음
  • 긴 네트워크 지연이나 멈춘 프로세스로부터 시스템을 보호하는 fencing token 생성 기능도 없음
  • 최선 노력 기반의 효율성 최적화 락이 필요하다면 Redis의 단일 노드 락 알고리듬을 쓰는 편이 나음
    • 조건부 set-if-not-exists로 락을 얻음
    • 값이 일치할 때만 원자적으로 삭제해 락을 해제함
    • 코드에 락이 근사적이며 가끔 실패할 수 있음을 명확히 문서화해야 함
    • 5개 Redis 노드 클러스터를 구성할 필요는 없음
  • 정확성이 필요한 락에는 Redlock을 쓰지 말고 ZooKeeper 같은 합의 시스템을 사용해야 함
    • 가능하면 락을 구현한 Curator recipes를 사용할 수 있음
    • 최소한 합리적인 트랜잭션 보장을 제공하는 PostgreSQL 같은 데이터베이스를 사용할 수 있음
    • 락 아래의 모든 리소스 접근에는 fencing token 검사를 강제해야 함
  • Redis는 의도한 용도에 맞게 쓰면 유용한 도구이며, 모든 도구에는 한계가 있으므로 그 한계를 알고 계획해야 함
  • 2016년 2월 9일 업데이트에서 Redlock 원저자인 Salvatore가 반박 글을 게시했지만, 결론은 유지됨

댓글과 토론

Hacker News 의견들
  • 직장에서는 Temporal을 쓰고 있고, 전용 워크플로와 시그널로 분산 락을 구현했음
    지금까지는 잘 동작하고, 락의 분산 처리 부분을 Temporal 기능에 맡기니 구현도 꽤 단순함

    • Temporal을 방금 알게 됐는데, 복잡한 인프라 위에서 고수준 작업 관리를 아주 명확하게 제공하는 워크플로의 성배처럼 보임
      이 분야에서 Temporal이 독보적인지, 아니면 비슷한 수준의 대안이 있는지 궁금함
      Uber에서 분리됐고 주요 벤더들이 쓴다니 충분히 실전 검증된 것처럼 들림
    • 흥미로움. 조금 더 자세히 설명해줄 수 있나? Temporal로 비슷한 걸 만들어보고 싶음
    • Temporal을 써보고 싶은데 불안정할 수 있다는 얘기를 들었음. 실제로는 잘 동작했나?
  • 분산 락에는 보통 PostgreSQL advisory lock을 사용함
    작업이 데이터베이스와 무관하더라도 트랜잭션을 시작하고 advisory lock을 잡으면, 앱이 직접 해제하거나 크래시 등으로 트랜잭션이 끝날 때까지 락이 유지됨
    지금까지는 꽤 안전하다고 느꼈지만, 데이터베이스 연결이 아직 정상인지 확인한 적이 없다는 걸 방금 깨달음
    데이터베이스 관련 작업이면 쿼리가 실패하면서 작업도 실패하겠지만, 그렇지 않으면 이미 락을 잃었는데도 모를 수 있음
    펜싱 토큰이나 원자적 연산 같은 것 없이 절대적인 정확성을 원하면 결국 모든 작업에 2단계 커밋이 필요한 건가 싶음

    • advisory lock에는 함정이 많음. [0] 참고
      아마 의도했던 걸 올바르게 하려면 “EXCLUSIVE”나 “ACCESS EXCLUSIVE”를 쓰거나, 작업에 대해 2단계 커밋 또는 멱등성을 확보해야 함
      [0] https://www.postgresql.org/docs/current/explicit-locking.htm...
    • 락에서 주의할 점은, 내가 알기로 락이 연결별이라는 것임
      대부분의 라이브러리는 보통 연결 풀을 쓰므로, 락 전용 연결을 확보하고 주기적인 락 확인도 반드시 그 연결로 해야 함
  • 예전에 이 블로그 댓글에 남긴 내 댓글과, 내 블로그에 쓴 답글을 읽어보면 좋겠음
    무작위 순서로 짚자면, 글쓴이는 알고리즘이 어떻게 동작하는지에 대한 핵심 지점을 놓쳤고, 그다음 남은 더 약한 근거들로 알고리즘을 거부했음
    현대 컴퓨터와 API에서는 대략적으로 올바른 시간만큼 기다리는 게 불가능하다는 말도 사실이 아님. GC 일시정지는 bounded이고 단조 시계도 동작하므로 받아들일 수 있는 가정임
    자동 해제 메커니즘 자체가 잠재적 경쟁 상태를 노출한다는 이유로 비판하는 것과, 알고리즘의 목표와 시스템 모델 안에서 비판하는 것은 다른 문제임
    Redlock은 수년간 많은 사용 사례에서 성공적으로 쓰였고, 타임아웃을 작업 완료 시간과 일반 운영체제에서 생길 수 있는 임의 일시정지보다 훨씬 크게 잡으면 경쟁 상태를 유발하기 매우 어려움
    물론 자동 해제 타임아웃을 너무 작게 잡고 작업이 그 시간만큼 쉽게 걸릴 수 있다면 설계 오류지만, 그건 Redlock 자체의 문제가 아님

    • 솔직히 예전부터 그 답글 블로그 글이 이해가 잘 안 됐음. 이 질문으로 공통점을 찾을 수 있을지도 모르겠음
      타임아웃이 꽤 짧고(예: 1~2초), 작업은 보통 그 타임아웃의 약 90%를 쓰며, RedLock 락을 잡은 동안 하는 작업이 다른 락 보유자와 절대 동시에 실행되면 안 되는 상황에서 RedLock을 쓰겠나?
      여기서 올바른 답은 항상 “아니오”라고 봄. 클라이언트가 작업을 끝내기 전에 임대가 만료될 위험이 매우 크기 때문임
      RedLock은 모든 상황에서 상호 배제를 보장할 수 없으므로 작업을 멱등적으로 바꿔야 하고, 이런 유형은 낙관적 락으로 구현하는 편이 좋음
    • 링크를 줄 수 있나?
  • 저수준과 알고리즘 지식을 다시 보강하는 중인데, 이 주제에 좋은 책이 뭐가 있을까? 저자가 쓴 책은 가지고 있음
    재미로 뭔가 만들어보고 싶은데, 자료들이 장난감 수준이거나 너무 복잡한 것뿐임

    • Alex Xu의 System Design Interview I, II를 추천함
      주제 하나를 골라 실제로 구현해보면 됨
  • 예전에 이 자료를 바탕으로 분산 락 블로그 글을 썼음: https://medium.com/sahibinden-technology/an-easy-integration...

  • “락에는 타임아웃이 있다(즉 임대다)”는 설명이 이상하게 들림
    첫째, 클라이언트가 크래시하면 애초에 timed lease가 없어도 OS나 supervisor가 락을 해제해야 하고, 둘 다 죽어도 연결이 결국 끊기며 네트워크 시스템이 리셋·타임아웃·하트비트 부재 등으로 이를 감지해 연결을 무효화한 뒤 락을 풀어야 함
    둘째, 클라이언트가 버그로 크래시하지 않은 채 락을 너무 오래 잡고 있는 게 문제라면, 어떤 supervisor가 이를 감지해 다른 이들을 위해 락을 풀기 전에 클라이언트를 죽여야 하는 것 아닌가?
    셋째, 이런 코너 케이스를 다루려고 타임아웃 있는 락을 둔다면, 실제 프로그램에 예외·시그널·종료 같은 방식으로 알려야 하지 않나? 그리고 락을 풀기 전에 프로그램이 통지받았는지 검증을 기다려야 하지 않나?
    타임아웃이 걸렸는데도 프로그램이 평범한 제어 흐름을 계속 실행하도록 허용하는 발상 자체가 문제의 근본 원인처럼 보이는데, 왜 다들 넘어가는지 모르겠음. 뭔가 명백한 이유를 놓치고 있는 건가?

    • 이건 뮤텍스가 아니라 그 분산 시스템 버전
      락을 자기 쪽에서 무효화하는 주체는 저장소 서비스이고, Redlock이 제공하지 않는 추가 보장 없이는 클라이언트가 자기 문제를 스스로 감지하지 못함
    • 서버가 클라이언트로부터 항상 RST나 FIN을 받을 것이라는 가정은 틀렸음
      어떤 경우에는 이 패킷들이 드롭되고, 원격 머신의 클라이언트는 이미 죽었는데 서버에는 열린 연결이 남아 있을 수 있음
      덧붙여 말하면, 비추천 누른 건 내가 아님
  • Deno와 Deno Deploy가 호스팅하는 Deno KV로 분산 락을 구현했음
    내부적으로 분산 데이터베이스인 FoundationDB를 사용하고, 로컬 장치에서 도는 Deno 인스턴스들이 같은 Deno KV에 연결해 락을 획득함
    PostgreSQL을 써도 SELECT FOR UPDATE로 동작하지만, 데이터베이스 자체는 분산형이 아님

  • 2018년에 우리 사용 사례에 Redis를 검토했지만, 결국 덜 화려한 해결책을 택했고 정말로 한 번도 실패하지 않았음
    사용 사례는 캠페인의 유한한 티켓 집합에서 식별자가 있는 티켓을 하나씩 나눠주는 것이었고, Ticketmaster가 공연장 좌석을 배정하는 것과 비슷했음
    요청이 오면 가능한 티켓을 제공하고, 요청의 메타데이터를 할당된 티켓에 붙인 뒤, 이후 요청 대상에서 제외해야 했음
    과거에는 초과 할당, 미달 할당, 중복 할당 같은 실패 캠페인이 있었기 때문에 정확성이 핵심이었음
    Redis로 락 획득, 락 확인, 작업 수행, 락 해제를 하는 단순 구현도 시도했지만 당시 우리에게는 운영 부담이 컸고, 그 길로 가지 않은 게 다행임
    최종 선택은 Postgres였음. 우리의 “분산 락”은 Postgres 고유 기능을 쓴 복합 UPDATE 문에 가까웠고, 요청을 일종의 집합 연산으로 바꿔 데이터베이스가 성공 레코드나 실패 표시를 반환하게 했음. ACID 트랜잭션이 승리했음
    정확성을 해결한 뒤에는 규모와 성능을 봤고, 초당 수백만 요청은 필요 없었지만 순간적인 급증 기준은 있었음
    클러스터 안에서 읽기·쓰기 데이터베이스 인스턴스를 최적화하고, 더 크거나 수요가 높은 캠페인을 지정된 시스템에 전략적으로 배치했으며, 2년 동안 최적화를 이어갔지만 티켓 배포 실패 캠페인은 단 한 번도 없었음
    분산 락 기술 전문가는 아니고, 해결해야 할 문제에 집중하며 몇 가지를 시도한 뒤 맞는 해법을 찾았을 뿐임

    • 초당 최대 5만 개의 짧은 원자적 트랜잭션이면 Postgres만 써도 된다는 점은 맞음
      UPDATE 트랜잭션이 몇 마이크로초만 지속되므로 문제를 중앙집중화할 수 있고, 그게 더 단순하고 빠르고 안전함
      하지만 글에서 설명하듯 이건 분산 문제가 아님
      분산 시스템의 락은 다중 스레드 앱의 뮤텍스와 다르며, 여러 노드와 네트워크가 각자 다양한 방식으로 독립적으로 실패할 수 있어 더 복잡함
      트랜잭션이 몇 초에서 몇 시간 걸릴 수 있고, 관련 머신이 락을 잡은 상태에서 실패할 수 있을 때 분산 락이 필요함
    • 이 사례가 보여주는 중요한 점은, 필요한 것은 락이 아니라 제약조건이라는 것임
      이 경우 제약은 “N개보다 많은 티켓을 팔지 말라”이고, 이런 문제의 현실적인 트래픽 규모 대부분은 전통적인 관계형 데이터베이스의 트랜잭션 동작으로 해결할 수 있으며 내부 락 관리는 데이터베이스에 맡기면 됨
      개발자들이 “분산 락을 만들겠다”로 너무 빨리 뛰어들지 않았으면 함. 거의 항상 더 나은 답이 있지만, 그 답은 애플리케이션마다 다름
    • 결국 답은, 생각과 달리 대부분의 경우 분산 락이 필요 없다는 것이었음 :)
    • 이건 콘서트별로 다른 인스턴스에 샤딩할 수 있어서 민망할 정도로 병렬화가 쉬운 문제처럼 보임
      새로 나온 Cloudflare의 SQLite 같은 것에 맞는 일일 수도 있음
    • 이게 가장 좋은 방식이고, 실제로 문제에 접근하는 유일하게 합리적인 방식임
      처음 이 내용을 읽은 곳은 여기였음: https://code.flickr.net/2010/02/08/ticket-servers-distribute...
  • 많은 엔지니어는 너무 늦기 전까지 정확성 문제에 진심으로 신경 쓰지 않음. 보안과 비슷함
    신경은 쓰더라도 자신이 하는 일이 올바른지 확인하지 않는 경우도 많음
    예를 들어 내 분야에서는 마이크로서비스, 액터, 프로세스가 네트워크로 메시지를 주고받는데, 내가 보는 구현의 95% 이상은 메시지가 유실되거나 순서가 바뀌어 처리될 수 있는 엣지 케이스가 있음
    하지만 이 문제를 고칠 만큼 인센티브가 정렬되어 있지 않음. 임원과 엔지니어의 보상 구조가 고객과 주주에게 최선인 결과와 맞지 않음

    • 마이크로서비스 자체가 종종 이 문제의 증상임
      별 이유 없이 함수 호출 사이에 네트워크 경계를 넣고 싶어 하고, 그다음 그 함수 호출을 위해 HTTP 서버와 클라이언트, JSON 직렬화·역직렬화를 끝없이 만들며, 운이 좋으면 gRPC를 쓰고, 그 네트워크 경계 너머로 분산 트랜잭션 같은 것을 다시 구현하려 함
      결국 피할 수 없는 “원격에서 벌어지는 섬뜩한 상호작용”을 처리하는 바쁜 일만 생김
    • 고치려면 먼저 측정하고 모니터링한 다음, 고객 경험을 나타내는 서비스 수준 목표를 세워야 함
      제품팀과 엔지니어링팀이 이에 합의해야 하고, SLO를 위반하면 시스템 안정성에 초점을 옮겨야 함
      모두를 설득하기는 어렵기 때문에 좋은 리더십이 필요함
      버그가 튀어나오고 새 기능이 느리거나 거의 없어서 고객이 이탈하기 시작하면, 품질을 프로세스의 일부로 만들어야 한다는 근거는 매우 쉬워짐
      성숙한 리더는 가능한 한 일찍 그 단계에 앞서 움직임
    • 그런 종류의 문제는 무고한 사람을 감옥에 보내거나 죽게 만들 수도 있음
      [0] https://en.wikipedia.org/wiki/British_Post_Office_scandal
    • 어느 정도 인센티브 정렬은 있다고 봄. 이런 엣지 케이스는 까다로워서 개발자들이 많은 지원 티켓을 처리해야 할 가능성이 높고, 그건 누구에게도 좋지 않음
      하지만 어제의 관리자들에게 제대로 만들 시간을 달라고 설득할 방법은 잘 보이지 않음
  • 이건 일을 너무 복잡하게 만듦
    글에서 말하는 펜싱 토큰 같은 게 있다면 락은 필요 없음
    토큰은 단조 증가할 필요도 없고, 클라이언트와 저장소가 함께 가진 수동적인 고유값이면 됨
    버전 토큰이라고 부르면, 단조 증가값이어도 되고 보통 더 쉬운 생성 UUID도 동작함. 기술적으로는 저장소 전체 데이터의 해시도 가능하지만 실용적이진 않음
    흐름은 이렇다. 클라이언트가 저장소에서 현재 버전 토큰과 수정할 데이터를 함께 가져오고, 저장소는 데이터와 토큰을 원자적으로 조회해 그 토큰이 해당 데이터 버전의 것임을 보장함
    그다음 클라이언트가 변경 사항과 함께 버전 토큰을 다시 보내고, 저장소는 현재 토큰이 전달된 토큰과 일치할 때만 변경을 받아들이며 새 버전 토큰을 원자적으로 생성함
    다른 이유로 락을 도입할 수는 있지만, 분산 시스템에서 저장소 무결성과는 독립적이어야 함
    “락”이라는 용어도 별로 좋아하지 않음. 임시적이고 보장되지 않으니 임대나 예약이 의미를 더 잘 전달할 수 있음

    • 설명하는 건 비교 후 교체(CAS) 이고 좋은 해법임
      복잡성을 데이터베이스 쪽으로 밀어 넣는 방식인데, 여기서는 분산 락이라는 점을 기억해야 함
      단일 데이터베이스라면 데이터베이스가 크래시해서 어떤 CAS 쓰기가 실제 반영됐는지 모르는 상태가 되기 전까지는 단순함
      높은 가용성과 다중 데이터센터 백업이 필요한 대형 시스템에서는 노드 실패 주변 시나리오 때문에 이 방식도 깨질 수 있어 꽤 복잡해짐
      보통 Paxos 형태의 트랜잭션 로그를 사용함. 분산 시스템에 쉬운 해법이 있다고 가정하면 안 됨. 늘 골치 아픔
    • 문제를 잘못 해석했고, 다른 문제에 대한 해법을 제안하고 있음
    • 이건 낙관적 락으로 알려져 있음. 하지만 분산 락 메커니즘이라고 부르지는 않겠음
    • 이 설명은 글에서 락을 쓰는 첫 번째 이유를 놓치고 있음
      효율성 관점에서는 락을 잡으면 같은 작업을 불필요하게 두 번 하는 것을 피할 수 있음. 예를 들어 비싼 계산 같은 것임
      락이 실패해 두 노드가 같은 작업을 하더라도 결과가 약간의 비용 증가나 같은 이메일 알림 중복 정도라면 사소할 수 있음
      하지만 여러 노드가 같은 작업을 하는 것은 예시에 적힌 것보다 훨씬 나쁘다고 봄. 확장 가능한 분산 처리 자체를 방해할 수 있기 때문임
    • 단조 증가 토큰을 쓰지 않으면 불일치 상태로 이어지지 않나?
      저장소 시스템에 노드가 두 개 있고, 읽기-수정-쓰기 프로세스가 두 개 돈다고 하자. 프로세스 1과 2가 모두 첫 토큰 abc를 얻음
      프로세스 1이 커밋해서 토큰이 cde로 바뀌고 변경이 노드 2로 스트리밍되지만, 네트워크 지연 때문에 노드 2에는 늦게 도착함
      그 사이 프로세스 2가 토큰 abc로 노드 2에 커밋하면, 노드 2는 아직 노드 1의 메시지를 받지 못했으니 변경을 받아들이고 시스템은 불일치 상태가 됨
      단조 증가 펜싱 토큰이 있으면 이런 일이 생기지 않음. 그 요구사항이 토큰을 제공하기 전에 노드들이 전체 작업 순서에 합의하도록 강제하기 때문임