基於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客戶端。

求贊 謝謝

相關文章
相關標籤/搜索