GCP Pub/Sub基准测试疑问:每次销毁连接为何比单连接更快?
问题背景
我是GCP Pub/Sub的新手,正在测试消息发布的性能基准,遇到了与预期不符的结果:
设置了两个测试场景,均需发布6条消息:
- 场景1:创建一个PubsubClient,完成6条消息发布后销毁客户端;
- 场景2:为每条消息单独创建新的PubsubClient,发布完成后立即关闭客户端。
基准测试代码如下:
func BenchmarkPublish(b *testing.B) { os.Setenv("PUBSUB_EMULATOR_HOST", "localhost:8085") ctx := context.Background() client, err := pubsub.NewClient(ctx, ProjectID) if err != nil { log.Fatalln("can't create client: ", err) } defer client.Close() topic := client.Topic(TopicID) b.ResetTimer() for i := 0; i < b.N; i++ { for _, fruit := range fruits { err := Publish(os.Stdout, ctx, TopicID, topic, fruit) if err != nil { log.Fatalln("can't publish message: ", err) } } } } func BenchmarkPublishAndKillConnectionEachTime(b *testing.B) { os.Setenv("PUBSUB_EMULATOR_HOST", "localhost:8085") ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { for _, fruit := range fruits { client, err := pubsub.NewClient(ctx, ProjectID) if err != nil { log.Fatalln("can't create client: ", err) } topic := client.Topic(TopicID) err = Publish(os.Stdout, ctx, TopicID, topic, fruit) if err != nil { log.Fatalln("can't publish message: ", err) } client.Close() } } }
Publish函数实现:
func Publish(w io.Writer, ctx context.Context, topicID string, t *pubsub.Topic, msg string) error { result := t.Publish(ctx, &pubsub.Message{ Data: []byte(msg), }) id, err := result.Get(ctx) if err != nil { return fmt.Errorf("pubsub: result.Get: %w", err) } fmt.Fprintf(w, "Published a message; msg ID: %v\n", id) return nil }
我原本认为BenchmarkPublish(单连接场景)的性能应优于BenchmarkPublishAndKillConnectionEachTime(每次销毁连接场景),但实际测试结果显示前者更慢。为何每次创建销毁客户端的基准测试性能反而更优?按认知,创建销毁客户端会消耗计算资源,单连接持续复用应更快才对。
1. Pub/Sub Go客户端的批处理机制是核心原因
GCP Pub/Sub Go客户端默认启用消息批处理:复用同一个客户端/Topic实例时,客户端会将多个Publish调用的消息攒成一批,等待默认10ms的延迟阈值或达到批量大小阈值后,再一次性发送到服务器。但你的测试中,每次调用Publish后立刻执行result.Get()——这个方法会阻塞直到当前消息(或所在批次)被服务器确认。
在单客户端场景中,6条消息会被自动合并成一批,result.Get()会等待完整的批处理延迟(哪怕单条消息也会触发等待);而在每次创建客户端的场景中,每个客户端仅处理1条消息,调用client.Close()时会立即flush所有未发送的批处理消息,跳过了默认的等待时间,因此单条消息的确认速度反而更快。
2. 客户端关闭时的强制flush行为
调用client.Close()时,Pub/Sub客户端会触发所有未完成批处理消息的即时发送,不会等待默认的批处理超时。场景2中每条消息发布后立刻关闭客户端,相当于强制跳过批处理等待,直接发送当前消息,在小消息量测试中,这个等待时间的占比极高,反而成为单客户端场景的性能瓶颈。
3. Pub/Sub模拟器的特性干扰
你使用的是本地Pub/Sub模拟器,它的消息确认速度远快于生产环境,批处理的10ms等待时间在测试中占比极大;而在生产环境中,网络延迟会远大于批处理等待时间,此时复用客户端的批处理优势会完全体现出来。
4. 日志输出的额外影响
测试中向os.Stdout打印大量日志,虽然两个场景都存在,但单客户端场景中批处理的确认是一次性返回多个消息ID,打印时机和次数的差异也会对基准测试结果产生微小干扰。
验证方法
若要验证批处理的影响,可以手动关闭批处理:
topic := client.Topic(TopicID).Settings(pubsub.TopicSettings{ PublishSettings: pubsub.PublishSettings{ DelayThreshold: 0, // 关闭批处理等待延迟 }, })
此时单客户端场景的性能会与多客户端场景接近甚至更优。
内容的提问来源于stack exchange,提问作者jun yan

