데이터 중심 애플리케이션 설계 Ch 8. 분산 시스템의 골칫거리

작성일: 2021-12-16 22:23

# 결함과 부분 장애

분산 시스템에서 시스템이 커질수록 구성 요소 중 하나가 고장날 가능성도 높다. 수천 개의 노드가 있는 시스템은 항상 뭔가 고장난 상태라고 가정하는게 합리적이다.

소프트웨어 운영자로서 결함이 발생하면 소프트웨어가 어떻게 동작하는지 알아야 한다. 최선의 상황을 바라기만 하는 것은 현명하지 못하다. 분산 시스템에서 의심, 비관주의, 편집증은 그 값어치를 한다.

분산 시스템이 동작하게 만드려면 부분 장애 가능성을 항상 받아들이고 소프트웨어에 내결함성 메커니즘을 넣어야 한다. 즉, 신뢰성 없는 구성 요소를 사용해 신뢰성 있는 시스템을 구축해야 한다.

# TCP/IP: 신뢰성 없는 구성 요소를 사용해 신뢰성 있는 시스템 구축한 예

  • 신뢰성 없는 IP(Internet Protocol)위에 신뢰성 높은 TCP(Transimission Control Protocal)를 두어 패킷 손실 시 재전송하고 순서에 맞춰 재조립되도록 보장해준다.

# 신뢰성 없는 네트워크

여기서 다루는 분산 시스템은 비공유 시스템, 즉 네트워크로 연결된 다수의 장비로 네트워크가 이 장비들의 유일한 통신 수단이다.

인터넷과 데이터센터 내부 이더넷은 비동기 패킷 네트워크다. 즉, 노드는 다른 노드로 메시지를 보낼 수 있지만 네트워크는 메시지가 언제 도착할 것인지 보장하지 않는다.

요청을 보내고 응답을 기다릴 때 잘못될 수 있는 경우는 많다.

  1. 요청이 손실됨(네트워크 케이블 뽑힘)
  2. 요청이 큐에서 대기하다가 나중에 전송됨(네트워크나 수신자에 과부하)
  3. 원격 노드에 장애(노드가 죽음)
  4. 원격 노드의 일시적인 중지(GC)
  5. 원격 노드가 요청을 처리했지만 응답이 네트워크에서 손실
  6. 원격 노드가 요청을 처리했지만 응답이 지연

전송 측은 패킷이 전송됐는지 아닌지조차 구별할 수 없다. 유일한 정보는 응답을 아직 받지 못했다는 것이다.

  • 이런 문제를 다루는 대표적인 방법은 타임아웃을 활용하여 응답이 도착하지 않았음을 가정한다.

# 현실의 네트워크 결함

  • 누구도 네트워크 문제에서 자유로울 수 없다. 상어가 해저 케이블을 물어뜯어서 손상시키기도 한다.
  • 시스템 환경에서 네트워크 결함이 드물더라도 일어날 수 있다는 사실은 소프트웨어가 이를 처리할 수 있어야 한다는 뜻이다.

# 결함 감지

  • 많은 시스템은 결함 있는 노드를 자동으로 감지할 수 있어야 한다.
    • 로드 밸런서는 죽은 노드로 요청을 보내면 안된다.
    • 단일 리더 복제를 사용하는 분산 데이터베이스 시스템에서 리더에 장애가 발생하면 팔로워 중 하나가 리더로 승격돼야 한다.
  • 하지만, 불행하게도 네트워크에 관한 불확실성 때문에 노드가 동작 중인지 아닌지 구별하기 어렵다.
    • TCP가 패킷이 전달됐다는 확인 응답을 했더라도 애플리케이션이 그것을 처리하기 전에 죽을 수도 있다.
    • 몇 번 재시도를 해 보고 타임아웃이 게속 발생하면 마침내 노드가 죽었다고 선언할 수 있다.

# 타임아웃과 기약 없는 지연

  • 타임아웃으로 결함을 감지할 수 있다면 타임아웃은 얼마나 길어야 할까?
  • 타임아웃이 길면 노드가 죽었다고 선언될 때까지 기다리는 시간이 길어진다.
  • 타임아웃이 짧으면 결함을 빨리 발견되지만 노드의 일시적 중단에도 죽었다고 잘못 판단할 위험이 있다.
    • 성급하게 노드가 죽었다고 선언하면 같은 동작이 여러번 수행될 수 있다.
    • 만약 과부하로 인해 노드가 죽었다고 잘못 판단하는 경우 리밸런싱 과정은 더욱 더 상태를 악화시킬 수 있다.
  • 고정된 타임아웃을 설정하는 대신 시스템이 지속적으로 응답 시간과 그들의 변동성을 측정해 유동적으로 타임아웃을 조절하게 하는 것이 좋다.

