소소한 데이터 이야기 – pt-online-schema-change 편 -

Overview

MySQL 5.6부터는 Online ddl 기능을 제공하기 시작하였지만, 사실은 이전에도 트리거 기반의 online alter 유틸로 서비스 중단없이 테이블 스키마 변경을 수행했었습니다. 이중 percona에서 제공해주는 pt-online-schema-change가 많이들 활용되고 있는데요. 오늘은 돌다리도 망치로 때려가면서 안정성에 신중히 접근한 우리의 케이스에 대해서 데이터 기준으로 얘기를 해보고자 합니다.

pt-online-schema-change?

얘기하기에 앞서서, 이 툴에 대해서 다시한번 짚어보겠습니다. 대충 동작 순서는 아래와 같이..

  • 변경할 스키마 구조의 임시 테이블을 생성하고,
  •  insert/update/delete 트리거를 만들어서 최근 변경 데이터를 동기화하고,
  • 처음부터 끝까지 일정 청크 사이즈로 읽으면서 임시 테이블에 복사한 후,
  • 완료되면 RENAME TABLE하여 완료

동작합니다.

pt-online-schema-change

pt-online-schema-change

조금더 시각화된 설명을 원한다면. 하단 블로그를 참고하세요.
>> http://small-dbtalk.blogspot.kr/2014/02/mysql-table-schema.html

Goals

24*365 서비스인만큼, 목표는 여전히 명쾌합니다. 심플하쥬?

  1. 무중단 스키마 변경
  2. 서비스 영향도 제로

그런데, 구닥다리 MySQL 버전을 사용하지 않으면서, 왜 pt-online-schema-change와 같은 툴 얘기를 꺼내냐고요? 우선은 상황에 따라 가능하지 않기 때문입니다.
>> https://dev.mysql.com/doc/refman/5.6/en/innodb-create-index-overview.html

DML이 블록킹되는 케이스(Permits Concurrent DML이 NO인 경우) 에서는 절대적으로 온라인 서비스 적용이 불가합니다.

혹은 아래와 같이 다수의 alter를 동시에 적용하고자 하는 케이스도 찾아볼 수 있고..

alter table tab
 add excol01 varhcar(10),
 add excol02 text,
 add key ix_col01(col01),
 add key ix_excol01(excol01);

로그성 테이블이 일정 사이즈 이상된 시점에 “파티셔닝 적용하는 케이스”도 생각해볼 수 있겠네요.

그렇기에, (개인적인 생각으로는) 아무리 online ddl 기능이 좋아질지라도, pt-online-schema-change와 같은 트리거 기반의 스키마 변경 유틸은 여전히 유효할 것으로 조심스레 예측해봅니다. 적용 여부 판단은 데이터쟁이의 판단 하에..ㅎㅎ

Risk Point

아무튼 지금까지 우리의 상황을 정리해보고자 한다면..

  • MySQL의 online ddl 사용 불가
  • 서비스 영향도 없은 무중단 스키마 변경

두 가지 상황이고, 이 난관 극복을 위해서 “트리거 기반의 유틸”인 pt-online-schema-change를 활용하기로 하였습니다.

우선 pt-online-schema-change 동작 로직 중, 트리거를 통한 트래픽 발생은 어느정도 예측할 수 있습니다. 타겟 테이블에 발생하는 트랜잭션 양만큼 딱 증가할 것이기에, 현재 데이터 변경량을 보면 어느정도 트랜잭션이 더 늘어날지는 어느정도 판단이 가능하죠.

문제는 처음부터 끝까지 청크 사이즈로 읽어가면서 임시 테이블에 데이터를 복사하는 경우 이 부분인데요. 데이터 복제를 위함이든, 데이터 복구를 위함이든, MySQL에는 바이너리 로그가 거의 필수입니다. 즉, 데이터 복사를 위한 처리 부분도 어떤 방식이든 바이너리 로그에 기록됩니다. 최근에는 바이너리 로그 포멧이 변경된 ROW 자체가 기록이 되는 ROW 포멧 방식으로 대부분 동작합니다. 게다가 만약 트랜잭션 ISOLATION LEVEL을 READ-COMMITTED 사용하고자 한다면, ROW FORMAT이 전제 조건입니다.

