基于go搭建微服务实践教程 (三)

原文地址
转载请注明原文及 翻译地址

在第三节,咱们要让咱们的accountservice作一些有用的事情。java

  • 声明一个 Account 结构
  • 嵌入一个键值对的存储,用来存储Account结构
  • 序列化结构为JSON,而且用于咱们的accounts/{accountId} HTTP服务

源代码


这篇博客中的全部代码能够从分支p3中获得。git

git checkout P3

声明一个Account结构


在咱们的项目中,在accountservice文件夹中建立一个model文件夹github

mkdir model

在model文件夹下建立一个文件名字为account.go并写入如下代码:数据库

package model

type Account struct {
    Id string `json:"id"`
    Name string `json:"name"`
}

这里面声明了Account,包含id和name。第一个字母的大小写表示做用域(大写=public, 小写=包内调用)。
在声明中,咱们也用到了内置的json.marshal函数对参数序列化的支持。json

嵌入一个键值对的存储


这里,咱们会用到BoltDB来存储键值对。这个包简单快速容易集成。咱们能够用go get来获得这个包segmentfault

go get github.com/boltdb/boltdb/b

以后,在/goblog/accountservice文件夹中,创建一个文件夹dbclient,在dbclient中建立文件boltclient.go。为了使mocking更容易,咱们先声明一个接口,用来制定实现者须要听从的方法。api

package dbclient

import (
        "github.com/callistaenterprise/goblog/accountservice/model"
)

type IBoltClient interface {
        OpenBoltDb()
        QueryAccount(accountId string) (model.Account, error)
        Seed()
}

在同一个文件中,咱们会实现这个接口。先定义一个封装了bolt.DB的指针的结构并发

// Real implementation
type BoltClient struct {
        boltDB *bolt.DB
}

这里是OpenBoltDb()的实现,咱们以后会加入剩下的两个函数。app

func (bc *BoltClient) OpenBoltDb() {
        var err error
        bc.boltDB, err = bolt.Open("accounts.db", 0600, nil)
        if err != nil {
                log.Fatal(err)
        }
}

这部分代码可能看起来有点奇怪,实际上是咱们给一个结构体绑定一个方法函数。咱们的结构体隐式的实现了三个方法中的一个。
咱们须要一个“bolt client”实例在某些地方。让咱们声明在它会用到的地方, 建立/goblog/accountservice/service/handlers.go,而且建立咱们结构体的实例:curl

package service
  
  import (
          "github.com/callistaenterprise/goblog/accountservice/dbclient"
  )
  
  var DBClient dbclient.IBoltClient

更新main.go,让他开始时候就打开数据库:

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()
}

咱们的微服务如今在启动时建立一个数据库。然而,在运行前咱们还须要完善代码:

启动时seed一些accounts


打开boltclient加入下面代码:

// 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)
}

想了解Bolt api的update函数如何工做。能够参看BoltDB的文档

如今咱们加入Query函数:

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来搜索BoltDB,以后返回一个Account结构或者error

如今你能够试一下运行:

> go run *.go
Starting accountservice
Seeded 100 fake accounts...
2017/01/31 16:30:59 Starting HTTP service at 6767

经过HTTP提供account服务

让咱们修改在/service/routes.go中的/accounts/{accountId}路由,让他返回一个seeded Account结构体。打开routes.go用GetAccount函数替换func(w http.ResponseWriter, r *http.Request)。咱们以后会建立GetAccount函数:

Route{
        "GetAccount",             // Name
        "GET",                    // HTTP method
        "/accounts/{accountId}",  // Route pattern
        GetAccount,
},

以后,更新service/handlers.go。加入GetAccount函数:

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)
}

这个GetAccount函数符合Gorilla中定义的handler函数格式。因此当Gorilla发现有请求/accounts/{accountId}时,会路由到GetAccount函数。让咱们跑一下试试:

> go run *.go
Starting accountservice
Seeded 100 fake accounts...
2017/01/31 16:30:59 Starting HTTP service at 6767

用curl来请求这个api。记住,咱们加入啦100个accounts.id从10000开始。

> curl http://localhost:6767/accounts/10000
{"id":"10000","name":"Person_0"}

不错,咱们的微服务如今经过HTTP应答JSON数据了

性能


让咱们分别看一下内存和CPU使用率:

启动后内存使用率

clipboard.png

2.1MB, 不错。加入内嵌的BoltDB和其余一些路由的代码以后增长了300kb,相比于最开始的消耗。让咱们用Gatling测试1K req/s。如今咱们但是真的返回真正的account数据而且序列化成JSON。

压力测试下的内存使用

clipboard.png

31.2MB。增长内嵌的数据库并无消耗太多资源,相比于第二章中简单的返回数据服务

性能和CPU使用

clipboard.png

1k req/s 用单核的10%左右。BoltDB和JSON序列化并无增长太多消耗。顺便看一下上面的java程序,用啦三倍多的CPU资源

clipboard.png

平均应答时间仍是小于1ms。
咱们再试一下更大的压力测试, 4k req/s。(你有可能须要增长OS层面能处理请求的最大值)

内存使用 4k req/s

clipboard.png

大约12MB 大约增加4倍。内存增加极可能是因为go运行或者Gorilla增长了内部goroutine的数量来并发处理请求。

4k req/s性能

clipboard.png

CPU使用率大约30%。当咱们运行在16GB RAM/core i7笔记本上,IO或者文件的访问将会先于CPU成为瓶颈。

clipboard.png

平均延迟升到1ms,95%的请求在3ms之下。咱们看到延迟增长了,可是我认为这个accountservice性能不错。

总结


下一篇咱们会讨论unit test。咱们会用到GoConvey同时mocking BoltDB客户端。

求赞 谢谢

相关文章
相关标签/搜索