go

LedisDB嵌入使用介绍

ledisdb现在可以支持嵌入式使用。你可以将其作为一个独立的lib(类似leveldb)直接嵌入到你自己的应用中去,而无需在启动单独的服务。

ledisdb提供的API仍然类似redis接口。首先,你需要创建ledis对象:

import "github.com/siddontang/ledisdb/ledis"

configJson = []byte('{
    "data_db" : 
    {
        "path" : "/tmp/testdb",
        "compression":true,
        "block_size" : 32768,
        "write_buffer_size" : 2097152,
        "cache_size" : 20971520
    }    
}
')

l, _ := ledis.Open(configJson)

data_db就是数据存储的leveldb位置,简单起见,所有的size配置全部使用byte作为单位。

然后我们选择一个db使用,

db, _ := l.Select(0)

类似redis,我们也只支持数字类型的db,最多16个db,索引范围为[0-15]。支持太多的db真没啥意义。

下面是一些简单的例子:

kv

db.Set(key, value)
db.Get(key)
db.SetNX(key, value)
db.Incr(key)
db.IncrBy(key, 10)
db.Decr(key)
db.DecrBy(key, 10)

db.MSet(KVPair{key1, value1}, KVPair{key2, value2})
db.MGet(key1, key2)

list

db.LPush(key, value1, value2, value3)
db.RPush(key, value4, value5, value6)

db.LRange(key, 1, 10)
db.LIndex(key, 10)

db.LLen(key)

hash

db.HSet(key, field1, value1)
db.HMSet(key, FVPair{field1, value1}, FVPair{field2, value2})

db.HGet(key, field1)

db.HGetAll()
db.HKeys()

zset

db.ZAdd(key, ScorePair{score1, member1}, ScorePair{score2, member2})

db.ZCard(key)

//range by score [0, 100], withscores = true and no limit
db.ZRangeByScore(key, 0, 100, true, 0, -1)

//range by score [0, 100], withscores = true and limit offset = 10, count = 10
db.ZRangeByScore(key, 0, 100, true, 10, 10)

db.ZRank(key, member1)

db.ZCount(key, member1)

ledisdb的源码在这里https://github.com/siddontang/ledisdb,欢迎反馈。

Go语言最佳实践

最近看了一篇关于go产品开发最佳实践的文章,go-in-procution。作者总结了他们在用go开发过程中的很多实际经验,我们很多其实也用到了,鉴于此,这里就简单的写写读后感,后续我也争取能将这篇文章翻译出来。后面我用soundcloud来指代原作者。

开发环境

在soundcloud,每个人使用一个独立的GOPATH,并且在GOPATH直接按照go规定的代码路径方式clone代码。

$ mkdir -p $GOPATH/src/github.com/soundcloud
$ cd $GOPATH/src/github.com/soundcloud
$ git clone git@github.com:soundcloud/roshi

对于go来说,通常的工程管理应该是如下的目录结构:

proj/
    src/
        modulea/
            a.go
        moudleb/
            b.go
        app/
            main.go
    pkg/
    bin/

然后我们在GOPATH里面将proj的路径设置上去,这样就可以进行编译运行了。这本来没啥,但是如果我们要将其代码提交到github,并允许另外的开发者使用,我们就不能将整个proj的东西提交上面,如果提交了,就很蛋疼了。外面的开发者可能这么引用:

import "github.com/yourname/proj/src/modulea"

但是我们自己在代码里面就可以直接:

import "github.com/yourname/proj/modulea"

如果外面的开发者需要按照去掉src的引用方式,只能把GOPATH设置到proj目录,如果import的多了,会让人崩溃的。

我曾今也被这事情给折腾了好久,终于再看了vitess的代码之后,发现了上面这种方式,觉得非常不错。

工程目录结构

如果一个项目中文件数量不是很多,直接放在main包里面就行了,不需要在拆分成多个包了,譬如:

github.com/soundcloud/simple/
    README.md
    Makefile
    main.go
    main_test.go
    support.go
    support_test.go

如果真的有公共的类库,在拆分成单独的包处理。

有时候,一个工程可能会包括多个二进制应用。譬如,一个job可能需要一个server,一个worker或者一个janitor,在这种情况下,建立多个子目录作为不同的main包,分别放置不同的二进制应用。同时使用另外的子目录实现公共的函数。

github.com/soundcloud/complex/
README.md
Makefile
complex-server/
    main.go
    main_test.go
    handlers.go
    handlers_test.go
complex-worker/
    main.go
    main_test.go
    process.go
    process_test.go
shared/
    foo.go
    foo_test.go
    bar.go
    bar_test.go

这点我的做法稍微有一点不一样,主要是参考vitess,我喜欢建立一个总的cmd目录,然后再在里面设置不同的子目录,这样外面就不需要猜测这个目录是库还是应用。

代码风格

代码风格这没啥好说的,直接使用gofmt解决,通常我们也约定gofmt的时候不带任何其他参数。

最好将你的编辑器配置成保存代码的时候自动进行gofmt处理。

Google最近发布了go的代码规范,soundcloud做了一些改进:

  • 避免命名函数返回值,除非能明确的表明含义。
  • 尽量少用make和new,除非真有必要,或者预先知道需要分配的大小。
  • 使用struct{}作为标记值,而不是bool或者interface{}。譬如set我们就用map[string]struct{}来实现,而不是map[string]bool。

