이 글은 특정 구현에 종속되는 내용을 제외한 이론 위주의 정리 글입니다.


Redis Persistence

Redis는 가지고 있는 데이터를 장애 발생 시 복구할 수 있도록 데이터를 영속화하는 방법을 제공합니다.

Redis 데이터 영속화는 관리하는 비즈니스 데이터의 성향과 중요도에 따라 사용하거나, 하지 않을 수 있습니다. 상황을 고려하지 않고 AOF의 Always 전략 등을 사용하는 행위는 Redis의 성능 하락과 응답 지연 문제를 심화시킬 수 있기 때문에 잘 고려하여야 합니다.

 

영속화를 사용하는 경우 Redis Server의 서비스 중단을 방지하기 위해 Child Background Process를 사용하는 것이 필수적이며, Process fork로 인한 메모리 사용량, 발생하는 Disk I/O 빈도와 저장된 파일 용량 등을 확인하여야 합니다.




RDB : Redis Database Backup

현재 Redis의 메모리에 존재하는 데이터의 스냅샷을 남기는 방식입니다. 즉 key와 value 형태의 값을 그대로 기록하는 방식입니다.

Key1 -> value
count -> 22


이 방식은 AOF 파일보다 작은 용량, 짧은 쓰기 지연 시간과 데이터 복구 시간을 가지고 있는 점이 특징입니다. Redis Replication 수행 시 사용되기도 하는 포맷이며 기본적으로 Origin에서 생성된 RDB 파일을 Replica node에게 전송하고, Replica에서는 임시 RDB 파일을 만들어 파일에 쓴 다음 동기화하는 방식을 취하고 있습니다.

  • RDB 파일은 AOF 파일보다 데이터 유실이 발생하기 쉽습니다. 스냅샷은 일정 주기마다 기록되기 때문에 최대 지정된 주기 만큼의 데이터를 유실할 가능성이 있습니다.
Replication 부하를 줄이기 위해 RDB를 Replica가 설치된 서버에서 수행할 수도 있습니다.


redis-conf에서 save 속성을 설정한 대로 RDB를 주기적으로 수행하며 필요하다면 SAVE, BGSAVE Command를 통해 수동으로 수행할 수 있습니다.

  • SAVE를 사용한 경우에는 모든 사용자 요청 및 작업을 중지하고 현재 동작 중인 프로세스(main)에서 RDB 파일을 생성하게 됩니다. - 비권장
  • BGSAVE를 사용한 경우에는 현재 프로세스는 계속 요청을 처리하고 fork를 통해 자식 프로세스를 생성하여 RDB 파일을 생성합니다. - 권장

 

BGSAVE 방식으로 fork를 진행한 후 데이터를 기록할 때 Linux의 특성상 COW에 의해 메모리 사용량이 최대 2배까지 늘어날 수 있습니다.

이는 Process fork를 통해 동작하여 처음에는 부모와 자식 프로세스가 동일한 메모리 영역을 공유하고 있지만, 부모 프로세스에서 데이터를 변경하면 해당 데이터는 다른 메모리 영역으로 복사되어 “부모에게만 참조되는” 상황 때문입니다.


Redis의 기본 설정에 의해 BGSAVE Command를 통한 RDB 작업이 실패한 경우 Redis Server가 Client의 쓰기 요청을 처리하지 않는 상황이 발생합니다. 이를 방지하기 위해 stop-writes-on-bgsave-error 설정 값을 no로 설정하여야 합니다.

  • no로 설정한 경우에는 Client 요청을 정상적으로 처리하게 되고 실패한 RDB를 다시 수행하지 않습니다.
  • 디스크 용량이나 깨짐, 사용 권한 등의 문제로 RDB가 실패할 수 있음을 인지하고 있어야 합니다.

관련 속성에는 dump 파일 생성 시 압축하도록 하는 rdbcompression (bool), 데이터 유효성 검사가 진행되도록 하는 rdbchecksum (bool), 파일 이름을 지정하는 dbfilename과 파일 저장 위치를 지정하는 dir 속성이 있습니다.



AOF : Append Only File

데이터를 변경하는 모든 Redis Command를 파일에 저장하는 방식입니다. Client에 의해 수행되었던 모든 데이터 추가, 수정 Command를 저장하고 또한 Text 파일 형태로 제공하여 쉽게 식별 가능하고 활용할 수 있기 때문에 장애 발생 시 최신 데이터를 복구함에 있어서 많은 이점을 가지는 방법입니다.

  • 커지는 파일 용량을 잘 고려해야 하며, Disk I/O 횟수나 비용으로 인해 큰 성능 저하를 일으킬 수 있는 방법입니다.
set key1 hello
set key1 value
set count 22
del key2

RDB 동작보다 우선순위가 높아 먼저 수행되고, 매 요청마다 수행됩니다. Client가 업데이트 명령을 요청하면 Redis는 먼저 해당 명령을 파일에 저장하고 파일 쓰기가 완료되었을 때 해당 명령을 실행해서 내용을 추가합니다.

이 방식은 redis-conf에 appendonly를 설정하면 수행하며, 필요하다면 BGREWRITEAOF Command를 통해 수동으로 AOF 파일을 재 작성할 수 있습니다.

또한 Append라는 의미에 맞게 매번 파일에 수행된 Command와 Metadata를 추가합니다. 그렇기에 파일 용량이 시간이 지남에 따라 계속 커지게 됩니다. 만약 OS에서 설정한 파일 용량 임계치를 넘어선다면 AOF 파일 기록이 중단되거나 Redis Server 재 시작 시 I/O 핸들러를 열거나 읽어오는 시간 등 때문에 서비스 제공 시간이 지연될 수 있습니다.

