第二部分,我们会:

  • 建立go项目
  • 写我们的第一个微服务
  • 使用Gorilla组件为HTTP请求提供JSON应答

我们从微服务基础开始,之后会搭建在我们的docker swarm上

介绍


对于内部请求还是外部请求,通过HTTP提供JSON应答不是唯一的选择。但我们在这里会主要讲解这一方法。当内部请求或者外部请求也是一个系统时,用RPC和二进制信息格式作为请求方式也是一个不错的选择,例如protocol buffers。go有内置的RPC支持,gRPC值得研究一下。然而,这里我们先讲解用内置的http package和Gorilla web tookit。

许多有用的框架(安全,追踪)依赖于HTTP头信息来传递请求的状态,这也是用HTTP的好处。在我们之后的博客中也会看到我们传递相关的ID和OAuth头信息。虽然其他的协议也支持相似的功能,但是许多框架实在HTTP上开发的,所以我尽量用他们来让我们的集成更直接。

创建go项目


如果你已经熟悉go开发,你可以略过这一段。
我觉得go的工作区需要一点时间来熟悉他。我习惯于用我的项目的根目录作为工作区的根目录。然而go组织一个工作区的方法有点奇怪,这和go编译器找寻源代码和依赖关系的方式有关。
推荐阅读: 
官方文档
go path and workspace

安装SDK


在我们写代码之前,我们需要安装go SDK。你可以参照官方文档

1. 建立根目录


所有命令都是基于OS X或者Linux开发环境。如果你是在windows上开发,请调整这些命令。

mkdir ~/goworkspace
cd goworkspace
export GOPATH=`pwd`

这里我们建立一个根目录并且让环境变量GOPATH指向这个文件。这个根目录会包含所有go的代码和第三方库。我建议你把GOPATH加入到.bash_profile中,所以你不需要每次都设置。

2. 创建第一个项目的文件夹和文件


现在我们在根目录,执行下面操作:

mkdir -p src/github.com/callistaenterprise

如果你想自己跟着打代码,执行下面操作:

cd src/github.com/callistaenterprise
mkdir -p goblog/accountservice
cd goblog/accountservice
touch main.go
mkdir service

或者你可以直接clone这个git仓库,转到p2分支。在文件夹src/github.com/callistaenterprise中,执行

git clone https://github.com/callistaenterprise/goblog.git
cd goblog
git checkout P2

现在我们可以开始了。

创建服务-main.go


main函数和其他语言中的一样,程序的接入点。让我们写一些代码来运行一下

package main

import (
        "fmt"
        )
        
var appName = "accountservice"

func main() {
    fmt.Printf("Starting %v\n", appName)
}

现在,运行它。确定你在这个文件夹下:
$GOPATH/src/github.com/callistaenterprise/goblog/accountservice

> go run *.go
Starting accountservice
>

完成,这段代码打印之后退出,现在让我们加上HTTP终端

建立一个HTTP服务


让项目更整洁,我们会把所有的HTTP服务放进service文件夹中

启动HTTP服务


创建一个文件webserve.go在/services文件夹中

package service

import (
        "net/http"
        "log"
)

func StartWebServer(port string) {

        log.Println("Starting HTTP service at " + port)
        err := http.ListenAndServe(":" + port, nil)    // Goroutine will block here

        if err != nil {
                log.Println("An error occured starting HTTP listener at port " + port)
                log.Println("Error: " + err.Error())
        }
}

我们用内置的net/http包来执行ListenAndServe,从而在指定端口开启HTTP服务。
更新main.go用一个写死的端口去请求StartWebServer函数

package main

import (
        "fmt"
        "github.com/callistaenterprise/goblog/accountservice/service"  // NEW
)

var appName = "accountservice"

func main() {
        fmt.Printf("Starting %v\n", appName)
        service.StartWebServer("6767")           // NEW
}

运行程序:

> go run *.go
Starting accountservice
2017/01/30 19:36:00 Starting HTTP service at 6767

现在我们有一个简单的HTTP服务监听6767端口,curl这个服务

curl http://localhost:6767
404 page not found

404是我们预料到的,因为我们还没有定义任何routes
停止这个服务-Ctrl+C

加入第一个路由


