MySQL Group Replication 素振り

公式のドキュメントを読みながら素振りしました。

グループレプリケーションでは、グループのメンバシップ管理、ノードの障害検出、追加ノードの同期、などが自動で行われます。一方でアプリケーションからの接続先をルーティングするような機能はないため、アプリケーションがクラスタのどのノードに接続するかの制御には別のなにかが必要です(MySQL Router とか HAProxy とか)。

Configuration

グループレプリケーションのための my.cnf の抜粋です。

# GTID レプリケーションに関する設定
# もちろん server_id はノードごとに固有の値が必要
server_id=1
gtid_mode=ON
enforce_gtid_consistency=ON
binlog_checksum=NONE

# 8.0.3 以前なら必要なオプション(それ以降ならデフォルト)
log_bin=binlog
log_slave_updates=ON
binlog_format=ROW
master_info_repository=TABLE
relay_log_info_repository=TABLE

# ここからグループレプリケーションに関する設定

# グループレプリケーションするなら XXHASH64 でなければならない
# 8.0.2 以降ならデフォルト
transaction_write_set_extraction=XXHASH64

# グループレプリケーションのプラグインをロード
plugin_load=group_replication.so

# 適当な方法で生成した UUID を指定する
# グループの名前になって GTID もこの UUIDから作られる
group_replication_group_name="470b4daf-6c34-415f-97ad-68cf76ef24e7"

# ノードの起動時にグループレプリケーションを自動的に開始しない
group_replication_start_on_boot=off

# グループレプリケーションのための自ノードのアドレスとポート番号
group_replication_local_address= "192.168.88.11:6606"

# グループのメンバーの一覧
group_replication_group_seeds= "192.168.88.11:6606,192.168.88.12:6606,192.168.88.13:6606"

# グループレプリケーションの開始時に新たなグループを作らない
group_replication_bootstrap_group=off

# MySQL 8.0 のユーザーはデフォルトで caching_sha2_password 認証なので準備が面倒
# だけどこれを設定しておけば面倒がなくなる、ただし中間者攻撃に脆弱
group_replication_recovery_get_public_key=ON

plugin_load=group_replication.so を指定しているのでサーバ起動時にグループレプリケーションのプラグインがロードされます。

後から INSTALL PLUGIN する場合は group_replication_* パラメータは loose-group_replication_* のように loose- をつけておかないと存在しないパラメータのエラーで起動がコケます。

MySQL 8.0 で作成されたユーザーはデフォルトで caching_sha2_password という認証方法を使うため、グループレプリケーションではいろいろ準備が必要なようなのですが、group_replication_recovery_get_public_key=ON を指定すればその準備が省略できます。ただし中間者攻撃に対して脆弱になります。

default_authentication_plugin=mysql_native_password とかでデフォルトの認証方法を変えておくか、ユーザーを作成するときに mysql_native_password を明示しておいても良いのかもしれません、通信経路が安全であることが間違いないなら。

docker-compose

docker-compose.yml

version: '3.4'

services:
  sv01:
    image: mysql:8
    container_name: sv01
    hostname: sv01
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      MYSQL_DATABASE: test
      MYSQL_PS1: "sv01> "
    networks:
      mysql:
        ipv4_address: 192.168.88.11
    command:
      - --server_id=1
      - --gtid_mode=ON
      - --enforce_gtid_consistency=ON
      - --binlog_checksum=NONE
      - --relay_log=relay-bin
      - --transaction_write_set_extraction=XXHASH64
      - --plugin-load=group_replication.so
      - --group_replication_group_name=470b4daf-6c34-415f-97ad-68cf76ef24e7
      - --group_replication_start_on_boot=off
      - --group_replication_local_address=192.168.88.11:6606
      - --group_replication_group_seeds=192.168.88.11:6606,192.168.88.12:6606,192.168.88.13:6606
      - --group_replication_bootstrap_group=off
      - --group_replication_recovery_get_public_key=ON

  sv02:
    image: mysql:8
    container_name: sv02
    hostname: sv02
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      MYSQL_DATABASE: test
      MYSQL_PS1: "sv02> "
    networks:
      mysql:
        ipv4_address: 192.168.88.12
    command:
      - --server_id=2
      - --gtid_mode=ON
      - --enforce_gtid_consistency=ON
      - --binlog_checksum=NONE
      - --relay_log=relay-bin
      - --transaction_write_set_extraction=XXHASH64
      - --plugin-load=group_replication.so
      - --group_replication_group_name=470b4daf-6c34-415f-97ad-68cf76ef24e7
      - --group_replication_start_on_boot=off
      - --group_replication_local_address=192.168.88.12:6606
      - --group_replication_group_seeds=192.168.88.11:6606,192.168.88.12:6606,192.168.88.13:6606
      - --group_replication_bootstrap_group=off
      - --group_replication_recovery_get_public_key=ON

  sv03:
    image: mysql:8
    container_name: sv03
    hostname: sv03
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      MYSQL_DATABASE: test
      MYSQL_PS1: "sv03> "
    networks:
      mysql:
        ipv4_address: 192.168.88.13
    command:
      - --server_id=3
      - --gtid_mode=ON
      - --enforce_gtid_consistency=ON
      - --binlog_checksum=NONE
      - --relay_log=relay-bin
      - --transaction_write_set_extraction=XXHASH64
      - --plugin-load=group_replication.so
      - --group_replication_group_name=470b4daf-6c34-415f-97ad-68cf76ef24e7
      - --group_replication_start_on_boot=off
      - --group_replication_local_address=192.168.88.13:6606
      - --group_replication_group_seeds=192.168.88.11:6606,192.168.88.12:6606,192.168.88.13:6606
      - --group_replication_bootstrap_group=off
      - --group_replication_recovery_get_public_key=ON

  sv04:
    image: mysql:8
    container_name: sv04
    hostname: sv04
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      MYSQL_DATABASE: test
      MYSQL_PS1: "sv04> "
    networks:
      mysql:
        ipv4_address: 192.168.88.14
    command:
      - --server_id=4
      - --gtid_mode=ON
      - --enforce_gtid_consistency=ON
      - --binlog_checksum=NONE
      - --relay_log=relay-bin
      - --transaction_write_set_extraction=XXHASH64
      - --plugin-load=group_replication.so
      - --group_replication_group_name=470b4daf-6c34-415f-97ad-68cf76ef24e7
      - --group_replication_start_on_boot=off
      - --group_replication_local_address=192.168.88.14:6606
      - --group_replication_group_seeds=192.168.88.11:6606,192.168.88.12:6606,192.168.88.13:6606
      - --group_replication_bootstrap_group=off
      - --group_replication_recovery_get_public_key=ON

networks:
  mysql:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.88.0/24

docker-compose で開始してコンテナに入ります。

docker-compose up -d sv01 sv02 sv03

docker-compose exec sv01 mysql
docker-compose exec sv02 mysql
docker-compose exec sv03 mysql

ユーザーの作成とバイナリログのリセット

プラグインがロードされていることを確認します。

/* [sv01/sv02/sv03] */
show plugins;

レプリケーションユーザーを作ります。

/* [sv01/sv02/sv03] */
CREATE USER rpl_user@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO rpl_user@'%';

レプリケーションチャンネル group_replication_recovery に↑で作成したユーザーの資格情報を使うように設定します。

/* [sv01/sv02/sv03] */
CHANGE MASTER TO MASTER_USER='rpl_user', MASTER_PASSWORD='password' FOR CHANNEL 'group_replication_recovery';

docker-compose up でデータベースが初期化された時点でバイナリログにいろいろ書き込まれているので、そのままだとレプリケーションを開始したときに競合するので、バイナリログをリセットしておきます。

/* [sv01/sv02/sv03] */
RESET MASTER;

なお、レプリケーションユーザーの作成などの、すべてのノードで個別に実行するためレプリケーションされたくないクエリは set sql_log_bin = 0|1 とかでバイナリログの ON/OFF を制御しつつクエリを実行するのが普通のようです。ただ、初回のセットアップでは「すべてのノードでいろいろ準備→RESET MASTER→レプリケーション開始」の方がわかりみがあるきがするので、いつもそうしてます。

グループレプリケーションの開始

グループを作成してグループレプリケーションを開始します。この作業は最初の1台だけで行います。

/* [sv01] */
SET GLOBAL group_replication_bootstrap_group=ON;
START GROUP_REPLICATION;
SET GLOBAL group_replication_bootstrap_group=OFF;

グループのメンバーを確認します。この時点では sv01 の1台しかありません。

/* [sv01] */
SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE FROM performance_schema.replication_group_members;
+-------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-------------+-------------+--------------+-------------+
| sv01        |        3306 | ONLINE       | PRIMARY     |
+-------------+-------------+--------------+-------------+

残りのノードでも開始します。このとき group_replication_bootstrap_group=ON を指定しません。指定すると同じ名前の別のグループが作成されてしまいます。

/* [sv02/sv03] */
START GROUP_REPLICATION;

グループのメンバーを確認します。うまくグループに参加できれいればどのノードで実行しても同じ結果が返ります。最初のノードである sv01 がプライマリになっています。

/* [sv01/sv02/sv03] */
SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE FROM performance_schema.replication_group_members;
+-------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-------------+-------------+--------------+-------------+
| sv01        |        3306 | ONLINE       | PRIMARY     |
| sv03        |        3306 | ONLINE       | SECONDARY   |
| sv02        |        3306 | ONLINE       | SECONDARY   |
+-------------+-------------+--------------+-------------+

sv01 で適当にデータを入れてみます。

/* [sv01] */
USE test;
CREATE TABLE t1 (c1 INT PRIMARY KEY, c2 TEXT NOT NULL);
INSERT INTO t1 VALUES (1, 'Luis');

他のノードにレプリケーションされています。

/* [sv02/sv03] */
USE test;
select * from t1;

プライマリ以外のノードは読み込み専用になってるので更新のトランザクションはエラーになります。

/* [sv02/sv03] */
INSERT INTO t1 VALUES (2, 'xx');
/* ERROR 1290 (HY000): The MySQL server is running with the --super-read-only option so it cannot execute this statement */

グループにインスタンスを追加

新しくノードを追加します。

docker-compose up -d sv04
docker-compose logs -f sv04
docker-compose exec sv04 mysql

プラグインがロードされていることを確認します。

/* [sv04] */
show plugins;

