跑通运维港——Docker、K8s 、普罗米修斯

如果把运维比作一座港口,那 Docker 是集装箱,Kubernetes 是调度系统,Prometheus 是监控室,其组件Grafana 就是监控室的大屏。本文用这套比喻,帮你一次性串起云原生运维核心技术。

一、Docker —— 集装箱

在没有集装箱的年代,货物散着搬:水果要冷藏、衣服怕潮、化工品要隔离,每种货都得单独安排装卸方式。软件部署也一样——"在我电脑上能跑啊"这句话之所以经典,就是因为每台机器的操作系统版本、依赖库、环境变量都不一样。

Docker 做的事情就是把你的应用连同它需要的一切(代码、运行时、依赖、配置)打包成一个标准集装箱,这个集装箱叫镜像(Image)。不管里面装的是 Java 服务还是 Python 脚本,只要集装箱规格统一,任何一台装了 Docker 的机器都能原封不动地把它跑起来。

几个关键概念对应到港口:

  • 镜像(Image):集装箱的设计图纸,只读模板,可以复制无数份。
  • 容器(Container):按图纸造出来、正在运输中的那个实体集装箱,是镜像的运行实例。
  • Dockerfile:造箱子的工序说明书,写明先装什么后装什么、暴露哪个端口、启动命令是什么。
  • Registry(仓库):集装箱堆场,Docker Hub 就是最大的公共堆场,私有仓库则是企业自己的堆场。

一个最简 Dockerfile 长这样:

FROM openjdk:17-slim
COPY target/app.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]

四行代码,一个集装箱就造好了。docker build -t my-app . 打包,docker run -p 8080:8080 my-app 开箱即用。

镜像分层:为什么构建那么快?

Docker 镜像不是一个整块文件,而是由一层层只读层(Layer)叠起来的。Dockerfile 中每一条指令(FROM、RUN、COPY……)都会产生新的一层。构建时 Docker 会缓存每一层,如果某条指令和上次完全一样且输入没变,就直接复用缓存,不重新执行。

这就是为什么你应该把变化频率低的指令放前面(比如安装依赖),把变化频繁的放后面(比如 COPY 你的代码)。这样改一行业务代码,前面几十层依赖安装全部命中缓存,构建从几分钟缩到几秒。

FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline        # 依赖层,很少变,缓存命中率高
COPY src ./src
RUN mvn package -DskipTests          # 代码层,经常变

