• 28/06 cập nhật đăng ký đăng nhập qua Google & Facebook

Ubuntu 22.04: curl 7.81 làm chết request HTTP/2 khi gửi song song >100 – cách nâng curl 8.x cho PHP-FPM (không đụng hệ thống)

TenHoshi

Administrator
Staff member
Trusted Uploader
Triệu chứng: PHP dùng curl_multi + HTTP/2 (gửi FCM, gọi API song song…) trên Ubuntu 22.04. Bắn lô hơn 100 request cùng lúc thì chỉ khoảng 100 cái thành công, phần còn lại trả HTTP 0, curl_error rỗng, mỗi lô treo 11–15 giây. Chạy vài phút sau thì lại nhanh, nên rất khó bắt bệnh.

Nguyên nhân: Ubuntu 22.04 đi kèm curl 7.81.0. Server HTTP/2 (vd. Google) chỉ cho khoảng 100 stream trên mỗi kết nối. Khi kết nối đã đầy, curl 7.81 mở một kết nối riêng cho từng request dư, và các kết nối này chết hàng loạt. Chạy cùng script với curl 8.x thì không bị.

Kết quả test thực tế, cùng VPS, cùng PHP 8.3, lô 300 request:
Code:
curl 7.81.0
round 1: 11.1s codes={"400":100,"0":200} conns=201
round 2: 11.0s codes={"400":100,"0":200} conns=201
curl 8.11.1
round 1: 0.3s codes={"400":300} conns=3
round 2: 0.1s codes={"400":300} conns=3
(Mã 400 là đúng vì test dùng token giả. Lỗi thật là mã 0.)


1. Kiểm tra VPS có bị không
Bash:
head -2 /etc/os-release; curl --version | head -1
PHP=/www/server/php/83/bin/php # đổi theo bản PHP của bạn (aaPanel). Không dùng aaPanel: PHP=$(which php)
$PHP -r 'echo curl_version()["version"], "\n";'
ldd $PHP | grep -Ei 'curl|ssl|nghttp2'
Nếu PHP báo 7.x và ldd trỏ tới /lib/x86_64-linux-gnu/libcurl.so.4 (PHP dùng libcurl của hệ thống) thì làm theo cách bên dưới được.

2. Các cách xử lý – ưu / nhược
A. Giữ curl cũ, chỉ chạy tối đa 100 request song song mỗi lô
  • Ưu: không đụng gì tới server, sửa 1 hằng số trong code.
  • Nhược: chậm hơn (lô nhỏ, nhiều vòng); vẫn dễ dính lại nếu chỗ khác trong code bắn song song nhiều.
B. Build curl 8.x vào /opt, chỉ cho php-fpm dùng qua LD_LIBRARY_PATH (cách mình đang dùng)
  • Ưu: không thay curl/libcurl của hệ thống → apt, git, các dịch vụ khác không bị ảnh hưởng.
  • Ưu: hoàn tác chỉ bằng xoá 1 dòng + restart; làm xong trong khoảng 10 phút, không downtime (php-fpm restart graceful).
  • Nhược: tự build nên phải tự theo dõi và cập nhật bản vá bảo mật của curl.
  • Nhược: cập nhật/cài lại PHP qua aaPanel có thể ghi đè file init → mất curl mới, phải thêm lại dòng.
  • Nhược: PHP CLI (cron chạy lệnh php file.php) vẫn dùng bản cũ, trừ khi thêm LD_LIBRARY_PATH vào lệnh cron.
C. Cài curl 8.x vào /usr/local/lib để thay libcurl cho toàn hệ thống
  • Ưu: mọi thứ (CLI, php-fpm, mọi bản PHP) đều dùng bản mới, không cần sửa init.
  • Nhược: ảnh hưởng tất cả chương trình dùng libcurl (apt, git…), lỗi là lỗi cả server; khó hoàn tác sạch.
D. Nâng Ubuntu lên 24.04 (curl 8.5 từ apt)
  • Ưu: bản chính thống, được Ubuntu vá bảo mật tự động.
  • Nhược: phải nâng cả hệ điều hành, có downtime, rủi ro vỡ aaPanel/PHP/extension; với VPS đang chạy thật thì nặng nhất.
  • Lưu ý: mình chưa test trực tiếp lỗi này trên 8.5, nên chạy lại bài test ở mục 5 sau khi nâng.
