- 분산 시스템을 다루는 것은 한 컴퓨터에서 실행되는 소프트웨어를 작성하는 일과는 근본적으로 다르다.
- 엔지니어로서의 임무는 모든 게 잘못되더라도 제 역할을 해내는 시스템을 구축하는 것이다.
- 네트워크 관련 문제, 시계 및 타이밍 문제
결함과 부분 장애
- 단일 컴퓨터에서는 하드웨어가 올바르게 동작하면 항상 같은 결과를 낸다.
- 컴퓨터는 의도적인 설계로 결함이 발생하면 잘못된 결과를 반환하기 보다 완전히 동작하지 않는다.
- 분산 시스템에서는 시스템의 어떤 부분을 잘 동작하지만 다른 부분은 예측할 수 없는 방식으로 고장난다.
- 부분 장애는 비결정적이라서 어렵다.
- 비결정성과 부분 장애 가능성이 분산 시스템을 다루기 어렵게 한다.
클라우드 컴퓨팅과 슈퍼 컴퓨팅
- 대규모 컴퓨터 시스템 구축 방법
- 구축 방법에 따라 결함 처리 방법도 매우 다르다.
- 슈퍼 컴퓨터는 단일 노드 컴퓨터에 가깝다. 장애 시점부터 재시작하면 된다.
- 인터넷 관련 애플리케이션은 사용자에게 지연시간이 낮은 서비스를 제공해야 한다는 점에서 온라인이다.
- 시스템이 커질수록 구성 요소 중 하나가 고장날 가능성도 높아진다.
- 분산 시스템이 동작하게 만들려면 부분 장애 가능성을 받아들이고 내결함성 메커니즘을 넣어야한다.
- 신뢰성 없는 구성 요소를 사용해 신뢰성 있는 시스템을 구축해야 한다.
- 분산 환경에서는 최선의 상황을 바라기보다는 발생 가능성이 상당히 낮은 결함도 광범위하게 고려해야 한다.
신뢰성 없는 네트워크
- 분산 시스템이 인터넷 서비스를 구축하는 주된 방법이 된 이유
- 하드웨어가 상대적으로 저렴하다.
- 클라우드 서비스를 사용할 수 있다.
- 지리적으로 분산시켜 높은 신뢰성을 확보한다.
- 인터넷과 데이터센터 내부 네트워크는 대부분 비동기 패킷 네트워크다.
- 메시지가 언제 도착할지 도착하는지는 보장하지 않는다.
- 요청을 보내고 응답을 기다릴 때 잘못될 수 있는 상황
- 요청 손실
- 요청이 큐에서 대기하다 나중에 전송
- 원격 노드에 장애가 발생
- 원격 노드가 응답을 뭠췄지만 나중에 다시 응답가능
- 원격 노드가 응답을 처리했지만 응답이 네트워크에서 손실
- 원격 노드가 요청을 처리했지만 응답이 지연되다가 나중에 전송될 수 있다.
- 전송 측은 패킷이 전송됐는지 아닌지 구별할 수 없다.
- 유일한 정보는 응답을 받지 못했다는 사실 뿐
- 이런 문제를 다루는 흔한 방법은 타임아웃이다.
현실의 네트워크 결함
- 현실에서는 다양한 이유로 네트워크 결함이 발생한다.
- 네트워크 장비를 중복 추가한다고 기대한만큼 결함이 줄어들지 않는다.
- 반드시 네트워크 결함을 견뎌내도록 처리할 필요는 없다.
- 소프트웨어가 네트워크 문제에 어떻게 반응하는지 알고 시스템이 그로부터 복구할 수 있도록 보장해야 한다.
결함 감지
- 특정한 환경에서는 노드가 동작하지 않는다고 명시적으로 피드백을 받을 수 있다.
- 노드가 실행 중인 장비에 연결할 수 있지만 목적지 포트에서 수신 대기하는 프로세스가 없다면 RST, FIN 패킷을 응답으로 보내 tcp연결을 닫는다.
- 노드가 요청을 처리하다 죽으면 원격 노드에서 데이터가 어느정도 처리됐는지 알 수 없다.
- 노드 프로세스가 죽었지만 노드 운영체제는 실행중이라면 다른 노드에게 프로세스가 죽은 사실을 알려 타임아웃이 만료되기를 기다릴 필요 없게 할 수 있다.
- 데이터센터 내 네트워크 스위치의 관리 인터페이스에 접근 가능하다면 하드웨어 수준을 감지할 수 있다.
- 일반적으로 아무 응답도 받지 못할 것이라고 가정해야 한다.
- 애플레케이션 수준에서 재시도 후 응답을 받지 못하면 노드가 죽었다고 선언할 수 있다.
타임아웃과 기약 없는 지연
- 적절한 타임 아웃을 설정할 간단한 답은 없다.
- 비동기 네트워크는 기약 없는 지연이 있다.
- 패킷이 도착하는 데 걸리는 시간에 상한치가 없다.
네트워크 혼잡과 큐 대기

