Xử lý 200 triệu yêu cầu mỗi ngày bằng CGI-bin
(simonwillison.net)- Jake Gold chạy một chương trình CGI viết bằng Go + SQLite trên AMD 3700X 16 luồng để kiểm tra CGI kiểu thập niên 1990 có thể chịu tải đến mức nào trên phần cứng hiện đại
- Kết quả cho thấy ngay cả trên phần cứng phổ thông, CGI vẫn có thể xử lý hơn 2.400 yêu cầu mỗi giây, tức hơn 200 triệu yêu cầu mỗi ngày
- Vì CGI khởi động, chạy rồi kết thúc một tiến trình cho mỗi yêu cầu, trước đây chi phí overhead rất lớn; để giảm chi phí này, các cách tiếp cận như PHP và FastCGI đã xuất hiện
- Trong datasette-ripgrep năm 2020, Simon Willison gọi CLI ripgrep viết bằng Rust để xử lý tìm kiếm, qua đó thay đổi niềm tin lâu nay rằng nên tránh chạy tiến trình trong lúc xử lý yêu cầu web
- Với các ngôn ngữ khởi động nhanh như Go và Rust cùng môi trường nhiều CPU, xử lý kiểu CGI có thể trở thành một lựa chọn thực tế hơn trước, nhưng không phải là cách được khuyến nghị nói chung
Nhìn lại hiệu năng CGI trên phần cứng hiện đại
- Serving 200 million requests per day with a cgi-bin là bài viết của Jake Gold thử nghiệm hiệu năng CGI kiểu thập niên 1990 bằng chương trình CGI Go + SQLite
- Môi trường thử nghiệm là hệ thống dùng AMD 3700X 16 luồng
- Kết quả cốt lõi là ngay cả với CGI, phần cứng phổ thông vẫn có thể xử lý hơn 2.400 yêu cầu mỗi giây, tức hơn 200 triệu yêu cầu mỗi ngày
- CGI hoạt động bằng cách khởi động một tiến trình riêng cho mỗi yêu cầu đến, chạy tiến trình đó rồi kết thúc
- Cộng đồng web thời kỳ đầu đã tạo ra PHP và FastCGI, vốn giữ mã thường trú trong bộ nhớ để giảm chi phí bổ sung, nhằm tránh overhead này
Vì sao CGI có thể không còn tệ như trước
- Một trong những nút thắt cổ chai của CGI trước đây là các web script được viết bằng những ngôn ngữ như Perl, Python, Java, vốn không được thiết kế với mục tiêu khởi động cực nhanh
- Ngày nay, nếu dùng Go và Rust, cách xử lý yêu cầu theo phong cách CGI có thể hoạt động hiệu quả hơn nhiều
- Năm 2020, khi tạo datasette-ripgrep, Simon Willison đã gọi công cụ CLI ripgrep viết bằng Rust qua shell để thực hiện tìm kiếm và thu được kết quả tốt
- Vì chương trình CGI chạy dưới dạng tiến trình riêng, nó cũng có thể phù hợp với kiến trúc tận dụng nhiều CPU
- Máy chủ hiện đại có thể có 384 luồng CPU
- Ngay cả VM nhỏ cũng có thể có 16 CPU
- Hiệu năng CPU và bộ nhớ cũng đã nhanh hơn trước rất nhiều
- Cách viết ứng dụng web kiểu năm 1998 cũng trở thành một đối tượng thử nghiệm thú vị khi gặp Go và Rust, nhưng không phải là lựa chọn mặc định mà hầu hết dịch vụ nên làm theo
1 bình luận
Các ý kiến trên Hacker News
Ngày nay CGI khá nhanh, kể cả khi dùng những thứ như Python
Nếu khởi động một script CGI tốn 400ms CPU và máy chủ có 64 core, thì có thể xử lý 160 request mỗi giây, tức 14 triệu lượt hit mỗi ngày trên mỗi máy chủ
Nếu một dịch vụ web không tính tài nguyên tĩnh mà còn chật vật với vài triệu request mỗi ngày, thì nút thắt không phải là thời gian khởi động tiến trình CGI
Trước đây có lẽ tôi đã gọi nó là “một công nghệ nhàm chán đã được hỗ trợ lâu trong thư viện chuẩn Python”, nhưng những maintainer Python còn lại dường như xem sự ổn định và tương thích ngược là có hại, và đã loại bỏ khỏi thư viện chuẩn những module quá nhàm chán và ổn định
module cgi đã bị loại bỏ trong Python 3.13
Tôi đã dùng Python hằng ngày trong phần lớn 25 năm qua nên vẫn có thói quen dùng Python để tạo prototype, nhưng giờ thấy tiếc vì điều đó và đang phân vân giữa JS và Lua
Điều thú vị là ở đó có liên kết tới https://peps.python.org/pep-0206/ ngày 14/7/2000, tức 25 năm trước, khi đó gói cgi cũng đã được mô tả là “thiết kế kém và giờ gần như không thể sửa được”
Có vẻ gói https://github.com/jackrosenthal/legacy-cgi cung cấp một bản thay thế drop-in cho module trong thư viện chuẩn
Hỗ trợ đó vẫn còn trong CGIHTTPRequestHandler của module http.server
Những gì có trong module cgi chỉ là vài hàm để parse dữ liệu form HTML
Nhưng tôi không chắc vì sao sau đó lại cân nhắc JS, vốn không có thư viện chuẩn đúng nghĩa, làm phương án thay thế
Lua cũng không có module CGI trong thư viện chuẩn
Giờ thì nó gần với việc tìm ra những phần lãng phí đến vô lý được đưa vào để phát hành nhanh, rồi dừng chúng lại
Nếu làm đúng cách, ứng dụng có thể hầu như không thêm độ trễ nào ngoài việc truy cập storage
Tôi học Python từ bản 1.6, nhưng chủ yếu dùng cho scripting hệ điều hành
Giai đoạn 1999–2003, tôi dùng Tcl trong các module Apache và IIS, rồi liên tục viết lại module bằng C và đã phải học quá nhiều bài học theo cách khó khăn
Gần đây tôi thử chạy binary Golang, RabbitMQ, Redis, MySQL đều trên cùng một máy chủ mini giá 350 USD, và nó xử lý ổn định 5.000 request mỗi giây
Quy đổi 24 giờ mỗi ngày là 400 triệu request
Thật ngạc nhiên khi các công cụ miễn phí ngày nay tuyệt vời đến vậy, và cũng ngạc nhiên không kém khi chúng ta vẫn trả nhiều tiền như thế cho các nhà cung cấp cloud
Đây không phải là so sánh hoàn toàn tương đương, nhưng quá trình phát triển và tinh chỉnh mọi thứ trên thiết bị dưới tầng hầm thật sự rất thú vị
Thật kỳ lạ khi cứ tiếp tục chịu khoản overhead này mà không có lý do nào ngoài “vì Google làm thế”
Tôi nhất định phải viết một bài về kiến trúc monolith dạng module mà chúng tôi đang dùng rất hiệu quả
Bạn có thể thuê máy chủ dedicated từ một công ty hosting và dùng thoải mái. Chỉ là sẽ bị ràng buộc khá nhiều bởi giới hạn băng thông hoặc lưu lượng truyền tải
Cloud được dùng vì có nhiều lợi ích liên quan. Ví dụ VC và nhà đầu tư cũng nắm cổ phần trong các công ty cloud, và họ sợ khoản đầu tư thất bại vì một đợt tăng vọt traffic tưởng tượng vốn thực tế sẽ không đến
Đội bán hàng cloud rất giỏi đánh vào nỗi bất an của nhà đầu tư
Nếu ổ đĩa hỏng khi tôi đang đi du lịch thì bó tay, và nếu lỡ tự khóa mình ra ngoài thì cũng không có terminal serial dự phòng để fallback
Cũng có thể cấu hình như dân r/homelab, nhưng khi đó không rõ liệu có thật sự tiết kiệm tiền hay không, nhất là nếu tính cả thời gian của tôi
Kết luận lại, tôi cho rằng nhà cung cấp cloud là một thương vụ khá tốt nhờ lợi thế kinh tế theo quy mô
Lạ là không thể lấy riêng từng thứ một
Hãy tưởng tượng có thể vận hành một doanh nghiệp hay dịch vụ lớn đến đâu chỉ với 4 ổ 20TB và một CPU vừa phải
Không dễ có được cấu hình như vậy từ nhà cung cấp cloud
Phần nói rằng “cộng đồng web thời kỳ đầu đã nhanh chóng học được rằng đây là ý tưởng tồi và phát minh ra các công nghệ như PHP” thì nói chính xác, công nghệ cốt lõi là mod_php
Bản thân PHP không khác Perl về cách thực thi, nhưng nhờ các lựa chọn thiết kế của mod_php so với mod_perl, script PHP có thể được ném thẳng lên server mà vẫn chạy nhanh, còn mod_perl thì cần một chút suy nghĩ và phép thuật để chạy được
Nó có quản lý nội dung, upload bài tập, lịch sự kiện, quản lý điểm, chat thời gian thực, forum, và tất cả đều là C thuần trên CGI, làm việc với nó đúng là địa ngục
Ngày tôi biết đến PHP, tôi gần như muốn khóc, vì mọi thứ mà trước đó tôi phải vất vả viết từ đầu bằng cách đọc RFC hoặc reverse-engineer HTTP thì trong PHP chỉ là các lệnh gọi hàm đơn giản
Tôi không còn phải debug một implementation urlencode vụng về hay mất cả ngày vì một ký tự carriage return kỳ lạ trong header HTTP nữa
Có vẻ phiên bản đầu tiên của module Apache là dành cho PHP/FI Version 2.0 vào năm 1996: https://www.php.net/manual/phpfi2.php#module
Nếu cgi-bin cần truy cập DB thì mỗi lần tiến trình khởi động đều phải mở kết nối
Việc giữ mã trong bộ nhớ như FastCGI không chỉ để tránh chi phí thời gian khởi động, mà còn vì có thể có pool kết nối DB, hoặc ít nhất là kết nối bền vững theo từng thread
Đã từng như vậy khi vừa làm “Python là single-thread nên cứ chạy nhiều instance” vừa làm “Python chậm nên cứ chạy nhiều instance”
Khi quy mô lớn lên, cuối cùng phải dùng pool kết nối dùng chung bên ngoài Python như pgbouncer, cùng rất nhiều tinh chỉnh, để xử lý tải mà không làm DB chết
Tất nhiên sau đó, khi triển khai lại bằng một ngôn ngữ đa luồng có hiệu năng ở mức ổn, mọi thứ lại trở nên rất đơn giản
Về cơ bản là chạy một daemon riêng đóng vai trò proxy
Nếu dùng Unix socket thay vì TCP/IP thì chi phí kết nối tương đối thấp hơn
Trên phần cứng này mà ứng dụng hello world đạt 2.400 request/giây thì chẳng phải hơi tệ sao
Cũng không rõ đánh đổi hiệu năng để lấy chính xác điều gì. Mã cũng đâu có đơn giản hơn
Những website như vậy chỉ là một phần trong tổng số
Có vẻ cũng chịu được một đợt traffic bomb từ HN
Hôm qua cũng đã được thảo luận: https://news.ycombinator.com/item?id=44464272
Ở công ty, thỉnh thoảng chúng tôi vẫn phục vụ thư mục cgi-bin cho các web app nội bộ cần làm nhanh
Nếu giữ mọi thứ đơn giản thì tính tiện dụng rất tốt
CGI không có nghĩa là phải trực tiếp print HTTP/1.0 ra stdout
Ví dụ, dùng wsgiref.handlers.CGIHandler tích hợp sẵn của Python thì có thể chạy bất kỳ ứng dụng WSGI nào như một script CGI
import wsgiref.handlers, flask
app = flask.Flask(name)
wsgiref.handlers.CGIHandler().run(app)
Chúng tôi chạy script bằng uwsgi và plugin CGI của nó[1]
Cảm giác cách này đơn giản và linh hoạt hơn việc chạy Apache hoặc lighttpd chỉ vì mod_cgi
Vì uwsgi chạy dưới dạng unit systemd, nên cũng có thể tận dụng toàn bộ các tính năng hardening và sandboxing của systemd
Điểm tiện lợi trong xử lý CGI của uwsgi mà mod_cgi không có là có thể chỉ định interpreter dùng cho một định dạng file cụ thể
cgi = /cgi-bin=/webapps/cgi-bin/src
cgi-allowed-ext = .py
cgi-helper = .py=/webapps/cgi-bin/venv/bin/python3 # all dependencies go here
Thời gian đến byte đầu tiên là 250~350ms, chấp nhận được với mục đích của chúng tôi
[1]: https://uwsgi-docs.readthedocs.io/en/latest/CGI.html
Gần đây tôi dùng Apache cho một side project, và một trong các lý do là tính năng .htaccess, nên cũng đã có cuộc trò chuyện tương tự
Đặt file .htaccess ở bất cứ đâu thì Apache có thể đọc nó như cấu hình server bổ sung cho mỗi request: https://httpd.apache.org/docs/2.4/howto/htaccess.html
Lý do lớn khiến người ta tránh việc này là hiệu năng. Mỗi request cần thêm một lần truy cập đĩa, và nếu có thể thì đưa vào file cấu hình chính luôn vẫn tốt hơn
Nhưng hiện nay hầu hết server đều có SSD, và nhiều khả năng Linux cũng có RAM dư để dùng cho cache hệ thống file
Tất nhiên Apache vẫn phải parse cấu hình cho từng request chứ không phải chỉ một lần, nên hiệu năng vẫn kém đi đôi chút
Dù vậy, xét việc CPU server ngày nay phần lớn mạnh hơn, trong nhiều trường hợp sử dụng thì có thể chấp nhận được
Side project này đang ở phiên bản rất sớm nhưng đã được dùng: https://github.com/StaticPatch/StaticPatch/tree/main
“Tôi không phải lập trình viên thực thụ. Tôi cứ ghép thứ này thứ kia cho đến khi nó chạy rồi chuyển sang việc khác. Các lập trình viên thực thụ sẽ nói ‘Ừ, nó chạy đấy, nhưng chỗ này chỗ kia đang rò rỉ bộ nhớ. Chẳng phải nên sửa sao?’ Tôi thì chỉ cần khởi động lại Apache sau mỗi 10 request.”
PHP đã tiến rất xa kể từ đó, nhưng phần lớn là quá trình sửa các sai lầm ban đầu
“Lý do PHP 8 tốt hơn nhiều là vì trong đó có ít mã của tôi hơn nhiều.”
Vì lựa chọn này mà 99,99% request HTTP bị chậm lại bởi những lần đọc đĩa không cần thiết
Tôi đã suy nghĩ thêm về việc này như một phần của quy trình tạo prototype nhanh
Với các ngôn ngữ JIT hiện đại, nếu không đi theo mô hình FastCGI thì thời gian khởi động có lẽ phần lớn sẽ bị chi phối bởi import
Tôi nảy ra ý này khi bắt đầu dùng web server h2o cho các script cục bộ; với mruby và handler FastCGI, có thể viết file cấu hình gọn gàng, nhanh chóng, và tốc độ cũng rất nhanh: https://h2o.examp1e.net/configure/fastcgi_directives.html
Một trường hợp hữu ích khác là khi cho phép khách hàng mở rộng phần mềm cục bộ bằng code của họ
Ví dụ, thay vì bắt họ dùng MCP để mở rộng công cụ AI, có thể để họ triển khai một cấu trúc request cụ thể bằng CGI
Tôi tự hỏi liệu bản thân dịch vụ MCP có thể được triển khai bằng CGI không
Framework MCP cũng có thể phơi bày chức năng dưới dạng một chương trình hỗ trợ cả hai chế độ thực thi
Có lẽ cần đào sâu hơn vào đặc tả
Với tôi,
inetdchính là CGINhờ vậy mà Internet trở nên thú vị hơn nhiều
Tôi từng tự host nhiều shell script bằng inetd, và thậm chí có cả một HTTP server viết hoàn toàn bằng Bash
VPS đó đã biến mất từ lâu, và hồi ấy tôi cũng không dùng quản lý phiên bản
Chiếc laptop dùng để viết nó cũng không còn
Dù vậy, nó thực sự rất vui
Việc triển khai dễ như Makefile + scp, còn kiểm thử là một Bash script khác kết hợp nhiều
netcatvới grepĐúng là những ngày tháng tuyệt vời