mita2 database life

主にMySQLに関するメモです

TiDB metadata lock で DDL が待たされた時の調査方法

前提

本エントリーは、tidb_enable_metadata_lock が ON であることを前提としています。

mysql> SELECT @@transaction_isolation, @@tidb_enable_metadata_lock, @@version;
+-------------------------+-----------------------------+--------------------+
| @@transaction_isolation | @@tidb_enable_metadata_lock | @@version          |
+-------------------------+-----------------------------+--------------------+
| REPEATABLE-READ         |                           1 | 8.0.11-TiDB-v8.1.2 |
+-------------------------+-----------------------------+--------------------+
1 row in set (0.00 sec)

DDLをブロックしているトランザクションの調査方法

TiDB にも MySQL 同様に metadata lock があり、DDLがクエリによってブロックされることがあります。

mysql.tidb_mdl_view でブロックしているトランザクションを確認できます。 metadata lock が競合せず、ブロックが発生していない場合は、何も表示されません。

tidb_mdl_view は、実行済みのクエリも含めて、ブロックしているトランザクションに含まれる全てのクエリを SQL_DIGESTS に出力してくれます。これは非常に便利ですね。 start_time も、直近のクエリの開始時間ではなく、トランザクションの開始時間です。

mysql>  SELECT * FROM mysql.tidb_mdl_view;
+--------+---------+------------+--------------------------------+------------+----------------------------+-------------------------------------------+
| job_id | db_name | table_name | query                          | session_id | start_time                 | SQL_DIGESTS                               |
+--------+---------+------------+--------------------------------+------------+----------------------------+-------------------------------------------+
|    108 | t       | t          | ALTER TABLE t ADD INDEX idx(a) | 3212836870 | 2025-09-11 21:01:30.919000 | ["begin","select `a` , now ( ) from `t`"] |
+--------+---------+------------+--------------------------------+------------+----------------------------+-------------------------------------------+
1 row in set (0.00 sec)

さて、ここで、追加のトランザクションを実行します。 なお、MySQL では MDL 待ち状態で、追加のトランザクションを実行すると、そのトランザクションは待たされますが、TiDB では DDL を追い越して、トランザクションを実行することができます(その代わり、後続のトランザクションが先行するDDLをブロックします)。

mita2db.hateblo.jp

mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT a, NOW() AS 2ndtry FROM t;
+------+---------------------+
| a    | 2ndtry              |
+------+---------------------+
|    1 | 2025-09-11 21:04:27 |
+------+---------------------+
1 row in set (0.00 sec)

tidb_mdl_view にも行が追加されることがわかりました。

mysql> mysql>  SELECT * FROM mysql.tidb_mdl_view;
+--------+---------+------------+--------------------------------+------------+----------------------------+-----------------------------------------------+
| job_id | db_name | table_name | query                          | session_id | start_time                 | SQL_DIGESTS                                   |
+--------+---------+------------+--------------------------------+------------+----------------------------+-----------------------------------------------+
|    108 | t       | t          | ALTER TABLE t ADD INDEX idx(a) | 3212836870 | 2025-09-11 21:01:30.919000 | ["begin","select `a` , now ( ) from `t`"]     |
|    108 | t       | t          | ALTER TABLE t ADD INDEX idx(a) | 3212836876 | 2025-09-11 21:04:26.073000 | ["select `a` , now ( ) as `2ndtry` from `t`"] |
+--------+---------+------------+--------------------------------+------------+----------------------------+-----------------------------------------------+
2 rows in set (0.00 sec)

トランザクションをコミット/ロールバックすると、tidb_mdl_view から消えます(そしてDDLの実行が開始されます)。

db tech showcase 2025 (1日目) に参加しました

毎年恒例の db tech showcase に今年も参加してきました。 DB界隈のみなさんと情報交換できて非常に有意義でした!

www.db-tech-showcase.com

以下、参加したセッションのメモ。書き間違いとかニュアンスの違いあったらごめんなさい。

1. Spanner 内部シャーディング 徹底解剖 2025

Spanner の最新情報

  • Spanner はRelational モデルだけでなく複数のデータモデルに対応してきた

  • 全文検索もできるようになりました!

    • 従来のRDBにおける全文検索の課題

      • リソース食いがち
      • トークナイザーの品質不足
    • Spannerでは Google の優秀なトークナイザが使える

      • とうきょうと→東京都 がヒット
      • ぐーぐる→Google,グーグル がヒット

分散DBにとってシャードとは

  • 分散DBはテーブルを分割し、複製する

    • 分割 → 性能向上
    • 複製 → 可用性と耐障害性のため
  • シャーディング(分割)と複製(レプリカ)はあらゆる場面で使われきた

  • 分散DB と従来の「シャーディング+レプリカ」の違い

  • Tablet, Shard, Partition, Region, Range, Chunk など内部的なシャードの名称はいろいろ

    • Spanner では Split と呼ぶ

Spanner の Split の基礎

  • Split ごとに同期レプリケーション(Paxos ベース)をしている

    • Leader は異なるゾーン(AZ) に分散配置され、耐障害性が確保されている
  • Spanner の「サーバ」とは

    • Spannerを1ノード増やすと3サーバ追加される
    • サーバはVMではなく軽量なコンテナ
    • Splitはサーバに割り当てられる、1つのサーバは1つ以上の Split の面倒を見る
  • Split はサーバ間を移動可能

    • 負荷の高いサーバから低いサーバへ割り当てが変更される
    • SplitはColossus(共有ストレージ)に置かれる、サーバに直接保存されるわけではない
    • 実際にデータが移動するわけではない

Split とは何か?

  • テーブルのデータは Primary Key の辞書順で並んでいる

    • Split は テーブルの特定のレンジを切り出したもの
    • Split のレンジは均一ではない
  • 分割する境界を Split Point と呼ぶ

  • スプリットはテーブル単位でできる?

    • No
    • DB内の全てのテーブルを結合したものを分割する
    • テーブルとテーブルをまたがった Split が出来ることもある
    • 作りたてのテーブルには分割点がないため、複数テーブルであってもSplit は 1つ

