背景#

在 Kubernetes 环境中部署 Spring Boot 应用时,actuator 端点是跟着业务走同一个端口,还是通过 management.server.port 单独开一个管理端口,是微服务基础配置里经常被讨论的问题。

很多团队在推进容器化和可观测基线时,往往会经历两种方案并存的混乱期:

  • 一部分服务单独开了管理端口,但部署清单或 Helm chart 里的 ServiceMonitor 仍然去抓业务端口,导致 Prometheus 出现大量 TargetDown 告警;
  • 开了管理端口的服务把 liveness 和 readiness 探针直接指向管理端口,业务工作线程耗尽后探针却依然正常返回;
  • 部分应用配置了 server.servlet.context-path,如果共用业务端口,端点前缀会跟着变动,导致监控抓取路径难以统一维护。

单独开端口不仅仅是修改一行配置,它背后涉及 Spring Boot 的双容器运行时模型、Kubernetes 探针有效性,以及集群内部网络的边界管理。

本篇的配置项与行为按 Spring Boot 4.1 参考文档核对,与 3.4、3.5 版本的机制保持一致。

运行时机制:双 WebServer 与子上下文#

在 Spring Boot 中,一旦为 management.server.port 指定了与 server.port 不同的端口,框架底层的初始化逻辑就会发生分流。

此时 Spring Boot 不只是简单地增加了一个端口监听,而是通过 ManagementContextAutoConfiguration 创建了一个独立的子应用上下文(child ApplicationContext),并在其中启动了第二个独立的内嵌 Web 服务实例(如 Tomcat、Jetty 或 Undertow)。

flowchart TB subgraph K8S["Kubernetes 节点与控制面"] Ingress["Ingress / 网关"] Kubelet["Kubelet (探针)"] Prometheus["Prometheus (采集)"] end subgraph POD["Spring Boot Pod"] subgraph MAIN_SERVER["主 Web 服务 (server.port: 8080)"] MainConn["业务连接与工作线程池"] BizLogic["业务接口 (/api/*)"] ProbeEp["探针路由 (/livez, /readyz)"] end subgraph MGMT_SERVER["管理 Web 服务 (management.server.port: 9090)"] MgmtConn["独立连接与工作线程池"] ActuatorEp["Actuator 端点 (/actuator/prometheus)"] end end Ingress -->|"业务请求"| MainConn MainConn --> BizLogic Kubelet -->|"就绪 / 活性探测"| MainConn MainConn --> ProbeEp Prometheus -->|"拉取指标"| MgmtConn MgmtConn --> ActuatorEp

这个子上下文拥有独立的连接池、工作线程池和 Filter 链:

  • 子上下文可以访问父上下文(主应用)中注册的 Spring Bean;
  • 父上下文无法直接引用仅在管理子上下文中定义的 Bean;
  • 两个 Web 服务在物理线程与 TCP 监听套接字上彻底解耦。

Spring Boot 参考文档对这种隔离机制的说明如下:

If your Actuator endpoints are deployed on a separate management context, the endpoints do not use the same web infrastructure (port, connection pools, framework components) as the main application

理解这种双 Web 服务架构,是评估单独端口各项利弊的关键前提。

单独管理端口解决的问题#

单独管理端口主要带来三个层面的收益:网络暴露面隔离、指标路径标准化,以及高负载下的监控可用性。

阻断外部网关的直接访问#

在典型的 Kubernetes 入口网络中,Ingress Controller 或 API Gateway 只会将流量转发到指定的业务 ServicePort(如 8080)。