如果一个函数有多个参数,并且单行长度很长,需要拆分,最好不用java的方式:

// Don't do this.
func process(dst io.Writer, readTimeout,
    writeTimeout time.Duration, allowInvalid bool,
    max int, src <-chan util.Job) {
    // ...
}

而是使用:

func process(
    dst io.Writer,
    readTimeout, writeTimeout time.Duration,
    allowInvalid bool,
    max int,
    src <-chan util.Job,
) {
    // ...
}

类似的,当构造一个对象的时候,最好在初始化的时候就传入相关参数,而不是在后面设置:

f := foo.New(foo.Config{    
    Site: "zombo.com",            
    Out:  os.Stdout,
    Dest: conference.KeyPair{
        Key:   "gophercon",
        Value: 2014,
    },
})

// Don't do this.
f := &Foo{} // or, even worse: new(Foo)
f.Site = "zombo.com"
f.Out = os.Stdout
f.Dest.Key = "gophercon"
f.Dest.Value = 2014

如果一些变量是后续通过其他操作才能获取的,我觉得就可以在后续设置了。

配置

soundcloud使用go的flag包来进行配置参数的传递,而不是通过配置文件或者环境变量。

flag的配置是在main函数里面定义的,而不是在全局范围内。

func main() {
    var (
        payload = flag.String("payload", "abc", "payload data")
        delay   = flag.Duration("delay", 1*time.Second, "write delay")
    )
    flag.Parse()
    // ...
}

关于使用flag作为配置参数的传递,我持保留意见。如果一个应用需要特别多的配置参数,使用flag比较让人蛋疼了。这时候,使用配置文件反而比较好,我个人倾向于使用json作为配置,原因在这里

日志

soundcloud使用的是go的log日志,他们也说明了他们的log并不需要太多的其他功能,譬如log分级等。对于log,我参考python的log写了一个,在这里。该log支持log级别,支持自定义loghandler。

soundcloud还提到了一个telemetry的概念,我真没好的办法进行翻译,据我的了解可能就是程序的信息收集,包括响应时间,QPS,内存运行错误等。

通常telemetry有两种方式,推和拉。

推模式就是主动的将信息发送给特定的外部系统,而拉模式则是将其写入到某一个地方,允许外部系统来获取该数据。

这两种方式都有不同的定位,如果需要及时,直观的看到数据,推模式是一个很好的选择,但是该模式可能会占用过多的资源,尤其是在数据量大的情况下面,会很消耗CPU和带宽。

soundcloud貌似采用的是拉模型。

关于这点我是深表赞同,我们有一个服务,需要将其信息发送到一个统计平台共后续的信息,开始的时候,我们使用推模式,每产生一条记录,我们直接通过http推给后面的统计平台,终于,随着压力的增大,整个统计平台被我们发挂了,拒绝服务。最终,我们采用了将数据写到本地,然后通过另一个程序拉取再发送的方式解决。

测试

soundcloud使用go的testing包进行测试,然后也使用flag的方式来进行集成测试,如下:

// +build integration

var fooAddr = flag.String(...)

func TestToo(t *testing.T) {
    f, err := foo.Connect(*fooAddr)
    // ...
}

因为go test也支持类似go build那种flag传递,它会默认合成一个main package,然后在里面进行flag parse处理。

这种方式我现在没有采用,我都是在测试用例里面直接写死了一个全局的配置,主要是为了方便的在根目录进行 go test ./…处理。不过使用flag的方式我觉得灵活性很大,后面如果有可能会考虑。

go的testing包提供的功能并不强,譬如没有提供assert_equal这类东西,但是我们可以通过reflect.DeepEqual来解决。

依赖管理

这块其实也是我非常想解决的。现在我们的代码就是很暴力的用go get来解决依赖问题,这个其实很有风险的,如果某一个依赖包更改了接口,那么我们go get的时候可能会出问题了。

soundcloud使用了一种vendor的方式进行依赖管理。其实很简单,就是把依赖的东西全部拷贝到自己的工程下面,当做自己的代码来使用。不过这个就需要定期的维护依赖包的更新了。

如果引入的是一个可执行包,在自己的工程目录下面建立一个_vendor文件夹(这样go的相关tool例如go test就会忽略该文件夹的东西)。把_vendor作为单独的GOPATH,例如,拷贝github.com/user/dep到_vendor/src/github.com/user/dep下面。然后将_vendor加入自己的GOPATH中,如下:

GO ?= go
GOPATH := $(CURDIR)/_vendor:$(GOPATH)

all: build

build:
    $(GO) build

如果引入的是一个库,那么将其放入vendor目录中,将vendor作为package的前缀,例如拷贝github.com/user/dep到vendor/user/dep,并更改所有的相关import语句。

因为我们并不需要频繁的对这些引入的工程进行go get更新处理,所以大多数时候这样做都很值。

我开始的时候也采用的是类似的做法,只不过我不叫vendor,而叫做3rd,后来为了方便还是决定改成直接go get,虽然知道这样风险比较大。没准后续使用godep可能是一个不错的解决办法。

构建和部署

soundcloud在开发过程中直接使用go build来构建系统,然后使用一个Makefile来处理正式的构建。

因为soundcloud主要部署很多无状态的服务,类似Heroku提供了很简单的一种方式:

