Observability:从零开始创建 Java 微服务并监控它 (二)

2022年12月12日   |   by mebius

这篇文章是继上一篇文章 “Observability:从零开始创建 Java 微服务并监控它 (一)” 的续篇。在上一篇文章中,我们讲述了如何创建一个 Java web 应用,并使用 Filebeat 来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用 APM 来监控应用并监督 web 服务的在线情况。

源码可以在地址https://github.com/liu-xiao-guo/java_observability进行下载。

摄入指标

指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有 1000 个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/最大值以获得更多指示。此外,这意味着你还需要考虑这些指标的持续时间。你需要每分钟一次还是每 10 秒一次?

为了从不同的角度查看你的应用程序,让我们获取一些指标。在此示例中,我们将使用 Metricbeat Prometheus 模块将数据发送到 Elasticsearch。

我们应用程序中使用的底层库是 micrometer.io,这是一个供应商中立的应用程序指标,结合其 Prometheus 支持tgcode来实现基于拉取的模型。你可以使用 Elastic 支持来实现基于推送的模型。这将要求用户在我们的应用程序中存储 Elasticsearch 集群的凭证数据。

向应用程序添加指标

1)将依赖项添加到我们的 build.gradle 文件中。

  // metrics via micrometer
  implementation 'io.micrometer:micrometer-core:1.5.4'
  implementation 'io.micrometer:micrometer-registry-prometheus:1.5.4'
  implementation 'org.apache.commons:commons-lang3:3.11'

2)将 micrometer 插件及其相应的导入添加到我们的 Javalin 应用程序中。

...
import io.javalin.plugin.metrics.MicrometerPlugin;
import io.javalin.core.security.BasicAuthCredentials;
...

Javalin app = Javalin.create(config -> {
   ...
   config.registerPlugin(new MicrometerPlugin());
);

App.java

package de.spinscale.javalin;

import io.javalin.Javalin;
import io.javalin.http.Handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.javalin.plugin.metrics.MicrometerPlugin;
import io.javalin.core.security.BasicAuthCredentials;

public class App {

    private static final Logger logger = LoggerFactory.getLogger(App.class);

    public static void main(String[] args) {
        Javalin app = Javalin.create(config -> {
            config.requestLogger((ctx, executionTimeMs) -> {
                String userAgent = ctx.userAgent() != null ? ctx.userAgent() : "-";
                logger.info("{} {} {} {} "{}" {}",
                        ctx.method(), ctx.req.getPathInfo(), ctx.res.getStatus(),
                        ctx.req.getRemoteHost(), userAgent, executionTimeMs.longValue());
            });

            conftgcodeig.registerPlugin(new MicrometerPlugin());
        });

        app.get("/exception", ctx -> {
            throw new IllegalArgumentException("not yet implemented");
        });
        
        app.exception(Exception.class, (e, ctx) -> {
            logger.error("Exception found", e);
            ctx.status(500).result(e.getMessage());
        });

        app.get("/", mainHandler());
        app.start(8000);
    }

    static Handler mainHandler() {
        return ctx -> {
            String userAgent = ctx.userAgent() != null ? ctx.userAgent() : "-";
            logger.info("This is an informative logging message, user agent [{}]", userAgent);
            ctx.result("Absolutely perfect");
        };
    }
}

3)添加一个新的 metrics 端点并确保 BasicAuthCredentials 类也被导入。

final Micrometer micrometer = new Micrometer();
app.get("/metrics", ctx -> {
  ctx.status(404);
  if (ctx.basicAuthCredentialsExist()) {
    final BasicAuthCredentials credentials = ctx.basicAuthCredentials();
    if ("metrics".equals(credentials.getUsername()) && "secret".equals(credentials.getPassword())) {
      ctx.status(200).result(micrometer.scrape());
    }
  }
});

在这里,Micrometer 类是一个名为 Micrometer.java 的自写类,它设置了几个指标监视器并为 Prometheus 创建了注册表,它提供了基于文本的 Prometheus 输出。

Micrometer.java

package de.spinscale.javalin;

