亚马逊AWS官方博客

使用 OpenTelemetry 实现现代化应用可观测性指南

正如我们首席技术官 Werner Vogels 博士经常喜欢说的那样:“任何事物总会出错,只是时间问题。” 没有绝对不会出错的应用程序,也没有永远一直运行的应用程序,我们必须假设我们将需要处理故障,并且通常会在您最不希望它们发生的时候出现。特别是在云原生领域,系统越来越复杂,运维人员需要在问题出现时快速发现问题,快速响应并解决问题,这就需要一个可观测的系统。

优秀的可观测性系统不仅面向运维人员,同时也面向开发人员,它能采集和分析来自应用程序和基础设施的数据,随时随地了解系统运行状态,不仅回答“系统发生了什么”,还要回答“为什么会这样”。使用者不仅可以通过该系统快速发现问题并定位到根因,还能通过定期巡检可观测性系统指标,及时发现系统的潜在问题并进行优化,从而避免真正的事故发生,做到防范于未然。

我们的解决方案基于 AWS Distro for OpenTelemetry(ADOT)提供了一个完全托管的平台,利用 Amazon Managed GrafanaAmazon Managed Service for Prometheus,和 Amazon OpenSearch Service 来存储和分析三个核心可观察性维度:指标、日志和跟踪。这些核心维度可以进行关联,以加快问题解决或回顾性分析,并且可以进一步增强解决方案,以构建应用程序堆栈的自愈能力。

一、方案架构

本文重点关注在 Amazon EKS 环境下,如何用开源组件进行全栈可观测性实践。

  1. EKS 控制平面的日志自动传输到 CloudWatch Logs,通过 Subscription 传输到 OpenSearch Service
  2. 应用将日志通过 Fluent Bit 传输至 OpenSearch Service(或如图例通过 DataPrepper 统一发送日志到 OpenSearch)
  3. EKS 控制平面和应用容器的性能指标通过 ADOT Collector 采集并存入 Amazon Managed Prometheus(AMP)进行持久化存储和分析,并使用 Amazon Managed Service of Grafana(AMG)进行可视化
  4. 应用产生的跟踪数据通过 ADOT Collector 发送 Data Prepper,再采集到支持 OpenTelemetry 协议的 OpenSearch
  5. AMP 通过 Alert Manager 设置告警,并通过 SNS 发送至 Lambda,再调用飞书/企业微信/自建运维平台等接口做告警通知

本博客也将通过一个 Springboot Restful 的 Java 应用,深入浅出引导您在不变更原代码的前提下,借力 OpenTelemetry 标准,使用 Amazon 的托管开源服务进行快速部署和设置,实现跟踪,日志,应用和 JVM Metrics 指标等全方面观测。

二、前置条件

您可以参考微服务化可观测 Workshop 的 CloudFormation 模版创建必要的环境,或者在已有的环境基础上构建,本文假设您已经创建了以下资源。

  1. 创建 EKS 集群,并创建安装好至少一个可工作的 Node Group
  2. 为 EKS 集群安装必要的 Add-on,包括 AWS Load Balancer Controller
  3. 一个内网可访问的 Amazon OpenSearch 集群,样例为使用 FineGain Access 精细化权限管控
  4. 一台拥有 Admin 权限的 Cloud9 IDE 或者 EC2,并安装好 AWS CLI, EKSCTL,KUBECTL

下载样例代码

# Download lab repository
git clone https://github.com/xzp1990/observability-blog

三、Spring Boot 的测试程序

为了更易展示可观测性,我们通过 Spring Boot 创建的两个简单 Restful API 服务:

  1. 客户通过网页访问/greeting 服务,/greeting 服务会向后端/person 服务确认该用户是否存在
  2. 如果用户名存在,则返回欢迎 member 界面,如果该用户不存在,则返回提示注册的消息

查看样例代码

Greeting Controller(GreetingController.java)会调用另外一个微服务 person 去检查该用户是否已经是会员,根据返回结果向用户展示不同的回复。

