meta charset utf-8: 404 จากที่อยู่รูปภาพภาษาเกาหลีที่ครอว์เลอร์ของ AdSense ทำให้เสีย และ php-fpm ที่เต็ม

สภาพแวดล้อมทดสอบตระกูล RHELAlmaLinux 9.8ระบบ Nginx และ Apachenginx ส่ง HTML แบบสแตติกของปลั๊กอินแคชออกไปโดยตรงตรวจสอบเมื่อ 2026-11-04
ที่อยู่ พาธ และตัวเลขบางส่วนเป็นค่าตัวอย่าง

หากฝั่งที่อ่านหาการประกาศ meta charset utf-8 ในส่วนต้นของ HTML ไม่พบ และส่วนหัวการตอบกลับก็ไม่มี charset ฝั่งที่อ่านจะต้องเดาการเข้ารหัสอักขระของเอกสาร หรืออ่านด้วยค่าเริ่มต้น บนเซิร์ฟเวอร์ WordPress ที่ใช้ระบบ Nginx และ Apache ครอว์เลอร์ของ AdSense ส่งคำขอจำนวนมากไปยังที่อยู่รูปภาพภาษาเกาหลี (ไม่ใช่ ASCII) ที่เสีย จนการจัดการ 404 ทำให้ worker ของ PHP เต็มทั้งหมด หลังจากติดตามเรื่องนี้จากล็อก จึงได้เพิ่ม charset ให้ส่วนหัวการตอบกลับของหน้าแคช และให้ nginx ตอบไฟล์สแตติกที่ไม่มีอยู่ด้วย 404 ทันที เพื่อไม่ให้คำขอเดิมกลับมาทำให้ worker เต็มอีก

1️⃣ meta charset utf-8: เหตุผลที่ต้องอยู่ภายใน 1,024 ไบต์แรก

เซิร์ฟเวอร์บอกการเข้ารหัสอักขระของเอกสารได้สองที่ คือ charset ที่ต่อท้ายส่วนหัวการตอบกลับ [Content-Type] และบรรทัด meta charset ที่เขียนไว้ใน HTML

มาตรฐาน HTML กำหนดให้การประกาศการเข้ารหัสอักขระอยู่ภายใน 1,024 ไบต์แรกเท่านั้น และแนะนำให้ฝั่งที่อ่านอ่านล่วงหน้าเฉพาะ 1,024 ไบต์แรกนั้นเพื่อค้นหา ดังนั้นแม้จะอยู่ใน head แต่ meta charset ที่ถูกดันไปหลัง 1,024 ไบต์ก็อาจหาไม่พบสำหรับฝั่งที่อ่านบางตัว

เมื่อเลือกการเข้ารหัส ฝั่งที่อ่านจะดู BOM (เครื่องหมายลำดับไบต์) ก่อน ถัดมาคือ charset ในส่วนหัวการตอบกลับ แล้วจึงเป็น meta charset ที่พบจากการอ่านล่วงหน้า 1,024 ไบต์แรก หากไม่มีทั้งสามอย่าง ก็จะเดาจากลักษณะไบต์หรืออ่านด้วยค่าเริ่มต้นที่กำหนดไว้ และในตารางค่าเริ่มต้นที่มาตรฐาน HTML เสนอไว้ locale ส่วนใหญ่ใช้ windows-1252

หากอ่านเอกสาร UTF-8 เป็น windows-1252 อักขระหนึ่งตัวที่ประกอบด้วยหลายไบต์ เช่น ตัวอักษรภาษาเกาหลี จะกลายเป็นอักขระอื่นหลายตัว

🧭 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️⃣ ที่อยู่รูปภาพภาษาเกาหลี (ไม่ใช่ ASCII) ที่ครอว์เลอร์ของ AdSense อย่าง Mediapartners-Google ทำให้เสีย

เมื่อตรวจหน้าแคชก่อนแก้โดยยึด 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 รายการ จากสคริปต์หนึ่งตัว (python-requests) 6 รายการ และจากผู้เยี่ยมชม 0 รายการ

เมื่อนำชื่อที่เสีย [대시보드-화면-예시.png] ไปแปลงกลับด้วย ตัวแปลง UTF-8 จะถูกตัดสินว่าเป็นกรณีที่อ่านไบต์ UTF-8 เป็น windows-1252 จนเสีย และได้ชื่อเดิม [대시보드-화면-예시.png] กลับมา

3️⃣ ขั้นตอนที่ 404 ของรูปภาพที่ไม่มีอยู่ทำให้ php-fpm เต็ม

