Close
Close full mode
logo만렙 개발자 키우기

(2) 데이터 액세스 확장

Git RepositoryEdit on Github
Last update: 9 months ago by nowwaterReading time: 22 min

빠르고 확장성 있는 데이터 액세스

(1) 분산 시스템 설계 및 핵심 고려사항 에서는 분산 시스템을 설계하는 데에 있어서 핵심적인 고려 사항에 대해 알아 보았다.

이번 장에서는 데이터 엑세스 확장에 대해 설명하려 한다.


LAMP 스택을 이용하여 만드는 애플리케이션같이 대부분의 간단한 웹 애플리케이션은 다음과 같은 구조를 가지고 있다.

image

이렇게 간단한 형태의 웹 애플리케이션을 많은 사용자가 사용하게 되면 두 가지 기술적인 문제에 직면하게 된다.

하나는 애플리케이션 서버에 대한 데이터 액세스를 확장성 있게 하는 것이고, 다른 하나는 데이터베이스에 대한 데이터 액세스를 확장성 있게 하는 것이다.

확장성이 있도록 설계된 애플리케이션에서는 애플리케이션 서버(혹은 웹 서버)는 최소화되고 때로는 shared-nothing 아키텍처를 가진다.

이 때문에 애플리케이션 서버 레이어는 수평적 확장이 가능해지게 된다. 그렇기 때문에 데이터베이스 레이어를 확장성 있게 만들 숙제만이 남게 된다. 데이터베이스 레이어는 확장성과 성능 향상을 위하여 많은 고민과 도전이 이루어지는 부분이다.


수 테라바이트 크기의 데이터가 있다고 가정해 보자.

우리는 사용자가 원하는(랜덤한) 데이터에 접근할 수 있도록 하고 싶다. 이는 이미지 애플리케이션 예에서 이미지 파일을 특정한 파일 서버에 위치시키는 것과 유사한 구조다.

image

수 테라바이트 크기의 데이터를 메모리에 올리는 것은 매우 높은 비용이 필요하기 때문에, 모든 데이터를 메모리에 저장하지 않으면서도 빠른 액세스가 가능하도록 하는 것은 매우 어려운 도전 과제가 된다. 여기서 성능에 가장 영향을 미치는 것은 디스크 I/O다.

디스크에서 데이터를 읽는 것은 메모리에서 데이터를 읽는 것보다 훨씬 더 느리다. 데이터가 많을 때에는 이러한 속도 차이가 더 크게 나타난다. 실제로 메모리 액세스는 디스크 순차적 읽기에 비하여 최소 6배 빠르고, 디스크 랜덤 읽기에 대해서는 십만 배 더 빠르다. (참고) 데이터 액세스 비교

게다가 유일 식별자(Unique ID) 처럼 크기가 작은 데이터를 찾는 것 또한 매우 어려운 일이 될 수도 있다.

이런 것들을 쉽게 하기 위한 다양한 방법이 존재한다.

그 중 가장 중요한 네 가지 방법으로는, 캐시, 프록시, 인덱스, 로드 밸런서가 있다.


캐시

최근에 요청받은 데이터는 다시 요청받을 확률이 높다는 지역성의 원리(Locality of Reference)에 기반한 방법이다.

캐시는 매우 짧은 시간동안 유지되는 메모리와 같다. 용량은 매우 제한적이지만, 통상적으로 원래의 데이터 저장소보다는 매우 빠르고 자주 액세스되는 데이터를 보유하고 있다.

아키텍처의 모든 단계에 위치할 수 있지만, 프론트엔드와 가까운 곳에 위치하는 경우가 많다. 왜냐하면 보통 캐시는 서비스의 백엔드까지 가는 시간적인 비용을 줄이기 위해서 사용하는 경우가 많기 때문이다.

캐시는 하드웨어, 운영체제, 웹 브라우저, 웹 애플리케이션 등 다양한 곳에서 사용하고 있다.


