第三部分: Go微服务 - 嵌入数据库和JSON
在第三部分,我们让accountservice做一些有意义的事情。
- 声明一个Account结构体。
- 嵌入简单的key-value存储,我们可以在里边存储Account结构。
- 将结构体序列化为JSON, 然后通过HTTP服务来为/accounts/{accountId}提供服务。
源代码
源代码位置: 。
声明Account结构体
结构体的详细说明可以参照参考链接部分的相关链接查看。
- 在我们的项目根目录accountservice下面创建一个名为model的目录。
- 在model目录下面创建account.go文件。
package modeltype Account struct { Id string `json:"id"` Name string `json:"name"`}
Account抽象成包含Id和Name的结构体。结构体的两个属性首字母为大写,表示声明的是全局作用域可见的(标识符首字母大写public, 首字母小写包作用域可见)。
另外结构体中还使用了标签(Tag)。这些标签在encoding/json和encoding/xml中有特殊应用。
假设我们定义结构体的时候没有使用标签,对于结构体通过json.Marshal之后产生的JSON的key使用结构体字段名对应的值。
例如:
type Account struct { Id string Name string}var account = Account{ Id: 10000, Name: "admin",}
转换为json之后得到:
{ "Id": 10000, "Name": "admin"}
而这种形式一般不是JSON的惯用形式,我们通常更习惯使用json的key首字母为小写的,那么结构体标签就可以派上用场了:
type Account struct { Id string `json:"id"` Name string `json:"name"`}var account = Account{ Id: 10000, Name: "admin",}
这个时候转换为JSON的时候,我们就得到如下结果:
{ "id": 10000, "name": "admin"}
嵌入一个key-value存储
为了简单起见,我们使用一个简单的key-value存储, 这是一个Go语言的嵌入式key-value数据库。它主要能为应用提供快速、可信赖的数据库,这样我们无需复杂的数据库,比如MySql或Postgres等。
我们可以通过go get获取它的源代码:
go get github.com/boltdb/bolt
接下来,我们在accountservice目录下面创建一个dbclient的目录,并在它下面创建boltclient.go文件。 为了后续模拟的方便,我们声明一个接口,定义我们实现需要履行的合约:
package dbclientimport ( "github.com/callistaenterprise/goblog/accountservice/model")type IBoltClient interface() { OpenBoltDb() QueryAccount(accountId string) (model.Account, error) Seed()}// 真实实现type BoltClient struct { boltDb *bolt.DB}func (bc *BoltClient) OpenBoltDB() { var err error bc.boltDB, err = bolt.Open("account.db", 0600, nil) if err != nil { log.Fatal(err) }}
上面代码声明了一个IBoltClient接口, 规定了该接口的合约是具有三个方法。我们声明了一个具体的BoltClient类型, 暂时只为它实现了OpenBoltDB方法。这种实现接口的方法,突然看起来可能感觉有点奇怪,把函数绑定到一个结构体上。这就是Go语言接口实现的特色。其他两个方法暂时先跳过。
我们现在有了BoltClient结构体,接下来我们需要在项目中的某个位置有这个结构体的一个实例。 那么我们就将它放到我们即将使用的地方, 放在我们的goblog/accountservice/service/handlers.go文件中。 我们首先创建这个文件,然后添加BoltClient的实例:
package serviceimport ( "github.com/callistaenterprise/goblog/accountservice/dbclient")var DBClient dbclient.IBoltClient
然后更新main.go代码,让它启动的时候打开DB。
func main() { fmt.Printf("Starting %v\n", appName) initializeBoltClient() // NEW service.StartWebServer("6767")}// Creates instance and calls the OpenBoltDb and Seed funcsfunc initializeBoltClient() { service.DBClient = &dbclient.BoltClient{} service.DBClient.OpenBoltDb() service.DBClient.Seed()}
这样我们的微服务启动的时候就会打开数据库。但是,这里还是什么都没有做。 我们接下来添加一些代码,让服务启动的时候可以为我们引导一些账号。
启动时填充一些账号
打开boltclient.go代码文件,为BoltClient添加一个Seed方法:
// Start seeding accountsfunc (bc *BoltClient) Seed() { initializeBucket() seedAccounts()}// Creates an "AccountBucket" in our BoltDB. It will overwrite any existing bucket of the same name.func (bc *BoltClient) initializeBucket() { bc.boltDB.Update(func(tx *bolt.Tx) error { _, err := tx.CreateBucket([]byte("AccountBucket")) if err != nil { return fmt.Errorf("create bucket failed: %s", err) } return nil })}// Seed (n) make-believe account objects into the AcountBucket bucket.func (bc *BoltClient) seedAccounts() { total := 100 for i := 0; i < total; i++ { // Generate a key 10000 or larger key := strconv.Itoa(10000 + i) // Create an instance of our Account struct acc := model.Account{ Id: key, Name: "Person_" + strconv.Itoa(i), } // Serialize the struct to JSON jsonBytes, _ := json.Marshal(acc) // Write the data to the AccountBucket bc.boltDB.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("AccountBucket")) err := b.Put([]byte(key), jsonBytes) return err }) } fmt.Printf("Seeded %v fake accounts...\n", total)}
上面我们的Seed方法首先使用"AccountBucket"字符串创建一个Bucket, 然后连续创建100个初始化账号。账号id分别依次为10000~10100, 其Name分别为Person_i(i = 0 ~ 100)。
前面我们在main.go中已经调用了Seed()方法,因此这个时候我们可以运行下当前的程序,看看运行情况:
> go run *.goStarting accountserviceSeeded 100 fake accounts...2017/01/31 16:30:59 Starting HTTP service at 6767
很不错!那么我们先暂停执行,使用Ctrl + C让服务先停下来。
添加查询方法
接下来我们可以为boltclient.go中添加一个Query方法来完成DB API。
func (bc *BoltClient) QueryAccount(accountId string) (model.Account, error) { // Allocate an empty Account instance we'll let json.Unmarhal populate for us in a bit. account := model.Account{} // Read an object from the bucket using boltDB.View err := bc.boltDB.View(func(tx *bolt.Tx) error { // Read the bucket from the DB b := tx.Bucket([]byte("AccountBucket")) // Read the value identified by our accountId supplied as []byte accountBytes := b.Get([]byte(accountId)) if accountBytes == nil { return fmt.Errorf("No account found for " + accountId) } // Unmarshal the returned bytes into the account struct we created at // the top of the function json.Unmarshal(accountBytes, &account) // Return nil to indicate nothing went wrong, e.g no error return nil }) // If there were an error, return the error if err != nil { return model.Account{}, err } // Return the Account struct and nil as error. return account, nil}
这个方法也比较简单,根据请求参数accountId在我们之前初始化的DB中查找这个账户的相关信息。如果成功查找到相关账号,返回这个账号的json数据,否则会返回nil。
通过HTTP提供账号服务
让我们修改在/service/routes.go文件中声明的/accounts/{accountId}路由,让它返回我们填充的账号其中一个记录。代码修改如下:
package serviceimport "net/http"// Defines a single route, e.g. a human readable name, HTTP method, pattern the function that will execute when the route is called.type Route struct { Name string Method string Pattern string HandlerFunc http.HandlerFunc}// Defines the type Routes which is just an array (slice) of Route structs.type Routes []Routevar routes = Routes{ Route{ "GetAccount", // Name "GET", // HTTP method "/accounts/{accountId}", // Route pattern GetAccount, },}
接下来,我们更新下/service/handlers.go,创建一个GetAccount函数来实现HTTP处理器函数签名:
var DBClient dbclient.IBoltClientfunc GetAccount(w http.ResponseWriter, r *http.Request) { // Read the 'accountId' path parameter from the mux map var accountId = mux.Vars(r)["accountId"] // Read the account struct BoltDB account, err := DBClient.QueryAccount(accountId) // If err, return a 404 if err != nil { w.WriteHeader(http.StatusNotFound) return } // If found, marshal into JSON, write headers and content data, _ := json.Marshal(account) w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Length", strconv.Itoa(len(data))) w.WriteHeader(http.StatusOK) w.Write(data)}
上面代码就是实现了处理器函数签名,当Gorilla检测到我们在请求/accounts/{accountId}的时候,它就会将请求路由到这个函数。 下面我们运行一下我们的服务。
> go run *.goStarting accountserviceSeeded 100 fake accounts...2017/01/31 16:30:59 Starting HTTP service at 6767
然后另外开一个窗口,curl请求accountId为10000的请求:
> curl http://localhost:6767/accounts/10000{"id":"10000","name":"Person_0"}
非常棒,我们微服务现在能够动态提供一些简单的数据了。你可以尝试使用accountId为10000到10100之间的任何数字,得到的JSON都不相同。
占用空间和性能
(FOOTPRINT在这里解释为占用空间, 内存空间)。
第二部分,我们看到在Galtling压测情况下空间占用信息如下:
同样我们再次对服务做个压测,得到的空间占用情况如下:
我们可以看到,在增加了boltdb之后,内存占用由2.1MB变成31.2MB, 增加了30MB左右,还不算太差劲。
每秒1000个请求,每个CPU核大概使用率是10%,BoltDB和JSON序列化的开销不是很明显,很不错!顺便说下,我们之前的Java进程在Galting压测下,CPU使用大概是它的3倍。
平均响应时间依然小于1毫秒。 可能我们需要使用更重的压测进行测试,我们尝试使用每秒4K的请求?(注意,我们可能需要增加OS级别的可用文件处理数)。
占用内存变成118MB多,基本上比原来增加到了4倍。内存增加几乎是因为Go语言运行时或者是因为Gorilla增加了用于服务请求的内部goroutine的数量,因此负载增加。
CPU基本上保持在30%。 我运行在16GB RAM/Core i7的笔记本上的, 我认为I/O或文件句柄比CPU更快成为性能瓶颈。
平均吞吐量最后上升到95%的请求在1ms~3ms之间。 确实在4k/s的请求时候,吞吐量受到了些影响, 但是个人认为这个小的accountservice服务使用BoltDB,执行还是相当不错的。
最后的话
下一部分,我们会探讨下使用GoConvey和模拟BoltDB客户端来进行单元测试。