Go 微服务 第3部分:嵌入式数据库和JSON

原文作者:Erik Lupander

原文地址:https://dzone.com/articles/go-microservices-part-3-embedded-database-and-json

在第3部分中,我们将使我们的账户服务(Accountservice)做一些有用的事情。

  • 声明一个 "Account" 结构
  • 在我们存储帐户结构的地方嵌入一个简单的键值存储
  • 将结构序列化为JSON格式并通过我们的 /accounts/{accountId} HTTP服务来提供

源代码

正如在本系列博文的所有即将发布的部分中,您可以通过克隆源代码(见第2部分)并切换到 P3 分支来获得该部分的完整源代码,即:

git checkout P3

声明帐户结构

有关 Go语言结构的更详细介绍,请查阅本指南

在我们的项目中,在 /accountservice 文件夹下创建一个名为 model 的文件夹。

mkdir model

现在,在model 文件夹中使用以下内容创建一个名为account.go 的文件:

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

这声明了我们的 Account 抽象概念,基本上是一个 id 与一个名字。第一个字母的大小写情况代表了作用域(大写==公共,小写为包作用域)。我们还使用内置支持来声明每个字段应如何由 Go语言中的 json.Marshal 函数序列化。

嵌入一​​个键值存储

为此,我们将使用 BoltDB 键值存储。这使用起来是很简单、快速和容易的。事实上,我们可以在声明使用它之前先行 go get 检索依赖:

go get github.com/boltdb/bolt

接下来,在 /goblog/accountservice 文件夹中,创建一个名为 "dbclient" 的新文件夹和一个名为 boltclient.go 的文件。为了让后面的模拟更容易,我们将首先声明一个接口来定义我们需要实现者来完成的协议:

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() 的实现。再往下一点我们就会将添加剩下的两个函数。

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

这一部分的Go语法起初可能会感觉有点奇怪,我们将函数绑定到结构。我们的结构现在隐式地实现了三个方法之一。

我们需要某处的一个 "bolt client" 实例。让我们把它放在 /goblog/accountservice/service/handlers.go 中。创建该文件并添加我们结构的实例:

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

我们的微服务现在应该在启动时创建一个数据库。但是,在运行之前,我们会添加一段代码,在启动时为我们自举 (bootstrap) 一些帐户。

在启动时播种 (send) 一些帐户

再次打开 boltclient.go 并添加以下函数:

// 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方法如何接受为我们完成工作的函数(func),请参阅BoltDB文档

我们现在已经完成了BoltDB部分。让我们再次构建并运行:

> 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 参数查询 BoltDB ,并返回一个 Account 结构一个错误。

通过 HTTP 提供帐户

