เปิด opcache.jit แล้วไม่ได้ผล เพราะ ionCube ทำให้ PHP ปิด JIT เอง
ค่า JIT ที่ตั้งไว้ในไฟล์ ini ไม่ได้แปลว่า JIT ทำงานอยู่ ต้องถามจาก FPM ตัวที่รับคำขอจริง และบนเครื่องที่ DirectAdmin ดูแล ต้องมีตัวคอยตั้งค่านั้นให้ใหม่ทุกครั้งที่ PHP ถูก build
สรุปใน 1 นาที
- อาการ/ปัญหา
- ตั้ง opcache.jit=tracing กับ jit_buffer_size ครบแล้ว แต่ PHP ปิด JIT เองเพราะมี ionCube Loader
- ใครควรอ่าน
- ผู้ดูแล PHP บน DirectAdmin ที่อยากเปิด JIT
- ทางแก้แบบสั้น
- พิสูจน์ในคำขอจริงของ FPM ไม่ใช่ CLI ถอด ionCube หลังรู้ว่าใครใช้ และตั้งตัวแก้ให้เองเมื่อ DirectAdmin build ทับ อย่าคาดหวังเกิน 0-5% กับ Laravel
พอดีผมไล่เร่งความเร็วเว็บจองบริการตัวหนึ่งบนโฮสต์รวมที่ใช้ DirectAdmin รัน PHP 8.4 แบบ FPM ส่วน Redis สำหรับ cache กับ session และการยืด TTL ที่ edge ทำไปแล้ว ขั้นต่อไปที่คิดไว้คือเปิด JIT ของ OPcache เพราะใน config มี jit_buffer_size 64M วางอยู่แล้ว ผมเลยเข้าใจไปเองว่าแค่นี้ JIT ก็ทำงานให้แล้ว ซึ่งผิดตั้งแต่ข้อแรก และพอแก้ข้อแรกแล้วก็ยังผิดอีกชั้นหนึ่ง คือ JIT ถูกปิดโดยที่ไม่มี error สักบรรทัดขึ้นหน้าเว็บ บทความนี้เล่าตามลำดับที่ผมเจอ
ตั้ง buffer อย่างเดียวไม่พอ และตั้ง jit=tracing แล้วก็ยังไม่การันตี
PHP 8.4 ไม่ได้เปิด JIT ให้ใช้งานจริงตามค่าเริ่มต้น ต้องตั้งทั้งโหมด opcache.jit และขนาด opcache.jit_buffer_size ถ้ามีแค่ buffer โดยไม่มีโหมด ก็ไม่มีโค้ดส่วนไหนถูกคอมไพล์เป็นเครื่อง ผมเลยเพิ่มสองบรรทัดนี้ลงไฟล์ของเราเอง ซึ่งชื่อขึ้นต้นด้วย 99 เพื่อให้ถูกอ่านทีหลังสุด
; php.conf.d/99-custom-opcache.ini
opcache.jit=tracing
opcache.jit_buffer_size=64M
ตรงนี้คือจุดที่ผมคิดว่าจบแล้ว แต่ JIT ของ PHP ทำงานโดยครอบการรันโค้ดของ Zend Engine ส่วนตัวขยายบางตัวทำเรื่องเดียวกัน ionCube Loader เป็นหนึ่งในนั้น เพราะต้องดักตอนรันไฟล์ที่เข้ารหัสไว้ เมื่อ PHP เริ่มทำงานแล้วเจอ extension แบบนี้โหลดอยู่ มันจะบันทึกข้อความว่า JIT ใช้ร่วมกับ extension บุคคลที่สามไม่ได้ แล้วปิด JIT ทิ้งเอง ค่าที่ตั้งไว้ยังอยู่ครบทุกบรรทัด buffer ก็ยังถูกจอง แต่ตัวเปิดปิดจริงเป็น false
บนเครื่องที่ผมดูแล PHP ที่ DirectAdmin build ให้ใส่ ionCube loader มากับ 8.2, 8.3 และ 8.4 ผ่านไฟล์ 10-directadmin.ini ซึ่ง DirectAdmin เป็นเจ้าของ ผมไม่ได้เป็นคนใส่ บรรทัดนั้นเลยอยู่ตรงนั้นมาตลอดโดยไม่มีใครเห็นว่ามันขวาง JIT
คำสั่ง php -i | grep jit หรือการเปิดไฟล์ ini ดูจะบอกได้แค่ว่าเราตั้งอะไรไว้ ไม่ได้บอกว่า PHP ใช้ค่านั้นจริงหรือเปล่า หลักฐานที่เชื่อได้มีอย่างเดียวคือ opcache_get_status() ที่รันในโปรเซสเดียวกับที่รับคำขอ
พิสูจน์ในคำขอจริงของ FPM ไม่ใช่ใน CLI
ข้อนี้คนมักพลาดคือเช็กด้วย php -v ใน SSH แล้วถือว่าผ่าน แต่ CLI เป็นอีก SAPI หนึ่ง มี ini ของตัวเอง มี opcache.enable_cli แยก และไม่ได้อ่านค่าเดียวกับ pool ของ FPM เลย ผลจาก CLI เลยพิสูจน์อะไรเกี่ยวกับ pool ไม่ได้
วิธีที่ผมใช้คือวางไฟล์ PHP ชั่วคราวชื่อสุ่มไว้ใน docroot ของโดเมนที่รันบน 8.4 เรียกผ่าน HTTPS หนึ่งครั้ง แล้วลบทิ้งทันที
<?php
// jit-probe-<random>.php (เรียกครั้งเดียวแล้วลบ)
$s = opcache_get_status(false);
echo json_encode([
'jit' => $s['jit']['on'] ?? null,
'buf' => $s['jit']['buffer_size'] ?? null,
'ioncube' => extension_loaded('ionCube Loader'),
]);
ถ้าไม่อยากวางไฟล์ probe อีกทางหนึ่งคือไปหาข้อความที่ PHP บันทึกไว้ตอน worker เริ่มทำงาน PHP บันทึกข้อความเรื่องนี้ไว้ตอนเริ่มทำงาน ถ้าเปิด error log ของ FPM จะเจอบรรทัดทำนองนี้ ซึ่งบอกชัดกว่าที่คิด แต่มันไปอยู่ใน log ไม่ได้ขึ้นหน้าเว็บ เลยไม่มีใครเห็นถ้าไม่ได้เปิดดู
# หาข้อความที่ PHP ปิด JIT เอง (ชื่อไฟล์ log ขึ้นกับการตั้งค่าของแต่ละเครื่อง)
grep -i "JIT is incompatible" /var/log/php-fpm84/error.log | tail -n 3
แต่ผมไม่ถือ log เป็นหลักฐานสุดท้าย เพราะ log บอกว่าเคยปิดตอนไหน ไม่ได้บอกว่าตอนนี้ pool ที่รับคำขออยู่เปิดหรือปิด probe ให้คำตอบของตอนนี้ log ให้ประวัติ ผมใช้ทั้งสองอย่างคู่กัน
ก่อนแก้ ผลที่ได้คือ jit เป็น false พร้อม ioncube เป็น true ทั้งที่ buf ขึ้นเป็นขนาด 64M ตามที่ตั้ง นั่นแหละคือคำตอบว่าทำไมที่ผ่านมาไม่มีอะไรเปลี่ยน

