Go微服務系列 - 第三部分 - 嵌入數據庫和JSON

第三部分: Go微服務 - 嵌入數據庫和JSON

在第三部分,咱們讓accountservice作一些有意義的事情。git

  • 聲明一個Account結構體。
  • 嵌入簡單的key-value存儲,咱們能夠在裏邊存儲Account結構。
  • 將結構體序列化爲JSON, 而後經過HTTP服務來爲/accounts/{accountId}提供服務。

源代碼

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

聲明Account結構體

結構體的詳細說明能夠參照參考連接部分的相關連接查看。golang

  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

假設咱們定義結構體的時候沒有使用標籤,對於結構體經過json.Marshal以後產生的JSON的key使用結構體字段名對應的值。 app

例如:curl

type Account struct {
    Id string
    Name string
}
var account = Account{
    Id: 10000,
    Name: "admin",
}

轉換爲json以後獲得:ide

{
    "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客戶端來進行單元測試。

參考連接

相關文章
相關標籤/搜索