하루 10억건 처리 서비스 만들기 - 병목과 Agingtest
하루 100억건 정도의 메세지가 한국에서 인기 있는 메신저의 트래픽 규모라고 합니다. 우리가 하루 10억건 정도의 요청을 처리하려면 무엇이 필요할까요. 하나씩 알아보고 10억 이상의 규모를 처리하는 시스템을 만들어봅시다.
앞에 있는 낮은 수준의 구성요소가 최대 용량을 결정
시스템의 최대 처리 용량은 앞에 있는 낮은 수준의 구성요소가 최대 용량을 결정합니다. 매우 간단한 아키텍처 하나를 보면 이야기합시다.
클라이언트 - 웹서버 - DB 실제로는 보통 클라이언트와 웹서버 사이에 로드 밸런서를 두지만, 설명을 위해 간략화 했습니다. 이 구조에서 요청자는 클라이언트고 우리가 제공하것중 가장 앞에 있는것은 웹서버입니다. 웹서버의 처리 용량이 하루 100만이고 DB의 처리 용량이 하루 10억이라면 이 시스템의 최대 처리 용량은 100만이 됩니다. 시스템 구성에서 앞에 있는 요소를 통해 일을 전달 받으므로 DB는 어쩔 수 없이 100만의 일만하게 됩니다.
클라이언트 - 로드밸런서 - 웹서버 - DB 가장 많이쓰는 전형적인 아키텍처입니다. 로드 밸런서의 처리 용량이 10만, 웹서버의 처리 용량이 100만, DB의 처리 용량이 10억이라면 이 시스템의 최대 처리 용량은 얼마일까요. 답은 10만입니다. 제공하는 가장 앞에 있는 시스템이 로드 밸런서이고 가장 용량이 작은 용량은 10만이기 때문입니다.
연습 문제 하나를 풀어 봅시다. 로드 밸런서의 처리 용량이 10억, 웹서버의 처리 용량이 100만, DB의 처리 용량이 10억이라면 이 시스템의 최대 처리 용량은 얼마일까요. 정답은 100만입니다. 로드 밸런서는 10억을 처리할 수 있으나 웹서버의 처리 용량이 100만이므로 웹서버가 처리할때까지 기다려야합니다. 그렇다면 처리 용량을 올리기 위해 어떤 부분을 바꿔야할까요. 네 웹서버의 처리 용량을 올려야합니다. 앞에 있는 낮은 수준의 구성요소가 웹서버이므로 다른 구성요소의 성능을 올려봤자 처리 용량이 늘지 않기 때문에 웹서버의 처리 용량을 올려야합니다. 이 과정을 병목점 분석이라고 합니다.
처리 용량을 올리기 위해서는 앞에 있는 낮은 수준의 구성요소의 성능을 올려야합니다. “앞에 있는 낮은 수준의 구성요소”가 병목점입니다.
병목 확인
우리가 사용하는 로드밸런서(하드웨어/소프트웨어)는 매우 처리량이 높기 때문에 대부분 로드밸런서 뒤에서 병목이 생깁니다. 특히 AWS나 Azure와 같은 오픈 클라우드를 쓰는 경우 로드밸런서의 경우 규모가 자동 조절되는 하나의 인프라로 관리되기 때문에 로드밸런서의 처리 용량을 걱정할필요가 없습니다. 직접 HAproxy와 같은 소프트웨어 또는 전용 하드웨어로 로드밸런서를 구성했을 경우에는 반드시 처리 용량을 확인해야합니다. 병목 확인 방법은 하드웨어 모니터링 도구로 할 수 있습니다. 일반적으로 모니터링 도구들은 CPU, 메모리, 디스크, 파일 오픈수등의 정보를 알려줍니다. 확인해야할 순서는 다음과 같습니다. 아래 요소들은 최소한 반드시 일정 기간 동안 하드웨어의 점유율과 로직의 작동 성능을 체크하는 Aging Test를 해야합니다.
- CPU
- 메모리
- 파일 오픈수
- 디스크
CPU의 사용률이 100% 보다 낮다는것은 일을 더 할 수 있다는것을 뜻합니다. CPU가 100%를 초과하면 일을 모두 처리 못하고 쌓아놓게됩니다. 과거에는 CPU 점유율을 반드시 40% 미만 혹은 60% 미만으로 유지하는것을 가이드라인으로 정하기도 했지만 현대의 운영에서는 구성요소가 하는 작업의 성격마다 다른 수치를 설정합니다. 가상환경에서는 같은 물리적인 CPU를 사용하는 다른 논리적인 어플리케이션 때문에 영향을 받기도합니다. 따라서 가끔 점유율이 오르는 상황은 너무 크게 신경쓸 필요가 없습니다. 우리가 확인해야하는 경우는 지속적으로 점유율이 높아지거나 낮은경우입니다. 높아지는 경우에는 하드웨어가 할 수 있는 일보다 시키는 일이 많아서 병목이 된다는뜻이고 낮아지는 경우는 앞에 있는 병목 때문에 하드웨어를 충분히 안쓰고 놀고 있다는뜻입니다. 따라서 낮은것도 좋은것이 아닙니다.
기본적으로 우리의 로직은 메모리에서 처리됩니다. 메모리의 용량이 부족하면 일부는 디스크를 사용합니다. 디스크의 경우 메모리 보다 접근 성능이 낮으므로메모리가 부족하여 디스크를 사용하게 되면 CPU의 사용률이 100% 보다 낮아도 처리용량이 떨어집니다. 메모리의 경우 메모리가 정말 부족하기 보다는 우리가 만든 코드가 조금씩 메모리를 쓰고 반환을 안하는 Memory Leak 때문에 문제가 생기는 경우가 많습니다. 따라서 이러한 시스템들은 재시작후 시간이 지날수록 가용한 메모리량이 줄어들다가 작동 불능상태가 됩니다. 따라서 대용량 처리를 하는 시스템은 반드시 Aging Test가 필요합니다. 새로 개발된 로직, 서비스등을 24시간, 48시간 등의 시간동안 메모리가 지속적으로 줄지 않는지 확인해야합니다. 이 테스트가 중요한 이유는 Memory Leak 발생하는 부분이 2개 이상인 경우 1개였을때 보다 매우 크게 찾기가 어렵기 때문에 문제 발생 즉시 찾아야합니다. 메모리의 경우 우리의 코드 뿐만아니라 운영체제도 사용하므로 반드시 우리의 코드가 사용하는 용량 외에 일정량의 메모리 공간을 확보해야합니다. 이는 서비스 마다 다르며 해당 서비스의 가이드라인에 잘 나와 있습니다. 예를들어 아파치 스파크 워크노드는 최소 1GB의 메모리를 운영체제용으로 남기고 톰캣과 같은 동기 웹서버는 메모리의 절반을 VertX 비동기 웹서버는 512MB를 남깁니다. 디스크의 형태도 병목이 될수 있습니다. 일반적으로 디스크는 SSD, HDD, Tape로 나뉩니다. 주로 SSD와 HDD를 쓰는데 SSD와 HDD의 IO 성능이 다릅니다. 데이터 검색용이라면 SSD가 데이터 저장용이라면 HDD가 적합합니다.
리눅스와 같은 운영체제는 모든것을 파일로 다룹니다. 네트워크 연결도 하나의 파일을 읽고 쓰는것으로 다룹니다. 이 때문에 최대 연결갯수가 최대 파일 오픈수에 의해 결정됩니다. 따라서 CPU, 메모리가 충분해도 파일 오픈수가 낮다면 다수의 연결을 다룰 수 없습니다. 이 요소는 CPU와 메모리와 달리 운영체제 마다 최대 설정이 있으므로 분산처리 하는 방법 외에는 방법이 없습니다. 대부분 클라우드를 구성하는 기본 이미지는 이 요소를 최대로 셋팅한 상태이므로 낮은 파일 오픈수 때문에 병목이 생기는 경우는 작습니다. 대부분의 경우 우리의 코드나 클라이언트의 코드에서 비정상적으로 연결을 안끊고 유지하기 때문에 결국 최대 오픈수를 넘어서 네트워크연결을 추가할 수 없게 됩니다. 이 경우 네트워크에서 분리후 다시 연결하던가 해당 시스템을 재시작해야합니다.
디스크의 경우 중요로그는 네트워크로 전달 전에 시스템이 셧다운 되는것을 막기 위해서 서비스가 실행되는 로컬에 저장하는데 대용량 서비스의 경우 이 로그가 쌓이는 속도가 빠르기 때문에 Shell Script나 LogStash와 같은 로그 통합툴을 이용해서 로컬에 있는 로그를 삭제하고 특정위치로 옮기는 관리가 필요합니다. 대부분의 운영체제는 운영체제가 사용하는 최소한의 여유 공간까지만 디스크 용량 사용을 허용하고 그 이상을 쓰게 되면 디스크를 쓸수 없으므로 서비스가 중단됩니다. 메모리가 충분해도 서비스에서 장애 발생시 극복하기 위해 임시파일을 IO하는 경우가 많습니다. 따라서 임시파일을 IO못하게 되므로 서비스는 중단됩니다.
위에 열거한 내용은 최소한의 모니터링 요소이고 서비스의 성격에 따라서 추가로 모니터링 해야합니다. 예를 들어 DB의 경우 Insert와 Select 횟수/처리속도, Kafka와 같은 메세지큐의 경우 Produce/Consume 비율을 확인해야합니다. 일반적인 모니터링 도구들은 이런 서비스 요소들을 모니터링 할 수 없으므로 대부분의 대용량 서비스 회사들은 자체 모니터링 도구를 개발하고 운영합니다.
실제 같은 Aging Test
대부분의 회사들은 실 운영을 하는 Live 서버와 개발용 Test 서버를 분리합니다. Aging Test도 Test 서버 환경에서 합니다. 하지만 Test서버와 Live서버는 코드는 같아도 트래픽이 들어오는 패턴, 백업등의 영향을 주는 프로세스등이 다르기 때문에 다른 결과가 나오기도 합니다. 작은 서비스 규모에서는 무시할 수 있는 정도이지만 대용량 서비스에서는 작은 차이가 여러번 중첩되어 큰차이를 만들기도합니다. 이 때문에 Test 서버 테스트후 Live 서버에서 테스트가 필요합니다. 오해가 없기를 바랍니다, Test 서버의 테스트 대신에 Live 서버 테스트를 하는것이 아니라 추가로 Live 테스트를 하는것입니다.
Load 나누기
구버전 시스템과 신버전 시스템을 동시에 라이브 서비스에 배포하고 구버전에서 0.9, 신버전에서 0.1 비율로 처리합니다. 로드밸런서를 통해 쉽게 설정할 수 있습니다. 특정 시간 단위로 신버전에서 처리하는 비율을 높입니다. 예를들어 24시간이 지나면 신버전에서 0.2 처리, 48시간이면 0.3처리 … 최종적으로 시번전으로 모든 로드를 보냅니다. 이 방법을 실 서비스를 통해 검증할 수 있는 장점이 있으나 Live Aging Test 방법 중 안전성이 떨어집니다.
요청 복제하기
요청이 들어오면 해당 요청을 테스트용으로 변환해서 다시 요청합니다. 예를 들어 오픈마켓의 A스토어에서 a 상품을 실사용자가 구매했다면, 가상 B 스토어에서 b 상품을 가상사용자가 구매한것처럼 요청을 복제합니다. 실 구매는 구 버전 시스템에서 처리하고 가상 구매는 신 버전 시스템에서 처리합니다. 안전성이 확인되면 신 버전으로 업데이트 합니다. 안전성의 뛰어나고 가상으로 만든 요청도 쉽게 삭제할 수 있으나 실제 요청이 아니므로 정확한 검증이 어렵고 가상 요청을 만드는 비용도 발생합니다.
Load 나누기와 요청 복제하기는 장단점이 분명한 방법으로 모두 사용합니다. 안전성이 중요한 결제등의 서비스 검증시에는 “요청 복제하기”를 그외의 경우에는 “Load 나누기”를 사용합니다.
참고자료
리눅스 파일 오픈 설정, https://medium.com/hbsmith/too-many-open-files-%EC%97%90%EB%9F%AC-%EB%8C%80%EC%9D%91%EB%B2%95-9b388aea4d4e
리눅스 디스크문제 해결, http://amazingguni.github.io/blog/2016/05/devops-chapter-4-disk
마이크로서비스 아키텍처에 대해 궁금한점이 있다면 포스트 하단의 링크(페이스북, 이메일)를 통해 문의주세요.