我们要开始搞事情了,我们定义第一个路由。在service文件夹中,创建routes.go

package service

import "net/http"

// Defines a single route, e.g. a human readable name, HTTP method and the
// 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

// Initialize our routes
var routes = Routes{

    Route{
        "GetAccount",                                     // Name
        "GET",                                            // HTTP method
        "/accounts/{accountId}",                          // Route pattern
        func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("Content-Type", "application/json; charset=UTF-8")
            w.Write([]byte("{\"result\":\"OK\"}"))
        },
    },
}

上面代码,我们定义路径/accounts/{accountsId},之后我们可以curl这个路径。Gorilla也支持复杂的路由包括正则匹配,schemes,方法,queries,头信息值等。所以不只限于路径和路径参数
现在,我们会返回一个JSON信息:

{"result":"OK"}

我们现在需要一些样板代码来启动Gorilla路由。在service文件夹中,创建router.go

package service

import (
    "github.com/gorilla/mux"
)

// Function that returns a pointer to a mux.Router we can use as a handler.
func NewRouter() *mux.Router {

    // Create an instance of the Gorilla router
    router := mux.NewRouter().StrictSlash(true)
    
    // Iterate over the routes we declared in routes.go and attach them to the router instance
    for _, route := range routes {
        
        // Attach each route, uses a Builder-like pattern to set each route up.
        router.Methods(route.Method).
                Path(route.Pattern).
                Name(route.Name).
                Handler(route.HandlerFunc)
    }
    return router
}

引入依赖


在import中我们定义一个依赖 github.com/gorilla/mux。
为了让以上文件编译及运行,我们需要用go get来获取定义的package

> go get

go工具会下载所有的源代码。之后这些代码会存储在$GOPATH/src/github.com/gorilla/mux的本地文件中。并编译到你的静态链接二进制文件。

小结


现在,重新看webserver.go并加入下面代码:

func StartWebServer(port string) {

        r := NewRouter()             // NEW
        http.Handle("/", r)          // NEW

这段代码吧我们刚刚写的路由加入http.Handle中。让我们运行代码:

> go run *.go
Starting accountservice
2017/01/31 15:15:57 Starting HTTP service at 6767

curl这个地址

> curl http://localhost:6767/accounts/10000
  {"result":"OK"}

我们第一个服务就做好了!

资源消耗和效率


由于我们在探索基于go的微服务的资源消耗和效率,我们做一个基测。我开发了一个简单的Gatling测试会用get方法请求accounts/{accountId}。如果你check out这部分的代码,你能看到测试在goblog/loadtest文件夹中.

运行测试

如果你想自己测试,确保accountservice正在你的localhost中运行。并且你已经checked out分支P2。你需要有Jave runtime和Apache Maven.
去文件夹/goblog/loadtest中执行下面的命令

> mvn gatling:execute -Dusers=1000 -Dduration=30 -DbaseUrl=http://localhost:6767

这将开始测试,输入参数是

  • user: 模拟的并发用户数
  • duration: 测试持续秒数
  • baseUrl: 我们测试服务的基础路径。当我们用docker swarm时,baseUrl将会变成swarm的公共IP.

测试结束后,结果会写入terminal中,同时一个漂亮的HTML报告会存入target/gatling/results/中

结果


之后我们的基测将会在docker swarm中进行,现在,我的2014年的旧mac还是要负重运行。
下面是内存消耗,在OS X的任务管理器中显示

clipboard.png

1.2MB,还不错。让我们用Gatling测试1K req/s.记住这是一个非常简单的,只返回hard code的字符串的程序

clipboard.png

1k req/s让accountservice消耗大约28MB的内存。这仍然是spring boot应用在开启时的十分之一。这将是有趣的去看一下这个数字怎么变当我们加入一些真的处理函数进去。

性能和CPU使用

clipboard.png

1k req/s 消耗单核的8%左右

clipboard.png

不清楚Gatling怎样处理毫秒的延迟,但是平均延迟是0ms.有一个请求用掉11ms。总体来看,accountservice性能相当好,能处理745 req/s。

接下来。。


下一部分,我们要让我们的accountserive做一些有用的事。我们要加入简单的数据库。我们也会尝试JSON序列化。同时检查这些会怎样影响消耗和性能。