数字堡垒 21 容器故事
引言
各位老铁们,今天想跟你们聊聊我们团队最近搞的一个大新闻——数字堡垒 21 项目的容器化改造。说实话,这半年多的时间里,我们从一开始对容器技术的一知半解,到最后把整个系统搬进 Docker 里,这里面的坑和经验,真的够写一本书了。
起因很简单:我们那个运行了三年多的项目,随着功能越来越多,部署一次简直要了亲命。测试环境、生产环境、开发环境各有各的脾气,不同机器上的依赖版本能差出十万八千里。每次发版,运维同事都要加班到凌晨,我就寻思,这不行啊,得找条出路。
正好那段时间容器概念火得不行,我就跟老板拍胸脯保证:给我三个月,还你一个全新的部署体验。老板看了我一眼,说行,那就干呗。结果这一干,还真是打开了新世界的大门。
那些年被部署折磨的日子
在说容器化之前,我得先跟你们倒倒苦水。你们能想象吗?我们之前部署一个后端服务,得先在目标机器上装 JDK、装 MySQL 客户端、装 Redis、装各种 Python 依赖包。每一台机器的配置都像开盲盒,你永远不知道下次部署会遇到什么奇奇怪怪的问题。
我记得最离谱的一次,测试环境部署成功了,结果生产环境死活起不来。查了两天发现,生产机器上的 OpenSSL 版本太老,Python 的 requests 库用不了。最后怎么解决的?升级 OpenSSL,结果把其他服务搞崩了,又回滚,来来回回折腾了一周。
那时候我就在想,有没有一种方法,能让我们把整个运行环境打包带走,就像把整个房间装进集装箱一样?后来一了解,嘿,Docker 不就是干这个的吗?
第一次亲密接触
说干就干,我先是花了几天时间把 Docker 的基础概念过了一遍。说实话,镜像、容器、Dockerfile 这些概念听起来挺抽象的,但真正用起来就发现,这玩意儿真是太香了。
我们先拿一个简单的微服务开刀,试试水。原来的部署脚本有几百行,什么创建用户、配置环境变量、复制文件、启动服务,全部要手动来。现在好了,一个 Dockerfile 全部搞定:
FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/app.jar /app/
COPY config/ /app/config/
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
就这么几行字,一个完整的运行环境就出来了。镜像里有什么?JDK 运行时、我们的应用包、配置文件,全部打包在一起。而且最重要的是,这个镜像是可以版本控制的,每次构建都会生成一个唯一的 tag,想回滚到哪个版本都行。
第一次在本机跑起来的时候,我激动得差点跳起来。docker run 一下,服务就起来了,docker ps 看一下,运行状态一目了然。这不比之前那种敲七八个命令方便多了?
踩坑实录:网络和存储的那些事儿
当然,容器化也不是一帆风顺的。我们在实际改造过程中,踩了不少坑,这里挑几个典型的跟你们说道说道。
网络打通是个技术活
我们原来系统里有很多内部服务互相调用,什么 RPC、HTTP 调用的一大堆。改成容器之后,每个服务都跑在自己的容器里,网络怎么通就成了大问题。
一开始我们用的是默认的 bridge 模式,结果容器之间互相访问不了。后来了解了 --link 参数,但这个方案已经被官方标记为 deprecated 了。最后用的是自定义网络:
docker network create my-network
docker run -d --network my-network --name service-a service-a-image
docker run -d --network my-network --name service-b service-b-image
这样两个容器就在同一个网络里,可以通过服务名直接互相访问。比如 service-b 想调用 service-a,直接用 http://service-a:8080 就行,比 IP 地址可方便多了。
数据持久化是个坑
还有一个大坑就是数据存储。我们有些服务需要把数据写到本地磁盘,比如日志、用户上传的文件什么的。容器有个特性,重启之后里边的数据会丢失,这可把我们坑惨了。
后来学了 docker volume,才算解决这个问题:
docker volume create my-data
docker run -v my-data:/app/data my-image
这样即使容器删了重建,数据还在 volume 里。不过这里有个小提醒,重要数据一定要定期备份,别问我为什么知道,都是泪。
配置管理要灵活
还有一个问题就是配置。不同环境需要不同的配置,生产环境、测试环境、开发环境的数据库地址、缓存地址都不一样。
我们最后的方案是用环境变量:
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
然后运行时通过 -e 参数注入环境变量:
docker run -e DB_HOST=192.168.1.100 -e DB_PORT=3306 my-app
这样一套镜像,不同环境只要改改环境变量就能跑,简直不要太方便。
编排的艺术:docker-compose 大显身手
单个容器玩转了之后,我们就开始研究怎么管理一堆容器。毕竟一个完整的系统可能有十几二十个服务,一个一个手动启动不得累死?
这时候 docker-compose 就登场了。这玩意儿,简直就是为我们这种微服务架构量身定做的。
我们写了一个 docker-compose.yml:
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
depends_on:
- redis
- db
environment:
- NODE_ENV=production
redis:
image: "redis:alpine"
db:
image: "postgres:13"
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
然后只要一句 docker-compose up -d,所有服务都起来了。而且它会自动处理依赖关系,先启动 db,再启动 web。服务之间还能通过服务名互相访问,简直不要太爽。
我们还利用 docker-compose 的健康检查功能:
services:
db:
image: postgres:13
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
web:
depends_on:
db:
condition: service_healthy
这样 web 服务会等 db 真正启动完成并通过健康检查之后才开始启动,彻底解决了服务启动顺序的问题。
自动化部署:从 CI/CD 到容器云
玩转了本地开发和测试,接下来就是生产环境了。我们把 Docker 跟 Jenkins 结合起来,实现了全自动的构建和部署流程。
流程是这样的:代码提交到 Gitlab → Jenkins 自动触发构建 → 构建 Docker 镜像并推送到私有镜像仓库 → 远程服务器拉取最新镜像 → 停止旧容器,启动新容器 → 运行健康检查 → 部署完成。
整个过程不需要人工干预,从代码提交到上线,最快只要十五分钟。这在之前是不可想象的。
我们还利用 Docker 的标签功能实现了蓝绿部署。两套环境,一个蓝,一个绿,新版本部署到不活跃的那套,健康检查通过后切换流量。出了问题?一键回滚,分分钟的事儿。
总结
说了这么多,总结一下这半年多的心得体会吧。
容器化真的香。它把我们的部署效率提升了不止一个量级,原来需要一天的部署工作,现在十分钟就搞定了。而且环境一致性得到了保证,再也不会出现“在我机器上能跑”这种尴尬的情况。
当然,容器不是万能的。它带来了新的复杂度,需要学习新的工具和概念。网络、存储、监控、日志收集,都需要重新考虑。但这些投入是值得的,长期来看收益远大于成本。
如果你也在被部署问题折磨,不妨试试容器化。从一个小服务开始,慢慢迁移,积累经验。相信我,当你看到 docker ps 那一排绿色 Up 状态的时候,那种成就感,真的太爽了。
好了,今天就聊到这儿。如果你们对容器化有什么问题,欢迎评论区留言,咱们一起交流。下次想听什么话题,也可以告诉我,咱们下期再见!
数字堡垒 21 容器故事
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法