這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
第三部分: Go微服務 - 嵌入資料庫和JSON
在第三部分,我們讓accountservice做一些有意義的事情。
- 聲明一個Account結構體。
- 嵌入簡單的key-value儲存,我們可以在裡邊儲存Account結構。
- 將結構體序列化為JSON, 然後通過HTTP服務來為/accounts/{accountId}提供服務。
原始碼
原始碼位置: https://github.com/callistaen...。
聲明Account結構體
結構體的詳細說明可以參照參考連結部分的相關連結查看。
- 在我們的項目根目錄accountservice下面建立一個名為model的目錄。
- 在model目錄下面建立account.go檔案。
package modeltype 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 dbclientimport ( "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 serviceimport ( "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 funcsfunc initializeBoltClient() { service.DBClient = &dbclient.BoltClient{} service.DBClient.OpenBoltDb() service.DBClient.Seed()}
這樣我們的微服務啟動的時候就會開啟資料庫。但是,這裡還是什麼都沒有做。 我們接下來添加一些代碼,讓服務啟動的時候可以為我們引導一些帳號。
啟動時填充一些帳號
開啟boltclient.go代碼檔案,為BoltClient添加一個Seed方法:
// Start seeding accountsfunc (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 *.goStarting accountserviceSeeded 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 serviceimport "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 []Routevar routes = Routes{ Route{ "GetAccount", // Name "GET", // HTTP method "/accounts/{accountId}", // Route pattern GetAccount, },}
接下來,我們更新下/service/handlers.go,建立一個GetAccount函數來實現HTTP處理器函數簽名:
var DBClient dbclient.IBoltClientfunc 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 *.goStarting accountserviceSeeded 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用戶端來進行單元測試。
參考連結
- 英文地址
- Go結構體和介面
- 微服務系列入口
- 下一節