第2020篇:私有化LLM服务的高可用——负载均衡与故障自动恢复
2026/4/30大约 7 分钟
第2020篇:私有化LLM服务的高可用——负载均衡与故障自动恢复
适读人群:需要将私有化LLM部署到生产环境的架构师和运维工程师 | 阅读时长:约18分钟 | 核心价值:设计LLM服务的高可用架构,避免单点故障导致的服务中断
我们的LLM服务上线第三周,GPU节点宕机了。
那台机器上跑着唯一的模型服务实例,宕机的那两个小时,所有AI功能全部不可用。客户投诉电话打来的时候,我当时手边正好有另一台备用GPU。那台GPU空了好几个月,就是因为我觉得"先跑通再说高可用"。
那之后我把高可用设计提到了和功能开发同等的优先级。
问题的本质
LLM服务的高可用比普通微服务难,难在哪里?
主要是两点:一是成本,多几台GPU服务器开销很大;二是状态,LLM推理本身是无状态的(每次请求独立),但模型加载很慢(几十秒到几分钟),快速故障恢复并不容易。
设计高可用方案时,需要在这几个维度做权衡:
对大多数企业来说,"主私有+降级到云API"的混合方案是性价比最高的选择。
负载均衡设计
如果有两台以上GPU节点,用Nginx做负载均衡是最简单的方案:
# nginx.conf - LLM服务负载均衡配置
upstream llm_servers {
# 使用最少连接算法(LLM请求耗时不均匀,最少连接比轮询更合理)
least_conn;
# 主服务器(A100 40GB,承接80%流量)
server 10.0.1.10:8080 weight=8 max_fails=2 fail_timeout=30s;
# 备用服务器(A10G 24GB,承接20%流量)
server 10.0.1.11:8080 weight=2 max_fails=2 fail_timeout=30s;
# 保持连接,避免频繁握手
keepalive 32;
}
server {
listen 80;
server_name llm-service.internal;
location /v1/ {
proxy_pass http://llm_servers;
# LLM请求可能很慢,超时要设长
proxy_read_timeout 120s;
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
# 保持连接
proxy_http_version 1.1;
proxy_set_header Connection "";
# 传递真实客户端IP(用于限流和审计)
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 关键:不要在这里做重试!LLM请求是非幂等的,重试会导致重复生成
proxy_next_upstream off;
}
# 健康检查接口
location /health {
proxy_pass http://llm_servers/health;
proxy_read_timeout 5s;
}
}这里有个容易犯错的地方:proxy_next_upstream off。很多人直觉上会设置"如果一台服务器失败就自动重试到另一台",但LLM请求是非幂等的——如果请求已经开始生成,中途重试会把同一个请求发给另一台服务器,导致生成两次甚至资源浪费。正确做法是把重试逻辑放在应用层,有明确的重试判断逻辑。
Java端的熔断降级
应用层必须实现自己的熔断和降级逻辑,不能完全依赖Nginx:
/**
* LLM服务客户端,内置熔断、降级、重试
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ResilientLlmClient {
// 私有化部署的主服务
private final ChatClient privateLlmClient;
// 云API备用(成本高但可靠)
private final ChatClient cloudLlmClient;
private final CircuitBreakerRegistry circuitBreakerRegistry;
private final MeterRegistry meterRegistry;
@PostConstruct
public void initCircuitBreaker() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 失败率超过50%开启熔断
.waitDurationInOpenState(Duration.ofSeconds(30)) // 熔断30秒后尝试半开
.permittedNumberOfCallsInHalfOpenState(3) // 半开状态测试3次请求
.slidingWindowSize(10) // 最近10次请求的失败率
.recordExceptions(Exception.class)
.ignoreExceptions(ValidationException.class) // 参数错误不计入熔断
.build();
circuitBreakerRegistry.addConfiguration("llm-private", config);
}
/**
* 调用LLM,自动处理熔断和降级
*/
public LlmResponse call(LlmRequest request) {
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(
"llm-private", "llm-private");
// 记录熔断状态
CircuitBreaker.State state = circuitBreaker.getState();
if (state == CircuitBreaker.State.OPEN) {
log.warn("私有LLM熔断中,直接降级到云API");
return callCloudWithFallback(request);
}
try {
// 用熔断器包装私有LLM调用
String content = circuitBreaker.executeCallable(() ->
callPrivateLlm(request));
meterRegistry.counter("llm.calls", "type", "private", "status", "success").increment();
return LlmResponse.success(content, "private");
} catch (CallNotPermittedException e) {
// 熔断器开启,不允许调用
log.warn("私有LLM熔断,降级到云API");
return callCloudWithFallback(request);
} catch (Exception e) {
log.error("私有LLM调用失败: {}", e.getMessage());
meterRegistry.counter("llm.calls", "type", "private", "status", "error").increment();
// 失败后立即尝试云API
return callCloudWithFallback(request);
}
}
private String callPrivateLlm(LlmRequest request) {
// 设置较短的超时(内网调用,不应该太慢)
return privateLlmClient.prompt()
.system(request.getSystemPrompt())
.user(request.getUserMessage())
.options(ChatOptions.builder()
.temperature(request.getTemperature())
.maxTokens(request.getMaxTokens())
.build())
.call()
.content();
}
private LlmResponse callCloudWithFallback(LlmRequest request) {
// 敏感数据检查:某些数据不允许发到云端
if (request.isContainsSensitiveData()) {
log.error("请求包含敏感数据,无法降级到云API");
return LlmResponse.error("服务暂时不可用,请稍后再试");
}
try {
String content = cloudLlmClient.prompt()
.system(request.getSystemPrompt())
.user(request.getUserMessage())
.call()
.content();
meterRegistry.counter("llm.calls", "type", "cloud", "status", "success").increment();
log.info("成功降级到云API");
return LlmResponse.success(content, "cloud");
} catch (Exception e) {
log.error("云API也失败了: {}", e.getMessage());
meterRegistry.counter("llm.calls", "type", "cloud", "status", "error").increment();
return LlmResponse.error("AI服务暂时不可用,请稍后重试");
}
}
/**
* 获取当前服务状态(用于监控和告警)
*/
public LlmServiceStatus getStatus() {
CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker("llm-private");
return LlmServiceStatus.builder()
.privateServiceState(cb.getState().name())
.privateServiceFailureRate(cb.getMetrics().getFailureRate())
.build();
}
}健康检查与自动恢复
光有熔断还不够,还需要主动检测服务状态,在服务恢复时自动切回:
@Component
@RequiredArgsConstructor
@Slf4j
public class LlmHealthChecker {
private final ResilientLlmClient llmClient;
private final AlertService alertService;
private final CircuitBreakerRegistry circuitBreakerRegistry;
// 专门用于健康检查的轻量级私有客户端
private final ChatClient privateLlmCheckClient;
private volatile boolean lastCheckPassed = true;
/**
* 定期检查私有LLM健康状态
* 使用轻量级的ping请求,不走业务熔断器
*/
@Scheduled(fixedDelay = 15000) // 每15秒检查一次
public void checkPrivateLlmHealth() {
long start = System.currentTimeMillis();
boolean checkPassed = false;
try {
// 用一个简单的请求测试连通性和响应时间
String response = privateLlmCheckClient.prompt()
.user("ping")
.call()
.content();
long latency = System.currentTimeMillis() - start;
checkPassed = true;
if (latency > 10000) {
// 响应太慢,记录警告但不触发熔断
log.warn("私有LLM响应慢: {}ms", latency);
alertService.warn("私有LLM响应时间超过10秒,可能存在性能问题");
}
} catch (Exception e) {
log.error("私有LLM健康检查失败: {}", e.getMessage());
}
// 状态变化时发告警
if (lastCheckPassed && !checkPassed) {
alertService.critical("私有LLM服务宕机,已自动降级到云API");
log.error("私有LLM不可用,切换到降级模式");
} else if (!lastCheckPassed && checkPassed) {
alertService.info("私有LLM服务恢复,自动切回");
log.info("私有LLM恢复,切回主服务");
// 服务恢复后,主动将熔断器从OPEN状态切换到HALF_OPEN
CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker("llm-private");
if (cb.getState() == CircuitBreaker.State.OPEN) {
cb.transitionToHalfOpenState();
}
}
lastCheckPassed = checkPassed;
}
/**
* GPU使用率监控(防止OOM)
*/
@Scheduled(fixedDelay = 60000) // 每分钟检查GPU状态
public void checkGpuUsage() {
try {
// 调用vLLM的metrics接口获取GPU使用率
// 实际实现:HTTP GET到 /metrics,解析Prometheus格式
double gpuMemUsage = getGpuMemoryUsage();
if (gpuMemUsage > 0.92) {
alertService.warn(String.format(
"GPU显存使用率%.1f%%,接近上限,可能影响服务稳定性",
gpuMemUsage * 100));
}
} catch (Exception e) {
log.debug("获取GPU状态失败: {}", e.getMessage());
}
}
private double getGpuMemoryUsage() {
// 实际实现:调用vLLM /metrics 接口解析 vllm:gpu_cache_usage_perc
return 0.0; // placeholder
}
}敏感数据的降级策略
一个经常被忽略的问题:降级到云API时,数据安全怎么保证?
/**
* 请求数据分级:决定是否允许发到云端
*/
@Component
public class DataSensitivityClassifier {
private static final List<Pattern> SENSITIVE_PATTERNS = List.of(
Pattern.compile("\\b\\d{6,18}\\b"), // 身份证号
Pattern.compile("\\b1[3-9]\\d{9}\\b"), // 手机号
Pattern.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"), // 邮箱
Pattern.compile("\\b(用户|客户|账号|密码|token|secret)\\b", Pattern.CASE_INSENSITIVE)
);
/**
* 检查请求中是否包含敏感数据
* 敏感数据不允许发到云端API
*/
public SensitivityLevel classify(LlmRequest request) {
String fullContent = request.getSystemPrompt() + " " + request.getUserMessage();
for (Pattern pattern : SENSITIVE_PATTERNS) {
if (pattern.matcher(fullContent).find()) {
return SensitivityLevel.HIGH;
}
}
// 可以扩展:调用专门的数据分类模型
return SensitivityLevel.LOW;
}
/**
* 脱敏处理(Low级别数据可以脱敏后发云端)
*/
public String anonymize(String text) {
String result = text;
// 替换手机号
result = result.replaceAll("\\b1[3-9]\\d{9}\\b", "1**********");
// 替换邮箱
result = result.replaceAll("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}", "***@***.com");
return result;
}
}高可用架构总结
把这些组件整合到一起,最终的架构是:
- Nginx:流量分发,两台GPU节点负载均衡
- Java熔断层:Resilience4j熔断,失败时自动降级
- 降级策略:降级时对数据做敏感性检查,高敏感不出内网
- 健康检查:主动探测服务状态,自动切回
- 告警:状态变化实时通知
这套架构的核心设计原则:私有服务优先,云API兜底,敏感数据不出内网。
GPU虽然贵,但比服务中断的代价便宜多了。
