autoskills 实战指南:Azure 成本查询 API 错误处理与重试策略全解析

原创2026-10-08 19:04:251,850 阅读

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,以下是发送请求前应自检的关键点:

  1. 顶层字段齐全:type、timeframe、dataset 必填;timeframe 为 Custom 时 timePeriod 必填(含 ISO 8601 格式的 from/to)。
  2. 粒度与范围匹配:Daily ≤ 31 天,Monthly/None ≤ 12 个月,绝对上限 37 个月。
  3. GroupBy 限制:最多 2 个维度、无重复列、聚合列与分组列不能相同(guardrails.md 的 Dataset Validation 表),聚合函数仅支持 Sum,Date 列在 Daily/Monthly 粒度下不能出现在聚合中。
  4. 过滤结构合法:and/or ≥ 2 个子表达式,not = 1 个子表达式。
  5. 维度兼容:维度必须是"协议类型 × Scope 类型"交集内的合法维度(参考 dimensions-by-scope.md 的可用性矩阵);EA 专属维度(如 DepartmentName、EnrollmentAccountName、BillingPeriod)用在 MOSP 订阅上会报校验错误。
  6. ResourceId 分组范围:ResourceId 分组仅在订阅与资源组 Scope 受支持,计费账号、管理组等更高层级不支持(见 guardrails.md 的 ResourceId Scope Restriction 表)。
  7. EA + 管理组:按 SubscriptionName 过滤必须同时带 SubscriptionId。
  8. 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:不重试,转向服务状态页确认服务健康。

八、总结

成本查询的错误处理可以浓缩为三句话:

  1. 看状态码定层级:400 查请求体、401/403 查认证授权、404 查 Scope、429 查限流、503 查服务。
  2. 读 error 对象找根因:统一 JSON 结构中的 code 是机器可读判据,details 藏着字段级线索。
  3. 只对 429 重试:取最长 retry-after 等待、最多 3 次;其余错误一律先修复再请求。

更关键的是,多数错误本可以在请求发出前避免——把 guardrails.md 的日期与过滤校验、dimensions-by-scope.md 的维度矩阵、request-body-schema.md 的请求体规范内化为查询前的自检习惯,配合 error-handling.md 的这份完整错误参考,就能让 Azure 成本查询既快又稳。

登录后查看全文
autoskills