#file:/observability-blog/sample-apps/12-greet/rest-service/src/main/java/com/example/restservice/GreetingController.java
@GetMapping("/greeting")
    public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
        
        String url = String.format("http://service-person.elb.svc.cluster.local/person?name=%s", name);
        logger.info(String.format("restTemplate url: %s", url));
        RestTemplate restTemplate = new RestTemplate();
        String result = restTemplate.getForObject(url, String.class);
            
        if (result.equals("true")){
            logger.info(String.format("The person %s is a member.", name));
            return new Greeting(counter.incrementAndGet(), String.format(template_member, name));
        }else{
            logger.info(String.format("The person %s is NOT a member, recommend to register.", name));
            return new Greeting(counter.incrementAndGet(), String.format(template_anonymous, name));
        } 
        
    }

Person Controller(PersonController.java)的功能演示做一次数据的查询,如果该用户存在则返回 true,否则返回 false。

#file:/observability-blog/sample-apps/13-person/rest-service/src/main/java/com/example/restservice/PersonController.java
private static final List<String> personlist= Arrays.asList("jason", "tracy", "olivia", "henry");
@GetMapping("/person")
    public String getFullName(@RequestParam(value = "name", defaultValue = "anonymous") String name) {
        
        logger.info(String.format("Verifing whether[ %s ] is a member", name));
        
        if (personlist.contains(name)){
            return "true";
        }else{
            return "false";
        } 
    }

构建并推送组件镜像到 ECR

  1. 获取或设置当前的帐号和区域
export ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account)
export TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
export AWS_REGION=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s 169.254.169.254/latest/dynamic/instance-identity/document | jq -r '.region')
aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com
  1. 构建并推送组件 greet 镜像到 ECR
-----
#build the greet jar project:
cd ~/environment/observability-blog/sample-apps/12-greet/rest-service
./mvnw clean package

#创建一个 repo greet
aws ecr create-repository --repository-name greet

cd ~/environment/observability-blog/sample-apps/12-greet/
docker build -t greet .
docker tag greet:latest ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/greet
docker  push ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/greet
  1. 构建并推送组件 person 镜像到 ECR
------
#build the person jar project:
cd ~/environment/observability-blog/sample-apps/13-person/rest-service
./mvnw clean package

#创建一个 repo person
aws ecr create-repository --repository-name person

cd ~/environment/observability-blog/sample-apps/13-person/
docker build -t person .
docker tag person:latest ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/person
docker  push ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/person

四、安装 ADOT 插件

ADOT 提供了一个安全的、适用于生产环境的 OpenTelemetry(OTel)发行版,允许进行仪表化和收集指标和跟踪数据。ADOT Operator 使用准入 webhooks 来变更和验证 Collector 自定义资源(CR)请求。在 Kubernetes 中,Webhook 需要一个 TLS 证书,API 服务器配置为信任该证书。有多种方法可以生成所需的 TLS 证书,但默认方法是手动安装最新版本的 cert-manager,所以在安装 ADOT 之前,我们需先安装 cert-Manager。

安装 Cert Manager

  1. 使用最新的 cer-manager yaml 文件直接在 EKS 集群中进行安装
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.8.2/cert-manager.yaml
  1. 通过以下命令确认 cer-manager 是否已经安装成功
jasonxie:~/environment $ kubectl get pod -w -n cert-manager
NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-66b646d76-bnhsf               1/1     Running   0          15d
cert-manager-cainjector-59dc9659c7-px44d   1/1     Running   0          15d
cert-manager-webhook-7d8f555998-7n66g      1/1     Running   0          15d

利用 EKS 附加组件将 ADOT Operator 部署到您的 Amazon EKS 集群

  1. 使用以下命令为 ADOT 在您的集群中应用所需的权限:
kubectl apply -f https://amazon-eks.s3.amazonaws.com/docs/addons-otel-permissions.yaml
  1. 使用以下命令将 ADOT Operator 安装到您的 Amazon EKS 集群中:
aws eks create-addon --addon-name adot --cluster-name <your_cluster_name>