import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.binder.jvm.JvmCompilationMetrics;
import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics;
import io.micrometer.core.instrument.binder.jvm.JvmHeapPressureMetrics;
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics;
import io.micrometer.core.instrument.binder.logging.Log4j2Metrics;
import io.micrometer.core.instrument.binder.system.FileDescriptorMetrics;
import io.micrometer.core.instrument.binder.system.ProcessorMetrics;
import io.micrometer.core.instrument.binder.system.UptimeMetrics;
import io.micrometer.prometheus.PrometheusConfig;
import io.micrometer.prometheus.PrometheusMeterRegistry;

public class Micrometer {

    final PrometheusMeterRegistry registry = new PrometheusMeterRegistry(new PrometheusConfig() {
        @Override
        public String get(String key) {
            return null;
        }

        @Override
        public String prefix() {
            return "javalin";
        }
    });

    public Micrometer() {
        Metrics.addRegistry(registry);
        new JvmGcMetrics().bindTo(Metrics.globalRegistry);
        new JvmHeapPressureMetrics().bindTo(Metrics.globalRegistry);
        new JvmThreadMetrics().bindTo(Metrics.globalRegistry);
        new JvmCompilationMetrics().bindTo(Metrics.globalRegistry);
        new JvmMemoryMetrics().bindTo(Metrics.globalRegistry);
        new Log4j2Metrics().bindTo(Metrics.globalRegistry);
        new UptimeMetrics().bindTo(Metrics.globalRegistry);
        new FileDescriptorMetrics().bindTo(Metrics.globalRegistry);
        new ProcessorMetrics().bindTo(Metrics.globalRegistry);
    }

    public String scrape() {
        return registry.scrape();
    }
}

4)重新编译你的应用程序并轮询指标端点。

curl localhost:8000/metrics -u metrics:secret

%title插图%num

这将返回基于行的响应,每行一个指标。 这是标准的 Prometheus 格式。

安装和配置 Metricbeat

要将指标发送到 Elasticsearch,需要 Metricbeat。 要下载并安装 Metricbeat,请使用适用于你系统的命令。我们可以到 Elastic 的官方网站Download Metricbeat • Ship Metrics to Elasticsearch | Elastic下载和你的 Elasticsearch 同样版本的 Metricbeat。针对我的 macOS,我使用如下的命令来进行下载:

curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-8.5.2-darwin-x86_64.tar.gz
tar xzvf metricbeat-8.5.2-darwin-x86_64.tar.gz

在 Merticbeat 的安装根目录下,我们会发现一个叫做 metricbeat.yml 的配置文件。

$ pwd
/Users/liuxg/elastic/metricbeat-8.5.2-darwin-aarch64
$ ls
LICENSE.txt              kibana                   metricbeat_org.yml
NOTICE.txt               logs                     module
README.md                metricbeat               modules.d
data                     metricbeat.reference.yml prometheus.yml
fields.yml               metricbeat.yml

我们需要来配置这个文件。和之前的 Filebeat 的配置类似,我们采用 API key 来进行配置:

metricbeat.yml

