autoskills 实战指南:Azure 成本查询 API 错误处理与重试策略全解析
autoskills 实战指南:Azure 成本查询 API 错误处理与重试策略全解析
导读:本文以 autoskills 仓库中 azure-cost 技能包的 Cost Query(成本查询)错误处理参考为核心,系统梳理 Azure Cost Management Query API 的 HTTP 状态码、常见错误场景、限流(429)重试策略与统一错误响应结构。读完本文,你将能够快速定位成本查询失败根因,掌握"只对 429 重试、其余先修复再重试"的正确姿势,并借助仓库内配套的 schema、guardrails 与维度矩阵文档把错误消灭在请求发出之前。
一、错误处理在 azure-cost 技能中的定位
在 azure-cost 这个统一 Azure 成本管理技能中,成本查询(Cost Query)是三大工作流之一(另两个是成本优化与成本预测)。完整查询流程定义在 cost-query/workflow.md:确定 Scope → 选择报告类型 → 设置时间范围 → 配置 dataset → 用 az rest 构造并执行请求 → 处理分页与错误。
其中最后一步"处理分页与错误"正是本文主题所在。workflow.md 中明确要求:分页时通过响应的 nextLink 继续拉取;遇到 HTTP 429 限流时,检查响应中所有 x-ms-ratelimit-microsoft.costmanagement-*-retry-after 响应头(qpu、entity、tenant),取最长的等待时间再重试,且在等待期内不得对同一 Scope 发送任何请求。而更详尽的错误清单,则集中收录在本文核心依据的 cost-query/error-handling.md 中。
二、HTTP 状态码速查表
Cost Management Query API 的错误响应首先体现在 HTTP 状态码上。下表是 error-handling.md 提供的完整速查表,覆盖了最常见的六类失败:
| 状态码 | 错误 | 触发原因 | 修复方法 |
|---|---|---|---|
| 400 | BadRequest |
请求体无效、使用了不受支持的维度、日期范围超限、过滤表达式格式错误 | 对照 request-body-schema.md 校验请求体;在 dimensions-by-scope.md 中核对维度兼容性 |
| 401 | Unauthorized |
认证令牌缺失或已过期 | 重新执行 az login 认证,或刷新 bearer token |
| 403 | Forbidden |
目标 Scope 上权限不足,缺少 Cost Management Reader 或等价角色 | 在对应 Scope 上授予 Cost Management Reader 或 Cost Management Contributor 角色 |
| 404 | NotFound |
Scope 不存在、订阅未找到、或资源组不存在 | 核对 Scope URL,确认订阅 ID 与资源组名称正确 |
| 429 | TooManyRequests |
触发限流:QPU、实体(entity)或租户(tenant)级节流 | 检查所有 x-ms-ratelimit-microsoft.costmanagement-*-retry-after 响应头(qpu、entity、tenant),取最长值等待后再重试 |
| 503 | ServiceUnavailable |
Cost Management 服务暂时不可用 | 前往 Azure 服务状态页确认服务健康状况 |
从源码结构看,这一速查表与 workflow.md 末尾的"Error Handling"小节遥相呼应,后者以更简洁的形式重申了同样六类状态的修复要点(校验 schema/日期/维度、
az login、授予 Cost Management Reader 角色、核对 Scope URL、检查限流头、查服务状态)。
三、常见错误场景与根因分析
状态码只能告诉我们"哪一层出了问题",而 error-handling.md 还给出了八类更细粒度的错误场景,直指请求体里的具体字段组合问题:
| 错误信息 / 场景 | 原因 | 修复方法 |
|---|---|---|
| "Agreement type X does not support Y scope" | Scope 类型与账号的协议类型(Agreement Type)不兼容 | 改用兼容的 Scope。EA 账号不能使用 Invoice Section Scope;MOSP 账号不能使用 Department Scope |
| "Dimension Z is not valid for scope" | 请求的维度在当前 Scope 与协议类型的组合下不可用 | 到 dimensions-by-scope.md 核对合法维度 |
| "SubscriptionName filter without SubscriptionId" | 在 EA + Management Group Scope 下,仅按 SubscriptionName 过滤而未同时按 SubscriptionId 过滤 |
在 SubscriptionName 过滤条件旁补充一个 SubscriptionId 过滤条件,详见 guardrails.md |
| 日期范围超出粒度限制 | Daily 粒度超过 31 天,或 Monthly/None 粒度超过 12 个月 |
系统会自动截断 from 日期;为避免静默截断,务必让范围落在限制之内 |
| 日期范围超出绝对上限(37 个月) | from 到 to 跨越超过 37 个月 |
将日期范围压缩到 37 个月以内,必要时拆分成多次查询 |
| "Request body is null or invalid" | 请求体中 JSON 缺失或格式错误 | 校验 JSON 语法,确保 type、timeframe、dataset 字段齐全 |
| 过滤结构无效 | and/or 的子表达式少于 2 个,或 not 的子表达式多于 1 个 |
确保 and/or 至少包含 2 个子表达式,not 恰好 1 个子表达式;单个条件直接使用过滤条件本身,不要套逻辑包装 |
| "The query usage is not supported for the scope" | 查询类型(如 AmortizedCost)在给定 Scope 上不受支持 |
换用其他 Scope 或查询类型,并非所有 Scope 都支持所有报告类型 |
BillingSubscriptionNotFound |
Scope URL 中的订阅 ID 无效,或与计费账号无关联 | 确认订阅 ID 存在且处于活动状态,并确认它归属于预期的计费账号 |
3.1 深度剖析:日期范围类错误
日期范围错误是成本查询最常见的 400 根因之一,其背后有完整的系统化校验规则(见 guardrails.md):
| 粒度 | 最大范围 | 截断行为 |
|---|---|---|
Daily |
31 天 | from 被截断为 to - 1 个月 + 1 天 |
Monthly |
12 个月 | from 被截断为 to - 12 个月 + 1 天 |
None |
12 个月 | from 被截断为 to - 12 个月 + 1 天 |
另外还有两条容易被忽视的规则:
- 绝对上限 37 个月:无论粒度如何,请求只要超过 37 个月就直接返回 HTTP 400(guardrails.md 明确标注这是 API 的绝对限制)。
- 最早起始日期:允许的最早
from日期是 2014 年 5 月 1 日。 - 未来日期静默调整:若
from/to都指向未来,整个时间段会被平移至去年同期的等价时段;若只有to在未来,则to会被调整为今天。这种调整是静默的——返回的数据覆盖的是调整后的时间段,而非你最初请求的日期。
⚠️ 经验之谈:出现"返回数据的时间范围与请求不符"这类疑似"玄学"问题时,先检查是否踩中了未来日期静默调整这条规则。
3.2 深度剖析:过滤结构约束
错误场景表中的"过滤结构无效"对应 guardrails.md 中的 Filter 约束规则:
| 规则 | 详情 |
|---|---|
and 运算符 |
必须包含 2 个或更多子表达式 |
or 运算符 |
必须包含 2 个或更多子表达式 |
not 运算符 |
必须恰好包含 1 个子表达式 |
在 request-body-schema.md 中对此有更完整的定义:and/or 的子表达式少于 2 个会直接校验失败。单个条件不要包进 and/or 包装层,直接以该条件作为 filter 的值即可;需要取反时用 not 且只能包裹 1 个子表达式。
3.3 深度剖析:EA + 管理组特殊场景
"SubscriptionName filter without SubscriptionId" 是 error-handling.md 重点标注的特殊场景。当 EA 协议 + Management Group Scope 组合下,仅用 SubscriptionName 过滤而不带 SubscriptionId 时,API 会返回如下错误信息:
"To view cost data, the subscription ID is needed. Select Subscriptions to find the ID for your subscription, and then ask your question again."
guardrails.md 给出了标准修复模板——SubscriptionId 与 SubscriptionName 必须成对出现:
{
"and": [
{
"dimensions": {
"name": "SubscriptionId",
"operator": "In",
"values": ["<subscription-id>"]
}
},
{
"dimensions": {
"name": "SubscriptionName",
"operator": "In",
"values": ["My Subscription"]
}
}
]
}
四、统一错误响应结构:读懂 error 对象
无论哪个状态码,Cost Management Query API 的错误响应都遵循一致的 JSON 结构(见 error-handling.md):
{
"error": {
"code": "<error-code>",
"message": "<human-readable-error-message>",
"details": [
{
"code": "<detail-code>",
"message": "<detail-message>"
}
]
}
}
各字段含义如下:
| 字段 | 说明 |
|---|---|
error.code |
机器可读的错误码(例如 BadRequest、BillingSubscriptionNotFound) |
error.message |
人类可读的错误描述 |
error.details |
可选数组,包含更具体的错误信息对象(每个对象同样有 code 与 message) |
排查技巧:优先以
error.code做程序化判断(如是否BadRequest),以error.message辅助人工定位字段级问题;details数组往往藏着比顶层message更精确的字段名提示。
五、重试策略:只有 429 值得重试
error-handling.md 用一个醒目的警示框定下基调:除了 429,任何错误都不要重试,其余错误都代表"必须先修复再重新发起请求"的问题。完整策略表如下:
| 状态码 | 是否重试 | 策略 |
|---|---|---|
| 429 | ✅ 是 | 检查响应中所有 x-ms-ratelimit-microsoft.costmanagement-*-retry-after 响应头(qpu-retry-after、entity-retry-after、tenant-retry-after),取最长值,在时长完全流逝前绝不重试,最多重试 3 次 |
| 400 | ❌ 否 | 修复请求。根据错误信息定位具体字段或校验问题 |
| 401 | ❌ 否 | 重新认证。令牌已过期或缺失 |
| 403 | ❌ 否 | 修复权限。在 Scope 上申请合适的 RBAC 角色分配 |
| 404 | ❌ 否 | 修复 Scope URL。确认资源存在 |
| 503 | ❌ 否 | 不重试。前往 Azure 服务状态页确认服务健康 |
| 5xx(其他) | ❌ 否 | 不重试。调查错误并检查服务健康 |
⚠️ 警告:除 429 外不要重试任何错误。其他所有错误都表明需要在再次发起请求前先解决问题。
5.1 429 限流的完整处理姿势
429 的处置之所以特殊,是因为限流背后有多个并行的配额维度。guardrails.md 给出了完整的限流阈值表:
| 限制维度 | 阈值 |
|---|---|
| 每用户(Per User) | 每分钟 20 个请求 |
| 每 Scope(Per Scope) | 每分钟 4 个请求 |
| 每租户(Per Tenant) | 每 10 秒 12 个请求、每分钟 60 个、每小时 600 个 |
| 每客户端类型(Per Client Type) | 每分钟 2,000 个请求 |
其中 per-scope 限制(4 次/分钟)是最具约束力的——对同一订阅、资源组或计费账号的连续查询共享这个配额。这解释了为什么 error-handling.md 要求"检查所有 retry-after 响应头":一次 429 响应可能同时携带多个维度的节流头,只有取最大值才能确保不再触碰任何已超限的配额。
| 响应头 | 含义 |
|---|---|
x-ms-ratelimit-microsoft.costmanagement-qpu-retry-after |
距 QPU 配额可重试的秒数 |
x-ms-ratelimit-microsoft.costmanagement-entity-retry-after |
距实体/Scope 配额可重试的秒数 |
x-ms-ratelimit-microsoft.costmanagement-tenant-retry-after |
距租户配额可重试的秒数 |
⚠️ 警告:不要在 retry-after 时长耗尽前重试。过早的重试会被拒绝,并可能延长节流期。
另外,QPU 配额本身还分 Premium(EA、MCA 企业级,配额更高)与非 Premium(MOSP、试用,配额更低)两个层级;高 QPU 分配同样不改变"最长等待"的处理原则。
5.2 分页:与错误处理并行的数据完整性保障
workflow.md 与 guardrails.md 共同强调了分页参数:页面大小默认 1,000 行、最大 5,000 行。响应中的 nextLink 字段存在时,必须继续请求以拉取后续页面——否则"没有报错"的查询也可能只拿回了部分数据,造成成本分析的误判。
六、把错误消灭在请求之前:预防性检查清单
结合 request-body-schema.md 与 guardrails.md,以下是发送请求前应自检的关键点:
- 顶层字段齐全:
type、timeframe、dataset必填;timeframe为Custom时timePeriod必填(含 ISO 8601 格式的from/to)。 - 粒度与范围匹配:
Daily≤ 31 天,Monthly/None≤ 12 个月,绝对上限 37 个月。 - GroupBy 限制:最多 2 个维度、无重复列、聚合列与分组列不能相同(guardrails.md 的 Dataset Validation 表),聚合函数仅支持
Sum,Date列在Daily/Monthly粒度下不能出现在聚合中。 - 过滤结构合法:
and/or≥ 2 个子表达式,not= 1 个子表达式。 - 维度兼容:维度必须是"协议类型 × Scope 类型"交集内的合法维度(参考 dimensions-by-scope.md 的可用性矩阵);EA 专属维度(如
DepartmentName、EnrollmentAccountName、BillingPeriod)用在 MOSP 订阅上会报校验错误。 - ResourceId 分组范围:
ResourceId分组仅在订阅与资源组 Scope 受支持,计费账号、管理组等更高层级不支持(见 guardrails.md 的 ResourceId Scope Restriction 表)。 - EA + 管理组:按
SubscriptionName过滤必须同时带SubscriptionId。 - GroupBy 与粒度的交互:GroupBy +
Daily粒度时,时间范围可能被调整为请求范围的最后一天;多天完整日粒度分组请确保范围在 31 天内。
其中过滤表达式的合法形态,可以对照 schema 中的标准示例(and 组合 + not 取反 tags):
"filter": {
"and": [
{
"dimensions": {
"name": "ResourceGroupName",
"operator": "In",
"values": ["rg-prod", "rg-staging"]
}
},
{
"not": {
"tags": {
"name": "Environment",
"operator": "Equal",
"values": ["dev"]
}
}
}
]
}
七、实战演练:一次带错误处理的完整成本查询
把上述内容串起来,一个"自带错误处理"的完整查询流程如下(请求体构造细节见 workflow.md 与 examples.md)。
第 1 步:构造请求体文件 temp/cost-query.json
{
"type": "ActualCost",
"timeframe": "MonthToDate",
"dataset": {
"granularity": "None",
"aggregation": {
"totalCost": {
"name": "Cost",
"function": "Sum"
}
},
"grouping": [
{
"type": "Dimension",
"name": "ServiceName"
}
]
}
}
第 2 步:用 az rest 执行查询(注意:SKILL.md 明确建议使用 REST API 而非 az costmanagement query,并始终携带 ClientType 请求头):
# Create temp folder
New-Item -ItemType Directory -Path "temp" -Force
# Query using REST API (more reliable than az costmanagement query)
az rest --method post `
--url "<scope>/providers/Microsoft.CostManagement/query?api-version=2023-11-01" `
--headers "ClientType=GitHubCopilotForAzure" `
--body '@temp/cost-query.json'
第 3 步:按错误分类处置
- 收到 200:检查
nextLink,若非空则继续拉取后续页面,直至nextLink为null。 - 收到 400:优先核对
error.details,结合日期范围(31 天/12 个月/37 个月)、过滤结构(and/or/not子表达式数量)与维度兼容性逐项排查。 - 收到 401/403:分别处理认证(
az login)与角色授权(Cost Management Reader)。 - 收到 404:核对 Scope URL 中的订阅 ID、资源组名称。
- 收到 429:解析全部三个
x-ms-ratelimit-microsoft.costmanagement-*-retry-after响应头,取最大值等待,最多重试 3 次。 - 收到 503/其他 5xx:不重试,转向服务状态页确认服务健康。
八、总结
成本查询的错误处理可以浓缩为三句话:
- 看状态码定层级:400 查请求体、401/403 查认证授权、404 查 Scope、429 查限流、503 查服务。
- 读 error 对象找根因:统一 JSON 结构中的
code是机器可读判据,details藏着字段级线索。 - 只对 429 重试:取最长 retry-after 等待、最多 3 次;其余错误一律先修复再请求。
更关键的是,多数错误本可以在请求发出前避免——把 guardrails.md 的日期与过滤校验、dimensions-by-scope.md 的维度矩阵、request-body-schema.md 的请求体规范内化为查询前的自检习惯,配合 error-handling.md 的这份完整错误参考,就能让 Azure 成本查询既快又稳。