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
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.
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-8Vị 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 -115199:<meta charsetCá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 -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 404Cá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.
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 itCá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-8curl -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-storeLỗ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 tra | Trước khi sửa | Sau khi sửa |
|---|---|---|
| Hình ảnh không tồn tại | 319.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ỏng | 1,05 giây (404 của WordPress) | 0,014 giây (404 của nginx) |
| Header phản hồi của HTML trong cache | text/html | text/html; charset=utf-8 |
| Header phản hồi do PHP vẽ trực tiếp | text/html; charset=UTF-8 | Như nhau (charset một lần) |
| Hình ảnh có tồn tại | 200 | Vẫ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
HTTP 404
Lỗi HTTP 404 (lỗi phía máy khách): nguyên nhân và cách khắc phục
Trang lỗi HTTP 404, một trong các mã trạng thái HTTP, xuất hiện khi không tìm thấy trang.
AdSense
Thiết lập ads.txt trong WordPress: khắc phục cảnh báo AdSense
ads.txt là tệp văn bản đặt ở thư mục gốc của trang web, công khai danh sách những người bán được ủy quyền bán không gian quảng cáo của tên miền này.
Cache WP Rocket
WP Rocket với URL tiếng Hàn: nguyên nhân nginx không tìm thấy tệp cache và cách khắc phục bằng njs
Với bài viết có slug tiếng Anh, nginx trả thẳng tệp cache, nhưng với bài viết có slug tiếng Hàn, dù có cùng tệp cache, tệp đó chỉ được trả sau khi PHP chạy một lần.
ℹ️ 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á.