데이터쟁이 입장으로 “슬로우 쿼리”를 다시 고민해보았습니다.

Overview

서비스를 하면 당연히 실행이 오래 걸리는 쿼리, 슬로우 쿼리는 발생합니다. 원인은 정말 비효율적인 쿼리인 것도 있겠지만,  때로는 Lock, Disk fault 등등 원인은 다양합니다. DB 내/외부 요소에 의해서, 슬로우 쿼리가 발생하게 되는데.. 이것을 늘 모니터링하고 적시에 바로 최적화 적용을 하는 것이야말로, 안정적인 서비스 최상 품질 보장의 첫 걸음이라고 생각합니다.

물론, 이 관련해서는 여러가지 방법론이 있겠지만, (예를들면 fluentd를 활용한 수집 방안) 슬로우 쿼리를 데이터로써 제가 생각하는 원칙으로 접근해보았습니다.

스크립트는 하단 github를 참고하시지요. 아마도 잘 될꺼예요. (50% 상상코딩이라..흐흐)
https://github.com/gywndi/kkb/tree/master/mysql_slow_log_gather

Requirements

우선 여기서 말하고자 하는 것은 슬로우 로그 파일 로테이션에 대한 내용이 아닙니다. 관련 내용은 하단 URL 을 참고하세요. ^^
>> https://www.percona.com/blog/2013/04/18/rotating-mysql-slow-logs-safely/

서비스 튜닝에 필수 요소인 슬로우 쿼리 제거.. 이 쿼리 로그 역시 “데이터” 중 한가지이고, 이 데이터의 특성을 고려해보고자, 다음 세 가지 질문을 나에게 던져보았습니다.

  • 실시간으로 데이터를 모아보아야 할 것일까?
  • 로그 수집이 정말 안정적일까?
  • 진짜 슬로우 데이터만 수집하고 있는가?

특히 두번째는 가장 중요한 질문입니다. 서비스 최적화를 위해 수행하는 프로세스가 서버의 안정성에 “일말의 부담”이 될 수 있다면, 하느니 못한 것과 같겠죠. 물론, 그동안 전혀 문제가 없었을 수도 있겠지만.. 주어진 외부 사례가 최악의 상황에서 적절한 대응책을 마련해주고 있는지.. 진지하게 고민을 해보았죠.

그리고 풀스캔 쿼리를 추출하기 위해 “log_queries_not_using_indexes” 옵션을 사용하는 경우, 의도된 파라메터로 조정된 쿼리 경우에는 “진짜 슬로우 쿼리”가 아니면 수집하고 싶지 않았습니다. 초당 수천건 이상 유입되는 상황에서 불필요한 내용은 걸러내는 것이 안정성에도 도움이 될테니요. ^^

Gather Slow Log

우선, 슬로우 쿼리 수집을 특별한 데몬을 띄워서 수행하지 않았습니다. 정상적으로 잘 수집하고 있는지 추가 관리 포인트가 들어가야할 것이고, 서버 대수가 급증할 수록 이 또한 굉장히 부담이기 때문이죠.

그래서 제 결론은 “한번 돌고 끝나는 스크립트”입니다. 주기마다 수집 스크립트가 “마지막 수집 시점 이후”부터 로그를 수집자는 것이죠.  제가 생각하는 슬로우 쿼리의 목적은 “사후 대응 및 튜닝”을 위한 수집이지, “실시간 모니터링”을 통한 즉각 대응을 위함이 아닙니다.

물론 간단하게라면, 이는 라인단위로 수행을 할 수 있겠지만.. 문제는 백만라인짜리 슬로우 쿼리 파일 경우에는 이 또한 부담이겠죠. 그래서, 마지막 처리 바이트 기준으로 이후부터 데이터를 읽고, 로그 수집을 합니다.

그런데 여기서 문제 몇가지가 더 발생합니다. 각각에 나름의 결론을 지어봅니다.

1. 지나치게 슬로우가 많이 쌓이면 어떻하지?

>> 결론 : 버리자. 대신 통보는 하자.

서버가 미쳐서(?) 10분간 1G이상의 데이터가 쌓인 경우를 상상해보죠. 이것을 어찌어찌 중앙 서버로 취합을 해야겠죠. 그런데.. 1G 데이터를 파싱하고, 처리하고, 전송하고.. 서버에 분명 영향을 미칠 것입니다. 특히나, 저정도 슬로우가 발생한 상황이라면, 거의 서비스 마비상태일 것인데.. 슬로우 수집이 서버에 부담을 가중시키는 것이 아닐지??

2. 파일이 바뀌면 어떻하지?

>> 결론 : 첨부터 읽어버리자. 대신 너무 크면 버리고 통보하자.

