目前 Frodo 涉及到的基础设施有 mysql, nginx, redis,服务模块有两个 python/fastapi 和 golang/gin。 如果本地部署的话需要配置的环境有点多,虚拟化(以 docker 容器形式)是目前(最)流行的部署方法。加上众多编排工具可选择,非常适合 Frodo 目前多服务的形式。本文先介绍 Frodo-v2.0 的 docker 部署方法,再以此为例,谈谈 docker-compose 编排多服务应用的特点和需要注意的地方。希望本文对大家开始实践部署多服务的容器编排应用能有启发~

Docker 部署 (推荐)

请确保本地 docker 及 docker-compose 可用,以下在docker-compose-1.25.5, MacOS-Majave 15.15版本测试通过。

修改配置文件

需要修改的文件为python_web/config/config.ini.modelgoadmin/config.ini.model,

[global]
debug = True
author = yzk
site_title = Zhikai-Yang Space
host_path = 202.117.47.47:9080 ## 将此配置改为你的环境入口地址,例如本机为localhost

剩余的配置,例如各个服务的端口号,如若出现本地占用的情况,情修改nginx.conf, docker-compose.yml相应的地方。

build 镜像

git clone https://github.com/LouisYZK/Frodo
cd Frodo
sh build.sh

此过程可能需要等待一定时间,过程中需要完成每层镜像的完整构建,如若出现某层因连接超时错误,请检查网络情况或重试。

启动

docker-compose up ## 启动

该命令正常会启动 5 个 server, 当 5 个 server 均正常运行才可正常使用,可使用命令docker-compose ps查看是否正常启动,如果存在退出Exited状态的容器,请仔细查看输出的日志和退出的原因。你也可以使用docker ps -a查看启动或退出的容器,使用docker logs <container_id>查看日志。

使用

启动后,请先创建一个管理员用户:

docker exec -it $(docker ps | grep frodo/pyweb | awk '{print $1}') python manager.py adduser

随后根据提示输入账密即可看到创建成功的提示。

接下来请访问<host_ip>:<nginx_server_port>/admin, 例如本机环境访问localhost:9080/admin, 用你刚才创建的账密登录,即可进入后台。

进入后台请按照 UI 提示创建几篇文章 (post), 而后访问前台localhost:9080观察是否生效。

用户侧边栏个性化配置在python_web/config.yaml不必重启docker-compose即可生效。

环境测试

理论上 docker 与部署环境无依赖关系。目前已经测试的环境有:

  • [x] Ubuntu LTS 16.06
  • [x] MacOS Majave 15
  • [ ] Windows ...
  • [ ] Rowsberry ARM ...

欢迎大家部署测试~

虚拟化思路

容器虚拟化编排需要考虑的问题很多,大家可以参考《Kubernetes in Action》这本书看看最主流的kubernetes是如何流程化讲述容器编排问题的。需要考虑的基本上 配置, 网络(通信), 存储是主要方面。而本次使用docker-compose进行编排的也是主要解决这三部分的问题。

容器: 是指使用了 linux-namespace, cgroups, AUFS, 虚拟网络等技术实现的独立隔离运行环境。与虚拟机相同的效果,但体积更轻量,部署更方便。docker 是目前主流的容器工具之一

容器编排: 是指在集群上调度容器生命周期的工具。负责所有容器的网络、存储、配置、通信、资源分配、节点分配、安全机制等的总编排。kubernetes (k8s) 是最就行的容器编排工具。docker-compose 是 docker 自身配套的简易编排工具,适用于小型项目和测试环境。

服务编排设计

第一步是考虑服务的拆分,云服务的时代提倡我们的服务不能再过于耦合,应尽量做到轻量化,一个服务专门做一件事。Frodo 的服务大致分为以下 5 个部分:

  • nginx: 总反向代理,负责 api 转发与静态文件转发。
  • mysql: 持久化数据
  • redis: 缓存与部分持久化
  • python_web: 使用 fastapi 实现的前台 API,返回的主要是 html (template)
  • golang_web: 使用 Gin 实现的后台 API,主要负责内容管理

划分完服务之后,就要考虑他们之间的关系,可以从配置、网络(通信)和存储展开。思考清楚他们的依赖关系对于编排文件的正确性十分重要。但在写编排文件之前,我们需要把各个服务模块的镜像(images)搞定,他们是服务(容器)启动的根本。

前三个工具的镜像可以从各个 hub 中获取,后面两个的镜像将在下节介绍制作的细节。

用户服务 Dockerfile

Dockerfile的语法可以从 docker 的官网上找到细节的指导,往往不可能一次写对,一般的过程是:写 dockerfile-> 测试构建 -> 构建成功 -> 尝试启动容器 -> 启动失败 -> 返回修改 Dockerfile. Dockerfile 写的如何也直接影响到了镜像的质量(稳定性和体积等)。

先来看 python 服务的镜像:

## 使用fastapi团队提供的python镜像环境,利于直接解决底层依赖
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 AS builds 

## pip 安装依赖库,可使用加速源
WORKDIR /install
COPY requirements.txt /requirements.txt
RUN pip install -r /requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple \
    && mkdir -p /install/lib/python3.7/site-packages \
    && cp -rp /usr/local/lib/python3.7/site-packages /install/lib/python3.7

## 多阶段构建,利于减轻镜像体积
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
COPY --from=builds /install/lib /usr/local/lib
WORKDIR /app
COPY . /app

需要解释的地方,tiangolo/uvicorn-gunicorn-fastapi:python3.7这个镜像是参考 fastapi 团队提供的全栈 web 模板镜像,项目地址在 tiangolo/full-stack-fastapi-postgresql . 拉取他们镜像的好处在于,此镜像本身解决了很多依赖问题,例如有些 pip 库可能需要安装 gcc 等依赖,直接使用 python 裸机镜像你需要大量时间 debug。 其次多阶段构建请参考附录文章,他的主要作用是减少构建时间、减小镜像体积,解决异构构建等 非常有用。

