2. Pub/Sub核心机制

Pub/Sub 是 Google Cloud 上典型的异步消息中间件,它的核心价值在于把“消息生产”和“消息消费”在时间与空间上彻底解耦。理解 Pub/Sub,关键不是记住几个 API 名称,而是理解它的一套对象模型与投递语义:主题(Topic)负责接收发布者写入的消息,订阅(Subscription)负责把消息投递给消费者,二者之间通过“一份消息、多份订阅副本”的方式实现一对多分发。发布者只面向主题,消费者只面向订阅,双方互不知道对方的存在,这正是 Pub/Sub 能够弹性伸缩的基础。

主题与订阅:一对多的分发模型

主题是消息的入口,命名形如 projects/my-project/topics/order-events。订阅是消息的出口,命名形如 projects/my-project/subscriptions/order-events-sub。一个主题可以挂多个订阅,每个订阅都会独立收到该主题的全部消息副本。也就是说,如果主题 order-events 同时挂了“实时风控订阅”和“离线归档订阅”,那么每条订单事件都会被这两个订阅各收到一次,互不影响、互不阻塞。这一点常被误解为“消息被瓜分”,实际上 Pub/Sub 的分发是复制式的,不是竞争式的。

订阅有两种投递方式:推送(Push)和拉取(Pull)。推送订阅要求你提供一个 HTTPS 端点,Pub/Sub 主动把消息 POST 过去,适合无状态服务、Cloud Run、Cloud Functions 这类场景;拉取订阅则由消费者主动调用 API 拉消息,适合批处理、需要精确控制消费速率的场景。选择哪种,取决于消费者是否具备公网可达的 HTTPS 端点,以及你是否需要自己掌控消费节奏。

发布者 Publisher publish() 主题 Topic order-events 消息入口 发布 订阅 A(拉取) risk-sub ackDeadline: 60s 订阅 B(推送) archive-sub pushEndpoint: HTTPS 订阅 C(拉取) analytics-sub 排序键: user_id 风控服务 pull + ack 归档函数 push 自动 ack 分析管道 pull + ack 死信队列 DLQ maxDeliveryAttempts: 5 dead-letter-topic 超过重试上限 消息保留 主题: 7 天 订阅: 未确认 7 天

消息发布与拉取:从写入到消费

发布消息时,发布者调用 publish,把消息体(data)和一组属性(attributes)交给主题。消息体是字节串,属性是键值对,常用于携带路由信息或业务标签。发布是异步的,客户端库会把消息批量打包后发送,以提升吞吐。发布成功后,Pub/Sub 会返回一个 messageId,它是消息在主题内的唯一标识,可用于日志追踪。

拉取侧,消费者调用 pull 或使用流式拉取(StreamingPull)。拉取到的每条消息都带有一个 ackId,这个 ID 不是消息 ID,而是本次投递的“租约凭证”。消费者处理完业务逻辑后,必须用这个 ackId 调用 acknowledge,Pub/Sub 才会认为该消息已被成功消费,并从订阅中删除。如果消费者没有及时确认,消息会被重新投递。

确认与重试:至少一次语义

Pub/Sub 默认提供至少一次(at-least-once)投递语义。这意味着同一条消息可能被投递多次,消费者必须做到幂等。确认机制的核心是确认截止时间(ackDeadline):订阅创建时设定,默认 10 秒,可配置为 10 秒到 600 秒。消费者拉取消息后,计时开始;如果在截止时间前没有 ack,Pub/Sub 会认为投递失败,把消息重新投递给其他消费者或同一消费者的下一次拉取。

对于处理时间较长的任务,可以在处理过程中调用 modifyAckDeadline 延长租约,避免消息被提前重投。重试不是无限次的,订阅可以配置重试策略:最小退避时间(如 10 秒)、最大退避时间(如 600 秒),退避时间会随失败次数指数增长。当投递次数超过 maxDeliveryAttempts(例如 5 次)后,消息会被转发到死信队列。

判断消息是否被重复投递,最直接的信号是业务侧收到相同 messageId 或相同业务主键。建议在消费端用 Redis 或数据库唯一约束做去重,而不是依赖 Pub/Sub 保证“只投一次”。

死信队列:兜住无法处理的消息

死信队列(Dead Letter Queue,DLQ)是一个普通的 Pub/Sub 主题,用于接收那些反复投递失败的消息。配置 DLQ 需要两步:先创建一个死信主题,再在订阅上设置 deadLetterPolicy,指定 deadLetterTopic 和 maxDeliveryAttempts。当某条消息的投递次数达到上限仍未 ack,Pub/Sub 会把它发布到死信主题,同时从原订阅中移除。

DLQ 的价值在于:它把“坏消息”从主消费链路中隔离出来,避免一条永远处理失败的消息反复占用消费者资源、拖慢整体吞吐。运维人员可以单独订阅死信主题,人工排查或编写修复程序。需要注意的是,DLQ 本身也需要一个订阅来消费,否则消息会一直留在死信主题里直到过期。

消息保留与排序键

消息保留分两个层面:主题级保留和订阅级保留。主题级保留决定消息在主题中最多存多久(默认 7 天,可配置 10 分钟到 31 天),即使没有任何订阅,消息也会保留;订阅级保留决定消息在订阅中未被确认时最多保留多久(默认 7 天,可配置 10 分钟到 31 天)。超过保留期的消息会被删除,无法再拉取。如果订阅配置了“保留已确认消息”,则 ack 之后的消息仍可在保留期内被重新拉取,用于回放场景。

排序键(ordering key)用于解决“同一实体消息必须按顺序处理”的问题。发布消息时,在 PublishRequest 中设置 orderingKey,例如 user_id 或 order_id。Pub/Sub 会保证同一个排序键的消息按发布顺序投递,不同排序键之间则没有顺序保证。启用排序需要订阅开启 enableMessageOrdering,并且消费者必须按顺序 ack;如果某条消息处理失败,后续同键消息会被阻塞,直到该消息被确认或进入死信队列。

机制 关键参数 典型取值 作用
确认截止时间 ackDeadlineSeconds 10~600 秒 控制消息重投时机
重试退避 minimumBackoff / maximumBackoff 10 秒 / 600 秒 控制失败后重试间隔
死信策略 maxDeliveryAttempts 5~100 次 超过次数转入 DLQ
消息保留 messageRetentionDuration 10 分钟~31 天 控制消息可回放窗口
排序键 orderingKey + enableMessageOrdering 业务主键 保证同键有序投递
排序键会带来吞吐限制:同一个排序键的消息是串行投递的。如果某个键的消息量极大,可能成为瓶颈。实践中应选择粒度合适的键,例如按 user_id 而不是按 region,避免热点键。

把这几项机制串起来看:发布者写入主题,主题把消息复制给每个订阅;消费者拉取后必须在 ackDeadline 内确认,否则重试;重试超过上限进入死信队列;消息在保留期内可回放;排序键保证同一业务实体的处理顺序。理解这套组合,才能在实际开发中正确设置参数、处理重复消息、隔离坏消息,并让系统在故障时仍然可控。