Search
Duplicate

08. 분산 시스템의 골칫거리

목차

개요

이봐 방금 널 만났어 네트워크가 느려터졌군 하지만 여기 내 데이터가 있으니 아마도 저장해주길 - 카일 킹스베리, 칼리 레이 젭슨과 네트워크 분단의 위험성(2013)
지금까지 여러 문제들과 이를 처리하기 위한 여러 대응에 대해 알아봤다.
(복제 서버 장애 복구, 복제 지연, 트랜잭션의 동시성 제어등)
이러한 결함에 대해 여러 얘기를 했지만 아직도 몇 개의 부분은 너무 낙관적이라고 한다.
이제 반대로 비관주의를 끌어올려 문제가 될 요지는 문제가 된다는 것에 대해 알아본다.
분산 시스템 핸들링은 단일 시스템 핸들링과는 너무나도 다르고, 문제가 될 여지도 더 많다.
엔지니어는 이러한 문제들이 발생하더라도 제 역할을 해내는 책임감있는 시스템을 구축해야 한다.
이번 포스팅에서는 잘못될 수 있는 포인트에 대해 비관적으로 알아보고 어떻게 어느 정도로 회피할 수 있는지에 대해 알아본다.

결함과 부분 장애

단일 시스템에서 돌아가는 소프트웨어는 결정적이다.
그리고 좋은 소프트웨어가 설치된 컴퓨터는 완전히 동작하거나 완전히 동작하지 않아야 한다.
그 중간 애매한 상태가 되지 않는다.
이건 컴퓨터 설계 시점에 의도적으로 선택한 것으로 잘못된 결과는 다루기도 어렵고 혼란만 가중시킬 수 있기 때문이다.
하지만,
네트워크로 연결된 여러 컴퓨터 환경에서 소프트웨어는 상황이 다르다.
분산 시스템에서는 단일 컴퓨터에서 제공하던 (결정적인)이상적인 시스템 모델을 보여줄 수 없다.
다음과 같은 상황과 같이 매우 광범위한 부분에서 잘못될 수 있다.
내 짧은 경력 동안 한 데이터 센터에서 발생한 장기간의 네트워크 분단, PDU(power distribution unit, 전원 분배 유닛)장애, 스위치 장애, 전체 랙에서 주기적으로 일어난 전원 사고, 전체 DC 백본 장애, 전체 DC 전원 장애를 겪었고, 저혈당증을 앓는 운전자가 자신의 포드 픽업 트럭을 DC의 HVAC(난방, 통풍, 공조) 시스템에 처박은 적도 있었다. 심지어 나는 운영 담당 직원도 아니였다. - 코다 헤일(Coda Hale)
분산 시스템에서는 전체가 아닌 어떤 부분만 예측할 수 없는 방식으로 고장날 수도 있는데, 이를 부분 장애(partial failure) 라고 한다.
부분 장애(partial failure)는 비결정적이기 때문에
여러 노드와 네트워크와 관련된 무엇인가를 시도하면
어쩔 때는 동작하지만, 어쩔 때는 실패할 수 있다.
뭔가 성공했는지 아닌지 알지 못할 수도 있다.
메시지가 네트워크를 거쳐 전송되는 시간도 비결정적이다.
이러한 비결정성과 부분 장애 가능성은 분산 시스템을 다루기 어렵게 한다.

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

대규모 컴퓨터 시스템 구축 방법에는 몇 가지 철학이 있다.
대규모 컴퓨팅의 한쪽 끝에는 고성능 컴퓨팅(high-performance coimputing, HPC)분야가 있다.
슈퍼 컴퓨터는 보통 일기예보나 분자 동력학처럼 비용이 큰 과학 계산 작업에 쓰인다.
다른 극단으론 클라우드 컴퓨팅이 있다.
멀티 테넌트 데이터센터
IP 네트워크로 연결된 상용 컴퓨터
신축적(elastic)/주문식(on-demand) 자원할당
계량 결제(metered billing)
전통적인 기업형 데이터 센터는 이 두 극단의 중간 지점에 있다.
이러한 철학에 따라 결함 처리 방법도 매우 다르다.

고성능 컴퓨팅

가끔씩 계산 상태를 지속성 있는 저장소에 체크포인트로 저장한다.
노드 하나에 장애 발생시 흔한 해결책으로는 전체 클러스터 작업 부하를 중단한다.
장애 복구 이후엔 마지막 체크포인트부터 재시작한다.
분산 시스템보다는 단일 노드 컴퓨터에 가깝다.
부분 장애를 전체 장애로 확대하는 방법으로 처리한다.

클라우드 컴퓨팅

인터넷 관련 애플리케이션은 사용자에게 지연 시간이 낮은 서비스를 제공해야 하기 때문에 클러스터 중단을 할 수 없다.
상용 장비를 사용해 구축하는데, 규모의 경제를 이유로 비용대비 높은 성능을 제공하지만 실패율도 높다.
지리적으로 분산된 배포를 할 때 통신은 인터넷을 거치기에 로컬에 비해 느리고 신뢰성도 떨어진다.
데이터센터의 네트워크는 IP와 이더넷을 주로 기반으로 하며 높은 양단 대역폭(bisection bandwidth)을 제공하기 위해 클로스 토폴로지(Clos topology)로 연결돼 있다.
분산 시스템을 동작하게 만들려면 부분 장애 가능성을 받아들이고 소프트웨어에 내결함성 메커니즘을 넣어야 한다.
이는 신뢰성 없는 구성 요소를 사용해서 신뢰성 있는 시스템을 구축해야 한다는 말이 된다.
시스템이 작더라도(몇 개의 노드만으로 구성된 시스템) 언젠가 결함이 생길 가능성이 있고 소프트웨어는 그 결함을 처리해야 하는데, 결함 처리는 소프트웨어 설계의 일부여야 하고 우리는 결함 발생시 소프트웨어가 어떻게 동작할지 알아야 한다.

신뢰성 없는 구성 요소를 사용해 신뢰성 있는 시스템 구축하기

믿지못할 요소들을 가지고 믿을 수 있는 시스템을 만든다는 말은 모순처럼 느껴질 수 있다.
하지만, 이 아이디는 오래된 아이디어로 이미 많이 사용되고 있다.
예를 들어 IP(Internet Protocol)는 패킷 누락, 지연, 중복등의 이슈로 신뢰성이 없지만, TCP(Transmission Control Protocol, 전송 제어 프로토콜)는 IP위에서 더욱 신뢰성이 높은 전송 계층을 제공한다. 손실된 패킷을 재전송, 중복 제거, 패킷 순서에 맞춰 재조립을 보장해준다.
물론, 이렇게 신뢰성을 높히는 것에도 제한은 있다.
TCP는 패킷 손실 및 중복, 순서가 섞이는 문제는 감춰주지만 네트워크 지연을 막아주지는 못한다.

신뢰성 없는 네트워크

지금까지 언급한 분산 시스템은 비공유 시스템으로, 네트워크로 연결된 다수의 장비들을 말한다.
각 장비는 자신만의 메모리와 디스크를 가지고 있고, 다른 장비의 메모리나 디스크에 접근할 수는 없다고 가정한다.
이러한 비공유가 시스템을 구축하는 유일한 방법은 아니지만 인터넷 서비스를 구축하는 주된 방법이 되었는데, 그 이유는 다음과 같다.
특별한 하드웨어가 필요없기에 상대적으로 저렴하다.
상품화된 클라우드 서비스를 활용할 수 있다.(AWS, GCP, NCP, …)
지리적으로 분산된 여러 데이터베이스에 중복 배치하여 신뢰성을 높힐 수 있다.

