Docker Stack 多服务

我们从简到繁看一下 Docker 的学习路线:

  • docker run:Single Engine(者称 Single-Host,单 Docker 节点)下单服务运行
  • docker-compose:Single Engine(或者称 Single-Host,单 Docker 节点)下多服务编排
  • docker swarm:Multi-Host(多 Docker 节点,集群)下单服务编排
  • docker stack:Multi-Host(多 Docker 节点,集群)下多服务编排

可以看到 docker stack 其实就是 docker-compose 多应用和 docker swarm 规模化两者的结合。

1   节点初始化

从 swarm 我们得知环境要求并不高,那么对 stack 也一样,接下来我们用三台主机进行实战部署,跟 swarm 一样对节点进行初始化,成为 swarm 集群,但不需要创建网络,因为网络创建我们通过编排文件进行:

姓名        地区                        内部 IP
binke01    asia-northeast1-a           10.146.0.2 (nic0)   
binke01-1  asia-northeast1-a           10.146.0.3 (nic0)   
binke01-3  asia-northeast1-a           10.146.0.5 (nic0)   
> docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
0kjqucshibpm35zhq7kizldp0     binke01             Ready               Active                                  18.09.5
qr7i763tagufpcyn4qf37b5fs *   binke01-1           Ready               Active              Leader              18.09.5
ogvwkwq0zxw3s05shey2ruzqa     binke01-3           Ready               Active                                  18.09.5

2   应用容器化

我们先来看一下容器化上下文:

> tree multigo
multigo
├── config.yaml
├── Dockerfile
├── docker-stack.yml
├── main.go
└── service
    ├── config.go
    └── redis.go

2.1   业务代码

我们继续使用计数器,需要两个应用,分别是Go web 服务器和 redis 记录数据应用,我们采用的代码和我们在用 Docker Compose 部署的多应用代码几乎一样:

cat <<EOF > $GOPATH/github.com/wpxun/multigo/main.go
package main

import (
  "fmt"
  "github.com/wpxun/multigo/service"
  "net/http"
  "os"
  "strconv"
)

func IndexHandler(w http.ResponseWriter, r *http.Request) {
  redis := service.GetRedis()
  val, err := redis.Incr("count").Result()
  if err != nil {
    panic(err)
  }
  host, _ := os.Hostname()
  fmt.Fprintln(w, "hello world "+ host +", visitors = " + strconv.FormatInt(val, 10) )
}

func main()  {
  http.Handle("/pattern", http.HandlerFunc(IndexHandler))
  http.ListenAndServe(":80", nil)
}
EOF
cat <<EOF > $GOPATH/github.com/wpxun/multigo/config.yaml
Redis:
  DialTimeout: 2000000000 #连接超时设定(s),默认200ms
  Network: tcp #网络连接协议
  Address: redis:6379 #连接地址(带端口)
  Password:  #密码
  Database: 0 #数据库,默认0
EOF
cat <<EOF > $GOPATH/github.com/wpxun/multigo/service/redis.go
package service

import (
    "github.com/go-redis/redis"
)

func GetRedis() *redis.Client {
    return redis.NewClient(&redis.Options {
        Addr:         Conf.Redis.Address,
        Password:     Conf.Redis.Password,
        DB:           Conf.Redis.Database,
        Network:      Conf.Redis.Network,
        DialTimeout:  Conf.Redis.DialTimeout,
    })
}
EOF
cat <<EOF > $GOPATH/github.com/wpxun/multigo/service/config.go
package service

import (
    "fmt"
    "gopkg.in/yaml.v2"
    "io/ioutil"
    "time"
)

type confstruct struct {
    Redis struct {
        Address     string          `yaml:"Address"`
        Database    int             `yaml:"Database"`
        DialTimeout time.Duration   `yaml:"DialTimeout"`
        Network     string          `yaml:"Network"`
        Password    string          `yaml:"Password"`
    } `yaml:"Redis"`
}