Split の変化の契機

  • SQLプロファイルの並列数から Split 数が推定できるウラワザがある

  • Split の 移動

    • Split とサーバの割り当ての変更
    • 軽めの処理
  • Split の 分割

    • いわゆるリシャード
    • 比較的 重い処理
    • ロックを一瞬取って、分割する
      • Tail latency が若干高くなる
      • 自動リトライでカバーされるため、エラーにはならない
    • 自動分割
      • 負荷ベース
    • 手動分割
      • 初期ローンチ時などあらかじめ負荷のため Split を細かく分割しておきたい場合に利用
  • Split 統計

    • 各 Split の負荷(スコア)が見れる画面
    • スコア50以上は分割される候補
    • 単一スプリット
  • 手動分割

    • 1ノードあたり、20個を目安に事前に分けておくと良い
  • Split の 結合

    • Split の有効期限 は 2日
    • 再結合可能になる期限
    • 必ず結合されるとは限らない
  • 手動の大量分割・移動には時間かかる

    • Spannerはゆるやかに Split を分割処理しようとする
    • ドキュメントでは 7日~12時間前までに実行開始しましょう と書かれている

感想

@takabow さんのセッション。いつもどおり、わかりやすく、スッと入ってくる説明でした。Spannerは非常に賢く Split を管理していて、もしもホットスポットが発生するような設計をしてしまっても、それなりの規模までなら耐えられてしまう印象を受けました。このあたりの肌感覚を得られたのは大きかったです。

2. Amazon Aurora DSQLの仕組みと特徴

事実上無制限のスケーラビリティ

  • コンピュート / コミット / ストレージ は個別に拡張できる
  • コンピュートの READ / WRITE の比率も動的に変更できる

  • DSQLのエンドポイントはVPC の外にある

    • VPC のNW制限をケアする必要なし
  • 認証はIAM認証のみ

インフラ管理の排除

  • エンドポイントしか見えない

アーキテクチャ

書き込み時の動作

  • COMMIT を打つと ADJUDICATOR へ情報が連携される

    • 競合の検知
    • 競合したらCOMMITがABORTする
  • ジャーナルに書き込んだら、クライアントにCOMMIT OKが返る

    • ストレージに書き込む前に、クライアントにOKが返る

読み取りの動作

  • クエリープロセッサーが直接ストレージを読む

    • ADJUDICATOR,ジャーナル,クロスバーは読み取りの際は利用されない
  • ジャーナル にしかない(ストレージにまだ書かれてない)データは抜けるのでは?

    • 対策がしてある
    • ストレージは今どこまでトランザクションが適用されているか知っている
    • 必要に応じてジャーナルから書き込まれるのをごく一瞬待ってから、応答する

ビジネス継続性 - 可用性

  • アクティブ・アクティブのマルチリージョン構成も可能
  • シングルリージョン構成の場合も、マルチAZ構成で高い冗長性

  • バックアップ

    • AWSバックアップサービスでバックアップする
  • メジャーバージョンアップ

  • 方法は未定だが、ダウンタイムは発生しない
  • メジャーアップのタイミングはコントロールできる

分散データベースとしての性能最適化やトランザクションの特徴

  • トランザクション開始〜コミット直前までは、リージョン内/AZ内の通信で完結する

    • COMMITされたら、リージョン間で通信して整合性をとる
  • 読み取りはローカルコピーから読み込まれる、リージョンやAZをまたがない

  • トランザクション分離レベル Snapshot Isolation (PostgreSQLのRepeatable Read相当) をサポート

    • たとえ、Read Commited にしたとしてもDSQLにおいては恩恵はない

料金

  • DPU (Distributed Processng Unit)
    • 机上での見積もりが難しい...
    • 1行70byteの単表に対してワークロードを流した料金:
      • 5000 PK SELECT/sec, 50 q/sec * {INSERT,UPDATE,DELETE}
      • 24h で $26 @ 東京リージョン
  • Storage (GB-month)
  • Data transfer / Backup and Restore

トランザクション管理

  • OCC (楽観的同時実行制御)

    • UPDATEやINSERTでロック待ちは発生しない一方
    • COMMIT 時点で ABORT が発生する
  • カタログキャッシュ

感想

新久保さんのセッション。初めてDSQLの詳しい話をききました。アーキテクチャの詳細が知れて非常に参考になりました。 アーキテクチャの違いが既存の分散DBと比較したときに、どのような差として現れてくるのか、今後、より調査してみたいところです。 本題からはずれるんですが、「DSQLはIAM認証のみをサポート」というのを聞いて、今後、IAM認証を標準的に利用していく必要性を感じました。

3. 河合塾が抱えていたDB運用におけるジレンマの改善 〜DBの可視化で何が変わったのか?〜

データベースの運用課題

  • 統合DBに負荷が集中

    • 受付、塾生ポータル、校舎業務系...
  • Oracle RAC 2台構成

  • 2022/04月から慢性的にCPU負荷が高い状態に...

  • 負荷対応に工数・時間がかかってしまう

    • AWRレポートの発行だったり、ログ取りといった、煩雑な手動作業

課題に対するアプローチの検討

  • 現状課題:トラブルシュートの手間を減らしたい

    • ありたい姿: ログの収集や保全を自動的に行える
  • 現状課題: 一部ログやメモリ上のデータなどは情報が一定期間で消失してしまう。過去の運用情報が把握できない。

    • ありたい姿: 長期間データを保全できる仕組み
  • 現状課題:時間解像度が低い

    • ありたい姿:高頻度に情報を収集したい

比較検討

  • Oracle Diag Pack、 Oracle Stats Pack
    • 可視化のIFがない
  • Database Performance Analyzer

    • SQLで取れる情報しか扱えない
  • (採用) MaxGauge for Oracle

    • 時間解像度が高い(秒単位)
    • メモリから直接情報を収集 (Direct Memory Access)
    • 買い切りライセンスモデル

MaxGaugeを導入して解決した課題

  • 稼働状態が時系列で見える化できた

    • 前回との比較、先月との比較、といった傾向分析もできるようになった
  • 負荷をかけているSQL特定し、システム開発ベンダーに改善を依頼しやすくなった

  • データベースチーム以外にもダッシュボードを提供できるようになった (民主化)

感想

