Serverless 架构下的 AI 应用——FaaS 适合跑 AI 推理吗
Serverless 架构下的 AI 应用——FaaS 适合跑 AI 推理吗
去年某个项目里,有人提议把 AI 推理服务部署到 AWS Lambda 上。理由听起来很合理:「不用管服务器,按调用次数计费,弹性好,省运维成本。」
我当时没有直接否定,因为「要不要用 Serverless」这个问题本身是对的,但「AI 推理是否适合 Serverless」需要具体分析。
最后的结论是:部分适合,部分不适合,差别很大。
这篇文章就来把这个问题讲清楚。不是给你一个「适合/不适合」的简单结论,而是把判断的依据和实测数据说清楚,让你能根据自己的场景做出判断。
一、Serverless 和 AI 应用的基本矛盾
先把核心矛盾说出来:Serverless(特指 FaaS,如 Lambda)的设计哲学是「短生命周期、无状态、快速启动」,而 AI 推理的特点是「启动慢(模型加载)、有时需要状态、调用耗时不均匀」。
这两者之间有天然的张力。
1.1 冷启动问题
Lambda 的冷启动对普通服务来说是几百毫秒,可以接受。但 AI 推理场景有两种类型:
类型 A:调用外部 AI 服务(如 OpenAI API)
- Lambda 本身只是转发请求,不加载模型
- 冷启动:JVM 启动 + Spring 初始化,大约 3-8 秒
- 一旦预热,每次调用只有网络延迟
类型 B:本地运行模型(如 ONNX/TensorFlow)
- Lambda 启动时需要加载模型文件(几十 MB 到几个 GB)
- 冷启动:JVM + Spring + 模型加载,可能 30-120 秒
- 根本不可用
所以「AI 推理用 Serverless」这个命题,对于 A 类是有讨论价值的,对于 B 类基本可以直接排除。
本文后面主要讨论 A 类:用 Lambda 调用外部 AI 服务。
二、架构对比
两种架构的核心差异:
| 维度 | Serverless (Lambda) | 常驻服务 |
|---|---|---|
| 启动时间 | 3-8秒(冷启动) | 一次性 |
| 空闲成本 | 0(按调用计费) | 固定成本 |
| 峰值弹性 | 自动 | 需要手动/自动扩容 |
| 数据库连接 | 每次冷启动重建 | 连接池复用 |
| 本地缓存 | 无法跨实例 | 可以(受实例限制) |
| 最大执行时间 | 15分钟(Lambda限制) | 无限制 |
| 流式响应 | 有限支持 | 原生支持 |
三、实测:Lambda 跑 AI 推理的延迟和成本
我做了一个对比测试,场景是:用 Spring Cloud Function(可部署到 Lambda)调用 OpenAI API,生成 200-300 字的摘要。
3.1 测试配置
Lambda 配置:
- 内存:512MB(低档)和 1024MB(中档)
- 运行时:Java 17(SnapStart 开启)
- 超时:30秒
对比:EC2 t3.small(2 vCPU,2GB,按需计费)3.2 延迟数据(100次请求测试)
Lambda 512MB(未开启 SnapStart):
冷启动延迟(约10%的请求):平均 6,240ms
热启动延迟(约90%的请求):
- P50: 1,820ms(其中 AI API 耗时约 1,500ms)
- P95: 3,420ms
- P99: 5,100msLambda 1024MB(开启 SnapStart):
冷启动延迟:平均 1,820ms(SnapStart 显著改善)
热启动延迟:
- P50: 1,780ms
- P95: 2,950ms
- P99: 3,800msEC2 t3.small(常驻):
P50: 1,710ms(基本等于 AI API 本身的延迟)
P95: 2,640ms
P99: 3,200ms结论:
- 热启动状态下,Lambda 和 EC2 的延迟差异不大(热启动的额外开销约 50-100ms)
- 冷启动是关键问题,开启 SnapStart 能显著改善
- P99 延迟 Lambda 明显高于 EC2,原因是冷启动会拉高尾部延迟
3.3 成本对比
假设:每天 50,000 次 AI 查询,每次平均 2 秒,内存 1GB
Lambda(含 SnapStart):
请求费用:50,000 × 0.0000002 = $0.01/天
计算费用:50,000 × 2s × 1GB × 0.0000166667/GB·s = $1.67/天
总计:约 $1.68/天 = ~$50/月EC2 t3.small:
按需价格:$0.0208/小时 × 24 × 30 = $14.98/月
但这个实例扛不住 50,000 次 AI 查询(AI API 延迟 1-2 秒)
实际可能需要 2-3 台:$30-$45/月ECS Fargate(0.25 vCPU, 0.5GB):
$0.04048/vCPU·h × 0.25 × 720h = $7.29/月 per 任务
2 个任务:$14.58/月成本结论: 在中等流量下(每天 50,000 次),Lambda 和 ECS Fargate 成本相当。低流量时 Lambda 明显便宜(空闲不收费),高流量时(每天 100 万次以上)Fargate/EC2 会更划算。
四、Spring Cloud Function 实现
Spring Cloud Function 让你写一次代码,可以部署到 Lambda、Azure Functions 或本地运行。
4.1 依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-aws</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>4.2 核心函数定义
@SpringBootApplication
public class AiLambdaApplication {
public static void main(String[] args) {
SpringApplication.run(AiLambdaApplication.class, args);
}
/**
* AI 摘要函数
* 输入:SummaryRequest(包含要摘要的文本)
* 输出:SummaryResponse(包含生成的摘要)
*/
@Bean
public Function<SummaryRequest, SummaryResponse> summarize(ChatClient chatClient) {
return request -> {
if (request.text() == null || request.text().isBlank()) {
return new SummaryResponse(null, "输入文本不能为空");
}
// 限制输入长度(避免 Token 超限和高成本)
String text = request.text().length() > 3000
? request.text().substring(0, 3000) + "..."
: request.text();
try {
String summary = chatClient.prompt()
.system("你是一个专业的文本摘要工具,用简洁的语言提炼核心内容,不超过200字。")
.user("请为以下文本生成摘要:\n\n" + text)
.call()
.content();
return new SummaryResponse(summary, null);
} catch (Exception e) {
return new SummaryResponse(null, "摘要生成失败: " + e.getMessage());
}
};
}
/**
* 情感分析函数
*/
@Bean
public Function<SentimentRequest, SentimentResponse> analyzeSentiment(ChatClient chatClient) {
return request -> {
String prompt = String.format("""
分析以下文本的情感倾向,返回 JSON:
{"sentiment": "POSITIVE|NEGATIVE|NEUTRAL", "score": 0.0-1.0, "reason": "分析原因"}
文本:%s
""",
request.text()
);
try {
String result = chatClient.prompt()
.user(prompt)
.options(OpenAiChatOptions.builder().temperature(0.0).build())
.call()
.content();
// 解析 JSON 结果
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(extractJson(result), SentimentResponse.class);
} catch (Exception e) {
return new SentimentResponse("NEUTRAL", 0.5, "分析失败: " + e.getMessage());
}
};
}
record SummaryRequest(String text, String language) {}
record SummaryResponse(String summary, String error) {}
record SentimentRequest(String text) {}
record SentimentResponse(String sentiment, double score, String reason) {}
}4.3 Lambda 处理器
/**
* AWS Lambda 入口处理器
* Spring Cloud Function 会自动路由到对应的 @Bean Function
*/
public class AiLambdaHandler
extends SpringBootRequestHandler<APIGatewayV2HTTPEvent, APIGatewayV2HTTPResponse> {
// Spring Cloud Function 处理路由,这里不需要写额外代码
}4.4 针对 Lambda 的配置优化
# application-lambda.yml
spring:
cloud:
function:
definition: summarize;analyzeSentiment # 声明可用函数
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
# Lambda 里设置严格的超时,避免请求挂起
timeout: 20s
# 禁用不必要的自动配置,加快启动速度
spring.autoconfigure.exclude:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
- org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
# 懒加载:只在第一次用到时才初始化 Bean(减少冷启动时间)
spring.main.lazy-initialization: true4.5 连接复用(关键优化)
Lambda 在热启动时会复用同一个实例,连接复用能显著减少每次请求的建连开销:
@Configuration
public class HttpClientConfig {
/**
* 配置支持连接池的 HTTP 客户端
* Lambda 热启动时复用连接,避免每次重新建立 TCP 连接
*/
@Bean
public CloseableHttpClient httpClient() {
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(10);
connectionManager.setDefaultMaxPerRoute(5);
// 设置合理的超时时间
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofSeconds(5))
.setResponseTimeout(Timeout.ofSeconds(25))
.build();
return HttpClients.custom()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(requestConfig)
// 复用连接
.setKeepAliveStrategy(
(response, context) -> TimeValue.ofSeconds(30)
)
.build();
}
}五、Lambda 的三个致命问题
在 AI 场景下,Lambda 有三个问题需要特别处理。
5.1 流式响应支持有限
SSE(Server-Sent Events)流式响应在 Lambda 上支持有限。Lambda 的传统调用模型是:请求进来,等待处理完成,返回完整响应。
AWS Lambda 响应流(Response Streaming)是比较新的特性,但对 Spring 的支持还不成熟。
解决方案:异步处理 + 轮询或 WebSocket
/**
* 异步处理模式:接收请求后异步处理,结果写入 DynamoDB 或 SQS
* 前端轮询获取结果
*/
@Bean
public Function<AsyncAIRequest, AsyncAIResponse> asyncAISummarize(
ChatClient chatClient,
DynamoDbClient dynamoDb) {
return request -> {
String taskId = UUID.randomUUID().toString();
// 立即返回 taskId,异步处理
CompletableFuture.runAsync(() -> {
try {
String result = chatClient.prompt()
.user(request.text())
.call()
.content();
// 写入 DynamoDB
dynamoDb.putItem(PutItemRequest.builder()
.tableName("ai-tasks")
.item(Map.of(
"taskId", AttributeValue.fromS(taskId),
"status", AttributeValue.fromS("COMPLETED"),
"result", AttributeValue.fromS(result),
"ttl", AttributeValue.fromN(
String.valueOf(Instant.now().plusSeconds(3600).getEpochSecond())
)
))
.build());
} catch (Exception e) {
dynamoDb.putItem(PutItemRequest.builder()
.tableName("ai-tasks")
.item(Map.of(
"taskId", AttributeValue.fromS(taskId),
"status", AttributeValue.fromS("FAILED"),
"error", AttributeValue.fromS(e.getMessage())
))
.build());
}
});
return new AsyncAIResponse(taskId, "PROCESSING");
};
}5.2 数据库连接问题
Lambda 的并发实例多,每个实例都有自己的连接池,容易打爆数据库的连接数上限。
解决方案:使用 RDS Proxy
@Configuration
@ConditionalOnCloudPlatform(CloudPlatform.AWS) // 只在 Lambda 环境生效
public class LambdaDataSourceConfig {
/**
* Lambda 环境使用 RDS Proxy
* 减少数据库连接压力,避免连接数爆炸
*/
@Bean
@Primary
public DataSource lambdaDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(System.getenv("RDS_PROXY_URL"));
config.setUsername(System.getenv("DB_USERNAME"));
config.setPassword(System.getenv("DB_PASSWORD"));
// Lambda 里每个实例的连接池很小
config.setMaximumPoolSize(2);
config.setMinimumIdle(0); // 空闲时不保持连接
config.setConnectionTimeout(3000); // 3秒超时
config.setIdleTimeout(10000); // 10秒空闲就释放
return new HikariDataSource(config);
}
}5.3 执行时间限制
Lambda 最长执行 15 分钟。大多数 AI 对话没问题,但 Agent 类应用(多轮工具调用、长时间推理)可能超时。
解决方案:长任务用 Step Functions 编排
/**
* 对于长时间运行的 AI Agent 任务
* 用 AWS Step Functions 编排多个 Lambda 函数
* 每个 Lambda 处理一个步骤,状态存储在 Step Functions 中
*/
@Bean
public Function<AgentStepInput, AgentStepOutput> agentStep(
ChatClient chatClient,
SfnClient sfnClient) {
return input -> {
// 执行当前步骤
String stepResult = executeStep(chatClient, input);
// 判断是否完成
boolean isDone = isTaskComplete(stepResult, input);
if (isDone) {
return new AgentStepOutput(stepResult, "COMPLETED", null);
} else {
// 继续下一步(由 Step Functions 调度下一个 Lambda 执行)
return new AgentStepOutput(stepResult, "CONTINUE",
buildNextStepInput(input, stepResult));
}
};
}六、适用场景和不适用场景
适合 Lambda 的 AI 场景:
- 异步文本处理(摘要、翻译、分类)
- 低频的智能问答(内部工具、运营后台)
- 事件触发的 AI 处理(文件上传触发内容分析)
- Webhook 处理(接收第三方事件后调用 AI 处理)
不适合 Lambda 的 AI 场景:
- 实时对话(流式响应 + 低延迟要求)
- 本地模型推理(冷启动太慢)
- 长时间 Agent 任务(超出15分钟限制)
- 高频查询(百万级/天,成本不划算)
七、总结和判断依据
在做「AI 应用用不用 Serverless」这个决策时,我用的判断框架是:
问三个问题:
- 你的流量模式是什么?低频间歇性 → Lambda;高频稳定 → 常驻服务
- 你有延迟敏感要求吗?P99 < 3 秒 → 需要 SnapStart 或常驻服务
- 你需要流式响应或长时间任务吗?需要 → 常驻服务
那个项目最终的方案:异步内容处理用 Lambda(每天约 2 万次,低频),在线对话 API 用 ECS Fargate(需要流式响应)。两种架构混用,各自负责最适合的场景。
不是说 Serverless 不好,而是它有它的适用边界。把这个边界想清楚,比盲目跟风「Serverless 万能」或「Serverless 不好用」都更有价值。
