들어가기 전에
Redis 2.0 버전을 기준으로 작성한 글입니다.
해당 버전의 코드는 다음 링크에서 확인하실 수 있습니다.
http://redisgate.kr/redis/introduction/redis_release2.php
해시 테이블 resize
해시 테이블을 resize 하는 과정에서 데이터를 옮겨야 하기 때문에 다른 요청을 수행할 수가 없다.
이를 해결하기 위해 2버전에선 db 당 해시 테이블을 2개로 만들어 관리한다.
0번 해시 테이블이 메인이지만, resize가 된 시점부터 1번 해시 테이블이 메인이 된다.
데이터를 모두 옮기면 1번 해시 테이블이 0번으로 변환된다.
이 작업은 cron을 통해 주기적으로 옮기거나 조회, 삽입, 수정, 삭제 과정에서도 점진적으로 옮겨 resize 과정에서 생기는 부담을 최소화한다.
이벤트 관리
기존엔 파일 이벤트를 하나로 관리했지만, 다음 이벤트들이 섞이는 문제가 있었던 것 같다.
- 처리를 준비할 파일 이벤트
- 처리해야 할 파일 이벤트
- 처리된 파일 이벤트
처리된 것과 처리해야 할 이벤트만 관리하는 fired 이벤트 리스트를 만들어 redis는 총 2개의 파일 이벤트를 관리한다.
특이점은, 읽기, 쓰기에 대한 마스킹이 없는 이벤트라도 epoll에 잡혔다면 처리한 프로세스로 판정한다.
이 fired 리스트에 타임 이벤트도 포함이 되나 싶었지만, 관련 코드에서 해당 리스트를 호출하는 코드는 없었다. 다만, 개발자가 여기에 대해 최적화가 필요하단 주석은 있었다.
공유 정수 도입
0 ~ 9999까지의 수를 사전해 등록해 메모리를 절약한다. 7.2 버전도 동일한 범위를 사용한다.
1버전의 공유 메모리 풀은, 입력되는 모든 값을 공유 메모리 풀에 등록하려 해 메모리 낭비가 있었다.
그래서 자주 사용되는 항목들만 공유 메모리 풀에 등록하도록 개선한 것 같다.
개인적인 생각이지만 9999까지 해놓은 이유는 다음 이유이지 않을까 싶다.
- 그 이상으로 가는 비중이 매우 적다.
- list, set, hash 등 다양한 자료구조를 사용하기 때문에 정수 폭을 넓히기엔 메모리 낭비가 있다.
이벤트 루프
select -> epoll
select 콜은 호출될 때마다 모든 소켓을 검사해, 처리할 준비가 됐는지 확인한다.
이 방법은 이미 준비된 파일 디스크립터도 다시 검사하게 돼, 대기 중인 소켓도 다시 검사하는 문제가 발생한다.
이를 해결하기 위해 epoll을 적용해 변동이 발생했을 때만 소켓을 확인할 수 있도록 개선했다.
알림 방식에 대해 찾아보니 다음 개념들이 있었다.
레벨 트리거: 특정 상태를 만족하면 알림이 발생한다. select 콜의 경우 레벨 트리거를 사용하기 때문에 준비된 소켓이라도 조건만 만족하면 계속 알림이 발생한다.
에지 트리거: 처음 상태를 확인할 때만 알림이 발생한다. epoll의 경우 이벤트만 확인 후, 다시 이벤트를 확인하지 않는다.
이 경우, 확인한 파일 디스크립터 변동을 감지하지 못하는 문제가 있어, epoll 은 레벨 트리거와 에지 트리거 방식을 지원한다.
파일 이벤트 처리 순서 명시
읽기 -> 쓰기 순으로 수행되며, 다음 상황을 만족하면 쓰기 관련 함수를 실행하지 않는다.
- 읽기 작업을 수행했고, 파일 이벤트에 명시된 읽기 함수와 쓰기 함수가 동일할 때(함수 포인터를 보고 비교)
파일이 읽기, 쓰기가 가능한 경우 두 파일 이벤트에 같은 함수가 들어가게 된다.
아래 그림에서 proc이 함수 포인터 매개변수다.
예를 들어 읽기, 쓰기 가능한 파일에 삭제 함수가 명시됐을 때
- 읽기 함수를 실행해 데이터를 삭제한다.
- 쓰기 함수를 실행해 데이터를 삭제한다. 하지만, 1번 작업에서 데이터를 삭제했기 때문에 수행되지 않는다.
그럼 반대로 해도 되지 않나? 란 생각이 들었지만, 반대의 경우도 읽기 과정에서 문제가 발생해 순서의 문제는 아니라고 생각했다.
가상 메모리 적용(2.6에서 없어짐)
redis가 메모리에 모든 데이터를 저장하도록 설계됐는데, 굳이? 란 생각도 들었다.
이후 버전들의 패치 목록을 찾아본 결과 2.6 버전부터 없어진 기능이 됐다.
잘 사용되지 않는 값들은 외부 공간을 활용해 메모리 사용을 최적화하려는 목적 외에 특별한 부분은 없었다.
zipmap 인코딩 적용(2.6에서 없어짐)
성능 문제로 2.6에서 제외되고 다른 인코딩 방식이 적용됐다.
간단히 설명하면 키, 값을 연속된 메모리 블록에 저장하는 방식이다.
이 방식의 문제가 키 탐색 비용이 O(N)이다. 해시 테이블로 찾는 게 아니라 순차적으로 탐색한다.
또한, 키, 값 쌍이 삭제될 때마다 메모리 블록을 재조정이 필요해 안정성이 떨어졌다.
2.6 버전 이후엔 이 문제들을 해결한 ziplist를 사용한다.
pub, sub 추가
채널 내 또는 특정 패턴을 만족하는 클라이언트에게 메시지를 발송할 수 있는 기능이 추가됐다.
- 발행자, 구독자: redis 클라이언트
- 브로커: redis 서버
redisServer, redisClient 둘 다 채널과 패턴에 대한 정보를 담고 있으며, 구조는 다음과 같다.
- pubsub_channels: (클라이언트 해시값, 클라이언트 리스트) 쌍을 가지는 딕셔너리로, 채널을 구독하는 클라이언트 정보들을 가지고 있다.
- pubsub_patterns: 클라이언트들이 관심 있어 하는 패턴 리스트. 문자열 리스트로 구성돼있다.
redisClient의 경우 패턴 객체를 그대로 관리하지만
redisSrerver는 패턴을 pubsubPattern 이란 객체로 한 번 더 추상화해서 관리한다.
2버전 redis 클라이언트가 다운되면 메시지가 모두 손실되는 문제가 있다.
이 문제를 해결하기 위해 5버전 대에서 Stream 이란 방법을 통해 로그 데이터로 복구하는 기능이 생겼다.
AOF(Append Only File) 추가
비정상적인 종료(크래시)를 대비해 데이터 셋 덤프를 남기는 기능이다.
쓰기 연산이 발생할 때마다 덤프 파일에 로그를 남겨, 재시작 할 때 덤프 파일을 읽어 환경을 재구성한다.
옵션을 보면, 메모리에 남겨뒀다 때가 되면 flush 하는 방식이다.
기본값은 No이며, Yes로 할 시, 다음 3가지 옵션 중 하나를 선택할 수 있다.
- always: 쓰기 연산이 발생할 때마다 바로 파일에 저장한다. 안정성 > 성능 이다.
- everysec: cron 작업이 수행될 때마다 파일에 저장한다. 권장 값이다.
- no: 운영체제가 알아서 파일에 저장한다. 안정성 < 성능이다.
no 방식에선 아마도 다음 기준으로 파일에 저장하지 않을까 싶다.
- 버퍼에 얼마나 찼는지
- 얼마나 기록이 안 되는지 (대규모 서비스에선 의미가 없을 것 같다.)
- 스케줄링 정책
- 명시적인 flush 요청을 받을 때
'개발' 카테고리의 다른 글
리눅스 환경에서 버전 관리 (0) | 2024.03.03 |
---|---|
서버에서 rm 실수를 줄일만한 방법 (0) | 2024.02.28 |
[java] Spring 애플 로그인 구현 과정 (0) | 2023.12.11 |
redis 1.0 분석 - 공유 메모리 풀 (0) | 2023.11.30 |
초기 Redis 분석 - 이벤트 루프 (0) | 2023.11.24 |