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