데이터 중심 애플리케이션 설계 | 08장. 분산 시스템의 골치거리

발표자 : 황윤호, 김은택

분산 시스템을 다루는 것은 한 컴퓨터에서 실행되는 소프트웨어를 작성하는 일과는 근본적으로 다르다.
엔지니어는 모든 게 잘못되더라도 제 역할을 해내는 시스템을 구축해야 한다.

결함과 부분장애

단일 컴퓨터 환경에서는 2가지로 예측이 가능하다.
분산 컴퓨팅 환경에서는 예측할 수 없는 방법으로 고장이 난다. 이를 부분장애라 한다

단일 컴퓨터 환경

  1. 정상적으로 동작
  2. 안 돌아감 -> “가끔 운수 나쁜날"로 보이기도 하지만, 대부분 잘못 작성한 소프트웨어의 결과이다

분산 컴퓨터 환경

  1. 네트워크로 연결된 여러 컴퓨터에서 실행되는 소프트웨어
  2. 분산 시스템에서는 시스템의 어떤 부분은 잘 동작하지만 다른 부분은 예측할 수 없는 방식으로 고장나는 것도 무리가 아니다. 이를 부분 장애(partial failure)라고 한다
  3. 부분장애는 비결정적이라서 다루기 어렵다
  4. 또한 뭔가 성공했는지 아닌지 알지 못하는 경우도 있다

클라우드 컴퓨팅과 슈퍼 컴퓨팅

고성능컴퓨팅

  • 수천개의 CPU를 가진 슈퍼컴퓨터
  • 보통 일기예보나 분자 동력학처럼 계산 비용이 매우 높은 과학 계산 작업에 사용

클라우드 컴퓨팅

  • 멀티 테넌트 데이터센터

  • IP 네트워크(이더넷)로 연결된 상용 컴퓨터 등 전통적인 기업형 데이터센터는 이 두 극단의 중간 지점에 있다

  • 슈퍼컴퓨터에서 실행되는 작업은 보통 가끔씩 계산 상태를 지속성 있는 저장소에 체크포인트로 저장한다

  • 장애가 발생하면 마지막 체크포인트로부터 계산을 재시작 한다. 단일 노드 컴퓨터에 가깝다

분산 시스템이 동작하게 만들려면 부분 장애 가능성을 받아들이고 소프트웨어에 내결함성 메커니즘을 넣어야 한다.
신뢰성 없는 구성 요소를 사용해 신뢰성 있는 시스템을 구축해야 한다. 완벽한 신뢰성은 없다.
결함 처리는 소프트웨어 설계의 일부여야 하며 결함이 발생하면 소프트웨어가 어떻게 동작할지 알아야 한다.

신뢰성 없는 네트워크

책에서 주로 다루는 분산시스템은 비공유 시스템이다. 즉 네트워크로 연결된 다수의 장비이다.
비공유가 시스템 구축하는 유일한 방법은 아니지만, 몇가지 이유로 인터넷 서비스를 구축하는 주된 방법이 되었다.

특별한 하드웨어가 필요하지 않아서 상대적으로 저렴하고, 상품화된 클라우드 서비스를 활용할 수 있으며, 지리적으로 분산된 여러 데이터센터에 중복 배치함으로써 높은 신뢰성을 확보할 수 있다.
각 노드들은 서로 다른 장비의 메모리나 디스크엔 접근할 수 없다. 네트워크가 유일한 통신수단이다.
이런 종류의 네트워크에서 노드는 다른 노드로 메시지(패킷)을 보낼 수 있지만 네트워크는 메시지가 도착할 것인지는 보장하지 않는다.

보장되지 않는 원인은 다양하다

  1. 요청이 손실
  2. 요청이 큐에서 대기하다 나중에 전송
  3. 원격 노드에 장애 발생
  4. 원격 노드가 일시적으로 응답을 멈췄지만 나중에 다시 응답 시작
  5. 응답이 네트워크상에서 유실
  6. 응답이 지연되다가 나중에 전송

전송 측은 패킷이 전송됐는지 아닌지조차 구변할 수 없다.
그러므로 수신측에서 응답 메시즈를 보내는 것이지만 응답 메시지도 손실되거나 지연될 수 있다.
이 문제를 다루는 흔한 방법은 타임아웃이다.

현실의 네트워크 결함

