☰
Java调用Python FastAPI:接口契约、超时连接池与联调排错
2026/10/27 15:46:02 网站建设 项目流程

Java 服务去调用 Python 用 FastAPI 搭的 Web 服务,这几年在算法能力接入、数据清洗、报表生成这类场景里越来越常见:模型和脚本用 Python 写起来快,主业务跑在 Java 的 Spring 体系里,两边只能靠 HTTP 接口握手。这篇东西不聊虚的,就把我实际做过几套这类集成后攒下的东西摊开讲——从接口契约怎么定、Java 侧客户端怎么选、连接池和超时怎么算,到 FastAPI 侧那些容易翻车的细节,再到联调时 422、415、502 挨个冒出来怎么查。适合正在做或准备做跨语言服务调用的后端同学,Java 基础和 Python 能看懂代码就够,不需要你两边都是专家。

1. 先想清楚:Java 为什么要绕一圈去调 Python

1.1 这种跨语言组合到底在解决什么问题

事情的起点通常很朴素:主业务系统是一套跑了多年的 Java 服务,订单、权限、账务这些逻辑稳得很,谁也不想动;而这次要加的能力——文本分类、图像处理、复杂报表计算、Excel 解析——用 Python 生态三五天能搞定,硬要塞进 Java 里要么找不到合适的库,要么性能和维护成本都不划算。

于是就有了这么个结构:Java 继续管业务编排和事务,Python 只负责那块“计算型”的活,中间用 HTTP 接口连起来。这个划分看着简单,但它带来一个很关键的隐性收益:Python 那部分可以独立部署、独立扩容、独立发版。算法迭代频繁的时候,你改完模型重启 Python 服务就行,Java 那边一行代码不用碰。反过来,Java 主服务滚动发布也不会把计算节点带崩。

代价也很明确:多了一跳网络,多了序列化和反序列化,多了一套要维护的接口契约。所以判断要不要这么拆,我的经验是看两条——这块能力是不是会被多个 Java 服务复用,或者它的迭代频率是不是明显高于主业务。两条占一条就值得拆,都不占的话,不如老老实实写在一个进程里,省掉后面一堆麻烦。

1.2 HTTP、消息队列、进程内嵌三条路怎么选

真要打通两边,路子其实不止一条,我把常见的三种摆在一起对比,这里的选择基本决定了后面所有工作量。

集成方式适用场景响应时效运维复杂度主要代价
同步 HTTP 调用需要立刻拿到结果,请求-响应语义清晰毫秒到秒级低强耦合,下游抖动会传导
消息队列异步批量任务、离线计算、允许延迟秒到分钟级中高需要额外中间件和结果回查机制
进程内嵌(子进程/JNI)单机工具型能力,调用量小微秒到毫秒低无法水平扩展,环境依赖难隔离

绝大多数人第一反应是走 HTTP,这个选择在“要即时结果”的场景里确实是最省事的。但要注意一点:HTTP 同步调用意味着上游的线程会被下游的耗时直接绑住。我在一个项目里见过 Java 侧 Tomcat 最大线程数 200,而 Python 侧某个接口 P99 到了 3 秒,结果高峰期 Java 的线程池瞬间打满,整个服务连带其他接口一起雪崩。这不是 HTTP 的错,是没做隔离和超时控制。

所以我的建议是:如果这块能力允许延迟(比如生成报表、批量打标),优先考虑 MQ 异步 + 结果回查,别硬上同步调用;如果必须同步,那 Java 侧一定要用独立的线程池或信号量做舱壁隔离,把这块的故障圈在一个小格子里,这是后面第 6 章会展开讲的东西。

1.3 一个具体的服务边界划分示例

拿一个我实际做过的场景说:某业务系统需要给上传的文档做结构化抽取,输入是一段纯文本,输出是若干个字段和置信度。划分是这样的——Java 负责接收请求、校验用户权限、落库、组装最终响应;Python 的 FastAPI 只暴露一个POST /api/v1/extract,输入文本和几个可选参数,输出结构化结果和耗时。

这条边界划下来有几条硬规矩:Python 侧不碰数据库,不读 Java 的业务表;Python 侧不感知用户是谁,鉴权信息由 Java 在请求头里透传一个内部令牌;Python 侧的任何一个字段变更都必须先在接口文档里体现,不能偷偷加字段。这些规矩听着啰嗦,但真到了两边各三五个人并行开发的时候,是它们救了你。