再来看 golang 的镜像

FROM golang:alpine as builder
## 设置环境变量 使用代理加速下载和MODULE模式
ENV GOPROXY="https://goproxy.io"
ENV GO111MODULE="on"
WORKDIR /app
COPY . /app
RUN go build -o admin ./admin.go

## 仍然使用多阶段构建
From alpine:latest

WORKDIR /root/
COPY --from=builder /app .
ENTRYPOINT ["./admin"]

golang 的镜像是我调时间最久的,也让我获得了一个奇怪的知识,同为编译型语言,golang 和 c++ 的编译与连接区别太大了,因此倒数第二行我们不能直接把编译好的文件拿来运行,需要拷贝他依赖的配置文件。(PS: 这一点后续得深入研究下 golang 的编译机制)

配置、网络与存储关系

做好镜像并测试无误后,可以写编排文件docker-compose.yml了,官网上的文档十分详细,具体到 Frodo 来讲,重要的是理清楚 5 部分的依赖关系。从配置、网络和存储来考虑:

上图中展示了网络通信结构,需要解释的是:

  • 容器桥接网络是指容器间互相使用内部端口和 host 通信,互相之间可见。与外界的通信依靠端口节点最终汇总到主机的 eth0 网络设备转发。这里使用 Nginx 充当了与外界通信的唯一入口。 桥接网络只是一种选择,也可以选择其他形式的网络的拓扑。

  • Volume 挂载,挂载是容器经常使用的特性,可以映射容器数据到主机,这里 mysql 和 redis 的数据就和宿主机上的某个数据卷相互映射,这样即使容器消失了数据也存在。

  • Static 静态文件抽离单独由 nginx 代理,这就需要在 python_web 和 golang_web 的配置文件中做出修改。

那么上述关系是如何实现的呢?主要依靠各个服务的配置 (python_web、goadmin 的config.model.ininginx.conf) 以及docker-compose.yml的配置。

先看docker-compose.yml

version: '3'
services:
  db:
    image: mysql
    restart: always
    environment:
      MYSQL_DATABASE: 'fast_blog'
      MYSQL_USER: 'root'
      MYSQL_PASSWORD: ''
      MYSQL_ROOT_PASSWORD: ''
      MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
    ports:
      - '3308:3306'
    volumes:
      - my-datavolume:/var/lib/mysql
    networks:
      - app-network

  redis:
    image: redis:alpine
    networks:
      - app-network
    ports:
      - '6378:6379'
  frodo_python:
    image: frodo/pyweb:latest
    networks:
      - app-network
    ports:
      - '9004:9004'
    expose:
      - '9004'
    volumes:
    ## 为了方便调试,生产环境可删除
      - ./python_web:/app
    depends_on:
      - db
      - redis
    environment:
      PYTHONPATH: $PYTHONPATH:/usr/local/src
    command: 'uvicorn main:app --host 0.0.0.0 --port 9004'

  frodo_golang:
    image: frodo/goweb
    ports:
      - '9003:9003'
    expose:
      - '9003'
    depends_on:
      - db
      - redis
    working_dir: /root
    command: sh -c './admin'
    networks:
      - app-network
  nginx:
    image: nginx
    working_dir: /data/static
    volumes:
    ## 映射配置文件和静态文件
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./static:/data/static
    ports:
      - "9080:9080"
    networks:
      - app-network
    depends_on:
      - frodo_python
      - frodo_golang
volumes:
  my-datavolume:
networks:
  app-network:
    driver: bridge

需要解释的有很多,除了注释的,最为重要的是[depends]这一配置,他规定了依赖,体现在各个服务的启动顺序上,nginx 的配置文件中需要分发服务至 python 与 golang, 而他们的初始化依赖于 mysql 和 redis 的服务地址,所以启动顺序十分重要。

再来看服务应用的配置,python 和 golang 的差别不大:

[global]
host_path = localhost

[database]
host = db
username = root
password = 
port = 3306
db = fast_blog
charset = utf8

[redis]
host = redis
redis_url = redis:6379
port = 6379

[port]
golang = 9003
fastapi = 9004

[server]
python = frodo_python
golang = frodo_golang

需要解释如下:

  • redis 和 mysql 的 host 使用了在 docker-compose.yml 中规定的 host 服务名。这点可以使用 docker-compose ps --service 查看,必须用此 host 才能发现彼此。

  • 端口号均使用容器桥接网络内部端口号

  • python 的 golang 的服务地址也相应地变化

最后是 nginx 的配置文件,他决定了转发服务的地址:

server {
        listen       9080;
    location  / {
        proxy_pass http://frodo_python:9004;
    }
    location /static {
        root /data;
    }

    location /api {
        proxy_pass http://frodo_golang:9003;
    }
    location /auth {
        proxy_pass http://frodo_golang:9003;
    }

    location /api/status {
        proxy_pass http://frodo_python:9004;
    }
   }

我们的应用就以9080为唯一入口进行访问,注意到 nginx 分发的地址都是容器桥接网络中的服务名,端口也是容器端口,因此 nginx 必须要在 5 个服务的最后启动才能找到所有的服务,不然启动会报错。

当你看到上图时,证明服务都已经启动,不过者不代表通信、存储和配置都已经完全正确,debug 的路程还很长,有时甚至要到个别容器内部查看原因。或者修改源码得到更多的日志。

最后需要注意的是,docker-compose 最好只用来测试,kubernetes 是一个更加全面、更加规范的工具,也是目前大型系统最流行的选择。希望本文对大家开始实践部署多服务的容器编排应用能有启发~