我们来修复我们在 /service/routes.go 中声明的 /accounts/{accountId} route,以便实际返回其中一个帐户结构。打开routes.go并替换内联的func(w http.ResponseWriter,r * http.Request){ 为一个我们将在稍后创建

的函数 GetAccount 引用:

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

接下来,使用满足 HTTP 处理函数签名的 GetAccount 函数更新 /service/handlers.go

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 检测到对 /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。请记住,我们播种(send)了100个以 Id 为10000开始的帐户。

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

妙啊!我们的微服务现在实际上是通过 HTTP 从底层存储提供 JSON 数据。

足迹和性能

让我们来看看与第2部分中相同的内存和CPU使用率指标:在我们简单的基于 Gatling 的负载测试之前,之中和之后。

启动后的内存使用情况

2.1 MB,还不错!添加嵌入的 BoltDB 和一些更多的代码来处理路由等,增加了300kb到我们最初的足迹。让我们开始运行 1K req/s 的 Gatling 测试。现在我们实际上返回一个从 BoltDB 中获取的真实 Account 对象,该对象也被序列化为JSON格式:

负载测试后的内存使用情况

31.2 MB的RAM。与第2部分的单纯的服务相比,使用嵌入式数据库服务 1K req/s

请求的额外开销非常小。

性能和CPU使用率

服务 1K req/s 使用单核的约10%性能。BoltDB 和 JSON 序列化的开销并不是很显著,好啊!顺便说一句 - 最上面的 java 进程是我们的 Gatling 测试,它实际上使用的CPU资源是其所测试软件的3倍。

平均响应时间仍小于1毫秒。

也许我们应该测试更重的负载,我们应该说 4K req/s?(请注意,可能需要增加操作系统级别的可用文件句柄数量):

在 4K Req/S 时内存占用

约120 mb。几乎正好增加了4倍。这种使用 n/o 并发请求的内存规模几乎可以肯定是由于 Golang 运行时或 Gorilla 可能会随着负载的增加而增加用于同时处理请求的内部 goroutines 的数量的原因。

在 4K Req/S 下的性能

在 4K req/s时 CPU 使用率保持在30%以下。在这一点上,换言之如运行在装有16 GB RAM / Core i7笔记本电脑上,我会说 IO 或文件句柄会比可用的CPU周期更快地受到瓶颈。

现在平均延迟时间最终会超过1毫秒,而95%的请求保持在3毫秒以下。我们确实看到在 4K req/s 延迟开始受冲击,尽管我个人认为带有嵌入 BoltDB 的小型 Accountservice 表现非常好。

与其他平台比较

人们可能会写一篇有趣的博客文章,关于基于JVM,NodeJS,CLR等实现的功能上等效的微服务来对这个 "accountservice" 进行基准测试。

2015年末,我对此做了一些简单的结论性的基准测试(使用Gatling测试),比较了Go 1.5 中实现的HTTP/JSON服务 + MongoDB访问与Spring Boot@Java 8和NodeJS。在这种特殊情况下,基于 JVM 和 Go 的解决方案规模相当而在延迟方面 Go 略微优于基于 JVM 的解决方案。NodeJS服务器的表现与其他系统非常相似,直到单核的CPU利用率达到100%,并且性能在延迟时间开始下降。

请不要把上面提到的基准测试作为某种事实,因为这只是我为自己的乐趣而做的一件快速而不严谨的事情。

因此,尽管我已经在 4K req/s 上使用Go 1.7作为 "accountservice" 的性能数据看起来非常令人印象深刻,但它们也可能与其他平台相同,尽管我猜测它们的内存使用会令人愉快。我猜你的数据可能会有所不同。

结语

在这个博客系列的下一部分,我们将看看使用 GoConvey 单元测试我们的服务并模拟 BoltDB 客户端。

本文的版权归 lwj786 所有,如需转载请联系作者。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏云飞学编程

Python学习,多进程了解一下!学爬虫不会用多进程能行吗?

首先我们先做一个小脚本,就用turtle画4个同心圆吧!这样在演示多进程的时候比较直观。代码如下:

583
来自专栏七夜安全博客

Scrapy爬取美女图片第三集 代理ip(上)

1354
来自专栏Java Web

模仿天猫实战【SSM版】——后台开发

2726
来自专栏walterlv - 吕毅的博客

C#/.NET 中推荐的 Dispose 模式的实现

发布于 2015-02-05 02:10 更新于 2018-06...

451
来自专栏Seebug漏洞平台

CVE-2017-5123 漏洞利用全攻略

原文:https://salls.github.io/Linux-Kernel-CVE-2017-5123/

3797
来自专栏Android 研究

Android跨进程通信IPC之1——Linux基础

由于Android系统是基于Linux系统的,所以有必要简单的介绍下Linux的跨进程通信,对大家后续了解Android的跨进程通信是有帮助的,本篇的主要内容如...

913
来自专栏牛肉圆粉不加葱

【容错篇】Spark Streaming的还原药水——Checkpoint

一个 Streaming Application 往往需要7*24不间断的跑,所以需要有抵御意外的能力(比如机器或者系统挂掉,JVM crash等)。为了让这成...

442
来自专栏Bug生活2048

Java学习笔记(十)——Thrift入门及一些基础知识介绍

什么是RPC框架呢?RPC全称为Remote Procedure Call,意为远程过程调用。

662
来自专栏牛客网

腾讯OMG后台开发面经

2680
来自专栏CSDN技术头条

NewSQL数据库大对象块存储原理与应用

一、前言 企业内容管理(EnterpriseContent Management,ECM)系统是一种管理非结构化内容的系统,传统代表为EMC Document...

1915

扫码关注云+社区