はじめに
SREグループの古越です。
MySQLサーバーのメトリクスを取得できるツールとしてMySQL Server Exporter (mysqld_exporter)を使ってみましたので紹介します。
PostgreSQLの可観測性向上としてpostgres_exporterについて解説した記事を以前書いていましたが、MySQL Server Exporterも同様の使い方が可能です。
こちらを使ってAurora MySQLの標準機能より詳細な情報を可視化出来たため、使い方や具体的な利用事例を解説します。
MySQL Server Exporterとは
MySQLサーバー向けのPrometheus Exporterです。Prometheus Exporterの概要については、postgres_exporterの記事の中で解説していたので、ここでは割愛します。
Aurora MySQLの場合は死活監視やパフォーマンス情報はCloudWatchなどクラウド側で収集される監視データで十分な場合がほとんどですが、場合によってはMySQLサーバーの統計情報を深堀りして活用したいケースがあります。
例えば、以下のような場合です。
- テーブルごとのレコード数、データ量を把握したい
- テーブルロック、行ロックの発生頻度や待ち時間を知りたい
- スロークエリの発生件数を観測したい
こうした場合には、MySQL Server Exporterで収集出来るメトリクスを利用することで解決できる可能性があります。
収集出来るメトリクス
Postgres Exporter同様にMySQL Server Exporterでもコレクターという単位で収集するメトリクスの種類を指定出来るようになっています。 メトリクスの種類は多岐にわたり、死活監視やレプリケーション状態を確認出来るものが代表的です。
Aurora MySQLのようなマネージドDBの場合はクラウド側で基本的なメトリクスやパラメータが収集されて見れるようになっているため、以下のようなメトリクスを追加で収集すると有意義です。
- MySQLサーバーステータス変数
- information_schema の一部テーブル
- performance_schema の一部テーブル
どういったコレクターが有るかはmysqld_exporterリポジトリのREADMEにあるコレクターフラグを確認してみてください。
構成例
基本的な使い方はPostgres Exporterと同様で、以下の2つの構成が考えられます。
- Kubernetes上のMySQLサーバーにサイドカーとしてExporterを起動する
- 外部のMySQLサーバーを監視する専用のExporterコンテナを立ち上げる
今回はAurora MySQLを対象としてるため2の構成を前提として解説していきます。
MySQL Server Exporterの試用
実際に動かしてどのようなメトリクスが取得出来るか確かめてみましょう。
dockerでlocal MySQLサーバとMySQL Server Exporterを起動する例を紹介します。
ローカル試験環境のイメージ図
docker comopseでこのような構成を作り、curlでメトリクスを取得してみましょう。
手順
docker-compose.yaml を以下のように記載します。
version: '3' services: # ---------- # MySQL # ---------- mysqld: image: mysql:8.0 platform: linux/x86_64 environment: MYSQL_ROOT_PASSWORD: P@ssw0rd MYSQL_DATABASE: main MYSQL_USER: exporter MYSQL_PASSWORD: P@ssw0rd ports: - 3306:3306 volumes: - ./init.sql:/docker-entrypoint-initdb.d/init.sql # ---------- # MySQL Server Expoter # ---------- mysqld_exporter: image: prom/mysqld-exporter command: - --config.my-cnf=/etc/mysql/my.cnf - --mysqld.address=mysqld:3306 - --no-collect.global_variables - --no-collect.slave_status - --no-collect.info_schema.innodb_cmpmem - --collect.info_schema.innodb_metrics - --collect.info_schema.processlist - --collect.info_schema.tables - --collect.perf_schema.eventsstatementssum - --collect.perf_schema.eventswaits - --collect.perf_schema.tablelocks - --collect.perf_schema.tableiowaits - --collect.info_schema.tables.databases=main depends_on: - mysqld ports: - 9104:9104 volumes: - ./my.cnf:/etc/mysql/my.cnf
詳細は割愛しますが、コマンド引数のオプションで無効/有効化したいコレクター設定を記載します。
この例では slave_status
や global_variables
を --no-collect
オプションで無効化しています。これはAurora MySQLの場合CloudWatchやパラメータグループを確認すれば良い値なので収集不要です。
また、パフォーマンス測定に有用なinformation_schemaやperformance_schemaの一部を取得するコレクターを有効化しています。
次にexporterの権限設定を init.sql
に記入します。
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'%';
MySQL Imageの環境変数 MYSQL_USER
で作成されるユーザはデフォルト設定でperformacne_schemaなどのテーブル参照権限が無いため、追加の権限設定が必要です。
MySQLコンテナでは /docker-entrypoint-initdb.d
の中にSQLを放り込んでおく事でコンテナ起動後の初期化が可能になっています。init.sqlをボリュームマウントして /docker-entrypoint-initdb.d
配下に押し込みましょう。
最後に my.cnf
にexporterの接続情報を記載します。
[client] user=exporter password=P@ssw0rd
MySQLの接続情報は my.cnf
に記入する方法がシークレット管理としても安全です。
上記はdockerでの例ですが、Kubernetes上で構成する場合は my.cnf
をSecretとして定義しPodにボリュームマウントすると安全に管理出来ます。
上述の3ファイル
- docker-compose.yml
- init.sql
- my.cnf
を配置したらdocker composeを起動します。
docker compose up
docker composeを起動したら、curlを叩いてメトリクスを確認してみましょう
curl -s http://localhost:9104/metrics
およそ2000行程度のテキストが出力される筈です。
以下のような mysql_
を含むメトリクスが多数含まれてれば正常に収集出来ています。
# HELP mysql_global_status_aborted_clients Generic metric from SHOW GLOBAL STATUS. # TYPE mysql_global_status_aborted_clients untyped mysql_global_status_aborted_clients 0 # HELP mysql_global_status_aborted_connects Generic metric from SHOW GLOBAL STATUS. # TYPE mysql_global_status_aborted_connects untyped mysql_global_status_aborted_connects 0 ...
MySQL Server Expoterでどういったメトリクスが取得出来るのかドキュメント化されてないため、ソースコードを読むか実際の出力を見るかの二択になります。 そのため、検証や微調整する際にはdockerで実際に動かして確かめるのが早いかと思います。
基本的にはMySQLサーバー上の値を収集しているため、グラフを作る場合はMySQLドキュメントを読みながらどのメトリクスを使うか探っていくことになるかと思います。
MySQL Server Expoterのデプロイ, Aurora向け設定
MySQL Server Expoter をKubernetes上でデプロイし、Aurora MySQLをモニタリングする設定手順について説明します。
Kubernetes上に設定する例として、SecretとDeploymentを使った設定例を紹介します。
構成イメージ図
本番環境で利用するAurora MySQLクラスターを想定し、1クラスタに2インスタンスを作っている構成を例にします。
構成や制約次第ですがMySQL Server Expoterは1プロセスで1サーバーを監視する前提で作られているため、基本的に Pod:DBインスタンス = 1:1
で設定するのが良いかと思います。
一応マルチターゲットがサポートされていますが、マルチターゲットの場合は監視対象を増やす度にPrometheusサーバーの設定変更が必要になります。
ターゲットが増える分MySQL Server Exporterの負荷も高まり、メトリクスが欠けてしまう事もありえます。DBインスタンス毎に1Podとする方がトータルで運用の手間は少ないと思っています。
設定手順
以下のようなAurora Clusterがある前提で説明します。
項目 | 値 |
---|---|
Aurora Cluster | myapp-dev |
Aurora DB Instance | myapp-dev-0, myapp-dev-1 |
Database | main |
1. DB用ユーザの作成
Aurora MySQLにて exporter というUserを作成します。
CREATE USER 'exporter'@'%' IDENTIFIED BY 'P@ssw0rd' WITH MAX_USER_CONNECTIONS 3; GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'%';
2. Secret, Deployment作成
Secret
パスワードを含む接続情報はmy.cnfに記載する形が好ましいです。以下Secretを設定してPod起動時に読み込むように設定します。
apiVersion: v1 kind: Secret metadata: name: mysqld-exporter-mycnf-myapp-dev labels: app.kubernetes.io/name: mysqld-exporter app: mysqld-exporter db-app: myapp db-cluster: myapp-dev type: Opaque stringData: my.cnf: |- [client] user=exporter password=P@ssw0rd
Deployment
mysqld_exporter Podを展開するDeploymentは以下のように設定します。
apiVersion: apps/v1 kind: Deployment metadata: name: mysqld-exporter-myapp-dev-0 labels: app.kubernetes.io/name: mysqld-exporter app: mysqld-exporter db-app: myapp db-cluster: myapp-dev db-instance: myapp-dev-0 spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: mysqld-exporter app: mysqld-exporter db-app: myapp db-cluster: myapp-dev db-instance: myapp-dev-0 template: metadata: annotations: prometheus.io/path: /metrics prometheus.io/port: "9104" prometheus.io/scrape: "true" labels: app.kubernetes.io/name: mysqld-exporter app: mysqld-exporter db-app: myapp db-cluster: myapp-dev db-instance: myapp-dev-0 spec: volumes: - name: mycnf secret: secretName: mysqld-exporter-mycnf-myapp-dev containers: - name: mysqld-exporter image: prom/mysqld-exporter:v0.15.1 imagePullPolicy: IfNotPresent ports: - containerPort: 9104 name: http protocol: TCP volumeMounts: - name: mycnf mountPath: /etc/mysql readOnly: true args: - --config.my-cnf=/etc/mysql/my.cnf - --mysqld.address=myapp-dev-0.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com:3306 - --no-collect.global_variables - --no-collect.slave_status - --no-collect.info_schema.innodb_cmp - --no-collect.info_schema.innodb_cmpmem - --collect.info_schema.innodb_metrics - --collect.info_schema.processlist - --collect.info_schema.tables - --collect.perf_schema.eventsstatementssum - --collect.perf_schema.eventswaits - --collect.perf_schema.tablelocks - --collect.perf_schema.tableiowaits livenessProbe: httpGet: path: / port: http readinessProbe: httpGet: path: / port: http resources: limits: memory: 32Mi requests: cpu: 10m memory: 32Mi
ポイント - annotationsにPrometheusサービスディスカバリ設定を記述 - Prometheusサーバ側でリラベル処理するため、Podのラベルにdb-cluster名やdb-instance名を書いておく
複数個のインスタンスがある場合はインスタンス毎に同様のDeploymentを追加するのが良いかと思います。 仮にAuroraのレプリカを増やした場合はその分追加する必要があるため、最低限helm等を使って簡単に展開出来る形にしておく事をおすすめします。
3. prometheusのscrape_job設定
PromQLを書く時にRDS cluster名などでフィルタしやすいようにリラベル処理を追加します。 Prometheus Serverのscrape_configsに以下ジョブを定義します。
# mysqld-exporter - job_name: 'mysqld-exporter' scrape_interval: 1m kubernetes_sd_configs: - role: pod relabel_configs: # ----- only mysqld-exporter ----- - source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name] regex: mysqld-exporter action: keep # ----- cluster metadata ----- - source_labels: [__meta_kubernetes_pod_label_db_cluster] regex: (.+) action: replace target_label: db_cluster - source_labels: [__meta_kubernetes_pod_label_db_instance] regex: (.+) action: replace target_label: db_instance
メトリクス収集の仕込みは以上です。
PromQLとグラフ化の例
MySQL Server Exporterで取得したメトリクスをPromQLを使ってグラフ化する例を幾つか紹介します。
収集するメトリクスは基本的に元データが存在しますので、そちらも含め紹介します。
テーブル毎のデータ量
- metrics:
mysql_info_schema_table_size
- source: information_schema.tables
グラフ例
PromQL
avg by (table) ( mysql_info_schema_table_size{ job="mysqld-exporter", db_app=~"myapp", db_cluster=~"myapp-dev", schema=~"main" } ) > 0
mysql_info_schema_table_size
はテーブル単位で実際に使用されたディスク領域のサイズを表します。
円グラフで表示するとテーブル毎の重みが分かってくるかと思います。
テーブル毎の概算レコード数
- metrics:
mysql_info_schema_table_rows
- source: information_schema.tables
グラフ例
PromQL例
avg by (table) ( mysql_info_schema_table_rows{ job="mysqld-exporter", db_app=~"myapp", db_cluster=~"myapp-dev", schema=~"main" } ) > 0
こちらはテーブルごとの概算行数を示します。information_schemaに保持されている時点で正確な行数ではありませんが、 select count(*)
を叩かなくてもレコード数の概算値を見れるようになるため、何らかの作業前の確認など活用範囲は広いかと思います。
行更新イベント数
- metrics:
mysql_global_status_innodb_row_ops_total
- source: サーバーステータス変数/
Innodb_rows_*
グラフ例
PromQL
avg by (operation) ( idelta( mysql_global_status_innodb_row_ops_total{ job="mysqld-exporter", db_app=~"myapp", db_cluster=~"myapp-dev" }[$__range] ) )
DBインスタンス上でどのような更新イベントが発生してるか大まかに判断出来るグラフになっていると思います。
mysql_global_status_innodb_row_ops_total
は以下4つのサーバーステータス変数が集約されており、operation
ラベルで分類されています。
PromQLで avg by (operation)
のようにグループ化すれば更新イベント毎に分割表示が可能です。
Slow Query発生数
- metrics:
mysql_global_status_slow_queries
- source: サーバーステータス変数/
Slow_queries
グラフ例
PromQL例
avg by (db_instance) ( idelta( mysql_global_status_slow_queries{ job="mysqld-exporter", db_app=~"myapp", db_cluster=~"myapp-dev" }[$__range] ) )
Slow Queryの説明は割愛しますが、Slow Query発生数はSRE観点では重要です。 例はSlow Queryが発生してないため、グラフとしては面白みに欠けますが理想的な状態です。 こちらのグラフを他のグラフと同時に表示し、時系列で見ていくと障害発生時に内情が見えやすくなるかと思います。
Slow Queryが度々発生する場合は何らかの改善が必要なケースが考えられますので、こちらを元にアラート化するのも運用上好ましいかと思います。 実際にGO株式会社のプロダクトの幾つかではこのメトリクスからSlow Queryアラートを設定しています。
テーブルロックの発生数
- metrics:
mysql_global_status_table_locks_immediate
mysql_global_status_table_locks_waited
- source: サーバーステータス変数
Table_locks_immediate
Table_locks_waited
テーブルロックに関して mysql_global_status_table_locks_immediate
, mysql_global_status_table_locks_waited
の2つのメトリクスが活用出来ます
ここでは複数のメトリクスを1つのグラフに集約した例を紹介します。
グラフ例
PromQL A
avg by (db_instance) ( idelta( mysql_global_status_table_locks_immediate{ job="mysqld-exporter", db_app=~"myapp", db_cluster=~"myapp-dev" }[$__range] ) )
PromQL B
avg by (db_instance) ( idelta( mysql_global_status_table_locks_waited{ job="mysqld-exporter", db_app=~"myapp", db_cluster=~"myapp-dev" }[$__range] ) )
immediate
はテーブルロックのリクエストが即座に付与された回数で
waited
はテーブルロックのリクエストが即座に付与されず、待機が必要だった回数です。
immediateが発生するのは特に異常ありません。注意が必要なのはwaitedです。
waitedが増えてクエリパフォーマンスが悪化している場合、クエリ最適化など対策が必要かもしれません。waitedについては状況次第でアラート化を検討して良いかと思います。
行ロック待ちの発生数
- metrics:
mysql_global_status_innodb_row_lock_waits
- source: サーバーステータス変数/
Innodb_row_lock_waits
グラフ例
PromQL
avg by (db_instance) ( idelta( mysql_global_status_innodb_row_lock_waits{ job="mysqld-exporter", db_app=~"myapp", db_cluster=~"myapp-dev" }[$__range] ) )
Innodb_row_lock_waits
は何等かの操作により行ロックを待機した回数を示すカウンターです。差分を見ると興味深いグラフが出てくるかと思います。
例では昼頃に行ロック待ちが瞬間的に上昇していたようですが、一瞬で収まっていますね。行ロック待ちが瞬間的に増える事は特に問題ありませんが、想定外に大量発生していたり徐々に増加しているような場合は注意が必要と思います。
行ロック取得の所要時間平均
- metrics:
mysql_global_status_innodb_row_lock_time_avg
- source: サーバーステータス変数/
Innodb_row_lock_time_avg
グラフ例
PromQL
avg by (db_instance) ( idelta( mysql_global_status_innodb_row_lock_time_avg{ job="mysqld-exporter", db_app=~"myapp", db_cluster=~"myapp-dev" }[$__range] ) )
Innodb_row_lock_time_avg
は行ロックの取得に要した平均時間が記録されています。
1つ前のロック待ち回数が増加した昼頃のタイミングでも行ロック取得までの平均時間には変化が無い事から、単にロック待ちが多かっただけでクエリパフォーマンスに影響ない事象だったと判断出来ますね。
備考: サーバーステータス変数の活用
上記した例に限らず mysql_global_status_*
で表現されるサーバーステータス変数はMySQL上で単調増加する値であるため、Prometheus上でもCounter形式として保存される形になっています。
このようなメトリクスはidelta関数を使ってGauge形式に変換することで上記のような活用が可能になります。他のサーバーステータス変数も同様に活用出来るかもしれません。
INDEXチューニング余地があるクエリ数
- metrics:
mysql_perf_schema_events_statements_sum_no_index_used
mysql_perf_schema_events_statements_sum_no_good_index_used
- source: performance_schema.events_statements_summary_global_by_event_name
SUM_NO_INDEX_USED
SUM_NO_GOOD_INDEX_USED
グラフ例
PromQL
avg by (db_instance) ( idelta( mysql_perf_schema_events_statements_sum_no_good_index_used{ job="mysqld-exporter", db_app=~"myapp", db_cluster=~"myapp-dev", }[$__range] ) )
performance_schemaのevents_statements_summary_global_by_event_nameテーブルから参照するようにコレクターを指定しているため、こちらを活用した少し詳細なグラフを作成することも可能です。 こちらのようにINDEXを使っていない、最適ではないINDEXを使っているクエリ数をグラフ化出来ます。
どのクエリが問題か特定は出来ないため別途調査が必要ですが、パフォーマンス改善を始める切り口になると思います。
注意点ですが、Aurora MySQLの場合はRDSの管理目的で内部的にINDEXを使わないクエリが1分間に60~120程度実行されているようです。クエリを発行していないAurora MySQL DBインスタンスで SUM_NO_INDEX_USED
のカウンターが動いている挙動は確認しました。
一定値は発生する前提でアプリケーションが発行するクエリ数と相関が有るか確認すると良さそうです。Aurora MySQLのコンソールから見れる Queries
等と突き合わせることで相関関係を判断出来ると思います。
まとめ
MySQL Server Exporterを使ってAurora MySQLの可観測性を高める方法を紹介しました。 RDSとして提供されているメトリクス以外のテーブル単位やSlow Queryなど詳細を可視化する事が出来ます。
デメリットとしてはReaderのスケールアウトに弱いという点ですが、WriterとReaderを1台ずつサンプリングするだけでも十分かと思います。 例えばFail Over Tierを高く設定した固定インスタンスを2台用意し、2台だけ監視する形でも一定の効果はあるかと思います。
利用上の注意点としては多数のコレクターを有効化するとMySQL Serverの負荷を高める事に繋がります。 本番環境ではコレクターを最低限に指定し、導入後に悪影響ないか注意深く観察しながら進めるとよいかと思います。
他にも活用出来るメトリクスがあればまた紹介したいと思います。