GoChatIAI -Go语言AI应用服务平台(3)

集成Jaeger链路追踪

01、引言

在分布式系统中,性能问题往往源于复杂的调用链路和网络延迟。为了解决这些问题,我们需要一种能够跟踪请求在系统中的完整路径,并分析潜在的性能瓶颈的技术。这种技术就是链路追踪。
● 链路追踪的核心思想是记录请求在分布式系统中的完整路径,包括请求的来源、经过的节点、处理时间等信息。通过将这些信息整合在一起,我们可以清晰地看到请求在系统中的实际路径,从而定位性能瓶颈。
● 在微服务系统中,少则五六个服务,多则上百个服务,如果某个环节出现问题了,一次调用可能涉及到很多服务,如果服务之间的日志没有关联,那么排查起来非常困难,这个时候就需要链路追踪。
● 链路追踪可以可视化地追踪请求从一个微服务到另一个微服务的调用情况,从而帮助问题的排查。另外一个方面就是链路追踪还可以帮助优化性能,可视化服务之间的依赖关系,并进行服务的监控与报警。
● 简单的实现就是在日志中定义一个统一的 TraceId, 串联整体调用链路,每个服务之间还会定义一个 spanId,标志服务内的调用链路。

02、链路追踪的作用

● 标识请求:为每个请求分配一个唯一的标识符,以便在系统中的各个节点进行关联。
● 记录日志:在每个节点上记录请求的标识符、处理时间等信息,并将这些信息发送到追踪系统。
● 聚合数据:将各个节点发送的日志数据进行聚合,形成完整的请求链路。
● 可视化展示:将聚合后的数据以图形化的方式展示出来,方便分析人员查看和诊断问题。

03、链路追踪应用

链路追踪在分布式系统中的应用非常广泛,主要包括以下几个方面:
● 性能优化:通过分析请求链路,找到性能瓶颈并进行优化,提高系统的吞吐量和响应时间。
● 故障排查:当系统出现故障时,可以通过链路追踪快速定位问题所在,帮助运维人员快速恢复系统。
● 监控与报警:通过实时监控请求链路,发现异常并及时报警,保证系统的稳定运行。
● 数据分析:通过对大量的链路数据进行分析,挖掘系统的潜在问题,为系统架构优化提供依据。

04、实践经验分享

在实际应用中,我们需要注意以下几点:
● 选择合适的链路追踪工具:根据系统的实际情况选择合适的链路追踪工具,如Zipkin、Jaeger等。
● 合理配置日志级别:在记录日志时,要根据实际需求配置合适的日志级别,避免产生过多的日志数据。
● 优化数据存储:对于大量的链路数据,需要选择合适的存储方案,如使用分布式存储或数据库集群来提高数据存储和查询效率。
● 培训与分析团队:提高分析团队对链路追踪工具的使用熟练度,以及问题诊断和分析能力。
总结来说,链路追踪是一种非常有效的分布式系统性能监控和诊断工具。通过深入了解其原理和实现方法,并结合实践经验进行应用,我们可以更好地管理和优化分布式系统,提高系统的稳定性、可靠性和性能。未来随着分布式系统的日益复杂,链路追踪技术的应用将会更加广泛和深入。我们期待更多的技术专家和开发者能够关注并推动链路追踪技术的发展,为解决分布式系统性能问题提供更多有效的解决方案。

05、微服务链路追踪方案

● Zipkin: ZipKin 是 Twitter 开源的一个实现分布式实时追踪系统,SpringCloud Sleuth 提供了与 Zipkin 的集成,通过在微服务中添加相关的依赖和配置,将追踪信息发送给 Zipkin 服务器,并且通过 Zipkin UI 实现可视化展示以及查询。
● Jaeger:Jaeger 是 Uber 开源的分布式追踪系统,也被纳入了 CNCF(云原生计算基金会)的维护。通过使用 SpringCloud Sleuth 和 Jaeger 客户端,将追踪信息发送到 Jaeger 实现监控信息的可视化。
● SkyWalking:SkyWalking 是 Apache 开源的一款应用系统监控与分析的工具,它提供了对 Java、Go、C++ 等多种语言的支持,并且可以与 Spring Cloud Sleuth 集成,将追踪数据发送到 SkyWalking 服务器进行可视化展示以及分析。
以上这些方案都可以和 Spring Cloud Sleuth 进行集成,然后完成链路追踪功能的实现。

06、jaeger安装和架构