여기서 우리의 상황에 세번째 항목을 붙여 아래와 같이 얘기해봅니다.

  • MySQL의 online ddl 사용 불가
  • 서비스 영향도 없은 무중단 스키마 변경
  • 바이너리 로그는 ROW 포멧으로 동작

처음부터 끝까지 데이터를 카피하는 상황에서, 바이너리 로그 사이즈가 기하급수적으로 늘어나는 것에 대한 영향도를 최소화해야 합니다. 대략 다음 두가지 정도?

  1. 네트워크 트래픽 과도로 인한 서비스 영향 발생 가능
  2. 바이너리 로그 과다 적재로 인한 디스크 Full 발생 가능

서비스에 직접적인 영향을 미칠 뿐만 아니라, 잘못하면 서비스 불능 상태로까지 이어질 수 있습니다. (특히 2번 케이스는.. 서비스 멈춰요~ ㅜㅜ)

Let’s solve

문제가 있으면 해결하면 되고, 해결할 수 없으면 대안을 마련하면 되고.. 아무튼.. 임팩 최소화 노력을 해보도록 하죠.

1. Reduce Chunk Size

Chunk 단위로 데이터를 카피하는 구조이기 때문에, 다량의 로그가 슬레이브 서버로 스파이크 튀듯이 전송되는 상황은 막아야합니다. 순간순간 바이너리 로그 폭발(?)이 일어나며 서비스 영향을 줄 수 있는 요소가 있습니다.

예를들자면, 1G짜리 테이블 100만건과 20G짜리 100만건 테이블 중, 우리에게 주어진 상황에서 더욱 리스크한 녀석은 누구일까요? -_-; 당연히 20G짜리 테이블입니다.

동일한 ROW 사이즈로 데이터 복사를 해버리면, 매 트랜잭션마다 꽤나 큰 바이너리로그가 한방에 생성됩니다. 특히 semi-sync를 쓰는 경우에는 이 전송에 따른 지연이 기존 트랜잭션에 영향을 줄 수도 있습니다. 그렇다면.. 이런 케이스에서 적용해볼 수 있는 방법은 Chunk Size를 줄여서 이런 리스크 요소를 최소화해보는 것입니다. 잘게잘게 잘라서.. 임팩을 줄여가면서.. 조금씩 조금씩.. (대신 쿼리량은 늘어나게 되버리는.. 쥬릅ㅜㅜ)

pt-online-schema-change 툴에서는 chunk-size 옵션으로 제거 가능하며, 이 값을 기본값(1000)을 적절하게 하향 조정해봅니다. 물론 각 Copy 사이사이마다 일정 시간 쉬어갈 수 있는 interval이 있다면.. 더욱 제어가 쉬웠을텐데. 아쉽게도, 아직은 제공하지 않습니다. (만들어서 percona에 적용해달라고 푸시해볼까요? ㅋㅋ)

아무튼 이렇게해서 만들어진 스크립트 실행 구문은 아래 형태를 보이겠네요.

pt-online-schema-change \
--alter "add excol01 varhcar(10)" D=db1,t=tbname \
--chunk-size=200 \
--defaults-file=/etc/my.cnf \
--host=127.0.0.1 \
--port=3306 \
--user=root \
--ask-pass \
--chunk-index=PRIMARY \
--charset=UTF8 \
--execute

2. Change Session Variables

테이블 사이즈가 너무 커서, 바이너리 로그를 담기에 여의치 않을 때를 생각해봅시다. 물론 미리미리 binlog purge하면서 과거 로그를 제거해볼 수 있겠지만, 사실 백업/복구 입장에서 “데이터 변경 이력 로그” 삭제는 리스크할 수도 있습니다. Point-In Recovery가 안될 수도 있기 때문이죠.

