글을 시작하기에 앞서 해당 글은 이토 나오야, 다나카 신지의 저서인 『웹 개발자를 위한 대규모 서비스를 지탱하는 기술』 를 바탕으로 요약 및 정리한 글임을 알려드립니다.
OS 캐시를 사용하면 대규모의 데이터도 효율적으로 처리할 수 있다. 이번 글에서는 OS 캐시 작동원리에 대해서 간단하게 살펴보고 어떻게 구성해야 효율적인지 방법들을 이야기하고자 한다.
OS 캐시란
리눅스에서는 페이지 캐시라고 하는 캐시 구조를 가지고 있다. 이를 설명하기 위해서는 리눅스의 가상메모리 원리에 대해서 이해해야 한다.
가상 메모리란 하드웨어 물리 메모리 어드레스를 OS에서 추상화한 구조를 의미한다. 만약 K 프로세스가 A라는 파일을 읽어야 한다고 요청하면 OS는 A 파일을 메모리에 올린 후 해당 메모리 어드레스를 직접 프로세스에게 넘기는 것이 아니라 일단 커널 내에서 추상화하여 전달한다. 이렇게 하는 이유는 여러가지가 있지만 일단 페이지 캐시 또한 이유에 포함된다. OS는 K 프로세스가 A 파일을 다 읽어서 더이상 필요 없어도 물리 메모리 할당을 해제하지 않는다. 그렇게 메모리에 올려두면 또 다른 프로세스가 A 파일을 요청할 때 디스크 I/O 없이 바로 전달할 수 있기 때문이다. 이것이 OS캐시의 기본원리이다.
가상 메모리는 메모리 영역을 4KB 블록 단위로 관리하며, 이 블록을 페이지
라고 한다. 그렇기에 4GB 메모리를 가진 서버도 8GB의 파일을 일부 캐싱할 수 있다. 캐싱은 파일단위로 이루어지는 것이 아니며, 해당 파일의 i노드 번호와 위치를 나타내는 오프셋 값을 키로 캐싱하기 때문이다.
또한 리눅스는 비어있는 메모리에 모두 캐싱을 진행하며, 프로세스 요청으로 메모리가 필요한 경우에는 가장 오래된 캐싱을 제거(LRU)하여 메모리를 확보해준다.
캐시를 사용하여 I/O 부하 줄이는 법
그렇기에 OS 캐시를 사용하면 OS가 더이상 디스크로 접근할 필요성이 낮아지기에 I/O 부하는 줄어들 수 있다. 그래서 데이터가 많아졌을 때 부하를 줄이는 가장 빠른 방법은 메모리를 추가하여 OS캐시가 더 많이 일어나도록 환경을 만들어주는 것이다. 특히 서버에 존재하는 데이터를 모두 캐싱할 수 있다면 이는 모든 데이터를 메모리에 올려두고 사용하는 것과 동일하다. 따라서 데이터 파일들을 적절하게 압축하여 저장한다면 더 많은 데이터들을 캐싱하여 사용할 수 있을 것이다.
하지만 메모리를 추가하는 것은 여러가지 이유로 한계가 있다. 실제 물리 서버에 추가할 수 있는 메모리 슬롯이 제한되어 있다는 점, 서비스 규모가 커질수록 스케일 업이 어렵다는 점 등이다. 따라서 하나의 서버 캐싱으로 해결할 수 없는 경우에는 스케일 아웃을 진행해야 한다.
locality를 살리는 분산
스케일 아웃을 진행할 때는 물리적인 서버 수만 늘리는 것이 아니라 데이터에 대한 액세스 패턴을 고려해서 분산시켜야 한다. 그렇게 설계하면 특정 엑세스마다 필요한 데이터가 모든 서버에 캐싱된 형태로 실행될 수 있다. 이런 분산 처리를 locality 방법이라고 한다.
locality 방법은 크게 파티셔닝과 요청 패턴에 따른 분할 두 가지가 있다. 파티셔닝 중 대표적인 방법은 테이블에 따른 분할이다. 이는 엑세스가 같이 발생하는 테이블들을 묶어서 같은 DB 서버에 저장해두는 방식이다. 이렇게 분할하면 각 DB 서버마다 모든 데이터가 캐싱되면서 데이터 요청 시 하나의 DB 서버에서 처리할 수 있다.
그 외에도 테이블 데이터에 따른 파티셔닝도 있다. 이 방법은 특정 테이블 하나를 여러 개의 작은 테이블로 분할하여 각각의 DB 서버로 나눠 저장하는 것이다. 에를 들어 회원정보 테이블이라면 ID a~c 범위는 1번 DB 서버에 저장하고 d~f 범위는 2번 DB서버에 저장해서 엑세스 요청이 들어올 때마다 ID를 확인해서 해당하는 DB서버로 요청을 전달하는 방식이다.
마지막으로 볼 방법은 요청 패턴에 따른 분할이다. 보통 웹서비스를 운영하다보면 일단 사용자 요청, API 요청, 봇 크롤링 요청 등 다양한 트래픽을 볼 수 있다. 이런 트래픽에 따라서 DB서버를 분할하여 처리하는 방법이다. 봇 크롤링 같은 경우에는 특성 상 매우 오래된 페이지에도 엑세스하며, 범위도 광범위하다. 그래서 캐싱데이터를 관리하기 어렵지만 봇에게는 빠르게 응답 처리를 진행할 필요가 없다. 반면 사용자 요청의 경우에는 최신 업데이트된 페이지로의 엑세스 요청이 대부분이며 이런 경우에는 최신 데이터들을 캐싱해두면 된다. 이런 식으로 요청자 분류에 따라서 다르게 대처할 수 있도록 분할할 수 있다.