meta charset utf-8:AdSense クローラーが韓国語の画像アドレスを壊した 404 と php-fpm の満杯問題

テスト環境RHEL系AlmaLinux 9.8Nginx と Apache の構成キャッシュプラグインの静的 HTML を nginx が直接返す2026-11-04 確認
アドレス、パス、数値の一部は例の値

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 文字が、別の複数の文字に変わります。

🧭 meta charset utf-8 인코딩 결정 순서
설명 다이어그램
🔍 순서
📄 응답
🔖BOM(바이트 순서 표시)있으면 그 인코딩으로 정함없음없음없음
📨Content-Type 머리의 charset있으면 그 값으로 정함charset 없음charset 없음
🏷️meta charset 줄앞 1,024바이트 안에 있으면 그 값으로 정함1,024바이트 뒤로 밀려 찾지 못함
➜
🔎 읽는 쪽
🔤문서 인코딩찾은 값으로 읽음찾은 값으로 읽음찾은 값으로 읽음짐작하거나 기본값 windows-1252로 읽음
인코딩을 정할 때는 BOM(바이트 순서 표시)을 먼저 보며, BOM이 있으면 그 인코딩으로 읽습니다.
BOM이 없으면 다음은 응답 머리 Content-Type 뒤에 붙는 charset입니다.
그다음이 앞 1,024바이트 미리 읽기로 찾은 meta charset입니다. head 영역 안에 있더라도 1,024바이트 뒤로 밀린 meta charset은 찾지 못하는 읽는 쪽이 생깁니다.
셋 다 없으면 바이트 모양으로 짐작하거나 정해 둔 기본값으로 읽게 됩니다. 머리에 charset이 없고 meta charset이 1,024바이트 뒤에 있으면, 대부분의 locale에서 기본값인 windows-1252로 읽힐 수 있습니다.

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 -1
15199:<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 -2
203.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 秒かかっていました。

🌐 Mediapartners-Google壊れた名前の画像アドレスへのリクエスト
nginx の画像 locationtry_files でディスク上のファイルを確認
ファイルありnginx がすぐに 200 を返す
ファイルなしtry_files の最後の項目であるバックエンドへ渡す
Apache → WordPress404 ページを PHP でレンダリング(中央値 1.4 秒)
php-fpm ワーカーリクエスト 1 つにワーカー 1 つ
プールの上限(pm.max_children)埋まると後のリクエストは待機(499、504)

壊れた名前の画像へのリクエストは 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-8
curl -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/htmltext/html; charset=utf-8
PHP が直接描いたレスポンスヘッダーtext/html; charset=UTF-8同じ(charset は 1 回)
存在する画像200200 のまま

リクエストが来てから 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 & おすすめコンテンツ

Google のクローラーのドキュメントは、Mediapartners-Google に向けたクロール設定が AdSense や Ad Manager のような広告製品に影響すると説明しています。このクローラーは robots.txt の全体ルール [User-agent: *] に従わないため、ブロックするには名前を個別に書く必要があります。壊れたアドレスの 404 が問題なら、存在しないファイルをウェブサーバーがすぐに 404 で処理するほうが、広告のクロールに手を触れずに済みます。

HTTP キャッシュの標準は、レスポンスを保存するときに受け取ったレスポンスヘッダーも一緒に保存するよう定めているため、修正前に CDN に保存されたコピーは charset のない [Content-Type: text/html] のまま返されます。保存期限が切れてオリジンサーバーから取り直すか、その記事のアドレスだけを CDN キャッシュから消せば、charset の付いた新しいヘッダーに変わります。

URL 標準は、アドレスのパスにある ASCII 以外の文字を UTF-8 のパーセントエンコードに変えてリクエストするよう定めているため、文書を UTF-8 で正しく読んだ側は元のファイル名でリクエストします。英字と数字だけのファイル名は、文書を windows-1252 で誤って読んでも同じ文字のまま残るので、レスポンスヘッダーと meta charset の両方が抜けたページでもアドレスが壊れません。

ℹ️ 提携について
当サイトのコンテンツにはアフィリエイトリンクが含まれています。訪問者がこのリンクを通じて商品やサービスを購入すると、当サイトは販売元から手数料を受け取ります。この過程において、購入者が支払う金額(イベント割引適用で金額が下がります↓)は上昇しません。掲載されている価格・割引・在庫情報は作成時点のものであり、実際とは異なる場合がありますので、ご購入前に販売元にて最終確認をお願いいたします。商品の選定および評価は独自の基準に基づいて行われており、手数料の支払いの有無は紹介順や評価内容に影響を与えません。

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

人間であることを証明してください: 0   +   1   =