이미지 서버 아키텍처에서 빠른 데이터 액세스를 가능하게 하기 위하여 캐시를 추가할 수 있는 두 가지 선택이 있다.

하나는 다음과 같이 캐시를 요청 노드에 추가하는 방법이다.

image

캐시를 요청 노드에 배치하는 것은 응답 데이터를 로컬 저장 공간에서 가져올 수 있게 한다.

매번 요청은 서비스로 보내지고, 요청 노드에 데이터가 존재하면 그 노드는 재빠르게 로컬에서 캐싱된 데이터를 보낸다. 만약 캐시에 데이터가 없다면 요청 노드는 디스크에서 데이터를 질의할 것이다. 여기서 캐시는 요청 노드의 메모리에 있을 수도 있고(매우 빠를 것이다) 요청 노드의 로컬 디스크에 존재할 수 있다(네트워크 스토리지를 사용하는 것보다 빠르다).

이러한 요청 노드를 여러 개로 확장하면 어떤 일이 발생할까?

image

위의 그림에서 볼 수 있듯 요청 노드를 여러 개로 확장시키면 각 노드가 각각의 캐시를 가질 수 있게 된다.

하지만 만약에 로드 밸런서가 임의로 요청을 분산시키면, 같은 요청이 다른 노드로 가게 될 수도 있다. => 캐시 미스 증가!

캐시 미스를 줄이면서 여러 개의 캐시를 사용하기 위해서 사용하는 방법이 전역 캐시분산 캐시 이다.

전역 캐시(Global Cache)

모든 노드가 오직 하나의 캐시 공간만을 사용한다.

서버나 어떤 종류의 파일 저장소를 추가해도 잘 동작하고, 원래의 저장소보다 빠르며 모든 요청 레이어 노드에서 접근이 가능하다.

요청 노드에서 각각의 요청은 로컬에 캐시를 가지고 있는 것과 마찬가지 방법으로 글로벌 캐시에 데이터를 질의한다.

이러한 종류의 캐시 사용은 클라이언트의 개수나 요청 개수가 급격하게 증가하면 하나의 캐시가 그 요청을 감당하지 못할 수도 있기 때문에 복잡해지기 쉽다.

하지만 이러한 아키텍처가 특정 상황에서는 매우 유용하다. (특화된 하드웨어를 써서 전역 캐시를 빠르게 만들거나, 캐시가 필요한 데이터의 양이 고정된 일정량일 경우)

전역 캐시는 두 가지 일반적인 방식이 존재한다.

캐시가 검색을 책임지는 전역 캐시

image

데이터 노드는 오직 캐시에만 데이터를 질의하고, 전역 캐시는 요청받은 데이터를 자기 자신에서 찾을 수 없을 때, 캐시 스스로가 저장 공간에 데이터를 질의하여 요청 노드에 데이터를 전달하도록 하는 방식.

요청 노드가 검색을 하는 전역 캐시

image

요청 노드가 전역 캐시에서 데이터를 질의하여 데이터가 없음을 확인하였을 때는 직접 스토리지에 질의하여 데이터를 가져오는 방식이다.


전역 캐시를 사용하는 대부분의 애플리케이션은 같은 요청이 여러 요청 노드로부터 발생하는 것을 막기 위해 캐시 스스로가 데이터 축출과 조회를 직접하는 캐시가 검색을 책임지는 전역 캐시를 사용한다.

그러나 요청 노드가 검색을 하는 전역 캐시 가 더 유용할 때도 있다. 예를 들어 큰 크기의 파일 제공을 위해 캐시를 사용하는 경우 하나의 파일이 캐시의 용량을 많이 사용하여 다른 파일이 캐시되지 않는다. 그러면 캐시 히트율이 낮아져 전반적인 캐시 미스가 증가하게 된다.

이 경우 자주 사용되는 데이터만 캐시에 위치하게 하는 것이 도움이 된다.

분산 캐시(Distributed Cache)

각각의 노드가 캐시 데이터를 갖는 방식

image

