다음은 웹 개발자를 위한 대규모 서비스를 지탱하는 기술을 읽고 정리한 내용입니다 🙌


[강의8] OS 캐시 구조

OS의 캐시 구조를 알고 애플리케이션 작성하기 - 페이지 캐시

  • OS는 메모리를 이용해서 캐시 구조를 갖추고 디스크 액세스를 줄인다.

Linux(x86) 페이징 구조

OS는 가장 메모리 구조를 가지고 있는데 논리적인 선형 어드레스를 물리적인 어드레스로 변환한다.

가상 메모리 구조

기본적인 OS 구조를 보면 OS에서 관리하고 있는 메모리 구조 있고, OS가 있으며 OS에서 돌아가는 프로세스가 존재한다. 프로세스에서 메모리가 필요한 경우 메모리에 직접 접근해서 주소를 가져오는 것이 아니라, OS를 통해서 비어있는 주소와 다른 주소를 반환한다.

왜 가상 주소를 반환할까?

개별 프로세스가 실제로 메모리의 어느 부분을 사용하는지 스스로 알고 있을 필요가 없고, 특정 번지에서 통일해서 시작하는 것으로 다루면 더 쉽기 때문이다.

  • 예) 유닉스에서 공유 라이브러리는 프로세스 내에서 지정된 주소로 할당이 되어 있는데 프로세스 내에서 이 특정 어드레스는 예약에 되어 있음. 따라서 시작주소가 다 다르면 메모리를 확보할 주소위치를 찾기가 어려움

어쨌든 OS 커널에서 메모리를 추상화해서 넘기고 있다 !!!

또한 OS에서 메모리를 확보할 때도 단일 바이트 만큼씩 액세스 하는 것이 아니라 4KB 정도의 블록을 확보해서 프로세스에 넘긴다. 블록 = 페이지 (즉, OS가 메모리를 확보하는 단위)

OS는 메모리 요청을 받을 때 필요한 만큼의 페이지를 확보해서 프로세스에 넘긴다.

Linux 페이지 캐시의 원리

OS는 확보한 페이지를 메모리상에 캐싱해둔다.

프로세스가 디스크에서 데이터를 읽어내는 과정

첫번째, 우선 디스크로부터 4KB 정도의 블록을 읽어냄

두번째, 우선 메모리에 해당 읽어낸 데이터를 위치시킴

  • 프로세스는 디스크에서 데이터를 직접 읽을 수는 없다 ! 프로세스가 액세스 할 수 있는 것은 가상 메모리 주소이기 때문이다.

세번째, OS는 메모리에 쓰인 블록의 해당 주소를 프로세스에 가상 주소로 변환해서 알려준다.

네번째, 프로세스는 해당 가상 주소로 메모리에 액세스 하게 된다.


이미지 출처: 대용량 서비스를 지탱하는 기술

페이지 캐시 등장 - 이후에 더이상 프로세스가 데이터가 필요하지 않더라도 메모리에 쓰인 블럭을 해제하지 않고 남겨둔다. 그럼 다음 프로세스가 같은 디스크에 액세스할 때 해당 페이지를 재사용한다.

페이지 캐시의 효과

리눅스의 페이지 캐시는 모든 I/O에 작용(예외를 제외하고)하여 디스크를 최초 읽은 이후 두번째 액세스부터 빨라진다. (OS를 계속 가동시켜두면 빨라진다. 재부팅 시 메모리에 캐시된 데이터는 없어진다.)

VFS

리눅스의 구조는 다음과 같다.

  • 최하위에 하드디스크를 조작하는 디바이스 드라이버 → 위에 여러 파일 시스템 (리눅스의 경우 ext3, ext2, ext4, 등등) → vfs (virtual file system)
  • 파일시스템은 다양한 함수를 갖추고 있는데 인터페이스를 통일하기 위해 있는 것이 vfs 이다.

VFS가 페이지 캐시 구조를 가지고 있다. → 어떤 파일 시스템을 이용하여 어떤 디스크를 읽어도 vfs를 통해서 동일한 구조로 캐싱이 된다.

