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 -

版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。

原文发布于微信公众号 - Golang语言社区(Golangweb)

原文发表时间:2018-08-31

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏点滴积累

Fish Shell

今天看到阮一峰同学的一篇博客(Fish shell 入门教程),讲述的非常详细、清楚,有兴趣的可以直接转去查看此文,本文仅提供一下个人使用心得。 一、fish ...

3666
来自专栏转载gongluck的CSDN博客

python笔记:#002#第一个python程序

第一个 Python 程序 目标 第一个 HelloPython 程序 Python 2.x 与 3​​.x 版本简介 执行 Python 程序的三种方式 ...

2773
来自专栏杨建荣的学习笔记

一个细小的空间问题触发的报警(r11笔记第68天)

今天有一个数据库服务器报警,报警信息是来自于一个异机备库。可以看到这台服务器空间只有300多G,而剩余空间只剩下了不到30G.所以这样一个问题就很奇怪了...

3657
来自专栏信安之路

ring3层恶意代码实例汇总

之前一期我们学习了 IAT 的基本结构,相信大家对 C++ 有了一个基本的认识,这一期放点干货,我把 ring3 层恶意代码常用的编程技术给大家整理了一下,所有...

1350
来自专栏比原链

剥开比原看代码12:比原是如何通过/create-account-receiver创建地址的?

Gitee地址:https://gitee.com/BytomBlockchain/bytom

1581
来自专栏iOS 开发杂谈

iOS多线程之一:基本概念

进程:就是一个正在执行的程序。 线程:是执行程序最基本的单元,它有自己栈和寄存器。

691
来自专栏Albert陈凯

2018-08-02 IntelliJ IDEA - Debug 调试多线程程序IntelliJ IDEA - Debug 调试多线程程序

https://blog.csdn.net/nextyu/article/details/79039566

1742
来自专栏斑斓

Redux框架reducer对状态的处理

前言 在react+redux项目里,关于reducer处理state的方式,在redux官方文档中有这样一段描述: 不要修改 state。 使用 Objec...

3715
来自专栏Java Web

Java I/O不迷茫,一文为你导航!

学习过计算机相关课程的童鞋应该都知道,I/O 即输入Input/ 输出Output的缩写,最容易让人联想到的就是屏幕这样的输出设备以及键盘鼠标这一类的输入设备,...

1152
来自专栏顶级程序员

Linux中高效编写Bash脚本的10个技巧

Linux开源社区(微信号:cn_linux) 英文:Aaron Kili,翻译:Linux中国/ch-cn 链接:linux.cn/article-8618...

3435

扫码关注云+社区

领取腾讯云代金券