metricbeat.config.modules:
  path: ${path.config}/modules.d/*.yml
  reload.enabled: false

name: javalin-metrics-shipper

output.elasticsearch:
  hosts: ["localhost:9200"]
  protocol: "https"

  # Authentication credentials - either API key or username/password.
  api_key: "xZ5uyIQBVVocAV-RwzpW:Szm88RvESbm88Bmk0SYFGQ"
  ssl.certificate_authorities: ["/Users/liuxg/elastic/elasticsearch-8.5.2/config/certs/http_ca.crt"]

processors:
  - add_host_metadata: ~
  - add_cloud_metadata: ~
  - add_docker_metadata: ~
  - add_kubernetes_metadata: ~

在上面,我们使用和 Filbeat 一样的 API key。我们可以使用如下的命令来禁止 system 模块的启用,尽管不必要。

./metricbeat modules disable system
$ ./metricbeat modules disable system
Module system is already disabled

我们需要启动 prometheus 模块。打入如下的命令:

 ./metricbeat modules enable prometheus
$ ./metricbeat modules enable prometheus
Module prometheus is already enabled

我接下来需要多 prometheus 模块进行配置。它的配资文件位于 modules.d 目录下。

vi modules.d/prometheus.yml 

prometheus.yml

$ pwd
/Users/liuxg/elastic/metricbeat-8.5.2-darwin-aarch64
$ cat  modules.d/prometheus.yml 
- module: prometheus
  period: 10s
  hosts: ["127.0.0.1:8000"]
  metrics_path: /metrics
  username: "metrics"
  password: "secret"
  use_types: true
  rate_counters: true

经过上面的配置后,我们可以使用如下的命令来检查我们的配置是否成功:

./metricbeat test config
$ ./metricbeat test config
Config OK
./metricbeat test output
$ ./metricbeat test output
elasticsearch: https://localhost:9200...
  parse url... OK
  connection...
    parse host... OK
    dns lookup... OK
    addresses: ::1, 127.0.0.1
    dial up... OK
  TLS...
    security: server's certificate chain verification is enabled
    handshake... OK
    TLS version: TLSv1.3
    dial up... OK
  talk to server... OK
  version: 8.5.2

从上面的输出中我们可以看到配置的格式以及连接到 Elasticsearch 都是成功的。我们使用如下的命令来测试 prometheus 模块:

./metricbeat test modules prometheus
$ ./metricbeat test modules prometheus
prometheus...
  collector...OK
    result: 
    {
     "@timestamp": "2022-12-01T06:29:14.806Z",
     "event": {
      "dataset": "prometheus.collector",
      "duration": 36580333,
      "module": "prometheus"
     },
     "metricset": {
      "name": "collector",
      "period": 10000
     },
     "prometheus": {
      "jvm_threads_states_threads": {
       "value": 10
      },
      "labels": {
       "instance": "127.0.0.1:8000",
       "job": "prometheus",
       "state": "runnable"
      }
     },
     "service": {
      "address": "http://127.0.0.1:8000/metrics",
      "type": "prometheus"
     }
    }

它表明我们的模块 prometheus 是成功的。

为了能在 Kibana 中能生产相应的 metricbeat-* index pattern,我们执行如下的命令:

./metricbeat setup
$ ./metricbeat setup
Overwriting ILM policy is disabled. Set `setup.ilm.overwrite: true` for enabling.

Index setup finished.
Loading dashboards (Kibana must be running and reachable)
Loaded dashboards

接下来我们运行 Metricbeat:

sudo chown root metricbeat.yml
sudo chown root modules.d/prometheus.yml 
sudo ./metricbeat -e

注意:你将以 root 身份运行 Metricbeat,因此你需要更改配置文件的所有权,或运行 Metricbeat 指定 –strict.perms=false。 请参阅配置文件所有权和权限

验证 Prometheus 事件是否流入 Elasticsearch。

GET metricbeat-*/_search?filter_path=**.prometheus,hits.total
{
  "query": {
    "term": {
      "event.module": "prometheus"
    }
  }
}

在 Kibana 中查看 Metrics

由于这是来自我们 Javalin 应用程序的自定义数据,因此没有用于显示此数据的预定义仪表板。

让我们检查每个 log level 的日志消息数。

GET metricbeat-*/_search
{
  "query": {
    "exists": {
      "field": "prometheus.log4j2_events_total.counter"
    }
  }
}

我们可以通过如下的命令得到文档的个数:

GET metricbeat-*/_search
{
  "query": {
    "range": {
      "prometheus.log4j2_events_total.counter": {
        "gte": 0
      }
    }
  }
}

可视化日志消息的数量随时间的变化,按 log level 划分。 从 Elastic Stack 7.7 开始,有一种创建可视化的新方法,称为 Lens。

%title插图%num

%title插图%num

%title插图%num

%title插图%num

%title插图%num

%title插图%num

随着时间的推移可视化打开的文件

第二个可视化是检查我们的应用程序中打开的文件数。

由于没有人能记住所有字段名称,让我们先再次查看指标输出。

curl -s localhost:8000/metrics -u metrics:secret | grep ^process
$ curl -s localhost:8000/metrics -u metrics:secret | grep ^process
process_files_max_files 10240.0
process_start_time_seconds 1.669871554172E9
process_files_open_files 30.0
process_uptime_seconds 6995.98
process_cpu_usage 6.685723040584351E-4