냉장고를 캐시, 그리고 식료품점을 저장 공간이라 비유해 보자. 그렇다면 분산 캐시는 식료품점에서 산 음식을 냉장고, 선반, 도시락과 같이 여러 곳에 나누어 두는 것과 유사하다. 가게에 갈 필요 없이 자주 먹는 것들을 집안에서 빠르게 찾는 것처럼 말이다.

일반적으로 consistent hashing 함수를 사용한다. 따라서 요청 노드가 어떤 특정한 데이터 조각을 찾으려 할 때 해시함수를 이용해 분산 캐시 내의 어디에서 데이터를 찾을 수 있는지 알 수 있다.

각각의 노드는 각각의 작은 캐시를 가지고 있다. 그리고 요청이 들어오면 원본 저장 공간으로 요청을 보내기 전에 다른 노드에 요청을 보낸다. 분산 캐시의 이런 점 때문에 요청 풀에 노드를 추가하면 전체 캐시 크기를 증가시킬 수 있다.

분산 캐시의 단점은 장애가 발생한 노드를 처리하는 방법이 필요하다는 것이다.

다른 노드에 여러 개의 복제본을 가지는 방법으로 해결하기도 한다. 이런 방식을 사용하면 문제 노드를 처리하기 위한 로직이 복잡해지기 쉽다. 요청 레이어에 새로운 노드를 추가하거나 제거하려 할 때 특히 그렇다. 물론 노드가 사라지고 캐시의 일부분이 분실되더라도 원본 데이터에 요청함으로써 필요한 데이터를 가져올 수 있다. 그래서 분산 캐시에 장애가 발생해도 총체적인 장애가 발생하는 것은 아니다.


캐시의 장점은 캐시가 올바르게만 구현되어 있다면 시스템을 더욱 빠르게 만들 수 있다는 것이다. 캐시를 이용해 더욱 더 많은 요청을 이전보다 더 빠르게 처리하게 할 수도 있다. 그러나 이러한 캐시 시스템에는 일반적으로 값 비싼 메모리와 같은 추가적인 저장 공간을 유지하기 위한 비용 문제가 항상 따른다. 공짜는 없다. 캐시는 빠른 처리를 가능하게 하는 굉장히 훌륭한 방법이다. 또한 캐시가 없다면 총체적인 성능 저하가 발생할 수 있는 심한 부하가 발생한 상황에서도 캐시가 있다면 완전하게 시스템의 기능들이 동작하게 하도록 할 것이다.

오픈소스 캐시 중 인기 있는 것 중 하나는 Memcached 다. (로컬캐시나 분산캐시 두 가지 모드로 동작 가능하다). 또한, 언어와 프레임워크에 따라서 선택 가능한 다른 오픈소스도 많이 있다.

Memcached는 많은 웹 사이트에서 사용된다. Memcached는 간단한 메모리 키-값 저장소임에도 굉장히 우수한 성능을 보여 준다. 그리고 임의의 데이터 저장과 조회가 O(1)로 최적화되어 있다.


프록시

다음은 데이터가 캐시에 없는 경우일 때에 대해 알아보자

기본적으로 프록시 서버라 하면 클라이언트의 요청을 백엔드 서버에 전달하는 역할을 수행하는 중간의 하드웨어 혹은 소프트웨어를 의미한다.

프록시는 요청을 필터링, 로깅, 변환(헤더에 속성을 더하고, 빼고, 암호화/복호화, 압축)하는데 사용한다.

image

프록시는 여러 서버에서 오는 요청을 받아 정리하여, 전체 시스템 관점에서 요청 트래픽을 최적화시키는 데도 도움이 된다.

데이터 액세스를 빠르게 하기 위해 프록시가 제공하는 방법 중의 하나로 Collapsed Forwarding 이라 부르는 것이 있는데, 같거나 비슷한 요청들을 모아 단 하나의 요청을 만들어 내는 것을 말한다.