비동기 패킷 네트워크(asynchronous packet network)

요청 후 응답을 받지 못했다면 (a) 요청이 손실됐는지, (b) 원격 노드가 다운됐는지, (c) 응답이 손실됐는지 구별할 수 없다.
인터넷과 데이터센터 내부 네트워크는 대부분 비동기 패킷 네트워크(asynchronous packet network)다. 즉, 노드에서 다른 노드로 패킷을 보낼 수는 있지만 패킷의 도착 여부나 도착 시간을 보장하지 않는다. 그렇기에 요청 후 응답을 기다릴 때 문제가 생길 수 있다.
1.
요청 손실(Ex: 랜선이 뽑히거나 정전등으로 물리적 손실)
2.
요청이 큐에서 대기하다 나중에 전송될 수 있다.
3.
원격 노드에 장애가 생겼을 수 있다.
4.
원격 노드가 일시 정지(Ex: 가비지 컬렉션 휴지가 길어질 수 있다.)후 다시 응답을 시작할 수 있다.
5.
원격 노드는 응답을 했지만 네트워크에서 손실될 수 있다.(Ex: 네트워크 스위치 설정 문제)
6.
원격 노드는 응답을 했지만 응답이 지연될 수 있다.(네트워크 장비 과부하)

타임아웃(timeout)

요청에 대한 응답이 여러 사유들로 인해 오지 못하는 경우 막연히 기다리고만 있을 수는 없다.
그렇기에 얼마 간의 시간이 지나면 응답 대기를 멈추고 응답이 도착하지 않는다고 가정한다.
여기서 타임아웃이 발생하더라도
원격 노드가 요청을 받았는지 알 수 없고,
요청을 포기했더라도 어딘가 큐에 저장된 요청이 서버에 도착할 수 있다.

현실의 네트워크 결함

한 회사에서 운영하는 데이터센터와 같이 제어된 환경에서도 네트워크 문제는 아주 흔하게 발생한다. 단일 장비의 연결 끊어짐, 전체 랙의 연결 끊어짐등 많은 네트워크 결함들이 발생한다.

네트워크 장비를 중복 추가해서 해결할 수 없을까?

연결이 끊어져도 다른 장비를 통해 연결을 하면 되지 않을까 생각할 수 있지만, 네트워크 중단의 주요 원인인 인적 오류(Ex: 스위치 설정 오류)에서 보호해주지 못하기에 우리의 기대만큼 결함을 줄여주지는 못한다.
공개적인 클라우드 서비스(Ex: EC2)보다는 잘 관리되는 비공개 데이터센터가 더 안정적인 환경을 제공할수도 있다. 하지만, 네트워크 문제로부터 면역이 되지는 않는다. 예를 들어
스위치의 SW 업데이트 중 생기는 문제는 네트워크 토폴로지 재구성을 유발할 수 있고, 네트워크 패킷은 그 동안 1분 이상 지연될 수 있다.
상어에게 해저 케이블이 물어뜯겨 손상될 수도 있다.
수신 패킷은 모두 누락하고 송신 패킷은 잘 보내는 네트워크 인터페이스가 있을 수 있다.
이러한 네트워크 결함들이 드물더라도 발생할 수 있다는 사실은 우리의 소프트웨어가 이를 처리할 수도 있어야 한다는 의미이기도 하다. 별도의 처리를 하지 않고, 테스트되지 않는다면 어떠한 문제가 생길지 예측할 수 없다.
(필자는 파이썬 플라스크 환경에서 삭제조건 조회 API 타임아웃으로 null 처리된 변수가 조건으로 들어가며 모든 Row가 삭제되는 상황을 실시간으로 지켜본 적이 있다….)

반드시 네트워크 결함을 견뎌내도록(tolearting) 처리할 필요는 없다.

네트워크 결함 발생 빈도가 낮다면 사용자에게 오류 메세지를 보여주는 것도 괜찮을 수 있다.
하지만, 소프트웨어가 네트워크 문제에 어떻게 반응하는지 알고 시스템이 그로부터 복구할 수 있도록 보장해야 한다.
고의로 네트워크 문제를 유발하여 시스템 반응을 테스트하는 것도 괜찮으며 카오스 몽키(Chaos Monkey)의 기반이 되는 생각이다.

결함 감지

시스템에서 결함을 감지하는 것은 중요하다.
결함을 감지해야
문제가 생긴 노드로 요청을 보내지 않을 수 있고,
(단일 리더 복제를 사용하는 경우) 리더에 장애가 생기면 팔로워 중 하나가 리더로 승격되야 한다.
그렇다면, 어떻게 노드에 결함이 있는지 확인을 할 수 있을까?
어떤 특정한 환경에서는 뭔가 동작하지 않는다는 명시적인 피드백을 받을 수도 있다.
노드가 실행 중인 장비에 연결할 수 있지만 목적지 포트에서 수신 대기하는 프로세스가 없다면 OS가 친절하게 RSTFIN 패킷을 응답으로 보내 TCP 연결을 닫거나 거부한다. 하지만, 노드가 요청을 처리하다 죽었다면 원격 노드에선 데이터가 어디까지 처리됐는지 알 수 없다.
노드 프로세스가 죽었지만 노드의 OS는 아직 실행중이라면 스크립트로 다른 노드에게 프로세스가 죽었다고 알려서 타임아웃을 기다리지 않고 역할을 넘겨받게 할 수 있다.(HBase가 이렇게한다.)
데이터센터 내 네트워크 스위치의 관리 인터페이스에 접근할 수 있으며 질의를 보내 HW 수준의 링크 장애를 감지할 수 있다. 이 방법은 스위치에 대한 접근을 할 수 없는 상황에선 배제되는 선택지이다.
접속하려는 IP 주소에 도달할 수 없다고 라우터가 확신하면 ICMP Destination Unreachable 패킷으로 응답할 수도 있다. 라우터에 이러한 장애 감지 능력이 없을 경우 네트워크의 다른 참여자들과 동일한 제한이 적용된다.

타임아웃과 기약 없는 지연

적절한 타임아웃 시간을 어느정도일까? 이에 대해 명쾌하게 답을 내리기는 어렵다.
타임아웃이 길다면
결정이 내려지기까지 기다리는 시간이 길어져 사용성이 떨어진다.
타임아웃이 짧다면
원격 노드의 문제가 일시적이라도 죽었다고 판단할 위험이 높아지고,
같은 동작을 한 번 이상 실행하게 될 수도 있다.
우리가 패킷의 최대 지연 시간이나 요청 처리 시간이 계산 가능한 일관된 수치라면 계산식을 이용해 타임아웃을 정할 수 있지만, 우리가 사용하는 시스템은 대부분 이런 시간을 보장하지 않는다.
비동기 네트워크는 기약 없는 지연(unbounded delay)이 있고, 서버 구현은 요청 처리 시간을 보장하지 못한다.

네트워크 혼잡과 큐 대기