파일이 바뀌는 경우라면.. 로그 로테이트가 이루어지면서 새로운 파일로 치환되는 경우(아이노드가 변경되는 케이스)를 생각해볼 수 있겠습니다. 이 경우에는 파일명은 같지만, 전혀 다른 데이터파일로 봐야합니다. 이런 케이스? 첨부터 읽자입니다.

파일을 조작하는 경우도 있겠죠. 물론, 스크립트 구조 상 세세하게 체크는 불가하지만, 적어도 마지막 포지션이 현재 파일 사이즈보다 작은지 체크정도는 가능합니다. 예를들어 “cat /dev/null > mysql-slow.log”를 수행해서, 깡통으로 만드는 경우입니다.

3. 슬로우 시간을 넘지않으면 어떻하지?

>> 결론 : 버리자.

슬로우 쿼리 수집의 목적은 다름아닌 이슈 있는 쿼리 수집입니다. 제 경우에는 이 시간이 0.5초이고, 이 시간 이상의 쿼리는 최대한 OLTP환경에서는 돌지 말아야 합니다. (사실 이 시간도 길다고 생각해요. ㅋㅋ)

그런데 때로는 관리자 의도에 의해, 풀스캔 쿼리를 찾아내기 위해 “log_queries_not_using_indexes” 옵션을 켜는 경우가 있는데.. 이 경우 풀스캔 쿼리가 무조건 슬로우로 기록되기 때문에.. 설혹 쿼리 시간이 굉장히 짧아도 다수 로그로 쌓이고 말지요.

(그럴일은 없겠지만..) 이 옵션을 켰을 때 10건짜리 테이블 풀스캔이 수백건 단위로 초당 유입이 된다면 어쩔까요? 이런 쿼리를 중앙에서 수집하는 것이 의미가 있을까요? 결정은 각자의 몫이겠지만, 제 입장에서는 “필요없다”입니다.

Parse Slow Log

위에서 언급한 기준으로 수집을 한 슬로우쿼리.. 이번에는 파싱을  해봅시다. 하단은 MySQL 5.7 기준, 슬로우 로그는 아래 포멧입니다.

# Time: 2017-06-27T07:34:39.086551Z
# User@Host: xxxxxx[xxxxxx] @  [127.0.0.1]  Id: 78316
# Schema:   Last_errno: 0  Killed: 0
# Query_time: 1.001540  Lock_time: 0.000000  Rows_sent: 1  Rows_examined: 0  Rows_affected: 0
# Bytes_sent: 85  Tmp_tables: 0  Tmp_disk_tables: 0  Tmp_table_sizes: 0
# QC_Hit: No  Full_scan: No  Full_join: No  Tmp_table: No  Tmp_table_on_disk: No
# Filesort: No  Filesort_on_disk: No  Merge_passes: 0
SET timestamp=1498548879;
select 
  1,
  2,
  3,
  4, 5, 
  sleep(1);

그런데.. 난제 발생..

처음에는 단순하게 접근(tail)을 했었지만.. 쿼리의 시작과 종료를 정의하는 포인트가 생각보다 난해합니다. 이 경우 “슬로우 쿼리 정보의 패턴을 매칭해서 관련 정보를 추출”하고, “슬로우 쿼리 시작 패턴이 다시 들어오기 전”까지를 사용자 쿼리로 인지시키는 과정이 필요합니다.

my $pattern1 = "^# Time: (.*)T(.*)\\.(.*)\$";
my $pattern2 = "^# User\@Host: (.*)\\[.*\\] @  \\[(.*)\\]  Id: (.*)\$";
..
my $pattern8 = "^SET timestamp=(.*);\$";
my $pattern9 = "^USE (.*);\$";

이 패턴을 벗어난 데이터는 쿼리로 인지하고, 위 패턴의 라인이 들어오기 전까지는 “사용자의 쿼리로 취합하자”입니다. 뭐 마지막 쿼리 수집을 위해 스크립트 종료 직전에 한번더 flush 날려주는 것은 서비스죠.

Send to..

파싱도 했으니, 중앙 서버로 보내야 합니다. 여기서 0.5초 이상되는 대상들만 선별해서 보내도록 하여, 불필요한 리소스 사용을 금지시켰습니다.

sub flush_log(){
  if($query_time gt 0.5){
    .. 어디론가 슬로우 쿼리를 쏘세 ..
  }
}

Conclusion

지금까지 안정적으로 사용했던 다른 방안도 있고 내부적으로 활용하고 계신 방법론도 있겠죠. 그렇지만, 슬로우 쿼리 수집을 수행하기에 앞서서, “슬로우 쿼리” 데이터에 대한 재정의가 필요하다고 생각했습니다.