レプリケーションユーザーを作って、バイナリログをリセットして、グループレプリケーションを開始します。

/* [sv04] */

/* ユーザー作成 */
CREATE USER rpl_user@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO rpl_user@'%';

/* レプリケーションチャンネルに資格情報を設定 */
CHANGE MASTER TO MASTER_USER='rpl_user', MASTER_PASSWORD='password' FOR CHANNEL 'group_replication_recovery';

/* バイナリログをリセット */
RESET MASTER;

/* グループを開始 */
START GROUP_REPLICATION;

新しいノードが追加されるとランダムに選ばれたグループのメンバー(ドナー)からデータの同期が行われ(リカバリプロセス)、同期が完了するとオンラインのメンバーとして使用できるようになります。

/* [sv04] */

/* メンバーの一覧表示 */
SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE FROM performance_schema.replication_group_members;

/* データの確認 */
USE test;
select * from t1;

運用中のグループにインスタンスを追加

リカバリプロセスは MySQL の通常のレプリケーションを利用して行われます。なので必要なバイナリログがグループのすべてのメンバーで既にパージされているとリカバリプロセスは失敗します。

グループのすべてのメンバからバイナリログをパージします。

/* [sv01/sv02/sv03] */
flush logs;
purge binary logs before now();
show binlog events;

新しいインスタンスを開始して、

docker-compose rm -fsv sv04
docker-compose up -d sv04
docker-compose logs -f sv04
docker-compose exec sv04 mysql
/* [sv04] */
CREATE USER rpl_user@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO rpl_user@'%';
CHANGE MASTER TO MASTER_USER='rpl_user', MASTER_PASSWORD='password' FOR CHANNEL 'group_replication_recovery';
RESET MASTER;
START GROUP_REPLICATION;

ログに次のようなものがたくさん出力されます。

[ERROR] [MY-010557] [Repl] Error reading packet from server for channel 'group_replication_recovery': Cannot replicate because the master purged required binary logs. Replicate the missing transactions from elsewhere, or provision a new slave from backup. Consider increasing the master's binary log expiration period. To find the missing transactions, see the master's error log or the manual for GTID_SUBTRACT. (server_errno=1236)
[ERROR] [MY-013114] [Repl] Slave I/O for channel 'group_replication_recovery': Got fatal error 1236 from master when reading data from binary log: 'Cannot replicate because the master purged required binary logs. Replicate the missing transactions from elsewhere, or provision a new slave from backup. Consider increasing the master's binary log expiration period. To find the missing transactions, see the master's error log or the manual for GTID_SUBTRACT.', Error_code: MY-013114

メンバーのリストを見てみると、追加はされていますが MEMBER_STATERECOVERING のままです。

/* [sv04] */
SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE FROM performance_schema.replication_group_members;
+-------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-------------+-------------+--------------+-------------+
| sv04        |        3306 | RECOVERING   | SECONDARY   |
| sv01        |        3306 | ONLINE       | PRIMARY     |
| sv03        |        3306 | ONLINE       | SECONDARY   |
| sv02        |        3306 | ONLINE       | SECONDARY   |
+-------------+-------------+--------------+-------------+

しばらくと MEMBER_STATEERROR になります。

/* [sv04] */
SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE FROM performance_schema.replication_group_members;
+-------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-------------+-------------+--------------+-------------+
| sv04        |        3306 | ERROR        |             |
+-------------+-------------+--------------+-------------+

他のメンバーからは見えなくなります。

/* [sv01] */
SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE FROM performance_schema.replication_group_members;
+-------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-------------+-------------+--------------+-------------+
| sv01        |        3306 | ONLINE       | PRIMARY     |
| sv03        |        3306 | ONLINE       | SECONDARY   |
| sv02        |        3306 | ONLINE       | SECONDARY   |
+-------------+-------------+--------------+-------------+

グループレプリケーションを開始した直後の、すべてのバイナリログがまだ残っている状態でインスタンスを追加するようなときは別として、運用中のグループにノードを追加するときは追加前に mysqldump 的なことが必要です。

公式のドキュメントだと MySQL Enterprise Backup を使用する例が記載されていましたが、mysqldump でも良いと思うし、データディレクトリの rsync(auto.cnf に注意)でも大丈夫だと思います。

試しに mysqldump してみます。

# 新しいインスタンスを開始
docker-compose rm -fsv sv04
docker-compose up -d sv04
docker-compose logs -f sv04

# レプリケーションユーザーの作成やバイナリログのリセット
docker-compose exec -T sv04 mysql <<'SQL'
  CREATE USER rpl_user@'%' IDENTIFIED BY 'password';
  GRANT REPLICATION SLAVE ON *.* TO rpl_user@'%';
  CHANGE MASTER TO MASTER_USER='rpl_user', MASTER_PASSWORD='password' FOR CHANNEL 'group_replication_recovery';
  RESET MASTER;
SQL

# 別のノードからダンプして新しいノードにインポート
docker-compose exec -T sv03 mysqldump \
    --all-databases --single-transaction --triggers --routines --events |
  docker-compose exec -T sv04 mysql

# コンテナに入る
docker-compose exec sv04 mysql

show master statusExecuted_Gtid_Set が他のノードと同じになっていれば大丈夫です。

/* [sv04] */
show master status;
+---------------+----------+--------------+------------------+------------------------------------------+
| File          | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                        |
+---------------+----------+--------------+------------------+------------------------------------------+
| binlog.000001 |      151 |              |                  | 470b4daf-6c34-415f-97ad-68cf76ef24e7:1-6 |
+---------------+----------+--------------+------------------+------------------------------------------+

グループレプリケーションを開始できます。

/* [sv04] */
START GROUP_REPLICATION;
SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE FROM performance_schema.replication_group_members;
+-------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-------------+-------------+--------------+-------------+
| sv04        |        3306 | ONLINE       | SECONDARY   |
| sv01        |        3306 | ONLINE       | PRIMARY     |
| sv03        |        3306 | ONLINE       | SECONDARY   |
| sv02        |        3306 | ONLINE       | SECONDARY   |
+-------------+-------------+--------------+-------------+

IPアドレスのホワイトリスト

MySQL の設定で group_replication_group_seeds にグループのインスタンスのアドレスを羅列していますが、別にグループのすべてのインスタンスが羅列されている必要はありません。

この設定は、インスタンスでグループレプリケーションを開始したときに、どこからグループの情報を取得するかの設定です。なのでグループ内の生きているメンバーのいずれかが指定されていれば大丈夫です。なので、グループへのインスタンスの追加は group_replication_group_seeds をいじらなくてもできます。

一方、グループに参加できるアドレスのホワイトリストの設定もあって(group_replication_ip_whitelist)、そこに含まれないアドレスのインスタンスはグループのインスタンスとの通信が拒否されます。ただし、デフォルトでサーバの I/F のサブネットが追加されるので、複数のサブネットにインスタンスが分散されるとかではない限りはデフォルトのままで良さそうです。

ノードの故障

おもむろにプライマリノードを強制終了します。

docker-compose kill -s KILL sv01

他のノード(sv02 とか)で次のようにログが表示されます。

[Warning] [MY-011493] [Repl] Plugin group_replication reported: 'Member with address sv01:3306 has become unreachable.'
[Warning] [MY-011499] [Repl] Plugin group_replication reported: 'Members removed from the group: sv01:3306'

メンバの一覧を観てみると、sv01 がなくなって sv04 がプライマリになってました。

/* [sv02] */
SELECT * FROM performance_schema.replication_group_members;
/*------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-------------+-------------+--------------+-------------+
| sv04        |        3306 | ONLINE       | PRIMARY     |
| sv03        |        3306 | ONLINE       | SECONDARY   |
| sv02        |        3306 | ONLINE       | SECONDARY   |
+-------------+-------------+--------------+------------*/

Monitoring Group Replication

グループのメンバやレプリケーションの詳細は下記のようにパフォーマンススキーマから得られます。

/* グループのメンバに関する情報 */
select * from performance_schema.replication_group_member_stats \G
select * from performance_schema.replication_group_members;

/* レプリケーションチャンネルに関する情報 */
select * from performance_schema.replication_connection_status \G
select * from performance_schema.replication_applier_status;

レプリケーションチャンネルには次の用途の2つが作成されています。

  • group_replication_recovery
    • 分散リカバリフェーズ(リカバリプロセス)で使用される
    • ノードを追加したときや故障ノードが復帰したときの同期用
  • group_replication_applier
    • グループで実行されたトランザクションを適用するために使用される
    • 要するに平時のトランザクションのレプリケーション

グループのメンバに関するパフォーマンステーブルのビューについて。

  • replication_group_member_stats
    • メンバーごとの待機中のトランザクション数とか処理済トランザクション数とかの統計
    • 特定のメンバーが遅れているとかでキューに溜まってるのを監視したりするのに使える
  • replication_group_members
    • メンバーのステータス

replication_group_members で表示されるメンバーのステータス。

  • ONLINE
    • 同期されている
  • RECOVERING
    • リカバリプロセス中
  • OFFLINE
    • メンバーはグループに属していない
    • プラグインがロード済でグループレプリケーションか開始していないとき
  • ERROR
    • リカバリプロセスやトランザクションの適用中にエラー
  • UNREACHABLE
    • メンバーがネットワーク的に到達不能

サービス監視とかするなら ONLINE 以外はなにかしら異常とみなして良さそうです。

Deploying in Multi-Primary or Single-Primary Mode

グループレプリケーションにはシングルプライマリモードとマルチプライマリモードがあります。デフォルトはシングルプライマリです。

グループのメンバーの中でシングルとマルチは混在できません。切り替えるためにはグループレプリケーション自体を再構成する必要があります。

マルチプライマリモードにすると、次のようなステートメントのチェックが行われるようになります。

  • トランザクションが SERIALIZABLE 分離レベルで実行されるとコミットが失敗する
  • CASCADE の外部キーを持つテーブルに対してトランザクションを実行するとコミットに失敗する

これらのチェックは group_replication_enforce_update_everywhere_checksFALSE にすると無効にできます。もちろん無効にするとノード間でデータ不整合が起こる可能性が生じると思います。

Single-Primary Mode

デフォルトのシングルプライマリモードでは、グループのただ一つのメンバーだけがプライマリで、それ以外は super-read-only=ON が設定されて読み取り専用になります。