컴퓨터 네트워크에서 패킷 지연의 변동성은 큐 대기 때문인 경우가 많다.
여러 장비가 같은 목적지로 네트워크 트래픽을 보내면 스위치 큐가 가득 찰 수 있다. 여기서는 모드 포트3으로 패킷을 보내려 한다.
여러 노드가 같은 목적지로 동시에 패킷을 보내려 할 경우
네트워크 스위치는 패킷을 큐에 넣고 한 번에 하나씩 네트워크 링크로 넘겨야 한다.
네트워크 혼잡(network congestion)이라면 패킷은 슬롯을 얻을 수 있을 때까지 기다려야 한다.
패킷이 목적지 장비에 도착했을 때 여유 CPU가 없다면, 여유가 생길 때까지 요청은 OS가 큐에 넣어둔다. 큐에서 대기하는 시간은 제각각 다를 수 있다.
가상 환경에서 실행되는 OS는 다른 가상 장비가 CPU 코어를 사용하는 동안 멈추는 경우가 흔하다. 이 시간 동안 가상 장비는 네트워크에서 어떤 데이터도 받아들일 수 없기에 가상 장비 모니터가 들어오는 데이터를 큐에 넣어서 버퍼링한다.
TCP는 흐름 제어(flow control)를 수행한다.
혼잡 회피(congestion avoidance)나 배압(backpressure)이라고도 하는 흐름 제어는
노드가 네트워크 링크나 수신 노드에 과부하를 가하지 않도록 자신의 송신율을 제한하는 것이다.
데이터가 네트워크로 들어가기 전부터 큐 대기를 할 수 있다는 의미

동기 네트워크 VS 비동기 네트워크

전화 네트워크처럼 데이터센터 네트워크를 신뢰성있게 만들수는 없을까?
실전화기
전화 네트워크에서 통화를 할 땐 회선(circuit)이 만들어지는데, 통화를 하는 두 명 사이에 있는 전체 경로를 따라 그 통화에 대해 고정되고 보장된 양의 대역폭이 할당되고, 통화가 끝날 때까지 유지된다
이러한 종류의 네트워크는 동기식이다. 데이터가 여러 라우터를 거치더라도 큐 대기 문제를 겪지 않는다. 네트워크에 다음 홉(hop)에 통화당 공간이 이미 할당됐기 때문이다.
또한, 큐 대기가 없기에 네트워크 종단 지연 시간의 최대치가 고정돼 있는데, 이를 제한 있는 지연(bounded delay)라고 한다.

그냥 네트워크 지연을 예측 가능하게 만들 순 없을까?

우리는 전화 네트워크의 회선(circuit)과 TCP의 차이점을 이해할 필요가 있다.
회선은 만들어져 있는 동안 다른 사용자가 없는 고정된 양의 대역폭이다.
하지만, TCP연결의 패킷은 특정 사용자에게 할당된게 아닌 가용한 네트워크의 대역폭을 기회주의적으로 사용한다. 그래서 가변 크기의 데이터 블록을 보내면 가능한 짧은 시간에 전송하려고 할 것이고, TCP 연결이 유휴 상태라면 어떤 대역폭도 사용하지 않는다.
네트워크와 인터넷이 전화 네트워크 같은 회선 교환(circuit-switch) 네트워크라면 회선 구성 시점의 왕복 시간의 최대치를 보장할 수 있지만, 이더넷과 IP는 큐 대기의 영향을 받는 패킷 교환 (packet-switch) 프로토콜이기에 네트워크에 기약 없는 지연이 있다.
그럼 어째서 데이터 센터 네트워크나 인터넷은 패킷 교환을 사용하는 걸까?
이는 바로 순간적으로 몰리는 트래픽(bursty traffic)에 최적화됐기 때문이다.
사용자가 정해져있는 전화 네트워크( 보통 1:1 통화)와는 다르게 특별한 대역폭 요구사항이 없고, 그저 빠를수록 좋다.
비동기 전송 모드(Asynchronous Traansfer Mode, ATM)처럼 회선 교환과 패킷 교환을 모두 지원하는 하이브리드 네트워크를 만드려는 시도도 있었지만 전화 네트워크의 중심 스위치 외에는 별로 채택되지 않았다.
결국
현재 배포된 기술로는 네트워크의 지연과 신뢰성에 대해 어떤 보장을 할 수 없다. 네트워크 혼잡, 큐 대기, 기약 없는 지연이 발생할 것을 염두해야 한다.
그렇기에 타임아웃에 대한 적절한 값을 유추하는 마법의 공식은 없으며 실험을 통해 결정해야 한다.

신뢰성 없는 시계

시계와 시간은 중요하다.
애플리케이션의 기능에는 이 두 가지 정보를 토대로 노출되고, 답변해야 하는 질문들이 여럿 존재한다. 지속 시간에 대한 답변과, 시점에 대한 답변을 해야하는데 예를 들면 다음과 같은 질문이 있다.
1.
지속 시간에 대한 질의
이 요청이 타임아웃됐나?
이 서비스의 99분위 응답 시간은?
이 서비스는 지난 5분간 평균 초당 몇 개의 질의를 처리했나?
사용자가 우리 사이트에서 얼마나 머물렀나?
2.
시점에 대한 질의
이 기사가 언제 게시됐나?
며칠 몇 시에 미리 알림 이메일을 보내야 하나?
이 캐시 항목은 언제 만료되나?
로그 파일에 남은 이 오류 메시지의 타임스탬프는 무엇인가?
분산 시스템에서 통신은 비동기적이기에 시간을 다루기 까다롭다.
거기에 네트워크에 있는 개별 장비는 각각 자신의 시계를 가지고 있다. 실제 하드웨어 장치로 보통 수정 발진기(quartz crystal oscillator)
이 장비는 완벽히 정확하진 않아 각 장비가 정확히 일치하지 않는다. 그래도 어느 정도 동기화는 가능한데 보통 네트워크 시간 프로토콜(Network Time Protocol, NTP)로 서버 그룹에서 보고한 시간에 따라 컴퓨터 시계를 조정할 수 있게 한다.
quartz crystal oscillator
이 서버들은 다시 GPS수신자 같은 더욱 정환한 시간 출처로부터 시간을 얻는다.

단조 시계 VS 일 기준 시계

컴퓨터는 최소 두 종류의 시계인 일 기준 시계(time-of-day clock)단조 시계(monotonic clock)를 가지고 있는데 두 종류가 다른 목적으로 사용되기에 구분이 필요하다.

일 기준 시계(time-of-day clock)

직관적으로 시계에 기대하는 일을 한다.
어떤 달력에 따라 현재 날짜와 시간을 반환한다
벽시계 시간(wall-clock time)이라고도 한다.
윤초는 세지 않고 에포크는 그레고리력에 따르면 UTC(협정세계시) 1970-01-01 자정을 가리킨다.
보통 NTP로 동기화된다.
로컬 시계가 NTP 서버보다 너무 앞서면 강제로 리셋되어 과거 시점으로 거꾸로 뛰는 것처럼 보일 수 있다.

단조 시계(monotonic clock)

타임아웃이나 서비스 응답 시간 같은 시간 구간을 재는 데 적합하다.
한 시점에서 단조 시계의 값을 확인하고 어떤 작업 이후 다시 시계를 확인해서 두 값의 차이로 시간의 흐름을 알 수 있다.
시계의 절대적인 값은 의미가 없다.
다른 두 대의 컴퓨터에서 나온 단조 시계 값을 비교하는건 의미가 없다는 뜻
NTP는 컴퓨터의 로컬 시계가 NTP보다 빠르거나 느리다는걸 발견하면
단조 시계가 진행하는 진도수를 조정할 수도 있다.(slewing)
분산 시스템에서 경과 시간을 재는 데 단조 시계를 쓰는 것은 보통 괜찮다.

시계 동기화와 정확도