이를 방지하기 위해 rewrite를 지속적으로 수행하여야 하는데 이 작업 또한 Disk I/O 작업이기 때문에 Redis 지연 이슈를 발생시킬 수 있습니다.

  • 위에서 설명한 appendfsync 전략과 auto-aof-rewrite-percentage, auto-aof-rewrite-min-size 속성을 조정하여 이를 알맞게 구성하여야 합니다.


AOF disk write(fsync, fdatasync) 전략에는 Always, Everysec, No가 존재합니다.

  • Always는 write를 수행할 때마다 fsync를 수행하는 방식으로 데이터를 완전히 보전하기 위해 사용하는 전략입니다. 쓰기 방식 중 성능이 제일 좋지 않아 DISK base DBMS를 사용하는 것과 비슷한 처리 성능을 가지게 됩니다.
  • Everysec는 매 초마다 fsync를 수행하는 방식으로 성능과 데이터 보존을 어느 정도 지킬 수 있는 최선의 선택입니다. (권장 방식)
    • 해당 AOF를 사용하고 있다면 최대 1초의 데이터 유실이 발생할 수 있습니다.
  • No는 Redis가 아닌 OS가 fsync를 수행하는 방식으로 Linux의 경우 30초마다 동기화를 수행합니다. 이때 Linux는 fsync가 아닌 fdatasync 방식으로 수행하는데 이는 파일 접근 시간, modify time, 파일 용량 등의 Metadata를 저장하지 않는 방식으로 더 빠른 성능을 보장합니다.



RDB의 경우 백업은 필요하나 데이터 손실이 발생해도 무관한 경우에 활용할 수 있고(단순 콘텐츠 캐싱 등), AOF의 경우 장애 상황 직전까지 모든 데이터가 보장되어야 할 중요한 비즈니스 데이터를 다룰 때 적용하게 됩니다.

비관적으로 한 작업의 실패를 가정하는 경우 RDB & AOF를 동시에 사용하여 안전하게 갈 수도 있습니다.



 

운영 시 주의할 점

비어있는 Master와 데이터를 가진 Slave가 서로 간의 Replication을 수행하지 않도록 할 것

Master와 Slave 사이의 최초 동기화 시점에는 서로 간의 데이터 Sync를 맞추려는 작업을 수행하게 됩니다. 만약 Master가 장애 발생으로 인해 아무 데이터도 없이 복구 조치 되었다면, 이와 연결된 Slave에도 emptyDB라는 함수가 호출되어 가지고 있는 모든 정보를 정리하게 됩니다.

// Sync Process() 
1. main() 
2. initServer()          // network 설정 수행 
3. serverCron()          // 종료, 에러 처리 수행 - Timer 기반, replicationCron() 호출 
4. replicationCron()     // Master와의 Connection 상태 확인 후 복원 수행, connectionWithMaster() 호출 
5. connectionWithMaster()// Replication이 설정된 경우 수행, Master와 실제 연결 시도 
5-1. syncWithMaster()    // Auth 수행 및 Sync 명령 전달, 임시 RDB 파일 생성 
6. readSyncBulkPayload() // Master에게 데이터를 받을 때 수행, sync 완료 후 emptyDB() 호출 
7. emptyDb()             // 현재 메모리 정리 
8. readSyncBulkPayload() // 5-1에서 만든 임시 RDB 파일을 메모리에 로드

이러한 Process가 수행되지 않도록 방지하기 위해 Master에 장애가 발생함을 감지하였을 경우, 해당 Master와 Replication을 수행하던 Slave Node를 Master로 승격 시켜 이러한 문제를 방지하는 것이 좋습니다.

  • Redis 5.0 미만 버전이라면 SLAVEOF NO ONE을 호출합니다.
  • Redis 5.0 이상 버전이라면 REPLICAOF NO ONE을 호출합니다.

 

Linux kernel에 THP 설정이 적용되어 있는지 확인할 것

THP란 대용량 메모리를 사용하는 Linux에서 관리되는 page의 양이 많아져 CPU가 메모리 요청을 할 때 Lookup 하는 TTB의 크기와 TLB 크기가 커지고 Hit ratio가 줄어들어 것을 메모리 접근에 대한 Overhead가 발생하는 상황을 개선하기 위해 추가된 기능입니다. 

 

기본적으로 제공되는 Page Size가 4K라면 해당 설정이 활성화된 상태에선 2MB 혹은 1GB 크기의 Huge page를 동적 할당하도록 변경되는데, CentOS / RHEL 6 이상 버전에는 기본적으로 적용되어 있다고 합니다. 

패치가 되었을지 확인은 못해봤네요

 

하지만 해당 설정은 Redis 운영에 있어서 치명적인 문제를 일으킬 수 있습니다.

  • Redis에서 메모리에 가지고 있는 데이터와 상관없이 큰 사이즈의 Page를 할당하여 Linux Host OOM을 유발할 수 있고 Redis Process의 oom_score 점수를 높일 수 있어 OOM-Killer에게 제거되는 상황이 발생할 수 있습니다.
  • 또한 Fork system call의 지연 시간을 증가시키고 앞서 서술했던 COW 문제를 심화시킬 수 있습니다. Persistence 설정으로 인해 BGSAVE 등이 호출되어 Process Memory를 fork 한다면 대량의 메모리를 2개의 child process가 참조하게 되어 이를 작업하는 시간이 오래 걸리고, 활발한 쓰기 요청을 처리하고 있는 Master Node라면 빈번한 메모리의 재할당 / 참조가 발생하여 정말 큰 메모리 공간을 점유할 수 있습니다. (Node max memory + 1GB[최대 페이지 크기]) x 2

 