FROM eclipse-temurin:17-jre-alpine   # 最终镜像只保留运行时
COPY --from=build /app/target/*.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

上面这段还展示了多阶段构建(Multi-stage Build):第一阶段用完整的 Maven 环境编译,第二阶段只拷贝一个 jar 到精简的 JRE 镜像里。最终镜像不包含 Maven、源码、编译中间产物,体积可以从 800MB 缩到 100MB 出头。生产环境只跑最终镜像,又小又安全。

容器网络:箱子之间怎么通信?

Docker 默认使用 bridge 模式:每个容器分配一个虚拟 IP,挂在 docker0 网桥下,容器之间通过 IP 互通,对外通过端口映射(-p 8080:8080)暴露。还有 host 模式(容器直接共享宿主机网络栈,性能最好但没有隔离)和 none 模式(完全断网,适合纯计算任务)。

实际项目中多个容器需要协作(比如应用 + MySQL + Redis),手动 docker run 三次太麻烦,这时候用 docker-compose

version: "3.8"
services:
  app:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      - mysql
      - redis
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root123
    volumes:
      - mysql-data:/var/lib/mysql
  redis:
    image: redis:7-alpine
volumes:
  mysql-data:

一个 docker-compose up -d 三个容器全部拉起,互相之间通过服务名(mysqlredis)直接访问,不用记 IP。

数据卷:集装箱里的东西不能丢

容器本身是临时的,删了重建后里面写的数据就没了。但数据库文件、上传的图片这些必须持久化。Docker 的解决方案是数据卷(Volume):把容器内的某个目录挂载到宿主机上,容器销毁后数据依然在。

docker run -v /host/data:/container/data my-app

上面 -v 左边是宿主机路径,右边是容器内路径。也可以用命名卷(-v my-volume:/container/data),Docker 自己管理存储位置,跨容器共享更方便。

二、Kubernetes —— 港口调度系统

一个集装箱好管理,但当你有几百上千个集装箱、几十台起重机(服务器)的时候,问题就来了:哪个箱子放哪台机器?某台起重机坏了箱子怎么转移?双十一货量暴增怎么临时加箱子?

Kubernetes(简称 K8s)就是这套港口调度系统。你不用关心具体哪台机器在跑哪个容器,只需要告诉 K8s:“我要 3 个副本的订单服务、2 个副本的支付服务”,它自己决定放在哪、挂了怎么拉起来、流量怎么分。

核心概念:

  • Pod:最小的调度单位,可以理解为"一组绑在一起运输的集装箱"。通常一个 Pod 里跑一个主容器。
  • Deployment:声明"我要几个 Pod、用什么镜像、怎么滚动更新"。K8s 会持续保证实际状态和你声明的一致——少了就补,多了就杀,镜像版本不对就滚动替换。
  • Service:给一组 Pod 一个固定的访问入口(虚拟 IP + 端口),不管背后的 Pod 怎么漂移、扩缩,外部始终通过 Service 访问。
  • Node:港口里的一台起重机(一台物理机或虚拟机),Pod 被调度到 Node 上运行。

一个典型的 Deployment 声明:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order
          image: my-registry/order-service:1.2.0
          ports:
            - containerPort: 8080

你只管写"我要 3 个",K8s 负责让现实永远等于这个期望。这就是声明式 API 的魅力。

调度流程:箱子怎么被分配到起重机上?

当你 kubectl apply 一个 Deployment 后,背后发生的事情大致是:API Server 收到请求写入 etcd → Controller Manager 发现"期望 3 个 Pod 但实际 0 个",创建 3 个 Pod 对象(此时状态为 Pending)→ Scheduler 根据一系列打分规则(资源够不够、亲和性、污点容忍等)给每个 Pod 选一个最合适的 Node → 选中 Node 上的 kubelet 拉镜像、启动容器。

整个过程你不需要干预,但理解这个流程能帮你看懂 kubectl describe pod 输出中的 Events 信息——比如 Pod 一直 Pending,多半是"没有 Node 满足资源请求"。

Pod 生命周期与探针

一个 Pod 从创建到销毁会经历:Pending(等待调度/拉镜像)→ Running(至少一个容器在跑)→ Succeeded / Failed(终态)。

但"Running"不代表"能正常服务"。容器可能启动了但应用还在初始化,也可能进程没挂但已经死锁了。K8s 用三种探针(Probe) 来解决这个问题:

  • startupProbe:应用启动慢(比如 Java 要 30 秒预热),在它通过之前,其他探针不生效,防止应用还没启动完就被杀掉。
  • livenessProbe:定期检测"进程还活着吗"。连续失败 N 次,K8s 会重启容器。典型场景:应用死锁了,进程还在但不响应。
  • readinessProbe:检测"能接流量吗"。失败时 Pod 从 Service 的 Endpoints 中摘除,不再接收新请求,但不会重启。典型场景:依赖的下游服务暂时不可用。
containers:
  - name: order
    image: my-registry/order-service:1.2.0
    ports:
      - containerPort: 8080
    startupProbe:
      httpGet:
        path: /actuator/health
        port: 8080
      failureThreshold: 30
      periodSeconds: 2
    livenessProbe:
      httpGet:
        path: /actuator/health/liveness
        port: 8080
      periodSeconds: 10
    readinessProbe:
      httpGet:
        path: /actuator/health/readiness
        port: 8080
      periodSeconds: 5

Spring Boot Actuator 天然暴露这些端点,配合 K8s 探针几乎是零成本接入。

配置管理:ConfigMap 与 Secret

集装箱里不应该硬编码配置(数据库地址、API Key 等),否则换个环境就得重新造箱子。K8s 的做法是把配置抽出来:

  • ConfigMap:存放非敏感配置(数据库 URL、开关参数),可以挂载为文件或注入为环境变量。
  • Secret:存放敏感信息(密码、Token),Base64 编码存储(注意:不是加密,生产环境建议配合 RBAC 或外部密钥管理系统)。
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  DB_URL: "jdbc:mysql://mysql-svc:3306/orders"
  LOG_LEVEL: "info"

Pod 中引用:

envFrom:
  - configMapRef:
      name: app-config

这样同一个镜像在开发、测试、生产环境跑,只需要换 ConfigMap,镜像本身不用动。

Ingress:港口的对外大门

Service 解决了集群内部访问的问题,但外部用户(浏览器、App)怎么进来?答案是 Ingress——它相当于港口的大门 + 前台,根据域名和路径把请求路由到不同的 Service:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: main-ingress
spec:
  rules:
    - host: order.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: order-service
                port:
                  number: 8080
    - host: pay.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: pay-service
                port:
                  number: 8080

Ingress 本身只是一条规则,真正干活的是 Ingress Controller(比如 Nginx Ingress Controller、Traefik),它监听 Ingress 资源变化并配置反向代理。

HPA:货量暴增时自动加箱子

双十一流量来了,手动 kubectl scale 太慢。K8s 的 HPA(Horizontal Pod Autoscaler) 可以根据 CPU 利用率、内存、甚至自定义指标自动扩缩 Pod 数量:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

含义:当 order-service 的平均 CPU 利用率超过 70% 就扩容,最多扩到 20 个 Pod;流量下去后自动缩回最少 3 个。整个过程全自动,你只需要确保 Pod 设置了 resources requests(否则 HPA 算不出利用率)。

三、Prometheus —— 港口监控室

港口跑起来了,但你不能靠肉眼盯着几百台起重机。你需要一个监控室,里面有人 7×24 小时记录每台设备的转速、温度、负载,一旦异常就拉警报。

Prometheus 就是这个监控室。它的工作方式是主动去拉(Pull)数据:每个服务暴露一个 /metrics 接口,Prometheus 每隔 15 秒(可配置)去采集一次,把数据存进自己的时序数据库。

几个关键点:

  • Exporter:装在设备上的传感器。比如 Node Exporter 采集机器 CPU/内存/磁盘,cAdvisor 采集容器指标,应用自己也可以暴露业务指标。
  • PromQL:监控室的查询语言。比如 rate(http_requests_total[5m]) 表示最近 5 分钟每秒请求数,up == 0 表示某个目标挂了。
  • Alertmanager:警报广播站。你在 Prometheus 里配规则(比如"CPU 超过 90% 持续 5 分钟"),触发后交给 Alertmanager 去发钉钉、发邮件、打电话。

一条简单的告警规则:

groups:
  - name: basic
    rules:
      - alert: HighCpuUsage
        expr: 100 - (avg by(instance)(irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "CPU 使用率超过 90%"

时序数据模型:监控室怎么记录数据?

Prometheus 存储的不是表格,而是时间序列(Time Series)。每一条时间序列由"指标名 + 一组标签"唯一标识,后面跟着一串按时间排列的数值点。比如:

http_requests_total{method="GET", handler="/orders", status="200"}
  → [1720000000, 1024] [1720000015, 1031] [1720000030, 1045] ...

标签(Label)是 Prometheus 数据模型的核心。同一个指标名加上不同标签组合,就是不同的时间序列。查询时你可以按标签过滤、聚合、分组,这就是 PromQL 强大的基础。

四种指标类型:传感器有不同种类

Prometheus 定义了四种 Metric 类型,对应不同的测量场景:

  • Counter(计数器):只增不减的累计值。比如请求总数、错误总数。查询时通常配合 rate() 算每秒速率,因为绝对值本身意义不大。
  • Gauge(仪表盘):可增可减的瞬时值。比如当前内存使用量、在线连接数、温度。直接看当前值就有意义。
  • Histogram(直方图):把观测值分到预定义的桶(bucket)里计数。比如请求耗时分布:多少请求 < 100ms、多少在 100ms~500ms、多少 > 1s。适合算 P99 延迟。
  • Summary(摘要):在客户端直接计算分位数(quantile)。和 Histogram 类似但分位数在采集端算好,服务端不能重新聚合。

实际开发中最常用的是 Counter 和 Histogram。Spring Boot Actuator + Micrometer 默认就会暴露 JVM 内存(Gauge)、HTTP 请求数(Counter)、请求耗时(Histogram)等指标,加个依赖就有。

服务发现:传感器太多怎么自动登记?

手动在 prometheus.yml 里一个个写 target 地址,服务一多就崩溃了。Prometheus 支持服务发现(Service Discovery),自动从各种来源获取要采集的目标列表:

scrape_configs:
  - job_name: 'kubernetes-pods'
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
        action: keep
        regex: true
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
        action: replace
        target_label: __metrics_path__

在 K8s 环境中,Prometheus 通过 API Server 自动发现所有带 prometheus.io/scrape: "true" 注解的 Pod,新服务上线、旧服务下线都自动感知,不用改配置重启。

Recording Rules:把常用查询预计算好

有些 PromQL 很复杂(比如按服务聚合的 P99 延迟),每次打开 Grafana 都现算一遍太慢。Recording Rules 让你把复杂表达式预先计算成一条新的时间序列,查询时直接读结果:

groups:
  - name: recording
    rules:
      - record: job:http_request_duration_seconds:p99
        expr: histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (job, le))

之后在 Grafana 里直接查 job:http_request_duration_seconds:p99 就行,响应速度从几秒变成毫秒级。

Prometheus 只管采集、存储和告警,数据怎么好看地展示出来,它不管——那是下一位的事。

四、Grafana —— 监控室大屏

监控室里光有数据不行,领导来视察你得有大屏。Grafana 就是这块大屏。

它本身不存数据,而是对接 Prometheus(以及 Elasticsearch、MySQL、InfluxDB 等几十种数据源),用 PromQL 查出数据后渲染成折线图、仪表盘、热力图、拓扑图。

实际使用中你大概率不会从零画面板,Grafana 社区有海量现成模板(Dashboard),导入一个 ID 就能用。比如导入 1860(Node Exporter Full),一整台机器的 CPU、内存、磁盘、网络指标就全部铺好了。

它还能配告警通知(和 Alertmanager 互补),支持按团队/项目分文件夹管理面板,支持变量下拉框让你在一个面板里切换查看不同服务、不同环境的指标。

五、四件套怎么串起来

把整座港口连起来看,数据流是这样的:

开发者写好代码
  → Docker 打包成镜像,推到 Registry(造好集装箱,送进堆场)
    → K8s 从 Registry 拉镜像,调度到 Node 上跑起来(调度系统安排起重机吊装)
      → 每个 Pod 暴露 /metrics 接口(每台设备装上传感器)
        → Prometheus 定时拉取指标存入时序库(监控室记录数据)
          → Grafana 从 Prometheus 查数据渲染大屏(大屏实时展示)
          → Alertmanager 触发告警通知到人(警报响了,运维去处理)

一句话总结:Docker 解决"怎么打包",K8s 解决"怎么跑",Prometheus 解决"怎么看",Grafana 解决"怎么展示"。四者各司其职,组合起来就是一套完整的云原生运维底座。

六、初学者进阶路线

如果你刚接触这套技术栈,不要试图一口气全学会。按阶段来,每个阶段都有一个可交付的"小成果",正反馈会推着你往下走。

第一阶段:Docker 入门(1~2 周)

目标是把自己的一个 Spring Boot 或 Node.js 项目容器化跑起来。具体做这几件事:写一个 Dockerfile,理解每条指令的作用;用 docker build 构建镜像,用 docker run 跑起来并验证端口映射;试试多阶段构建,对比一下镜像体积的变化;用 docker-compose 把应用 + MySQL + Redis 一起编排起来,体会"一键拉起整套环境"的爽感。

这个阶段结束后,你应该能回答:镜像分层缓存是怎么回事?容器和虚拟机有什么区别?数据卷解决了什么问题?

第二阶段:K8s 基础操作(2~3 周)

用 Minikube 或 Kind 在本地起一个单节点集群(别一上来就搭多节点,本地够用了)。然后按这个顺序练:kubectl run 跑一个 Pod → 写 Deployment YAML 管理副本 → 创建 Service 暴露访问 → 配置 ConfigMap 注入环境变量 → 给 Pod 加上 liveness 和 readiness 探针 → 写一个 Ingress 按域名路由。

每做一步就用 kubectl get podskubectl describe podkubectl logs 去观察状态变化。这个阶段的核心收获是理解"声明式"思维:你描述期望状态,K8s 负责让现实收敛到期望。

第三阶段:监控体系搭建(1~2 周)

给应用加上 Micrometer + Actuator 依赖,确认 /actuator/prometheus 端点能输出指标。然后本地用 docker-compose 起 Prometheus + Grafana(社区有现成的 compose 文件),配置 Prometheus 抓取你的应用,在 Grafana 里导入 Spring Boot 模板面板(ID: 12900),看到自己应用的 QPS、延迟、JVM 内存曲线。

再进一步:写一条告警规则(比如"5xx 错误率 > 5% 持续 2 分钟"),接上 Alertmanager 发到自己的邮箱或钉钉。当告警真的响起来的时候,你会对整套监控体系有非常具象的理解。

第四阶段:K8s 进阶(持续)

有了前面的基础,可以逐步深入:HPA 自动扩缩容、资源 requests/limits 与 QoS 等级、RBAC 权限控制、Helm Chart 打包部署、Namespace 多环境隔离、PV/PVC 持久化存储、NetworkPolicy 网络策略。这些不需要一次学完,在实际项目中遇到哪个就学哪个。

第五阶段:生产级运维(长期)

到了这个阶段,关注点从"怎么跑起来"变成"怎么跑得稳":CI/CD 流水线(GitLab CI / Jenkins / ArgoCD)实现代码提交到自动部署;日志体系(EFK 或 Loki)补齐可观测性的另一块拼图;链路追踪(Jaeger / SkyWalking)定位分布式调用链中的瓶颈;Prometheus 高可用(Thanos / VictoriaMetrics)解决长期存储和跨集群查询。

一些学习资源推荐

Docker 官方文档的 Get Started 部分写得非常友好,跟着做一遍就够入门了。K8s 推荐先读官方文档的 Concepts 部分建立全局认知,再跟着 “Kubernetes the Hard Way” 或者 B 站上尚硅谷/黑马的 K8s 教程动手。Prometheus 和 Grafana 的官方文档都有很好的入门教程,另外《Prometheus 监控实战》这本书适合系统性学习。

最重要的一条建议:一定要动手。看十遍文章不如自己 kubectl apply 一次然后看它报错、排查、修复。运维这东西,踩坑踩出来的理解远比看来的深刻。

更多推荐