동기 네트워크 대 비동기 네트워크
- 전화 네트워크는 극단적인 신뢰성을 지닌다.
- 통화를 할 때 회선이 만들어지고 통화에 대한 고정되고 보장된 양의 대역폭이 할당된다.
그냥 네트워크 지연을 예측 가능하게 만들 수는 없을까?
- 전화나 영상 통화는 전송되는 비트 개수가 고정되어 있어 회선을 사용할 수 있다.
- 순간적으로 몰리는 트래픽을 회선을 처리하기에는 비효율적이다.
- 그래서T TCP는 네트워크 용량에 맞춰 전송률을 동적으로 조절한다.
- 서비스 품질(QOS)와 진입 제어를 사용해 패킷 네트워크에서 회선 교환을 흉내낼 수 있다.
- 멀티 테넌트 데이터센터, 공개 클라우드, 인터넷 통신에도 사용할 수 없다.
- 현재 기술로는 네트워크 혼잡, 큐 대기, 기약 없는 지연이 발생한다 가정해야 한다.
신뢰성 없는 시계
애플리케이션은 다양한 방식으로 시계에 의존한다.
- 이 요청이 타임아웃됐나?
- 사용자가 우리 사이트에서 시간을 얼마나 보냈나?
- 이 기사가 언제 게시 됐나?
- 로그 파일에 남은 오류 메시지의 타임스탬프는 무엇인가?
→ 1,2는 지속시간을 측정하고 5,8은 시점을 기술한다.
- 개별 장비는 자신의 시계를 갖고 있다.
- 네트워크 시간 프로토콜을 사용해 시계를 조정할 수 있게 한다.
단조 시계 대 일 기준 시계
현대 컴퓨터는 최소 두 가지 종류의 시계를 갖고 있다.
일 기준 시계
직관적으로 시계에 기대하는 일을 한다.
- 일 기준 시계는 보통 NTP로 동기화된다.
- 이상적으로는 장비마다 동일한 타임스태프를 갖는다.
- 로컬 시계가 NTP 서버보다 너무 앞서면 강제로 리셋되어 과거 시점으로 거꾸로 뛰는 것처럼 보일 수 있다.
단조 시계
단조 시계는 타임아웃이나 서비스 응답 시간 같은 구간을 재는 데 적합하다.
→ 단조 시계란 이름은 항상 앞으로 흐른다는 사실에서 나왔다.
- NTP는 로컬 시계가 NTP 서버보다 빠르거나 느리면 조정할 수 있다.
- 0.05% 올리거나 내리는 것을 허용
- 앞이나 뒤로 뛰게 할 수는 없다.
- 분산 시스템에서 경과 시간을 재는 데 단조 시계를 쓰는 것은 괜찮다.
- 다른 노드의 시계 사이에 동기화가 돼야 한다는 가정이 없고 부정확해도 민감하지 않기 때문이다.
시계 동기화와 정확도
정확한 시계를 알려주는 방법은 기대만큼 정확하지 않을 수 있다.
- 컴퓨터 수정 시계는 아주 정확하지는 않다.
- 컴퓨터 시계가 NTP 서버와 너무 차이가 많이나면 강제 리셋됨
- NTP와 방화벽으로 통신이 안될 수 있다.
- 네트워크 지연에 의존적
- 윤초 문제
- 가상 장비
시계 정확도가 중요해서 상당한 자원을 투입할 생각이 있다면 시계 정확도를 매우 높힐수 있다.
→ GPS 수신기, 정밀 시간 프로토콜, 세심한 배포 및 모니터링
동기화된 시계에 의존하기
소프트웨어의 어떤 부분이 정확히 동기화된 시계에 의존한다면 그 결과는 극적인 고장보다는
조용하고 미묘한 데이터 손실이 발생할 가능성이 높다.
→ 동기화된 시계가 필요하다면 모든 장비 시계 차이를 모니터링해야 한다.
이벤트 순서화용 타임스탬프