状态字段的值将为”CREATING”,直到完成为止。

  1. 使用以下命令验证 ADOT 是否已安装并运行:
aws eks describe-addon --addon-name adot --cluster-name <your_cluster_name>
  1. 您也可以通过 EKS Console 的“Add-ons”中查看和管理。

五、Metrics 指标数据可视化

创建 Amazon Prometheus Workspace

使用命令行创建 Amazon Prometheus Workspace:

aws amp create-workspace --alias observability-blog --tags env=lab,app=springboot

部署 ADOT Collector 去自动采集 Metrics

  1. 创建一个 IAM Roles for Service Account(IRSA)给 Collector Pod 使用

通过 IRSA 对 pod 进行 IAM 授权是在 EKS 集群上为资源提供权限的最佳方式。下面的命令将使用 AWS CloudFormation 创建一个名为 otel-collector 的 K8s Namespace,创建一个名为 amp-iamproxy-ingest-role 的 K8s 服务账号,并创建一个新的 IAM 角色,附加了 AmazonPrometheusRemoteWriteAccess 策略。

eksctl create iamserviceaccount \
--name amp-iamproxy-ingest-role \
--namespace otel-collector \
--cluster <your_cluster_name> \
--attach-policy-arn arn:aws:iam::aws:policy/AmazonPrometheusRemoteWriteAccess \
--approve \
--override-existing-serviceaccounts
--region <your_region> 
  1. 通过样例创建 OTEL Collector 去自动采集 Metrics 指标

进入存放 Prometheus Collector 的配置文件 otel-collector-prometheus.yaml 所在的目录

cd ~/environment/observability-blog/sample-apps/01-otel-collector/kubernetes

查看该 OTEL 配置指标模版,此文件对 Kubernetes 中常用的 pod,service 等日志信息都做了配置,并通过 relabel_configs 设置让您可以在发布 deployment/services 时指定 annotation 来动态更新指标监听配置(如 path,port 和 schema)。

WORKSPACE_ID=$(aws amp list-workspaces --alias observability-blog | jq .workspaces[0].workspaceId -r)
AMP_ENDPOINT_URL=$(aws amp describe-workspace --workspace-id $WORKSPACE_ID | jq .workspace.prometheusEndpoint -r)
AMP_REMOTE_WRITE_URL=${AMP_ENDPOINT_URL}api/v1/remote_write
OTEL_COLLECTOR_NAMESPACE="otel-collector"
curl -O <otel-collector-prometheus.yaml>
sed -i -e s/AWS_REGION/$AWS_REGION/g otel-collector-prometheus.yaml
sed -i -e s^AMP_WORKSPACE_URL^$AMP_REMOTE_WRITE_URL^g otel-collector-prometheus.yaml
sed -i -e s^OTEL_COLLECTOR_NAMESPACE^$OTEL_COLLECTOR_NAMESPACE^g otel-collector-prometheus.yaml
kubectl apply -f ./otel-collector-prometheus.yaml

验证 OTEL Collector 是否正常运行

kubectl get all -n otel-collector

结果样例如:

jasonxie:~/environment/observability-blog/kubernetes $ kubectl get all -n otel-collector
NAME                                           READY   STATUS    RESTARTS   AGE
pod/observability-collector-746dd54ffd-rx2h7   1/1     Running   0          37s

NAME                                         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
service/observability-collector-monitoring   ClusterIP   10.100.13.19   <none>        8888/TCP   37s

NAME                                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/observability-collector   1/1     1            1           37s

NAME                                                 DESIRED   CURRENT   READY   AGE
replicaset.apps/observability-collector-746dd54ffd   1         1         1       38s
jasonxie:~/environment/observability-blog/kubernetes $ 

