工程

Elastic RUM 浅谈

作者

如果我让您误以为是要品一杯朗姆酒调制的美妙鸡尾酒,却发现我说的是此 RUM 而非彼朗姆时,那就抱歉了!但请相信,Elastic RUM 也绝非徒有其名!让我们一起来“呷一口”吧!不过我要提醒您的是,本篇博客介绍了大量细节,看完这些内容是需要花点时间的!

什么是 RUM?

Elastic 真实用户监测可捕获用户与 Web 浏览器的交互,并能够从性能角度让您详细查看 Web 应用程序的“真实用户体验”。Elastic RUM 代理是 JavaScript 代理,这意味着它支持任何基于 JavaScript 的应用程序。RUM 可对您的应用程序提供有价值的见解。RUM 的一些常见优势包括:

  • RUM 性能数据可帮助您确定瓶颈,并发现站点性能问题如何影响访问者的体验
  • 通过 RUM 捕获的用户代理信息,您可以确定客户最常用的浏览器、设备和平台,以便能够明智地对您的应用程序进行优化
  • 结合位置信息和 RUM 提供的个体用户性能数据,可帮助您了解网站在全球不同区域的性能
  • RUM 可针对应用程序的服务级别协议 (SLA) 提供见解和测量数据

通过 Elastic APM 开始使用 RUM

在本篇博客中,我将系统地逐步介绍如何为一个具有 React 前端和 Spring Boot 后端的简单 Web 应用程序装载测量工具。您会看到使用 RUM 代理有多么简单。另外,您还会看到 Elastic APM 如何通过一个全面的分布式跟踪视图将前端和后端性能信息结合在一起。如果您有兴趣获取更多详细信息,请参阅我先前发表的博客,以大体了解 Elastic APM 和分布式跟踪

要使用 Elastic APM 真实用户监测,您必须先安装 Elastic Stack 和 APM 服务器。当然,您可以下载最新的 Elastic Stack 和 APM 服务器,并在计算机上进行本地安装。但是,最简单的方法是创建一个 Elastic Cloud 试用帐户,在几分钟之内便可准备好您的集群。在默认 I/O 优化模板中,APM 已处于启用状态。从现在开始,我假设您已经准备好了一个集群。

样例应用程序

我们要打算为其装载测量工具的这个应用程序是一个简单的汽车数据库应用程序,由一个 React 前端和一个对内存中汽车数据库提供 API 访问的 Spring Boot 后端组成。这个应用程序特意编写得非常简单。主要是想从零开始为您演示详细的测量工具装载代码的步骤,以便您可以按照相同的步骤为自己的应用程序装载测量工具。

具有一个 React 前端和一个 Spring 后端的简单应用程序

在您笔记本电脑上的任意找一个位置,创建一个名为 CarApp 的目录。然后,将前端和后端应用程序都克隆到这个目录下。

git clone https://github.com/adamquan/carfront.git
git clone https://github.com/adamquan/cardatabase.git

正如您看到的,这个应用程序非常简单。React 前端中只有几个组件,后端 Spring Boot 应用程序中也只有几个类。按照 GitHub 中的说明,为前端和后端构建并运行应用程序。您应该看到类似下面的内容。您可以从中浏览、筛选汽车以及执行 CRUD 选项。

简单的 React 用户界面

现在,应用程序开始运行后,接下来我们就可以使用 RUM 代理来完成测量工具装载了。

RUM 开箱即用的各种测量工具装载代码功能

我们先开始安装和配置 RUM 代理。有两种操作方法:

  1. 对于服务器端应用程序,您可以将 RUM 代理作为一个依赖项来安装,然后再进行初始化。

    npm install @elastic/apm-rum --save
        
  2. 使用 HTML 配置安装 RUM 代理。

    <script src="https://unpkg.com/@elastic/apm-rum@4.0.1/dist/bundles/elastic-apm-rum.umd.min.js">
    </script>
    <script>
      elasticApm.init({
        serviceName: 'carfront',
        serverUrl: 'http://localhost:8200',
        serviceVersion:'0.90'
      })
    </script>
        

由于我们的前端是 React 应用程序,所以我们准备使用第一种方法。在 index.js 所在的目录下,有一个名为 rum.js 的文件,其中包含以下代码:

import { init as initApm } from '@elastic/apm-rum'
var apm = initApm({
 // Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space)
 serviceName: 'carfront',
 // Set the version of your application
 // Used on the APM Server to find the right sourcemap
 serviceVersion:'0.90',
 // Set custom APM Server URL (default: http://localhost:8200)
 serverUrl: 'https://aba7c3d90b0b4820b05b0a9df44c096d.apm.us-central1.gcp.cloud.es.io:443',
 // distributedTracingOrigins: ['http://localhost:8080'],
})
export default apm;

