meta charset utf-8: lỗi 404 do địa chỉ hình ảnh tiếng Hàn bị trình thu thập của AdSense làm hỏng và php-fpm bị đầy

Môi trường thử nghiệmHọ RHELAlmaLinux 9.8Cấu hình Nginx và Apachenginx trả thẳng HTML tĩnh của plugin cacheKiểm tra ngày 2026-11-04
Một số địa chỉ, đường dẫn và con số là giá trị ví dụ

Nếu bên đọc không tìm thấy khai báo meta charset utf-8 ở phần đầu HTML và header phản hồi cũng không có charset, bên đọc phải đoán mã hóa ký tự của tài liệu hoặc đọc bằng giá trị mặc định. Trên một máy chủ WordPress cấu hình Nginx và Apache, trình thu thập của AdSense dồn dập gửi yêu cầu tới các địa chỉ hình ảnh tiếng Hàn (ngoài ASCII) bị hỏng, và việc xử lý 404 làm đầy toàn bộ worker PHP. Sau khi lần theo nhật ký, tôi đã thêm charset vào header phản hồi của các trang cache và cho nginx trả thẳng 404 với tệp tĩnh không tồn tại, để cùng những yêu cầu đó không còn làm đầy worker nữa.

1️⃣ meta charset utf-8: lý do phải nằm trong 1.024 byte đầu

Máy chủ có thể cho biết mã hóa ký tự của tài liệu ở hai chỗ: charset gắn sau header phản hồi [Content-Type], và dòng meta charset viết bên trong HTML.

Chuẩn HTML quy định khai báo mã hóa ký tự chỉ được nằm trong 1.024 byte đầu, và khuyên bên đọc chỉ đọc trước 1.024 byte đầu đó để tìm. Vì vậy, dù nằm trong head, meta charset bị đẩy ra sau 1.024 byte vẫn có thể bị một số bên đọc bỏ sót.

Khi quyết định mã hóa, bên đọc xem BOM (dấu thứ tự byte) trước, tiếp theo là charset trong header phản hồi, sau đó là meta charset tìm được khi đọc trước 1.024 byte đầu. Nếu không có cả ba, bên đọc đoán theo hình dạng byte hoặc dùng giá trị mặc định đã định sẵn, và trong bảng giá trị mặc định mà chuẩn HTML đề xuất, phần lớn locale là windows-1252.

Khi đọc tài liệu UTF-8 bằng windows-1252, một ký tự gồm nhiều byte, như một chữ tiếng Hàn, sẽ biến thành nhiều ký tự khác.

🧭 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️⃣ Địa chỉ hình ảnh tiếng Hàn (ngoài ASCII) bị trình thu thập của AdSense Mediapartners-Google làm hỏng

Khi kiểm tra trang cache trước lúc sửa theo mốc 1.024 byte đầu, header phản hồi Content-Type không có charset, còn dòng meta charset nằm ở byte thứ 15.199. Vì vậy trang cache mà trình thu thập của AdSense Mediapartners-Google nhận được không có mã hóa nào tìm được ở cả hai chỗ.

curl -sI https://example.com/sample-post/ | grep -i '^content-type'
content-type: text/html

Ở cấu hình này, nếu bạn thêm chuỗi truy vấn sau địa chỉ, trang cache sẽ bị bỏ qua và phản hồi do PHP vẽ trực tiếp được trả về, và phản hồi đó có kèm charset.

curl -sI 'https://example.com/sample-post/?nocache=1' | grep -i '^content-type'
content-type: text/html; charset=UTF-8

