[TOC]html
做为一个C/C++的开发者而言,开启Golang语言开发之路是很容易的,从语法、语义上的理解到工程开发,都可以快速熟悉起来;相比C、C++,Golang语言更简洁,更容易写出高并发的服务后台系统java
转战Golang一年有余,经历了两个线上项目的洗礼,总结出一些工程经验,一个是总结出一些实战经验,一个是用来发现自我不足之处mysql
Go语言是谷歌推出的一种全新的编程语言,能够在不损失应用程序性能的状况降低低代码的复杂性。Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序能够媲美C或C++代码的速度,并且更加安全、支持并行进程。linux
我基于Golang的两个实际线上项目都是IM系统,本文基于现有线上系统作一些总结性、引导性的经验输出。nginx
既然是IM系统,那么必然须要TCP长链接来维持,因为Golang自己的基础库和外部依赖库很是之多,咱们能够简单引用基础net网络库,来创建TCP server。通常的TCP Server端的模型,能够有一个协程【或者线程】去独立执行accept,而且是for循环一直accept新的链接,若是有新链接过来,那么创建链接而且执行Connect,因为Golang里面协程的开销很是之小,所以,TCP server端还能够一个链接一个goroutine去循环读取各自链接链路上的数据并处理。固然, 这个在C++语言的TCP Server模型中,通常会经过EPoll模型来创建server端,这个是和C++的区别之处。git
关于读取数据,Linux系统有recv和send函数来读取发送数据,在Golang中,自带有io库,里面封装了各类读写方法,如io.ReadFull,它会读取指定字节长度的数据github
为了维护链接和用户,而且一个链接一个用户的一一对应的,须要根据链接可以找到用户,同时也须要可以根据用户找到对应的链接,那么就须要设计一个很好结构来维护。咱们最初采用map来管理,可是发现Map里面的数据太大,查找的性能不高,为此,优化了数据结构,conn里面包含user,user里面包含conn,结构以下【只包括重要字段】。golang
// 一个用户对应一个链接
type User struct {
uid int64
conn *MsgConn
BKicked bool // 被另外登录的一方踢下线
BHeartBeatTimeout bool // 心跳超时
。。。
}
type MsgConn struct {
conn net.Conn
lastTick time.Time // 上次接收到包时间
remoteAddr string // 为每一个链接建立一个惟一标识符
user *User // MsgConn与User一一映射
。。。
}
复制代码
创建TCP server 代码片断以下正则表达式
func ListenAndServe(network, address string) {
tcpAddr, err := net.ResolveTCPAddr(network, address)
if err != nil {
logger.Fatalf(nil, "ResolveTcpAddr err:%v", err)
}
listener, err = net.ListenTCP(network, tcpAddr)
if err != nil {
logger.Fatalf(nil, "ListenTCP err:%v", err)
}
go accept()
}
func accept() {
for {
conn, err := listener.AcceptTCP()
if err == nil {
// 包计数,用来限制频率
//anti-attack, 黑白名单
...
// 新建一个链接
imconn := NewMsgConn(conn)
// run
imconn.Run()
}
}
}
func (conn *MsgConn) Run() {
//on connect
conn.onConnect()
go func() {
tickerRecv := time.NewTicker(time.Second * time.Duration(rateStatInterval))
for {
select {
case <-conn.stopChan:
tickerRecv.Stop()
return
case <-tickerRecv.C:
conn.packetsRecv = 0
default:
// 在 conn.parseAndHandlePdu 里面经过Golang自己的io库里面提供的方法读取数据,如io.ReadFull
conn_closed := conn.parseAndHandlePdu()
if conn_closed {
tickerRecv.Stop()
return
}
}
}
}()
}
// 将 user 和 conn 一一对应起来
func (conn *MsgConn) onConnect() *User {
user := &User{conn: conn, durationLevel: 0, startTime: time.Now(), ackWaitMsgIdSet: make(map[int64]struct{})}
conn.user = user
return user
}
复制代码
TCP Server的一个特色在于一个链接一个goroutine去处理,这样的话,每一个链接独立,不会相互影响阻塞,保证可以及时读取到client端的数据。若是是C、C++程序,若是一个链接一个线程的话,若是上万个或者十万个线程,那么性能会极低甚至于没法工做,cpu会所有消耗在线程之间的调度上了,所以C、C++程序没法这样玩。Golang的话,goroutine能够几十万、几百万的在一个系统中良好运行。同时对于TCP长链接而言,一个节点上的链接数要有限制策略。redis
每一个链接须要有心跳来维持,在心跳间隔时间内没有收到,服务端要检测超时并断开链接释放资源,golang能够很方便的引用须要的数据结构,同时对变量的赋值(包括指针)很是easy
var timeoutMonitorTree *rbtree.Rbtree
var timeoutMonitorTreeMutex sync.Mutex
var heartBeatTimeout time.Duration //心跳超时时间, 配置了默认值ssss
var loginTimeout time.Duration //登录超时, 配置了默认值ssss
type TimeoutCheckInfo struct {
conn *MsgConn
dueTime time.Time
}
func AddTimeoutCheckInfo(conn *MsgConn) {
timeoutMonitorTreeMutex.Lock()
timeoutMonitorTree.Insert(&TimeoutCheckInfo{conn: conn, dueTime: time.Now().Add(loginTimeout)})
timeoutMonitorTreeMutex.Unlock()
}
如 &TimeoutCheckInfo{},赋值一个指针对象
复制代码
Golang中,不少基础数据都经过库来引用,咱们能够方便引用咱们所须要的库,经过import包含就能直接使用,如源码里面提供了sync库,里面有mutex锁,在须要锁的时候能够包含进来
经常使用的如list,mutex,once,singleton等都已包含在内
list链表结构,当咱们须要相似队列的结构的时候,能够采用,针对IM系统而言,在长链接层处理的消息id的列表,能够经过list来维护,若是用户有了回应则从list里面移除,不然在超时时间到后尚未回应,则入offline处理
mutex锁,当须要并发读写某个数据的时候使用,包含互斥锁和读写锁
var ackWaitListMutex sync.RWMutex
var ackWaitListMutex sync.Mutex
复制代码
once表示任什么时候刻都只会调用一次,通常的用法是初始化实例的时候使用,代码片断以下
var initRedisOnce sync.Once
func GetRedisCluster(name string) (*redis.Cluster, error) {
initRedisOnce.Do(setupRedis)
if redisClient, inMap := redisClusterMap[name]; inMap {
return redisClient, nil
} else {
}
}
func setupRedis() {
redisClusterMap = make(map[string]*redis.Cluster)
commonsOpts := []redis.Option{
redis.ConnectionTimeout(conf.RedisConnTimeout),
redis.ReadTimeout(conf.RedisReadTimeout),
redis.WriteTimeout(conf.RedisWriteTimeout),
redis.IdleTimeout(conf.RedisIdleTimeout),
redis.MaxActiveConnections(conf.RedisMaxConn),
redis.MaxIdleConnections(conf.RedisMaxIdle),
}),
...
}
}
复制代码
这样咱们能够在任何须要的地方调用GetRedisCluster,而且不用担忧实例会被初始化屡次,once会保证必定只执行一次
singleton单例模式,这个在C++里面是一个经常使用的模式,通常须要开发者本身经过类来实现,类的定义决定单例模式设计的好坏;在Golang中,已经有成熟的库实现了,开发者无须重复造轮子,关于何时该使用单例模式请自行Google。一个简单的例子以下
import "github.com/dropbox/godropbox/singleton"
var SingleMsgProxyService = singleton.NewSingleton(func() (interface{}, error) {
cluster, _ := cache.GetRedisCluster("singlecache")
return &singleMsgProxy{
Cluster: cluster,
MsgModel: msg.MsgModelImpl,
}, nil
})
复制代码
若是说goroutine和channel是Go并发的两大基石,那么接口interface是Go语言编程中数据类型的关键。在Go语言的实际编程中,几乎全部的数据结构都围绕接口展开,接口是Go语言中全部数据结构的核心。
严格来讲,在 Golang 中并不支持泛型编程。在 C++ 等高级语言中使用泛型编程很是的简单,因此泛型编程一直是 Golang 诟病最多的地方。可是使用 interface 咱们能够实现泛型编程,以下是一个参考示例
package sort
// A type, typically a collection, that satisfies sort.Interface can be
// sorted by the routines in this package. The methods require that the
// elements of the collection be enumerated by an integer index.
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less reports whether the element with
// index i should sort before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}
...
// Sort sorts data.
// It makes one call to data.Len to determine n, and O(n*log(n)) calls to
// data.Less and data.Swap. The sort is not guaranteed to be stable.
func Sort(data Interface) {
// Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached.
n := data.Len()
maxDepth := 0
for i := n; i > 0; i >>= 1 {
maxDepth++
}
maxDepth *= 2
quickSort(data, 0, n, maxDepth)
}
复制代码
Sort 函数的形参是一个 interface,包含了三个方法:Len(),Less(i,j int),Swap(i, j int)。使用的时候无论数组的元素类型是什么类型(int, float, string…),只要咱们实现了这三个方法就可使用 Sort 函数,这样就实现了“泛型编程”。
这种方式,我在项目里面也有实际应用过,具体案例就是对消息排序。
下面给一个具体示例,代码可以说明一切,一看就懂:
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s: %d", p.Name, p.Age)
}
// ByAge implements sort.Interface for []Person based on
// the Age field.
type ByAge []Person //自定义
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func main() {
people := []Person{
{"Bob", 31},
{"John", 42},
{"Michael", 17},
{"Jenny", 26},
}
fmt.Println(people)
sort.Sort(ByAge(people))
fmt.Println(people)
}
复制代码
隐藏具体实现,这个很好理解。好比我设计一个函数给你返回一个 interface,那么你只能经过 interface 里面的方法来作一些操做,可是内部的具体实现是彻底不知道的。
例如咱们经常使用的context包,就是这样的,context 最早由 google 提供,如今已经归入了标准库,并且在原有 context 的基础上增长了:cancelCtx,timerCtx,valueCtx。
若是函数参数是interface或者返回值是interface,这样就能够接受任何类型的参数
在一个项目工程中,为了使得代码更优雅,须要抽象出一些模型出来,同时基于C++面向对象编程的思想,须要考虑到一些类、继承相关。在Golang中,没有类、继承的概念,可是咱们彻底能够经过struct和interface来创建咱们想要的任何模型。在咱们的工程中,抽象出一种我自认为是相似MVC的模型,可是不彻底同样,我的以为这个模型抽象的比较好,容易扩展,模块清晰。对于使用java和PHP编程的同窗对这个模型应该是再熟悉不过了,我这边经过代码来讲明下这个模型
首先一个model包,经过interface来实现,包含一些基础方法,须要被外部引用者来具体实现
package model
// 定义一个基础model
type MsgModel interface {
Persist(context context.Context, msg interface{}) bool
UpdateDbContent(context context.Context, msgIface interface{}) bool
...
}
复制代码
再定义一个msg包,用来具体实现model包中MsgModel模型的全部方法
package msg
type msgModelImpl struct{}
var MsgModelImpl = msgModelImpl{}
func (m msgModelImpl) Persist(context context.Context, msgIface interface{}) bool {
// 具体实现
}
func (m msgModelImpl) UpdateDbContent(context context.Context, msgIface interface{}) bool {
// 具体实现
}
...
复制代码
model 和 具体实现方定义并实现ok后,那么就还须要一个service来统筹管理
package service
// 定义一个msgService struct包含了model里面的UserModel和MsgModel两个model
type msgService struct {
msgModel model.MsgModel
}
// 定义一个MsgService的变量,并初始化,这样经过MsgService,就能引用并访问model的全部方法
var (
MsgService = msgService{
msgModel: msg.MsgModelImpl,
}
)
复制代码
调用访问
import service
service.MsgService.Persist(ctx, xxx)
复制代码
总结一下,model对应MVC的M,service 对应 MVC的C, 调用访问的地方对应MVC的V
在MVC模型的基础下,咱们还须要考虑另一点,就是基础资源的封装,服务端操做必然会和mysql、redis、memcache等交互,一些经常使用的底层基础资源,咱们有必要进行封装,这是基础架构部门所须要承担的,也是一个好的项目工程所须要的
redis,咱们在github.com/garyburd/redigo/redis的库的基础上,作了一层封装,实现了一些更为贴合工程的机制和接口,redis cluster封装,支持分片、读写分离
// NewCluster creates a client-side cluster for callers. Callers use this structure to interact with Redis database
func NewCluster(config ClusterConfig, instrumentOpts *instrument.Options) *Cluster {
cluster := new(Cluster)
cluster.pool = make([]*client, len(config.Configs))
masters := make([]string, 0, len(config.Configs))
for i, sharding := range config.Configs {
master, slaves := sharding.Master, sharding.Slaves
masters = append(masters, master)
masterAddr, masterDb := parseServer(master)
cli := new(client)
cli.master = &redisNode{
server: master,
Pool: func() *redis.Pool {
pool := &redis.Pool{
MaxIdle: config.MaxIdle,
IdleTimeout: config.IdleTimeout,
Dial: func() (redis.Conn, error) {
c, err := redis.Dial(
"tcp",
masterAddr,
redis.DialDatabase(masterDb),
redis.DialPassword(config.Password),
redis.DialConnectTimeout(config.ConnTimeout),
redis.DialReadTimeout(config.ReadTimeout),
redis.DialWriteTimeout(config.WriteTimeout),
)
if err != nil {
return nil, err
}
return c, err
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
if time.Since(t) < time.Minute {
return nil
}
_, err := c.Do("PING")
return err
},
MaxActive: config.MaxActives,
}
if instrumentOpts == nil {
return pool
}
return instrument.NewRedisPool(pool, instrumentOpts)
}(),
}
// allow nil slaves
if slaves != nil {
cli.slaves = make([]*redisNode, 0)
for _, slave := range slaves {
addr, db := parseServer(slave)
cli.slaves = append(cli.slaves, &redisNode{
server: slave,
Pool: func() *redis.Pool {
pool := &redis.Pool{
MaxIdle: config.MaxIdle,
IdleTimeout: config.IdleTimeout,
Dial: func() (redis.Conn, error) {
c, err := redis.Dial(
"tcp",
addr,
redis.DialDatabase(db),
redis.DialPassword(config.Password),
redis.DialConnectTimeout(config.ConnTimeout),
redis.DialReadTimeout(config.ReadTimeout),
redis.DialWriteTimeout(config.WriteTimeout),
)
if err != nil {
return nil, err
}
return c, err
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
if time.Since(t) < time.Minute {
return nil
}
_, err := c.Do("PING")
return err
},
MaxActive: config.MaxActives,
}
if instrumentOpts == nil {
return pool
}
return instrument.NewRedisPool(pool, instrumentOpts)
}(),
})
}
}
// call init
cli.init()
cluster.pool[i] = cli
}
if config.Hashing == sharding.Ketama {
cluster.sharding, _ = sharding.NewKetamaSharding(sharding.GetShardServers(masters), true, 6379)
} else {
cluster.sharding, _ = sharding.NewCompatSharding(sharding.GetShardServers(masters))
}
return cluster
}
复制代码
总结一下:
memcached客户端代码封装,依赖 github.com/dropbox/godropbox/memcache, 实现其ShardManager接口,支持Connection Timeout,支持Fail Fast和Rehash
实际开发过程当中,常常会有这样场景,每一个请求经过一个goroutine协程去作,如批量获取消息,可是,为了防止后端资源链接数太多等,或者防止goroutine太多,每每须要限制并发数。给出以下示例供参考
package main
import (
"fmt"
"sync"
"time"
)
var over = make(chan bool)
const MAXConCurrency = 3
//var sem = make(chan int, 4) //控制并发任务数
var sem = make(chan bool, MAXConCurrency) //控制并发任务数
var maxCount = 6
func Worker(i int) bool {
sem <- true
defer func() {
<-sem
}()
// 模拟出错处理
if i == 5 {
return false
}
fmt.Printf("now:%v num:%v\n", time.Now().Format("04:05"), i)
time.Sleep(1 * time.Second)
return true
}
func main() {
//wg := &sync.WaitGroup{}
var wg sync.WaitGroup
for i := 1; i <= maxCount; i++ {
wg.Add(1)
fmt.Printf("for num:%v\n", i)
go func(i int) {
defer wg.Done()
for x := 1; x <= 3; x++ {
if Worker(i) {
break
} else {
fmt.Printf("retry :%v\n", x)
}
}
}(i)
}
wg.Wait() //等待全部goroutine退出
}
复制代码
Golang 的 context很是强大,详细的能够参考个人另一篇文章 Golang Context分析
这里想要说明的是,在项目工程中,咱们常常会用到这样的一个场景,经过goroutine并发去处理某些批量任务,当某个条件触发的时候,这些goroutine要可以控制中止执行。若是有这样的场景,那么我们就须要用到context的With 系列函数了,context.WithCancel生成了一个withCancel的实例以及一个cancelFuc,这个函数就是用来关闭ctxWithCancel中的 Done channel 函数。
示例代码片断以下
func Example(){
// context.WithCancel 用来生成一个新的Context,能够接受cancel方法用来随时中止执行
newCtx, cancel := context.WithCancel(context.Background())
for peerIdVal, lastId := range lastIdMap {
wg.Add(1)
go func(peerId, minId int64) {
defer wg.Done()
msgInfo := Get(newCtx, uid, peerId, minId, count).([]*pb.MsgInfo)
if msgInfo != nil && len(msgInfo) > 0 {
if singleMsgCounts >= maxCount {
cancel() // 当条件触发,则调用cancel中止
mutex.Unlock()
return
}
}
mutex.Unlock()
}(peerIdVal, lastId)
}
wg.Wait()
}
func Get(ctx context.Context, uid, peerId, sinceId int64, count int) interface{} {
for {
select {
// 若是收到Done的chan,则立马return
case <-ctx.Done():
msgs := make([]*pb.MsgInfo, 0)
return msgs
default:
// 处理逻辑
}
}
}
复制代码
在大型项目工程中,为了更好的排查定位问题,咱们须要有必定的技巧,Context上下文存在于一整条调用链路中,在服务端并发场景下,n多个请求里面,咱们如何可以快速准确的找到一条请求的前因后果,专业用语就是指调用链路,经过调用链咱们可以知道这条请求通过了哪些服务、哪些模块、哪些方法,这样能够很是方便咱们定位问题
traceid就是咱们抽象出来的这样一个调用链的惟一标识,再经过Context进行传递,在任何代码模块[函数、方法]里面都包含Context参数,咱们就能造成一个完整的调用链。那么如何实现呢 ?在咱们的工程中,有RPC模块,有HTTP模块,两个模块的请求来源确定不同,所以,要实现全部服务和模块的完整调用链,须要考虑http和rpc两个不一样的网络请求的调用链
const TraceKey = "traceId"
func NewTraceId(tag string) string {
now := time.Now()
return fmt.Sprintf("%d.%d.%s", now.Unix(), now.Nanosecond(), tag)
}
func GetTraceId(ctx context.Context) string {
if ctx == nil {
return ""
}
// 从Context里面取
traceInfo := GetTraceIdFromContext(ctx)
if traceInfo == "" {
traceInfo = GetTraceIdFromGRPCMeta(ctx)
}
return traceInfo
}
func GetTraceIdFromGRPCMeta(ctx context.Context) string {
if ctx == nil {
return ""
}
if md, ok := metadata.FromIncomingContext(ctx); ok {
if traceHeader, inMap := md[meta.TraceIdKey]; inMap {
return traceHeader[0]
}
}
if md, ok := metadata.FromOutgoingContext(ctx); ok {
if traceHeader, inMap := md[meta.TraceIdKey]; inMap {
return traceHeader[0]
}
}
return ""
}
func GetTraceIdFromContext(ctx context.Context) string {
if ctx == nil {
return ""
}
traceId, ok := ctx.Value(TraceKey).(string)
if !ok {
return ""
}
return traceId
}
func SetTraceIdToContext(ctx context.Context, traceId string) context.Context {
return context.WithValue(ctx, TraceKey, traceId)
}
复制代码
对于http的服务,请求方多是客户端,也能是其余服务端,http的入口里面就须要增长上traceid,而后打印日志的时候,将TraceID打印出来造成完整链路。若是http server采用gin来实现的话,代码片断以下,其余http server的库的实现方式相似便可
import "github.com/gin-gonic/gin"
func recoveryLoggerFunc() gin.HandlerFunc {
return func(c *gin.Context) {
c.Set(trace.TraceKey, trace.NewTraceId(c.ClientIP()))
defer func() {
...... func 省略实现
}
}()
c.Next()
}
}
engine := gin.New()
engine.Use(OpenTracingFunc(), httpInstrumentFunc(), recoveryLoggerFunc())
session := engine.Group("/sessions")
session.Use(sdkChecker)
{
session.POST("/recent", httpsrv.MakeHandler(RecentSessions))
}
这样,在RecentSessions接口里面若是打印日志,就可以经过Context取到traceid
复制代码
access log是针对http的请求来的,记录http请求的API,响应时间,ip,响应码,用来记录并能够统计服务的响应状况,固然,也有其余辅助系统如SLA来专门记录http的响应状况
Golang语言实现这个也很是简单,并且这个是个通用功能,建议能够抽象为一个基础模块,全部业务都能import后使用
大体格式以下:
http_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'
"%a", "${RemoteIP}",
"%b", "${BytesSent|-}",
"%B", "${BytesSent|0}",
"%H", "${Proto}",
"%m", "${Method}",
"%q", "${QueryString}",
"%r", "${Method} ${RequestURI} ${Proto}",
"%s", "${StatusCode}",
"%t", "${ReceivedAt|02/Jan/2006:15:04:05 -0700}",
"%U", "${URLPath}",
"%D", "${Latency|ms}",
"%T", "${Latency|s}",
具体实现省略
复制代码
最终获得的日志以下:
2017-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 -
2017-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 -
2017-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的语言特性,可是是工程项目里面必要的,在Golang项目中的具体实现代码片断以下:
package switches
var (
xxxSwitchManager = SwitchManager{switches: make(map[string]*Switch)}
AsyncProcedure = &Switch{Name: "xxx.msg.procedure.async", On: true}
// 使能音视频
EnableRealTimeVideo = &Switch{Name: "xxx.real.time.video", On: true}
)
func init() {
xxxSwitchManager.Register(AsyncProcedure,
EnableRealTimeVideo)
}
// 具体实现结构和实现方法
type Switch struct {
Name string
On bool
listeners []ChangeListener
}
func (s *Switch) TurnOn() {
s.On = true
s.notifyListeners()
}
func (s *Switch) notifyListeners() {
if len(s.listeners) > 0 {
for _, l := range s.listeners {
l.OnChange(s.Name, s.On)
}
}
}
func (s *Switch) TurnOff() {
s.On = false
s.notifyListeners()
}
func (s *Switch) IsOn() bool {
return s.On
}
func (s *Switch) IsOff() bool {
return !s.On
}
func (s *Switch) AddChangeListener(l ChangeListener) {
if l == nil {
return
}
s.listeners = append(s.listeners, l)
}
type SwitchManager struct {
switches map[string]*Switch
}
func (m SwitchManager) Register(switches ...*Switch) {
for _, s := range switches {
m.switches[s.Name] = s
}
}
func (m SwitchManager) Unregister(name string) {
delete(m.switches, name)
}
func (m SwitchManager) TurnOn(name string) (bool, error) {
if s, ok := m.switches[name]; ok {
s.TurnOn()
return true, nil
} else {
return false, errors.New("switch " + name + " is not registered")
}
}
func (m SwitchManager) TurnOff(name string) (bool, error) {
if s, ok := m.switches[name]; ok {
s.TurnOff()
return true, nil
} else {
return false, errors.New("switch " + name + " is not registered")
}
}
func (m SwitchManager) IsOn(name string) (bool, error) {
if s, ok := m.switches[name]; ok {
return s.IsOn(), nil
} else {
return false, errors.New("switch " + name + " is not registered")
}
}
func (m SwitchManager) List() map[string]bool {
switches := make(map[string]bool)
for name, switcher := range m.switches {
switches[name] = switcher.On
}
return switches
}
type ChangeListener interface {
OnChange(name string, isOn bool)
}
// 这里开始调用
if switches.AsyncProcedure.IsOn() {
// do sth
}else{
// do other sth
}
复制代码
prometheus + grafana 是业界经常使用的监控方案,prometheus进行数据采集,grafana进行图表展现。
Golang里面prometheus进行数据采集很是简单,有对应client库,应用程序只需暴露出http接口便可,这样,prometheus server端就能够按期采集数据,而且还能够根据这个接口来监控服务端是否异常【如挂掉的状况】。
import "github.com/prometheus/client_golang/prometheus"
engine.GET("/metrics", gin.WrapH(prometheus.Handler()))
复制代码
这样就实现了数据采集,可是具体采集什么样的数据,数据从哪里生成的,还须要进入下一步:
package prometheus
import "github.com/prometheus/client_golang/prometheus"
var DefaultBuckets = []float64{10, 50, 100, 200, 500, 1000, 3000}
var MySQLHistogramVec = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: "allen.wu",
Subsystem: "xxx",
Name: "mysql_op_milliseconds",
Help: "The mysql database operation duration in milliseconds",
Buckets: DefaultBuckets,
},
[]string{"db"},
)
var RedisHistogramVec = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: "allen.wu",
Subsystem: "xxx",
Name: "redis_op_milliseconds",
Help: "The redis operation duration in milliseconds",
Buckets: DefaultBuckets,
},
[]string{"redis"},
)
func init() {
prometheus.MustRegister(MySQLHistogramVec)
prometheus.MustRegister(RedisHistogramVec)
...
}
// 使用,在对应的位置调用prometheus接口生成数据
instanceOpts := []redis.Option{
redis.Shards(shards...),
redis.Password(viper.GetString(conf.RedisPrefix + name + ".password")),
redis.ClusterName(name),
redis.LatencyObserver(func(name string, latency time.Duration) {
prometheus.RedisHistogramVec.WithLabelValues(name).Observe(float64(latency.Nanoseconds()) * 1e-6)
}),
}
复制代码
捕获异常是否有存在的必要,根据各自不一样的项目自行决定,可是通常出现panic,若是没有异常,那么服务就会直接挂掉,若是可以捕获异常,那么出现panic的时候,服务不会挂掉,只是当前致使panic的某个功能,没法正常使用,我的建议仍是在某些有必要的条件和入口处进行异常捕获。
常见抛出异常的状况:数组越界、空指针空对象,类型断言失败等;Golang里面捕获异常经过 defer + recover来实现
C++有try。。。catch来进行代码片断的异常捕获,Golang里面有recover来进行异常捕获,这个是Golang语言的基本功,是一个比较简单的功能,很少说,看代码
func consumeSingle(kafkaMsg *sarama.ConsumerMessage) {
var err error
defer func() {
if r := recover(); r != nil {
if e, ok := r.(error); ok {
// 异常捕获的处理
}
}
}()
}
复制代码
在请求来源入口处的函数或者某个方法里面实现这么一段代码进行捕获,这样,只要经过这个入口出现的异常都能被捕获,并打印详细日志
error错误,能够自定义返回,通常工程应用中的作法,会在方法的返回值上增长一个error返回值,Golang容许每一个函数返回多个返回值,增长一个error的做用在于,获取函数返回值的时候,根据error参数进行判断,若是是nil表示没有错误,正常处理,不然处理错误逻辑。这样减小代码出现异常状况
若是某些状况下,没有捕获异常,程序在运行过程当中出现panic,通常都会有一些堆栈信息,咱们如何根据这些堆栈信息快速定位并解决呢 ?
通常信息里面都会代表是哪一种相似的panic,如是空指针异常仍是数组越界,仍是xxx;
而后会打印一堆信息出来包括出现异常的代码调用块及其文件位置,须要定位到最后的位置而后反推上去
分析示例以下
{"date":"2017-11-22 19:33:20.921","pid":17,"level":"ERROR","file":"recovery.go","line":16,"func":"1","msg":"panic in /Message.MessageService/Proces s: runtime error: invalid memory address or nil pointer dereference github.com.xxx/demo/biz/vendor/github.com.xxx/demo/commons/interceptor.newUnaryServerRecoveryInterceptor.func1.1 /www/jenkins_home/.jenkins/jobs/demo/jobs/demo--biz/workspace/src/github.com.xxx/demo/biz/vendor/github.com.xxx/demo/commons/ interceptor/recovery.go:17 runtime.call64 /www/jenkins_home/.jenkins/tools/org.jenkinsci.plugins.golang.GolangInstallation/go1.9/go/src/runtime/asm_amd64.s:510 runtime.gopanic /www/jenkins_home/.jenkins/tools/org.jenkinsci.plugins.golang.GolangInstallation/go1.9/go/src/runtime/panic.go:491 runtime.panicmem /www/jenkins_home/.jenkins/tools/org.jenkinsci.plugins.golang.GolangInstallation/go1.9/go/src/runtime/panic.go:63 runtime.sigpanic /www/jenkins_home/.jenkins/tools/org.jenkinsci.plugins.golang.GolangInstallation/go1.9/go/src/runtime/signal_unix.go:367 github.com.xxx/demo/biz/vendor/github.com.xxx/demo/mtrace-middleware-go/grpc.OpenTracingClientInterceptor.func1 /www/jenkins_home/.jenkins/jobs/demo/jobs/demo--biz/workspace/src/github.com.xxx/demo/biz/vendor/github.com.xxx/demo/m trace-middleware-go/grpc/client.go:49 github.com.xxx/demo/biz/vendor/github.com/grpc-ecosystem/go-grpc-middleware.ChainUnaryClient.func2.1.1 /www/jenkins_home/.jenkins/jobs/demo/jobs/demo--biz/workspace/src/github.com.xxx/demo/biz/vendor/github.com/grpc-ecosystem/go-gr pc-middleware/chain.go:90 github.com.xxx/demo/biz/vendor/github.com/grpc-ecosystem/go-grpc-middleware/retry.UnaryClientInterceptor.func1 复制代码
问题分析
经过报错的堆栈信息,能够看到具体错误是“runtime error: invalid memory address or nil pointer dereference”,也就是空指针异常,而后逐步定位日志,能够发现最终致使出现异常的函数在这个,以下:
github.com.xxx/demo/biz/vendor/github.com.xxx/demo/mtrace-middleware-go/grpc.OpenTracingClientInterceptor.func1
/www/jenkins_home/.jenkins/jobs/demo/jobs/demo--biz/workspace/src/github.com.xxx/demo/biz/vendor/github.com.xxx/demo/m
trace-middleware-go/grpc/client.go:49
复制代码
通常panic,都会有上述错误日志,而后经过日志,能够追踪到具体函数,而后看到OpenTracingClientInterceptor后,是在client.go的49行,而后开始反推,经过代码能够看到,多是trace指针为空。而后一步一步看是从哪里开始调用的
最终发现代码以下:
ucConn, err := grpcclient.NewClientConn(conf.Discovery.UserCenter, newBalancer, time.Second*3, conf.Tracer)
if err != nil {
logger.Fatalf(nil, "init user center client connection failed: %v", err)
return
}
UserCenterClient = pb.NewUserCenterServiceClient(ucConn)
复制代码
那么开始排查,conf.Tracer是否是可能为空,在哪里初始化,初始化有没有错,而后发现这个函数是在init里面,而后conf.Tracer确实在main函数里面显示调用的,main函数里面会引用或者间接引用全部包,那么init就必定在main以前执行。
这样的话,init执行的时候,conf.Tracer尚未被赋值,所以就是nil,就会致使panic了
项目中若是可以有一些调试debug接口,有一些pprof性能分析接口,有探测、健康检查接口的话,会给整个项目在线上稳定运行带来很大的做用。 除了pprof性能分析接口属于Golang特有,其余的接口在任何语言都有,这里只是代表在一个工程中,须要有这类型的接口
咱们的工程是经过etcd进行服务发现和注册的,同时还提供http服务,那么就须要有个机制来上下线,这样上线过程当中,若是服务自己尚未所有启动完成准备就绪,那么就暂时不要在etcd里面注册,不要上线,以避免有请求过来,等到就绪后再注册;下线过程当中,先从etcd里面移除,这样流量再也不导入过来,而后再等待一段时间用来处理还未完成的任务
咱们的作法是,start 和 stop 服务的时候,调用API接口,而后再在服务的API接口里面注册和反注册到etcd
var OnlineHook = func() error {
return nil
}
var OfflineHook = func() error {
return nil
}
// 初始化两个函数,注册和反注册到etcd的函数
api.OnlineHook = func() error {
return registry.Register(conf.Discovery.RegisterAddress)
}
api.OfflineHook = func() error {
return registry.Deregister()
}
// 设置在线的函数里面分别调用上述两个函数,用来上下线
func SetOnline(isOnline bool) (err error) {
if conf.Discovery.RegisterEnabled {
if !isServerOnline && isOnline {
err = OnlineHook()
} else if isServerOnline && !isOnline {
err = OfflineHook()
}
}
if err != nil {
return
}
isServerOnline = isOnline
return
}
SetOnline 为Http API接口调用的函数
复制代码
对于http的服务,通常访问都经过域名访问,nginx配置代理,这样保证服务能够随意扩缩容,可是nginx既然配置了代码,后端节点的状况,就必需要可以有接口能够探测,这样才能保证流量导入到的节点必定的在健康运行中的节点;为此,服务必需要提供健康检测的接口,这样才能方便nginx代理可以实时更新节点。
这个接口如何实现?nginx代理通常经过http code来处理,若是返回code=200,认为节点正常,若是是非200,认为节点异常,若是连续采样屡次都返回异常,那么nginx将节点下掉
如提供一个/devops/status 的接口,用来检测,接口对应的具体实现为:
func CheckHealth(c *gin.Context) {
// 首先状态码设置为非200,如503
httpStatus := http.StatusServiceUnavailable
// 若是当前服务正常,并服务没有下线,则更新code
if isServerOnline {
httpStatus = http.StatusOK
}
// 不然返回code为503
c.IndentedJSON(httpStatus, gin.H{
onlineParameter: isServerOnline,
})
}
复制代码
// PProf
profGroup := debugGroup.Group("/pprof")
profGroup.GET("/", func(c *gin.Context) {
pprof.Index(c.Writer, c.Request)
})
profGroup.GET("/goroutine", gin.WrapH(pprof.Handler("goroutine")))
profGroup.GET("/block", gin.WrapH(pprof.Handler("block")))
profGroup.GET("/heap", gin.WrapH(pprof.Handler("heap")))
profGroup.GET("/threadcreate", gin.WrapH(pprof.Handler("threadcreate")))
profGroup.GET("/cmdline", func(c *gin.Context) {
pprof.Cmdline(c.Writer, c.Request)
})
profGroup.GET("/profile", func(c *gin.Context) {
pprof.Profile(c.Writer, c.Request)
})
profGroup.GET("/symbol", func(c *gin.Context) {
pprof.Symbol(c.Writer, c.Request)
})
profGroup.GET("/trace", func(c *gin.Context) {
pprof.Trace(c.Writer, c.Request)
})
复制代码
// Debug
debugGroup := engine.Group("/debug")
debugGroup.GET("/requests", func(c *gin.Context) {
c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
trace.Render(c.Writer, c.Request, true)
})
debugGroup.GET("/events", func(c *gin.Context) {
c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
trace.RenderEvents(c.Writer, c.Request, true)
})
复制代码
前面有讲到过,在代码里面须要有开关和降级机制,并讲了实现示例,那么若是须要可以实时改变开关状态,而且实时生效,咱们就能够提供一下http的API接口,供运维人员或者开发人员使用。
// Switch
console := engine.Group("/switch")
{
console.GET("/list", httpsrv.MakeHandler(ListSwitches))
console.GET("/status", httpsrv.MakeHandler(CheckSwitchStatus))
console.POST("/turnOn", httpsrv.MakeHandler(TurnSwitchOn))
console.POST("/turnOff", httpsrv.MakeHandler(TurnSwitchOff))
}
复制代码
单元测试用例是必须,是自测的一个必要手段,Golang里面单元测试很是简单,import testing 包,而后执行go test,就可以测试某个模块代码
如,在某个user文件夹下有个user包,包文件为user.go,里面有个Func UpdateThemesCounts,若是想要进行test,那么在同级目录下,创建一个user_test.go的文件,包含testing包,编写test用例,而后调用go test便可
通常的规范有:
以下:
// user.go
func UpdateThemesCounts(ctx context.Context, themes []int, count int) error {
redisClient := model.GetRedisClusterForTheme(ctx)
key := themeKeyPattern
for _, theme := range themes {
if redisClient == nil {
return errors.New("now redis client")
}
total, err := redisClient.HIncrBy(ctx, key, theme, count)
if err != nil {
logger.Errorf(ctx, "add key:%v for theme:%v count:%v failed:%v", key, theme, count, err)
return err
} else {
logger.Infof(ctx, "now key:%v theme:%v total:%v", key, theme, total)
}
}
return nil
}
//user_test.go
package user
import (
"fmt"
"testing"
"Golang.org/x/net/context"
)
func TestUpdateThemeCount(t *testing.T) {
ctx := context.Background()
theme := 1
count := 123
total, err := UpdateThemeCount(ctx, theme, count)
fmt.Printf("update theme:%v counts:%v err:%v \n", theme, total, err)
}
在此目录下执行 go test便可出结果
复制代码
一般,一个包里面会有多个方法,多个文件,所以也有多个test用例,假如咱们只想测试某一个方法的时候,那么咱们须要指定某个文件的某个方案
以下:
allen.wu@allen.wudeMacBook-Pro-4:~/Documents/work_allen.wu/goDev/Applications/src/github.com.xxx/avatar/app_server/service/centralhub$tree .
.
├── msghub.go
├── msghub_test.go
├── pushhub.go
├── rtvhub.go
├── rtvhub_test.go
├── userhub.go
└── userhub_test.go
0 directories, 7 files
复制代码
总共有7个文件,其中有三个test文件,假如咱们只想要测试rtvhub.go里面的某个方法,若是直接运行go test,就会测试全部test.go文件了。
所以咱们须要在go test 后面再指定咱们须要测试的test.go 文件和 它的源文件,以下:
go test -v msghub.go msghub_test.go
复制代码
在测试单个文件之下,假如咱们单个文件下,有多个方法,咱们还想只是测试单个文件下的单个方法,要如何实现?咱们须要再在此基础上,用 -run 参数指定具体方法或者使用正则表达式。
假如test文件以下:
package centralhub
import (
"context"
"testing"
)
func TestSendTimerInviteToServer(t *testing.T) {
ctx := context.Background()
err := sendTimerInviteToServer(ctx, 1461410596, 1561445452, 2)
if err != nil {
t.Errorf("send to server friendship build failed. %v", err)
}
}
func TestSendTimerInvite(t *testing.T) {
ctx := context.Background()
err := sendTimerInvite(ctx, "test", 1461410596, 1561445452)
if err != nil {
t.Errorf("send timeinvite to client failed:%v", err)
}
}
复制代码
go test -v msghub.go msghub_test.go -run TestSendTimerInvite
go test -v msghub.go msghub_test.go -run "SendTimerInvite"
复制代码
指定目录便可 go test
go test工具给咱们提供了测试覆盖度的参数,
go test -v -cover
go test -cover -coverprofile=cover.out -covermode=count
go tool cover -func=cover.out
服务端开发者若是在mac上开发,那么Golang工程的代码能够直接在mac上编译运行,而后若是须要部署在Linux系统的时候,在编译参数里面指定GOOS便可,这样能够本地调试ok后再部署到Linux服务器。
若是要部署到Linux服务,编译参数的指定为
ldflags=" -X ${repo}/version.version=${version} -X ${repo}/version.branch=${branch} -X ${repo}/version.goVersion=${go_version} -X ${repo}/version.buildTime=${build_time} -X ${repo}/version.buildUser=${build_user} "
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "${ldflags}" -o $binary_dir/$binary_name ${repo}/
复制代码
对于GC,咱们要收集起来,记录到日志文件中,这样方便后续排查和定位,启动的时候指定一下便可执行gc,收集gc日志能够重定向
export GIN_MODE=release
GODEBUG=gctrace=1 $SERVER_ENTRY 1>/dev/null 2>$LOGDIR/gc.log.`date "+%Y%m%d%H%M%S"` &
复制代码
整个项目包括两大类,一个是本身编写的代码模块,一个是依赖的代码,依赖包须要有进行包管理,本身的编写的代码工程须要有一个合适的目录进行管理 main.go :入口 doc : 文档 conf : 配置相关 ops : 运维操做相关【http接口】 api : API接口【http交互接口】 daemon : 后台daemon相关 model : model模块,操做底层资源 service : model的service grpcclient : rpc client registry : etcd 注册 processor : 异步kafka消费
.
├── README.md
├── api
├── conf
├── daemon
├── dist
├── doc
├── grpcclient
├── main.go
├── misc
├── model
├── ops
├── processor
├── registry
├── service
├── tools
├── vendor
└── version
复制代码
go容许import不一样代码库的代码,例如github.com, golang.org等等;对于须要import的代码,可使用 go get 命令取下来放到GOPATH对应的目录中去。
对于go来讲,其实并不care你的代码是内部仍是外部的,总之都在GOPATH里,任何import包的路径都是从GOPATH开始的;惟一的区别,就是内部依赖的包是开发者本身写的,外部依赖的包是go get下来的。
依赖GOPATH来解决go import有个很严重的问题:若是项目依赖的包作了修改,或者干脆删掉了,会影响到其余现有的项目。为了解决这个问题,go在1.5版本引入了vendor属性(默认关闭,须要设置go环境变量GO15VENDOREXPERIMENT=1),并在1.6版本以后都默认开启了vendor属性。 这样的话,全部的依赖包都在项目工程的vendor中了,每一个项目都有各自的vendor,互不影响;可是vendor里面的包没有版本信息,不方便进行版本管理。
目前市场上经常使用的包管理工具主要有godep、glide、dep
godep的使用者众多,如docker,kubernetes, coreos等go项目不少都是使用godep来管理其依赖,固然缘由多是早期也没的工具可选,早期咱们也是使用godep进行包管理。
使用比较简单,godep save;godep restore;godep update;
可是后面随着咱们使用和项目的进一步增强,咱们发现godep有诸多痛点,目前已经逐步开始弃用godep,新项目都开始采用dep进行管理了。
godep的痛点:
godep若是遇到依赖项目里有vendor的时候就可能会致使编译不过,vendor下再嵌套vendor,就会致使编译的时候出现版本不一致的错误,会提示某个方法接口不对,所有放在当前项目的vendor下
godep锁定版本太麻烦了,在项目进一步发展过程当中,咱们依赖的项目(包)多是早期的,后面因为升级更新,某些API接口可能有变;可是咱们项目若是已经上线稳定运行,咱们不想用新版,那么就须要锁定某个特定版本。可是这个对于godep而言,操做着实不方便。
godep的时候,常常会有一些包须要特定版本,而后包依赖编译不过,尤为是在多人协做的时候,本地gopath和vendor下的版本不同,而后本地gopath和别人的gopath的版本不同,致使编译会遇到各类依赖致使的问题
glide也是在vendor以后出来的。glide的依赖包信息在glide.yaml和glide.lock中,前者记录了全部依赖的包,后者记录了依赖包的版本信息
glide create # 建立glide工程,生成glide.yaml glide install # 生成glide.lock,并拷贝依赖包 glide update # 更新依赖包信息,更新glide.lock
由于glide官方说咱们不更新功能了,只bugfix,请你们开始使用dep吧,因此鉴于此,咱们在选择中就放弃了。同时,glide若是遇到依赖项目里有vendor的时候就直接跪了,dep的话,就会滤掉,不会再vendor下出现嵌套的,所有放在当前项目的vendor下
golang官方出品,dep最近的版本已经作好了从其余依赖工具的vendor迁移过来的功能,功能很强大,是咱们目前的最佳选择。不过目前尚未release1.0 ,可是已经能够用在生成环境中,对于新项目,我建议采用dep进行管理,不会有历史问题,并且当新项目上线的时候,dep也会进一步优化而且可能先于你的项目上线。
dep默认从github上拉取最新代码,若是想优先使用本地gopath,那么3.x版本的dep须要显式参数注明,以下
dep init -gopath -v
复制代码
godep是最初使用最多的,可以知足大部分需求,也比较稳定,可是有一些不太好的体验;
glide 有版本管理,相对强大,可是官方表示再也不进行开发;
dep是官方出品,目前没有release,功能一样强大,是目前最佳选择;
go vendor 缺失致使import屡次致使panic
本工程下没有vendor目录,然而,引入了这个包“github.com.xxx/demo/biz/model/impl/hash”, 这个biz包里面包含了vendor目录。
这样,编译此工程的时候,会致使一部分import是从oracle下的vendor,另外一部分是从gopath,这样就会出现一个包被两种不一样方式import,致使出现重复注册而panic
fatal error: concurrent map read and map write
并发编程中最容易出现资源竞争,之前玩C++的时候,资源出现竞争只会致使数据异常,不会致使程序异常panic,Golang里面会直接抛错,这个是比较好的作法,由于异常数据最终致使用户的数据异常,影响很大,甚至没法恢复,直接抛错后交给开发者去修复代码bug,通常在测试过程当中或者代码review过程当中就可以发现并发问题。
并发的处理方案有二:
Golang不容许包直接相互import,会致使编译不过。可是有个项目里面,A同窗负责A模块,B同窗负责B模块,因为产品需求致使,A模块要调用B模块中提供的方法,B模块要调用A模块中提供的方法,这样就致使了相互引用了
咱们的解决方案是: 将其中一个相互引用的模块中的方法提炼出来,独立为另一个模块,也就是另一个包,这样就不至于相互引用
Golang进行json转换的时候,经常使用作法是一个定义struct,成员变量使用tag标签,而后经过自带的json包进行处理,容易出现的问题主要有:
golang使用一年多以来,我的认为golang有以下优势:
【"欢迎关注个人微信公众号:Linux 服务端系统研发,后面会大力经过微信公众号发送优质文章"】