腾讯mini项目-【指标监控服务重构】2023-08-16

今日已办

v1

验证 StageHandler 在处理消息时是否为单例,【错误尝试】

go 复制代码
type StageHandler struct {
}

func (s StageHandler) Middleware1(h message.HandlerFunc) message.HandlerFunc {
	return func(msg *message.Message) ([]*message.Message, error) {
		log.Logger.Info("StageHandler Middleware 1")
		fmt.Printf("%p\n", &s)
		return h(msg)
	}
}

func (s StageHandler) Middleware2(h message.HandlerFunc) message.HandlerFunc {
	return func(msg *message.Message) ([]*message.Message, error) {
		log.Logger.Info("StageHandler Middleware 2")
		fmt.Printf("%p\n", &s)
		return h(msg)
	}
}

func (s StageHandler) Middleware3(h message.HandlerFunc) message.HandlerFunc {
	return func(msg *message.Message) ([]*message.Message, error) {
		log.Logger.Info("StageHandler Middleware 3")
		fmt.Printf("%p\n", &s)
		return h(msg)
	}
}

func (s StageHandler) Handler1(msg *message.Message) error {
	log.Logger.Info("StageHandler Handler 1")
	fmt.Printf("%p\n", &s)
	return nil
}

v2

  • 定义不同 Handler
go 复制代码
type CrashHandler struct {
	Topic string
}

func (s CrashHandler) Handler1(msg *message.Message) error {
	log.Logger.Info(s.Topic + ": CrashHandler Handler 1 start")
	fmt.Printf("%p\n", &s)
	time.Sleep(1 * time.Second)
	log.Logger.Info(s.Topic + ": CrashHandler Handler 1 end")
	return nil
}

type LagHandler struct {
	Topic string
}

func (s LagHandler) Handler1(msg *message.Message) error {
	log.Logger.Info(s.Topic + ": LagHandler Handler 1 start")
	fmt.Printf("%p\n", &s)
	time.Sleep(1 * time.Second)
	log.Logger.Info(s.Topic + ": LagHandler Handler 1 end")
	return nil
}
  • 添加到router中
go 复制代码
	for _, topic := range topics {
		var category string
		var handlerFunc message.NoPublishHandlerFunc
		if strings.Contains(topic, performance.CategoryCrash) {
			category = performance.CategoryCrash
			handlerFunc = CrashHandler{Topic: category}.Handler1
		} else if strings.Contains(topic, performance.CategoryLag) {
			category = performance.CategoryLag
			handlerFunc = LagHandler{Topic: category}.Handler1
		} else {
			continue
		}

		handler := router.AddNoPublisherHandler(topic+"test-handler", topic, subscriber, handlerFunc)
	}
  • 结论
    • handler 实例会不断创建
    • 不同的 handler 可以并行处理不同主题的消息
    • 相同的 handler 在处理该主题的消息时是顺序的

官方文档: Message Router (watermill.io)

订阅者可以一次消费一条消息,也可以并行消费多条消息

  • Single stream of messages 是最简单的方法,这意味着在调用 msg.Ack() 之前,订阅者将不会收到任何新消息
  • Multiple message streams 仅部分订阅者支持。通过一次订阅多个主题分区 ,可以并行消费多条消息,甚至是之前未确认的消息(例如,Kafka 订阅者就是这样工作的) Router 通过运行并发 HandlerFuncs(每个分区一个)来处理此模型

v3

存在并发安全问题

  1. 公用一个上下文
  2. 频繁的修改上下文中的字段值
  3. 不同Handler和MiddleWare存在并发

解决思路

  • 将一次消息处理会使用到的数据集合定义为一个结构体
go 复制代码
type ContextData struct {
	Status int
	Event  schema.Event

	AppID         string // API 上报
	FetchScenario string // API 上报
}
  • 使用message的Context来传递这个数据
  • 移除掉 ProfileCtx 的相关设计
  • 使用watermillzap.Logger来替换本身的 LoggerAdapter,更加直观且与原项目适配

完整代码

profile/internal/watermill/consumer/consumer_context.go

go 复制代码
// Package consumer
// @Author xzx 2023/8/11 18:53:00
package consumer

import (
	"context"
	kc "github.com/Kevinello/kafka-client"
	"github.com/ThreeDotsLabs/watermill"
	"github.com/ThreeDotsLabs/watermill/message"
	"github.com/ThreeDotsLabs/watermill/message/router/middleware"
	"github.com/ThreeDotsLabs/watermill/message/router/plugin"
	"github.com/garsue/watermillzap"
	"github.com/qiulin/watermill-kafkago/pkg/kafkago"
	"go.uber.org/zap"
	"profile/internal/config"
	"profile/internal/connector"
	"profile/internal/log"
	"profile/internal/schema/performance"
	"strings"
	"time"
)