Vị trí bắt đầu của meta charset trong tệp cache có thể kiểm tra tới từng byte bằng [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

Các thẻ img trong cùng tệp cache chứa nguyên tên tệp đa ngôn ngữ UTF-8 (ở đây là tiếng Hàn), không mã hóa phần trăm. Khi đọc HTML này bằng windows-1252, một cái tên như [대시보드-화면-예시.png] biến thành các ký tự như [대시보드-화면-예시.png], và khi các ký tự đó lại được mã hóa phần trăm theo UTF-8 rồi gửi yêu cầu, máy chủ sẽ tìm một tệp không tồn tại.

Khi lọc từ nhật ký truy cập các lỗi 404 hình ảnh mà trình thu thập này nhận được, các địa chỉ có tên bị hỏng hiện ra nối tiếp nhau.

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

Các địa chỉ còn lại trong nhật ký trùng khớp tới từng byte với giá trị có được khi đọc byte UTF-8 của tên tệp gốc thành ký tự windows-1252 rồi mã hóa lại sang UTF-8. Trong nhật ký hôm đó, các yêu cầu tới địa chỉ hình ảnh bị hỏng theo cùng kiểu có 202 yêu cầu từ trình thu thập này, 6 yêu cầu từ một tập lệnh (python-requests), và 0 yêu cầu từ khách truy cập.

Khi đưa tên bị hỏng [대시보드-화면-예시.png] vào công cụ Bộ chuyển đổi UTF-8 để khôi phục, nó được nhận diện là trường hợp byte UTF-8 bị đọc bằng windows-1252 mà hỏng, và tên gốc [대시보드-화면-예시.png] hiện ra.

3️⃣ Quá trình các lỗi 404 của hình ảnh không tồn tại làm đầy php-fpm

Vì hình ảnh ở các địa chỉ bị hỏng không có trên máy chủ, nginx chuyển yêu cầu sang Apache theo mục cuối của try_files trong quy tắc location hình ảnh, và WordPress vẽ trang 404 bằng PHP. Xem 587 lỗi 404 được ghi vào ban ngày cùng hôm, mỗi trang 404 không có cache mất trung vị 1,4 giây.

🌐 Mediapartners-GoogleYêu cầu địa chỉ hình ảnh có tên bị hỏng
location hình ảnh của nginxtry_files kiểm tra tệp trên đĩa
Có tệpnginx trả thẳng 200
Không có tệpChuyển sang backend, mục cuối của try_files
Apache → WordPressKết xuất trang 404 bằng PHP (trung vị 1,4 giây)
Worker php-fpmMỗi yêu cầu một worker
Giới hạn pool (pm.max_children)Khi đầy, yêu cầu sau phải chờ (499, 504)

Các yêu cầu hình ảnh có tên bị hỏng dồn lại hơn 200 yêu cầu một chút trong 3 phút kể từ 20:40, và 133 yêu cầu trong số đó kết thúc bằng 404. Mỗi yêu cầu được cấp một worker php-fpm, và vì yêu cầu vẫn liên tục đến trong khi mỗi lỗi 404 mất 4 đến 6 giây, pool chạm giới hạn lúc 20:40:55.

Thời điểm pool chạm giới hạn được ghi lại trong nhật ký lỗi php-fpm dưới dạng cảnh báo 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

Các yêu cầu đến sau khi pool đầy phải chờ worker trống. Lỗi 404 chờ lâu nhất kết thúc sau 58 giây, các yêu cầu mà trình thu thập ngắt sau khi chờ 60 giây được ghi là 499, và một yêu cầu không nhận được phản hồi từ backend được ghi là 504.

Việc tải trước cache, các tác vụ theo lịch (Action Scheduler) chạy cùng lúc và wp-cron chạy mỗi phút đã được kiểm tra và loại khỏi nguyên nhân. Tải trước chạy cả ngày với cùng một tốc độ thấp, các tác vụ theo lịch lúc đó chỉ xử lý việc tải trước, còn wp-cron chạy bằng PHP dòng lệnh nên không dùng worker php-fpm.

4️⃣ nginx charset utf-8 và trả 404 ngay cho tệp tĩnh không tồn tại

Tôi đã thêm một dòng [charset utf-8;] vào location của nginx trả các trang cache để header phản hồi có charset, và với hình ảnh và tệp tĩnh trong thư mục tải lên, nếu không có trên đĩa thì nginx trả thẳng 404 thay vì chuyển sang WordPress.

# Khối http: chỉ đặt giá trị Cache-Control cho phản hồi 404
map $status $up404_cc { 404 'no-store'; default ''; }

# location trả thẳng HTML trong cache
location / {
    try_files $cache_file @backend;
    charset utf-8;
}

# Thư mục tải lên: hình ảnh không tồn tại nhận 404 ngay, không chuyển sang backend
location ~* ^/wp-content/uploads/.+[.](jpg|jpeg|png)$ {
    try_files $uri =404;
    add_header Cache-Control $up404_cc always;
}

map đặt trong khối http, và vì nginx không gửi header có giá trị là chuỗi rỗng, phản hồi 200 không bị gắn no-store. location dạng regex được áp dụng từ trên xuống và cái khớp trước sẽ thắng, nên tôi đặt nó trước location hình ảnh có sẵn, và thêm một location cùng kiểu cho các tệp tĩnh như css, js và phông chữ.

Sau khi sửa, gửi lại yêu cầu tới cùng địa chỉ thì header phản hồi của trang cache đã có 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)'
# Trước khi sửa: lỗi 404 do WordPress vẽ
HTTP/2 404
content-type: text/html; charset=UTF-8
cache-control: no-cache, must-revalidate, max-age=0, no-store, private
# Sau khi sửa: lỗi 404 do nginx trả thẳng
HTTP/2 404
content-type: text/html
cache-control: no-store

Lỗi 404 của hình ảnh không tồn tại đã chuyển từ trang WordPress vẽ 319.058 byte trong 1,01 giây sang trang mặc định của nginx trả 555 byte trong 0,015 giây. Chỉ gắn [Cache-Control: no-store] cho lỗi 404 giúp CDN không lưu lỗi 404 của hình ảnh không tồn tại. Với phản hồi mà PHP đã gửi kèm charset, nginx không gắn thêm charset lần nữa, nên charset chỉ xuất hiện một lần trong header phản hồi.

