原文作者:吳德寶AllenWu
作爲一個C/C++的開發者而言,開啓Golang語言開發之路是很容易的,從語法、語義上的理解到工程開發,都能夠快速熟悉起來;相比C、C++,Golang語言更簡潔,更容易寫出高併發的服務後臺系統
轉戰Golang一年有餘,經歷了兩個線上項目的洗禮,總結出一些工程經驗,一個是總結出一些實戰經驗,一個是用來發現自我不足之處
Golang語言簡介
Go語言是谷歌推出的一種全新的編程語言,可以在不損失應用程序性能的情況下降低代碼的複雜性。Go語言專門針對多處理器系統應用程序的編程進行了優化,使用Go編譯的程序可以媲美C或C++代碼的速度,而且更加安全、支持並行進程。
基於Golang的IM系統架構
我基於Golang的兩個實際線上項目都是IM系統,本文基於現有線上系統做一些總結性、引導性的經驗輸出。
Golang TCP長連接 & 併發
既然是IM系統,那麼必然需要TCP長連接來維持,由於Golang本身的基礎庫和外部依賴庫非常之多,我們可以簡單引用基礎net網絡庫,來建立TCP server。一般的TCP Server端的模型,可以有一個協程【或者線程】去獨立執行accept,並且是for循環一直accept新的連接,如果有新連接過來,那麼建立連接並且執行Connect,由於Golang裏面協程的開銷非常之小,因此,TCP server端還可以一個連接一個goroutine去循環讀取各自連接鏈路上的數據並處理。當然, 這個在C++語言的TCP Server模型中,一般會通過EPoll模型來建立server端,這個是和C++的區別之處。
關於讀取數據,Linux系統有recv和send函數來讀取發送數據,在Golang中,自帶有io庫,裏面封裝了各種讀寫方法,如io.ReadFull,它會讀取指定字節長度的數據
爲了維護連接和用戶,並且一個連接一個用戶的一一對應的,需要根據連接能夠找到用戶,同時也需要能夠根據用戶找到對應的連接,那麼就需要設計一個很好結構來維護。我們最初採用map來管理,但是發現Map裏面的數據太大,查找的性能不高,爲此,優化了數據結構,conn裏面包含user,user裏面包含conn,結構如下【只包括重要字段】。
1// 一個用戶對應一個連接 2type User struct { 3 uid int64 4 conn *MsgConn 5 BKicked bool // 被另外登陸的一方踢下線 6 BHeartBeatTimeout bool // 心跳超時 7 。。。 8} 9 10type MsgConn struct { 11 conn net.Conn 12 lastTick time.Time // 上次接收到包時間 13 remoteAddr string // 爲每個連接創建一個唯一標識符 14 user *User // MsgConn與User一一映射 15 。。。 16}
建立TCP server 代碼片段如下
1func ListenAndServe(network, address string) { 2 tcpAddr, err := net.ResolveTCPAddr(network, address) 3 if err != nil { 4 logger.Fatalf(nil, "ResolveTcpAddr err:%v", err) 5 } 6 listener, err = net.ListenTCP(network, tcpAddr) 7 if err != nil { 8 logger.Fatalf(nil, "ListenTCP err:%v", err) 9 } 10 go accept() 11} 12 13func accept() { 14 for { 15 conn, err := listener.AcceptTCP() 16 if err == nil { 17 18 // 包計數,用來限制頻率 19 20 //anti-attack, 黑白名單 21 ... 22 23 // 新建一個連接 24 imconn := NewMsgConn(conn) 25 26 // run 27 imconn.Run() 28 } 29 } 30} 31 32 33func (conn *MsgConn) Run() { 34 35 //on connect 36 conn.onConnect() 37 38 go func() { 39 tickerRecv := time.NewTicker(time.Second * time.Duration(rateStatInterval)) 40 for { 41 select { 42 case <-conn.stopChan: 43 tickerRecv.Stop() 44 return 45 case <-tickerRecv.C: 46 conn.packetsRecv = 0 47 default: 48 49 // 在 conn.parseAndHandlePdu 裏面通過Golang本身的io庫裏面提供的方法讀取數據,如io.ReadFull 50 conn_closed := conn.parseAndHandlePdu() 51 if conn_closed { 52 tickerRecv.Stop() 53 return 54 } 55 } 56 } 57 }() 58} 59 60// 將 user 和 conn 一一對應起來 61func (conn *MsgConn) onConnect() *User { 62 user := &User{conn: conn, durationLevel: 0, startTime: time.Now(), ackWaitMsgIdSet: make(map[int64]struct{})} 63 conn.user = user 64 return user 65}
TCP Server的一個特點在於一個連接一個goroutine去處理,這樣的話,每個連接獨立,不會相互影響阻塞,保證能夠及時讀取到client端的數據。如果是C、C++程序,如果一個連接一個線程的話,如果上萬個或者十萬個線程,那麼性能會極低甚至於無法工作,cpu會全部消耗在線程之間的調度上了,因此C、C++程序無法這樣玩。Golang的話,goroutine可以幾十萬、幾百萬的在一個系統中良好運行。同時對於TCP長連接而言,一個節點上的連接數要有限制策略。
連接超時
每個連接需要有心跳來維持,在心跳間隔時間內沒有收到,服務端要檢測超時並斷開連接釋放資源,golang可以很方便的引用需要的數據結構,同時對變量的賦值(包括指針)非常easy
1var timeoutMonitorTree *rbtree.Rbtree 2var timeoutMonitorTreeMutex sync.Mutex 3var heartBeatTimeout time.Duration //心跳超時時間, 配置了默認值ssss 4var loginTimeout time.Duration //登陸超時, 配置了默認值ssss 5 6type TimeoutCheckInfo struct { 7 conn *MsgConn 8 dueTime time.Time 9} 10 11 12func AddTimeoutCheckInfo(conn *MsgConn) { 13 timeoutMonitorTreeMutex.Lock() 14 timeoutMonitorTree.Insert(&TimeoutCheckInfo{conn: conn, dueTime: time.Now().Add(loginTimeout)}) 15 timeoutMonitorTreeMutex.Unlock() 16} 17 18如 &TimeoutCheckInfo{},賦值一個指針對象
Golang 基礎數據結構
Golang中,很多基礎數據都通過庫來引用,我們可以方便引用我們所需要的庫,通過import包含就能直接使用,如源碼裏面提供了sync庫,裏面有mutex鎖,在需要鎖的時候可以包含進來
常用的如list,mutex,once,singleton等都已包含在內
- list鏈表結構,當我們需要類似隊列的結構的時候,可以採用,針對IM系統而言,在長連接層處理的消息id的列表,可以通過list來維護,如果用戶有了迴應則從list裏面移除,否則在超時時間到後還沒有迴應,則入offline處理
- mutex鎖,當需要併發讀寫某個數據的時候使用,包含互斥鎖和讀寫鎖
1var ackWaitListMutex sync.RWMutex 2var ackWaitListMutex sync.Mutex
3.once表示任何時刻都只會調用一次,一般的用法是初始化實例的時候使用,代碼片段如下
1var initRedisOnce sync.Once 2 3func GetRedisCluster(name string) (*redis.Cluster, error) { 4 initRedisOnce.Do(setupRedis) 5 if redisClient, inMap := redisClusterMap[name]; inMap { 6 return redisClient, nil 7 } else { 8 } 9} 10 11func setupRedis() { 12 redisClusterMap = make(map[string]*redis.Cluster) 13 commonsOpts := []redis.Option{ 14 redis.ConnectionTimeout(conf.RedisConnTimeout), 15 redis.ReadTimeout(conf.RedisReadTimeout), 16 redis.WriteTimeout(conf.RedisWriteTimeout), 17 redis.IdleTimeout(conf.RedisIdleTimeout), 18 redis.MaxActiveConnections(conf.RedisMaxConn), 19 redis.MaxIdleConnections(conf.RedisMaxIdle), 20 }), 21 ... 22 } 23}
這樣我們可以在任何需要的地方調用GetRedisCluster,並且不用擔心實例會被初始化多次,once會保證一定只執行一次
4.singleton單例模式,這個在C++裏面是一個常用的模式,一般需要開發者自己通過類來實現,類的定義決定單例模式設計的好壞;在Golang中,已經有成熟的庫實現了,開發者無須重複造輪子,關於什麼時候該使用單例模式請自行Google。一個簡單的例子如下
1import "github.com/dropbox/godropbox/singleton" 2 3 var SingleMsgProxyService = singleton.NewSingleton(func() (interface{}, error) { 4 cluster, _ := cache.GetRedisCluster("singlecache") 5 return &singleMsgProxy{ 6 Cluster: cluster, 7 MsgModel: msg.MsgModelImpl, 8 }, nil 9})
Golang interface 接口
如果說goroutine和channel是Go併發的兩大基石,那麼接口interface是Go語言編程中數據類型的關鍵。在Go語言的實際編程中,幾乎所有的數據結構都圍繞接口展開,接口是Go語言中所有數據結構的核心。
interface - 泛型編程
嚴格來說,在 Golang 中並不支持泛型編程。在 C++ 等高級語言中使用泛型編程非常的簡單,所以泛型編程一直是 Golang 詬病最多的地方。但是使用 interface 我們可以實現泛型編程,如下是一個參考示例
1package sort 2 3// A type, typically a collection, that satisfies sort.Interface can be 4// sorted by the routines in this package. The methods require that the 5// elements of the collection be enumerated by an integer index. 6type Interface interface { 7 // Len is the number of elements in the collection. 8 Len() int 9 // Less reports whether the element with 10 // index i should sort before the element with index j. 11 Less(i, j int) bool 12 // Swap swaps the elements with indexes i and j. 13 Swap(i, j int) 14} 15 16... 17 18// Sort sorts data. 19// It makes one call to data.Len to determine n, and O(n*log(n)) calls to 20// data.Less and data.Swap. The sort is not guaranteed to be stable. 21func Sort(data Interface) { 22 // Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached. 23 n := data.Len() 24 maxDepth := 0 25 for i := n; i > 0; i >>= 1 { 26 maxDepth++ 27 } 28 maxDepth *= 2 29 quickSort(data, 0, n, maxDepth) 30}
Sort 函數的形參是一個 interface,包含了三個方法:Len(),Less(i,j int),Swap(i, j int)。使用的時候不管數組的元素類型是什麼類型(int, float, string…),只要我們實現了這三個方法就可以使用 Sort 函數,這樣就實現了“泛型編程”。
這種方式,我在項目裏面也有實際應用過,具體案例就是對消息排序。
下面給一個具體示例,代碼能夠說明一切,一看就懂:
1type Person struct { 2Name string 3Age int 4} 5 6func (p Person) String() string { 7 return fmt.Sprintf("%s: %d", p.Name, p.Age) 8} 9 10// ByAge implements sort.Interface for []Person based on 11// the Age field. 12type ByAge []Person //自定義 13 14func (a ByAge) Len() int { return len(a) } 15func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 16func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age } 17 18func main() { 19 people := []Person{ 20 {"Bob", 31}, 21 {"John", 42}, 22 {"Michael", 17}, 23 {"Jenny", 26}, 24 } 25 26 fmt.Println(people) 27 sort.Sort(ByAge(people)) 28 fmt.Println(people) 29}
interface - 隱藏具體實現
隱藏具體實現,這個很好理解。比如我設計一個函數給你返回一個 interface,那麼你只能通過 interface 裏面的方法來做一些操作,但是內部的具體實現是完全不知道的。
例如我們常用的context包,就是這樣的,context 最先由 google 提供,現在已經納入了標準庫,而且在原有 context 的基礎上增加了:cancelCtx,timerCtx,valueCtx。
如果函數參數是interface或者返回值是interface,這樣就可以接受任何類型的參數
基於Golang的model service 模型【類MVC模型】
在一個項目工程中,爲了使得代碼更優雅,需要抽象出一些模型出來,同時基於C++面向對象編程的思想,需要考慮到一些類、繼承相關。在Golang中,沒有類、繼承的概念,但是我們完全可以通過struct和interface來建立我們想要的任何模型。在我們的工程中,抽象出一種我自認爲是類似MVC的模型,但是不完全一樣,個人覺得這個模型抽象的比較好,容易擴展,模塊清晰。對於使用java和PHP編程的同學對這個模型應該是再熟悉不過了,我這邊通過代碼來說明下這個模型
- 首先一個model包,通過interface來實現,包含一些基礎方法,需要被外部引用者來具體實現
1package model 2 3// 定義一個基礎model 4type MsgModel interface { 5 Persist(context context.Context, msg interface{}) bool 6 UpdateDbContent(context context.Context, msgIface interface{}) bool 7 ... 8}
2. 再定義一個msg包,用來具體實現model包中MsgModel模型的所有方法
1package msg 2 3type msgModelImpl struct{} 4 5var MsgModelImpl = msgModelImpl{} 6 7func (m msgModelImpl) Persist(context context.Context, msgIface interface{}) bool { 8 // 具體實現 9} 10 11func (m msgModelImpl) UpdateDbContent(context context.Context, msgIface interface{}) bool { 12 // 具體實現 13 14} 15 16...
3. model 和 具體實現方定義並實現ok後,那麼就還需要一個service來統籌管理
1package service 2 3// 定義一個msgService struct包含了model裏面的UserModel和MsgModel兩個model 4type msgService struct { 5 msgModel model.MsgModel 6} 7 8// 定義一個MsgService的變量,並初始化,這樣通過MsgService,就能引用並訪問model的所有方法 9var ( 10 MsgService = msgService{ 11 msgModel: msg.MsgModelImpl, 12 } 13)
4. 調用訪問
1import service 2 3service.MsgService.Persist(ctx, xxx)
總結一下,model對應MVC的M,service 對應 MVC的C, 調用訪問的地方對應MVC的V
Golang 基礎資源的封裝
在MVC模型的基礎下,我們還需要考慮另外一點,就是基礎資源的封裝,服務端操作必然會和mysql、redis、memcache等交互,一些常用的底層基礎資源,我們有必要進行封裝,這是基礎架構部門所需要承擔的,也是一個好的項目工程所需要的
redis
redis,我們在github.com/garyburd/redigo/redis的庫的基礎上,做了一層封裝,實現了一些更爲貼合工程的機制和接口,redis cluster封裝,支持分片、讀寫分離
1// NewCluster creates a client-side cluster for callers. Callers use this structure to interact with Redis databasefunc NewCluster(config ClusterConfig, instrumentOpts *instrument.Options) *Cluster { 2 cluster := new(Cluster) 3 cluster.pool = make([]*client, len(config.Configs)) 4 masters := make([]string, 0, len(config.Configs)) for i, sharding := range config.Configs { 5 master, slaves := sharding.Master, sharding.Slaves 6 masters = append(masters, master) 7 8 masterAddr, masterDb := parseServer(master) 9 10 cli := new(client) 11 cli.master = &redisNode{ 12 server: master, 13 Pool: func() *redis.Pool { 14 pool := &redis.Pool{ 15 MaxIdle: config.MaxIdle, 16 IdleTimeout: config.IdleTimeout, 17 Dial: func() (redis.Conn, error) { 18 c, err := redis.Dial( "tcp", 19 masterAddr, 20 redis.DialDatabase(masterDb), 21 redis.DialPassword(config.Password), 22 redis.DialConnectTimeout(config.ConnTimeout), 23 redis.DialReadTimeout(config.ReadTimeout), 24 redis.DialWriteTimeout(config.WriteTimeout), 25 ) if err != nil { return nil, err 26 } return c, err 27 }, 28 TestOnBorrow: func(c redis.Conn, t time.Time) error { if time.Since(t) < time.Minute { return nil 29 } 30 _, err := c.Do("PING") return err 31 }, 32 MaxActive: config.MaxActives, 33 } if instrumentOpts == nil { return pool 34 } return instrument.NewRedisPool(pool, instrumentOpts) 35 }(), 36 } // allow nil slaves 37 if slaves != nil { 38 cli.slaves = make([]*redisNode, 0) for _, slave := range slaves { 39 addr, db := parseServer(slave) 40 41 cli.slaves = append(cli.slaves, &redisNode{ 42 server: slave, 43 Pool: func() *redis.Pool { 44 pool := &redis.Pool{ 45 MaxIdle: config.MaxIdle, 46 IdleTimeout: config.IdleTimeout, 47 Dial: func() (redis.Conn, error) { 48 c, err := redis.Dial( "tcp", 49 addr, 50 redis.DialDatabase(db), 51 redis.DialPassword(config.Password), 52 redis.DialConnectTimeout(config.ConnTimeout), 53 redis.DialReadTimeout(config.ReadTimeout), 54 redis.DialWriteTimeout(config.WriteTimeout), 55 ) if err != nil { return nil, err 56 } return c, err 57 }, 58 TestOnBorrow: func(c redis.Conn, t time.Time) error { if time.Since(t) < time.Minute { return nil 59 } 60 _, err := c.Do("PING") return err 61 }, 62 MaxActive: config.MaxActives, 63 } if instrumentOpts == nil { return pool 64 } return instrument.NewRedisPool(pool, instrumentOpts) 65 }(), 66 }) 67 } 68 } // call init 69 cli.init() 70 71 cluster.pool[i] = cli 72 } if config.Hashing == sharding.Ketama { 73 cluster.sharding, _ = sharding.NewKetamaSharding(sharding.GetShardServers(masters), true, 6379) 74 } else { 75 cluster.sharding, _ = sharding.NewCompatSharding(sharding.GetShardServers(masters)) 76 } return cluster 77}
總結一下:
- 使用連接池提高性能,每次都從連接池裏面取連接而不是每次都重新建立連接
- 設置最大連接數和最大活躍連接(同一時刻能夠提供的連接),設置合理的讀寫超時時間
- 實現主從讀寫分離,提高性能,需要注意如果沒有從庫則只讀主庫
- TestOnBorrow用來進行健康檢測
- 單獨開一個goroutine協程用來定期保活【ping-pong】
- hash分片算法的選擇,一致性hash還是hash取模,hash取模在擴縮容的時候比較方便,一致性hash並沒有帶來明顯的優勢,我們公司內部統一建議採用hash取模
- 考慮如何支持雙寫策略
memcache
memcached客戶端代碼封裝,依賴 github.com/dropbox/godropbox/memcache, 實現其ShardManager接口,支持Connection Timeout,支持Fail Fast和Rehash
goroutine & chann
實際開發過程中,經常會有這樣場景,每個請求通過一個goroutine協程去做,如批量獲取消息,但是,爲了防止後端資源連接數太多等,或者防止goroutine太多,往往需要限制併發數。給出如下示例供參考
1package main 2 3import ( 4 "fmt" 5 "sync" 6 "time" 7) 8 9var over = make(chan bool) 10 11const MAXConCurrency = 3 12 13//var sem = make(chan int, 4) //控制併發任務數 14var sem = make(chan bool, MAXConCurrency) //控制併發任務數 15 16var maxCount = 6 17 18func Worker(i int) bool { 19 20 sem <- true 21 defer func() { 22 <-sem 23 }() 24 25 // 模擬出錯處理 26 if i == 5 { 27 return false 28 } 29 fmt.Printf("now:%v num:%v\n", time.Now().Format("04:05"), i) 30 time.Sleep(1 * time.Second) 31 return true 32} 33 34func main() { 35 //wg := &sync.WaitGroup{} 36 var wg sync.WaitGroup 37 for i := 1; i <= maxCount; i++ { 38 wg.Add(1) 39 fmt.Printf("for num:%v\n", i) 40 go func(i int) { 41 defer wg.Done() 42 for x := 1; x <= 3; x++ { 43 if Worker(i) { 44 break 45 } else { 46 fmt.Printf("retry :%v\n", x) 47 } 48 } 49 }(i) 50 } 51 wg.Wait() //等待所有goroutine退出 52}
goroutine & context.cancel
Golang 的 context非常強大,詳細的可以參考我的另外一篇文章 Golang Context分析
這裏想要說明的是,在項目工程中,我們經常會用到這樣的一個場景,通過goroutine併發去處理某些批量任務,當某個條件觸發的時候,這些goroutine要能夠控制停止執行。如果有這樣的場景,那麼咱們就需要用到context的With 系列函數了,context.WithCancel生成了一個withCancel的實例以及一個cancelFuc,這個函數就是用來關閉ctxWithCancel中的 Done channel 函數。
示例代碼片段如下
1func Example(){ 2 3 // context.WithCancel 用來生成一個新的Context,可以接受cancel方法用來隨時停止執行 4 newCtx, cancel := context.WithCancel(context.Background()) 5 6 for peerIdVal, lastId := range lastIdMap { 7 wg.Add(1) 8 9 go func(peerId, minId int64) { 10 defer wg.Done() 11 12 msgInfo := Get(newCtx, uid, peerId, minId, count).([]*pb.MsgInfo) 13 if msgInfo != nil && len(msgInfo) > 0 { 14 if singleMsgCounts >= maxCount { 15 cancel() // 當條件觸發,則調用cancel停止 16 mutex.Unlock() 17 return 18 } 19 } 20 mutex.Unlock() 21 }(peerIdVal, lastId) 22 } 23 24 wg.Wait() 25} 26 27 28func Get(ctx context.Context, uid, peerId, sinceId int64, count int) interface{} { 29 for { 30 select { 31 // 如果收到Done的chan,則立馬return 32 case <-ctx.Done(): 33 msgs := make([]*pb.MsgInfo, 0) 34 return msgs 35 36 default: 37 // 處理邏輯 38 } 39 } 40}
traceid & context
在大型項目工程中,爲了更好的排查定位問題,我們需要有一定的技巧,Context上下文存在於一整條調用鏈路中,在服務端併發場景下,n多個請求裏面,我們如何能夠快速準確的找到一條請求的來龍去脈,專業用語就是指調用鏈路,通過調用鏈我們能夠知道這條請求經過了哪些服務、哪些模塊、哪些方法,這樣可以非常方便我們定位問題
traceid就是我們抽象出來的這樣一個調用鏈的唯一標識,再通過Context進行傳遞,在任何代碼模塊[函數、方法]裏面都包含Context參數,我們就能形成一個完整的調用鏈。那麼如何實現呢 ?在我們的工程中,有RPC模塊,有HTTP模塊,兩個模塊的請求來源肯定不一樣,因此,要實現所有服務和模塊的完整調用鏈,需要考慮http和rpc兩個不同的網絡請求的調用鏈
traceid的實現
1const TraceKey = "traceId" 2 3func NewTraceId(tag string) string { 4 now := time.Now() 5 return fmt.Sprintf("%d.%d.%s", now.Unix(), now.Nanosecond(), tag) 6} 7 8func GetTraceId(ctx context.Context) string { 9 if ctx == nil { 10 return "" 11 } 12 13 // 從Context裏面取 14 traceInfo := GetTraceIdFromContext(ctx) 15 if traceInfo == "" { 16 traceInfo = GetTraceIdFromGRPCMeta(ctx) 17 } 18 19 return traceInfo 20} 21 22func GetTraceIdFromGRPCMeta(ctx context.Context) string { 23 if ctx == nil { 24 return "" 25 } 26 if md, ok := metadata.FromIncomingContext(ctx); ok { 27 if traceHeader, inMap := md[meta.TraceIdKey]; inMap { 28 return traceHeader[0] 29 } 30 } 31 if md, ok := metadata.FromOutgoingContext(ctx); ok { 32 if traceHeader, inMap := md[meta.TraceIdKey]; inMap { 33 return traceHeader[0] 34 } 35 } 36 return "" 37} 38 39func GetTraceIdFromContext(ctx context.Context) string { 40 if ctx == nil { 41 return "" 42 } 43 traceId, ok := ctx.Value(TraceKey).(string) 44 if !ok { 45 return "" 46 } 47 return traceId 48} 49 50func SetTraceIdToContext(ctx context.Context, traceId string) context.Context { 51 return context.WithValue(ctx, TraceKey, traceId) 52}
http的traceid
對於http的服務,請求方可能是客戶端,也能是其他服務端,http的入口裏面就需要增加上traceid,然後打印日誌的時候,將TraceID打印出來形成完整鏈路。如果http server採用gin來實現的話,代碼片段如下,其他http server的庫的實現方式類似即可
1import "github.com/gin-gonic/gin" 2 3func recoveryLoggerFunc() gin.HandlerFunc { 4 return func(c *gin.Context) { 5 c.Set(trace.TraceKey, trace.NewTraceId(c.ClientIP())) 6 defer func() { 7 ...... func 省略實現 8 } 9 }() 10 c.Next() 11 } 12} 13 14engine := gin.New() 15engine.Use(OpenTracingFunc(), httpInstrumentFunc(), recoveryLoggerFunc()) 16 17 18 session := engine.Group("/sessions") 19 session.Use(sdkChecker) 20 { 21 session.POST("/recent", httpsrv.MakeHandler(RecentSessions)) 22 } 23 24 25這樣,在RecentSessions接口裏面如果打印日誌,就能夠通過Context取到traceid
access log
access log是針對http的請求來的,記錄http請求的API,響應時間,ip,響應碼,用來記錄並可以統計服務的響應情況,當然,也有其他輔助系統如SLA來專門記錄http的響應情況
Golang語言實現這個也非常簡單,而且這個是個通用功能,建議可以抽象爲一個基礎模塊,所有業務都能import後使用
1大致格式如下: 2 3http_log_pattern='%{2006-01-02T15:04:05.999-0700}t %a - %{Host}i "%r" %s - %T "%{X-Real-IP}i" "%{X-Forwarded-For}i" %{Content-Length}i - %{Content-Length}o %b %{CDN}i' 4 5 "%a", "${RemoteIP}", 6 "%b", "${BytesSent|-}", 7 "%B", "${BytesSent|0}", 8 "%H", "${Proto}", 9 "%m", "${Method}", 10 "%q", "${QueryString}", 11 "%r", "${Method} ${RequestURI} ${Proto}", 12 "%s", "${StatusCode}", 13 "%t", "${ReceivedAt|02/Jan/2006:15:04:05 -0700}", 14 "%U", "${URLPath}", 15 "%D", "${Latency|ms}", 16 "%T", "${Latency|s}", 17 18具體實現省略
最終得到的日誌如下:
12017-12-20T20:32:58.787+0800 192.168.199.15 - www.demo.com:50001 "POST /arcp/unregister HTTP/1.1" 200 - 0.035 "-" "-" 14 - - 13 - 22017-12-20T20:33:27.741+0800 192.168.199.15 - www.demo.com:50001 "POST /arcp/register HTTP/1.1" 200 - 0.104 "-" "-" 68 - - 13 - 32017-12-20T20:42:01.803+0800 192.168.199.15 - www.demo.com:50001 "POST /arcp/unregister HTTP/1.1" 200 - 0.035 "-" "-" 14 - - 13 -
版權申明:內容來源網絡,版權歸原創者所有。除非無法確認,我們都會標明作者及出處,如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝。