後端優化分為四個方向
- 元件配置調優,偏運維
- 架構調優,偏架構
- 代碼層面的調優,偏開發
- CDN 加速
配置調優
以 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 伺服器(或者更多),用上 Keepalived + VIPA 組合確定高可用。
可以設定多個 VIPA ,分布到不同機器上,這些機器互為主備。接着讓域名同時解析到這些 VIPA。這樣可以充分利用多台 Nginx 伺服器,并且保證高可用。
從讀性能和寫性能兩方面入手。
提高讀性能:
-
添加從機(備援資料),讀寫分離。讀取資料時,從不同的從機讀取。
一般一主三從,兩從用于提供服務,一從用于背景通路。
背景通路的服務如果是大資料服務,則可為這台機器設定更多索引來提升讀性能。但會給運維帶來維護的麻煩,是以慎用。通常來說保持與其他伺服器相同的配置。
-
水準切分。将表中的舊資料轉存到同庫其他表或者其他庫。
可以優先考慮分庫。因為磁盤滿的時候,還是要把表遷移到其他庫。
- 垂直切分。将表中不常用的和長度較大的字段拆到另一張表。
- 冷熱分離。如果隻有近三個月的資料通路量大,則将近三個月的資料盡量放到固态硬碟。将三個月之前的資料放到機械硬碟。
- 索引外置。把資料備援一份到 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,減少接口調用的耗時。
總體思想是盡可能減少資料量,盡可能早結束查詢,盡可能命中索引,盡可能減小鎖的粒度。
在執行語句前,先用 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 日志,一旦復原,需要更多時間
-
避免資料類型隐式轉換
隐式轉換會使索引失效

本文采用知識共享署名 2.5 中國大陸許可協定進行許可。歡迎轉載,演繹或用于商業目的,但是必須保留本文的署名 schaepher(包含連結)。如您有任何疑問或者授權方面的協商,請給我留言。