$ git push bazooka master
$ bazooka scale -r <new> -n 4 ...
$ # validate
$ bazooka scale -r <old> -n 0 ...

这方面,我们直接使用一个简单的Makefile来构建系统,如下:

all: build 

build:
    go install ${SRC}

clean:
    go clean -i ${SRC}

test:
    go test ${SRC} 

应用程序的发布采用最原始的scp到目标机器在重启的方式,不过现在正在测试使用salt来发布应用。而对于应用程序的启动,停止这些,我们则使用supervisor来进行管理。

总结

总的来说,这篇文章很详细的讲解了用go进行产品开发过程中的很多经验,希望对大家有帮助。

高性能NoSQL LedisDB介绍

起因

ledisdb是一个参考ssdb,采用go实现,底层基于leveldb,类似redis的高性能nosql数据库,提供了kv,list,hash以及zset数据结构的支持。

我们现在的应用极大的依赖redis,但随着我们用户量越来越大,redis的内存越来越不够用,并且replication可能还会导致超时问题。虽然后续我们可以通过添加多台机器来解决,但是在现有机器配置下面,我们仍希望单台机器承载更多的用户。另外,因为业务的特性,我们其实并不需要将所有的数据放到内存,只需要存放当前活跃用户。

经过我们的调研,发现ssdb已经很好的帮我们解决了这个问题,它提供了跟redis一致的接口(当然有些地方还是稍微不同),但是底层采用leveldb进行存储。根据其官网的描述,性能已经接近甚至超越了redis。

本着造轮子的精神,我决定用go实现一个类似的db,取名为ledisdb,也就是level-redis-db,为啥不用现成的ssdb,我觉得有如下几个原因:

  • go语言开发的快速,这点毋庸置疑,虽然性能上面铁定离c++的代码有差距,但是我能够快速的进行原型搭建并实验。实际上,我在很短的时间里面就开发出了ledisdb,让我后续继续开发有了信心。
  • leveldb的研究,我一直很想将leveldb应用到我们的项目中,作为本机热点数据的首选数据存储方式,通过ledisdb,让我对leveldb的使用有了很多经验。
  • redis的熟悉,虽然我用了很久的redis,但是很多redis的命令仍然需要去查手册,通过实现ledisdb,我更加熟悉了redis的命令,同时,因为要了解这个命令redis如何实现,对redis内部又重新来了一次剖析。

在准备开发ledisdb的时候,我就在思索一个问题,我需不需要开发另一个redis?其实这是一个很明确的问题,我不需要另一个redis。ledisdb虽然参考了redis,但为了实现简单,有时候我做了很多减法或者变更,譬如对于zset这种数据结构,我就只支持int64类型的score,而redis的score是double类型的,具体原因后续讲解zset的时候详细说明。

所以,我们可以认为,ledisdb是一个基于redis通信协议,提供了多种高级数据结构的nosql数据库,它并不是另一个redis。

编译安装

因为ledisdb是用go写的,所以首先需要安装go以及配置GOROOT,GOPATH。

mkdir $WORKSPACE
cd $WORKSPACE
git clone git@github.com:siddontang/ledisdb.git src/github.com/siddontang/ledisdb

cd src/github.com/siddontang/ledisdb

#构建leveldb,如果已经安装了,可忽略
./build_leveldb.sh  

#安装ledisdb go依赖
. ./bootstap.sh     

#配置GOPATH等环境变量
. ./dev.sh          

go install ./... 

具体的安装说明,可以查看代码目录下面的readme。

Example

使用ledisdb很简单,只需要运行:

./ledis-server -config=/etc/ledis.json

ledisdb的配置文件采用json格式,为啥选用json,我在使用json作为主要的配置格式里面有过说明。

我们可以使用任何redis客户端连接ledisdb,譬如redis-cli,如下:

127.0.0.1:6380> set a 1
OK
127.0.0.1:6380> get a
"1"
127.0.0.1:6380> incr a
(integer) 2
127.0.0.1:6380> mset b 2 c 3
OK
127.0.0.1:6380> mget a b c
1) "2"
2) "2"
3) "3"

leveldb

因为leveldb是c++写的,所以在go里面需要使用,cgo是一种很好的方式。这里,我直接使用了levigo这个库,并在上面进行了封装,详见这里。虽然有一个go-leveldb,无奈仍不能用。

cgo的性能开销还是有的,这点在我做benchmark的时候就明显感觉出来,不过后续优化的空间很大,譬如将多个leveldb的调用逻辑该用c重写,这样只需要一次cgo就可以了。不过这个后续在考虑。

leveldb的一些参数在构建编译的时候是需要调整的,这点我没啥经验,只能google和参考ssdb。譬如下面这几个:

+ db/dbformat.h

// static const int kL0_SlowdownWritesTrigger = 8;
static const int kL0_SlowdownWritesTrigger = 16;

// static const int kL0_StopWritesTrigger = 12;
static const int kL0_StopWritesTrigger = 64;

+ db/version_set.cc

//static const int kTargetFileSize = 2 * 1048576;
static const int kTargetFileSize = 32 * 1048576;

//static const int64_t kMaxGrandParentOverlapBytes = 10 * kTargetFileSize;
static const int64_t kMaxGrandParentOverlapBytes = 20 * kTargetFileSize;

相关参数的调优,只能等我后续深入研究leveldb了在好好考虑。