ก่อนถอด ionCube ต้องรู้ก่อนว่าใครใช้
จะถอด loader ออกก็ต้องแน่ใจก่อนว่าไม่มีใครบนเวอร์ชันนั้นพึ่งมัน ผมไล่ดูแล้วพบว่ามีแอปบิลลิ่งเก่าตัวเดียวที่ต้องใช้ ionCube และมันไม่ได้รันอยู่บน 8.4 เลย ส่วนทางฝั่ง 8.4 ผมสแกนทุกบัญชีที่ใช้ 8.4 หาไฟล์ที่เข้ารหัสด้วย ionCube ไฟล์แบบนี้ขึ้นต้นด้วยตัวบอกชนิด //ICB0 ตามด้วยข้อมูลสุ่ม
# อ่านอย่างเดียว ไม่แก้อะไร ทีละบัญชีที่ใช้ PHP 8.4
for u in webuser1 webuser2; do
n=$(nice -n19 ionice -c3 grep -rlsI --include='*.php' -e '//ICB0' \
--exclude-dir=Maildir --exclude-dir=.redis /home/$u | wc -l)
echo "$u: $n"
done
ผมเลือกสแกนจากเนื้อไฟล์ ไม่ได้เชื่อรายชื่อโปรแกรมที่ลูกค้าบอกมา เพราะโฮสต์รวมมีเว็บที่ลูกค้าติดตั้งธีมหรือปลั๊กอินเองได้ ไฟล์ที่เข้ารหัสอาจมากับแพ็กเกจที่เจ้าของเว็บไม่เคยรู้ว่ามี ถ้าวันหนึ่งมีบัญชีใหม่ย้ายเข้ามาพร้อมโค้ดแบบนั้น การสแกนต้องรันซ้ำก่อนจะบอกว่า 8.4 ปลอดภัยสำหรับการถอด loader
ผลคือทุกบัญชีที่ใช้ 8.4 ไม่เจอไฟล์เข้ารหัสสักไฟล์ ผมเลยถอด loader ออกเฉพาะ 8.4 ได้โดยไม่กระทบใคร ส่วนรุ่นที่แอปบิลลิ่งใช้อยู่ผมไม่แตะ
ลำดับที่เปลี่ยนจริง และตัวกันพลาด
ผมเขียนเป็นสคริปต์เดียวที่ทำสำรอง ไฟล์ RESTORE สั้น ๆ และย้อนกลับเองถ้าผลไม่เป็นไปตามคาด ลำดับคือสแกน แล้วคอมเมนต์บรรทัด loader ในไฟล์ที่ DA เป็นเจ้าของ (เฉพาะ 8.4 เท่านั้น) แล้วใส่ค่า JIT ในไฟล์ 99 ของเรา จากนั้น reload FPM แล้วค่อย probe
INI=/usr/local/php84/lib/php.conf.d/10-directadmin.ini
CUS=/usr/local/php84/lib/php.conf.d/99-custom-opcache.ini
sed -i -E 's|^(zend_extension=.*ioncube_loader_lin_8\.4\.so)|;\1 ; off for JIT|' "$INI"
grep -q '^opcache.jit=' "$CUS" || printf '%s\n' \
'opcache.jit=tracing' 'opcache.jit_buffer_size=64M' >> "$CUS"
/usr/local/php84/sbin/php-fpm -t -y /usr/local/php84/etc/php-fpm.conf
systemctl reload php-fpm84 # graceful ไม่ใช่ restart
สองจุดที่ผมยืนยันได้ว่าควรทำเสมอ จุดแรกคือ reload ไม่ใช่ restart worker ที่กำลังตอบคำขออยู่จะทำงานต่อจนจบแล้วค่อยถูกแทน เว็บไม่สะดุด จุดที่สองคือเก็บสถานะ HTTP ของโดเมนทั้งหมดบน 8.4 ไว้ก่อนแก้ แล้วเก็บอีกชุดหลังแก้ ถ้าโดเมนไหนจากต่ำกว่า 500 กลายเป็น 500 ขึ้นไปสคริปต์ย้อนไฟล์ทั้งสองกลับให้เอง
- สแกนไฟล์ ionCube ของทุกบัญชีบนเวอร์ชันที่จะแก้ ถ้าเจอแม้ไฟล์เดียวให้หยุดและดูก่อน
- สำรองไฟล์ ini ทั้งสอง พร้อมเขียนวิธีย้อนกลับไว้ข้างกัน
- คอมเมนต์ loader เฉพาะเวอร์ชันนั้น ใส่โหมดกับ buffer ในไฟล์ 99
- ทดสอบ config ด้วย
php-fpm -tแล้ว reload แบบ graceful - probe ในคำขอจริงจนได้ jit เป็น true และ ioncube เป็น false แล้วลบไฟล์ probe
- เทียบสถานะ HTTP ก่อน/หลังของทุกโดเมน และดู log หา segfault
ไฟล์วิธีย้อนกลับที่วางไว้ข้างไฟล์สำรองสั้นมาก เจตนาให้คนที่เปิดมาตอนตีสามอ่านรู้เรื่องโดยไม่ต้องจำว่าเมื่อก่อนทำอะไรไว้
# RESTORE: คืนไฟล์ ini ทั้งสองจากชุดสำรอง แล้ว reload
cp -a /root/backup-php84-jit/10-directadmin.ini /usr/local/php84/lib/php.conf.d/
cp -a /root/backup-php84-jit/99-custom-opcache.ini /usr/local/php84/lib/php.conf.d/
/usr/local/php84/sbin/php-fpm -t -y /usr/local/php84/etc/php-fpm.conf && systemctl reload php-fpm84
หลังแก้ ผลจาก probe ออกมาเป็น jit true และ ioncube false สถานะ HTTP ของทุกโดเมนบน 8.4 เหมือนเดิมทุกตัว และใน log ไม่มี segfault
ก่อนไปเรื่องกับดัก ผมสรุปเป็นตารางว่าค่าแต่ละอย่างควรอยู่ไฟล์ไหน เพราะนี่คือสิ่งที่ตัดสินว่าค่าจะรอดจากการ build หรือไม่
| รายการ | ไฟล์ที่ควรอยู่ | ใครเป็นเจ้าของ | รอดจาก build ไหม |
|---|---|---|---|
| zend_extension ของ ionCube | 10-directadmin.ini | DirectAdmin | ไม่รอด ถูกสร้างใหม่ |
| opcache.jit และ buffer | 99-custom-opcache.ini | เรา | รอด DA ไม่แตะ |
| pm และ realpath_cache_ttl รายผู้ใช้ | php-fpm.conf.custom2 | เรา | รอด |
| ตัวแก้บรรทัด loader กลับ | cron.daily และ hook หลัง build | เรา | รอด แต่ต้องรันซ้ำหลัง build |
จะเห็นว่าค่าที่ผมคุมเองรอดหมด ส่วนที่ไม่รอดมีอยู่ข้อเดียวคือบรรทัดของ ionCube ที่อยู่ในไฟล์ของ DA ปัญหาเลยแคบลงเหลือการคอยคอมเมนต์บรรทัดเดียวนั้นซ้ำ ซึ่งสคริปต์ทำได้ไม่ยาก
กับดัก DirectAdmin เขียนไฟล์ ini ทับทุกครั้งที่ build
เรื่องที่ทำให้ผมต้องกลับมาแก้คือสัปดาห์เดียวกันนั้น ผมถอด PHP รุ่นเก่าออกและเพิ่ม PHP 8.5 เป็นช่องเสริม พอ CustomBuild build เสร็จ บรรทัด ionCube กลับมาอยู่ใน 10-directadmin.ini เหมือนเดิม และ JIT ก็ปิดอีกครั้งโดยที่ไม่มีอะไรเตือน เพราะ CustomBuild สร้างไฟล์นั้นใหม่ทุกครั้งที่สั่ง build php ตัวคอมเมนต์ที่ผมใส่ไว้ก็ไปพร้อมกับไฟล์เก่า
ผมมีสคริปต์ check-customizations.sh ที่รันทุกวันผ่าน cron.daily และรันซ้ำหลัง build ผ่าน hook ของ CustomBuild ทำหน้าที่คอมเมนต์บรรทัดนั้นกลับและใส่ค่า JIT ให้ใหม่ ภาพรวมของแนวคิดหลายชั้นแบบนี้เขียนไว้แล้วใน ตอนปิดท้ายซีรีส์ย้าย DA และเรื่องไฟล์ 99-custom-*.ini อยู่ใน ตอนเรื่อง CustomBuild ผมจะไม่เล่าซ้ำ เหลือแค่ของใหม่สองข้อที่เจอรอบนี้
hook หลัง build ก็พอ
ผมวางว่าสคริปต์ที่ผูกกับ hook จะแก้ให้ทันทีที่ build เสร็จ แต่ในความจริงมี hook รันก่อนที่ DirectAdmin จะเขียนไฟล์ ini รอบสุดท้าย ผลคือการแก้ของ hook ถูกทับอีกครั้งทันที
รันซ้ำอีกรอบหลัง build
cron.daily ยังอยู่เป็นตาข่ายชั้นล่าง และหลังสั่ง build ทุกครั้งผมรันสคริปต์เองอีกรอบหลังผ่านไปไม่กี่นาที แล้วค่อย probe ซ้ำว่า jit เป็น true
ข้อสองคือรายการเวอร์ชันในลูปของสคริปต์ตอนแรกครอบคลุมแค่ 8.1 ถึง 8.4 พอเพิ่ม 8.5 เข้ามาก็ไม่มีใครดูแล และตัว build ของ 8.5 ก็มี ionCube loader มาให้ด้วย JIT บนช่องนั้นเลยใช้ไม่ได้จนกว่าจะถอด loader เหมือนกัน ต้องเพิ่มเวอร์ชันลงลูปทุกครั้งที่เปิดช่องใหม่ ไม่งั้นการแก้ไม่ครอบคลุม
คาดหวังให้ถูก JIT ของแอป Laravel ได้ราว 0-5% เท่านั้น
ตรงนี้ผมพูดตรง ๆ ว่าเว็บแบบ Laravel ใช้เวลาไปกับ IO และตัวเฟรมเวิร์กเป็นหลัก ไม่ใช่ลูปคำนวณหนัก ๆ ที่ JIT ถนัด ส่วนตัวผมคาดไว้ว่าได้ประมาณ 0 ถึง 5% ผมไม่ได้เก็บตัวเลข before/after ที่วัดจริงไว้ เลยไม่ขอเขียนเปอร์เซ็นต์ที่ได้ ถ้าใครอยากรู้ว่าเว็บตัวเองได้เท่าไร ต้องวัดเอง
ถ้าจะวัดให้ได้ตัวเลขของเว็บตัวเอง ผมแนะนำให้เลือกหน้าที่ใช้งานจริงหน้าเดียว ยิงซ้ำหลายรอบขณะ JIT ปิด แล้วเปิดแล้วยิงซ้ำด้วยจำนวนเท่ากัน เทียบค่ากลางไม่ใช่ค่าเฉลี่ย เพราะรอบแรกหลัง reload ช้ากว่าปกติเสมอจากการที่ OPcache ยังไม่อุ่น และต้องรอให้ buffer ของ JIT เริ่มมีโค้ดที่ร้อนพอก่อนด้วย ความต่างระดับ 2 ถึง 3% จะจมหายในความผันผวนของเครือข่ายง่าย ๆ ถ้าวัดผ่านอินเทอร์เน็ตจริง

