需求控制
保护您的图免受高昂的GraphQL运算的影响
此功能仅在 GraphOS企业版.
计划中可用 .
在预览期间,我们欢迎您提供反馈,特别是以下方面的反馈
需求控制系统的工作流程是否容易遵循和实施。
是否有任何功能缺失,导致您无法在生产中使用需求控制。
关于需求控制
当配置了需求控制后,路由器会计算每个操作的复杂度值,或成本。您可以收集遥测数据和分析指标以确定路由器所服务的操作的成本范围。然后,您可以针对每个操作配置最大成本限制,高于此限制的路由器将拒绝该操作。
需求控制工作流程
按照此工作流程配置和微调您的路由器的需求控制:
- 测量现有操作的成本。
- 改进成本估算模型。
- 调整您的
preview_demand_control
配置并实施成本限制。
测量现有操作的成本
首先,测量您的路由器所服务的操作的成本。
- 在您的
router.yaml
中,将需求控制配置为measure
模式并定义要监控的结果的遥测数据。例如:- 将
preview_demand_control.mode
设置为measure
。 - 定义操作成本的自定义直方图。
- 将
# Demand control enabled in measure mode.preview_demand_control:enabled: true# Use measure mode to monitor the costs of your operations without rejecting any.mode: measurestrategy:# Static estimated strategy has a fixed cost for elements.static_estimated:# The assumed returned list size for operations. Set this to the maximum number of items in a GraphQL listlist_size: 10# The maximum cost of a single operation, above which the operation is rejected.max: 1000# Basic telemetry configuration for cost.telemetry:exporters:metrics:common:views:# Define a custom view because cost is different than the default latency-oriented view of OpenTelemetry- name: cost.*aggregation:histogram:buckets:- 0- 10- 100- 1000- 10000- 100000- 1000000# Example configured for Prometheus. Customize for your APM.prometheus:enabled: true# Basic instrumentationinstrumentation:instruments:supergraph:cost.actual: true # The actual costcost.estimated: # The estimated costattributes:cost.result: true # Of the estimated costs which of these would have been rejectedcost.delta: true # Actual - estimated
💡 提示
分析操作成本时,如果您的直方图不够精细或范围不够广泛,您可以修改遥测配置中的视图:
telemetry:exporters:metrics:common:views:- name: cost.*aggregation:histogram:buckets:- 0 # Define the buckets here- 10- 100- 1000 # More granularity for costs in the 1000s- 2000- 3000- 4000
- 通过您的路由器发送一些请求并观察您的APM中的
cost.*
度量。
您应该能够将APM配置为查找cost.*
直方图,并根据total属性上的cost.estimated
来获取将会被拒绝的请求的比例。这将允许您看到成本直方图。
来自Prometheus端点的操作成本直方图示例:
# TYPE cost_actual histogramcost_actual_bucket{otel_scope_name="apollo/router",le="0"} 0cost_actual_bucket{otel_scope_name="apollo/router",le="10"} 3cost_actual_bucket{otel_scope_name="apollo/router",le="100"} 5cost_actual_bucket{otel_scope_name="apollo/router",le="1000"} 11cost_actual_bucket{otel_scope_name="apollo/router",le="10000"} 19cost_actual_bucket{otel_scope_name="apollo/router",le="100000"} 20cost_actual_bucket{otel_scope_name="apollo/router",le="1000000"} 20cost_actual_bucket{otel_scope_name="apollo/router",le="+Inf"} 20cost_actual_sum{otel_scope_name="apollo/router"} 1097cost_actual_count{otel_scope_name="apollo/router"} 20# TYPE cost_delta histogramcost_delta_bucket{otel_scope_name="apollo/router",le="0"} 0cost_delta_bucket{otel_scope_name="apollo/router",le="10"} 2cost_delta_bucket{otel_scope_name="apollo/router",le="100"} 9cost_delta_bucket{otel_scope_name="apollo/router",le="1000"} 7cost_delta_bucket{otel_scope_name="apollo/router",le="10000"} 19cost_delta_bucket{otel_scope_name="apollo/router",le="100000"} 20cost_delta_bucket{otel_scope_name="apollo/router",le="1000000"} 20cost_delta_bucket{otel_scope_name="apollo/router",le="+Inf"} 20cost_delta_sum{otel_scope_name="apollo/router"} 21934cost_delta_count{otel_scope_name="apollo/router"} 1# TYPE cost_estimated histogramcost_estimated_bucket{cost_result="COST_OK",otel_scope_name="apollo/router",le="0"} 0cost_estimated_bucket{cost_result="COST_OK",otel_scope_name="apollo/router",le="10"} 5cost_estimated_bucket{cost_result="COST_OK",otel_scope_name="apollo/router",le="100"} 5cost_estimated_bucket{cost_result="COST_OK",otel_scope_name="apollo/router",le="1000"} 9cost_estimated_bucket{cost_result="COST_OK",otel_scope_name="apollo/router",le="10000"} 11cost_estimated_bucket{cost_result="COST_OK",otel_scope_name="apollo/router",le="100000"} 20cost_estimated_bucket{cost_result="COST_OK",otel_scope_name="apollo/router",le="1000000"} 20cost_estimated_bucket{cost_result="COST_OK",otel_scope_name="apollo/router",le="+Inf"} 20cost_estimated_sum{cost_result="COST_OK",otel_scope_name="apollo/router"}cost_estimated_count{cost_result="COST_OK",otel_scope_name="apollo/router"} 20
直方图示例图
您还可以绘制当前配置下允许或拒绝的操作的百分比:
虽然估算成本可能与实际成本不完全匹配,但您可以使用这些指标来确认以下情况
- 是否有任何操作的成本被低估
- 将
static_estimated.list_size
设置为实际的最大列表大小 - 设置什么内容
static_estimated.max
作为允许操作的最大成本
在这个例子中,由于当前配置,不到一半的请求会被拒绝。查询的成本也被低估,因为cost.delta
不为零。
- 为了确定哪些操作被拒绝,定义一个遥测自定义工具,用于报告当操作的成本超过配置的成本限制时已被拒绝的情况:
telemetry:instrumentation:instruments:supergraph:# custom instrumentcost.rejected.operations:type: histogramvalue:# Estimated cost is used to populate the histogramcost: estimateddescription: "Estimated cost per rejected operation."unit: deltacondition:eq:# Only show rejected operations.- cost: result- "COST_ESTIMATED_TOO_EXPENSIVE"attributes:graphql.operation.name: true # Graphql operation name is added as an attribute
当您有许多操作名称,例如一个公共的互联网数据接口时,这个自定义工具可能不合适。您可以添加条件来减少返回的操作数量。例如,使用一个条件,只有当成本变化量大于一个阈值时才输出结果:
telemetry:instrumentation:instruments:supergraph:# custom instrumentcost.rejected.operations:type: histogramvalue:# Estimated cost is used to populate the histogramcost: estimateddescription: "Estimated cost per rejected operation."unit: deltacondition:all:- eq: # Only show rejected operations- cost: result- "COST_ESTIMATED_TOO_EXPENSIVE"- gt: # Only show cost delta > 100- cost: delta- 100
- 现在,您应该可以配置您的APM来查看哪些操作成本太高。通过绘图工具,如top-N或热图,可视化直方图可能很有用。
例如,以下表格显示了操作的估算成本:
操作名称 | 估算成本 |
---|---|
ExtractAll | 9020 |
GetAllProducts | 1435 |
GetLatestProducts | 120 |
GetRecentlyUpdated | 99 |
FindProductByName | 87 |
其中ExtractAll
操作的成本非常大,因此它是被拒绝的好候选。
此外,cost.delta
指标值——实际成本与估算成本之间的差异——表明用于成本估算假定的列表大小过大或过小。在此示例中,正的cost.delta
表示实际列表大小大于估算列表大小。因此static_estimated.list_size
可以降低,以更接近实际情况。
改进成本估算模型
您应该迭代改进您的成本估算模型。精确的成本估算是识别和预防可能损害您的子图的关键。
前一步通过 ejemplo operations 发现了实际成本和估算成本之间有可察觉的差异。您可以通过向您的 GraphQL 模式中的字段添加遥测工具来更深入地理解这种差异——从而调整配置的列表大小。
例如,您可以为您的 GraphQL 模式中的每个字段生成一个直方图:
telemetry:exporters:metrics:common:views:- name: graphql.*aggregation:histogram:buckets:- 0- 10- 100- 1000- 10000- 100000- 1000000instrumentation:instruments:graphql:list.length: true
此配置会产生许多度量指标,可能对于您的 APM 是昂贵的。为了减少指标生成量,您可以对工具设置条件。
对于本示例,您可以设置一个条件,将工具限制于具有特定名称的操作。您还可以仅显示 GraphQL 字段的列表大小的直方图:
telemetry:instrumentation:instruments:graphql:graphql.list.length.restricted: # custom instrumentunit: lengthdescription: "histogram of list lengths"type: histogramvalue:list_length: valuecondition:all:- eq:- operation_name: string- "GetAllProducts"
来自 Prometheus 终端的输出可能如下所示
graphql_list_length_restricted_bucket{graphql_field_name="allProducts",graphql_type_name="Query",otel_scope_name="apollo/router",le="0"} 0graphql_list_length_restricted_bucket{graphql_field_name="allProducts",graphql_type_name="Query",otel_scope_name="apollo/router",le="10"} 9graphql_list_length_restricted_bucket{graphql_field_name="allProducts",graphql_type_name="Query",otel_scope_name="apollo/router",le="100"} 20graphql_list_length_restricted_bucket{graphql_field_name="allProducts",graphql_type_name="Query",otel_scope_name="apollo/router",le="1000"} 20graphql_list_length_restricted_bucket{graphql_field_name="allProducts",graphql_type_name="Query",otel_scope_name="apollo/router",le="10000"} 20graphql_list_length_restricted_bucket{graphql_field_name="allProducts",graphql_type_name="Query",otel_scope_name="apollo/router",le="100000"} 20graphql_list_length_restricted_bucket{graphql_field_name="allProducts",graphql_type_name="Query",otel_scope_name="apollo/router",le="1000000"} 20graphql_list_length_restricted_bucket{graphql_field_name="allProducts",graphql_type_name="Query",otel_scope_name="apollo/router",le="+Inf"} 20graphql_list_length_restricted_sum{graphql_field_name="allProducts",graphql_type_name="Query",otel_scope_name="apollo/router"} 218graphql_list_length_restricted_count{graphql_field_name="allProducts",graphql_type_name="Query",otel_scope_name="apollo/router"} 20
您可以为您的 APM 配置图表直方图
图表显示allProducts
字段的实际列表大小最多为 100,因此您可以更新static_estimated.list_size
为 100:
preview_demand_control:enabled: truemode: measurestrategy:static_estimated:list_size: 100 # Updated to measured actual max list sizemax: 1000
重新运行路由器并使用更新的static_estimated.list_size
重新测量成本,应导致生成新的直方图和拒绝操作的百分比。例如:
尽管没有报告更多的成本变化,但估算的成本已增加。您仍然需要调整最大成本。
查看前N条操作,您可能会看到估算的成本已更新。例如:
操作名称 | 估算成本 |
---|---|
ExtractAll | 390200 |
GetAllProducts | 44350 |
GetLatestProducts | 11200 |
GetRecentlyUpdated | 4990 |
FindProductByName | 1870 |
除ExtractAll
之外的所有操作均在可接受的成本范围内。
执行成本限制
确定您的操作的成本估算模型后,应更新并强制执行新的成本限制。
从前一步中,您可以设置最大成本,允许所有操作(除ExtractAll
之外):
preview_demand_control:enabled: truemode: enforce # Change mode from measure to enforcestrategy:static_estimated:list_size: 100max: 50000 # Updated max cost allows all operations except ExtractAll
下一步
继续监控操作的成本,并在估算模型变得不准确时采取行动。例如,如果列表项的最大数量发生变化,请更新估算模型。
您可以在APM中设置警报,以针对可能需要更改您的需求控制设置的事件。需要警报的事件包括
- 需求控制拒绝请求数的意外增加。
- 数据列表的最大列表大小增加。
- 增量指标增加。
💡 提示
使用分页API可以帮助避免列表字段返回任意大量元素的情况。
计算操作成本
当您的路由器接收到请求时,其查询计划器生成并发送一系列子请求到子图中。
为了计算一个操作的总成本,路由器将基于子请求的操作类型和其字段的GraphQL元素类型计算出的总成本相加。
每种操作类型的成本:
突变 | 查询 | 订阅 | |
---|---|---|---|
类型 | 10 | 0 | 0 |
每种GraphQL元素类型在每个操作类型中的成本:
突变 | 查询 | 订阅 | |
---|---|---|---|
对象 | 1 | 1 | 1 |
接口 | 1 | 1 | 1 |
联合 | 1 | 1 | 1 |
标量 | 0 | 0 | 0 |
枚举 | 0 | 0 | 0 |
例如,假设以下查询返回包含六种产品和十个评论的响应:
query ExampleQuery {topProducts {namereviews {author {name}}}}
假设每个审查恰好有一位作者,查询的总成本为26。
估计和实际成本
对于具有列表字段的操作,路由器必须运行该操作以获取其实际列表项数。在没有实际列表大小的情况下,操作的成本只能在执行前估计,此时假设列表的大小。
操作执行后,可以根据实际的列表大小计算出每个操作的估算成本。
ⓘ 注意
估算操作成本与实际操作成本之间的差异仅归因于列表字段假设与实际大小之间的差异。
测量和执行模式
在推出需求控制之前,您首先需要收集针对您图执行的查询信息,以便您可以决定何时拒绝请求。
路由器的需求控制功能支持一种测量模式,允许您在不影响运行的服务的情况下收集此信息。您可以定义遥测仪器来监控您的操作并确定其最大成本阈值。
收集足够的数据后,然后您可以在配置中设置最大成本和列表大小限制,并将需求控制设置为执行模式,此时它将拒绝超出限制的成本操作。
配置需求控制
要启用路由器中的需求控制,请配置preview_demand_control
选项,如下所示:
preview_demand_control:enabled: truemode: measurestrategy:static_estimated:list_size: 10max: 1000
当preview_demand_control
启用时,路由器会测量每个操作的成本,并可根据额外的配置执行操作成本限制。
使用以下设置自定义preview_demand_control
:
选项 | 有效值 | 默认值 | 描述 |
---|---|---|---|
enabled | 布尔值 | false | 将true 设置为测量操作成本或执行操作成本限制。 |
mode | measure , enforce | -- | - measure 收集有关操作成本的信息。- enforce 拒绝超出配置成本限制的操作 |
strategy | static_estimated | -- | static_estimated 在将操作发送到子图之前估计操作的成本 |
static_estimated.list_size | 整数 | -- | 为返回列表的字段假设的最大列表大小。 |
static_estimated.max | 整数 | -- | 接受操作的最大成本。成本高于此值的操作将被拒绝。 |
需求控制的遥测
💡 提示
新到router遥测?请参阅Router Telemetry.
您可以为router定义遥测以收集成本信息并深入了解发送到您的路由器的操作的成本:
- 通过操作名称生成操作成本直方图,其中估算成本大于任意值。
- 将成本信息附加到跨度。
- 每当估计值和实际值之间的成本差大于任意值时,生成日志消息。
仪器
仪器 | 描述 |
---|---|
cost.actual | 测量执行后的操作的实际成本。 |
cost.estimated | 执行操作之前估计的成本。 |
cost.delta | 实际成本与估计成本之间的差异。 |
属性
为 cost
设置的属性可以应用于仪器、跨度以及事件——在 supergraph
属性使用的任何地方。
属性 | 值 | 描述 |
---|---|---|
cost.actual | 布尔值 | 测量执行后的操作的实际成本。 |
cost.estimated | 布尔值 | 执行操作之前估计的成本。 |
cost.delta | 布尔值 | 实际成本与估计成本之间的差异。 |
cost.result | 布尔值 | 成本计算的返回代码。 COST_OK 或一个 错误代码 |
选择器
为 cost
设置的选择器可以应用于仪器、跨度以及事件——在 supergraph
属性使用的任何地方。
键 | 值 | 默认 | 描述 |
---|---|---|---|
cost | estimated ,actual ,delta ,result | 估计的、实际的或增量成本值,或结果字符串 |
示例
示例仪器
启用包含 cost.result
属性的 cost.estimated
仪器:
telemetry:instrumentation:instruments:supergraph:cost.estimated:attributes:cost.result: truegraphql.operation.name: true
示例跨度
在 supergraph
跨度上启用 cost.estimated
属性:
telemetry:instrumentation:spans:supergraph:attributes:cost.estimated: true
示例事件
当 cost.delta
大于 1000 时记录错误:
telemetry:instrumentation:events:supergraph:COST_DELTA_TOO_HIGH:message: "cost delta high"on: event_responselevel: errorcondition:gt:- cost: delta- 1000attributes:graphql.operation.name: truecost.delta: true