OpenTelemetry问题:Kubernetes中Zipkin副本间追踪数据拆分
在Scala项目中使用OpenTelemetry实现链路追踪,通过Zipkin查看追踪数据,当前遇到同一请求的追踪数据被拆分到两个Zipkin副本中的问题,具体情况如下:
项目代码
OpenTelemetry Tracer初始化代码
object OpenTelemetry { val urlForZipkin ="http://zipkin:9411/api/v2/spans" private val exporter: ZipkinSpanExporter = ZipkinSpanExporter.builder() .setEndpoint(applicationConfig.getString("urlForZipkin")) .build() private val labelSet: Attributes = Attributes.builder().put("service.name", "my-service").build() private val tracerProvider: SdkTracerProvider = SdkTracerProvider.builder() .addSpanProcessor(SimpleSpanProcessor.create(exporter)) .setResource(Resource.create(labelSet)) .build() private val openTelemetry: OpenTelemetry = OpenTelemetrySdk.builder() .setTracerProvider(tracerProvider) .build() val tracer: Tracer = openTelemetry.getTracer("myTracer") }
Span使用示例
val span = tracer.spanBuilder("my-route: Span").startSpan() val childSpan = tracer.spanBuilder("child-span") .setParent(Context.current.`with`(span)) .startSpan()
Kubernetes部署配置
Zipkin Deployment
apiVersion: apps/v1 kind: Deployment metadata: name: zipkin-deployment namespace: my-ns spec: replicas: 2 selector: matchLabels: app: zipkin-label template: metadata: name: zipkin-pod labels: app: zipkin-label spec: containers: - name: zipkin image: openzipkin/zipkin ports: - containerPort: 9411
Zipkin Service
--- apiVersion: v1 kind: Service metadata: name: zipkin namespace: my-ns spec: type: NodePort selector: app: zipkin-label ports: - port: 9411 targetPort: 9411
问题现象
同一请求的所有Span拥有相同的Trace ID,但由于Kubernetes Service默认的轮询负载均衡策略,这些Span会被分发到不同的Zipkin副本,导致无法在单个Zipkin实例中查看完整链路;当Zipkin副本数设为1时,所有追踪数据能正常聚合。
需求:让同一请求的所有追踪数据发送到同一个Zipkin副本。
方案1:修改Kubernetes Service的会话亲和性
通过设置Service的会话亲和性,让同一个应用Pod的所有请求都路由到同一个Zipkin副本,这样同一请求的所有Span(来自同一个应用Pod)会被发送到同一个Zipkin实例。
修改Zipkin Service的配置,添加会话亲和规则:
apiVersion: v1 kind: Service metadata: name: zipkin namespace: my-ns spec: type: NodePort selector: app: zipkin-label ports: - port: 9411 targetPort: 9411 # 添加会话亲和配置 sessionAffinity: ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 3600 # 可根据业务需求调整超时时间
优势:无需修改应用代码,仅通过Kubernetes配置实现;缺点:亲和性基于应用Pod的IP,若应用本身有多个副本,同一请求的Span可能分布在不同应用Pod,仍会被发送到不同Zipkin副本。
方案2:应用层基于Trace ID哈希选择Zipkin实例
在应用中实现基于Trace ID的哈希路由逻辑,确保同一Trace ID的所有Span发送到同一个Zipkin副本。
步骤1:维护Zipkin实例地址列表
通过Kubernetes DNS服务发现或配置中心获取所有Zipkin实例的地址,示例:
// 示例:两个Zipkin实例的具体地址 val zipkinEndpoints = List( "http://zipkin-0.zipkin.my-ns.svc.cluster.local:9411/api/v2/spans", "http://zipkin-1.zipkin.my-ns.svc.cluster.local:9411/api/v2/spans" )
步骤2:实现自定义SpanProcessor
基于Trace ID哈希选择对应的Zipkin Exporter:
// 自定义SpanProcessor,根据Trace ID路由到指定Zipkin实例 class TraceIdAwareSpanProcessor(exporters: List[ZipkinSpanExporter]) extends SpanProcessor { override def onStart(parentContext: Context, span: ReadWriteSpan): Unit = {} override def onEnd(span: ReadOnlySpan): Unit = { val traceId = span.getTraceId // 基于Trace ID哈希计算索引,选择对应的Exporter val index = Math.abs(traceId.hashCode) % exporters.size exporters(index).export(List(span)) } override def isStartRequired: Boolean = false override def isEndRequired: Boolean = true } // 初始化对应每个Zipkin实例的Exporter private val exporters = zipkinEndpoints.map(endpoint => ZipkinSpanExporter.builder().setEndpoint(endpoint).build() ) // 使用自定义SpanProcessor初始化TracerProvider private val tracerProvider: SdkTracerProvider = SdkTracerProvider.builder() .addSpanProcessor(new TraceIdAwareSpanProcessor(exporters)) .setResource(Resource.create(labelSet)) .build()
优势:能确保同一Trace ID的所有Span发送到同一个Zipkin副本,不受应用副本数影响;缺点:需要修改应用代码,且需维护Zipkin实例列表的更新逻辑。
方案3:配置Zipkin共享存储后端
不限制Span发送到哪个Zipkin副本,而是让所有Zipkin副本共享同一个存储后端(如MySQL、Redis、Elasticsearch),查询时Zipkin会从共享存储中聚合所有Span,展示完整链路。
以MySQL为例,修改Zipkin Deployment配置:
spec: template: spec: containers: - name: zipkin image: openzipkin/zipkin ports: - containerPort: 9411 env: - name: STORAGE_TYPE value: mysql - name: MYSQL_HOST value: mysql-service.my-ns.svc.cluster.local - name: MYSQL_PORT value: "3306" - name: MYSQL_DB value: zipkin - name: MYSQL_USER value: zipkin_user - name: MYSQL_PASS value: zipkin_password
优势:无需修改应用代码,Zipkin副本可水平扩展;缺点:需要额外部署和维护共享存储服务。
内容的提问来源于stack exchange,提问作者Asif khAn