단조 시계와는 다르게 일 기준 시계는 NTP 서버나 다른 외부 시간 출처에 맞춰 설정돼야 유용하다.
하지만, 시계가 정확한 시간을 알려주게하는 방법은 생각보다 정확도, 신뢰성이 높지 않다.
드리프트(drift) 현상 (더 빠르거나 느리게 실행)이 생긴다.
시계 드리프트는 온도에 따라 변한다.
구글은 자신들의 서버에 200ppm(parts per million)의 시계 드리프트가 있다고 가정한다.
30초마다 서버와 재동기화되는 시계에 6밀리초의 드리프트가 생기는 것과 같다.
컴퓨터 시계와 NTP서버가 너무 많은 차이가 나면
동기화가 거부되거나
로컬 시계가 강제로 리셋될 수 있다.
방화벽 문제로 노드와 NTP서버가 막히면, 잘못된 시간 정보가 그대로 남고 얼마간 알려지지 않을수도 있다.
설정에 따라 네트우어크 지연이 크면 NTP 클라이언트가 완전히 포기할 수도 있다.
윤초가 발생하면 1분의 길이가 59초 혹은61초가 되어 윤초를 고려하지 않고 설계된 시스템에서는 문제가 생길 수 있다.
윤초 조정을 하루에 걸쳐 서서히 수행함으로써 NTP 서버가 거짓말을 하도록 하여 처리할수도 있다.(문지름(smearing))
가상 장비에서 HW 시계는 가상화돼서 정확한 시간 엄수가 필요한 애플리케이션에게 추가적인 어려움이 생긴다.
CPU 코어가 가상 장비 사이에서 공유될 때 각 VM은 다른 VM이 실행되는 동안 수십 밀리초 동안 멈춘다.
시계 정확도가 매우 중요한 업무(대표적으로 금융권)에서는 많은 자원을 투입해 시계 정확도를 높힐 수 있다. 이런 정확도는 보통
GPS 수신기
정밀 시간 프로토콜(Precision TIme Protocol, PTP)
세심한 배포 및 모니터링
등을 사용해 달성할 수 있지만, 상당한 노력과 기술이 필요하다.

동기화된 시계에 의존하기

이번 장 처음에 비관적으로 문제가 될 여지에 대해 알아보겠다고 했었다.
시계 역시 간단하지만, 비관적으로 고려해보면 네트워크가 대부분의 시간에 잘 동작하더라도, 결함이 생길 것을 가정해야 하고, 잘못된 시계에 대해서 소프트웨어는 대비할 필요가 있다.

어떻게 시계가 잘못되었다고 판단할까?

시계가 잘못되었다는 사실은 어떻게 알 수 있을까?
장비의 수정 시계에 결함이 있거나 NTP 클라이언트의 설정이 잘못되었다면 시계는 드리프트가 생겨 실제 시간과 달라질 수 있다.
소프트웨어의 어떤 부분이 정화깋 동기화된 시계에 의존할 경우 조용하고 미묘한 데이터 손실이 발생하기 쉽다.
따라서 동기화된 시계가 필요한 소프트웨어를 사용한다면 모든 장비 사이의 시계 차이를 모니터링해야 한다. 다른 노드와 시계가 너무 차이나는 노드는 죽은 것으로 파악해 클러스터에서 제거돼야 한다. 이런 모니터링을 통해 피해가 더 커지기전에 결함을 발견할 수 있도록 보장할 수 있다.

이벤트 순서화용 타임스탬프

B는 A보다 인과성 측면에서 나중에 쓰지만 B가 쓸 때 사용하는 타임스탬프가 더 이르다.
위 그림은 다중 리더 복제를 쓰는 DB에서 일 기준 시간을 위험하게 사용하는 예를 보여준다.
1.
클라이언트 A노드1에 x= 1을 쓴다.
이 쓰기는 노드3으로 복제된다.
2.
클라이언트 B노드3의 x를 증가시킨다(x = 2)
3.
두 쓰기는 노드2로 복제된다.
위 과정에서 쓰기가 다른 노드로 복제될 때 쓰기 발생 노드의 일 기준 시계에 따른 타임스탬프가 붙는다. 그럼에도 불구하고 타임스탬프로 이벤트 순서를 올바르게 정할 수 없다.
x = 2는 나중에 쓰여졌지만 타임스탬프를 보면 x = 1 쓰기 타임스탬프는 42.004초이고 x = 2 쓰기 타임스탬프는 42.003초다. 노드 2가 두 이벤트를 받으면 x = 1이 더 최신으로 판단해 x=2 쓰기를 버리게되고, 클라이언트 B의 증가 연산은 손실된다.
이 충돌 해소 전략은 최종 쓰기 승리(last write wins, LWW)인데, 다중 리더 복제나 카산드라와 리악같은 리더 없는 DB에서 많이 사용된다. 타임스탬프를 클라이언트에서 생성하더라도 LWW의 근본 문제를 바꾸진 못한다.
LWW는
DB 쓰기가 추적 불가능하게 사라질 수 있다. (위 그림의 시나리오처럼)
순차 쓰기가 빠르게 실행되는 것과 동시 쓰기를 구분하지 못한다.
인과성 위반을 막으려면 버전 벡터 같은 부가적인 인과성 추적 메커니즘이 필요하다.
두 노드가 독립적으로 동일한 타임스탬프를 가진 쓰기 작업을 만들 수도 있다.
부가적인 값으로 충돌 해소를 시도할 수도 있지만, 이 역시 인과성 위반으로 이어질 수 있다.
이러한 이유로 LWW를 이용해 최근 값을 유지하고 싶더라도 이 최근이라는 정의는 로컬 일 기준 시계에 의존하는데, 이 시계가 틀릴 수 있다는 점을 인지해야 한다.

논리적 시계(logical clock)

진동하는 수정(quartz crystal) 대신 증가하는 카운터를 기반으로 하는 이벤트 순서화의 안전한 대안으로, 이벤트의 상대적인 순서(이벤트가 다른 이벤트의 앞(혹은 뒤)에 일어났는가)만 측정한다.
이의 반대인 일 기준 시계와 단조 시계는 실제 경과 시간을 측정하기에 물리적 시계(physical clock)라고도 한다.

시계 읽기는 신뢰 구간이 있다.

장비가 얼마나 자세하게 볼 수 있는지와 얼마나 정확한지는 구분해야 한다.
내가 아무리 정밀하게 볼 수 있다고, 그것이 정확하게 본다는 의미는 아니다.
즉, 로컬 네트워크의 NTP 서버와 매분 동기화 하더라도 부정확한 수정 시계에서 발생하는 드리프트는 몇 밀리초가 될 수 있고, 공개 인터넷에 있는 NTP 서버를 사용하면 수십 밀리초가 될 수 있고, 여기에 네트워크 혼잡이 추가된다면 100밀리초 이상으로 드리프트는 늘어날 수 있다.
그렇기에 시계 읽기를 시점이 아닌 신뢰 구간에 속하는 시간의 범위로 읽는게 낫다고 한다.
불확실성 경계
시간 출처를 기반으로 계산할 수 있고,
GPS 수신기나 컴퓨터에 직접 부착된 원자 (세슘) 시계가 있을 경우
제조사에서 제공하는 예상 오류범위가 있다.
이제 시간을 서버로부터 얻는다면 불확실성은 서버와 마지막으로 동기화한 시간 이후로 예상되는 시계 드리프트에 NTP 서버의 불확실성을 더한 뒤 그 서버와 통신할 때 걸리는 네트워크 왕복 시간을 더한 값을 기반으로 한다. ( 첫 번째 근사치로, 서버를 믿을 수 있다고 가정한다.)
노출되지 않는 불확실성 대부분의 시스템은 이 불확실성을 노출하지 않는다. clock_gettime()이라는 메서드를 호출해도 반환값은 해당 타임스탬프의 예상 오차를 말해주지 않기 때문에 신뢰 구간이 5밀리초인지 5년인지 알 수 없다.
트루타임(TrueTime) 스패너(Spanner)에 있는 구글 API로 로컬 시계의 신뢰 구간을 명시적으로 보고한다. 이 API에 현재 시간을 요청하면 가능한 타임스탬프 범위 중 가장 이른 것과 가장 늦은 것을 가리키는 두 개의 값[earliest, latest]을 받는다.