わかりやすい説明で、可視化の重要性を再認識できました。 メモリから直接情報を収集できるのはすごい (Oracleソースコードが公開されてないのに、どうやって実現しているんでしょう!?)。 プロセスがハングアップしてても情報収集できるとか。

4. Aurora Audit Log 深掘り実践 〜Insight SQL Testing を通じた運用改善への道〜

Aurora 3.0 バージョンアップ対応

  • 150クラスターのv2→v3のバージョンアップを完了

  • バージョンアップを支えた内製ツール

    • DB Catalog
      • Schema などの情報をためているレポジトリ
    • カスタムアップグレードチェッカー
      • mysqlsh の upgrade_checker をベースに自社独自の視点を追加
  • アップグレードチェッカーをプロダクトサイトに提供

    • DBREはあくまでもリアクティブに対応する方針で進めていた
  • プロダクトサイドからは専門家(DBRE)により踏み込んだチェックをしてほしという要求があった

    • クエリリプレイのソリューションを検討した

Insight SQL Testing

  • Audit Log をリプレイするソリューション

  • 移行元と移行先、両方にクエリを投げて比較してくれる

    • エラー検知
    • パフォーマンス劣化の検知
  • 3.3億行をリプレイした

    • 問題があると検知されたクエリ:486万クエリ
  • 減ったとはいえ 486万クエリもある

  • 対応が必要なクエリを絞り込む作業をした

    • クエリのノーマライズ、既知の問題のフィルター
    • 実行タイミングに依存するクエリ(現在時刻が関係する処理、ORDER BY未指定 など)の除外
    • パフォーマンス劣化は劣化度合いにより足切りした

Aurora Audit Log とは

  • Aurora Audit Log は MariaDB Audit Plugin ベース
  • メリット
    • Aurora 標準機能で機能利用に追加コストなし
    • AWS Console 、 IaC で管理できる
    • セキュリティ監査への対応
  • デメリット

    • クエリ全文が記録されることによる個人情報リスク
    • ログの保管コスト
  • 活用例

    • Retcode
      • 0 以外を検索してトラブルシュートに活用
    • Connection ID
      • その時間のセッション数を類推できる
      • 該当セッションの継続時間
  • Audit Log の出力順序は保証されない

    • QUERY, CONNECT, DISCONNECT みたいな矛盾した順番で出てくることもある
    • パースするときに注意が必要
  • Insight SQL Testing への連携方法

  • S3 へ 時間指定して export
  • SQL Testing に取り込める形式(CSV)に変換するツールが提供されている
  • ログの境目で CONNECT/DISCONNECT がぬけてるケースもちゃんとハンドリングされている

Audit Log を活用した内製ツール

  • Audit Log に向き合ってみて、普段の運用にも活用できそうだと思った。

  • Audit Log をパースして MySQL に入れるツールを作成

    • 分析ができるようになった
  • 利用例

    • 1本あたりのクエリのコストを見積もるのも出来そう
  • 正確な情報を把握する重要性

感想

@_awache さんのセッション。あわっちさんの人柄あふれる内容で、自分も「プロアクティブにプロダクト側を支援していかなかくては。」と感じさせる内容でした。

5. Aurora から Spanner へ Time Tree

移行の背景

  • データ量の継続的な増加
  • Aurora の様々な上限に達しそう

  • 利用状況

    • 65 Million User, 27 Billion Records
  • 将来を考えるとさまざまな上限が不十分

  • プロジェクトのタイムライン

    • 2018: 課題認識
    • 2019: チーム内で課題共有
    • 2021/06: クリティカルな課題して社内共有
    • 2023/01: プロジェクト稼働開始
    • 2024/01: 移行作業開始
    • 2025/01: 移行完了

Spanner に決めた理由

  • フルマネージド
  • Google での運用実績
  • オートスケーリング
  • 他の Google Cloud サービスとの連携
  • Google Workspace との連携
    • Google WorkspaceのIDでSpannerへのアクセス管理ができる

Pre マイグレーション

  • MySQL アップデート (Aurora v2 → v3)

    • 事前のスキーマ整理のためインスタントDDLを利用したい。
    • 照合順序の見直しなど
  • スキーマ整理

    • Spannerでインターリーブを利用予定だったので、親子関係を設定できるようにカラム追加
  • クレンジング

    • 移行に向けて不要なデータの整理

マイグレーション

  • spanner-migration-tool (SMT) を利用した

  • source データベースと dist データベースのスキーマ情報をマッピングしたファイルを作成

  • 自動的にデータ移行のパイプラインが作成される

    • DataStream -> Cloud Storage -> PubSub -> ComputeEngine -> Spanner

SMTのカスタムロジック

  • MySQLのAUTO_INCREMENTを利用していた

    • Spanner へ移行するにあたり、 AUTO_INCREMENT に Bit Reverse Seq を適用する必要があった
    • SMTで用意されているDataflowにカスタムロジックを入れた
  • しかし、SMTでは親子関係が考慮されずにエラーに

    • Google から改良バージョンが提供されて解決
  • SMTの新バージョンに不具合があり、実際は従来のバージョンで移行した

    • SMTの新バージョンに問題があった場合に備えていた
    • 従来のSMTを読み解いて、親子関係を考慮したものをバックアッププランとして事前準備

移行後

  • 運用コスト

    • ゼロになった
    • バージョンアップも自動で勝手にやってくれる
    • オートスケール
  • サービス開発コスト

    • 学習コストが増
  • 金銭的コスト

    • 若干増加したが、運用コストを考えるとトータルコストは削減できた
  • セキュリティ

    • Google ID との連携により改善
  • パフォーマンス

    • Spanner のシャーディングによるレイテンシ劣化は影響なかった
  • タイミング

    • サービスで様々な企画が進められる前に実行できてよかった
  • 社内文化への影響

    • SpannerはGraph や 全文検索など様々な検索ができる
    • Spannerの勉強会を実施し、結果的にエンジニアがより積極的にサービス改善に関わる文化が情勢されてきた

感想

Time Tree 金井 さんのセッション。 プロジェクトマネージメントやステイクホルダーとの目線合わせなど、技術的な部分以外の重要性も認識させてくれる内容でした。

LT