这就是初始化 RUM 代理的全部操作!下面是一些配置的简短解释:

  1. 服务名称: 必须设置服务名称。它在 APM UI 中表示您的应用程序,因此,请用有意义的名称来命名。
  2. 服务版本: 这是您应用程序的版本。APM 服务器也会使用这个版本号来查找正确的源映射。我们稍后将详细讨论源映射。
  3. 服务器 URL: 这是 APM 服务器的 URL。请注意,APM 服务器的 URL 通常可从公共 Internet 进行访问,因为 RUM 代理会从 Internet 上的最终用户浏览器向其报告数据。
  4. 我们稍后再讨论 distributedTracingOrigins

熟悉 Elastic APM 后端代理的用户可能会感到奇怪,为什么不在这里传递 APM 令牌。这是因为 RUM 代理实际上并不使用加密 APM 令牌。令牌仅用于后端代理。由于前端代码是公开的,因此,加密令牌并不能提供额外的安全性。

我们将在加载应用程序时加载这个 JavaScript 文件,并将其放在我们要定制装载测量工具的位置。现在,让我们先看看在未对测量工具装载代码进行任何定制的情况下,有哪些开箱即用的功能。为此,我们只需在 index.js 中包含 rum.js 即可。index.js 文件导入 rum.js 并设置一个页面加载名称。如果不设置页面加载名称,您会在 APM UI 中看到该页面加载列为“Unknown”(未知),这样不是很直观。index.js 类似如下。

import apm from './rum'
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
apm.setInitialPageLoadName("Car List")
ReactDOM.render(<App />, document.getElementById('root'));
serviceWorker.unregister();

生成一些流向您应用程序的流量。登录 Kibana 并单击 APM UI。您应看到列出一个名为“carfront”的服务。单击该服务名称会将您转到事务页面。您应看到默认的“page-load”列表和“car list”页面加载内容。如果看不到,请在时间选取器中确保将时间范围选定为“Last 15 minutes”(过去 15 分钟)。单击“car list”链接,您将看到类似如下图所示的浏览器交互瀑布视图:

Elastic APM 中的样例事务

是否对 RUM 代理默认捕获的信息量感到惊奇?请特别关注一下 timeToFirstBytedomInteractivedomCompletefirstContentfulPaint标记。将鼠标移到黑点上即可看到名称。它们提供了有关内容检索和呈现这些内容的浏览器的详细信息。另外,还要关注一下有关从浏览器加载资源的所有性能数据。只需初始化 RUM 代理,无需对测量工具装载代码进行任何定制,便可获得所有这些开箱即用的详细性能指标!当出现性能问题时,这些指标可帮助您轻松确定问题是因为后端服务慢、网络慢所致,还是仅仅因为客户端浏览器慢所致。这非常吸引人!

针对需要温习知识的用户,下面来简要解释一下 Web 性能指标。请记住,对于像 React 这样的现代 Web 应用程序框架,由于 React 的异步特性,这些指标可能只表示网页的“静态”部分。例如,如您稍后会看到的,在 domInteractive 后可能仍然会加载动态内容。

  • timeToFirstByte 是浏览器在请求后等待从 Web 服务器接收第一条信息的时间。它表示网络和服务器端这二者结合的处理速度。
  • domInteractive 是用户代理将当前文档就绪状态设置为“交互式”(这意味着浏览器已完成对所有 HTML 的解析并且 DOM 构造已完成)前一刻的时间。
  • domComplete 是用户代理将当前文档就绪状态设置为“完成”(这意味着页面及图像等所有子资源已完成下载并准备就绪)前一刻的时间。下载旋转图标已停止旋转。
  • firstContentfulPaint 是浏览器呈现来自 DOM 的内容第一个位的时间。这对用户来说是一个重要的里程碑,因为它提供了页面实际加载的反馈。

灵活的定制测量工具装载代码

正如您刚才所看到的,RUM 代理为您的浏览器交互提供了详细且开箱即用的测量工具装载代码。您也可以根据需要对测量工具装载代码进行定制。例如,因为 React 应用程序是单页面应用程序,删除汽车不会触发“页面加载”,因此,RUM 不会默认捕获删除汽车的性能数据。针对此类情况,我们就可以使用定制事务。