해당 설정이 활성화 되었는지에 대한 여부는

// 활성화 시 [always], 비 활성화 시 [never]
/sys/kernel/mm/transparent_hugepage/enabled

파일 정보를 통해 확인할 수 있습니다.

 

해당 내용을 찾아보면서 THP의 이점을 누리려면 Redis 시작 시 최초 메모리 할당 이후에 재 할당이 일어나지 않아야 될 것같다고 생각이 들었는데 그것이 어떤 상황일지 궁금했습니다. 고정된 인원에 대한 리더보드를 관리하는 Redis나 Hyperloglog 등을 사용해 집계를 수행하고 일정 주기로 데이터를 삭제하는 느낌일 것 같은데, 실제로 사용하고 계신 분이 있다면 댓글로 남겨주시길 부탁드립니다! ㅎㅎ

 

 

참고 자료

 

 

이 글은 특정 구현에 종속되는 내용을 제외한 이론 위주의 정리 글이며 기존에 작성했었던 CI / CD 글을 수정한 것입니다.

 

 

 

자동화되지 않은 개발 프로세스의 문제점

상용 서비스, 소프트웨어는 일반적으로 여러 명의 개발자가 협업을 진행하기 때문에 코드의 관리와 배포 후 발생하는 장애 대처 용도로 Git과 같은 버전 관리 도구를 사용하여 각 코드에 대한 이력과 메시지를 남기고 병합하는 과정을 거치게 됩니다.

 

이러한 프로세스를 계속 수행되다 보면 기존 코드(테스트, 스키마 설정 값, 충돌)와 문제가 발생할 수 있지만 이를 빨리 식별하지 못해 운영 환경에서 서비스 장애가 발생하는 경우가 있을 수 있습니다.

 

또한 개발, 운영팀이 수동으로 코드를 빌드하고 테스트 및 FTP 등의 프로토콜을 통한 패키지 전달 및 실행이라는 하나의 배포 흐름을 수작업으로 수행하게 되어 주기적으로 팀의 리소스가 낭비되게 됩니다. (인력, 시간, 돈 등)

  • 이는 서비스의 크기가 커짐에 따라서, 하나의 서비스가 여러 개의 마이크로 서비스로 분리된 상황이나 사용자 트래픽의 급증으로 인해 빨리 서버를 증설해야 할 때 더욱 큰 문제를 야기할 수 있습니다.

 

 

CI / CD 란?

예시로 만든 이미지이며 우측 상단에 있는 이미지는 https://www.plutora.com/blog/understanding-ci-cd-pipeline

 

Application 개발 배포 프로세스를 자동화하여 다음 버전을 짧은 주기로 배포하여 사용자에게 제공하는 방법 또는 시스템을 의미합니다. 이를 통해 사용자의 피드백을 받는 주기도 짧아짐으로써 더 나은 서비스를 제공하는데 도움이 됩니다.

  • Application Life Cycle 전체에 걸쳐 지속적인 자동화와 모니터링을 제공한다.
  • CI/ CD를 도입함으로써 개발자들은 비즈니스 요구 사항, 코드 작성에만 집중할 수 있다.
  • CI/ CD 파이프 라인에서 실행되는 회귀, 성능, 유닛, 통합 테스트 등 지속적인 테스트 커버리지와 검증을 요구함으로써 좀 더 안전한 서비스를 제공할 수 있는 여지가 있다.

 

 

 

CI : Continuous Integration

자동화된 프로세스를 통해 애플리케이션 코드 변경 사항이 정기적으로 빌드 및 테스트(유닛, 통합)되어 공유되는 Repository에 병합되는 것을 의미합니다.

  • 개발자 간의 코드 충돌을 방지하기 위한 목적을 가지며, 기존 코드와 신규 코드의 충돌이 발생하면 빠르게 발견하고 수정할 수 있는 식별성을 지닙니다.
  • 통합 프로세스의 자동화와 일관성을 제공하여 사용되는 리소스를 줄이고 요구 사항에 따른 코드 변경을 더 자주 수행하게 끔 촉진합니다.

 

 

 

CI의 필수 구성 요소

  • Repository management
    • CI를 도입하기 위해서는 Version control 도구와 Git-flow와 같은 Branch 전략은 필수적인 요소입니다. Local, Remote, Origin Repository를 각각 관리하여 conflict가 발생할 여지를 줄이고 branch check-out기능과 Commit을 통해 사용 가능한 코드의 식별 및 버전, 이력 관리를 수행해야 합니다.
  • Build-automation
    • 개발, 운영 팀은 Build script (Shell, python) 또는 표준화된 Build tool이나 Open-source (jenkins, gitlab, circle ci 등)를 사용하여 Code Build를 수행해야 하며, 이전 Build version에 대한 백업 버전을 관리하여 예기치 못한 상황에서 복구할 수 있는 여지를 남겨 놓아야 하고 이 모든 것은 자동적으로 수행되어야 합니다.
  • Self-test (Test-automation)
    • 빠른 수정과 장애 상황 방지를 위해서는 기존에 작성된 Unit Test나 Sonarqube와 같은 Third-part library 들을 연동하여 자동화된 테스트 수행 및 커버리지 관리가 되어야 합니다.
  • Source code versioning
    • 정기적인 Source commit을 통해 개발자 간의 이슈 최소화와 작업 상황에 대한 공유가 지속적으로 이루어져야 하고 Commit을 통한 Build를 수행하여 지속적인 Test Cycle을 수행하여야 합니다.

 

 

 

CD : Continuous Delivery / Deployment