# TCP vs UDP

  • TCP는 패킷이 손실되면 자동으로 재전송을 시도한다.
    • 애플리케이션에서는 이를 모르지만 그 결과로 생긴 지연으로 판단할 수 있다.
  • UDP는 흐름 제어를 하지 않고 손실된 패킷을 재전송하지 않는다. 네트워크 지연이 크게 변하게 하는 원인 중 일부를 제거한다.
    • 그러므로, UDP는 지연된 데이터의 가치가 없는 상황에 선택하면 좋다.
    • 화상 회의나 인터넷 전화는 지연된 데이터의 가치가 없기 때문에 UDP를 사용한다.

# 동기 네트워크 vs 비동기 네트워크

  • 동기식 네트워크의 대표적인 예시는 전화 네트워크이다. 전화 네트워크는 극단적인 신뢰성을 가진다.
    • 전화 네트워크에서 통화할 때는 회선(circuit)이 만들어지며 통화가 끝날 때까지 유지된다.
    • 동기식 네트워크는 이미 특정 공간만큼의 회선이 할당되어 있기 때문에 데이터가 여러 라우터를 거치더라도 큐 대기 문제를 겪지 않는다.
  • 동기식 네트워크와 같이 회선을 할당하는 방식은 통화와 같은 초당 전송하는 비트 수가 고정되어 있는 경우 회선이 적절하지만 웹 페이지 요청과 같이 순간적으로 몰릴 수 있는 데이터 전송에 효율적이지 못하다.
  • 비동기 네트워크의 대표적인 예시는 인터넷이다. 인터넷은 대역폭을 동적으로 공유한다.
    • 전송 측은 가능하면 빨리 패킷을 보내기 위해 서로 밀치며 네트워크 스위치가 빈번하게 어떤 패킷을 보낼지(대역폭을 할당할지) 결정한다. 이 방법은 큐 대기가 생길 수 있지만 선로를 효율적으로 이용할 수 있다.
    • 이 방식은 자원을 최대한 효율적으로 사용한다. 하지만 지연이라는 큰 변동이 생기게 된다.
    • 통화와 같이 회선을 점유하는 방식은 해당 회선이 점유한 대역폭만큼을 계속 보유하기 때문에 실제 대역폭 만큼 데이터를 전송하지 않더라도 대역폭은 게속 할당된다. 대신 지연의 변동은 적다.

# 신뢰성 없는 시계

네트워크에 있는 개별 장비는 자신의 시계를 갖고 있다. 이 장치는 완벽히 정확하지 않아서 각 장비는 자신만의 시간 개념이 있으며 이는 다른 장비보다 약간 빠를 수도 느릴 수도 있다.

# 일 기준 시계 대 단조 시계

  • 현대 컴퓨터는 최소 두 가지 종류의 시계를 갖고 있다. 일 기준 시계(time-of-day clock)단조 시계(monotinic clock)다.

# 1) 일 기준 시계

  • 일 기준 시계는 벽시계 시간이라고도 하며 현재 날짜와 시간을 반환한다.
    • Java의 System.currentTimeMillis()는 epoch이래로 흐른 밀리초를 반환한다.
  • 일 기준 시계는 보통 NTP로 동기화된다.
    • NTP로 동기화를 하더라도 네트워크 지연이 있기 때문에 모든 분산시스템에서 완벽히 동일한 일 기준 시계를 가지는건 불가능하다.

# 2) 단조 시계

  • 단조 시계는 항상 앞으로만 흐르는 시계로 컴퓨터 별로 고유한 값을 가진다.
    • Java의 System.nanoTime()가 대표적인 예다.
    • 컴퓨터 별로 고유하기 때문에 다른 컴퓨터의 단조 시계와 비교하는건 의미가 없다.
  • 단조 시계는 타임아웃이나 서비스 응답 시간 같은 지속 시간과 같이 두 시점 사이에 흐른 시간이 얼마인지 재는데 적합하다.
  • 로컬 시계가 NTP보다 빠르거나 느릴 때 단조 시계가 진행하는 진도수를 조정할 순 있지만 단조 시계가 앞이나 뒤로 뛰게 할 수는 없다.
  • 단조 시계의 해상도는 보통 상당히 좋기 때문에 분산 시스템에서 경과 시간을 재는 데 단조 시계를 쓰는 것이 일반적으로 좋다.

# 시계 동기화와 정확도

  • 하드웨어 시계와 NTP의 시계는 정확하지 않다. 다양한 사례로 시계의 정확도가 어긋날 수 있다.
    • 장비의 온도에 따라 하드웨어 시계에 영향을 줄 수 있다.
    • NTP 서버와의 지연으로 인해 오차가 발생할 수 있다.
  • 카산드라는 충돌 해소 전략으로 최종 쓰기 승리(LWW)를 사용하는데 시계는 정확하지 않기 때문에 이로 인해 문제가 발생할 수도 있다.
    • 가장 최근 값을 유지한다 하더라도 결국 최근의 정의는 로컬 일 기준 시계에 의존하기 때문에 완벽히 정확할 수 없다는 것을 아는게 중요하다.