创建 Amazon Grafana Workspace

  1. 参考官方文档通过 console 创建 Grafana Workspace
    1. a.在 AWS Console 中打开 Amazon Grafana
    2. b.点击 Create workspace
    3. c.输入 Workspace 的名字,如 demo-amg,点击下一步
    4. d.如果没有 SAML(如 Akta),也可以使用 AWS SSO(AWS IAM Identity Center)进行验证登陆,可参考此处创建 AWS SSO 用户,这里假设已经创建好了该用户
    5. e.“Data Sources”那里需要选择“Amazon Managed Service for Prometheus“作为数据来源
    6. f.选择创建 Create workspace,并等待创建成功
    7. g.添加 Grafana 登录管理员用户。这里以添加 AWS SSO 用户为例。等待上一步执行成功后,点击进入该 workspace,在“Authentication”一栏里,点击“Configure users and user groups“,添加已经创建好的 AWS SSO 用户。之后再点击“Action“ → ”Make Admin”,把该用户设置为管理员
  2. 添加已经创建好的 Prometheus 的 data source
    1. a.在 Grafana 中间输入 aws,选择“AWS Data Source”
    2. b.选择 AMP 所在的区域,并点选上一步创建的 AMP observability-blog
    3. c.点击“Add 1 Data Source”添加

  1. 3.导入一个公共的 Grafana 仪表板,以便可以可视化来自 Kubernetes 环境的指标
    1. a.入口:在 Dashboard 页面,点击右边 New → Import 导入模版

  1. b.加载 3119 模版。在导入页面中,在“通过com导入”文本框中输入3119,然后点击加载
  2. c.选择数据源。在底部的下拉菜单中选择 Amazon Managed Service for Prometheus 数据源,然后点击导入

完成后,您将能够通过 Amazon Managed Service for Prometheus 数据源查看显示来自 EKS 集群的 Grafana 仪表板,如下所示

六、跟踪和日志数据采集准备

因为当前 OTEL Collector 不能直接持久化数据到 Opensearch 中,所以需要用到 Data Prepper 去把 Trace 跟踪数据进行转储到 OpenSearch。考虑到配置的易用性,也把 FluentBit DaemonSet 监听应用日志的目标指向 Data Prepper,让其统一转储到 OpenSearch。

安装 Data Prepper

  1. 进入“02-data-prepper” 目录
cd /observability-blog/sample-apps/02-data-prepper/kubernetes
  1. 在 data-prepper.yaml 文件中,搜索以下管道:raw-pipeline(第 25 行),service-map-pipeline(第 37 行)和log-pipeline(第 50 行),配置已经创建好的 OpenSearch Endpoint 地址,用户名和密码,在生产环境推荐使用 IAM Role for Service Account(IRSA)去进行授权,去避免需要用到明文的密码
...
raw-pipeline #Line 25
...
 sink: #Line 31
        - opensearch:
            hosts: [ "https://__AOSDomainEndpoint__" ]
            username: "__AOSDomainPassword__"
            password: "__AOSDomainUserName__"
...
service-map-pipeline #Line 36
...
 sink: #Line 43
        - opensearch:
            hosts: [ "https://__AOSDomainEndpoint__" ]
            username: "__AOSDomainPassword__"
            password: "__AOSDomainUserName__"
...
log-pipeline #Line 49
...
sink: #Line 61
        - opensearch:
            hosts: [ "https://__AOSDomainEndpoint__" ]
            username: "__AOSDomainPassword__"
            password: "__AOSDomainUserName__"
            index: sample_app_logs
  1. 创建 Data Prepper 的相关服务:
# kubectl apply -f data-prepper.yaml 
namespace/data-prepper created
configmap/data-prepper-config created
service/data-prepper created
service/data-prepper-metrics created
deployment.apps/data-prepper created

安装 FluentBit DaemonSet 采集应用日志数据

  1. 进入“00-fluentBitr” 目录
cd /observability-blog/sample-apps/00-fluentBit/kubernetes
  1. 在 fluentbit.yaml 文件中已经配置好了 traceInfo 去识别日志中的 Trace ID 和 Span ID,这里无需修改
...
#Line 153
        [PARSER]
        Name        traceInfo
        Format      regex
        Regex       trace_id=(?<traceId>[^ ]+) span_id=(?<spanId>[^ ]+) resource.service.name=(?<serviceName>[^]]+)
  1. 创建 FluentBit DaemonSet 服务