DBA/DBREしくじり先生 ~やっちゃったな実録集~

speakerdeck.com

@tomomo1015 さんの過去のやらかしの振り返りLT。1つ1つの事例をサクッと説明されてたんですが、実際はもっといろいろあったんだろうな... と感じるLTでした。どこかでもっと詳しく話してほしい(笑)

DBのスキルで生き残る技術 - AI時代におけるテーブル設計の勘所

@soudai1025 さんの説明はいつも、心に刺さるんですよね。ということで、ワールドトリガーを読み始めました(おい)

speakerdeck.com

いまさらMySQLの非同期レプリケーションでのHAの難しさについて考える

@yoku0825 さんの MySQLでしっかりとしたHAを組むことの難しさを訴えるLT。ただただ、共感しかない。

speakerdeck.com

Gemini Code Assist に プルリク上 で DDL をレビューさせる

Gemini Code Assist に DDL をレビューさせてみました。

レビューさせるDDL

CREATE TABLE users (id int, user_name TEXT, create datetime);
CREATE TABLE user_items (id int, user_id VARCHAR(10), itemName TEXT, status VARCHAR(250), created datetime);

以下のような問題点を意図的に仕込みました。はたして、Gemini Code Assist はうまく指摘してくれるのでしょうか?

  1. Primary Key が指定されていない
  2. user_nameNOT NULL の指定漏れ
  3. 予約語 create が使用されてしまっている
  4. usersiduser_itemsuser_id の型の不一致
  5. userscreateuser_itemscreated の表記揺れ
  6. 命名規則 が CamelCase と SnakeCase の混在
  7. 不必要に大きな型 (user_name, status)

Gemini Code Assist を Github にインストール

developers.google.com

何も指示を与えない状態

とりあえず、何も指示をあたえず、デフォルトの状態でレビューさせてみます。 PRを作ると自動的に Gemini Code Assist がレビューを開始し、コメントをつけてくれます。1分程度で、結果が返ってきました。

https://github.com/samitani/DDLreview/pull/1

Gemini Code Assist

結果は以下でした。このリポジトリDDLしかありません。情報が限られた中でのレビューということを考えると、かなり良い感じだと感じました。 正直、この状態でも十分満足です。

  1. ✅ Primary Key が指定されていない
  2. user_nameNOT NULL の指定漏れ
  3. 予約語 create が使用されてしまっている
  4. usersiduser_itemsuser_id の型の不一致
  5. userscreateuser_itemscreated の表記揺れ
  6. 命名規則 が CamelCase と SnakeCase の混在
  7. ❌ 不必要に大きな型 (user_name, status)

それ以外にも、外部キー制約が必要な点が指摘されてます。

型が妥当かどうかは、ドメイン知識がないと判断が難しいのでしょう。アプリケーションのコードが一緒にコミットされてれば、検知してくれるかもしれません。

指示を与えてあらためてレビューさせる

.gemini/styleguide.md に指示を記述できます。 適当に指示を追加してレビューさせてみましょう。

% cat .gemini/styleguide.md
日本語で回答してください。

# DDL をレビューするときのポイント

* 外部キー制約は任意とします。ただし、既存のテーブルで外部キー制約をすでに利用している場合は、常に外部キー制約を利用するようにしてください。
* カラム名の表記ゆれがないか確認してください。
* TEXT型が利用されている場合、VARCHAR型を検討してください。
* フラグやステータスといった値のバリエーションが固定されているカラムにはENUM型を検討してください。

https://github.com/samitani/DDLreview/pull/2

  1. ✅ Primary Key が指定されていない
  2. user_nameNOT NULL の指定漏れ
  3. 予約語 create が使用されてしまっている
  4. usersiduser_itemsuser_id の型の不一致
  5. userscreateuser_itemscreated の表記揺れ
  6. 命名規則 が CamelCase と SnakeCase の混在
  7. ✅不必要に大きな型 (user_name, status)

7 の型については指摘が入るようになりました。 5 の表記ゆれは今回もスルーされました。もうちょっと詳しく指示しないとダメかもしれません。

初期状態では 外部キー制約を This is crucial と強く利用を勧めてきてましたが、指示を考慮して、今回は マイルドな指摘へと変化してます。

データ型を揃えることで、データ整合性を保つための外部キー制約 (FOREIGN KEY (user_id) REFERENCES users(id)) の追加も可能になります。

指示を改良してリトライ

表記ゆれを見逃さないように「似たカラム名がある場合、表記がゆれていないか確認してください」と怪しいケースを積極的にひろうように指示してみます。 また、外部キーを任意としたので、代わりにインデックスの有無を確認するよう指示を足しました。

% git diff
diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md
index c110d42..9038135 100644
--- a/.gemini/styleguide.md
+++ b/.gemini/styleguide.md
@@ -3,6 +3,7 @@
 # DDL をレビューするときのポイント

 * 外部キー制約は任意とします。ただし、既存のテーブルで外部キー制約をすでに利用している場合は、常に外部キー制約を利用するようにしてください。
-* カラム名の表記ゆれがないか確認してください。
+* キーとなるカラムにインデックスが追加されているか確認してください。
+* 似たカラム名がある場合、表記がゆれていないか確認してください。
 * TEXT型が利用されている場合、VARCHAR型を検討してください。
 * フラグやステータスといった値のバリエーションが固定されているカラムにはENUM型を検討してください。

https://github.com/samitani/DDLreview/pull/3

今回は表記ゆれもバッチリ検知してくれました 👏

中 (Medium): created カラム名も users テーブルと合わせて created_at に統一すると、スキーマ全体の一貫性が高まります。

高 (High): 外部キー候補である user_id カラムにインデックスがありません。検索パフォーマンス向上のため、インデックスの追加を強く推奨します。1

Gemini Code Assist すばらしい。

go-sysbench のカスタムシナリオが書きやすくなりました

go-sysbench

go-sysbench は Go 言語で書かれた、sysbench のクローンです。複雑なカスタムシナリオを sysbench より簡単に作れることを目的として作りました。

github.com