E. Biên dịch lại PHP với curl mới
  • Ưu: sạch nhất, không cần biến môi trường.
  • Nhược: tốn công nhất; aaPanel cập nhật PHP là build đè mất.

3. Chọn bản curl nào – ưu / nhược
  • 7.81.0 (mặc định Ubuntu 22.04): Ubuntu vá bảo mật tự động. Nhược: dính lỗi HTTP/2 ở trên khi bắn hơn 100 request song song.
  • 8.5.0 (mặc định Ubuntu 24.04): cài bằng apt, được vá tự động. Nhược: phải lên Ubuntu 24.04, không cài được trên 22.04 bằng apt.
  • 8.11.1 (bản mình build, đã test lô 300 qua): chạy được với OpenSSL 3.0.2 + nghttp2 1.43 có sẵn trên 22.04, không cần cài thêm thư viện lạ. Nhược: tự build, tự vá.
  • Bản 8.x mới nhất: nhiều sửa lỗi nhất. Nhược: có thể đòi thư viện mới hơn bản có sẵn của 22.04 — nếu ./configure báo thiếu thì cài thêm hoặc lùi về 8.11.1.


4. Làm theo cách B (từng bước)
Bước 1 – Build curl vào /opt/curl8 (không ảnh hưởng gì đang chạy):
Bash:
apt-get install -y build-essential libssl-dev libnghttp2-dev zlib1g-dev
cd /usr/local/src && wget https://curl.se/download/curl-8.11.1.tar.gz && tar xzf curl-8.11.1.tar.gz && cd curl-8.11.1
./configure --prefix=/opt/curl8 --with-openssl --with-nghttp2 --without-libpsl \ --enable-shared --disable-static --enable-versioned-symbols
make -j"$(nproc)" && make install
/opt/curl8/bin/curl --version | head -1 # curl 8.11.1 ... nghttp2/...
--enable-versioned-symbols để PHP không in cảnh báo "no version information available" lúc khởi động (thiếu cũng chạy được, chỉ là log bẩn).

Bước 2 – Thử bằng CLI trước (chưa đụng web):
Bash:
LD_LIBRARY_PATH=/opt/curl8/lib $PHP -r 'echo curl_version()["version"], "\n";' # → 8.11.1
Rồi chạy bài test ở mục 5 với cả bản cũ và bản mới để so sánh.

Bước 3 – Áp cho php-fpm (aaPanel, ví dụ PHP 8.3 → file init /etc/init.d/php-fpm-83):
Bash:
F=/etc/init.d/php-fpm-83
grep -n 'LD_LIBRARY_PATH' $F || sed -i '2i export LD_LIBRARY_PATH=/opt/curl8/lib' $F
$F restart
for p in $(pgrep -f 'php-fpm: master'); do echo "$p $(grep -q curl8 /proc/$p/maps && echo curl8 OK || echo CHUA) $(tr '\0' ' ' </proc/$p/cmdline)"; done
Dòng master của bản PHP vừa sửa phải báo curl8 OK. Master của bản PHP khác (vd. 8.1) báo CHUA là bình thường. Không dùng aaPanel mà chạy systemd thì thêm Environment=LD_LIBRARY_PATH=/opt/curl8/lib bằng systemctl edit php8.3-fpm.

Kiểm tra qua web (tạo file tạm rồi xoá ngay):
Bash:
echo '<?php echo curl_version()["version"];' > /www/wwwroot/TEN_SITE/_cv.php
curl -s https://TEN_SITE/_cv.php; echo; rm -f /www/wwwroot/TEN_SITE/_cv.php # → 8.11.1

Hoàn tác:
Bash:
sed -i '/LD_LIBRARY_PATH=\/opt\/curl8/d' /etc/init.d/php-fpm-83 && /etc/init.d/php-fpm-83 restart