让我们看一下 process_files_open_files 指标。 这应该是一个很少改变的相当静态的值。 如果你运行在 JVM 中存储数据或打开和关闭网络套接字的应用程序,则此指标会根据负载增加或减少。 对于 Web 应用程序,这是相当静态的。 让我们弄清楚为什么在我们的小型 Web 应用程序中打开了 30 个文件。

运行将在进程列表中包含你的应用程序的 jps。

$ jps
22178 Elasticsearch
17443 GradleServer
80996 Jps
17543 GradleDaemon
7754 Launcher
17930 GradleServer
18029 GradleDaemon
21006 javalin-app-all.jar
7278 
17390 gradle-language-server.jar
22096 CliToolLauncher
17459 org.eclipse.equinox.launcher_1.6.400.v20210924-0641.jar
19315 GradleDaemon
17907 gradle-language-server.jar
17556 GradleDaemon
51189 Logstash
17945 org.eclipse.equinox.launcher_1.6.400.v20210924-0641.jar
17467 GradleDaemon
17628 GradleDaemon

从上面的输出中我们可以看出来javalin-app-all.jar 对应的 process id 值为21006。我们使用如下的命令:

lsof -p 21006

你将看到比所有正在打开的文件更多的输出,因为一个文件也是一个正在发生的 TCP 连接。

添加一个端点以通过长时间运行的 HTTP 连接来增加打开文件的数量(每个连接也被视为一个打开的文件,因为它需要一个文件描述符),然后对其运行 wrk。我们在 App.java 中进行如下的修改:

App.java

...
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
...

