第 12 篇:Kubernetes 滚动发布、探针与优雅停机
0. 本篇定位
这是容器化后端工程实践系列的第 12 篇。前面几篇已经讨论了镜像、配置、网络、存储和 Kubernetes 基础对象;本篇关注一个更贴近生产事故的问题:新版本如何上线、旧版本如何退出、流量如何切换、失败后如何回滚。
本文不把 Kubernetes 发布讲成命令速查,而是围绕一条后端服务发布链路展开:Deployment 创建新的 ReplicaSet,RollingUpdate 根据 maxSurge 和 maxUnavailable 控制替换速度,startupProbe 保护慢启动,readinessProbe 决定是否接流量,livenessProbe 处理不可恢复的进程异常,Pod 终止时通过 SIGTERM、preStop、terminationGracePeriodSeconds 和应用自身的 graceful shutdown 完成连接排空。
读完后你应该能回答四类问题:发布参数如何影响可用性和容量;三类探针应该分别检查什么;Pod 下线时请求为什么仍可能中断;发布失败时应该沿着哪些 Kubernetes 对象、事件、日志和指标建立证据链。
1. 问题背景
后端服务上线失败,很多时候不是代码完全不可用,而是发布过程中的边界没有对齐。常见现象包括:新 Pod 刚启动就进入 Service endpoints,数据库连接池还没初始化完就接到请求;livenessProbe 配得太激进,把慢启动的应用反复杀掉;旧 Pod 收到终止信号后立刻退出,正在处理的请求被中断;发布卡住后只看到 rollout status 没结束,却不知道该看 ReplicaSet、Pod event、probe 失败还是资源调度。
这些问题的共同点是:它们发生在代码、运行时、Kubernetes 控制器和流量入口的交界处。单独看应用日志,可能只看到连接失败;单独看 Pod 状态,可能只看到 NotReady 或 CrashLoopBackOff;单独看网关日志,可能只看到短暂 502/503。真正的排障需要把这些证据串起来。
本篇文章的核心判断是:发布不是把镜像换掉,而是把“新实例可接流量”和“旧实例可安全退出”这两个条件表达清楚。Deployment strategy 决定替换节奏,探针决定实例状态,优雅停机决定退出质量,rollout/rollback 命令决定变更可追踪和可恢复。
2. 核心概念
2.1 Deployment strategy:发布节奏的声明
Deployment 的 strategy 描述新旧 Pod 如何替换。默认类型是 RollingUpdate,它不会一次性删除所有旧 Pod,而是在控制器循环中逐步创建新 Pod、等待新 Pod Ready、再缩减旧 ReplicaSet。这个过程的目标是让期望副本数、实际副本数、可用副本数逐步收敛。
maxSurge 表示滚动发布期间可以额外创建多少 Pod,解决的是“先加新实例再删旧实例”的容量问题。maxUnavailable 表示发布期间最多允许多少 Pod 不可用,解决的是“最低可用容量”的问题。两者不是越大越好:maxSurge 太大会瞬间放大资源和下游连接压力,maxUnavailable 太大会降低服务冗余。
例如 4 副本服务使用 maxSurge: 1、maxUnavailable: 0 时,发布期间最多 5 个 Pod,且旧版本不会在新 Pod Ready 前减少可用容量;如果使用 maxSurge: 0、maxUnavailable: 1,控制器会先减少一个旧 Pod 再创建新 Pod,资源更省,但发布窗口内可用容量会下降。
2.2 三类探针:启动、接流量、存活
startupProbe、readinessProbe、livenessProbe 经常被混用,但它们的职责不同。startupProbe 只回答“应用是否已经完成启动阶段”,启动探针成功前,其他探针不会生效,适合保护 JVM 冷启动、缓存预热、迁移检查等慢启动场景。
readinessProbe 回答“这个 Pod 当前是否可以接收业务流量”。它失败时,Pod 不会被杀死,而是会从 Service endpoints 中移除。后端服务的 readiness 应该检查本实例能否处理请求,例如 HTTP 服务端口已监听、必要配置已加载、关键依赖处于可接受状态。不要把所有下游短暂抖动都做成强依赖,否则会造成集体摘流。
livenessProbe 回答“这个容器是否已经进入不可恢复状态,需要重启”。它失败会触发 kubelet 重启容器,所以要非常克制。典型 liveness 检查应该判断进程是否死锁、事件循环是否卡死、核心线程是否不可恢复,而不是检查数据库、Redis 或外部 API 的瞬时可用性。
2.3 优雅停机:Kubernetes 与应用的协作
Pod 终止不是一个瞬间动作。Kubernetes 删除 Pod 时,会把 Pod 标记为 Terminating,并从 endpoints 中移除;kubelet 会执行 preStop hook,然后向容器主进程发送 SIGTERM;如果在 terminationGracePeriodSeconds 内进程没有退出,才会发送 SIGKILL。
这意味着优雅停机至少包含两层。Kubernetes 层负责停止新流量进入、给应用一个退出窗口;应用层负责收到 SIGTERM 后停止接收新请求、等待存量请求完成、关闭线程池、释放连接池、提交或回滚事务。任何一层缺失,都可能导致发布期间出现短暂 5xx 或请求中断。
对 Spring Boot 来说,server.shutdown=graceful 和 spring.lifecycle.timeout-per-shutdown-phase 是应用侧的关键配置。它们要与 Kubernetes 的 terminationGracePeriodSeconds 配套:Kubernetes 的 grace 时间应大于应用优雅停机时间,并预留网关摘流、preStop 延迟和日志刷新的余量。
3. 运行机制
3.1 从变更提交到新 Pod 接流量
一次典型滚动发布可以拆成以下时间线:
1. CI 构建新镜像,并更新 Deployment 中的 image/tag/digest2. Deployment Controller 发现 PodTemplate 变化,创建新的 ReplicaSet3. 根据 maxSurge/maxUnavailable 创建新 Pod 或缩减旧 Pod4. kubelet 拉取镜像、启动容器、执行 startupProbe5. startupProbe 成功后,readinessProbe 开始决定是否进入 endpoints6. 新 Pod Ready 后,Service/EndpointSlice 把流量纳入新实例7. 控制器继续缩减旧 ReplicaSet,直到新版本达到期望副本数这里最容易误判的是第 5 步。容器 Running 不等于服务可接流量,应用端口监听也不等于业务就绪。真正影响用户请求的是 readiness 与 endpoints 的关系:只有 Ready 的 Pod 才应该被 Service 发现并接收流量。
如果发布过程中短暂出现 502/503,需要区分是新 Pod 过早 Ready、旧 Pod 过早退出、网关连接复用未排空,还是应用本身新版本错误。不同原因看到的证据不同,修复位置也不同。
3.2 从旧 Pod 终止到连接排空
旧 Pod 被缩减时,Kubernetes 会先进入 Terminating 状态,再完成 hook、信号和强制终止流程。可以把它理解成下面这条链路:
Pod deletion requested -> Pod marked Terminating -> removed from Service endpoints -> preStop hook runs -> SIGTERM sent to PID 1 -> application graceful shutdown -> process exits before grace timeoutpreStop 的常见用法不是做复杂业务逻辑,而是给外部负载均衡、Ingress、Service mesh 或连接复用一点传播时间。例如 sleep 10 可以让 endpoints 摘除结果更稳定地传播到流量入口。但它不是万能方案:如果应用收到 SIGTERM 后仍然立刻退出,preStop 只能推迟死亡,不能保证存量请求被正确处理。
连接排空要同时看入口层和应用层。入口层需要停止把新请求转发到旧 Pod,应用层需要停止接收新请求并等待 in-flight request 完成。长连接、WebSocket、SSE、消息消费者、异步任务和批处理作业都需要单独设计退出策略,不能只依赖 HTTP 请求的优雅停机。
3.3 从失败发布到回滚证据链
Deployment 的 rollout 状态来自控制器对新旧 ReplicaSet、Pod 可用副本数和进度条件的判断。kubectl rollout status deployment/<name> 只能告诉你发布是否完成,不能直接告诉你失败原因。真正排障要继续看:
kubectl describe deployment <name>kubectl get rs -l app=<app>kubectl describe pod <pod>kubectl logs <pod> --previouskubectl get events --sort-by=.lastTimestamp如果新 Pod 因探针失败无法 Ready,Deployment 会卡在 progressing;如果镜像拉取失败,会在 Pod event 中看到 ImagePullBackOff;如果容器反复被 liveness 杀掉,restartCount、Last State 和 kubelet event 会形成证据;如果资源不足,会出现 Pending、FailedScheduling 或节点资源压力。
回滚也不是魔法。kubectl rollout undo deployment/<name> 回到的是 Deployment 的历史 PodTemplate revision,通常能恢复镜像和 Pod 模板字段,但不能自动回滚数据库迁移、外部配置、缓存结构、消息格式和下游接口契约。因此回滚前要确认失败是否只来自应用版本,还是已经涉及不可逆状态变更。
4. 最小可运行示例
4.1 Deployment 示例:把发布、探针和停机关联起来
下面是一个面向 Spring Boot HTTP 服务的最小示例。它不是生产模板,但覆盖了本文讨论的关键字段:
apiVersion: apps/v1kind: Deploymentmetadata: name: order-apispec: replicas: 4 revisionHistoryLimit: 5 progressDeadlineSeconds: 300 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: order-api template: metadata: labels: app: order-api spec: terminationGracePeriodSeconds: 45 containers: - name: order-api image: registry.example.com/order-api:2026-07-05-001 ports: - containerPort: 8080 lifecycle: preStop: exec: command: ["sh", "-c", "sleep 10"] startupProbe: httpGet: path: /actuator/health/startup port: 8080 periodSeconds: 5 failureThreshold: 24 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 periodSeconds: 5 failureThreshold: 3 livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 periodSeconds: 10 failureThreshold: 3这个例子里的关键点是:发布策略先保证容量,startup 给慢启动窗口,readiness 决定接流量,liveness 保持克制,termination grace 给应用退出时间,preStop 给流量摘除传播时间。
4.2 Spring Boot 示例:让应用理解 SIGTERM
Spring Boot 需要显式打开优雅停机,并暴露适合 Kubernetes 探测的健康端点。常见配置如下:
server: shutdown: graceful
spring: lifecycle: timeout-per-shutdown-phase: 30s
management: endpoint: health: probes: enabled: true health: livenessstate: enabled: true readinessstate: enabled: truetimeout-per-shutdown-phase 不应该大于 Kubernetes 的 terminationGracePeriodSeconds。如果应用最多等待 30 秒处理存量请求,Pod 的 grace 可以设置为 45 秒或 60 秒,给 preStop、endpoint 传播和日志刷新留出空间。
如果服务有消息消费、定时任务或异步线程池,还要在停机阶段停止拉取新消息、等待正在处理的任务完成,并为超时任务设计补偿机制。否则 HTTP 层优雅退出了,后台任务仍可能被 SIGKILL 截断。
4.3 验证示例:只看成功不够
本地或测试环境验证时,不应只看 kubectl rollout status 成功,还要观察状态变化:
kubectl rollout status deployment/order-apikubectl get pod -l app=order-api -wkubectl get endpoints order-api -wkubectl describe pod <new-pod>kubectl logs <old-pod>理想现象是:新 Pod 先 Running,再通过 startup,然后 Ready 并进入 endpoints;旧 Pod 进入 Terminating 后从 endpoints 消失,应用日志记录收到关闭信号并完成存量请求;发布完成后新 ReplicaSet 副本数达到期望值,旧 ReplicaSet 缩为 0。
还要主动制造失败:把 readiness 改成错误路径,观察 rollout 如何卡住;把 liveness 配得过短,观察容器是否反复重启;发起长请求后删除 Pod,观察请求是否能在 grace 时间内完成。这些演练比单纯跑通成功路径更有价值。
5. 配置逐行拆解
5.1 strategy、maxSurge、maxUnavailable
strategy.type: RollingUpdate 表示 Deployment 用滚动方式替换 Pod。它适合无状态或已处理状态边界的后端服务;如果服务不能同时存在新旧版本,就不能只靠 RollingUpdate,需要配合兼容发布、灰度、蓝绿或停机窗口。
maxSurge 可以是数字也可以是百分比。它越大,新版本扩容越快,但瞬时资源占用和下游连接数越高。对 CPU、内存、数据库连接池、消息消费者并发敏感的服务,不能只看 Kubernetes 节点容量,还要看数据库、缓存、网关和第三方接口是否能承受发布期间的额外副本。
maxUnavailable 控制发布期间最低可用容量。对 2 副本服务,maxUnavailable: 1 意味着发布期间可用容量可能下降一半;对只有 1 副本的服务,允许不可用就等价于接受短暂停机。生产 HTTP API 通常更偏向 maxSurge: 1、maxUnavailable: 0,但前提是集群和下游容量允许。
5.2 startup、readiness、liveness 参数
探针的结果由 periodSeconds、timeoutSeconds、failureThreshold、successThreshold 等参数共同决定。不要只看单个字段,要计算窗口。例如 periodSeconds: 5、failureThreshold: 24 的 startup 最长容忍约 120 秒启动时间。
readiness 的失败阈值要在“快速摘流”和“避免抖动”之间平衡。太敏感会让服务在短暂 GC、网络抖动或依赖毛刺时频繁进出 endpoints;太迟钝会让已经不可接流量的实例继续承接请求。对后端 API 来说,readiness 可以检查本地 HTTP 服务、必要配置、核心依赖的轻量状态,但不应把所有非关键依赖都作为强门槛。
liveness 要比 readiness 更保守。把数据库连接失败放进 liveness,可能在数据库短暂抖动时把所有 Pod 一起重启,形成雪崩。更好的做法是:readiness 负责摘流,应用内部熔断和降级负责处理依赖异常,liveness 只处理应用自身已经无法恢复的状态。
5.3 preStop、terminationGracePeriodSeconds 与 progressDeadlineSeconds
preStop 是容器终止前执行的 hook,执行时间计入 terminationGracePeriodSeconds。如果 grace 是 30 秒,preStop sleep 10 后应用只剩大约 20 秒完成退出。很多发布中断来自这里的时间预算被误算。
terminationGracePeriodSeconds 是 Kubernetes 给容器从 SIGTERM 到 SIGKILL 的总窗口。它要覆盖 endpoint 摘除传播、preStop、应用优雅停机、连接池关闭和日志刷新。它不是越长越好,过长会拖慢发布和节点维护;也不能过短,否则长请求和后台任务会被强杀。
progressDeadlineSeconds 是 Deployment 判断发布是否超时的阈值。它要大于合理启动时间和 readiness 通过时间。如果 startup 最长允许 120 秒,镜像拉取和调度还需要几十秒,progressDeadlineSeconds: 60 就会过早把发布标记为失败。
6. 后端工程场景
6.1 Spring Boot HTTP API
Spring Boot HTTP API 最常见的问题是“进程启动了,但业务没准备好”。JVM 预热、连接池初始化、Flyway/Liquibase 检查、配置中心拉取、缓存加载都可能发生在端口监听之后。readiness 应该覆盖这些业务就绪条件,而 startup 应该给它们足够时间。
优雅停机时,Tomcat、Jetty、Undertow 会停止接收新请求,并等待已有请求完成。团队需要结合接口超时时间设置停机窗口:如果网关超时是 30 秒,应用 graceful 等待 30 秒,Kubernetes grace 至少要更长,否则最慢请求仍可能被截断。
还要注意 PID 1 问题。容器中的 Java 进程必须能收到 SIGTERM。如果用 shell 脚本启动 Java,应使用 exec java ...,避免信号只到 shell 而没有传给 JVM。
6.2 网关、连接池与长连接
发布期间的 502/503 不一定来自 Pod 本身,也可能来自入口层连接复用。Service endpoints 更新需要传播,Ingress Controller、Service mesh sidecar、外部负载均衡器也可能维护到旧 Pod 的连接。preStop sleep 的价值就在于给这些组件一点排空时间。
数据库连接池也会在滚动发布时放大压力。假设每个 Pod 最大 50 个数据库连接,4 副本服务发布时 maxSurge: 2,瞬时可能多出 100 个连接。如果数据库连接上限没有余量,新版本还没接流量就可能因连接池初始化失败而无法 Ready。
长连接比短 HTTP 请求更复杂。WebSocket、SSE、gRPC streaming 和消息消费都需要明确下线策略:停止接受新连接、通知客户端重连、限制最大连接存活时间、保存消费位点、保证任务幂等。否则滚动发布会变成随机断连。
6.3 CI/CD 与发布门禁
CI/CD 不应该只负责把镜像推到集群,还应该验证发布声明是否满足最低工程约束。比如禁止生产服务缺少 readiness,禁止 liveness 检查外部数据库,要求 terminationGracePeriodSeconds 大于应用 graceful shutdown 配置,要求 revisionHistoryLimit 保留可回滚版本。
发布门禁还应检查镜像 tag 或 digest 是否可追踪。生产环境不要依赖可漂移的 latest,否则 rollout history 只能看到模板变了,却无法稳定复现当时的制品。推荐在 Deployment 中记录镜像 digest、Git commit、构建号和发布单号。
回滚流程要提前演练。一次标准发布应该能回答:如何查看当前 revision;如何回到上一个 revision;回滚后如何验证 endpoints、Pod 状态、业务错误率;哪些变更不能通过 rollout undo 回滚,需要配置回退或数据库兼容方案。
7. 常见错误与排障
7.1 发布后短暂 502/503
现象:发布窗口内用户偶发 502/503,发布完成后恢复。第一步不要直接回滚,而是确认错误发生在新 Pod 接流量、旧 Pod 下线,还是网关连接转发阶段。
证据链可以这样建立:查看网关日志中的上游地址和时间点;同时 kubectl get endpoints -w 看 Pod 是否过早进入或退出 endpoints;查看新 Pod readiness 通过时间;查看旧 Pod 是否收到 SIGTERM 后立刻退出;对齐应用日志中的请求 ID。
常见修复包括:让 readiness 等待业务真正就绪;降低 maxUnavailable;增加 preStop 排空时间;开启 Spring Boot graceful shutdown;调整入口层连接排空配置。不要只增加重试,重试会掩盖发布边界问题,并可能放大下游压力。
7.2 Pod 反复重启或 rollout 卡住
Pod 反复重启时,先看 kubectl describe pod 中的 event 和 container state。Last State: Terminated、Exit Code、Reason、Restart Count 能区分是进程崩溃、OOMKilled、liveness 失败还是命令错误。
如果 rollout 卡住,继续看新 ReplicaSet 和 Pod:是否 Pending、是否 ImagePullBackOff、是否 startup 迟迟不成功、是否 readiness 失败、是否资源不足。Deployment 只展示聚合状态,真正原因通常在 Pod event、容器日志和探针响应里。
高概率错误是把 liveness 当成 readiness 使用。依赖短暂不可用时,readiness 失败会摘流,liveness 失败会重启;后者可能让服务在下游抖动时集体重启,导致恢复更慢。
7.3 回滚后仍异常
回滚后仍异常,通常说明问题不只在 Deployment PodTemplate。可能的原因包括:ConfigMap/Secret 已被改成新配置;数据库迁移不可逆;缓存结构与旧代码不兼容;消息格式已经被新版本写入;外部服务开关没有回退。
排查时要同时比较镜像、环境变量、配置版本、数据库 schema、feature flag 和流量入口。kubectl rollout history 只能说明 Deployment revision,不能覆盖所有外部状态。
工程上要把回滚设计前置:数据库变更采用向前兼容;消息字段先兼容读再切换写;配置变更有版本和审计;发布单里明确哪些步骤可自动回滚,哪些步骤需要人工确认。
8. 生产化边界
8.1 从学习 YAML 到生产模板
学习 YAML 可以把字段写在一个文件里,但生产模板要沉淀成平台约束。比如所有 HTTP API 默认带 readiness 和保守 liveness;所有 Spring Boot 服务默认开启 graceful shutdown;所有 Deployment 保留 revision 历史;所有生产发布禁止 latest。
模板还要允许差异化。低流量内部服务、核心交易服务、长连接服务、批处理服务的发布策略不同,不能用同一个 maxSurge、同一个 grace 时间、同一个探针路径解决所有问题。
生产模板的价值不是减少几行 YAML,而是把团队共识固化下来:什么是必须配置,什么可以按服务调整,什么需要架构评审,什么上线前必须演练。
8.2 观测、告警与审计
发布相关观测至少包括:Deployment rollout 状态、ReplicaSet 变化、Pod Ready 时间、探针失败次数、容器重启次数、网关 5xx、应用错误率、P95/P99 延迟、数据库连接数和消息积压。
告警要能区分“发布中可预期波动”和“发布导致事故”。例如单个 Pod readiness 短暂失败不一定要报警,但发布窗口内 5xx 升高、多个 Pod liveness 重启、rollout 超过 deadline、可用副本低于阈值都应该触发关注。
审计要能回答谁在什么时候发布了什么镜像、用了什么配置、触发了哪个 revision、是否执行过 rollback。没有审计的发布系统,事故复盘只能靠聊天记录和记忆。
8.3 容量、依赖与兼容性边界
滚动发布不是免费的。maxSurge 会放大副本数,副本数会放大 CPU、内存、数据库连接、缓存连接、消息消费者并发和外部 API 调用。生产发布前必须把 Kubernetes 容量和下游容量一起评估。
兼容性比回滚更重要。新旧版本在发布窗口内会同时存在,因此接口、数据库、缓存、消息、定时任务都要支持短时间双版本共存。只要存在“不兼容但同时运行”的窗口,RollingUpdate 就可能把一次发布变成随机故障。
对于不能双版本共存的变更,需要拆分步骤:先发布兼容读,迁移数据,再切换写,最后清理旧逻辑。不要把所有变化放进一个镜像版本里赌一次发布成功。
9. 什么时候用,什么时候不用
9.1 适合使用 RollingUpdate 的场景
RollingUpdate 适合无状态 HTTP API、可以水平扩展的后台服务,以及已经处理好数据兼容和流量摘除的服务。它的优势是简单、原生、可观测,并且与 Deployment 的 revision 和 rollback 能力自然结合。
如果服务可以同时存在新旧版本,readiness 能准确表达接流量状态,优雅停机能处理存量请求,那么 RollingUpdate 是默认优先选择。此时重点不是引入复杂发布平台,而是把基础字段配置正确、把排障证据链建立起来。
对大多数 Spring Boot API 来说,先把 RollingUpdate、三类探针和 graceful shutdown 做扎实,比过早引入复杂灰度系统更现实。
9.2 不宜只靠 RollingUpdate 的场景
如果新旧版本不能同时运行,RollingUpdate 就不够。例如数据库 schema 不兼容、消息格式不兼容、缓存 key 语义变化、长连接协议不兼容、任务消费不能重复,都会让新旧 Pod 共存变得危险。
如果发布风险需要按用户、租户、地区或流量比例逐步放大,也不应只靠 Deployment 原生滚动发布。此时需要灰度、金丝雀、蓝绿、流量镜像或 feature flag,把“副本替换”和“业务流量切换”分开控制。
如果服务只有单副本且无法扩容,RollingUpdate 不能提供真正的无损发布。它最多让发布过程可声明、可回滚,但不可用窗口仍然存在,需要通过扩容、主备、蓝绿或维护窗口解决。
9.3 团队采用顺序
建议采用顺序是:先给所有服务补齐 readiness,再校准 liveness,然后为慢启动服务增加 startup,接着开启应用优雅停机,最后调整发布策略和回滚流程。这个顺序符合事故收益:先避免坏实例接流量,再避免错误重启,再处理下线中断。
团队成熟后,可以把这些能力沉淀到脚手架、Helm chart、Kustomize base、平台模板或 CI 校验中。不要依赖每个开发手写 YAML,因为发布字段一旦写错,问题通常只会在生产流量下暴露。
最终目标是让团队形成统一语言:Ready 不等于 Running,Live 不等于依赖可用,Terminating 不等于已经无流量,Rollback 不等于所有状态都回退。只要这些边界清楚,发布事故的定位速度会明显提升。
10. 下一篇衔接
本篇解决的是后端服务在 Kubernetes 中如何安全发布、如何接流量、如何退出、如何回滚的问题。它把 Deployment、ReplicaSet、Pod、Service endpoints、探针和应用停机逻辑串成了一条发布链路。
下一篇会进入“后端 Kubernetes 部署模板”,把本文的经验沉淀成可复用的声明:镜像、端口、资源、配置、探针、发布策略、停机策略、观测标签和环境差异如何组织成团队可以长期维护的模板。
如果你现在还只能记住几条命令,建议回到本文的三条主线再过一遍:新 Pod 什么时候接流量,旧 Pod 什么时候退出,发布失败时证据从哪里来。能回答这三件事,才算真正理解 Kubernetes 发布。