해당 용어는 지속적인 서비스 제공 혹은 지속적인 배포를 의미하며, 이 두 가지 용어는 상호 교환적으로 사용됩니다. 최종적으로는 소프트웨어나 서비스를 좀 짧은 주기로 배포, 반영하는 것을 목표로 합니다.

 

이를 통해 지속적으로 발생하는 피드백, 신규 요구 사항에 대해 유기적으로 대처할 수 있습니다.

 

 

Continuous Delivery

지속적인 제공은 개발자들이 적용한 변경 사항이 자동화된 프로세스(Pipeline)를 통해 빌드, 테스트를 거쳐 Repository에 자동으로 업로드되는 것을 뜻하며 운영팀은 이 Repository의 최신 상태를 운영 환경에 실시간으로 빠르게 적용할 수 있습니다.

  • 최소한의 리소스로 새로운 코드를 빠르게 수정하고 온전히 통합하는 것을 목표로 합니다.
    • 각 변경 사항은 작은 단위로 최소화하여 문제가 발생하였을 때 확인해야 하는 영역을 줄입니다.
    • Build, Test, Integration 프로세스를 계속 확인할 수 있으며 문제가 발생한 부분을 빠르게 식별합니다.

 

 

Continuous Deployment

지속적인 배포는 변경 사항이 생긴 Repository에서 고객이 사용하는 환경이나 테스트를 위한 스테이지 서버로 자동으로 배포, 출시하는 것을 뜻하며 이는 지속적인 수작업으로 인해 개발 팀이나 운영 팀의 리소스 소모 또는 프로세스 병목이 발생하는 것을 방지합니다.

  • 최소한의 리소스로 변경된 사항을 빠르게 각각의 환경(Dev, Test, Stage, Production)으로 출시하고 검증하는 것을 목표로 합니다.
  • 통합 테스트, QA, 배포 단계를 자동화하여 사용되던 리소스를 최소화하기에 소프트웨어나 서비스에 집중할 수 있는 리소스가 남게 됩니다. 이는 결국 실제 Code와 Test를 작성하는 것에 집중할 수 있고 Legacy code를 Refactoring, Restructuring 할 수 있는 시간을 만듭니다.

 

 

 

CI / CD Tools

CI / CD를 구성하기 위해 사용하는 도구들을 나열합니다.

  • Source code management tools
    • Git, AWS Code Commit, SVN, Kraken, Source tree
  • Build management tools
    • Maven, Gradle, PyBuilder, make, ANT
  • Test automation, quality tools
    • Selenium, Junit, Cucumber, Sonarqube, Jacoco
  • CI tools
    • Jenkins, Jenkins X, Gitlab, Circle CI, AWS Code Build, Bamboo, Hudson..
  • CD tools
    • Argo CD, AWS Code Deploy..
  • Configuration, Provisioning tools
    • Ansible, Puppet, Chef..
  • Monitoring tools
    • AWS Cloud Watch, Nagios, Ganglia, Sensu..

 

 

도구마다 사용법이나 정의하는 문법 등이 다르기에 도구의 어떠한 점이 중요한 것인가 (Build 서버 구축 여부, Plugin 제공 여부나 생태계의 크기 등)를 고민해보고 있습니다만 우선은 회사에서 사용하는 것이라도 열심히 해야겠습니다. ㅎㅎ 

  • 현재 회사에서는 Gitlab-CI의 Docker base Runner 타입으로 사용하고 있으나 따로 CD가 구성되어 있지는 않습니다. 상황에 알맞은 CD 방법을 고려해보고 도입하는 게 중요한 상황인 것 같습니다.

 

 

참고 자료

 

이 글은 특정 구현에 종속되는 내용을 제외한 이론 위주의 정리 글입니다.

 

 

 

복제가 필요한 이유?

우리가 제공하는 API 서비스는 일반적으로 Stateless 한 형태를 준수하여 개발하여야 합니다.

 

이는 State를 다른 서비스(RDBMS, Global Session Storage - Redis 등)에서 관리하게 하고, API 서비스는 필요에 의해 조회하도록 하여 Stateful로 개발하였을 때 발생하는 API 서비스 간의 상태 동기화 이슈나 확장성 저하 문제를 최소화할 수 있기 때문입니다.

 

이러한 구조는 트래픽 급증에 대응하는 Scale-out을 가능케 합니다.

 

 

이런 형태로 개발하게 되면 결국 다수의 API 서비스가 조회하는 State를 관리하는 서비스에는 대량의 부하가 전달되어 자연스레 해당 서비스에 장애가 발생할 확률이 증가하게 됩니다. 이때 저장되어야 할 데이터가 유실되거나 (Transaction 실패, Connection Timeout 등) 또는 서비스가 중단된다면 이는 전체 서비스에 영향을 미치는 큰 문제가 됩니다. (SPoF)

 

이를 대비하기 위해 우리는 High Availability 이라는 속성을 만족하도록 서비스 설계를 진행하여야만 합니다. 그리고 이 속성을 지원하는 설계 방법 중 하나로는 복제 구조가 있습니다.

 

 

 

 

복제?

복제란 기본적으로 네트워크로 연결된 여러 물리 장비에 동일한 데이터를 분산 및 복사하여 저장하는 것을 의미합니다.

 

이를 통해 우리는 서비스 데이터를 좀 더 안정적으로 보관할 수 있으며, 다른 네트워크 간의 복제 시 특정 네트워크의 전체 장애를 대비할 수 있고 또한 동일한 데이터를 가지고 있기 때문에 이를 활용하여 서비스 API의 조회 요청을 여러 장비로 분산시킬 수도 있습니다.

  • 이렇게 서비스 API의 조회 요청을 여러 장비(RDBMS, MongoDB, Redis 등)로 분산 시키는 행위는 Read Query Load Balancing 또는 Query Off Loading이라고 합니다.