public static void main(String[] args) {
...
    final Executor executor = CompletableFuture.delayedExecutor(20, TimeUnit.SECONDS);
    app.get("/wait", ctx -> {
        CompletableFuture future = CompletableFuture.supplyAsync(() -> "done", executor);
        ctx.result(future);
    });
...

每个 future 都会延迟 20 秒,这意味着单个 HTTP 请求保持打开状态 20 秒。

让我们运行一个 wrk 工作负载。

wrk -c 100 -t 20 -d 5m http://localhost:8000/wait
$ wrk -c 100 -t 20 -d 5m http://localhost:8000/wait
Running 5m test @ http://localhost:8000/wait
  20 threads and 100 connections

结果显示只发送了 20 个请求,考虑到处理时间,这是合理的。现在让我们在 Kibana 中使用 Lens 构建可视化。

%title插图%num

使用 Infrastructure 应用

现在让我们看一下 Kibana 中的 Infrastructure 应用程序。 选择ObservabilityInfrastructure

你只会看到来自单个 shipper 的数据。 尽管如此,当你运行多个服务时,并且能够按 Kubernetes pod 或主机对其进行分组的能力使你能够发现 CPU 或内存消耗增加的主机。

%title插图%num

%title插图%num

%title插图%num

这是 Javalin 应用发出的总事件计数器的面积图。 它正在上升,因为有一个组件轮询一个端点,而该端点又会生成另一条日志消息。 更陡峭的窥探是由于发送了更多的请求。 但是,突然下降是从哪里来的呢? JVM 重启。 由于这些指标不会持久化,因此它们会在 JVM 重启时重置。 考虑到这一点,通常最好记录速率而不是计数器字段。

检测应用程序

可观察性的第三个部分是应用程序性能管理 (APM)。 APM 设置包括一个接受数据的 APM 服务器(并且已经在我们的 Elastic Cloud 设置中运行)和一个将数据传送到服务器的代理。

代理有两个任务:检测 Java 应用程序以提取应用程序性能信息并将该数据发送到 APM 服务器。

APM 的核心思想之一是能够在整个堆栈中跟踪用户会话的流程,无论你是否有数十个微服务或单体应用来响应你的用户请求。这意味着能够在整个堆栈中标记请求。

要完全捕获用户活动,你需要在用户的浏览器中开始使用真实用户监控 (RUM),直至你的应用程序,该应用程序会向你的数据库发送 SQL 查询。

数据模型

尽管 APM 格局非常分散,但术语通常是相似的。两个最重要的术语是 Span 和 Transactions。更为详细的资料可以参考文章 “Observability:应用程序性能监控/管理(APM)实践”。

Transaction 封装了一系列 span,其中包含有关一段代码执行的信息。让我们看一下 Kibana APM 应用程序的屏幕截图。

%title插图%num

这是一个 Spring Boot 应用程序。 UserProfileController.showProfile() 方法被调用,被标记为事务。 里面有两个跨度。 首先,使用 Elasticsearch REST 客户端向 Elasticsearch 发送请求,然后使用 Thymeleaf 呈现响应。 在这种情况下,对 Elasticsearch 的请求比渲染要快。

Java APM 代理可以自动检测特定框架。 Spring 和 Spring Boot 支持很好,上面的数据是在 Spring Boot 应用中添加 agent 创建的; 无需配置。

目前有适用于 Go、.NET、Node、Python、Ruby 和浏览器 (RUM) 的代理。 不断添加代理,因此您可能需要查看 APM 代理文档。

将 APM 代理添加到你的代码

你有两个选项可以将 Java 代理工具添加到您的应用程序中。

首先,你可以在调用 java 二进制文件时通过参数添加代理。 这样,它不会干扰应用程序的打包。 该机制在启动时检测应用程序。

首先下载代理,可以查看最新版本

%title插图%num

你也可以使用如下的命令来进行下载:

wget https://repo1.maven.org/maven2/co/elastic/apm/elastic-apm-agent/1.34.1/elastic-apm-agent-1.34.1.jar

指定启动时的代理以及将 APM 数据发送到的配置参数。 在启动 Java 应用程序之前,让我们为在 Elastic Cloud 中运行的 APM 服务器获取一个 API 密钥。

当你检查你在 Elastic Cloud 中的部署并单击左侧的 APM 时,您将看到可以使用的 APM Server Secret Token。 你也可以从那里复制 APM 端点 URL。

java -javaagent:/path/to/elastic-apm-agent-1.34.1.jar
  -Delastic.apm.service_name=javalin-app 
  -Delastic.apm.application_packages=de.spinscale.javalin 
  -Delastic.apm.server_urls=$APM_ENDPOINT_URL 
  -Delastic.apm.secret_token=PqWTHGtHZS2i0ZuBol 
  -jar build/libs/javalin-app-all.jar

针对我们的本地部署,我们可以参考之前的文章 “Observability:使用 Elastic Agent 提取应用程序跟踪 – Elastic Stack 8.0” 来安装 Elastic Agent 及 APM 集成。

我们可以通过如下的方式来安装 Elastic Agent 及 Fleet Server:

$ sudo ./elastic-agent install 
>   --fleet-server-es=https://192.168.0.3:9200 
>   --fleet-server-service-token=AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL3Rva2VuLTE2Njk4ODAzNjU3MjA6b29rTGZPRExUSGVwTFpHR0RxNEZfZw 
>   --fleet-server-policy=fleet-server-policy 
>   --fleet-server-es-ca-trusted-fingerprint=07b5d9d9b686daacb996df14a25ef22d2385cfd1b66f1bc5dbfb8f4e1983c64a
Password:
Elastic Agent will be installed at /Library/Elastic/Agent and will run as a service. Do you want to continue? [Y/n]:Y
{"log.level":"info","@timestamp":"2022-12-01T15:41:37.693+0800","log.origin":{"file.name":"cmd/enroll_cmd.go","file.line":403},"message":"Generating self-signed certificate for Fleet Server","ecs.version":"1.6.0"}
{"log.level":"info","@timestamp":"2022-12-01T15:41:40.893+0800","log.origin":{"file.name":"cmd/enroll_cmd.go","file.line":792},"message":"Fleet Server - Starting","ecs.version":"1.6.0"}
{"log.level":"info","@timestamp":"2022-12-01T15:41:44.898+0800","log.origin":{"file.name":"cmd/enroll_cmd.go","file.line":773},"message":"Fleet Server - Running on policy with Fleet Server integration: fleet-server-policy; missing config fleet.agent.id (expected during bootstrap process)","ecs.version":"1.6.0"}
{"log.level":"info","@timestamp":"2022-12-01T15:41:45.187+0800","log.origin":{"file.name":"cmd/enroll_cmd.go","file.line":471},"message":"Starting enrollment to URL: https://liuxgm.local:8220/","ecs.version":"1.6.0"}
{"log.level":"info","@timestamp":"2022-12-01T15:41:46.607+0800","log.origin":{"file.name":"cmd/enroll_cmd.go","file.line":273},"message":"Successfully triggered restart on running Elastic Agent.","ecs.version":"1.6.0"}
Successfully enrolled the Elastic Agent.
Elastic Agent has been successfully installed.

%title插图%num

%title插图%num

%title插图%num

%title插图%num

%title插图%num

%title插图%num

%title插图%num

由于我们的 Fleet Server 及 Agents 是在同一个机器上运行的,我们选择 Existing hosts。点击上面的 Save and continue 按钮:

%title插图%num

%title插图%num

%title插图%num

这样我们为 Fleet Server Policy 添加了一个 apm-1 的集成。

我们接下在 terminal 中运行如下命令:

  java -javaagent:./elastic-apm-agent-1.34.1.jar
  -Delastic.apm.service_name=javalin-app 
  -Delastic.apm.application_packages=de.spinscale.javalin 
  -Delastic.apm.server_urls=http://localhost:8200 
  -Delastic.apm.secret_token=PqWTHGtHZS2i0ZuBol 
  -jar build/libs/javalin-app-all.jar

%title插图%num

你现在可以继续打开 APM UI,你应该会看到流入的数据。

%title插图%num

%title插图%num

%title插图%num

自动依附

如果你不想更改应用程序的启动选项,独立代理允许您附加到主机上运行的 JVM。

这需要你下载独立的 jar。 你可以在官方文档上找到链接。

要列出本地运行的 Java 应用程序,你可以运行:

java -jar /path/to/apm-agent-attach-1.34.1-standalone.jar --list

由于我通常在我的系统上运行多个 Java 应用程序,因此我指定要附加到的应用程序。 此外,请确保你已经停止了已附加代理的 Javalin 应用程序,并在没有配置附加代理的情况下启动常规 Javalin 应用程序。

java -jar /tmp/apm-agent-attach-1.33.1-standalone.jar --pid 30730 
  --config service_name=javalin-app 
  --config application_packages=de.spinscale.javalin 
  --config server_urls=$APM_ENDPOINT_URL 
  --config secret_token=PqWTHGtHZS2i0ZuBol

上面的消息将返回如下内容:

2022-12-01 15:04:48.144  INFO Attaching the Elastic {apm-agent} to 30730
2022-12-01 15:04:49.649  INFO Done

因此,现在代理已附加到具有特殊配置的正在运行的应用程序。更多阅读请参阅 “https://elasticstack.blog.csdn.net/article/details/118481955”。

虽然前两种可能性都有效,但你也可以使用第三种方法:将 APM 代理用作直接依赖项。 这允许你在我们的应用程序中编写自定义 span 和 transaction。

程序化设置

编程设置允许你通过源代码中的一行 java 附加代理。

1)添加 java 代理依赖项。

