Redis 관리 업무를 보거나, Redis를 통해서 개발을 한 경험이 있는 프로그래머라면,
한 번쯤 Redis에서 특정 key 패턴을 만족하는 모든 키를 지워줘야 하는 상황을 만날 수 있다.
이럴 경우 redis-cli 에 접속해서 key 값 하나하나씩 삭제하는 방식은
key의 개수가 별로 크지 않을 때에는 상관없지만,
실제로 운영되는 Redis 데이터 베이스에서는 특정 key 패턴을 만족하는 key의
개수가 엄청 많기 때문에, 사실상 위의 방법은 불가능하다고 봐야 한다.
그럼 해당 기능이 필요할 때마다, 간단하게 사용할 수 있는 api를 만들어두면
업무를 수행하는데 훨씬 편해질 것이다.
해당 기능을 수행하는 api 를 python을 통해서 구현해 보자.
일단 특정 키패턴을 만족하는 모든 키를 삭제해 주는 api를 만들기 위해서는
Redis 아키텍처에 대한 이해가 필요하다.
아래와 같이 기본적인 Redis의 성질에 대해서 이해한 뒤 api를 구성해 보자.
운영되는 Redis 가 Cluster mode라고 가정하고 api를 구현해 보자.
Master node, Slave node 란?
Redis Cluster에서 Master role과 Slave role의 차이점에 대해 알아보자.
마스터 노드 (Master Node)
- 마스터 노드는 클러스터 내에서 데이터를 저장하고 관리하는 주체이다.
- 클라이언트의 쓰기 요청을 처리하고 해당 데이터를 저장한다.
- 데이터 변경이 발생하면 변경된 데이터를 슬레이브 노드에게 복제한다.
- 레디스 클러스터는 보통 여러 개의 마스터 노드로 구성되며,
각 마스터 노드는 단일 데이터셋의 일부를 관리한다.
슬레이브 노드 (Slave Node)
- 슬레이브 노드는 마스터 노드의 데이터를 복제하는 역할을 수행한다.
- 마스터 노드의 데이터 변경 사항을 실시간으로 수신하여 자동으로 동기화된다.
- 클라이언트의 읽기 요청을 처리하기 위해 데이터를 제공한다.
- 슬레이브 노드는 마스터 노드의 데이터를 복제하기 때문에
마스터 노드에 장애가 발생하면 슬레이브 노드 중 하나를 새로운 마스터로 승격시킬 수 있다.
- 슬레이브 노드는 여러 개의 마스터 노드로부터 데이터를 복제할 수 있으며,
복제된 데이터는 읽기 요청을 처리하는 데 사용된다.
- 마스터 노드와 슬레이브 노드 간의 데이터 복제는 비동기적으로 이루어진다.
즉, 마스터 노드에 데이터 변경이 발생하면 변경된 데이터를
실시간으로 슬레이브 노드에게 전달하는 것이 아니라,
일정 주기로 데이터를 전송하여 복제한다.
이로 인해 슬레이브 노드는 마스터 노드와의 데이터 일관성이나 실시간 동기화를 보장하지 않을 수 있다.
하지만, 데이터의 가용성을 높이고 읽기 처리 성능을 향상하는 장점이 있다.
Redis Key 삭제 구현 방법론
아래와 같이 구성된 Redis Cluster 가 존재한다고 가정해 보자.
Master node는 세 개로 구성되어 있고,
Slave node 가 각 Master node 당 두 개씩 구성되어 있다.
특정 API에서 새로운 Redis key를 생성하여 쓰려고 하면,
해당 키는 해시 함수에 의해 해시 값으로 변환되고,
이 해시 값에 기반하여 특정 마스터 노드에 데이터가 저장된다.
이를 통해 데이터가 균등하게 분산되고, 각 마스터 노드는 일부 데이터셋을 관리하게 됩니다.
또한 마스터 노드에 Redis key 가 쓰인다면 해당 정보와 동일한 키 정보가
Slave node에 복사되는 작업이 진행된다.
그럼 Redis cluster에 저장된 특정 Key를 제거하기 위해서는
Master node에 접근해야 한다는 결론이 나오게 된다.
쓰기는 Master node 만 가능하기 때문이다.
물론 Slave node에서 특정 키를 찾아서(Read)
해당 마스터 노드를 찾은 다음 삭제작업(Write)을 진행해도 되지만,
번거로우므로 Master node에서 Key를 삭제하는 작업을 진행한다고 가정하자.
대략적인 구성도는 아래와 같다.
redis_scan_delete_single.py 코드가
server_list 파일 내에 저장된 특정 json 파일을 읽어서
특정 Redis Cluster에 접속한 후에,
관리자가 지우길 원하는 Redis prefix pattern에 대해서 입력을 해주면,
지워주는 방식으로 코드를 구성해 보자.
server_log는 기본적으로 해당 파이썬 코드가 실행하면서
생기는 정보들과 오류에 대한 로그가 남는 곳이다.
delete_log는, 관리자가 특정 Redis key 패턴에 해당하는 키들을
지우려고 시도할 때, 어떤 키들이 지워졌는지 로그로 기록하는 곳이다.
아래에는 redis_scan_delete_single.py의 소스코드이다.
import json
from rediscluster import RedisCluster
import redis
import argparse
from datetime import datetime
import logging
import logging.handlers
"""
Function that records the log
"""
def setupLogging(log_inst_name, path, log_filename):
file_handler = logging.handlers.TimedRotatingFileHandler('{}/{}.log'.format(path, log_filename), when="midnight", backupCount=10)
file_handler.setFormatter(logging.Formatter('[ %(asctime)s ] %(levelname)s : %(message)s'))
logger = logging.getLogger(log_inst_name)
logger.setLevel(logging.INFO)
logger.addHandler(file_handler)
return logger
"""
Function that writes deleted keys to the log file
"""
def deleteLogWriter(content):
delete_now = datetime.now()
formatted_date = delete_now.strftime("%Y_%m_%d")
path = './delete_log/delete_key_{}.log'.format(formatted_date)
with open(path, "a") as f:
f.write("[{}] {} DELETED. \n".format(delete_now,content))
f.close()
"""
Error class to occur if user input is incorrect
"""
class InputError(Exception):
def __init__(self):
message = '===================================[WARN] Please execute it as below ===================================\n\n'
message += 'python3 redis_scan_delete_single.py --h [server name] --a [password] --k [key prefix] --c [scan count] --ttl [ttl option]\n\n'
message += '[EX] python3 redis_scan_delete_single.py --h server1 --a 1234 --k test-dev::file --c 100 --ttl y\n\n'
print(message)
super().__init__("Redis parameter error occurred")
"""
Redis Master Node Class Objects
"""
class RedisMaster:
def __init__(self, node_ip, node_port):
self.node_ip = node_ip
self.node_port = node_port
"""
Redis Cluster Connection Function
"""
def redisClusterConn(startup_nodes,redis_password,logger):
rc = None
try:
if (redis_password == None):
rc = RedisCluster(startup_nodes=startup_nodes, decode_responses=True)
else:
rc = RedisCluster(startup_nodes=startup_nodes, decode_responses=True, password=redis_password)
rc.ping()
logger.info('{} : Redis Cluster Connected.'.format(startup_nodes))
except Exception as e:
print("Unable to connect to Redis cluster. Please check the ip/port or password.")
logger.error("Redis Cluster Connection Error")
finally:
return rc
"""
Functions that allow access to a particular redis node
"""
def redisConn(host, port, redis_password, logger):
rc_node = None
try:
rc_node = redis.Redis(host=host, port=port, password=redis_password)
rc_node.ping()
logger.info('{}:{} Redis Node Connected.'.format(host,port))
except Exception as e:
print("Unable to connect to Redis server. Please check the ip/port or password.")
logger.error("Redis Cluster Connection Error")
finally:
return rc_node
"""
Function that determines if a particular redis node is a MASTER ROLE
"""
def checkMasterNode(rc,logger):
master_list = []
try:
cluster_info = rc.cluster_nodes()
for node_info in cluster_info:
redis_role = None
if (node_info['flags'][0] == 'myself'): redis_role = node_info['flags'][1]
else: redis_role = node_info['flags'][0]
if (redis_role == 'master'):
redis_addr = node_info['host']
redis_port = node_info['port']
redis_node = RedisMaster(redis_addr,redis_port)
master_list.append(redis_node)
except Exception as e:
logger.error(str(e))
finally:
return master_list
"""
Function to read redis cluster information stored in json form
"""
def redisClusterInfoRead(path, logger):
data = None
try:
with open(path, "r") as f:
data = json.load(f)
f.close()
except Exception as e:
logger.error("Failed to read information from Redis cluster.")
finally:
return data
"""
Functions that handle user input parameters
"""
def verifyInputData(logger):
parm_dict = {}
default_host_list = ['server1','server2','server3']
parser = argparse.ArgumentParser()
parser.add_argument("--h", type=str, default=None, help="Redis server : Cluster name(host)")
parser.add_argument("--a", type=str, default=None, help="Redis server : authentication")
parser.add_argument("--k", type=str, default=None, help="Redis server : key name")
parser.add_argument("--c", type=int, default=None, help="Redis server : Unit to scan")
parser.add_argument("--ttl", type=str, default=None, help="Redis server : TTL option")
args = parser.parse_args()
try:
c_name = args.h.lower()
c_auth = args.a
c_key = args.k
c_scan = args.c
c_ttl = args.ttl
if ((c_name == None) or (c_scan == None) or (c_name not in default_host_list)):
raise InputError
if (c_key is None or c_key == ''): c_key = '*'
else: c_key += '*'
if c_ttl is not None and c_ttl in ['y','Y']: c_ttl = 'y'
else: c_ttl = 'n'
parm_dict['c_name'] = c_name
parm_dict['c_auth'] = c_auth
parm_dict['c_key'] = c_key
parm_dict['c_scan'] = c_scan
parm_dict['c_ttl'] = c_ttl
except Exception as e:
logger.error(str(e))
finally:
return parm_dict
"""
Function that UNLINKs all keys that satisfy a particular key pattern
"""
def redisKeyPatternUnlink(rc_node, redis_key_prefix, scan_cnt, ttl_yn, redis_pw, server_logger, delete_logger):
ttl_ym_log = 'APPLY' if ttl_yn == 'y' else "NOT APPLY"
rc_node = redisConn(rc_node.node_ip, rc_node.node_port, redis_pw, server_logger)
server_logger.info('{} -> {} KEY SCAN AND DELETE. SCAN count : {}, TTL option : {}'.format(rc_node,redis_key_prefix,scan_cnt,ttl_ym_log))
try:
# Start Redis SCAN
cursor = 0
while True:
# Scan for keys that fit the pattern
cursor, keys = rc_node.scan(cursor=cursor, match=redis_key_prefix, count=scan_cnt)
# Execute KEY UNLINK
for key in keys:
if (ttl_yn == 'y' and rc_node.ttl(key) != -1):
delete_logger.info('not ttl host:{} , key : {}'.format(rc_node,key))
rc_node.unlink(key)
elif (ttl_yn == 'n' and rc_node.ttl(key) == -1):
delete_logger.info('host:{} , key : {}'.format(rc_node,key))
rc_node.unlink(key)
# Determine if the scan is complete
if cursor == 0:
break
except Exception as e:
server_logger.error(str(e))
finally:
rc_node.close()
"""
Main function that erases Redis data
"""
def main():
now = datetime.now().strftime("%Y-%m-%d")
server_logger = setupLogging('server_log','./server_log',now)
server_logger.info("Redis SCAN-UNLINK start")
params_dict = verifyInputData(server_logger)
if (params_dict == None): return -1
redis_path = './server_list/' + params_dict['c_name'] + ".json"
redis_pw = params_dict['c_auth']
redis_prefix = params_dict['c_key']
scan_cnt = params_dict['c_scan']
ttl_yn = params_dict['c_ttl']
delete_logger = setupLogging('delete_log','./delete_log','{}_{}_deleted'.format(now, redis_prefix))
startup_nodes = redisClusterInfoRead(redis_path,server_logger)
rc = redisClusterConn(startup_nodes, redis_pw, server_logger)
if (rc == None): return -1
master_list = checkMasterNode(rc, server_logger)
if (len(master_list) == 0):
rc.close()
return -1
try:
for node in master_list:
redisKeyPatternUnlink(node, redis_prefix, scan_cnt, ttl_yn, redis_pw, server_logger, delete_logger)
except Exception as e:
server_logger.error(str(e))
finally:
rc.close()
server_logger.info("Redis SCAN-UNLINK end")
"""
python3 redis_scan_delete_single.py --h server1 --a 1234 --c 1000 --ttl y
python3 redis_scan_delete_single.py --h server1 --a 1234 --c 2000
"""
if __name__ == '__main__':
main()
현재 Redis cluster의 각 Master 노드에는 아래와 같은 Key가 존재한다.
위에서, seunghwan-key로 시작하는 모든 키를 삭제해주고 싶다고 해보자.
그럼 관리 서버 내에서 아래와 같이 파이썬 코드를 실행시켜 주자.
python3 redis_scan_delete_single.py --h server1 --a 1234 --k seunghwan-key --c 100
파라미터의 뜻은 아래와 같다.
h : 어떤 서버파일인지 정해준다.
a : 해당 Redis cluster 접속 비밀번호
k : 삭제해주고 싶은 Redis key의 패턴
c : 몇 개씩 SCAN 하여 키를 제거할 것인지 SCAN의 개수
Redis cluster 내에서 키를 삭제하는 방법은
특정 키패턴에 대해서 하나하나 모두 SCAN 하면서 지워주는 방법뿐이다.
서버로그와 삭제로그를 보면 실제로 아래와 같이 확인할 수 있다.
<server_log>
서버로그에는 '2023-06-11.log'와 같이 특정 날짜정보가 적힌 로그파일이 존재한다.
해당 로그파일 내부는 아래와 같이 쓰여 있다.
<delete_log>
삭제로그 내부에는 '2023-06-11_seunghwan-key*_deleted.log'와 같은 이름의 로그파일이 존재한다.
날짜와 지우고자 하는 key패턴정보가 파일 이름으로 구성되어 있다.
해당 로그파일 내부는 아래와 같이 쓰여있다.
Redis Cluster 각 노드에서도 해당 Key 정보는 모두 지워진 것을 확인할 수 있다.
'Redis' 카테고리의 다른 글
[REDIS] 좋아요 구현 (2) | 2024.04.29 |
---|---|
[Redis] Key 삭제 (0) | 2023.05.24 |
[Redis] SCAN (2) | 2023.03.28 |
[Redis] Redis 설치 (0) | 2022.10.12 |
[Redis] Redis Cluster (0) | 2022.08.11 |