이후 설명에서는 Master - Slave 용어가 특정 시기의 차별을 나타냄으로 이제 사용되지 않기에 Origin - Replica로 대체하여 작성하도록 하겠습니다.

MongoDB나 특정 Opensource 에서는 Primary - Secondary로 명명하기도 합니다.

 

 

 

 

복제 구현 방식?

복제 로그 구현 방식으로는 크게 Statement based, Write-ahead logging based, Row based log 3가지가 존재합니다.

 

 

Statement based는 Origin 모든 쓰기 요청의 구문을 특정 영역(버퍼, 큐 등)에 기록하고 write를 수행한 다음 Replica에게 해당 기록을 전송하는 방식입니다.

 

Origin이 INSERT, UPDATE, DELETE 구문 수행 기록을 전송하면 각 Replica는 이를 파싱 하고 실행하여 보유한 데이터를 동일한 상태로 만들어 갑니다.

  • 식별 값 자동 생성, 노드가 보유한 데이터를 의존하는 처리 (UPDATE ~ WHERE) 등은 각 노드마다 정확하게 같은 순서로 실행되어야만 합니다. 인과성의 보장 필요
  • 이 방식의 장점은 복제하는 데이터가 경량이기에 Network I/O 비용이 감소하고, 감사 등의 운영 상 이슈가 발생하였을 때 대처하기 좋다는 장점이 있습니다.
  • 이 방식의 단점은 현재 날짜나 시간, 임의의 숫자를 구하는 비 결정적 함수 혹은 요청에 대해서 각 Replica마다 다른 결과를 얻고 반영하여 데이터 일관성 문제가 발생할 수 있다는 점입니다.
또한 부수 효과를 일으키는 요청 (Trigger, Procedure) 또한 결과가 비 결정적이라면 다른 결과를 얻을 수 있습니다.

 

 

이러한 문제를 해결하기 위해 Origin이 구문을 기록할 때 모든 비결정적 함수 호출과 부수 효과 요청에 대해서 고정 값을 반환하게 끔 할 수 있으나, 여러 예외 케이스가 있기 때문에 일반적으로 다른 복제 방법을 사용하는 것을 선호합니다.

  • MySQL에서는 지속적으로 구문 기반 복제를 사용해왔으며 사용된 구문에 비 결정적 함수가 존재한다면 논리적 로그 기반 복제로 변경하여 사용도록 되어 있습니다.
  • VoltDB도 구문 기반 복제를 사용하고 있지만, MySQL과 다르게 트랜잭션 단위를 결정적이 되게 끔 제약하여 문제를 해결하고 있습니다.
  • Redis 또한 구문 기반의 복제를 사용하고 있습니다.

 

 

 

 

Write ahead logging based는 말 그대로 저 수준의 로그인 WAL을 활용하여 Replica에도 동일한 복제 데이터를 구축할 수 있다는 점을 이용하는 방법입니다. 이 방식은 Postgresql과 Oracle 등에서 사용하고 있습니다.

Origin는 모든 write 요청에 대해서 WAL을 기록하고 Replica에게 Network I/O를 통해 전송합니다. 그리고 Replica는 전송받은 WAL을 그대로 반영하여서 최종적으로 Origin와 동일한 데이터를 구성하게 됩니다.

  • 이 방식의 장점은 Replica에 데이터를 복제할 때의 성능이 다른 방법보다 매우 빠르다는 점입니다. 또한 로그는 인과적으로 저장되는 것을 보장하기 때문에 WAL 동기화 비용 또한 최소화됩니다.
  • 이 방식의 단점은 WAL가 코어 수준에서 사용되는 데이터를 기술하기 때문에 저장소에서 사용하는 엔진에 대한 버전 의존성이 생겨 다른 버전을 사용하는 Origin, Replica 간의 복제가 거의 불가능하다는 점입니다.
이러한 제약 때문에 버전 업데이트 시에는 Replica를 우선 업데이트한 뒤 Origin를 업데이트해야 합니다

 

 

 

 

Row based log는 WAL에서 발생하던 문제점인 저장소 엔진 의존성을 제거하기 위해 구상된 방법으로 추상화된 다른 형태의 로그를 활용하여 데이터를 복제하도록 구현되어 있습니다. 일반적으로 로우 단위로 구성된 데이터베이스 테이블을 기준으로 write 로그를 작성하여 모든 칼럼에 속한 값을 같이 로그로 남깁니다.

 

이렇게 논리적 로그를 데이터와 WAL 중간에 추가함으로써 엔진 간의 하위 호환성을 유지하는 것이 수월해졌으며, 다른 버전이나 다른 저장소 엔진 기반으로 동작하는 노드와의 복제 또한 가능하게 되었습니다.

또한 기록된 로그를 파싱을 하는 것도 수월하여 분석, 로그 시스템, 외부 시스템에 내용 전송 등 여러 측면에서 유용한 방식입니다.

 

 

MySQL에서 사용하는 이진 로그 복제는 해당 방식을 활용합니다.

  • 이 방식의 장점은 WAL의 장점을 대부분 수용하면서도 하위 호환성을 지키고, 복제할 수 있다는 점입니다.
  • 이 방식의 단점은 Origin과 Replica의 테이블 Schema가 동일하여야 한다는 점, 실제 값을 복제하기 때문에 반영하는데 상대적으로 지연 시간이 발생하는 점 그리고 특정 기록이나 대용량의 로그를 반영하기 위해 Bin log로 쓰는데 동시성 문제가 발생할 수 있다는 점입니다.

 