性能测试

任何一个服务端服务没有性能测试报告那就是耍流氓,我现在只是简单的用了redis_benchmark进行测试,测试环境为一台快两年的老爷mac air机器。

测试语句:

redis-benchmark -n 10000 -t set,incr,get,lpush,lpop,lrange,mset -q

redis-benchmark默认没有hash以及zset的测试,后续我在自己加入。

leveldb配置:

compression       = false
block_size        = 32KB
write_buffer_size = 64MB
cache_size        = 500MB

redis

SET: 42735.04 requests per second
GET: 45871.56 requests per second
INCR: 45248.87 requests per second
LPUSH: 45045.04 requests per second
LPOP: 43103.45 requests per second
LPUSH (needed to benchmark LRANGE): 44843.05 requests per second
LRANGE_100 (first 100 elements): 14727.54 requests per second
LRANGE_300 (first 300 elements): 6915.63 requests per second
LRANGE_500 (first 450 elements): 5042.86 requests per second
LRANGE_600 (first 600 elements): 3960.40 requests per second
MSET (10 keys): 33003.30 requests per second

ssdb

SET: 35971.22 requests per second
GET: 47393.37 requests per second
INCR: 36630.04 requests per second
LPUSH: 37174.72 requests per second
LPOP: 38167.94 requests per second
LPUSH (needed to benchmark LRANGE): 37593.98 requests per second
LRANGE_100 (first 100 elements): 905.55 requests per second
LRANGE_300 (first 300 elements): 327.78 requests per second
LRANGE_500 (first 450 elements): 222.36 requests per second
LRANGE_600 (first 600 elements): 165.30 requests per second
MSET (10 keys): 33112.59 requests per second

ledisdb

SET: 38759.69 requests per second
GET: 40160.64 requests per second
INCR: 36101.08 requests per second
LPUSH: 33003.30 requests per second
LPOP: 27624.31 requests per second
LPUSH (needed to benchmark LRANGE): 32894.74 requests per second
LRANGE_100 (first 100 elements): 7352.94 requests per second
LRANGE_300 (first 300 elements): 2867.79 requests per second
LRANGE_500 (first 450 elements): 1778.41 requests per second
LRANGE_600 (first 600 elements): 1590.33 requests per second
MSET (10 keys): 21881.84 requests per second

可以看到,ledisdb的性能赶redis以及ssdb还是有差距的,但也不至于不可用,有些差别并不大。至于为啥lrange比ssdb高,我比较困惑。

后续的测试报告,我会不断在benchmark文件里面更新。

Todo。。。。。。

ledisdb还是一个非常新的项目,比起ssdb已经在生产环境中用了很久,还有很多路要走,还有一些重要的功能需要实现,譬如replication等。

欢迎有兴趣的童鞋一起参与进来,在漫漫程序开发路上有一些好基友可是很幸运的。

为什么使用Go

这里,我并不打算引起语言争论的口水仗,我并不是什么大牛,对语言的造诣也不深,只是想通过自己实际的经历,来说说为什么我在项目中选择go。

其他语言的经历

C++

在接触go之前,我已经有多年的c++开发经验。主要用在游戏服务端引擎开发以及P2P上面,那可是一段痛并快乐的时期,以至于我看到任何的程序钉子问题都觉得可以用c++这把锤子给敲定。但是对于互联网项目开发来说,除非你的团队整体的c++技术水平nb,并且有很强的代码规范,不然真可能是一场灾难,更别说我们现有团队几乎没其他人会这玩意了。

本来,我打算在现有项目中的推送系统中使用c++,并用业余时间写好了一个网络底层库libtnet,但后来还是决定打住,因为没有人能够协助我开发。令我比较欣慰的是,libtnet有一个游戏公司在使用,现处于内部测试阶段,即将放出去,我倒是很期待他们的好消息。

Lua

在做游戏的过程中,我也学会了lua这门语言,并且还有幸接触并完善了云风在Lua 不是 C++中提到的那个恐怖的lua,c++粘合层。

lua真的是一门非常好的语言,性能高,开发快速。不光游戏公司大量使用,在互联网领域,因为openresty的流行,一些公司(包括我们)也开始在web端使用lua进行开发。(颇为自豪的是还给openresty反馈过几个bug)

但是,lua因为太短小精悍,功能库并不多,很多需要自己去实现,而且,写出高性能,高质量lua代码也并不是很容易的事情。另外,因为其动态语言的特性,我们也栽了不少坑,这个后续在详说。

Python

在我来现有的团队之前,他们就已经使用python进行整个系统的开发,甚至包括客户端GUI(这对客户端童鞋当时就是一个灾难,后来换成Qt就舒爽了)。

python的好处不必说,从数不清的公司用它进行开发就知道,库非常丰富,代码简洁,开发迅速。

但是,在项目中经过两年多python开发之后,我们渐渐出现了很多问题:

  • 性能,python的性能是比较偏低的,对于很多性能热点代码,通常都会采用其他的方式实现。在我们的项目中,需要对任何API调用进行签名认证,认证服务我们开始使用的是tornado实现,但很不幸运的是,放到外网并没有顶住压力。所以我们引入openresty,专门用以解决性能问题。
  • 部署,python的库因为太丰富了,所以我们的童鞋引入了很多的库,个人感觉在pythoner的世界里面,可没有造轮子的兴趣。有时候发版本的时候,我们会因为忘记安装一个库导致程序无法运行。这可能跟我们团队没有成熟运维经验有关,后续通过salt,puppet这种发布工具应该能解决。
  • 质量,通常我们都认为,因为python代码的简洁,我们很容易的能写出高质量的代码,但是如果没有好的代码控制手段,用python也仍然能写出渣的代码,我甚至觉得因为其灵活性,可能会更容易写出烂的代码。这可以说是我们团队的教训。