01、windwos手动解压安装
1:安装包下载地址;https://www.jaegertracing.io/download/
jaeger-1.58.0-windows-amd64.tar.gz
2:然后解压
图片
3:命令行启动
图片

● 6831 链路生成日志的端口
● 16686 ui访问端口 : http://127.0.0.1:16686/search

图片

07、GO集成Jaeger

https://github.com/jaegertracing/jaeger-client-go
● 参考文档:https://github.com/jaegertracing/jaeger-client-go/blob/master/config/example_test.go

● 整合jaeger组件

go get github.com/uber/jaeger-client-go

核心思想

中间件:创建根 span 并注入到 context.Context
控制器:将 gin.Context 传递给 service 层
Service 层:从 context 获取父 span,创建子 span 并继续传递
Helper 层:从 context 获取当前 span,可以继续创建更深层的子 span
Context 传递:始终使用带 span 的 context 在函数间传递
这样,从 HTTP 请求到最底层的数据库操作或外部 API 调用,所有的操作都会在同一个链路追踪中显示,方便你监控和调试。

拦截器

package middleware

import (
	"GoChatAI/global"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/opentracing/opentracing-go"
	"github.com/uber/jaeger-client-go"
	jaegercfg "github.com/uber/jaeger-client-go/config"
	"go.uber.org/zap"
)

func Trace() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		// Jaeger 配置
		cfg := jaegercfg.Configuration{
			Sampler: &jaegercfg.SamplerConfig{
				Type:  jaeger.SamplerTypeConst,
				Param: 1,
			},
			Reporter: &jaegercfg.ReporterConfig{
				LogSpans:           true,

				//LocalAgentHostPort: fmt.Sprintf("%s:%d",
				//	global.Config.JaegerConfig.Host, global.Config.JaegerConfig.Port),

				// 改用 CollectorEndpoint http连接
				CollectorEndpoint: fmt.Sprintf("http://%s:%d/api/traces",
					global.Config.JaegerConfig.Host,
					global.Config.JaegerConfig.Port),
			},
			ServiceName: "GoChatAI",
		}

		// 获取链路
		tracer, closer, err := cfg.NewTracer(jaegercfg.Logger(jaeger.StdLogger))
		if err != nil {
			global.SugarLog.Error("创建jaeger的tracer失败", zap.Error(err))
			panic(err)
		}

		// 设置全局链路
		opentracing.SetGlobalTracer(tracer)
		defer closer.Close()

		// 从 HTTP 请求头中提取 span 上下文(如果有)
		wireContext, err := opentracing.GlobalTracer().Extract(
			opentracing.HTTPHeaders,
			opentracing.HTTPHeadersCarrier(ctx.Request.Header),
		)

		var startSpan opentracing.Span
		if err != nil {
			// 如果没有传入的 span 上下文,创建新的 root span
			startSpan = opentracing.StartSpan(ctx.Request.URL.Path)
		} else {
			// 如果有传入的 span 上下文,创建 child span
			startSpan = tracer.StartSpan(ctx.Request.URL.Path, opentracing.ChildOf(wireContext))
		}
		defer startSpan.Finish()

		// 将 span 注入到请求头中,以便传递给下游服务
		err = tracer.Inject(startSpan.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(ctx.Request.Header))
		if err != nil {
			global.SugarLog.Error("注入span到请求头失败", zap.Error(err))
		}

		// 创建一个包含 span 的 context,存入 gin.Context
		ctxWithSpan := opentracing.ContextWithSpan(ctx.Request.Context(), startSpan)
		ctx.Request = ctx.Request.WithContext(ctxWithSpan)

		// 将 tracer 和 span 存入 gin.Context
		ctx.Set("tracer", tracer)
		ctx.Set("parentSpan", startSpan)

		ctx.Next()

	}
}

核心方法

// 发送消息
func (s *SessionService) ChatSend(c *gin.Context, userName string, sessionID string, userQuestion string, modelType string) (string, code.Code) {

	// 从 gin.Context 中获取带 span 的 context
	ctx2 := c.Request.Context()

	// 从 context 中获取父 span
	parentSpan := opentracing.SpanFromContext(ctx2)
	if parentSpan == nil {
		// 如果没有父 span,创建一个新的
		parentSpan = opentracing.GlobalTracer().StartSpan("ChatSend")
		defer parentSpan.Finish()
		ctx2 = opentracing.ContextWithSpan(ctx2, parentSpan)
	}

	// 添加业务标签到父 span
	parentSpan.SetTag("userName", userName)
	parentSpan.SetTag("sessionID", sessionID)
	parentSpan.SetTag("modelType", modelType)

	// 步骤1:获取AIHelper(创建子 span)
	aiInfo, code_ := s.getAIHelper(ctx, parentSpan, userName, sessionID, modelType)
	if code_ != code.CodeSuccess {
		return "", code_
	}

	// 步骤2:生成AI回复(创建子 span)
	aiResponse, code_ := s.generateResponse(ctx, parentSpan, userName, userQuestion, aiInfo)
	if code_ != code.CodeSuccess {
		return "", code_
	}

	return aiResponse, code.CodeSuccess
}