2. 接口契约先定死,别急着写代码

2.1 URL 结构与版本号放在哪里

我见过太多项目,接口写到一半发现要改返回结构,然后就是两边同时改、同时发、同时回滚的连环事故。根源都是契约没先定。所以第一件事:把 URL 和版本号定下来。

我的习惯是统一前缀/api/v1/,版本号放在路径里而不是请求头里。理由很实际——放路径里,你用浏览器、curl、Swagger 页面点一下就能验证;放头里,多一层心智负担,排查问题时还容易忘。等真要做 v2 且需要灰度时,就在网关层按流量比例把 v1 和 v2 分流,路径式版本对网关更友好。

FastAPI 里这样组织:

from fastapi import APIRouter, FastAPI app = FastAPI(title="extract-service", version="1.0.0") api_v1 = APIRouter(prefix="/api/v1", tags=["v1"]) @api_v1.get("/health") def health(): return {"status": "up"} app.include_router(api_v1)

路径上再补一条原则:动词别进 URL。/extract、/classify、/summary这种名词化的资源路径,比/doExtract、/runClassify好在哪?好处在于后面加缓存、加幂等、加审计的时候,路径语义是稳定的。这个不算硬性规矩,但团队里统一了会省很多口水。

2.2 请求体和响应体的字段约定

字段约定这块,我踩过最疼的坑是命名风格不一致。Java 那边习惯驼峰requestId,Python 那边pydantic默认也是蛇形request_id更自然,两边一混,序列化出来对不上,报错信息还特别含糊。

解决办法有两个:一是统一用蛇形,Java 侧在 DTO 上加@JsonProperty("request_id")显式映射;二是统一用驼峰,Python 侧给 Pydantic 模型配别名。我个人选前者,因为 Python 侧改起来更省事,而且FastAPI生成的/docs页面读起来更顺眼。不管选哪个,写进文档,全项目一个标准,别一个接口蛇形一个接口驼峰。

响应体我一律包一层信封结构,不用裸数据:

{ "code": 0, "message": "ok", "request_id": "8f3a1c2e-...", "data": { "fields": [{"key": "amount", "value": "1200.00", "score": 0.93}], "elapsed_ms": 87 } }

为什么要包这一层?因为业务错误和系统错误需要区分。HTTP 状态码只能表达“请求本身成不成功”,表达不了“业务上有没有取到结果”。比如输入文本合法但抽取不到任何字段,HTTP 返回 200 是对的,但code可以给个非零值让 Java 侧做不同处理。这层信封就是给业务语义留的位置。

2.3 错误码和异常的统一映射

FastAPI 默认的校验失败返回 422,格式是它的标准结构{"detail": [...]};业务异常你如果直接raise HTTPException,返回又是{"detail": "..."}。这两种结构跟上面那个信封完全不一样,Java 侧解析起来就得写两套逻辑。

我通常的做法是注册全局异常处理器,把这两类都收敛成信封:

from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from starlette.exceptions import HTTPException as StarletteHTTPException @app.exception_handler(RequestValidationError) async def on_validation_error(request: Request, exc: RequestValidationError): return JSONResponse( status_code=422, content={"code": 40001, "message": "参数校验失败", "request_id": getattr(request.state, "request_id", ""), "data": {"errors": exc.errors()}}, ) @app.exception_handler(StarletteHTTPException) async def on_http_error(request: Request, exc: StarletteHTTPException): return JSONResponse( status_code=exc.status_code, content={"code": exc.status_code * 100, "message": str(exc.detail), "request_id": getattr(request.state, "request_id", ""), "data": None}, )

错误码段位我一般这么分:4 开头的是调用方问题(参数错、缺字段、超限),5 开头的是服务端问题(内部异常、依赖不可用)。这个分段不是为了好看,是为了让 Java 侧的重试逻辑能一眼判断——4 开头的重试一百次也没用,5 开头的才值得退避重试。这条区分后面讲熔断降级时会用到。

3. Java 侧客户端选型与最小可用实现

3.1 四种 HTTP 客户端的横向对比

Java 里能发 HTTP 请求的东西太多了,我列一下实际项目里用得最多的四种:

