後端的優化

後端優化分爲三個方向

  • 組件配置調優,偏運維
  • 架構調優,偏架構
  • 代碼層面的調優,偏開發

配置調優

以 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。

代碼層面

分爲:

  • 減少連接次數
  • 多線程/多進程
  • 緩存
  • 數據庫

減少連接次數

例如項目中有一個模塊,要傳輸腳本到目標機器上執行。分爲兩步:

  1. 傳輸腳本
  2. 執行

要建立兩次連接。

優化方式:將腳本 base64_encode,然後把執行命令拼接在後面。

echo "base64_encoded string" | base64 -d -i > /usr/local/src/xxx.sh; bash xxx.sh "param0";

多線程/多進程

碰到有多個耗時任務,爲每個任務創建一個新的線程或者進程執行。

緩存

分爲應用內緩存和外置緩存。

應用內緩存有些場景需要自己維護多臺機器之間的緩存信息,根據情況使用。

外置緩存(如 Redis/Memcached)。

將請求外部接口的數據緩存到 Redis,減少接口調用的耗時。

MySQL(InnoDB)

總體思想是儘可能減少數據量,儘可能早結束查詢,儘可能命中索引,儘可能減小鎖的粒度。

在執行語句前,先用 Explain 查看執行計劃,儘量命中索引,避免全表掃描。

  1. 儘量避免使用 select *,需要多少字段拿多少字段

  2. 非唯一索引儘量使用 limit

  3. 使用索引來代理 limit 處理分頁
    limit 會掃描前面不要的數據,然後逐一拋棄。在 Where 裏面指定 ID 範圍會更快。
    業務層提供上一頁和下一頁的操作,避免用戶一次跳多頁。URL 要使用 after_xxx ,避免用戶直接修改 page。例如 GitHub 的 release 列表界面。

  4. 用 Union 替代 OR
    注:MySQL 的優化器會嘗試使用索引合併來自動優化 OR。

  5. 當數據集不會重複時,用 Union All 替代 Union

  6. 聯合索引最左匹配原則

  7. 聯合索引在範圍查詢的字段後就不會再走索引了

  8. 刪除由最左匹配原則覆蓋的索引

  9. 使用 like 時,避免把 % 放前面
    放在前面不走索引。

  10. 使用 Where 加更精確的條件限制來減少傳輸的數據量
    以前見過判斷用戶登錄用戶名密碼的時候,把整個用戶表查出來再逐一判斷的代碼。

  11. 避免對索引列使用 MySQL 內置函數。

  12. 優先使用 Inner Join 而不是其他 Join。

  13. 如果使用 Left Join 或者 Right Join,驅動表數據量儘可能小。

  14. 避免在索引列上使用不等號。如果索引能用範圍掃描,則使用範圍操作符。
    例如 a != 1,轉化爲 a < 1 AND a > 1。

  15. 大量數據使用批量分塊插入數據
    其中一個影響因素是鎖。一個事務插入已知數量的多條數據,只需獲取一次鎖。

  16. 使用覆蓋索引
    使用索引就能獲取想要的值,不需要從數據表中讀。
    用於輔助索引。
    因爲索引的執行順序是:

    • 用輔助索引找到主鍵
    • 通過主鍵索引找到數據
      如果 select 的值只包括輔助索引和主鍵,則使用覆蓋索引。
  17. 儘量不要在 select 字段多的時候使用 Distinct

  18. 批量刪除數據要謹慎

    • 分批操作。
    • 如果全部數據刪除,且不需要恢復,則使用 truncate 。
    • 如果不是全部刪除,則把保留的數據插入到新表,再整個刪除舊錶。

    批量刪除會加鎖
    批量刪除過程要寫 undo 日誌,一旦回滾,需要更多時間

  19. 避免數據類型隱式轉換
    隱式轉換會使索引失效

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章