ผมไม่ย้าย release หลักของ PHP ทั้งเครื่อง เพราะจะดึงโดเมนอื่นไปด้วย และ symlink php ที่ใช้ร่วมกันถูกเรียกจาก cron ของผู้เช่ารายอื่น ผมไม่เปิด HTTP/2 บน Apache เพราะ nginx ด้านหน้าคุย h2 กับ client อยู่แล้ว ส่วน Apache เห็นแต่ทราฟฟิกพร็อกซีจาก localhost
ของที่ปรับพร้อมกันในรอบนี้และคุ้มกว่า JIT คือค่า pool ของ FPM รายผู้ใช้ ใส่ใน php-fpm.conf.custom2 ของผู้ใช้นั้น (ตัวแปร |CUSTOM2| ใน template ของ pool) ค่านี้ DirectAdmin ไม่เขียนทับ ผมตั้ง pm=dynamic ด้วยจำนวน worker ที่พอดีกับผู้ใช้คนนั้น และ realpath_cache_ttl ที่ยาวขึ้น เวลาจะให้มีผลกับผู้ใช้คนเดียวใช้ task queue ของ DA
# ให้ DA เขียน config ใหม่เฉพาะผู้ใช้นี้ (value=phpfpm ไม่ทำอะไร, rewrite_confs กระทบทุกคน)
echo "action=rewrite&value=httpd&user=webuser" >> /usr/local/directadmin/data/task.queue
/usr/local/directadmin/dataskq d
สรุปคือ ตั้ง JIT แล้วต้องถาม FPM ว่ามันเปิดจริงไหม ต้องรู้ก่อนว่าใครใช้ ionCube ก่อนถอด และต้องมีตัวคอยตั้งค่านั้นให้ใหม่หลังทุก build ส่วนจะเอา JIT ไปหรือเปล่าก็ตามความจำเป็นของแต่ละเครื่อง ถ้าแอปที่เข้ารหัสด้วย ionCube มีหลายตัวบน 8.4 ก็แลกกันไม่ได้ ต้องเลือกอย่างใดอย่างหนึ่ง ผมยังค้างเรื่องวัดเวลาตอบจริงก่อนและหลังเปิด JIT ไว้เป็นงานต่อ เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
อยากให้เราทำให้ไหม?
ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



ความคิดเห็น 0
ยังไม่มีความคิดเห็น — เป็นคนแรกเลย