在AWS Fargate上部署Grafana Loki和Grafana Tempo而不用Kubernetes的指南

488 阅读7分钟

在Seniorlink,我们提供服务和技术来支持家庭照顾他们的亲人在家里。在过去的两年里,我们在美国各地扩展了我们的项目,因此我们对观察我们的应用系统的需求也在增长。

我们密切关注着Grafana LokiGrafana Tempo产品的发展;我们计划与Loki整合,以取代昂贵且难以维护的Graylog(Elasticsearch)集群,并使用Tempo引入分布式跟踪,以提高我们的服务观察能力。

Loki和Tempo都继承了Cortex的核心部分,遵循类似的架构模式。数据通过分发器和摄取器流入存储后端,并通过查询前端和查询工作者读取数据。系统的每一个点都是独立可扩展的。了解系统架构以及写和读路径的流程将大大有助于你实施AWS Fargate部署。

我们的应用程序仍然主要部署在EC2上。我们还没有达到Kubernetes适合的规模或复杂性,但我们希望我们的Loki/Tempo部署尽可能是短暂的。我们决定为这两个系统采用纯AWS ECS Fargate部署。Fargate是AWS的无服务器容器编排解决方案,允许我们运行容器工作负载,而不必担心主机管理问题。

不过Grafana实验室团队主要是为Kubernetes部署而开发的,所以这篇文章将讨论将Loki或Tempo部署到AWS Fargate时需要克服的一些挑战。Fargate没有Helm或Jsonnet的配置,我们必须研究Grafana实验室团队的系统,并从头合成到我们的AWS基础设施。

任务配置

部署到Fargate的第一个挑战是服务的配置。

Kubernetes提供了许多将数据填充到pod中的选项(ConfigMaps、Secrets、InitContainers等等),但在Fargate上,存储是短暂的,除非你依赖EFS(NFS mount)。

Fargate任务定义接受环境变量和秘密,但其他一切都必须建立在图像中或在运行时拉出。我们想避免为几个配置文件使用EFS或安装AWS CLI来从S3拉取配置的成本和笨拙。Loki/Tempo的配置足够复杂,把所有东西都指定为CLI参数会很乏味,而且容易出错。我们选择使用Grafana docker图像作为基础层,并使用模板化的配置文件(使用Go模板语法)和Gomplate,这是一个小工具,可以使用各种数据源渲染Go模板。

然后,这些图像被发布到我们的AWS图像仓库(AWS ECR)。Loki和Tempo原生不允许通过环境变量进行配置,但Gomplate工具让我们可以解决这个问题,这样Fargate任务就可以轻松地将数据从AWS填充到服务中。

只要通过命令行参数或环境变量指定 "目标 "微服务,所有Loki和Tempo服务都可以共享同一个配置文件,所以你只需要做一个配置:

chunk_store_config:
 chunk_cache_config:
   redis:
     endpoint: "{{ .Env.REDIS_ENDPOINT }}:6379"
     expiration: 6h

在容器启动时,一个入口脚本运行gomplate,用AWS ParameterStore的值渲染配置文件,然后启动Loki或Tempo作为主容器进程。我们在图像中添加了tiini,以管理容器作为PID1,并捕获/转发SIGTERM和SIGKILL信号给应用程序。这种模式在我们的Loki和Tempo镜像中都有使用:

#!/bin/sh
/etc/loki/gomplate -f /etc/loki/loki.tmpl -o /etc/loki/loki-config.yml --chmod 640
loki -config.file /etc/loki/loki-config.yml 

服务发现

当Loki和Tempo在一个分布式系统中运行时,它们必须在服务对等体之间协调数据和活动。底层的Cortex库允许有几种方式来做这件事,例如etcd或Consul或Gossip成员列表。

成员列表最像Kubernetes提供的无头服务,但在Fargate上你需要引入一些额外的部分。AWS ECS Discovery(即AWS CloudMap)为Fargate部署提供了一个类似的机制。ECS Discovery将ECS服务与Route53 A或SRV记录(如果你希望端口可被发现)联系起来,并在容器启动时自动(去)注册目标。

首先创建一个服务发现的命名空间,然后是每个服务的发现定义。最后,Fargate服务引用ECS发现命名空间。下面是这一配置的一个示例terraform片段。完成后,成员列表配置引用了CloudMap DNS记录,该记录将包含注册的Fargate任务的所有IPv4地址:

resource "aws_service_discovery_private_dns_namespace" "this" {
 name = ecs.yourdomain.com
 vpc  = data.aws_vpc.this.id
}
resource "aws_service_discovery_service" "ingester" {
 name = "ingester"
 
 dns_config {
   namespace_id   = aws_service_discovery_private_dns_namespace.this.id
   routing_policy = "MULTIVALUE"
   dns_records {
     ttl  = 5
     type = "A"
   }
 }
 health_check_custom_config {
   failure_threshold = 5
 }
}

值得注意的是,一些Loki和Tempo服务会查询容器网络设备,以发现它们的IP地址并将其注册到成员列表中。这个查询是通过Cortex库完成的,默认情况下是寻找`eth0`作为设备。Fargate 1.4.0(现在在AWS中被选为默认的 "最新")使用eth0作为AWS的内部设备,并将容器的外部网络接口移至`eth1`。这表现为加入成员列表的容器,但完全无法相互通信,因为注册地址是内部容器IP,而不是实际的网络IP。如果你看到成员列表条目与你的VPC子网CIDRs不匹配,你就遇到了这个问题解决方法是,如果你使用Fargate 1.4.0,在Loki/Tempo配置中设置首选网络设备:

ingester:
 lifecycler:
   # for faragate 1.4.0 use eth1; use the default for other platforms
   interface_names: ["eth1"]

API进入

一旦Loki或Tempo启动并运行,你就需要一种方法将流量路由到适用的服务,以便Grafana、Promtail或Grafana Agent能够到达微服务后端。这需要按路径分配流量,所以我们转向AWS应用负载平衡器。根据每个服务的API,Loki和Tempo的配置略有不同。请参考具体的Grafana文档以了解其差异,但下面是允许Grafana从单一入口使用分布式查询-前端和查询器服务的路由配置:

resource "aws_alb_listener_rule" "query_frontend" {
 listener_arn = aws_alb_listener.https.arn
 action {
   type             = "forward"
   target_group_arn = aws_alb_target_group.query_frontend.arn
 }
 condition {
   path_pattern {
     values = [
       "/loki/api/v1/query",
       "/loki/api/v1/query_range",
       "/loki/api/v1/label*",
       "/loki/api/v1/series"
     ]
   }
 }
}
 
resource "aws_alb_listener_rule" "querier_tail_websocket" {
 listener_arn = aws_alb_listener.https.arn
 action {
   type             = "forward"
   target_group_arn = aws_alb_target_group.querier.arn
 }
 condition {
   path_pattern {
     values = [
       "/loki/api/v1/tail"
     ]
   }
 }
}

启动和优雅关机

如果你需要修改官方的Loki或Tempo镜像来运行初始化和配置脚本,你还需要确保处理信号捕获。如前所述,一个很好的轻量级选项是tini,但你也可以在脚本中建立自己的sigterm trapper,如果这更适合你的组织:

#!/bin/sh
pid=0
sigterm_handler() {
 if [ $pid -ne 0 ]; then
   kill -TERM "$pid"
   wait "$pid"
 fi
 exit 143;
}
 
trap 'kill ${!}; sigterm_handler' TERM
# add your tasks here
pid="$!"
echo "PID=$pid"
 
# wait forever
while true
do
 tail -f /dev/null & wait ${!}
done

优雅关闭对于Loki和Tempo摄取器来说尤其值得关注,因为它们可能有未填充的块没有刷新到后端。根据你的日志量、痕迹和配置,这些数据块可能会在采集器上停留一个小时或更长时间。在没有持久性存储的纯Fargate部署中,这是个问题。

Kubernetes摄取器能够使用持久性卷索赔来恢复摄取器重启或关闭之间的数据,而不会丢失。捕获并转发来自Fargate的SIGTERM是确保摄取器有足够时间将数据块冲到存储的第一步。第二步是增加ECS容器定义中的`stop_timeout`参数。AWS允许的最大时间是120秒,之后Fargate会发出最后的SIGKILL来终止任务。

最近对Loki和Tempo服务API的修改也包括了一个冲洗端点,以迫使摄取者将块提交到备份存储(在我们的例子中是AWS S3)并关闭摄取者。我们强烈建议使用这个端点作为你的init系统或自动化的一部分,以防止数据丢失。

总结

在Seniorlink,我们在生产中使用Loki和AWS Fargate已经一年多了,大约四个月前开始部署Tempo,同时将OpenTelemetry追踪整合到我们的应用程序中。我们的Loki服务目前每秒摄取约1,000行,Tempo平均每秒摄取250个跨度和50个追踪,这些都来自于目前的一组仪器化应用。采用Grafana、Loki和Tempo使我们能够对应用行为有一个统一的看法,并减少应用故障排除的分析时间。