みなさん、こんにちは!WordPressの深い内部構造の世界へようこそ。
普段からWordPressでサイトを構築していると、「アクセス数が急増してデータベース(DB)が重くなってきた…」という課題にぶつかることがありますよね。
単一のデータベースサーバーでは限界を迎えたとき、次に検討するのが「マスター・スレーブ( Primary / Replica )構成」による読み取り負荷分散です。
しかし、単純に「書き込み(INSERT/UPDATE)はマスター、読み取り(SELECT)はスレーブ」と機械的に振り分けるだけでは、大規模なサイト運用において「投稿したはずの最新記事が一瞬表示されない!」「ユーザーが更新したプロフィールが元に戻ったように見える!」といった致命的な現象(レプリケーション遅延問題)を引き起こしてしまいます。
今回は、WordPressのコアクラスである `$wpdb` の内部挙動を紐解きながら、レプリケーション遅延を安全に回避し、読み取り負荷分散を安全に実現する高度な設計パターンを分かりやすく解説していきますね!
ここをクリアできれば、大規模インフラにも耐えうるWordPressアーキテクチャの基礎はバッチリマスターできますよ。一緒に学んでいきましょう!
—
1. なぜ「単純な読み書き分離」では失敗するのか?
まずは、データベースの「レプリケーション(複製)」と「遅延(Lag)」のイメージを掴んでおきましょう。
マスター・スレーブ構成とレプリケーション遅延の仕組み
マスターDBに書き込まれたデータは、ネットワーク経由で非同期にスレーブDBへとコピー(レプリケーション)されます。
[ ユーザーの書き込み ]
│
▼
┌───────────────┐ レプリケーション ┌───────────────┐
│ マスターDB │ ── (わずかな遅延) ──>│ スレーブDB │
└───────────────┘ [0.01〜0.5秒など] └───────────────┘
▲ │
│ (直後の読み取り) │ (通常の読み取り)
└─────── [ Read-Your-Own-Writes ] ───┘
マスターにデータが書き込まれてから、スレーブにそのデータが完全に反映されるまでには、わずかですがタイムラグ(数ミリ秒〜数秒)が発生します。これがレプリケーション遅延(Replication Lag)です。
「自分の書き込みが読めない」問題(Read-Your-Own-Writes)
例えば、ユーザーが記事を投稿したり、コメントを書き込んだりした直後を考えてみてください。
1. ユーザーがコメントを送信(マスターDBに `INSERT`)
2. 画面がリロードされ、最新のコメント一覧を取得(スレーブDBへ `SELECT`)
3. しかし! スレーブDBへの同期が数ミリ秒遅れているため、今送信したコメントが一覧に表示されない!
4. ユーザーは「あれ?送信失敗したかな?」と思ってもう一度送信してしまう…
これが、いわゆる 「 Read-Your-Own-Writes(自分の書き込みの一貫性) 」 が破綻した状態です。
プログラミング初学者や他言語から来られた方が最初にハマりやすいポイントですね。
この問題を解決するには、「書き込みを行ったセッション(または一定時間)は、読み取りクエリであっても強制的にマスターDBへルーティングする」 という戦略が必要になります。
—
2. WordPressの `$wpdb` と `db.php` ドロップインの仕組み
WordPressは内部で `$wpdb` というグローバルオブジェクトを使ってデータベースと通信しています。
実は、WordPressコアには `db.php` という「ドロップイン」機構 が備わっており、`wp-content/db.php` を配置することで、WordPress標準の `$wpdb` クラスを丸ごと独自のカスタムクラスに差し替えることができる仕組みになっているんです。
wp-settings.php
│
├─ wp-content/db.php が存在するかチェック?
│ ├─ [YES] -> カスタム $wpdb クラスをロード!
│ └─ [NO] -> wp-includes/wp-db.php(標準の wpdb)をロード
この仕組みを利用することで、プラグインやテーマのコードを一切変更することなく、データベースアクセスの最深部でクエリを解析し、マスターかスレーブかを自動判別して動的に接続先を切り替えることができるようになります。
—
3. レプリケーション遅延を克服する『マスター・ピン留め(Sticky Master)』パターン
レプリケーション遅延対策の標準的な設計パターンが 「マスター・ピン留め(Sticky Master Strategy)」 です。
ロジックの考え方
1. 初期状態: 通常の `SELECT` クエリは、負荷分散のために「スレーブDB」へ送る。
2. 書き込み発生: `INSERT`, `UPDATE`, `DELETE`, `REPLACE` などのクエリが発行された瞬間、フラグを立てる(マスターにピン留め)。
3. 追従処理: 一度書き込みが行われたら、そのリクエスト中の以降のすべての `SELECT` クエリ は、スレーブではなく「マスターDB」へ送る。
4. セッション維持(必要に応じて): ユーザーの操作(CookieやTransients)に書き込みタイムスタンプを保持し、書き込みからN秒以内は次以降のリクエストでもマスターへルーティングする。
まずは一番確実で基本となる「同一リクエスト(一回のリクエスト処理)内でのピン留めロジック」を実装してみましょう!
—
4. 実践!カスタム `$wpdb` クラスの設計とコード
それでは、実際に `wp-content/db.php` として機能するカスタムデータベースクラスのコード例を見ていきましょう。
他言語からのステップアップでも理解しやすいように、接続の遅延評価(Lazy Connection)やクエリ解析のコメントを丁寧に入れてあります。
/
defined( ‘ABSPATH’ ) || exit;
// WordPress標準の wp-includes/wp-db.php を読み込む
require_once ABSPATH . WPINC . ‘/wp-db.php’;
class Advanced_Replication_wpdb extends wpdb {
/ @var resource|mysqli|null マスターDB接続 /
protected $master_dbh = null;
/ @var resource|mysqli|null スレーブDB接続 /
protected $slave_dbh = null;
/ @var bool リクエスト内で書き込み(Master Pinning)が発生したかのフラグ /
protected $has_written = false;
/
- コンストラクタ
- ※ここでは即時接続せず、クエリ実行時に接続する Lazy Loading(遅延接続)を採用します
/
public function __construct( $dbuser, $dbpassword, $dbname, $dbhost ) {
// 親クラスの初期化(変数などのセット)
parent::__construct( $dbuser, $dbpassword, $dbname, $dbhost );
}
/
- DB接続のオーバーライド(Lazy Loading)
- 実際にクエリが呼ばれたタイミングで、必要に応じてマスター/スレーブに接続します
/
public function db_connect( $allow_bail = true ) {
// WordPressコアとの互換性のため、デフォルトはマスター接続を確立
$this->connect_to_master();
return $this->dbh ? true : false;
}
/
- マスターDBへの接続を確立
/
protected function connect_to_master() {
if ( null !== $this->master_dbh ) {
$this->dbh = $this->master_dbh;
return;
}
// DB_HOST(マスターのIP/ホスト名)を利用して接続
$this->master_dbh = mysqli_init();
@mysqli_real_connect( $this->master_dbh, DB_HOST, DB_USER, DB_PASSWORD, DB_NAME );
if ( mysqli_connect_errno() ) {
$this->master_dbh = null;
return;
}
$this->dbh = $this->master_dbh;
$this->init_charset(); // 文字コードの設定
}
/
- スレーブDBへの接続を確立
/
protected function connect_to_slave() {
if ( null !== $this->slave_dbh ) {
$this->dbh = $this->slave_dbh;
return;
}
// スレーブ用ホスト名(例: wp-config.php等で定義した DB_SLAVE_HOST)
$slave_host = defined( ‘DB_SLAVE_HOST’ ) ? DB_SLAVE_HOST : DB_HOST;
$this->slave_dbh = mysqli_init();
@mysqli_real_connect( $this->slave_dbh, $slave_host, DB_USER, DB_PASSWORD, DB_NAME );
// スレーブ接続に失敗した場合は、安全のためにマスターDBへフォールバックする
if ( mysqli_connect_errno() ) {
$this->slave_dbh = null;
$this->connect_to_master(); // フォールバック
return;
}
$this->dbh = $this->slave_dbh;
$this->init_charset();
}
/
- クエリ実行メソッドのオーバーライド
- 全ての query(), get_results(), get_row() 等は最終的にこの query() を通ります
/
public function query( $query ) {
// 1. クエリのタイプ(書き込み系か読み取り系か)を判別
$is_write = $this->is_write_query( $query );
// 2. ルーティング戦略の決定
if ( $is_write ) {
// 書き込みクエリの場合:マスターへ接続 + ピン留めフラグをON
$this->has_written = true;
$this->connect_to_master();
} elseif ( $this->has_written || $this->is_transaction_active() ) {
// 既にこのリクエスト内で書き込みが発生している、またはトランザクション中の場合:マスターから読む
$this->connect_to_master();
} else {
// 安全な読み取り(SELECT)の場合:スレーブへルーティング
$this->connect_to_slave();
}
// 3. 親クラス(wpdb)の標準クエリ実行ロジックを呼び出す
return parent::query( $query );
}
/
- クエリ文字列を解析して書き込み系かどうか判別する
- @param string $query SQL文
- @return bool 書き込み系クエリの場合 true
/
protected function is_write_query( $query ) {
// 空白を除去し、先頭のキーワードを確認
$trimmed_query = ltrim( $query, ” \t\r\n(” );
// 正規表現で SELECT / SHOW / DESCRIBE / EXPLAIN 以外のクエリを判定
if ( preg_match( ‘/^(?:INSERT|UPDATE|DELETE|REPLACE|CREATE|ALTER|DROP|TRUNCATE|LOCK|UNLOCK)\b/i’, $trimmed_query ) ) {
return true;
}
// SELECT … FOR UPDATE などの排他ロッククエリはマスターに送る必要がある
if ( preg_match( ‘/\bFOR\s+UPDATE\b/i’, $query ) || preg_match( ‘/\bLOCK\s+IN\s+SHARE\b/i’, $query ) ) {
return true;
}
return false;
}
/
- トランザクション中かどうかを判定する(簡易実装)
/
protected function is_transaction_active() {
// トランザクション管理を行っている場合、処理が完了するまでマスターに固定する
return false;
}
}
// グローバル変数 $wpdb をカスタムクラスで初期化
$GLOBALS[‘wpdb’] = new Advanced_Replication_wpdb( DB_USER, DB_PASSWORD, DB_NAME, DB_HOST );
—
5. 初学者が陥りやすい文法・運用の罠と対策
この構成を導入する際、現場でよく発生するトラブルと注意点を整理しておきますね。
① `SELECT … FOR UPDATE` やトランザクションの未考慮
WordPressのプラグインによっては、整合性を保つために自前で `START TRANSACTION` や `SELECT … FOR UPDATE` を発行することがあります。
もし `FOR UPDATE` を含んだ `SELECT` クエリがスレーブDBへ送られてしまうと、エラーになるか、最悪の場合ロックが効かずにデータが壊れてしまいます。
- 対策: 先ほどの `is_write_query()` のように、`FOR UPDATE` などの特殊な構文もマスターへ送るよう正規表現でチェックしましょう。
② WordPress管理画面( `/wp-admin/` )での挙動
管理画面での操作は、投稿の更新、設定変更、メタデータの書き換えなどが頻繁に行われます。
管理画面全体でスレーブ読み取りを行うと、レプリケーション遅延によって「設定を保存したのに画面に反映されない」といった問合せが多発します。
- 対策: `is_admin()` が `true` の場合、あるいは `wp-admin` 配下では常にマスターDBに固定接続するルールを設けると、運用トラブルを劇的に減らすことができます。
// 例: 管理画面なら強制的にマスターを使う判定を追加
if ( is_admin() ) {
$this->connect_to_master();
return parent::query( $query );
}
③ メタデータ(`wp_postmeta`)のキャッシュと整合性
WordPressは `get_post_meta()` を呼び出す際、裏側で `wp_posts` や `wp_postmeta` の データを「オブジェクトキャッシュ(`wp_cache_`)」に保存します。
マスターで `update_post_meta()` を実行した際、WordPressはオブジェクトキャッシュをクリアしますが、直後の `SELECT` が遅延しているスレーブに向いてしまうと、古いスレーブのデータを再びキャッシュに書き込んでしまう(Cache Pollution:キャッシュ汚染) が発生します。
これが発生すると、レプリケーション遅延が収まった後もキャッシュ期限が切れるまで永続的に古いデータが表示され続けてしまいます。
「一度でも書き込んだリクエストは、そのリクエスト内の全クエリをマスターに送る」 というピン留めロジックが、キャッシュ汚染を防ぐためにもいかに重要かが分かりますね!
—
6. まとめ
お疲れ様でした!今回はWordPressのデータベースアーキテクチャにおける「レプリケーション遅延」と「読み取り負荷分散」の設計について深掘りしました。
最後に重要なポイントをおさらいしておきましょう。
1. レプリケーション遅延(Lag) があるため、単純に `SELECT` をスレーブに逃がすだけでは「自分の書き込みが見えない」問題が発生する。
2. WordPressの `db.php` ドロップイン機能 を活用することで、コアコードを汚さずに `$wpdb` のルーティング処理をオーバーライドできる。
3. マスター・ピン留め(Sticky Master)戦略 を導入し、リクエスト内で書き込み(INSERT/UPDATE)が発生したら、以降の `SELECT` も確実にマスターDBへルーティングする。
4. 管理画面 や `FOR UPDATE`、キャッシュ汚染(Cache Pollution) への配慮を忘れない。
データベース構造や仕組みを正しく知ることで、WordPressは世界規模の巨大トラフィックにも耐えられる非常に堅牢なシステムへと進化します。
ここまでの概念とコードロジックが脳内でトレースできるようになれば、データベース設計とWordPress内部コアの基本はバッチリマスターですよ!
ぜひご自身の開発環境やテスト環境で `db.php` の挙動を試してみてくださいね。応援しています!