後端優化分爲三個方向
- 組件配置調優,偏運維
- 架構調優,偏架構
- 代碼層面的調優,偏開發
配置調優
以 Nginx、PHP、MySQL 爲例。
LNMP中web高併發優化配置以及配置詳解
https://phpartisan.cn/news/55.html
Nginx
從簡單粗暴的角度,就是提高連接數。
增加進程數,每個 CPU 配置一個進程。
進程數配置項: worker_processes
CPU 配置項: worker_cpu_affinity ,該選項使得 Nginx 每個進程都執行在不同 CPU
提高單進程允許的最多連接數。
配置項: worker_connections
理論上一臺機器的最大連接數 = worker_processes * worker_connections
PHP-FPM
總體思想是控制進程數。
選項:pm
- static
固定進程數。如果是 PHP 專用服務器,則可以將其設置爲固定,並給定一個比較大的值。 - dynamic (默認)
根據以下幾個因素變化:- 啓動時進程數
- 最大進程數
- 至少有多少個空閒進程,少了就創建新空閒進程
- 至多有多少個空閒進程,多了就銷燬空閒進程
每個 PHP-FPM 進程大致佔用 20 MB 的內存,用內存除以 20 MB 就是極限數量。但是要注意,如果設置極限數量,在有其他應用佔用較大內存時,會導致服務異常。
PHP
去掉沒有用到的擴展。
啓用 OPCache 擴展。
MySQL(InnoDB)
MySQL 的內存緩存大小對於性能的影響較大。
MySQL 的緩存分爲兩部分:
- 索引
- 行記錄
配置項是: innodb_buffer_pool_size
這也是索引不能加太多的原因。索引加太多會導致索引佔用更多的緩存,進而使得行記錄的緩存減少。
索引的更多優化:
-
索引不要加到重複數據多的列上。
索引有一個參數 Cardinality,用於評估索引中唯一值的數目的估值。如果該值和錶行數的比值小於一定程度,則不會使用索引。 -
字段太長應使用部分索引。
-
使用短 ID 作爲主鍵。因爲輔助索引的葉子節點存儲的是主鍵,如果主鍵太大,會使得輔助索引也變大。因此通常使用自增 ID 而不是 UUID 作爲主鍵。
-
必要情況下創建聯合索引。多條件情況下,單表只會命中其中一個單列索引。
架構調優
瓶頸主要在數據庫。
Nginx
使用雙 Nginx 服務器(或者更多),用上 Keepalived + VIPA 組合確保高可用。
可以設置多個 VIPA ,分佈到不同機器上,這些機器互爲主備。接着讓域名同時解析到這些 VIPA。這樣可以充分利用多臺 Nginx 服務器,並且保證高可用。
MySQL(InnoDB)
從讀性能和寫性能兩方面入手。
提高讀性能:
- 添加從機(冗餘數據),讀寫分離。讀取數據時,從不同的從機讀取。
一般一主三從,兩從用於提供服務,一從用於後臺訪問。後臺訪問的服務如果是大數據服務,則可爲這臺機器設置更多索引來提升讀性能。但會給運維帶來維護的麻煩,所以慎用。通常來說保持與其他服務器相同的配置。
- 水平切分。將表中的舊數據轉存到同庫其他表或者其他庫。
可以優先考慮分庫。因爲磁盤滿的時候,還是要把表遷移到其他庫。 - 垂直切分。將表中不常用的和長度較大的字段拆到另一張表。
- 冷熱分離。如果只有近三個月的數據訪問量大,則將近三個月的數據儘量放到固態硬盤。將三個月之前的數據放到機械硬盤。
- 索引外置。把數據冗餘一份到 Elastic Search 裏面。
- 外部緩存。業務數據緩存到 Redis 裏面。Cache Aside Pattern。
注:所有數據冗餘都會帶來數據一致性的問題。
兩種一致性問題:
-
主從不一致
- 業務允許時無視不一致
- 強制讀主。從庫讀不到時再去主庫讀一次。
- 選擇性讀主(Redis)。數據更新通知 Redis,毫秒級緩存,查詢前先看更新的數據是否在 Redis 裏面,有則讀主。
-
緩存不一致(Redis)
發生在寫後立即讀。緩存了舊數據。
通過 binlog 瞭解主從同步進度,同步完刪除緩存。
提高寫性能:
- 多主多寫
要解決 ID 衝突的問題。兩種方式:- 設置不同起始 ID ,提高自增 ID 步長(會導致數據庫配置不一致)
- 客戶端生成 ID。生成 ID 的方式可以參考分佈式 ID 的幾種生成方式。
分庫:
- 單 key
場景:用戶表查詢比登錄多
其他字段如果要加速,則專門做一個單字段到 UID 的映射表(可放入緩存加速) - 1 對多
場景:用戶查訂單比訂單查用戶多
用戶訂單。訂單 ID 攜帶用戶 ID 的信息。讓同一個用戶的訂單落在同一個庫。 - 多對多
場景:關注與粉絲。
創建兩個庫,分別用其中一個字段作爲分庫依據。 - 多 key
場景:買家比賣家查訂單多,查訂單比查用戶多
忽略最少的部分,退化爲 1 對多。
架構不能爲 1% 的性能而帶來 20% 甚至更高的複雜性。
服務
無狀態化,可根據需要橫向擴容。
用 JWT(Json Web Token)驗證身份。
文件存儲放分佈式文件存儲上面,如 MinIO。
代碼層面
分爲:
- 減少連接次數
- 多線程/多進程
- 緩存
- 數據庫
減少連接次數
例如項目中有一個模塊,要傳輸腳本到目標機器上執行。分爲兩步:
- 傳輸腳本
- 執行
要建立兩次連接。
優化方式:將腳本 base64_encode,然後把執行命令拼接在後面。
echo "base64_encoded string" | base64 -d -i > /usr/local/src/xxx.sh; bash xxx.sh "param0";
多線程/多進程
碰到有多個耗時任務,爲每個任務創建一個新的線程或者進程執行。
緩存
分爲應用內緩存和外置緩存。
應用內緩存有些場景需要自己維護多臺機器之間的緩存信息,根據情況使用。
外置緩存(如 Redis/Memcached)。
將請求外部接口的數據緩存到 Redis,減少接口調用的耗時。
MySQL(InnoDB)
總體思想是儘可能減少數據量,儘可能早結束查詢,儘可能命中索引,儘可能減小鎖的粒度。
在執行語句前,先用 Explain 查看執行計劃,儘量命中索引,避免全表掃描。
-
儘量避免使用 select *,需要多少字段拿多少字段
-
非唯一索引儘量使用 limit
-
使用索引來代理 limit 處理分頁
limit 會掃描前面不要的數據,然後逐一拋棄。在 Where 裏面指定 ID 範圍會更快。
業務層提供上一頁和下一頁的操作,避免用戶一次跳多頁。URL 要使用 after_xxx ,避免用戶直接修改 page。例如 GitHub 的 release 列表界面。 -
用 Union 替代 OR
注:MySQL 的優化器會嘗試使用索引合併來自動優化 OR。 -
當數據集不會重複時,用 Union All 替代 Union
-
聯合索引最左匹配原則
-
聯合索引在範圍查詢的字段後就不會再走索引了
-
刪除由最左匹配原則覆蓋的索引
-
使用 like 時,避免把 % 放前面
放在前面不走索引。 -
使用 Where 加更精確的條件限制來減少傳輸的數據量
以前見過判斷用戶登錄用戶名密碼的時候,把整個用戶表查出來再逐一判斷的代碼。 -
避免對索引列使用 MySQL 內置函數。
-
優先使用 Inner Join 而不是其他 Join。
-
如果使用 Left Join 或者 Right Join,驅動表數據量儘可能小。
-
避免在索引列上使用不等號。如果索引能用範圍掃描,則使用範圍操作符。
例如 a != 1,轉化爲 a < 1 AND a > 1。 -
大量數據使用批量分塊插入數據
其中一個影響因素是鎖。一個事務插入已知數量的多條數據,只需獲取一次鎖。 -
使用覆蓋索引
使用索引就能獲取想要的值,不需要從數據表中讀。
用於輔助索引。
因爲索引的執行順序是:- 用輔助索引找到主鍵
- 通過主鍵索引找到數據
如果 select 的值只包括輔助索引和主鍵,則使用覆蓋索引。
-
儘量不要在 select 字段多的時候使用 Distinct
-
批量刪除數據要謹慎
- 分批操作。
- 如果全部數據刪除,且不需要恢復,則使用 truncate 。
- 如果不是全部刪除,則把保留的數據插入到新表,再整個刪除舊錶。
批量刪除會加鎖
批量刪除過程要寫 undo 日誌,一旦回滾,需要更多時間 -
避免數據類型隱式轉換
隱式轉換會使索引失效