方案依赖成本异步支持连接池适合场景
JDK HttpClient(11+)零依赖支持 CompletableFuture内置轻量调用、不想引第三方包
OkHttp单个 jar原生异步内置且好用需要精细控制超时和连接复用
Spring RestTemplate随 Spring 自带阻塞式需配连接工厂老项目、简单同步调用
Spring WebClient随 WebFlux 自带完全响应式需配 Reactor Netty高并发、非阻塞链路

选哪个?我的判断逻辑很简单:如果你的项目已经在 Spring Boot 里且是同步栈,RestTemplate配一个HttpComponentsClientHttpRequestFactory就够了,别为了“先进”硬上 WebClient,响应式链路的调试成本会把你劝退。如果是新项目或者调用量不小,我偏向 OkHttp,原因就一条——它的超时控制和连接池配置写起来最直白,出问题时参数一眼能看清。

JDK 自带的HttpClient这两年我也用得多了,尤其是写工具类或者不想给项目加依赖的时候。它唯一的坑是默认走 HTTP/2,某些老版本网关对 HTTP/2 支持不完整会导致偶发的连接异常,遇到这种就显式降级到 HTTP/1.1。

3.2 用 JDK HttpClient 写一个不引任何第三方包的调用

先给一个能直接跑的最小版本,方便你验证网络通不通:

import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.UUID; public class ExtractClient { private final HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofMillis(800)) .version(HttpClient.Version.HTTP_1_1) .build(); public String extract(String jsonBody, String token) throws Exception { HttpRequest req = HttpRequest.newBuilder() .uri(URI.create("http://127.0.0.1:8000/api/v1/extract")) .timeout(Duration.ofMillis(2500)) .header("Content-Type", "application/json; charset=utf-8") .header("Authorization", "Bearer " + token) .header("X-Request-Id", UUID.randomUUID().toString()) .POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8)) .build(); HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (resp.statusCode() != 200) { throw new IllegalStateException("接口返回异常状态:" + resp.statusCode() + ",响应体:" + resp.body()); } return resp.body(); } }

这段代码里有两个细节值得单独说。第一,字符集必须显式指定StandardCharsets.UTF_8,ofString和ofString的 BodyHandler 都要写。不写的话在某些平台默认字符集下,中文会变成乱码。第二,connectTimeout和timeout是两个东西——前者是 TCP 握手时间,后者是整个请求(含读响应)的时限。很多人只设了前者,结果下游接口 hang 住,上游线程跟着一起 hang,这也是第 1 章说的那个雪崩场景的来源。

3.3 OkHttp + Jackson 的生产级封装

要上生产,就得考虑连接复用、反序列化、异常归一。这里是 OkHttp 的写法:

import okhttp3.*; import java.io.IOException; import java.util.concurrent.TimeUnit; public class FastApiClient { private final OkHttpClient client; private final String baseUrl; private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); public FastApiClient(String baseUrl) { this.baseUrl = baseUrl; this.client = new OkHttpClient.Builder() .connectTimeout(800, TimeUnit.MILLISECONDS) .readTimeout(2500, TimeUnit.MILLISECONDS) .writeTimeout(1500, TimeUnit.MILLISECONDS) .callTimeout(3000, TimeUnit.MILLISECONDS) .connectionPool(new ConnectionPool(32, 5, TimeUnit.MINUTES)) .retryOnConnectionFailure(true) .build(); } public String post(String path, String json, String reqId) throws IOException { Request req = new Request.Builder() .url(baseUrl + path) .header("X-Request-Id", reqId) .post(RequestBody.create(json, JSON)) .build(); try (Response resp = client.newCall(req).execute()) { if (!resp.isSuccessful()) { throw new IOException("HTTP " + resp.code() + " body=" + (resp.body() == null ? "" : resp.body().string())); } return resp.body().string(); } } }

这里几个参数得说说。callTimeout是整次调用的总闸,比 read + write 之和还长一点点比较合理,作用是兜住“读完一段又来一段”的慢速攻击式响应。retryOnConnectionFailure(true)只对连接层失败重试,不重试业务错误,这个认知很多新手搞混——业务错误重试必须自己在上层做,而且要谨慎(第 6 章会讲哪些能重试)。

ConnectionPool的两个参数是最大空闲连接数和保活时间。给 32 是拍脑袋吗?不是,按第 3.4 节的公式算出来的量级。保活 5 分钟是为了让高频调用时连接能复用,省掉 TCP 三次握手和 TLS 协商。

3.4 连接池大小和超时预算怎么算

这是我觉得最值得展开讲的一段,因为大多数线上问题都出在这里。

