MySQL binlog파서와 memcached plugin의 콜라보레이션!

Overview

6개월도 훌쩍 넘은 시간에. 간만에 포스팅합니다. 그동안 OGG javaue든, MySQL Binlog파서든.. 흐르는 데이터를 핸들링하는 고민으로 하루하루를 지내왔던 것 같아요. 그러던 중 이전 포스팅에서 주제로 삼았던, InnoDB memcached plugin을 Binlog parsing을 통해 데이터를 맞추면 좋을 것 같다는 생각이 들었습니다.
오늘 이 자리에서는 이런 답답함을 극복하고자, Binlog 이벤트를 활용하여, 최신 데이터를 유지시키는 방안에 대해서 이야기를 해보도록 하겠습니다.

MySQL Binary log?

MySQL에서 데이터복제를 위해서는 Binnary Log(binlog)를 쓰게 되는데, 이중 ROW 포멧으로 만들어지는 이벤트를 활용하여 다양한 데이터 핸들링이 가능합니다.
file

ROW Event는 특정 테이블에 대한 정보를 알려주는 Table map event가 우선 선행하고, 해당 테이블과 연계된 데이터가 뒤따르게 됩니다. 참고로, (InnoDB에서) 트랜잭션 처리가 이루어지면 아래와 같은 이벤트로 바이너리 로그에 기록이 됩니다.

[SQL Event] begin;
[TABLE MAP Event] tableA
  [WRITE ROW Event] data[]
  [WRITE ROW Event] data[]
  [WRITE ROW Event] data[]
[TABLE MAP Event] tableB
  [UPDATE ROW Event]  <data[], data[]>
  [UPDATE ROW Event] < data[], data[]>
[XID Event]

그리고, MySQL에서는 binlog_row_image 에 따라 전체데이터(FULL)로 기록할 것인지, 최소한의 정보(MINIMAL)만 기록할 것인지 설정할 수 있습니다.

"FULL" binlog_row_image

변경 이전/이후 모든 데이터를 기록하게 되는데, 많은 정보가 포함되어 있는만큼 다양한 핸들링을 해볼수 있습니다만.. 바이너리 로그 사이즈가 지나치게 커질 수 있다는 리스크가 있습니다.
게시판 조회수를 예로 들자면.. 게시물내용과, 게시물조회수가 동일한 테이블에 있는 상황이라면, 단순히 조회수 1이 증가할 뿐인데.. 이에 대한 바이너리 로그에는 게시물내용까지 포함되어 기록되는 결과로 이어집니다. (잘나가는 사이트에서는 환장할지도 몰라용.)

"MINIMAL" binlog_row_image

변경된 데이터와 PK 데이터만 포함하는.. 정말 최소한의 데이터만 바이너리로그에 기록합니다. 이렇게되면, 로그 사이즈 리스크에서는 자유로워졌지만.. 자유로운 데이터 핸들링에서는 아무래도 제약이 있을 수밖에 없습니다.

참고로, FULL이든 MINIMAL이든, 어떤 칼럼들이 변경되었는지에 대한 정보를 BIT SET로 묶어서 TABLE MAP Event에 포함시켜 전달을 하죠. MINIMAL 이미지 에서도 제대로 처리하기 위해서는 이녀석을 잘 보고, 정말로 변경된 칼럼을 잘 선별해야 합니다. 🙂

Binlog parser & Memcached plugin

자! Binlog 포멧에 대한 구구절절한 이야기는 이정도로 하고, MySQL Row format의 이런 특성을 통해서 무엇을 상상해볼 수있을 지 이야기를 해보도록 하겠습니다. (바로 아래와 같은 상상?)
file

이런 구조에서 고민을 해야할 것은 두가지 정도라고 생각하는데요.

1. DDL 작업
기존 서비스DB에, Memcached plugin 설정 시 칼럼 맵핑 작업을 통해, 여러 칼럼들을 묶어서 memcache protocal에 실어나를 수 있습니다만.. InnoDB plugin으로 인한 대상 테이블에 대한 DDL 제약이 생각보다 많이 생기게 됩니다.

2. 최신 캐시 데이터
캐시서비스를 운영한다면, 가장 최신의 데이터를 캐시에 유지하는 것이라고 포인트라고 개인적으로 생각합니다.

