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) 구조로 쉽게 모니터링을 할 수 있을지에 대해 이야기를 해보도록 할께요.

MySQL 파티셔닝 테이블 SELECT가 느려요.

Overview

네이티브 파티셔닝 적용 이전의 MySQL은, 파티셔닝 파일들은 각각이 테이블로써 관리되었죠. 그래서, table cache 로 인한 메모리 부족 현상은 인지하고 있었습니다만.. 이것 외에는 특별히 성능 저하 요소는 없다고 생각해왔어요. (http://small-dbtalk.blogspot.com/2013/09/mysql-table-cache.html)

그런데, 얼마전 서버당 4개의 데이터베이스를 만들고, 각각 데이터베이스 안에 26개월로 분할된 파티셔닝된 테이블을 넣고, 간단한 Range scan 성능 테스트를 하였는데.. 말도안되는 수치를 보였습니다. 이 관련하여 간단하게 이에 대해 알아보도록 할께요. 🙂

Problem

하단과 같은 테이블 구조에서, 단순히 최근 10건의 데이터만 끌어오는 형식의 SQL을 다수 실행시켜 간단한 트래픽을 주었을 때.. 성능적으로 별다른 문제는 없을 것이라고 생각을 했습니다. 우리의 메모리는 기대치보다 훨씬 웃돌았기 때문에.. ㅎㅎ (참고로, InnoDB 버퍼풀 사이즈 대비 데이터 사이즈는 약 10배 이상이지만, 최근 파티셔닝 사이즈를 따지면, 버퍼풀 안에 충분히 들어올만한 상황이었습니다.)

## table
CREATE TABLE `tab` (
`COL01` varchar(16) COLLATE utf8mb4_bin NOT NULL,
`PAR_DATE` varchar(8) COLLATE utf8mb4_bin NOT NULL,
`COL02` decimal(10,0) NOT NULL,
`COL03` decimal(10,0) NOT NULL,
.. skip ..
`APLY_TMST2` timestamp(3) NOT NULL,
PRIMARY KEY (`COL01`,`PAR_DATE`,`COL02`,`COL03`),
) ENGINE=InnoDB
PARTITION BY RANGE COLUMNS(PAR_DATE)
(PARTITION PF_201707 VALUES LESS THAN ('20170801') ENGINE = InnoDB,
 PARTITION PF_201708 VALUES LESS THAN ('20170901') ENGINE = InnoDB,
 .. skip ..
 PARTITION PF_201907 VALUES LESS THAN ('20190801') ENGINE = InnoDB,
 PARTITION PF_201908 VALUES LESS THAN ('20190901') ENGINE = InnoDB);

## Query
select * from tab
where col01 = ?
order by par_date desc, col02 desc, col03 desc
limit 10

그러나.. 예상과는 다르게, 말도안되는 퍼포먼스와, 이상한 IO패턴을 보이는 결과를 보였죠.

Why?

결론부터 이야기를 하자면, InnoDB. 파티셔닝에서.. 실행계획을 세우는 단계에서 모든 파티션에 약 1~3개 정도의 페이지를 읽게되는데.. 이런 동작으로 인하여 엄청난 비효율이 발생하고 마는 것이죠. 모든 파티션에 해당하는 블록들이 InnoDB 버퍼풀에 올라와있지 않기 때문이죠. (실제로 메모리에 존재할지라도, 쿼리당 수십개의 페이지에 접근한다는 것 역시도 부하라고 생각합니다.)

실행계획 단계의 요 부분에서, 불필요하게 파티셔닝을 접근을 시도해서, 이런 비효율을 발생시키는데…

for (part_id = m_part_info->get_next_used_partition(part_id);
     part_id < m_tot_parts;          part_id = m_part_info->get_next_used_partition(part_id)) {
  index = m_part_share->get_index(part_id, keynr);
  .. skip ..
  int64_t n = btr_estimate_n_rows_in_range(index, range_start, mode1,
                                           range_end, mode2);
  n_rows += n;
  DBUG_PRINT("info", ("part_id %u rows %ld (%ld)", part_id, (long int)n,
                      (long int)n_rows));
}

출처: https://github.com/mysql/mysql-server/blob/4869291f7ee258e136ef03f5a50135fe7329ffb9/storage/innobase/handler/ha_innopart.cc#L3281

사실, 이렇게 대략적인 데이터 건 수를 판단한 후 일부 파티셔닝에 접근할 필요없이, 모든 파티셔닝에 데이터가 존재한다는 가정으로 데이터에 접근을 해도 큰 무리없다고 생각이 마구마구 드는 시점이네요.

Solution

사실 당장의 솔루션은 없습니다. 그냥, 파티셔닝 수를 최소화하는 수밖에 없답니다. 미네랄..ㅜㅜ

단, Percona MySQL경우에는 하단과 같이 추가 파라메터(force_index_records_in_range, records_in_range)를 추가하여, 해당 테이블에 데이터가 특정 건수(할당한 파라메터값)으로 지정을 함으로써 전체 파티셔닝에 접근하지 않도록 우회할 수 있는데요. (이렇게 유도하기까지 꽤나 어려웠어요.ㅠㅠ)

깃헙: https://github.com/dutow/percona-server/commit/677aa7c8aa64ca0aca25c07813e48309e5f06109

이렇게 소스를 변경해서, 테스트트를 해본 결과 놀라운 성능 변화가 있었습니다.

이런 말도 안되는 성능을 보였던 녀석이!!

set records_in_range = 0;
set force_index_records_in_range = 0;
=============================
Device: r/s rkB/s %util
disk01 28364.00 453824.00 100.00
disk01 29050.00 464800.00 99.80
disk01 31827.00 509232.00 99.90
disk01 29660.00 474560.00 99.40
disk01 31570.00 505120.00 99.90

time opr ms/opr
11:31:18 4553 11.01
11:31:19 4639 10.76
11:31:20 4581 10.86

단 몇줄 수정만으로도, 이런 성능 수치를 보였답니다. 대충 봐도 4배정도 향상이 이루어졌습니다, ㅎㅎ (4500 qps vs 17000 qps)

set records_in_range = 1000;
set force_index_records_in_range = 1000;
=============================
Device: r/s rkB/s %util
disk01 24930.00 398896.00 96.70
disk01 25671.00 410720.00 97.10
disk01 24245.00 387920.00 96.90
disk01 24938.00 399008.00 97.10
disk01 25155.00 402480.00 97.10

time opr ms/opr
11:36:48 17156 2.89
11:36:49 16873 2.94
11:36:50 16965 2.92

이 픽스는 다음번 GA 릴리즈 버전에 포함되어서 나오게 될 예정이고.. 동시에 Oracle 커뮤니티 버그 사이트에서 진행하던 동일 이슈도 나름 긍정적인 형태로 진행 중에 있는 듯 하군요.

관련 : https://bugs.mysql.com/bug.php?id=96233

Conclusion

엄청난 갯수의 파티셔닝을 가진 상황에서, 이상하게 쿼리가 느린 것을 경험하신 분들! 파티셔닝 테이블 조회 시 이와 같은 이슈로 리소스가 충분히 활용되고 있지 않은지를 한번쯤 확인을 해보세요. ㅎㅎ 어서어서 오라클에서도 관련된 픽스가 이루어져서, 이런 비효율이 없어지기를 빌어봅니다. ㅋ

간만에 끄적인 블로그를 너무 서술한듯한 느낌이네요. (오늘 ifkakao 발표 후인지라.. 피곤..ㅠㅜ) 이 다음부터는 더욱 재미난 내용을 붙이도록 하겠습니다. 꾸벅.

PMM팁1탄! MySQL을 READ-ONLY 기준으로 표기해보기.

Overview

어느덧 1월이 마무리되어가는 이 시점.. 한달 내내 놀다 시간 보내기에는 아쉬움이 많이 남아, 블로그 한두개 정도는 남겨보고자, 아주 간만에 노트북 앞에 앉습니다. 가장 기억 속에 맴도는 주제를 찾던 중, 작년 나름 많은 분석을 했었던 내용들을 한번 몇가지 주제로 정리해보고자 합니다.

PMM(Percona Monitoring and Management)이라는 녀석으로 퉁 쳐서 이야기를 했지만, 사실 이번에 이야기할 내용은 Prometheus 쿼리와 Grafana를 사용하는 간단한 꼼수(?)에 대한 이야기입니다.

혹시 PMM이 어떤 녀석인지 궁금하시다면? PMM 이야기 1편 – INTRO 편을 읽어보주세요. ㅎㅎ

Definition

이 주제는 MySQL의 READ-ONLY 설정 값에 따른 서버들 표기로 되어 있지만, 제 입장에서는 이는 곧 마스터/슬레이브를 구분하는 주제이기도 합니다. 물론, MySQL을 다양한 토폴로지로 이를테면, (M)-(M/S)-(S)와 같은 체이닝 구조로도 서비스를 운영해볼 수 있겠지만.. 개인적인 생각으로는 다양한 장애를 대비했을 때 가장 간단한 구성이 최고의 강력함을 선사한다고 생각을 하기에.. 제 입장에서 READ-ONLY가 ON으로 활성화된 녀석을 슬레이브로라고 정의합니다.

  1. 마스터 (READ_ONLY: OFF)
    – 데이터 변경의 주체가 되는 인스턴스
    (M)-(M/S)-(S) 와 같은 중간 체이닝 구조에서 (M/S)는 마스터로 인지
  2. 슬레이브 (READ_ONLY: OFF)
    데이터 변경이 불가한 인스턴스

개인의 정의에 따라 다르게 쿼리가 나오겠지만, 제가 원하는 그림은, “READ_ONLY가  ON/OFF에 따른 서버 분류” 혹은 “전체 인스턴스 기준”에 따른 표현입니다.

Grafana Variable

MySQL에서 READ-ONLY는 ON/OFF 정도만을 의미합니다만, 여기서는 전체 서버를 의미하는 값으로 2값을 추가로 의미부여해보았습니다.

  • var_read_only : 0 (READ-ONLY  OFF)
  • var_read_only : 1 (READ-ONLY  ON)
  • var_read_only : 2 (ALL)

Grafana에서 새로운 Variable을 추가하는 방법은 대시보드의 Setting의 Variables 탭에서 새롭게 추가하는 것으로~ (설명은 패스) 그리고, 아래와 같이 Custom 타입으로 2,0,1로 정의해봅니다. 2가 제일 앞인 이유는.. 그냥 기본 값으로 지정을 해보기 위함이죠.

Grafana Variable - READ-ONLY
Grafana Variable – READ-ONLY

그런데 문제는, 이렇게 만든 Variable 셀렉트 박스를 보면, 표기가 2,0,1로 보여지고 있어서 직관적이지가 않습니다. 2->ALL, 0->OFF, 1->ON으로 셀렉트 박스에 표현되는 것이 사용상으로 괜찮겠죠? (물론 사용에는 무리가 없지만..)

Grafana의 UI 세팅에서는 불가(아직까지는)한 듯 하고.. 이는 Grafana를 Export한 후 약간 조작 후 다시 Import하는 과정이 필요합니니다. “Share Dashboard” > “Export” 탭으로 들어가서 json 파일로 내린 후 열어보면, 아래와 같이 “text” 부분도 역시 value와 같은 값으로 설정되어있는데,

{
  "allValue": null,
  "current": {
    "text": "2",
    "value": "2"
  },
  "hide": 0,
  "includeAll": false,
  "label": "Read-only",
  "multi": false,
  "name": "read_only",
  "options": [
    {
      "selected": true,
      "text": "2",
      "value": "2"
    },
    {
      "selected": false,
      "text": "0",
      "value": "0"
    },
    {
      "selected": false,
      "text": "1",
      "value": "1"
    }
  ],
  "query": "2,0,1",
  "type": "custom"
}

위 text 붉은 부분을 아래와 같이 직관적인 문구로 바꾸어주고, 해당 Dashboard를 다시 Import해주면~

{
  "allValue": null,
  "current": {
    "text": "ALL",
    "value": "2"
  },
  "hide": 0,
  "includeAll": false,
  "label": "Read-only",
  "multi": false,
  "name": "read_only",
  "options": [
    {
      "selected": true,
      "text": "ALL",
      "value": "2"
    },
    {
      "selected": false,
      "text": "OFF",
      "value": "0"
    },
    {
      "selected": false,
      "text": "ON",
      "value": "1"
    }
  ],
  "query": "2,0,1",
  "type": "custom"
}

최종적으로 Grafana 화면에서 직관적인 문구로 확인이 가능합니다. 쉽게 조작이 어렵다는 점이.. 참으로 서글프네요. 아무튼 첫번째 검색어 세팅 완료!

Grafana Read-Only Variable Modify
Grafana Read-Only Variable Modify

Promethues Query

Grafana 에서 원하는 조건대로 전달할 수 있는 기반이 만들었으니, 이번에는 데이터를 조회하는 Prometheus 쿼리를 작성해보도록 하겠습니다. 이 자리에서, Prometheus 쿼리를 하나하나 설명하지는 않을 것으므로, Prometheus 공식 사이트 메뉴얼을 한번씩은 꼭 방문하셔서, 쿼리 구문을 읽어봐 주시와요~ (여기여기요~)

제 관심사는, READ-ONLY ON/OFF에 따른 그룹핑된 결과물입니다. 앞선 정의에서, read_only 값이 0이면 마스터, read_only가 1이면 슬레이브, 그리고 2이면 전체 서버를 의미하는 것으로 이야기를 했었죠. Grafana에서 전달해주는 이 데이터를 기반으로 원하는 데이터들만 선별해서 가져와보도록 하겠습니다.

우선 아래와 같이 max_over_time으로 1분간 최대값을 가져오는 Prometheus 쿼리로 현재 시스템 로드를 추출해봅니다. 이는, 전체 인스턴스에 해당하는 결과값들입니다.

max_over_time(node_load1[1m])

READ-ONLY 상태 값에 대한 조건을 추가하여, READ-ONLY ON/OFF에 해당하는 결과를 조회해봅니다. 여기서 “on (instance)” 항목은 어떤 데이터를 기준으로 데이터 연산을 처리할 것인가를 의미하는데, SQL 기준이라면, 아무래도 JOIN 조건이라고 보면 무관하겠네요.

max_over_time(node_load1[1m]) 
  and on (instance) (mysql_global_variables_read_only == $read_only)

그런데 여기서 문제가 하나 생겼습니다. READ-ONLY 값이 존재하는 경우에는 큰 문제없이 쿼리 질의가 되지만, 마스터/슬레이브를 섞어서 한번에 봐야하는 케이스, 즉 전체 서버를 모두 표현하는 경우 정상적으로 쿼리가 동작하지 않는다는 것이죠.

그래서, 여기서 극강의 꼼수 하나를 더 슬그머니 밀어봅니다. ㅋ 세 값의 연산 결과가 2일때만 1 이상이 되는 공식을 만들기만 하면 해결이 될 듯 하죠? 그리고 “OR” 조건을 묶어서, 우선 연산 처리를 하고, 부합되지 않는 경우 READ-ONLY 부분이 동작하도록 쿼리 작성을 해보도록 한다면..? 각 인스턴스 단위로 “무조건 존재”하는 메트릭을 손 꼽아 보자면..??

“up{job=’linux’}” 값을 이용해볼 수 있겠습니다. 이 값의 결과는 0 또는 1이며, Exporter가 존재하는 한 무조건 존재한다고 가정해볼 수 있습니다. 즉, $read_only가 0과 1인 경우 음수로 나오며, 2 이상인 경우에만 1이상의 양수 값이 나오도록 아래와 같이 쿼리를 작성해볼 수 있겠습니다.

max_over_time(node_load1[1m]) 
  and on (instance) (up{job='linux'} <= $read_only^2-3 or mysql_global_variables_read_only == $read_only)

이렇게되면, 우선 2인 경우에는 첫번째 up 조건에서 필터링 되면서 뒤의 mysql_global_variables_read_only 부분을 참조하지 않습니다. (or는 앞의 결과가 없는 경우 수행) 그러나 만약 read_only 필터링 조건이 들어오면서 0 또는 1로 연산이 되면, up 결과는 존재하지 않으므로, mysql_global_variables_read_only 값을 체크하면서 최종적으로 원하는 형태로 데이터를 추출해 내는 것이죠.

설명이.. 쿨럭.. 어렵.. (한국말좀 알려주세요. ㅠㅠ) 그래서 준비했습니다. 어떤 결과로 보여지는지..

테스트로 로컬에 VM을 구성해서 아래와 같은 구조로 MySQL 서버를 세팅한 후에 PMM exporter를 띄워보았습니다. node01과 node03은 당연히 데이터 변경의 주체 역할을 가지므로, read_only를 OFF로 해놓았고요.

node01 : Master
  ㄴ node02 : Slave
node03:  Master(Single)

Case1. ALL Instance

pmm-server 인스턴스를 포함해서 모든 서버들이 모두 보여집니다.

ALL Server
ALL Server

Case2. READ-ONLY: OFF

의도한대로, READ-ONLY가 OFF인 인스턴스만 모여서 같이 그래프를 보여주죠. 참고로, READ-ONLY 속성을 가지고 있는 mysql  인스턴스 데이터들만 표기해주므로, pmm-server 인스턴스는 대상 리스트에서 빠집니다. (당연한 이야기지요. READ-ONLY 속성으로 조회를 했으니..)

READ-ONLY OFF
READ-ONLY OFF

Case3. READ-ONLY: ON

마지막으로.. READ-ONLY가  ON인  슬레이브 녀석만 별도로 보여줍니다. 여기서도 역시 pmm-server는 빠집니다.

READ-ONLY ON
READ-ONLY ON

Conclusion

사실은 별 것 없습니다. 제게 필요한 정보를 추출해보기 위해서.. 그냥 사소한 고민을 해보았었고, 약간의 꼼수로 필터링을 할 수 있도록 쿼리를 작성한 것 뿐이지요.

제가 한 일이라고는..

  1. Grafana에서 READ-ONLY 표기를 직관적으로 표기하기 위한 대시보드 Import 꼼수
  2. 2가지 단계(전체, READ-ONLY ON/OFF)로 필터링할 수 있도록 Prometheus 꼼수 쿼리 작성

이 긴 글에서 위 두 가지 정도로 보여지네요. 허허..;; 그렇기 때문에, 초고수 분들에게는 우스운 포스팅일 수도 있습니다. 그렇지만.. 수십대가 넘어가는 마스터/슬레이브가 섞인 환경에서.. 한눈에 시스템의 리소스 현황을 파악하는 경우.. 롤에 따른 분류가 생각보다 크나큰 의미가 있습니다. ^^

제 문제 상황에서 해결방안을 고민한 아이디어이며, 더욱 좋은 번뜩이는 아이디어가 있으면 같이 나눴으면 하는 생각으로.. 장기 휴가 후 복귀를 얼마 남겨둔 입장에서 포스팅 하나 날려봅니다.

심심해서, 간단하게 만들어본 대시보드를 아래 GitHub에 올려놓았으니, Grafana에 Import를 해서 직접 보시면 이해가 빠를 듯 하네요.
>> https://github.com/gywndi/kkb/blob/master/pmm-dashboard/My_Dashboard.json

PS. 이 예제는 이해를 위해, 가장 기초적인 요건만 고려해서 만든 것입니다. MySQL이 죽어있거나, mysql_up이 OFF인 경우, 즉 read_only 파라메터가 정상 수집이 되지 않는 경우는 풀 수 있는 난제로 남겨두고 도망갑니다. ㅋ

좋은 밤 되세요. ^^