Golang工程經驗(上)

原文作者:吳德寶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等都已包含在內

  1. list鏈表結構,當我們需要類似隊列的結構的時候,可以採用,針對IM系統而言,在長連接層處理的消息id的列表,可以通過list來維護,如果用戶有了迴應則從list裏面移除,否則在超時時間到後還沒有迴應,則入offline處理
  2. 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編程的同學對這個模型應該是再熟悉不過了,我這邊通過代碼來說明下這個模型

  1. 首先一個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}

總結一下:

  1. 使用連接池提高性能,每次都從連接池裏面取連接而不是每次都重新建立連接
  2. 設置最大連接數和最大活躍連接(同一時刻能夠提供的連接),設置合理的讀寫超時時間
  3. 實現主從讀寫分離,提高性能,需要注意如果沒有從庫則只讀主庫
  4. TestOnBorrow用來進行健康檢測
  5. 單獨開一個goroutine協程用來定期保活【ping-pong】
  6. hash分片算法的選擇,一致性hash還是hash取模,hash取模在擴縮容的時候比較方便,一致性hash並沒有帶來明顯的優勢,我們公司內部統一建議採用hash取模
  7. 考慮如何支持雙寫策略

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 -

版權申明:內容來源網絡,版權歸原創者所有。除非無法確認,我們都會標明作者及出處,如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章