sysbench はシンプルで、非常に使いやすいベンチマークツールで、長年愛用してきました。Lua 言語でカスタムシナリオを書くこともできます。 ただ、Lua 言語で複雑なカスタムシナリオを書くのは難しいと感じてました(Lua 言語に慣れていないということもあると思います)。 そこで、使い勝手や出力は sysbench のまま、より柔軟性の高い Go言語で sysbench のクローンを作りました。

$ go-sysbench --help
Usage:
  go-sysbench [options]... [oltp_read_only|oltp_read_write] [prepare|run]

Application Options:
      --threads=                        number of threads to use (default: 1)
      --events=                         limit for total number of events (default: 0)
      --time=                           limit for total execution time in seconds (default: 10)
      --report-interval=                periodically report intermediate statistics with a specified interval in seconds. 0 disables intermediate reports (default: 0)
      --histogram=[on|off]              print latency histogram in report (default: off)
      --percentile=                     percentile to calculate in latency statistics (1-100) (default: 95)
      --tables=                         number of tables (default: 1)
      --table_size=                     number of rows per table (default: 10000)
      --table-size=                     alias of --table_size
      --db-driver=[mysql|pgsql|spanner] specifies database driver to use (default: mysql)
      --db-ps-mode=[auto|disable]       prepared statements usage mode (default: auto)
      --version                         show version

MySQL:
      --mysql-host=                     MySQL server host (default: localhost)
      --mysql-port=                     MySQL server port (default: 3306)
      --mysql-user=                     MySQL user (default: sbtest)
      --mysql-password=                 MySQL password [$MYSQL_PWD]
      --mysql-db=                       MySQL database name (default: sbtest)
      --mysql-ssl=[on|off]              use SSL connections (default: off)
      --mysql-ignore-errors=            list of errors to ignore, or "all" (default: 1213,1020,1205)

PostgreSQL:
      --pgsql-host=                     PostgreSQL server host (default: localhost)
      --pgsql-port=                     PostgreSQL server port (default: 5432)
      --pgsql-user=                     PostgreSQL user (default: sbtest)
      --pgsql-password=                 PostgreSQL password [$PGPASSWORD]
      --pgsql-db=                       PostgreSQL database name (default: sbtest)
      --pgsql-ssl=[on|off]              use SSL connections (default: off)
      --pgsql-ignore-errors=            list of errors to ignore, or "all" (default: 40P01,23505,40001)

Spanner:
      --spanner-project=                Spanner Google Cloud project name
      --spanner-instance=               Spanner instance id
      --spanner-db=                     Spanner database name (default: sbtest)

Help Options:
  -h, --help                            Show this help message

今回修正した内容

go-sysbench の初期実装は、シナリオとベンチマーカー本体のロジックが十分整理できておらず、少しカスタムしにくい実装でした。 今回、インターフェイスを整理して、完全に独立したライブラリとして使えるようにしました。

使い方

github.com/samitani/go-sysbench を import して、計測したい処理だけ記述すればOKです。

  • Init()でDBの接続など、準備を行い、Event() 関数に計測したい処理を記述します。
  • Event() 関数は Read/Write/Others/IgnoreError 数を返す必要があります、これらの数は集計され、ベンチマーク結果に出力されます。
  • Event() 関数が1回実行されると、1回トランザクションが成功したとみなされます(結果のtransactions がインクリメントされます)。
package main

import (
        "context"
        "fmt"
        "os"

        "database/sql"

        _ "github.com/go-sql-driver/mysql"

        "github.com/samitani/go-sysbench"
)

type CustomBenchmark struct {
        db *sql.DB
}

// when Runner.Prepare(), Runner.Run() is called, Init() is called once in advance.
func (b *CustomBenchmark) Init(ctx context.Context) error {
        db, err := sql.Open("mysql", "root:password@/my_database")
        if err != nil {
                return err
        }

        err = db.Ping()
        if err != nil {
                return err
        }

        b.db = db
        return nil
}

// when Runner.Prepare(), Runner.Run() is called, Done() is called once at the end.
func (b *CustomBenchmark) Done() error {
        b.db.Close()
        return nil
}

// when Runner.Prepare() is called, Prepare() is called once.
func (b *CustomBenchmark) Prepare(ctx context.Context) error {
        // nothing to do
        return nil
}

// when Runner.Run() is called, PreEvent() is called once before event loop.
func (b *CustomBenchmark) PreEvent(ctx context.Context) error {
        // nothing to do
        return nil
}

// when Runner.Run() is called, Event() is called in a loop
func (b *CustomBenchmark) Event(ctx context.Context) (numReads, numWrites, numOthers, numIgnoredErros uint64, err error) {
        var readCount uint64 = 0
        var writeCount uint64 = 0

        // something you want to measure
        for i := 0; i < 5; i++ {
                rows, err := b.db.QueryContext(ctx, "SELECT NOW()")
                if err != nil {
                        return readCount, 0, 0, 0, err
                }
                defer rows.Close()

                // fetch rows from server
                for rows.Next() {
                }

                readCount = readCount + 1
        }

        return readCount, writeCount, 0, 0, nil
}

func main() {
        bench := &CustomBenchmark{}

        r := sysbench.NewRunner(&sysbench.RunnerOpts{
                Threads:        10,
                Events:         0,
                Time:           60,
                ReportInterval: 1,
                Histogram:      "on",
                Percentile:     95,
        }, bench)

        if err := r.Run(); err != nil {
                fmt.Println(err)
                os.Exit(1)
        }
}

そして、結果は見慣れた sysbench の形式で出力されます。

$ ./main
Running the test with following options:
Number of threads: 10
Report intermediate results every 1 second(s)

[ 1s ] thds: 10 tps: 5072.00 qps: 25360.00 (r/w/o: 25360.00/0.00/0.00) lat (ms,95%): 2.91 err/s 0.00 reconn/s: N/A
[ 2s ] thds: 10 tps: 5162.00 qps: 25810.00 (r/w/o: 25810.00/0.00/0.00) lat (ms,95%): 2.91 err/s 0.00 reconn/s: N/A
[ 3s ] thds: 10 tps: 5180.00 qps: 25900.00 (r/w/o: 25900.00/0.00/0.00) lat (ms,95%): 2.86 err/s 0.00 reconn/s: N/A
[ 4s ] thds: 10 tps: 5387.00 qps: 26935.00 (r/w/o: 26935.00/0.00/0.00) lat (ms,95%): 2.76 err/s 0.00 reconn/s: N/A
[ 5s ] thds: 10 tps: 5340.00 qps: 26700.00 (r/w/o: 26700.00/0.00/0.00) lat (ms,95%): 2.86 err/s 0.00 reconn/s: N/A
[ 6s ] thds: 10 tps: 5199.00 qps: 25995.00 (r/w/o: 25995.00/0.00/0.00) lat (ms,95%): 2.86 err/s 0.00 reconn/s: N/A
[ 7s ] thds: 10 tps: 5422.00 qps: 27110.00 (r/w/o: 27110.00/0.00/0.00) lat (ms,95%): 2.76 err/s 0.00 reconn/s: N/A
[ 8s ] thds: 10 tps: 5193.00 qps: 25965.00 (r/w/o: 25965.00/0.00/0.00) lat (ms,95%): 2.86 err/s 0.00 reconn/s: N/A
[ 9s ] thds: 10 tps: 5283.00 qps: 26415.00 (r/w/o: 26415.00/0.00/0.00) lat (ms,95%): 2.81 err/s 0.00 reconn/s: N/A
<snip>
Latency histogram (values are in milliseconds)
       value  ------------- distribution ------------- count
       0.127 |                                         1
       0.312 |                                         1
       0.318 |                                         1
       0.448 |                                         1
       0.546 |                                         1
       0.576 |                                         1
SQL statistics:
    queries performed:
        read:                            1572640
        write:                           0
        other:                           0
        total:                           1572640
    transactions:                        314535 (5242.20 per sec.)
    queries:                             1572640 (26210.44 per sec.)
    ignored errors:                      0      (0.00 per sec.)
    reconnects:                          N/A    (N/A per sec.)

General statistics:
    total time:                          60.0005s
    total number of events:              314535

Latency (ms):
         min:                                    0.13
         avg:                                    1.91
         max:                                    8.12
         95th percentile:                        2.86
         sum:                               599624.51

Threads fairness (Event distribution by threads):
    events (avg/stddev):           31453.5000/69.05
    execution time (avg/stddev):   59.9625/0.00

より、実用的な例は oltp_read_only, oltp_read_write の実装をみてください。

MySQL SYSTEM_USER 権限を持ったユーザの操作には SYSTEM_USER 権限 が必要

動的権限と静的権限

「動的権限は、従来の静的権限が細分化されたもの。静的権限を持っていれば、動的権限はREVOKEしても影響はない」と考えていました。 例えば、SUPER 権限 (静的) を持っていれば、BINLOG_ADMIN 権限 (動的) がなくとも、バイナリログをパージできます。

mysql> SHOW GRANTS;
+-------------------------------------------+
| Grants for super@localhost                |
+-------------------------------------------+
| GRANT SUPER ON *.* TO `super`@`localhost` |
+-------------------------------------------+
1 row in set (0.00 sec)

mysql>  PURGE BINARY LOGS TO 'binlog.000008';
Query OK, 0 rows affected (0.01 sec)

しかし、SYSTEM_USER 権限は、静的権限では満たせない仕様がありました。

dev.mysql.com

SYSTEM_USER 権限によるアカウント保護

簡単に言うと、SYSTEM_USER を持つユーザ(システムユーザ)の権限を変更したり、ユーザを削除するには、SYSTEM_USER 権限を持っている必要があります。 SUPER 等の静的権限では、システムユーザを変更できません。

スマートスタイルさんの記事がわかりやすいです。

https://blog.s-style.co.jp/2020/06/6097/

システムユーザを作成する。

mysql> CREATE USER 'system_user'@'localhost';
Query OK, 0 rows affected (0.09 sec)

mysql> GRANT SYSTEM_USER ON *.* TO  'system_user'@'localhost';
Query OK, 0 rows affected, 1 warning (0.04 sec)
-- 自分 (root) から SYSTEM_USER を 剥奪
mysql> REVOKE SYSTEM_USER ON *.* FROM 'root'@'localhost';
Query OK, 0 rows affected, 1 warning (0.01 sec)

システムユーザに権限を付与することも、削除することもできなくなりました。

mysql> GRANT SELECT ON *.* TO 'system_user'@'localhost';
ERROR 1227 (42000): Access denied; you need (at least one of) the SYSTEM_USER privilege(s) for this operation

mysql> DROP USER 'system_user'@'localhost';
ERROR 1227 (42000): Access denied; you need (at least one of) the SYSTEM_USER privilege(s) for this operation

SYSTEM_USER 権限を復活(付与)する

SYSTEM_USER 権限の付与は SUPER 権限を持っていれば可能です。

mysql> GRANT SYSTEM_USER  ON *.* TO 'root'@'localhost';
Query OK, 0 rows affected, 1 warning (0.01 sec)

まとめ

  • SYSTEM_USER 権限を REVOKE してしまうと、例え SUPER 権限をもってても、システムユーザの変更はできない
  • SUPER権限を持つユーザを持っていれば、SYSTEM_USER権限 を付与することは可能

「マネージドサービスを実装するために、追加された仕組みなんだろうなぁ」と思いました

BCP/DR:バックアップ・レプリカ・マルチリージョンクラスターの比較

  • 目標復旧時点(Recovery Point Objective = RPO)
  • 目標復旧時間(Recovery Time Objective = RTO)

バックアップの遠隔地保管

  • RPO

    • RPO はバックアップ頻度に依存
    • 日次バックアップであれば、RPOは1日、最大で1日分のデータがロスト
  • RTO

    • 比較的、長い
    • データベースのサイズに比例
    • 特にサイズの大きいデータベースのリカバリは時間がかかる
  • コスト
    • ストレージ保管コスト+バックアップを転送するコストがかかる
    • 基本的には低コストだが、クラウドではネットワーク転送コストが高くつくため、バックアップのサイズが大きいと、レプリカのほうが安くつく場合がある