dependencies {
  ...
  implementation 'co.elastic.apm:apm-agent-attach:1.17.0'
  ...
}

2)在我们的 main() 方法开始时检测应用程序。

我们还没有配置任何端点或 API 令牌。 虽然文档建议使用 src/main/resources/elasticapm.properties 文件,但我更喜欢使用环境变量,因为这可以防止将 API 令牌提交到你的源或合并另一个存储库。 保险库等机制允许您以这种方式管理你的秘密。

3)我们在本地项目的根目录中创建一个叫做 .env 的文件。其内容如下:

ELASTIC_APM_SERVICE_NAME=javalin-app
ELASTIC_APM_SERVER_URLS=https://APM_ENDPOINT_URL
ELASTIC_APM_SECRET_TOKEN=PqWTHGtHZS2i0ZuBol

根据我们的配置,.env 的内容如下:

$ pwd
/Users/liuxg/demos/apm/java_monitor
$ cat .env
ELASTIC_APM_SERVICE_NAME=javalin-app
ELASTIC_APM_SERVER_URLS=http://localhost:8220
ELASTIC_APM_SECRET_TOKEN=PqWTHGtHZS2i0ZuBol

4)你现在可以像以前一样运行 java 应用程序。

java -jar build/libs/javalin-app-all.jar