당연하겠지만, 서버에 모든 데이터를 실시간으로 취합(쓰든 안쓰든)해서 관리하면 좋습니다. 그러나, 서버의 리소스를 고려하고, 이 수집이 오히려 전체 서비스에 영향을 줄 수 있는 상황을 고려해봐야 합니다. 더구나, 서비스를 안정적으로 만들기 위해 수집하는 데이터라면 더욱 그렇겠죠. 게다가 활용되지 않은 데이터는 죽은 데이터로.. 공간만 잡아먹는 몬스터일 뿐이닐테니요.

이 자리를 빌어 말하고 싶었던 것은.. “데이터에 대한 정의”가 명확하게 하고 있는지입니다. 정말로, 내가 쓰려고 수집하는 데이터인지.. 그냥 통상적으로 수집했던 데이터인지..

가벼운 주제.. 슬로우 쿼리 수집 방법론을 빌어 가벼운 썰을 풀어보앗네요. ㅎㅎㅎ

[MySQL] 바쁜 서비스 투입 전, 이런 캐시 전략 어때요?

Overview

데이터베이스를 운영한다는 것은 최적의 상태로 리소스를 “쥐어짜면서” 가장 효율적으로 데이터를 끄집어내야할텐데요. 지금은 SSD 디스크 도입으로 상당 부분 Disk I/O가 개선되었다지만, 여전히 메모리 효율은 굉장히 중요합니다. 특히나 Page Cache와 같은 항목이 과다하게 메모리를 점유하게 되면.. 다른 프로세스 효율에도 영향을 미칠 뿐만 아니라, 때로는 메모리 부족 현상으로 인하여 스왑 메모리에도 영향을 줄 수 있습니다.

서론은 짧게.. 이번에 DBMS 구조를 그려가면서, 실제 리소스 사용에 걸림돌이 되는 몇몇 요소에 대한 우리의 사례를 얘기해보도록 하겠습니다.

Page Cache

OS에서는 파일을 읽고 쓸때 디스크 I/O를 최소화하기 위해서 내부적으로 Page Cache 관리합니다. 매번 디스크에서 파일을 읽고 쓰면 효율이 떨어지기 때문인데.. SQLite과 같은 파일 기반의 DB라이브러리나, 정적인 데이터 쪽 Disk I/O가 빈번하게 발생하는 곳에서는 극도의 효율을 보여주겠죠.

$ free
             total       used       free     shared    buffers     cached
Mem:       2043988    1705544     338444       7152     188920     330648
-/+ buffers/cache:    1185976     858012
Swap:      4800508          0    4800508

문제는 솔루션 자체적인 캐시 공간을 가지는 경우, 이를테면 MySQL InnoDB에서와 같이 InnoDB_Buffer_Pool 영역을 가지는 경우에는 이러한 Page Cache 큰 의미가 없습니다. DB에서 1차적으로 가장 빈도있게 액세스 요청하는 곳은 자체 버퍼 캐시이기 때문이지요. 그래서 이러한 경우 Page Cache 사용을 최소화하도록 유도하거나, 아예 캐시레이어를 태우지 않고 바로 데이터 파일로 접근(O_Direct)하는 것이 좋습니다. 이러한 경우를 떠나, TokuDB와 같이 Page Cache에서 압축된 파일을 보관하는 것이 효율면에서 좋은 경우.. 다른 엉뚱한 곳에서 캐시 영역을 점유하지 않도록 유도하는 것도 필요합니다.