Yêu cầu đã kiểm traTrước khi sửaSau khi sửa
Hình ảnh không tồn tại319.058 B, 1,01 giây (404 của WordPress)555 B, 0,015 giây (404 của nginx)
Hình ảnh có tên bị hỏng1,05 giây (404 của WordPress)0,014 giây (404 của nginx)
Header phản hồi của HTML trong cachetext/htmltext/html; charset=utf-8
Header phản hồi do PHP vẽ trực tiếptext/html; charset=UTF-8Như nhau (charset một lần)
Hình ảnh có tồn tại200Vẫn là 200

Cách xử lý 404 này không hợp với các trang web dùng plugin hình ảnh tạo ảnh thu nhỏ hoặc WebP bằng PHP sau khi yêu cầu đến. Vì nginx trả 404 trước, hình ảnh sẽ không bao giờ được tạo. Nếu cấu hình của bạn tạo sẵn tệp WebP khi tải lên, bạn có thể áp dụng nguyên như vậy.

5️⃣ Lý do meta charset bị đẩy ra sau: WP Rocket Delay JS

Ngay cả sau khi thêm charset vào header phản hồi, meta charset trong HTML cache vẫn nằm ở byte thứ 15.199. Trong trang do PHP vẽ trực tiếp, cùng dòng này nằm ở byte thứ 135.

Khi bật Delay JS của WP Rocket, lúc tạo tệp cache, các tập lệnh chạy trễ như lazyload được chèn ngay sau head, rồi một bước khác chuyển meta charset trở lại đầu head. Bước chuyển này áp regex lên toàn bộ tài liệu, nên với bài viết lớn khoảng 1,2 MB, nó vượt giới hạn backtrack của PHP và thất bại.

Khi regex thất bại, WP Rocket lưu cache nguyên HTML đã chèn tập lệnh nhưng chưa chuyển được meta charset, và kết quả là meta charset bị đẩy ra ngoài 1.024 byte đầu. Khi tái hiện riêng với cùng thiết lập PHP, việc chuyển thất bại khi giới hạn backtrack ở giá trị mặc định 1.000.000 và ở 5.000.000, và thành công từ 10.000.000.

Lần này tôi giải quyết bằng cách thêm charset vào header phản hồi và không sửa vị trí của meta charset. Nâng giới hạn backtrack sẽ khôi phục hành vi gốc của WP Rocket, nhưng giới hạn của mọi regex chạy trong cùng PHP cũng tăng gấp 10 lần, nên tôi không khuyên làm vậy.

🔢 FAQ & nội dung đề xuất

Tài liệu về trình thu thập của Google cho biết thiết lập thu thập dành cho Mediapartners-Google ảnh hưởng tới các sản phẩm quảng cáo như AdSense và Ad Manager. Trình thu thập này không tuân theo quy tắc chung của robots.txt [User-agent: *], nên muốn chặn thì phải ghi riêng tên của nó. Nếu vấn đề là lỗi 404 của địa chỉ bị hỏng, để máy chủ web trả thẳng 404 cho tệp không tồn tại sẽ không đụng tới việc thu thập quảng cáo.

Chuẩn cache HTTP quy định khi lưu phản hồi phải lưu kèm các header phản hồi đã nhận, nên các bản sao CDN lưu trước khi sửa vẫn được gửi đi dưới dạng [Content-Type: text/html] không có charset. Khi hết hạn lưu và được lấy lại từ máy chủ gốc, hoặc khi bạn chỉ xóa địa chỉ của bài viết đó khỏi cache CDN, chúng sẽ chuyển sang header mới có charset.

Chuẩn URL quy định ký tự ngoài ASCII trong đường dẫn phải được đổi sang mã hóa phần trăm UTF-8 trước khi gửi yêu cầu, nên bên đọc đã đọc đúng tài liệu bằng UTF-8 sẽ yêu cầu bằng tên tệp gốc. Tên tệp chỉ dùng chữ Latinh và chữ số vẫn giữ nguyên dù tài liệu bị đọc nhầm bằng windows-1252, nên địa chỉ không bị hỏng, kể cả ở trang thiếu cả charset trong header phản hồi lẫn meta charset.

ℹ️ Thông báo tiếp thị liên kết
Nội dung trên trang này có chứa liên kết affiliate (tiếp thị liên kết). Khi khách truy cập mua sản phẩm hoặc dịch vụ qua các liên kết này, trang sẽ nhận hoa hồng từ bên bán. Trong quá trình đó, số tiền người mua phải trả (sẽ giảm khi có chương trình khuyến mãi ↓) không hề tăng lên. Giá, khuyến mãi và tình trạng hàng được đăng theo thời điểm viết bài và có thể khác với thực tế, vì vậy hãy kiểm tra lại lần cuối với bên bán trước khi mua. Việc chọn và đánh giá sản phẩm dựa trên tiêu chí riêng của trang; việc có nhận hoa hồng hay không không ảnh hưởng đến thứ tự giới thiệu hay nội dung đánh giá.

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

Prove your humanity: 8   +   7   =