아직 신뢰성 있는 네트워크를 만드는 완전한 방법은 없다.
한 회사의 제어된 환경에서도 네트워크 문제는 놀랄만큼 흔하게 발생한다.
또한, 공개된 클라우드 서비스에서도 네트워크 결함은 자주 발생한다.
네트워크 결함이 드물더라도 결함이 일어날 수 있다는 사실은 인지하고 소프트웨어가 이를 처리할 수 있도록 설계 해야 한다.

오류 처리가 정의되고 테스트되지 않는다면 나쁜 일이 제멋대로 생길 수 있다.
반드시 네트워크 결함을 견뎌내도록 처리할 필요는 없다.
네트워크가 믿을 만하다면 문제가 있을 때 그냥 사용자에게 오류 메시지를 보여주는 것도 타당한 방법이다.

그러나 SW가 네트워크 문제에 어떻게 반응하는지 알고 시스템이 그로부터 복구할 수 있도록 보장해야 한다.

결함 감지

많은 시스템은 결함 있는 노드를 자동으로 감지할 수 있어야 한다.

  • 로드 밸런서는 죽은 노드로 요청을 그만 보내야 한다.
  • 단일 리더 복제를 사용하는 분산 데이터베이스에서 리더에 장애가 나면 팔로워 중 하나가 리더로 승격되어야 한다.

타임아웃과 기약 없는 지연

타임아웃만이 결함을 감지하는 확실한 수단이라면 타임아웃은 얼마나 길어야 할까? 유감스럽게도 간단한 답은 없다

  • 타임아웃이 길면 노드가 죽었다고 선언될 때까지 기다리는 시간이 길어진다
  • 타임아웃이 짧으면 결함을 빨리 발견하지만 응답이 일시적으로 느려졌어도 죽었다고 선언할 위험이 높아진다

노드가 죽었다고 선언되면 그 노드의 책무는 다른 노드로 전달돼야 해서 다른 노드와 네트워크에 추가적인 부하를 준다.
특히 노드가 실제로는 죽지 않았고 과부하 때문에 응답이 느릴 뿐일 수도 있다. 그 부하를 다른 노드로 전달하면 연쇄 장애를 유발할 수 있다.

네트워크 혼잡과 큐 대기
네트워크에서 패킷 지연의 변동성은 큐 대기 때문인 경우가 많다

  1. 여러 노드가 동시에 같은 목적지로 패킷을 보내려고 하면 네트워크 스위치는 패킷을 큐에 넣고 한번에 하나씩 네트워크 링크로 넘겨준다.
  2. 네트워크 링크가 붐비면 패킷은 슬롯을 얻을 수 있을때까지 잠시 기다릴 수도 있다. 이를 네트워크 혼잡이라고 한다.
  3. TCP는 흐름 제어를 수행하며 혼잡회피나 배압을 조절하여 과부하고 되지 않도록 송신을 제한하기도 한다.
  4. TCP는 타임아웃 안에 확인 응답을 받지 않으면 패킷이 손실됐다고 간주하고 재전송한다.

지연 시간에 민감한 애플리케이션은 TCP 대신 UDP를 사용한다

UDP는 흐름 제어를 하지 않고 손실된 패킷을 재전송하지 않으므로 네트워크 지연이 크게 변하게 하는 원인 중 일부를 제거한다

UDP는 지연된 데이터의 가치가 없는 상황에서 선택하면 좋다

이처럼 신뢰성과 지연 변동성 사이에 트레이드 오프 관계가 있다

동기 네트워크 대 비동기 네트워크
전화 네트워크는 극단적인 신뢰성을 지닌다. 종단 지연시간이 낮아야 하며 목소리의 음성 샘플을 전송할 대역폭이 충분해야 한다. 통화를 할 때는 회선이 만들어진다.
두 명 사이에 있는 전체 경로를 따라서 그 통화에 대해 고정되고 보장된 양의 대역폭이 할당된다. ISDN 네트워크는 초당 4000프레임의 고정된 비율로 실행된다.
이런 종류의 네트워크는 동기식이다. 데이터가 여러 라우터를 거치더라도 큐 대기 문제를 겪지 않는다.

그냥 네트워크 지연을 예측 가능하게 만들 수는 없을까?
데이터센터 네트워크와 인터넷이 circuit-switch 네트워크라면 왕복 시간의 최대치를 보장할 수 있다. 이더넷과 IP는 큐 대기의 영향을 받는 packet-switch 프로토콜이고 따라서 네트워크에 기약 없는 지연이 있다. 이 프로토콜에는 회선의 개념이 없다.