한마디로, VFS의 역할은 파일시스템 추상화와 성능에 관여하는 페이지 캐시 부분이다.

리눅스는 페이지 단위로 디스크를 캐싱

왜 파일 캐시가 아니라 페이지 캐시일까 ?

만일 메모리에 남은 여유 메모리 공간이 1.5 GB이고 필요한 파일이 4GB 일 경우 문제가 발생한다.

OS는 파일(현재 4GB 단위) 기준으로 캐싱하는 것이 아니라 블록 단위(4KB 단위)만으로 캐싱한다. 특정 파일의 읽어낸 일부분만 캐싱한다.

  • 페이지 == 가상 메모리의 최소단위

LRU

만일 적은 여유분의 메모리에 4GB 파일을 모두 읽게 된다면 LRU(Least Recently Used) 방식으로 캐싱이 최신화 된다. 따라서 DB 서버도 계속 구동시키면 캐시가 최적화되어 I/O 부하가 내려간다.

어떻게 캐싱이 될까

리눅스는 파일을 i노드 번호라는 번호로 식별하고 어느 위치에서 시작하는 오프셋을 제공하여 두가지 정보를 함께 캐싱한다. 따라서 파일 전체가 아닌 일부분을 캐싱할 수 있다.

이 키가 너무 많으면 파일이 클 경우 데이터 찾는 것이 어렵다고 여겨질 수 있는 데이터 구조는 Radix Tree라는 구조로 탐색 속도가 떨어지지 않는다.

메모리가 비어있으면 캐싱

리눅스는 메모리가 비어있으면 모두 캐싱 → 프로세스에 메모리가 필요하면 오래돈 캐시를 버리고 메모리 확보

  • 메모리 상황 알아보기

    sar -r 명령어를 통해서 kbcached(kilo byte cached) 부분과 %memused 부분으로 확인할 수 있다. 주로 꽉찬 메모리를 확인할 수 있는데 이것은 문제가 아니다. 본래 리눅스는 가용 가능한 메모리에 조금씩 디스크를 모두 캐싱하고 추가 메모리가 필요하면 오래된 캐시를 파기한다.

메모리 늘려서 I/O 부하 줄이기

메모리보다 디스크에 저장된 용량이 적으면 디스크의 모든 파일이 메모리에 캐싱되어 디스크 액세스가 일어나지 않게 된다. 따라서 메모리가 늘어날 수록 I/O 부하가 줄어든다.

페이지 캐시는 투과적으로 작용

부팅 직후 파일을 그렇게 많이 읽지 않았을 때 그 이후 갑자기 큰 파일을 읽으면 해당 파일이 캐싱이 되기 때문에 갑자기 메모리 사용 용량이 높아진다.