5. Script test (lô N request HTTP/2 song song)
Script dưới đây gửi request validate_only tới FCM với token giả. FCM chỉ kiểm tra rồi trả 400, không có thông báo nào gửi tới máy thật. Tham số đầu là đường dẫn file service account JSON của Firebase.
PHP:
<?php
$sa=json_decode(file_get_contents($argv[1]),true); $N=(int)($argv[2]??300); $rounds=(int)($argv[3]??3);
$b64=fn($s)=>rtrim(strtr(base64_encode($s),'+/','-_'),'=');
$now=time(); $h=$b64(json_encode(['alg'=>'RS256','typ'=>'JWT']));
$c=$b64(json_encode(['iss'=>$sa['client_email'],'scope'=>'https://www.googleapis.com/auth/firebase.messaging','aud'=>'https://oauth2.googleapis.com/token','iat'=>$now,'exp'=>$now+3600]));
openssl_sign("$h.$c",$sig,$sa['private_key'],'SHA256');
$ch=curl_init('https://oauth2.googleapis.com/token');curl_setopt_array($ch,[CURLOPT_POST=>1,CURLOPT_RETURNTRANSFER=>1,CURLOPT_POSTFIELDS=>http_build_query(['grant_type'=>'urn:ietf:params:oauth:grant-type:jwt-bearer','assertion'=>"$h.$c.".$b64($sig)])]);
$tok=json_decode(curl_exec($ch),true)['access_token']??''; if(!$tok) exit("no token\n");
$url='https://fcm.googleapis.com/v1/projects/'.$sa['project_id'].'/messages:send';
echo 'curl ', curl_version()['version'], "\n";
$mh=curl_multi_init(); curl_multi_setopt($mh,CURLMOPT_PIPELINING,CURLPIPE_MULTIPLEX);
for($r=1;$r<=$rounds;$r++){ $hs=[];$t0=microtime(true); for($i=0;$i<$N;$i++){ $c=curl_init($url); curl_setopt_array($c,[CURLOPT_POST=>1,CURLOPT_POSTFIELDS=>json_encode(['validate_only'=>true,'message'=>['token'=>'fake'.$i,'data'=>['a'=>'b']]]), CURLOPT_HTTPHEADER=>["Authorization: Bearer $tok",'Content-Type: application/json'],CURLOPT_RETURNTRANSFER=>1,CURLOPT_TIMEOUT=>15,CURLOPT_CONNECTTIMEOUT=>10, CURLOPT_HTTP_VERSION=>CURL_HTTP_VERSION_2TLS,CURLOPT_PIPEWAIT=>true]); curl_multi_add_handle($mh,$c); $hs[]=$c; } do{$st=curl_multi_exec($mh,$run); if($run) curl_multi_select($mh);}while($run>0&&$st===CURLM_OK); $codes=[];$errs=[];$conn=[]; foreach($hs as $c){$k=curl_getinfo($c,CURLINFO_HTTP_CODE);$codes[$k]=($codes[$k]??0)+1; if($k==0){$e=curl_error($c);$errs[$e]=($errs[$e]??0)+1;} $conn[curl_getinfo($c,CURLINFO_LOCAL_PORT)]=1; curl_multi_remove_handle($mh,$c);} printf("round %d: %.1fs codes=%s conns=%d errs=%s\n",$r,microtime(true)-$t0,json_encode($codes),count($conn),json_encode($errs));
}
Bash:
echo "== curl cũ =="; $PHP /root/h2test.php /duong-dan/service-account.json 300 3 2>/dev/null
echo "== curl mới =="; LD_LIBRARY_PATH=/opt/curl8/lib $PHP /root/h2test.php /duong-dan/service-account.json 300 3 2>/dev/null
Đạt: bản mới cả 3 round codes={"400":300}, không có mã 0, conns chỉ vài cái.

6. Lưu ý sau khi làm
  • Mỗi lần cập nhật PHP qua aaPanel: kiểm tra lại bằng file _cv.php, ra 7.81 thì chạy lại bước 3.
  • Theo dõi tin bảo mật của curl (curl.se/docs/security.html); có bản vá thì build lại bản mới vào /opt/curl8 rồi restart php-fpm.
  • Muốn tăng lô song song lên trên 100 thì nên đợi chạy thật ổn định một đợt rồi mới tăng.
 

Guest Post 2022

Top