在我们的当前版本(APM 真实用户监测 JavaScript 代理 4.x)中,对于 AJAX 调用和不会触发页面加载的单页应用 (SPA) 调用,用户必须手动创建事务。在像 JSF 这类的框架中,您对 JavaScript 几乎没有控制权。因此,对于启动 AJAX 请求的按钮单击而言,手动创建事务是不可行的。即使开发人员对 AJAX 请求有直接控制权,为大型应用程序装载测量工具也是非常耗费精力的。我们打算增强 RUM 代理,以便在当前没有活动事务的情况下,它会自动为这些请求创建事务。这将使自动测量工具装载覆盖应用程序的更多方面,无需开发人员以编程方式向他们的应用程序添加跟踪逻辑。

通过前端应用程序中的“New Car”(添加新汽车)按钮,您可以向数据库添加新汽车。我们将为代码装载测量工具,以捕获添加新汽车的性能。打开组件目录中的文件 Carlist.js。您将看到以下代码:

// Add new car
addCar(car) {
    // Create a custom transaction
    var transaction = apm.startTransaction("Add Car", "Car");
    apm.addTags(car);
    fetch(SERVER_URL + 'api/cars',
        {
            method:'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body:JSON.stringify(car)
        })
        .then(res => this.fetchCars())
        .catch(err => console.error(err))
}
fetchCars = () => {
    fetch(SERVER_URL + 'api/cars')
        .then((response) => response.json())
        .then((responseData) => {
            this.setState({
                cars: responseData._embedded.cars,
            });
        })
        .catch(err => console.error(err));
        // End the current transaction at the end of the response call back
        var transaction = apm.getCurrentTransaction()
        if (transaction) transaction.end()
}

这段代码创建了一个名为“Add Car”类型为“Car”的新事务。然后,它使用 car 标记了该事务以提供上下文信息。该事务在响应回调结束时结束。

从应用程序的 Web UI 中添加新汽车。在 Kibana 中单击 APM UI。您应看到列出的一个“Add Car”事务。请确保在“Filter by type”(按类型筛选)下拉列表中选择“Car”。默认情况下,它显示“page-load”事务。

在 Elastic APM 中按类型筛选

单击“Add Car”事务链接。您应看到定制事务“Add Car”的性能信息:

在 Elastic APM 中按“car”类型筛选的事务样例

单击“Tags”(标记)选项卡。您将看到我们添加的标记。标记和日志可向您的 APM 跟踪添加有价值的上下文信息。

在 Elastic APM 中按类型标记浏览

这就是执行定制测量工具装载代码所需要的一切 — 简单而强大!如需了解更多详细信息,请参阅 API 文档

Kibana 中强大的定制 APM 可视化

Elastic APM 提供了精心设计的 APM UI 和内置的 APM 仪表板,可以将代理捕获的所有 APM 数据直接进行可视化。另外,您还可以创建自己的定制可视化。例如,RUM 代理捕获的用户 IP 和用户代理数据相当于客户非常详细的信息。有了用户 IP 和用户代理的所有信息,就可以创建如下图所示的可视化视图,在地图上显示 Web 流量的来源,以及客户使用的操作系统和浏览器。您可以使用采集节点管道来充实和转换 APM 数据。通过所有这些信息,您可以更明智地优化应用程序。

在 Kibana 仪表板中可视化 Elastic APM 数据

通过分布式跟踪查看大局

另外,我们还会为后端 Spring Boot 应用程序装载测量工具,以便您在一个视图中就能完整地查看从 Web 浏览器到后端数据库的整个事务。Elastic APM 分布式跟踪可支持您实现这一点。

在 RUM 代理中配置分布式跟踪

默认情况下,RUM 代理中已启用分布式跟踪。但是,它只包含对同域的请求。若要包含跨域请求,您必须设置 distributedTracingOrigins 配置选项。此外,您还必须在后端应用程序中设置 CORS 策略,这点将在下一节中讨论。

对于我们的应用程序,从 http://localhost:3000 为前端提供服务。若要包含对 http://localhost:8080 的请求,我们需要向 React 应用程序添加 distributedTracingOrigins 配置。这是在 rum.js 内完成的。代码已经在这个文件中了,只需取消注释该行即可。

var apm = initApm({
  ...
  distributedTracingOrigins: ['http://localhost:8080']
})

这实际上是告诉代理向针对 http://localhost:8080 的请求添加分布式跟踪 HTTP 标头 (elastic-apm-traceparent)。

要在服务器端使用开箱即用的默认测量工具装载代码,您需要下载 Java 代理,并用它启动您的应用程序。 下面是我配置 Eclipse 项目来运行后端 Spring Boot 应用程序的操作。您必须使用自己的 APM URL 和 APM 令牌来进行配置。