왜 데이터센터 네트워크와 인터넷은 패킷을 교환을 사용할까? 순간적으로 몰리는 트래픽에 최적화됐기 때문이다. 순간적으로 몰리는 데이터 전송에 회선을 쓰면 네트워크 용량을 낭비하고 전송이 불필요하게 느려진다. TCP는 가용한 네트워크 용량에 맞춰 데이터 전송률을 동적으로 조절한다.

타임아웃에 올바른 값은 없다. 실험을 통해 결정해야 한다.

신뢰성 없는 시계

  1. 이 요청이 타임아웃됐나?
  2. 이 서비스의 99분위 응답 시간은 어떻게 되나?
  3. 이 서비스는 지난 5분 동안 평균 초당 몇 개의 질의를 처리했나?
  4. 사용자가 우리 사이트에서 시간을 얼마나 보냈나?
  5. 이 기사가 언제 게시됐나?
  6. 며칠 몇 시에 미리 알림 이메일을 보내야 하나?
  7. 이 캐시 항목은 언제 만료되나?
  8. 로그 파일에 남은 이 오류 메시지의 타임스탬프는 무엇인가?

단조 시계 대 일 기준 시계

일 기준 시계 날짜와 시간을 반환

NTP (Network Time Protocal) 로 동기화 - 신뢰성이 높지 않음

방화벽 네트워크 지연시간 (최대 1초) 윤초 발생 - 1분의 길이가 59초나 61초가 됨 - NTP 서버에서 하루에 걸쳐 서서히 수행함 모바일 / 임베디드 장치는 신뢰성이 없음 단조 시계 지속시간을 재는데 적합

다른 단조 시계와의 비교는 의미가 없음

동기화된 시계에 의존하기 - 문제점 문제의 원인: 시계가 잘못된다는 것을 눈치채지 못함

확연한 오류 보다는 미묘한 데이터의 손실 시계가 차이나는 노드는 죽은 것으로 선언되고 제거되어야 함 최종 쓰기 승리 (Last write wins, LWW)

리더 없는 데이터베이스 (다중 리더 복제, 카산드라, 리악)

데이터베이스 쓰기가 불가사의하게 사라짐 - 이 사실을 아는 것이 중요 전역 스냅숏용 동기화된 시계 (구글 케이스) 스패너(spanner) - 구글 트루타임(TrueTime) API

신뢰 구간을 명시적으로 보고 각각 가장 이른 타임스탬프와 가장 늦은 타임스탬프를 포함하는 구간이 겹치지 않는다면, 분명히 B는 A보다 나중에 실행됐다. 스패너는 인과성을 반영하기 위해 B를 신뢰 구간 만큼 지연 실행한다. 구글은 각 데이터센터간에 GPS / 원자 시계 배치 - 약 7밀리초 이내 동기화 프로세스 중단 파티션마다 리더가 하나씩 있는 데이터베이스가 있다고 가정, 리더만 쓰기를 받아들임

질문) 리더 노드가 여전히 리더인지 어떻게 알 수 있을까?

한 가지 방법은 리더가 다른 노드들로부터 임차권(lease)을 얻는 것 - 특정 시점에 오직 하나의 리더만 존재

시나리오 리더로 남아있으려면 주기적으로 갱신해야 함 장애가 난 경우, 임차권 갱신을 멈추므로 만료 시 다른 노드가 리더 역할을 넘겨 받음 while (true) { request = getIncomingRequest();

// 항상 임차권이 적어도 10초는 남아 있게 보장한다. 

if (lease.expiryTimeMillis - System.currentTimeMillis() < 10_000) { lease = lease.renew(); }

if (lease.isValid()) { process(request); } } 문제점은 무엇인가?

동기화된 시계에 의존함 시간을 확인하는 시점과 요청이 처리되는 시점 사이에 매우 짧은 시간이 흐른다고 가정 만약 중간에 프로그램이 멈춘다면? 리더를 다른 노드가 넘겨 받음 이 쓰레드가 멈춰있었다고 누구도 알려주지 않음 ⇒ 계속 작업이 처리됨 (안전하지 않은 처리) 시스템이 멈추는 경우 stop-the-word : GC 중단 가상환경에서 suspend 발생 ⇒ 메모리 내용 저장 ⇒ 이후 재개 (노트북인 경우 동면 중에도 발생) 쓰레드의 컨텍스트 스위칭 느린 디스크 I/O 연산 유닉스인 경우 SIGSTOP (^Z) 명령 💡 단일 장비에서 유용한 도구