이 방식에서 제공하는 로그 정보는 이렇게 기록됩니다.

  • 삭제된 Row based log는 고유하게 식별하기 위한 메타 데이터를 포함하는데 일반적으로 PK를 사용하고 PK가 없다면 모든 칼럼의 예전 값들을 Logging 하는 방식으로 구현됩니다.
  • 갱신된 Row based log는 모든 칼럼의 새로운 값과 식별하는데 필요한 메타 데이터를 포함하여 기록됩니다.
  • 여러 Row를 동시에 수정하는 Transaction의 경우 우선 그 Row 수만큼 로그 레코드를 생성한 다음 해당 Transaction이 커밋되었음을 나타내는 정보를 같이 표시합니다.

 

 

 

복제 동기화 방식?

복제 동기화 방식으로는 크게 Synchronous, Semi-synchronous, Asynchronous 3가지가 존재합니다.

 

 

Synchronous Replication은 Origin에 데이터를 반영한 직후나 반영하기 전에 Replica node에 먼저 복제 요청을 전송하여 정상 수신이 되었을 때 이후 처리를 진행하는 방식입니다. 이를 통해 분산 저장된 데이터의 일관성과 무결성을 지키는데 집중합니다.

  • 이 방식의 장점은 분산된 장비 간의 데이터 일관성과 무결성을 가지는 것을 보장한다는 점이며, Origin가 작동하지 않는 상황에서도 Replica를 통해 올바른 데이터를 사용할 수 있음을 알 수 있다는 것입니다.
  • 이 방식의 단점은 네트워크 지연 문제, Replica의 처리 지연, 장애 발생 등으로 인해 확인 요청에 대한 응답을 하지 않는다면, 새로운 데이터를 쓰는 것이 불가능한 상태에 빠진다는 점입니다. 이때 기본적으로 Origin은 Replica가 응답하기 전까지 Blocking 상태에 빠집니다.
해당 구조에서는 Origin에 장애가 발생하지 않더라도 Replica 문제로 인해 이를 바라보고 Write 하는 모든 서비스로 장애가 전파될 수 있습니다.

 

 

또한 Scale-out이 될수록 발생하는 응답을 수신하는 지연 시간이 길어지고 위와 같은 상황이 발생할 확률이 증가하여 서비스의 확장성이 매우 떨어지게 됩니다.

 

 

 

Semi-synchronous Replication은 Synchronous Replication을 현실적으로 풀어내는 방법 중 하나입니다. 해당 방식은 한 시점에 최소한 하나의 Replica와 Synchronous Replication를 진행하는데, 해당 Replica가 추가된 Transaction을 반영하면 Origin에 해당 내용을 반영합니다. 이를 통해 최소한의 데이터 무결성을 보장합니다.

  • 그 외 Replica들은 Asynchronous Replication을 수행하는 방식이며, Synchronous Replication를 진행하는 대상은 현재 (Synchronous Replication) 대상에 장애 혹은 처리 지연이 발생하여 변경될 수 있습니다.
  • 이 방식의 장점은 적어도 하나 2개의 서비스에서 데이터 일관성과 무결성을 보장한다는 점이며 그러면서도 Synchronous Replication의 단점을 최소화한다는 것입니다.
  • 이 방식의 단점은 최신의 데이터를 가진, 즉 데이터 일관성과 무결성을 보장하는 Replica가 Synchronous Replication을 수행 중인 Replica가 아닐 수 있다는 점이며 또한 여전히 다른 Replica와 Origin 간의 데이터 불일치가 발생한다는 점입니다.

조회하는 Node에 따라 데이터 일관성 깨질 수 있는 문제는 여전하지만, 현재 Node 개수와 대비하여 읽어올 읽기-쓰기 Node 개수를 조정하면 최종적으로 지연 시간과 일관성을 보완할 수 있습니다.

또한 Synchronous Replication을 통해 데이터를 넘겨받는 Node가 꼭 최신 상태가 아닐 수 있습니다. MySQL의 Bin Log를 활용한 Semi-Sync Replication은 모든 Replica가 동일한 Bin log를 바라보고 있고, 결국에는 동일한 Transaction을 같은 시점에 수신받을 수 있기 때문에 Asynchronous Replication을 수행하는 Replica가 오히려 먼저 최신 상태에 도달할 수 있습니다.

기타 설명된 동작은 구현 방식에 따라 달라질 수 있습니다.

 

 

 

Asynchronous Replication은 Origin에 반영된 데이터 정보를 Replica들이 지속적으로 수집하여 반영하는 High Availability에 좀 더 초점을 맞춘 방식입니다.

  • 이 방식의 장점은 서비스에서 요청한 데이터의 Write를 재시도를 통해 계속 보장할 수 있다는 점이며, Synchronous Replication에 비해 쓰기에 대한 서비스 지연 시간이 짧아지고 확장성이 좋다는 것입니다.
  • 이 방식의 단점은 Origin에 장애가 발생하였을 경우 네트워크 지연 등으로 인해 최신 데이터가 유실될 수 있다는 점과 Origin와 Replica 간의 데이터 일관성이 일정 시간이 지나야 만족된다는 점이 있습니다.(Eventual Consistency)
부가적으로 Asynchronous Replication을 수행하면서 새로운 Origin을 선출하였더라도 이전 Origin이 반영했던 혹은 반영 중이던 데이터를 온전히 가지지 못하게 됩니다.

 

 

