有时,应用级并行性是不够的,在开发过程中看似简单的事情在部署过程中可能会变得复杂。分布式系统提供了许多在单机上开发时无法发现的挑战。这些应用增加了监控、编写需要强一致性保证的应用以及服务发现等方面的复杂性。此外,您必须始终注意单个故障点,例如数据库,否则,当单个组件发生故障时,您的分布式应用可能会失败。
本章将探讨管理分布式数据、编排、容器化、度量和监视的方法。这些将成为编写和维护微服务和大型分布式应用工具箱的一部分。
在本章中,我们将介绍以下配方:
- 与 concur 一起使用服务发现
- 利用 Raft 实现基本共识
- 与 Docker 一起使用集装箱化
- 编排和部署策略
- 监控应用
- 收集指标
要遵循本章中的所有方法,请按照以下步骤配置您的环境:
-
从下载 Go 1.12.6 或更高版本并安装到您的操作系统上 https://golang.org/doc/install 。
-
从安装领事 https://www.consul.io/intro/getting-started/install.html 。
-
打开终端或控制台应用,创建并导航到项目目录,如
~/projects/go-programming-cookbook
,所有代码都将从此目录运行和修改。 -
将最新代码克隆到
~/projects/go-programming-cookbook-original
并(可选)从该目录工作,而不是手动键入示例:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
当对应用使用微服务方法时,最终会有大量服务器侦听各种 IP、域和端口。这些 IP 地址因环境而异(分段与生产),在服务之间进行配置时,保持它们的静态可能会很困难。您还想知道机器或服务何时因网络分区而停机或无法访问。当网络的两个部分无法相互连接时,就会发生网络分区。例如,如果两个数据中心之间的交换机发生故障,则一个数据中心内的服务无法到达另一个数据中心内的服务。Concur 是一个提供很多功能的工具,但在这里,我们将探讨如何向 Concur 注册服务并从其他服务查询服务。
这些步骤包括编写和运行应用:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter11/discovery
的新目录并导航到它。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/discovery
您应该会看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/discovery
- 从
~/projects/go-programming-cookbook-original/chapter11/discovery
复制测试,或者以此为机会编写一些自己的代码! - 创建一个名为
client.go
的文件,其内容如下:
package discovery
import "github.com/hashicorp/consul/api"
// Client exposes api methods we care
// about
type Client interface {
Register(tags []string) error
Service(service, tag string) ([]*api.ServiceEntry,
*api.QueryMeta, error)
}
type client struct {
client *api.Client
address string
name string
port int
}
//NewClient iniitalizes a consul client
func NewClient(config *api.Config, address, name string, port
int) (Client, error) {
c, err := api.NewClient(config)
if err != nil {
return nil, err
}
cli := &client{
client: c,
name: name,
address: address,
port: port,
}
return cli, nil
}
- 创建一个名为
operations.go
的文件,其内容如下:
package discovery
import "github.com/hashicorp/consul/api"
// Register adds our service to consul
func (c *client) Register(tags []string) error {
reg := &api.AgentServiceRegistration{
ID: c.name,
Name: c.name,
Port: c.port,
Address: c.address,
Tags: tags,
}
return c.client.Agent().ServiceRegister(reg)
}
// Service return a service
func (c *client) Service(service, tag string)
([]*api.ServiceEntry, *api.QueryMeta, error) {
return c.client.Health().Service(service, tag, false,
nil)
}
- 创建一个名为
exec.go
的文件,其内容如下:
package discovery
import "fmt"
// Exec creates a consul entry then queries it
func Exec(cli Client) error {
if err := cli.Register([]string{"Go", "Awesome"}); err != nil {
return err
}
entries, _, err := cli.Service("discovery", "Go")
if err != nil {
return err
}
for _, entry := range entries {
fmt.Printf("%#v\n", entry.Service)
}
return nil
}
- 创建一个名为
example
的新目录并导航到它。 - 创建一个名为
main.go
的文件,其内容如下:
package main
import "github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter11/discovery"
func main() {
if err := discovery.Exec(); err != nil {
panic(err)
}
}
- 使用
consul agent -dev -node=localhost
命令在单独的终端中启动 CONSOR。 - 运行
go run main.go
命令。 - 您还可以运行以下命令:
$ go build $ ./example
您应该看到以下输出:
$ go run main.go
&api.AgentService{ID:"discovery", Service:"discovery", Tags:
[]string{"Go", "Awesome"}, Port:8080, Address:"localhost",
EnableTagOverride:false, CreateIndex:0x23, ModifyIndex:0x23}
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
concur 提供了一个健壮的 Go-API 库。当你第一次开始的时候,它会让你感到害怕,但是这个食谱展示了你如何包装它。进一步配置 CONSOR 超出了本配方的范围;这显示了在给定密钥和标记时注册服务和查询其他服务的基础知识。
可以使用它在启动时注册新的微服务,查询所有依赖的服务,并在关闭时取消注册。您可能还希望缓存这些信息,这样您就不会对每个请求都使用 concur,但是这个方法提供了一些基本工具,您可以在此基础上进行扩展。领事代理人还快速有效地提出这些重复请求(https://www.consul.io/intro/getting-started/agent.html )。一旦你成功了
Raft 是一种一致性算法。它允许分布式系统保持共享和管理状态(https://raft.github.io/ )。建立一个筏式系统在很多方面都很复杂——首先,你需要达成共识,选举才能发生和成功。当您使用多个节点时,这可能很难引导,并且很难开始。可以在单个节点/引线上运行基本群集。但是,如果需要冗余,则至少需要三个节点来防止单个节点发生故障时的数据丢失。此概念称为仲裁,在仲裁中,您必须维护(n/2)+1 个可用节点,以确保新日志可以提交到 Raft 集群。基本上,如果您能够保持仲裁,集群将保持健康和可用。
这个方法实现了一个基本的内存 Raft 集群,构造了一个可以在某些允许的状态之间转换的状态机,并将分布式状态机连接到一个可以触发转换的 web 处理程序。这在实现 Raft 所需的基本有限状态机接口或测试时非常有用。本食谱使用https://github.com/hashicorp/raft 用于基础筏的实施。
以下步骤包括编写和运行应用:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter11/consensus
的新目录并导航到它。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/consensus
您应该会看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/consensus
- 从
~/projects/go-programming-cookbook-original/chapter11/consensus
复制测试,或者以此为机会编写一些自己的代码! - 创建一个名为
state.go
的文件,其内容如下:
package consensus
type state string
const (
first state = "first"
second = "second"
third = "third"
)
var allowedState map[state][]state
func init() {
// setup valid states
allowedState = make(map[state][]state)
allowedState[first] = []state{second, third}
allowedState[second] = []state{third}
allowedState[third] = []state{first}
}
// CanTransition checks if a new state is valid
func (s *state) CanTransition(next state) bool {
for _, n := range allowedState[*s] {
if n == next {
return true
}
}
return false
}
// Transition will move a state to the next
// state if able
func (s *state) Transition(next state) {
if s.CanTransition(next) {
*s = next
}
}
- 创建一个名为
raftset.go
的文件,其内容如下:
package consensus
import (
"fmt"
"github.com/hashicorp/raft"
)
// keep a map of rafts for later
var rafts map[raft.ServerAddress]*raft.Raft
func init() {
rafts = make(map[raft.ServerAddress]*raft.Raft)
}
// raftSet stores all the setup material we need
type raftSet struct {
Config *raft.Config
Store *raft.InmemStore
SnapShotStore raft.SnapshotStore
FSM *FSM
Transport raft.LoopbackTransport
Configuration raft.Configuration
}
// generate n raft sets to bootstrap the raft cluster
func getRaftSet(num int) []*raftSet {
rs := make([]*raftSet, num)
servers := make([]raft.Server, num)
for i := 0; i < num; i++ {
addr := raft.ServerAddress(fmt.Sprint(i))
_, transport := raft.NewInmemTransport(addr)
servers[i] = raft.Server{
Suffrage: raft.Voter,
ID: raft.ServerID(addr),
Address: addr,
}
config := raft.DefaultConfig()
config.LocalID = raft.ServerID(addr)
rs[i] = &raftSet{
Config: config,
Store: raft.NewInmemStore(),
SnapShotStore: raft.NewInmemSnapshotStore(),
FSM: NewFSM(),
Transport: transport,
}
}
// configuration needs to be consistent between
// services and so we need the full serverlist in this
// case
for _, r := range rs {
r.Configuration = raft.Configuration{Servers: servers}
}
return rs
}
- 创建一个名为
config.go
的文件,其内容如下:
package consensus
import (
"github.com/hashicorp/raft"
)
// Config creates num in-memory raft
// nodes and connects them
func Config(num int) {
// create n "raft-sets" consisting of
// everything needed to represent a node
rs := getRaftSet(num)
//connect all of the transports
for _, r1 := range rs {
for _, r2 := range rs {
r1.Transport.Connect(r2.Transport.LocalAddr(), r2.Transport)
}
}
// for each node, bootstrap then connect
for _, r := range rs {
if err := raft.BootstrapCluster(r.Config, r.Store, r.Store, r.SnapShotStore, r.Transport, r.Configuration); err != nil {
panic(err)
}
raft, err := raft.NewRaft(r.Config, r.FSM, r.Store, r.Store, r.SnapShotStore, r.Transport)
if err != nil {
panic(err)
}
rafts[r.Transport.LocalAddr()] = raft
}
}
- 创建一个名为
fsm.go
的文件,其内容如下:
package consensus
import (
"io"
"github.com/hashicorp/raft"
)
// FSM implements the raft FSM interface
// and holds a state
type FSM struct {
state state
}
// NewFSM creates a new FSM with
// start state of "first"
func NewFSM() *FSM {
return &FSM{state: first}
}
// Apply updates our FSM
func (f *FSM) Apply(r *raft.Log) interface{} {
f.state.Transition(state(r.Data))
return string(f.state)
}
// Snapshot needed to satisfy the raft FSM interface
func (f *FSM) Snapshot() (raft.FSMSnapshot, error) {
return nil, nil
}
// Restore needed to satisfy the raft FSM interface
func (f *FSM) Restore(io.ReadCloser) error {
return nil
}
- 创建一个名为
handler.go
的文件,其内容如下:
package consensus
import (
"net/http"
"time"
)
// Handler grabs the get param ?next= and tries
// to transition to the state contained there
func Handler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
state := r.FormValue("next")
for address, raft := range rafts {
if address != raft.Leader() {
continue
}
result := raft.Apply([]byte(state), 1*time.Second)
if result.Error() != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
newState, ok := result.Response().(string)
if !ok {
w.WriteHeader(http.StatusInternalServerError)
return
}
if newState != state {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("invalid transition"))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(newState))
return
}
}
- 创建一个名为
example
的新目录并导航到它。 - 创建一个名为
main.go
的文件,其内容如下:
package main
import (
"net/http"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter11/consensus"
)
func main() {
consensus.Config(3)
http.HandleFunc("/", consensus.Handler)
err := http.ListenAndServe(":3333", nil)
panic(err)
}
- 运行
go run main.go
命令。或者,也可以运行以下命令:
$ go build
$ ./example
您现在应该看到以下输出:
$ go run main.go
2019/05/04 21:06:46 [INFO] raft: Initial configuration (index=1): [{Suffrage:Voter ID:0 Address:0} {Suffrage:Voter ID:1 Address:1} {Suffrage:Voter ID:2 Address:2}]
2019/05/04 21:06:46 [INFO] raft: Initial configuration (index=1): [{Suffrage:Voter ID:0 Address:0} {Suffrage:Voter ID:1 Address:1} {Suffrage:Voter ID:2 Address:2}]
2019/05/04 21:06:46 [INFO] raft: Node at 0 [Follower] entering Follower state (Leader: "")
2019/05/04 21:06:46 [INFO] raft: Node at 1 [Follower] entering Follower state (Leader: "")
2019/05/04 21:06:46 [INFO] raft: Initial configuration (index=1): [{Suffrage:Voter ID:0 Address:0} {Suffrage:Voter ID:1 Address:1} {Suffrage:Voter ID:2 Address:2}]
2019/05/04 21:06:46 [INFO] raft: Node at 2 [Follower] entering Follower state (Leader: "")
2019/05/04 21:06:47 [WARN] raft: Heartbeat timeout from "" reached, starting election
2019/05/04 21:06:47 [INFO] raft: Node at 0 [Candidate] entering Candidate state in term 2
2019/05/04 21:06:47 [DEBUG] raft: Votes needed: 2
2019/05/04 21:06:47 [DEBUG] raft: Vote granted from 0 in term 2\. Tally: 1
2019/05/04 21:06:47 [DEBUG] raft: Vote granted from 1 in term 2\. Tally: 2
2019/05/04 21:06:47 [INFO] raft: Election won. Tally: 2
2019/05/04 21:06:47 [INFO] raft: Node at 0 [Leader] entering Leader state
2019/05/04 21:06:47 [INFO] raft: Added peer 1, starting replication
2019/05/04 21:06:47 [INFO] raft: Added peer 2, starting replication
2019/05/04 21:06:47 [INFO] raft: pipelining replication to peer {Voter 1 1}
2019/05/04 21:06:47 [INFO] raft: pipelining replication to peer {Voter 2 2}
- 在单独的终端中,运行以下命令:
$ curl "http://localhost:3333/?next=second"
second
$ curl "http://localhost:3333/?next=third"
third
$ curl "http://localhost:3333/?next=second"
invalid transition
$ curl "http://localhost:3333/?next=first"
first
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
当应用启动时,我们初始化多个 Raft 对象。每个人都有自己的地址和交通工具。InmemTransport{}
功能还提供了连接其他传输的方法,称为Connect()
。一旦这些连接建立,筏群将举行选举。在 Raft 集群中通信时,客户端必须与领导者通信。在我们的例子中,一个处理程序可以与所有节点通信,因此处理程序负责拥有Raft
领导者的call Apply()
对象。这反过来在所有其他节点上运行apply()
。
InmemTransport{}
函数允许所有内容驻留在内存中,从而简化了选择和引导过程。实际上,除了测试和概念验证之外,这并没有多大帮助,因为 Goroutines 可以自由访问共享内存。更注重生产的实现将使用 HTTP 传输之类的东西,以便服务实例可以跨机器通信。这可能需要一些额外的簿记或服务发现,因为服务实例必须侦听和服务,并且能够发现和建立彼此之间的连接。
Docker 是一种用于包装和装运应用的容器技术。其他优势还包括可移植性,因为无论主机操作系统如何,容器都将以相同的方式运行。它提供了虚拟机的许多优点,但是在一个更轻量级的容器中。可以限制单个容器和沙箱环境的资源消耗。在本地为您的应用提供一个公共环境以及将代码交付生产时,这可能非常有用。Docker 是用 Go 编写的,并且是开源的,所以很容易利用客户端和库。此配方将为基本 Go 应用设置 Docker 容器,存储有关容器的一些版本信息,并演示如何从 Docker 端点命中处理程序。
根据以下步骤配置您的环境:
- 有关配置环境的步骤,请参阅本章中的技术要求部分。
- 从安装 Dockerhttps://docs.docker.com/install 。这也包括 Docker Compose。
这些步骤包括编写和运行应用:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter11/docker
的新目录并导航到它。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/docker
您应该会看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/docker
- 从
~/projects/go-programming-cookbook-original/chapter11/docker
复制测试,或者以此为机会编写一些自己的代码! - 创建一个名为
dockerfile
的文件,其内容如下:
FROM alpine
ADD ./example/example /example
EXPOSE 8000
ENTRYPOINT /example
- 创建一个名为
setup.sh
的文件,其内容如下:
#!/usr/bin/env bash
pushd example
env GOOS=linux go build -ldflags "-X main.version=1.0 -X
main.builddate=$(date +%s)"
popd
docker build . -t example
docker run -d -p 8000:8000 example
- 创建一个名为
version.go
的文件,其内容如下:
package docker
import (
"encoding/json"
"net/http"
"time"
)
// VersionInfo holds artifacts passed in
// at build time
type VersionInfo struct {
Version string
BuildDate time.Time
Uptime time.Duration
}
// VersionHandler writes the latest version info
func VersionHandler(v *VersionInfo) http.HandlerFunc {
t := time.Now()
return func(w http.ResponseWriter, r *http.Request) {
v.Uptime = time.Since(t)
vers, err := json.Marshal(v)
if err != nil {
w.WriteHeader
(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write(vers)
}
}
- 创建一个名为
example
的新目录并导航到它。 - 创建一个包含以下内容的
main.go
文件:
package main
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter11/docker"
)
// these are set at build time
var (
version string
builddate string
)
var versioninfo docker.VersionInfo
func init() {
// parse buildtime variables
versioninfo.Version = version
i, err := strconv.ParseInt(builddate, 10, 64)
if err != nil {
panic(err)
}
tm := time.Unix(i, 0)
versioninfo.BuildDate = tm
}
func main() {
http.HandleFunc("/version",
docker.VersionHandler(&versioninfo))
fmt.Printf("version %s listening on :8000\n",
versioninfo.Version)
panic(http.ListenAndServe(":8000", nil))
}
- 导航回起始目录。
- 运行以下命令:
$ bash setup.sh
您现在应该看到以下输出:
$ bash setup.sh
~/go/src/github.com/PacktPublishing/Go-Programming-Cookbook-
Second-Edition/chapter11/docker/example
~/go/src/github.com/PacktPublishing/Go-Programming-Cookbook-
Second-Edition/chapter11/docker
~/go/src/github.com/PacktPublishing/Go-Programming-Cookbook-
Second-Edition/chapter11/docker
Sending build context to Docker daemon 6.031 MB
Step 1/4 : FROM alpine
---> 4a415e366388
Step 2/4 : ADD ./example/example /example
---> de34c3c5451e
Removing intermediate container bdcd9c4f4381
Step 3/4 : EXPOSE 8000
---> Running in 188f450d4e7b
---> 35d1a2652b43
Removing intermediate container 188f450d4e7b
Step 4/4 : ENTRYPOINT /example
---> Running in cf0af4f48c3a
---> 3d737fc4e6e2
Removing intermediate container cf0af4f48c3a
Successfully built 3d737fc4e6e2
b390ef429fbd6e7ff87058dc82e15c3e7a8b2e
69a601892700d1d434e9e8e43b
- 运行以下命令:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b390ef429fbd example "/bin/sh -c /example" 22 seconds ago Up 23
seconds 0.0.0.0:8000->8000/tcp optimistic_wescoff
$ curl localhost:8000/version
{"Version":"1.0","BuildDate":"2017-04-
30T21:55:56Z","Uptime":48132111264}
$docker kill optimistic_wescoff # grab from first output
optimistic_wescoff
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
该配方创建了一个脚本,用于编译 Linux 体系结构的 Go 二进制文件,并在main.go
中设置各种私有变量。这些变量用于返回版本端点上的版本信息。编译二进制文件后,将创建一个包含该二进制文件的 Docker 容器。这允许我们使用非常小的容器映像,因为 Go 运行时是二进制文件中自包含的。然后,我们运行容器,同时公开容器侦听 HTTP 通信的端口。最后,我们curl
在 localhost 上打开端口并查看返回的版本信息。
Docker 使编排和部署更加简单。在这个配方中,我们将建立到 MongoDB 的连接,然后插入一个文档并从 Docker 容器中查询所有文档。此配方将设置与使用 NoSQL 的相同的环境,使用第 6 章中的 MongoDB 和 mgo配方、所有关于数据库和存储的内容,但将在容器内运行应用和环境,并使用 Docker Compose 来协调和连接它们。
这可以在以后与 Docker Swarm 结合使用,Docker Swarm 是一个集成的 Docker 工具,允许您管理集群、创建和部署可轻松放大或缩小的节点,以及管理负载平衡(https://docs.docker.com/engine/swarm/ )。容器编排的另一个好例子是 Kubernetes(https://kubernetes.io/ ),一个由 Google 使用 Go 编程语言编写的容器编排框架。
以下步骤包括编写和运行应用:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter11/orchestrate
的新目录并导航到它。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/orchestrate
您应该会看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/orchestrate
- 从
~/projects/go-programming-cookbook-original/chapter11/orchestrate
复制测试,或者以此为机会编写一些自己的代码! - 创建一个名为
Dockerfile
的文件,其内容如下:
FROM golang:1.12.4-alpine3.9
ENV GOPATH /code/
ADD . /code/src/github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/docker
WORKDIR /code/src/github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/docker/example
RUN GO111MODULE=on GOPROXY=off go build -mod=vendor
ENTRYPOINT /code/src/github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/docker/example/example
- 创建一个名为
docker-compose.yml
的文件,其内容如下:
version: '2'
services:
app:
build: .
mongodb:
image: "mongo:latest"
- 创建一个名为
config.go
的文件,其内容如下:
package mongodb
import (
"context"
"fmt"
"time"
"github.com/mongodb/mongo-go-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// Setup initializes a mongo client
func Setup(ctx context.Context, address string) (*mongo.Client, error) {
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
fmt.Println(address)
client, err := mongo.NewClient(options.Client().ApplyURI(address))
if err != nil {
return nil, err
}
if err := client.Connect(ctx); err != nil {
return nil, err
}
return client, nil
}
- 创建一个名为
exec.go
的文件,其内容如下:
package mongodb
import (
"context"
"fmt"
"github.com/mongodb/mongo-go-driver/bson"
)
// State is our data model
type State struct {
Name string `bson:"name"`
Population int `bson:"pop"`
}
// Exec creates then queries an Example
func Exec(address string) error {
ctx := context.Background()
db, err := Setup(ctx, address)
if err != nil {
return err
}
conn := db.Database("gocookbook").Collection("example")
vals := []interface{}{&State{"Washington", 7062000}, &State{"Oregon", 3970000}}
// we can inserts many rows at once
if _, err := conn.InsertMany(ctx, vals); err != nil {
return err
}
var s State
if err := conn.FindOne(ctx, bson.M{"name": "Washington"}).Decode(&s); err != nil {
return err
}
if err := conn.Drop(ctx); err != nil {
return err
}
fmt.Printf("State: %#v\n", s)
return nil
}
- 创建一个名为
example
的新目录并导航到它。 - 创建一个包含以下内容的
main.go
文件:
package main
import mongodb "github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/orchestrate"
func main() {
if err := mongodb.Exec("mongodb://mongodb:27017"); err != nil {
panic(err)
}
}
- 导航回起始目录。
- 运行
go mod vendor
命令。 - 运行
docker-compose up -d
命令。 - 运行
docker logs orchestrate_app_1
命令。您现在应该看到以下输出:
$ docker logs orchestrate_app_1
State: docker.State{Name:"Washington", Population:7062000}
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
这种配置有利于本地开发。运行docker-compose up
命令后,重建本地目录,Docker 使用最新版本建立到 MongoDB 实例的连接,并开始对其进行操作。此配方使用 go mod 供应商进行依赖关系管理。因此,我们禁用了go mod cache
并告诉go build
命令使用我们创建的供应商目录。
这可以在启动需要连接到外部服务的应用时提供良好的基线;第 6 章关于数据库和存储中的所有配方都可以使用这种方法,而不是创建数据库的本地实例。对于生产环境,您可能不希望在 Docker 容器后面运行数据存储,但通常也会有用于配置的静态主机名。
有多种方法可以监视 Go 应用。最简单的方法之一是设置 Prometheus,一个用 Go(编写的监控应用 https://prometheus.io )。这是一个应用,它根据您的配置文件轮询端点,并收集有关应用的大量信息,包括 goroutine 的数量、内存使用情况等。此应用将使用上一个配方中的技术来设置 Docker 环境来托管普罗米修斯并连接到它。
以下步骤包括编写和运行应用:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter11/monitoring
的新目录并导航到它。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/monitoring
您应该会看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/monitoring
- 从
~/projects/go-programming-cookbook-original/chapter11/monitoring
复制测试,或者以此为机会编写一些自己的代码! - 创建一个名为
Dockerfile
的文件,其内容如下:
FROM golang:1.12.4-alpine3.9
ENV GOPATH /code/
ADD . /code/src/github.com/agtorre/go-
cookbook/chapter11/monitoring
WORKDIR /code/src/github.com/agtorre/go-
cookbook/chapter11/monitoring
RUN GO111MODULE=on GOPROXY=off go build -mod=vendor
ENTRYPOINT /code/src/github.com/agtorre/go-
cookbook/chapter11/monitoring/monitoring
- 创建一个名为
docker-compose.yml
的文件,其内容如下:
version: '2'
services:
app:
build: .
prometheus:
ports:
- 9090:9090
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
image: "prom/prometheus"
- 创建一个名为
main.go
的文件,其内容如下:
package main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
http.Handle("/metrics", promhttp.Handler())
panic(http.ListenAndServe(":80", nil))
}
- 创建一个名为
prometheus.yml
的文件,其内容如下:
global:
scrape_interval: 15s # By default, scrape targets every 15
seconds.
# A scrape configuration containing exactly one endpoint to
scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=<job_name>` to any
timeseries scraped from this config.
- job_name: 'app'
# Override the global default and scrape targets from this job
every 5 seconds.
scrape_interval: 5s
static_configs:
- targets: ['app:80']
- 运行
go mod vendor
命令。 - 运行
docker-compose up
命令。您现在应该看到以下输出:
$ docker-compose up
Starting monitoring_prometheus_1 ... done
Starting monitoring_app_1 ... done
Attaching to monitoring_app_1, monitoring_prometheus_1
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Starting prometheus (version=1.6.1, branch=master, revision=4666df502c0e239ed4aa1d80abbbfb54f61b23c3)" source="main.go:88"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Build context (go=go1.8.1, user=root@7e45fa0366a7, date=20170419-14:32:22)" source="main.go:89"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Loading configuration file /etc/prometheus/prometheus.yml" source="main.go:251"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Loading series map and head chunks..." source="storage.go:421"
prometheus_1 | time="2019-05-05T03:10:25Z" level=warning msg="Persistence layer appears dirty." source="persistence.go:846"
prometheus_1 | time="2019-05-05T03:10:25Z" level=warning msg="Starting crash recovery. Prometheus is inoperational until complete." source="crashrecovery.go:40"
prometheus_1 | time="2019-05-05T03:10:25Z" level=warning msg="To avoid crash recovery in the future, shut down Prometheus with SIGTERM or a HTTP POST to /-/quit." source="crashrecovery.go:41"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Scanning files." source="crashrecovery.go:55"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="File scan complete. 43 series found." source="crashrecovery.go:83"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Checking for series without series file." source="crashrecovery.go:85"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Check for series without series file complete." source="crashrecovery.go:131"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Cleaning up archive indexes." source="crashrecovery.go:411"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Clean-up of archive indexes complete." source="crashrecovery.go:504"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Rebuilding label indexes." source="crashrecovery.go:512"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Indexing metrics in memory." source="crashrecovery.go:513"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Indexing archived metrics." source="crashrecovery.go:521"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="All requests for rebuilding the label indexes queued. (Actual processing may lag behind.)" source="crashrecovery.go:540"
prometheus_1 | time="2019-05-05T03:10:25Z" level=warning msg="Crash recovery complete." source="crashrecovery.go:153"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="43 series loaded." source="storage.go:432"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Starting target manager..." source="targetmanager.go:61"
prometheus_1 | time="2019-05-05T03:10:25Z" level=info msg="Listening on :9090" source="web.go:259"
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。- 将浏览器导航至
http://localhost:9090/
。你应该看到与你的应用相关的各种指标!
这个方法在 Go 中创建一个简单的处理程序,该处理程序使用 prometheus Go 客户机将有关正在运行的应用的统计信息导出到 prometheus。我们将应用连接到运行在 docker 中的 prometheus 服务器,并使用 docker compose 处理网络连接和启动。prometheus.yml
文件中指定了收集数据的频率设置、应用服务的端口以及应用的名称。一旦两个容器都启动,prometheus 服务器就开始在指定端口上采集和监视应用。它还公开了一个 web 界面,我们可以在浏览器中访问该界面,以查看有关我们应用的更多信息。
Prometheus 客户端处理程序将向 Prometheus 服务器返回有关应用的各种统计信息。这允许您将多个 Prometheus 服务器指向一个应用,而无需重新配置或部署该应用。这些统计数据中的大多数都是通用的,对于检测内存泄漏等方面都是有益的。许多其他解决方案要求您定期向服务器发送信息。下一个配方收集度量将演示如何将自定义度量发送到 Prometheus 服务器。
除了有关应用的一般信息外,发布特定于应用的指标也很有帮助。例如,我们可能希望收集计时数据或跟踪事件发生的次数。
此配方将使用github.com/rcrowley/go-metrics
包收集度量并通过端点公开它们。您可以使用各种导出器工具将度量导出到普罗米修斯(Prometheus)和 InfluxDB 等地方,这些工具也是用 Go 编写的。
根据以下步骤配置您的环境:
- 有关配置环境的步骤,请参阅本章中的技术要求部分。
- 运行
go get github.com/rcrowley/go-metrics
命令。
这些步骤包括编写和运行应用:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter11/metrics
的新目录并导航到它。 - 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/metrics
您应该会看到一个名为go.mod
的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/metrics
- 从
~/projects/go-programming-cookbook-original/chapter11/metrics
复制测试,或者以此为机会编写一些自己的代码! - 创建一个名为
handler.go
的文件,其内容如下:
package metrics
import (
"net/http"
"time"
metrics "github.com/rcrowley/go-metrics"
)
// CounterHandler will update a counter each time it's called
func CounterHandler(w http.ResponseWriter, r *http.Request) {
c := metrics.GetOrRegisterCounter("counterhandler.counter",
nil)
c.Inc(1)
w.WriteHeader(http.StatusOK)
w.Write([]byte("success"))
}
// TimerHandler records the duration required to compelete
func TimerHandler(w http.ResponseWriter, r *http.Request) {
currt := time.Now()
t := metrics.GetOrRegisterTimer("timerhandler.timer", nil)
w.WriteHeader(http.StatusOK)
w.Write([]byte("success"))
t.UpdateSince(currt)
}
- 创建一个名为
report.go
的文件,其内容如下:
package metrics
import (
"net/http"
gometrics "github.com/rcrowley/go-metrics"
)
// ReportHandler will emit the current metrics in json format
func ReportHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
t := gometrics.GetOrRegisterTimer(
"reporthandler.writemetrics", nil)
t.Time(func() {
gometrics.WriteJSONOnce(gometrics.DefaultRegistry, w)
})
}
- 创建一个名为
example
的新目录并导航到它。 - 创建一个名为
main.go
的文件:
package main
import (
"net/http"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter11/metrics"
)
func main() {
// handler to populate metrics
http.HandleFunc("/counter", metrics.CounterHandler)
http.HandleFunc("/timer", metrics.TimerHandler)
http.HandleFunc("/report", metrics.ReportHandler)
fmt.Println("listening on :8080")
panic(http.ListenAndServe(":8080", nil))
}
- 运行
go run main.go
。或者,也可以运行以下命令:
$ go build $ ./example
您现在应该看到以下输出:
$ go run main.go
listening on :8080
- 从单独的 shell 运行以下命令:
$ curl localhost:8080/counter
success
$ curl localhost:8080/timer
success
$ curl localhost:8080/report
{"counterhandler.counter":{"count":1},
"reporthandler.writemetrics": {"15m.rate":0,"1m.rate":0,"5m.rate":0,"75%":0,"95%":0,"99%":0,"99.9%":0,"count":0,"max":0,"mean":0,"mean.rate":0,"median":0,"min":0,"stddev":0},"timerhandler.timer":{"15m.rate":0.0011080303990206543,"1m.rate":0.015991117074135343,"5m.rate":0.0033057092356765017,"75%":60485,"95%":60485,"99%":60485,"99.9%":60485,"count":1,"max":60485,"mean":60485,"mean.rate":1.1334543719787356,"median":60485,"min":60485,"stddev":0}}
- 尝试再点击所有端点几次,看看它们是如何变化的。
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
gometrics
将您的所有指标保存在注册表中。设置后,您可以使用任何度量发射选项,例如counter
或timer
,它将在注册表中存储此更新。有多个导出器将度量导出到第三方工具。在本例中,我们设置了一个以 JSON 格式发出所有度量的处理程序。
我们设置了三个处理程序—一个递增计数器,一个记录退出处理程序的时间,另一个打印报告(同时递增额外的计数器)。如果度量发射器当前不以线程安全的方式存在,GetOrRegister
函数对于以原子方式获取或创建度量发射器非常有用。或者,您可以提前注册所有内容一次。