曾 棕 根
(寧波職業技術學院電子信息工程學院 浙江 寧波 315800)
LNMP架構免費開源、功能強大,是當前最流行的Web服務器架構,與大數據和人工智能服務器存在密切的關系。由于LNMP架構技術牽涉面廣、安裝與配置非常復雜且可借鑒的高端經驗少,按照默認配置無法發揮該架構穩定、安全和高效的特征。LNMP服務器出現安全漏洞、在訪問高峰期網頁卡死甚至數據庫崩潰成為常態,因此對LNMP生產服務器進行技術改進成為當前亟待解決的問題。
歷經七年對LNMP生產服務器的管理與深入研究,解決了LNMP架構安裝、運行中遇到的各種問題,對該架構進行了全方位的技術改進,使得該架構安全、快速、抗擁塞。本文系統地論述了該架構在上述三方面所做的深度技術改進。
LNMP架構是指由Linux內核的操作系統、Nginx服務器、MariaDB數據庫服務器和PHP腳本服務器組成的PHP動態網站運行架構,組成該架構的四個軟件都是免費開源的[1]。生產環境下,推薦使用的Linux操作系統是CentOS-7 (1908),其穩定、快速、安全;Nginx是一款高性能低開銷的Web服務器,目前最新的穩定版本是1.16.2[2];MariaDB是MySQL數據庫的一個分支,由開源社區維護,代碼更新速度最為快速,目前最新穩定版本為10.3.11; PHP網頁服務器則是用來編譯和運行PHP腳本,目前最新版本為7.2.24。圖1為LNMP架構數據流動示意圖。

