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 条评论
登录 后参与评论

相关文章

来自专栏酷玩时刻

微信公众号开发之推广支持

前几篇文章详细介绍了微信App支付、公众号支付、微信红包、微信刷卡以及支付宝支付,今天来聊聊 推广支持之生成带参数的二维码、长链接转短链接

2083
来自专栏好好学java的技术栈

面试官问的hibernate和mybatis常见面试题

我是一名java开发人员,hibernate以及mybatis都有过学习,在java面试中也被提及问道过,在项目实践中也应用过,现在对hibernate和myb...

3531
来自专栏WindCoder

Eclipse常用配置

或者ALT+SHIFT +W ,在弹出的菜单中选择System Explorer 

1941
来自专栏jeremy的技术点滴

dubbo起步

4076
来自专栏工科狗和生物喵

【计算机本科补全计划】Java学习笔记(一) 安装配置 (Mac Sublime3) 红黄蓝

正文之前 标题后面为啥要加三个字呢。蹭热度不至于,就想着,让更多人知道么。毕竟我以后也会有当爸的一天~ 要是那些人渣站在悬崖上,旁边没啥人看着,我上去踢一脚是做...

3787
来自专栏黑泽君的专栏

day45_Webservice学习笔记_01

Web service 即web服务,它是一种跨编程语言和跨操作系统平台的远程调用技术即跨平台远程调用技术。

1131
来自专栏北京马哥教育

黑客用Python:检测并绕过Web应用程序防火墙

Web应用防火墙通常会被部署在Web客户端与Web服务器之间,以过滤来自服务器的恶意流量。而作为一名渗透测试人员,想要更好的突破目标系统,就必须要了解目标系统的...

1341
来自专栏人人都是极客

Linux中形形色色的接口API和ABI

如果将内核比作一座工厂,那么Linux中众多的接口就是通往这个巨大工厂的高速公路。这条路要足够坚固,禁得起各种破坏(Robust)。要能跑得了运货的卡车,还要能...

1584
来自专栏FreeBuf

如何用0day漏洞黑掉西部数据NAS存储设备

我们以入侵和破解设备为乐,今天,要向大家展示的是近期我们对西部数据(Western Digital )网络存储设备(NAS)的漏洞发现和入侵利用过程。点击阅读原...

2419
来自专栏Albert陈凯

分布式通信的几种方式(EJB、RMI、RPC、JMS、web service杂谈)

http://blog.csdn.net/jiaolong724/article/details/21089347 RPC(remote produce cal...

4158

扫码关注云+社区