这里,我并没有喷python的意思,它真的是一门好语言,我能够通过它快速的构建原型,验证我的想法,而且还一直在使用。只是在项目中,我们的一些疏忽,导致代码不可控了,到了不得不重构的地步了。

Why GO?

前面说了我的语言经历,以及项目到了重构地步的原因,但是为什么会是go呢?我们可以有很多其他的选择,譬如java,erlang,或者仍然采用python。我觉得有很多因素考量:

  • 静态,go是一门静态语言,有着强类型约束,所以我们不太可能出现在python中变量在运行时类型不匹配(譬如int + string)这样的runtime error。 在编译阶段就能够帮我们发现很多问题,不用等到运行时。(当然,这个静态语言都能做到)

  • 代码规范,很多人都比较反感go强制的编码规范,譬如花括号的位置。但我觉得,就因为强制约定,所以大家写出来的go代码样子都差不多,不用费心再去深究代码样式问题。而且我发现,因为规范统一,我很容易就能理解别人写的代码。

  • 库支持,go的库非常丰富,而且能通过go get非常方便的获取github,google code上面的第三方库(质量你自己得担着了),再不行,用go自己造轮子也是很方便的,而且造的轮子通常都比较稳定。

  • 开发迅速,不得不说,当你习惯用go开发之后,用go开发功能非常的快,相对于静态语言c++,开发的效率快的没话说,我觉得比python都不差,而且质量有保证。我们花了不到一个星期进行推送服务核心功能开发,到现在都没怎么变动,稳定运行。

  • 部署方便,因为是静态的,只需要build成一个可运行程序就可以了,部署的时候直接扔一个文件过去,不需要像python那样安装太多的依赖库。

GO特性

go现在的这个样子,有些人喜欢,有些人不喜欢,我无法知道为啥google那帮人把go设计成这样,但是我觉得,既然存在,就有道理,我只需要知道什么该用,什么不该用就可以了。

gc

GO提供了gc,这对于c++的童鞋来说,极大的减少了在内存上面犯错的机会,只是go的gc这个效率还真的不好恭维,比起java来说,还有很大的提升空间。

所以有时候写代码,我们还得根据tuning来提升gc的效率,譬如采用内存池的方式来管理大块的slice分配,采用no copy的方式来进行string,slice的互转。

不过go1.3貌似gc性能有了很大的改善,这点让我比较期待。

defer

go的defer其实是一个让人又爱又恨的东西,对于防止资源泄露,defer可是一个很不错的东西,但是滥用defer可是会让你面临很严重的内存问题,尤其是像下面的代码:

for {
    defer func(){
        //do somthing
    }
}

别以为go会在调用完成defer之后就好好的进行gc回收defer里面的东西,在我们进行内存profile的时候,发现大量的内存占用都是defer引起的。所以使用起来需要特别谨慎。

但我觉得,这个go应该会稍微改善,在go1.3里面,也有了对defer的优化。

error

也许error是一个让人争议很大的东西,现代方式的exception那里去呢?但是我觉得error能够非常明确的告诉使用者该函数会有错误返回,如果使用exception,除非文档足够详细,我还真不知道哪里就会蹦出一个异常了。

只是,go又提供了类似exception的defer,panic,recover,这是要闹哪出。

其实这篇文章我觉得已经解释的很好了,go程序的惯例是对外的API使用error,而内部错误处理可以用defer,recover和panic来简化流程。

其实这倒跟我一贯的编程准则对应,在团队在用python进行开发的时候,我们都明确要求库对外提供的API需要使用返回值来表示错误,而在内部可以使用try,catch异常机制。

interface

go提供了interface来进行抽象编程。何谓接口,最通常的例子就是鸭子的故事,“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子“。

在go里面,interface就是一堆方法的集合,如果某个对象实现了这些方法,那么该对象可以就算是该interface。使用interface,我们可以很方便的实现非侵入式编程,进行模块功能的替换。

对于长时间沉浸c++和python的童鞋来说,一下子要用interface来解决抽象问题,可能会很不适应。但当习惯之后,你会发现,其实interface非常的灵活方便。

写到后面

在使用的时候,我们需要知道go到底适用在什么地方,譬如我们现在也就将API服务使用go重构,我们可没傻到用go去替换openresty。

总之,go是一门很新的语言,国内也已经有很多公司开始吃这个螃蟹,也有成功的例子了,而我们也正开始了这段旅程。

Go MoonMQ使用说明

在上一篇moonmq的介绍中(这里),我仅仅简短的罗列了一些moonmq的设计想法,但是对于如何使用并没有详细说明,公司同事无法很好的使用。

对于moonmq的使用,其实很简单,样例代码在这里,我们只需要处理好broker,consumer以及publisher的关系就可以了。

首先,我们需要启动一个broker,因为moonmq现在只支持tcp的自定义协议,所以broker启动的时候需要指定一个listen address。