전역 스냅숏용 동기화된 시계

이전에 알아봤던 스냅숏 격리 구현은 단조 증가하는 트랜잭션ID가 필요하다.
스냅숏보다 나중에 쓰기가 실행됐다면 그 내용은 스냅숏 트랜잭션에게 보이지 않는다. 단일 노드 DB에서는 단순한 카운터로 트랜잭션 ID를 생성하는게 충분하다.
하지만, DB가 여러 데이터센터에 있는 장비에 분산되어 있는 경우 코디네이션이 필요하다.
즉, 전역 단조 증가 트랜잭션 ID를 생성하기 어려워진다.
일 기준 시계의 타임스탬프로 트랜잭션 ID를 사용해서 나중에 생성된 트랜잭션이 높은 타임스탬프를 가지고도록 하면 좋겠지만, 이런 타임스탬프에 대한 불확실성이 문제가 된다.
스패너(Spanner)는 타임스탬프를 이용한다. 스패너의 스냅숏 구현은 트루타임 API가 보고한 시계 신뢰 구간을 사용하며 다음과 같은 관찰을 기반으로 한다. 각각 이른 타임스탬프/ 늦은 타임스탬프를 포함하는 두 개의 신뢰 구간이 있고, 이 두 구간이 겹치지 않을 경우 A보다 B가 나중에 실행됐다. 그리고 스패너는 읽기 쓰기 트랜잭션을 커밋하기 전 일부러 신뢰 구간의 길이만큼 기다림으로써 신뢰구간이 겹치지 않도록 한다.

프로세스 중단

파티션마다 리더가 하나씩 있는 DB가 있다고 가정할 때 어떻게 파티션별로 하나의 리더가 있음을 보장하고, 쓰기 권한을 줄 수 있을까?

임차권(lease) 받기