여러 개의 노드에서 Hyeonsu ZZANG이라고 하는 같은 데이터를 요청한다고 생각해보자. 그리고, 요청 데이터는 캐시에 없다. 만약 요청이 프록시를 지나가게 된다면 똑같은 요청들은 하나의 요청으로 바뀔 것이고 단 한번의 Hyeonsu ZZANG 읽기 연산이 발생한다.

물론 요청을 그룹화하는 데 드는 시간 때문에 각각의 요청에는 더 많은 레이턴시가 발생할 수도 있다. 그러나 부하가 높은 상황에서는 성능이 향상될 것이다. 특히 똑같은 데이터가 반복적으로 읽힐 때 말이다. 이것은 캐시와 비슷하지만, 캐시가 데이터나 문서를 저장하는 것과는 다르다. 프록시는 여러 클라이언트가 원하는 문서를 제공하기 위해 요청이나 콜을 최적화시키는 방법이다.

image

요청을 collapse하기 위해 프록시 서버 사용

예를 들어 LAN 프록시에서는 클라이언트들이 인터넷에 연결하기 위해 모두 각각의 IP를 가질 필요가 없다. 그리고 LAN은 같은 내용을 요청하는 경우에는 한 번만 호출한다.(collapse) 대부분의 프록시가 캐시이기도 하기 때문에 개념상 혼란스러울 수도 있지만, 모든 캐시가 프록시처럼 동작하는 것은 아니다.

프록시를 사용하는 다른 방법으로는 공간적으로 가까운 데이터에 대한 요청을 묶어주는 것이 있다. 이러한 전략은 요청의 데이터 로컬리티를 최대화하여 요청 지연을 줄일 수 있다.

image

지역적으로 가까운 데이터 요청을 collapse하는 프록시

수많은 요청이 B의 일부분을 요청하고 있다고 가정한다. (B:partB1, B:partB2 ..)
공간적 로컬리티를 인식하는 프록시를 설치한다. 그리고 프록시는 bigB를 요청한다.

이러한 방식은 클라이언트 수가 테라바이트 크기 데이터의 일부분을 랜덤하게 요청할 때 요청 시간을 굉장히 단축시킬 수 있다. 프록시는 여러 번의 요청을 한 번에 처리하기 때문에 높은 로드 상황이나 캐시 사용이 제한적인 상황에서 특히 유용하다.

프록시와 캐시를 함께 사용하는 것은 무의미하지만, 같이 사용하게 될 때는 캐시를 프록시 앞에 두는 것이 최선이다.
많은 사람들이 참여하는 마라톤 레이스에서 빠른 주자들이 먼저 출발하는 것처럼 말이다. 캐시는 데이터를 메모리에서 가져오고 보통은 매우 빠르다. 그리고 같은 결과를 반환하는 여러 개의 요청도 문제가 되지 않는다. 이 때문에 프록시가 캐시 앞에 있으면 캐시로 요청이 오기 전에 추가적인 지연만 생기고 성능이 저하된다.


인덱스

빠른 데이터 액세스를 위해서 인덱싱 전략을 사용하는 것은 굉장히 잘 알려져 있는 방법이다.

인덱스를 사용하면 데이터 양이 증가할 때 쓰기가 느려지게 된다. 왜냐하면 쓰기를 할 때에는 데이터 기록과 아울러 빠른 읽기를 위해 인덱스를 업데이트해야 하기 때문이다.

일반적인 관계형 데이터베이스에서처럼 이러한 개념이 많은 데이터를 다룰 때에도 적용된다. 물론 인덱스를 이용할 때는 사용자들이 어떠한 식으로 데이터에 접근할지에 대한 충분한 고려가 필요하다.

데이터 크기가 수 테라바이트지만 전달해야 할 데이터 크기가 작을 때는(예를 들어 1KB정도), 데이터 액세스를 최적화하기 위해 인덱스는 필수적이다. 색인이 없이 엄청나게 많은 데이터셋에서 극히 일부를 찾는 것은 굉장한 도전이다. 왜냐하면 수용할 수 있을 만한 시간 안에 반복적으로 데이터를 액세스하여 원하는 데이터를 찾는 것이 불가능하기 때문이다.