#启动broker
./simple_broker -addr=127.0.0.1:11182

启动了broker之后,我们就可以向该broker发送消息

#向test这个queue发送 hello msg
./simple_publisher -addr=127.0.0.1:11182 -queue=test -msg=hello

然后在另一个shell里面接收消息

#接收test这个queue的消息
./simple_consumer -addr=127.0.0.1:11182 -queue=test

#output get msg: hello

如果没有消息,那么consumer就会一直等待,直到接收到消息。

这里详细说一下consumer的实现,

  • 创建一个与broker的连接

      //create a client for use
      client := NewClient(config)
    
      //get a usable connection
      conn, _ := client.Get()
    
  • 绑定queue

      //bind a queue
      //queue name : test
      //routingKey : ""
      //noAck : true
      ch, _ := conn.Bind("test", "", true)
    
  • 接收消息

      //receive msg, block to wait until a msg received
      msg := ch.GetMsg()
      println(msg)
    
  • 回执消息

      //if channel noAck is false, we must ack
      ch.Ack()
    

从上面的例子可以看出,使用moonmq很方便,后续我准备加入http的支持,使其更容易使用。

moonmq的代码在这里https://github.com/siddontang/moonmq

Go的LevelDB接口

leveldb是一个很强悍的kv数据库,自然,我也希望能在go中使用。

如果有官方的go leveldb实现,那我会优先考虑,譬如这个,但是该库文档完全没有,并且在网上没发现有人用于实战环境,对其能否在生产环境中使用打上问号,保险起见,我还是决定不使用。

因为leveldb有c的接口,所以通过cgo能很方便的进行集成,所以我决定采用该种方式,幸运的是,已经有人做了cgo的版本,也就是levigo

使用levigo,需要编译安装leveldb,如果需要压缩支持还需要编译snappy,为了简单,我写了一个构件脚本,如下:

#!/bin/bash
#refer https://github.com/norton/lets/blob/master/c_src/build_deps.sh

#你必须在这里设置实际的snappy以及leveldb源码地址
SNAPPY_SRC=./snappy
LEVELDB_SRC=./leveldb

SNAPPY_DIR=/usr/local/snappy
LEVELDB_DIR=/usr/local/leveldb

if [ ! -f $SNAPPY_DIR/lib/libsnappy.a ]; then
    (cd $SNAPPY_SRC && \
        ./configure --prefix=$SNAPPY_DIR && \
        make && \
        make install)
else
    echo "skip install snappy"
fi