プライマリがコケたときに新しいプライマリの選出は group_replication_member_weight で重みつけできます。ただしグループ内で MySQL のバージョンに差異があると下位メジャーバージョンのものから優先的に選択されます。

プライマリがコケた後、クライアントアプリケーションを再ルーティングする前に、新しいプライマリがレプリケーションに関係するリレーログを適応しきるのを待ったほうが良いです。これは普通のレプリケーションでも同じで、リレーログが適応し切る前にクライアントを新しいプライマリにルーティングすると、クライアントからの更新とリレーログによる更新が競合する可能性があります。

プライマリとなっているメンバーの検索は performance_schema.replication_group_members テーブルの MEMBER_ROLE を見ればわかります。

Tuning Recovery

新規ノードの追加や故障ノードが復帰するとき、グループのメンバからランダムで1つが選ばれて、そのノードからデータを取得する。この処理はリカバリプロセスと呼ばれて、選ばれたノードはドナーと呼ばれる。

ドナーへの接続が失敗したときは自動的に別のドナーが選択されて接続が再試行される。接続のリトライの限界に達するとリカバリプロセスはエラーで終了する。

リカバリプロセスでは単なる接続エラーだけではなく、いろいろなエラーを検出して別のドナーで再試行が行われる。

  • パージされたデータ
    • ドナーでリカバリに必要なデータが既にパージされている
  • 重複データ
    • 新しいノードが既に持っているデータと、ドナから同期されるデータとが競合したとき
    • 他のドナーには切り替えずにエラーにすることも考えられるけど、不均質なグループではあるメンバーは競合するトランザクションを持っていて、あるメンバーは競合するトランザクションを持っていない可能性があるため、このエラーが発生したときも他のドナーで再試行されます
  • その他のエラー
    • リカバリスレッドがコケたとき・・えーとまあなんかエラー?です

リカバリプロセスは普通のMySQLレプリケーションの実装に依存しているため、ここで説明した再試行とは別に、普通のMySQLレプリケーションとしての再試行も行われることがある。

リカバリの再試行回数は group_replication_recovery_retry_count パラメータで設定できる。

group_replication_recovery_reconnect_interval で再試行のインターバルを設定できる。再試行の都度、毎回インターバルが挟まれるわけではなく、すべてのドナーで一通り失敗したときだけインターバルが挟まれて次のドナー(一度失敗している)で再試行される。

Network Partitioning

クオラムのためにグループの過半数が生きている必要がある。ただしサーバがグループから自発的に去ったときはグループのメンバに去ることが伝えられるので、その時点でグループが再構成されるのでクオラム数も再計算される。

クオラム低下による停止からの復帰の方法。まず、生きてるノードでアドレスを確認して、

/* 生きてるすべてのノードで確認 */
SELECT @@group_replication_local_address;

グループのメンバシップを強制的に変更する。

SET GLOBAL group_replication_force_members="192.168.88.11:6606,192.168.88.12:6606";

Group Replication Requirements

グループレプリケーションに使用するサーバの要件。

  • InnoDB ストレージエンジン
    • トランザクション前提です
  • 主キー
    • トランザクションの競合の検出のために主キーが必要です
  • IPv4 ネットワーク
    • IPv6 は未対応です
  • ネットワークパフォーマンス
    • それなりに太い帯域とそれなりに低いレイテンシが前提です

制限

認証プロセス?(トランザクションのコミット時にグループのメンバで合意を得るプロセス)ではギャップロックが利用できない(InnoDB の外部ではギャップロックに関する情報を利用できないため)。なのでトランザクション分離レベルには READ COMMITTED を使うことをおすすめする(たぶん要するにトランザクションがグループに伝播されるときにトランザクションが発生したノード以外ではギャップロックが利かないということだと思います。それで困るのはマルチプライマリのときだけだと思うのでシングルプライマリなら関係ないように思います)。

LOCK TABLESGET_LOCK() は使用できない(これもシングルプライマリなら関係ないように思います。プライマリでは利きますよね? というか LOCK TABLES とか GET_LOCK() とかなんて普段使わないですね。。。)。

マルチプライマリモードでは SERIALIZABLE 分離レベルはサポートしていない。

マルチプライマリモードでは同じオブジェクトに対して異なるサーバで DDL と DML が同時に実行されると DDL の競合が検出されないリスクがある?

マルチプライマリモードでは CASCADE を指定した外部キー制約をサポートしていない。カスケード操作が行われるときに検出できない競合が生じることがあるため。なのでマルチプライマリモードでは group_replication_enforce_update_everywhere_checks を有効にすることをおすすめする。シングルプライマリモードなら問題ない。

グループ内での複製に5秒以上を要するような通信があるとグループ内の通信の失敗を引き起こす可能性がある。LOAD DATA INFILE などで使用するファイルを小さく分割するなどの対応が必要。

マルチプライマリモードでは SELECT .. FOR UPDATE でデッドロックすることがある。これはグループのメンバー間でロックが共有されないため。 (SELECT .. FOR UPDATE は普通にデッドロックする可能性はあると思うのだけど・・どういうこと?)

グローバルレプリケーションフィルタは使用できない。一部のメンバーでトランザクションをフィルタするとグループが一貫のある状態で同期できなくなるため。グループ外のサーバとのレプリケーションなどのグループレプリケーションに直接関係しないレプリケーションチャンネルになら使用できる。 (いわゆる replicate-do-db とかのことだと思われる)

さいごに

GTID レプリケーションと mysqlfailover に毛が生えたようなものかと思ってたんですけど

とかを見るに単純にバイナリログを転送しているだけではなさそうです。ただシングルプライマリだと実質単純にバイナリログを転送しているだけになりそうな気もするので、グループレプリケーションの最大の旨味はマルチプライマリモードですかね? 最もノードのヘルスチェックやフェイルオーバーの自動化の面だけでも十分メリットあると思うので今後使えそうなら使っていきたいかも。

それと、グループ全体をシャットダウンすると group_replication_bootstrap_group が再び必要になります。これはちょっと面倒ですね。いわゆる本番環境なら止めることはないので良いですけど、検証環境とかで上げたり下げたりすることがあると運用が面倒そうです。

mysqldump で default-character-set とか hex-blob とか

とある MySQL のダンプファイルをインポートしようとしたところ、次のような警告が表示されました。

Warning (Code 1300): Invalid utf8 character string: 'FFFFFF'

インポートに失敗しているわけではないですがやや気持ち悪いです。

原因

blob とかのバイナリを含むテーブルを mysqldump して mysql でインポートしようとしたためです。

create table t (x blob, z text);
insert into t values (x'FFFF0123456789ABCDEF', 'あいうえお');
select hex(x), z from t;
+----------------------+-----------------+
| hex(x)               | z               |
+----------------------+-----------------+
| FFFF0123456789ABCDEF | あいうえお      |
+----------------------+-----------------+
mysqldump test t > dump.sql
mysql test < dump.sql
#=> Warning (Code 1300): Invalid utf8 character string: 'FFFF01'

バイナリを含むテーブルを普通に mysqldump すると文字列リテラルにバイナリのバイトシーケンスがそのまま出力されます(エスケープとかされないという意味)。

そのようなダンプファイルを mysql に流すと、リテラルの中身を utf8 として読もうとして↑のような警告になります(エラーになることもあったっけ?)。

もちろん SQL 的にエスケープが必要なものはエスケープされます(シングルクオートとか)。

insert into t values (x'302739', 'あいうえお');
mysqldump test t | less
#=> INSERT INTO `t` VALUES ('0\'9','あいうえお');

解決方法(hex-blob)

mysqldump--hex-blob オプションを付けるのがスタンダードな解決方法です。

$ mysqldump --help | grep -A1 -- --hex-blob
  --hex-blob          Dump binary strings (BINARY, VARBINARY, BLOB) in
                      hexadecimal format.

このオプションを付けてダンプするとバイナリ部分は16進の形式でダンプされます。

INSERT INTO `t` VALUES (0xFFFF0123456789ABCDEF,'あいうえお');

解決方法(default-character-set)

リテラルの部分を utf8 として解釈しようとするから警告が出ているわけなので、インポート時に --default-character-set=binary などと指定する方法も考えられます。が・・・これはうまくいきません。

mysql test --default-character-set=binary < dump.sql
#=> Warning (Code 1300): Invalid utf8 character string: 'FFFF01'

なぜなら普通に mysqldump するとダンプファイルの先頭のあたりに SET NAMES utf8 が出力されるためです。

無理やり削ってやれば大丈夫です。

sed '/SET NAMES/d' dump.sql | mysql test --default-character-set=binary

ただし、この方法はダンプファイルに出力されている文字のエンコーディングと、データベースの(テーブルの(カラムの))エンコーディングが一致している前提が必要です。もっとも、今日日はどちらも utf8 とか utf8mb4 などで揃っているだろうのでまず問題は無いと思いますが。

あるいはダンプ時に --default-character-set=binary を指定しても良いです。

mysqldump --default-character-set=binary test t > dump.sql
mysql test < dump.sql

あるいはダンプ時に SET NAMES を出力しないオプションもあります。これなら mysql--default-character-set=binary を活かせられます。

mysqldump --skip-set-charset test t > dump.sql
mysql --default-character-set=binary test < dump.sql

mysqldump の default-character-set とは

そもそも mysqldump に指定する --default-character-set とはいったいなんなのか・・これはダンプファイルに出力される文字エンコーディングです。

例えば次のように複数の文字エンコーディングが混在しているようなテーブルで、

create table t (
    s text charset sjis,
    e text charset ujis,
    u text charset utf8
);

insert into t values ('あ', 'あ', 'あ');
select * from t;

元の文字エンコーディングがなんであったとしても mysqldump--default-character-set で指定したエンコーディングに変換されて出力されます。

mysqldump --default-character-set=sjis test t | grep INSERT | nkf -Sw
#=> INSERT INTO `t` VALUES ('あ','あ','あ');
mysqldump --default-character-set=ujis test t | grep INSERT | nkf -Ew
#=> INSERT INTO `t` VALUES ('あ','あ','あ');
mysqldump --default-character-set=utf8 test t | grep INSERT
#=> INSERT INTO `t` VALUES ('あ','あ','あ');

--default-character-set=binary なら素のまま出力されます(ので化けてる、空文字に見えてる部分には印字不可能なバイトシーケンスがあります)。

