跨云服务商&网络Docker Swarm集群搭建
in Linux with 2 comments

跨云服务商&网络Docker Swarm集群搭建

in Linux with 2 comments

前言

由于隔一段时间各个云服务商都会搞活动,然后就会剁手入一个,手上已经有4个云服务器了。

然后家里有用 PVE 搞了个虚拟机化,来运行软路由,NAS之类的家庭服务。由于有高配强迫症,组了台16核32线程的服务器,导致性能严重过剩,就琢磨着能不能和云服务器组网,来组建一个小集群

最终选定方案是用 zerotier 搭建VPN组内网,docker swarm 来组建集群,基于此安装管理面板,以及https 证书,网关服务日志记录搜索之类的,当然还有服务滚动更新,期间遇到一些坑,记录一下

Zerotier 搭建内网

首先请去 zerotier 组成账号,以及创建一个网络,这里网上教程很多,搜一下就有了。我给个简单的安装以及加入网络的代码。

# 安装
curl -s https://install.zerotier.com | sudo bash
# 加入 zerotier 后台自己创建的网络
sudo zerotier-cli join xxx

安装 Docker 并配置加速镜像源

可以按照 腾讯云 的文档,来配置,这里就不赘述了

https://cloud.tencent.com/document/product/1207/45596?from=information.detail.腾讯云加速docker

初始化集群管理节点&加入Worker

初始化集群 Manager

注意把192.168.xxx.xx 替换成你自己 zerotier 后台中的ip

sudo docker swarm init --advertise-addr=192.168.xxx.xx:2377 --data-path-addr=192.168.xxx.xx --data-path-port 5789

可以注意到我指定了--data-path-addr=192.168.xxx.xx --data-path-port 5789

这是因为云服务的网络也是基于 vxlan, 占用了docker默认的4789端口,导致如果不指定端口,会导致集群虽然能组建成功,但是docker容器之间的网络不通。如加入了同一个network,node1中的容器,ping 不通 node2中的容器,这就失去了组建集群的意义了。

这是需要特别注意,踩了好久最后通过搜索才发现,我一度以为是不是这是厂商为了卖自己的集群服务,禁止了用户自建的可能。来源可以参考

加入Worker

在其他服务器中运行,加入到集群当中

# manager节点中运行,获取加入集群的命令
sudo docker swarm join-token worker

# 在 manager 以外的节点中运行,加入到集群当中
sudo docker swarm join --token xxx 192.168.xxx.xx:2377 

在 manager 节点运行 sudo docker node ls 查看加入的node状态

Node 提权降权操作

我将我所有的云服务器都作为流量的出入口节点,家里虚拟机的流量将会通过域名指定的云服务器来对外开放。
我是用的是 traefik 作为网关及容器内的负载均衡, 由于treafik 需要监听docker的event事件,节点必须是manager 才能有权限,所以我将所有的云服务器都提升为 manager

# 将worker节点升级为manager节点
sudo docker node promote swarm-node1
 
# 将manager节点降级为worker节点
sudo docker node demote swarm-node1

创建 Swarm 网络

所有需要跨 Node 通信的容器,都需要加入该网络

# 创建一个名为 proxy 的网络
sudo docker network create -d overlay --attachable proxy

测试集群容器网络是否互通

# 在所有 Node 中都起一个容器
sudo docker service create --mode global --network proxy --name web srampal/nginx-netutils:2

# 在任意节点中获取到nginx-netutils容器的ip
sudo docker network inspect proxy
"Containers": {
  "39a532786c2c23a1033f7899afe0973bdac9100191b2077306477129f78eafe4": {
    "Name": "nginx-netutils.1.atc36jt29aidgbtgqx95hfefu",
    "EndpointID": "8368996ff2921687ec57ce51412a987c95390b5cb9bd757c6094a74e48ca6640",
    "MacAddress": "02:42:0a:00:01:68",
    "IPv4Address": "10.0.1.104/24",
    "IPv6Address": ""
  }
}

# 在其他节点的容器中ping上面的ip,检测网络是否通
sudo docker exec xxxId ping 10.0.1.104

Traefik网关及负载均衡

由于配置过多,我这里直接贴上我现在的配置+注释,这是Treafik 的后台面板

image_2.png

version: '3.4'