if [ ! -f $LEVELDB_DIR/lib/libleveldb.a ]; then
    (cd $LEVELDB_SRC && \
        echo "echo \"PLATFORM_CFLAGS+=-I$SNAPPY_DIR/include\" >> build_config.mk" >> build_detect_platform &&
        echo "echo \"PLATFORM_CXXFLAGS+=-I$SNAPPY_DIR/include\" >> build_config.mk" >> build_detect_platform &&
        echo "echo \"PLATFORM_LDFLAGS+=-L $SNAPPY_DIR/lib -lsnappy\" >> build_config.mk" >> build_detect_platform &&
        make SNAPPY=1 && \
        make && \
        mkdir -p $LEVELDB_DIR/include/leveldb && \
        install include/leveldb/*.h $LEVELDB_DIR/include/leveldb && \
        mkdir -p $LEVELDB_DIR/lib && \
        cp -af libleveldb.* $LEVELDB_DIR/lib)
else
    echo "skip install leveldb"
fi

function add_path()
{
  # $1 path variable
  # $2 path to add
  if [ -d "$2" ] && [[ ":$1:" != *":$2:"* ]]; then
    echo "$1:$2"
  else
    echo "$1"
  fi
}

export CGO_CFLAGS="-I$LEVELDB_DIR/include -I$SNAPPY_DIR/include"
export CGO_LDFLAGS="-L$LEVELDB_DIR/lib -L$SNAPPY_DIR/lib -lsnappy"
export LD_LIBRARY_PATH=$(add_path $LD_LIBRARY_PATH $SNAPPY_DIR/lib)
export LD_LIBRARY_PATH=$(add_path $LD_LIBRARY_PATH $LEVELDB_DIR/lib)

go get github.com/jmhodges/levigo 

对于leveldb在go里面的使用,levigo做了很好的封装,但是有一点我不怎么习惯,在leveldb中,对于read和write的操作,都需要传入一个Option的东西,这玩意大多数时候都是一个默认Option对象,没必要每次在go里面进行创建删除。所以我对其进行了封装,提供了如下的接口,这样使用的都是默认的option。

func (db *DB) Put(key, value []byte) error 
func (db *DB) Get(key []byte) ([]byte, error)
func (db *DB) Delete(key []byte) error 

同时对于iterator,我参考c++的模型,提供了iterator以及reverse_iterator两种模式,如下:

func (db *DB) Iterator(begin []byte, end []byte, limit int) *Iterator 
func (db *DB) ReverseIterator(rbegin []byte, rend []byte, limit int) *Iterator 

具体的代码在这里

Go使用pprof调试Goroutine

有一段时间,我们的推送服务socket占用很不正常,我们自己统计的同时在线就10w的用户,但是占用的socket竟然达到30w,然后查看goroutine的数量,发现已经60w+。

每个用户占用一个socket,而一个socket,有read和write两个goroutine,简化的代码如下:

c, _ := listerner.Accept()

go c.run()

func (c *conn) run() {
    go c.onWrite()
    c.onRead()
}

func (c *conn) onRead() {
    stat.AddConnCount(1)

    //on something

    stat.AddConnCount(-1)

    //clear
    //notify onWrite to quit
}

当时我就怀疑,用户同时在线的统计是正确的,也就是之后的clear阶段出现了问题,导致两个goroutine都无法正常结束。在检查代码之后,我们发现了一个可疑的地方,因为我们不光有自己的统计,还会将一些统计信息发送到我们公司的统计平台,代码如下:

ch = make([]byte, 100000)
func send(msg []byte) {
    ch <- msg
}

//在另一个goroutine的地方,
msg <- msg
httpsend(msg)

我们channel的缓存分配了10w,如果公司统计平台出现了问题,可能会导致channel阻塞。但到底是不是这个原因呢?

幸运的是,我们先前已经在代码里面内置了pprof的功能,通过pprof goroutine的信息,发现大量的goroutine的当前运行函数在httpsend里面,也就是说,公司的统计平台在大并发下面服务不可用,虽然我们有http超时的处理,但是因为发送的数据量太频繁,导致整体阻塞。

临时的解决办法就是关闭了统计信息的发送,后续我们会考虑将其发送到自己的mq上面,虽然也可能会出现mq服务不可用的问题,但是说句实话,比起自己实现的mq,公司的统计平台更让我不可信。

这同时也给了我一个教训,访问外部服务一定要好好处理外部服务不可用的问题,即使可用,也要考虑压力问题。

对于pprof如何查看了goroutine的问题,可以通过一个简单的例子说明:

package main

import (
    "net/http"
    "runtime/pprof"
)

var quit chan struct{} = make(chan struct{})

func f() {
    <-quit
}

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/plain")

    p := pprof.Lookup("goroutine")
    p.WriteTo(w, 1)
}

func main() {
    for i := 0; i < 10000; i++ {
        go f()
    }

    http.HandleFunc("/", handler)
    http.ListenAndServe(":11181", nil)
}

这上面的例子中,我们启动了10000个goroutine,并阻塞,然后通过访问http://localhost:11181/,我们就可以得到整个goroutine的信息,仅列出关键信息:

goroutine profile: total 10004

10000 @ 0x186f6 0x616b 0x6298 0x2033 0x188c0
#    0x2033    main.f+0x33    /Users/siddontang/test/pprof.go:11

可以看到,在main.f这个函数中,有10000个goroutine正在执行,符合我们的预期。

在go里面,还有很多运行时查看机制,可以很方便的帮我们定位程序问题,不得不赞一下。

Go Timer实现

在go自带的timer实现中,采用的是通常的最小堆的方式,具体可以参见这里

最小堆能够提供很好的定时精度,但是,在实际情况中,我们并不需要这样高精度的定时器,譬如对于一个连接,如果它在2分钟以内没有数据交互,我们就将其删除,2分钟并不需要那么精确,多几秒少几秒都无所谓的。

以前我们单独实现了一个timingwheel,采用的是channel close的方式来处理低精度,超大量timer定时的问题,详见这里

但是timingwheel只有After接口,远远不能满足实际的需求,于是我按照linux timer的实现方式,依葫芦画瓢,弄了一个go版本的实现。linux timer的实现,参考这篇

后续用go timer来表示我自己实现的timer。

在linux中,我们使用tick来表示一次中断的时间,用jiffies来表示系统自启动以来流逝的tick次数。在go timer中,我们在创建一个wheel的时候,需要指定一次tick的时间,如下:

func NewWheel(tick time.Duration) *Wheel

Wheel是go timer统一对timer进行管理的地方。对于每一次tick,我们采用go自带的ticker进行模拟。

为了便于外部使用,我仍然提供的是跟go自己timer一样的接口,譬如:

func NewTimer(d time.Duration) *Timer

在NewTimer中,参数d是一个time duration,我们还需要根据tick来进行换算,得到go timer中实际的expires,也就是在多少次jiffies后该timer触发。

譬如,NewTimer参数为10s,tick为1s,那么经过10个jiffies之后,该timer就会超时触发。如果tick为500ms,那么需要经过20个jiffies之后,该timer才会被触发。

所以timer超时jiffies的计算如下:

expires = wheel.jiffies + d / wheel.tick

详细的代码在https://github.com/siddontang/golib/tree/master/timer

在Go中使用JSON作为主要配置

最近在用go重构,在先前的代码中,我们使用的ini文件进行配置,但是因为很多历史遗留问题,导致配置混乱,维护困难,自然也需要考虑重构了。

通用配置格式

通用的配置格式有很多,常用的就有ini,json,yaml,xml等,当然为了通用我们不考虑自定义的配置格式。那如何选择呢?

首先,xml我们就不用考虑了,到现在为止我都没觉得用这玩意配置起来有多方便,反而很臃肿,可能java系的童鞋会比较青睐。

再来考虑ini,ini文件对于简单应用的配置可以说是非常方便的,如果配置没有太多的层次结构,使用ini就能完全满足我们的需要,即使有,我们也能够通过加入特定前缀来解决。譬如,我们可能有如下redis配置:

[ModuleA]

persistent_redis_addr = 127.0.0.1:6379
persistent_redis_password = admin

cache_redis_addr = 127.0.0.1:6380
cache_redis_password = admin

然后,需求变化了,我们需要另一个持久化redis服务来做相关事情,于是配置就可能变成了下面这样:

[ModuleA]

persistent_redis_addr = 127.0.0.1:6379
persistent_redis_password = admin

persistent2_redis_addr = 127.0.0.1:6379
persistent2_redis_password = admin

cache_redis_addr = 127.0.0.1:6380
cache_redis_password = admin

虽然通过前缀命名能解决层级问题,但总觉得是对程序员不怎么友好的。

why json

剩下的就是json和yaml,可以这么说,这两个都算是比较好的轻量级配置格式,而且对程序员非常友好,而且go里面都可以通过定义struct,将层级结构的配置映射到相应的struct里面。

但是我还是决定选择json作为我们go代码的默认配置格式,最主要的原因在于go的json包有一个杀手级别的RawMessage实现。而这个在我能找到的yaml包中没有。

RawMessage主要是用来告诉go延迟解析用的。当我们定义了某一个字段为RawMessage之后,go就不会解析这段json,这样我们就可以将其推后到各自的子模块单独解析。

假设有一个功能,后台存储可能是redis或者mysql,但是只会使用一个,可能我们会按照如下方式写配置:

redis_store : {
    addr : 127.0.0.1
    db : 0
},

mysql_store : {
    addr : 127.0.0.1
    db : test
    password : admin
    user : root
}

store : redis

对应的class为

type Config struct {
    RedisStore struct {
        Addr string
        DB int
    }

    MysqlStore Struct {
        Addr string
        DB string
        Password string
        User string
    }

    Store string
}

如果这时候我们在增加了一种新的store,我们需要在Config文件里面在增加一个新的field,但是实际我们只会使用一种store,并不需要写这么多的配置。

我们可以使用RawMessage来处理:

type Config struct {
    Store string
    StoreConfig json.RawMessage
}

如果使用redis,对应的配置文件为

store: redis
store_config: {
    addr : 127.0.0.1
    db : 0
}

如果使用mysql,对应的配置文件为

store: mysql
store_config: {
    addr : 127.0.0.1
    db : test
    password : admin
    user : root
}

go读取配置文件之后,并不会处理RawMessage对应的东西,而是由我们代码自己对应的store模块去处理。这样无论配置文件怎么变动,store模块做了什么变动,都不会影响Config类。

而在各个模块中,我们只需要自己定义相关config,然后可以将RawMessage直接解析映射到该config上面,譬如,对于redis,我们在模块中有如下定义

type RedisConfig config {
    Addr string `json:"addr"`
    DB int `json:"db"`
}

func NewConfig(m json.RawMessage) *RedisConfig {
    c := new(RedisConfig)

    json.Unmarshal(m, c)

    return c
}

json的不足

使用json也还有很多蛋疼的地方,最大的问题就在于注释,在json中,可不能这样写:

{
    //this is a comment
    /*this is a comment*/ 
}