// Consume
// @Description
// @Author xzx 2023-08-16 22:52:52
func Consume() {
	logger := watermillzap.NewLogger(log.Logger)

	publisher, subscriber := newPubSub(logger)
	router, err := message.NewRouter(message.RouterConfig{}, logger)
	if err != nil {
		log.Logger.Fatal("creates a new Router with given configuration error", zap.Error(err))
	}

	router.AddPlugin(plugin.SignalsHandler)
	router.AddMiddleware(
		middleware.Retry{
			MaxRetries:      3,
			InitialInterval: time.Millisecond * 100,
			Logger:          logger,
		}.Middleware,
		middleware.Recoverer,
	)

	getTopics := kc.GetTopicReMatch(strings.Split(config.Profile.GetString("kafka.topicRE"), ","))
	topics, err := getTopics(config.Profile.GetString("kafka.bootstrap"))
	if err != nil {
		log.Logger.Fatal("get topics failed", zap.Error(err))
		return
	}

	for _, topic := range topics {
		var category string
		var handlerFunc message.HandlerFunc
		if strings.Contains(topic, performance.CategoryCrash) {
			category = performance.CategoryCrash
			handlerFunc = CrashWriteKafka
		} else if strings.Contains(topic, performance.CategoryLag) {
			category = performance.CategoryLag
			handlerFunc = LagWriteKafka
		} else {
			continue
		}
		router.AddHandler(category, topic, subscriber, connector.GetTopic(category), publisher, handlerFunc).
			AddMiddleware(
				UnpackKafkaMessage,
				InitPerformanceEvent,
				AnalyzeEvent)
	}

	if err = router.Run(context.Background()); err != nil {
		log.Logger.Error("runs all plugins and handlers and starts subscribing to provided topics error", zap.Error(err))
	}
}

// newPubSub
// @Description
// @Author xzx 2023-08-16 22:52:45
// @Param logger
// @Return message.Publisher
// @Return message.Subscriber
func newPubSub(logger watermill.LoggerAdapter) (message.Publisher, message.Subscriber) {
	marshaler := kafkago.DefaultMarshaler{}
	publisher := kafkago.NewPublisher(kafkago.PublisherConfig{
		Brokers:     []string{config.Profile.GetString("kafka.bootstrap")},
		Async:       false,
		Marshaler:   marshaler,
		OTELEnabled: false,
		Ipv4Only:    true,
		Timeout:     100 * time.Second,
	}, logger)

	subscriber, err := kafkago.NewSubscriber(kafkago.SubscriberConfig{
		Brokers:       []string{config.Profile.GetString("kafka.bootstrap")},
		Unmarshaler:   marshaler,
		ConsumerGroup: config.Profile.GetString("kafka.group"),
		OTELEnabled:   false,
	}, logger)
	if err != nil {
		log.Logger.Fatal("Unable to create subscriber", zap.Error(err))
	}
	return publisher, subscriber
}

profile/internal/watermill/consumer/consumer_stage.go

go 复制代码
// Package consumer
// @Author xzx 2023/8/12 10:01:00
package consumer

import (
	"context"
	"encoding/json"
	"github.com/ThreeDotsLabs/watermill/message"
	"go.uber.org/zap"
	"profile/internal/connector"
	"profile/internal/log"
	"profile/internal/schema"
	"profile/internal/schema/performance"
	"profile/internal/state"
)

type ContextData struct {
	Status int
	Event  schema.Event

	AppID         string // API 上报
	FetchScenario string // API 上报
}

// UnpackKafkaMessage
// @Description
// @Author xzx 2023-08-12 12:27:30
// @Param h
// @Return message.HandlerFunc
func UnpackKafkaMessage(h message.HandlerFunc) message.HandlerFunc {
	return func(msg *message.Message) ([]*message.Message, error) {
		var data ContextData
		// 反序列化,存入通用结构体
		if contextErr := json.Unmarshal(msg.Payload, &data.Event); contextErr != nil {
			data.Status = state.StatusUnmarshalError
			return nil, contextErr
		}
		log.Logger.Info("[1-UnpackKafkaItem] unpack kafka item success", zap.Any("event", data.Event))

		msg.SetContext(context.WithValue(msg.Context(), "data", data))
		return h(msg)
	}
}

