클라이언트 핸들링
- 일반적으로 TCP 포트를 사용한다.
- 설정 파일을 통해 매개변수를 설정한다.
unixsocket /tmp/redis.sock
unixsocketperm 777
매개변수를 설정하면 원하는 경로에 소켓 파일을 생성하고 해당 파일의 권한을 지정할 수 있다.
$ redis-cli -s /tmp/redis.sock
유닉스 소켓 파일의 경로를 사용해 레디스 서버에 연결한다.
레디스는 멀티플렉싱(Multiplexing) 방식을 사용한다.
- 하나의 통신 채널을 통해 여러 데이터 스트림을 전송할 수 있다.
- 다중 클라이언트 지원을 가능하게 한다.
- 클라이언트 요청을 비동기적으로 처리한다.
client *createClient(connection *conn) {
client *c = zmalloc(sizeof(client));
if (conn) {
connEnableTcpNoDeplay(conn);
if (server.tcpkeepalive)
connKeepAlive(conn, server.tcpkeepalive);
connSetReadHandler(conn, readQueryFromClient);
connSetPrivateData(conn, c);
}
(redis/src/networking.c 파일)
레디스는 클라이언트 커넥션을 생성할 때 TCP_NODELAY 옵션을 사용하여, 지연 없이 가능한 한 빨리 패킷으로 전송하려 시도한다.
클라이언트 버퍼 제한
- 레디스는 각 클라이언트마다 클라이언트 출력 버퍼(client output buffer)를 생성한다.
- 클라리언트에 반환할 데이터를 임시로 저장하기 위함
- 출력 버퍼 크기에 대한 제한을 둬서, 버퍼 크기가 일정 수준 이상으로 증가할 경우 클라이언트 연결을 종료한다.
- 하드 제한: 고정된 제한값, 임계치에 도달하면 클라이언트 연결을 가능한 한 빨리 닫는다.
- 소프트 제한: 시간에 따라 다름, 일정 시간동안 일정 크기보다 큰 출력 버퍼를 유지할 경우 연결을 닫는다.
- 일반 클라이언트는 출력 버퍼 크기 제한이 0으로 설정되어 있다.
- pub/sub 클라이언트: 하드 제한 32MB, 소프트 제한 60초당 8MB
- 복제본을 위한 출력 버퍼 크기 제한: 하드 제한 256MB, 소프트 제한 60초당 64MB
> CONFIG SET client-output-buffer-limit <class> <hard-limit> <soft-limit> <soft-limit-duration>
CONFIG SET으로 설정값을 변경할 수 있다.
- <class>: normal, slave(replica), pubsub 중 하나
- normal: 일반 레디스
- slave(replica): 레디스 복제본 클라이언트
- pubsub: pub/sub 클라이언트
- <hard-limit>: 하드 제한 값
- <soft-limit>: 소프트 제한 값
- <soft-limit-duration>: 소프트 제한이 적용되는 시간 간격
클라이언트 쿼리 버퍼는 클라이언트에서 받은 커맨드를 레디스에서 잠시 보관하는 내부 버퍼의 역할을 한다.
- client-query-buffer-limit 설정을 변경해서 이용할 수 있다.
클라이언트 이빅션
서버는 가장 많은 메모리를 사용하는 연결부터 해제하려고 시도하는데, 이 기능을 클라이언트 이빅션(client eviction)이라고 한다.
- maxmemory-clients 설정값은 레디스에 연결된 모든 클라이언트의 최대 총메모리 사용량을 정의한다.
- redis.conf에서 설정하거나, CONFIG SET 커맨드를 이용해 변경할 수 있다.
maxmemory-clients 1G
maxmemory-clients 5%
바이트 단위의 특정 크기를 직접 정의하거나, 퍼센트 기호를 사용해서 정의할 수 있다.
Timeout과 TCP Keepalive
- 특정 시점에서 활동이 없는 클라이언트를 정리하려면 타임아웃 설정을 사용해 유휴 연결을 해제할 수 있다.
- tcp-keepalive는 연결된 클라이언트에게 주기적으로 TCP ACK을 보내고, 클라이언트로부터 응답이 없는 경우에 연결을 끊는 설정이다.
> CONFIG SET timeout 600
OK
타임아웃 파라미터를 설정하면 클라이언트 소프트웨어의 버그로 인해 레디스에 유휴 연결이 쌓여서 서비스 장애가 발생하는 상황을 방지할 수 있다.
파이프라이닝
파이프라이닝: 클라이언트가 연속적으로 여러 개의 커맨드를 레디스 서버에 보낼 수 있도록 하는 기능
- 왕복 시간을 줄일 수 있다.
- 레디스 서버의 처리량을 향상시킬 수 있다.
- 주의할 점
- 명령을 일정한 개수로 나누어 배치(batch) 형태로 서버에 보내는 것이 좋다.
- 배치 사이즈는 충분한 테스트를 통해 결정하는 것이 좋다.
$ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG
레디스 서버에 줄바꿈을 이용해 동시에 실행할 여러 개의 커맨드를 한 번에 보내면 된다.
테스트 #1
레디스에 접속해서 100만 개의 키를 가져온 뒤, 타입과 메모리 사용량을 파악하는 코드
import redis
# Redis 서버에 연결
r = redis.Redis(host='레디스 주소', port=6379, db=0)
keys = [] # 키 목록을 저장할 리스트
type_counts = {} # 각 데이터 타입의 횟수를 추적하는 딕셔너리
total_memory_usage = 0 # 모든 키의 메모리 사용량 합계를 저장하는 변수
cursor = 1 # SCAN 명령을 사용해 키를 검색하기 위한 커서
# 모든 키 스캔
while cursor != 0:
cursor, partial_keys = r.scan(cursor=cursor, count=10000)
keys.extend(partial_keys)
for key in kyes:
# 키의 데이터 타입 조회
key_type = r.type(key).decode('utf-8')
# 키의 메모리 사용량 조회
memory_usage = r.memory_usage(key)
total_memory_usage += memory_usage
if key_type in type_counts:
type_counts[key_type] += 1
else:
type_counts[key_type] = 1
for key_type, count int type_counts.items():
print(f'Type: {key_type}, Count: {count}')
average_memory_usage = total_memory_usage / len(keys) if len(keys) > 0 else 0
print(f'Average Memory Usage: {average_memory_usage} bytes')
Type: 'string', Count: 6197600
Type: 'set', Count: 36922
Type: 'zset', Count: 48
Type: 'hash', Count: 1
Type: 'list', Count: 1
Average Memory Usage: 754.1511653085408
real 265m2.968s
user 15m12.969s
sys 6m36.587s
1200만 개의 키가 있는 레디스에서 수행했을 때 4시간 20분이 걸렸다.
테스트 #2
파이파라인을 이용하는 방식으로 변경
import redis
# Redis 서버에 연결
r = redis.Redis(host='레디스 주소', port=6379, db=0)
keys = [] # 키 목록을 저장할 리스트
type_counts = {} # 각 데이터 타입의 횟수를 추적하는 딕셔너리
total_memory_usage = 0 # 모든 키의 메모리 사용량 합계를 저장하는 변수
cursor = 1 # SCAN 명령을 사용해 키를 검색하기 위한 커서
# 모든 키 스캔
while cursor != 0:
cursor, partial_keys = r.scan(cursor=cursor, count=10000)
keys.extend(partial_keys)
pipeline = r.pipeline() # 파이프라인 생성
for i, key in enumerate(keys):
pipeline.type(key) # 데이터 타입 조회
pipeline.memory_usage(key) # 메모리 사용량 조회
# 매 batch_size번째 키나 마지막 키에 대해 파이프라인 실행
if (i+1) % batch_size == 0 or i == len(keys) - 1:
responses = pipeline.execute() # 파이프라인 실행
for j in range(0, len(responses), 2):
key_type = responses[j]
memory_usage = responses[j+1]
total_memory_usage += memory_usage
if key_type in type_counts:
type_counts[key_type] += 1
else:
type_counts[key_type] = 1
for key_type, count int type_counts.items():
print(f'Type: {key_type}, Count: {count}')
average_memory_usage = total_memory_usage / len(keys) if len(keys) > 0 else 0
print(f'Average Memory Usage: {average_memory_usage} bytes')
Type: 'string', Count: 6197600
Type: 'set', Count: 36922
Type: 'zset', Count: 48
Type: 'hash', Count: 1
Type: 'list', Count: 1
Average Memory Usage: 754.1511653085408
real 3m33.937s
user 3m25.115s
sys 0m1.669s
3분 33초만에 같은 작업을 완료했다. (약 142배 성능 향상)
클라이언트 사이드 캐싱
클라이언트 측에서 데이터를 로컬에 캐싱하고 필요할 때 해당 데이터를 반환한다.
데이터의 정합성(업데이트된 데이터)를 처리하는 방법을 고려해야 한다. (트래킹)
- 기본 모드: 레디스 서버가 클라이언트가 액세스한 키를 기억해서, 동일한 키가 수정될 때 무효 메시지를 전송한다.
- 👎 레디스의 서버에서 이를 기억해야 하기 때문에 메모리 비용이 든다.
- 👍 정확하게 클라이언트가 갖고 있는 키에 대해서만 무효한 메시지를 보낼 수 있다.
- 브로드캐스팅 모드: 레디스 서버가 특정 프리픽스에 대해 접근한 클라이언트만 기억한다.
- 👍 기본 모드보다 레디스 서버에서 사용하는 메모리가 적다.
- 👎 특정 프리픽스를 가진 키를 기억해야 하며, 자신이 소유하지 않은 키라 하더라도 해당 프리픽스와 일치하는 키가 변경될 때마다 변경 메시지를 수신한다. (CPU 자원 소비)