mysqldump --default-character-set=binary test t | grep INSERT | nkf -Sw
#=> INSERT INTO `t` VALUES ('あ','、「','縺);
mysqldump --default-character-set=binary test t | grep INSERT | nkf -Ew
#=> INSERT INTO `t` VALUES ('','あ',');
mysqldump --default-character-set=binary test t | grep INSERT
#=> INSERT INTO `t` VALUES ('','','あ');

さいごに

バイナリを含むテーブルのダンプには --hex-blob を付けましょう。~/.my.cnf に書いても可。

--default-character-set=binary は個人的にオススメできません。本来テキストファイルであるはずのファンプファイルにバイナリのバイトシーケンスが含まれるのは違和感がありすぎます。--default-character-set=binary を使うことで無駄な変換が行われず、複数のエンコーディングを同時に使っている状況なら意義があることもあるかもしれませんが、今日日はデータベースも mysqldumputf8 で統一されているでしょう。もしどうしてもおかしなエンコーディングを使う必要があるなら(sjis とか)それこそ BLOB とかでバイナリで放り込めばいいんじゃないでしょうか。

MySQL の GTID レプリを replicate-do-db でフィルタすると欠番になる?

スレーブ側で--replicate-do-や--replicate-ignore-などのルールを使ってフィルタリングをすると、GTIDに欠番ができて、連番が連続しなくなるため、SHOW SLAVE STATUSの出力が大変なことになってしまう。GTIDを用いるときは、フィルタリングしないのほうが無難である。

漢(オトコ)のコンピュータ道: MySQLレプリケーションの運用が劇的変化!!GTIDについて仕組みから理解する

replicate-do-db などでスレーブでフィルタしていると GTID に欠番が生じてスレーブの Executed_Gtid_Set がすごいことになる。ということだと思うのですが、GTID はトランザクション単位で採番されるものの replicate-do-db とかはステートメント単位とかだと思うので(1つのトランザクションの中にフィルタされる更新とされない更新が混ざることがある)、直感的にはトランザクションの一部がフィルタされたときはマスターとスレーブでトランザクションの内容に差異が生じて、全部フィルタされたときも GTID が欠番にはならずに空のトランザクションになりそうな気がしたので、試してみました。

試したバージョンは次の通り。

mysqld --version
/usr/sbin/mysqld  Ver 8.0.11 for Linux on x86_64 (MySQL Community Server - GPL)

docker-compose.yml

version: '3.4'

services:
  sv01:
    image: mysql:8
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      MYSQL_DATABASE: test
    networks:
      mysql:
        ipv4_address: 192.168.88.11
    command:
      - --default_authentication_plugin=mysql_native_password
      - --skip-name-resolve
      - --character-set-server=utf8
      - --innodb-file-per-table
      - --log-bin=mysql-bin
      - --sync-binlog=1
      - --relay-log=relay-bin
      - --log-slave-updates
      - --skip-slave-start
      - --binlog-format=row
      - --replicate-do-db=test
      - --slave-exec-mode=IDEMPOTENT
      - --master-info-repository=TABLE
      - --relay-log-info-repository=TABLE
      - --relay-log-recovery=ON
      - --gtid-mode=ON
      - --enforce-gtid-consistency
      - --server-id=1

  sv02:
    image: mysql:8
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      MYSQL_DATABASE: test
    networks:
      mysql:
        ipv4_address: 192.168.88.12
    command:
      - --default_authentication_plugin=mysql_native_password
      - --skip-name-resolve
      - --character-set-server=utf8
      - --innodb-file-per-table
      - --log-bin=mysql-bin
      - --sync-binlog=1
      - --relay-log=relay-bin
      - --log-slave-updates
      - --skip-slave-start
      - --binlog-format=row
      - --replicate-do-db=test
      - --slave-exec-mode=IDEMPOTENT
      - --master-info-repository=TABLE
      - --relay-log-info-repository=TABLE
      - --relay-log-recovery=ON
      - --gtid-mode=ON
      - --enforce-gtid-consistency
      - --server-id=2

networks:
  mysql:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.88.0/24

docker-compose で環境を立ち上げる。

docker-compose up
docker-compose exec sv01 bash
docker-compose exec sv02 bash

レプリケーション用のユーザーを作成。

# [sv01/sv02]
mysql mysql -v <<'SQL'
CREATE USER 'repl'@'%' IDENTIFIED BY 'pass';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
SQL

バイナリログがわかりやすくなるように reset master しとく。

# [sv01/sv02]
mysql mysql -v <<'SQL'
reset master;
SQL

レプリケーションを開始。

# [sv02]
mysql mysql -v <<'SQL'
change master to
  MASTER_HOST = '192.168.88.11',
  MASTER_USER = 'repl',
  MASTER_PASSWORD = 'pass',
  MASTER_AUTO_POSITION = 1;
start slave;
SQL

--replicate-do-db=test により test データベース以外はスレーブでフィルタされるようになっているので、適当に別のデータベースを作るなどしてマスターを更新します。

/* [sv01] */
use test
create table t (id int not null primary key, no int not null);
insert into t values (1, 111);

/* [sv01/sv02] */
select * from t;

/* [sv01] */
create database hoge;
use hoge
create table h (id int not null primary key, no int not null);
insert into h values (1, 111);

/* [sv01] */
use test
insert into t values (2, 222);

GTID を見てみます。

/* [sv01/sv02] */
show master status \G
show slave status \G

見た感じ欠番が生じてる雰囲気はありません。次のように mysqlbinlog を見比べてみても、

mysqlbinlog mysql-bin.000001 --include-gtids=6930f785-7376-11e8-9b24-0242c0a8580b:5 --base64-output=DECODE-ROWS -v

sv01

BEGIN
/*!*/;
# at 1197
#180619  5:22:14 server id 1  end_log_pos 1245 CRC32 0xb5b056b1         Table_map: `hoge`.`h` mapped to number 107
# at 1245
#180619  5:22:14 server id 1  end_log_pos 1289 CRC32 0xe5c002ce         Write_rows: table id 107 flags: STMT_END_F
### INSERT INTO `hoge`.`h`
### SET
###   @1=1
###   @2=111
# at 1289
#180619  5:22:14 server id 1  end_log_pos 1320 CRC32 0xf366fedf         Xid = 122
COMMIT/*!*/;

sv02

BEGIN
/*!*/;
# at 1265
#180619  5:22:14 server id 1  end_log_pos 1332 CRC32 0x7b096e01         Query   thread_id=17    exec_time=0     error_code=0
SET TIMESTAMP=1529385734/*!*/;
COMMIT

スレーブでフィルタされた処理は空のトランザクションとなって記録されているようです。

ので replicate-do-db などを用いてフィルタしてもスレーブで GTID に欠番がでることはなさそうです。

さいごに

参考にした記事はだいぶ古く(3年半ぐらい前)、バージョンも 5.6 とかなので、今回試した 8.0.11 だとその辺の事情も変わってるのかもしれません。

現代は GTID レプリケーションと replicate-do-db などを併用しても問題ない、と思います。

MySQL GTID レプリケーション素振り

