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

좋은 하루 되세용.

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

Overview

꽤나 오래전의 일이었습니다. MariaDB에서 Handler Socket이 들어간 이후 얼마 후인 것으로 기억합니다. MySQL lab버전에 memcached plugin 기능이 추가되었고, memcache protocal로 InnoDB 데이터에 직접 접근할 수 있는 길이 열린 것이었죠. (아마도 거의 8년 정도 전의 일이었던 것같은..) 아무튼 당시, 이것에 대해 간단하게 테스트만 해보고, MySQL을 캐시형태로 잘 활용할 수 있겠다라는 희망만 품고 지나버렸다는 기억이 나네요.

이제 Disk는 과거의 통돌이 디스크가 아니죠. 기계 장치를 탈피하여, 이제는 모터없는 전자기기.. SSD의 시대가 도래하였습니다. 통돌이 대비, 어마어마한 수치의 Random I/O를 제공해주는만큼, 이제 DB 데이터에 새로운 패러다임(?)으로 접근할 수 있겠다는 시점이 온 것 같아요.

말이 거창했습니다. 8년이 훌쩍 지난 지금 MySQL 5.7에서 InnoDB memcached plugin 활용성 테스트를 해보았어요.

MySQL memcached plugin?

MySQL 5.6부터 들어왔던 것 같은데.. InnoDB의 데이터를 memcached 프로토콜을 통해서 접근할 수 있다는 것을 의미합니다. 플러그인을 통하여 구동되기 때문에.. 캐시와 디비의 몸통은 하나이며, InnoDB 데이터에 직접 접근할 수 있기도 하지만, 캐시 공간을 별도로 두어.. 캐시처럼 사용할 수도 있어요. (옵션을 주면, set 오퍼레이션이 binlog에 남는다고 하던데.. 해보지는 않음 ㅋㅋ)

innodb memcached
출처: https://dev.mysql.com/doc/refman/8.0/en/innodb-memcached-intro.html

그런데, 만약.. 데이터베이스의 테이블 데이터를.. memcached 프로토콜로 직접적으로 access할 수 있다면 어떨까요? 메모리 위주로 데이터 처리가 이루어질 때.. 가장 많은 리소스를 차지하는 부분은 바로 쿼리 파싱과 옵티마이징 단계입니다. 만약 PK 조회 위주의 서비스이며.. 이것들이 자주 변하지 않는 데이터.. 이를테면 “서비스토큰”이라든지, “사용자정보” 같은 타입이라면..?

  • PK 조회 위주의 서비스
  • 변경 빈도가 굉장히 낮음

심지어 이 데이터는 이미 InnoDB 라는 안정적인 데이터베이스 파일로 존재하기 때문에.. 예기치 않은 정전이 발생했을지라도, 사라지지 않습니다. 물론, 파일의 데이터를 메모리로 올리는 웜업 시간이 어느정도 소요될테지만.. 최근의 DB의 스토리지들이 SSD 기반으로 많이들 구성되어가는 추세에서, 웜업 시간이 큰 문제는 될 것 같지는 않네요.

MySQL InnoDB memcached plugin을 여러 방식으로 설정하여 사용할 수 있겠지만, 오늘 이야기할 내용은, memcache 프로토콜만 사용할 뿐, 실제 액세스하는 영역은 DB 데이터 그 자체임을 우선 밝히고 다음으로 넘어가도록 하겠습니다.

Configuration

오라클 문서(innodb-memcached-setup)를 참고할 수도 있겠습니다만, 오늘 이 자리에서는 memcached 플러그인을 단순히 memcache 프로토콜을 쓰기 위한 용도 기준으로만 구성해보도록 하겠습니다.

우선, InnoDB 플러그인 구성을 위해 아래와 같이 memcached 관련된 스키마를 구성합니다. 🙂 여기서 $MYSQL_HOME는 MySQL이 설치된 홈디렉토리를 의미하며, 각자 시스템 구성 환경에 따라 다르겠죠.

$ mysql -uroot < $MYSQL_HOME/share/innodb_memcached_config.sql

자, 이제 간단한 성능 테스트를 위한 환경을 구성해볼까요? 먼저 아래와 같이 테스트 테이블 하나를 생성하고, 성능 테스트를 위한 약 100만 건의 데이터를 생성해봅니다.

## 테이블 생성
mysql> CREATE DATABASE `memcache_test`;
mysql> CREATE TABLE `memcache_test`.`token` (
    ->  `id` varchar(32) NOT NULL,
    ->  `token` varchar(128) NOT NULL,
    ->  PRIMARY KEY (`id`)
    ->);

## 테스트 데이터 생성
mysql> insert ignore into token 
    -> select md5(rand()), concat(uuid(), md5(rand())) from dual;
mysql> insert ignore into token 
    -> select md5(rand()), concat(uuid(), md5(rand())) from token;
... 반복 ...

mysql> select count(*) from token;
+----------+
| count(*) |
+----------+
|   950435 |
+----------+