非同期レプリカを遠隔地に設置

  • RPO
    • ミリ秒〜数秒レベル
    • トランザクションログ(MySQLでいうとバイナリログ)を転送するのにかかる時間だけ考えれば良い
    • MySQL の Applier Thread の遅延はRPOに影響しない。SHOW SLAVE STATUSSeconds_Behind_Master の 値 は RPOには関係ない。データの転送は Receiver IO Thread がバイナリログを読み取った時点で完了している。
  • RTO
    • 比較的、短め
    • データのリカバリが不要
    • 復旧時に、レプリカの台数やスペックを変更する場合はその時間も考慮
  • コスト
    • レプリカの台数や構成に依存
    • すぐに切り替えられるよう、本番と同じ構成にすると高コスト
    • 最小構成であれば比較的低コスト
      • テーブルを限定することも可能

マルチリージョンクラスタ

分散データベースを想定。DBサーバを複数のリージョンに配置し、複数のリージョンにまたがった単一のクラスターを構成する。 データは複数のリージョンに同期的に書き込まれる(複製される)。

  • RPO
    • RPO: 0。データロストなし。
  • RTO
    • 最短。タイムアウトの設定にもよるが、秒レベルで障害サイトが切り離される。
    • 復旧作業自体が不要。
  • コスト
    • 基本的に高い
  • その他
    • クエリの性能(レイテンシ)が悪化する
    • 切り戻しが容易
    • 分散データベースを用いるため、3リージョン以上

ざっくりとしたまとめ

手法 RPO RTO コスト
日次バックアップの遠隔地保管 1日 長め 低め
レプリカ 数秒 構成に依存 中ぐらい
マルチリージョンクラスタ 秒レベル 高い

MySQL バージョンアップ時の動的権限の追加処理について理解する

動的権限 / Dynamic Privilege

MySQL 8.0 で、動的権限と呼ばれる、従来より細かい範囲で操作を許可できる権限が追加されました。

gihyo.jp

バージョンアップ時に自動的に動的権限が付与される

一部の静的権限(従来の権限)を保持しているユーザには、MySQL バージョンアップ時に動的権限が自動的に付与されます。 例えば、MySQL 5.7 で作成した SUPER 権限保持ユーザの権限は、MySQL 8.0 へのアップグレードで以下のように変化します。

mysql> SELECT @@version;
+-----------+
| @@version |
+-----------+
| 5.7.44    |
+-----------+
1 row in set (0.00 sec)

mysql> SHOW GRANTS FOR super57@'localhost';
+---------------------------------------------+
| Grants for super57@localhost                |
+---------------------------------------------+
| GRANT SUPER ON *.* TO 'super57'@'localhost' |
+---------------------------------------------+
1 row in set (0.00 sec)
  • 8.0.28 バージョンアップ後
mysql> SELECT @@version;
+-----------+
| @@version |
+-----------+
| 8.0.28    |
+-----------+
1 row in set (0.00 sec)

mysql> SHOW GRANTS FOR super57@'localhost' \G
*************************** 1. row ***************************
Grants for super57@localhost: GRANT SUPER ON *.* TO `super57`@`localhost`
*************************** 2. row ***************************
Grants for super57@localhost: GRANT AUDIT_ABORT_EXEMPT,AUDIT_ADMIN,AUTHENTICATION_POLICY_ADMIN,BINLOG_ADMIN,BINLOG_ENCRYPTION_ADMIN,CLONE_ADMIN,CONNECTION_ADMIN,ENCRYPTION_KEY_ADMIN,GROUP_REPLICATION_ADMIN,INNODB_REDO_LOG_ENABLE,PERSIST_RO_VARIABLES_ADMIN,REPLICATION_APPLIER,REPLICATION_SLAVE_ADMIN,RESOURCE_GROUP_ADMIN,RESOURCE_GROUP_USER,ROLE_ADMIN,SERVICE_CONNECTION_ADMIN,SESSION_VARIABLES_ADMIN,SET_USER_ID,SYSTEM_USER,SYSTEM_VARIABLES_ADMIN,TABLE_ENCRYPTION_ADMIN,XA_RECOVER_ADMIN ON *.* TO `super57`@`localhost`
2 rows in set (0.00 sec)

SUPER 権限は将来的に、削除が予定されています。互換性の維持のため、このように自動的に動的権限へと変換してくれるのでしょう。

バージョンアップ後に作成したユーザには動的権限の自動追加はされない

バージョンアップの過程で、動的権限の追加は行われます。同じ静的権限を付与したとしても、バージョンアップ後に改めて作成したユーザには、動的権限は追加されません。 同じ GRANT 文を実行して作成したユーザでも、バージョンアップを経たかどうかで表示される権限が異なってしまいます。

  • MySQL 8.0.28 で新たにユーザ作成した場合
mysql> SELECT @@version;
+-----------+
| @@version |
+-----------+
| 8.0.28    |
+-----------+
1 row in set (0.00 sec)

mysql> CREATE USER super8028@'localhost' IDENTIFIED BY 'XXXXXXXXXXXXX';
Query OK, 0 rows affected (0.01 sec)

mysql> GRANT SUPER ON *.* TO super8028@'localhost';
Query OK, 0 rows affected, 1 warning (0.00 sec)

-- 新規に作成したSUPERを持つユーザ、動的権限は明示的に指定されない限り付与されない
mysql> SHOW GRANTS FOR super8028@'localhost' \G
*************************** 1. row ***************************
Grants for super8028@localhost: GRANT SUPER ON *.* TO `super8028`@`localhost`
1 row in set (0.00 sec)

ただ、実際に、許可されるオペレーションに差はないはずです。 動的権限は基本的に、静的権限が細分化されたものであり、静的権限(例えばSUPER)を保持していれば、動的権限がなくても同様の操作が許可されているはずです。

マイナーアップデートでも発生する

MySQL 8.0 ではマイナーバージョンで追加された、動的権限があります。とみたさんの MySQL Parameters で見ると非常にわかりやすいです。

マイナーバージョンアップ時も動的権限の追加は発生します。動的権限の追加は、権限ごとに「その動的権限を保持しているユーザがいないこと」が発動(権限追加)の条件になります。

  • 例) BINLOG_ENCRYPTION_ADMIN 権限を追加する処理
