此文章分为 镜像篇、容器篇、生产调试 三部分,适合对容器化技术和生产环境部署有一定经验的同学。 都是干货,希望对阅读本文的你有所帮助。
背景
公司项目需要上线阿里专有云生态,部署在Alibaba Cloud Linux 操作系统平台的 Docker 容器中。 这个项目原生产环境部署十分复杂,存在近10个产品服务组件,部署过程脚本有30余个。 项目容器化时,遇到了非常多的问题,本文会记录解决问题的思路与方案。 本文为镜像篇,大致内容如下:
- 如何选择基础镜像?
- 镜像构建
- 如何优化产出镜像的体积?
如何选择基础镜像?
基础镜像是个人定制镜像构建的最上层镜像,一般包含一个最小化的操作系统和一些基础包。我们需要以基础镜像为根基,搭建项目的整体运行环境。 选择基础镜像时应最少考虑以下几点
- CPU架构
- 操作系统
- 镜像体积
CPU架构
不同CPU架构的处理器,指令集是不完全相同的。因此同一段代码在编译后,在x86平台与arm平台的汇编指令不同。 首先,你应该了解你的项目使用编译型语言(C++,go)还是解释型语言(Java,Python)。
编译型语言
如果是编译型语言,选择的基础镜像架构最好与代码编译平台保持一致,这样可以减少跨平台编译带来的工作量。
例如:你的c++项目在x86平台编译,而你想要使用arm平台的基础镜像,你需要想办法将c++项目编译为arm架构,才能在arm平台正常运行,此过程会存在一些代码层面的变更。
解释型语言
对于解释型语言来说,镜像的CPU架构影响不大,只需要在镜像中安装对应的运行环境即可,不存在代码层面的变更。
例如:你的java项目需要在x86镜像平台,只需要在镜像构建时安装x86的jdk即可,x86的jdk编译代码为x86指令集。
操作系统
选择主流的、正在维护的操作系统即可,例如Ubuntu。 这样可以保证你的项目可以更新最新的系统漏洞,提高安全性。 并且为了轻量化,在满足运行环境的基础上,选择的基础镜像体积越小越好。
镜像构建
镜像需要包含哪些东西?
在镜像构建前,想清楚哪些东西需要塞到镜像里面。
运行环境与依赖
无论你的项目是哪种开发语言,运行环境和依赖是必不可少的。 以Java项目举例,如果此项目依赖于jdbc.jar,镜像中就必须准备好相关RPM/deb/xx 的jdbc依赖。如果项目构建时项目文件和依赖打包在一起,则不需要额外安装依赖,例如SpringBoot项目bootJar时将依赖一起打入Jar包,因此不需要额外安装依赖。
项目包
构建好的项目包也要放入镜像中
数据持久化方案
为了容器的迭代,像配置、数据库数据、日志等重要数据持久化是必然的。如果你的项目运行时,有非常多的目录需要持久化,就需要用docker run -v 或者 docker-compose.yml中定义非常多数据卷,这样不够优雅且难以管理。 例如,某项目需要持久化 "/etc/a"、"/etc/b"、"/etc/c"、/var/log/a、/var/log/b、/var/log/c 这几个文件夹,那么你的docker-compose.yml可能是下面这样的,非常不优雅。并且项目迭代如果产生新的需要持久化的目录,还需要继续在docker-compose.yml文件中堆积,很不方便。
docker-compose.yml
version: '3'
services:
web:
image: 项目名:TAG
volumes: # 挂载数据卷
- /etc/a-data:/etc/a
- /etc/b-data:/etc/b
- /etc/c-data:/etc/c
- /var/log/a -data:/var/log/a
- /var/log/b-data:/var/log/b
- /var/log/c-data:/var/log/c
volumes: # 定义数据卷
/etc/a-data:
/etc/b-data:
/etc/c-data
/var/log/a-data
/var/log/b-data
/var/log/c-data
我们可以用符号链接(Symbolic Link)优化目录结构
shell
#!/bin/bash
# 原文件夹迁移
mkdir /var/allConfig
mkdir /var/allLog
mv /etc/a /etc/b /etc/c /var/allConfig
mv /var/log/a /var/log/b /var/log/c /var/allLog
# 建立原文件夹的软链接
ln -s /var/allConfig/a /etc/
ln -s /var/allConfig/b /etc/
ln -s /var/allConfig/c /etc/
ln -s /var/allLog/a /etc/
ln -s /var/allLog/b /etc/
ln -s /var/allLog/c /etc/
优化后,仅需关注 /var/allLog /var/allConfig 文件夹的持久化
ENTRYPOINT入口
提前准备好容器初始化脚本 此脚本需要做的事情:1.容器初始化 2.主进程与子进程控制 注意事项:1.环境变量的传播 2.systemd通信(如果使用了Systemd) 等... 此部分在容器篇详细说明。
镜像体积优化
Dockerfile镜像构建中,每一命令都会堆叠一层镜像,这方面的知识不再赘述。 需要注意的是
- 层级:由于镜像 copy on write 的特点,如果当前层级需要读取或修改其他层的镜像的文件,会完整复制此文件至当前镜像层,这样两个层级都存在此文件,如此造成双倍空间占用。
- 文件及缓存:当前层级删除其他层级的文件,并不会减少其他层级的体积,资源清理必须在一次镜像操作内完成。
- 分阶段构建:构建时区分构建阶段与运行阶段,只保留需要的文件至运行阶段以减少镜像体积
那么我们的核心思想:1.尽可能减少镜像层级,避免层级之间的文件修改 2.每次操作末尾,清理不必要文件及缓存 3.初始化操作在容器启动时完成(例如Postgresql init db) 4.以分阶段构建的方式,仅保留构建阶段生产的必要文件
例如:
dockerfile
# 优化前
FROM node:14
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
# 优化后
FROM node:14 as builder
WORKDIR /app
COPY . .
RUN npm install && npm run build
FROM node:14-alpine
WORKDIR /app
COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/node_modules /app/node_modules
CMD ["npm", "start"]
下班后时间不多,今天就讲到这里,一周后更新 "容器篇"。 文中如有描述不对的地方欢迎大家指正!