그런데, ROW format에는 어떤 경우는 PK에 대한 정보는 반드시 포함이 되어 있다고 했죠! 그렇다면, 위 그림과 같이, Binlog 이벤트를 받으면, 전달받은 PK를 기반으로 소스DB에서 데이터를 SELECT하고, 결과를 JSON으로 변환해서 타겟DB에 덮어쓰는 것은 어떨까요?
스키마로 묶여있던 칼럼들을 JSON으로 변환(스키마리스)해서 넣기 때문에, DDL 작업으로부터 자유롭습니다. 그리고, 데이터 변경 시 소스에서 가장 최신의 데이터로 적용하기 때문에, 데이터 일관성 측면에서도 유리합니다. (Binlog에 저장된 순으로 데이터를 기록하게 되면, 결과적으로 변경되는 과정이 사용자에게 노출될 수도 있겠죠?)

Binlog이벤트를 받아서, 최신 데이터를 JSON형태로 유지를 시키면, 어플리케이션에서는 그냥 memcache protocal을 통해 데이터를 GET만 하면, 굉장히 빠른 속도로 데이터 처리가 가능해지는 것이죠.

참고로 MySQL InnoDB memcached 플러그인에 대한 내용은 예전에 시리즈로 포스팅을 한 적이 있습니다. (그냥 참고삼아.ㅎ)

uldra-binlog-json-transfer

최근 밤에 잠이 잘 안와서, 간단하게 구현해보았습니다. 개인적으로 CDC관련된 자체 아류작이 워낙 많다보니.. 이제는 하루이틀 집중하면, 간단한 처리 구현은 아예 찍어내고 마네요. -_-;;
https://github.com/gywndi/uldra-binlog-json-transfer
file

Conclusion

MySQL Binlog 이벤트 처리를 활용하여 Memcached 데이터를 잘 응용해볼 수 있는 방안에 대해서 정리해봤습니다. 그리고, 고민의 결과는 이미 위에서 깃헙에 공유를 해놓았고요. ^^

uldra-binlog-json-transfer로 만든 내용은 아래와 같습니다.
1. MySQL Binlog parser
2. PK로 소스 테이블에서 데이터 SELECT
3. 결과를 JSON으로 변환하여 타겟DB에 저장

프로토타입 수준으로 만들어놓은 프로젝트이기에.. 지적질 환영이고, 의견 교류 너무 좋아하고 환장합니다. 🙂

다들 코로나 멋지게 극복하세요.

MySQL InnoDB의 메모리 캐시 서버로 변신! – 활용편 –

Overview

벌써 새해가 밝았네요. 새해 복 많이 받고 계시쥬?

판교 생활을 한지도 벌써 만 7년을 훌쩍 지나, 8년을 향해 가고 있군요. 2020년 우주의 원더키디(아재 인증)의 그 시간이 이렇게나 빠르게 찾아올 줄은 그때의 저는 몰랐답니다. ㅠㅠ

오늘 주제는, 그동안 MySQL innodb memcached 플러그인의 마지막편, (지극히 개인적인 의견인) 서비스적인 활용 편입니다. 상상의 날개를 펼쳐서, 서비스 최우선적인 활용을 위해 무엇을 꿈꿔볼 수 있을지, 이야기 해보고자 합니다. (이전 포스팅은 하단을 참고요.)

1탄. MySQL InnoDB의 메모리 캐시 서버로 변신! – 설정편 –
2탄. MySQL InnoDB의 메모리 캐시 서버로 변신! – 모니터링편 –

두 시리즈에 이은 마지막 활용편 시작합니다.

InnoDB memcached strength

제 기준으로 가장 매혹적인 부분은 memcached protocal로 데이터를 빠르게 접근을 하면서도, 데이터 자체가 디스크에 존재한다는 것입니다. 즉, 휘발성이 아닌 데이터 특성은 굉장히 매혹적이죠.

일반적으로 캐시 시스템에서는 유한한 메모리 리소스를 효율적으로 활용하기 위해 보통은 TTL(캐시에서 살아있을 수 있는 시간)을 둡니다. 캐시 데이터가 언제까지 유효한지를 지정을 하고, 해당 시간이 지나버리면, 가차없이 메모리에서 날려버리는 것인데.