圖1 LNMP架構數據流動示意圖
用戶總是在瀏覽器中發出PHP網頁查看請求,Nginx收到用戶的PHP程序調用請求后,將此請求發送給PHP-FPM服務器,PHP-FPM服務器則調用PHP進程去編譯和運行PHP程序,PHP程序則通過SQL指令從MariaDB數據庫中去存取所需信息,MariaDB將SQL指令運行后以二維表的形式將結果返回給PHP進程,PHP進程則將記錄以HTML語法進行包裝,PHP-FPM服務器會將此HTML文本傳送給Nginx服務器,Nginx服務器則將此HTML文本返回給客戶端瀏覽器,客戶端瀏覽器則解釋此HTML文本中的HTML標簽,用戶即可從瀏覽器中看到此PHP程序的運行結果。
由圖1可見,數據的流動是以單一、固定的路徑傳輸的,數據總是從客戶端瀏覽器中發出,經過Nginx、PHP-FPM、MariaDB三項服務,最后按原路返回,構成一個閉環。因此,要提高LNMP架構的安全性和性能,就必須對LNMP架構每一項服務進行技術改進。
確保在互聯網環境下的訪問安全是服務器建設中最重要的問題。LNMP架構服務器安全措施包括磁盤規劃、以編譯方式安裝最新源代碼、開放有限端口、限定登錄賬戶、規劃文件讀取權限和使用HTTPS訪問協議等。
服務器上應該使用多個物理磁盤,構建某種磁盤陣列,某個磁盤損壞時可以直接用新磁盤替換,從而不影響服務器的運行。例如RAID 5,服務器上有3個以上硬盤即可做此模式,一份數據分成3份寫,如果一個硬盤壞了,其他數據不會受損失,抽出壞的硬盤再插入新的硬盤即可[3]。
在安裝CentOS操作系統時,應該將系統所需的不同的目錄分別安裝在不同的物理磁盤分區上,以避免某一個目錄出現錯誤時殃及其他目錄或整個磁盤。表1列出了不同分區和所需的磁盤空間,其中/boot引導分區用來存儲Linux內核,/根分區用來存儲CentOS操作系統所需的其他文件,而服務器中所安裝的其他軟件如Nginx、MariaDB、PHP及其需要保存的數據則放在/opt分區內。還可以進一步細化,將/根分區中的用戶目錄和臨時目錄等放于不同的分區中。這樣確保了運行中出現的問題限定在某一個分區內,能最大限度地減少損失。
如果LNMP服務器配有專用的大容量磁盤陣列服務器時,可以使用網線將二者的網卡直連,兩塊直連網卡使用192.168.0.x地址組建成一個局域網,在LNMP服務器中采用mount命令將磁盤陣列服務器掛載到CentOS操作系統的一個目錄中,如mount 192.168.0.100:/vol1/mnt/array,最后將CentOS操作系統中LNMP架構的用戶數據或者備份數據直接寫在/mnt/array即磁盤陣列設備上,這樣就確保了LNMP架構在物理層面上的安全性。
LNMP架構中Nginx、MariaDB和PHP都是開源軟件,官方一直在做功能、性能或安全性上的改進,因此不宜采用LNMP一鍵安裝包或RPM安裝包,而應去官網下載最新的適用于Linux操作系統的源代碼包,直接在CentOS操作系統上現場配置、編譯和安裝。這樣可以確保得到最新的穩定版本,而且可以生成最適合服務器CPU運行的二進制代碼。同時,管理員也清楚每個軟件都安裝到哪個文件夾中,便于日后的升級和維護。
CentOS操作系統安裝好后,應該立即使用yum-y update命令對操作系統進行一次全面更新,而且要定期使用這個命令更新系統。系統更新后,采用rpm-q kernel查看是否升級了內核,若升級,則需重啟操作系統,重啟后會自動運行最新內核。這時,使用uname-a查看正在運行的內核,并用rpm-e將舊版本的內核刪除,否則分配給內核的200 MB磁盤空間會不夠用,下次將無法升級新版內核。
還要立即升級CentOS操作系統中的gcc、cmake和libzip,這三個軟件都是編譯其他軟件(如Nginx、MariaDB和PHP)所必須的。
通過g++ --version命令,可以看到CentOS中g++的版本為g++ (GCC) 4.8.5 20150623 (Red Hat 4.8.5-39),由于該版本太老,因此須到官網(https://gcc.gnu.org/)上下載gcc-9.2.0.tar.gz源代碼編譯安裝。安裝完成后,必須要把/usr/lib64/中的libstdc++.so.6鏈接到新版本的/usr/local/lib64/libstdc++.so.6.0.27文件,否則g++編譯器將無法使用gcc-9.2.0。
從cmake官方網站(https://cmake.org/download/)上下載最新的cmake-3.15.1.tar.gz源代碼包,編譯安裝完成后,調用cmake-version時,會出現“段錯誤 (core dumped)”而無法使用,執行hash -r命令可以解決此錯誤。
libzip是編譯PHP所必須的支持包,CentOS默認沒有安裝libzip,要去官網(https://libzip.org/)下載最新版本的libzip-1.5.2.tar.gz源代碼包,編譯安裝后,運行libzip時會出現找不到 zipconf.h的錯誤,還需復制/usr/local/lib/libzip/include/zipconf.h到/usr/local/include/zipconf.h位置。
Nginx、MariaDB和PHP都需要從官網上下載最新源代碼,然后在本機上現場編譯安裝,Nginx安裝在/opt中,而MariaDB和PHP安裝在/usr/local/文件夾中。要定期關注這三個軟件的更新情況,并要隨官方的修訂及時更新它們,以確保最近發現的漏洞能被及時修復。
特別地,以前PHP使用Oracle官方發布的MySQL驅動libmysql與MariaDB服務進行通信,為了避免版權問題和程序的非可控性,PHP已自行開發了mysqlnd本地驅動替代libmysql,所以不再需要先安裝MariaDB再安裝PHP了,傳統的安裝PHP的方式中,在編譯PHP時,需要指定以下幾項:
--with-mysql=/usr/local/mysql
--with-mysqli=/usr/local/mysql/bin/mysql_config
--with-pdo-mysql=/usr/local/mysql
現在使用mysqlnd驅動,編譯PHP時,上述本條配置應該改為:
--with-mysql=mysqlnd
--with-mysqli=mysqlnd
--with-pdo-mysql=mysqlnd
安裝完成后,如果在phpinfo輸出的mysqli項中發現Client API library version為mysqlnd 5.0.12-dev-20150407,說明mysqlnd驅動mysqlnd 5.0.12-dev-20150407已經安裝成功,PHP腳本可以使用pdo_mysql和mysqli這兩種API Extensions通過mysqlnd驅動與MariaDB數據庫服務器通信。
CentOS操作系統中各種軟件都采用OpenSSL函數庫進行加密,但OpenSSL在2014年4月8日曝光了一個名為heartbleed的漏洞。利用該漏洞,約30%以https開頭網址的用戶登錄賬號密碼通過網絡被竊取[4]。所以必須從OpenSSL官網(https://www.openssl.org)下載,將OpenSSL 1.0.2k-fips 26 Jan 2017升級為OpenSSL 1.1.1d 10 Sep 2019。同時,使用Open-SSL加密函數庫的Nginx、PHP和OpenSSH都必須采用新安裝的OpenSSL加密函數庫重新編譯安裝,以確保網站訪問安全和服務器SSH遠程訪問安全。
LNMP服務器編譯安裝好后,為確保服務器在互聯網上的訪問安全,必須在開放有限端口、限定登錄賬戶、規劃文件讀取權限等方面作出限制。
通常情況下,服務器只開放80號Web服務端口、22號SSH遠程訪問端口和443號https訪問端口,同時關閉不需要的服務,這樣可以減少暴露在互聯網中的漏洞。同時,刪除firewalld.service防火墻,安裝iptables-services防火墻,這樣易于使用。
同時,在CentOS操作系統中,要禁止在ssh中使用root直接登錄,要求用戶先用普通賬號登錄CentOS操作系統,再通過su命令輸入root賬號密碼才可以登錄root賬號,這樣可以防止針對root賬號的遠程暴力破解。因此,首先要在CentOS中創建一個普通賬號,再在/etc/ssh/sshd_config中設置PermitRootLogin no即可。
在PHP-FPM服務器的配置文件php-fpm.conf中,設置user=www和group=www,使得只有www組的www用戶才可能訪問PHP-FPM服務。
MariaDB數據庫服務必須指定允許來自某臺客戶機的某個賬號來連接自己。在MySQL數據庫的user表中,只留下Host為localhost和127.0.0.1和User為root的這兩條記錄,以此只允許來自當前服務器的root賬號連接MariaDB數據庫服務,從而確保了MariaDB數據庫服務的安全。
對CentOS操作系統中每個文件和文件夾,都可以通過chgrp命令設置其隸屬于哪個組,通過chown命令來設置它隸屬于哪個用戶,并通過chmod命令來設置所有者、所屬組其他用戶和其他組對它的操作權限。操作權限用4、2、1數字來代表,4表示讀取權限,2表示寫入權限,1表示執行權限[5]。如775這三個數字代表擁有者、組用戶、其他用戶的權限分別為7=4+2+1、7=4+2+1、5=4+1,第一個數字7表示所有者具有讀取、寫入和執行權限,第二個數字7表示與所有者同組的用戶具有讀取、寫入和執行權限,第三個數字5表示其他用戶具有讀取和執行權限。對訪問用戶、訪問組和其他組的操作權限限定,使得CentOS操作系統中的文件訪問得到有效地保護。
Nginx默認采用HTTP協議與客戶端瀏覽器通信,該協議采用明文方式傳輸網頁和用戶賬號密碼,容易受到網絡中間人的竊密,所以極不安全。Nginx服務器必須采用HTTPS安全傳輸協議與用戶端瀏覽器通信。用戶在首次訪問Nginx服務器時,Nginx服務器會將使用私鑰加密的包含對應公鑰的數字證書CA發送到客戶端瀏覽器中,以后用戶發送訪問請求給Nginx服務器時,先用瀏覽器中的數字證書中的公鑰對信息進行加密,再發送給Nginx服務器;Nginx服務器在接收到用戶的訪問信息后,用服務器上的對應的私鑰解開信息;在完成用戶提交的請求后,將相應網頁用服務器中對應的私鑰加密后,再發送給客戶端瀏覽器;客戶端瀏覽器接收到信息后,會用瀏覽器中該Nginx服務器對應的數字證書中的公鑰解密,完成雙向加密通信。
將私鑰和包含對應公鑰的數字證書拷貝到Nginx服務器上后,只需在Nginx的配置文件中編寫如下配置即可:
server {
listen 443 ssl;
server_name localhost;
ssl_certificate /etc/pki/tls/certs/server.cer;
ssl_certificate_key /etc/pki/tls/private/server.key;
重啟Nginx服務器后,客戶端瀏覽器就可以采用https://方式來訪問網站了,這樣確保了通信安全。
對軟件的運行方式進行優化,才可以充分發揮服務器硬件性能,使得服務器處理Web請求的速度大大加快,從而避免服務器橫向擴展,一方面降低了服務器購置成本,另一方面降低了軟件管理上的復雜度。可以從如下幾個方面進行技術改進來加快LNMP服務器的處理速度:用內存代替磁盤、使用PHP緩存、多進程并行處理、優化InnoDB存儲引擎性能和壓縮后再傳輸。
內存的存取速度遠高于磁盤的存取速度,因此把網站的程序文件和數據庫直接放在內存中,將會大大提高網站的訪問速度。目前最大容量內存是單條128 GB,一塊CPU可以插8條內存,也就是1 TB。如果是雙路CPU,最大內存可達8 TB。網站代碼磁盤占用量一般在1 GB以內,MariaDB數據庫的數據文件一般在20 GB以內,因此一臺64 GB的服務器就可以將網站和數據庫全部放入內存直接存取。
將磁盤文件放入內存的方法,就是使用TMPFS文件系統。TMPFS,即臨時文件系統,是最好的基于RAM的文件系統,是由Linux內核的VM子系統管理的。TMPFS初始容量默認是物理內存大小的一半[6]。指定的TMPFS內存空間并不會被鎖定獨占,只有掛載存儲文件后才會占用相應大小的空間。
使用TMPFS文件系統最大的風險是意外掉電后該文件系統中的文件會全部消失。為了防止服務器意外掉電,確保數據安全,一方面定期自動備份數據庫,另一方面,確保服務器上的雙電源都有效。使用通信型UPS不間斷電源通過聯機USB線實時監測市電,一旦發現斷電,立即會切換到電源供電,并自動將內存中的文件保存到磁盤中,再自動關閉CentOS服務器,最后自動關閉UPS。整個處理過程只需5分鐘,所以UPS只需600 W的功率,如LADIS H1000 600 W型UPS。
使用PHP緩存,可以大大減少同一網頁再次被訪問時PHP的編譯時間開銷,從而大大加快網頁訪問速度。
PHP是解釋型語言而非編譯型語言,每次調用PHP文件時,翻譯器先將PHP文件翻譯成易于執行的中間代碼(即opcode字節碼)后再執行,執行完后所占用的內存馬上被釋放,基本上所有數據包括opcode字節碼在此時都被銷毀。opcode字節碼并非目標機器代碼,不能直接在硬件上運行。該PHP文件第二次被調用時,同樣還是會被重新轉換為字節碼,但很多時候文件內容幾乎是一樣的,比如靜態HTML文件生成字節碼后其內容很久都不會改變,這樣就非常浪費時間。
為了避免上述問題,PHP開發了Opcache組件。該組件的前身是Optimizer+,它是PHP的官方公司Zend開發的一款閉源但可以免費使用的PHP優化加速組件,2013年3月中旬Optimizer+改名為Opcache,在PHP源代碼中已經包含了該組件。啟用PHP的Opcache組件后,PHP會緩存PHP字節碼,使得再次調用相同的PHP文件時,無須編譯,直接從Opcache緩存中獲取字節碼去運行;同時,Opcache還應用了一些代碼優化模式,使得代碼執行更快,從而加速PHP的執行,使CPU消耗少了許多,PHP網頁的響應時間也大幅縮短。圖2是PHP程序使用Opcache緩存后的生命周期示意圖。
可以看到,傳統的PHP的整個運行過程為:Request請求(Nginx等)→Zend引擎讀取.php文件→掃描其詞典和表達式→解析文件→創建要執行的計算機代碼(稱為Opcode) →執行Opcode→ Response返回。使用了Opcache組件后,如果PHP網頁沒有變化,就直接從Opcache緩存中讀取中間代碼,再去執行,避免了CPU的重復計算。
為了充分利用CPU多核特征,Nginx和PHP都設計為多進程的工作方式,以提高處理效率。從圖3所示的LNMP架構示意圖中可以看出,客戶端瀏覽器與Nginx管理進程交互后,Nginx監控進程就會把具體的工作任務分配給一個空閑的Nginx工作進程,對于用戶的PHP網頁請求,Nginx工作進程會把PHP的編譯和運行工作交給PHP-FPM管理進程,PHP-FPM則把具體的工作任務分配給一個空閑的PHP FastCGI工作進程,PHP FastCGI會把要訪問的PHP網頁編譯并運行,并存取MariaDB數據庫,最后把結果組織成HTML超文本,回傳給用戶端瀏覽器。

圖3 LNMP架構示意圖
因此,為Nginx和PHP-FPM設置合適的工作進程數量和工作方式,可以加快LNMP整體架構的處理速度。Nginx通過worker_processes指令本配置開啟多少個工作進程,其數量一般為CPU的邏輯核心數量(即CPU超線程總量)。例如,雙CPU共16個物理核心,每個物理核心上有2個超線程,那么worker_processes應該設置為32。
PHP-FPM進程池(pm)有static(靜態)、dynamic(動態)和ondemand(按需)三種PHP FastCGI工作進程啟動方式。
如果將pm設置為static,那么PHP FastCGI工作進程始終保持pm.max_children個數目。
如果將pm設置為dynamic,那么PHP-FPM管理進程首先啟動pm.start_servers個工作進程,遇到訪問高峰時,會自動增加工作進程數量,確保任何時候都有pm.min_spare_servers個空閑進程存在。訪問高峰過后,如果空閑進程多于pm.max_spare_servers個時,多余的空閑進程會被殺掉;而同時能存活的最大工作進程總量為pm.max_children個。
如果將pm設置為ondemand,那么剛開始時php-fpm管理進程不會創建任何工作進程,當有請求時才會創建;當進程在pm.process_idle_timeout秒后還處于空閑狀態就會被殺掉,默認時長為10秒。因此,在完全空閑時,只有一個PHP-FPM管理進程存在。在此模式下,能同時存活的工作進程最大數量為pm.max_children個。
要根據具體情況來選擇這三種PHP-FPM的進程管理方式。如果內存足夠大,可以選用static方式,按一個PHP進程占用30 MB內存的標準來確定PHP進程設置數量,由于不用在使用時臨時創建PHP進程,因此這種方式效率最高,作為首選方式。如果服務器內存不寬裕,則可以選用dynamic方式,動態應付訪問高峰,能兼顧內存與效率。ondemand方式把節約內存放在首位,其缺點是遇到訪問高峰或者如果pm.process_idle_timeout的值太短的話,服務器會頻繁創建進程,用戶會感受到明顯的網絡延遲。
MariaDB的存儲引擎采用插件式工作方式,而InnoDB存儲引擎作為事務型數據庫的首選引擎,支持事務安全表(ACID),能與MariaDB數據庫系統完美結合。MariaDB系統接收的用戶SQL請求和傳輸的數據最后都要通過InnoDB存儲引擎與磁盤設備交互。LNMP架構中用戶請求數據鏈最后都要經過InnoDB存儲引擎,因此InnoDB數據庫的存取性能決定了LNMP架構的性能。MariaDB中的InnoDB實際上是XtraDB,它是InnoDB的增強版,但MariaDB的配置文件中仍然標記為InnoDB。InnoDB存儲引擎采用緩沖池來緩存數據,最后按設定的時間間隔將數據寫到磁盤中去持久化。
設置InnoDB的緩存池的大小innodb_buffer_pool_size為所有內存的80%以上。當然,需要除去CentOS操作系統、PHP進程和Nginx進程的內存開銷,余下的都設置為InnoDB的緩存。在64 GB的內存下,一般16 GB InnoDB緩存就足夠使用了,其中操作系統、TMPFS文件系統、Nginx進程和PHP進程分配16 GB內存,其余32 GB全部分配給MariaDB的線程。
innodb_log_files_in_group日志文件分為3組就足夠了[7]。此外,為了避免在日志文件覆蓋時產生不必要的緩沖池刷新活動,每個日志文件的大小應該正好占緩存大小的25%。這樣,innodb_log_file_size正好設置為innodb_buffer_pool_size的1/12。
假如InnoDB緩沖的大小為16 GB時,每個innodb_log_file_size就是1 365 MB了。
InnoDB內核中允許的線程數量依賴服務器硬件性能,設置太高會導致線程抖動。一般將innodb_thread_concurrency設置為16即可。
InnoDB存儲引擎中用來同步操作系統I/O的寫線程innodb_write_io_threads和讀線程innodb_read_io_threads在UNIX/Linux操作系統上都設置為8個。
innodb_flush_log_at_trx_commit的值可以設置為0、1或2。設置為2時,每個交易在提交時都會寫日志到文件緩存中,且大約每秒將日志文件緩存刷新到磁盤中去持久化,這樣使得InnoDB的日志存盤時間開銷達到最少,又兼具數據安全。
經過前面的LNMP服務器性能優化后,處理PHP頁面的速度得到了加快,訪問LNMP架構的性能瓶頸就落在網頁從Nginx服務器傳輸到用戶瀏覽器的網絡帶寬上了。HTTP協議使用gzip壓縮網頁內容,能極大減少傳輸內容的大小,從而使網頁訪問速度得到大幅提升。
gzip是GNUzip的縮寫,它是一個GNU自由軟件的文件壓縮程序,由Jean-loup Gailly和Mark Adler一起開發。其工作原理是:在一個文本文件中找出類似的字符串,并臨時替換它們,使整個文件變小。這種形式的壓縮對Web來說非常適合, 因為HTML和CSS文件通常包含大量的重復的字符串,例如空格、標簽。檢測表明,Nginx啟用gzip壓縮后,網頁字節數縮小為原先的20%,其壓縮率高達80%,有效減少了網絡帶寬開銷,大幅提高了網頁的瀏覽速度。
gzip的壓縮過程如下:
(1) 瀏覽器發送HTTP Request給Web服務器,Request中有Accept-Encoding: gzip、deflate字符串,它告訴服務器,當前的瀏覽器支持gzip壓縮。
(2) Web服務器接到Request后, 生成原始的Response,其中有原始的Content-Type和Content-Length。
(3) Web服務器通過gzip,來對Response進行編碼, 編碼后header中有Content-Type和Content-Length(壓縮后的大小),并且增加了Content-Encoding:gzip字符串,然后把Response發送給瀏覽器。
(4) 瀏覽器接到Response后,根據Content-Encoding:gzip字符串來對Response進行解碼,獲取到原始Response后,顯示出網頁。
Nginx服務器是通過ngx_http_gzip_module、ngx_http_gzip_static_module、ngx_http_gunzip_module三個模塊對指令進行解析處理。其中:ngx_http_gzip_module模塊對Response進行壓縮;ngx_http_gzip_static_module負責搜索和發送經過gzip處理的數據;ngx_http_gunzip_module用于對后端服務器或者預壓縮數據解壓,為了防止瀏覽器不能解壓,用此模塊解壓。
Nginx服務器中,gzip的壓縮級別gzip_comp_level可以設置為1~9級,級別越高,壓縮率更高,但服務器的CPU開銷也越大,訪問高峰時,會影響LNMP架構的整體性能。同時,由于壓縮級別越高,壓縮比提高并不是非常明顯,所以需要權衡壓縮比與CPU開銷增長之間的關系,當gzip_comp_level設置為6時,性價比最好。
Nginx通過在nginx.conf配置文件中添加gzip on指令來啟用gzip壓縮功能。為了避免對于訪問同一個文件時被重復壓縮,可以開啟靜態壓縮模塊ngx_http_gzip_static_module, 在使用gzip_static on 指令后,Nginx會直接讀取之前已經壓縮好的文件(文件名為*.gz),而不是現場動態壓縮,這樣再次加快了網頁訪問速度。
上文對LNMP架構各個環節的優化最大可能地縮短了服務器處理延遲和線路處理延遲,使服務器能在0.5秒左右完成單個用戶的HTTP請求處理的全部環節,用戶獲得了極快的網頁響應體驗。
但當LNMP架構遇到瞬間大量的HTTP請求時,服務器就會出現響應緩慢、卡死甚至數據庫服務器崩潰的典型現象。因此,對服務器進行抗擁塞改進成為整個LNMP架構服務器技術改進的重中之重。
實踐表明,遇到訪問高峰時,服務器出現擁塞的原因在于:(1) MariaDB數據庫默認采用了實驗室模式的線程模型,使得訪問高峰時響應緩慢;(2) PHP工作進程數過多,導致MariaDB連接數同步變大,造成數據庫服務崩潰。
PHP與MariaDB建立連接后,MariaDB通過工作線程來處理來自PHP的請求。由于MariaDB數據庫默認的線程模型是one-thread-per-connection,即會為每個PHP-MariaDB連接創建一個獨立的線程,處理PHP請求的任務完成后,該線程的生命周期就結束了。遇訪問高峰時,MariaDB創建的線程數量激增,導致操作系統頻繁的上下文切換和對系統資源的競爭,造成訪問阻塞,LNMP架構吞吐量急劇下降,直到無法正常提供HTTP服務。
Oracle公司的MySQL 5.5商用版采用線程池插件(Thread Pool Plugin)有效地解決了這個問題。由開源社區維護的MariaDB從5.5版本開始內建了線程池(Thread Pool),先將連接分配到不同的組的隊列里,以語句(statement)為最小單位,排隊處理用戶的SQL請求,不光減少了同時運行的線程的數量,還減少了上下文切換的次數;尤其是通過優先隊列和普通隊列運行機制,保證了最短時間內完成對一個事物(transaction)的處理,減少了并行處理事務的機率,最大可能地防止了對熱點資源的競爭產生死鎖;同時,線程的復用降低了創建和銷毀線程的CPU開銷,使得在訪問高峰時MariaDB數據庫能保持恒定的最高的吞吐量,有效解決了阻塞問題[8]。
為了適應不同的運行場景,MariaDB線程池提供了以下8個可調節參數:
thread_handling=pool-of-threads
thread_pool_size=32
thread_pool_priority=auto
thread_pool_prio_kickup_timer=1 000
thread_pool_stall_limit=60 ms
thread_pool_oversubscribe=10
thread_pool_max_threads=65 536
thread_pool_idle_timeout=500
通常只需要在MariaDB的配置文件my.cnf中設置thread_handling=pool-of-threads線程池就能很好地運行了,其余7個參數MariaDB默認會直接復制CentOS操作系統的線程池運行參數。
線程池通過使用分組的方法來限制和平衡并發,隔離了用戶連接和SQL請求語句,高優先級隊列使一個完整的事務能盡快完成,減少了并行事務的數量,最大可能降低事務對熱點資源的搶占,同時使用Timer定時線程防止線程組運行停滯,實現了高并發下平穩的高吞吐量,防止LNMP架構訪問高峰時的阻塞現象。
Thread pool取得了良好的應用效果,根據Oracle MySQL官方的性能測試:在并發達到128個連接以后,沒有線程池的MySQL性能會迅速降低。使用線程池以后,性能不會出現波動,會一直保持在較好的狀態運行。在讀寫模式下,128個連接以后,有線程池的MySQL比沒有線程池的MySQL性能高出60倍。在只讀模式下,512個連接以后,有線程池的MySQL比沒有線程池的MySQL性能高出18倍。
在LNMP架構中,來自客戶端的請求通過Nginx傳達到PHP-FPM服務器中,PHP-FPM服務器將這些用戶請求排隊發送給PHP工作進程去處理,每個PHP工作進程都會與MariaDB建立一個連接,MariaDB則通過某種線程模型去處理來自PHP連接的SQL請求。通過觀察發現,PHP工作進程的數量與MariaDB所創建的連接connection數量是相等。也就是說,如果PHP進程模型采用的是dynamic(動態)方式的話,同時有1 000個用戶來連接LNMP服務器,那么PHP-FPM服務器將創建1 024個PHP工作進程,此時,MariaDB數據庫服務器也將創建1 024個connection連接,并同時將這1 000個連接交給自己的工作線程去處理。在如此大的連接處理的壓力下,MariaDB的工作線程,無論是one-thread-per-connection每個連接一個線程的模式還是pool-of-threads進程池處理模式,都會無力運轉。因此,固定PHP進程數量,成為解決問題的關鍵。
所以,PHP應該采用static(靜態)工作方式,設置為64個固定的靜態進程并行處理就可以了,MariaDB同時建立的connection連接數最大就是64,這很好地保護了后端的MariaDB數據庫服務器的正常運行。
通常,可以根據先MariaDB最大允許的連接數量反過來確定PHP工作進程設置的最大數量。例如,MariaDB以每個連接42.2 MB的最大內存用量來計算的話,如果分配32 GB的內存用量給MariaDB連接使用,那么MariaDB的最大連接數max_connections可以設置為776,也就是說PHP工作進程數量也可以設置為776。但考慮到每個PHP工作進程最大30 MB的內存開銷和LNMP架構整體CPU計算、內存開銷情況,一般PHP設置為64個進程同時運行就足夠了。以此很好地保護了MariaDB服務器,即使遇到訪問高峰,MariaDB數據庫服務器承受的計算壓力也穩定在一個可承受水平上,不至于被壓垮。
被檢測的LNMP服務器是閃電Moodle服務器https://mood.nbpt.edu.cn/;服務器CPU型號及數量是Intel(R) Xeon(R) CPU E5-2640 v3 @ 2.60 GHz,2塊,共16核心32線程;內存型號及容量是DDR 3,64 GB;硬盤是SCSI硬盤,轉速15 000 r/min,共4塊300 GB的硬盤,建了Raid 5磁盤陣列。
Nginx中網頁傳輸采用gzip壓縮后,經過http://tool.chinaz.com/Gzips/Default.aspx?q=mood.nbpt.edu.cn網站統計,壓縮率(估計值)達到了73.07%,即減少了73.07%的數據傳輸的體積。
經國內某知名的網絡安全公司對該LNMP服務器進行的權威安全性檢測,沒有發現緊急、高危和中危漏洞,通過了ITSS(信息技術服務標準)、ITIL v3(信息技術基礎設施庫)、ISO/IEC 20000(IT服務管理國際標準體系)、ISO/IEC 27000(信息技術-安全技術-信息安全管理體系-要求)系列服務標準下的安全評測。使用的檢測軟件是WebScan(V6.0.1.9),Engine Version是V6.1.79, Policy Version是V6.1.109。
對LNMP服務器整體性能測試,采用Chrome瀏覽器自帶的Network網頁加載計時工具,從瀏覽器發出PHP讀取MariaDB數據庫的請求,到瀏覽器收到運行結果,即針對一個完整的Request和Response的PHP存讀取數據庫操作計時,發現優化前https://mood.nbpt.edu.cn/index.php主頁中PHP訪問MariaDB加載耗時穩定在352 ms左右;而優化后,主頁加載耗時穩定在124 ms左右,處理速度達到優化前的2.8倍,優化效果明顯。
在整個LNMP架構中,擁塞出現在MariaDB服務這個節點中。因此,采用sysbench OLTP RO對MariaDB數據庫進行了只讀壓力測試,考察其在高線程時的吞吐量TPS,即每秒鐘系統能夠處理的交易或事務的數量,如圖4所示。

圖4 兩種線程調度器吞吐量對比
可以看出,在高達4 096個線程并發讀取的情況下,新的pool-of-threads線程池吞吐量TPS能穩定在5 250;而傳統的thread-per-connection調度器TPS則急速下降到268,幾乎被壓垮。測試表明,使用線程池設計的MariaDB數據庫服務器具有抗擁塞特征,能應付網絡訪問高峰。
本文詳細論述了LNMP服務器在安全性、處理速度和抗擁塞性上的深度技術改進,應用在閃電moodle服務器(網址 https://mood.nbpt.edu.cn/)中,該服務器得到了多年的應用檢驗,用戶體驗度高,取得了很好的應用效果。
本文提出的技術改進對構建于Linux操作系統上的各類系統如Docker等的性能優化都具有重要的參考價值。隨著技術進步,人們對LNMP服務器不斷深入研究,LNMP服務器也必將更加安全、快速和穩固。