GCP发布/订阅:使用goroutines使多个订阅者在一个应用程序中运行
我在收到GCP发布/订阅的消息时发现一种奇怪的行为。 以下代码是我如何使用注册订阅的 gcp.goGCP发布/订阅:使用goroutines使多个订阅者在一个应用程序中运行,go,google-cloud-pubsub,Go,Google Cloud Pubsub,我在收到GCP发布/订阅的消息时发现一种奇怪的行为。 以下代码是我如何使用注册订阅的 gcp.go package gcp import ( "context" "path" "runtime" "google.golang.org/api/option" "cloud.google.com/go/pubsub" ) // PubsubClient
package gcp
import (
"context"
"path"
"runtime"
"google.golang.org/api/option"
"cloud.google.com/go/pubsub"
)
// PubsubClient is the GCP pubsub service client.
var PubsubClient *pubsub.Client
// Initialize initializes GCP client service using the environment.
func Initialize(env, projectName string) error {
var err error
ctx := context.Background()
credentialOpt := option.WithCredentialsFile(getFilePathByEnv(env))
PubsubClient, err = pubsub.NewClient(ctx, projectName, credentialOpt)
return err
}
// GetTopic returns the specified topic in GCP pub/sub service and create it if it not exist.
func GetTopic(topicName string) (*pubsub.Topic, error) {
topic := PubsubClient.Topic(topicName)
ctx := context.Background()
isTopicExist, err := topic.Exists(ctx)
if err != nil {
return topic, err
}
if !isTopicExist {
ctx = context.Background()
topic, err = PubsubClient.CreateTopic(ctx, topicName)
}
return topic, err
}
// GetSubscription returns the specified subscription in GCP pub/sub service and creates it if it not exist.
func GetSubscription(subName string, topic *pubsub.Topic) (*pubsub.Subscription, error) {
sub := PubsubClient.Subscription(subName)
ctx := context.Background()
isSubExist, err := sub.Exists(ctx)
if err != nil {
return sub, err
}
if !isSubExist {
ctx = context.Background()
sub, err = PubsubClient.CreateSubscription(ctx, subName, pubsub.SubscriptionConfig{Topic: topic})
}
return sub, err
}
func getFilePathByEnv(env string) string {
_, filename, _, _ := runtime.Caller(1)
switch env {
case "local":
return path.Join(path.Dir(filename), "local.json")
case "development":
return path.Join(path.Dir(filename), "development.json")
case "staging":
return path.Join(path.Dir(filename), "staging.json")
case "production":
return path.Join(path.Dir(filename), "production.json")
default:
return path.Join(path.Dir(filename), "local.json")
}
}
package main
import (
"context"
"fmt"
"log"
"net/http"
"runtime"
"runtime/debug"
"runtime/pprof"
"time"
"rpriambudi/pubsub-receiver/gcp"
"cloud.google.com/go/pubsub"
"github.com/go-chi/chi"
)
func main() {
log.Fatal(http.ListenAndServe(":4001", Route()))
}
func Route() *chi.Mux {
InitializeSubscription()
chiRoute := chi.NewRouter()
chiRoute.Route("/api", func(r chi.Router) {
r.Get("/_count", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Number of goroutines: %v", runtime.NumGoroutine())
})
r.Get("/_stack", getStackTraceHandler)
})
return chiRoute
}
func InitializeSubscription() {
gcp.Initialize("local", "fifth-bonbon-277102")
go pubsubHandler("test-topic-1", "test-topic-1-subs")
go pubsubHandler("test-topic-2", "test-topic-2-subs")
go pubsubHandler("test-topic-3", "test-topic-3-subs")
// ....
return
}
func getStackTraceHandler(w http.ResponseWriter, r *http.Request) {
stack := debug.Stack()
w.Write(stack)
pprof.Lookup("goroutine").WriteTo(w, 2)
}
func pubsubHandler(topicID string, subscriptionID string) {
topic, err := gcp.GetTopic(topicID)
fmt.Println("topic: ", topic)
if err != nil {
fmt.Println("Failed get topic: ", err)
return
}
sub, err := gcp.GetSubscription(subscriptionID, topic)
fmt.Println("subscription: ", sub)
if err != nil {
fmt.Println("Get subscription err: ", err)
return
}
err = sub.Receive(context.Background(), func(ctx context.Context, msg *pubsub.Message) {
messageHandler(subscriptionID, ctx, msg)
})
if err != nil {
fmt.Println("receive error: ", err)
}
}
func messageHandler(subscriptionID string, ctx context.Context, msg *pubsub.Message) {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered from panic.")
msg.Ack()
}
}()
fmt.Println("message of subscription: ", subscriptionID)
fmt.Println("Message ID: ", string(msg.ID))
fmt.Println("Message received: ", string(msg.Data))
msg.Ack()
time.Sleep(10 * time.Second)
}
main.go
package gcp
import (
"context"
"path"
"runtime"
"google.golang.org/api/option"
"cloud.google.com/go/pubsub"
)
// PubsubClient is the GCP pubsub service client.
var PubsubClient *pubsub.Client
// Initialize initializes GCP client service using the environment.
func Initialize(env, projectName string) error {
var err error
ctx := context.Background()
credentialOpt := option.WithCredentialsFile(getFilePathByEnv(env))
PubsubClient, err = pubsub.NewClient(ctx, projectName, credentialOpt)
return err
}
// GetTopic returns the specified topic in GCP pub/sub service and create it if it not exist.
func GetTopic(topicName string) (*pubsub.Topic, error) {
topic := PubsubClient.Topic(topicName)
ctx := context.Background()
isTopicExist, err := topic.Exists(ctx)
if err != nil {
return topic, err
}
if !isTopicExist {
ctx = context.Background()
topic, err = PubsubClient.CreateTopic(ctx, topicName)
}
return topic, err
}
// GetSubscription returns the specified subscription in GCP pub/sub service and creates it if it not exist.
func GetSubscription(subName string, topic *pubsub.Topic) (*pubsub.Subscription, error) {
sub := PubsubClient.Subscription(subName)
ctx := context.Background()
isSubExist, err := sub.Exists(ctx)
if err != nil {
return sub, err
}
if !isSubExist {
ctx = context.Background()
sub, err = PubsubClient.CreateSubscription(ctx, subName, pubsub.SubscriptionConfig{Topic: topic})
}
return sub, err
}
func getFilePathByEnv(env string) string {
_, filename, _, _ := runtime.Caller(1)
switch env {
case "local":
return path.Join(path.Dir(filename), "local.json")
case "development":
return path.Join(path.Dir(filename), "development.json")
case "staging":
return path.Join(path.Dir(filename), "staging.json")
case "production":
return path.Join(path.Dir(filename), "production.json")
default:
return path.Join(path.Dir(filename), "local.json")
}
}
package main
import (
"context"
"fmt"
"log"
"net/http"
"runtime"
"runtime/debug"
"runtime/pprof"
"time"
"rpriambudi/pubsub-receiver/gcp"
"cloud.google.com/go/pubsub"
"github.com/go-chi/chi"
)
func main() {
log.Fatal(http.ListenAndServe(":4001", Route()))
}
func Route() *chi.Mux {
InitializeSubscription()
chiRoute := chi.NewRouter()
chiRoute.Route("/api", func(r chi.Router) {
r.Get("/_count", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Number of goroutines: %v", runtime.NumGoroutine())
})
r.Get("/_stack", getStackTraceHandler)
})
return chiRoute
}
func InitializeSubscription() {
gcp.Initialize("local", "fifth-bonbon-277102")
go pubsubHandler("test-topic-1", "test-topic-1-subs")
go pubsubHandler("test-topic-2", "test-topic-2-subs")
go pubsubHandler("test-topic-3", "test-topic-3-subs")
// ....
return
}
func getStackTraceHandler(w http.ResponseWriter, r *http.Request) {
stack := debug.Stack()
w.Write(stack)
pprof.Lookup("goroutine").WriteTo(w, 2)
}
func pubsubHandler(topicID string, subscriptionID string) {
topic, err := gcp.GetTopic(topicID)
fmt.Println("topic: ", topic)
if err != nil {
fmt.Println("Failed get topic: ", err)
return
}
sub, err := gcp.GetSubscription(subscriptionID, topic)
fmt.Println("subscription: ", sub)
if err != nil {
fmt.Println("Get subscription err: ", err)
return
}
err = sub.Receive(context.Background(), func(ctx context.Context, msg *pubsub.Message) {
messageHandler(subscriptionID, ctx, msg)
})
if err != nil {
fmt.Println("receive error: ", err)
}
}
func messageHandler(subscriptionID string, ctx context.Context, msg *pubsub.Message) {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered from panic.")
msg.Ack()
}
}()
fmt.Println("message of subscription: ", subscriptionID)
fmt.Println("Message ID: ", string(msg.ID))
fmt.Println("Message received: ", string(msg.Data))
msg.Ack()
time.Sleep(10 * time.Second)
}
当我在InitializeSubscription
中有一些pubsubHandler
时,它工作得非常好。但是当我在initialize函数(大约10个或更多处理程序)中添加更多的pubsubHandler
时,事情开始变得有趣起来。ack永远不会到达pubsub服务器,使得消息根本没有ack(我在metrics explorer中检查了AcknowledgeRequest
,没有ack请求)。因此,消息会不断返回到订户。另外,当我重新启动应用程序时,有时它不会收到任何消息,无论是新消息还是未确认的消息
对于pubsubHandler
函数中的每个订阅对象,我似乎通过将NumGoroutines
设置为1
找到了一个解决方法
func pubsubHandler(topicID string, subscriptionID string) {
....
sub, err := gcp.GetSubscription(subscriptionID, topic)
....
sub.ReceiverSettings.NumGoroutines = 1
err = sub.Receive(context.Background(), func(ctx context.Context, msg *pubsub.Message) {
messageHandler(subscriptionID, ctx, msg)
})
....
}
我的问题是,这是故意的行为吗?导致这些意外行为的根本原因是什么?或者我的实现是完全错误的,为了达到预期的结果?(一个应用程序内的多个订阅)。或者,在创建订阅处理程序时,是否有任何最佳实践可遵循
据我所知,来自pubsub.Subscription
的Receive
函数本质上是一个阻塞代码。因此,当我试图在goroutines中运行它时,它可能会导致意外的副作用,特别是如果我们不限制可能处理消息的goroutines的数量。我的推理正确吗
谢谢你的回答,祝你度过愉快的一天
编辑1:将示例更新为完整代码,因为pubsub客户端不是直接导入main.go before中的。我认为问题可能是您处理消息的速度(目前每条消息10秒)。如果您一次收到太多的消息,您的客户端可能会不知所措,这将导致积压的消息
我建议使用并增加
ReceiveSettings.NumGoroutines
,使其高于默认值10。如果发布率很高,还可以增加MaxOutstandingMessages,或者通过将其设置为-1来完全禁用该限制。这会告诉客户端一次保留更多的消息,这是每个接收
呼叫共享的限制。我认为问题可能是您处理消息的速率(当前为每条消息10秒)。如果您一次收到太多的消息,您的客户端可能会不知所措,这将导致积压的消息
我建议使用并增加
ReceiveSettings.NumGoroutines
,使其高于默认值10。如果发布率很高,还可以增加MaxOutstandingMessages,或者通过将其设置为-1来完全禁用该限制。这会告诉客户端一次保留更多消息,这是每个接收呼叫共享的限制。谢谢您的回答。因为我在time.Sleep
之前发送了ack,所以处理速率不应该小于每条消息10秒吗?另外,如果我增加接收设置。NumGoroutines
是否意味着将处理每条消息的goroutine的数量将增加,或者这是生成的处理任何传入消息的goroutine的最大数量?通常,您希望在确认
之前进行大量处理。由于handle funct中有time.Sleep
,因此在函数返回之前,消息仍将保留。增加NumGoroutines
会增加生成用于拉取消息的goroutine的数量。如果您想要更多的goroutine来处理消息,请增加MaxOutstandingMessages
。谢谢您的回答。因为我在time.Sleep
之前发送了ack,所以处理速率不应该小于每条消息10秒吗?另外,如果我增加接收设置。NumGoroutines
是否意味着将处理每条消息的goroutine的数量将增加,或者这是生成的处理任何传入消息的goroutine的最大数量?通常,您希望在确认
之前进行大量处理。由于handle funct中有time.Sleep
,因此在函数返回之前,消息仍将保留。增加NumGoroutines
会增加生成用于拉取消息的goroutine的数量。如果需要更多的goroutine来处理消息,请增加MaxOutstandingMessages
。