เนื่องจากรูปภาพของที่อยู่ที่เสียไม่มีอยู่บนเซิร์ฟเวอร์ nginx จึงส่งคำขอต่อไปยัง Apache ตามรายการสุดท้ายของ try_files ในกฎ location ของรูปภาพ และ WordPress สร้างหน้า 404 ด้วย PHP เมื่อดู 404 จำนวน 587 รายการที่บันทึกในช่วงกลางวันของวันเดียวกัน หน้า 404 ที่ไม่มีแคชหนึ่งหน้าใช้เวลามัธยฐาน 1.4 วินาที

🌐 Mediapartners-Googleคำขอที่อยู่รูปภาพที่ชื่อเสีย
location ของรูปภาพใน nginxtry_files ตรวจไฟล์บนดิสก์
มีไฟล์nginx ตอบ 200 ทันที
ไม่มีไฟล์ส่งต่อไปยัง backend ซึ่งเป็นรายการสุดท้ายของ try_files
Apache → WordPressเรนเดอร์หน้า 404 ด้วย PHP (มัธยฐาน 1.4 วินาที)
worker ของ php-fpmหนึ่งคำขอต่อหนึ่ง worker
ขีดจำกัดของพูล (pm.max_children)เมื่อเต็ม คำขอถัดไปต้องรอ (499, 504)

คำขอรูปภาพที่ชื่อเสียหลั่งไหลเข้ามากว่า 200 รายการในช่วง 3 นาทีตั้งแต่ 20:40 และ 133 รายการในนั้นจบด้วย 404 worker ของ php-fpm ถูกจัดสรรหนึ่งตัวต่อหนึ่งคำขอ และเมื่อคำขอยังเข้ามาต่อเนื่องขณะที่ 404 แต่ละรายการใช้เวลา 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

คำขอที่เข้ามาหลังพูลเต็มต้องรอ worker ที่ว่าง 404 ที่รอนานที่สุดจบใน 58 วินาที คำขอที่ครอว์เลอร์รอ 60 วินาทีแล้วตัดไปถูกบันทึกเป็น 499 และคำขอหนึ่งรายการที่ไม่ได้รับการตอบกลับจาก backend ถูกบันทึกเป็น 504

การโหลดแคชล่วงหน้าและงานตามกำหนดเวลา (Action Scheduler) ที่ทำงานในเวลาเดียวกัน รวมถึง wp-cron ที่ทำงานทุกนาที ได้ตรวจแล้วและตัดออกจากสาเหตุ การโหลดล่วงหน้าทำงานด้วยความเร็วต่ำเท่าเดิมตลอดทั้งวัน งานตามกำหนดเวลาในช่วงนั้นประมวลผลเฉพาะงานโหลดล่วงหน้า และ wp-cron ทำงานด้วย PHP แบบบรรทัดคำสั่งจึงไม่ใช้ worker ของ php-fpm

4️⃣ nginx charset utf-8 และ 404 ทันทีสำหรับไฟล์สแตติกที่ไม่มีอยู่

ได้เพิ่มบรรทัด [charset utf-8;] หนึ่งบรรทัดใน location ของ nginx ที่ส่งหน้าแคชออกไป เพื่อให้ส่วนหัวการตอบกลับมี charset และสำหรับรูปภาพกับไฟล์สแตติกในโฟลเดอร์อัปโหลด หากไม่มีอยู่บนดิสก์ nginx จะคืน 404 ทันทีโดยไม่ส่งต่อไปยัง WordPress

# บล็อก http: กำหนดค่า Cache-Control เฉพาะการตอบกลับ 404
map $status $up404_cc { 404 'no-store'; default ''; }

# location ที่ส่ง HTML จากแคชออกไปโดยตรง
location / {
    try_files $cache_file @backend;
    charset utf-8;
}

# โฟลเดอร์อัปโหลด: รูปภาพที่ไม่มีอยู่ได้ 404 ทันทีโดยไม่ส่งต่อไปยัง backend
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 แบบ regex จะใช้ตัวที่ตรงก่อนจากบนลงล่าง จึงวางไว้ก่อน location ของรูปภาพเดิม และเพิ่ม location แบบเดียวกันอีกหนึ่งตัวสำหรับไฟล์สแตติกอย่าง css, js และฟอนต์