이 경우에서는 데이터 측면에서 조금 다르게 접근해 보자면, 우선 서비스 환경은 아래와 같습니다.

  • 현재 트랜잭션 ISOLATION LEVEL은 READ-COMMITTED이다.
  • 현재 바이너리 로그는 ROW 포멧이다.

그렇다면.. 데이터를 카피하는 백그라운드 프로세스 기준에서도 위 조건을 충족해야할까요? 데이터 카피시 발생하는 쿼리를 SQL기반의 statement 방식으로 바이너리로그에 기록을 해보면 안될까요?

pt-online-schema-change에서의 세션 파라메터를 아래와 같이 지정을 해본다면,

  • COPY 프로세스의  트랜잭션 ISOLATION LEVEL은 REPEATABLE-READ이다.
  • COPY 프로세스의  바이너리 로그는 STATEMENT 포멧이다.

상황으로 접근해보면 어떨까요? pt-online-schema-change에서는 세션 파라메터로 set-vars 옵션에 각 파라메터 지적을 콤마로 구분해서 적용해볼 수 있습니다.

pt-online-schema-change \
--alter "add excol01 varhcar(10)" D=db1,t=tbname \
--chunk-size=200 \
--defaults-file=/etc/my.cnf \
--host=127.0.0.1 \
--port=3306 \
--user=root \
--ask-pass \
--chunk-index=PRIMARY \
--charset=UTF8 \
--set-vars="tx_isolation='repeatable-read',binlog_format='statement'" \
--execute

사실 이렇게 수행을 하면, 바이너리 로그 사이즈는 걱정할 필요 없습니다. 게다가 네트워크 트래픽도 거의 차지 않을 것이고. 참으로 안전해보이고, 무조건 이렇게 사용하면 될 것 처럼 생각할 수도 있겠습니다만.. 적어도 이로인한 영향도는 미리 파악하고 사용하는 것이 좋겠죠?

1. isolation level 에 따른 Locking

아무래도 isolation level 이 한단계 높은 수위(read-committed -> repeatable-read)로 관리되다보니, Lock 영향도를 무시할 수 없겠죠? “next key lock“이라든지.. “gap lock“이라든지.. 이런 영향도를 동일하게 받을 수 있다는 점을 인지하고 있어야 합니다. (물론 대용량 테이블에서는 영향도가 제한적이기는 합니다. ㅎㅎ)

2. 슬레이브는 여전히 ROW FORMAT

마스터에서는 STATEMENT FORMAT으로 바이너리로그 기록이 잘 되고는 있습니다만, 문제는 슬레이브에서는 여전히 “ROW FORMAT”으로 기록됩니다. 이건 쿼리 패턴(insert ignore .. select .. )에 따른 어쩔 수 없는 요소로.. 슬레이브는 그냥 주기적으로 purge하면서 대응을 하는 것이 제일 현명해 보이네요. 아.. log-slave-updates 옵션을 ON 상태로 운영하는 경우만 해당되겠네요.

Conclusion

물이 흐르듯 데이터도 흐릅니다. 물길이 변하면 유속이 바뀌듯, 데이터도 마찬가지입니다.

흐름을 “잘 제어하기” 위해 온라인 툴을 활용하였고, 내가 원하는 모양으로 만들기 위해 거쳐갈 “데이터의 흐름“을 생각해보았습니다. 그리고, 발생할 수 있는 “리스크를 예측“해볼 수 있었죠.

예전에는 아무 생각없이 썼던, 손 쉬운 online alter 툴로만 인지를 했었지만.. 서비스가 무시무시하게 사악(?)해지고 나니 돌다리도 쇠망치로 두드려보며 건너보게 되더군요.

사실 이것이 정답은 아닙니다. 더 좋은 방안도 있을 것이고. 효율적인 개선안도 있을 것이고. 그렇지만, 데이터쟁이답게, 닥쳐올 미션들을 “데이터의 흐름”에 촛점을 두어 앞으로도 “장애없는 서비스“를 만들어가도록 노력만큼은 변함이 없을 것입니다. :-)

좋은 밤 되세요. ㅋ

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

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 파라메터 설정

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

좋은 밤 되세요. ^^