数字堡垒 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 状态的时候,那种成就感,真的太爽了。

好了,今天就聊到这儿。如果你们对容器化有什么问题,欢迎评论区留言,咱们一起交流。下次想听什么话题,也可以告诉我,咱们下期再见!