第三部分: Go微服务 - 嵌入数据库和JSON
在第三部分,我们让accountservice做一些有意义的事情。
- 声明一个Account结构体。
- 嵌入简单的key-value存储,我们可以在里边存储Account结构。
- 将结构体序列化为JSON, 然后通过HTTP服务来为/accounts/{accountId}提供服务。
源代码
源代码位置: https://github.com/callistaen...。
声明Account结构体
结构体的详细说明可以参照参考链接部分的相关链接查看。
- 在我们的项目根目录accountservice下面创建一个名为model的目录。
- 在model目录下面创建account.go文件。
package model
type 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存储BoltDB, 这是一个Go语言的嵌入式key-value数据库。它主要能为应用提供快速、可信赖的数据库,这样我们无需复杂的数据库,比如MySql或Postgres等。
我们可以通过go get获取它的源代码:
go get github.com/boltdb/bolt
接下来,我们在accountservice目录下面创建一个dbclient的目录,并在它下面创建boltclient.go文件。 为了后续模拟的方便,我们声明一个接口,定义我们实现需要履行的合约:
package dbclient
import (
"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 service
import (
"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 funcs
func initializeBoltClient() {
service.DBClient = &dbclient.BoltClient{}
service.DBClient.OpenBoltDb()
service.DBClient.Seed()
}
这样我们的微服务启动的时候就会打开数据库。但是,这里还是什么都没有做。 我们接下来添加一些代码,让服务启动的时候可以为我们引导一些账号。
启动时填充一些账号
打开boltclient.go代码文件,为BoltClient添加一个Seed方法:
// Start seeding accounts
func (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 *.go
Starting accountservice
Seeded 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 service
import "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 []Route
var routes = Routes{
Route{
"GetAccount", // Name
"GET", // HTTP method
"/accounts/{accountId}", // Route pattern
GetAccount,
},
}
接下来,我们更新下/service/handlers.go,创建一个GetAccount函数来实现HTTP处理器函数签名:
var DBClient dbclient.IBoltClient
func 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 *.go
Starting accountservice
Seeded 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客户端来进行单元测试。