%title插图%num

如果你想在你的 IDE 中运行它,你可以手动设置环境变量或搜索支持 .env 文件的插件。

稍等几分钟,让我们最后看一下 APM 应用程序。

%title插图%num

%title插图%num

如你所见,这与前面显示的 Spring Boot 应用程序有很大的不同。 未列出不同的端点; 尽管包括错误,但我们可以看到每分钟的请求数。

唯一的 transaction 来自单个 servlet,这不是很有帮助。 让我们尝试通过引入自定义程序化 transaction 来解决这个问题。

定制 transactions

1)添加另一个依赖项。

dependencies {
  ...
  implementation 'co.elastic.apm:apm-agent-attach:1.17.0'
  implementation 'co.elastic.apm:apm-agent-api:1.17.0'
  ...
}

2)修复 transaction 的名称以包括 HTTP 方法和请求路径:

import co.elastic.apm.api.ElasticApm;

...

app.before(ctx -> ElasticApm.currentTransaction()
  .setName(ctx.method() + " " + ctx.path()));

重新启动你的应用程序并查看流入的数据。 测试几个不同的端点,尤其是抛出异常的端点和触发 404 的端点。

curl http://localhost:8000
curl http://localhost:8000/exception
curl http://localhost:8000/404

我们分别运行上面的命令几次。

%title插图%num

%title插图%num

4)添加另一个端点以查看 transaction 的强大功能,该 transaction 轮询另一个 HTTP 服务。 你可能听说过 wttr.in,这是一种从中轮询天气信息的服务。 让我们实现一个将请求转发到该端点的 HTTP 代理方法。 让我们使用 Apache HTTP 客户端,这是最典型的 HTTP 客户端之一。

这是我们的新端点。

import org.apache.http.client.fluent.Request;

...

public static void main(String[] args) {
...

    app.get("/weather/:city", ctx -> {
        String city = ctx.pathParam("city");
        ctx.result(Request.Get("https://wttr.in/" + city + "?format=3").execute()
            .returnContent().asBytes())
            .contentType("text/plain; charset=utf-8");
    });

...

5)我们使用如下的命令来进行测试:

curl http://localhost:8000/weather/Beijing

%title插图%num

$ curl http://localhost:8000/weather/Beijing
Beijing: ☀️   -2C

让我们检查一下 APM UI。

%title插图%num

/weather/Beijing 的事务现在包含一个 span,显示检索天气数据所花费的时间。 因为 HTTP 客户端是自动检测的,所以不需要做任何事情。

如果该 URL 的 city 参数具有高基数,这将导致提及大量 URL 而不是通用端点。 如果你想防止这种情况发生,可以使用 ctx.matchedPath() 将每次对天气 API 的调用记录为 GET /weather/:city。 然而,这需要通过删除 app.before() 处理程序并将其替换为 app.after() 处理程序来进行一些重构。

app.after(ctx -> ElasticApm.currentTransaction().setName(ctx.method()
  + " " + ctx.endpointHandlerPath()));

%title插图%num

我们再次运行上面的请求,并再次回到 APM UI 界面。我们这次看到的是:

%title插图%num

显然这次我们只看到一个 transaction:/weather/:city,而不以每个城市分别命令的 transaction。

设置 Uptime

到目前为止,我们的应用程序中有一些基本的监控功能。 我们索引日志(带跟踪),我们索引指标,我们甚至可以在我们的应用程序中查看以找出单个性能瓶颈,这要归功于 APM。 但是,仍然存在一个弱点。 到目前为止所做的一切都在应用程序中,但所有用户都是通过互联网访问应用程序的。

如何检查我们的用户是否具有我们的 APM 数据向我们建议的相同体验。 想象一下,在你的应用程序前面有一个滞后的负载均衡器,这会使你为每个请求额外花费 50 毫秒。 那将是毁灭性的。 或者 TLS 协商代价高昂。 即使这些外部事件都不是你的错,你仍然会受到影响,应该尝试减轻这些影响。 这意味着您需要先了解它们。