kubectl apply -f fluentbit.yaml

安装 OTEL trace collector 去采集 Trace 跟踪数据到 data prepper 服务

  1. 进入“01-otel-collector” 目录
cd /observability-blog/sample-apps/01-otel-collector/kubernetes
  1. 查看 Collector 的配置 ymal: otel-collector-trace-aos.yaml,可以根据生产环境压测配置相应的 CPU 数量
apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
metadata:
  name: otel
  namespace: otel-collector
spec:
  mode: deployment
  resources:
    requests:
      cpu: "1"
    limits:
      cpu: "1"
  config: |
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:55680
            
    exporters:
      otlp/data-prepper:
        endpoint: data-prepper.data-prepper.svc.cluster.local:21890
        tls:
          insecure: true
      
    service:
      pipelines:
        traces:
          receivers: [otlp]
          exporters: [otlp/data-prepper]
  1. 查看 collector 的运行情况
jasonxie:~/environment/observability-blog/01-otel-collector/kubernetes $ kubectl get all -n otel-collector                                                 READY   STATUS    RESTARTS   AGE
pod/my-collector-metrics-collector-594cc9f59-c7c4g   1/1     Running   0          13s
pod/otel-collector-75cf5b767f-j8xdx                  1/1     Running   0          2m38s

NAME                                                TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)     AGE
service/my-collector-metrics-collector-monitoring   ClusterIP   10.100.31.124   <none>        8888/TCP    13s
service/otel-collector                              ClusterIP   10.100.86.180   <none>        55680/TCP   2m38s
service/otel-collector-headless                     ClusterIP   None            <none>        55680/TCP   2m38s
service/otel-collector-monitoring                   ClusterIP   10.100.120.41   <none>        8888/TCP    2m38s

NAME                                             READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/my-collector-metrics-collector   1/1     1            1           13s
deployment.apps/otel-collector                   1/1     1            1           2m38s

NAME                                                       DESIRED   CURRENT   READY   AGE
replicaset.apps/my-collector-metrics-collector-594cc9f59   1         1         1       13s
replicaset.apps/otel-collector-75cf5b767f                  1         1         1       2m38s

七、使用自动注入功能采集程序跟踪数据

OpenTelemetry Operator 可以支持通过注入的方式去应用程序自动加载 instrumentation 依赖包,该功能支持热门的.NET,Java,Node.js 和 Python services 等框架。本 blog 中使用了应用广泛的 Spring Boot Java 框架作为示例,下面将演示如何通过自动注入,实现零代码改动地接入 OpenTelemetry 方案。

使用自动注入简化 Instrumentation 工作

参考 Injecting Auto-instrumentation 文档,在运行测试用例的 Namespace “elb”创建 Instrumentation, 当该 Namespace 有使用的 instrumentation.opentelemetry.io/inject-java: "true" annotation 的 deployment 时,Instrumentation 会在该 deployment 启动时,把相应的 java 包注入其中。您也可以参考文档 Manual Instrumentation 使用手动的方式在代码中去做 Instrumentation。

下面 yaml 文件配置了一个 Instrumentation,它会让同一 namespace(这里的 namespace=elb)下的配置了 instrumentation.opentelemetry.io/inject-java: "true" (参考 12-greet/kubernetes/greet-deployment.yaml)的 deployment 在启动时,会把自动把依赖的 java 包加载进来,免去了为每个 java 工程手动配置依赖包。关于如何为 java 工程加载依赖包,您可以参考这里

#12-greet/kubernetes/instrumentation-java.yaml
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: demo-instrumentation
  namespace: elb
spec:
  exporter:
    endpoint: http://otel-collector.otel-collector.svc.cluster.local:55680
  
  propagators:
    - tracecontext
    - baggage
  sampler:
    type: parentbased_traceidratio
    argument: "1"
  resource:
    addK8sUIDAttributes: true
  java:
    image:  public.ecr.aws/aws-observability/adot-autoinstrumentation-java:v1.27.0
    env:
      - name: OTEL_METRICS_EXPORTER
        value: "prometheus"
      - name: OTEL_EXPORTER_PROMETHEUS_PORT
        value: "9464"