더구나, 실제로 이러한 큰 데이터셋은 하나의 물리적 장치에 있는 것이 아니라 여러 곳 혹은 엄청나게 많은 물리적 장치에 나뉘어 위치해 있을 수 있다. 이 말은 우리가 원하는 데이터가 어디에 있는지 알 수 있는 방법이 필요하다는 것이다. 이를 해결하기 위한 가장 좋은 방법이 인덱스다.

image

인덱스는 목차와 같이 데이터가 어디에 위치하는지 알려주는 역할을 한다.

예를 들어 B의 part2에 있는 데이터를 찾고 있을 때, 어디에서 찾을지 어떻게 알 수 있을까?

만약 데이터 타입에 따라서 정렬된 색인이 있다면(A, B, C라고 하는) 색인을 통해서 data B의 원본 위치를 알 수 있다. 그리고나면 B에서 실제 원하는 데이터를 가져올 수 있게 된다.

이러한 색인은 보통은 메모리에 있거나, 들어오는 클라이언트 요청과 굉장히 가까운 곳에 위치해 있다. BerkeleyDB와 트리 형태의 데이터 구조는 이러한 정렬된 리스트를 저장하고 색인을 사용하는 이상적이고 보편적인 방법이라 할 수 있다.


image

멀티 레이어 인덱스

때때로 맵의 형태를 가진 여러 개의 레이어로 이루어진 인덱스도 있다.이런 인덱스에서는 특정한 데이터를 얻을 때까지 한 위치에서 데이터가 있는 다음 위치로 이동할 수 있게 한다


인덱스는 같은 데이터를 다른 여러 개의 뷰로 만드는데 사용할 수 있다. 특히 많은 데이터를 다룰 때 추가적인 데이터 복사본을 사용하여 재정렬하지 않고, 다른 필터를 정의하고 정렬할 수 있다는 것은 인덱스의 엄청난 장점이다.

예를 들어, 이미지 호스팅 시스템에서 책의 페이지를 이미지로 호스팅한다고 생각해보자. 클라이언트는 책의 이미지를 텍스트로 찾고, 시스템은 그에 해당하는 책의 내용을 찾아 준다. 이때 모든 책의 이미지는 파일로 저장되기 위해서 많은 서버를 사용한다. 그리고 원하는 페이지를 찾아서 화면에 랜더링해서 보여줄 수도 있다. 임의의 단어나 단어 집합으로의 접근이 굉장히 쉬워야 하기 때문에 색인 뿐 아니라 인버티드 인덱스가 필요하다. 그리고 정확한 페이지의 위치로 가는 데 추가적인 방법이 필요하다. 그리고 그 페이지에 해당하는 정확한 이미지를 찾을 수 있어야 한다.

중첩된 인덱스(nested indexes) 구조는 하나의 큰 인버티드 인덱스를 사용하는 것보다 적은 공간을 사용할 수 있게 한다.

규모가 큰 시스템에서 중간 인덱스(intermediate indexes)는 매우 중요하다. 데이터가 많다면 압축이 된다 하더라도 인덱스 크기는 꽤 클 수 있고, 저장하는 것에도 그만큼의 비용이 필요하기 때문이다.

만약 전 세계에 약 1억 권의 책이 있다고 가정해 보자. 그리고 계산을 간단하게 하기 위해 각 책은 10페이지 분량이라고 해보자. 각 페이지에는 250개의 단어가 있다면, 전 세계 책들에 쓰인 단어는 총 2500억 개가 된다. 각각의 단어가 평균 5개의 알파벳으로 구성되어 있다면 한 단어당 5바이트가 필요하게 된다. 그렇다면 각각의 단어에 대해서 인덱스를 만드는 것은 테라바이트 단위가 넘는 저장 공간이 필요하다는 결론이 내려진다. 여기에 단어 군에 대한 정보나, 데이터의 위치에 대한 정보, 단어 발생 횟수에 대한 정보를 만드는 것을 추가해야 한다면 더욱 많은 저장 공간이 필요하게 된다.