先说超时预算。假设你的 Java 接口对外承诺 1 秒内返回,这个接口里要调一次 Python,那 Python 这步能分配多少时间?我的算法是:总预算减去本地处理(查库、序列化)大约 200ms,再减去网络抖动预留 100ms,剩下的 700ms 就是给 Python 调用的一整块。然后这块还要再切:连接 200ms,写请求 100ms,等响应 400ms,这样最坏情况加起来也不超过 700ms,不会击穿对外承诺。这个“逐层切预算”的思路,比拍一个 3 秒了事要靠谱得多。

再说连接池大小。用利特尔法则估算:需要的并发连接数 ≈ QPS × 平均耗时(秒)。举个例子,Java 侧峰值 QPS 300,Python 接口平均耗时 40ms = 0.04 秒,那并发大约是 12。考虑到毛刺和流量波动,连接池给 24 到 32 就足够,给再大只是徒增 Python 侧的并发压力,反而把对方压垮。

还有一个很常见的坑:连接池设了,但超时没设,一个慢请求把连接占死,池子很快就耗尽,日志里表现是大量Connection pool is exhausted或者timeout waiting for connection。这时候别急着调大池子,先去查是不是有调用没有正确关闭Response——OkHttp 的Response必须close(),否则连接不会归还,用try-with-resources是最省心的写法。

4. FastAPI 侧要配合做的几件事

4.1 Pydantic 模型把校验前移

Python 这边最容易忽略的是入参校验。FastAPI 天生配 Pydantic,但很多人只写类型不写约束,等于把脏数据放进来了。我的习惯是每个字段都带上边界:

from pydantic import BaseModel, Field from typing import List, Optional class ExtractRequest(BaseModel): text: str = Field(..., min_length=1, max_length=20000, description="待抽取的原始文本") fields: List[str] = Field(default_factory=list, max_length=50) threshold: float = Field(0.5, ge=0.0, le=1.0) request_id: Optional[str] = Field(None, max_length=64)

max_length这类约束不是形式主义。我就遇到过 Java 侧因为上游数据异常,往text里塞了 30 万字符,Python 侧没做限制,直接进了模型推理,一次吃了 4 个 G 内存,容器被 OOM 干掉。加上max_length=20000之后,超长请求在入口就被 422 拦掉,响应时间从分钟级降到毫秒级。

另外,Field(..., description=...)里的描述会直接出现在/docs页面上,Java 同学照着文档写 DTO 就不会猜错字段含义。这个成本几乎为零,收益却很高,我建议养成习惯。

4.2 异步路由里的阻塞调用是最大陷阱

这一条我必须重点说,因为它是最隐蔽的性能杀手。FastAPI 的路由如果声明成async def,它跑在事件循环里;你在里面写任何同步阻塞代码——requests.get、time.sleep、读大文件、跑同步模型推理——整个事件循环都会卡住,所有并发请求一起排队。

我见过一个服务,压测 QPS 只有 30 就上不去了,一看代码:

@app.post("/api/v1/extract") async def extract(req: ExtractRequest): data = requests.post("http://somewhere/internal", json={...}) # 阻塞 return data.json()

修复有两条路。如果这个活确实是 CPU 密集的(比如模型推理),就把路由改成普通def,FastAPI 会自动丢到线程池里执行:

@app.post("/api/v1/extract") def extract(req: ExtractRequest): # 注意这里去掉了 async result = heavy_sync_work(req.text) return {"code": 0, "message": "ok", "data": result}

默认线程池大小是 40,可以通过anyio调整。如果这个活是 IO 密集的(调下游接口、查缓存),那就应该换成httpx.AsyncClient或aiohttp,真正走异步:

import httpx @app.post("/api/v1/extract") async def extract(req: ExtractRequest): async with httpx.AsyncClient(timeout=2.0) as ac: r = await ac.post("http://somewhere/internal", json={"text": req.text}) return {"code": 0, "message": "ok", "data": r.json()}

怎么判断该用哪条?我的经验标准很粗暴:这段代码占用 CPU 的时长能不能忽略。能忽略就走async+ 异步库;不能忽略就走同步def。别在异步路由里混着写同步阻塞调用,那等于自己给自己埋雷。

4.3 鉴权、限流和请求 ID 透传

