대규모 서비스를 지탱하는 기술
서론
'대규모 서비스를 지탱하는 기술'라는 책을 공부하고 정리해보는 포스트입니다.
1~5장, 6~15장을 묶어서 2개의 포스트로 나눠 작성할 예정입니다.
2011년에 나온 책이고 절판되었지만, 개발자들 사이에서 현재까지도 회자되는 책이어서 읽어보았습니다. (중고 도서로 가격이 아주 비싸고, 국회도서관 우편 복사 비용도 비쌉니다... 😦)
웹을 지탱하는 기술 + 대규모 서비스를 지탱하는 기술 책 2권을 우편복사 요청했는데 비용이 너무 비싸더군요...
✔️ 주의사항 → 앞서 언급한대로 2011년 즉, 10년이 넘은 책이라 다소 변경된 내용이 많아 틀린 내용이 있을 수 있습니다.
이 책의 목적을 제 마음대로 해석했을 때, 대규모 서비스로 성장하면서 경험했던 내용을 간접 경험하며 대규모 서비스에 대한 전체적인 그림과 근본적인 접근법
을 통해 통찰
을 얻기 위함이라고 생각합니다.
마치 스프링 MVC나 JPA같은 기술 자체를 배우기보다는 HTTP에서 겪은 문제 또는 RDB에서 겪은 문제를 살펴보며 어떻게 해결했는지 전체적인 그림을 이해하는 것과 같습니다.
마찬가지로 대규모 서비스에 필요한 여러 기술들 AWS, Redis, Kafka, ES, ...등의 학습보다는 서비스가 성장하면서 겪게 되는 스케일아웃, 캐시, 메시지 큐, 검색 알고리즘 등의 기반에 대한 근본적인 접근을 하고 있습니다.
이러한 생각의 과정, 기본 원리 등을 간접 경험하면서 대규모 서비스를 구축할 때 깨달음을 얻는 것이 목표라고 생각합니다.
1. 대규모 웹 서비스 개발
대규모 웹 서비스를 개발하는데 필요한 기초지식과 노하우를 학습하기
대규모의 정의
- 대규모 트래픽이 있을 수 있고, 대규모 데이터가 있을 수 있고, 둘 다 있을 수 있다.
- 트래픽이나 저장된 데이터의 용량이 일정 기준을 넘었냐가 기준이 아니다.
- 서버 1대로 원활한 서비스를 제공할 수 없어 스케일아웃(scale-out)이 필요한 경우를 대규모로 정의한다. (책이 아닌 필자 개인 의견)
- 그 이유는 웹 서버, DB, ... 등 여러 미들웨어를 포함하여 확장이 일어날 때 고려해야할 것들이 굉장히 많아지고 시행착오를 겪기 때문이다.
- 서버 1대로 원활한 서비스를 제공할 수 없어 스케일아웃(scale-out)이 필요한 경우를 대규모로 정의한다. (책이 아닌 필자 개인 의견)
스케일아웃을 가정하고 고민해볼만한 것
- 요청을 어떻게 여러 서버에 분산할 것인가?
- DB를 분산했을 때 데이터 동기화 문제처리는 어떻게 할 것인가?
- 분산 환경에서 주는 네트워크 지연시간(latency)은 어떻게 처리할까?
- 서비스간 트랜잭션 처리, 장애 복구는 어떻게 할까?
- 여러 대 운용은 어떻게 자동화할 것인가?
2. 대규모 데이터 처리 입문
대규모 데이터 처리의 어려움
- 메모리 내에서 계산할 수 없다.
- 계산을 위해서 메모리에 모든 데이터를 올려놓고 동작하다보면 가상메모리를 포함한 디스크를 사용하게 되고 I/O가 빈번해지면서 느려진다.
- 디스크 저장 위치 즉, 지역성을 이용하여 OS에서 1바이트씩 읽는 것이 아닌 벌크로 읽어주는 특성을 이용해야한다. (메모리와 디스크 속도차)
부하의 종류
- CPU 부하 - 동영상 인코딩, 대규모 데이터 통계와 같은 계산 등
- I/O 부하 - 대규모 쓰기 또는 전문 검색 등
부하의 병목 찾아내는 과정
- Load Average 확인
top
또는uptime
등의 명령어로 시스템 전체에 부하상황을 확인한다.- Load Average가 높지 않다? → 소프트웨어의 설정이나 네트워크, 원격 서버등에 문제가 없는지 확인한다.
- Load Average가 높다? → CPU인지 I/O인지 확인한다.
- CPU, I/O 중 어디에 병목인지 확인
sar
또는vmstat
으로 시간 경과에 따른 CPU 사용률, I/O 대기율의 추이를 확인한다.- CPU 사용률이 높다?
- 사용자 애플리케이션 프로세스의 문제인지, 시스템 프로세스가 문제인지 확인한다. (
top
,strace
,oprofile
) - CPU 부하가 높은데 디스크나 메모리 용량에 문제가 없다면 그냥 프로그램이 필요 이상으로 폭주하고 있는 것으로 봐야한다.
- 사용자 애플리케이션 프로세스의 문제인지, 시스템 프로세스가 문제인지 확인한다. (
- I/O 대기율이 높다?
- 극단적으로 메모리를 소비하는 프로세스가 있는지 확인한다.
- 프로그램 오류로 메모리를 사용하는 프로그램을 개선한다.
- 메모리 크기 자체가 모자라면 증가시킨다.
- CPU 사용률이 높다?
기본적으로 프로그램에 문제가 없는지 부하를 추적하면서 최적화해나가는 튜닝의 과정을 거쳐야 한다.
프로그래밍 요령
큰 틀에서 봤을 때 코드를 작성하면서 상시로 생각해야할 것 3가지가 있다.
- SSD까지 갈 필요없이 메모리 안에서 처리할 수 있도록 하기
- 데이터 증가에 강한 알고리즘 사용하기
- 데이터 압축, 검색 기술
프로그래밍 근간
OS가 디스크를 읽을 때도 블록 단위로 캐시하고, OS가 프로세스에 필요한 메모리의 값을 읽을 때도 페이지 단위로 캐시한다. 엄밀하게 분리해서 이해해야할 필요가 있다.
- OS 캐시
- 페이지 캐시 : 디스크로부터 벌크(페이지 블록 단위)로 읽어 메모리에 적재하고 프로세스에서 한 번 사용한 이후에도 메모리를 비우지 않고 갖고 있는다.
- 기본적으로 OS는 최대한 남아있는 메모리에 페이지를 캐시하고자 하기 때문에 메모리 점유율이 높아진다고해서 메모리가 얼마 남지 않았다! 라고 생각하면 안된다.
- 파일 캐시 : 메모리의 크기보다 큰 파일을 캐시하고자 할 때, 그 큰 파일의 일부분만 읽어내어 캐시한다.
- 원리 → 리눅스는
inode
(아이노드)라는 것으로 파일을 식별한다. 따라서 해당 파일의 어디 부분부터 시작할지를 나타내는offset
과inode
를 캐시하여 파일의 일부를 캐시하고 있을 수 있다.
- 원리 → 리눅스는
- 버퍼 캐시 : 파일시스테의 메타데이터와 관련된 데이터를 캐시하는 부분
- 페이지 캐시 : 디스크로부터 벌크(페이지 블록 단위)로 읽어 메모리에 적재하고 프로세스에서 한 번 사용한 이후에도 메모리를 비우지 않고 갖고 있는다.
- 분산을 고려한 RDBMS 운용
- 대규모 환경에 적절한 알고리즘, 데이터 구조
3. OS캐시와 분산
앞서 나온 내용대로라면 메모리양을 충분히 늘려주면 대부분 캐시될 것이고 성능 저하가 오지 않게 되는 것 같지만 꼭 그렇지는 않다.
캐시되지 않아서 대용량처리가 불가능한지 아닌지 정확한 분석이 필요하다.
부하
CPU 사용률과 I/O 대기율이라는 지표를 확인해서 부하가 얼마나 걸리는지 확인하면 좋다.
sar
리눅스 명령어를 통해 확인한다.
- %user = 사용자 모드에서의 CPU 사용률
- %system = 시스템 모드에서의 CPU 사용률
- %iowait = I/O 대기율
sar -q
: Load Average 확인
sar -r
: 메모리 사용현황 확인
sar -W
: 스왑 발생상황 확인
- pswpin/s : 1초 동안 스왑인된 페이지 수
- pswpout/s : 1초 동안 스왑아웃된 페이지 수
국소성, 지역성을 이용한 DB 파티셔닝
단일 서버에서는 메모리를 늘려서 어느정도 OS 캐시를 이용할 수 있도록 튜닝할 수 있지만 분산 서버 환경에서는 서버를 늘려도 메모리에 같은 내용이 캐시되므로 의미가 없어진다.
이러한 경우에 국소성, 지역성을 이용한 캐시를 해야한다. (= 서버 마다 메모리에 서로 다른 데이터를 캐시하고 있을 수 있도록 해야한다.)
메모리 캐시 용량을 늘리기 위해서 액세스 패턴에 따라 DB 서버 부하를 나눌 수 있다.
예를들어 A액세스 패턴은 주로 같이 조회되는 것들이 (게시판, 게시글, 댓글, 첨부파일) 데이터고, B액세스 패턴은 주로 같이 조회되는게 (상품, 카테고리, 회원) 데이터라면 (게시판, 게시글, 댓글, 첨부파일)테이블을 하나의 DB 서버에 놓고 A액세스 패턴으로 접근했을 때 로드밸런싱할 수 있도록 하는 것입니다.
마찬가지로 B액세스 패턴으로 접근하면 (상품, 카테고리, 회원) 데이터가 있는 다른 DB 서버로 로드밸런싱 하는 것입니다.
실질적으로 딱 특정 액세스 패턴에 맞게 데이터를 분리할 수는 없을 것이지만, 하나의 방법이라는 것을 인지해야합니다.
그래서 이와 같이 테이블 단위로 분할할 수도 있고 하나의 테이블에서도 데이터 단위로도 분할할 수 있습니다.
예를들어 사용자 아이디가 a~c , d~f, ... 이렇게 시작하는 사람들을 그룹으로 구분하여 파티셔닝 하는 것입니다.
4. 분산을 고려한 DB 운용
인덱스 올바르게 운용하기
DB에서 대용량 처리의 근본은 OS 캐시를 이용하는 것이므로 DB에서 관리하는 데이터의 크기와 물리적인 메모리의 크기를 잘 조절하는 것이 중요하다.
→ 물리 메모리의 크기보다 DB가 관리하는 데이터의 크기가 작은 것이 제일 좋다. 그러면 DB가 관리하는 모든 데이터를 메모리에 캐시가 가능하기 때문에 처리속도가 엄청나게 개선되기 때문이다.
create table, 스키마의 중요성
별 생각없이 DB 테이블의 컬럼하나 추가 생성하는 경우가 있는데, 이를 주의해야한다.
대규모의 대용량 서비스를 운영하다보면 레코드 즉, 데이터의 수가 엄청나게 많아지기 때문이다.
주문 테이블에 레코드가 3억 건이 있다고 가정하고 주문 테이블에 8바이트짜리 내용이 담길 컬럼 하나를 추가하면 3억 * 8바이트 = 약 3GB가 증가되기 때문이다.
그러면 DB가 관리할 데이터의 크기가 3GB가 증가했고 이에 대해 OS 캐시를 적용하기 위해서 물리 메모리도 3GB 증가시켜야할 수도 있다.
즉, 컬럼 몇 개만 증가해도 관리해야할 데이터의 크기가 증가하면서 대용량 처리에 부담이 갈 수 있다!
정규화에 대한 의문
컬럼 분리, 최소화 즉, DB정규화를 하는 것이 OS캐시를 활용하기 위해서 최적인가?에 대한 의문이 있을 수 있다.
이에 대한 해답은 역시나 실무적으로 비즈니에서 따라 트레이드오프(trade-off)를 생각해야한다는 것이다.
정규화를 하면 DB가 관리하는 데이터의 크기가 적어지므로 OS 캐시 활용하기엔 좋지만 테이블이 분리되면서 데이터를 조회할 때 조인(join)을 해야하는 등 쿼리가 복잡해서 오히려 성능이 떨어질 수 있기 때문이다.
실질적으로 조회 성능을 테스트해보고 적절하게 정규화를 진행할지 말지를 정하는 게 좋다.
DB 인덱스의 문제점
DB컬럼에 대해 인덱스를 만들어 놓으면 일반적으로 B+트리 알고리즘에 의해서 조회속도가 엄청나게 개선될 수 있다.
DB 인덱스(색인)의 문제점은 사실 복합인덱스에서 나온다.
실무적으로 where 조건에 하나의 컬럼만 들어가지 않는다. 조건에 다중 컬럼이 들어가기 마련인데 이럴 때 문제가 발생한다.
우선 MySQL 같은 경우 한 번의 쿼리에 하나의 인덱스만 사용하는 특징이 있다.
select * from member where name like '김%' order by timestamp
따라서 위와 같은 쿼리를 날릴 때, name과 timestamp에 각각 인덱스를 설정했다하더라도 어느 한 쪽의 인덱스만 이용하게 된다.
이러한 경우 복합인덱스로 (name, timestamp) 쌍으로 설정해두면 되지만... 순서가 또 중요한 등의 여러 문제가 있다.
만들어놓은 인덱스는 WHERE
, JOIN
, ORDER BY
순서로 적용
MySQL에서 인덱스를 타고 있는지 확인하는 방법은 쿼리 앞에 explain
이라는 키워드를 넣고 실행하는 것이다. EXPLAIN FORMAT=JSON
도 가능하다.
확장을 전제로한 시스템 설계
마스터(Master) - 슬레이브(Slave) 구조를 갖는 레플리케이션(replication)기능을 지원한다.
마스터-슬레이브 구조는 마스터의 데이터를 지속적으로 폴링하면서 동일한 데이터로 동기화 및 복사하여 슬레이브가 갖게되는 구조다.
조회쿼리는 슬레이브로 가게하고 데이터를 갱신하는 insert, update, delete, ...와 같은 쿼리는 마스터로만 가게하는 방법으로 분산 처리할 수 있다.
아름다워보이지만, 이 구성에 단점이 있다.
슬레이브는 확장할 수 있지만 마스터를 확장할 수 없는 것이다.
대부분의 서비스는 통상적으로 쓰기보다는 조회가 훨씬 많기 마련이지만, 쓰기가 많은 애플리케이션을 개발하는 경우에는 또 생각해볼게 많다. 우선은 테이블을 나눠주는 것을 생각해볼 수 있다는 것과 꼭 RDB를 이용해야하는 게 아니라면 다른 방법으로 우회하는 것도 방법이라는 것만 알아둔다.
파티셔닝을 전제로한 시스템 설계
테이블간의 조인이 있는 A, B테이블에 대해서 파티셔닝을 하면 안된다. (정확히는 지양해야한다.)
이유는 MySQL에서 기본적으로 서로 다른 DB서버에 있는 테이블간의 조인을 지원하지 않기 때문이다. (최신 MySQL 버전(5.1이상)에서는 가능하다.)
JOIN 배제 - where ... in ... 쿼리 이용
SELECT url
FROM entry
INNER JOIN bookmark on entry.eid = bookmark.eid
WHERE bookmark.uid = 169848 limit 5;
위와 같이 조인을 하는 방법을 사용하지 않고 아래와 같이 쿼리를 두 개로 나눠서 in 쿼리를 사용해도 같은 결과를 얻을 수 있다.
SELECT eid
FROM bookmark
WHERE uid = 169848 limit 5;
결과 : 0,4,5,6,7
SELECT url
FROM entry
WHERE eid in (0,4,5,6,7);
파티셔닝의 트레이드오프
파티셔닝의 장점으로 부하를 분산하고 OS 캐시 적중률을 높일 수 있는 것이 있었다.
역시나 장점만 있는게 아니다.
- 단점
- 운용이 복잡하다.
- 어떤 데이터가 어느 DB서버에서 있는지 파악하기 어렵다.
- 고장으로 인한 장애확률이 높다.
- 슬레이브 서버가 적으면 장애 복구시 새로운 서버에 데이터를 복사하는 등의 작업을 거치면서 서비스가 불가능해진다. 이는 곧 서비스를 못하는 장애로 이어진다.
- 운용이 복잡하다.
5. 대규모 데이터 처리
전문 검색 같은 처리를 하기에는 RDB에서는 한계가 있다.
따라서 이와 같은 경우에는 검색을 위한 미들웨어(아파치 루씬 기반의 엘라스틱서치 같은 것?)를 쓰는게 더 바람직하다.
RDB에 있는 데이터를 배치, 스케줄링을 통해 주기적으로 복사하고 역색인을 만들어서 검색을 하는 방법이다.
찾고자 하는 키워드가 포함되어있냐 아니냐만 중요한 것이 아니다.
1~5장 정리
대규모 서비스에서 원활하게 성능을 보여주기 위해 고려해야 하는 것들을 가볍게 알아보았다.
대규모 트래픽을 분산해주기 위해서는 로드밸런서가 있어야 한다.
데이터를 제공하는 입장에서는 대규모 트래픽에 대한 대응으로 "캐시"가 가장 효과적인 대책이다.
I/O 대책에 대한 "기반"은 "OS"에 있다.
OS 캐시 즉 메모리를 충분히 가지고 있어야하며 한 대의 서버에서 해결이 불가능한 규모라면 분산 서버 환경을 고려해야하고 국지성을 이용해서 되도록 많은 데이터들이 메모리에 캐시되도록 해야한다.
뿐만 아니라 한정된 메모리에서 최대한 오래 유의미한 캐시가 될 수 있도록 데이터를 작게 관리하면 좋다.
'기타 개발 스킬' 카테고리의 다른 글
나는 어떤 응답을 만들었는가(부제 : 그놈의 '기초', '기본'은 무엇인가) (26) | 2021.06.01 |
---|---|
대규모 서비스를 지탱하는 기술 6~15장 (서버와 인프라 구축) (2) | 2021.05.16 |
2020년 어느 개발자의 회고🚩(공부에 멋이 밴놈? 보자 보자 어디보자 넌 다음에 보자) (8) | 2021.01.11 |
개발자 커뮤니티 활동의 중요성(with KSUG 페이스북 커뮤니티 1만명 가입 이벤트 당첨 후기) (12) | 2020.08.10 |
윈도우에서 NTP Server 구축, NTP Client 설정하는 방법, 배치 파일 스크립트 작성하기 (폐쇄망에서 시간 동기화하는 요구사항 처리하기) (29) | 2019.12.04 |