이렇게 중간 인덱스를 만들고 데이터를 더 작은 섹션으로 나타내는 것은 많은 데이터를 다루기 쉽게 해준다. 데이터는 많은 서버에 퍼져 있지만 인덱스를 이용하기 때문에 빠르게 접근 가능하다. 인덱스는 정보 검색의 초석이며 오늘날 검색 엔진의 기본이다. 이 글에서는 아주 기본적인 내용만 다루고 있지만, 인덱스를 어떻게 하면 작고, 빠르게, 더 많은 정보를 다룰 수 있게 하고 아주 매끄럽게 업데이트할지 (race condition이 발생하지 않도록 하는 것과 검색 적합도 점수를 관리하는 상황에서 대량의 업데이트를 하기 위해 새 데이터를 추가하거나 기존 데이터를 바꿀 때에 대한 기술적인 문제가 남아 있다)에 대한 이슈에 대해서는 수많은 연구가 진행되고 있다.

데이터를 빠르고 쉽게 찾을 수 있게 하는 것은 중요하다. 그리고 인덱스는 이를 가능하게 하는 효과적이고 간단한 도구다.


로드 밸런서(Load Balancers)

서비스 요청을 여러 노드에게 분배하는 일을 한다.

로드 밸런서는 어떤 아키텍처에서든 중요하다.

로드 밸런서를 이용하여 하나의 시스템에서 여러 개의 노드가 투명하게 서비스 안에서 똑같은 기능을 수행할 수 있게 한다.

image

로드 밸런서의 주 목적은 동시에 오는 수많은 커넥션을 처리하고 해당 커넥션이 요청 노드 중의 하나로 전달될 수 있게 하는 것이다. 그리고 단지 노드를 추가하는 것만으로 서비스가 확장성을 가질 수 있도록 한다.

로드 밸런서의 서비스 요청을 처리하는 방법에는 다양한 알고리즘이 있다.

  • 랜덤 선택

  • 라운드 로빈 선택

  • CPU나 메모리 사용률 등의 특정 범주에 따라 노드 선택

로드 밸런서는 소프트웨어로 구현될 수도 있고 하드웨어 제품이 될 수도 있다.

오픈소스 로드 밸런서 중 많이 사용되고 있는 것은 HAProxy 이다.

분산 시스템에서 로드 밸런서는 들어오는 모든 요청이 거쳐가는 시스템의 프런트엔드에 위치하고는 한다. 복잡한 분산 시스템에서는 다음과 같이 여러 개의 로드 밸런서를 사용하는 것이 드문 경우가 아니다.

image

프록시처럼 어떤 로드 밸런서는 요청의 종류를 파악하고 해당 요청을 처리할 수 있는 노드에 전달하는 기능을 가지고 있다.

기술적으로 이러한 형태를 리버스 프록시 라고 부른다.


로드 밸런서를 사용할 때 어려운 문제 중 하나는 세션 데이터를 관리하는 것이다.

온라인 쇼핑 사이트에서 서비스를 이용하는 사용자가 단 한 명뿐이라면 쇼핑 카트에 담아 놓은 상품들을 해당 사용자가 다음에 방문할 때까지 유지하는 것은 그다지 어려운 문제가 아니다(사이트에 재방문하였을 때 쇼핑카트에 이전에 담아 놓은 상품이 있다면 구매 확률이 높아지기 때문에 이것은 매우 중요한 기능이다).

그러나 방문 때마다 다른 세션을 사용한다면 유저의 카트 정보를 일관성 있게 유지할 수 없게 된다. 이러한 문제를 해결하는 한 가지 방법은 세션을 고정하도록(session sticky) 하는 것이다.

이 방법으로 사용자의 요청이 전달될 노드를 고정시킬 수 있다. 그러나 이 방식은 automatic failover와 같은 신뢰성과 관련된 기능을 사용할 수 없게 한다.

