前言
作为前端,你有没有过这种时刻:
- 上线时运维同学说"Pod 起不来",你点点头其实没听懂;
- 看到 SRE 发来的 K8s deploy yaml,里面
replicas、Service、Ingress一堆词,每个都认识,连起来就不知道在说什么; - 自己设了
NODE_OPTIONS=--max-old-space-size=4096,结果运维说 Pod 要给 8GB 内存,多出来那一半内存被谁吃了? - 看到项目里同时出现 Ingress 和 OpenResty / Nginx,搞不清它俩是不是同一个东西,谁替代谁?
这些困惑的根因是同一个:前端的训练让我们对"客户端怎么渲染"非常熟,但对"我们写的代码到底是怎么跑在服务器上的"几乎没有清晰的认知模型。
K8s(Kubernetes)已经是容器编排领域被广泛采用的基础设施,主流互联网公司的服务部署多数都绕不开它或它的某种衍生形态。它本身并不算特别复杂,但它的"名词系统"对前端来说有点陡。
这篇文章试图用 一张嵌套图 + 一个贯穿例子 把 K8s 最核心的 5 个名词(Container / Pod / Deployment / Service / Ingress)讲清楚。你不需要变成运维,只需要在跨团队沟通和读 yaml 时能"对上号"。
一、先看这张图:5 个名词的"空间关系"
放下文字,先看图:
怎么看这张图:
- 嵌套矩形 = 包含关系:Container 在 Pod 里、N 个 Pod 在 Deployment 里------这是 K8s 真实的层级。
- 实线箭头 = 请求流向:外部请求经过反向代理进 Service。
- 虚线箭头 = 选中关系 :Service 通过 label selector "认领" 一组 Pod,自动维护"哪些 Pod 还可用"------不是物理包含。
- 颜色对应下面 5 个小节:橙=外部入口,紫=Container,蓝=Pod,绿=Deployment,黄=Service。
整篇文章会用一个例子串到底:一个叫 my-web-app 的 Next.js 服务要上线 v1.2.3 版本。
二、五大名词:一次讲清
每个名词都用三段回答:
- 是什么?
- 没有它会怎样?(它解决了什么问题)
- 在 v1.2.3 这个例子里它具体是什么?
再用一句话点出它的"边界"------它不管什么。
2.1 Container(容器)
是什么 :把 Node 22 + server.js + node_modules + Alpine OS 库 打成一张不可变的快照(image),运行起来就是一个 container。可以理解成 "自带 node_modules、自带 Node、自带 OS 的便携可执行包"。
没有它会怎样 :本地 pnpm dev 没事,上服务器各种报错------Node 版本对不上、native 模块编译失败、glibc 不兼容......容器把"运行环境"和"代码"一起打成不可变快照,消除"在我机器上是好的"问题。
在 v1.2.3 例子里 :CI 跑 docker buildx build --push 把代码打成镜像 registry.example.com/my-web-app:v1.2.3。镜像本身只是个 tar.gz,不"在运行";需要被一个调度器拉下来"启动",才会变成下一节的 Pod。
🚧 边界感 :Container 不管"要几个副本 / 挂了谁补"------那是 Deployment 的事;也不管"外面怎么找到我"------那是 Service / Ingress 的事。它只管 把环境 + 代码打成一张静态快照。
📌 Image vs Container = 类 vs 实例
这是前端最容易混淆的一对概念:
- image 是不可变的二进制快照(一个
tar.gz),存放在镜像仓库(Harbor / GHCR / Docker Hub)里,本身 不"在运行"; - container = 从这张 image
new出来的一次具体运行(有 PID、占内存、能被kill); - 同一张 image 可以同时跑起 N 个 container(= 后面 Deployment 那一节讲的 N 个 Pod 的核心进程);
Dockerfile是这张 image 的 "build 脚本",决定快照里有什么。
类比熟悉的面向对象:image 是类(class),container 是从这个类实例化出的对象(new Foo())。
2.2 Pod
是什么 :1 个或多个 container 一起"开机运行"的最小单位。一个 Pod 通常只跑一个业务 container;如果还需要"陪跑"的辅助进程(比如日志收集器、Istio 流量代理、本地配置注入器 这类),会以 sidecar(副容器) 的形式塞进同一个 Pod,和主容器共享网络/存储。Pod 启动后会分到一个集群内的临时 IP(如 10.244.3.17)。
没有它会怎样:你只有镜像和孤立的 container 进程;没人统一管"这一组进程的网络命名空间、共享存储、生命周期事件"。Pod 是 K8s 调度的最小颗粒------调度器只调 Pod,不调单个 container。
在 v1.2.3 例子里 :v1.2.3 镜像在某台机器上启动 = 一个 Pod。同样镜像可以同时启动 N 个 Pod(即 N 副本),分布在不同机器上,每个 Pod 各有内部 IP。Pod 重启 IP 就变------所以下一节才需要 Service 套个稳定名。
🚧 边界感:Pod 只管"自己这一个实例的运行";不知道还有没有同伴、也不负责"挂了重建"------那是 Deployment 的事。
💭 前端高频踩坑:NODE_OPTIONS 4GB ≠ Pod 4GB
很多前端会困惑:NODE_OPTIONS=--max-old-space-size=4096 已经给 Node 设了 4GB,K8s 平台还要给 Pod 配 8GB------多出来的 4GB 是浪费了吗?
不是浪费,是必须的。原因是 --max-old-space-size 只管 V8 的 JS 堆 (GC 管的那一坨对象),而 Pod 的内存上限管的是整个容器进程能用的物理内存,包括:
- V8 JS 堆(你设的 4GB 这一块)
- V8 内部缓冲(code cache、栈、metadata,几百 MB)
- Node native 分配(Buffer / ArrayBuffer / Worker / native addon,不归 V8 GC 管)
- 同 Pod 内的 sidecar 副容器(也算 Pod 内存配额)
- 临时峰值缓冲(Pod 限制是硬上限,触顶直接被 K8s OOMKill;V8 触顶只是触发 GC)
经验值:Pod 内存上限通常要比 Node 堆上限留 1-2 倍余量,给 native + sidecar + 峰值用。
如果反过来 Pod 限制 < Node 堆限制,会出现一个最难排查的故障:Node 还没来得及 GC 就被 K8s 杀掉了,应用代码里没有任何 JS 报错,表现为"莫名重启"。
2.3 Deployment
是什么 :一段 YAML,声明 "我要 N 个用 v1.2.3 镜像的 Pod 一直健康跑着" 。它管两件事:副本数 和 滚动更新。
没有它会怎样 :Pod 挂了没人补;从 v1.2.3 升到 v1.2.4 要自己写一长串 shell------"先起一个新的、等它健康再下一个老的、出错怎么回滚"。Deployment 把这些做成了声明式:你只描述目标状态,K8s 自动往那个状态收敛。
在 v1.2.3 例子里 :my-web-app 这个 Deployment 写着:
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-web-app
spec:
replicas: 10
template:
spec:
containers:
- name: app
image: registry.example.com/my-web-app:v1.2.3
CI/CD 平台把 image 改成 v1.2.4 后,K8s 按 RollingUpdate 节奏一个个换 Pod,老 Pod 挂了自动起新的补位。
🚧 边界感 :Deployment 不管"对外怎么访问、流量怎么进"------那是 Service / Ingress 的事;它只管该有的 Pod 应该长什么样、应该有几个。
2.4 Service(NodePort / ClusterIP)
是什么 :一组 Pod 的稳定 DNS 名 + 内置负载均衡 。给你一个不变的名字(如 my-web-app-svc),K8s 自动维护"这个名字背后到底是哪些可用的 Pod IP",并按负载均衡策略把请求转发过去。
没有它会怎样:上游(反向代理 / 其他服务)要直连 Pod IP,每次 Pod 重启都得 reload 上游配置;多副本时还要自己写负载均衡。
两种常见类型:
ClusterIP(默认):只在集群内可访问,用于服务之间互相调用。NodePort:在集群每台机器(Node)上开同一个固定端口(在 30000-32767 范围内),集群外 的进程访问任一<NodeIP>:<那个端口>都会路由到这组 Pod。集群外的反向代理(Nginx / OpenResty)就是靠 NodePort 进入集群。
在 v1.2.3 例子里 :上游 Nginx conf 里写 proxy_pass http://my-web-app-svc;,K8s 内部自动选一个健康 Pod 投递;新 v1.2.3 Pod 启动会被自动加入 Service 的"可用 Pod 列表",老 v1.2.2 Pod 下线就被自动摘掉------上游 Nginx 配置一直不用动。
🚧 边界感 :Service 不感知请求来源(域名 / 路径),只负责追踪后端 Pod 的健康状态并按负载均衡策略转发请求;至于"域名 / 路径如何映射到具体 Service",是上一层 Ingress / OpenResty 的职责。
2.5 Ingress vs OpenResty(最容易混淆的一对)
先一句话定性:
Ingress 是"抽象规则",OpenResty 是"软件实现"。它们不在一个层面,但容易被并列对比。
| 维度 | Ingress | OpenResty |
|---|---|---|
| 它是什么 | K8s 的一种资源类型(一段 YAML),描述"什么域名 / 什么路径 → 哪个 Service" | 一个具体软件(Nginx + LuaJIT),自己会跑一个进程 |
| 谁去执行 | 必须有一个 Ingress Controller (住在 K8s 集群里)去消费这段 YAML------这个 Controller 通常本身就是 OpenResty / Nginx / Envoy | 就是它自己------一组独立部署的 OpenResty 进程 |
| 能干多复杂 | 只能做声明式路由(host / path → Service),复杂规则要靠 annotation 拼字符串 | 能写 Lua,任意定制:灰度抽样、cookie 改写、A/B、鉴权、按用户分流...... |
| 变更链路 | 改 YAML → kubectl apply → Controller 自动 reload |
改 conf → 手动通知运维 → reload OpenResty 进程 |
| 在哪 | K8s 集群内 | 通常在 K8s 集群外(也有放集群内的) |
💡 所以"为什么会同时用 Ingress 和 OpenResty"?
它们处在请求链路的不同段,不是"用谁不用谁"的二选一:
- 简单业务(文档站、内部工具):直接走集群内的 Ingress,YAML 一改即生效,运维成本低。
- 复杂业务(C 端主站、灰度发布、需要 Lua 自定义逻辑) :在 K8s 集群外架一组独立 OpenResty 集群做边缘网关,承担灰度、鉴权、改写等复杂逻辑,再
proxy_pass进 K8s 的 NodePort Service。
🚧 边界感 :Ingress / OpenResty 都不管"Pod 健康不健康、有几个副本"------那是 Deployment + Service 的事;它们只管外部请求来了,按规则把它递给哪个 Service 名。
三、一个请求是怎么打到你代码的
把上面 5 个名词串起来,看一个完整请求生命周期:
负载均衡 + TLS 卸载"] S -->|"HTTP"| OR["反向代理
OpenResty / Ingress Controller"] OR -->|"按域名/路径匹配 + 灰度规则"| SVC["K8s Service
my-web-app-svc"] SVC -->|"负载均衡选一个健康 Pod"| POD["Pod"] POD --> CONT["Container 里的
Node 进程跑 server.js"]
每一跳的职责:
| 节点 | 职责 |
|---|---|
| SLB | 公网入口;做 TLS 卸载(HTTPS 解密成 HTTP);流量分发到下游节点 |
| 反向代理(OpenResty / Ingress) | 按域名 / 路径决定转给哪个 Service;自定义路由、灰度、改写、限流 |
| Service | 在 K8s 集群里给一组 Pod 一个稳定 DNS 名 + 内置 LB;自动剔除挂掉的 Pod |
| Pod | 调度执行的最小单位;持有运行中的 container 实例 |
| Container | 真正跑你 server.js 的进程 |
读到这里你应该能回答标题里那个问题:一个请求从反向代理出来后,凭什么稳定打到正确版本的代码?
答案是:反向代理认 Service 名(不变),Service 自动维护"哪些 Pod 还可用",Deployment 保证"v1.2.3 镜像的 Pod 始终有 N 个在跑",Container 是这个 Pod 里真正干活的进程。
四层各管一段,互不渗透,组合起来共同支撑了云原生服务的稳定性。
四、三个常见误区一次澄清
误区 1:Image 跟 Container 是一回事?
不是。前面讲了:image = 类,container = 实例。
实际表现是:你 docker pull 一张 image 到本地,它就静静地躺在磁盘上,不占 CPU 不占内存;只有 docker run 起来后才会变成一个 container(一个真正的进程)。
误区 2:Ingress 就是 OpenResty / Nginx 的 K8s 版本?
不是。Ingress 是规则 ,OpenResty / Nginx 是实现。K8s 集群里跑的"Ingress Controller"经常本身就是 OpenResty / Nginx------这种"用同一个软件、扮演不同角色"的设定确实容易让人糊涂。
记住一点:当文档/对话里说"我们改一下 Ingress",指的是改 YAML;说"我们改一下 OpenResty",指的是改 nginx.conf。
误区 3:NODE_OPTIONS=4096 就够了,Pod 给 4GB 不就行了?
不行。前面 §2.2 的 💭 框已经详细讲过:--max-old-space-size 只管 V8 JS 堆,Pod 内存还要装下 V8 内部缓冲 + Node native 分配 + sidecar + 峰值缓冲。
经验比例:Pod 内存 ≈ Node 堆 × 1.5 ~ 2。
反过来 Pod 限制 < Node 堆限制 时,会出现"应用没报错但 Pod 莫名重启"的最难排查故障------Pod 被 K8s OOMKill 时,应用层没有任何线索(不像 V8 OOM 会抛出 JavaScript heap out of memory)。
五、总结:给前端的几条具体建议
- 沟通时用对名词:跟运维说"Pod 挂了" vs "Container 挂了" 是不同的诊断方向;跟前端 leader 说"加 Pod 副本" vs "加 Pod 内存" 是不同的成本。
- 看到别人发的 K8s yaml 不慌 :知道
kind: Deployment管副本,kind: Service管访问,kind: Ingress管路由,剩下的 annotation / labels 大多是配置项,遇到再查。前端通常不直接写这类 yaml,但能看懂能极大提升跨团队沟通效率。 - Pod 内存配置要留余量 :在自己的 Dockerfile / dev 脚本里设
NODE_OPTIONS=--max-old-space-size=X时,Pod 内存最少要 X × 1.5。 - 本地能跑 ≠ Pod 能跑:本地是无限制环境,Pod 是受限环境,要意识到 native 模块(Sharp、Canvas、Prisma 等)和 worker 线程会显著吃 native 内存。
- Ingress 还是 OpenResty 不重要:你只需要知道"集群入口在哪、配置归谁管"。如果是 Ingress,PR 改 YAML 自己就能提;如果是 OpenResty conf,要找运维改。
六、延伸阅读(纯官方资料)
- Kubernetes 基础教程 :Kubernetes Basics ------ 从零理解核心对象
- K8s 核心概念 :Pods · Deployment · Service · Ingress
- OpenResty 官方 :openresty.org
- Nginx 入门 :Beginner's Guide
- Docker 概念 :Get started · Overview ------ image / container / Dockerfile 关系
- Next.js Self-hosting :官方 self-hosting 指南(理解 standalone 部署)
- Nginx Ingress Controller :kubernetes.github.io/ingress-ngi... ------ K8s 集群内最常见的 Ingress 实现
写在最后
如果你读完后能在下次跟 SRE / 运维对话时听懂他们在说哪一层 ,并且看 yaml 时心里有图,这篇文章的目的就达到了。
云原生是我们写的代码最终运行的环境。把这 5 个名词在心里摆对位置,下次再听到 Pod / Service / Ingress,就能跟得上节奏、少踩坑。
你在工作中踩过 K8s 哪些坑?欢迎评论区聊聊,我可能会针对高频问题再写一篇排查向的续集。