이러한 문제를 해결하는 방법은 단순히 무시하고 넘어가는 것이지만 이 경우 서비스의 내구성이 매우 떨어지게 됩니다. 시스템의 확장성이나 HA가 높은 것은 좋지만 저장되어야 할 데이터의 유실은 도메인, 서비스에 따라서 심각한 문제가 되기 때문입니다.

 

그렇기에 지속적으로 좋은 성능과 HA를 제공하면서도 데이터 유실이 없는 복제 방법이 개발되고 있는 상황입니다. 이를 통해 성공적인 결과물로 구현된 방식 중 하나로는 체인 복제 방식이 있습니다.

 

 

 

 

기타

Trigger-Based Replication은 특정 데이터나 이기종 데이터베이스 간의 Replication을 위해 서비스에서 Oracle의 Golden gate, Batch Framework, 데이터베이스의 Trigger나 Procedure를 활용하는 방식입니다.

 

서비스에 복제를 위한 코드를 등록하고 데이터베이스 시스템에서 데이터가 변경이 있을 때 이벤트 등을 받거나 직접 실행하도록 하여 데이터를 가공하고 다른 서비스로 전달시킬 수 있습니다.

  • 이 방식의 장점은 해당 방식은 다른 복제 방식에 비해 유연하다는 점입니다.
  • 이 방식의 단점은 다른 복제 방식에 비해 많은 Overhead를 동반한다는 점이며, 이를 수행하는 서비스 또한 장애 지점이 될 수 있고, 작성된 코드를 통한 버그가 발생할 확률이 매우 높습니다.
Oracle의 Databus나 Postgresql의 Bucardo가 이러한 방식으로 동작합니다.

 

 

 

 

마치면서. 현재 제가 있는 팀에서는 Galera Cluster라는 Opensource를 도입하여 사용하고 있습니다. 이것은 설정에 따라 특정 동작에서 Semi-synchronous Replication과 Asynchronous Replication 방식을 지원하는 MySQL, MariaDB Cluster입니다. 요약하고 넘어가자면,

  • Multi Master 구조의 경우에는 데이터 일관성 유지를 위해 Certification-Based Replication를 사용하게 됩니다. 이는 Dead lock의 주범입니다..
  • Replica들은 기본적으로 Asynchronous Replication을 통해 데이터를 처리하다가 recv queue의 정해진 임계 값을 만족했을 때 해당 Replica와 Flow Control이라는 것을 발생시켜 이때에는 Asynchronous Replication를 중지하고 recv queue에 있는 것을 처리하는 방식으로 구현되어 있습니다.

 

Galera Cluster에서 설명하는 것을 보면 virtually synchronous replication을 수행한다고 합니다. 이는 논리적으로 완전 동기이나 write나 commit이 별개로 수행되고 replication은 비동기로 동작하기 때문에 그렇게 명명되었다고 하네요..

 

 

 

 

참고 자료

 

이 글은 특정 구현에 종속되는 내용을 제외한 이론 위주의 정리 글입니다.

 

 

 

장애 감지 알고리즘이 필요한 이유?

우리가 개발하는 서비스가 단일 시스템이 아닌 분산 시스템 구조를 지닌다면 특정 서비스에 장애가 발생하더라도 모든 서비스 노드에게 전파되지 않기 때문에, 사용자 요청으로 인해 식별되기 전까지 모를 수 있습니다.

 

장애가 발생한 서비스에 따라서 시스템에 잘못된 값을 설정하거나 중복이 발생할 수 있고 (Index Sharding Service) 사용자가 보는 캐싱된 데이터가 소실되는 경우도 발생할 수 있습니다. (Feed Cache Service 등)

  • 물론 모든 장애가 서비스 종료, 충돌과 같은 완전 중단 상태를 의미하는 것은 아니며, 상황에 따라 부하로 인한 응답 지연, 네트워크 지연 이슈 혹은 데드락으로 인한 것일 수 있습니다.(이런 상태는 시간이 지나면 해결되는 문제이기 때문입니다.) 그렇기에 좀 더 정확한 감지 방식이 필요합니다.
이러한 장애 상황을 빠르게 대처하기 위해 필요한 것이 장애 감지 알고리즘 혹은 감지 방식 입니다. 이러한 방식은 실제 구현하는 장애 감지 서비스뿐만 아니라, Redis, Zookeeper, Kafka, Cassandra, Akka 등의 Opensource 등에서도 사용되고 있습니다.

 

 

 

 

장애 감지 알고리즘의 속성?

장애 감지 알고리즘의 속성으로는 크게 완전성, 효율성, 정확성이 있으며 이중 효율성과 정확성은 사용하는 방법에 따라서 조정이 가능한 속성입니다.

특정 자료에서는 완성도와 정확도 만을 이야기하기도 합니다.

 

 

완전성

특정 서비스에 장애가 발생하면 모든 서비스는 해당 서비스에 장애가 발생한 사실을 인지하고 이와 관련된 처리를 수행하여 결과를 반환하여야 한다는 속성입니다.

 

 

효율성

얼마나 빨리 장애를 감지하는지 평가하는 속성이자 척도입니다. 이는 장애 상태에 빠진 서비스를 얼마나 빨리 감지하느냐 에 따라서 효율성을 결정하게 됩니다.

 

 

정확성

얼마나 정확하게 장애를 감지했는지 평가하는 속성이자 척도입니다. 이 속성은 위에서 잠깐 언급했던 것과 같이 정상적인 서비스이지만 다량의 요청을 처리하며 발생하는 부하, 네트워크 지연 이슈 혹은 데드락으로 인해 일정 시간 동안 응답하지 못하는 상태의 서비스를 장애로 판단하느냐 에 따라서 정확성을 결정하게 됩니다.

 

 

효율성과 정확성의 트레이드 오프

