본문 바로가기

카테고리 없음

[개발자를 위한 레디스] 12장 클라이언트 관리

반응형
SMALL

클라이언트 핸들링

  • 일반적으로 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 자원 소비)
반응형
LIST