뮤텍스 (mutex) 세마포어 (semaphore) 원자적 카운터 (atomic counter) 잠금없는 (lock-free) 자료구조 블로킹 큐 (blocking queue)

응답시간 보장 실시간 운영체계 (real-time operating system, RTOS) - 항공기, 로켓, 로봇, 자동차 산업 등

최악의 실행시간 명시 동적 메모리 할당이 금지될 수 있음 막대한 양의 테스트 필요 ⇒ 프로그래밍 언어, 라이브러리, 도구의 범위를 엄격히 제한함

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

네트워크는 신뢰할 수 없는 구간이다. 결국 부분 장애, 신뢰성 없는 시계, 프로세스 중단에 시달릴 수 있다.
신뢰성 있는 소프트웨어를 만드는 방법에 도움을 주는 개념을 소개한다.

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

하나의 노드만 보고 정상 운영 중인지 판단하기 힘들다.

예) full GC 로 중단된 서비스로 인해 죽었다고 판단했는데 다시 살아나서 하던 작업을 계속함

대신 정족수, 즉 노드 사이의 투표에 의존한다 - 특정한 노드 하나에 대한 의존을 줄이기 위해 결정을 하려면 여러 노드들로부터 어떤 최소 개수의 투표를 받아야 한다.

노드의 과반수 이상을 정족수로 삼는 것이 가장 흔하다. (3대 중 1대 오류, 5대 중 2대 오류 시라도 정상 동작)

리더와 잠금 시스템이 단독으로 움직여야 할 때

  • 스플릿 브레인을 피하기 위해 오직 한 노드만 데이터베이스 파티션의 리더가 될 수 있다.
  • 특정한 자원이나 객체에 동시에 쓰거나 오염시키는 것을 방지하기 위해 오직 하나의 트랜잭션이나 클라이언트만 어떤 자원 이나 객체의 잠금을 획득할 수 있다.
  • 사용자명으로 사용자를 유일하게 식별할 수 있어야 하므로 오직 한 명의 사용자만 특정한 사용자명으로 등록할 수 있다.

… 이미지 …

펜싱 토큰

… 이미지 …

잠금 서비스로 주키퍼를 사용하면 트랜잭션 ID zxid나 노드 버전 eversion을 펜싱 토큰으로 사용할 수 있다. 이들은 단조 증가가 보장되므로 필요한 속성을 지닌다.

비잔틴 결함

분산 시스템 문제는 노드가 “거짓말”(임의의 결함이 있거나 오염된 응답을 보냄)을 할지도 모른다는 위험이 있다면 훨씬 더 어려워진다. 이런 동작을 비잔틴 결함(Byzantine fault) 이라고 하며 이렇게 신뢰할 수 없는 환경에서 합의에 도달하는 문제를 비잔틴 장군 문제(Byzantine Generals Problem) 라고 한다. 일부 노드가 오작동하고 프로토콜을 준수하지 않거나 악의적인 공격자가 네트워크를 방해하더라도 시스템이 계속 올바르게 동작한다면 이 시스템은 비잔틴 내결함성을 지닌다(Byzantine fault-tolerant) 라고 한다. 대체로 현실적이지 않으므로 전통적인 메커니즘(인증, 접근 제어, 암호화, 방화벽 등)이 여전히 공격자로부터 보호하는 수요 수단으로 사용되고 있다.

약한 형태의 거짓말

  • 네트워크 패킷에 대한 오염 (하드웨어 문제, 운영체제, 드라이버, 라우터 버그로 인함)
  • 보통 TCP / UDP 체크섬으로 검출됨
  • 어플리케이션에서 체크섬 사용하여 문제 해결
  • 공개적 어플리케이션은 사용자 입력을 신중하게 살균해야 함
  • 값에 대한 범위 확인, 적절한 메모리 할당을 위한 입력값 크기 제한, 기본적인 정상 점검 확인
  • NTP 클라이언트는 여러 서버 주소를 설정하는 것이 도움이 됨

시스템 모델과 현실

시스템 모델: 시스템에서 발생할 것으로 예상되는 결함의 종류를 어떻게든 정형화한 것