sar 명령어로 os 지표 확인하기

  1. 과거 OS 데이터 확인하기 - sar -f {/var/log/sa/sa04} | head 명령어로 과거 데이터의 로그 파일을 확인하여 장애 발생 원인을 확인 할 수 있다.

    • 프로그램 교체 후 전후 비교를 위해서 위 sar 데이터를 활용할 수 있음
  2. 현재 데이터 확인하기 - sar 1 3

    • 1초 간격으로 3회 동안 OS 데이터를 확인
    • 지금 이 순간 시스템에서 일어나고 있는지 확인할 수 있음
  3. 멀티 코어일 경우 sar -p 옵션으로 CPU 별 데이터 확인 가능

  4. 디폴트 sar (sar -u에 해당함) -


    이미지 출처: 대용량 서비스를 지탱하는 기술

    • user → 사용자 모드에서 CPU가 소비된 시간 비율
    • nice → nice로 스캐줄링의 우선도 변경한 프로세스가 사용자모드에서 CPU를 소비한 비율
    • system → 시스템 모드에서 CPU가 소비된 시간 비율
    • iowait → CPU가 디스크 I/O대기 위해 Idle 상태로 소비한 시간 비율
    • steal → OS 가상화 이용시 다른 가상 CPU 계산으로 대기된 시간 비율
    • idle → CPU가 디스크 I/O 등으로 대기되지 않고 Idle 상대로 소비한 시간 비율 (프로세스가 실행하고 있지 않은 상태)
  5. sar -q - Load Average 확인

    • 실행큐에 쌓여있는 프로세스 수, 시스템상의 프로세스 사이즈, load average 참조 가능
    • 시간 흐름에 따른 값의 추이를 추척 가능
  6. sar -r - 메모리 사용 현황 확인

    • 시간 추이에 따른 메모리 정도, 용도 확인 가능
    • sar -W와 조합해 스왑 발생 시간대의 메모리 사용 상황 확인 가능
  7. sar -W- 스왑 발생상황 확인

    • pswpin/s → 1초 동안 스왑인 되고 있는 페이지 수
    • pswpout/s → 1초 동안 스왑아웃 되고 있는 페이지 수
    • 스왑이 발생하면 서버 전송량이 떨어진다. 만일 메모리 부족으로 잦은 스왑이 발생하고 있는지 확인할 수 있다.

[강의9] I/O 부하를 줄이는 방법

캐시를 전제로 한 I/O 줄이는 방법

I/O 대책의 기본은 캐시이다 !!

  • 첫번째 접근법 → 데이터 크기보다 물리 메모리 사이즈가 크다면 모두 캐싱할 수 있다.

대규모 데이터에 데이터 압축이 중요하다. 압축해서 저장할 경우 디스크 전부를 메모리에 캐싱해둘 수도 있다.

  • 두번째 접근법 → 경제적 비용과 밸런스 고려

점점 서버와 높은 용량의 메모리 가격이 내려가면서 압축 알고리즘에 지나친 에너지를 쏟을 필요가 없는 경우도 많다. 밸런스를 고려하는 것이 중요하다.

복수 서버로 확장 - 캐시로 해결 안되는 규모인 경우

현재 인프라의 구조가 프록시 ↔ WAS ↔ DB 인 경우에 다음과 같이 서버를 확장할 수 있다.

  1. WAS 서버를 늘린다.
    • CPU 부하를 낮추고 분산시키기 위해서이다.
    • 단순히 늘리면 된다.
  2. DB 서버를 늘린다.
    • 캐싱 용량을 늘리거나 효율을 높이고자 할 때 늘린다.
    • 하지만 I/O 분산에는 국소성을 고려해야하며 마냥 늘려서 좋은 것은 아니다.

대수만 늘려서 확정성 확보할 수 없다.

  • 캐시 용량이 부족해서 DB 서버 대수를 확보했지만 부족한 캐싱 용량의 상황까지 그대로 복제될 수 있다.
    • A 서버에서 조회하며 캐싱했는데 부족한 것이 B 서버에서도 동일하게 일어남
    • 어느정도 빨라질 수는 있겠지만 증설비용대비 성능향상은 좋지 않다.

+ I/O 부하 줄이기와 페이지 캐시

리눅스에서 sar 명령어로 메모리 상황을 확인했을 때 항상 메모리가 부족해보일수도 있다. 하지만 리눅스의 페이지 캐시 원리는 리눅스는 가능한 남아있는 메모리를 페이지 캐시로 활용한다 라는 것이다. 따라서 부팅 후 시간이 지날수록 sar의 kbmemfree 는 줄어들 수밖에 없다.

페이지 캐시에 의한 I/O 부하 경감 효과

  • 많은 데이터가 있는 상황에서 메모리를 증설하고 sar -P 로 확인해보면 %iowait 의 확연한 차이를 볼 수 있다.

  • sar -r 를 사용하면 커널이 캐시를 확보하고 있는 정도를 확인할 수 있다. 커널이 확보하고 있는 캐시용량과 어플리케이션에서 다루는 데이터의 용량을 비교하여 데이터량이 더 많을 경우 메모리 증설을 검토하여 디스크 액세스를 줄인다.

    vmstat을 사용하면 디스크 액세스 정도를 확인할 수 있다.

  • 메모리 증설이 어려운 경우 데이터 분할하여 각각 서버에 위치한다. → 캐시 올릴 데이터 비율이 올라가고 I/O 횟수가 줄어든다.