-javaagent:/Users/aquan/Downloads/elastic-apm-agent-1.4.0.jar 
-Delastic.apm.service_name=cardatabase 
-Delastic.apm.application_packages=com.packt.cardatabase
-Delastic.apm.server_urls=https://aba7c3d90b0b4820b05b0a9df44c096d.apm.us-central1.gcp.cloud.es.io:443 
-Delastic.apm.secret_token=jeUWQhFtU9e5Jv836F

配置 Eclipse 以向 Elastic APM 发送后端数据

现在,从浏览器刷新您的汽车列表以生成另一个请求。转到 Kibana APM UI,查看最后一个“car list”页面加载。您应会看到类似以下屏幕截图所示的内容。如您所看到的,来自浏览器的客户端性能数据和服务器端性能数据(包括 JDBC 访问)都很好地显示在了一个分布式跟踪中!注意,分布式跟踪的不同部分有不同的颜色。请记住,这是您获得的默认跟踪,无需在服务器端对测量工具装载代码执行任何定制,只需使用代理启动应用程序即可。感受一下 Elastic APM 和分布式跟踪的强大功能!

Elastic APM 中的分布式跟踪(后端和前端)

对于真正关注上述时间线可视化的读者,您可能会想知道为什么“Car List”页面加载事务在 193 毫秒(即 domInteractive 时间)结束之后,仍会从后端提供数据。很好的问题!这是因为提取调用在默认情况下是异步的。浏览器“认为”它完成了对所有 HTML 的解析,DOM 构造在 193 毫秒完成,因为它加载了从 Web 服务器提供的所有“静态”HTML 内容。另一方面,React 仍然异步从后端服务器加载数据。

跨域资源共享 (CORS)

RUM 代理只是分布式跟踪中的一小部分。为了使用分布式跟踪,我们还需要正确配置其他组件。通常情况下,您必须配置的一个组件是跨域资源共享(即“臭名昭著”的 CORS)!这是因为前端和后端服务通常是分开部署的。对于同域策略,如果未正确配置 CORS,则从另一个域到后端的前端请求将会失败。从根本上说,CORS 是服务器端检查是否允许来自不同域请求的一种方法。若要详细了解跨域请求和为什么此过程是必需的,请参阅有关跨域资源共享的 MDN 页面。

它对我们有什么意义?它包含两层意义:

  1. 就像我们所做的那样,我们必须设置 distributedTracingOrigins 配置选项。
  2. 通过这项配置,RUM 代理还会在真正的 HTTP 请求之前发送一个 HTTP OPTIONS 请求,以确保支持所有的标头和 HTTP 方法,并且允许该域的请求。具体而言,http://localhost:8080 将接收具有以下标头的 OPTIONS 请求:

    Access-Control-Request-Headers: elastic-apm-traceparent
    Access-Control-Request-Method: [request-method]
    Origin: [request-origin]
        
    APM 服务器应使用下面这些标头和 200 状态代码来做出响应:

    Access-Control-Allow-Headers: elastic-apm-traceparent
    Access-Control-Allow-Methods: [allowed-methods]
    Access-Control-Allow-Origin: [request-origin]
        

Spring Boot 应用程序中的 MyCorsConfiguration 类正是这样做的。有多种方式可配置 Spring Boot 来完成此操作,但我们在这里将使用基于筛选器的方法。它会配置我们的服务器端 Spring Boot 应用程序,以允许来自任何域的具有任何 HTTP 标头和任何 HTTP 方法的请求。对于您的生产应用程序,您不需要启用此配置。

@Configuration
public class MyCorsConfiguration {
    @Bean
    public FilterRegistrationBean<CorsFilter> corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<CorsFilter>(new CorsFilter(source));
        bean.setOrder(0);
        return bean;
    }
}

在本篇博客的最后部分,我们来介绍 RUM 代理的一个更强大的功能:源映射。源映射通过准确地告诉您原始源代码(而不是隐晦的简化代码)中所发生错误的位置,可让您更加轻松地对应用程序错误进行故障排查。

使用源映射轻松进行故障排查

出于性能原因,针对生产部署对 JavaScript 包进行简化是一种常见的做法。但是,对简化的代码进行故障排查必定会变得异常困难。下面的屏幕截图显示了 RUM 代理捕获的某个生产版本的一条错误消息。如您所看到的,异常堆栈跟踪没有多大意义,因为它是简化的代码。所有错误行都显示类似 2.5e9f7401.chunk.js 这样的 javascript 文件,并且由于简化方式的原因,它总是指向“line 1”。在这种情况下,如果能完整看到开发时的源代码,岂不是更好?

可以用源映射解析 Elastic APM 中的简化代码