หลังแก้แล้วส่งคำขอที่อยู่เดิมอีกครั้ง ส่วนหัวการตอบกลับของหน้าแคชก็มี 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)'
# ก่อนแก้: 404 ที่ WordPress สร้าง
HTTP/2 404
content-type: text/html; charset=UTF-8
cache-control: no-cache, must-revalidate, max-age=0, no-store, private
# หลังแก้: 404 ที่ nginx คืนให้ทันที
HTTP/2 404
content-type: text/html
cache-control: no-store

404 ของรูปภาพที่ไม่มีอยู่เปลี่ยนจากหน้า WordPress ที่ใช้ 1.01 วินาทีสร้าง 319,058 ไบต์ มาเป็นหน้าเริ่มต้นของ nginx ที่คืน 555 ไบต์ใน 0.015 วินาที การใส่ [Cache-Control: no-store] เฉพาะ 404 ทำให้ CDN ไม่เก็บ 404 ของรูปภาพที่ไม่มีอยู่ ส่วนการตอบกลับที่ PHP ส่งพร้อม charset อยู่แล้ว nginx จะไม่เติม charset ซ้ำ จึงมี charset ในส่วนหัวการตอบกลับเพียงครั้งเดียว

คำขอที่ตรวจก่อนแก้หลังแก้
รูปภาพที่ไม่มีอยู่319,058B, 1.01 วินาที (404 ของ WordPress)555B, 0.015 วินาที (404 ของ nginx)
รูปภาพที่ชื่อเสีย1.05 วินาที (404 ของ WordPress)0.014 วินาที (404 ของ nginx)
ส่วนหัวการตอบกลับของ HTML จากแคชtext/htmltext/html; charset=utf-8
ส่วนหัวการตอบกลับที่ PHP สร้างโดยตรงtext/html; charset=UTF-8เหมือนเดิม (charset ครั้งเดียว)
รูปภาพที่มีอยู่200ยังเป็น 200

การจัดการ 404 แบบนี้ไม่เหมาะกับเว็บไซต์ที่ใช้ปลั๊กอินรูปภาพซึ่งสร้างภาพย่อหรือ WebP ด้วย PHP หลังมีคำขอเข้ามา เพราะ nginx คืน 404 ไปก่อน รูปภาพจึงไม่ถูกสร้าง หากระบบของคุณสร้างไฟล์ WebP ไว้ล่วงหน้าตอนอัปโหลด ก็ใช้ได้ตามเดิม

5️⃣ เหตุผลที่ meta charset ถูกดันไปข้างหลัง: WP Rocket Delay JS

แม้เพิ่ม charset ให้ส่วนหัวการตอบกลับแล้ว meta charset ใน HTML จากแคชก็ยังอยู่ที่ไบต์ที่ 15,199 เหมือนเดิม ในหน้าที่ PHP สร้างโดยตรง บรรทัดเดียวกันนี้อยู่ที่ไบต์ที่ 135

เมื่อเปิด Delay JS ของ WP Rocket ตอนสร้างไฟล์แคชจะใส่สคริปต์สำหรับหน่วงการทำงานอย่าง lazyload ไว้ถัดจาก head ทันที แล้วมีขั้นตอนย้าย meta charset กลับไปไว้หน้าสุดของ head ขั้นตอนย้ายนี้ใช้ regex กับทั้งเอกสาร ในโพสต์ขนาดใหญ่ประมาณ 1.2MB จึงเกินขีดจำกัด backtrack ของ PHP และล้มเหลว

เมื่อ regex ล้มเหลว WP Rocket จะแคช HTML ตามสภาพที่มีสคริปต์ใส่เข้าไปแล้วแต่ยังย้าย meta charset ไม่ได้ ผลคือ meta charset ถูกดันออกไปนอก 1,024 ไบต์แรก เมื่อทดลองซ้ำแยกต่างหากด้วยการตั้งค่า PHP เดียวกัน การย้ายล้มเหลวเมื่อขีดจำกัด backtrack เป็นค่าเริ่มต้น 1,000,000 และ 5,000,000 และสำเร็จตั้งแต่ 10,000,000 ขึ้นไป

ครั้งนี้แก้ด้วยการเพิ่ม charset ให้ส่วนหัวการตอบกลับ และไม่ได้แก้ตำแหน่งของ meta charset การเพิ่มขีดจำกัด backtrack ทำให้ WP Rocket กลับมาทำงานแบบเดิม แต่ขีดจำกัดของ regex ทุกตัวที่ทำงานใน PHP เดียวกันจะเพิ่มขึ้น 10 เท่า จึงไม่แนะนำ