内部服务之间调用,鉴权通常不用搞太复杂,但也不能完全不设防。我一般采用共享密钥方案:Java 侧在Authorization头里带一个固定 token,Python 侧用依赖注入校验:

from fastapi import Header, HTTPException, Depends, Request import uuid, time, logging INTERNAL_TOKEN = "读自环境变量的密钥" async def verify_token(authorization: str = Header(default="")): if not authorization.startswith("Bearer "): raise HTTPException(status_code=401, detail="缺少凭证") if authorization[7:] != INTERNAL_TOKEN: raise HTTPException(status_code=403, detail="凭证无效")

请求 ID 的透传是我强烈建议加的,代价极低但排查问题时的价值巨大。做法是加一个中间件,从请求头读X-Request-Id,没有就生成一个,塞进request.state,同时在所有日志里带上:

@app.middleware("http") async def add_request_id(request: Request, call_next): rid = request.headers.get("X-Request-Id") or str(uuid.uuid4()) request.state.request_id = rid start = time.perf_counter() response = await call_next(request) cost = (time.perf_counter() - start) * 1000 logging.getLogger("access").info( "rid=%s path=%s status=%s cost=%.1fms", rid, request.url.path, response.status_code, cost) response.headers["X-Request-Id"] = rid return response

有了这个,线上用户报障的时候,Java 日志里一搜 request_id,Python 那边的对应日志一段不少地能捞出来。这个体验差别,做过跨服务排查的人都懂。

限流我一般放在网关或者 Nginx 层做,Python 进程内做限流意义不大——它管不住自己前面那一层。如果确实要在进程内挡一道,slowapi这类库能用,但要知道它是单进程内存计数,多副本部署时每个副本各算各的,不是全局精确值。

5. 联调现场:从 422 到 502 的完整排查路径

5.1 拿 curl 和 /docs 做第一轮验证

Java 代码一旦报错,第一件事不是改 Java,而是先把 Python 这一端摘干净。顺序我固定成三步。

第一步,curl直连,不经过任何 Java 代码:

curl -i -X POST http://127.0.0.1:8000/api/v1/extract \ -H "Content-Type: application/json" \ -H "Authorization: Bearer <token>" \ -d '{"text":"测试文本内容","threshold":0.5}'

如果这条通了,问题基本就在 Java 侧;如果不通,Java 代码先放一边,专心查 Python。

第二步,打开http://127.0.0.1:8000/docs,FastAPI 自带的可交互文档会把所有字段的类型、是否必填、约束范围列出来。Java 侧 DTO 字段名拼错、类型对不上(比如把数字传成字符串),在这里一眼就能看出来。

第三步,把 Java 实际发出去的报文抓出来。最省事的办法是在客户端打日志把json字符串原样输出,然后拿去和curl的报文逐字符比对。我遇到过不止一次,问题就是多了一个空格、少了一对引号,或者是 Java 侧toString()生成的 JSON 里带了个null字面量而 Python 侧该字段不允许为空。

5.2 常见问题速查表

这张表是我这些年攒下来的,基本覆盖了八成以上的联调故障:

现象大概率原因排查动作
422 参数校验失败字段名不一致、类型不对、缺必填字段看响应detail里的loc,它直接指出哪个字段错了
415 不支持的媒体类型忘了设Content-Type: application/json检查请求头,注意别写成text/plain
401/403token 没带、过期或环境变量没配用curl直连验证 token 本身有效
中文变问号或乱码字符集没显式指定Java 侧UTF_8,Python 侧中间件统一编码
读超时,但 Python 日志显示很快Java 侧超时设得太短,或中间有代理层缓冲逐层测时延,检查有无 Nginx 缓冲
502 Bad GatewayNginx 等待上游超时,或 Python 进程已挂看 Nginx error log 和容器状态
连接池耗尽Response未关闭,或超时设置不当检查try-with-resources,看连接数指标
偶发连接被重置HTTP/2 与网关不兼容Java 侧显式降级到 HTTP/1.1
高并发下 QPS 上不去异步路由里混了同步阻塞调用改为同步def或换异步 HTTP 库

表里最值得展开说两条。一个是 422 里的loc字段,很多人只看到“校验失败”就开始瞎猜,其实loc会直接告诉你["body", "request_id"]这种路径,点对点定位。另一个是 502,它几乎从来不是 Python 代码的锅,而是 Nginx 的proxy_read_timeout比 Python 实际耗时要短,或者 Python 进程压根没起来,docker ps一看便知。

