外观设置

本站

Docker、K8s、Jenkins 有什么区别?沿着一次部署看清它们的分工

2026-09-26 · Docker · 部署 · 软件工程

假设你写了一个 AI 问答程序:输入问题,检索资料,调用模型,返回答案。本机试了十几个问题,效果不错;把代码发给同学,对方却停在了安装依赖这一步。

等环境终于配好,你又想:能不能放到服务器上,让大家直接打开网页用?于是开始搜索部署教程,眼前出现了 Docker、Compose、Kubernetes、Jenkins、Nginx 和 CI/CD。

你只是想把程序交给别人,怎么突然多了这么多东西?

原因在于,“部署”包含好几个不同的问题:环境怎样准备,服务怎样连接,故障后由谁恢复,新版本怎样交付。先给它们各自找一个位置:

  • Docker:制作和运行容器镜像,交付应用及其运行依赖。
  • Docker Compose:用配置描述一组服务及其网络、存储关系。
  • Kubernetes(K8s):在集群中调度应用,并持续让实际状态接近期望状态。
  • Jenkins:把测试、构建、发布等步骤组织成可以重复执行的流水线。

它们不要求成套使用。下面沿着这个问答项目的成长过程,看看问题是怎样逐个出现的。

Docker:把运行条件一起交出去

最初发给同学的几个 Python 文件,并不包含程序运行的全部条件。你的电脑还有特定版本的 Python、第三方包、系统库和环境配置。代码相同,环境却不同,结果当然可能不同。

Docker 改变了交付的单位。我们可以用 Dockerfile 描述如何准备环境,把代码、运行时和依赖构建成镜像,再由目标机器根据镜像创建容器。Docker 官方概述

这三个名字需要分清:Dockerfile 是构建说明,镜像是构建产物,容器是由镜像创建的实例。 同一份镜像可以启动多个容器,每个实例有自己的运行状态。镜像还可以上传到镜像仓库,供服务器拉取;镜像仓库分发的是镜像,Git 仓库管理的则主要是源代码。

环境描述得清楚,交接就不再依赖“你还漏装了哪个包”。不过,要让构建尽量可重复,仍需记录依赖和基础镜像的版本;只把一串随时可能安装到新版本的命令写进 Dockerfile,并不能保证每次产物相同。

容器也不是一台完整的小电脑。在常见的 Linux 容器场景下,应用仍是进程,通常共享宿主环境的 Linux 内核;它的进程视图、网络和文件环境经过隔离。目标硬件与底层支持仍然重要,例如 GPU 容器还需要宿主机驱动和相应容器配置。NVIDIA 容器工具安装说明

如果你已经用过 Conda,可以把区别理解为:Conda 主要帮你管理软件环境,Docker 还提供镜像交付及容器运行机制。它们可以结合使用,但简单项目未必需要叠加所有工具。

跑起来以后,还要处理端口和数据

容器日志显示程序启动成功,浏览器却打不开,这是另一类问题:程序已经运行,不代表请求能够到达它。

在常见的桥接网络配置下,需要分别看应用监听和宿主机端口发布。假设应用在容器内监听 0.0.0.0:8000,下面这段启动命令可以用来理解端口和存储的关系。它假定 paper-qa:0.1 镜像已经准备好,应用把上传文件写到 /app/uploads:

docker run -d --name paper-qa -p 127.0.0.1:18000:8000 -v paper-uploads:/app/uploads paper-qa:0.1

这里的 18000:8000 表示宿主机端口转发到容器端口,两者不必相同。127.0.0.1 把这个入口绑定在宿主机回环地址上,适合本机访问或由宿主机上的反向代理转发;要让其他机器直接访问,就需要根据实际网络调整入口和放行规则。Docker 端口发布说明

应用在容器内监听的地址,也不能和宿主机的发布地址混为一谈。若应用只监听容器自己的回环地址,单纯增加端口映射未必就能解决问题。

再看 -v:它把名为 paper-uploads 的数据卷挂到容器的上传目录。这样重新创建应用容器时,可以继续挂载同一个卷。相反,如果文件只写入容器自身的可写层,删除该容器就会丢掉这一层。停止容器与删除容器不是同一件事。Docker 数据卷说明

由此可以分清两项经常混淆的工作:持久化让数据独立于应用实例存在,备份则应对误删、损坏等情况。数据放进卷里,不等于已经有了备份。

Compose:把整个项目的启动方式写下来

问答程序继续发展,可能需要数据库保存会话,需要后台进程解析论文,还需要缓存。此时,一个项目已经包含几个互相协作的服务。

Compose 可以用一份 compose.yaml 描述这些服务:各用哪个镜像、怎样连接、哪些数据需要保存。它的价值不只是少敲几条命令,而是让项目结构变成可阅读、可交接的配置。

最容易踩的坑是数据库地址。在通常的独立容器网络里,后端容器的 localhost 指向后端自己,并不是数据库容器。在 Compose 默认网络中,服务可以通过服务名互相发现。如果数据库服务叫 db,后端连接的就可以是 db:5432。Compose 网络说明

另一个坑是把启动顺序当成可用性。数据库容器被创建后,可能还在初始化。Compose 的依赖条件可以配合健康检查等待服务就绪,但“依赖已启动”和“依赖可以响应业务请求”必须分别判断。Compose 启动顺序说明