🔢 FAQ & เนื้อหาแนะนำ

เอกสารครอว์เลอร์ของ Google ระบุว่าการตั้งค่าการครอว์ลที่กำหนดให้ Mediapartners-Google มีผลกับผลิตภัณฑ์โฆษณาอย่าง AdSense และ Ad Manager ครอว์เลอร์นี้ไม่ทำตามกฎรวมของ robots.txt [User-agent: *] หากจะบล็อกต้องเขียนชื่อแยกไว้ หากปัญหาคือ 404 ของที่อยู่ที่เสีย การให้เว็บเซิร์ฟเวอร์ตอบไฟล์ที่ไม่มีอยู่ด้วย 404 ทันทีจะไม่กระทบการครอว์ลโฆษณา

มาตรฐานแคช HTTP กำหนดให้เก็บส่วนหัวการตอบกลับที่ได้รับไว้พร้อมกับการตอบกลับ สำเนาที่ CDN เก็บไว้ก่อนแก้จึงยังส่งออกไปเป็น [Content-Type: text/html] ที่ไม่มี charset เหมือนเดิม เมื่อหมดอายุและดึงใหม่จากเซิร์ฟเวอร์ต้นทาง หรือเมื่อล้างเฉพาะที่อยู่ของโพสต์นั้นออกจากแคช CDN ก็จะเปลี่ยนเป็นส่วนหัวใหม่ที่มี charset

มาตรฐาน URL กำหนดให้แปลงอักขระที่ไม่ใช่ ASCII ในพาธของที่อยู่เป็นการเข้ารหัสแบบเปอร์เซ็นต์ UTF-8 ก่อนส่งคำขอ ฝั่งที่อ่านเอกสารเป็น UTF-8 ได้ถูกต้องจึงขอด้วยชื่อไฟล์เดิม ชื่อไฟล์ที่ใช้เพียงอักษรละตินและตัวเลขยังคงเหมือนเดิมแม้เอกสารจะถูกอ่านผิดเป็น windows-1252 ที่อยู่จึงไม่เสีย แม้ในหน้าที่ไม่มีทั้ง charset ในส่วนหัวการตอบกลับและ meta charset

🔗 เนื้อหาแนะนำ
การวัดครอว์เลอร์อื่น ครอว์เลอร์ AI: วัดการเข้ามาของ GPTBot และ ClaudeBot และตั้งค่าบล็อกด้วย robots.txt และ nginx ครอว์เลอร์ AI คือบอตที่บริษัท AI ใช้เก็บหน้าเว็บเพื่อฝึกโมเดล ค้นหาด้วย AI และตอบคำถามของผู้ใช้ UA เป็นข้อความที่ฝั่งที่ส่งคำขอเขียนส่งมาเอง ใครก็ส่งคำขอโดยใส่ชื่อ GPTBot หรือ ClaudeBot ได้ อ่านบทความ → เส้นทางของคำขอจากครอว์เลอร์ AI: robots.txt หยุดได้เฉพาะบอตที่อ่านกฎ การบล็อก UA ใน nginx ดูแค่ชื่อ และผู้แอบอ้างที่ยืมชื่อบอตที่อนุญาตจะถูกปฏิเสธด้วยการเทียบช่วง IP ที่ประกาศ

ℹ️ ประกาศเรื่องลิงก์แอฟฟิลิเอต
เนื้อหาของเว็บไซต์นี้มีลิงก์แอฟฟิลิเอต หากผู้เข้าชมซื้อสินค้าหรือบริการผ่านลิงก์เหล่านี้ เว็บไซต์นี้จะได้รับค่าคอมมิชชันจากผู้ขาย ในกระบวนการนี้ ราคาที่ผู้ซื้อจ่าย(ราคาจะลดลงเมื่อมีโปรโมชัน↓)จะไม่สูงขึ้นแต่อย่างใด ราคา ส่วนลด และข้อมูลสต็อกที่แสดงเป็นข้อมูล ณ เวลาที่เขียนและอาจต่างจากความเป็นจริง โปรดตรวจสอบกับผู้ขายอีกครั้งก่อนซื้อ การเลือกและการประเมินสินค้าเขียนตามเกณฑ์ของเราเอง และการได้รับค่าคอมมิชชันหรือไม่ไม่มีผลต่อลำดับการแนะนำหรือเนื้อหาการประเมิน

ใส่ความเห็น

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องข้อมูลจำเป็นถูกทำเครื่องหมาย *

Prove your humanity: 6   +   1   =