services:
  proxy:
    image: traefik:v2.4
    environment:
      - TZ=Asia/Shanghai
      # 用于acme.sh获取https证书
      - ALICLOUD_ACCESS_KEY=xxx
      - ALICLOUD_SECRET_KEY=xxx
    command:
      # 开启监听 Docker 事件
      - '--providers.docker.endpoint=unix:///var/run/docker.sock'
      # 开启集群模式
      - '--providers.docker.swarmMode=true'
      # 忽略没有 traefik.enable=true 标签的容器
      - '--providers.docker.exposedbydefault=false'
      # 使用 proxy 网络,proxy为上面创建的 swarm overlay 网络
      - '--providers.docker.network=proxy'
      # 定一个一个名为 http 的入口,端口为80
      - '--entrypoints.http.address=:80'
      - '--entrypoints.https.address=:443'
      # 开启 https 入口的 tls
      - '--entrypoints.https.http.tls=true'
      # 定义 mysql 的入口
      - '--entrypoints.mysql.address=:3306'
      - '--api'
      # 开启请求日志,明确不使用 UTC,采用容器时区
      - '--accesslog=true'
      - '--accesslog.fields.names.StartUTC=drop'
      # - '--accesslog.filepath=/var/log/traefik/access.log'
      
      # - '--log.level=DEBUG'
      # - '--log.filePath=/var/log/traefik/traefik.log'
      # 具体域名证书的申请,域名必须指向当前机器
      # - '--certificatesresolvers.letsencryptresolver.acme.httpchallenge=true'
      # - '--certificatesresolvers.letsencryptresolver.acme.httpchallenge.entrypoint=web'
      # 泛域名证书
      - '--certificatesresolvers.letsencryptresolver.acme.dnschallenge.provider=alidns'
      - '--certificatesresolvers.letsencryptresolver.acme.email=xxx@gmail.com'
      - '--certificatesresolvers.letsencryptresolver.acme.storage=/www/config/acme.json'
      # 使用 letsencrypt 的测试环境
      # - '--certificatesresolvers.letsencryptresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory'
    ports:
      # 为了解决流量在 node 节点中跳两次的问题
      # https://github.com/traefik/traefik/issues/1880
      - target: 80
        published: 80
        protocol: tcp
        mode: host
      - target: 443
        published: 443
        protocol: tcp
        mode: host
      - target: 3306
        published: 3306
        protocol: tcp
        mode: host
      # 这样写会导致node1入口的流量被docker负载均衡到node2,就算服务只在node1上部署
      # - 80:80
      # - 443:443
      # - 3306:3306
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      # letsencrypt-config 为远程卷,为了解决多机共享证书
      - letsencrypt-config:/www/config/:ro
      # 将本机时区映射到容器,解决日志时间错乱的问题
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    # 日志上报到 splunk
    logging: 
      driver: splunk
      options:
        splunk-token: xxxxx-xxx-xxxx-xxx-xxxxx
        splunk-url: http://192.168.xxx.xx:8088/
        splunk-format: raw
    networks:
      - proxy
    deploy:
      # 部署到所有节点当中
      mode: global
      update_config:
        # 更新时将会一个一个更新
        parallelism: 1
        # 更新失败将会回滚
        failure_action: rollback
      restart_policy:
        # 如果不是非0状态退出,这回执行重启
        condition: on-failure
        # 重启间隔时间
        delay: 5s
        # 第一次启动失败之后,继续重试3次
        max_attempts: 3
        # 检测容器是否启动成功的等待时间
        window: 120s
      placement:
        # 只在 manager 节点中部署
        constraints: [node.role == manager]
      labels:
        # 开启 traefik 监听
        - 'traefik.enable=true'
        # 定一个名为 traefik 的节点,入口为上面定义的 http 端口80
        - 'traefik.http.routers.traefik.entrypoints=http'
        # 路由到 traefik.xxx.com
        - 'traefik.http.routers.traefik.rule=Host(`traefik.xxx.com`)'
        # 定义一个名为 traefik-https-redirect 的中间件,将会吧 http 302 到 https
        - 'traefik.http.middlewares.traefik-https-redirect.redirectscheme.scheme=https'
        # 使用 traefik-https-redirect
        - 'traefik.http.routers.traefik.middlewares=traefik-https-redirect'
        # 定一个名为 traefik-secure的节点,入口为上面定义的 https 端口 443
        - 'traefik.http.routers.traefik-secure.rule=Host(`traefik.xxx.com`)'
        - 'traefik.http.routers.traefik-secure.entrypoints=https'
        # 使用内置中间件 authtraefik,访问需要账号密码
        - 'traefik.http.routers.traefik-secure.middlewares=authtraefik'
        # 下面的设置将会申请泛域名证书
        - 'traefik.http.routers.traefik-secure.tls=true'
        - 'traefik.http.routers.traefik-secure.tls.certresolver=letsencryptresolver'
        - 'traefik.http.routers.traefik-secure.tls.domains[0].main=xxx.com'
        - 'traefik.http.routers.traefik-secure.tls.domains[0].sans=*.xxx.com'
        # 使用 traefik 内置的服务
        - 'traefik.http.routers.traefik-secure.service=api@internal'
        # Swarm 模式下必须手动指定对外端口
        - 'traefik.http.services.traefik-secure.loadbalancer.server.port=80'
        # 设置 authtraefik 中间件密码,所有的单个 $ 需要替换为 $$ ,生成密码 echo $(htpasswd -nb user yourpassword) | sed -e s/\\$/\\$\\$/g
        - 'traefik.http.middlewares.authtraefik.basicauth.users=user:&&xxxxx&&xxxx'

