![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5yMkBzYkVTY3IDOhVjZyYzNzM2MjdTO3QTOmZmYlJWOk9CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
spacex
最近小夥伴們剛完成廣告系統,第二個直接服務于業務的項目。踩了一些坑,更收獲了不少知識。總結出來與大家分享,沒什麼高大尚技術,都是周邊的小技巧,加深對 go 語言的了解,适合新手,老鳥勿噴。
包管理
很多人都認為 go 的包管理不夠友好,深有感覺。特别是在 github 上給别人提 patch, 我先 fork 到自已目錄下面,如果原作者有引用自已路徑下面的庫,這就麻煩了。
另外一個是版本管理,每個人的 gopath 下面同樣的庫可能有不同版本,官方提供了一個 Godep 來控制版本,我看很多開源項目也在用。但是如果想管理除 go 以外的依賴呢?
我們使用相對路徑的方式,将引用到的庫集中放到 submodule 中,如下圖:
包管理
我司将所有語言第三方庫都放到 tinder 裡面,包括多個組之間共用的 thrfit IDL 檔案。Go 第三方庫都放到 golang/lib 目錄下面,共用的内部庫放到 golang/src/common 下面,每次 install 編譯程式時将 gopath 指定到目前項目下的相對目錄。
更新20160629:現在依賴使用govender,第三方的IDL使用submodule
逾時控制與請求跟蹤
由于業務對時延要求高,給我們定 50ms 逾時時間。大家肯定會想到用 Channel 和 timer 來做控制,但是我們還想跟蹤請求在内部的一系列操作,否則 Debug 日志一大堆,無法定位。
此時想到了 golang.org/x/net/context 庫,官方文檔很詳細,用于跨 API 調用很友善,vitess 中大量使用這個庫。// A Context carries a deadline, a cancelation signal, and other values across
// API boundaries.
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
Deadline() (deadline time.Time, ok bool)
// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
// See http://blog.golang.org/pipelines for more examples of how to use
// a Done channel for cancelation.
Done()
// Value returns the value associated with this context for key, or nil
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
// Use context values only for request-scoped data that transits
// processes and API boundaries, not for passing optional parameters to
// functions.
// Packages that define a Context key should provide type-safe accessors
// // userKey is the key for user.User values in Contexts. It is
// // unexported; clients use user.NewContext and user.FromContext
// // instead of using this key directly.
Value(key interface{}) interface{}
}
一個請求過來,每一次流轉都要攜帶 context.Context, 并且首先檢測是否逾時,如果逾時或是被取消,那麼直接傳回。另外 context.Context 會攜帶每次請求 ID,這是由業務傳過來的字段,如果為空,内部會生成一個 uuid 來辨別。
最終執行的代碼邏輯
逾時參數由業務傳過來,根據 timeout 生成 context.Context,最終函數要麼由 ctx.Done 逾時傳回,要麼從 rr channel 中擷取業務結果傳回。 真實業務請求會開啟一個匿名 goroutine, 傳入的 context.Context 攜帶了 logid, 内部打日志都會先列印 logid.
cancel signal
在每個耗時請求(redis/mysql)的入口,都會先檢測是否逾時。
goroutine和panic
這塊學藝不精,不像 actor 有父子關系,函數派生出來的 goroutine 如果panice 會挂掉整個程式,比如如下代碼:
錯誤示例
最開始程式如上圖,原以為會捕獲到 do_something 産生的 panic, 還是太年輕啊。要将 recover 放置在 go func 入口。
緩存髒資料
我們會在 redis 緩存使用者資訊,過期時間 6 小時,如果沒有再 fallback 到資料庫,另外還有一個程式内置 lru cache.
程式更新後,發現測試邏輯不對,uid 始終為0,fix 這個問題後,緩存這時就出現了髒資料。這時有兩個辦法,選擇了第2個。1. 使用 redis-port 批量清除無效緩存
2. 再次更新程式,内部修訂錯誤資料
php thrift 逾時問題
這個問題蠻頭痛,網上也有人遇到過 thrift中的逾時(timeout)坑。底層有三個逾時時間 connect, send 和 recv,最初都設定的 100ms,線上每天大量逾時報錯,後來我們将 recv timeout 調到 1000ms 線上就安靜了。
另外兩個 connect, send 仍然是 100ms,我們更傾向于底層驅動的逾時時間稍長一些,由業務層來控制逾時 ( context 庫)。
對象池
對象池是不同于連接配接池,兩個概念的東西。連接配接池特指 redis/mysql 的長連接配接,常駐記憶體。而對象池是内部執行個體,使用對象池可以減少程式 GC 壓力。目前常用的有兩種 sync.Pool 和 channel 模拟的對象池。官方有對 sync.Pool 的詳細說明,對象會在兩個 GC 之間被回收釋放,而 channel 則會常駐。
對象池
代碼很簡單也易懂,Get 時 channel 有資料就傳回,沒有直接 New。至于 channel 緩沖大小,要根據業務壓力來定。
内部服務注冊
在全局 map 注冊服務,這也算是 go 程式标配了,最出名的就是官方 database 庫注冊 mysql driver 的代碼
服務注冊
實作在 driver.Driver 接口的服務,直接注冊進來即可,使用時直接根據 name 找到 driver。
ServerOnRun
服務内部子產品有大量初始化的需求,對于全局變量等直接扔到 init() 函數裡即可,但是對于依賴外部服務 (mysql/redis/servervice),在程式啟動時連接配接句柄都不存在,就不能扔到 init() 裡。
一種做法就是在各個子產品裡寫 init_xx()等方法,然後在 main() 啟動初始化外部配置後,去調用,不過這樣在 main 裡維護就很麻煩。
是以要在全局定義 ServerOnRun, 每個無法由 init() 完成的初始化都在這裡進行注冊,最後由 main 周遊 ServerOnRun 來執行即可。
thrift字段變更問題
業務更新改動,時常會加字段,并且為了相容現有代碼,必須設為 optional。另外有時還遇到要将字段類型由 int 換成 string 的問題,比較麻煩,前期還是要設計好
json序列化
程式内部有大量的json序列化需求,官方的稍慢,采用比較流行的 ffjons
有疑問加站長微信聯系(非本文作者)