이제, 위에서 생성한 테이블을 memcached plugin이 보도록 설정 정보를 수정을 하고, 최종적으로 플러그인을 올려줍니다.

## 캐시 정책 변경(get만 가능하고, 캐시 영역없이 InnoDB에서 직접 읽도록 세팅)
mysql> update cache_policies set 
    ->   get_policy = 'innodb_only', 
    ->   set_policy = 'disabled', 
    ->   delete_policy='disabled', 
    ->   flush_policy = 'disabled';

## 기존 정책 삭제 후, token 신규 정책 추가
mysql> delete from containers;
mysql> insert into containers values ('token', 'memcache_test', 'token', 'id', 'token', 0,0,0, 'PRIMARY');

## InnoDB memcached 플러그인 구동
mysql> INSTALL PLUGIN daemon_memcached soname "libmemcached.so"; 

이 과정을 거치면, 디비 데이터를 memcached protocol로 직접 접근할 수 있는 환경이 만들어집니다. 기본 포트로 11211 포트가 개방되며, 이 포트를 통해 memcache protocal 로 get 오퍼레이션을 수행하면 InnoDB 데이터 영역으로부터 직접 데이터를 가져올 수 있습니다. (물론, 다수의 칼럼이 있는 경우)

즉, SQL 변경한 데이터를 직접 읽을 수 있다는 것이죠. (테이블 칼럼을 구분자로 맵핑하여 memcache의 VALUE를 만들 수 있으나, 여기서는 스킵합니다.)

Performance

간단한 테스트를 해보았습니다. SQL vs memcached!! 생성한 데이터 전체 ID값 기준으로 랜덤한 아래 패턴의 쿼리를 각각 초당 약 7만 건의 쿼리를 수행하여 시스템 리소스를 측정하였습니다.

## SQL ##
SELECT * FROM token WHERE id = ${id}

## Memcache ##
get @@token.${id}

SQL과 memcache 모두 7만 쿼리 처리는 큰 무리없이 처리 하지만, 시스템 리소스 측면에서는 큰 차이를 보였습니다. SQL인 경우 30~40% 리소스를 사용하는 반면에, memcache에서는 대략 5% 내외의 리소스를 사용할 뿐 아주 여유있는 시스템 상황을 보입니다. SQL 파싱와 옵티마이징 단계를 스킵한, 단순 데이터 GET 처리 효과이겠죠.

SQL vs memcache

이왕 할 테스트, 장기적으로 약 10일간 초당 약 6~7만 건 쿼리를 지속적으로 주면서 안정성 테스트를 해보았습니다.
일단, 이 기간동안 memcache GET 오퍼레이션 에러 카운트는 0건입니다. 초단위 평균 응답속도 결과는 초당 평균 0.3ms~0.5ms 사이로 좋은 응답 속도를 보였습니다. 무엇보다, 제일 느렸던 것은 대략 1.5ms 정도로.. DB datafile의 B-Tree로 직접 데이터를 읽을지라도, 캐시로써 전혀 손색없는 속도를 보였습니다. 🙂

+-----+----------+
| ms  | seconds  |
+-----+----------+
| 0.2 |       99 |
| 0.3 |   661736 |
| 0.4 |   162582 |
| 0.5 |     5686 |
| 0.6 |     1769 |
| 0.7 |     1004 |
| 0.8 |      576 |
| 0.9 |      310 |
| 1.0 |      159 |
| 1.1 |       77 |
| 1.2 |       29 |
| 1.3 |       12 |
| 1.4 |        4 |
| 1.5 |        1 |
+-----+----------+

Conclusion

MySQL InnoDB에 데이터가 변경이 되면, 이를 memcached protocol로 접근할 수 있기 때문에.. 이것을 잘 활용하면 복잡한 캐시 관리 없이 쉽게 캐시 레이어 구성이 가능해보입니다.

  • 세션 혹은 토큰 데이터(사용자 데이터)를 MySQL복제 구성하고, 이것으로 실시간 서비스를 한다면?
  • Binlog를 통한 리플리케이터를 하나 작성해서, 원하는 형태로 데이터를 조작(ex, json)한 이후, 이것을 memcache protocal로 데이터를 접근한다면?
  • 코드 혹은 공통 정보에 대한 데이터(거의 안변하는 데이터)를 캐시 구성 없이 GET 오퍼레이션으로 바로 서비스에 사용한다면??

서비스의 요구 사항에 맞게, 데이터를 원하는 모습으로, 적재적소에 MySQL InnoDB memcached plugin을 잘 활용할 수 있다면, 휘발성에서 오는 한계점, 동기화 이슈 등등 많은 부분을 정말 단순한 구조로 해결할 수 있을 것으로 기대됩니다.

다음 편은, 이렇게 구성한 memcached plugin을 어떻게 PMM(prometheus & grafana) 구조로 쉽게 모니터링을 할 수 있을지에 대해 이야기를 해보도록 할께요.