# 使用外部手动创建的 proxy 网络
networks:
  proxy:
    external: true

volumes:
  # sudo docker plugin install vieux/sshfs 安装。注意,所有node都要执行安装
  # 在集群中共享数据,比如证书
  letsencrypt-config:
    driver: vieux/sshfs:latest
    driver_opts:
      sshcmd: 'ubuntu@192.168.xxx.xxx:/home/'
      password: 'xxxx'

部署一个服务

version: '3.4'

services:
  helloworld:
    image: traefik/whoami
    networks:
      - proxy
    deploy:
      labels:
        - 'traefik.enable=true'
        - 'traefik.http.routers.helloworld.entrypoints=http'
        - 'traefik.http.routers.helloworld.rule=Host(`helloworld.xxx.top`)'
        - 'traefik.http.middlewares.helloworld-https-redirect.redirectscheme.scheme=https'
        - 'traefik.http.routers.helloworld.middlewares=helloworld-https-redirect'
        - 'traefik.http.routers.helloworld-secure.entrypoints=https'
        - 'traefik.http.routers.helloworld-secure.rule=Host(`helloworld.xxx.top`)'
        - 'traefik.http.routers.helloworld-secure.tls=true'
        - 'traefik.http.routers.helloworld-secure.service=helloworld'
        # 注意,Swarm 模式下必须手动指定对外端口
        - 'traefik.http.services.helloworld.loadbalancer.server.port=80'
networks:
  proxy:
    external: true

滚动更新、回滚、重启策略,及资源限制

  appserver
    image: juzisang/xxx
    networks:
      - proxy
    deploy:
      # 生成的副本数量
      replicas: 2
      # 升级时的配置
      update_config:
        # 每次更新两个
        parallelism: 2
        # 每组更新的间隔时间
        delay: 10s
        # 升级失败则回滚 pause rollback continue ,默认 pause 
        failure_action: rollback
      resources:
        # 限制内存最高占用1024M,单核cpu的50%
        limits:
          cpus: '0.50'
          memory: 1024M
        # 最低保留512M内存,单核0.25
        reservations:
          cpus: '0.25'
          memory: 512M
      placement:
        constraints:
          # 部署到管理机
          - 'node.role == worker'
          # 部署到对应标签的
          - 'node.labels.role==node1' 
      # 容器异常退出之后的重启策略
      restart_policy:
        # 以非0返回值退出
        condition: on-failure
        # 间隔 5s 重启
        delay: 5s
        # 重试3次
        max_attempts: 3
        # 等待至多120s来检测是否启动成功
        window: 120s

安装 swarmpit 面板

swarmpit 可以用于监控集群状态,操纵节点回滚,升级,已经查看日志等操作

image_3.png

这是我的配置,也是基于官方 docker-compose.yml 基础,加上了 traefik 的配置

version: '3.3'