만약 TTL을 무한하게 제공을 해볼 수 있다면 어떨까요?
예를 들어, 어플리케이션의 Token 정보라든지, 쉽게 변하지 않을 사용자 정보라든지, 혹은 컨텐츠 정보성 데이터라든지.. TTL을 길게 가져갈 수록 좋을만한 서비스에 적용을 해본다면 어떻까 생각해볼 수 있겠죠. 일반적으로 트래픽이 많이 몰리던 상황(서비스에 따라 다르겠지만)은, TTL이 24시간을 넘지 못해서, 다음날 비슷한 시간대의 트래픽이 상당히 몰릴 때 였던 것 같습니다.

file

이런 서비스에 조금 전 언급한대로, TTL이 없는 형태로 memcache 프로토콜로 데이터를 접근해볼 수 있다면?? 개인적인 생각으로는 엄청난 효율을 발휘해볼 수 있다고 봅니다. ㅎㅎ

Beyond physical memory

메모리/디스크도 아무래도 제한된 리소스이기 때문에. TTL을 무한히 가져갈 수는 없습니다. TTL을 늘리려면 아무래도, 물리 머신의 디스크와 메모리를 점유해야 하기 때문이죠. 자, 그렇다면, 아래 그림처럼 동일한 데이터를 용도에 따라 영역을 다르게 접근해볼 수 있다면? (각 노드는 리플리케이션 복제본)
file
동일한 데이터이지만, 어플리케이션에서 그룹핑해서 영역을 나눠서 본다면, 각 노드 안의 메모리 버퍼풀에 존재하는 데이터는 각각 다를것이고, 결과적으로 가장 효율적인 퍼포먼스를 보여줄 수 있습니다. (동일한 데이터 찢어서 처리하기)
심지어, 특정 노드에서 장애가 발생해서 다른 노드로 역할을 넘겼을 시에도 데이터 일관성에 대해서 걱정할 필요는 없겠죠. ㅎ 모든 노드가 동일한 데이터를 가졌을테니요.

여기에 추가로 발상을 한번더 살짜쿵 전환을 해서. 데이터 복제 측면으로 접근을 해봅시다. 개인적인 생각이지만, MySQL의 강점은 바로 데이터 복제입니다. 그리고 다양한 스토리지 엔진에 효과적으로 데이터 복제를 가능하게 해주는 Binary Log 이야말로, MySQL이 가진 최고의 매력포인트라고 저는 생각을 합니다.

file

데이터를 MySQL Replication을 통해서 동기화하는 방법도 있지만, MySQL memcached 테이블에 JSON 형태로 직접 넣고, 이것을 memcache plugin으로 꺼내쓰는 것도 좋은 활용도일 것 같네요. ^^

Conclusion

지금까지 총 3번에 걸쳐서 MySQL memcached plugin에 대해서 살펴보았습니다. 1탄에서는 설정 및 퍼포먼스, 2탄에서는 모니터링, 그리고 마지막으로 3탄에서는 뜬그룹 잡는 활용편으로 주제를 잡았었는데요.

마지막 3탄인 오늘 이야기하고 싶은내용은 이렇습니다.

서비스 특성에 따라 다르지만, 적어도 TTL을 최대한 늘릴 수록 효율적인 서비스에서는 최고의 퍼포먼스를 발휘해볼 수 있다라는 이야기를 전하고 싶습니다. 그리고, 캐시 데이터 실시간 적용에는 MySQL Replication을 활용하거나, 혹은 별도의 Binlog 기반의 리플리케이터를 하나 생성해보는 것도 좋은 활용도일 것이고요.

다음 주제로는, 정말 심플하게 접근해볼 수 있는 MySQL Binlog Parser를 이야기해보도 좋을 듯 합니다. ^^

새해 첫 주, 알찬 한주가 되기를 바라면서, 오늘 제 글을 마무리 하겠습니다. 새해 복 많이 받으세요. ^^

MySQL InnoDB의 메모리 캐시 서버로 변신! – 모니터링편 –

Overview

MySQL memcached plugin 2탄! 모니터링편입니다.
어떤 초호화 솔루션일지라도, 시스템의 정확한 상태를 파악할 수 없다면, 사용하기에는 참으로 꺼려집니다. 그래서 어떤 방법이든, 가장 효율적인 모니터링 방안을 찾아봐야 하겠는데요. 저는 개인적으로는 prometheus를 활용한 metric수집을 선호합니다.
오늘 이 자리에서는 Prometheus에서 MySQL InnoDB memcached plugin을 모니터링 하는 방법에 대해서 이야기를 해보도록 하겠습니다. 🙂

Why prometheus?