var Conf confstruct

func init() {
    GetYaml("config", &Conf)
}

func GetYaml(filename string, out interface{}) {
    yamlFile, err := ioutil.ReadFile(fmt.Sprintf("%s.yaml", filename))
    if err != nil {
        fmt.Println("Read config file error:", err.Error())
    }

    err = yaml.Unmarshal(yamlFile, out)

    if (err != nil) {
        fmt.Println("Unmarshal config file error:", err.Error())
    }
}

EOF

2.2   容器化

编写 Dockerfile,采用多阶段构建方式,使得镜像只有 12.9MB。另外 stack 要求提前构建好并推送到创建,也就是 docker-stack.yml 不能用在运行的时候才 build 镜像,原因是多节点部署中,其它节点并没有构建上下文。另外 redis 我们使用官方的 redis 镜像。

# 多阶段构建
# 第一阶段,391MB,编译前准备:go 和 git 工具、代码依赖库
FROM golang:1.12.4-alpine3.9  AS front
RUN set -xe && \
    apk add git && \
    go get -v github.com/go-redis/redis && \
    go get -v gopkg.in/yaml.v2

# 分成两次 RUN 目的是可复用上面的缓存,编译 go 代码
COPY . /go/src/github.com/wpxun/multigo
RUN set -xe && \
    go install github.com/wpxun/multigo


# 第二阶段,14.6MB;仅仅复制了可执行程序和程序的配置文件
FROM alpine:3.9
ENV GOM_VERSION   1904.1
COPY --from=front /go/bin /go/src/github.com/wpxun/multigo/config.yaml /go/bin/
EXPOSE 80
WORKDIR /go/bin
CMD ["/go/bin/multigo"]

3   分析 Stack 文件

Stack 一直是期望的 Compose——完全集成到 Docker 中,并能管理应用的整个生命周期。

cat <<EOF > $GOPATH/github.com/wpxun/multigo/docker-stack.yml
version: "3.7"
services:
  goweb:
    image: "wpxun/multigo:v1"
    ports:
      - target: 80
        published: 80
    networks:
      - counter-net
    deploy:
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
      replicas: 8
      update_config:
        parallelism: 2
        failure_action: rollback

  redis:
    image: "redis:5.0.4-alpine3.9"
    networks:
      - counter-net
    deploy:
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
      placement:
        constraints:
          - 'node.role == worker'

networks:
  counter-net:

volumes:
  counter-vol:
EOF

在该文件整体结构中,定义了 4 种顶级关键字:

  • version: 其要求的 version ≥ 3.0
  • services: 定义了两个服务,这部分也是核心内容,接下来会讲解
  • networks: 创建一个网络,驱动为默认。stack 编排文件的默认驱动是 overlay(swarm),而 compose 编排文件的默认驱动是 bridge(local)。
  • volumes: 创建一个卷

4   部署应用

docker stack deploy -c docker-stack.yml multigo

> docker stack ls
NAME                SERVICES            ORCHESTRATOR
multigo             2                   Swarm

> docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE                   PORTS
8vcqoh0yry5v        multigo_goweb       replicated          8/8                 wpxun/multigo:v1        *:80->80/tcp
cygtoce4mfkt        multigo_redis       replicated          1/1                 redis:5.0.4-alpine3.9

5   管理应用

部署成功之后,所有的 node 节点的 IP 都可以访问到服务,而非仅仅 Leader 节点。

6   删除 Stack

docker stack rm,一定要谨慎,删除 Stack 不会进行二次确认,服务和网络会删除,但卷不会删除,这是因为卷的设计初衷是保存持久化数据,其生命周期独立于容器、服务以及 Stack 之外。

参考文献 [1] Nigel Poulton. 深入浅出 Dokcer. 版次:2019年4月第1版 [2] Docker Swarm or Kubernetes — Help me decide. https://stackshare.io/stackups/docker-swarm-vs-kubernetes