对于一台服务器上的小项目,Compose 往往已经足以把部署交代清楚。如果它满足当前需求,没有必要为了用上更大的工具立刻迁移。

K8s:持续管理期望状态

当应用要分布在多台机器上,或者需要运行多个副本,问题会变成:放在哪台机器?少了一个实例由谁补?更新时如何逐步替换?

K8s 的切入口是“期望状态”。手工启动三个进程,是执行一次动作;声明希望保持三个副本,则是在描述一个持续目标。控制器观察实际状态,并尝试把它调整到目标状态。Deployment 就是用于管理副本与更新的常见资源。Kubernetes Deployment 文档

初学时,先理解三个对象就够进入讨论:

  • Pod:K8s 调度和运行应用的基本单位,可以包含一个或多个紧密协作的容器,这些容器共享网络等资源。Pod 文档
  • Deployment:声明应用镜像、副本数量和更新策略,通过控制器管理相关实例。
  • Service:为一组后端提供稳定的访问抽象,让调用方不必一直追踪会变化的 Pod 地址。Service 文档

这些机制也有前提。集群资源不足时,声明十个副本不会凭空得到十份算力;应用存在同样的代码错误,补出来的实例也可能继续失败。

AI 服务还很适合说明三种探针的区别。模型加载很慢时,启动探针给初始化留出判断空间;就绪探针决定实例是否适合接收流量;存活探针用于识别需要重启的故障。就绪失败通常意味着暂时不向它分流,不能简单等同于重启。Kubernetes 探针文档

假如把“模型还没加载完”误判成“程序卡死”,反复重启反而会让它永远加载不完。工具提供了机制,检查条件仍需符合应用的实际行为。

同样,三个副本各把会话存在自身内存里,不会自动得到共享会话;多个模型进程也可能分别占用内存或显存。增加副本之前,还要考虑状态放在哪里、资源是否够用。

Jenkins:把发布过程变成可重复的流程

无论最后使用单个容器、Compose 还是 K8s,每次更新都可能重复这些步骤:获取代码、运行测试、构建镜像、上传镜像、更新应用、检查结果。

Jenkins 可以把这些步骤编排成流水线,并保存执行记录。流水线通常写在 Jenkinsfile 中,随代码一起管理。Jenkins Pipeline 文档

例如,可以这样设计一次发布:

代码变更 → 测试 → 构建镜像 → 上传镜像
                               ↓
                      更新测试环境并检查
                               ↓
                      审批后更新正式环境

这是一种流程选择,不是安装 Jenkins 后自动得到的能力。执行任务的节点需要相应工具和权限:测试 Python 项目需要 Python 环境,构建镜像需要构建工具,更新服务器需要部署权限。Jenkins 负责组织这些动作,执行条件仍要准备好。Jenkinsfile 使用说明

顺便分清 CI/CD 的含义。持续集成强调频繁整合变更并执行自动检查;持续交付要求通过检查的版本随时可以发布,正式上线可以保留人工决定;持续部署则把满足条件的变更自动发布到生产。因此,有 CI/CD 不等于每次提交都会立即改动线上服务。

流水线通过,也只说明配置进去的步骤通过了。对问答项目,接口能够返回成功,不足以证明答案质量没有退步。我会保留一小组固定问题,检查关键检索和回答效果,而不是仅看服务器是否启动。

Jenkins 只是组织交付流程的一种工具。学习时更值得掌握的是每一步做什么、如何判断成功、失败后停在哪里;这些理解可以迁移到其他流水线平台。

发布路线与访问路线,要分开看

把工具连起来时,最好在脑中保留两张图。

一张是开发者发布新版本的路线:

Git 仓库 → Jenkins 测试与构建 → 镜像仓库
                                   ↓
                         Compose 或 K8s 更新应用

另一张是普通用户访问服务的路线:

浏览器 → 网络入口 → 应用 → 数据库或模型服务

Jenkins 通常位于第一条路线,用户每次提问不需要经过它。Nginx 这样的反向代理则常出现在第二条路线:接收请求,再转交给对应后端。

所以,“用 Docker 运行 Jenkins,再让 Jenkins 发布容器化应用”并不矛盾。工具本身怎样运行,和它负责什么工作,是两个不同的问题。

一个小项目怎样开始,才不容易迷路

如果是第一次把项目交给别人用,我更倾向于逐步增加工具:先弄清应用的启动命令、监听地址和日志,再描述容器环境;确实有多个服务时,用 Compose 组织起来;手工发布步骤已经清楚后,再写进流水线。是否采用 K8s,则看多节点管理和团队平台需求。

这是一条学习路线,不是项目必须经历的成熟度排名。托管平台也可能替你承担其中一些环节。

每增加一层,都应该能回答:它刚刚解决了哪个具体问题?同时保留几项朴素的能力:知道当前版本,能找到错误日志,清楚数据在哪里,知道怎样恢复重要数据。应用回滚也不能代替数据库恢复;新版本删除了字段,换回旧镜像不会自动把字段和内容找回来。

对这个假想的问答项目,一个很好的阶段目标是:另一个人根据项目文件就能启动服务;更新容器后,上传的论文还在;请求变慢时,能够区分时间花在检索还是模型调用。

到了这一步,“我电脑上能跑”才开始变成可以交接的服务。以后是否需要更大的集群、更复杂的流水线,就有具体问题可以依据了。

柯西 Alakazam

评论