타이밍 가정

  • 동기식 모델: 네트워크 지연, 프로세스 중단, 시계 오차에 모두 제한이 있다고 가정한다.
  • 부분 동기식 모델: 부분 동기는 시스템이 대부분의 시간에는 동기식 시스템처럼 동작하지만 때때로 네트워크 지연, 프로세스 중단, 시계 드리프트의 한계치를 초과한다는 뜻이다
  • 비동기식 모델: 이 모델에서 알고리즘은 타이밍에 대한 어떤 가정도 할 수 없다.

노드 장애

  • 죽으면 중단하는(crash-stop) 결함: 노드가 어느 순간에 갑자기 응답하기를 멈추면 이후로 그 노드는 영원히 사용할 수 없고 결코 되돌아오지 않는다는 뜻이다.
  • 죽으면 복구하는(crash-recovery) 결함: 노드가 어느 순간에 죽을 수 있지만 알려지지 않은 시간이 흐른 후에는 아마도 다시 응답하기 시작할 것이라고 가정한다.
  • 죽으면 복구하는 모델에서 노드는 메모리에 있는 상태는 손실되지만 죽어도 데이터가 남아 있는 안정된 저장소(즉 비휘발성 디스크 저장소)가 있다고 가정한다.
  • 비잔틴(임의적인) 결함: 노드는 지난 절에서 설명한 것처럼 다른 노드를 속이거나 기만하는 것을 포함해 전적으로 무슨 일이든 할 수 있다.

알고리즘의 정확성
속성을 기술하여 항상 만족하는지 확인한다.

예) 펜싱 토큰

  • 유일성: 펜싱 토큰 요청이 같은 값을 반환하지 않는다.
  • 단조 일련번호: 요청 x 가 토큰 tx 를, 요청 y가 토큰 ty를 반환했고 y가 시작하기 전에 x가 완료됐다면 tx < ty 를 만족한다.
  • 가용성: 펜싱 토큰을 요청하고 죽지 않은 노드는 결국에는 응답을 받는다.

안전성(safety)과 활동성(liveness)

  • 안전성
    • 안전성 속성이 위반되면 그 속성이 깨진 특정 시점을 가리킬 수 있다(예를 들어 유일성 속성이 위반되면 중복된 펜싱 토큰 을 반환한 특정 연산을 식별할 수 있다).
    • 안전성 속성이 위반된 후에는 그 위반을 취소할 수 없다. 이미 손상된 상태다.
  • 활동성
    • 활동성 속성은 반대로 동작한다. 어떤 시점을 정하지 못할 수 있지만(예를 들어 노드가 요청을 보냈지만 아직 응답을 받지 못했을 수도 있다) 항상 미래에 그 속성을 만족시킬 수 있다는 희망이 있다.

안전성 ⇒ 항상 만족되기를 요구 (모든 노드나 네트워크 전체 오류 시에도 잘못된 결과를 반환하지 않는다)

활동성 ⇒ 경고를 하는 것이 허용 (네트워크 단절이 있다면 단절 기간의 한계를 둔다)

현실 대응
분산 시스템의 정확성을 따져보는 데 매우 유용, 다만 한계성은 명확하다.

  • 죽으면 복구되는 모델: 노드가 죽어도 데이터는 남아있다고 가정함
    • 디스크 오염
    • 하드웨어 오류
    • 잘못된 설정으로 인한 데이터 유실

컴퓨터 과학과 컴퓨터 공학의 차이

  • 실제 구현에는 불가능하다고 가정했던 일이 발생하는 경우를 처리해야할 수 있다.
    • printf(“너라서 짜증나”)
    • exit(666)

그럼에도 추상 시스템 모델은 중요하다!

  • 현실 시스템의 복잡함 ⇒ 추론할 수 있는 관리 가능한 결함의 집합 추출 ⇒ 문제를 이해하고 체계적으로 해결

정리

  • 네트워크 패킷은 언제나 손상되거나 지연될 수 있다.
  • 노드의 시계는 시간을 신뢰할 수 없다.
  • 프로세스는 실행 도중 stop-the-world 를 맞이할 수 있다.

부분 실패는 분산 시스템의 뚜렷한 특성

감지부터가 어려움의 시작 (타임아웃을 쓰긴 하지만 이는 네트워크 장애와 노드 장애를 구별하지 못함)
엄격한 실시간 응답 보장과 네트워크 지연 제한을 두는 것은 가능하지만 비용이 매우 크고 자원 사용률이 낮아짐

다음장은 모든 분산 시스템의 문제에 대처하는 해결책이다!




최종 수정 : 2022-04-08