但是,我们又不可能不写一点注释来说明配置项是干啥的,所以,通常采用的是引入一个comment字段的方式,譬如:

{
    "_comment" : "this is a comment",
    "key" : "value"
}

另外,json还需要注意的就是写的时候最后一项不能加上逗号,这样的json会因为格式错误无法解析的。

{
    "key" : "value",
}

最后那个逗号可是不能要的,但是实际写配置的时候我们可是经常性的随手加上了。

但是,总的来说,json对于go的项目还是很友好的,我不光在项目中推行了,在自己的开源项目中,也大量的采用了json作为主要的配置文件。

Go中Slice与String的无内存copy

在go里面,string和slice的互换是需要进行内存拷贝的,虽然在底层,它们都只是用 pointer + len来表示的一段内存。

通常,我们不会在意string和slice的转换带来的内存拷贝性能问题,但是总有些地方需要关注的,刚好在看vitess代码的时候,发现了一种很hack的做法,string和slice的转换只需要拷贝底层的指针,而不是内存拷贝。当然这样做的风险各位就要好好担当了:

func String(b []byte) (s string) {
    pbytes := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    pstring := (*reflect.StringHeader)(unsafe.Pointer(&s))
    pstring.Data = pbytes.Data
    pstring.Len = pbytes.Len
    return
}

func Slice(s string) (b []byte) {
    pbytes := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    pstring := (*reflect.StringHeader)(unsafe.Pointer(&s))
    pbytes.Data = pstring.Data
    pbytes.Len = pstring.Len
    pbytes.Cap = pstring.Len
    return
}

在我的测试例子中,slice转string之后,如果slice的值有变化,string也会跟着改变,如下:

b := []byte("hello world")

a := String(b)

b[0] = 'a'

println(a)  //output  aello world

但是string转slice之后,就不能更改slice了,如下:

a := "hello world"

b := Slice(a)

b[0] = 'a'  //这里就等着崩溃吧

//但是可以这样,因为go又重新给b分配了内存
b = append(b, "hello world"…)

上面为什么会崩溃我猜想可能是string是immutable的,可能对应的内存地址也是不允许改动的。

另外,上面这个崩溃在defer里面是recover不回来的,真的就崩溃了,原因可能就跟c的非法内存访问一样,os不跟你玩了。