5.3 日志和链路追踪怎么串起来

两边日志各自为政是跨服务调试最大的痛点。除了前面说的X-Request-Id透传,还有两件事值得做。

一是把耗时结构化打出来。Java 侧记下“发出请求的时刻、收到响应的时刻、响应体大小”;Python 侧记下“收到请求、各阶段耗时、返回状态”。两边用同一个 request_id 串联,你在日志系统里按这个 key 一搜,完整的时间线就出来了,哪一段慢一目了然。

二是日志格式统一成 key=value,别用自由文本。rid=xxx cost=87ms status=200这种格式,用 grep 能筛,用日志平台能直接解析成字段做聚合。写成“本次请求耗时 87 毫秒”就只能人眼看了。

6. 上线前必须做的三件事

6.1 压测脚本和要看的关键指标

本地通了不代表能上生产。压测这件事,我一般用wrk或者ab直接打 Python 端,先摸清单机的容量上限:

wrk -t4 -c100 -d30s --latency \ -s post.lua http://127.0.0.1:8000/api/v1/extract

post.lua里放一个固定请求体。重点看三个数:P99 延迟、吞吐量、错误率。P99 往往比平均值更能反映真实体感,因为它代表最慢那 1% 的请求——而这 1% 才是用户抱怨的来源。我通常要求 P99 不超过对外承诺时长的一半,留出余量给流量突增。

然后要把 Java 侧一起压。这时候关注的是另一组指标:Java 应用线程池活跃数、连接池占用、GC 频率。有一次我压测发现 Python 侧指标很漂亮,但 Java 侧响应时间一路飙高,最后定位到是 Jackson 反序列化大响应体时 CPU 打满,跟网络半点关系没有。所以跨语言调用,两端的资源都得看。

6.2 重试、熔断和降级的边界

重试不是越多越好,得先分类。网络层错误(连接超时、连接重置)可以重试,但要限制次数,我一般给 2 次,且必须配指数退避加随机抖动,否则一堆客户端同时重试会把下游直接打死。业务层错误(4 开头的错误码)绝对不能重试,参数错了重试一万次还是错。5 开头的服务端错误,要看接口是否幂等——抽取、分类这类纯计算接口天然幂等,可以重试;如果有落库、有状态变更,就不能随便重。

熔断用Resilience4j之类现成组件就行,关键是阈值别拍脑袋。我一般配:最近 20 次调用里失败率超过 50% 就打开熔断,打开后 30 秒进入半开状态,放一个请求过去试探。这个组合在实测里比较稳,既能快速止损,又不会因为一两次偶发失败就长期熔断。

降级就更简单了:给 Python 调用套一层try-catch,熔断打开或者超时的时候,返回一个默认结果或者明确提示“当前无法处理,请稍后再试”。这个兜底逻辑一定要提前写,别等线上出事了临时加。

6.3 容器化部署与健康检查

部署这块,docker-compose或者 K8s 都行,但有两点必须做。

第一,健康检查要区分“存活”和“就绪”。只暴露一个/health返回200是不够的,如果它依赖某个模型文件还没加载完就返回成功,流量进来照样报错。我一般做两个端点:/health/live只检查进程在不在,/health/ready检查模型是否加载完成、依赖是否连通。

@app.get("/health/ready") def ready(): if not MODEL_LOADED: raise HTTPException(status_code=503, detail="模型尚未加载") return {"status": "ready"}

第二,优雅停机。FastAPI 配合 uvicorn 的时候,收到 SIGTERM 会先停止接受新请求,再把在途请求跑完。但很多人写启动命令时用了--workers多进程,停机过程会稍复杂一点。实际经验是给容器配置一个terminationGracePeriodSeconds,比最慢接口的耗时再长 5 秒,基本不会出现请求被硬砍的情况。

另外 Nginx 那一层如果做了代理,记得把proxy_read_timeout调到大于 Python 的最慢响应,并且关掉proxy_buffering(如果是流式返回)。这两个参数没配对,就是前面说的那种“Python 说很快,Java 说超时”的经典现象。

还有一个我自己的小习惯,前后端联调阶段就把slowapi或者自定义中间件把超过 1 秒的请求单独打一条 warn 日志,带上 request_id 和参数摘要。这条日志在灰度阶段救过我好几次——正式流量里总会冒出一些压测造不出来的长尾请求,提前知道总比被打穿之后才知道强。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询