# 신뢰 구간을 활용한 순서 보장

  • 분산 시스템에서 각 시스템별로 시간차를 보장할 수 있는 신뢰 구간이 있다면 이를 통해 순서를 보장할 수 있다.
    • 신뢰 구간이 5ms이고 A작업이 1ms에 시작됐고 B작업이 7ms에 시작됐다고하면 A작업은 B작업보다 빠른 시점에 수행되었음을 확신할 수 있다.
    • 이런식으로 순서를 보장하기 위해선 신뢰 구간까지 기다려야 하기 때문에 신뢰 구간을 최대한 짧게 유지하는 것이 중요하다.

# 지식, 진실, 그리고 거짓말

# 진실은 다수결로 결정된다

  • 분산 시스템은 한 노드에만 의존할 수 없다. 노드는 언제든 장애가 나서 잠재적으로 시스템이 멈추고 복구할 수 없게 될 수 있다. 각 노드는 자신의 판단을 믿을 수 없다.
  • 여러 분산 알고리즘은 정족수를 활용한다.
    • 정족수는 노드가 죽었다고 선언하는 것에 관한 결정에서도 사용된다.
    • 정족수를 이룬 노드들이 다른 노드를 죽었다고 선언하면 그 노드는 여전히 살아있을지 몰라도 죽었다고 간주되어야 한다.

# 리더와 잠금

  • 리더와 잠금을 분산 시스템에서 구현하려면 주의해야 한다.
    • 어떤 노드가 이전에 리더였더라도 네트워크가 잠시 중단되었을 뿐 실제로 그 노드가 살아있음에도 불구하고 다른 노드가 그 노드를 죽었다고 선언해 새로운 리더를 선출했을 수도 있다.
  • 위 예시는 잠금을 잘못 구현해서 생긴 데이터 오염 버그를 보여준다.
  • 클라이언트 1이 임차권을 획득하고 stop-the-world로 중단이 되었을 때 임차권이 만료되어 클라이언트 2가 쓰기를 수행했지만 클라이언트 1은 여전히 임차권을 보유했다고 잘못 판단하여 쓰기 충돌이 발생한다.

# 펜싱 토큰

  • 위와 같은 문제를 해결하기 위한 단순한 기법으로 펜싱(fencing)기법이 있다.
  • 잠금 서버가 잠금이나 임차권을 승인할 때 마다 값이 하나씩 증가하는 펜싱 토큰도 반환하다.
  • 클라이언트가 쓰기 요청을 보낼 때 자신의 현재 펜싱 토큰을 포함하도록 하여 이 토큰 값을 비교하여 쓰기를 수행하도록 판별할 수 있다.

# 비잔틴 결함

  • 펜싱 토큰은 부주의에 의한 오류에 빠진 노드를 감지하고 차단할 수 있다. 그러나 노드가 고의로 시스템 보장을 무너뜨리려 한다면 가짜 펜싱 토큰을 보내기만 하면 된다.
  • 보통 노드들이 신뢰성은 없을 수 있지만 정직하다고 가정한다. 노드가 거짓말을 할지도 모른다는 위험이 있다면 훨씬 더 어려워 진다.
  • 이러한 동작을 비잔틴 결함이라고 하며 이렇게 신뢰할 수 없는 환경에서 합의에 도달하는 문제를 비잔틴 장군 문제라고 한다.
    • 일부 노드가 오작동하고 악의적인 공격자가 네트워크를 방해하더라도 시스템이 계속 올바르게 동작한다면 비잔틴 내결함성을 지닌다고 한다.
    • 이런 관심사는 항공기와 같은 시스템에서 필요로 한다.
  • 대부분의 서버 측 데이터 시스템에서는 조직이 모든 노드를 제어하고 관리하기 때문에 비잔틴 내결함성 솔루션을 배치하는 것은 실용적이지 못하다.
  • 비잔틴 내결함성은 중앙 권한 없는 Peer-to-peer 네트워크에 더 적합하다.

# 정리

분산 시스템에서 나타날 수 있는 신뢰성 문제는 광범위 하다.

  • 네트워크로 패킷을 보내려고할 때 언제나 패킷이 손실되거나 지연될 수 있다. 응답도 손실되거나 지연될 수 있으므로 응답을 받지못하면 메시지가 잘 전달됐는지 확신할 수 없다.
  • 노드의 시계는 다른 노드의 시계와 심하게 맞지 않을 수 있고 시간이 갑자기 앞뒤로 뛸 수도 있다.
  • 프로세스는 실행 도중 어느 시점에서든지 상당한 시간동안 멈출 수 있고 다른 노드들에 의해 죽었다고 선언될 수 있으며 잠시 멈춘노드는 자신이 죽었다는걸 알지 못할 수도 있다.

부분 실패가 생길 수 있다는 사실은 분산 시스템의 뚜렷한 특성이다.

  • 분산 시스템에서 우리는 구성 요소의 일부가 고장 나더라도 전체로서의 시스템은 계속 동작할 수 있도록 부분 실패에 대한 내성을 소프트웨어에 내장하려고 노력한다.
  • 정확한 메커니즘이 없어 원격 노드의 생존을 파악하기 위해 타임아웃을 사용하지만 노드의 일시적인 중단, 네트워크 장애로 인해 확신할 수 없다.