應用容器化優化指南 - Golang篇
摘要: 前言 隨著容器技術的興起,越來越多不同型別的應用開始使用容器的方式進行交付。Golang作為伺服器端非常熱門的一門語言同時也是容器技術的主要編寫語言備受關注。那麼將一個Golang應用進行容器化的時候,需要注意哪些事情,在出現問題時該如何進行調優和診斷呢? 先談談Golang本身的設計 Golang是谷歌釋出的第二款開源程式語言。
前言
隨著容器技術的興起,越來越多不同型別的應用開始使用容器的方式進行交付。Golang作為伺服器端非常熱門的一門語言同時也是容器技術的主要編寫語言備受關注。那麼將一個Golang應用進行容器化的時候,需要注意哪些事情,在出現問題時該如何進行調優和診斷呢?
先談談Golang本身的設計
Golang是谷歌釋出的第二款開源程式語言。Golang專門針對多處理器系統應用程式的程式設計進行了優化,使用Golang編譯的程式可以媲美C或C++程式碼的速度,而且更加安全、支援並行程序。Golang在容器相關的場景和領域以及高併發的伺服器程式場景下扮演著非常重要的角色。
Golang具有如下三個特點:
- 簡潔 快速 安全
- 並行 有趣 開源
- 記憶體管理 陣列安全 編譯迅速
在學習一門語言前,通常我會主要關注如下三個方面:第一這門語言的特性是什麼;第二這門語言解決的場景和問題是什麼;第三這門語言的內部設計是否有需要注意的地方。上面的介紹已經為我們解答了第一個和第二個問題,那麼接下來我們主要來討論第三個問題。那麼Golang的這些優秀的特性內部的設計方式是什麼樣子的,使用起來是否有什麼需要特別注意的呢?為了詳細解答這個問題,我們將問題拆分成了二個部分分別為大家解答。
- Golang是如何實現並行的
高併發是Golang被大家接納和認可的最重要一環。對於大型的網際網路專案而言,高併發可以說是應用效能的立足之本,再棒的功能與特性也不如穩定執行來得讓人安心。從前大家在關注C10K問題,而現在越來越多的人開始思考如何解決C10M問題。從C10K問題到C10M問題,解決問題的方式已經不是簡簡單單的調整核心引數那麼簡單的。更多的是要從架構甚至應用自身的角度來解決,一個高效的併發模型,可以從應用程式的交付壓榨系統的效能。目前比較成熟的併發模型,主要是通過程序、執行緒與協程三種不同方式來進行實現的。
程序是具有一定獨立功能的程式關於某個資料集合上的一次執行活動
執行緒是程序的一個實體,是CPU排程和分派的基本單位,它是比程序更小的能獨立執行的基本單位.執行緒自己基本上不擁有系統資源,只擁有一點在執行中必不可少的資源(如程式計數器,一組暫存器和棧),但是它可與同屬一個程序的其他的執行緒共享程序所擁有的全部資源。執行緒間通訊主要通過共享記憶體,上下文切換很快,資源開銷較少,但相比程序不夠穩定容易丟失資料。
協程是一種使用者態的輕量級執行緒,協程的排程完全由使用者控制。協程擁有自己的暫存器上下文和棧。協程排程切換時,將暫存器上下文和棧儲存到其他地方,在切回來的時候,恢復先前儲存的暫存器上下文和棧,直接操作棧則基本沒有核心切換的開銷,可以不加鎖的訪問全域性變數,所以上下文的切換非常快。
Golang的併發模型是基於協程的,而協程在Linux底層的排程是依賴程序的排程的,而這之間的轉換都通過Golang自身的排程器進行了管理,無需開發者關心。但是這個時候有經驗的開發者就會提出問題了,golang本身是編譯型的語言沒有類似JVM一樣的虛擬機器可以在執行時指定引數,那麼Goroutine這種方式是否有引數需要設定來保證效能。
此處給大家講述一個關於Goroutine棧擴容的問題,我們內部有一個全雙工的高併發寫離線資料的服務,在底層資料出現消費慢的時候快速出現OOM,問題產生的原因就是由於Goroutine棧擴容,最後可以通過通過拆分Goroutine的邏輯到上半段和work group的方式實現,由於篇幅的原因不過多的贅述,可以參考如下這篇部落格更深入瞭解棧擴容的問題。
- Golang的記憶體是如何管理的
記憶體管理對於C++與Java的開發者而言是最熟悉不過的了。C++的開發者必須通過程式碼手動的申請與釋放記憶體,因此必須熟悉記憶體佈局和使用;Java的開發者雖然有JVM幫助進行記憶體的管理與回收,但JVM不同的記憶體引數配置會導致程式因為回收記憶體帶來不同的效能表現。而Golang作為一門高階程式語言,同樣無需開發者直接操作記憶體,但是Golang中的GC設計是存在一些缺欠的。主要的問題在GC時的卡頓上,具體的問題可以參考如下文章,不過這點也無需大家特別關心,建議直接使用Golang1.9以後的版本進行編譯即可。深入瞭解Golang的GC可以參考如下文章
Golang容器化建議
- 常規容器化建議
首先需要進行的是常規的容器化優化,具體的內容可以參考如下文章進行體積的精簡和優化。
- Golang 中DNS的問題
不同語言對於DNS的Lookup處理會有所不同,在Java或者Node.JS等常見的語言和框架中對DNS Lookup都提供語言級別的內建的Cache,而在Golang中卻不存在類似的能力,這會導致對於高併發的場景中,Golang程式有可能會出現大量的DNS查詢,而在kubernetes中,DNS是通過內部的coredns或者kube-dns的方式提供的,因此有可能會因為大流量的Golang DNS導致叢集異常,為了解決這個問題,建議開發者在Golang的Dockerfile中整合nscd進行DNS的Cache,具體的操作步驟可以參考如下文件
- Golang 中GC的問題
在本文的上面的部分,為大家講解了Golang GC的一些缺欠以及如何避免GC問題的方式,在容器化的時候是否還需要做其他的優化呢?面對記憶體的異常,我們要如何定位是一個GC的問題呢?這裡要給大家介紹的是Golang自來的pprof,pprof是Golang語言中內建的效能調優工具,可以協同Flame-Graph,排查CPU效能、記憶體效能、GC回收等問題,建議在容器的場景中,在程式碼中整合pprof,並通過環境變數的方式進行開關設定,容器的Dockerfile中保留埠的保留,當出現問題的時候可以設定環境變數的方式進行開啟,快速進行線上問題的診斷。pprof的使用,可以參考如下文章
- Golang 中CGO的問題
我們知道Golang作為一門編譯型的語言,可以通過開啟CGO的標籤,直接使用C的程式碼並編譯為二級制檔案直接使用。但是這種方式非常不建議在容器中開啟,特別是使用類似Alpine這種最小映象的場景下。因為開啟CGO的場景下,會動態連結系統的C庫,而在Alpine上,很多的目錄佈局是有所差異的,另外有些最簡化的版本glibc的支援並不完善,因此非常不建議使用CGO的方式編譯Golang在容器中使用。 - Golang 中監控的建議
容器中很多監控的方式都無法很好的直接複用,建議大家使用更Docker的方式來解決,例如使用Prometheus的方式暴露Golang應用內部的指標進行監控,這也是目前非常多Golang開源專案的標配了。使用方式參考下文,與容器服務結合可以參考如下文章 - Golang 中效能的問題
Golang與容器的結合通常是為了高效能的場景,那麼通常需要對核心引數進行部分的調整,具體的調整方式可以參考如下文章
最後
Golang相對而言算是非常”省心“的一門語言了,在老版本的Golang中還需要通過runtime設定GOMAXPROCS,但是在最新版本的Golang中已經基本無需關心runtime的任何引數設定了,這些引數就像nginx的auto一樣,會隨著探測的配置自動變化,而在容器中,我們依然需要GOMAXPROCS,因為GOMAXPROCS的識別方式是通過獲取系統資源的方式確定的,而在容器中是通過只讀掛載宿主機的檔案實現的,因此獲取的資源還是宿主機的數值。因此,Golang的應用容器化,更多的還是要做好標準映象優化的步驟,以及在程式碼級別做好避免觸發GC和Goroutine的問題。
阿里雲雙十一1折拼團活動:已滿6人,都是最低折扣了
【滿6人】1核2G雲伺服器99.5元一年298.5元三年 2核4G雲伺服器545元一年 1227元三年
【滿6人】1核1G MySQL資料庫 119.5元一年
【滿6人】3000條國內簡訊包 60元每6月