Instrumentation 配置说明:

  1. Exporter 目标为内网的 otel collector “http://otel-collector.otel-collector.svc.cluster.local:55680”
  2. 使用特定版本的 java instrumentation “image:  public.ecr.aws/aws-observability/adot-autoinstrumentation-java:v1.27.0”, 也可以不指定
  3. 指定 OTEL_METRICS_EXPORTER 的端口为“9464”,OTEL 将以此端口来采指标 Metrics 数据
  4. 参考文档设定 OTEL 的指标 exporter 为“prometheus”的 pull 模式,而不是默认的 push 到 OTEL endpoint 模式。应用程序的 JVM metrics 就可以通过 prometheus Collector 直接采集,并制定(可参考 Grafa Dashboard 导入模板)

八、配置应用日志 log 包括 Trace 和 Span ID

在 Java 应用程序的用户日志中配置 TraceID 和 SpanID 数据注入非常简单。 一般来说,将日志包的 OpenTelemetry-Java-Instrumentation 添加到项目依赖项中即可,生产环境推荐使用如 Log4j2 的成熟日志收集引擎。

在上一小节中配置了 instrumentation 后,应用中已经自动注入了 Trace 的 Context信息(trace_id,span_id,trace_flags),只需对日志 Pattern 做下配置就能将 Trace 信息打印出来,下面是 java 工程中的 pom.xml 文件的示例:

  1. 用 Log4j2 替代 spring-boot 自带的 logger 的配置
<!-- Exclude Spring Boot's Default Logging -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        
        <!-- Add Log4j2 Dependency -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
  1. Log4j2 属性和格式配置:输出 trace_id、span_id 等字段,详见 log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
    <Properties>
        <Property name="LOG_PATTERN">
            %d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${hostName} --- %logger{36} trace_id=%X{trace_id} span_id=%X{span_id} resource.service.name=%X{service.name} -  [%15.15t] %-40.40c{1.} : %m%n%ex
        </Property>
    </Properties>
    <Appenders>
        <Console name="ConsoleAppender" target="SYSTEM_OUT" follow="true">
            <PatternLayout pattern="${LOG_PATTERN}"/>
        </Console>
    </Appenders>
    <Loggers>
        <Logger name="com.example.log4j2demo" level="debug" additivity="false">
            <AppenderRef ref="ConsoleAppender" />
        </Logger>

        <Root level="info">
            <AppenderRef ref="ConsoleAppender" />
        </Root>
    </Loggers>
</Configuration>

部署样例程序

查看 greet-deployment.yaml 部署文件:

  1. 通过 opentelemetry.io/inject-java: "true" 实现自动加载 OTEL 的 Opentelemetry-javaagent
  2. 通过 prometheus.io/scrape|port|path annotation 实现让 OTEL Prometheus 自动采 JVM 的 metrics
  3. 请替换所需 image 的$ACCOUNT_ID 和$AWS_REGION 来引入存放在您 ECR 的镜像 (person-deployment.yaml 也一样修改)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: greet
  namespace: elb
spec:
  replicas: 1
  selector:
    matchLabels:
      app: greet
  template:
    metadata:
      labels:
        app: greet
      annotations:
        instrumentation.opentelemetry.io/inject-java: "true"
        prometheus.io/scrape: "true"
        prometheus.io/port: "9464"
        prometheus.io/path: /
    spec:
      containers:
        - name: greet
            image: ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/greet:latest
          ports:
            - name: tcp
              containerPort: 8080

部署 greet 应用:

kubectl create namespace elb
cd cd /home/ec2-user/environment/observability-blog/sample-apps/12-greet/kubernetes
kubectl apply -n elb -f instrumentation-java.yaml
kubectl apply -n elb -f greet-deployment.yaml
kubectl apply -n elb -f greet-service.yaml
kubectl apply -n elb -f greet-ingress.yaml
kubectl get all -n elb

