本文是对我的博客项目的虚拟化部署思路设计与实践的记录,希望对大家使用容器编排部署多服务的应用有所启发。
项目的地址在:
项目地址: python/fastapi + golang/gin + Vue + docker 基于异步技术栈的个人博客系统
同时对该博客项目之前有几篇思路介绍性的文章供大家参阅:
Python-FastAPI 使用asyncio生态圈开发异步博客(一)数据篇
Python-FastAPI 基于asyncio异步生态开发异步博客(二)通信逻辑篇
Python-FastAPI 异步框架开发博客系统(三)--异步特性篇
虚拟化思路
容器虚拟化编排需要考虑的问题很多,大家可以参考《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.ini
及nginx.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是一个更加全面、更加规范的工具,也是目前大型系统最流行的选择。希望本文对大家开始实践部署多服务的容器编排应用能有启发~
有疑问加站长微信联系(非本文作者)
本文来自:Segmentfault
感谢作者:LouisYZK
查看原文:docker编排部署多服务Web应用(python/fastapi, go/gin, nginx, mysql, redis)