写给前端的 K8s 入门:用一张图和一个例子搞懂 5 个核心概念

前言

作为前端,你有没有过这种时刻:

  • 上线时运维同学说"Pod 起不来",你点点头其实没听懂;
  • 看到 SRE 发来的 K8s deploy yaml,里面 replicasServiceIngress 一堆词,每个都认识,连起来就不知道在说什么;
  • 自己设了 NODE_OPTIONS=--max-old-space-size=4096,结果运维说 Pod 要给 8GB 内存,多出来那一半内存被谁吃了?
  • 看到项目里同时出现 IngressOpenResty / Nginx,搞不清它俩是不是同一个东西,谁替代谁?

这些困惑的根因是同一个:前端的训练让我们对"客户端怎么渲染"非常熟,但对"我们写的代码到底是怎么跑在服务器上的"几乎没有清晰的认知模型。

K8s(Kubernetes)已经是容器编排领域被广泛采用的基础设施,主流互联网公司的服务部署多数都绕不开它或它的某种衍生形态。它本身并不算特别复杂,但它的"名词系统"对前端来说有点陡。

这篇文章试图用 一张嵌套图 + 一个贯穿例子 把 K8s 最核心的 5 个名词(Container / Pod / Deployment / Service / Ingress)讲清楚。你不需要变成运维,只需要在跨团队沟通和读 yaml 时能"对上号"。


一、先看这张图:5 个名词的"空间关系"

放下文字,先看图:

flowchart LR EXT["外部请求来源 OpenResty / Ingress"] SVC["Service my-web-app-svc"] EXT --> SVC subgraph DEP ["Deployment: my-web-app"] direction TB subgraph POD1 ["Pod #1"] C1["Container"] end subgraph POD2 ["Pod #2"] C2["Container"] end subgraph POD3 ["Pod #3"] C3["Container"] end end SVC -.-> POD1 SVC -.-> POD2 SVC -.-> POD3 style EXT fill:#fff4e6,stroke:#d97706,color:#1f2937 style SVC fill:#fef3c7,stroke:#a16207,color:#1f2937 style DEP fill:#dcfce7,stroke:#15803d,color:#1f2937 style POD1 fill:#dbeafe,stroke:#1d4ed8,color:#1f2937 style POD2 fill:#dbeafe,stroke:#1d4ed8,color:#1f2937 style POD3 fill:#dbeafe,stroke:#1d4ed8,color:#1f2937 style C1 fill:#ede9fe,stroke:#6d28d9,color:#1f2937 style C2 fill:#ede9fe,stroke:#6d28d9,color:#1f2937 style C3 fill:#ede9fe,stroke:#6d28d9,color:#1f2937

怎么看这张图

  • 嵌套矩形 = 包含关系: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 个名词串起来,看一个完整请求生命周期:

flowchart TB U["用户浏览器"] -->|"DNS 解析"| D["DNS"] D -->|"TLS 加密"| S["云厂商 SLB
负载均衡 + 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)。


五、总结:给前端的几条具体建议

  1. 沟通时用对名词:跟运维说"Pod 挂了" vs "Container 挂了" 是不同的诊断方向;跟前端 leader 说"加 Pod 副本" vs "加 Pod 内存" 是不同的成本。
  2. 看到别人发的 K8s yaml 不慌 :知道 kind: Deployment 管副本,kind: Service 管访问,kind: Ingress 管路由,剩下的 annotation / labels 大多是配置项,遇到再查。前端通常不直接写这类 yaml,但能看懂能极大提升跨团队沟通效率。
  3. Pod 内存配置要留余量 :在自己的 Dockerfile / dev 脚本里设 NODE_OPTIONS=--max-old-space-size=X 时,Pod 内存最少要 X × 1.5。
  4. 本地能跑 ≠ Pod 能跑:本地是无限制环境,Pod 是受限环境,要意识到 native 模块(Sharp、Canvas、Prisma 等)和 worker 线程会显著吃 native 内存。
  5. Ingress 还是 OpenResty 不重要:你只需要知道"集群入口在哪、配置归谁管"。如果是 Ingress,PR 改 YAML 自己就能提;如果是 OpenResty conf,要找运维改。

六、延伸阅读(纯官方资料)


写在最后

如果你读完后能在下次跟 SRE / 运维对话时听懂他们在说哪一层 ,并且看 yaml 时心里有图,这篇文章的目的就达到了。

云原生是我们写的代码最终运行的环境。把这 5 个名词在心里摆对位置,下次再听到 Pod / Service / Ingress,就能跟得上节奏、少踩坑。

你在工作中踩过 K8s 哪些坑?欢迎评论区聊聊,我可能会针对高频问题再写一篇排查向的续集。

相关推荐
凌睿马6 小时前
离线的银河麒麟系统部署ollama
云原生·eureka
java1234_小锋7 小时前
【吊打面试官系列-ZooKeeper面试题】zookeeper 是如何保证事务的顺序一致性的?
分布式·zookeeper·云原生
my19587021357 小时前
ZooKeeper分布式协调从入门到实战
分布式·zookeeper·云原生
oioihoii7 小时前
ZooKeeper 三节点集群部署:别再单机玩,高可用强一致集群这样搭
分布式·zookeeper·云原生
云游牧者9 小时前
K8S-Helm包管理全解-从入门到Chart开发实战指南
云原生·容器·kubernetes·helm·chart模板
codeejun9 小时前
每日一Go-66、K8s 蓝绿发布 & 金丝雀发布实战:Service 切流量 + Ingress 灰度一次讲透
开发语言·golang·kubernetes
口喜口喜9 小时前
K3s 安装笔记(CentOS 7.9)
kubernetes
Elastic 中国社区官方博客9 小时前
一个查询,无限 Elasticsearch Serverless 项目:跨项目搜索介绍
大数据·elasticsearch·搜索引擎·信息可视化·云原生·serverless·全文检索
思诺学长10 小时前
从0理解Feed流系统:技术原理、架构设计与实战指南
云原生