왜냐하면 사용자의 쇼핑카트에는 항상 같은 콘텐츠가 있어야 하지만 요청을 처리했던 노드에 문제가 생겨서 failover가 발생하면 해당 정보가 유지되지 않기 때문이다. 그렇기 때문에 해당 노드가 비활성화되면 해당 세션의 데이터는 더 이상 유효하지 않다고 판단할 필요가 있다(가급적 애플리케이션 차원에서 이에 대한 고려를 하지 않는 것이 좋겠지만). 물론 이러한 세션 문제는 브라우저 캐시, 쿠키, URL Rewriting 같은 기술을 이용하여 해결할 수도 있다.

단지 몇 개의 노드만 있을 뿐이라면 라운드 로빈 DNS와 같은 방식이 합리적일 것이다. 로드 밸런서 자체의 비용이 높기도 하지만 불필요한 복잡함을 증가시킬 수도 있기 때문이다. 물론 대규모의 시스템에서는 랜덤이나 라운드 로빈같은 단순한 방식은 물론이고 시스템 사용률이나 처리량을 고려한 복잡한 방식을 사용하는 다양한 알고리즘과 스케쥴링을 사용하고 있다.

이러한 로드 밸런싱 알고리즘은 모두 네트워크 트래픽과 분산 요청을 제어하면서 Auto failover이상 노드 제거(응답이 없을 때)와 같은 신뢰성 관련한 기능을 제공하기도 한다. 그러나 이러한 향상된 기능이 장애 분석을 방해하기도 한다.

예를 들어 부하가 굉장히 높은 상황에서 로드 밸런서는 느리거나 타임아웃이 발생한 노드를 제거한다(요청이 많기 때문에). 그러나 이런 일은 다른 노드의 상황을 악화시킬 뿐이다.

이런 경우에 대비하기 위하여 시스템의 전체적인 상황을 파악할 수 있는 모니터링이 중요하다. 왜냐하면 노드가 줄어들어 전체 시스템 트래픽과 처리량이 줄어든 것으로 이해할 수 있지만, 실제로 각각의 노드는 최대 부하치에 이르고 있는 상황일 수 있기 때문이다.

로드 밸런서는 시스템 용량을 확장시키는 쉬운 방법이다. 그리고 이 글에서 다루는 다른 기술들과 마찬가지로 분산 시스템 아키텍처에서는 필수적인 것이라고 할 수 있다. 로드 밸런서는 노드에 대한 헬스체크 기능을 제공하기도 한다. 반응이 없거나 과부하 상태에 있는 노드를 풀에서 제거하여 시스템 이중화의 장점을 이용할 수 있게 한다.

큐(Queues)

지금까지 설명한 여러 기술은 읽기를 빠르게 할 수 있는 방법에 관한 것이었다. 그러나 시스템이 확장성 있도록 설계하려면 쓰기에 대한 고려 또한 필요하다.

처리 과정이 간단하고 작은 데이터 베이스를 사용하는 시스템에서라면 쓰기는 빠르다. 하지만 복잡한 시스템에서의 쓰기는 전체 수행 시간을 예상할 수 없을 정도로 복잡한 것이 되고는 한다.

예를 들어, 데이터가 여러 곳에 분산된 서버나 인덱스에 쓰여야 하고, 당시의 시스템 부하 상태가 높다면 쓰기 연산은 매우 오랜 시간이 걸리게 된다.

이럴 때 시스템의 성능과 가용성을 얻기 위해서 사용하는 보편적인 방법은 큐를 사용하는 것이다.

image

동기적인 요청

원격 서비스 형태인 클라이언트가 중앙의 서버에 요청을 보내는 시스템이 있다고 가정한다.

이때 각 클라이언트는 서버에 요청을 보낸다. 그리고 서버는 요청에 따른 작업을 가능한 빠르게 처리하고 그 결과를 각각의 클라이언트에게 전달한다. 하나의 서버가, 들어오는 클라이언트의 모든 요청을 처리하는 작은 시스템이라도 데이터 양이 적다면 별 문제없이 작동할 수 있다. 하지만 하나의 서버가 자신이 해결할 수 있는 요청보다 더 많은 요청을 받게 되면, 각 클라이언트는 다른 클라이언트의 요청이 끝나기 전까지 기다려야 한다.