// InitPerformanceEvent
// @Description
// @Author xzx 2023-08-12 12:27:35
// @Param h
// @Return message.HandlerFunc
func InitPerformanceEvent(h message.HandlerFunc) message.HandlerFunc {
	return func(msg *message.Message) ([]*message.Message, error) {
		data := msg.Context().Value("data").(ContextData)
		event, contextErr := performance.EventFactory(data.Event.Category, data.Event.Dimensions, data.Event.Values)
		if contextErr != nil {
			data.Status = state.StatusEventFactoryError
			return nil, contextErr
		}
		log.Logger.Info("[2-InitPerformanceEvent] Consume performance event success", zap.Any("event", data.Event))
		data.Event.ProfileData = event

		msg.SetContext(context.WithValue(msg.Context(), "data", data))
		return h(msg)
	}
}

// AnalyzeEvent
// @Description
// @Author xzx 2023-08-12 12:27:38
// @Param h
// @Return message.HandlerFunc
func AnalyzeEvent(h message.HandlerFunc) message.HandlerFunc {
	return func(msg *message.Message) ([]*message.Message, error) {
		data := msg.Context().Value("data").(ContextData)

		contextErr := data.Event.ProfileData.Analyze()
		if contextErr != nil {
			data.Status = state.StatusAnalyzeError
			return nil, contextErr
		}
		log.Logger.Info("[3-AnalyzeEvent] analyze event success", zap.Any("event", data.Event))
		// clear dimensions and values
		data.Event.Dimensions = nil
		data.Event.Values = nil

		msg.SetContext(context.WithValue(msg.Context(), "data", data))
		return h(msg)
	}
}

// CrashWriteKafka
// @Description
// @Author xzx 2023-08-12 15:09:15
// @Param msg
// @Return []*message.Message
// @Return error
func CrashWriteKafka(msg *message.Message) ([]*message.Message, error) {
	data := msg.Context().Value("data").(ContextData)

	toWriteBytes, contextErr := json.Marshal(data.Event)
	if contextErr != nil {
		data.Status = state.StatusUnmarshalError
		return nil, contextErr
	}

	msg = message.NewMessage(data.Event.ID, toWriteBytes)

	log.Logger.Info("[4-CrashWriteKafka] write kafka success", zap.String("topic", connector.GetTopic(data.Event.Category)), zap.String("id", data.Event.ID), zap.String("msg", string(toWriteBytes)))
	return message.Messages{msg}, nil
}

func LagWriteKafka(msg *message.Message) ([]*message.Message, error) {
	data := msg.Context().Value("data").(ContextData)

	toWriteBytes, contextErr := json.Marshal(data.Event)
	if contextErr != nil {
		data.Status = state.StatusUnmarshalError
		return nil, contextErr
	}

	msg = message.NewMessage(data.Event.ID, toWriteBytes)

	log.Logger.Info("[4-LagWriteKafka] write kafka success", zap.String("topic", connector.GetTopic(data.Event.Category)), zap.String("id", data.Event.ID), zap.String("msg", string(toWriteBytes)))
	return message.Messages{msg}, nil
}

测试

上报PERF_LAG Event可以并发处理 2 条消息,不必等待上一条消息处理完

多次测试发现是由于两条消息走了不同的 Handler

暂未修复,明明是同一主题的两条消息却都走了两条不同的链路,而且 publisher 最后写回的主题也是写到了不同的主题上,并且上报另一个类型的事件,即另一个主题的消息却无法触发消费者消费!

暂定先写死两个主题名称测试是否正常

明日待办

  1. 开会讨论项目规划和任务分工
  2. 继续完成需求
相关推荐
gopher95111 小时前
go语言 数组和切片
开发语言·golang
gopher95111 小时前
go语言Map详解
开发语言·golang
Python私教1 小时前
Go语言现代web开发15 Mutex 互斥锁
开发语言·前端·golang
goTsHgo2 小时前
从底层原理上解释 ClickHouse 的索引
大数据·clickhouse
Hello.Reader2 小时前
ClickHouse 与 Quickwit 集成实现高效查询
python·clickhouse·django·全文检索
java知路6 小时前
go 以太坊代币查余额
golang
老李的森林6 小时前
嵌入式开发--STM32延时函数重构
stm32·单片机·嵌入式硬件·重构·延时
默子昂7 小时前
yolo自动化项目实例解析(三)重构 1.85
yolo·重构·自动化
gopher95117 小时前
go语言基础入门(一)
开发语言·golang
拓端研究室TRL7 小时前
Python用TOPSIS熵权法重构粮食系统及期刊指标权重多属性决策MCDM研究|附数据代码...
开发语言·python·重构