MySQL Server Exporterを使ってAurora MySQLの可観測性向上

はじめに

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の場合はクラウド側で基本的なメトリクスやパラメータが収集されて見れるようになっているため、以下のようなメトリクスを追加で収集すると有意義です。

どういったコレクターが有るかはmysqld_exporterリポジトリのREADMEにあるコレクターフラグを確認してみてください。

構成例

基本的な使い方はPostgres Exporterと同様で、以下の2つの構成が考えられます。

  1. Kubernetes上のMySQLサーバーにサイドカーとしてExporterを起動する
  2. 外部の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_statusglobal_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を使ってグラフ化する例を幾つか紹介します。
収集するメトリクスは基本的に元データが存在しますので、そちらも含め紹介します。

テーブル毎のデータ量

グラフ例

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 はテーブル単位で実際に使用されたディスク領域のサイズを表します。
円グラフで表示するとテーブル毎の重みが分かってくるかと思います。

テーブル毎の概算レコード数

グラフ例

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(*) を叩かなくてもレコード数の概算値を見れるようになるため、何らかの作業前の確認など活用範囲は広いかと思います。

行更新イベント数

グラフ例

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発生数

グラフ例

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については状況次第でアラート化を検討して良いかと思います。

行ロック待ちの発生数

グラフ例

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 は何等かの操作により行ロックを待機した回数を示すカウンターです。差分を見ると興味深いグラフが出てくるかと思います。 例では昼頃に行ロック待ちが瞬間的に上昇していたようですが、一瞬で収まっていますね。行ロック待ちが瞬間的に増える事は特に問題ありませんが、想定外に大量発生していたり徐々に増加しているような場合は注意が必要と思います。

行ロック取得の所要時間平均

グラフ例

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チューニング余地があるクエリ数

グラフ例

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の負荷を高める事に繋がります。 本番環境ではコレクターを最低限に指定し、導入後に悪影響ないか注意深く観察しながら進めるとよいかと思います。

他にも活用出来るメトリクスがあればまた紹介したいと思います。