각 파티션의 리더가 팔로워로부터 임차권(lease)을 얻는 방법이 있다.
일종의 리더 권한에 대한 동의서이자 쓰기 동의서라고 볼 수 있는데, 이 임차권이 만료될 때까지 임차권을 가지고 있는 노드가 리더라고 알 수 있게 된다. 그렇기에 계속 리더 권한을 유지하기 위해서는 임차권은 만료되기 전에 주기적인 갱신이 되야 한다.
만약, 리더 노드가 죽는다면 임차권은 만료될테니 다른 노드가 리더 역할을 받을 수 있다.
while (true) { request = getIncomingRequest(); //항상 임차권이 최소 10초는 남아있게 보장한다. if(lease.expiryTimeMillis - System.currentTimeMillis() < 10000) { lease = lease.renew(); } if(lease.isValid()) { process(request); } }
Java
복사
요청 처리 루프
하지만, 이 코드는 문제가 있다.
1.
동기화된 시계에 의존한다.
: 임차권 만료 시간이 다른 장비에서 설정됐는데 로컬 시스템 시계와 비교한다.
Q. 그렇다면 단조 시계만 사용하도록 프로토콜을 수정하면 해결될까?
A. 그렇지 않다.
위 코드에서는 System.currentTimeMillis(), process(request), 두 코드에 대해서 처리 시간이 매우 짧다고 가정한다. 하지만, 예기치 못한 이유로 실행 중 중단이 발생해서 임차권의 유효시간보다 긴 시간 중단이 생길 경우,
다른 노드가 이미 리더 역할을 넘겨받았을 확률이 있고,
(전) 리더 노드는 누구에게도 자신의 권한이 사라졌거나 정지됐었다고, 전달받지 못했기 때문에
다음 반복 회차까지 임차권이 만료됐음을 알지 못한다.
그렇다면 스레드가 만료시간보다 오랫동안 멈출 수 있다는건 어떤 상황일까?
JVM같은 프로그래밍 언어 런타임은 모든 스레드를 멈춰야 하는 가비지 컬렉터(Garbage Collector, GC)가 있다.
stop-the-world GC 중단은 몇 분씩 지속될수도 있다.
핫스팟 JVM의 CMS같은 동시적인 가비지 컬렉터도 애플리케이션 코드와 완전히 병렬적으로 실행될 순 없다.
가상 환경에서 가상 장비는 서스펜드(suspend)됐다가 재개될 수 있다.
재부팅 없이 가상 장비를 호스트 → 호스트로 라이브 이전(live migration)하는데 사용된다.
(노트북같은 경우) 사용자가 노트북 덮개를 덮는 등 물리적으로 서스펜드할 수 있다.
OS의 스레드 컨텍스트 스위칭이나 하이퍼바이저가 다른 가상 장비로 스위칭되면(가상 장비에서 실행중일 때) 현재 실행 중인 스레드는 임의 지점에서 멈출 수 있다.
가상 장비는 다른 가상 장비에서 소비된 CPU시간을 스틸 타임(steal time)이라 한다.
애플리케이션이 동기식으로 디스크 접근을 하면 스레드가 느린 디스크 I/O 연산이 완료되길 기다리느라 중단될 수 있따.
OS가 디스크로 스왑(페이징) 할 수 있게 설정됐다면 단순한 메모리 접근만 해도 페이지를 디스크에서 메모리로 로딩하게 하는 페이지 폴트가 발생할 수 있다. 그 동안 스레드는 멈춘다.
극단적인 환경에선 OS가 페이지를 메모리 안팍으로 스와핑하느라 실제 작업을 거의 못할 수도 있다(thrashing)
이 문제를 피하기 위해 서버 장비에서 페이징은 종종 비활성화된다.
스래싱 위험을 감수하는 것 보다는 프로세스를 죽여서 메모리를 확보하는게 낫다.
위와 같은 문제 상황들이 생길 경우 실행 중인 스레드를 어떤 시점에 선점(preempt)하고 얼마간의 시간이 흐른 후 재개할 수 있다. 선점된 스레드는 이를 알아채지 못한다.
단일 장비에서는 mutex, semaphore, atomic counter, lock-free 자료구조, blocking queue등의 스레드 안전하게 해주는 도구들을 이용할 수 있지만, 분산 시스템용으로 바로 변형할 수 없다.
분산 시스템에선 공유 메모리가 없고 신뢰성 없는 네트워크를 통해 메시지를 보낼 수만 있기 때문이다.
즉, 분산 시스템의 노드는 어느 시점에 실행이 꽤 오랜 시간 멈출 수 있다고 가정해야 한다.
이 멈춰있는 시간 동안 외부의 시간은 계속 흐르고, 멈춘 노드가 응답이 없어 죽었다고 할 수 있다.
하지만, 멈춘 노드 입장에서는 다시 실행되더라도 시계를 확인하기 전까진 잠들었다는 것을 알 수 없다.

응답 시간 보장

스레드와 프로세스의 기약 없는 시간 중단을 노오오오력을 해서 중단의 원인을 제거할 수 있다.
소프트웨어 중에서는 명시된 시간 내에 응답을 못하면 큰 문제가 생길 수 있는 것들이 있다.
주로 항공기, 로켓, 로봇, 자동차 같은 물체를 제어하는 컴퓨터는 선세 입력에 빠르게 예측 가능하게 응답해야 한다. 그렇기에 이런 시스템에선 소프트웨어가 응답해야 하는 데드라인(deadline)이 명시된다.
그래서 이 데드라인을 지키지 못할 경우 전체 시스템 장애가 생길 수 있는데 이를 엄격한 실시간 시스템(hard real-time)이라고 한다.
실시간은 정말 실시간인가? 임베디드 시스템에서 실시간은 시스템이 명시된 타이밍 보장을 모든 상황에서 만족하도록 신중하게 설계되고 테스트됐다는 뜻이다. 웹에서 실시간이라는 단어를 좀 더 모호하게 사용하는 것과 대조적이다. 웹에서의 실시간은 서버가 클라이언트에게 데이터를 푸시하고 엄격한 응답 시간 제약 없이 스트림 처리하는 것을 나타낸다.

RTOS(Real-Time Operating System)

시스템에서 실시간 보장을 제공하기 위해선 소프트웨어 스택의 모든 수준에서 지원이 필요하다.
우선,
프로세스가 명시된 간격의 CPU 시간을 할당받을 수 있게 보장되도록 스케줄링해 주는 실시간 운영체제(RTOS)가 필요하고,
라이브러리 함수는 최악의 실행 시간을 문서화 해야 하고,
동적 메모리 할당은 제한되거나 완전히 금지될 수도 있고,
이러한 보장을 만족한다는 것을 확신하기 위한 많은 테스트와 측정이 필요하다.
실제로 현재 임베디드쪽 개발을 하고 있는 대학교 동기들의 말을 들어보면, try-catch문 같이 비용이 있는 문법이나 로직도 작성하지 않는다고 한다.
한 가지 유념해야 할 건 실시간고성능은 동일하지 않다. 실시간 시스템의 주요 관심사는 빠른 응답이기 때문에 오히려 처리량은 더 낮을 수도 있다.
그렇기에 서버측 데이터 처리 시스템에게 실시간 보장은 경제적이지도, 적절하지도 않다.

가비지 컬렉션의 영향을 제한하기

언어 런타임은 객체 할당률, 여유 메모리 공간 추적이 가능하기 때문에
가비지 컬렉션 시점에 대한 유연서을 어느정도 가지고 있다
최근 나온 아이디어중 하나로 GC 중단이
노드가 잠시 계획적으로 중단되는 것으로 간주하고
노드가 가비지 컬렉션을 하는 동안
클라이언트로부터의 요청을 다른 노드들이 처리하게 하는 것이다.
런타임이 애플리케이션에게 노드가 곧 GC 중단이 필요하다는 경고를 할 수 있다면 애플리케이션은 해당 노드로 요청을 멈추고 그 노드가 잔여 요청을 처리한 뒤 요청이 없는 동안 GC를 실행하기를 기다릴 수 있다.
이러한 방법은 GC 중단을 클라이언트로부터 감추고 응답 시간의 상위 백분위를 줄여준다. 지연 시간에 민감한 금융 거래 시스템 중에 이 방법을 쓰는 것도 있다.
이러한 아이디어의 변종은
수명이 짧아 컬렉션을 빨리 할 수 있는 객체만 가비지 컬렉터를 사용하고
수명이 긴 객체의 전체 GC가 필요할 만큼 객체가 쌓이기 전에
프로세스를 재시작하는 방법이다.

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

단일 시스템에서는 신경쓰지 않아도 되는 여러 부분이 분산 시스템으로 넘어오며, 고민해야 하는 포인트가 되었다. 분산 시스템에서는
공유 메모리가 없고,
지연 변동이 큰 신뢰할 수 없는 네트워크를 통해 메세지를 보낼 수 있을 뿐이며,
부분 장애와
신뢰성 없는 시계,
프로세스 중단에 시달릴 수 있다.
네트워크 환경에서 노드는 무엇 하나 확실하게 알지 못한다.
네트워크를 통해 받은(혹은 받지 못한) 메시지를 기반으로 추측할 뿐이다.
다행히 분산 시스템에서 우리는 동작(시스템 모델)에 관해 정한 가정을 명시하고, 이런 가정을 만족시키는 방식으로 실제 시스템을 설계할 수 있다. 어떤 시스템 모델 내에서 알고리즘이 올바르게 동작하는지 증명할 수 있다.
기반 시스템 모델이 매우 적은 보장만 제공하더라도 신뢰성 있는 동작을 달성할 수 있다는 뜻이다.

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

위에서 언급한 비대칭적인 결함이 있는 네트워크를 생각해보자.
노드가 메세지 수신은 가능하지만 송신이 모두 유실되거나 지연된다고 하면, 해당 노드는 내부적으로 잘 동작하더라도 다른 노드는 응답을 받지 못하기 때문에 해당 노드가 죽었다고 선언할 수 있다.
그럼 해당 노드는 살아있지만 죽은 노드 취급을 받게 될 것이다.
아니면, 이러한 네트워크 결함을 노드가 파악하더라도 다른 노드들이 그 노드가 죽었다고 선언하고 한쪽 연결이 끊긴 노드는 그에 대해 아무 일도 할 수 없다.
그리고 마지막으로 GC의 stop-the-world 가비지 컬렉션 중단이 분 단위로 오래걸린다고 가정하면, 중단된 시간동안 요청도, 응답도 진행되지 않기 때문에 다른 노드들은 마찬가지로 해당 노드가 죽었다고 판단할 수 있다.
이러한 여러 상황의 공통점은 노드가 상황에 대한 자신의 판단을 반드시 믿을 수 있지 않다는 것이다. 분산 시스템은 어느 한 노드에만 의존할 수가 없다. 언제든 노드에 장애가 발생하고 잠재적으로 시스템이 멈추거나 복구조차 불가능할 수 있기 때문이다.

정족수(quorum)

그래서 여러 분산 알고리즘에서는 정족수(quorum) 혹은 다수결로 결정한다.
위에서 살펴본 여러 시나리오들도 모두 다수결로 판단해 진행하는 것이다. 이 때는 대상 노드가 다수결에 따라 살았어도 죽은것이라고 결과가 나온다면, 그 결정에 따라야 한다.

리더와 잠금

시스템에서 오직 하나의 뭔가가 필요할 때가 자주 있다.
스플릿 브레인을 피하기 위해 오직 한 노드만 DB 파티션의 리더가 될 수 있다.
스플릿 브레인: 특수한 상황 또는 네트워크 토폴로지를 갖는 Production 환경에서, 네트워크 파티셔닝 장애로 인해 시스템이 Sub-Cluster로 쪼개짐에 따라 각 Sub-Cluster가 스스로를 Primary 또는 정상적인 서비스라고 인식하는 것을 의미한다.
특정한 자원이나 객체에 동시에 쓰거나 오염시키는 것을 방지하기 위해 오직 하나의 트랜잭션이나 클라이언트만 어떤 자원이나 객체의 잠금을 획득할 수 있다.
사용자명으로 사용자를 유일하게 식별할 수 있어야 하므로 오직 한 명의 사용자만 특정한 사용자명으로 등록할 수 있다.
분산 시스템에서 이를 구현하기 위해선 주의해야 한다.
노드 자신이 리더라고 믿을지라도 노드의 정족수가 무조건 동의한다는 의미는 아니기에 GC중단이나 네트워크 지연등으로 노드가 죽었다고 판단한 다른 노드들이 다른 리더를 선출할 수 있다.
이 때, 죽었다고 파악된 노드가 사실은 죽지 않았기에 계속 리더인 것처럼 동작한다면, 리더가 하나 이상이기에 문제가 발생할 수 있다.
분산 잠금의 잘못된 구현: 클라이언트 1은 임차권이 만료됐지만 여전히 유효하다고 생각해 저장소의 파일을 오염시킨다.
위 그림은 방금 얘기한 문제를 다루는데, 클라이언트1이 가비지 컬렉션을 하느라 멈춘 시간동안 임차권이 만료되어 클라이언트2가 임차권을 획득하여 작업을 했는데, 클라이언트가 가비지 컬렉션 이후 저장소에 쓰기 작업을 수행하게 되면, 데이터 오염이 발생한다.

펜싱 토큰

임차권을 이용할 때 문제가 리소스에 대한 쓰기 접근을 아직도 자신이 리더라고 믿는 노드들을 막지 못하는 문제가 있는데, 이를 해결하기 위한 기법으로 펜싱(fencing)이 있다.
펜싱 토큰이 증가하는 순서에 맞는 쓰기만 허용함으로써 저장소에 대한 접근을 안전하게 만들기
잠금 서버가 잠금, 임차권을 승인할 때 펜싱 토큰(fencing token)도 반환한다고 가정한다.
펜싱 토큰은
잠금이 승인될 때마다 증가하는 숫자로,
클라이언트가 쓰기 요청을 저장소 서비스로 보낼 때마다
자신의 현재 펜싱 토큰을 포함하도록 요구할 수 있다.
즉, 저장소 서비스에서는 펜싱 토큰의 유효성을 확인해서 자신이 아직 리더라고 믿는 노드의 쓰기 접근을 막을 수 있다.
잠금 서비스로 주키퍼를 사용하면 트랜잭션 ID(zxid)나 노드 버전(cversion)을 펜싱 토큰으로 사용할 수 있다. 이들은 단조 증가가 보장되기에 필요한 속성을 지닌다.
그리고 서버 측에서 토큰을 확인하는 것은
결점으로 보이지만, 대부분 좋다.
서비스의 클라이언트들이 항상 잘 동작한다고 믿는건 현명하지 못하다.
서비스를 실행하는 사람들의 우선 사항과 매우 다른 우선 사항을 가진 사람들이 실행하는 경우가 흔하기 때문이다.

비잔틴 결함

일시적인 문제로 중단되어 정족수로 리더가 아니게 된 노드의 쓰기 권한을 검증하기 위해 펜싱 토큰을 사용했다.
하지만, 이 펜싱 토큰은 악의적인 의도로 위조될 수 있고, 이처럼 분산 시스템에서 노드가 거짓말을 할지도 모른다는 위험이 있다. 이런 동작을 비잔틴 결함(Byzantine fault)이라고 하며, 이렇게 신뢰할 수 없는 환경에서 합의에 도달하는 문제를 비잔틴 장군 문제(Byzantine Generals Problem)라고 한다.
비잔틴 장군 문제 두 장군 문제(Two Generals Problem)을 일반화 한 것으로, 이 문제에선 두 군대의 장군이 전투 계획에 동의해야 하는 상황을 가정한다. 비잔틴 버전 문제에선 n명의 장군이 동의해야 하고, 이 가운데 배신자들이 있다는 사실에 방해를 받는다. 하지만, 누가 배신자인지는 미리 알려지지 않는다. 비잔티움(Byzantium)은 고대 그리스 도시였으며 심하게 복잡하고 관료주의적이며 정직하지 못하다는 뜻에서 비잔틴으로부터 이름이 유래됐다.
특정 노드가 프토토콜을 준수하지 않거나 악의적인 공격자에 의해 공격을 받더라도 시스템이 올바르게 동작한다면 이 시스템은 비잔틴 내결함성(Byzantine fault-tolerant)을 지닌다고 할 수 있다.
그런데, 비잔틴 내결함성 알고리즘은 웹 애플리케이션에서 적용이 힘들다. 왜냐햐면
대부분의 비잔틴 내결함성 알고리즘은 노드의 2/3 이상의 압도적 다수가 올바르게 동작하기를 요구하는데,
버그를 막는 데 이 방법을 쓰려면 동일한 소프트웨어를 독립적으로 구현한 것이 네 개 있어야 하고 네 가지 구현 중 하나에서만 버그가 있기를 바래야 한다.
마찬가지로 프로토콜이
취약점, 보안 침해, 악성 공격에서 우리를 보호할 수 있다면 매력적이겠지만
대부분의 시스템에서 공격자가 노드 하나를 침해할 수 있다면
모든 노드를 침해할 수도 있다.
그래서 결국, 전통적인 메커니즘(인증, 접근 제어, 암호화, 방화벽 등)이 여전히 보호 수단으로 사용되고 있다.

약한 형태의 거짓말

노드들이 정직하다고 가정하더라도 다음과 같은 약한 형태의 거짓말로부터 보호해주는 메커니즘을 추가하는건 가치가 있을 수 있다.
하드웨어 문제
소프트웨어 버거
잘못된 설정때문에 유효하지 않은 메세지
이러한 메커니즘은 완전한 비잔틴 내결함성을 지니지는 않지만, 그럼에도 더욱 나은 신뢰성으로 향하는 간단하고 실용적인 발걸음이다.
예를 들면,
네트워크 패킷은 종종 HW 문제나 OS, 드라이버, 라우터 등의 버그로 오염되는데, 가끔 TCP, UDP의 체크섬으로도 검출이 안되는 경우가 있다.
⇒ 애플리케이션 수준 프로토콜에서 체크섬을 쓰는 것처럼 단순한 수단을 쓰면 충분하다.
공개적으로 접근 가능한 애플리케이션은 입력에 대한 검증이 중요하다. 값이 합당한 범위인지, 문자열의 크기가 너무 크지 않은지등…
⇒ 기본적인 정상성 점검(sanity-checking)을 하는게 좋다.
NTP 클라이언트는 여러 서버 주소를 설정할 수 있다. 동기화를 할 때 클라이언트는 모든 서버에 접속해서 그들의 오차를 추정한 후 서버 중 다수가 어떤 시간 범위에 동의하는지 확인할 수 있다. 대다수의 서버가 정상이라면 잘못된 시간을 보고하는 NTP 서버를 이상치(outlier)로 검출해 동기화 대상에서 제거할 수 있다.

시스템 모델과 현실

분산 시스템의 여러 이슈를 해결하기 위한 알고리즘이 많이 설계되고 있다.
이러한 알고리즘이 유용하려면 지금까지 살펴본 다양한 결함들을 견딜 수 있어야 한다.
알고리즘은
실행되는 HW / SW 설정의 세부 사항에 심한 의존성을 가지지 않아야 하고,
그러기 위해서는
시스템에서 발생할 것으로 예상되는 결함의 종류를 어떻게든 정형화해야 한다.
시스템 모델(system model)을 정의해서 정형화하는데, 시스템 모델은 알고리즘이 가정하는 것을 기술한 추상화다. 타이밍 가정에 대해선 세 가지 시스템 모델이 흔히 사용된다.

동기식 모델

: 네트워크 지연, 프로세스 중단, 시계 오차에 모두 제한이 있다고 가정한다.
시계가 정확하게 동기화되고, 네트워크 지연이 없다고 암시하는 것은 아니고, 해당 사항에 대해 상한치를 초과하지 않을 것임을 안다는 것이다.
사실, 동기식 모델은 기약 없는 지연과 중단이 발생하는 대부분의 현실 시스템에서 현실적인 모델이 아니다.

부분 동기식 모델

대부분의 시간에는 동기식 시스템처럼 동작하지만 때때로 네트워크 지연, 프로세스 중단, 시계 드리프트의 한계츠를 초과한다는 뜻이다. 많은 시스템에서 현실적인 모델로 보통은 잘 동작하지만 가끔씩 타이밍 가정이 조각 날지도 모른다는 사실을 고려해야 한다.
이런 일이 발생하면 네트워크 지연, 중단, 시계 오차는 제멋대로 커질 수 있다.

비동기식 모델

이 모델에서 알고리즘은 타이밍에 대한 어떠한 가정도 할 수 없다.
심지어 시계가 없을 수도 있다. (타임아웃을 쓸 수 없다는 의미기도 하다.)
어떤 알고리즘은 비동기식 모델용으로 설계할 수 있겠지만 매우 제한적이다.
이러한 타이밍 문제 외에 노드 장애도고려해야 한다. 가장 널리 쓰이는 노드용 시스템 모델은 다음과 같다.

죽으면 중단하는 (crash-stop) 결함

알고리즘은 노드에 장애가 나는 방식이 죽는 것 뿐이라고 가정한다.
노드가 응답을 멈추면 그 이후로 그 노드는 영원히 사용할 수 없다는 뜻이다.

죽으면 복구하는(crash-recovery) 결함

노드가 어느 순간 죽을 수 있지만, 시간이 흐르면 다시 응답하기 시작할 것이라고 가정한다. 죽으면 복구하는 모델에서 노드는 메모리에 있는 상태는 손실되지만 죽어도 데이터는 남아 있는 안정된 저장소가 있다고 가정한다.

비잔틴(임의적인) 결함

노드는 다른 노드를 속이거나 기만하는 것을 포함해 전적으로 무슨 일이든 할 수 있다.

알고리즘의 정확성

알고리즘이 정확하다(correct)는 게 어떤 의미인지 정의하기 위해 알고리즘의 속성(property)을 기술할 수 있다. 즉, 정확하다는 게 어떤 뜻인지 정의하고 싶은 분산 시스템의 속성을 써볼 수 있다.
위에서 알아봤던 펜싱 토큰을 생성한다면 이 알고리즘은 다음 속성을 지녀야 한다.
유일성
⇒ 펜싱 토큰 요청이 같은 값을 반환하지 않는다.
단조 일련번호
⇒ 요청 x가 토큰 tx를, 요청 y가 토큰 ty를 반환했고 y가 시작하기 전에 x가 완료됐다면 tx < ty를 만족한다.
가용성
⇒ 펜싱 토큰을 요청하고 죽지 않은 노드는 결국엔 응답을 받는다.
알고리즘은 시스템 모델에서 발생하리라고 가정한 모든 상황에서 그 속성들을 항상 만족시키면 해당 시스템 모델에서 정확하다.
물론, 알고리즘이 어떻든 노드가 모두 죽거나 네트워크 지연이 무한정 길어진다면 할 수 있는건 없다.

안정성과 활동성

안정성(safety)활동성(liveness)을 구별할 필요가 있다.
알고리즘의 정확성에서 언급한 유일성과 단조 일련번호가 안전성이라면, 가용성은 활동성 속성이라 할 수 있다. 무엇이 이 속성 구분을 하는 기준이 되는 걸까?
안전성은 나쁜 일이 일어나지 않는다.
활동성은 좋은 일은 결국 일어난다.
물론, 이러한 좋고 나쁨은 주관적이니 너무 큰 의미를 부여할 필요는 없다.
안전성과 활동성에 대한 실제 정의는 정확하고 수학적이다.
안전성 속성이 위반되면 그 속성이 깨진 특정 시점을 가리킬 수 있다
Ex: 유일성 속성이 위반되면 중복된 펜싱 토큰을 반환한 특정 연산을 식별할 수 있다.
속성 위반 후 위반을 취소할 수 없다. 이미 손상된 상태다.
활동성 속성은 어떤 시점을 정하지 못할 수 있지만, 항상 미래에 그 속성을 만족시킬 수 있다는 희망이 있다.
노드의 요청이 응답을 아직 받지 못했지만, 결국 응답을 받을 수 있다는 희망
이 안전성과 활동성 속성을 구별하면 시스템 모델을 다루는데도 도움이 된다.
분산 알고리즘은 시스템 모델의 모든 상황에서
안전성 속성이 항상 만족되기를 바라고, ( 과정도 중요하다)
활동성 속성에 대해서는 경고를 하는게 허용된다. (결과가 중요하다)

시스템 모델을 현실 세계에 대응시키기

안전성 및 활동성 속성과 시스템 모델은 분산 시스템의 정확성을 따져보는 데 매우 유용하다.
하지만,
현업에서 알고리즘을 구현할 때 현실의 지저분한 사실들이 우리를 괴롭게 만들고
시스템 모델은 현실의 단순화된 추상화라는게 명백해진다.
그럼 여기서 우릴 곤란하게 만든다는 현실의 지저분한 사실이라는게 뭘까?
몇 가지 예를 들어보자.
1.
죽으면 복구하는 시스템 모델에서 알고리즘은 일반적으로 안정된 저장소에 있는 데이터는 노드가 죽어도 남아 있다고 가정한다.
⇒ 하지만 디스크에 있는 데이터가 오염되거나,
⇒ 하드웨어 오류나 잘못된 설정으로 데이터가 지워지면 어떻게 될까?
2.
정족수 알고리즘은 노드가 저장했다고 선언한 데이터를 기억하고 있다는 것에 의존한다.
⇒ 노드가 기억 상실을 앓거나 저장한 데이터를 잃어버릴 수 있다면,
⇒ 정족수 조건을 꺠트리고 따라서 알고리즘의 정확성도 깨트리게 된다.

이론과 실전은 다르다.

알고리즘은 이론적으로 설명할 때는 그냥 어떤 일이 일어나지 않는다고 가정할 수 있다.
그리고 비비잔틴(non-Byzantine) 시스템에선 일어날 수 있는 결함과 일어날 수 없는 결함에 대해 어떤 가정을 해야 한다.
하지만 실제 구현에선 여전히 불가능하다고 가정했던 일이 발생하는 일이 발생하는 경우를 처리하는 코드를 포함시켜야 할 수도 있다. 그 코드가 엉망인 상황을 정리하는 것 뿐이라도 말이다.
그렇다고 이론적인 추상 시스템 모델이 쓸모 없는 것은 아니다. 오히려 반대다.
추상 시스템 모델은 현실 시스템의 복잡함에서 우리가 추론할 수 있는 관리 가능한 결합의 집합을 뽑아내어 문제를 이해하고 체계적으로 해결하려고 노력할 수 있게 하는 데 도움이 된다.
또한, 시스템 모델에서 그것들의 속성이 항상 성립된다고 보여줌으로써 알고리즘이 올바르다고 증명할 수 있다.
물론, 알고리즘이 올바르다고 증명됐더라도 현실 시스템에서의 구현도 언제나 올바르게 동작한다는 뜻은 아니다.
그래도 알고리즘의 이론적 분석은 실제 시스템에서 오랫동안 숨어있다가 나중에 수면위로 등장하는 것보다 훨씬 낫다. 즉, 이론적 분석과 경험적 실험은 똑같이 중요하다.