跨度(Spans)
在 Elastic Cloud Hosted 中,APM Server 接收来自 Elastic APM Agent 的数据并将其转换为 Elasticsearch 文档。而在 Elastic Cloud Serverless 中,实际上并没有运行 APM Server,而是由托管摄取服务 (managed intake service) 接收并转换数据。
跨度 (Spans)包含有关特定代码路径执行的信息。它们测量从活动开始到结束的过程,并且可以与其他跨度具有父子关系。
代理(Agents)会自动对各种库进行插桩,以从您的应用程序内部捕获这些跨度,但您也可以使用代理 API 对特定的代码路径进行自定义插桩。
除其他外,跨度可以包含
- 指向其父级 事务 的
transaction.id属性。 - 指向其父级跨度或事务的
parent.id属性。 - 其开始时间和持续时间。
- 一个
name、type、subtype和action——有关按 APM 代理划分的跨度名称模式和示例,请参阅 跨度名称/类型对齐 表。此外,一些 APM 代理会针对公共 跨度类型/子类型规范 进行测试。 - 可选的
stack trace。堆栈跟踪由堆栈帧组成,代表调用栈上的函数调用。它们包括函数名、文件名和路径、行号等属性。
大多数代理将关键字字段(如 span.id)限制为 1024 个字符,将非关键字字段(如 span.start.us)限制为 10,000 个字符。
出于性能原因,APM 代理可以选择有目的地对跨度进行采样或省略。这有助于防止出现边缘情况(例如包含 100 多个跨度的长时间运行事务),否则这些情况会使代理和 APM Server 或托管摄取服务过载。发生这种情况时,应用程序 UI 将显示丢弃的跨度数量。
要配置每个事务记录的跨度数量,请参阅相关的代理文档
- Android: 尚不支持
- Go:
ELASTIC_APM_TRANSACTION_MAX_SPANS - iOS: 尚不支持
- Java:
transaction_max_spans - .NET:
TransactionMaxSpans - Node.js:
transactionMaxSpans - PHP:
transaction_max_spans - Python:
transaction_max_spans - Ruby:
transaction_max_spans
代理将跨度与事务分开流式传输到 APM Server 或托管摄取服务。因此,不可预见的错误可能会导致跨度丢失。代理知道一个事务应该有多少个跨度;如果预期跨度的数量不等于 APM Server 或托管摄取服务收到的跨度数量,应用程序 UI 将计算差值并显示一条消息。
跨度与事务一起存储在以下数据流中
- 应用程序跟踪:
traces-apm-<namespace> - RUM 和 iOS 代理应用程序跟踪:
traces-apm.rum-<namespace>
请参阅数据流(Data streams)以了解更多信息。
此示例展示了跨度文档在 Elasticsearch 中索引时的外观。
展开 Elasticsearch 文档
[
{
"@timestamp": "2017-05-30T18:53:27.154Z",
"agent": {
"name": "elastic-node",
"version": "3.14.0"
},
"ecs": {
"version": "1.12.0"
},
"event": {
"outcome": "unknown"
},
"http": {
"request": {
"method": "GET"
},
"response": {
"status_code": 200
}
},
"labels": {
"span_tag": "something"
},
"observer": {
"hostname": "ix.lan",
"type": "apm-server",
"version": "8.0.0"
},
"parent": {
"id": "945254c567a5417e"
},
"processor": {
"event": "span",
"name": "transaction"
},
"service": {
"environment": "staging",
"name": "1234_service-12a3"
},
"span": {
"action": "query",
"db": {
"instance": "customers",
"statement": "SELECT * FROM product_types WHERE user_id=?",
"type": "sql",
"user": {
"name": "readonly_user"
}
},
"duration": {
"us": 3781
},
"http": {
"method": "GET",
"response": {
"status_code": 200
}
},
"http.url.original": "https://:8000",
"id": "0aaaaaaaaaaaaaaa",
"name": "SELECT FROM product_types",
"stacktrace": [
{
"abs_path": "net.js",
"context": {
"post": [
" ins.currentTransaction = prev",
" return result",
"}"
],
"pre": [
" var trans = this.currentTransaction",
""
]
},
"exclude_from_grouping": false,
"filename": "net.js",
"function": "onread",
"library_frame": true,
"line": {
"column": 4,
"context": "line3",
"number": 547
},
"module": "some module",
"vars": {
"key": "value"
}
},
{
"exclude_from_grouping": false,
"filename": "my2file.js",
"line": {
"number": 10
}
}
],
"start": {
"us": 2830
},
"subtype": "postgresql",
"sync": false,
"type": "db"
},
"timestamp": {
"us": 1496170407154000
},
"trace": {
"id": "945254c567a5417eaaaaaaaaaaaaaaaa"
},
"transaction": {
"id": "945254c567a5417e"
},
"url": {
"original": "https://:8000"
}
},
{
"@timestamp": "2017-05-30T18:53:42.281Z",
"agent": {
"name": "js-base",
"version": "1.3"
},
"destination": {
"address": "0:0::0:1",
"ip": "0:0::0:1",
"port": 5432
},
"ecs": {
"version": "1.12.0"
},
"event": {
"outcome": "unknown"
},
"observer": {
"ephemeral_id": "2f13d8fa-83cd-4356-8123-aabfb47a1808",
"hostname": "goat",
"id": "17ad47dd-5671-4c89-979f-ef4533565ba2",
"type": "apm-server",
"version": "8.0.0"
},
"parent": {
"id": "85925e55b43f4342"
},
"processor": {
"event": "span",
"name": "transaction"
},
"service": {
"environment": "staging",
"name": "serviceabc"
},
"span": {
"action": "query.custom",
"db": {
"instance": "customers",
"statement": "SELECT * FROM product_types WHERE user_id=?",
"type": "sql",
"user": {
"name": "readonly_user"
}
},
"destination": {
"service": {
"name": "postgresql",
"resource": "postgresql",
"type": "db"
}
},
"duration": {
"us": 3781
},
"id": "15aaaaaaaaaaaaaa",
"name": "SELECT FROM product_types",
"start": {
"us": 2830
},
"subtype": "postgresql",
"type": "db.postgresql.query"
},
"timestamp": {
"us": 1496170422281000
},
"trace": {
"id": "85925e55b43f4342aaaaaaaaaaaaaaaa"
},
"transaction": {
"id": "85925e55b43f4342"
}
},
{
"@timestamp": "2017-05-30T18:53:27.154Z",
"agent": {
"name": "elastic-node",
"version": "3.14.0"
},
"ecs": {
"version": "1.12.0"
},
"event": {
"outcome": "unknown"
},
"observer": {
"ephemeral_id": "2f13d8fa-83cd-4356-8123-aabfb47a1808",
"hostname": "goat",
"id": "17ad47dd-5671-4c89-979f-ef4533565ba2",
"type": "apm-server",
"version": "8.0.0"
},
"parent": {
"id": "945254c567a5417e"
},
"processor": {
"event": "span",
"name": "transaction"
},
"service": {
"environment": "staging",
"name": "1234_service-12a3"
},
"span": {
"duration": {
"us": 32592
},
"id": "1aaaaaaaaaaaaaaa",
"name": "GET /api/types",
"start": {
"us": 0
},
"subtype": "external",
"type": "request"
},
"timestamp": {
"us": 1496170407154000
},
"trace": {
"id": "945254c567a5417eaaaaaaaaaaaaaaaa"
},
"transaction": {
"id": "945254c567a5417e"
}
},
{
"@timestamp": "2017-05-30T18:53:27.154Z",
"agent": {
"name": "elastic-node",
"version": "3.14.0"
},
"ecs": {
"version": "1.12.0"
},
"event": {
"outcome": "unknown"
},
"observer": {
"ephemeral_id": "2f13d8fa-83cd-4356-8123-aabfb47a1808",
"hostname": "goat",
"id": "17ad47dd-5671-4c89-979f-ef4533565ba2",
"type": "apm-server",
"version": "8.0.0"
},
"parent": {
"id": "945254c567a5417e"
},
"processor": {
"event": "span",
"name": "transaction"
},
"service": {
"environment": "staging",
"name": "1234_service-12a3"
},
"span": {
"action": "post",
"duration": {
"us": 3564
},
"id": "2aaaaaaaaaaaaaaa",
"name": "GET /api/types",
"start": {
"us": 1845
},
"subtype": "http",
"type": "request"
},
"timestamp": {
"us": 1496170407154000
},
"trace": {
"id": "945254c567a5417eaaaaaaaaaaaaaaaa"
},
"transaction": {
"id": "945254c567a5417e"
}
},
{
"@timestamp": "2017-05-30T18:53:27.154Z",
"agent": {
"name": "elastic-node",
"version": "3.14.0"
},
"child": {
"id": [
"4aaaaaaaaaaaaaaa"
]
},
"ecs": {
"version": "1.12.0"
},
"event": {
"outcome": "unknown"
},
"observer": {
"ephemeral_id": "2f13d8fa-83cd-4356-8123-aabfb47a1808",
"hostname": "goat",
"id": "17ad47dd-5671-4c89-979f-ef4533565ba2",
"type": "apm-server",
"version": "8.0.0"
},
"parent": {
"id": "945254c567a5417e"
},
"processor": {
"event": "span",
"name": "transaction"
},
"service": {
"environment": "staging",
"name": "1234_service-12a3"
},
"span": {
"duration": {
"us": 13980
},
"id": "3aaaaaaaaaaaaaaa",
"name": "GET /api/types",
"start": {
"us": 0
},
"type": "request"
},
"timestamp": {
"us": 1496170407154000
},
"trace": {
"id": "945254c567a5417eaaaaaaaaaaaaaaaa"
},
"transaction": {
"id": "945254c567a5417e"
}
}
]
在某些情况下,APM 代理可能会在一个事务中收集大量非常相似或相同的跨度。例如,如果在循环内部捕获跨度,或者在未优化的 SQL 查询中使用多个查询而不是连接来获取相关数据时,就会发生这种情况。
在这种情况下,每个事务的跨度上限(默认情况下为 500 个跨度)可能会很快达到,导致代理停止为给定事务捕获可能更相关的跨度。
捕获相似或相同的跨度通常没有帮助,特别是当它们的持续时间非常短时。它们还会使 UI 显得杂乱,并导致处理和存储开销。
为了解决这个问题,APM 代理可以将相似的跨度压缩为单个跨度。压缩后的跨度保留了大部分原始跨度信息,包括总体持续时间和它所代表的跨度数量。
无论采用哪种压缩策略,如果符合以下条件,则跨度有资格进行压缩
- 它尚未传播其跟踪上下文。
- 它是一个 exit 跨度(例如数据库查询跨度)。
- 其结果不是
"failure"。
APM 代理在两种策略之间进行选择,以决定是否可以压缩相邻的跨度。在这两种策略中,只需在内存中保留一个先前的跨度。这确保了代理不需要大量的内存即可启用跨度压缩。
如果两个相邻的跨度具有相同的以下内容,代理将使用 same-kind 策略
- 跨度类型
- 跨度子类型
destination.service.resource(例如数据库名称)
如果两个相邻的跨度具有相同的以下内容,代理将使用 exact-match 策略
- 跨度名称
- 跨度类型
- 跨度子类型
destination.service.resource(例如数据库名称)
您可以在代理的配置设置中指定最大跨度持续时间。持续时间长于指定值的跨度将不会被压缩。
对于 "Same-Kind" 策略,默认的最大跨度持续时间为 0 毫秒,这意味着默认情况下禁用 "Same-Kind" 策略。对于 "Exact-Match" 策略,默认限制为 50 毫秒。
以下代理支持跨度压缩,并且可以使用下面列出的选项进行配置
| Agent | Same-kind 配置 | Exact-match 配置 |
|---|---|---|
| Go 代理 | ELASTIC_APM_SPAN_COMPRESSION_SAME_KIND_MAX_DURATION |
|
| Java Agent | span_compression_same_kind_max_duration |
span_compression_exact_match_max_duration |
| .NET 代理 | SpanCompressionSameKindMaxDuration |
|
| Node.js Agent | spanCompressionSameKindMaxDuration |
|
| Python 代理 | span_compression_same_kind_max_duration |
OpenTelemetry 跨度映射到 Elastic APM 事务和跨度的方式如下
- 根跨度(如入口点)映射到 APM 事务。
- 子跨度(如内部操作和数据库查询)映射到 APM 跨度。
下表总结了 OpenTelemetry 跨度种类与 Elastic APM 实体之间的映射关系。
| OpenTelemetry 跨度种类 | 映射到 APM | 示例 |
|---|---|---|
SERVER |
事务(Transaction) | 传入的 HTTP 请求 (GET /users/{id}) |
CONSUMER |
事务(Transaction) | 消息队列消费者事件 |
CLIENT |
跨度 | 传出的数据库查询 (SELECT * FROM users) |
PRODUCER |
跨度 | 向队列发送消息 |
INTERNAL |
跨度 | 内部函数执行 |
以下示例显示了 OpenTelemetry 跨度
[
{
"traceId": "abcd1234",
"spanId": "root5678",
"parentId": null,
"name": "GET /users/{id}",
"kind": "SERVER"
},
{
"traceId": "abcd1234",
"spanId": "db1234",
"parentId": "root5678",
"name": "SELECT FROM users",
"kind": "CLIENT"
}
]
先前的 OTel 跨度由 Elastic APM 存储如下
Transaction: GET /users/{id}
├── Span: SELECT FROM users