如果 actuator 与业务共用端口,/actuator/** 默认会与业务接口暴露在同一路径空间下。为了避免敏感信息泄漏,必须在入口层(Ingress 规则、WAF 或网关路由过滤器)逐一配置黑名单拦截规则,每次新增路由或服务时都存在遗漏的风险。

将 actuator 挪到单独的管理端口后,只要不对外发布该端口的 Service,外部流量在网络拓扑结构上就完全无法触达 actuator 端点。

指标路径与 context path 解耦#

当应用配置了 server.servlet.context-path 时,共用端口与单独端口在路径解析上有本质区别。文档指出了两者的计算基准:

Unless the management port has been configured to expose endpoints by using a different HTTP port, management.endpoints.web.base-path is relative to server.servlet.context-path (for servlet web applications) or spring.webflux.base-path (for reactive web applications). If management.server.port is configured, management.endpoints.web.base-path is relative to management.server.base-path.

如果共用业务端口,每个微服务如果 context path 不同,Prometheus 指标端点就会变成 /<context-path>/actuator/prometheus,ServiceMonitor 必须为每个服务单独指定 path。

而在单独管理端口下,管理服务直接以根路径启动(management.server.base-path 默认为空),所有应用的指标路径都可以统一为 /actuator/prometheus,集群维度的抓取配置无需为每个应用定制路径。

业务线程池打满时保留可观测通道#

当主应用的业务线程池由于数据库慢查询、外部依赖超时或突发流量打满时,主端口上的新 HTTP 连接会出现阻塞或拒绝。

由于管理端口运行在独立的内嵌 Web 容器上,拥有专属的 Acceptor 线程与 Worker 线程池,Prometheus 的定期指标抓取依然能够正常完成,运维人员也能通过 /actuator/metrics 或 /actuator/threaddump 调取实时堆栈信息进行排查,不至于因业务拥堵而失去系统的可观测性。

引入单独端口后的隐患与应对#

独立 Web 容器带来了运行隔离,同时也改变了流量拓扑,容易在探针、集群内部网络以及监控资源模型上引入新的盲区。

探针健康误报与 add-additional-paths 的路径规则#

管理端口拥有独立的连接与线程,这既是优势,也是探针配置中最隐蔽的陷阱。

如果直接将 Kubernetes 的 livenessProbe 和 readinessProbe 打在管理端口上(如 httpGet: { port: 9090, path: /actuator/health/readiness }),当主业务端口因死锁或线程耗尽无法接收任何业务流量时,管理端口的健康检查依旧会返回 HTTP 200。Kubernetes 控制面会判定该 Pod 处于健康状态,既不会在 Service 端点列表中摘除它,也不会重启容器,导致用户流量持续流入这个已无响应的节点。

文档明确警示了这种风险:

In this case, a probe check could be successful even if the main application does not work properly (for example, it cannot accept new connections). For this reason, it is a good idea to make the liveness and readiness health groups available on the main server port.

应对方式是开启额外的探针路径映射,将探针拉回主业务端口:

server:
  port: 8080
management:
  server:
    port: 9090
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  endpoint:
    health:
      probes:
        enabled: true
        add-additional-paths: true

配置 management.endpoint.health.probes.add-additional-paths: true 后,Spring Boot 会自动在主业务端口(server.port)上暴露快捷的 /livez 和 /readyz 端点。

需要注意的是端点与 server.servlet.context-path 的组合行为:因为这两个快捷端点是挂载在主业务 Web 服务下的,如果应用配置了 server.servlet.context-path: /myapp,kubelet 探测的完整路径必须是 /<context-path>/livez(如 /myapp/livez),直接请求根路径 /livez 会返回 404。

Kubernetes Deployment 中的探针应配置为访问主业务端口与对应路径:

livenessProbe:
  httpGet:
    path: /livez
    port: http
  initialDelaySeconds: 20
  periodSeconds: 10
readinessProbe:
  httpGet:
    path: /readyz
    port: http
  initialDelaySeconds: 15
  periodSeconds: 5

这样探针流量能够真实检验业务 Web 容器的连接接收与调度能力,管理端口则仅作为监控采集使用。

Kubernetes 采集拓扑:ServiceMonitor 与 PodMonitor#

单独开启管理端口后,基础设施维度的监控采集配置也需要相应调整。

在主流的 Prometheus Operator 体系下,有两种常见的抓取配置方式:

  1. 基于 ServiceMonitor 抓取:ServiceMonitor 是通过 Kubernetes Service 发现后端端点的。如果使用单独的管理端口,必须在应用的 Service 资源中显式定义 management 端口:

    apiVersion: v1
    kind: Service
    metadata:
      name: sample-app
    spec:
      ports:
        - name: http
          port: 8080
          targetPort: http
        - name: management
          port: 9090
          targetPort: management
    ---
    apiVersion: monitoring.coreos.com/v1
    kind: ServiceMonitor
    metadata:
      name: sample-app
    spec:
      endpoints:
        - port: management
          path: /actuator/prometheus
    

    三层之间靠统一的端口名称(如 management)串联。如果某个服务自行改用其他端口号或端口名,全局统一的 Helm 模板就容易失效。

  2. 基于 PodMonitor 抓取:如果团队不希望为了监控而在业务 Service 中增加 management 端口声明,可以使用 PodMonitor 直接通过 Pod IP 与容器端口进行采集:

    apiVersion: monitoring.coreos.com/v1
    kind: PodMonitor
    metadata:
      name: sample-app
    spec:
      podMetricsEndpoints:
        - port: management
          path: /actuator/prometheus
    

集群内部东西向访问与端点暴露最小化#

在云原生安全讨论中,依靠「换端口」实现的安全只是边界防护的第一步,并不等同于纵深防御。

单独的管理端口虽然不会被 Ingress 路由,但在 Kubernetes 集群内部,任何同集群甚至跨 namespace 的 Pod 在默认网络策略下依然可以直接通过 PodIP:9090 发起 TCP 请求。

若要真正保障管理端口的安全性,需要结合以下三层防护:

  1. 最小化暴露端点:生产环境严禁配置 management.endpoints.web.exposure.include: "*"。通常只暴露 health,info,prometheus。高风险端点(如 /actuator/env、/actuator/heapdump、/actuator/shutdown)一旦泄漏,攻击者可能借此读取配置中心凭证甚至触发远程执行。
  2. Kubernetes NetworkPolicy 限制:在支持 NetworkPolicy 的 CNI(如 Cilium 或 Calico)环境下,配置网络策略仅允许监控系统所在的 namespace(如 monitoring)访问 Pod 的 management 端口。
  3. 安全凭据与 Spring Security:如果管理端点包含了除基础指标之外的受限能力,仍需结合 Spring Security 配置独立的 SecurityFilterChain,或者通过 Service Mesh 的 mTLS 与授权策略进行请求鉴权。

超越 Spring Boot:多语言体系与不同规模团队的选择#

将「业务数据面」与「监控管控面」在网络端口与处理线程上物理隔离,是各类服务端应用架构在进程内部都需要面对的通用问题。

其他语言与基础软件的类似取舍#

在其他语言和基础中间件中,虽然底层运行时各异,但遇到的设计矛盾如出一辙:

  • Go 语言(net/http):社区常见的生产实践是在 main 函数中启动两个 http.Server。主端口(如 8080)挂载业务路由(如 Gin 或 gRPC),后台启动一个独立端口(如 9090)挂载 promhttp.Handler() 与 net/http/pprof。核心考量在于避免监控端点被业务的鉴权或限流中间件拦截,更在于防范 pprof(CPU 与内存火焰图分析)这类重开销诊断接口随业务路由暴露到公网。
  • Python(WSGI / ASGI):在 Gunicorn 或 Uvicorn 的多进程模型中,如果在业务工作进程中暴露指标,一旦工作进程被耗时的同步 I/O 或密集计算阻塞,Prometheus 抓取和健康检查就会超时。业界通常借助 prometheus_client.multiprocess 结合独立的后台导出进程,或者由应用直接将指标推送到本地 DaemonSet(如 OpenTelemetry Collector),避开在业务进程上拉取。
  • Node.js:由于单线程事件循环(Event Loop)机制,如果主线程正在进行繁重的 JSON 序列化或密集计算,即使在同一个 Node.js 进程中监听两个不同端口,管理端口依然会随事件循环一起阻塞。因此重度使用 Node.js 的团队往往需要借由 Worker Thread 导出,或通过外部 Sidecar 进行旁路探活。
  • Envoy 代理:Envoy 的架构是控制面与数据面分离的典型代表。处理客户端流量的 Listener(80 / 443 等)属于数据面,而指标(/stats/prometheus)、配置导出(/config_dump)和下线控制(/quitquitquit)严格收拢在独立的 Admin 端口(默认 9901)。因为该端口具备修改运行时日志和直接终止进程的能力,生产实践中通常要求将其绑定在 127.0.0.1 或通过 NetworkPolicy 严防死守。

不同公司的选择倾向与原因#

不同类型的团队在做这个取舍时,所受到的现实制约截然不同:

  • 金融与强合规机构(FinTech、银行):普遍强制要求物理分离管理端口。PCI-DSS、等保以及严格的内部合规审计,要求生产环境的管理控制接口与外部业务数据必须有清晰的网络隔离边界。相比于部署清单的复杂度,他们更在意「即便入口反向代理配置失误,内网管理端点也绝无可能被外网访问」的确定性。这种多端口维护成本通常由基础架构团队封装进统一的平台模板。
  • 大规模互联网平台(如大型电商、流媒体):普遍推行标准化的独立 Admin 端口或统一 Sidecar。在每秒数十万甚至数百万次请求的高并发场景下,业务线程池打满、慢查询堵塞连接是常见故障。如果监控探活与排障通道与业务共用,服务一旦过载,外部无法抓取指标和堆栈,内部健康探针发生误判,极易导致连环雪崩。因此「即便业务挂死,可观测通道也不能断」是其高可用架构的硬性底线。
  • 中小型研发团队与初创团队:则往往倾向于共用业务端口。在没有专职平台工程团队维护通用 Helm 模板时,管理端口带来的双端口映射、探针重定向和 ServiceMonitor 错位会带来明显的维护负担。中小型团队通常选择在 API 网关或 Ingress 层统一配置阻断规则(如拦截 /actuator/**),以较低的运维成本换取交付效率。

怎么选:决策矩阵与推荐配置#

两种端口模式的权衡要点对比如下:

评估维度 共用业务端口(server.port) 单独管理端口(management.server.port)
入口网络暴露 与业务接口混用,须在 Ingress/网关显式拦截 /actuator/** Ingress 仅暴露业务端口,管理端口天然不透出集群外
探针真实性 探针与业务请求共用连接与分发通道,能够如实反映容器状态 须配合 add-additional-paths 将探针打回业务端口,否则存在健康误报
指标路径一致性 受各服务 server.servlet.context-path 影响,不易全局统一 管理服务根路径独立,全局统一为 /actuator/prometheus
部署资源模型 一个端口对应一套 Service 与 ServiceMonitor,配置最轻量 容器、Service、ServiceMonitor 须同时维护两套端口声明
极端负载表现 业务线程耗尽时,指标采集容易发生超时丢点 独立内嵌 Web 容器,业务拥堵时监控抓取不受影响
运行时开销 单个 Web 容器实例,内存与基础线程开销最小 实例化两个 Web 容器,在轻量 Pod 场景下会增加一定内存底噪

选型建议#

  • 优先共用业务端口的场景:纯集群内调用的后端微服务、没有配置复杂的 context path、容器内存预算较为紧凑,且入口处有统一的 API 网关或 Ingress 集中阻断 /actuator/**。此时共用端口配置最简单,探针无需额外配置,也不存在多端口维护成本。
  • 推荐单独管理端口的场景:服务直接面向外部网络或 Ingress 路由较多、不同服务的 context path 差异较大、业务流量波动剧烈且需要确保故障时指标采集可用。在此场景下单独端口收益明显,但团队需要建立两项工程规范:
    1. 统一管理端口的名称与端口号(如统一命名为 management,端口统一为 9090),并在 Helm 通用模板中固化;
    2. 统一开启 management.endpoint.health.probes.add-additional-paths: true,并将 Kubernetes 的存活与就绪探针严格指向业务端口上的 /livez 与 /readyz。

小结#

  • 单独管理端口的核心机制在于拉起了一个独立的子上下文与第二个内嵌 Web 容器,换取了线程池隔离与路径规范化,代价是更高的部署协同要求与额外的内存开销。
  • 采用单独端口时,探针必须通过 add-additional-paths 映射回主业务端口;将探针直接指向管理端口会导致容器在业务端口假死时依然误报健康。
  • 单独端口是网络入口维度的隔离手段,不是访问控制的替代品;生产环境仍需严格收敛端点暴露范围,配合 NetworkPolicy 或应用层鉴权实现完整的纵深防护。
  • 数据面与管控面的端口分离是服务端工程的共性问题;是否启用取决于团队对高负载下观测韧性、合规边界与部署协同成本的综合权衡。

参考资料#

相关文章#