PHP hiện tại là ngôn ngữ được triển khai rộng rãi trên Internet.
Tuy nhiên PHP lại hay được nhìn nhận là 1 ngôn ngữ có khả năng hiệu suất (performance) kém, do đó khi các website, api có lượng truy cập lớn lại nghĩ tới sử dụng các ngôn ngữ khác như Nodejs, Golang…
Mặc dù vậy chúng ta vẫn có thể cải thiện điều này. Bài viết này sẽ hướng dẫn các bạn 1 số cách cải thiện tốc độ xử lý của PHP, cụ thể là PHP-fpm trên server Nginx.
PHP-fpm là gì?
Không phải tất cả Developer đều quan tâm tới tất cả các khía cạnh của DevOps, và trong số đó không phải ai cũng hiểu cách server xử lý trong 1 thời gian ngắn. Một điều thú vị là khi có 1 request tới server PHP thì không phải PHP xử lý nó trực tiếp mà là các máy chủ (Nginx / Apache), sau đó các máy chủ này mới quyết định truyền dữ liệu, type, header tới PHP.
Trong các ứng dụng sử dụng PHP hiện tại, thì phần lớn file index được cấu hình là index.php, file sẽ nhận mọi request từ server tới. Bây giờ thì chính xác server đã gọi tới các file php như thế nào? Nếu tìm hiểu vấn đề này sâu xa thì tất mất thời gian, do đó ở bài này chúng ta chỉ tìm hiểu 1 cách sơ lược thôi. Đại khái trong giai đoạn mà web server Apache thống trị thì PHP là 1 module trong đó. Khi mà mỗi request được gửi tới server, server sẽ tiến hành xử lý 1 tiến trình (process), process này sẽ bao gồm xử lý php. Phương thức này sẽ gọi mod_php trong Apache, như vậy php là 1 module. Cách tiếp cận này có các hạn chế nhất định, điều mà Nginx đã giải quyết với php-fpm.
Đối với php-fpm thì trách nhiệm quản lý cũng như các quy trình xử lý đều nằm trong chương trình PHP trong server. Nói 1 cách khác là web server (cụ thể là Nginx) sẽ ko quan tâm file PHP ở đâu, nó được load như thế nào nếu như biết cách gửi và nhận data từ nó. Nếu như bạn muốn, bạn có thể coi PHP trong trường hợp này như là 1 server khác và nó sẽ quản lý các tiến trình php trong các request được gửi tới.
Nếu như bạn đã setup server Nginx, bạn sẽ thấy các cấu hình như sau:
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
Chú ý dòng fastcgi_pass unix:/run/php/php7.2-fpm.sock; Cấu hình này chỉ ra cho Nginx sẽ kết nối tới tiến trình PHP thông qua socket có tên là php7.2-fpm.sock . Do đó tất cả các request Nginx gửi data tới file này và nhận output sau đó trả về cho browser.
Tóm lại điều chúng ta cần nắm được ở đây là
- PHP không trực tiếp nhận các request từ trình duyệt, các web server (Nginx) sẽ chặn chúng
- Các web server biết cách kết nối tới các tiến trình PHP và gửi các dữ liệu tới chúng.
- Sau khi các tiến trình PHP xử lý xong chúng sẽ trả output về các web server, web server sẽ trả lại các client
Vậy cuối cùng PHP-FPM là cái gì?
FPM là viết tắt của “Fast Process Manager” điều này có nghĩa là PHP chạy trên server không phải là một tiến trình đơn lẻ, mà nó bao gồm nhiều tiến trình được sinh ra, điều khiển, tắt bới trình quản lý fpm. Nó chính là trình quản lý mà web server sẽ gửi các request tới.
Tại sao phải tối ưu php-fpm?
Nếu server của bạn vẫn đang đáp ứng tốt nhu cầu sử dụng, cũng như đang ổn định thì tất nhiên không việc gì phải tối ưu nó cả. Tuy nhiên trong trường hợp server của bạn đang không đáp ứng đủ nhu cầu, cũng như lượng server của bạn bị hạn chế thì việc tối ưu từng server lại là điều cần thiết.
Một khía cạnh khác là Nginx được xây dựng để xử lý 1 số lượng công việc khổng lồ, nó có thể đáp ứng hàng nghìn kết nối, và thật lãng phí nếu như bạn không sử dụng hết sức mạnh của nó để tối ưu tài nguyên vì Nginx sẽ phải chờ tiến trình PHP hiện tại xử lý xong mới xử lý tới request tiếp theo, điều này không đúng với những ưu điểm mà Nginx được tạo ra.
Vậy tối ưu php-fpm như thế nào?
File cấu hình của php-fpm ở các server có thể sẽ khác nhau, với ubuntu thì nó được để tại /etc/php/7.2/fpm/php-fpm.conf (Đây là đường dẫn của php version 7.2)
Một vài config ban đầu bạn có thể thấy
;;;;;;;;;;;;;;;;;;;;; ; FPM Configuration ; ;;;;;;;;;;;;;;;;;;;;; ; All relative paths in this configuration file are relative to PHP's install ; prefix (/usr). This prefix can be dynamically changed by using the ; '-p' argument from the command line. ;;;;;;;;;;;;;;;;;; ; Global Options ; ;;;;;;;;;;;;;;;;;; [global] ; Pid file ; Note: the default prefix is /var ; Default Value: none pid = /run/php/php7.2-fpm.pid ; Error log file ; If it's set to "syslog", log is sent to syslogd instead of being written ; into a local file. ; Note: the default prefix is /var ; Default Value: log/php-fpm.log error_log = /var/log/php7.2-fpm.log
Một vài điểm cần chú ý trong cấu hình trên:
pid = /run/php/php7.2-fpm.pid
Cấu hình này chỉ ra file chứa process id của tiến trình php-fpm
error_log = /var/log/php7.2-fpm.log
Còn đây là cấu hình file mà php sẽ ghi log.
Bên cạnh đó còn 3 cấu hình
emergency_restart_threshold 10 emergency_restart_interval 1m process_control_timeout 10s
Hai dòng đầu chỉ ra rằng nếu 10 quy trình con không thành công trong vòng 1 phút thì php-fpm sẽ tự động khởi động lại. Điều này nghe có vẻ “yếu” nhưng thực ra PHP là một tiến trình ngắn, nó làm tốn dữ liệu vì vậy việc khởi động lại trong trường hợp lỗi sẽ giải quyết được nhiều vấn đề. Cấu hình thứ 3 là thời gian các tiến trình con chờ đợi tín hiệu từ tiến trình cha .
Thực ra bên trên vẫn chưa phải là cấu hình chính của php-fpm. Bởi vì để phục vụ các request từ server, php-fpm tạo ra một pool mới để quản lý các tiến trình, và nó sẽ có các cấu hình khác nhau. Ví dụ 1 pool có tên là www. File config là: /etc/php/7.2/fpm/pool.d/www.conf
; Start a new pool named 'www'. ; the variable $pool can be used in any directive and will be replaced by the ; pool name ('www' here) [www] ; Per pool prefix ; It only applies on the following directives: ; - 'access.log' ; - 'slowlog' ; - 'listen' (unixsocket) ; - 'chroot' ; - 'chdir' ; - 'php_values' ; - 'php_admin_values' ; When not set, the global prefix (or /usr) applies instead. ; Note: This directive can also be relative to the global prefix. ; Default Value: none ;prefix = /path/to/pools/$pool ; Unix user/group of processes ; Note: The user is mandatory. If the group is not set, the default user's group ; will be used. user = www-data group = www-data
Cấu hình mặc định thường như sau:
pm = dynamic pm.max_children = 5 pm.start_servers = 3 pm.min_spare_servers = 2 pm.max_spare_servers = 4 pm.max_requests = 200
Chú ý “dynamic” ở đây có ý nghĩa như thế nào. Xem các option có thể setting:
; Choose how the process manager will control the number of child processes. ; Possible Values: ; static - a fixed number (pm.max_children) of child processes; ; dynamic - the number of child processes are set dynamically based on the ; following directives. With this process management, there will be ; always at least 1 children. ; pm.max_children - the maximum number of children that can ; be alive at the same time. ; pm.start_servers - the number of children created on startup. ; pm.min_spare_servers - the minimum number of children in 'idle' ; state (waiting to process). If the number ; of 'idle' processes is less than this ; number then some children will be created. ; pm.max_spare_servers - the maximum number of children in 'idle' ; state (waiting to process). If the number ; of 'idle' processes is greater than this ; number then some children will be killed. ; ondemand - no children are created at startup. Children will be forked when ; new requests will connect. The following parameter are used: ; pm.max_children - the maximum number of children that ; can be alive at the same time. ; pm.process_idle_timeout - The number of seconds after which ; an idle process will be killed. ; Note: This value is mandatory.
Giải thích sơ lược thì như sau
pm.max_children = Số process con (child processes) tối đa được tạo (tương đương tổng số request có thể phục vụ)
pm.start_servers = Tổng số child processes được tạo khi khởi động php-fpm (được tính bằng công thức `min_spare_servers + (max_spare_servers – min_spare_servers) / 2` )
pm.min_spare_servers = Tổng số child process nhàn rỗi tối thiểu được duy trì.
pm.max_spare_servers = Tổng số child process nhàn rỗi tối đa được duy trì.
Chúng ta có các option sau:
- Static: fix cứng 1 số lượng process được duy trì
- Dynamix: set số lượng min và max các process có thể được duy trì ở bất cứ thời điểm nào
- ondemand: Các process sẽ được tạo ra và tắt đi theo yêu cầu
Vậy các con số này có ý nghĩa như thế nào? Nếu như web server của bạn có lượng truy cập rất ít thì việc chọn cấu hình là dynamic sẽ rất phí tài nguyên, trong trường hợp này bạn có thể set pm.min_spare_servers = 3 và chọn option “ondemand” để server quyết định số lượng process.
Trong trường hợp ngược lại với 1 web site có số lượng truy cập lớn, thì nên setting về static, và setting với số lượng tối đa mà server có thể sử dụng.
Cách tính pm.max_children
B1. Xác định số lượng bộ nhớ cần thiết trung bình cho 1 process php-fpm
ps -ylC php-fpm --sort:rss
Ouput sẽ tương tự như sau, column RSS sẽ chứa giá trị mà chúng ta cần xác định
S UID PID PPID C PRI NI RSS SZ WCHAN TTY TIME CMD <br>S 0 24439 1 0 80 0 6364 57236 - ? 00:00:00 php-fpm <br>S 33 24701 24439 2 80 0 61588 63335 - ? 00:04:07 php-fpm <br>S 33 25319 24439 2 80 0 61620 63314 - ? 00:02:35 php-fpm
Như kết quả ở trên là khoảng 61620KB, tương đương với ~60MB/process
B2: Tính toán ra pm.max_children
Ví dụ server hiện tại có khoảng 4GB RAM, đang chạy cả web và DB, chúng ta sẽ tính con số ước lượng là DB hoạt động khoảng 1GB và 0.5G dành cho buffer. Với các con số như vậy thì dung lượng RAM còn lại cho hoạt động của php-fpm tương đương với: 4 – 1 – 0,5 = 2,5 GB RAM hoặc 2560 Mb.
pm.max_children = 2560 Mb / 60 Mb = 42
Làm tròn con số xuống (để đảm bảo server hoạt động không bị quá tải), thì pm.max_children = 40
Cách tính pm.min_spare_servers
pm.min_spare_servers
có giá trị tương đương với 20% của pm.max_children
.
Nếu như với gía trị ở trên thì pm.max_spare_servers = 60% * 40 = 24
Cũng có hướng dẫn ghi là pm.max_spare_servers = (cpu cores) * 4
Cách tính pm.max_requests
Tham số này chính là số lượng request xử lý đồng thời mà server có thể chịu tải được, giá trị phụ thuộc vào pm.max_children
và số lượng request trên 1s vào server. Con số này tính toán cũng khá hên xui, nhưng có thể có 1 phương pháp là sử dụng tool ab
của apache sau đó giảm giá trị dần dần sao cho phù hợp.
ab -n 5000 -c 100 http://domain.com/
Khi chạy command trên thì có nghĩa là tạo ra 5000 request với 100 session hoạt động cùng lúc truy cập vào url http://domain.com
. Giá trị pm.max_requests
có thể set là 1000 sau đó tăng/giảm dần đến mức phù hợp (Phù hợp là khi server vẫn còn chịu được tải).
Cấu hình Nginx để tăng performance
Mở file /etc/nginx/nginx.conf
và điều chỉnh theo các hướng dẫn sau.
Trước hết bạn cần nắm công thức:
max_clients = worker_processes * worker_connections
số lượng người truy cập tối đa Nginx có thể phục vụ bằng thông số worker_processes
nhân với worker_connections
. Mặc định sau khi cài đặt Nginx thì
worker_processes = 1
worker_connections = 1024.
Các bạn cần chỉnh lại worker_processes bằng với số lượng CPU core trong server bạn cấu hình. Có thể xem số CPU Core thông qua lệnh sau:
cat /proc/cpuinfo |grep processor
Với số lượng CPU bằng 4 bạn có thể setting worker_processes
= 4, như vậy lượng truy cập tối đa sẽ là 4 * 1024
Để tăng thêm số lượng connection bạn cũng có thể thay đổi thêm
worker_rlimit_nofile 2048;
Cấu hình này giúp bạn có thể setting worker_connections
lên tới 2048
Ngoài ra các bạn cũng cần thiết giới hạn kích thước body của các http request và buffer dùng xử lý http request thông qua việc thêm hai thông số sau đây vào file cấu hình
client_max_body_size 20m;
client_body_buffer_size 128k;
Tăng tốc bằng cache file. Với các file tĩnh trên server thì chúng ta hoàn toàn có thể cache lại bằng config
location ~* .(jpg|jpeg|gif|png|css|js|ico|xml)$ {
access_log off;
log_not_found off;
expires 360d;
}
Anh em đọc tham khảo. Bài cóp nhặt từ nhiều nguồn, khá hữu ích cho các anh em PHP.