为什么您不能忽视分布式追踪以实现可观测性

导航至

可观测性是一个热门话题,但很多人并不真正了解它的含义。现在每个人都在阅读关于监控与可观测性的文章,我有机会体验我认为是这场运动背后的主要概念。

首先,监控很复杂。仪表板无法扩展,因为它们通常只在发生中断后才显示您需要的信息,而且在某些时候,在图表中寻找峰值变成了一种令人眼睛疲劳的练习。这不是监控——这只是一种“不太”聪明的方式来理解某些东西不起作用。换句话说,监控只是冰山一角,而坚实的基础是您对系统的了解。

然而,如今分布式系统过于复杂,难以理解。单个请求可以在许多服务和应用程序之间来回传递,有些甚至不是您的公司拥有的。处理请求的每个参与者都可能失败,当这种情况发生时,您需要一种方法来回答基本问题:“刚刚发生了什么?” 可观测性通过一套工具、方法和思维模式来实现这一点,使您和您的团队能够了解系统。

特别是,我认为分布式追踪是您需要在工具箱中拥有的可观测性工具。在这里,我分享我的经验,这些经验让我得出这个结论。

我在 InfluxData 工作,担任 SRE,特别是在我们的 SaaS 产品 InfluxDB Cloud 上。几个月前,在经历了疯狂的增长之后,我们了解到我们自研的编排器(当时是这样)是不可持续的,它没有给我们足够的信心来了解事情是否处于正常运行状态。幸运的是,我们非常了解代码,因为我们是从头开始编写的,因为我们没有使用标准的配置管理工具或编排器(如 Kubernetes)——我们编写了自己的小型编排器来解决我们的特定用例。它设置在 AWS 上每个客户的隔离环境中的守护程序上,其中包含 InfluxDB、Chronograf、Kapacitor 和我们的服务提供的其他附加组件。

我们决定从重要且最复杂的流程开始重构应用程序

  • 集群创建:为新客户创建子网、安全组、实例的流程;
  • 集群终止:当客户停止为服务付费时使用;
  • 附加组件创建:启动 Grafana、Kapacitor 或 Chronograf 等附加组件。

为此,我们使用了一种称为 响应式规划 的模式。基本上,您创建一个计划(例如集群创建),并将计划拆分为不同的步骤

  • 创建安全组;
  • 配置入口/出口权限;
  • 创建实例;
  • 等待实例运行;
  • 创建负载均衡器。

有一个调度器接受此计划并逐个执行每个步骤,最重要的是它第二次执行整个计划。第二次执行计划时,它应该不返回任何步骤,因为所有步骤都应该已经执行,这意味着计划已解决。这在这种情况下非常棒,因为它迫使您仔细检查一切是否都在正常运行。例如,计划检查安全组是否在 AWS 上创建,如果已创建,则不再返回该步骤。这使得配置非常可靠。

另一个好处是所有执行日志都在同一个位置,即调度器内。这意味着只有一个地方可以查看以了解正在发生的事情。调度器看起来像这样

func (s *Scheduler) Execute(ctx context.Context, p Plan) error {
	for {
                       // Create the plan.
		steps := p.Create(ctx)
		if len(steps) == 0 {
			break
		}
		err := s.react(ctx, steps)
		if err != nil {
			return err
		}
	}
	return nil
}

The react function is recursion because steps can return new steps.

func (s *Scheduler) react(ctx context.Context, steps []Procedure) error {
	for _, step := range steps {
		span, _ := opentracing.StartSpanFromContext(ctx, step.Identifier())
		step.WithSpan(span)

		logger := s.logger
		f := []zapcore.Field{zap.String("step", step.Identifier())}
		zipkinSpan, ok := span.Context().(zipkin.SpanContext)
		if ok == true && zipkinSpan.TraceID.Empty() == false {
			f = append(f, zap.String("trace_id", zipkinSpan.TraceID.ToHex()))
		}
		logger = s.logger.With(f...)
		step.WithLogger(logger)

		innerSteps, err := step.Do(ctx)
		if err != nil {
                             ….
		}
		span.Finish()
		if len(innerSteps) > 0 {
			if err := s.react(ctx, innerSteps); err != nil {
				return err
			}
		}
	}
	return nil
}

在每个步骤中,除了 Do() 函数之外,要执行的逻辑还有两个函数:WithLoggerWithSpan。我添加此函数仅用于可观测性目的。如您所见,我正在使用 opentracing 来检测我的计划。

通过这几行代码,我能够配置记录器始终公开 trace_id,这样我就可以轻松地按请求查询我的日志。在步骤内部,我可以使用 span 轻松访问并根据 span 内部发生的事情获得更多上下文。例如,我们使用 etcd。在 etcd 中保存信息的步骤包含键和值。当我查看跟踪时,我可以了解记录在计划执行期间如何更改。

此外,我们有一个前端,它记录与后端交互的每个 trace_id

Nov 15 19:04:45  PATCH https://what.net/v1/clusters/idg trace_id:d572232a8fed45fa 422

我能够从日志中获取此信息,因为后端将 trace_id 作为 HEADER 返回。如果您能够教用户附加它(例如,在支持票证中),这是另一种从日志或直接从特定请求中查找跟踪的简单方法。

然而,几天前,我们遇到了一个问题。一些集群(其中一小部分)在创建过程中失败,但 AWS 没有报告任何错误(顺便说一句,我们也跟踪所有 AWS 请求)。所有资源都已创建,但 EC2 未在目标组中注册。

集群创建是一个复杂的流程,因为它与 AWS 有 40 多个交互,并且可能需要 10 多分钟才能完成所有步骤。但是查看跟踪,很容易理解错误发生在哪里。这是集群运行的 EC2 部分的快照,并将它们注册到负载均衡器

每个步骤都有一个名称,我正在查看的步骤是 register_cluster_node_to_lb。如您所见,它调用了 AWS.EC2 一次以获取要附加的所有实例 ID,并调用了两次 AWS.ELBv2 服务:一次获取正确的目标组,一次注册实例。

将此跟踪与失败的跟踪进行比较,可以很容易地看到在第二个屏幕中只有一个对 AWS.ELBv2 的请求。所以这就是问题所在!最终是一个未妥善处理的错误。

此特定应用程序不处理大量负载,但具有关键流程,其中故障排除能力非常重要。了解计划如何执行是一项功能,而不是一个选项。这就是为什么我们跟踪所有 AWS 请求的原因;例如,甚至请求和响应

可观测性不仅在生产中很重要,在开发中也很重要,因为它教会您程序如何工作。这就是为什么我在开发中也使用此设置的原因。可观测性是提高您开发功能的速度和质量的能力。这很棒,因为它使可观测性非常容易改进和保持最新。或者,监控仅在生产中重要,您通常不关心在开发周期中触发警报。

我们目前在生产环境中运行此系统,这给了我必要的信心,以便在我们的支持团队要求我对客户问题进行故障排除时正确地帮助他们。这特别有帮助,因为在几秒钟内,我可以直观地了解故障发生在哪里,因为响应式计划突出显示了要重构、测试或优化的相关代码。

我认为分布式追踪是开发周期的关键,因为现代应用程序的架构与旧应用程序的架构截然不同——不是因为现代应用程序更小或更微型,而是因为存在许多与外部数据库、第三方服务的集成点等等。断路器和重试策略使调试变得更加复杂,并且如果某些故障不经常发生,则难以预测或处理成本非常高。考虑到这一点,对于某些故障,最好有一种快速了解新问题的方法,而不是尝试预测和避免每种可能的故障,因为这根本不可能了。