1

第三部分: Go微服务 - 嵌入数据库和JSON

在第三部分,我们让accountservice做一些有意义的事情。

  • 声明一个Account结构体。
  • 嵌入简单的key-value存储,我们可以在里边存储Account结构。
  • 将结构体序列化为JSON, 然后通过HTTP服务来为/accounts/{accountId}提供服务。

源代码

源代码位置: https://github.com/callistaen...

声明Account结构体

结构体的详细说明可以参照参考链接部分的相关链接查看。

  1. 在我们的项目根目录accountservice下面创建一个名为model的目录。
  2. 在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压测情况下空间占用信息如下:

clipboard.png

同样我们再次对服务做个压测,得到的空间占用情况如下:

clipboard.png

我们可以看到,在增加了boltdb之后,内存占用由2.1MB变成31.2MB, 增加了30MB左右,还不算太差劲。

clipboard.png

每秒1000个请求,每个CPU核大概使用率是10%,BoltDB和JSON序列化的开销不是很明显,很不错!顺便说下,我们之前的Java进程在Galting压测下,CPU使用大概是它的3倍。

clipboard.png

平均响应时间依然小于1毫秒。 可能我们需要使用更重的压测进行测试,我们尝试使用每秒4K的请求?(注意,我们可能需要增加OS级别的可用文件处理数)。

clipboard.png

占用内存变成118MB多,基本上比原来增加到了4倍。内存增加几乎是因为Go语言运行时或者是因为Gorilla增加了用于服务请求的内部goroutine的数量,因此负载增加。

clipboard.png

CPU基本上保持在30%。 我运行在16GB RAM/Core i7的笔记本上的, 我认为I/O或文件句柄比CPU更快成为性能瓶颈。

clipboard.png

平均吞吐量最后上升到95%的请求在1ms~3ms之间。 确实在4k/s的请求时候,吞吐量受到了些影响, 但是个人认为这个小的accountservice服务使用BoltDB,执行还是相当不错的。

最后的话

下一部分,我们会探讨下使用GoConvey和模拟BoltDB客户端来进行单元测试。

参考链接


老将廉颇
878 声望297 粉丝