最近小伙伴们刚完成广告系统,第二个直接服务于业务的项目。踩了一些坑,更收获了不少知识。总结出来与大家分享,没什么高大尚技术,都是周边的小技巧,加深对 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() <-chan struct{}
// 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.
在每个耗时请求(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
有疑问加站长微信联系(非本文作者)