源映射在此处就可大显身手了。您可以为捆绑包生成源映射并将其上传到 APM 服务器。然后,APM 服务器能够将错误从简化代码转换到原始源代码,使您更容易理解错误消息。

让我们看看如何针对前端 React 应用程序执行此操作。我们将为我们的应用程序生成一个生产版本,并上传源映射。您可以使用以下命令创建一个生产版本:

npm run build

您应在该生产版本末尾看到以下消息:

The build folder is ready to be deployed.
You may serve it with a static server:
  serve -s build

有关生产版本的更多详细信息,请参阅:https://facebook.github.io/create-react-app/docs/production-build

确保安装 Serve(如果尚未安装的话):

npm install -g serve

使用以下命令可将您的 React 应用程序设置为生产模式下的服务器:

serve -s build

当 React 应用程序处于生产模式时,React Developer Tools for Chrome 图标将显示黑色背景。当 React 应用程序处于开发模式时,该图标显示红色背景。确保您正在运行的是生产版本。

现在,如果您单击错误按钮来生成一个错误,并从 Kibana APM UI 中检查它时,将会看到如上一个屏幕截图中所示的简化错误堆栈。

接下来,让我们加载源映射,看看它的魔力!生成的源映射位于 $APP-PATH/carfront/build/static/js 目录下。进入该目录,可以看到三个 JavaScript 文件的三个源映射文件。运行以下命令将它们上传到 APM 服务器。请确保更改 URL、文件名和其他参数,以匹配您的应用程序版本、生产版本和环境。另外,还要确保使用 APM 服务器提供的授权令牌。

curl https://aba7c3d90b0b4820b05b0a9df44c096d.apm.us-central1.gcp.cloud.es.io:443/v1/rum/sourcemaps -X POST \
  -F sourcemap="@./main.b81677b7.chunk.js.map" \
  -F service_version="0.90" \
  -F bundle_filepath="http://localhost:5000/static/js/main.b81677b7.chunk.js" \
  -F service_name="carfront" \
  -H "Authorization:Bearer jeUWQhFtU9e5Jv836F"
curl https://aba7c3d90b0b4820b05b0a9df44c096d.apm.us-central1.gcp.cloud.es.io:443/v1/rum/sourcemaps -X POST \
  -F sourcemap="@./runtime~main.fdfcfda2.js.map" \
  -F service_version="0.90" \
  -F bundle_filepath="http://localhost:5000/static/js/runtime~main.fdfcfda2.js" \
  -F service_name="carfront" \
  -H "Authorization:Bearer jeUWQhFtU9e5Jv836F"
curl https://aba7c3d90b0b4820b05b0a9df44c096d.apm.us-central1.gcp.cloud.es.io:443/v1/rum/sourcemaps -X POST \
  -F sourcemap="@./2.5e9f7401.chunk.js.map" \
  -F service_version="0.90" \
  -F bundle_filepath="http://localhost:5000/static/js/2.5e9f7401.chunk.js" \
  -F service_name="carfront" \
  -H "Authorization:Bearer jeUWQhFtU9e5Jv836F"

请特别注意,服务版本是一个字符串,与您在 React 应用程序中配置的服务版本完全相同。在本例中,我们使用 service_version="0.90" 上传,服务版本在应用程序中设置为 "0.90"。如果使用 service_version="0.9"(缺少尾部的 0)加载源映射,它将无法工作!

一旦服务映射加载到 APM 服务器,就可以从 Dev Tools 中找到所有带有此请求的源映射(您的版本可能不同):

GET apm-6.6.1-sourcemap/_search

生成另一个错误,并从 APM UI 错误选项卡再次检查堆栈跟踪。您将看到类似以下屏幕截图所示的堆栈跟踪,完美地反映了您的原始源代码!现在故障排查和确定问题就是如此简单!

实施源映射后,在 Elastic APM 中可以看到实际的代码

总结

希望通过这篇博客清晰地阐释了,使用 Elastic RUM 为应用程序装载测量工具不仅简单易行,而且功能极其强大。结合后端服务的其他 APM 代理,通过分布式跟踪,RUM 可让您从最终用户角度整体查看应用程序性能。

再强调一下,若要开始使用 Elastic APM,您可以下载 Elastic APM 服务器,在本地运行,也可以创建一个 Elastic Cloud 试用帐户,在几分钟内便可准备好集群。

一如既往地,如果您想展开讨论或有任何问题,请在 Elastic APM 论坛上留言。快乐执行 RUM!

ElasticON Global 2021

Join us at ElasticON Global for free!

Our biggest event of the year is back Oct 5-7. Take your organization's search, observability, or security capabilities to a whole new level.