Skip to content

Latest commit

 

History

History
1150 lines (875 loc) · 41.9 KB

File metadata and controls

1150 lines (875 loc) · 41.9 KB

十一、分布式系统

有时,应用级并行性是不够的,在开发过程中看似简单的事情在部署过程中可能会变得复杂。分布式系统提供了许多在单机上开发时无法发现的挑战。这些应用增加了监控、编写需要强一致性保证的应用以及服务发现等方面的复杂性。此外,您必须始终注意单个故障点,例如数据库,否则,当单个组件发生故障时,您的分布式应用可能会失败。

本章将探讨管理分布式数据、编排、容器化、度量和监视的方法。这些将成为编写和维护微服务和大型分布式应用工具箱的一部分。

在本章中,我们将介绍以下配方:

  • 与 concur 一起使用服务发现
  • 利用 Raft 实现基本共识
  • 与 Docker 一起使用集装箱化
  • 编排和部署策略
  • 监控应用
  • 收集指标

技术要求

要遵循本章中的所有方法,请按照以下步骤配置您的环境:

  1. 下载 Go 1.12.6 或更高版本并安装到您的操作系统上 https://golang.org/doc/install

  2. 安装领事 https://www.consul.io/intro/getting-started/install.html

  3. 打开终端或控制台应用,创建并导航到项目目录,如~/projects/go-programming-cookbook,所有代码都将从此目录运行和修改。

  4. 将最新代码克隆到~/projects/go-programming-cookbook-original并(可选)从该目录工作,而不是手动键入示例:

$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original

与 concur 一起使用服务发现

当对应用使用微服务方法时,最终会有大量服务器侦听各种 IP、域和端口。这些 IP 地址因环境而异(分段与生产),在服务之间进行配置时,保持它们的静态可能会很困难。您还想知道机器或服务何时因网络分区而停机或无法访问。当网络的两个部分无法相互连接时,就会发生网络分区。例如,如果两个数据中心之间的交换机发生故障,则一个数据中心内的服务无法到达另一个数据中心内的服务。Concur 是一个提供很多功能的工具,但在这里,我们将探讨如何向 Concur 注册服务并从其他服务查询服务。

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter11/discovery的新目录并导航到它。
  2. 运行以下命令:
$ 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    
  1. ~/projects/go-programming-cookbook-original/chapter11/discovery复制测试,或者以此为机会编写一些自己的代码!
  2. 创建一个名为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
        }
  1. 创建一个名为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)
        }
  1. 创建一个名为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
}
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个名为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)
            }
        }
  1. 使用consul agent -dev -node=localhost命令在单独的终端中启动 CONSOR。
  2. 运行go run main.go命令。
  3. 您还可以运行以下命令:
$ 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}
  1. go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

concur 提供了一个健壮的 Go-API 库。当你第一次开始的时候,它会让你感到害怕,但是这个食谱展示了你如何包装它。进一步配置 CONSOR 超出了本配方的范围;这显示了在给定密钥和标记时注册服务和查询其他服务的基础知识。

可以使用它在启动时注册新的微服务,查询所有依赖的服务,并在关闭时取消注册。您可能还希望缓存这些信息,这样您就不会对每个请求都使用 concur,但是这个方法提供了一些基本工具,您可以在此基础上进行扩展。领事代理人还快速有效地提出这些重复请求(https://www.consul.io/intro/getting-started/agent.html )。一旦你成功了

利用 Raft 实现基本共识

Raft 是一种一致性算法。它允许分布式系统保持共享和管理状态(https://raft.github.io/ )。建立一个筏式系统在很多方面都很复杂——首先,你需要达成共识,选举才能发生和成功。当您使用多个节点时,这可能很难引导,并且很难开始。可以在单个节点/引线上运行基本群集。但是,如果需要冗余,则至少需要三个节点来防止单个节点发生故障时的数据丢失。此概念称为仲裁,在仲裁中,您必须维护(n/2)+1 个可用节点,以确保新日志可以提交到 Raft 集群。基本上,如果您能够保持仲裁,集群将保持健康和可用。

这个方法实现了一个基本的内存 Raft 集群,构造了一个可以在某些允许的状态之间转换的状态机,并将分布式状态机连接到一个可以触发转换的 web 处理程序。这在实现 Raft 所需的基本有限状态机接口或测试时非常有用。本食谱使用https://github.com/hashicorp/raft 用于基础筏的实施。

怎么做。。。

以下步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter11/consensus的新目录并导航到它。
  2. 运行以下命令:
$ 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    
  1. ~/projects/go-programming-cookbook-original/chapter11/consensus复制测试,或者以此为机会编写一些自己的代码!
  2. 创建一个名为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
            }
        }
  1. 创建一个名为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
}
  1. 创建一个名为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
  }
}
  1. 创建一个名为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
        }
  1. 创建一个名为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
  }
}
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个名为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)
        }
  1. 运行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}
  1. 在单独的终端中,运行以下命令:
$ 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
  1. go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

当应用启动时,我们初始化多个 Raft 对象。每个人都有自己的地址和交通工具。InmemTransport{}功能还提供了连接其他传输的方法,称为Connect()。一旦这些连接建立,筏群将举行选举。在 Raft 集群中通信时,客户端必须与领导者通信。在我们的例子中,一个处理程序可以与所有节点通信,因此处理程序负责拥有Raft领导者的call Apply()对象。这反过来在所有其他节点上运行apply()

InmemTransport{}函数允许所有内容驻留在内存中,从而简化了选择和引导过程。实际上,除了测试和概念验证之外,这并没有多大帮助,因为 Goroutines 可以自由访问共享内存。更注重生产的实现将使用 HTTP 传输之类的东西,以便服务实例可以跨机器通信。这可能需要一些额外的簿记或服务发现,因为服务实例必须侦听和服务,并且能够发现和建立彼此之间的连接。