部署 person 应用:

cd cd /home/ec2-user/environment/observability-blog/sample-apps/13-person/kubernetes
kubectl apply -n elb -f person-deployment.yaml
kubectl apply -n elb -f person-service.yaml
kubectl get all -n elb

获取到 ingress 的地址:

jasonxie:~ $ kubectl get ingress -n elb
NAME        CLASS   HOSTS   ADDRESS                                                              PORTS   AGE
greet-alb   alb     *       k8s-elb-greetalb-02a8f3262c-1728699503.us-east-1.elb.amazonaws.com   80      36h
jasonxie:~ $ 

在命令行中或者在浏览器中访问 greet 程序(请自行替换 query 参数 name):

$ curl http://k8s-elb-greetalb-02a8f3262c-1728699503.us-east-1.elb.amazonaws.com/greeting?name=jason
{"id":3,"content":"Hello, dear Member jason!"}
$ curl http://k8s-elb-greetalb-02a8f3262c-1728699503.us-east-1.elb.amazonaws.com/greeting?name=lucy
{"id":4,"content":"Hello, lucy, Please fee free to register!"}

查看日志,检查 trace 的上下文 Context 是否注入成功。

$ kubectl logs pod/greet-b77ddbf7-fzkrs -n elb | tail -1
Defaulted container "greet" out of: greet, opentelemetry-auto-instrumentation (init)
2023-07-10 01:31:57.332  INFO greet-b77ddbf7-fzkrs --- com.example.restservice.GreetingController trace_id=64ab5f8d9d519aa5afc115372451f72b span_id=a0cd4ab6a276a02f resource.service.name= -  [nio-8080-exec-5] c.e.r.GreetingController : The person jason is a member.

$ kubectl logs pod/person-b6bdd68b5-gnf9n -n elb | tail -1 
Defaulted container "person" out of: person, opentelemetry-auto-instrumentation (init)
2023-07-10 01:31:57.325  INFO person-b6bdd68b5-gnf9n --- com.example.restservice.PersonController trace_id=64ab5f8d9d519aa5afc115372451f72b span_id=5ffa9fcfa2d80c8b resource.service.name= -  [nio-8080-exec-1] c.e.r.PersonController : Verifing whether[ jason ] is a member

可以看到上面 greet 和 person 输出日志的 trace_id 是相同的,但 span_id 不同,则说明他们处于同一个 trace 中的不同 span。该应用日志也已经通过 Fluentbit 采集到了 Opensearch, 下面我们将通过 OpenSearh 对 Trace 和 Log 进行联动查询。

九、通过 Opensearch 的可观测性插件实时检索 Trace 和日志

配署 Opensearch 日志的索引

  1. 连接 Opensearch:因为 Opensearch 存放在私有子网里面,我们使用在 VPC 公有子网的一台 EC2 作为 Proxy Server 去进行 Opensearch 的连接,以下样例为直接使用 ssh 建立通道在本地连接。
sudo ssh -L 443:<openserach domain>:443 -i "<your-pem-key.pem>" ec2-user@<your public ec2 ip>
  1. 打开 Opensearch Dashboard,访问 https://localhost/_dashboards/

  1. 创建索引模式。要在 OpenSearch 仪表板中搜索日志,需要先创建一个索引模式(Index Pattern)。请导航到堆栈管理(Stack Management),在左侧附近选择索引模式(Index Patterns),点击”创建索引模式”按钮(Create Index pattern),输入在 data-prepper 配置的日志模式“sample_app_logs*”, 并点击下一步(Next)。

  1. 在 Time Field 中选择“time”,并点击 Create index pattern 完成创建。您就可以在 OpenSearch 的 Discover 中查看日志了。

