歡迎光臨
每天分享高質量文章

小米DevOps團隊針對容器的Nginx優化

背景

容器技術越來越普遍,很多公司已經將容器技術作為基礎架構的一部分,容器中可以運行任何軟體,包括 Web Server、Application Server、資料庫和儲存系統等,其中 Nginx 作為 Web Server 使用也非常的普遍,接下來本文簡要分析下 Nginx 在容器內使用遇到的一點小問題。
我們在物理機上配置 Nginx 時通常會將 Nginx 的 worker 行程數配置為 CPU 核心數並且會將每個 worker 系結到特定 CPU 上,這可以有效提升行程的 Cache 命中率,從而減少記憶體訪問損耗,不放過任何能夠榨取系統性能的機會;對於需要手動配置 Nginx 行程個數的場景不在本文的討論範疇內,例如:磁盤 IO 密集型業務可能會導致 Nginx 行程阻塞,我們通常會將 Nginx 的行程數設置為 CPU 核數的 2 倍,用於提高整體的併發。
在 Nginx 配置中指定 worker_processes 指令的引數為 auto 來自動檢測系統的 CPU 核心數從而啟動相應個數的 worker 行程,那麼在 Linux 系統上 Nginx 是怎樣獲取 CPU 核心數的呢?答案是通過系統呼叫 sysconf(_SC_NPROCESSORS_ONLN) 獲取到系統當前可用的 CPU 核心數。如果我們在一個 CPU 是 32 cores 的物理機上啟動 Nginx,那麼 sysconf(_SC_NPROCESSORS_ONLN) 傳回值為 32。

存在問題

假如我們將 Nginx 放進 Docker 啟動的容器內,sysconf(_SC_NPROCESSORS_ONLN) 的傳回值是多少呢?
通過 docker run 啟動一個帶有 Nginx 的容器,暫時不對此容器的 CPU 資源做任何限制也就是可以使用物理機上的所有資源,我們來觀察 Nginx 行程啟動的行程數(確認 Nginx 配置中的 worker_processes 指令設置為 auto),答案其實大家都清楚的 Nginx 啟動了 32 個 worker 行程。
接下來我們對容器的 CPU 資源做限制,通過 docker run 時指定 –cpuset-cpus=”0,1″ 引數系結容器內的行程到 CPU-0 和 CPU-1 上,然後再來觀察 Nginx 行程啟動的行程數,同樣還是 32 個 worker 行程;對容器設置 cpu-shares 和 cpu-quota 也會得到同樣的結果。
那麼問題來了:
  1. 與我們預期的相符嗎?

  2. 指定了 –cpuset-cpus 能使用的核心數為 2 個,為什麼獲取到的 CPU 核心數還是 32 呢?

第 1 個問題:
很多人都是知道的,我們更期望的結果對於上邊的設置只啟動兩個 worker 行程,行程得到的 CPU 時間片期望被 2 個行程分攤,現在需要被 32 個行程分攤;從 Nginx 角度來看想要獲得更多的時間片就需要減少在這個 CPU 上運行的行程,這樣整體性能才會提升。對於 Nginx 來說也就是期望根據可用的 CPU 核數啟動相應的行程數,而不是根據物理機上可用的 CPU 核數來設置行程數。
第 2 個問題:
對於容器來說目前還只是一個輕量級的隔離環境,它並不是一個真正的操作系統,那麼在容器中獲取可用 CPU 核心數和 Memory 大小均是物理機配置。在沒有容器的時候很多軟體依賴於操作系統的資源進行初始化配置的,例如:JVM 根據 CPU 核數啟動相應的 gc 執行緒,根據物理機的 memroy 設置堆大小。

壓測對比

我們通過一個簡單的壓測對比一下在容器中 Nginx 啟動不同 worker 行程對 QPS 和 Latency 影響有多大。
物理機:32cores
容器引數:
cpu-quota=400000(即容器內的行程最多可以使用 400% 的 CPU)
壓測指令:
wrk -t 32 -c 500 -d 180 http://container_ip
提前準備:
容器內安裝 Openresty、將 worker_processes 修改為 4 和 32,關閉 access 日誌,響應資料為 541byte。
以下是 Nginx 的 QPS 和 Latency 壓測結果,QPS 從 12 萬 + 降到了 4 萬 +,Latency 也從 6+ms 降到了 25+ms。