페이지 캐시는 한번의 read에서 시작된다.

  • 캐싱하지 못한 데이터는 직접 디스크에서 읽는다. 서버를 재부팅한 경우 메모리의 캐시는 초기화 되므로 모든 액세스에 I/O를 발생시킨다.
  • 대규모 DB 서버인 경우 모든 DB 액세스마다 디스크 I/O가 발생해서 DB가 lock에 걸리는 경우도 많다.
  • 따라서 필요한 경우 필요데이터를 전체 한번 전체적으로 읽어 다시 캐싱하는 방법도 필요하다.
  • I/O 바운드가 높은 서버인 경우 페이지 캐시가 최적화 되었는지 확인한다.

[강의10] 국소성을 살리는 분산

국소성을 고려한 분산이란

서버를 여러대 확장해서 캐시 용량을 늘리기 위해서는 국소성(locality)을 고려해서 분산시켜야한다.

  • DB의 경우 서비스 패턴과 처리방식에 따라 데이터 액세스 경향이 한쪽으로 치우친다.
    • 어떤 서비스 패턴이 엔트리 A에 많이 접근하고 다른 서비스는 테이블B에 많이 접근한다면 1, 2를 분산하여 한쪽에만 액세스 하도록 할 수 있다.
    • 이것을 고려하지 않으면 여전히 서버 1에서 두 패턴이 모두 일어나게 되므로 캐시를 위한 메모리 용량이 부족하게 된다.
    • 즉, 액세스 패턴을 고려하여 국소성을 적용한 분산을 하라!!

파티셔닝 - 국소성 분산1

한 대였던 DB를 여러대의 서버로 분할하는 방법이다. → 제일 간단한 분할 방법은 테이블 단위 분할

  • 테이블 단위 분할 - 같이 액세스 하는 경우가 많은 테이블을 같은 서버에 위치시키고 그 밖의 것들을 다른 서버에 위치
  • 테이블 데이터 분할 - 하나의 테이블을 여러 테이블로 분할
    • 예를 들어 책에 나온 예시로는 하나의 테이블을 앞 알파벳에 따라서 데이터를 분할함
    • 국소성이 올라가 캐싱이 잘됨
    • 단점: 분할이 너무 작게 된다면 데이터를 한번 병합해야 함

요청 패턴을 ‘섬’으로 분할 - 국소성 분산2

요청의 종류에 따라서 요청을 보내는 서버를 나누는 것이다. - 특이한 경우이기는 함


이미지 출처: 대용량 서비스를 지탱하는 기술

  • 캐싱하기 쉬운 요청, 캐싱하기 어려운 요청을 처리하는 섬을 나눔 → 전자는 국소성으로 높은 캐시 적중률을 냄

페이지 캐시를 고려한 운용의 기본 원칙

  1. OS를 가동한 직후에 서버를 투입하지 않는다 → 캐시가 쌓여있지 않기 때문이다.
    • OS를 기동하고 자주 사용하는 DB의 파일을 한번 cat 하여 메모리에 올린다. 이후 로드밸런서에 편입한다.
  2. 성능 테스트를 할 경우 초기값은 버려야한다. → 최초 캐시가 최적화 되어 있지 않은 단계이므로 속도가 확연히 차이가 나게 된다.

+ 부하분산과 OS의 동작원리

  • OS 캐시, 멀티스레드나 멀티프로세스, 가상 메모리구조, 파일시스템 등과 같은 OS 지식이 있어야 부하분산을 잘 할 수 있음.
  • 요청 분배에는 LVS 사용법, MySQL 아파치와 같은 미들웨어 사용법 등이 있다.