- 최종 쓰기 승리가 된다.
- 최근의 정의는 로컬 일 기준 시계에 의존하면 틀릴 수 있다는 사실을 기억하자
- NTP 동기화를 정확하게 할 수는 없다.
- 논리적 시계는 이벤트 순서화의 안전한 대안이다.
시계 읽기는 신뢰 구간이 있다.
공개 인터넷에 있는 NTP 서버를 사용하면 달성 가능한 최선 정확도는 수십 밀리초이고 혼잡이 있으면 오차는 100밀리초 이상으로 급증한다.
- 시간을 서버로부터 얻는다면 불확실성은 마지막 동기화한 시간 후로 예상되는 NTP 서버의 불확실성 + 네트워크 왕복시간
- 스패너에 있는 구글 트루타임 api를 요청하면 가장 이른것과 가장 늦을 것을 받는다.
- 구간의 너비는 마지막으로 동기화된 이후로 얼마나 지났는지에 의존한다.
전용 스냅숏용 동기화된 시계
스냅숏 격리 구현은 단조 증가하는 트랜잭션 ID가 필요하다.
→ 여러 데이터센터에 있는 여러 장비에 데이터베이스가 있을 때는 전역 단조 증가 트랜잭션 ID를 생성하기 어렵다.
- 구글 스패너는 유일하게 동기화된 시계를 사용한다.
- 시계 신뢰구간을 측정하고
- 두 구간이 겹치지 않는다면 인과 관계를 파악할 수 있다.
- 인과성을 보장하기 위해 의도적으로 신뢰 구간의 길이만큼 커밋 전에 기다린다.
프로세스 중단
- 파티션마다 리더 하나씩 있고 리더만 쓰기를 받아들이도록 허용된다.
- 노드가 리더인지 안전하게 쓰기를 받아들일 수 있는지 알아야 한다.
- 한 가지 방법은 리더가 다른 노드들로부터 임차권을 얻는 것이다.