이유는 단순합니다. 이미 만들어져 있는 exporter가 굉장히 많다는 것, 만약 원하는 것들이 있다면 나의 구미에 맞게 기능을 추가해서 쉽게 접근할 수 있다는 것! 즉, 오픈소스라는 것!! 무엇보다 Time-series 기반의 데이터 저장소인 Prometheus로 정말로 효율적으로 모니터링 매트릭 정보를 수집할 수 있다는 것! Prometheus는 로그 수집에 최적화 되어 있다고 과언이 아닙니다.

file

이미 MySQL관련하여 Prometheus 기반으로 대규모 모니터링을 하고 있고.. alerting을 위해 자체적으로 구성한 "pmm-ruled"로 다수의 시스템을 무리없이 이슈 감지하고 있으니, 이것을 시도 안할 이유가 전혀 없습니다. (트래픽은 쥐꼬리만한, 글 몇개 없는 영문 블로그 투척..ㅋㅋ)

참고로 prometheus으로 공식적으로 모니터링을 할 수 있는 exporter들이 이렇게나 많답니다. 써본것은 별로 없지만, 이런 시스템을 새롭게 시작할지라도.. 모니터링에서는 한시름 놓을 수 있겠다는.. -_-;;
https://prometheus.io/docs/instrumenting/exporters/

Start! memcached exporter

Prometheus에서는 공식적으로 하단 exporter로 memcached를 모니터링합니다.
https://github.com/prometheus/memcached_exporter

이렇게 받아서 컴파일을 하면 되고..

$ go get github.com/prometheus/memcached_exporter
$ cd $GOPATH/src/github.com/prometheus/memcached_exporter/
$ make
$ ls -al memcached_exporter
-rwxr-xr-x  1 gywndi  staff  12507644  9 19 21:11 memcached_exporter

바로 이전에 구성을 했던 MySQL InnoDB memcached plugin이 있는 곳을 향하여 exporter를 올려봅니다.

$ ./memcached_exporter --memcached.address=10.5.5.12:11211
INFO[0000] Starting memcached_exporter (version=, branch=, revision=)  source="main.go:795"
INFO[0000] Build context (go=go1.11.5, user=, date=)     source="main.go:796"
INFO[0000] Starting HTTP server on :9150                 source="main.go:827"

Problem

그런데 문제가 생겼네요.
http://10.5.5.101:9150/metrics에 접근해서, memcached exporter가 수집해서 뿌려주는 metric 정보를 확인해보았는데.. exporter에서 아래와 같은 이상한 에러를 뱉어낸 것이죠. (참고로, exporter를 올린 곳의 아이피는 10.5.5.101입니다.)

ERRO[0024] Could not query stats settings: memcache: unexpected stats line format "STAT logger standard error\r\n"  source="main.go:522"

심지어 exporter에서는 아래와 같이 memcached_up 이 "0"인 상태.. 즉, memcached가 죽어있다는 형태로 데이터를 뿌려줍니다. 모니터링을 위해 붙인 exporter가 memcached 데몬이 늘 죽어있다고 이야기를 하면 큰일날 이야기겠죠. ㅠㅠ

# HELP memcached_up Could the memcached server be reached.
# TYPE memcached_up gauge
memcached_up 0

MySQL memcached plugin에서 stats settings 결과는 아래와 같습니다.

$ telnet 127.0.0.1 11211
Trying 10.5.5.12...
Connected to 10.5.5.12.
Escape character is '^]'.
stats settings
STAT maxbytes 67108864
STAT maxconns 1000
..skip ..
STAT item_size_max 1048576
STAT topkeys 0
STAT logger standard erro   <-- 이녀석!!
END

문제는 저 윗부분에서 4개의 단어로 이루어진 저 부분에서 발생한 문제이지요. memcached exporter에서 stats settings을 처리하는 statusSettingsFromAddr 함수에서, 결과가 3개의 단어로만 이루어진 것을 정상 패턴으로 인지하고, 그 외에는 무조건 에러로 리턴하는 부분에서 발생한 것인데요.

memcache.go 파일의 가장 하단에 위치한 statusSettingsFromAddr 함수 내부의 이 부분이 원인입니다.

stats := map[string]string{}
for err == nil && !bytes.Equal(line, resultEnd) {
    s := bytes.Split(line, []byte(" "))
    if len(s) != 3 || !bytes.HasPrefix(s[0], resultStatPrefix) {
        return fmt.Errorf("memcache: unexpected stats line format %q", line)
    }
    stats[string(s[1])] = string(bytes.TrimSpace(s[2]))
    line, err = rw.ReadSlice('\n')
    if err != nil {
        return err
    }
}