与 Docker 一起使用集装箱化

Docker 是一种用于包装和装运应用的容器技术。其他优势还包括可移植性,因为无论主机操作系统如何,容器都将以相同的方式运行。它提供了虚拟机的许多优点,但是在一个更轻量级的容器中。可以限制单个容器和沙箱环境的资源消耗。在本地为您的应用提供一个公共环境以及将代码交付生产时,这可能非常有用。Docker 是用 Go 编写的,并且是开源的,所以很容易利用客户端和库。此配方将为基本 Go 应用设置 Docker 容器,存储有关容器的一些版本信息,并演示如何从 Docker 端点命中处理程序。

准备

根据以下步骤配置您的环境:

  1. 有关配置环境的步骤,请参阅本章中的技术要求部分。
  2. 安装 Dockerhttps://docs.docker.com/install 。这也包括 Docker Compose。

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter11/docker的新目录并导航到它。
  2. 运行以下命令:
$ 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    
  1. ~/projects/go-programming-cookbook-original/chapter11/docker复制测试,或者以此为机会编写一些自己的代码!
  2. 创建一个名为dockerfile的文件,其内容如下:
        FROM alpine

        ADD ./example/example /example
        EXPOSE 8000
        ENTRYPOINT /example 
  1. 创建一个名为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 
  1. 创建一个名为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)
            }
        }
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个包含以下内容的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))
        }
  1. 导航回起始目录。
  2. 运行以下命令:
$ 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
  1. 运行以下命令:
$ 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
  1. go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行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 编程语言编写的容器编排框架。

怎么做。。。

以下步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter11/orchestrate的新目录并导航到它。
  2. 运行以下命令:
$ 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    
  1. ~/projects/go-programming-cookbook-original/chapter11/orchestrate复制测试,或者以此为机会编写一些自己的代码!
  2. 创建一个名为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
  1. 创建一个名为docker-compose.yml的文件,其内容如下:
        version: '2'
        services:
         app:
         build: .
         mongodb:
         image: "mongo:latest"
  1. 创建一个名为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
}
  1. 创建一个名为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
}
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个包含以下内容的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)
  }
}
  1. 导航回起始目录。
  2. 运行go mod vendor命令。
  3. 运行docker-compose up -d命令。
  4. 运行docker logs orchestrate_app_1命令。您现在应该看到以下输出:
$ docker logs orchestrate_app_1
State: docker.State{Name:"Washington", Population:7062000}
  1. go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

这种配置有利于本地开发。运行docker-compose up命令后,重建本地目录,Docker 使用最新版本建立到 MongoDB 实例的连接,并开始对其进行操作。此配方使用 go mod 供应商进行依赖关系管理。因此,我们禁用了go mod cache并告诉go build命令使用我们创建的供应商目录。

这可以在启动需要连接到外部服务的应用时提供良好的基线;第 6 章关于数据库和存储中的所有配方都可以使用这种方法,而不是创建数据库的本地实例。对于生产环境,您可能不希望在 Docker 容器后面运行数据存储,但通常也会有用于配置的静态主机名。

监控应用

有多种方法可以监视 Go 应用。最简单的方法之一是设置 Prometheus,一个用 Go(编写的监控应用 https://prometheus.io )。这是一个应用,它根据您的配置文件轮询端点,并收集有关应用的大量信息,包括 goroutine 的数量、内存使用情况等。此应用将使用上一个配方中的技术来设置 Docker 环境来托管普罗米修斯并连接到它。

怎么做。。。

以下步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter11/monitoring的新目录并导航到它。
  2. 运行以下命令:
$ 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    
  1. ~/projects/go-programming-cookbook-original/chapter11/monitoring复制测试,或者以此为机会编写一些自己的代码!
  2. 创建一个名为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
  1. 创建一个名为docker-compose.yml的文件,其内容如下:
        version: '2'
        services:
         app:
         build: .
         prometheus:
         ports: 
         - 9090:9090
         volumes: 
         - ./prometheus.yml:/etc/prometheus/prometheus.yml
         image: "prom/prometheus"
  1. 创建一个名为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))
        }
  1. 创建一个名为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']
  1. 运行go mod vendor命令。
  2. 运行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" 
  1. go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。
  2. 将浏览器导航至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 编写的。

准备

根据以下步骤配置您的环境:

  1. 有关配置环境的步骤,请参阅本章中的技术要求部分。
  2. 运行go get github.com/rcrowley/go-metrics命令。

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter11/metrics的新目录并导航到它。
  2. 运行以下命令:
$ 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    
  1. ~/projects/go-programming-cookbook-original/chapter11/metrics复制测试,或者以此为机会编写一些自己的代码!
  2. 创建一个名为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)
        }
  1. 创建一个名为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)
            })
        }
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个名为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))
        }
  1. 运行go run main.go。或者,也可以运行以下命令:
$ go build $ ./example

您现在应该看到以下输出:

$ go run main.go
listening on :8080
  1. 从单独的 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}}
  1. 尝试再点击所有端点几次,看看它们是如何变化的。
  2. go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。
  3. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

gometrics将您的所有指标保存在注册表中。设置后,您可以使用任何度量发射选项,例如countertimer,它将在注册表中存储此更新。有多个导出器将度量导出到第三方工具。在本例中,我们设置了一个以 JSON 格式发出所有度量的处理程序。

我们设置了三个处理程序—一个递增计数器,一个记录退出处理程序的时间,另一个打印报告(同时递增额外的计数器)。如果度量发射器当前不以线程安全的方式存在,GetOrRegister函数对于以原子方式获取或创建度量发射器非常有用。或者,您可以提前注册所有内容一次。