Percona에서 InnoDB의 Fork로 컴파일하여 배포하는 XtraDB 스토리지 엔진에는 이러한 요구사항에 맞게 데이터파일 및 트랜잭션 로그(Redo/Undo) 접근에 Page Cache를 끼지 않고, 직접 데이터 파일에 접근할 수 있도록 innodb_flush_method에 ALL_O_DIRECT 옵션을 제공해줍니다. ( https://www.percona.com/doc/percona-server/5.6/scalability/innodb_io.html#innodb_flush_method )

문제는 ALL_O_DIRECT를 사용했을지라도, 여전히 Binary Log 혹은 기타 로그(General Log, Error Log, Slow Log)에 대해서는 여전히 Page Cache를 사용한다는 점입니다. 만약 쓰기 작업이 굉장히 빈번한 서비스에서는 다수의 바이러리 로그가 발생할 것이고.. 이로인해 Page Cache 영역이 생각보다 과도하게 잡힐 소지가 분명 있습니다.

그래서 요시노리가 오픈한 unmap_mysql_logs 를 참고하여, 우리의 요구사항에 맞게 조금 변형하여 재구성해보았습니다. ( https://github.com/yoshinorim/unmap_mysql_logs )

  1. 서버에 기본 이외에 설치는 “굉장히” 어렵다. 우리는 금융이니까.
  2. 서버마다 컴파일하기는 싫고, 복/붙만으로 처리하고 싶다.
  3. unmap 대상을 유연하게 변경하고 싶다.(추가 컴파일 없이)

그래서 파라메터로 특정 파일을 전달받고, 해당 파일을 unmap 수행하도록 아래와 같이 main 프로그램을 추가하여 재작성하였고, 로그 대상은 별도의 쉘 스크립트로 제어하여 매번 컴파일 없이도 유연성있게 제어하도록 변경하였습니다.

int main(int argc, char* argv[]){
  if(argc == 1){
    fputs("no options\n", stderr);
    exit(1);
  }

  int i = 0, r = 0;
  for (i = 1; i < argc; i++){
    printf("unmap processing.. %s.. ", argv[i]);
    unmap_log_all(argv[i]) == 0 ? printf("ok\n") :  printf("fail\n");
  }
}

자세한 내용은 하단 github을 참고하세요. 🙂
https://github.com/gywndi/kkb/tree/master/mysql_cache_unmap

Dirty Page

Dirty Page는 얘기를 여러번 해도 부족함이 전혀 없습니다. 다른 것보다 가장 문제시 되는 것은 하단 붉은색 표기입니다. 매 5초(vm.dirty_writeback_centisecs)마다 pdflush 같은 데몬이 깨어나, 더티 플러시를 해야할 조건이 왔는지를 체크하게 되는데요. 문제는 기본값 10은 메모리의 10% 비율이라는 말이지요. 데이터 변경량이 적은 경우는 문제가 안되는데, 굉장히 바쁜 서버 경우에는 이로 인해 문제가 디스크 병목이 발생할 수 있겠습니다.

한번에 다량의 데이터를 디스크로 내리지 말고, 조금씩 자주 해서 디스크의 부담을 최소화하자는 것인데요. RAID Controller에 Write-Back이 있을지라도 혹은 디스크 퍼포먼스가 엄청 좋은 SSD일지라도 수백메가 데이터를 한번에 내리는 것은 부담이기 때문이죠. 특히 크고 작은 I/O가 굉장히 빈번한 “데이터가 살아 움직이는” DBMS 인 경우!!

스왑 메모리 또한 서비스 효율 측면에서 굉장히 이슈가 있는데요. 아무리 SSD 디스크의 강력한 Disk I/O가 받혀줄지라도, 실제 메모리의 속도를 절대적으로 따라갈 수 없습니다. 페이지 캐시든 다른 요소이든, 비효율적인 어떤 요인에 의해 DBMS에서 사용하는 메모리 영역(InnoDB경우 Innodb_buffer_pool 이죠)이 절대로 스왑 메모리로 빠져서는 안됩니다.

$ sysctl -a | grep dirty
vm.dirty_background_ratio = 10
vm.dirty_background_bytes = 0
vm.dirty_ratio = 20
vm.dirty_bytes = 0
vm.dirty_writeback_centisecs = 500
vm.dirty_expire_centisecs = 3000

$ sysctl -a | grep swap
vm.swappiness = 60

그래서 아래와 같이 더티 파라메터를 비율이 아닌 고정값으로 임계치가 설정되도록 vm.dirty_background_bytes 값을 변경해줍니다. 그리고, 스왑 메모리 사용을 최대한 지양하고자.. vm.swappiness 파라메터를 1로 설정하도록 합니다.

$ sysctl vm.dirty_background_bytes=100000000
$ sysctl -w vm.swappiness=1

뭐.. 개인적으로는 스왑이 발생해서 서비스 품질이 떨어지는 것보다는, 스왑 없이 메모리 부족 현상으로 해당 프로세스가 OOM으로 강제 킬되어 명확하게 장애로 노출이 되어서 페일오버 단계로 명쾌하게 넘어가는 것이 좋다고 생각합니다만.. 아무래도 금융 서비스인만큼.. 보수적으로 접근해서 스왑 파라메터를 1로 설정하였습니다.

아. 위처럼 커널 파라메터를 바꿔봤자, OS 재시작되면 원래 기본 값으로 돌아갑니다. 반드시 /etc/sysctl.conf 파일에 파라메터 값을 추가하세요.

Dentry

금융권 DB를 구성하면서, 처음으로 겪은 현상이었습니다. 이상하게 메모리 사용률이 높은 것이지요.

위와같이 매 5분마다 Page Cache를 unmap함에도 불구하고, 전체적으로 메모리 사용량이 거의 95%를 훌쩍 넘게 사용하는 현상을 보였습니다. 처음에는 MySQL 버퍼 캐시 영역이 차지한다고 생각을 했었는데.. slabtop으로 메모리 사용 현황을 살펴본 결과 dentry(하단 강진우 님 브런치 참고) 에서 대부분 점유 중인 것을 확인할 수 있었습니다. (당시 상황 캡쳐를 못해서.. 그냥 재시작 후 3일된 서버로.. 대체합니다.)

$  slabtop
 Active / Total Objects (% used)    : 15738668 / 15746877 (99.9%)
 Active / Total Slabs (% used)      : 783666 / 783672 (100.0%)
 Active / Total Caches (% used)     : 116 / 204 (56.9%)
 Active / Total Size (% used)       : 2949065.50K / 2950582.13K (99.9%)
 Minimum / Average / Maximum Object : 0.02K / 0.19K / 4096.00K

  OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME                   
14565320 14565248  99%    0.19K 728266       20   2913064K dentry
926480 925875  99%    0.10K  25040       37    100160K buffer_head
 50848  50814  99%    0.98K  12712        4     50848K ext4_inode_cache

참고로, 우리는 금융 서비스이기 때문에.. 보안 강화(?)를 위해 타 서버와의 curl로 REST API 사용을 하기 위해서는 일단 기본적으로 https로 백업 결과 혹은 서버 상태 전송을 중앙 서버로 전송을 합니다. (거의 필수로)

그리고 카카오의 슈퍼 루키 강진우(alden)님의 조언을 얻어.. curl로 https로 이루어진 REST API를 호출하면서 발생하는 현상임을 확인했습니다. (https://brunch.co.kr/@alden/28)

참고로 하단은 내부적으로 이 관련 내용을 증명/분석(?)하기위해, 우리 팀 슈퍼 에이스 성세웅 님의 테스트 결과를 훔쳐옵니다. 🙂

$ unset NSS_SDB_USE_CACHE
$ strace -fc -e trace=access curl --connect-timeout 5 --no-keepalive \
  "https://testurl/api/host.jsp?code=sync&host_name=`hostname -s`" > /dev/null
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.000081           0      2731      2729 access

자! 이번엔 Mr.강의 조언에 따라 “export NSS_SDB_USE_CACHE=yes” 를 설정한 이후 결과를 살펴봅시다.

$ export NSS_SDB_USE_CACHE=yes
$ strace -fc -e trace=access curl --connect-timeout 5 --no-keepalive \
  "https://testurl/api/host.jsp?code=sync&host_name=`hostname -s`" > /dev/null
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0        10         7 access

2729 VS 7.. NSS_SDB_USE_CACHE가 세팅되지 않은 상태에서 간단한 프로그램으로 루핑을 돌면서 curl API를 호출했을 때 선형적으로 dentry 영역이 증가하는 것을 확인할 수 있었습니다. /etc/profile에 “export NSS_SDB_USE_CACHE=yes” 추가하여 비정상적인 메모리 사용 현상은 사라지게 되었죠. ㅎㅎ

Conclusion

그간의 경험들이 굉장히 무색해지는 산뜻한 경험입니다. “조금더 편하게, 조금더 유연하게, 조금더 안정적으로”를 지난 몇 달 간 고민을 했었고, 운영 시 최소한의 “사람 리소스”로 “최대의 효율”을 획득할 수 있도록 발버둥쳤고, 그 과정에서 많은 것을 배웠습니다.

  1. Page Cache를 주기적으로 unmap하여 메모리 효율 증대
  2. Dirty/Swap관련 파라메터 조정
  3. dentry 캐시 사용에 따른 NSS_SDB_USE_CACHE 파라메터 설정

오픈된 지식으로 많은 것을 얻었고, 저 역시 얻은 것을 다른 누군가에게 오픈을 해서, 누군가의 새로운 오픈된 지식을 얻고자 이렇게 블로그에 정리를 해봅니다. (사실은 여기저기 흩어진 정보들이 너무 산발되어 정리할 필요성을 느꼈기에.. ㅋㄷㅋㄷ, 그래도 힘드네요. ㅠㅠ)

좋은 밤 되세요. ^^

[MySQL] 슬레이브 하나 더 추가했을 뿐인데.. :-)

Overview

MySQL의 꽃중의 꽃은 역시 비동기 방식의 데이터 복제라고 볼 수 있는데요. 지극히 개인적인 생각이기는 하지만, 슬레이브 노드를 데이터 일관성이 반드시 필요한 상황에서의 READ 스케일아웃을 “제외”하고는, 지금의 MySQL을 있게한 결정적인 한방이라고 봅니다. 물론 소셜 서비스에 따른 기존 스케일업으로는 도저히 감당할 수 없는 데이터 사이즈와 비용 요소도 직접적인 영향을 주었겠지만요.

뜬구름잡는 얘기는 여기까지로 마무리하고.. 슬레이브 노드를 READ 분산 혹은 배치 서버 용도를 제외하고는 스탠바이 용도로 한대를 더 추가하는 경우는 극히 드뭅니다. 뭐, 나랏님이 시키는대로.. 반드시 수십 km 떨어진 IDC에 DR 서버를 구축해야한다는 “법적인 제약”인 경우를 제외하고는요.

이유야 어쨌든, 이전 운영하는 서버 클러스터 노드와는 다르게 3대의 DB 서버를 세트 하나로 구성하게 되었는데요. 나름 고민에 고민을 거쳐, 최종적으로 현재 구조로 마무리하면서 얻게된 잇점과 효율에 대해서 얘기를 풀어보도록 하겠습니다.

2-Nodes vs 3-Nodes

서버를 두대로 구성하는 경우는 일반적으로 아래와 같이 마스터/슬레이브 구조로 데이터를 복제하는 경우입니다. 평시에는 Master에서 모든 서비스를 하고 장애 시 슬레이브로 커넥션을 (자동이든/수동이든) 넘겨서 서비스 다운타임을 최소화하기 위함입니다. 데이터가 논리적으로 복제가 되기 때문에, 슬레이브를 활용하여 꽤 무거운 배치 혹은 데이터 처리를 서비스 영향없이 수행 가능합니다.

MYSQL-2-NODES-REPL

비슷하게 비슷한 목적을 가졌지만, 앞선 경우에는 데이터를 논리적으로 복제를 해서 원격에 데이터를 위치시키는 것과 다르게, 스토리지를 활용하여 서버 장애에 대응한 경우도 있습니다. 이 경우 아쉽게도 서버 장애에는 대응이 가능하나, 스토리지 장애에는 “스토리지 이중화”외에는 방안이 없으며, 스탠바이 노드는 디비 데몬이 내려가 있기 때문에, 정말 1도 활용할 수 없다는 아쉬움이 남습니다. 게다가 “물리적으로 저장공간을 공유”하는 구조이기 때문에, NIC를 통해 논리적으로 데이터를 복사하는 앞선 경우와는 다르게 “스토리지 물리적인 장소”에 영향을 받을 수밖에 없습니다.

MYSQL-2-NODES-STORAGE

일반적으로 2대의 노드로는 위 두 가지 경우를 크게 벗어나는 경우는 없습니다. 두가지 경우 모두 둘 중 하나만 서비스에 투입이 되기 때문에, 한대는 거의 대부분 서비스에 직접적인 트래픽을 받지 않습니다. (뭐 첫번째 경우 배치 쿼리 정도는 날려볼만 하겠지만요.)

그러나 타의든 자의든, 일단 세 대를 아래와 같은 형태로 운영을 해야하는 상황이 도래합니다. 아래와 같이 3대 노드로 데이터를 구성하는 경우, MySQL은 세트 하나 늘어날 시에 세대씩 증가하기 때문에관리 포인트는 2-노드인 경우보다 훨씬 많아집니다. 게다가 MySQL의 꽃은 샤딩.. 무한 확장을 꿈꾸며 데이터 라우팅을 지대로 하는 것이 묘미니까요. ㅎㅎ

MYSQL-3-NODES-REPL

또한 노드가 늘어남에 따라 “복제 시 전송되는 로그양”도 적은 비용이 아닙니다. (금전적인 것 이외에도 말입니다.)

사용하지 않을 장비만 늘어나고, 이에 따라 관리 포인트만 증가하는 현실 속에서, 이 글을 쓰기 시작하였고, 어찌됐건 기존 1M-1S 구조에서와는 다른 “얻은점”에 대해서 얘기를 풀어보도록 하겠습니다. (공감하신다면, 야밤에 박수 세번 짝짝짝)

1. Semi-Sync Replication

데이터 자체가 굉장히 민감한 서비스(이를테면, 돈이 오가는 서비스)에서는, 절대적으로 데이터의 유실은 허용될 수 없습니다. 트랜잭션이 실패할지라도, 장애 시 데이터 유실은 상상조차 해서는 안되는 것이죠.

MySQL5.5 부터는 이런 현상을 최소화하기 위해, semi-sync replication 기술을 도입하였습니다. 이는 마스터와 슬레이브 간의 데이터 동기화를 100% 보장할 수는 없지만, 적어도 복제 로그(릴레이로그)에 대한 보장을 어느정도 하겠다는 것을 의미합니다.

일반적으로 5.5부터 언급되는 MySQL semi-sync는 “AFTER_COMMIT”모드로, 커밋 이후 사용자에게 응답을 주기 직전(엔진에는 이미 데이터 적용 완료)에 잠시 슬레이브 릴레이 로그를 기다리는 형태로 동작을 하였습니다.

mysql-semisync-5.5

MySQL 5.7.2 부터는 Lossless Replication이라는 멋진 이름 하에 “AFTER_SYNC”모드의 semi-sync 옵션이 추가가 되었는데, 간단하게 얘기하자면, 엔진에 커밋이 되기 직전에 슬레이브 릴레이 로그 저장을 잠시 기다려보겠다는 것을 의미합니다. (아래 빨간 글씨)

mysql-semisync-5.7

그런데, AFTER_SYNC든 AFTER_COMMIT이든 공통적으로 가지는 문제가 하나 있는데.. “트랜잭션 응답 시간이 슬레이브의 빠른 응답에 의존적”이라는 것입니다. 만약, 슬레이브가 한 대인 상황에서 “슬레이브”가 장애가 나게 된다면, 서비스 트랜잭션이 일시적으로 블록킹 형태로 머물러있게 됩니다.

물론 rpl_semi_sync_master_timeout 파라메터를 조정해서, 슬레이브의 응답을 기다리는 최대 시간을 조정하여, 빠르게 Async모드로 돌아오도록 조정할 수 있지만.. 기본값이 10초인 상황을 인지 못한 채, 운영하다보면 영문도 모른채 서비스에 이상을 감지할 수 있습니다. (여기서, 분명히 구분해야할 것은, 서비스 투입되지 않은 “슬레이브” 이상으로 발생하는 상황이라는 점입니다.)

그런데, 슬레이브 노드 하나를 더 추가함으로써 “특정 슬레이브 노드의 지연”이 마스터의 트랜잭션 지연으로 이어지지 않습니다. 둘 중 한군데에서 응답이 오면 마스터는 더이상 기다리지 않고 트랜잭션을 완료 처리하기 때문이죠. 물론 몇 군데의 응답을 기다릴지도 옵션으로 지정할 수 있지만, 굳이 그럴 이유는 없어보입니다. (이유는 MHA에서..)

semi-sync-3-node

슬레이브 하나를 더 추가했을 뿐인데.. semi-sync 를 더욱 안정적으로 멋지게 사용할 수 있는 발판이 되었습니다. 눈누난나~

2. Backup & Restore

백업/복구.. 데이터 솔루션을 운용하는 입장에서는 반드시 초반부터 계획을 잡아야하는 상황인데요. 앞선 블로그 “세상만사 귀찮은 MySQL DBA를 위한 자동 복구 시나리오” 에서 어떻게 자동화를 하는지에 대해서 공유한 적이 있었습니다. 슬레이브를 한대 더 추가함으로써 복구 시 서비스 품질 관련하여 곁다리로 하나 더 얻게된 점이 하나 더 있는데요.

이전에 슬레이브 한대로만 서비스를 운영한다면, 마스터 장비 페일오버 이후 데이터 복구 수행 시, 그 날 새벽 백업 데이터를 활용하여 슬레이브를 복구해야만 합니다. 물론, (서비스에 투입된)마스터에서 직접 받아와도 되겠지만, 수백기가 데이터를 전송하는 동안 네트워크 트래픽을 비롯해서 디스크 I/O를 고려한다면, 서비스 안정성 측면에서 좋은 선택을 아니라고 봅니다.

새벽 데이터로 슬레이브 구성을 한다는 말은 곧, 새벽부터 쌓이기 시작한 “바이너리로그”를 모두 끌어와서 현재 시점까지 리플레이하여 적용하겠다는 말인데요. 굉장히 바쁜 서버 경우에는 하루 70~80G이상 바이너리 로그가 쌓이기도 하는데.. 가져오는 것도 부담일 뿐만 아니라, 적용하는 시간 조차도 굉장히 부담이 됩니다. (23시간 전의 데이터로 복구를 한다고 생각을 해보세요. 바쁜서버는 10시간 넘게도 걸릴 수도 있습니다.)

자. 여기서 슬레이브를 하나 더 추가를 해보았다면, 어떤 것을 얻을 수 있을까요?  슬레이브 구성이 필요한 상황이 오더라도, 아래와 같이 슬레이브로부터 데이터를 받아와서 바로 원래 구조로 쉽게 돌아갈 수 있게 됩니다. 서비스 영향도는 거의 제로에 가깝게.. 굳굳굳

Restore_From_Slave_Data
Restore_From_Slave_Data

3. Collaboration with MHA

개인적인 관점에서.. MHA야말로 신의 한 수라고 생각합니다.ㅋㅋ

MHA? 개념이 아직 생소하신 분도 있겠죠. 간단하게 설명하자면, MHA는 Master의 헬스 체크를 주기적(3초단위 쿼리를 날리면서)으로 수행하면서, 설정된 카운트 이상 실패 시 스탠바이 슬레이브로 마스터의 롤을 체인지시켜주는 “요시노리”가 오픈소스로 공개한 MySQL HA 솔루션입니다.

재미있는 것은 MHA가 마스터 장애를 감지하고, 슬레이브로 마스터의 롤을 넘기는 과정에서 살아있는 슬레이브들의 리플리케이션 또한 신규 마스터 기준으로 재구성을 해주는데요. 이 경우 기존 마스터로부터 전달받은 릴레이 로그를  비교 및 버전 동기화를 함으로써 최종적으로 정상적인 리플리케이션 형태로 만들어줍니다.

semi-sync-repl

“릴레이 로그”를 비교한다라는 것은.. 앞서 1번에서 언급했던 “Semi-Sync Replication”을 생각해볼 수 있는데요. Semi-Sync로 구성된 리플리케이션 클러스터 내부에서는.. “커밋된 트랜잭션”의 릴레이 로그는 “슬레이브 어디엔가에는 반드시 존재한다”는 가정하에 동작합니다.

AFTER_SYNC 모드로 semi-sync를 운영하고 있다면, 커밋된 트랜잭션의 릴레이로그는 “슬레이브 중 어딘가”에는 반드시 존재할 것이고, 이 경우 마스터 장애 시 “어딘가에 존재하는 릴레이로그”를 활용한 복구로 절대적으로 데이터 유실 가능성은 제로에 가깝게 될 것이다라고 돌려 말을 해봐도 되겠습니다.

4. High Availability

2번 백업에서도 얘기했던 것과 일맥상통하는 얘기일 수 있겠습니다.

슬레이브가 한대인 경우에 마스터 장애가 발생했을 시에는 늘 걱정되는 것이 바로 2차 장애였습니다. 장애 후 복구까지는 어찌됐건 싱글 DB로 데이터를 운영하고 있는 것이고.. 이 한대에 이슈가 발생하면 “메가톤급 서비스 장애”와 “데이터 유실”이라는 부담감을 가지고 늘 지내왔죠. 그렇기때문에, 새벽 몇시가 되건 “유휴장비”를 시스템부서로부터 제공받아 빠르게 슬레이브를 붙이는 것이 장애 대응 1순위 작업이었죠. 서비스는 소중하니까요. 🙂

그런데, 슬레이브를 하나 더 붙였을 뿐인데.. 2차 장애에 대응해서 우리에게는 하나 더 선택지가 생겼습니다.

  1. MHA를 2노드로 설정해서 다시 모니터링한다.
  2. 모니터링을 하지 않되, 문제 시 바로 남은 슬레이브로 롤체인지 한다.

이 두가지 모두, 정책적으로 운영을 어떻게 할 지에 대한 선택일 뿐, 2차 장애에 대한 대응은 동일합니다. 다른 점이라면, 자동으로 넘길지 혹은 수동으로 확인하고 넘길지 두 가지 입니다. 2차 장애에 대응할 수 있는 새로운 선택지가 생겼다는 사실은 운영 부담을 압도적으로 줄일 수 있는 계기가 되었습니다.

5. ETC

이것말고도 생각해보면 많습니다. 정말로 사용하지 않는 슬레이브에 개발자 접근 권한을 줘서 “무슨 짓”을 하든 적어도 서비스 간접적인 영향마저도 최소화 한다거나.. 엄청난 쿼리를 한번 날려본다거나..

Conclusion

슬레이브를 하나 더 추가했을 뿐인데.. 생각보다 많은 것을 얻었습니다.

  1. MHA+semi-sync로 데이터 신뢰성과 안정성
  2. 2차 장애에 대한 가용성 정책
  3. 백업/복구 효율성

물론 한대 더 추가함으로써 운영 리소스가 더 들어갈 수 있습니다. 그러나. 이런 것들은 자동화함으로써.. “다수 서버 운영 시 허들이 될만한 부분은 모두 “잔머리 굴려서” 자동화” 하면 되겠죠. 실제 제가 운영하고 있는 서비스에서는 “정말 크리티컬한 명령”을 제외하고는 자동화하는 정책을 굉장히 지향하고 있습니다.

서비스 특성에 따라 이것이 정답은 아니겠지만, 제가 운영하는 “OLTP 위주”의 “데이터 신뢰”가 대단히 중요한 서비스에서는 꽤나 좋은 데이터 프로세싱 “철학”이자 새로운 “경험”이라 생각해봅니다.

간만에 긴 주저리 블로깅이었습니다. 짝짝짝~! 좋은 주제로 다시 만나용~