Actuator:端点权限、健康就绪与观测
生产可用性不是"接口通了":端点先要被启用和暴露,再经安全链授权,最后由健康、指标、追踪和审计各自回答不同问题。本文固定 Boot4.0.9 / Framework7.0.10 / JDK21,安全运行线为Security7.0.8,观测链为Micrometer1.16.8、micrometer-tracing-bridge-otel1.6.8与OTel SDK1.55.0。组件接线和用户Bean替换见集成正文,Security过滤链机制见Security原文;本篇只讲生产就绪这一层。全部观察来自boot-readiness实验的真实运行,验收记录见 docs/audits/java-depth-u24-verification.md。
可用、暴露与授权是三条链
一个端点要可达,依次经过:端点被创建(Bean存在)、被Web暴露(management.endpoints.web.exposure.include)、被当前安全链放行。三条链任何一条断开都给出不同响应码,不要用一个404/401概括。
Boot4的Web默认只暴露health。本票默认安全场景实测:匿名访问/actuator/health得到200且readiness组为UP;匿名访问/actuator/info是401——端点没被暴露本应是404,但默认管理安全链先要求认证;带默认ops凭据访问则确实404。所以"401还是404"同时透露了暴露与授权两层状态,排障要分层读。
原码:v4.0.9 ManagementWebSecurityAutoConfiguration节选。默认管理链只放行健康探针路径,其余全部要求认证:
@AutoConfiguration(before = ServletWebSecurityAutoConfiguration.class,
afterName = { "org.springframework.boot.health.autoconfigure.actuate.endpoint.HealthEndpointAutoConfiguration",
"org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration" })
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ RequestMatcher.class, WebEndpointAutoConfiguration.class })
@ConditionalOnDefaultWebSecurity
public final class ManagementWebSecurityAutoConfiguration {
@Bean
@Order(SecurityFilterProperties.BASIC_AUTH_ORDER)
SecurityFilterChain managementSecurityFilterChain(Environment environment, HttpSecurity http) {
http.authorizeHttpRequests((requests) -> {
if (ClassUtils.isPresent("org.springframework.boot.health.actuate.endpoint.HealthEndpoint",
getClass().getClassLoader())) {
requests.requestMatchers(healthMatcher(), additionalHealthPathsMatcher()).permitAll();
}
requests.anyRequest().authenticated();注意@ConditionalOnDefaultWebSecurity:一旦应用声明了自己的SecurityFilterChain,这条默认管理链整体退让(实验中defaultManagementChain由true变false),健康路径的permitAll也不再由Boot兜底。探针在Boot4默认开启:AvailabilityProbesAutoConfiguration对management.endpoint.health.probes.enabled使用matchIfMissing=true,无需Kubernetes环境或手工开关,/actuator/health/readiness、/actuator/health/liveness分组随健康端点暴露。
自定义安全链必须覆盖管理与业务入口
自定义链接管后,管理端点和业务接口由同一份授权规则裁决。本票custom-security场景的规则是:/actuator/health/**、/livez、/readyz、/lab/open、/lab/gated放行,/actuator/**与/lab/**要求OPS角色,其余要求认证,HTTP Basic。
实测矩阵:匿名或错误口令访问/actuator/info是401(未通过认证),reader凭据是403(通过认证但无OPS角色),ops凭据是200。三个码对应认证失败、授权失败、授权通过,排障时先分清在哪一层失败,再改规则或凭据。
写自定义链时最容易漏的是业务兜底:只写/actuator/**规则会让其余请求落入anyRequest,如果忘了authenticated()或误配permitAll,业务接口等于开放。默认用户源(inMemoryUserDetailsManager)同样会随@ConditionalOnDefaultWebSecurity退让,自定义链要自己提供UserDetailsService或外部身份源;实验中自定义ops/reader用户后defaultUsers为false。CSRF、会话固定与凭据存储的机制细节回Security原文。
敏感值、写操作与缓存
env端点默认不回显值,management.endpoint.env.show-values=never是显式收紧;实验在配置里放了合成秘密lab.synthetic-secret=synthetic-private-do-not-return,授权OPS访问/actuator/env的完整JSON中确认该值不存在。脱敏是展示层责任,不改变值在Environment中的存在与绑定,也不是凭据管理方案——生产凭据仍应经外部注入。
健康明细同样分级:management.endpoint.health.show-details=when-authorized配合management.endpoint.health.roles=OPS后,匿名和reader的/actuator/health只有顶层status没有components,ops才有各indicator明细。这避免健康明细向未授权方泄露内部拓扑。
端点不只读。自定义@Endpoint(id="labcontrol")提供@ReadOperation与@WriteOperation,实验枚举到READ与WRITE两类操作;写操作经授权后仍会改变状态(计数器递增),把它暴露到公网等于开放管理面。info端点可缓存(management.endpoint.info.cache.time-to-live),指标与env等也可配置缓存;缓存换来了读放大下降,代价是观察滞后,验证时不能拿缓存响应当作"刚发生的证据"。
监听、started 与 ready 的时间线
Boot生命周期分四段:WebServerInitializedEvent(业务口与可能的管理口先后listening)、ApplicationStartedEvent(started)、ApplicationRunner执行、ApplicationReadyEvent(ready)。实验用受控runner卡在started与ready之间,实测事件序:business-listening、management-listening都早于started,started早于runner-enter,release文件出现后runner退出、ready最后出现。
这段时间线解释了"端口已开但未就绪":started时容器已接受连接,但runner未跑完、ApplicationReadyEvent未发布,readiness仍是REFUSING_TRAFFIC。实测此时/actuator/health/readiness返回503 OUT_OF_SERVICE,/lab/gated返回503,而liveness是200 UP——进程活着,只是还没准备好接流量。
release后/actuator/health/readiness变为200 UP,/lab/gated变200。就绪切换的代价极小,但前提是业务真的把初始化放在runner/ready之前完成;把重初始化挪到首个请求再做的"懒加载"会让readiness说谎,LB据此放流量就会打出慢请求。
业务依赖与两种探针的责任
liveness回答"进程是否该被重启",readiness回答"是否该接流量",两者都由ApplicationAvailability的状态机驱动,但对应不同状态。实验在同一受控时点实测:liveness组200 UP(CORRECT),readiness组503 OUT_OF_SERVICE(REFUSING_TRAFFIC)。
Boot4把可用性枚举映射为标准健康Status,不是把枚举名当状态字符串返回。原码:v4.0.9 ReadinessStateHealthIndicator节选。
public class ReadinessStateHealthIndicator extends AvailabilityStateHealthIndicator {
public ReadinessStateHealthIndicator(ApplicationAvailability availability) {
super(availability, ReadinessState.class, (statusMappings) -> {
statusMappings.add(ReadinessState.ACCEPTING_TRAFFIC, Status.UP);
statusMappings.add(ReadinessState.REFUSING_TRAFFIC, Status.OUT_OF_SERVICE);
});
}
@Override
protected AvailabilityState getState(ApplicationAvailability applicationAvailability) {
return applicationAvailability.getReadinessState();
}
}要读枚举本身,用ApplicationAvailability.getReadinessState()(实验的/lab/open返回REFUSING_TRAFFIC即此来源)。探针之外,整体health还聚合各HealthIndicator:实验注册了依赖检查indicator,真实GET合成依赖,body异常即DOWN并带failure detail;依赖宕机把整体health拖成DOWN,但不会自动改readiness——把外部依赖纳入探针需要业务显式发布AvailabilityChangeEvent,不能指望health聚合替你摘流。
management.endpoint.health.probes.add-additional-paths=true额外在业务端口挂出/livez与/readyz,实测匿名200 UP;这是给不经管理端口的负载均衡器用的路径,与/actuator/health/readiness是同一状态的两个入口。
管理端口不能代替业务端口
management.server.port把actuator挪到独立端口,实验用--management.server.port=0实测业务口与管理口是两个不同端口。分离的价值是网络层隔离:管理面可以只对内网开放,业务面保持最小暴露。
但管理端口活着不等于业务就绪——两个端口同属一个进程、同一条时间线,readiness在ready前同样503;实验同时观察到业务口/lab/open放行、readiness探针在管理口拒绝。反过来,业务端口的/livez//readyz(add-additional-paths)也不依赖管理端口存在。K8s的liveness/readinessProbe该指向哪个口,取决于隔离拓扑,而不是"管理口更标准"的错觉;错误指向会让探针测到错误的面。
指标:真实计数与标签预算
指标的价值在于计数真实。实验三次调用/lab/count后读取/actuator/metrics/u24.orders.accepted,COUNT实测为3、tagoutcome=accepted——这是业务代码meters.counter(...).increment()的真实累积,不是start自带的JVM指标。读取端点只做聚合展示,产出在埋点处;没有埋点的行为不会凭空出现在指标里。
标签要按低基数设计:outcome这类有限枚举可以,order id、用户名、URL全参数不可以——每个唯一组合都是一个新的meter,MeterRegistry无界基数会同时吃内存和后端存储。Boot4的management.metrics.use-global-registry=false可以关掉向全局registry的自动注册,本票借此只保留显式registry,避免测试进程污染共享后端。导出(OTLP/Prometheus等)是另一条链:本机关闭OTLP导出不代表生产关闭,指标语义与导出通道分别验收。
追踪:Builder、采样与结束的 Span
追踪要成立需要三件事:入口接住上游trace上下文、出口把上下文带给下游、span真正结束并被导出。实验注入traceparent,traceId 111…1被SERVER span继承,parentId是上游span 222…2;通过自动配置的RestClient.Builder发起的出口调用在下游实测收到同一traceId的traceparent,且Builder上的customizer header(X-U24-Client: auto-customizer)一并到达——观测customizer是Builder配置路径的一部分,见集成正文。
对照实验:RestClient.create()裸客户端同一调用,下游traceparent与customizer header均为空。换了客户端就丢观测不是玄学,是绕过了Boot的Builder配置链;想保留就注入自动Builder或用RestClientBuilderConfigurer。
采样决定哪些trace被保留:management.tracing.sampling.probability=1.0全采样用于本票断言,生产通常低得多——"这条请求没有trace"优先怀疑采样,其次才是怀疑传播。span结束是异步的:实验通过SDK的forceFlush后在进程内exporter读到的全部span都ended=true,含SERVER与CLIENT。没有flush/导出,span可能还在内存里,"看不到"不等于"没发生"。跨进程traceId一致的完整证据是下游真实收到header,而非本地日志拼接。
审计:事件收集与持久责任
审计端点聚合认证、授权与业务自定义事件。实验一次错误口令、一次reader越权、一次正常登录后,/actuator/auditevents实测包含AUTHENTICATION_FAILURE、AUTHORIZATION_FAILURE、AUTHENTICATION_SUCCESS,以及业务通过AuditApplicationEvent发布的自定义U24_ORDER_REPLAY——审计协议对业务事件开放,不限于Security内置类型。
默认InMemoryAuditEventRepository有容量上限(本票显式设为64)且重启即失,只适合观察与测试;生产合规需要JDBC/JPA等持久实现并自证不可抵赖。认证成功/失败事件由Security的audit listener产生,但何时记、记什么主体,取决于认证链真实执行;绕过认证器的测试路径不会产生真实审计事件。审计是事后追责证据,不是实时入侵检测,也不代替业务日志。
构建信息、镜像与部署证据
spring-boot-maven-plugin的build-info目标生成META-INF/build-info.properties,BuildProperties随之可用;实测/actuator/info返回build.artifact=readiness-server、group、version、time。这不是手写常量,是构建产物自带的可核对元数据——线上拿到一个jar,info端点能回答"这是哪次构建",与镜像tag、SBOM、签名共同构成部署证据链。
镜像与平台是另一层:Buildpacks/分层jar解决的是镜像构建效率与可复现,K8s探针、摘流、ConfigMap是运行平台的责任。本票只验证进程内证据(info、健康、观测),不声明镜像扫描、签名策略或滚动发布正确;它们由后续部署票承接。把info当版本号唯一来源也有风险:缓存(上一节的TTL)与旧实例未下线都会让info滞后于真实部署。
受控实验与失败恢复
本票实验的原则:每个断言来自真实运行,失败路径与成功路径同样被验证。健康indicator的DOWN分支、错误口令的401、越权的403、readiness的503、裸客户端的无trace,都是主动构造的负例,不是只测happy path后"推测"边界。
恢复验证同样真实:受控runner的release文件出现后,readiness从503回到200、gated恢复200;依赖fixture从503恢复后health回升。实验进程由运行器负责终止并wait(强杀视为失败),临时release文件在finally中删除;这些清理是实验可信度的一部分——泄漏的进程和文件会让下一次运行的观察混入旧状态。
复跑入口是JAVA_HOME=$(/usr/libexec/java_home -v21) python3 labs/boot-readiness/run.py,证据JSON记录命令、时间、退出码、制品哈希与全部观察;开发期日志留在cache,不混入归档。
架构师变化条件:门禁、预算与恢复
引入Actuator前先回答三个问题:暴露面最小到什么程度(默认只health,逐项加白名单而不是*);谁有权访问管理面(独立端口+网络隔离+角色分级,还是与管理同口+强认证);预算在哪里(指标基数上限、审计存储量、trace采样率与后端容量)。三者都没有默认正确值,随规模变化要重新算。
故障与升级时,观测面本身也要被设计:指标后端宕机时业务是否降级(use-global-registry=false式的解耦即是防御);trace采样在高故障率下是否要动态调高;审计持久化失败是阻塞业务还是异步丢弃——丢审计通常是合规风险,阻塞业务是可用性风险,取舍要显式写进设计。升级Boot版本时优先复跑探针语义(本票已见Boot4把状态映射为UP/OUT_OF_SERVICE)、默认暴露与安全链退让行为,而不是默认"Actuator向后兼容"。就绪摘流的最终责任在平台:readiness只提供信号,LB/K8s执行动作;信号与动作之间的延迟(缓存、轮询间隔)是演练要测的真实指标。
闭卷验收
本页 14 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。