Modify code

그래서 이것을 아래와 같이 4글자까지 정상 패턴으로 인지하도록 변경을 했습니다. 물론, 정상 패턴을 3단어로만 했던 원작자의 정확한 의도는 모르지만요.. ㅠㅠ

stats := map[string]string{}
for err == nil && !bytes.Equal(line, resultEnd) {
    s := bytes.Split(line, []byte(" "))
    if len(s) == 3 {
        stats[string(s[1])] = string(bytes.TrimSpace(s[2]))
    } else if len(s) == 4 {
        stats[string(s[1])] = string(bytes.TrimSpace(s[2])) + "-" + string(bytes.TrimSpace(s[2]))
    } else {
        return fmt.Errorf("memcache: unexpected stats line format %q", line)
    }
    line, err = rw.ReadSlice('\n')
    if err != nil {
        return err
    }
}

이제 컴파일하고, 다시 memcached exporter를 구동해볼까요?

$ cd $GOPATH/src/github.com/prometheus/memcached_exporter
$ go build .
$ ./memcached_exporter --memcached.address=10.5.5.12:11211
INFO[0000] Starting memcached_exporter (version=, branch=, revision=)  source="main.go:795"
INFO[0000] Build context (go=go1.11.5, user=, date=)     source="main.go:796"
INFO[0000] Starting HTTP server on :9150                 source="main.go:827"

문제없이 잘 올라왔고, http://10.5.5.101:9150/metrics에 접근해도 정상적으로 memcached 구동 상태를 명확하게 보여주고 있군요.

# HELP memcached_up Could the memcached server be reached.
# TYPE memcached_up gauge
memcached_up 1   <-- 요기요기요기

External metric with pmm-admin

PMM을 구성하는 것에 대해서는 이 자리에서 설명하지 않겠습니다.

$ pmm-admin add external:metrics memcached 10.5.5.101:9150=memcached01 --interval=10s

그러면 이런 모양으로 MySQL InnoDB memcached로부터 상태 매트릭 정보를 수집하게 됩니다.
file

만약, 추가로 memcached라는 job 이름으로 하나를 더 추가하고 싶다면?? 이렇게 하면 됩니다요.

$ pmm-admin add external:metrics memcached \
   10.5.5.101:9150=memcached01 \
   10.5.5.102:9150=memcached02 \
   --interval=10s

이제부터는 매 10초마다 memcached 에 접근해서 상태 정보를 수집해서 prometheus에 넣습니다. 이로써, MySQL memcached plugin을 모니터링하기 위한 데이터 수집단계가 모두 마무리 되었습니다. ㅎㅎ
prometheus에서 { job="memcached" } 쿼리 결과 매트릭을 활용해서, 초당 get 트래픽 뿐만 아니라, get miss 카운트도 충분히 확인 가능합니다. 이런 것들을 잘 활용한다면.. memcached 데몬의 실시간 모니터링 뿐만 아니라 트래픽 트랜드도 쉽게 확인할 수 있겠네요.

Conclusion

Grafana로 필요한 그래프를 만들어야하는 단계가 남았지만.. 여기서는 선호도에 따른 내용이라.(사실 저도.. get 오퍼레이션 유입 카운트와.. 히트율 정보.. 단 하나의 모니터링만 해놓은지라;; ㅋㅋㅋ 쓸 얘기가 없네요.)

  • memcached plugin으로 모니터링
  • MySQL memcached plugin에 맞게 소스 일부 수정
  • pmm-admin을 활용하여, 동적으로 exporter 등록 및 데이터 수집

세상에는 널린 지식이 많고.. 쉽게 가져다 쓸 수 있지만. 잘 쓰려면.. 발생하는 문제에도 열린(?) 마음으로 당황하지 말고 접근을 하면.. 진짜 손 안대고 코를 시원하게 풀 수 있는 다양한 길이 여기저기 많이 열려 있는 듯 합니다.

참고로 위 이슈는 하단 두 개의 깃헙에서 쪼르고 있습니다. (해줄지는 모르겠지만..ㅠㅠ)
https://github.com/prometheus/memcached_exporter/pull/71
https://github.com/grobie/gomemcache/pull/1

좋은 하루 되세용.