🏥

[WordPress]特定記事へのアクセス集中によるDBダウン対処事例

に公開

はじめに

WordPressサイトの話です。
割と簡単に導入できる分、運用が進むにつれて色々とパフォーマンスの問題が生じます。
バズったり攻撃を受けた際の対処となると急にハードルが上がるのは、安価なレンタルサーバーを利用してるケースが多いためです。今回はそんな事例です。

今回相談を受けた事案

某レンタルサーバーの共用サーバープランでした。
WordPressで会員制サイトを運用していたが、急に「データベース接続エラー」になったというもの。
何らかのサーバへの負荷増であることはすぐに分かりましたが、残念ながら担当者と連絡が取れず、
Cloudflareのアカウント情報が不明、DNSサーバの引っ越しも不可という状態だったので、現環境で何とかしないといけない状況でした。

調査

DB

とりあえずindex.htmlを置いてWordPressを動かさないようにしてみたものの、サイトトップへのアクセスでは無さそうで、以前としてDBへの異常なアクセスが続いていました。

それでもSSHでアクセスして、運よくDB接続できたので、プロセス数を確認するとおそらく上限値であろう切りの良い数字で貼りついてます。しかもKILLしてもすぐに増えます。

SHOW PROCESSLIST;

サイト内部からの攻撃の可能性 ←なし

こちらはローカルにファイルを落としてスキャンしてみましたが、ツールでも目視でも改竄箇所は見つかりませんでした。また、WordPressの脆弱性を突いた形跡も見られませんでした。

アクセスログ

昨日の分だけで1.2Gbありました。
中を見てみると途中から特定URL(/hoge としておきます)へのアクセスでした。
どうもバズったというものでなく、何らかの意図でこの記事URLにだけ集中してアクセスしてます。
※この類の話はたまにありますね。社会問題を取り扱うコンテンツなど特にそうです。

直接原因:特定記事URLへのアクセス集中によるDB接続エラー

  • 特定の記事記事URLに対してのアクセス集中により、DB接続数が50件の上限に張り付いた。
  • 外部連携しているWebhookのリトライも走り走り、更にリクエストが増加していた。
  • 結果として「データベース接続エラー」状態に陥った。

対処案の検討

この記事を非公開にすればいいんですが、単にバズってる可能性も0ではないので、記事を温存することにしました。

一次対処 : .htaccessで403

取り急ぎ、諸々確認作業でDBへの負荷を減らすために.htaccessに追記して403を返すようにしました。

RewriteEngine On
RewriteCond %{REQUEST_URI} ^/hoge$
RewriteRule ^.*$ - [R=403,L]

二次対処:記事コンテンツを静的HTMLにする

①記事HTMLの生成

対象となる記事をSTG環境で表示させ、Chromeの「名前をつけて保存」してHTML化しました。
※テスト環境やSTG環境(本番同等環境)が無い場合は、データベース内の記事コンテンツHTMLを取得するか、バックアップしたSQLファイルから取り出す必要があると思います。

余計なファイルが生成されたり、javascriptについては「xxxxxx.js.ダウンロード」の拡張子にされてしまうので、それをGrep置換したり体裁を整えました。

尚、ログインしたユーザだけが記事の続きを見れるので、続きの有り無しで2パターンのHTMLを作成しました。

full.html,teaser.html

②記事の物理PATHの作成

1)サーバーのドキュメントルート直下に物理フォルダを作成

/hoge/

2)その中に記事のHTMLファイルを保存

full.html(会員向けフル記事)
teaser.html(非会員向けティーザー記事)

3)アクセス時には以下の簡易PHPでCookieを判定し、表示を切り替える

**これにより、暫定対処は可能可能になりますが、勿論Cookieの偽装対応はできませんので、あくまでも一時的に特定URLへのリクエストをしのぐのが目的です。
**

<?php
// キャッシュ制御:ログイン状態に応じた誤キャッシュを防止
header('Cache-Control: private, no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
header('Vary: Cookie');

$is_logged_in = false;
foreach ($_COOKIE as $key => $value) {
    if (strpos($key, 'wordpress_logged_in_') === 0) {
        $is_logged_in = true;
        break;
    }
}

if ($is_logged_in) {
    readfile('full.html');
} else {
    readfile('teaser.html');
}
?>

課題など

メリット

DBを完全にバイパスできる
→ アクセス集中でもMySQLの同時接続数制限にかからない

応急処置として即導入可能
→ FTPでアップロードするだけで利用可能

注意点

  • Cookie判定は「ログイン中かどうか」のみ。会員権限の精密な判定は行えません。今回はDB負荷を下げることが最優先課題なので、多少会員向けコンテンツが見られる事よりも、サーバ復旧を優先しました。

  • 記事更新のたびにHTMLを再生成する必要があります。

  • 長期的には、WAFやキャッシュプラグイン、CDN導入と組み合わせて対策するのが望ましいです。

本対処

CloudFlareのアカウント情報を入手次第、WAFでブロックします。

また、法的な対応も考えられるので、CloudFlareのアクセスログを保管します。
※残念ながら今回のサイトのアクセスログには、クライアントのIPでなくCloudFlareのIPが届いてました。

最終的にはAWS等堅牢な環境へのサーバ移転を行う予定ですが、一旦これで寝ようと思います。

最後に

数年以上運用に成功しているWordPressサイトは、色々と次のステップに進む段階に来てると思います。
インフラ増強や記事のアーカイブへの分離とキャッシュ化、場合によってはSEO対策の見直しなども発生します。最近別件で15年以上前に構築したサーバーの移転も行いましたが、そろそろユーザー思いのレンタルサーバーもPHP5系は切り捨てると思います。

PHP8系へのバージョンアップではプログラムの改修も発生します。
おそらく長年放置されたサーバの場合、開発時のメンバーはいないでしょう。

せっかく運用に成功したサイトに突然の終わりが告げられるかもしれません。

実際そういう事例もありました。
”PostgreSQLのサポート終了のお知らせ”というメールが届いたものの、何のことか分からずに放置したら、DBが削除され、バックアップも無いのでサービス終了したという事例です。
DB設計書も初期のデータも無いので、さすがにこれはどうにもなりませんでした。

サイト立ち上げの最初の半年ぐらいはお試し感覚でも良いのですが、個人サイトでも何らかの収益があるのであれば、しっかりとした運用設計を行う事が大事だと思います。

全然技術文書っぽくなくなってしまいましたね。。。
乱文失礼しました。

Discussion