创建 Opensearch 应用分析

  1. 在 Amazon OpenSearch Dashboards 中,打开页面左上角的汉堡菜单,选择 OpenSearch 插件下的”Observability”。在左侧面板选择”Application Analytics”菜单,然后点击”Create Application”。

  1. 在”Create Application”界面,将”Name”字段输入为”Greet Application”,在”Description”字段中提供一个描述,比如”Create a greet application”。

  1. 在”Composition”下展开”Log source”部分,将”sample_app_logs”作为”Sample Application”的日志来源。在”Base Query”字段中输入以下 PPL 查询。在输入时,您将可以选择日志来源:source = sample_app_logs; 在”Services & entities”部分展开,选择以下所有服务:greet,person,将它们包含在”Sample Application”中进行调查。将其余设置保持不变,然后点击底部的”Create”按钮,完成应用程序的组合。

查看 Opensearch 可观测性 Dashboard

让我们检查从后端服务生成的日志和跟踪信息。导航到”Observability”插件下的”Application Analytics”屏幕,点击”Greet application”以分析应用程序中的跟踪组。

在”Overview”选项卡中,有一个名为”Latency by trace group”的表格。该视图将跟踪按照 HTTP 方法和路径进行分组,以便您可以查看特定操作的平均延迟、错误率和趋势。

在”Services”选项卡中,您可以看到应用程序中服务的两个视图。第一个表格视图列出了各个服务及与每个服务相关的趋势,如平均延迟、错误率和吞吐量。第二个视图是”Application Composition Map”,是一个交互式地图,显示各个服务之间的连接关系。

确保在选项卡右上角选择了正确的时间窗口,以查看生成的跟踪和相关项目。

在”Traces & Spans”选项卡中,点击其中一个 Trace Id 以查看单个跟踪。 跟踪详情界面将弹出,显示每个跟踪的”Time spent by service”视图、时间线和每个跨度的列表部分。

关闭跟踪详情窗口,并通过选择 TraceID 后面的剪贴板图标来复制 TraceID。

转到”Log Events”选项卡,并通过在”Base Query”字段中编写以下查询来搜索刚刚复制的 TraceID。当您输入文本时,自动完成将帮助您构建查询。将 TraceID 粘贴到查询表达式中。这将显示与 TraceID 相关的日志消息。

where traceId = '在引号之间粘贴您的 TraceID'

查询表达式应类似于下面的截图。点击”Refresh”按钮以搜索日志。它应该显示与 TraceID 相关的日志消息。日志行还提供详细的应用信息,如有错误也会包含有关错误发生原因和遇到异常的代码行的信息。

十、总结

本文介绍了如何使用 OpenTelemetry 实现现代化应用的可观测性。通过 AWS Distro for OpenTelemetry(ADOT)和 Amazon 托管的服务,可以收集和分析应用程序和基础设施的指标、日志和跟踪数据。博客重点讲解了在 Amazon EKS 环境下使用开源组件实现全栈可观测性的实践,包括日志传输、性能指标采集、应用跟踪和告警设置等。通过一个 Spring Boot 样例程序,展示了如何使用 OpenTelemetry 和 Amazon 的托管服务快速部署和设置跟踪、日志和指标等观测功能。运维工程师通过 Trace ID,可以快速的从海量日志中快速的查找到问题关联日志,减少故障的定位时间 mttk(mean time to know),而不是依靠工程师的经验。此外,ADOT 的 trace 信息里有很多的基础设施相关信息,如 POD ID 和 EC2 实例 ID 等,可以用来快速的在 Prometheus 里联动查看指标,帮助运维人员快速发现和解决问题,提高应用程序的可靠性和稳定性。

本篇作者

谢志鹏

亚马逊云科技技术客户经理,负责企业级客户的架构设计、成本优化和技术支持等工作,在亚马逊云科技支持多个知名游戏公司在全球的项目发行工作;拥有十年以上 IT 架构、运维经验,在云安全、应用微服务化架构等方面拥有丰富的实战经验。

吴传文

亚马逊云科技解决方案架构师,负责企业级客户的架构设计和咨询,在亚马逊云科技支持多个知名游戏公司在全球的项目发行工作;在互联网应用构建、网络优化加速、多媒体服务、WEB3 等方面有丰富的实战经验。