Uptime 不仅可以让你监控服务的可用性,还可以绘制延迟随时间变化的图表,并在 TLS 证书到期时收到通知。

设置

要将正常运行时间数据发送到 Elasticsearch,需要 Heartbeat(轮询组件)。 要下载并安装 Heartbeat,请使用适用于你系统的命令。我们可以到地址Download Heartbeat • Uptime Monitoring & Elasticsearch | Elastic下载适合你 Elasticsearch 的版本。针对我的 macOS,我使用如下的命令:

curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-8.5.2-darwin-aarch64.tar.gz
tar xzvf heartbeat-8.5.2-darwin-aarch64.tar.gz

和之前的 Metricbeat 及 Filebeat 一样,我们可以使用同样的 API key 来进行配置。当我们解压缩 heartbeat 后,我们需要配置 heartbeat.yml 文件。

heartbeat.yml

ame: heartbeat-shipper

output.elasticsearch:
  hosts: ["localhost:9200"]
  protocol: "https"

  # Authentication credentials - either API key or username/password.
  api_key: "xZ5uyIQBVVocAV-RwzpW:Szm88RvESbm88Bmk0SYFGQ"
  ssl.certificate_authorities: ["/Users/liuxg/elastic/elasticsearch-8.5.2/config/certs/http_ca.crt"]

heartbeat.monitors:
  - type: http
    id: javalin-http-app
    name: "Javalin Web Application"
    urls: ["http://localhost:8000"]
    check.response.status: [200]
    schedule: '@every 15s'

  - type: http
    id: httpbin-get
    name: "httpbin GET"
    urls: ["https://httpbin.org/get"]
    check.responstgcodee.status: [200]
    schedule: '@every 15s'

  - type: tcp
    id: javalin-tcp
    name: "TCP Port 8000"
    hosts: ["localhost:8000"]
    schedule: '@every 15s'

processors:
  - add_observer_metadata:
      geo:
        name: asia-beijing
        location: "39.923423, 116.405654"

我们使用如下的命令来测试一下:

./heartbeat test config
$ ./heartbeat test config
Config OK
./heartbeat test output
$ ./heartbeat test output
elasticsearch: https://localhost:9200...
  parse url... OK
  connection...
    parse host... OK
    dns lookup... OK
    addresses: ::1, 127.0.0.1
    dial up... OK
  TLS...
    security: server's certificate chain verification is enabled
    handshake... OK
    TLS version: TLSv1.3
    dial up... OK
  talk to server... OK
  version: 8.5.2

我们使用如下的命令来进行配置:

./heartbeat setup
$ ./heartbeat setup
Overwriting ILM policy is disabled. Set `setup.ilm.overwrite: true` for enabling.

Index setup finished.

我们接下来使用如下的命令来启动 heartbeat:

./heartbeat -e

%title插图%num

要查看 Uptime 应用程序,请选择 Observability → Uptime。 概述看起来像这样。

%title插图%num

%title插图%num

你可以看到监视器列表和全局概览。让我们看看其中一个警报的详细信息。单击 Javalin Web 应用程序。

您可以看到最后一次计划检查的执行情况,但每次检查的持续时间可能更有趣。你可以查看其中一项检查的延迟是否增加。

%title插图%num

通过配置多个在全球范围内运行的 Heartbeats,你可以比较延迟并确定您需要哪个数据中心来运行你的应用程序以靠近你的用户。

监视器的持续时间在几毫秒内,因为它非常快。检查 httpbin.org 端点的监视器,你会看到更长的持续时间。在这种情况下,每个请求大约需要 1000 毫秒。这并不奇怪,因为端点不在附近,并且你需要为每个请求发起 TLS 连接,这是昂贵的。

不要低估这种监控的重要性。此外,考虑这只是一个开始,因为下一步是让合成器监视应用程序的正确行为,例如,以确保你的结帐过程始终有效。

接下我们停止运行 javalin web 应用。稍等一会儿,我们看到:

%title插图%num

从图中,我们可以看到有两个服务的运行状态变为红色,也即它们是挂了。

文章来源于互联网:Observability:从零开始创建 Java 微服务并监控它 (二)