이러한 종류의 동기적 행동은 클라이언트의 성능을 심각하게 저하시킨다. 왜냐하면 클라이언트는 요청에 대한 응답을 받기 전까지는 아무런 일도 수행하지 않고 기다리기 때문이다. 시스템의 로드를 줄이기 위해 서버를 추가한다고 해서 이와 같은 문제가 해결되는 것도 아니다. 또한 클라이언트의 성능을 최대화시키기 위해서 로드 밸런싱을 한다는 것도 굉장히 어려운 문제다. 이러한 문제를 효과적으로 풀기 위해서는 클라이언트의 요청과 서비스를 처리하기 위해서 처리되는 일 사이에 추상화가 필요하다.

image

요청을 처리하기 위해 사용되는 큐

이에 대한 해답은 다. 처리해야 할 일이 들어오면 그것은 큐에 쌓이고, 워커가 일을 처리할 수 있는 상황이 되면 워커 자신이 할 일을 큐에서 꺼내 처리한다. 이렇게 큐를 사용하는 경우로는 데이터베이스로 보내는 간단한 쓰기 요청일 수도 있고, 문서의 썸네일 이미지를 만드는 것과 같이 복잡한 작업일 수도 있다.

클라이언트가 큐로 작업 요청을 보내고 난 다음에는 그 결과를 기다릴 필요가 없다. 대신 큐에 요청이 잘 쌓였다는 응답(Acknowledgement)만 받는다. 이 응답은 나중의 클라이언트가 받게 될 결과에 대한 참조값으로 이용된다.

큐의 장점은 클라이언트가 비동기 방식으로 동작할 수 있게 한다는 데에 있다. 클라이언트의 요청과 그것에 대한 응답 사이에 전략적 추상화를 제공하는 것이다. 반면 동기적인 시스템에서는 요청과 응답의 차이가 없기 때문에 각각이 분리되어 관리될 수 없다. 비동기 방식에서는 클라이언트는 작업을 요청하고 서비스는 요청을 잘 받았다는 ACK를 해당 클라이언트에게 전해 준다. 그 다음 클라이언트는 결과를 받을 때까지 주기적으로 작업의 상태를 체크하다가, 결과 값이 생성되었다는 것을 알게 되면 결과 값을 요청하여 받는다.

클라이언트는 비동기적인 요청을 기다리는 동안 다른 일들을 처리하는 것이 가능하다. 심지어 또 다른 비동기 요청을 하는 것도 가능하다.

비동기 방식은 큐와 메시지가 분산 시스템에서 어떻게 이용되고 있는지를 잘 보여준다.

큐는 서비스의 정지와 장애에 대비하는 보호 장치를 제공한다. 일시적인 서버 장애 때문에 처리되지 않은 요청을 다시 보내는 굉장히 견고한 큐를 만드는 것은 꽤 쉬운 편이다. 그리고 서비스의 장애 상황을 직접 클라이언트에서 처리하게 하는 것 보다 큐를 사용하는 것이 QoS(quality-of-service) 측면에서 더 바람직하다.

규모가 큰 분산 시스템에서 서로 다른 부분들끼리 분산 통신을 하는데 있어 큐는 필수적인 것이라 할 수 있으며 큐를 구현하기 위한 다양한 방법들이 있다. 오픈소스 큐는 RabbitMQ, ActiveMQ, BeanstalkD 같은 것이 있다. 때에 따라서는 Zookeeper나 데이터 저장소인 Redis를 큐로 사용하기도 한다.

참고

Scalable Web Architecture And Distributed Systems

🕋 Backend — Previous
(1) 분산 시스템 설계 및 핵심 고려사항
Next — 🕋 Backend
멀티 모듈 프로젝트