解決方法

從以上壓測資料可以看出,Nginx 在設置 worker 行程數為 4 和 32 時 QPS 和 Latency 有很大的差距的,瞭解了以上問題我們該如何解決呢?
方法 1:
先來說一下普遍使用的 Lxcfs,對於上邊提到的場景是不適用的,Lxcfs 目前僅支持改變容器的 CPU 視圖(/proc/cpuinfo 檔案內容)並且只有 –cpuset-cpus 引數可以生效,對於系統呼叫 sysconf(_SC_NPROCESSORS_ONLN) 傳回的同樣還是物理機的 CPU 核數。
方法 2:
通過創建引導程式根據容器可以使用的物理資源自動計算出合理值並設置應用程式的啟動引數,例如:通過 shell 腳本動態修改 Nginx 的 worker 行程數。
方法 3:
應用程式自行解析容器內的 cgroup 信息,並設置程式的啟動引數。Docker 在 1.8 版本以後將容器分配的 cgroup 信息掛載進了容器內部,在容器內可以通過解析 cgroup 信息獲取到當前容器可以使用的資源信息。例如:JDK 10 中引入了支持 Docker 容器的資源檢測並配置 JVM 的運行時引數,它的原理就是解析容器內的 cgroup 信息配置 gc 執行緒數以及堆大小。
方法 4:
劫持系統呼叫 sysconf,在類 Unix 系統上可以通過 LD_PRELOAD 這種機制預先加載個人編寫的的動態鏈接庫,在動態鏈接庫中劫持系統呼叫 sysconf 並根據 cgroup 信息動態計算出可用的 CPU 核心數。

小結

我們團隊也參考了 JVM 的實現並根據 Nginx 的代碼風格給 Nginx 打了一個 patch,使 Nginx 的 worker_processes auto 引數能夠根據當前容器的可用資源自動計算出合理的 worker 行程數,同時也提交給了 Nginx 社區,但是很遺憾 Nginx 社區負責人 Maxim 並不願意接受這種實現方式,他更希望的是容器能透明支持 sysconf(_SC_NPROCESSORS_ONLN) 系統呼叫的功能,而不是用這種解析 cgroup 檔案的方式實現,於是我們就實現了一個可以劫持系統呼叫 sysconf 的動態鏈接庫。
可能有人會有疑問,為什麼 JVM 能接受解析 cgroup 檔案這種方式,而 Nginx 卻不能接受這種方式呢?
根據我的理解目前這個小問題對 Nginx 不是最痛的,不支持也不妨礙使用,另一點是 Nginx 作者以及現在的主要維護者 Maxim 都有重度代碼潔癖,從代碼風格以及代碼中幾乎無註釋可以感受的到,Nginx 推崇的是代碼即文件,要求寫代碼的人像寫文件一樣使代碼的可讀性非常高,對於這種用幾百行代碼解決的問題他們更不能忍受。而 JVM 支持的這種方式很大原因是這個問題的確很痛,網上有很多人都有報 JVM 在容器內的配置不合理導致運行時出現各種問題,所以目前通用的解決方案也只能是解析 cgroup 檔案來自動化支持。
本文轉載自公眾號: 小米運維,點擊查看原文

Kubernetes專案實戰訓練營

Kubernetes專案實戰訓練將於2018年8月17日在深圳開課,3天時間帶你系統掌握Kubernetes本次培訓包括:Docker介紹、Docker鏡像、網絡、儲存、容器安全;Kubernetes架構、設計理念、常用物件、網絡、儲存、網絡隔離、服務發現與負載均衡;Kubernetes核心組件、Pod、插件、微服務、雲原生、Kubernetes Operator、集群災備、Helm等,點擊下方圖片查看詳情。

赞(0)

分享創造快樂