이 코드는 동기화된 시계에 의존해서 동기화가 깨지면 이상한 일을 할 수 있다.
- 로컬 단조 시계를 사용하더라도 문제가 있다.
- 쓰레드가 오래 멈추는 다양한 이유
- GC 발생
- 가상 환경에서 가상 장비는 서스펜드됐다가 재개될 수 있다.
- 운영체제가 다른 스레드로 컨텍스트 스위치하거나 하이퍼바이저가 다른 가상 장비로 스위치되면 스레드는 멈출 수 있다.
- 장비 부하가 높으면 다시 실행되는 데 시간이 좀 걸릴 수도 있다.
- 애플리케이션이 동기식으로 디스크에 접근
- 운영체제 디스크 스왑
- 단일 장비에서 다중 스레드 코드를 작성할 때 그 코드를 스레드 안전하게 만들 수 있는 좋은 도구가 많다.
- 분산 시스템의 노드는 어느 시점에 실행이 상당 시간 멈출 수 있다고 가정해야 한다.
응답 시간 보장
- 항공기, 로켓, 자동차, 물리적 물체를 제어하는 시스템
- 응답해야 하는 데드라인이 명시된다.
- 엄격한 실시간 시스템
- 서버측 데이터 처리 시스템에게 실시간 보장은 적절하지 않다.
- 비실시간 환경에서 운영될 때 발생하는 중단 시간 불안정의 영향을 많이 받는다.
가비지 컬렉션의 영향을 제한하기
- 런타임이 애플리케이션에게 노드가 곧 GC 중단이 필요하다는 경고를 할 수 있다면
- 해당 노드로 요청을 멈추게 할 수 있다.
- 이런 방법은 클라이언트로부터 GC 중단을 감추고 응답 시간의 상위 백분위를 줄여준다.
→ 수명이 짧은 객체만 가비지 컬렉터를 사용하고 수명이 긴 객체의 전체 gc가 필요할만큼 쌓이기 전에 프로세스를 재시작하는 방법도 있다.
지식, 진실, 그리고 거짓말
분산 시스템
- 공유 메모리가 없다.
- 지연 변동이 큰 신뢰할 수 없는 네트워크를 통해 메세지를 보내야만 한다.
- 부분 장애
- 신뢰성 없는 시계
- 프로세스 중단
네트워크 기반으로 데이터를 보내야 하기 때문에 어떤 것도 확실히 알지 못한다.
-> 메세지를 기반으로 추측할 뿐이다.
분산 시스템에서 우리는 동작(시스템 모델)에 관해 정한 가정을 명시하고, 가정을 만족시키는 방식으로 설계할 수 있다.
진실은 다수결로 결정된다.
- 노드는 자신에게 보내지는 메세지는 모두 받을 수 있지만, 보내는 메세지는 유실되거나 지연된다.
이러한 상황은 노드가 정상적으로 메세지를 받을 수 있는 상태여도
Timeout이 발생하면 노드를 죽었다고 판단한다.
- 한쪽 연결이 끊긴 노드는 보내는 메세지가 다른 노드로부터 응답을 받지 못하는 것을 알아내서 결함이 있는게 확실하다고 깨달을 수 있다.
그럼에도 다른 노드들이 노드가 죽었다고 잘못 선언하고, 한쪽 연결이 끊긴 노드는 그에 대해 아무 일도 할 수 없다.
- 긴 stop-the-world 가비지 컬렉션 중단
모든 스레드는 GC에 선점되고 1분동안 멈춘다.
결과적으론 아무 요청도 처리되지 못하고 아무 응답도 전송되지 않는다.
기다리면서 재시도 하다가 결국엔 노드가 죽었다고 판단한다.
GC가 끝나고 스레드들은 실행은 재개한다.
노드는 언제든 장애가 날 수 있기 때문에 이러한 것을 방지하기 위해 정족수를 사용한다
리더와 잠금
- 스플릿 브레인을 피하기 위해 오직 한 노드만 데이터베이스 파티션의 리더가 될 수 있다.
- 특정한 자원이나 객체에 동시에 쓰거나 오염시키는 것을 방지하기 위해 오직 하나의 트랜잭션이나 클라이언트만 어떤 자원이나 객체의 잠금을 획득할 수 있다.
- 식별자로 사용자를 유일하게 식별할 수 있어야 하므로 오직 한 명의 사용자만 특정한 사용자 명을 사용할 수 있다.
분산 시스템에서 주의할 점
- 노드가 스스로를 “선택된 자”라고 믿더라도 노드의 정족수도 반드시 동의한다는 뜻은 아니다.
- 이전에 리더였어도 죽었다고 판단하면 노드는 강등되고 다른 리더가 이미 선출되어 있을 수 있다.
잠금을 잘못 구현해서 생긴 데이터 버그 예시
![[Pasted image 20240127233638.png]]
요구사항
- 어떤 파일을 한 번에 클라이언트 하나씩만 접근하도록 보장하고 싶다.
- 그 파일에 접근하기 전에 잠금 서비스로부터 임차권을 얻도록 해서 이 기능을 구현하려고 한다.
문제점
- 클라이언트가 멈춰 있으면 임차권은 만료된다.
- 다른 클라이언트가 새로운 임차권을 얻어 쓰기를 시작한다.
- 멈췄던 클라이언트가 돌아오면 여전히 유효하다고 생각하고 파일에 다시 쓰기를 한다.
- 쓰기는 충돌된다.
펜싱 토큰
시스템을 방해할 수 없도록 보장하기 위한 가장 단순한 방법 : 펜싱(fencing)
펜싱 토큰 예제
![[Pasted image 20240127234009.png]]
펜싱 토큰(fencing token)
- 잠금이나 임차권을 승인할 때 펜싱 토큰도 반환한다고 가정한다.
- 쓰기 요청을 저장소 서비스로 보낼 때마다 현재 펜싱 토큰을 포함하도록 한다.
잠금 서비스로 주키퍼를 사용하면 트랜잭션 id zxid나 노드 버전 cversion을 펜싱 토큰으로 사용할 수 있다.
이들은 단조 증가가 보장되므로 필요한 속성을 지닌다.
하지만 이러한 것을 사용할 때 잠금으로 보호받지 않는 요청을 처리하지 않도록 일종의 확인이 필요하다.
서버 측에서 토큰을 확인하는 것은 아주 좋다.
비잔틴 결함
펜싱 토큰은 부주의에 의한 오류에 빠진 노드를 감지하고 차단할 수 있다.
그러나 노드가 고의로 시스템의 보장을 무너뜨리려 한다면, 가짜 펜싱 토큰을 포함한 메세지를 보내기만 하면 된다.
비잔틴 결함(Byzantine fault)
- 노드가 받지 않은 메세지를 받았다고 주장하는 경우
- 합의에 도달하는 문제를 비잔틴 장군 문제(Byzantine Generals Problem) 이라고 한다.
비잔틴 장군 문제
두 군대의 장군이 소통할 때 전령을 통해서 한다.
전령(네트워크 패킷)은 때때로 늦거나 실종된다.
또한 배신자가 있어서 사실에 방해를 줄 수도 있다.
가짜나 허위 메세지를 보내 다른 장군들을 속이거나 혼란스럽게 하려고 시도할 수 있다.
일부 노드가 오작동하고 프로토콜을 준수하지 않거나 악의적인 공격자가 네트워크를 방해하더라도 올바르게 동작하면 비잔틴 내결함성(Byzantine fault tolerant)을 지닌다.
예시 )
- 항공 우주 산업 환경에서 CPU나 메모리가 오염되면 사람이 모두 죽거나 로켓이 우주 정거장에 충돌하는 일이 발생할 수 있다.
- 악의를 가지고 보낸 메세지를 구분해야 한다.
- 서로 신뢰할 수 없는 단체들이 중앙 권한에 기대지 않고 트랜잭션이 발생했는지 아닌지를 판단하기 위해 블록체인 같은 P2P 네트워크를 고려할 수 있다.
대부분 시스템은 비잔틴 결함이 없다고 가정한다.
웹 애플리케이션에선 클라이언트의 행동이 임의적이고 악의적이라고 예상한다.
- 입력 확인(Input validation)
- 살균(sanitization)
- 출력 이스케이핑(output escaping)
허용된 것인지 아닌지를 결정하는 권한을 서버에게 준다.
비잔틴 내결함성은 이런 중앙 권한이 없는 P2P 네트워크에 더 적절하다.
소프트웨어 버그가 비잔틴 결함으로 간주할 수 있지만, 도움이 되진 않는다.
- 버그를 막으려면 독립적으로 구현한 것이 N개가 있어야하고 그 중에 버그가 하나만 있길 바래야 한다.
약한 형태의 거짓말
- 사용자 입력을 신중하게 살균한다.
- 합당한 범위에 속하는지 확인
- 메모리를 대량으로 할당해서 서비스 거부가 발생하지 않도록 크기 제한.
- NTP 클라이언트는 여러 서버 주소를 설정한다.
- 서버 중 다수가 어떤 시간 범위에 동의하는지 확인할 수 있다.
- 잘못된 서버를 검출해서 동기화 대상에서 제거할 수 있다.
시스템 모델과 현실
시스템 모델(system model)
- 동기식 모델
- 네트워크 지연, 프로세스 중단, 시계 오차에 모두 제한이 있다고 가정
- 어떤 고정된 상한치를 초과하지 않는다고 생각한다.
- 현실적인 모델은 아니다.
- 기약없는 지연과 중단은 발생할 수 있기 때문이다.
- 비동기식 모델
- 어떠한 가정도 할 수 없다.
- 시계가 없을 수도 있다.(Timeout 사용 불가)
- 부분 동기식 모델
- 대부분의 시간에는 동기식 시스템처럼 동작한다.
- 하지만 가끔 네트워크 지연, 프로세스 중단, 시계 드리프트의 한계치를 초과할 수 있다.
- 가끔 어떤 타이밍 가정이 산산조각 날지도 모른다.
- 현실적인 모델
노드 장애
- 죽으면 중단하는(crash-stop) 결함
- 노드에 장애가 나는 방식은 하나다.
- 즉 죽는 것뿐이라고 가정한다.
- 응답하기를 멈추면 이후로 노드는 영원히 사용할 수 없다.
- 죽으면 복구하는(crash-recovery) 결함
- 알려지지 않은 시간이 흐른 후에는 아마도 다시 응답하기 시작할 것
- 노드는 메모리에 있는 상태는 손실된다.
- 하지만 안정된 저장소가 있다고 가정한다.(비휘발성)
- 비잔틴 결함
- 다른 노드를 속이거나 기만하는 것을 포함해 전적으로 무슨 일이든 할 수 있다.
죽으면 복구하는 결함을 지닌 부분 동기식 모델이 가장 유용한 모델이다.
알고리즘 정확성
예제 )
- 잠금에 사용할 펜싱 토큰
- 유일성
- 펜싱 토큰 요청이 같은 값을 반환하지 않는다.
- 단조 일련번호
- 가용성
모든 상황에서 그 속성들을 항상 만족시키면 해당 시스템 모델에서 정확하다.
하지만 모든 노드가 죽거나 모든 네트워크 지연이 갑자기 무한히 길어진다면 어떤 알고리즘이라도 아무것도 할 수 없다.
안전성과 활동성
- 안전성
- 나쁜일은 일어나지 않는다.
- 유일성
- 단조 일려번호
- 위반되면 그 속성이 깨진 특정 시점을 가리킬 수 있다.
- 위반된 후에는 그 위반을 취소할 수 없다.(이미 손상된 상태)
- 활동성
- 좋은 일은 결국 일어난다.
- 가용성
- 결국에는 이라는 단어를 포함한다.
- ex) 최종적 일관성(eventual consistency)
- 어떤 시점을 정하지 못할 수 있지만, 항상 미래에 그 속성을 만족시킬 수 있다는 희망이 있다.
분산 알고리즘에선 안전성 속성이 항상 만족되기를 요구하는게 일반적이다.
잘못된 결과를 반환하지 않는다고 보장해야 한다.
활동성 속성은 경고를 하는 게 허용된다.
예시 )
- 노드의 다수가 죽지 않고 네트워크 중단으로부터 복구됐을 때만 요청이 응답을 받아야 한다.
- 부분 동기식에선 결국 동기식 상태로 돌아오기를 기대한다.
시스템 모델을 현실 세계에 대응 시키기
분산 시스템의 안전성과 활동성을 평가하는 데는 시스템 모델이 유용하다. 그러나 실제 구현에서는 저장소 오염, 하드웨어 오류 등의 현실적인 문제를 고려해야 한다. 정족수 알고리즘은 노드의 데이터 기억 상태를 중요시하며, 이를 잃을 경우 정확성이 위협된다. 추상 시스템 모델은 문제를 이해하고 해결하는 데 도움이 되지만, 현실 구현에서는 항상 적용되지 않을 수 있다. 이론적 분석과 경험적 실험이 함께 필요하다.