従来のレプリケーションとの違いをざっくりと。

  • ノードに UUID が付与されて「UUID+連番」ですべてのトランザクションに一意なID(GTID)がつく
  • 各ノードが「適用済の GTID」を持っているので循環レプリケーションやマスター切り替えが容易にできる
  • 「適用済の GTID」はバイナリログに書かれているのでスレーブでもバイナリログ必須(log-slave-updates
  • GTID はトランザクション単位で付与されるのでトランザクションテーブル必須(MyISAM 不可)

GTID レプリケーションをセットアップ

docker-compose.yml

version: '3.4'

services:
  sv01:
    image: mysql:8
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      MYSQL_DATABASE: test
    networks:
      mysql:
        ipv4_address: 192.168.88.11
    command:
      - --default_authentication_plugin=mysql_native_password
      - --skip-name-resolve
      - --character-set-server=utf8
      - --innodb-file-per-table
      - --log-bin=mysql-bin
      - --sync-binlog=1
      - --relay-log=relay-bin
      - --log-slave-updates
      - --skip-slave-start
      - --binlog-format=row
      - --binlog-do-db=test
      - --slave-exec-mode=IDEMPOTENT
      - --master-info-repository=TABLE
      - --relay-log-info-repository=TABLE
      - --relay-log-recovery=ON
      - --gtid-mode=ON
      - --enforce-gtid-consistency
      - --server-id=1

  sv02:
    image: mysql:8
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      MYSQL_DATABASE: test
    networks:
      mysql:
        ipv4_address: 192.168.88.12
    command:
      - --default_authentication_plugin=mysql_native_password
      - --skip-name-resolve
      - --character-set-server=utf8
      - --innodb-file-per-table
      - --log-bin=mysql-bin
      - --sync-binlog=1
      - --relay-log=relay-bin
      - --log-slave-updates
      - --skip-slave-start
      - --binlog-format=row
      - --binlog-do-db=test
      - --slave-exec-mode=IDEMPOTENT
      - --master-info-repository=TABLE
      - --relay-log-info-repository=TABLE
      - --relay-log-recovery=ON
      - --gtid-mode=ON
      - --enforce-gtid-consistency
      - --server-id=2

  sv03:
    image: mysql:8
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      MYSQL_DATABASE: test
    networks:
      mysql:
        ipv4_address: 192.168.88.13
    command:
      - --default_authentication_plugin=mysql_native_password
      - --skip-name-resolve
      - --character-set-server=utf8
      - --innodb-file-per-table
      - --log-bin=mysql-bin
      - --sync-binlog=1
      - --relay-log=relay-bin
      - --log-slave-updates
      - --skip-slave-start
      - --binlog-format=row
      - --binlog-do-db=test
      - --slave-exec-mode=IDEMPOTENT
      - --master-info-repository=TABLE
      - --relay-log-info-repository=TABLE
      - --relay-log-recovery=ON
      - --gtid-mode=ON
      - --enforce-gtid-consistency
      - --server-id=3

networks:
  mysql:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.88.0/24

up してコンテナの中へ。

docker-compose up
docker-compose exec sv01 bash
docker-compose exec sv02 bash
docker-compose exec sv03 bash

すべてのノードにレプリケーション用のユーザーを作る。--binlog-do-db=test してるのでこれはレプリケーションされません。

# [sv01/sv02/sv03]
mysql mysql -v <<'SQL'
CREATE USER 'repl'@'%' IDENTIFIED BY 'pass';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
SQL

レプリケーションを設定する。sv01 -> sv02 -> sv03 -> sv01 のような循環レプリケーションします。

# [sv01]
mysql mysql -v <<'SQL'
change master to
  MASTER_HOST = '192.168.88.13',
  MASTER_USER = 'repl',
  MASTER_PASSWORD = 'pass',
  MASTER_AUTO_POSITION = 1;
start slave;
SQL

# [sv02]
mysql mysql -v <<'SQL'
change master to
  MASTER_HOST = '192.168.88.11',
  MASTER_USER = 'repl',
  MASTER_PASSWORD = 'pass',
  MASTER_AUTO_POSITION = 1;
start slave;
SQL

# [sv03]
mysql mysql -v <<'SQL'
change master to
  MASTER_HOST = '192.168.88.12',
  MASTER_USER = 'repl',
  MASTER_PASSWORD = 'pass',
  MASTER_AUTO_POSITION = 1;
start slave;
SQL

適当なノードでテーブルを作ったり行を挿入したりすると、すべてのノードにレプリケーションされます。

# [sv01/sv02/sv03]
mysql test
/* [sv03] */
create table t (id int not null primary key, no int not null);

/* [sv01] */
insert into t values (1, 111);

/* [sv02] */
insert into t values (2, 222);

/* [sv01/sv02/sv03] */
select * from t;

slave-exec-mode=IDEMPOTENT

(GTID とは関係ないですが)slave-exec-mode=IDEMPOTENT の効果により、行ベースレプリケーションで行の競合がエラーにならなくなっています。

試しに行の挿入で競合を発生させてみます。

/* [sv01/sv02/sv03] */
stop slave;

/* [sv01] */
insert into t values (9, 901);

/* [sv02] */
insert into t values (9, 902);

/* [sv03] */
insert into t values (9, 903);

/* [sv01/sv02/sv03] */
start slave;
select * from t;
show slave status \G

普通ならSQLスレッドでエラーになってレプリケーションが止まるところですが slave-exec-mode=IDEMPOTENT の効果によってエラーになりません。

ちなみにノードごとに行の値は次のようになっていました。

# [sv01]
+----+-----+
| id | no  |
+----+-----+
|  9 | 902 |
+----+-----+

# [sv02]
+----+-----+
| id | no  |
+----+-----+
|  9 | 903 |
+----+-----+

# [sv03]
+----+-----+
| id | no  |
+----+-----+
|  9 | 901 |
+----+-----+

この結果から推測するに、競合した場合は後勝ちで上書きされるようです。

なので例えば2台で双方向レプリケーションしているときに両方に同じ主キーのレコードを書き込むと、ノード1にはノード2に書き込んだ内容が、ノード2にはノード1に書き込んだ内容がのこることになりますね、うーん?

マスターの切り替え

GTID ならマスターの切り替えも簡単です。試しに sv02 が死んだと仮定して sv03 のマスターを sv01 に変えてみます。

/* [sv02] */
stop slave;

/* [sv03] */
stop slave;
change master to MASTER_HOST = '192.168.88.11';
start slave;

sv01 を更新すると sv03 にレプリケーションされます。

/* [sv01] */
insert into t values (4, 444);

/* [sv03] */
select * from t;
show slave status \G

従来のレプリケーションだとマスターが変わればログポジションも変わるので mysqldump で再同期が必要なケースです(MHA なんかはこの辺うまくやってた気がする)。

戻すのも簡単です。

/* [sv02] */
start slave;

/* [sv03] */
stop slave;
change master to MASTER_HOST = '192.168.88.12';
start slave;

競合したときのリカバリ

slave-exec-mode=IDEMPOTENT の効果によって行ベースの競合はエラーになりませんが、DDL なら簡単に競合によってレプリケーションが停止します。

/* [sv01/sv02] */
stop slave;

/* [sv01] */
create table x (sv01 int not null primary key);

/* [sv02] */
create table x (sv02 int not null primary key);

/* [sv01/sv02] */
start slave;

/* [sv01/sv02/sv03] */
show slave status \G

sv01 や sv02 で次のようなエラーでレプリケーションが止まっています。

Error 'Table 'x' already exists' on query. Default database: 'test'. Query: 'create table x (sv02 int not null primary key)'

従来のレプリケーションなら SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1 でスキップするステートメントの数を指定するのですが、GTID レプリケーションではこれはできません。

GTID の場合はまず show slave statusRetrieved_Gtid_SetExecuted_Gtid_Set でエラーになっている GTID を特定します。例えば次のようにでした。

Retrieved_Gtid_Set:
  c7399d3d-72d0-11e8-a858-0242c0a8580c:1-4,
  c7502877-72d0-11e8-b3c2-0242c0a8580d:1-3
Executed_Gtid_Set:
  c7399d3d-72d0-11e8-a858-0242c0a8580c:1-3,
  c7502877-72d0-11e8-b3c2-0242c0a8580d:1-3,
  c75314e4-72d0-11e8-a322-0242c0a8580b:1-5

Retrieved_Gtid_Set にあって Executed_Gtid_Set に無いものがレプリケーションされていない GTID です。比較すると UUID c7399d3d-72d0-11e8-a858-0242c0a8580c について Retrieved_Gtid_Set1-4Executed_Gtid_Set1-3 なので、エラーになっているのは c7399d3d-72d0-11e8-a858-0242c0a8580c:4 だということがわかります。

このエラーの原因となっている GTID を次の手順で空のトランザクションで上書きします。

SET GTID_NEXT='c7399d3d-72d0-11e8-a858-0242c0a8580c:4';
BEGIN;
COMMIT;
SET GTID_NEXT='AUTOMATIC';

この時点でスレーブを開始すればレプリケーションは復帰しますが、この手順で復帰させたときは同じ GTID のバイナリログがノードによって異なる内容になっているため、誤動作の元になるかもしれないので次のようにバイナリログを削除しておくのが良いらしいです。

/* バイナリログの一覧を表示して最後のログをメモる */
show master logs;

/* バイナリログをローテート */
FLUSH LOGS;

/* バイナリログの削除 */
PURGE BINARY LOGS TO 'mysql-bin.000003';

この後、スレーブを開始できます。

start slave;
show slave status \G

もちろん、テーブルがノードによって異なる定義になってしまっている問題は残ったままなので、その問題の解決はまた別に行う必要があります。

スレーブの追加や再同期

スレーブの追加や再同期は mysqldump で流し込んだ後に change master tostart slave するだけで良いです。

# [sv04]
mysqldump -h sv03 --all-databases --triggers --routines --events | mysql

# [sv04]
mysql mysql -v <<'SQL'
change master to
  MASTER_HOST = '192.168.88.13',
  MASTER_USER = 'repl',
  MASTER_PASSWORD = 'pass',
  MASTER_AUTO_POSITION = 1;
start slave;
SQL

GTID が有効なサーバからの mysqldump では GTID レプリケーションに必要な↓みたいなのがデフォで含まれています。

--
-- GTID state at the beginning of the backup
--

SET @@GLOBAL.GTID_PURGED=/*!80000 '+'*/ 'c1ea378a-72d5-11e8-a345-0242c0a8580d:1,
c7399d3d-72d0-11e8-a858-0242c0a8580c:1-6,
c7502877-72d0-11e8-b3c2-0242c0a8580d:1-3,
c75314e4-72d0-11e8-a322-0242c0a8580b:1-6';

「これらの GTID のバイナリログは削除されてるので無いです、でもかつてあったということは覚えていて」みたいなニュアンスかと思います。

その他のメモ

binlog-do-db とか replicate-do-db とか

binlog-do-dbbinlog-ignore-db はマスターのバイナリログの出力に作用するオプションなので、マスターでバイナリログへの出力の段階でフィルタされ、スレーブへ転送されるログやスレーブで適用されるログは、マスターでフィルタされたバイナリログのみになる。

一方で replication-do-dbreplicate-ignore-db はスレーブのSQLスレッドの実行に作用するオプションなので、マスターでのバイナリログの出力やスレーブへ転送されるバイナリログの量には影響せず、マスターから転送されたバイナリログをスレーブがどれだけ実行するかの制御にしかならない。

なので、マスター~スレーブの転送量の削減のために replication-do-db は使えない。その目的なら binlog-do-db を使わなければならない。

ただし binlog-do-db だとその設定をおこなったマスタからレプリケーションするすべてのスレーブに設定が影響する。replication-do-db ならスレーブごとにレプリケーションするDBを選択できるのでフレキシブル。

ステートメントベースレプリケーションと行ベースレプリケーション

binlog-do-db とか replicate-do-db が作用するデータベースについて、ステートメントベースなら USE で選択されているカレントのデータベースによって制御される。

例えば binlog-do-db=test の場合、カレントデータベースが test であるクエリのみがレプリケーションの対象となる。実際に作用されたデータベースではないことに注意。例えば下記のようになる。

/* カレントが test なのでレプリケーションされる */
use test
insert into hoge.t_user values (1);

/* カレントが hoge なのでレプリケーションされない */
use hoge
insert into test.t_user values (1);

一方で、行ベースでは、カレントのデータベースではなく実際に作用するデータベースで制御される。そのため↑の例だと逆になる。

なお binlog-format=row と設定したからといって必ず行ベースになるわけではない。そもそも行ベースではないもの、例えば DDL は行ベースにならない(しようがない)のでステートメントベースになる。

なので binlog-format=row かつ replicate-do-db=test しているとき次のようになる。

use test
/* 行ベースなのでレプリケーションされない */
insert into hoge.t_user values (1);
/* ステートメントベースなのでレプリケーションされる */
create table hoge.t (id int not nul primary key);

use hoge
/* 行ベースなのでレプリケーションされる */
insert into test.t_user values (1);
/* ステートメントベースなのでレプリケーションされない */
create table test.t (id int not nul primary key);

一方で create database drop database alter databasereplicate-do-db で指定の通りに動く。つまりカレントデータベースではなく作用されるデータベースでフィルタされる( https://dev.mysql.com/doc/refman/8.0/en/replication-rules-db-options.html の下の方の Important の説明)。

さいごに

GTID レプリケーションではいくつか制限があります。

が、(既存の非GTIDからのマイグレーションとかはともかく)新規にやる分には問題なさそうな気がするので、今後 MySQL でレプリケーションするなら GTID は常に有効で良いと思う。

MD や LVM 使っている CentOS 6 に Kickstart で CentOS 7 を上書き

例えば CentOS 6 で下記のようなディスク構成だった場合、

zerombr
bootloader --location=mbr --driveorder=vda,vdb
clearpart --all --initlabel

part raid.11 --ondrive=sda --asprimary --size 500
part raid.12 --ondrive=sdb --asprimary --size 500

part raid.21 --ondrive=sda --asprimary --size 1 --grow
part raid.22 --ondrive=sdb --asprimary --size 1 --grow

raid /boot --fstype=ext4 --device=md0 --level=1 raid.11 raid.12
raid pv.01 --fstype=ext4 --device=md1 --level=1 raid.21 raid.22

volgroup vg0 pv.01

logvol /     --vgname=vg0 --size=4096 --name=lv_centos6 --fstype=ext4
logvol swap  --vgname=vg0 --size=1024 --name=lv_swap
logvol /data --vgname=vg0 --size=1024 --name=lv_data --fstype=ext4

下記のように Kickstart すれば、既存のボリュームグループに CentOS 7 のルートボリュームを作成してセットアップできる。

bootloader --location=mbr --driveorder=vda,vdb
clearpart --none

# 作成済のパーティションを RAID で使う
part raid.11 --onpart=sda1
part raid.12 --onpart=sdb1

# /boot を RAID1 で作る
raid /boot --fstype=xfs --device=md0 --level=1 raid.11 raid.12

# 作成済のボリュームグループに論理ボリュームを作成
logvol /     --vgname=vg0 --size=4096 --name=lv_centos7 --fstype=xfs

# 作成済の論理ボリュームを使う
logvol swap  --vgname=vg0 --name=lv_swap --useexisting
logvol /data --vgname=vg0 --name=lv_data --useexisting --noformat

ただしこれだと /boot は上書きされるので CentOS 6 のルートボリュームは残っているものの CentOS 6 を起動することはできない。


1年ちょいぐらい前にこのようなことを行ったメモを発掘したのだけど、一体なんのためにこんなことしたのだろう・・/data を維持したまま CentOS 6 -> 7 にリプレースしたかったのだと思うけど。

Prometheus -> InfluxDB でダウンサンプリングしてみたメモ

半年くらい前に社内の勉強会っぽい何かで話すために書いていたけど結局やらずにお蔵入りしたメモ。

半年くらい前なのでわりと古いです


Prometheus は単体だとダウンサンプリングができないので長期のデータ保持には向いていません。デフォルトだと 15 日しか保持しないし、もっと長期に渡って保持するように設定すると凄まじい量のストレージを喰います。

他のリソース監視・可視化ツールであれば、5 分毎のデータを 1 日保持、30 分毎のデータを 1 週間保持、2 時間毎のデータを 1 ヶ月保持、1 日ごとのデータを 1 年間保持、のように段階的に粒度を荒くしたデータを長期に保持できるようになっていたりします。

Prometheus の場合、データの粒度と保持期間が異なる別の Prometheus を用意して Prometheus -> Prometheus とデータを流したり(Federation)、Prometheus から別の時系列データベースにデータを書き込んだりすることで(Remote write)、ダウンサンプリングっぽいことができます。

そこで、試しに社内のいくつかのサーバを監視している Prometheus のメトリクスを InfluxDB に流してダウンサンプリングしてみました。

remote_storage_adapter (remote_storage_bridge) のインストール

以前セットアップしたときは remote_storage_bridge というものが必要だったんですが、今は remote_storage_adapter という名前に変わってました。たぶん次のような感じでビルドできます(別にビルドに Docker を使う必要は無いですが)。

docker run --rm -v /usr/local/bin:/go/bin:rw golang \
  go get github.com/prometheus/prometheus/documentation/examples/remote_storage/remote_storage_adapter

remote_storage_bridge の起動やら Prometheus の設定とか InfluxDB のセットアップとかは別記事に簡単に書いています。

グラフ

1 時間毎のデータを 1 ヶ月保持したものです。

f:id:ngyuki:20171114204706p:plain

ダウンサンプリングの前後の比較です。上のグラフがダウンサンプリング前の 1 分毎のデータ、下のグラフがダウンサンプリング後の 1 時間毎のグラフです。ダウンサンプリングには平均値を使っています。メトリクスの種類によっては最大値とかのが良いかも?

f:id:ngyuki:20171114204639p:plain

さいごに

大抵の場合は Prometheus のデフォルトの 15 日で十分な気がするのですが、たまに長期の傾向が知りたいことあります(1 年間を通して見て、あーこの時期はあれがあったわー、とか)。

今は、短期グラフのために Prometheus を、長期グラフのために Cacti を併用していたりするのですが・・・

Prometheus -> InfluxDB でダウンサンプリングする場合、InfluxDB の保持期間ごとに Grafana で別々にグラフを作る必要があってちょっと面倒な感じもしました(Cacti・・というか RRDTool ならデータを取得する期間に応じて自動的に適切な粒度を拾ってきてくれる)。が、そもそも短期と長期でグラフをみるきっかけ違うし、そんなには困らないかな?

長期グラフを見ているときに特定の期間にドリルダウンしても短期グラフに変わらないのが不便かも・・

テンプレート変数でどうにかするものなのだろうか。

Prometheus 使ってみたメモ

9ヶ月ぐらい前?に社内の勉強会っぽいなにかで話そうと思ってたけど結局やらなくてお蔵入りになっていたメモ。

このとき試した Prometheus はだいぶ古いです・・・たぶん 1.5 ぐらいです。今が 2.0 とかなので色々変わっていると思います


メモ

  • 監視対象でエージェントを実行してマネージャーが HTTP でメトリクスを取得するいわゆる PULL 型
    • エージェントは Exporter と呼ばれる
  • ワンバイナリで他に依存もない
    • インストールが超楽
  • 標準的な Exporter がデフォでめちゃくちゃいろんなメトリクスを取ってくる
    • とりあえず収集して、必要に応じでアラートとか可視化とかする、というメンタルで使える
    • あらかじめ監視設計を難しく考えなくてもとりあえず始めることができる
  • 外形監視や SNMP 監視もできることはできる
    • 外形監視や SNMP 監視のための Exporter がある
    • その Exporter を Prometheus に監視対象として設定する
    • Prometheus から PULL されたときに Exporter が実際の監視をする
  • Prometheus ではまともな可視化はできない
    • Grahana で可視化が基本
  • アラートや可視化には PromQL というクエリ式が使える
    • そこそこフレキシブルに書ける
  • デフォで 3G ぐらいメモリを使うらしい
  • ダウンサイジングができずデフォでは 15 日で消える
    • 期間は増やせるが 1 年とかのデータを保持するのは辛そう
    • InfluxDB などの別の時系列データベースにデータを送れる
    • ダウンサイジングしたければそっちでやる

参考

Prometheus セットアップ

下記のようにバイナリをダウンロードして実行してもいいし、

docker でも簡単に実行できます。

docker run \
  -v ~/data/prometheus:/prometheus:rw \
  -v ./prometheus.yml:/etc/prometheus/prometheus.yml:ro \
  -p 9090:9090 \
  prom/prometheus

設定ファイル prometheus.yml は次のような感じです。

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: prometheus
    scrape_interval: 5s
    static_configs:
      - targets: [localhost:9090]

デフォルトだと 9090 ポートでリッスンするのでブラウザで下記の URL を開くと画面が閲覧できます。

この画面は Expression browser と呼ばれるもので PromQL というクエリ式でグラフを表示したりできます。

本格的な可視化は Grafana でやるとして Grafana に設定するためのクエリ式を調べるのに使うと良いです。

監視対象 に Node exporter をセットアップ

↑の設定だと Prometheus 自身のメトリクスしか収拾されていないので、他のサーバのメトリクスも収集してみます。

監視するサーバには Node exporter をインストールします。

バイナリをダウンロードして実行します、設定ファイルとかはありません。

./node_exporter

試しに社内のサーバにインストールしたときの手順とかは下記の通り。

# CentOS 7 ... 再起動すると消えるのでちゃんとやるなら systemd のユニットファイルを作る
curl -L https://github.com/prometheus/node_exporter/releases/download/v0.14.0-rc.1/node_exporter-0.14.0-rc.1.linux-amd64.tar.gz |
  tar zxf -
cd node_exporter-0.14.0-rc.1.linux-amd64
systemd-run --unit=node_exporter ./node_exporter
systemctl status node_exporter

# CentOS 6
wget https://github.com/prometheus/node_exporter/releases/download/v0.14.0-rc.1/node_exporter-0.14.0-rc.1.linux-amd64.tar.gz
tar zxf node_exporter-0.14.0-rc.1.linux-amd64.tar.gz
cd node_exporter-0.14.0-rc.1.linux-amd64
cat <<EOS> /etc/init/node_exporter.conf
start on runlevel [2345]
stop on runlevel [!2345]
chdir $PWD
exec ./node_exporter
respawn
EOS
initctl start node_exporter

Node exporter を起動したサーバの http://localhost:9100/metrics を叩くとメトリクスがだらーっと出ていることがわかります。

これを prometheus.yml で次のような感じで設定します。

scrape_configs:
  - job_name: node
    static_configs:
      - targets:
          - vm01
    relabel_configs:
      - source_labels: [__address__]
        target_label: instance
      - source_labels: [__address__]
        replacement: ${0}:9100
        target_label: __address__

クエリ

Prometheus の画面でクエリを入力するとグラフが見えます。

instant-vector

メトリクス名をそのまま書くと instant-vector となります。これは時刻がインデックスな配列みたいなものなのです。

コードで表現すると下記のような感じ?

[
  "12:00": 100,
  "12:01": 101,
  "12:02": 103,
  "12:03": 104,
  "12:04": 109,
]

ロードアベレージのような GAUGE 値は instant-vector をそのままグラフに表示できます。

# ロードアベレージ
node_load1

range-vector

node_cpu とかは COUNTER 値なので、そのまま表示してもあまり意味がありません。

node_cpu[5m] のように角括弧で時間を指定すると range-vector になります。これは要素が5分の範囲の配列である配列のようなものです。

コードで表現すると下記のような感じ?(実際にはこんな非効率な持ち方はしていない)

[
  "12:09": [
    "12:04": 10000,
    "12:05": 10000,
    "12:06": 10100,
    "12:07": 10300,
    "12:08": 10400,
    "12:09": 10900,
  ],
  "12:10": [
    "12:05": 10000,
    "12:06": 10100,
    "12:07": 10300,
    "12:08": 10400,
    "12:09": 10900,
    "12:10": 11100,
  ],
]

これはそのままだとグラフにできませんが range-vector を instant-vector に変換する関数を使えばグラフにできます。

例えば rate は range-vector範囲の最初と最後のポイントから導かれる1秒間の増加分です。なので、下記のようにすると CPU 使用率が求められます。

# CPU 使用率 ... 元が 100ミリ秒単位の CPU カウンタなので秒間の増加値がパーセンテージとして使える
rate(node_cpu[5m])

フィルタ

複数の監視対象がある場合、メトリクスのラベルでフィルタできます。

例えば特定のインスタンスのロードアベレージだけ表示してみたり。

node_load1{instance="sv01"}

否定の条件や正規表現も使えます。

node_load1{instance!="sv01"}
node_load1{instance=~"vm.*"}
node_load1{instance!~"vm.*"}

正規表現は文字列全体にマッチする必要があります(前後に ^$ が付く感じ)。また、わりと貧弱で言明とか使えません。

フィルタは監視対象を表す instance ラベルだけでなく、例えば node_cpu なら CPU の状態とかごとにラベルがあるので、例えば特定の状態をフィルタしたりできます。

rate(node_cpu{mode!="idle",instance="sv01"}[5m])

集計

CPU 使用率の場合、CPU コアごとにラベル付けされて記録されているため、複数コアだとコアごとに表示されます。

rate(node_cpu{mode!="idle",instance="vm01"}[5m])

集計関数を使えば合計した結果にできます。

sum(rate(node_cpu{mode!="idle",instance="vm01"}[5m]))

SQL での GROUP BY もできます。例えば下記の様にすれば mode ごとに集計できます。

sum(rate(node_cpu{mode!="idle",instance="vm01"}[5m])) by (mode)

こう書いても一緒です。

sum by (mode)(rate(node_cpu{mode!="idle",instance="vm01"}[5m]))

どのラベルで集計するか、ではなく、どのラベルを除いて集計するか、も出来ます。

例えば下記のようにすれば cpu ラベルを除いたラベルで集計されます。

sum(rate(node_cpu{mode!="idle",instance="vm01"}[5m])) without (cpu)

演算子

instant-vector は四則演算もできます。例えば下記のように CPU コア数に対しての CPU 使用率が計算できます。

sum(rate(node_cpu{mode!="idle"}[5m]) * 100) by (instance) /
  count(node_cpu{mode="system"}) by (instance)

メトリクス名をフィルタに使う

__name__ でメトリクス名をフィルタにできます。この方法を使えば1つのクエリで複数のメトリクスが同時に表示できます。

# ロードアベレージを 1s 5s 15s 全部表示
{__name__=~"node_load.*"}

# メモリ(Buffers + Cached + MemFree)
{__name__=~"node_memory_(Buffers|Cached|MemFree)"}

Grafana を使う場合はクエリ式を縦に並べればいいだけなのであんまり使うことはないです。

アラート

Prometheus のクエリ式の結果を元にメールとか Slack とかでアラートできます。

例えば下記のようなクエリ式でアラートできます。

ALERT cpu_usage
    IF sum(rate(node_cpu{mode!="idle"}[2m]) * 100) by (job, instance) /
       count(node_cpu{mode="system"}) by (job, instance) > 80
    FOR 30s
    LABELS { severity = "warning" }
    ANNOTATIONS {
        summary = "[{{ $labels.job }} {{ $labels.instance }}] cpu usage",
        description = "cpu usage now:{{$value}}",
    }

Grafana で複数の監視対象をずらーっと表示

Templating と Repeat for を使えば、同じ種類の複数の監視対象のグラフをずらーっと並べたりできます。

Cacti のように検索条件を指定してアドホックに表示とかはできなさそうだけど。

Grafana のダッシュボードのインポートとエクスポート

Grafana はダッシュボードの作成は基本的に WebUI ぽちぽちなのですが、Grafana の API を使えばダッシュボードの定義のインポートやエクスポートがサクッとできます。

Grafana の API は下記のように呼べます。

# /api/org の例
curl \
  -H "Authorization: Bearer $GRAFANA_API_KEY" \
  -H "Content-type: application/json" \
  "http://localhost:3000/api/org"

下記のスクリプトを用意してサクッと呼べるようにします。

#!/bin/bash

set -eu

api=${1#/}
shift
exec curl -H "Authorization: Bearer $GRAFANA_API_KEY" -H "Content-type: application/json" http://localhost:3000/$api "$@"

ダッシュボードをエクスポートします。

./grafana-api.sh /api/dashboards/db/servers | jq . > grafana/servers.json

トップレベルのキーに metadashboard がありますが、必要なのは dashboard だけです。

{
  "meta": {
    "...snip..."
  },
  "dashboard": {
    "...snip..."
  }
}

インポートするときは、新規追加なのか上書きなのかで微妙に異なります。

新規追加の場合は .dashboard.idnull にする必要があります。

cat grafana/servers.json |
  jq '. * {dashboard:{id:null}}' |
  ./grafana-api.sh /api/dashboards/db -X POST -d @-

上書きする場合はさらに .overwritetrue にする必要があります(同じ名前のダッシュボードが上書きされます)。

cat grafana/servers.json |
  jq '. * {dashboard:{id:null},overwrite:true}' |
  ./grafana-api.sh /api/dashboards/db -X POST -d @-

外形監視

外形監視も Blackbox exporter を使えばできます。

  • Prometheus に Blackbox exporter を監視対象として追加
    • このとき外形監視したいサーバをパラメータとして設定
  • Prometheus から PULL されるときパラメータも渡される
    • そのパラメータに基づいて Blackbox exporter が PING や HTTP で監視する
    • 結果を Blackbox exporter のメトリクスとして Prometheus に返す

Blackbox exporter 自体は外形監視先の情報を持っていないので、Blackbox exporter の設定は下記のように、どのように監視するか、しかありません。

modules:
  http_2xx:
    prober: http
    timeout: 5s
    http:
      valid_status_codes: [] # Defaults to 2xx
      method: GET
      headers:
        Host: example.com
  ssh_banner:
    prober: tcp
    timeout: 5s
    tcp:
      query_response:- expect: "^SSH-2.0-"
  icmp:
    prober: icmp
    timeout: 5s

このように設定された Blackbox exporter に対して Prometheus が下記のようにメトリクスを PULL することで実際の監視が実行されます。

SNMP

SNMP での監視も Snmp Exporter で外形監視と同じような仕組みで動きます。

ただ、監視する OID を Snmp Exporter でだばーっと設定する必要があります。例えば次のように。

default:
  version: 2
  auth:
    community: hoge
  walk:
    - 1.3.6.1.4.1.2021.4
    - 1.3.6.1.4.1.2021.11
  metrics:
    - { name: snmp_memTotalSwap      , oid: 1.3.6.1.4.1.2021.4.3.0   , type: gauge }
    - { name: snmp_memAvailSwap      , oid: 1.3.6.1.4.1.2021.4.4.0   , type: gauge }
    - { name: snmp_memTotalReal      , oid: 1.3.6.1.4.1.2021.4.5.0   , type: gauge }
    - { name: snmp_memAvailReal      , oid: 1.3.6.1.4.1.2021.4.6.0   , type: gauge }
    - { name: snmp_memTotalFree      , oid: 1.3.6.1.4.1.2021.4.11.0  , type: gauge }
    - { name: snmp_memMinimumSwap    , oid: 1.3.6.1.4.1.2021.4.12.0  , type: gauge }
    - { name: snmp_memBuffer         , oid: 1.3.6.1.4.1.2021.4.14.0  , type: gauge }
    - { name: snmp_memCached         , oid: 1.3.6.1.4.1.2021.4.15.0  , type: gauge }
    - { name: snmp_ssCpuRawUser      , oid: 1.3.6.1.4.1.2021.11.50.0 , type: counter }
    - { name: snmp_ssCpuRawNice      , oid: 1.3.6.1.4.1.2021.11.51.0 , type: counter }
    - { name: snmp_ssCpuRawSystem    , oid: 1.3.6.1.4.1.2021.11.52.0 , type: counter }
    - { name: snmp_ssCpuRawIdle      , oid: 1.3.6.1.4.1.2021.11.53.0 , type: counter }
    - { name: snmp_ssCpuRawWait      , oid: 1.3.6.1.4.1.2021.11.54.0 , type: counter }
    - { name: snmp_ssCpuRawKernel    , oid: 1.3.6.1.4.1.2021.11.55.0 , type: counter }
    - { name: snmp_ssCpuRawInterrupt , oid: 1.3.6.1.4.1.2021.11.56.0 , type: counter }
    - { name: snmp_ssIORawSent       , oid: 1.3.6.1.4.1.2021.11.57.0 , type: counter }
    - { name: snmp_ssIORawReceived   , oid: 1.3.6.1.4.1.2021.11.58.0 , type: counter }
    - { name: snmp_ssRawInterrupts   , oid: 1.3.6.1.4.1.2021.11.59.0 , type: counter }
    - { name: snmp_ssRawContexts     , oid: 1.3.6.1.4.1.2021.11.60.0 , type: counter }
    - { name: snmp_ssCpuRawSoftIRQ   , oid: 1.3.6.1.4.1.2021.11.61.0 , type: counter }
    - { name: snmp_ssRawSwapIn       , oid: 1.3.6.1.4.1.2021.11.62.0 , type: counter }
    - { name: snmp_ssRawSwapOut      , oid: 1.3.6.1.4.1.2021.11.63.0 , type: counter }

こまかいメモ

ここから下はこまかいことのメモ。

ストレージについて

https://prometheus.io/docs/operating/storage/

Memory usage

  • インデックスの場合は LevelDB を使用する
  • バルクサンプルデータの場合は独自のストレージ?
    • 1024 バイトのチャンクごとに整理される
    • チャンクは time series ごとにファイルに保存される
  • 現在使用されているすべてのチャンクをメモリに保持する
  • 最近使用されたチャンクは storage.local.memory-chunks までメモリに保持する
    • デフォルトは 1048576 で増やしたり減らしたりの調整をしても良い
  • サーバのメモリ使用量は storage.local.memory-chunks * 1024 よりも遥かに大きくなる
    • オーバーヘッドがあるので。また、単にサンプルを保存する以外のこともしているので
    • どの程度のオーバーヘッドがあるかは使用方法による
    • 設定値よりも多くのチャンクをメモリに乗せることもある
      • 使用するすべてのチャンクをメモリに乗せる必要があるので
    • 少なくとも 3 倍ぐらいのメモリを使用する
    • 下記のメトリクスでどれぐらい使われているか見ることができる
      • prometheus_local_storage_memory_chunks
      • process_resident_memory_bytes
  • 大量の time series を含む PromQL クエリは LevelDB バックエンドインデックスを大量に使用する
    • その種のクエリを実行するならインデックスキャッシュサイズを調整する必要がある

Settings for high numbers of time series

  • 100,000 を超える time series を扱うならストレージ設定の調整が必要
  • 本質的には、各 time series の特定の数のチャンクをメモリの保持する必要がある
    • storage.local.memory-chunks のデフォルトは 1048576 です
    • 300,000 series までは、series ごとに平均 3 のチャンクがある
    • もっと多くの series を扱うなら storage.local.memory-chunks を増やすべき
    • とりあえず最初は series の 3 倍にしておくと良い
  • 設定されているメモリチャンクよりも多くの series がアクティブになった場合・・・
    • 設定値よりも多くのチャンクをメモリに読む必要があるのだが・・・
    • 設定値を 10% 以上上回ると、設定値が 5% 以下になるまでサンプルの取得を抑止する
      • スクラップやルールの評価をスキップすることで
    • これはとても良くないことです
  • spinning disk に書き込むときは storage.local.max-chunks-to-persist の値を上げる
    • とりあえず最初は storage.local.memory-chunks の 50% ぐらいにしておくと良い
    • storage.local.max-chunks-to-persist はディスクに書き込まれるのを待つチャンクの数
    • 待機チャンクがこの値を超えると設定値の 95% に下がるまでサンプルの取り込みを調整する
    • series が 1M なら storage.local.memory-chunks は 3M ぐらい必要
    • このうち 2M が永続化可能なので・・・
    • storage.local.max-chunks-to-persist を 2M 以上にすると・・・
    • storage.local.memory-chunks の設定にかかわらず・・・
    • メモリ内に 3M 以上のチャンクが簡単に生成される

Helpful metrics

  • prometheus_local_storage_max_memory_chunks
    • storage.local.memory-chunks の設定値
  • prometheus_local_storage_memory_series
    • メモリに保持されているシリーズ数
  • prometheus_local_storage_memory_chunks
    • メモリに保持されているチャンク数
  • prometheus_local_storage_chunks_to_persist
    • ディスクに永続化する必要のあるメモリチャンク数
  • prometheus_local_storage_persistence_urgency_score
    • 緊急度スコア (0...1)
  • prometheus_local_storage_rushed_mode
    • 緊急モードのフラグ (0 or 1)

うーん? 下記のように設定しておけば OK かな。

  • storage.local.memory-chunks
    • 総メトリクス数の 3 倍
    • 総メトリクス数ってどうやって取れば良い?
    • prometheus_local_storage_memory_series で良い?
  • storage.local.max-chunks-to-persist
    • ↑の半分

Node exporter の調整

デフォだと dm-X みたいな LVM のディスクとか tmpfs とか nfs とかのファイルシステムとか vnat とかのインタフェースの情報まで取ってきてしまうので、除外パターンを調整する。

./node_exporter \
  -collector.diskstats.ignored-devices '^(dm-|[sv]d[a-z]|sr|drbd)\d+$' \
  -collector.filesystem.ignored-fs-types '^(sys|proc|root|rpc_pipe|tmp|n)fs$' \
  -collector.filesystem.ignored-mount-points '^/(sys|proc|dev|run)($|/)' \
  -collector.netdev.ignored-devices '^(vnet\d+|p5p\d+|br\d+|br\d+-nic|lo)$'

設定後は下記のようにコマンドでサッと確認する。

curl -s http://127.0.0.1:9100/metrics | grep node_filesystem_avail
curl -s http://127.0.0.1:9100/metrics | grep node_disk_bytes_read
curl -s http://127.0.0.1:9100/metrics | grep node_network_receive_bytes

PromQL でも下記のように確認する。

count(node_disk_bytes_read) without (instance)
count(node_filesystem_avail) without (instance)
count(node_network_receive_bytes) without (instance)

メモリ使用量

メモリ使用量は Node exporter の node_memory_* なメトリクスで詳細に取れるけど、下記でもリアルなメモリ使用量が取れる。

process_resident_memory_bytes
process_virtual_memory_bytes

この値は Prometheus でも Node exporter でも取れるっぽい(Prometheus 自身に Node exporter をセットアップしなくてもメモリ使用量は取れる、という意味)。

予測監視

あと何秒でディスクが枯渇するか、のような予測監視を deriv とか使ってできる。

(
  node_filesystem_size{instance='sv01'} - node_filesystem_free{instance='sv01'}
) / deriv(node_filesystem_free{instance='sv01'}[3d])

irate と rate

irate のが細かい変化が見れる、大まかな変化が知りたい時やアラートで FOR しているときは rate のが良い。

rate[5m] とかの範囲の最初と最後が、irate は範囲の最後の2点、irate での [5m] とかの範囲は「最後の2点」をどこまで遡るかの指定(無限に遡るわけにはいかないので)。

つまり、rate[5m] は 5 分間の平均を意味するので範囲の広さによって明らかにグラフが変わるが、irate[5m] はこの範囲の最後の 2 点という意味なので、範囲を広くしてもグラフは変わらない。ただし、短くしすぎて範囲内に観測点が 1 つしかなくなるとグラフが表示できなくなるので、それなりの広さにしておく必要がある。

アラートで FOR しているときは、閾値を超えた時間がそれだけ続いたらアラートにする、という意味になる。irate だとグラフが激しく振れるので、FOR が設定されているととアラートされにくくなる。

memory_chunks の監視

下記のように max_memory_chunks に対する memory_chunks の割合を監視したりしてみたけど、 たぶんあるだけ使うだろうのであんまりが意味ない(全データがメモリに乗せる前提とかじゃない限り)。

prometheus_local_storage_memory_chunks / prometheus_local_storage_max_memory_chunks * 100 > 70

chunks_to_persist とか、

prometheus_local_storage_chunks_to_persist / prometheus_local_storage_max_chunks_to_persist * 100 > 70

persistence_urgency_score とかで監視するのが良いだろう。

prometheus_local_storage_persistence_urgency_score * 100 > 60

InfluxDB にデータを渡す

Prometheus で remote_write を使えば取得したメトリクスを HTTP で外に投げることができる。この機能を用いて InfluxDB などの他の時系列データベースへデータを送ることができる。

InfluxDB を起動します。

docker run -p 8086:8086 -p 8083:8083 -e INFLUXDB_ADMIN_ENABLED=true \
  -v ~/data/influxdb:/var/lib/influxdb:rw influxdb

http://localhost:8083/ で管理画面を開いて、データベースとユーザーを作成します。

DROP DATABASE prometheus;
CREATE DATABASE prometheus;
CREATE USER prometheus WITH PASSWORD 'password';
GRANT ALL ON prometheus TO prometheus;

長期保存のためにデータを間引くための2つの RP を作成します。

CREATE RETENTION POLICY "tmp" ON prometheus DURATION 1d REPLICATION 1;
CREATE RETENTION POLICY "monthly" ON prometheus DURATION 30d REPLICATION 1 DEFAULT;

さらに Continuous Query を作成します。

CREATE CONTINUOUS QUERY monthly ON prometheus
BEGIN
  SELECT mean(value) as value
  INTO prometheus."monthly".:MEASUREMENT
  FROM prometheus."tmp"./.*/
  GROUP BY time(1h), job, virt, instance
END

Prometheus と InfluxDB を繋ぐためのコマンドをインストールします。

docker run -v /usr/local/bin:/go/bin:rw golang \
  go get github.com/prometheus/prometheus/documentation/examples/remote_storage/remote_storage_bridge

起動します。

INFLUXDB_PW=password remote_storage_bridge \
  -influxdb-url http://influxdb:8086 \
  -influxdb.database prometheus \
  -influxdb.username prometheus \
  -influxdb.retention-policy tmp

Prometheus で下記のように設定します。

remote_write:
  url: "http://remote_storage_bridge:9201/receive"
  write_relabel_configs:
    - source_labels: [__name__]
      regex: node_load.*
      action: keep

InfluxDB でクエリを実行すると書き込まれていることがわかります。

SHOW RETENTION POLICIES;
SHOW CONTINUOUS QUERIES
SHOW MEASUREMENTS;
SELECT * FROM tmp.node_load1;
SELECT * FROM monthly.node_load1;
SELECT * FROM node_load1;

"tmp" には Prometheus で取得したのと同じ間隔で記録されています。一方、"monthly" には Continuous Query により1時間毎の平均になった値が記録されます。

Grafana で InfluxDB のデータを見る

次のようなクエリで InfluxDB のデータを取得できます。

SELECT mean(value) FROM tmp.node_load1 WHERE time > now() - 1h GROUP BY time(5m), instance

Grafana に設定するときは time > now() - 1h$timeFilter に、5m$interval に置き換えます。

SELECT mean(value) FROM tmp.node_load1 WHERE $timeFilter GROUP BY time($interval), instance fill(null)

SELECT mean(value) FROM tmp./^node_load/ WHERE time > now() - 1h GROUP BY time(5m) fill(null)

他の Prometheus からデータを取ってくる(Federation)

他の Prometheus サーバからデータを取ってきたりできる。

データを取る元の prometheus.yml で下記のように設定するとできる。データを取る先(データ元)で特に設定は必要ないっぽい。

scrape_configs:
  - job_name: federate
    scrape_interval: 2m
    honor_labels: true
    metrics_path: /federate
    params:
      match[]:
        - instance_mode:cpu:rate
    static_configs:
      - targets:
        - other-prometheus:9090

保持期間やスクレイプの時間を変更した Prometheus を別に設けてそいつに Federate することでダウンサイジングしたりできる。