분산 시스템은 전 세계의 구조를 바꿨다. 웹 브라우저가 지구상 어딘가에 있는 웹 서버에 접속하면 클라이언트/서버 분산 시스템이라는 구조에 한 구성원이 된다. Google이나 Facebook의 웹 서비스를 사용한다는 것은 하나의 기계를 사용하는 것이 아니다. 수천대로 이루어진 기계들이 사이트의 특정 서비스를 제공하기 위해서 서로 협력하고 있다.
1. 분산 시스템의 등장 배경 (Introduction)
분산 시스템은 여러 대의 독립된 컴퓨터가 네트워크로 연결되어, 사용자에게는 마치 하나의 시스템처럼 보이게 하는 소프트웨어 모음이다. 핵심 과제는 '어떻게 하면 신뢰할 수 없는 부품(메시지 손실, 기기 장애 등)을 가지고 신뢰할 수 있는 시스템을 구축할 것인가?'에 있다.
분산 시스템의 핵심 사안은 실패와 고장의 극복이다. 개별 구성 요소들은 자주 고장 나지만 기계들을 고장 없는 시스템처럼 보이도록 만들 수가 있다.
2. 통신의 기초 (Communication Basics)
컴퓨터 간의 통신은 기본적으로 메시지 교환을 통해 이루어진다.
- 비신뢰성: 네트워크 하드웨어(카드, 스위치, 라우터 등)는 완벽하지 않으며 패킷의 손실, 지연, 순서 바뀜 등이 빈번하게 발생한다.
- 패킷 손실의 원인: 비트 손상, 하드웨어 손상 외에도 라우터나 수신 호스트의 메모리 버퍼가 부족할 때 패킷을 버리게 되는 경우가 가장 근본적인 원인이다.
패킷 손실은 네트워킹에서 근본적인 문제이다. 그렇다면 어떻게 대처해야 할까?
3. 신뢰할 수 없는 통신 계층 (Unreliable Communication Layers)
간단한 방법은 아무런 조치도 취하지 않는 것이다. 어떤 응용 프로그램들은 패킷 손실 시 대응 방법을 가지고 있기 때문에 메시지 계층과 직접 통신하도록 하는 것이 이로울 때도 있다.
가장 대표적인 예시로 UDP/IP가 있다. UDP를 사용하기 위해서는 소켓 API를 이용하여 통신지정(Communication end point)을 생성한다. 다른 편 기계의 프로세스들은 UDP 데이터그램을 원래의 프로세스로 전송한다.
- 특징: 데이터를 보낼 뿐 도착 여부를 보장하지 않는 비연결형 프로토콜이다.
- 체크섬(Checksum): UDP는 데이터의 변조 여부를 확인하기 위해 체크섬 기능을 포함하지만, 패킷 손실 자체를 막지는 못한다.
4. 신뢰할 수 있는 통신 계층 (Reliable Communication Layers)
신뢰할 수 없는 네트워크 위에서 안정적인 통신을 보장하기 위해 다음과 같은 기법을 사용한다.
- 확인 응답(Acknowledgment, ACK): 메시지를 정상 수신했음을 송신자에게 알리는 짧은 메시지이다.
- 타임아웃 및 재전송(Timeout and Retry): 일정 시간 내에 ACK가 오지 않으면 메시지가 유실된 것으로 판단하고 다시 보낸다.
- 시퀀스 카운터(Sequence Counter): 메시지에 번호를 매겨 중복 수신된 메시지를 식별하고 폐기함으로써 '정확히 한 번' 전달되는 효과를 낸다.
- TCP/IP: 이러한 복잡한 처리를 대신 해주는 가장 널리 쓰이는 신뢰성 프로토콜이다.
5. 통신 추상화 (Communication Abstractions)
분산 시스템 개발의 복잡도를 낮추기 위해 통신 과정을 추상화한다.
- 분산 공유 메모리(DSM): 여러 머신의 메모리를 하나의 거대한 가상 주소 공간으로 보이게 하지만, 장애 대응과 성능 문제로 인해 현재는 거의 사용되지 않는다.
- 원격 프로시저 호출(RPC): 원격지에 있는 함수를 마치 로컬 함수처럼 호출하게 해주는 방식이다. 클라이언트는 프로시저 호출을 하고 잠시 후에 결과를 리턴 받는다. 서버는 공지할(export) 루틴을 정의한다. 나머지는 RPC 시스템이 두 부분으로 나누어 담당한다.
- 스텁 생성기(stub generator) 또는 프로토콜 컴파일러(protocol compiler)라고 함
- 런타임 라이브러리(run-time library)
5.1 스텁 생성기(Stub Generator)
인터페이스를 정의하면 통신에 필요한 코드를 자동 생성한다. 스텁 생성기의 장점으로는
- 수작업으로 코드를 작성할 경우 발생할 수 있는 작은 실수를 막아준다.
- 스텁 컴파일러가 코드를 최적화 할 수 있기 때문에 성능을 개선할 수 있다.
스텁 컴파일러에서 고려해야 할 몇 가지 중요한 문제들이 있다.
- 복잡한 구조의 인자나 다수의 인자를 전달하는 문제 : 복잡한 자료 구조를 어떻게 패키지화해서 전송할 것인가? 예를 들어 write() 시스템 콜을 사용할 때는 세 개의 인자를 전달해야 한다. int 형의 파일 디스크립터와 버퍼의 포인터 그리고 써야 할 바이트를 나타내는 크기를 전달해야 한다. 만약 RPC 패키지가 포인터를 전달했다면 그 포인터를 어떻게 해석해야 하는지 알아야 정확한 동작을 할 수 있다.
해결 방안 : 일반적으로 잘 알려진 데이터 형을 사용하거나 자료 구조에 해석하는 방법에 대한 내용을 추가하여 컴파일러가 바이트의 어떤 부분을 직렬화할지 알 수 있도록 한다. - 병행성을 고려하며 서버를 구성해야 한다. : 단순한 서버는 간단한 반복문에서 요청을 대기하며 한 번에 한 요청씩 처리한다. 하지만, 엄청나게 비효율 적일 것이다. 만약 RPC 호출이 차단되면(예, I/O를 기다리며) 서버의 자원이 낭비된다.
해결 방안 : 흔한 구성 방식은 쓰레드 풀을 사용하는 것이다. 이 구성 방식은 서버를 시작할 때 정해진 수의 쓰레드들을 생성한다. RPC 호출이 도착하면 메인 쓰레드가 워커 쓰레드로 보낸다. 메인 쓰레드는 계속 RPC 호출을 받기 위해 대기한다. 또 다른 요청이 도착하면 다시 다른 워커 쓰레드에게 전달한다. 이 방식은 RPC 호출의 올바른 동작을 위해 락이나 기타 동기화 기법들을 써야 하기 때문에 프로그래밍 복잡도가 늘어난다.
5.2 런타임 라이브러리
런타임 라이브러리는 RPC 시스템에서 대부분의 중요한 일을 책임진다. 성능과 신뢰성에 관한 대부분의 문제들을 처리하고 있다.
- 원격 서비스의 위치를 찾는 문제 : 가장 간단한 방법은 기존의 시스템을 활용하는 것이다. 현재의 인터넷 프로토콜이 사용하는 호스트의 이름과 포트 번호를 활용하는 것이다. 그런 시스템에서 클라이언트는 RPC 서비스를 실행하기를 원하는 기계의 호스트 이름 또는 IP 주소 그리고 포트 번호를 반드시 기록한다. 그다음, 패킷이 특정 주소로부터 시스템에 있는 임의의 다른 기계로 전달될 수 있는 메커니즘을 제공해야 한다. - DNS와 이름 해석 방법
- RPC를 어떤 전송 계층 프로토콜 위에 만들지를 결정해야 한다 : TCP보다는 UDP에 RPC 패키지들은 구현을 한다. 그러면 효율적으로 RPC 계층을 만들 수 있지만 RPC 시스템이 신뢰성 담보를 책임져야 한다. RPC 계층은 앞서 설명했던 타임아웃/재시도 그리고 ack방식을 충분히 활용하여 원하는 수준의 신뢰도를 달성한다. 통신 계층은 순서 번호 같은 것을 사용하여 각 RPC가 단 한 번만 또는 많아야 한 번만 발생하도록 보장한다.
5.3 RPC 구현 시 고려해야 할 기타 문제들
RPC가 단순히 함수를 호출하는 것처럼 보이게 하려면, 보이지 않는 곳에서 다음과 같은 복잡한 문제들을 해결해야 한다.
1. 데이터 조립 및 분해 (Fragmentation and Reassembly)
네트워크 패킷은 한 번에 보낼 수 있는 크기(MTU)가 정해져 있다. 만약 RPC를 통해 전달하려는 인자나 결과 값이 이 크기보다 크다면 시스템은 이를 여러 개로 쪼개서 보내야 하며(Fragmentation), 받는 쪽에서는 이를 다시 순서대로 합쳐야 한다(Reassembly). 이 과정에서 패킷이 하나라도 유실되면 전체 메시지를 처리할 수 없으므로 신뢰성 있는 관리가 필수적이다.
2. 바이트 순서 표시 (Byte Ordering / Endianness)
서로 다른 아키텍처를 가진 컴퓨터끼리 통신할 때 가장 흔히 발생하는 문제다.
- 빅 엔디안 (Big Endian): 상위 바이트부터 메모리에 저장하는 방식 (예: 0x1234를 12, 34 순으로 저장).
- 리틀 엔디안 (Little Endian): 하위 바이트부터 메모리에 저장하는 방식 (예: 0x1234를 34, 12 순으로 저장).
- 해결책: 서로 다른 엔디안 방식을 사용하는 컴퓨터가 통신하면 숫자가 완전히 다르게 해석될 수 있다. 따라서 RPC 시스템은 데이터를 보낼 때 표준화된 네트워크 바이트 순서를 따르거나, 마샬링 과정에서 이를 변환하는 작업을 수행한다.
3. 클라이언트에게 비동기적 실행을 허가할 것인가?
어떤 RPC는 동기적으로 동작한다. 즉, 클라이언트가 프로시저 호출을 요청하면 그 결과가 리턴될 때까지 대기한다. 대기 시간이 길어질 수도 있다. 클라이언트가 다른 일을 처리할 수 있도록 어떤 RPC 패키지는 RPC를 비동기적으로 호출하기도 한다. 비동기 RPC가 호출이 되면 RPC 패키지는 요청을 보내고 즉시 리턴한다. 클라이언트는 그 후에 다른 RPC를 호출한다거나 유용한 연산을 하는 식의 다른 작업을 자유롭게 진행할 수 있다. 그때 RPC를 호출하면, 현재 진행 중인 RPC를 대기토록 한다. 완료되면 리턴된 인자들을 접근할 수 있다.
'Deep Dive > OS' 카테고리의 다른 글
| [OSTEP] 스터디 20주차 Andrew File System (AFS) (0) | 2026.01.13 |
|---|---|
| [OSTEP] 스터디 19주차 Part.2 : 네트워크 파일 시스템(NFS) (0) | 2026.01.06 |
| [OSTEP] 스터디 18주차 Flash 기반 SSD (0) | 2025.12.29 |
| [OSTEP] 스터디 17주차 Part.2 Data Integrity and Protection (0) | 2025.12.23 |
| [OSTEP] 스터디 17주차 Part.1 Crash Consistency: FSCK and Journaling (0) | 2025.12.23 |