services:
  app:
    image: swarmpit/swarmpit:latest
    environment:
      - TZ=Asia/Shanghai
      - SWARMPIT_DB=http://db:5984
      - SWARMPIT_INFLUXDB=http://influxdb:8086
    volumes:
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:8080']
      interval: 60s
      timeout: 10s
      retries: 3
    networks:
      - proxy
    deploy:
      resources:
        limits:
          cpus: '0.50'
          memory: 1024M
        reservations:
          cpus: '0.25'
          memory: 512M
      placement:
        constraints: 
          - node.labels.role==node2
      labels:
        - 'traefik.enable=true'
        - 'traefik.http.routers.swarmpit.entrypoints=http'
        - 'traefik.http.routers.swarmpit.rule=Host(`swarmpit.xxx.com`)'
        - 'traefik.http.middlewares.swarmpit-https-redirect.redirectscheme.scheme=https'
        - 'traefik.http.routers.swarmpit.middlewares=swarmpit-https-redirect'
        - 'traefik.http.routers.swarmpit-secure.entrypoints=https'
        - 'traefik.http.routers.swarmpit-secure.rule=Host(`swarmpit.xxx.com`)'
        - 'traefik.http.routers.swarmpit-secure.tls=true'
        - 'traefik.http.routers.swarmpit-secure.service=swarmpit'
        - 'traefik.http.services.swarmpit.loadbalancer.server.port=8080'

  db:
    image: couchdb:2.3.0
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
      - db-data:/opt/couchdb/data
    networks:
      - proxy
    deploy:
      placement:
        constraints: 
          - node.labels.role==node2
      resources:
        limits:
          cpus: '0.30'
          memory: 256M
        reservations:
          cpus: '0.15'
          memory: 128M

  influxdb:
    image: influxdb:1.7
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
      - influx-data:/var/lib/influxdb
    networks:
      - proxy
    deploy:
      placement:
        constraints: 
          - node.labels.role==node2
      resources:
        limits:
          cpus: '0.60'
          memory: 512M
        reservations:
          cpus: '0.30'
          memory: 128M

  agent:
    image: swarmpit/agent:latest
    environment:
      - TZ=Asia/Shanghai
      - DOCKER_API_VERSION=1.35
    volumes:
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - proxy
    deploy:
      mode: global
      labels:
        swarmpit.agent: 'true'
      resources:
        limits:
          cpus: '0.10'
          memory: 64M
        reservations:
          cpus: '0.05'
          memory: 32M

networks:
  proxy:
    external: true

volumes:
  db-data:
    driver: local
  influx-data:
    driver: local

安装 splunk

version: '3.4'

services:
  splunk:
    image: splunk/splunk:latest
    networks:
      - proxy
    environment:
      - TZ=Asia/Shanghai
      - SPLUNK_START_ARGS=--accept-license
      - SPLUNK_PASSWORD=xxxx
      # - SPLUNK_UPGRADE=true
    volumes:
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
      # 导出配置,防止重启丢配置
      - splunk-var:/opt/splunk/var
      - splunk-etc:/opt/splunk/etc
    ports:
      # 用于外部服务上传日志
      - target: 8088
        published: 8088
        protocol: tcp
        mode: host
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
      placement:
        constraints: 
          - node.labels.role==node3
      labels:
        - 'traefik.enable=true'
        - 'traefik.http.routers.splunk.entrypoints=http'
        - 'traefik.http.routers.splunk.rule=Host(`splunk.xxx.com`)'
        - 'traefik.http.middlewares.splunk-https-redirect.redirectscheme.scheme=https'
        - 'traefik.http.routers.splunk.middlewares=splunk-https-redirect'
        - 'traefik.http.routers.splunk-secure.entrypoints=https'
        - 'traefik.http.routers.splunk-secure.rule=Host(`splunk.xxx.com`)'
        - 'traefik.http.routers.splunk-secure.tls=true'
        - 'traefik.http.routers.splunk-secure.service=splunk'
        - 'traefik.http.services.splunk.loadbalancer.server.port=8000'
networks:
  proxy:
    external: true

volumes:
  splunk-var:
    driver: local
  splunk-etc:
    driver: local

启动

sudo docker stack deploy -c proxy-compose.yml proxy
sudo docker stack deploy -c splunk-compose.yml splunk 
sudo docker stack deploy -c swarmpit-compose.yml swarmpit 

补充

Responses
  1. sbilly

    你怎么知道 splunk 的 IP 地址呢?docker 里面应该是随机分配吧。

    Reply
    1. @sbilly

      用的 Zerotier 分配的ip,端口直接映射到 splunk 部署的机器上。

      Reply