meta charset utf-8:AdSense クローラーが韓国語の画像アドレスを壊した 404 と php-fpm の満杯問題
HTML の先頭部分で meta charset utf-8 の宣言が見つからず、レスポンスヘッダーにも charset がないと、読み取る側は文書の文字コードを推測するか既定値で読むことになります。Nginx と Apache 構成の WordPress サーバーで、AdSense クローラーが壊れた韓国語(非 ASCII)の画像アドレスにリクエストを集中させ、404 の処理で PHP ワーカーがすべて埋まった件をログで追跡しました。その結果をもとに、キャッシュ版のレスポンスヘッダーに charset を付け、存在しない静的ファイルは nginx がすぐに 404 で処理するようにして、同じリクエストが再びワーカーを埋めないようにしました。
1️⃣ meta charset utf-8:先頭 1,024 バイト以内にあるべき理由
サーバーは文書の文字コードを 2 か所で伝えられます。レスポンスヘッダー [Content-Type] の後ろに付く charset と、HTML の中に書く meta charset の行です。
HTML 標準は、文字コードの宣言を先頭 1,024 バイト以内に置くよう定め、読み取る側にも先頭 1,024 バイトだけを事前に読んで探すよう勧めています。そのため head 要素の中にあっても、1,024 バイトより後ろに押し出された meta charset は見つけられない読み取り側が出てきます。
文字コードを決めるときは、まず BOM(バイト順マーク)を見て、次にレスポンスヘッダーの charset、その次に先頭 1,024 バイトの事前読み込みで見つけた meta charset を見ます。3 つともなければ、バイトの形から推測するか、決めておいた既定値で読むことになり、HTML 標準が提案する既定値の表では、ほとんどの locale が windows-1252 です。
UTF-8 の文書を windows-1252 で読むと、韓国語の文字のように複数のバイトでできた 1 文字が、別の複数の文字に変わります。
2️⃣ AdSense クローラー Mediapartners-Google が壊した韓国語(非 ASCII)の画像アドレス
先頭 1,024 バイトを基準に、修正前のキャッシュ版を確認したところ、レスポンスヘッダーの Content-Type には charset がなく、meta charset の行は 15,199 バイト目にありました。そのため AdSense クローラー Mediapartners-Google が受け取ったキャッシュ版は、どちらの場所からも文字コードを見つけられない状態でした。
curl -sI https://example.com/sample-post/ | grep -i '^content-type'content-type: text/htmlこの構成では、アドレスの後ろにクエリ文字列を付けるとキャッシュ版を飛ばして PHP が直接描いたレスポンスが返り、そのレスポンスには charset が付いていました。
curl -sI 'https://example.com/sample-post/?nocache=1' | grep -i '^content-type'content-type: text/html; charset=UTF-8キャッシュファイルの中で meta charset が始まる位置は、[grep -b] でバイト単位まで確認できます。
grep -bo '<meta charset' /var/www/html/wp-content/cache/wp-rocket/example.com/sample-post/index-https.html | head -115199:<meta charset同じキャッシュファイルの img タグには、UTF-8 の多言語ファイル名(この場合は韓国語)がパーセントエンコードされずにそのまま入っていました。この HTML を windows-1252 で読むと、[대시보드-화면-예시.png] という名前が [대시보드-화면-예시.png] のような文字に変わり、その文字を再び UTF-8 でパーセントエンコードしてリクエストすると、サーバーにないファイルを探すことになります。
アクセスログから、このクローラーが受けた画像の 404 を抜き出すと、壊れた名前のアドレスが続けて出てきます。
grep 'Mediapartners-Google' /var/log/nginx/access.log | grep ' 404 ' | grep 'GET /wp-content/uploads/' | cut -d' ' -f1,4,7,9 | head -2203.0.113.3 [03/Nov/2026:20:40:51 /wp-content/uploads/2025/07/---1024x682.png 404
203.0.113.1 [03/Nov/2026:20:40:51 /wp-content/uploads/2025/07/---300x159.jpg 404ログに残ったアドレスは、元のファイル名の UTF-8 バイトを windows-1252 の文字として読み、再び UTF-8 でエンコードした値とバイト単位まで同じでした。同じ形で壊れた画像アドレスへのリクエストは、その日のログでこのクローラーが 202 件、スクリプト 1 つ(python-requests)が 6 件で、訪問者からのリクエストは 0 件でした。
壊れた名前 [대시보드-화면-예시.png] を UTF-8変換ツールで元に戻すと、UTF-8 のバイトを windows-1252 で読んで壊れたケースと判定され、元の名前 [대시보드-화면-예시.png] が出てきます。
3️⃣ 存在しない画像の 404 が php-fpm を埋めた流れ
壊れたアドレスの画像はサーバーにないため、nginx は画像の location ルールで try_files の最後の項目に従ってリクエストを Apache に渡し、WordPress が 404 ページを PHP で描きました。同じ日の昼間に記録された 404 の 587 件を見ると、キャッシュのない 404 ページ 1 つに中央値 1.4 秒かかっていました。
壊れた名前の画像へのリクエストは 20:40 から 3 分間に 200 件あまり集中し、そのうち 133 件が 404 で終わりました。php-fpm ワーカーはリクエスト 1 つに 1 つずつ割り当てられ、404 1 件に 4〜6 秒かかる間もリクエストが続いたため、20:40:55 にプールの上限に達しました。
プールの上限に達した瞬間は、php-fpm のエラーログに php-fpm max_children 警告として残ります。
grep 'max_children' /var/log/php-fpm/error.log[03-Nov-2026 20:40:55] WARNING: [pool www] server reached pm.max_children setting (20), consider raising itプールが埋まった後に来たリクエストは、空いたワーカーを待ちました。いちばん長く待った 404 は 58 秒で終わり、クローラーが 60 秒待って切ったリクエストは 499、バックエンドの応答を受け取れなかったリクエスト 1 件は 504 として記録されました。
同じ時刻に動いていたキャッシュのプリロードと予約タスク(Action Scheduler)、1 分ごとに実行される wp-cron は、確認の結果、原因から外しました。プリロードは一日中同じ低い速度で動いており、予約タスクは同じ時間帯にプリロードの作業だけを処理していて、wp-cron はコマンドラインの PHP で実行されるため php-fpm ワーカーを使いません。
4️⃣ nginx の charset utf-8 と、存在しない静的ファイルはすぐ 404
キャッシュ版を返す nginx の location に [charset utf-8;] の 1 行を加えてレスポンスヘッダーに charset が付くようにし、アップロードフォルダーの画像と静的ファイルは、ディスクになければ WordPress に渡さず nginx がすぐに 404 を返すようにしました。
# http ブロック:404 レスポンスにだけ Cache-Control の値を決める
map $status $up404_cc { 404 'no-store'; default ''; }
# キャッシュ版 HTML を直接返す location
location / {
try_files $cache_file @backend;
charset utf-8;
}
# アップロードフォルダー:存在しない画像はバックエンドに渡さずすぐ 404
location ~* ^/wp-content/uploads/.+[.](jpg|jpeg|png)$ {
try_files $uri =404;
add_header Cache-Control $up404_cc always;
}map は http ブロックに置きます。値が空文字列なら nginx はそのヘッダーを送らないので、200 のレスポンスには no-store が付きません。正規表現の location は上から先に一致したものが適用されるため、既存の画像 location より前に置き、css や js、フォントのような静的ファイルにも同じ方式の location をもう 1 つ追加しました。
修正後に同じアドレスを再びリクエストすると、キャッシュ版のレスポンスヘッダーに charset が付きました。
curl -sI https://example.com/sample-post/ | grep -i '^content-type'content-type: text/html; charset=utf-8curl -sI https://example.com/wp-content/uploads/2025/07/no-such-image.png | grep -i -E '^(HTTP|content-type|cache-control)'# 修正前:WordPress が描いた 404
HTTP/2 404
content-type: text/html; charset=UTF-8
cache-control: no-cache, must-revalidate, max-age=0, no-store, private
# 修正後:nginx がすぐに返した 404
HTTP/2 404
content-type: text/html
cache-control: no-store存在しない画像の 404 は、319,058 バイトを 1.01 秒で描いていた WordPress のページから、555 バイトを 0.015 秒で返す nginx の既定ページに変わりました。404 にだけ [Cache-Control: no-store] を付けて、CDN が存在しない画像の 404 を保存しないようにしました。PHP がすでに charset を付けて送ったレスポンスには nginx が charset を重ねて付けないため、レスポンスヘッダーには 1 回だけ出ました。
| 確認したリクエスト | 修正前 | 修正後 |
|---|---|---|
| 存在しない画像 | 319,058B、1.01 秒(WordPress の 404) | 555B、0.015 秒(nginx の 404) |
| 壊れた名前の画像 | 1.05 秒(WordPress の 404) | 0.014 秒(nginx の 404) |
| キャッシュ版 HTML のレスポンスヘッダー | text/html | text/html; charset=utf-8 |
| PHP が直接描いたレスポンスヘッダー | text/html; charset=UTF-8 | 同じ(charset は 1 回) |
| 存在する画像 | 200 | 200 のまま |
リクエストが来てから PHP でサムネイルや WebP を生成する画像プラグインを使うサイトには、この 404 処理は合いません。nginx が先に 404 を返すため、画像が生成されないからです。アップロード時に WebP をファイルとして先に作っておく構成なら、そのまま適用できます。
5️⃣ meta charset が後ろに押し出された理由:WP Rocket の Delay JS
レスポンスヘッダーに charset を付けた後も、キャッシュ版 HTML の meta charset は 15,199 バイト目にそのまま残っています。PHP が直接描いたページでは 135 バイト目にあった行です。
WP Rocket の Delay JS をオンにすると、キャッシュファイルを作るときに head の直後へ lazyload のような遅延実行用のスクリプトを入れ、続けて meta charset を head の先頭へ移し直す段階を通ります。この移す段階は文書全体に正規表現をかける方式なので、約 1.2MB の大きな記事では PHP の backtrack 上限を超えて失敗します。
正規表現が失敗すると、WP Rocket はスクリプトだけが入り meta charset は移せなかった HTML をそのままキャッシュし、その結果 meta charset が先頭 1,024 バイトの外に押し出されました。同じ PHP 設定で別に再現したところ、backtrack 上限が既定値の 1,000,000 と 5,000,000 のときは失敗し、10,000,000 から移すことに成功しました。
今回はレスポンスヘッダーに charset を付ける方法で解決し、meta charset を前に移す修正はしていません。backtrack 上限を上げれば WP Rocket の本来の動作は戻りますが、同じ PHP で動くすべての正規表現の上限が 10 倍に増えるため、おすすめしません。
🔢 FAQ & おすすめコンテンツ
HTTP 404
HTTP 404 エラー(クライアントエラー)の原因と解決
HTTP ステータスコードの 1 つである HTTP 404 エラーページは、ページが見つからないときに表示されます。
AdSense
WordPressのads.txt設定:AdSenseの警告を解決する
ads.txt はサイトのルートに置くテキストファイルで、このドメインの広告枠を販売できる正規の販売者が誰なのかを公開する一覧です。
WP Rocket のキャッシュ
WP Rocket 韓国語URL:nginxがキャッシュファイルを見つけられない原因とnjsでの解決
英語スラッグの記事は nginx がキャッシュファイルを直接返しますが、韓国語スラッグの記事は同じキャッシュファイルがあっても、PHP が一度実行された後にそのファイルを返すことになります。
ℹ️ 提携について
当サイトのコンテンツにはアフィリエイトリンクが含まれています。訪問者がこのリンクを通じて商品やサービスを購入すると、当サイトは販売元から手数料を受け取ります。この過程において、購入者が支払う金額(イベント割引適用で金額が下がります↓)は上昇しません。掲載されている価格・割引・在庫情報は作成時点のものであり、実際とは異なる場合がありますので、ご購入前に販売元にて最終確認をお願いいたします。商品の選定および評価は独自の基準に基づいて行われており、手数料の支払いの有無は紹介順や評価内容に影響を与えません。