业务代码直连推理服务、密钥散落在仓库里,本质是把「谁能调用」和「调用多快」两件事都塞进了业务进程。更稳妥的做法是:把鉴权判定收敛到网关层,业务侧只持有调用网关所需的身份凭证,且凭证从环境变量或配置中心注入,不进代码仓库、不进镜像。限流同理,通常放在网关层按调用方维度做,服务侧只保留必要的配额兜底。这套划分适合多业务共用一套推理服务的场景;如果只有一个内部调用方、且无法引入网关,也可以先在服务侧做鉴权,但要接受密钥分发和轮换成本落在服务侧。
先把鉴权位置定在网关层:业务代码只认网关地址,凭证从环境变量或配置中心读取,仓库里不出现明文密钥。网关负责身份校验和调用方维度的限流,服务侧只做资源与配额的兜底。上线前用缺少凭证、凭证错误、触发限流三种情形各打一次请求,从返回码和网关访问日志确认拦截点生效;不要凭「配置写好了」就当鉴权已生效。
画出业务、网关、DeepInfer 三点之间的调用链路
先把链路画出来,再决定鉴权写在哪。一个可用的最小链路是:业务进程 → 接入网关(鉴权 + 限流 + 路由)→ DeepInfer。业务进程不再直接持有推理服务的访问密钥,只持有网关签发的调用身份。
业务服务 A ─┐
业务服务 B ─┼─→ 接入网关(校验凭证 / 计数限流 / 转发)─→ DeepInfer
定时任务 C ─┘ │
└─→ 访问日志:调用方、返回码、耗时、trace id各层职责边界建议按下面的方式划分,边界比实现更重要:
| 层 | 应承担 | 不应承担 |
|---|---|---|
| 业务服务 | 读取自身凭证、设置超时与重试上限、把失败映射成业务动作 | 保存 DeepInfer 的服务密钥、实现限流算法、判断配额余额 |
| 接入网关 | 校验调用方凭证、按调用方维度限流、统一转发与访问日志 | 理解业务语义、替业务决定重试还是降级 |
| DeepInfer 服务侧 | 完成推理、返回明确的错误码与配额信息 | 代替网关做调用方身份管理、向业务暴露内部密钥 |
如果业务直连 DeepInfer 而不经过网关,鉴权和限流都只能落在服务侧,这时候至少要保证:每个调用方一个独立凭证、凭证可单独吊销、服务侧能按凭证维度统计调用量。缺了这三点,出问题时无法定位是哪个业务打满了配额。
把凭证改成从环境变量或配置中心读取
目标是让密钥不出现在代码仓库、CI 日志和镜像层里。做法是代码只声明「从哪个变量名取值」,具体值由部署环境注入。下面是不绑定具体接口的通用骨架,参数名和客户端按实际使用的库替换。
import os
class InferenceClient:
def __init__(self, client):
# 只读取变量名,值为空时直接失败,避免静默降级
token = os.environ.get("DEEPINFER_GATEWAY_TOKEN")
if not token:
raise RuntimeError("missing DEEPINFER_GATEWAY_TOKEN")
self._client = client
self._headers = {
"Authorization": f"Bearer {token}",
"X-Caller": os.environ.get("SERVICE_NAME", "unknown"),
}
self._base_url = os.environ["DEEPINFER_GATEWAY_URL"]
self._timeout = float(os.environ.get("DEEPINFER_TIMEOUT_S", "5"))补充几点容易被忽略的:镜像构建阶段不要把 .env 打进镜像,密钥通过部署时的环境变量或配置中心下发;日志打印请求头前先遮蔽 Authorization;凭证按环境区分,预发和生产的凭证不要复用;凭证要有轮换路径,轮换时支持新旧凭证短时间并存,否则每次换密钥都是一次停机操作。
在调用侧加上超时与重试次数上限
推理服务变慢时,没有超时的调用会把业务线程一路挂住,最终拖垮上游。超时建议按「连接超时 + 读取超时」分别设置,读取超时按业务能接受的最长等待来定,而不是按服务端平均值来定。
# 通用示意,具体参数名按所用客户端调整
client = build_client(
base_url=os.environ["DEEPINFER_GATEWAY_URL"],
connect_timeout=1.0,
read_timeout=float(os.environ.get("DEEPINFER_TIMEOUT_S", "5")),
max_retries=2, # 重试次数上限,含首次调用之外的重试
retry_on_status=[429, 502, 503, 504],
)重试有前提:只有幂等请求才值得重试。纯推理请求如果没有副作用,通常可以重试;但如果请求里带了下游写入、计费扣减或任务提交,重试前必须确认接口侧有幂等键,否则会把一次失败变成两次副作用。另外要对重试做退避,遇到限流类返回码时按响应头里的等待时间退避,而不是立即重打。
用返回码或日志确认鉴权与限流真的生效
配置写完不等于生效,至少在预发环境把下面三种情形各走一遍,看返回码和日志两侧是否对得上。
- 缺少凭证:请求不带 Authorization 头。观察点:网关应返回 401 一类未认证结果,业务侧日志应能看到本地就抛出了「凭证缺失」,网关访问日志应记录被拒绝的调用方标识。
- 凭证不正确:带一个格式正确但无效的凭证。观察点:返回码同样是 401/403,但网关日志里应能区分「无凭证」和「凭证校验失败」,否则排查时无法判断是配置漏注入还是值写错。同时确认业务日志里没有把凭证原文打出来。
- 触发限流:在测试环境用较高并发打同一凭证。观察点:出现 429 一类限流返回码,响应头里通常带可等待时间,网关日志能看到该调用方的计数被拒绝,而其他调用方不受影响。
验证时建议在请求里带一个 trace id,并确保它同时出现在业务日志和网关访问日志中,这样一条链路可以从业务侧追到网关侧,而不是靠猜。命令行的最小验证可以在本地直接打网关地址,比在业务代码里加断点更快。
把失败类型映射成业务侧可读的处理动作
上线后最耗时间的不是报错本身,而是每次报错都要回头翻框架代码判断「这个错该不该重试」。提前把错误类型映射成固定动作,业务代码里就只剩分支。
| 错误类型 | 判断依据 | 处理动作 |
|---|---|---|
| 网络连接失败、读取超时 | 连接异常或超时异常 | 重试:在重试上限内退避重试,超过上限按降级处理 |
| 限流 | 429 一类返回码,或响应头给出等待时间 | 重试:按等待时间退避;持续限流则转降级,返回缓存结果或默认文案 |
| 服务端暂时不可用 | 502/503/504 一类返回码 | 重试:有限次数重试,仍失败则降级并记录调用方与 trace id |
| 凭证缺失或无效 | 401/403,且重试不会改变结果 | 直接抛出:配置问题,不重试,告警到负责部署的人 |
| 请求参数不合法 | 400 一类返回码,响应体给出字段提示 | 直接抛出:属于调用方代码缺陷,进入日志和测试用例 |
| 下游明确的业务拒绝 | 返回体中带业务错误码 | 直接抛出:交给业务逻辑决定提示方式,不自动重试 |
映射表的关键是「重试 / 降级 / 直接抛出」三类不要混着写,每个错误只落一类。降级时明确返回什么内容、是否对用户可见;直接抛出的错误要带上调用方标识和 trace id,否则日志里只有一行异常,无法回连到具体请求。