-- Add the privilege BINLOG_ENCRYPTION_ADMIN for every user who has the privilege SUPER
-- provided that there isn't a user who already has the privilige BINLOG_ENCRYPTION_ADMIN.
SET @hadBinLogEncryptionAdminPriv = (SELECT COUNT(*) FROM global_grants WHERE priv = 'BINLOG_ENCRYPTION_ADMIN');
INSERT INTO global_grants SELECT user, host, 'BINLOG_ENCRYPTION_ADMIN', IF(grant_priv = 'Y', 'Y', 'N')
FROM mysql.user WHERE super_priv = 'Y' AND @hadBinLogEncryptionAdminPriv = 0;
COMMIT;

この結果、アップグレードパスとユーザ作成のタイミングによって、権限の異なるユーザが出来てしまい、ややこしいです。 例えば、MySQL 5.7 → 8.0.28 → 8.0.40 とバージョンアップし、それぞれのバージョンでユーザを追加したとします。最終的にそれぞれのユーザの権限は以下になります。

-- MySQL 5.7 時点で作成したユーザ
mysql> SHOW GRANTS FOR super57@'localhost' \G
*************************** 1. row ***************************
Grants for super57@localhost: GRANT SUPER ON *.* TO `super57`@`localhost`
*************************** 2. row ***************************
Grants for super57@localhost: GRANT AUDIT_ABORT_EXEMPT,AUDIT_ADMIN,AUTHENTICATION_POLICY_ADMIN,BINLOG_ADMIN,BINLOG_ENCRYPTION_ADMIN,CLONE_ADMIN,CONNECTION_ADMIN,ENCRYPTION_KEY_ADMIN,GROUP_REPLICATION_ADMIN,INNODB_REDO_LOG_ENABLE,PERSIST_RO_VARIABLES_ADMIN,REPLICATION_APPLIER,REPLICATION_SLAVE_ADMIN,RESOURCE_GROUP_ADMIN,RESOURCE_GROUP_USER,ROLE_ADMIN,SERVICE_CONNECTION_ADMIN,SESSION_VARIABLES_ADMIN,SET_USER_ID,SYSTEM_USER,SYSTEM_VARIABLES_ADMIN,TABLE_ENCRYPTION_ADMIN,XA_RECOVER_ADMIN,TELEMETRY_LOG_ADMIN ON *.* TO `super57`@`localhost`
2 rows in set (0.00 sec)

-- MySQL 8.0.28 時点で作成したユーザ
-- AUDIT_ABORT_EXEMPT などは、super57 がすでに、保持しているため追加されない
mysql> SHOW GRANTS FOR super8028@'localhost' \G
*************************** 1. row ***************************
Grants for super8028@localhost: GRANT SUPER ON *.* TO `super8028`@`localhost`
*************************** 2. row ***************************
Grants for super8028@localhost: GRANT TELEMETRY_LOG_ADMIN ON *.* TO `super8028`@`localhost`
2 rows in set (0.00 sec)

-- MySQL 8.0.40 時点で作成したユーザ
mysql> SHOW GRANTS FOR super8040@'localhost' \G
*************************** 1. row ***************************
Grants for super8040@localhost: GRANT SUPER ON *.* TO `super8040`@`localhost`
1 row in set (0.00 sec)

追加された動的権限を剥奪してもバージョンアップで復活する

上記で記載したように、各動的権限が追加される条件は、バージョンではなく、「その動的権限を保持しているユーザがいないこと」です。 そのため、静的権限だけで運用しようと自動的に追加された動的権限を剥奪したとしても、またバージョンアップをすると、復活してしまいます *1

追加される動的権限の対応表

mysql_system_tables_fix.sql に動的権限の付与処理が書かれています。動的権限が追加される権限を整理してみました (8.0のみ、8.4 は確認してません)。

SUPER だけでなく、CREATE USER 権限 や SELECT 権限 に対して付与される動的権限もあります。また、SYSTEM_VARIABLES_ADMINAUTHENTICATION_POLICY_ADMIN のような、動的権限 → 動的権限 の対応も存在しているようです。

権限を管理するツールを自前で開発していると、この自動追加処理の内容を加味して実装する必要があり面倒でした。 個人的には、この処理は、MySQLのバージョンアップ時に強制せず、任意のタイミングで管理者が別途、実行できるほうが良かったように思います。

※赤字はDynamic Privilege

対象となる権限 追加される Dynamic Privilege
SUPER AUDIT_ADMIN
BACKUP_ADMIN
BINLOG_ADMIN
BINLOG_ENCRYPTION_ADMIN
CLONE_ADMIN
CONNECTION_ADMIN
ENCRYPTION_KEY_ADMIN
GROUP_REPLICATION_ADMIN
INNODB_REDO_LOG_ENABLE
PERSIST_RO_VARIABLES_ADMIN
REPLICATION_APPLIER
REPLICATION_SLAVE_ADMIN
RESOURCE_GROUP_ADMIN
RESOURCE_GROUP_USER
ROLE_ADMIN
SERVICE_CONNECTION_ADMIN
SESSION_VARIABLES_ADMIN
SET_USER_ID
SYSTEM_USER
SYSTEM_VARIABLES_ADMIN
TABLE_ENCRYPTION_ADMIN
TELEMETRY_LOG_ADMIN
XA_RECOVER_ADMIN
RELOAD INNODB_REDO_LOG_ARCHIVE
FLUSH_OPTIMIZER_COSTS
FLUSH_STATUS
FLUSH_USER_RESOURCES
FLUSH_TABLES
CREATE USER APPLICATION_PASSWORD_ADMIN
PASSWORDLESS_USER_ADMIN
SET_USER_ID SYSTEM_USER
SELECT SHOW_ROUTINE
SYSTEM_VARIABLES_ADMIN AUTHENTICATION_POLICY_ADMIN
SYSTEM_USER AUDIT_ABORT_EXEMPT
FIREWALL_EXEMPT
SYSTEM_VARIABLES_ADMIN SENSITIVE_VARIABLES_OBSERVER

まとめ

  • バージョンアップ時に動的権限が自動的に追加されるケースがある
  • バージョンアップを経たかどうかで、権限が変わる。環境ごとに権限が意図せず異っている場合、この処理が原因かも。

*1:一部の権限は mysql.session などのシステムユーザにも付与されるため、復活しないものもあると思います