개발자님께서는 입사 초기 팀원들 그리고 일을 하면서 뼈저리게 느끼는 부분이 "장애는 언제든지 발생할 수 있다는 점"이라고 한다.
SW라면 개발 능력의 범주 안에서 직접 트러블 슈팅을 하거나 리소스를 많이 사용한다면 로직을 개선하며 최적화하는 등을 할 수 있지만,
HW를 구성하는 CPU, RAM 등 부품 중 어느 하나라도 문제가 발생하면 PM(Physical Machine) 혹은 HV(Hypervisor)에 서비스 아웃(서비스가 중단되거나 제공 불가 상태)을 동반한 점검이 필요하며 실제로 운영 도중 특정 벤더의 일부 부품 전체에서 문제가 발견돼 긴 시간 동안 서비스 아웃을 반복한 경험도 있다고 합니다.
HA(High Availability)
Redis는 인 메모리 데이터베이스로 디스크 스토리지를 사용하는 타 DBMS와는 다르다.
따라서, 메모리(RAM)은 휘발성이라는 특성을 가졌기 때문에 프로세스가 죽으면 데이터 유실을 각오해야한다.
이를 방지하기 위해 LINE의 Redis는 '1 Primary - 1 Replica' 구조를 채택하고 있다고 한다.
그런데 Relica를 2개 이상 두면 더 안전하고 편리하지 않을까?라는 의문이 드는데 장단점이 있다고 한다.
위 그림의 왼쪽과 같이'1 Primary - 2 Replica' 구조의 경우 Primary노드에서 장애가 발생했을 때 어느 Replica 노드가 Primary로 승격될지를 결정하기 위한 추가 과정이 필요하다. 치명적인 단점은 아니지만 Replica 노드 사이에 발생할 충돌 가능성까지 고려하여 '1 Primary - 1 Replica' 구조로 운영 중이라고 합니다. 또한 Replica 노드를 최소화해 비용 측면에서의 최적화 또한 이룰 수 있었다고 한다.
그러나 장애 발생으로 Replica가 Primary로 승격됐을 때 해당 하드웨어 문제가 해결되기 전까지는Primary 노드 하나만 존재하게 돼 아직 다소 위험한 상태
Replica가 Primary로 승격됐을때 이 마저 다운되면?
위와 같이 Primary노드만 남아있는 상태에서 해당 Primary 마저 다운되면 데이터 손실로 이어질 수 있기 때문에 고가용성을 유지하기 위해 '1 Primary' 상태를 방치하지는 않는다.
해결책은 바로 Replica 노드를 하나 더 추가하는 것이다.
팀 차원에서 고사양 장비를 몇 대씩 준비를 해둔 뒤, 필요하면 서버를 추가로 발급 받아 이 서버들이 언제든 Redis를 실행시킬 준비를 한다.
그런 다음 서비스 아웃이 발견되는 즉시 Primary 노드만 남은 서비스에 임시 Replica 노드를 추가하는 작업을 진행한다.
LINE Redis DevOps 팀에서 클라우드 환경에서 관리되지 않는 PM의 경우 해당 작업을 매번 수작업으로 진행했다고 했는데 이러한 상황이 실제로 일주일에 몇 번씩이나 때를 가리지 않는다고 발생한다고 한다.
그렇다면 클라우드 환경에서는 어떻게 작업할까?
클라우드 환경에서 Auto Healing 도입
LINE의 Redis 규모가 거의 아시아에서 탑으로 만약 클라우드 환경에서 관리하는 Redis까지 각 서버의 모든 점검 상황에서 위 대응을 반복해야한다면 정말 힘들 것이다. PM과 같이 매번 수작업을 하는 것은 불가능하고, 그렇다고 남은 서버 하나만 믿고 방치해둘 수는 없었기에 이를 해결하기 위해 사내 클라우드 환경에 오토 힐링을 도입했다고 한다.
서버 점검이 진행될 때 새로운 서버를 발급받아 추가하는 것이다.
차이점이라고하면 새롭게 추가된 서버가 반납되는 것이 아니라 점검 대상인 서버가 반남된다는 것이다.
클라우드 환경이기 때문에 PM과 달리 간단히 서버를 발급받고 교체할 수 있기 때문에 자동화 할 수 있었다고 한다.
서비스 아웃이 발생하는 경우에는 Redis 상태를 확인할 필요가 있기 때문에 야간에도 일어나서 확인해야 한다는 점은 변하지 않았지만 HA 구성을 유지하도록 자동화 프로세스를 구축한 것만으로도 공수를 크게 줄일 수 있었다고 한다.
클라우드 서비스를 활용하면서 DBA에게 생기는 장점
데이터베이스 수가 증가해도, 관리 공수가 정비례로 증가하지 않음
확장 및 축소에 용이
Redis Cluster 뜯어보기 START
내가 구축한 Redis Cluster는 기존에 독립적으로 동작하던 Redis 노드들을 하나의 클러스터로 묶어 구성한 것이다. 이 클러스터는 3개의 Primary 노드와 각 Primary에 대응하는 3개의 Replica 노드로 총 6개의 노드로 이루어져 있다. 전체 해시 슬롯(0번부터 16383번까지)은 3개의 Primary 노드에 나누어 분배되며, 키의 해시 값에 따라 해당 슬롯을 가진 Primary에 데이터가 저장된다.(이렇게 슬롯 기반으로 데이터를 분산 저장하는 샤딩 방식으로 각 노드의 부하를 분산) 또한, 이 구조에서는 Primary나 Replica 노드를 필요에 따라 추가하거나 제거할 수 있고, 특정 해시 슬롯을 새로운 Primary로 옮겨 데이터 재분배를 할 수도 있으며 클러스터에서 Primary, Replica 노드를 제거하기도 가능했다.
가장 흥미로웠던 Hash Slot
위의 과정에서 꽤나 신기했던 점이 바로 기전 RDB와 다른 샤딩?이다.
사실 직접 해본적은 없지만 RDB(MySQL)가 Hash-Based Sharding을 구성했을 때 샤드가 하나씩 늘어날 때마다 기존 데이터들을 수동?으로 이동시켜야했는데 Redis의 경우 Hash Slot을 사용하여 이러한 문제를 해결했다.
Redis는 키를 슬롯에 매핑하기 위해 CRC16 해시 함수를 사용합니다. 구체적으로는 다음 과정을 거칩니다.
키에 {} 패턴이 있는지 확인
키 안에 {와 }가 모두 존재하면, 그 사이의 문자열만 해시 대상으로 사용.
예를 들어 "user:{1000}"와 "order:{1000}"는 모두 "1000"에 대해 해시 계산을 수행하므로, 동일한 슬롯에 매핑. 이는 트랜잭션이나 파이프라인 처리 시 같은 노드에서 키를 다루게 하여 네트워크 비용을 줄이는 데 유용.
{는 있으나 }가 없거나, {} 사이에 아무 문자도 없는 경우, 전체 키를 해시.
CRC16 해시 계산
crc16() 함수는 CCITT 표준을 따르는 XMODEM CRC-16 알고리즘.
이때 성능 최적화를 위해 crc16tab이라는 256개 원소의 미리 계산된 테이블을 활용한다. ➜ redis/src/crc16.c에 static const uint16_t crc16tab[256]= {0x0000,0x1021, ... , 0x1ef0};가 있다.
매 바이트마다 테이블 조회와 비트 연산을 통해 빠르게 CRC 값을 갱신합니다.
슬롯 번호 결정
해시 값의 하위 14비트(0x3FFF 마스크 적용)를 사용한다.
14비트를 쓰는 이유는 214=163842^{14} = 16384214=16384이므로 슬롯 수와 정확히 일치하기 때문
이렇게 계산된 슬롯 번호는 0~16383 범위에 속하며, 해당 슬롯을 담당하는 노드로 키가 저장된다.
[CRC16 계산 과정 연산 과정] CRC16 암호화 과정에 대해서도 한번 관찰해봤다. Redis에서 특정 키에 대한 데이터 저장 요청이 들어왔을 때, crc16() 함수는 다음 순서로 동작한다. 우선, uint16_t crc = 0x0000 즉, 16bit의 0으로 초기화된다. 그런 다음 for Loop를 돌게되는데, 이 루프는 문자열의 각 바이트마다 CRC 값을 갱신한다. CRC16tabl[...] 내부 ① (crc >> 8)로 현재 CRC의 상위 8비트만 남긴다. ② ((crc >> 8) ^ *buf++) 상위 8비트와 key의 새로 읽은 바이트를 XOR. ③테이블(crc16tab)은 256개(0~255) 인덱스만 가지고 있기에 & 0x00FF을 통해 하위 8비트만 남긴다. 0x00FF를 binary로 펼쳐보면 00000000 11111111 (총 16비트). 따라서 &연산으로 하위 8비트만 남길 수 있음. 즉, ((crc>>8) ^ *buf++)&0x00FF 결과값은 0~255 범위 → crc16tab의 인덱스로 사용. ④ 위에서 만든 인덱스로 미리 계산된 CRC 결과값을 테이블에서 가져옵니다. 이렇게 하면 느린 비트 연산 없이 빠르게 계산. crc 변수 갱신 ⑤ (crc << 8) ^ crc16tab[...] 기존 CRC를 왼쪽으로 밀고, 테이블에서 가져온 값을 XOR 해서 최종 CRC 값 갱신.
이렇게 key값을 시작부터 끝까지 byte를 하나씩 돌면서 crc를 갱신시켜 반횐되는 최종 crc가 해시의 결과이다.
keyHashSlot()에서 crc16() 반환값 처리(최종 slot 결정) 추가적으로 그렇게 반환된 crc16()를 keyHashSlot()에서 crc16(key,keylen) & 0x3FFF 다음과 같이 처리한다. 0x3FFF = 0011 1111 1111 1111 (2진수, 14비트가 모두 1)으로 하위 14비트만 남기는 비트 마스크인데, 왜 하필 14bit? Redis Cluster는 총 16384개의 해시 슬롯을 사용한다고 했는데, ‣ HashSlot의 총 갯수인 16384 = 2¹⁴ → 딱 14비트로 표현 가능 ‣ CRC16은 16비트(0~65535) 값을 반환 → 그대로 쓰면 슬롯 범위를 넘음 ‣ 하위 14비트만 남기면 0~16383 범위가 됨