정확성을 설명하면서 장애 감지 시스템의 결과가 거짓일 수 있다는 점을 알려드렸습니다. 즉 정상 프로세스에 장애가 발생하였다고 잘못 판단하는 경우가 있는데 이러한 경우는 효율성이 높은 알고리즘에서 주로 발생하게 됩니다.

 

효율성을 중시하는 알고리즘일수록 빠르게 결과를 도출하기 위해 검증하는 시간과 로직을 최소한으로 하기 때문입니다. 그렇다면 반대로 정확성을 중시하는 알고리즘은 장애를 감지하고 판단하는데 시간이 오래 걸리지만 좀 더 적은 리소스로 서비스의 상태를 관리할 수 있습니다.

  • 여기서 말하는 리소스는 이는 잘못된 결과로 인해 서비스를 죽이고 다시 배포하는 등의 작업 리소스를 이야기합니다.

 

결국 효율성과 정확성을 완벽하게 충족하는 장애 감지 서비스는 없으며, 상황에 따라서 올바른 감지 시스템을 구성하고 사용하는 것이 중요합니다.

 

 

 

 

Ping과 Heartbeat

Ping과 Heartbeat는 유사한 장애 감지 방식입니다. 장애를 감지하는 서비스는 주기적으로 다른 서비스들에게 메시지를 보내고(broadcasting) 결과를 받아 정상적인 상태임을 판단하게 됩니다.

이 2가지 방식은 Timeout을 사용하느냐(Ping), 하지 않느냐(Heartbeat)로 구분 지을 수 있습니다.

여기에서 설명하는 Heartbeat는 All to All 방식의 Heartbeat입니다. 

 

 

Ping은 메시지를 보내는 주기와 Timeout 설정을 기준으로 감지 정확도가 좌우됩니다. 그렇기에 서비스 A에게 보낸 1번째 메시지의 응답이 2번째 메시지 전송 이후에 도착한다면, 장애 감지 시스템은 이 서비스를 장애가 발생한 서비스로 판단할 수 있습니다.

  • 즉 특정 서비스의 상태를 파악할 수 있는 방법이 아니며, 요청에 대한 결과라는 단순한 지표를 기준으로 장애를 탐지하게 됩니다.
  • 이는 완전성을 지키고 효율성 또한 극대화되었으나, 정확도가 낮은 장애 알고리즘임을 의미합니다.

 

 

Heartbeat는 Timeout을 사용하지 않는 대신에 각각의 서비스가 특정 서비스에 몇 번의 요청을 전송하였는지 기록하여 설정된 임계 값에 도달하였을 때 장애로 판단하는 방식입니다. 그렇기에 정확도를 높이기 위해서는 적절한 임계 값 설정이 필요합니다.

  • 초기에 전송되는 메시지에는 발신자의 정보와 동일 메시지의 중복 전송을 방지하기 위해 고유한 식별자를 기록하게 됩니다.

 

 

 

Heartbeat outsourcing

Heartbeat outsourcing은 기본적으로 Heartbeat의 동작 방식을 따르나 각각의 서비스가 모든 서비스에게 메시지 요청을 보내는 것(broadcasting) 이 아니라 인접한 서비스에만 메시지를 날려 장애를 감지하고 판단하는 방식입니다.

 

그렇기에 각 서비스는 모든 서비스 존재를 알고 있지 않도록 범위를 나누어 메시지 요청으로 인한 장애 감지의 신뢰성과 떨어진 효율성을 어느 정도 보완하게 됩니다.

  • 각 서비스는 주변에 위치하는 서비스들에게만 요청을 전파하는 방식으로 동작합니다.
  • Heartbeat를 사용하는 서비스의 수가 수백 또는 수천 개라고 가정하였을 때, 패킷이 지나는 네트워크의 크기가 커지고 자연스레 네트워크 이슈가 발생할 여지도 증가하여 장애로 잘못 판단하는 경우가 늘어날 수 있기 때문입니다.

 

 

 

φι-accrual detection

https://medium.com/@arpitbhayani/phi-φ-accrual-failure-detection-79c21ce53a7a

 

이 방식은 서비스의 상태를 프로세스 충돌 확률을 연속적으로 계산하고(Time series) 이를 통해 장애를 결정하는 방식입니다.

  • cassandra, akka, Hazelcast 등의 opensource가 해당 방식을 사용 및 지원하고 있습니다.

 

우선 특정 배열이나 리스트 형태의 자료구조 또는 TDB에 메시지 응답 도착 시간을 저장하고 Sliding Window 알고리즘을 활용하여 일정 범위의 값을 읽어와 다음 메시지의 응답 시간 또는 응답 확률을 예측한 뒤 실제 메시지를 요청하여 연산된 결과와 비교합니다.

 

그리고 비교한 결과를 통해 Suspicion level을 계산하여 이를 기준으로 노드의 중단, 부하 또는 정상 상태를 판단하게 됩니다.

  • Suspicion level 임계 값을 동적으로 조정할 수 있으며, 다양한 임계 값을 지정하여 여러 가지 행위를 수행할 수도 있습니다.

 

φι-accrual detection을 사용하기 위해서는 지속적인 메시지에 대한 표본을 구하고 이를 저장할 자료구조를 가지고 있어야 하며, 표본에 대한 평균과 분산 값을 계산하여 분포 도를 추정한 뒤 도착할 확률과 프로세스 활성 상태의 정확도를 나타내는 φι를 계산해야 합니다.

  • 또한 특정 임계 값을 지정하여 상태를 결정하는 방법과 상태에 따른 Callback을 구성하여야 합니다.

 

 

 

 

참고 자료

+ Recent posts