데이터베이스/Nosql

Redis Cluster mode에서 mget, mset, pipeline과 같은 멀티 키 명령어 사용하기.

반응형

redis에 만약 200 ~ 300개가 넘는 캐시 정보를 계속 request를 날리면 레이턴시가 발생할 가능성이 크기 때문에 이런경우에 mget, mset, pipeline 등 멀티키 명령어를 사용한다.

하지만 redis가 single mode일 때는 아무 상관이 없지만 cluster mode인경우에는 다음과 같은 오류를 발생 시킨다.

"CROSSSLOT Keys in request don't hash to the same slot"

무슨 오류일까?? 처음에 redis가 싱글모드로 돌고 있던 stage에서 테스트를 해서 정상적으로 멀티키 명령어가 잘 되는줄 알고 배포 했다가 라이브에서 위와 같은 오류가 발생했다...

너무 당황해서 바로 수정했다.

무엇이 문제 였을까? 하면서 문서를 찾아보니 redis에는 16384의 키 슬롯이 있는데 클러스터 모드인 경우 이 키 슬롯들이 각 node에 분할되기 때문에 멀티키 명령어를 사용할 수 없다고 한다.

만약 멀티키 명령어를 사용하고 싶다면 그 키들은 모두 같은 키 슬롯에 들어있어야 한다.

방법은 다음과 같다.

 

같은 키 슬롯에 데이터 넣기


동일한 키 슬롯에 저장하고 싶으면 강제로 지정하는 해야한다. 이때 저장하고자 하는 키들 앞에 {..}의 값을 붙여주고 저장하는 것이다. 예를 들어 저장하고자 하는 데이터에 {product}.products1, {product}.products2로 저장하면 이 두개는 동일한 키 슬롯에 포함되게 된다.

이는 aws elasticcache페이지에 상세히 나와있으니 참고하면 된다.

https://aws.amazon.com/ko/premiumsupport/knowledge-center/elasticache-crossslot-keys-error-redis/

 

ElastiCache 오류 "CROSSSLOT Keys in request don't hash to the same slot" 해결

이 오류는 키가 동일한 노드가 아니라, 동일한 해시 슬롯에 있어야 하기 때문에 발생합니다. 샤딩된 Redis ElastiCache 클러스터(클러스터 모드 활성화됨)에서 다중 키 작업을 구현하려면 키는 동일한 해시 슬롯으로 해시되어야 합니다. 해시 태그를 사용하여 키를 동일한 해시 슬롯에 강제로 배치할 수 있습니다. 이 예제에서는 "myset2" 및 "myset" 세트가 동일한 노드에 있습니다. 이 오류를 해결하려면 해시 태그를 사용하여 키를 동일한 해시

aws.amazon.com

하지만 여기서 중요한건, 우리가 클러스터 모드를 사용하는 이유는 한군데이 집중시키지 않고 값을 분산시켜서 부하를 막기 위해서인데 이를 하나의 키 슬롯에 데이터를 모두 집어 넣을 경우 의미가 없어진다.

그럼 키를 각 노드마다 분산시키면서 멀티키 명령어를 사용할 수 있는 방법이 있을까?

 

클러스터 모드에서 각 노드별 key slot에 데이터 삽입하여 멀티 명령어 사용하기


생각을 바꿔보았다. 싱글모드에서는 키 슬롯 16384개가 존재하고 mset, mget, pipeline등의 명령어가 사용 가능했다. 

그럼 각 노드를 하나의 싱글모드의 레디스라고 생각하면 어떨까? 하는 생각으로 전략을 바꿔봤다.

처음에는 가능할지 모르고 작업을 진행했는데 정상적으로 동작해서 기분이 좋았다.

#작업 순서 (명령어는 ioredis를 기준으로 작성하였다.)

1. 클러스터 모드에서 동작중인 master node에 대한 정보를 가져온다. (cluster mode에는 master 노드와 slave노드가 있는데 slave node는 단순 readonly이고 마스터 노드 값을 가지고 있기 때문에 가져올 필요 없다.)

const masterNodes = redis.nodes('master')