func (s *SessionService) getAIHelper(
	parentSpan opentracing.Span,
	userName string,
	sessionID string,
	modelType string) (interface{}, code.Code) {
	// 创建子 span
	getAIHelperSpan := opentracing.StartSpan(
		"get_aihelper",
		opentracing.ChildOf(parentSpan.Context()),
	)
	defer getAIHelperSpan.Finish()

	// 添加子 span 入参标签
	getAIHelperSpan.SetTag("userName", userName)
	getAIHelperSpan.SetTag("sessionID", sessionID)
	getAIHelperSpan.SetTag("modelType", modelType)

	// 业务逻辑:获取 AIHelper
	manager := aihelper.GetGlobalManager()
	config := map[string]interface{}{
		"apiKey": "your-api-key", // TODO: 从配置中获取
	}

	helper, err := manager.GetOrCreateAIHelper(userName, sessionID, modelType, config)
	if err != nil {
		// 记录错误到 span
		getAIHelperSpan.SetTag("error", true)
		getAIHelperSpan.LogKV("error", err.Error())
		global.SugarLog.Error("ChatSend GetOrCreateAIHelper error:", err)
		return nil, code.AIModelFail
	}

	return helper, code.CodeSuccess

}

// generateResponse 生成AI回复(子功能)
func (s *SessionService) generateResponse(
	ctx context.Context,
	parentSpan opentracing.Span,
	userName string,
	userQuestion string,
	aiHelper interface{},
) (string, code.Code) {

	// 创建子 span
	createRespSpan := opentracing.StartSpan(
		"generate_response",
		opentracing.ChildOf(parentSpan.Context()),
	)
	defer createRespSpan.Finish()

	// 添加子 span 标签
	createRespSpan.SetTag("userName", userName)
	createRespSpan.SetTag("questionLength", len(userQuestion))

	// 从 context 或参数中获取 helper
	helper, ok := aiHelper.(*aihelper.AIHelper)
	if !ok {
		createRespSpan.SetTag("error", true)
		createRespSpan.LogKV("error", "invalid ai helper type")
		return "", code.AIModelFail
	}

	// 调用 helper 的 GenerateResponse 方法,传递带 span 的 context
	aiResponse, err := helper.GenerateResponse(userName, ctx, userQuestion)
	if err != nil {
		createRespSpan.SetTag("error", true)
		createRespSpan.LogKV("error", err.Error())
		global.SugarLog.Error("ChatSend GenerateResponse error:", err)
		return "", code.AIModelFail
	}

	// 记录响应长度
	createRespSpan.SetTag("responseLength", len(aiResponse.Content))

	return aiResponse.Content, code.CodeSuccess
}

链路效果

图片

Docker安装jaeger

安装jaeger
● 官网:https://www.jaegertracing.io/
● docker安装地址如下:https://www.jaegertracing.io/docs/1.58/getting-started/
● 安装包下载地址;https://www.jaegertracing.io/download/

  -p 6831:6831/udp \  # Agent端口(如果使用Agent)
  -p 16686:16686 \ # Jaeger UI 端口
  -p 4317:4317 \ # OTLP gRPC端口
  -p 4318:4318 \ # OTLP HTTP端口
  -p 14268:14268 \ # HTTP端口,接收直传的span


docker run -d --name jaeger \
  --network mynet \
  -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 4317:4317 \
  -p 4318:4318 \
  -p 14250:14250 \
  -p 14268:14268 \
  -p 14269:14269 \
  -p 9411:9411 \
  jaegertracing/all-in-one

配置文件

[jaegerConfig]
host = "jaeger"
port = 14268

后续功能开发

1.用户会话信息:如用户信息,读多写少,可缓存 5-10 分钟。
2.热点聊天历史:最近消息,可用 LRU 缓存。

posted @ 2026-03-17 16:48  oow3b  阅读(21)  评论(0)    收藏  举报