前阵子接了个任务,要把 Azure OpenAI 接入生产环境。一开始想得挺简单,选个区域、部署模型、拿到 key 就开干。结果真正一跑才发现,单区域的玩法根本扛不住生产流量:配额用着用着就见底、偶发性 429 打到客户端、某个区域的模型版本还不一定和你要的一致。最后我把 Azure API Management 拉进来当统一网关,把多个区域的 Azure OpenAI 资源全部塞到后面,用一套多区域负载均衡方案把所有请求管了起来,顺带解决了密钥管理、限流、日志审计这一堆杂事。这篇文章就是把我的选型逻辑、架构设计、策略写法和踩坑记录完整摆出来,给正在用 Azure OpenAI 做生产项目、被区域故障和配额折腾到头大的工程师做个参考。
1. 为什么需要多区域负载均衡,以及为什么网关选型这么关键
1.1 单区域部署的四个真实痛点
先说我在单区域部署时遇到的状况。
第一个痛点是配额瓶颈。Azure OpenAI 不是一个“按请求无限量”的服务,每个部署都有明确的 TPM(Tokens Per Minute)限制。你以为买了企业版就能随便调,实际上一分钟能处理的 token 总数是写死的。业务一上来,查资料、写摘要、做分类,各种请求一起打过去,TPM 很快就爆,爆了就返回 429。解决办法要么是不断提工单申请提高配额,要么就是拆到多个区域,把流量分散开。
第二个痛点是区域故障并非不可能。云服务再怎么保证高可用,也保不齐某个区域出现异常、某个模型实例临时不可用。单区域部署时,只要那一个入口出问题,整个系统就跟着哑火。这放在开发 demo 里无所谓,放在生产环境就是事故。
第三个痛点是谁都想沾点“网络红利”。两家客户,一个在北美,一个在欧洲,你只部署在美国东部,欧洲那边每次调用都要跨个大洋,延迟高不说,还可能涉及数据合规问题。多区域部署能有效降低延迟、满足数据驻留要求。
第四个痛点看起来不是技术问题,却最容易被忽略:密钥管理混乱。多区域就要多个 Azure OpenAI 资源,每个资源都有自己的 key,如果这些 key 散落在各业务代码里,轮换一次密钥就要发一次版本,想想都头大。
1.2 方案对比:APIM、Traffic Manager、Azure Load Balancer 与 F5/Nginx 的区别
刚开始我脑子里也闪过几个方案,包括 Azure Traffic Manager、Azure Load Balancer、Nginx,以及传统硬件负载均衡 F5 那一类设备。但仔细比完之后,结论非常明确:这个场景最适合的就是 APIM。
Traffic Manager 本质是 DNS 层面的流量路由,它能把域名解析到不同区域的 VIP,做“区域级”的故障转移,确实很适合 Azure OpenAI 多区域入口调度。但它只能做到“把用户带到正确的门口”,一旦请求进了门,它管不了请求头改写、密钥注入、限流、缓存这些应用层操作。
Azure Load Balancer 是四层负载均衡,能干 TCP/UDP 的活儿,转发效率高,但它不解析 HTTP 头,更不认识 api-key。OpenAI 的请求是标准的 HTTP 应用请求,四层 LB 能做的非常有限。
传统 F5 这类硬件负载均衡器,如果你企业内部本来就有,也能拿来做应用层转发,但把它放到 Azure 云原生场景里就很别扭:你得管硬件、管证书、管高可用、管更新,运维成本不低,而且和 Azure 的托管服务之间还要自己打通网络。在纯 PaaS 服务面前,这个方案性价比不高。
Nginx 也能做反向代理和负载均衡,但你自己要维护服务器、做健康检查、做监控、做密钥存储,又回到老路上——所有基础设施都要自己宠着。
APIM 的价值在于它是专门为 API 网关设计的 PaaS 服务,能运行策略、动态改写请求、管理密钥、限流、缓存、打日志,而且天然支持多区域部署。对 Azure OpenAI 这种“按 key 鉴权 + 多终结点 + 需要业务策略”的服务来说,APIM 几乎就是为它量身定做的接口层。
1.3 多区域负载均衡的几种常见策略
多区域负载均衡不是只有“一人一半”这一种玩法,配的时候要先想清楚目标,我拆成三种常见场景。
第一种是“就近接入”。用户从哪个区域进来,流量就尽量交给离他最近的 OpenAI 区域处理。这种场景适合全球业务,核心是降低延迟。
第二种是“按量分摊”。多个区域同时在线,把请求轮询或按权重分发到不同区域,目的是绕开 TPM 配额瓶颈,提升整体吞吐。
第三种是“主备故障转移”。平时所有流量都打到一个区域,只有这个区域挂了才切到另一个区域。这种架构最简单,也最稳,适合预算有限但要求高可用的项目。
在 APIM 策略里,我强烈不建议用“网关内存计数器”去实现轮询。因为 APIM 一旦启用多区域部署,后面是多网关实例,每个实例的内存状态是独立的,你在 A 区域网关里数了 1、2、3,B 区域网关根本不知道。轮询的计数一旦分散到多实例,就失去全局意义了。更靠谱的做法是按请求特征哈希,比如根据用户 ID、会话 ID 或者某个业务维度去分配区域,这样同一个用户总是被路由到同一个区域,不仅均衡,还方便追踪问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体架构设计与容量规划
2.1 目标架构长什么样
我最终落地的架构可以分三层来看。
最外层是流量入口。我在 APIM 前面挂了 Azure Traffic Manager,做“区域级”的调度和故障转移。Traffic Manager 按地理路由,把来自不同地域的请求引导到对应区域的 APIM 网关。
中间层是 APIM 网关层。APIM 是 Premium 层级,启用了多区域部署,可以在多个 Azure 区域各放一个网关实例。这个网关层做的是应用层动作:校验订阅密钥、改写请求头、注入 OpenAI 的 API key、按用户特征选择具体的后端区域、执行限流、输出日志。
最底层是 Azure OpenAI 多区域资源。我在两个区域分别创建了 OpenAI 资源,部署了相同的模型副本,每个资源有自己的终结点和 key。APIM 通过策略把请求转发到其中一个区域。
层与层之间的关系用一句话概括:Traffic Manager 决定“去哪扇门”,APIM 决定“进哪间房”,OpenAI 负责干活。
2.2 先算一笔账:TPM 配额与 QPS 之间的换算
很多人上来就配限流,数字拍脑袋填,结果要么浪费配额,要么还是被 429 打爆。这里必须会算账。
Azure OpenAI 的配额是按 TPM 计的,但它对外提供的接口是按请求计数的。一个请求消耗的 token 数 = 输入 token 数 + 输出 token 数。平均每个用户请求消耗多少 token,取决于你的场景。
举例来说,假设我给 eastus 区域部署了一个 gpt-4o,配额是 240K TPM,也就是每分钟 240,000 token。如果业务平均每个请求消耗 800 token(含输入输出),理论上一分钟能处理的请求数是:
240,000 ÷ 800 = 300 请求/分钟
也就是约 5 QPS。这还不是最终答案,因为大部分模型还有并发数限制,实际达不到理论值,而且要给突发流量留缓冲。我会按理论值的 80% 做 APIM 限流上限,也就是每分钟 240 个请求,即约 4 QPS。
如果你有两个区域,同样的部署规格,整体吞吐就是约 8-10 QPS。这个计算方式虽然粗糙,但好在够直观。上线之后再看监控数据微调。
2.3 区域选择和模型部署的注意点
区域选择不能只看离用户近不近,还要看模型可用性。Azure OpenAI 不同模型的部署区域是有限的,有的模型在 eastus 有,在 westeurope 可能就没有。选区域前先在 Azure 门户里查一下模型可用矩阵。
另外,同一个模型的版本也要对齐。跨区域部署时,尽量让两个区域跑同一个模型版本,否则流量切过去后 prompt 效果不一致,用户会一头雾水。
还有一点容易忽略:模型部署名要保持一致。我在 eastus 部署了名为 gpt-4o-prod 的模型,在 westeurope 也用了完全相同的部署名,这样 APIM 策略里的 URL 路径才能通用。如果两个区域的部署名不一致,你就得在策略里维护多套路径映射,麻烦得多。
如果你有多区域数据驻留的合规要求,那 APIM 的区域副本也要尽量和 OpenAI 资源放在同一个合规边界内。比如客户数据只能在某个地理区域内流转,那就要把 APIM 网关、Traffic Manager 路由、OpenAI 资源全部限制在这个地理区域内。
3. 实操:从零搭建 APIM 多区域 OpenAI 网关
3.1 准备 Azure OpenAI 资源与模型部署
这一步没什么花活,但有几个小细节。
我先创建两个 Azure OpenAI 资源,一个放在 eastus,一个放在 westeurope。每个资源创建好后,记录终结点信息,类似:
code复制https://openai-eastus-001.openai.azure.com/
https://openai-westeurope-001.openai.azure.com/
然后分别进入 OpenAI Studio,各部署一个模型。部署名我用的是 gpt-4o-prod,模型版本保持完全一致。
密钥也要准备好。每个资源有两把 key,通常叫 KEY 1 和 KEY 2,这两把 key 后续要放进 APIM 的命名值(Named Values)里统一管理。我要特别提醒一句:不要把 key 直接写在策略代码里。策略代码会进入 Git 仓库,还可能被团队成员看到,密钥一旦暴露就要重新轮换,非常麻烦。正确做法是存在 APIM 的 Named Values 中,策略里引用变量名。
我在 APIM 里建了四个命名值:
openai-key-eastus,存 eastus 资源的 KEY 1openai-key-westeurope,存 westeurope 资源的 KEY 1openai-endpoint-eastus,存 eastus 的终结点地址openai-endpoint-westeurope,存 westeurope 的终结点地址
这样后续策略里只需要引用变量名,密钥值本身不会出现在代码仓库里。
3.2 创建 APIM 实例并部署多区域网关
APIM 不是所有层级都支持多区域。只有 Premium 层级才有这个能力,价格不便宜,但如果你真的要做生产级多区域负载均衡,这个是硬门槛。我做的第一步是在 Azure 门户创建一个 APIM Premium 实例,区域选择 eastus 作为主区域。
创建完成之后,进入 APIM 实例的“部署 + 基础结构”->“区域”,点击添加区域,把 westeurope 加进去。系统会在 westeurope 自动创建一个网关副本。这一步做完,你就拥有两个区域的 APIM 网关入口了。
这里有个容易搞混的点:APIM 的多区域部署,管理平面仍然在主区域,附加区域只有数据平面(网关)。也就是说,即使 westeurope 的网关副本在正常工作,你通过门户做配置修改时,走的还是主区域的管理平面。如果主区域故障,网关还能继续转发流量,但管理操作可能暂时不可用。这个特性在生产设计时要考虑进去。
3.3 配置 API 和后端命名值
在 APIM 里,每个你对外暴露的接口都叫一个 API。我建了一个 API,URL 后缀设置成了 openai/*,让它匹配所有 /openai/... 路径下的请求。这样前端调用时,URL 是 APIM 的网关地址加上 /openai/deployments/gpt-4o-prod/chat/completions,策略再把请求转到真实的 Azure OpenAI 终结点。
后端(Backend)我提前创建了两个,分别指向 eastus 和 westeurope 的 OpenAI 终结点。创建后端时,只在基础信息里填上终结点地址,不需要填客户端密钥。密钥是在入站策略里动态注入的,这样同一份请求通过不同后端转发时,可以携带不同的 api-key。
如果你想更灵活,也可以不在后端实体里写死地址,而是在策略里直接动态 set-backend-service 指定 base URL。我在下文策略示例里用的就是这种方法,方便你直接上手。
3.4 核心策略:负载均衡 + 密钥注入 + 限流 + 日志
策略(Policy)是 APIM 的灵魂,也是这次配置的核心。我的思路是这样的:在入站阶段,根据请求头中的用户标识计算一个哈希值,用这个哈希值决定请求打到哪个区域。然后把对应的命名值取出来,注入到请求头里,同时做限流。
下面是我在入站策略里用的核心片段。
xml复制<inbound>
<base />
<!-- 从自定义请求头取出用户标识,作为一致性哈希的 key -->
<set-variable name="userId" value="@(context.Request.Headers.GetValueOrDefault("X-User-Id", "anonymous"))" />
<!-- 计算区域索引:字符串哈希后按 2 取模 -->
<set-variable name="regionIndex" value="@{
var userId = (string)context.Variables["userId"];
int code = 0;
foreach (char c in userId) { code += c; }
return code % 2;
}" />
<!-- 根据区域索引动态选择终结点和密钥 -->
<set-variable name="backendBaseUrl" value="@((string)context.Variables["regionIndex"] == "0" ? "https://openai-eastus-001.openai.azure.com" : "https://openai-westeurope-001.openai.azure.com")" />
<set-variable name="backendKey" value="@((string)context.Variables["regionIndex"] == "0" ? "{{openai-key-eastus}}" : "{{openai-key-westeurope}}")" />
<!-- 切换后端 -->
<set-backend-service base-url="@((string)context.Variables["backendBaseUrl"])" />
<!-- 注入 OpenAI 的鉴权头 -->
<set-header name="api-key" exists-action="override">
<value>@((string)context.Variables["backendKey"])</value>
</set-header>
<!-- 限流:每分钟最多 30 个请求,按订阅维度 -->
<rate-limit calls="30" renewal-period="60" />
</inbound>
这个策略有几个点需要解释。
第一,为什么用“字符串哈希后取模”而不是“轮流分配”?因为 APIM 多实例下轮询不可靠,而哈希路由能保证同一个用户总是去同一个区域,对于后续排查问题、追踪日志都友好。当然纯哈希在某些极端流量下会倾斜,你可以把区域数扩大,或者用更均匀的哈希算法,但作为起步方案已经够用。
第二,backendKey 里我直接引用了 {{openai-key-eastus}} 这类命名值。在策略 XML 里,命名值是用双花括号引用的,APIM 会在运行时把它替换成实际的密钥值。这段 XML 本身不暴露密钥。
第三,rate-limit 的 30 次每分钟是我根据前面的 TPM 计算逻辑临时填的。你上线前一定要按自己的配额和请求体量重新算一遍。
除了入站策略,我还在出站阶段加了日志埋点,把每次请求实际路由到哪个区域、返回状态码是什么记录下来:
xml复制<outbound>
<base />
<trace source="openai-gateway" severity="information">
<message>backend region: @((string)context.Variables["regionIndex"] == "0" ? "eastus" : "westeurope"), status: @(context.Response.StatusCode)</message>
</trace>
</outbound>
这里用的是 APIM 的 <trace> 策略,配合 Application Insights 可以看到每一次调用的情况。
3.5 故障转移怎么落地:Traffic Manager 与策略重试的配合
上面的策略解决了负载均衡,但“某个 OpenAI 区域真的挂了”这种情况怎么处理?我在实际项目里用了两层机制。
第一层是区域级的故障转移,交给 Traffic Manager。我在 APIM 实例前面配置了 Traffic Manager,协议选择 HTTPS,探测端口 443,探测路径就用 APIM 网关的自定义域名根路径。每个 APIM 网关区域作为一个端点。正常情况下,Traffic Manager 把欧洲的流量指向 westeurope 网关,北美的流量指向 eastus 网关。一旦某个 APIM 区域探测失败,Traffic Manager 会把该区域的流量自动切到另一个区域的网关入口。
第二层是请求级的故障转移,通过 APIM 策略的 retry 机制做重试切换。如果你的某个 OpenAI 区域已经过载(429)或返回 5xx,你可以让 APIM 在同一个请求内自动换到另一个区域再试一次。下面是一个参考思路:
xml复制<backend>
<retry condition="@(context.Response != null && (context.Response.StatusCode == 429 || context.Response.StatusCode >= 500))" count="1" interval="1" first-fast-retry="true">
<choose>
<when condition="@(context.Response != null && (context.Response.StatusCode == 429 || context.Response.StatusCode >= 500))">
<!-- 如果第一次请求失败,把区域索引取反,切到另一个区域 -->
<set-variable name="regionIndex" value="@(1 - int.Parse((string)context.Variables["regionIndex"]))" />
<set-variable name="backendBaseUrl" value="@((string)context.Variables["regionIndex"] == "0" ? "https://openai-eastus-001.openai.azure.com" : "https://openai-westeurope-001.openai.azure.com")" />
<set-variable name="backendKey" value="@((string)context.Variables["regionIndex"] == "0" ? "{{openai-key-eastus}}" : "{{openai-key-westeurope}}")" />
<set-backend-service base-url="@((string)context.Variables["backendBaseUrl"])" />
<set-header name="api-key" exists-action="override">
<value>@((string)context.Variables["backendKey"])</value>
</set-header>
<forward-request />
</when>
<otherwise>
<forward-request />
</otherwise>
</choose>
</retry>
</backend>
这个写法在逻辑上说得通,但我必须提醒:APIM 的 retry 策略在真实场景里有不少边界情况,比如输出来自流式接口时的表现、重试过程中请求体是否可复用等。所以这一段我只当作“思路参考”给你,真上生产之前一定要用压测脚本完整验证。更稳妥的做法还是以 Traffic Manager 的区域级切换为主,把策略里的重试作为辅助。
3.6 用 Application Insights 观察多区域流量
APIM 的信息要在一个地方汇总,我选了 Application Insights。直接在 APIM 的“日志”设置里接入 Application Insights,配置好 instrumentation key,然后 APIM 会自动把请求日志推过去。
我还在出站策略里加了 trace,这样在 Application Insights 的查询页面里,就能按请求维度看到每条请求用的是哪个后端区域,状态码多少,耗时多少。排查问题时非常方便。
监控的核心不是看图好看,而是要设定报警规则。我做了两个报警:一是 429 数量突增,说明限流阈值设得偏低或者后端配额不足;二是 5xx 错误率超过阈值,说明某个区域可能有问题,需要人工介入。
4. 常见问题与排查技巧实录
4.1 429 与配额冲突:限流值到底怎么设
这是上线后被问得最多的一个问题。APIM 的 rate-limit 限流策略和 Azure OpenAI 的 TPM 配额是两个独立体系。APIM 先限一层,OpenAI 后端再限一层,如果你 APIM 这层设得太保守,明明后端还有余量,客户端也拿不到请求;如果设得太宽,后端 429 就会继续往外冒。
我的建议是:APIM 的限流值设为后端理论容量的 60%-80%,给突发流量留缓冲。同时,在 APIM 侧开启对 429 的重试机制,但重试次数不要太多,1 次即可。重试太多会造成请求堆积,反而把后端压垮。
还有一个小技巧:把 OpenAI 的 429 响应头和 APIM 自己的限流响应头统一。客户端看到 Retry-After 头就知道过多久再重试,体验会好很多。
4.2 流量没有按预期切走的排查顺序
如果你配置完发现某个区域的流量还是跑到了另一个区域,先别怀疑策略。我遇到这种情况时一般按以下顺序排查。
第一步,看 X-User-Id 这个请求头在客户端有没有传。如果客户端没传,我的策略会把它当成 anonymous,那么所有用户都落到同一个区域,看起来就像“负载均衡完全没生效”。
第二步,看 APIM 策略里哈希计算的结果。可以用 return-response 临时把变量打印到响应体里,比如:
xml复制<return-response>
<set-status code="200" reason="OK" />
<set-body template="none">@(context.Variables["regionIndex"] + " " + context.Variables["backendBaseUrl"])</set-body>
</return-response>
看到输出了什么,马上就能判断是哈希逻辑写错了,还是命名值引用错误。
第三步,看 Traffic Manager 的探测状态。如果某个 APIM 网关端点被标记为降级,流量会被全部导到另一个区域,那就不一定是策略的问题,而是区域网关本身不健康。
4.3 几个容易忽略的细节:缓存、证书、日志成本
缓存是个双刃剑。APIM 支持响应缓存,但 OpenAI 的输出大多是高度动态的,缓存命中率通常很低。我建议只在非常确定的场景下开启,比如一个固定 prompt、固定参数的分类任务。开启缓存时,缓存键一定要考虑请求头中的用户标识,否则很容易把 A 用户的结果返回给 B 用户。
证书方面,如果你给 APIM 配置了自定义域名,一定要在多区域部署前把证书准备好,并且把证书绑定到所有网关区域。我在配置多区域时吃过这个亏:主区域域名证书正常,附加区域的网关证书一直报错,排查了半天才发现证书是在主区域绑定后忘了同步到附加区域。
日志成本也是一个隐藏的坑。APIM 自带日志,Application Insights 也会采集日志和 trace,如果每个请求都打大量 trace,账单会涨得很快。上线后我把 trace 调整为只记录错误和慢请求,直接省了一笔不小的费用。
4.4 常见问题速查表
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 所有流量都打到同一个区域 | 客户端未传用户标识,哈希值全部相同 | 检查请求头,或者改用请求体中会话 ID |
| 429 仍然很多 | APIM 限流值大于后端 TPM 容量 | 按 TPM 换算 QPS,再乘 0.8 设置限流值 |
| 502 Bad Gateway | APIM 无法访问 OpenAI 终结点 | 检查终结点地址、网络配置、API 版本号 |
| 策略改了但不生效 | 保存策略时未点击“保存”,或者从上到下策略顺序不对 | 检查 APIM 保存状态,查看 base 标签位置 |
| Traffic Manager 切换后仍访问旧网关 | DNS 缓存未过期 | 检查 TTL,设置合理的 DNS 缓存时间 |
| 输出流式接口不稳定 | 流式响应经过 APIM 重试后出现断流 | 流式场景慎重开启重试,优先依赖 Traffic Manager 切换 |
最后分享一个实际落地时的经验
这套架构我前后改了三个版本才最终跑稳。第一个版本只在 APIM 里做了哈希路由,没有 Traffic Manager,单点问题还在;第二个版本加了 Traffic Manager,但 APIM 策略里的哈希逻辑没有考虑多网关实例,走了很多弯路;第三个版本才是现在用的这套“Traffic Manager + APIM 多区域网关 + 用户特征哈希路由”,前面配了限流,后面接了 Application Insights,才算真正达到生产要求。
如果你也想做类似的事情,我的建议是不要一上来就堆砌所有功能。第一步先把两个区域的 OpenAI 资源准备好,用最简单的轮询或哈希路由跑通;第二步加上限流和密钥注入;第三步才考虑 Traffic Manager 和多区域故障转移。每加一层,都要在测试环境里验证清楚再上生产。多区域负载均衡不是因为看起来高级才做,而是为了让自己晚上能安心睡觉。架构越简单,越不容易半夜被报警电话吵醒。