2. 각 노드별 key slot 현황을 확인하고 masterNode와 매핑한다. key slot 현황을 확인하기 위해서는 cluster slots 사용한다. ioredis에서는 redis.cluster('slots')

127.0.0.1:7001> cluster slots
1) 1) (integer) 0
   2) (integer) 4095
   3) 1) "127.0.0.1"
      2) (integer) 7000
   4) 1) "127.0.0.1"
      2) (integer) 7004
2) 1) (integer) 12288
   2) (integer) 16383
   3) 1) "127.0.0.1"
      2) (integer) 7003
   4) 1) "127.0.0.1"
      2) (integer) 7007
3) 1) (integer) 4096
   2) (integer) 8191
   3) 1) "127.0.0.1"
      2) (integer) 7001
   4) 1) "127.0.0.1"
      2) (integer) 7005
4) 1) (integer) 8192
   2) (integer) 12287
   3) 1) "127.0.0.1"
      2) (integer) 7002
   4) 1) "127.0.0.1"
      2) (integer) 7006
const slots = await redis.cluster('slots');

// 각 슬롯정보에 있는 ip, port정보를 확인하여 각 master와 슬롯의 범위를 정리한다.
{
 'ip' : ,
 'port' : ,
 'node' : masterNode[0],
 'slotStart' : 0
 'slotEnd': 4460
},
{
 'ip' : ,
 'port' : ,
 'node' : masterNode[1],
 'slotStart' : 4461
 'slotEnd': 8845
},
.
..
...
....

3. 찾고자하는 키들의 slot 정보를 정리하고 slot 범위에 맞는 오브젝트에 키 값을 배열에 정리한다. slot 정보를 뽑는 알고리즘이 CRC16이기 때문에 이를 이용해서 구할수도 있고 npm에 모듈로 나와있는게 있으니 활용해도 된다. (https://www.npmjs.com/package/cluster-key-slot)

{
 'ip' : ,
 'port' : ,
 'node' : masterNode[0],
 'slotStart' : 0
 'slotEnd': 4460,
 'keys': ['product::1', 'product::2', 'product::5'] // 범위가 0 ~ 4460에 속하고 masterNode[0]에 존재하는 키 슬롯에 저장되는 키들
},
{
 'ip' : ,
 'port' : ,
 'node' : masterNode[1],
 'slotStart' : 4461
 'slotEnd': 8845,
 'keys': ['product::4', 'product::3', 'product::6'] // 범위가 4461 ~ 8845에 속하고 masterNode[1]에 존재하는 키 슬롯에 저장되는 키들
},
.
..
...
....

4. 그리고 정리된 Object를 순회하면서 각 키에 있는 데이터를 조회하기 위한 pipeline을 생성하고 get명령어를 매핑하고 실행시킨다. 없는 데이터는 pipeline을 set으로 설정하고 exec 실행시킨다.

const objs = [{
 'ip' : ,
 'port' : ,
 'node' : masterNode[0],
 'slotStart' : 0
 'slotEnd': 4460,
 'keys': ['product::1', 'product::2', 'product::5']
},
{
 'ip' : ,
 'port' : ,
 'node' : masterNode[1],
 'slotStart' : 4461
 'slotEnd': 8845,
 'keys': ['product::4', 'product::3', 'product::6']
}];

for(const obj of objs) {
 const pipeline = obj.node.pipeline();
 
 obj.keys.forEach(keySlot => pipeline.get(key => pipeline.get);
 
 // 결과
 const ret = await keys.exec();
 
 //..... 없으면 없는 것들은 set로 pipeline 열어서 설정(예를 들어서 keys 배열에 1, 2번째 값이 없다고 가정)
 const setPipeline = obj.node.pipeline();
 
 pipeline.set(obj.keys[1]);
 pipeline.set(obj.keys[2]);
 
 await setPipeline.exec();
}

 

소스를 모두 다 적기에는 양도 많기 때문에 간단하게 내가 진행한 방식에 대한 설명만 적었다.

이 글을 보고 방법을 찾고자 하는 사람들에게 도움이 되지 않을수도 있지만  클러스터 모드에서 레디스의 멀티키 사용법에 대해 자료가 없어서 고민을 많이 했었기 때문에 작은 힌트라도 되었으면 좋겠다.

반응형