Binary Data를 Text로 바꾸는 Encoding(Binary-to-Text Encoding schemes)의 하나로써 Binary Data를 Character Set에 영향을 받지 않는 공통 ASCII 영역의 문자로만 이루어진 문자열로 바꾸는 Encoding이다.
Base64를 글자 그대로 직역하면 64진법이라는 뜻이다. 64진법은 컴퓨터한테 특별한데 그 이유는 64가 2의 제곱수 64=2^6이며 2의 제곱수에 기반한 진법 중 화면에 표시되는 ASCII 문자들로 표시할 수 있는 가장 큰 진법이기 때문이다.
(ASCII에는 제어문자가 다수 포함되어 있기 때문에 화면에 표시되는 ASCII 문자는 128개가 되지 않는다.)
2020-05-25 13:44:29.123965 N Proxy.cpp:112 predixy listen in 0.0.0.0:7617
2020-05-25 13:44:29.124092 N Proxy.cpp:143 predixy running with Name:PredixyExample Workers:4
2020-05-25 13:44:29.124417 N Handler.cpp:456 h 0 create connection pool for server 192.168.50.15:6380
2020-05-25 13:44:29.124436 N ConnectConnectionPool.cpp:42 h 0 create server connection 192.168.50.15:6380 8
2020-05-25 13:44:29.125279 N ClusterServerPool.cpp:174 redis cluster create new group 29c5855872ff38f9f2a4203094fc4928fec7744f 192.168.50.15:6379@16379 master -
2020-05-25 13:44:29.125298 N ClusterServerPool.cpp:174 redis cluster create new group b1e905ca7d3fb5f743930565994aef6eb48cfe95 192.168.50.16:6379@16379 master -
2020-05-25 13:44:29.125308 N ClusterServerPool.cpp:174 redis cluster create new group 186deee637ea73f136d671f674320b424c3dddf4 192.168.50.14:6379@16379 master -
~~~~~~~~~~~~~~~~~~~~~~~~
Test
1. Client Get Query
# redis-cli -p 7617 -h 192.168.50.13 get hello
"world"
predixy가 실행중인 서버에서 hello key의 world value를 리턴하였다.
만약 predixy가 없는 상황이라면 redis cluster에 직접 질의를 해야하고 보통의 구조에서는 하나의 Master에서 모든 요청을 처리하기때문에 부하가 발생할 수 있다.
2. Log
2020-05-25 13:46:37.252826 N Handler.cpp:456 h 3 create connection pool for server 192.168.50.14:6380
2020-05-25 13:46:37.252874 N ConnectConnectionPool.cpp:42 h 3 create server connection 192.168.50.14:6380 40
2020-05-25 13:46:38.012174 N Handler.cpp:373 h 1 accept c 192.168.50.10:35870 42 assign to h 0
2020-05-25 13:46:38.012860 N Handler.cpp:212 h 0 remove c 192.168.50.10:35870 42 with status 2 End
192.168.50.10에서 온 요청을 redis master 중 요청한 key를 가진 slot이 있는 master (192.168.50.14) 로 전달하였다.
3. redis latency check (predixy support)
# redis-cli -p 7617 -h 192.168.50.13 INFO Latency all
# LatencyMonitor
LatencyMonitorName:all
<= 100 36 3 100.00%
T 12 36 3
# ServerLatencyMonitor
ServerLatencyMonitorName:192.168.50.14:6379 all
ServerLatencyMonitorName:192.168.50.15:6379 all
ServerLatencyMonitorName:192.168.50.16:6379 all
ServerLatencyMonitorName:192.168.50.14:6380 all
ServerLatencyMonitorName:192.168.50.15:6380 all
ServerLatencyMonitorName:192.168.50.16:6380 all
ServerLatencyMonitorName:192.168.50.14:6381 all
ServerLatencyMonitorName:192.168.50.15:6381 all
ServerLatencyMonitorName:192.168.50.16:6381 all
predixy에서 지원하는 기능 중 하나로 각 Redis node에 대한 Latency를 확인할 수 있다.
레디스는 단일 인스턴스만으로도 운영이 가능하지만 물리 머신이 가진 메모리의 한계를 초과하는 데이터를 저장하고 싶거나 failover에 대한 처리를 통해 HA를 보장하려면 센티널이나 클러스터 등의 운영 모드를 선택해서 사용해야 한다.
운영 모드 (Operation Modes)
단일 인스턴스 (Single Instance)
HA를 지원하지 않는다.
센티널 (Sentinel)
HA를 지원하며 Master/Slave Replication 구조를 가진다.
Sentinel Process는 Redis와 별도의 Process로 동작하며 여러개의 독립적인 Sentinel Process들이 협동하여 운영된다 (SPOF 아님)
안정적인 운영을 위해서는 최소 3개 이상의 Sentinel Instance가 필요하며 FailOver를 위해 과반수 이상의 Vote가 필요하다.
Redis Process가 실행되는 각 서버마다 각각 Sentinel Process를 띄어놓거나 Redis에 접근하는 Application Server에 Sentinel Process를 띄어놓고 사용한다. (여러가지 구성이 가능)
지속적으로 Master/Slave가 제대로 동작하는지 모니터링하다가 Master에 문제가 감지되면 자동으로 FailOver를 수행한다.
Application에서 Sentinel에서 Master가 어떤 Instance인지 확인하여 Master 접근하는 방식을 사용한다면 Master 장애로 FailOver시 Application에서도 자동으로 Master 변경이 가능하다.
즉, Application에서 Sentinel에 연결하여 현재 Master를 조회하는 방식이다.
클러스터 (Cluster)
HA와 Sharding을 지원한다. Sentinel과 동시에 사용하는거이 아니라 완전히 별도의 솔루션이다.
DataSet을 자동으로 여러 노드에 나눠서 저장해준다. Redis Cluster 기능을 지원하는 Client를 써야만 Data Access 시에 올바른 노드로 Redirect가 가능하다.
Cluser Node들은 기본설정인 6379 포트를 Data Port로 사용하며 Cluster는 Cluster Bus Port를 사용하여 자체적인 바이너리 프로토콜을 통해 Node to Node 통신을 한다. (보통 Cluster Bus Port는 6379에 10000을 더한 16379 Port를 사용한다.)
이 Cluster Bus Port를 통해 Node 간 Failure Detection, Configuration Update, FailOver Authorization 등을 수행한다.
Sharding은 최대 1000개의 Node로 Sharding해서 사용가능하며 그 이상은 권장하지 않는다. Consistent Hashing을 사용하지 않는 대신 Hash Slot이라는 개념을 도입하여 사용한다.
Hash Slot을 결정하는 방법으로는 CRC16(key) Mod 13684로 CRC16을 이용하면 16384개의 Slot에 균일하게 잘 분배된다.
노드별 자유롭게 Hash Slot을 할당 가능하다.
예를 들면 A Node에 0개부터 5500개, B Node에 5501개부터 11000개, C Node에 11001개부터 16383개를 할당할 수 있다.
운영중단 없이 Hash Slot을 다른 노드로 이동시키는것이 가능하며 노드를 추가하거나 삭제하고 노드별 Hash Slot 할당량을 조절할 수 있다. (노드의 개수에 따라 Slot할당을 다르게 가져갈 수 있다.)
Multiple Key Operations를 수행하려면 모든 키값이 같은 Hash Slot에 들어와야 한다.
이를 보장하기 위해 Hash Tag라는 개념을 도입하여 사용한다. {} 안에 있는 값으로만 Hash 계산을 한다. ({foo}_mykey}, {foo}_your_key})
Fail Over를 위해 클러스터의 각 노드를 N대로 구성 가능하다. Master 1대 / Slave Replication (N대) 구성이다.
Async Replication (Master -> Slave Replication 과정에서 Ack를 받지 않는다.) 즉, 데이터를 보내지면 정확히 도착하였는지에 대한 응답을 받지않는 것이다.
그렇기 때문에 데이터 손실 가능성이 존재한다. (Master가 Client 요청을 받아서 Ack를 완료한 후 받은 Client의 요청을 Slave Replication으로 보내기전에 Master가 죽는 경우가 존재한다.)
Redis Client는 Cluster 내의 어떤 노드든 쿼리를 날려도 된다. (Slave에 쿼리를 날리는 것도 가능하다. 예를 들어 GET my_key 와 같은 쿼리)
그렇게 되면 쿼리를 받은 노드가 해당 쿼리를 분석하고 해당 키를 자신이 갖고 있다면 바로 찾아서 리턴하고 그렇지 않은 경우 해당 키를 저장하고 있는 노드의 정보를 리턴한다.
Client는 전달받은 노드의 정보로 다시 쿼리를 보내 요청한 쿼리에 대한 정보를 전달받는다. (리턴은 MOVED 3999 127.0.0.1:6381 형식으로 응답받는데 이 내용으로 다시 쿼리를 보낸다.)
메모리 동작 방식
Key가 만료되거나 삭제되어 Redis가 메모리를 해제하더라도 OS에서 해제된 분량만큼 바로 메모리가 확보되지는 않는다. (꼭 Redis만 해당되는 얘기는 아님)
예를 들어 5GB는 3GB의 데이터를 메모리에서 해제하더라도 여전히 OS 메모리 사용량은 5GB로 잡혀있다.
하지만 다시 Data를 추가하면 Logically Freed된 영역, 즉 해제되었지만 OS에 반환되지 않은 영역에 할당되므로 물리적인 5GB를 넘지는 않는다.
따라서 Peak Memory Usage를 기준으로 Memory를 잡아야한다. 예를 들어 5GB 사용하고 가끔 10GB가 필요한 경우가 있다면 10GB 이상을 가진 머신이 필요한것이다.
MaxMemory 설정을 해두는 것이 좋은데 설정을 하지 않으면 Memory를 무한히 사용하다가 OOM이나 머신에 장애가 발생할 가능성이 높아진다.
MaxMemory 설정시의 Eviction Policy (Memory 해제 정책) 는 아래와 같다.
No-Eviction : 추가 Memory를 사용하는 Write Command에 대해 Error를 리턴
Allkeys-LRU : 전체 아이템 중에서 LRU (페이지 교체 알고리즘으로 가장 마지막에 사용된 페이지를 새로운 페이지로 교체하는 방식)
Volatile-LRU : Expire 되는 아이템 중에서 LRU
Volatile-TTL : Expire 되는 아이템 중 TTL이 얼마 안남은 순으로 교체
RDB Persistence를 사용한다면 MaxMemory를 실제 가용 Memory의 45% 수준으로 설정하는 것을 권장한다. Snapshot을 찍을때 현재 사용중인 Memory 양만큼 필요하기 때문이다. (5%는 오버헤드에 대비하기 위해 여유분)
만약 RDB Persistence를 사용중이지 않다면 95%정도로 사용하여도 된다.
동일 Key-Value Data를 저장한다고 가정했을 때 Cluster Mode를 사용할 경우 Single Instance 보다 1.5 ~ 2배 정도 Memory를 더 사용하는 것에 주의해야 한다.
Redis Cluster의 경우 내부적으로 Cluster안에 저장된 Key를 Hash Slot으로 Mapping하기 위한 Table을 가지고 있기 때문에 추가적인 Memory OverHead가 발생한다.
이 때문에 Key의 숫자가 많아질수록 Memory OverHead가 더 빈번히 발생한다. 4버전 부터는 이전 버전보다 Memory 최적화 기능이 포함되어 Memory를 더 적게 사용하지만 여전히 Single Instance보다 많은 Memory를 필요로 한다.
주요 특수 기능
다양한 데이터 구조 지원
단순히 Key - Value 문자열만 저장하는 것이 아니라 고수준의 데이터 구조를 사용가능 하다.
Ex) Hash, Set, List, SortedSet, ETC
Hash에는 HSET(key, fileds, value), HGET(key,filed)가 있다.
Web Application에서 특정 유저 ID를 Key로 두고 해당 유저의 세부 정보 (Name, Email 등)를 filed로 둔다.
이렇게 하면 특정 유저와 관련된 정보들을 한번에 삭제하는 등 하나의 Namespace처럼 사용할 수있다.
Hash Key당 1000개 정도의 field까지는 Redis가 zipmap을 이용해 압축해서 저장한다. 즉, 1000개의 Key에 저장될 내용이 하나의 Key에 1000개의 field로 저장되는 것이다.
하지만 CPU 사용량이 증가하므로 Application 특성에 맞게 적정 갯수를 잘 선택해야 한다.
외부에서 HAProxy의 VIP로 curl 명령 실행시 뒷단의 Web 서버로 LoadBalancing이 되는 것이 확인된다.
앞단에서 HAProxy Master역할을 하는 것은 Keepalived의 Master로 설정된 노드이다.
이제 Master VM 장애 발생을 가정하고 Failover 테스트를 진행한다.
Slave에는 Haproxy Service가 실행 중이다.
(기본 커널 파라미터가 셋팅이 안된상태에서 Slave의 HAProxy 데몬은 실행에 실패한다.테스트를 위해 Master를 Off후 서비스를 올린 뒤에 다시 Master를 On 시켜 테스트 환경을 만든 것이다.
데몬이 실패한 이유는 haproxy에 의해 VIP가 네트워크 인터페이스에 바인딩되어야하는데 커널파라미터가 비활성화 값이므로 실패하였다.)
Master VM을 Poweroff 하였을때 Keepalived에 의해 Slave VM이 Master로 승격되었다.
외부에서 HAProxy로 curl 명령 실행시 무중단으로 실행되는 것도 확인되었다.
이제 다시 Master VM을 실행시켜 FailBack을 할 예정이다.
Slave는 정상이나 Master가 다시 정상으로 돌아왔을 경우 HAProxy에 대한 응답은 실패했다.
Master에서 확인해보니 아래와 같이 HAProxy Service가 실패하였다.
실패 원인
haporxy는 haproxy.cfg에 있는 VIP를 바인딩해야하는데 VIP가 현재 로드밸런서에 없으므로 오류를 발생시킨다. (오류 내용 - 프록시 : 소켓을 바인드 할 수 없음)
Master -> Slave로 FailOver하는 경우 Keepalived에 의해 VIP가 옮겨간다. Slave에서 서비스 구동을 하다가 Master가 정상으로 돌아오면 Keepalived는 가중치에 의해 다시 Master가 트래픽을 받도록 변경한다.
이 상태에서 VIP도 Master로 옮겨오는데 Keepalived가 VIP를 Master의 네트워크 인터페이스에 바인딩하고 HAProxy가 서비스가 실행되면 커널파라미터에 상관없이 HAProxy에서 VIP를 네트워크 인터페이스에서 인식하여 정상적으로 FailBack이 된다.
# vim /lib/systemd/system/haproxy.service
위 방법은 Service의 Boot Type에서 notify를 idle로 변경한 것이다.
notify의 경우 simple과 동일하나 unit이 구동되면 systemd에 시그널을 보낸다. (unit이 시작된 즉시 systemd는 유닛의 시작이 완료되었다고 판단한다. 그런데 다른 유닛과 통신하기 위해 소켓을 사용하는 경우 이 설정을 사용하면 안된다.)
HAProxy의 경우 Keepalived와 통신하기 때문에 HA를 구성하기 위해서는 Type이 notify가 되어서는 안된다. 이런 부분을 해결해주는 것이 net.ipv4.ip_nonlocal_bind 파라미터이다.
Master에서 Type을 notify -> idle로 변경하였을 경우 net.ipv4.ip_nonlocal_bind=0 이더라도 정상적으로 FailBack이 진행된다.
하지만 Slave의 경우 Unit의 Type을 변경하더라도 서비스는 실패가 된다. (실패한 이유는 Keepalived의 VIP가 Master에 있기때문에 통신할 소켓이 없어서 발생한다. 네트워크 인터페이스의 바인딩 문제)
net.ipv4.ip_nonlocal_bind=1 일경우 Unit의 Type에 상관없이 HAProxy 서비스는 정상 시작된다.
2. 커널 파라미터 변경후 FailOver
net.ipv4.ip_nonlocal_bind=1 로 변경한 경우 정상적으로 FailOVer & FailBack이 진행된다. 다면 FailBack 과정에서 짧은시간동안 순단이 발생할 수 있으며 순단시에는 HTTP Code 503을 발생한 후 정상적으로 HTTP Code 200을 반환한다.
실제 운영환경에서는 자동 FailBack이 아닌 수동으로 FailBack을 진행하는 것을 권장한다. 잠시동안이라도 발생하는 네트워크 순단현상을 최대한 방지하기 위해서이다.
3. 정리
Master & Slave 구성에서 Master는 Service Unit의 Type 값을 변경해 정상적인 FailBack을 할 수 있고 Slave에서는 커널 파라미터를 변경하여 FailOver 환경을 만들수 있으나 정상적인 방법은 아니므로 기본 커널 파리미터 값을 변경하여 HA를 구성하는 것을 권장한다.
간단하게 설명하면 VIP (가상 IP)를 기반으로 작동하며 Master 노드를 모니터링하다가 해당 노드에 장애가 발생했을시 Stanby 서버로 Failover되도록 지원한다.
즉, Heartbeat 체크를 하다가 Master에 장애발생시 Slave로 FailOver하는 역할을 하는 것이다.
아래 그림은 keepalived의 구조이다.
LoadBalancing을 하기위해서 LVS (Linux Virtual Server)의 구성 요소인 IPVS를 사용한다.
Keepalived와 IPVS를 조합해 HAProxy와 같은 LoadBalancer를 사용하여 L7 LoadBalancing을 할 수도 있지만 Keepalived에서 제공하는 LoadBalancing 기능을 사용하여 L4 LoadBalancing을 할 수도 있다.
Keepalived는 HA를 하기위해 VRRP Protocol을 사용하며 LoadBalancer 설정을 위한 LVS와는 독립적으로 동작한다.
Keepalived는 LoadBalancing 쪽에서 사용되는 LVS(IPVS)와 NAT, Masquerading에 쓰이는 Netfilter와 VIP 할당 및 해제에 쓰이는 Netlink, VRRP Advertisement 패킷 전송을 위해 사용되는 Multicast와 같은 Kernel 컴포넌트로 구성되어 있다.
Keepalived의 HA는 자신의 IP 주소와는 별개로 VIP를 설정해두고 문제가 생겼을때 이 VIP를 다른곳으로 인계하여 같은 IP주소를 통해서 서비스가 지속되도록 해주는것이 핵심인데 이 부분은 Keepalived가 VIP 할당 및 해제를 자동으로 해주기 때문에 별도의 설정을 하지 않아도 문제가 없다.
HA구성에서 Master의 장애인지 검출할 수 있는 방법은 아래와 같다.
ICMP (L3)
ping을 사용하는 것이다.
네트워크 구간이 정상이고 서버가 살아있다면 ICMP echo 요청에 대한 응답이 돌아올 것이지만 ICMP 패킷이 바이러스 침투나 공격으로 사용되는 사례가 많기때문에 ICMP를 차단하는 경우가 많아 잘 사용하지 않는다.
TCP 요청 (L4)
서비스를 올린 후 방화벽 허용 확인을 위해 telnet 명령을 실행해 정상 체크를 하기도 한다. 이는 TCP 요청이 정상적으로 응답하는지 확인하는 방법이다.
서비스가 살아 있어 Port Listen은 하고 있지만 서버나 프로그램의 오류가 있어 HTTP 500 Code를 반환하는 경우 정확한 확인이 불가능한 단점이 있다.
HTTP 요청 (L7)
실제 실행중인 서비스에 Heathcheck를 위한 Endpoint를 두고 서비스에 요청을 날려 200 OK가 반환되는지 확인하는 것이다.
HTTP 요청 자체가 ICMP나 TCP 요청에 비해 무겁기 때문에 고려가 필요하다.
Keepalived는 Heathcheck를 위해 TCP, HTTP, SSL, MISC와 같은 방법을 사용한다.
TCP_CHECK
비동기방식으로 Time-Out TCP 요청을 통해 장애를 검출하는 방식이다.
응답하지 않는 서버는 Server Pool에서 제외한다.
HTTP_GET
HTTP Get 요청을 날려 서비스의 정상 동작을 확인한다.
SSL_GET
HTTP Get과 같은 방식이지만 HTTPS 기반이다.
MISC_CHECK
시스템상에서 특정 기능을 확인하는 Script를 돌려 그 결과가 0인지 1인지를 가지고 장애를 검출하는 방법이다.
네트워크가 아니라 시스템상에서 돌고 있는 서비스의 정상 동작을 확인하는데 유용하다.
Keepalived 구성
환경
VitualBox
OS : Ubuntu 18.04
VIP : 192.168.219.119
Active : 192.168.219.120
Stanby : 192.168.219.121
1. keepalived 설치 (Active & Stanby)
# apt update
# apt install -y keepalived
2. keepalived 설정 (Active & Stanby)
2.1) Active Node 설정
# vim /etc/keepalived/keepalived.conf
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 200
advert_int 1
authentication {
auth_type PASS
auth_pass 7388
}
virtual_ipaddress {
192.168.219.119
}
}
2.2) Stanby Node 설정
vrrp_instance VI_1 {
state BACKUP
interface eth0
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 7388
}
virtual_ipaddress {
192.168.219.119
}
}
3. 테스트
3.1) Fail Over Test
# ping 192.168.219.119
3.2) Active Node Poweroff
# poweroff
3.3) Fail Over Log Check
Active Node가 Poweroff 되자 Keepalived 데몬이 VIP (192.168.219.119) 를 Stanby Node에 인계하여 FailOver를 하였다.