這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
上周末參加 GopherChina 第三屆大會,感受不錯。經過三年時間,Go 的發展非常火爆。會議規模從原來的幾百人到上千人,還有很多站在座位兩側聽的朋友。大會的內容也是從 Go 本身,到架構,到容器等相關領域都有涉及,可以說乾貨不錯。辦大會大會是一件辛苦事,非常感謝 Astaxie 一直以來的努力。講好一個主題,也會需要很多技巧的,也非常感謝參與大會的各個講師。言歸正傳,來聊聊大會的內容。
語言上 的 Go
每種語言都有自己的特色,Go 也不例外。學習 Go 的時候,難免會帶入別的語言經驗,造成一定的麻煩。因此從 Go 的方式來理解 Go,是 Go 語言開發人員必須經曆的過程。Tony Bai 從 Go 語言的角度,分享了 Go 的思維模式。
Go is about orthogonal composition of simple concepts with preference in concurrency.Go 是在偏好並發的環境下的簡單概念/事物的正交組合
從這句話就可以總結出幾個 Go 開發的原則:
- 事物的簡單化,邏輯單元不要太大;不是一個函數從頭到尾,是多個小函數組合起來的大函數
- 正交組合,邏輯單元之間的無關性;因為函數之間的無關性,才可以複用,也才可以並發
- 並發背景的需要,即業務是可以同時的,不是線性
這一些原則還是需要很多的開發技巧來實現的,比如介面的垂直組合,如:
type ReadWriter interface { Reader // 這裡體現了邏輯單元的簡單化 Writer}
根據介面來更適配的接受邏輯水平擴充:
func ReadAll(r io.Reader) ([]byte, error)// 可以支援檔案流,網路資料流等ReadAll(*os.File)ReadAll(*http.Response.Body)
最後我比較感興趣的就是 error 處理的內斂,比如:
// *bufio.Writerfunc (b *Writer) Write(p []byte) (nn int, err error) { if b.err != nil { return nn, b.err // 錯誤在結構內部 } ... ...}// usagebuf := bufio.NewWriter(fd)buf.WriteString("hello, ") // 這樣就不需要每行 if err != nilbuf.WriteString("gopherchina ")buf.WriteString("2017")if err := buf.Flush() ; err != nil { return err}
上面提到介面的垂直擴充,francesc 的分享就更深入的聊了 Go 中 interface{} 的提示。interface{} 我的理解有兩個意思。當 interface{} 帶有方法的時候,是行為的定義,如:
type Reader interface{ Read(data []byte)(n int,err error) // 注意定義的時候寫一下變數名,否則不是啥意思}
這樣的介面就可以彌補 Go 沒有泛型的不足。泛型的時候返回如 <List,Map> 其實是不對的,真正的意義很可能是 <Iterator>。在 Go 的編程中就逼迫你需要這樣的思維來理解,找出需要泛型的時候資料類型的共通之處,執行相同的操作(如果就是不同的行為,寫倆函數不是更好?)。
interface{} 另一個特殊情境就是空介面,對應的代碼就是需要類型推斷:
func do(v interface{}){ switch t := v.(type){ case int: fmt.Printf("int - %d",t) case error: fmt.Printf("error - %s",t.Error()) default: fmt.Printf("interface - %v",t) }}
不到萬不得已不要這麼寫代碼。否則需要推斷類型的 case 越來越多,代碼可維護性瞬間下降。
ezbuy 的分享有很多微服務相關的內容,選型 gRPC 的使用,context tracing 等問題。不過我最在意的是他們的開發環境搭建工具 Goflow。Go 的包管理機制一直為人所詬病。就算 vendor 解決了一些第三方包依賴的問題,但是非常的粗糙和直接。同時專案管理時 GOPATH 中既有第三方庫,又有自己的項目代碼,也給工程實踐造成麻煩。Goflow 從分享中看很像 gb + package registry。Goflow 修改系統變數 GOPATH 到目前的目錄,再從內網 registry 下載第三方包到 vendor 目錄。真的非常方便,是個不錯的解決方案。而且國內網路訪問很多包資源不是很順暢,內網有 registry 提供很大的便利。這一套東西在公司內部使用我覺得非常棒。
另外他是唯一提及用 internal 包的分享。例如代碼結構:
--product | |---internal | | | |---product_get.go | |---product_search.go | | |---product.go
代碼和功能更加清晰明確。回頭我也嘗試一下這樣的提示。
架構上 的 Go
Go 的使用情境中,很多是大規模的並發服務。並發服務的承受能力,不僅是 Go 一種語言的事情,而且與整體的架構設計密不可分。很多的講師從架構的層面,來談 Go 在整體中的角色和相關的使用。
七牛的講師分享了 Go 在他們的大資料分析系統 Pandora 中很小一塊的應用。大資料分析的系統,簡單的想象也會有資料擷取、資料處理、資料分析和分析結果落地這些過程。這次分享是分析資料結果落地的組件的細節。再來聯想,落地會有什麼問題:資料丟失、資料轉送延遲、多種下遊輸出方式,如何多執行個體分布式等。整個分享聊到了幾乎所有分布式架構都會用到的手段:
- 資料進入記憶體隊列,記憶體隊列可能 dump 到磁碟隊列
- 資料的事務化,保證進出;進入失敗重播,流出確保正確處理
- 程式關機重啟等特殊狀態的資料離線
- 多執行個體之間的平衡演算法
整體聽下來,都是比較常規的操作。動用一般手段,加上合理的架構設計,就可以承載大量的服務,這就足夠了。(不要瞎折騰)
TiDB 的講師分享談資料庫實現本身的內容較多。因為 TiDB 是個分散式資料庫,可以想象,一個查詢請求來到,經過查詢最佳化之後,具體的查詢運算任務實際執行的過程,需要去詢問 meta 資料來源表結構、索引結構等資訊,然後去具體的 TiKV 執行個體進行資料檢索。很有可能一次查詢需要從很多 TiKV 執行個體擷取資料,顯然是 並發 的邏輯,Go 是非常適合的。
我比較有興趣的是,TiKV 是支援一些簡單查詢運算的。就是查詢任務的某些細節可以下方到 TiKV,如 LIMIT 10 。TiKV 就可以聰明的返回有限數量的結果,在 SQL 層進行彙總。否則各個 TiKV 來一大波資料在 SQL層彙總,太浪費了。
訊息佇列是 Go 很常見的應用情境,有贊 為大家分享 NSQ 訊息佇列的改造之路。整個分享裡聽到了幾個有意思的點:
- 資料隊列的讀取方式是遊標,在整段上一部分一部分的挪。( NSQ 的資料隊列有記憶體和檔案隊列,消費不過來寫檔案了)
- 因為是遊標,就可以並發的讀隊列。多個遊標在同一條隊列上讀取資料,似乎維護起來複雜度比較高。另一個角度讓我想起
ringbuffer
- 並發讀隊列,也是 channel 的常見模型,1 producer -> n consumer
- 隊列太多計時器太多,使用 time channel 來統一管理。其實就是 時間輪 吧???
微服務是比較熱門的議題。很久以前分布式架構就有模組劃分。隨著 docker 的興起,模組的維度太大,更小的業務單元 + docker 容器化,形成的叢集成為現在微服務一種流行的實現方式。微服務化之後,商務程序就打散在各種微服務之間。我們需要跟蹤和同級資料在各個微服務中的運行狀態,trace 成為非常重要的議題,Bilibili 的 毛劍 的分享很大一部分就在聊 B 站在服務化過程中,資料 trace 的方式。
因為業務的多樣性,絕大多數的 trace 都是侵入式的。最開始毛劍是在 Go 的標準庫 net/rpc 上添加 tracing 的 context,同時利用標準庫 context.Context 還可以控制資料流。context 可以針對 rpc 做很多微服務必須的事情。 如 context.WithTimeout() 控制微服務要求的逾時,context.Value() 寄存很多連結、使用者、操作的相關資料,用於鑒權、過濾和統計等。再如負載平衡中,用戶端請求中 context 記錄服務端請求權重,自動做到均衡的發送資料到壓力小的服務端。(這裡也可以想見服務端是無狀態的)
聊到之後毛劍用的都是成熟的工具,訊息佇列 kafka,緩衝 redis + 修改的 twemproxy,分布式跟蹤 google dapper ,儲存 Hbase 和 ES 等。合理的架構設計 + 成熟的工具就可以承受大規模的業務。和七牛的分享給我的經驗是類似的。最後還有一點好玩的,B 站最早的代碼是基於 DedeCMS 魔改的,全部代碼揉在一起,幾乎無法控制,哈哈哈哈!
第二天 Grab 的高超談到 Go 在 Grab 的應用也說到微服務下 context tracing 的使用,定位問題。分享中更引人注意的是代碼專案管理的過程。Grab 將代碼放在一個 git repository 中,按照團隊命名空間規範目錄結構。這樣簡單的做到了職責區分,也做到了代碼對加大的透明化。或許說,如果要進行大規模的修改,各個團隊之間可以通過代碼更容易的討論和重構。另外,極致的代碼複用,和簡單的依賴管理,統一的版本更新,也帶來了項目整體穩定性。這些都是好處,但是一個項目大家同時更新代碼,肯定會有衝突的情況。之後 code test 和 review 的自動化過程,為這個問題提供瞭解決方案。
代碼協作工具 Phabricator 為 code review 提供的極大的便利,自動進行代碼格式正常化,單元測試和覆蓋率檢測。這樣衝突的代碼會造成單元測試的失敗,程式碼涵蓋範圍下降等問題,通過 slack 等立即提示到開發人員。即使代碼衝突,也可以立刻跟進修改。況且程式碼程式庫是透明的,對方修改什麼內容你可以去閱讀,參照之後修改自己的內容。一套自動化測試的工具搭建下來,大倉庫 對 Grab 而言利大於弊。
360 分享 poseidon 搜尋平台的技術細節中有很多可以參考的資訊。從舊的 C++ 過度到 Go 的過程中使用 cgo,帶來很多麻煩,最終選擇用 Go 完全重寫舊的代碼。可見,cgo 在很多情況下是吃力不討好的,謹慎選擇。另外對於跨 goroutine 的 panic 捕獲的問題,他將 error 內嵌到資料結構內,和上文提到的處理模式一樣。那麼在運算資料的 goroutine 就可以進行 panic-recover 將錯誤打入資料中。處理資料運算結果的 goroutine 就可以通過 Data.Error 來擷取這次資料運算是否是出現了錯誤。360 說他們的處理量是日均 100 萬億條,比較想知道是多少台機器撐起的業務。
容器裡 的 Go
Go 的一大明星應用就是 docker。docker 幾乎相關的所有內容都是 Go 語言開發的,比如 Docker Hub, Kubernetes , etcd 等。我日常使用 Docker 也就是本機多個開發環境的切換。對於大數量的容器編排和日常維護還是沒啥概念的。聽著 Docker 的分享更多死在漲姿勢。
鄧洪超 介紹的 Kubernetes Operator 讓我眼前一亮。Kubernetes Operator 是為瞭解決有狀態的服務容器化的問題。比如 etcd 動態配置各個節點的位置等。Kubernetes Operator 就負責在一個統一的地方監聽配置等狀態資訊的變化,根據變化啟動和停止容器,使整個容器群的狀態和定義的一樣。他現場示範修改 Kubernetes Operator 配置中容器內服務的版本號碼,容器群的數量等資訊,Kubernetes 自動啟動和關閉容器,滿足配置的要求。看到這些,覺得智能化營運的進步,尤其配套服務比如編排等越來越趨向自動化和智能化,可喜可賀。
華為 馬道長 分享的是 DevOps 的升級版 ContainerOps。 大公司的開發業務內容很多很複雜。如果按照一般的 DevOps 搭建環境,安裝各種需要的配套設施,再搭建當前業務開發以來的別的業務模組,還需要開發測試營運一條龍,非常複雜咯。docker 協助解決搭建環境和安裝基礎設施的問題。在 docker 中開發測試和 vm 中是一樣的。那問題就在搭建依賴的別的業務模組這個問題上。按我的理解 ContainerOps 中的 Component 就是來解決這個問題。你要開發某項業務過程,就定義一種 Component,業務輸入到業務輸出。Component 中就使用 k8s 從 registry 拉起需要的別的業務模組的 docker 鏡像,在內部形成完整的業務流。然後人員在這個業務流上開發與測試。這個對於大公司的開發,相對於 DevOps 是簡化了很多。同時為了構建一系列的 docker 鏡像,業務本身的模組拆分或者說微服務化,也為以後的日常維護、部署和擴充節省了很多事情。
另外,馬道長的 ppt 還是比較好理解的,的流程就很好知道基於 ContainerOps 的工作流程是如何啟動並執行。
VMWare 對於 Harbor 的介紹是很全面的。從 Harbor 解決的 docker 鏡像分發問題的需求分析,到具體實現遇到的一些問題都有提及。我比較在意的是他代碼中的 worker pool 和 狀態機器 state。我隱約記得上次為了回答 QQ 群中小夥伴的提問,去翻了 Harbor 的 worker pool 的代碼,發現很像 Handling 1 Million Requests per Minute with Go 文章中的設計。這樣看來,worker pool 的 pattern 大家的想法都是差不多了哈哈。
極限 的 Go
Dave Cheney 分享的 Go 的 #Pragma 滿滿的黑科技。編譯器指令一直是非常讓人困惑的東西。除非非常瞭解編譯器本身實現,否則很容易鬧出無法收拾的事情。go://nosqlit 和 go://noinline 是比較容易理解的編譯器操作指令。goroutine 的連續棧模型帶來的是否 split 的問題,而新的 SSA 後端編譯也帶來是否要被 inline 的問題。我對於 Go 本身實現的理解並不深,大概只能有這樣的影響。具體的內容還需要我繼續深入的研究。
#Pragma 聽聽就好,千萬別用。
廣發證券的分享非常有意思。證券業務高頻交易的要求:超低延遲、超高並發、超高可靠性和超嚴格監管。無論是什麼語言開發都面臨巨大的挑戰。Go 面臨這些問題時我們需要如何應對,很有參考意義。
首先面臨的問題是 GC 停頓。Go 1.8 已經把 GC STW 時間壓縮到 < 100 μs,可以說已經解決的很多問題。不過從代碼的角度,還是可以做很多事情降低 GC 的消耗。一個是 goroutine 池。這一點在 fasthttp 的使用得到的印證,效率是標準庫的幾倍,而記憶體和 GC 和標準庫差別不大。另外是可以控制數量的對象池。Go 標準庫的 sync.Pool 太過粗放,有些情形下非常需要自己寫一個對象池來做精細的控制。還有一點就是變數逃逸的問題。如果變數逃逸到 heap 上,就會影響 GC 的效能(需要掃描變數)。因此需要使用學習一些代碼技巧,避免逃逸到堆上的情形。
還提到多級 Map 的最佳化。大 Map 是非常影響 GC 效能的。應對策略就是大而化小,比如多個小 Map 降低掃描的時間 (Go 的 GC Mark-Sweep 中的 Mark 是並發的)。另外訪問 Map 是需要加鎖保證並發安全。大 Map 的鎖粒度太大,好像一個倉庫只有一個門,人進去關門就不能再進去,直到裡面的人出來。非常影響並發的操作。Map 大而化小後,相當於有多個門了。而且多級 Hash 分散後的 Map,粒度很小,門很多。即使 goroutine 數量巨大,門多了之後,同時訪問一個門的情況明顯減少。即讀取資料的過程幾乎不會遇到鎖競爭的問題。
另外還分享了網卡的 offload 的問題,利用硬體分區將小包組成大包發送。
題外話
這次的 GopherChina 內容滿滿,從 Go 語言層面,到代碼技巧,架構設計,以及容器應用都有涉及。不過,內容過多,不是什麼領域大家都有興趣。還是希望在 Go 語言本身和代碼編程方面有更多的內容。從需求分析到架構設計,都是代碼編寫的前奏。為了應付設計,我的 Go 程式變成什麼模樣。為了實現功能,我的 Go 程式提供哪些功能。希望有更多的講師可以從架構層面落到代碼的層面。架構設計的原則和工具手段都是類似的,但是各家公司的業務不同,最終形成的架構模型是千差萬別的。Go 作為架構中的一份子,想讓會為這個架構做適應的設計。而這種設計,更能啟發開發人員如何更好地使用 Go 這門語言。
GopherChina 曆屆大會的分享內容:
https://github.com/gopherchina/conference