LightOnOCR-2-1B与SpringBoot微服务集成实战
LightOnOCR-2-1B与SpringBoot微服务集成实战
最近在帮一个客户做文档数字化平台,他们每天要处理几千份PDF和扫描件,原来的方案是调用第三方OCR服务,成本高不说,延迟还大,经常卡顿。客户问我有没有办法自己搭一套,既能控制成本,又能保证稳定。
我调研了一圈,发现LightOnOCR-2-1B这个模型挺有意思的。它只有10亿参数,但在OCR任务上的表现比很多大模型还好,关键是速度快、成本低。不过光有模型还不够,得把它包装成企业能用的服务才行。
这篇文章我就来分享一下,怎么把LightOnOCR-2-1B集成到SpringBoot微服务架构里,搭建一个可扩展的文档处理平台。我会从最基础的API设计开始,一步步讲到服务发现、负载均衡这些企业级功能,让你看完就能动手搭建自己的OCR服务。
1. 为什么选择LightOnOCR-2-1B?
在开始写代码之前,咱们先聊聊为什么选这个模型。你可能听说过PaddleOCR、Tesseract这些老牌OCR工具,它们确实不错,但各有各的问题。
PaddleOCR虽然轻量,但在复杂文档(比如学术论文、带公式的PDF)上表现一般。Tesseract开源早,但多语言支持需要单独训练,而且对布局复杂的文档处理起来比较吃力。
LightOnOCR-2-1B有几个特点特别适合企业场景:
第一是端到端处理。传统OCR都是流水线式的:先检测文字区域,再识别文字,最后还要做后处理。LightOnOCR-2-1B一步到位,输入图片直接输出结构化文本,省去了中间环节,出错概率小。
第二是速度快。根据官方数据,在单张H100显卡上,它能达到5.71页/秒的处理速度。这是什么概念呢?处理1000页文档,电费加算力成本不到1分钱。对于每天要处理大量文档的企业来说,这个成本优势太明显了。
第三是支持结构化输出。它不只是提取文字,还能保留文档的层级结构,比如标题、段落、列表、表格,甚至能把数学公式转成LaTeX代码。这对于后续的文档分析、知识库构建特别有用。
第四是模型小。1B参数意味着对硬件要求不高,16GB显存的显卡就能跑起来,部署成本低。
我实际测试过,拿一份双栏排版的学术论文PDF给它处理,识别准确率在95%以上,表格和公式都还原得很好。下面这张图展示了处理前后的对比:

左边是原始PDF截图,右边是LightOnOCR-2-1B提取后的Markdown文本。可以看到,它不仅识别了文字,还保留了章节结构、表格格式,数学公式也转换成了规范的LaTeX代码。
2. 整体架构设计
咱们要搭建的不是一个简单的OCR工具,而是一个企业级的文档处理平台。这意味着它要能应对高并发、要稳定可靠、要容易扩展。
我设计的架构是这样的:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 客户端应用 │───▶│ API网关 │───▶│ OCR服务集群 │
│ (Web/移动端) │ │ (Spring Cloud │ │ (多实例部署) │
└─────────────────┘ │ Gateway) │ └─────────────────┘
└─────────────────┘ │
│ │
┌─────────────────┐ ┌─────────────────┐
│ 服务注册中心 │◀───│ 模型推理服务 │
│ (Nacos/Eureka) │ │ (vLLM + GPU) │
└─────────────────┘ └─────────────────┘
│ │
┌─────────────────┐ ┌─────────────────┘
│ 配置中心 │ │
│ (动态配置) │ │
└─────────────────┘ │
│
┌─────────────────┐
│ 存储服务 │
│ (MinIO/OSS) │
└─────────────────┘
我来解释一下各个组件的作用:
API网关:所有请求都先到这里,它负责路由、限流、鉴权。比如客户端上传一个PDF,网关收到后转发给OCR服务。
OCR服务集群:这是我们的业务逻辑层,用SpringBoot实现。它不直接跑模型,而是调用后端的模型推理服务。我们可以部署多个实例,通过负载均衡分摊压力。
模型推理服务:这才是真正跑LightOnOCR-2-1B模型的地方。我们用vLLM框架部署,它专门为大规模语言模型推理优化,支持并发请求、动态批处理,能充分利用GPU。
服务注册中心:微服务之间要能互相发现。OCR服务需要知道模型推理服务在哪,模型推理服务上线或下线时要通知OCR服务。
配置中心:所有服务的配置集中管理,比如模型参数、超时时间、重试策略,改配置不用重启服务。
存储服务:用户上传的文件要存起来,处理结果也要存。我用MinIO搭建对象存储,它兼容S3协议,用起来简单。
这个架构的好处是解耦和可扩展。OCR服务和模型推理服务分开,模型升级不影响业务逻辑;服务可以水平扩展,流量大了就加机器;各个组件职责清晰,出了问题好排查。
3. 环境准备与基础搭建
3.1 模型推理服务部署
咱们先从最底层的模型推理开始。LightOnOCR-2-1B支持用vLLM部署,这是目前效率比较高的方式。
首先准备一台带GPU的服务器,显存至少16GB。我用的是一台RTX 4090,24GB显存,跑这个模型绰绰有余。
用Docker部署最简单,创建一个docker-compose.yml文件:
version: '3.8'
services:
vllm-ocr:
image: vllm/vllm-openai:latest
container_name: lighton-ocr-vllm
command: >
--model lightonai/LightOnOCR-2-1B
--trust-remote-code
--port 8000
--max-num-seqs 16
--gpu-memory-utilization 0.8
--served-model-name lighton-ocr
--limit-mm-per-prompt '{"image": 1}'
environment:
- VLLM_ATTENTION_BACKEND=FLASH_ATTN
volumes:
- ~/.cache/huggingface:/root/.cache/huggingface
ports:
- "8000:8000"
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
restart: unless-stopped
几个关键参数解释一下:
--model lightonai/LightOnOCR-2-1B:指定要加载的模型--max-num-seqs 16:最大并发序列数,根据GPU显存调整--gpu-memory-utilization 0.8:GPU内存使用率,0.8表示用80%的显存--limit-mm-per-prompt '{"image": 1}':限制每个请求最多1张图片
启动服务:
docker-compose up -d
等几分钟,模型加载完成后,访问 http://服务器IP:8000/docs 能看到OpenAI兼容的API文档。这意味着我们可以用和调用ChatGPT一样的方式调用这个OCR服务。
3.2 SpringBoot项目初始化
接下来创建我们的OCR微服务。我用的是SpringBoot 3.x和Java 17。
# 用Spring Initializr创建项目
curl https://start.spring.io/starter.zip \
-d type=maven-project \
-d language=java \
-d bootVersion=3.2.0 \
-d baseDir=ocr-service \
-d groupId=com.example \
-d artifactId=ocr-service \
-d name=ocr-service \
-d description=OCR微服务 \
-d packageName=com.example.ocr \
-d packaging=jar \
-d javaVersion=17 \
-d dependencies=web,validation,data-jpa,cloud-starter-gateway,cloud-starter-loadbalancer \
-o ocr-service.zip
unzip ocr-service.zip
cd ocr-service
主要依赖:
spring-boot-starter-web:Web框架spring-boot-starter-validation:参数校验spring-cloud-starter-gateway:API网关spring-cloud-starter-loadbalancer:负载均衡
项目结构大概长这样:
ocr-service/
├── src/
│ ├── main/
│ │ ├── java/com/example/ocr/
│ │ │ ├── controller/ # 控制器
│ │ │ ├── service/ # 业务逻辑
│ │ │ ├── client/ # 服务调用客户端
│ │ │ ├── config/ # 配置类
│ │ │ └── OcrServiceApplication.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── ...
│ └── test/
└── pom.xml
4. RESTful API设计与实现
4.1 定义API接口
一个好的API设计要兼顾易用性和扩展性。我设计了三个核心接口:
- 文件上传接口:接收用户上传的文档
- OCR处理接口:触发OCR识别
- 结果查询接口:获取处理结果
先看DTO(数据传输对象)定义:
// OCR请求DTO
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OcrRequest {
@NotBlank(message = "文件ID不能为空")
private String fileId;
@NotNull(message = "处理选项不能为空")
private ProcessOptions options;
// 处理选项
@Data
@Builder
public static class ProcessOptions {
private Boolean extractTables = true; // 是否提取表格
private Boolean extractFormulas = true; // 是否提取公式
private OutputFormat outputFormat = OutputFormat.MARKDOWN; // 输出格式
public enum OutputFormat {
MARKDOWN, // Markdown格式
PLAIN_TEXT, // 纯文本
JSON // 结构化JSON
}
}
}
// OCR响应DTO
@Data
@Builder
public class OcrResponse {
private String taskId; // 任务ID
private String status; // 状态:PENDING, PROCESSING, COMPLETED, FAILED
private String result; // 识别结果(JSON或文本)
private String errorMessage; // 错误信息
private LocalDateTime createdAt; // 创建时间
private LocalDateTime completedAt; // 完成时间
}
// 文件上传响应
@Data
@Builder
public class FileUploadResponse {
private String fileId; // 文件唯一ID
private String originalName; // 原始文件名
private String fileUrl; // 文件访问URL
private Long fileSize; // 文件大小
private String contentType; // 文件类型
}
4.2 控制器实现
控制器层负责接收HTTP请求,调用服务层处理,返回响应。
@RestController
@RequestMapping("/api/v1/ocr")
@Slf4j
@Validated
public class OcrController {
@Autowired
private FileService fileService;
@Autowired
private OcrService ocrService;
// 文件上传
@PostMapping("/upload")
public ResponseEntity<FileUploadResponse> uploadFile(
@RequestParam("file") MultipartFile file) {
log.info("收到文件上传请求,文件名: {}", file.getOriginalFilename());
FileUploadResponse response = fileService.uploadFile(file);
return ResponseEntity.ok(response);
}
// 提交OCR任务
@PostMapping("/process")
public ResponseEntity<OcrResponse> processDocument(
@Valid @RequestBody OcrRequest request) {
log.info("收到OCR处理请求,文件ID: {}", request.getFileId());
OcrResponse response = ocrService.submitOcrTask(request);
return ResponseEntity.accepted().body(response);
}
// 查询任务状态
@GetMapping("/tasks/{taskId}")
public ResponseEntity<OcrResponse> getTaskStatus(
@PathVariable String taskId) {
OcrResponse response = ocrService.getTaskStatus(taskId);
return ResponseEntity.ok(response);
}
// 批量处理(企业场景常用)
@PostMapping("/batch-process")
public ResponseEntity<BatchOcrResponse> batchProcess(
@Valid @RequestBody BatchOcrRequest request) {
log.info("收到批量处理请求,文件数量: {}", request.getFileIds().size());
BatchOcrResponse response = ocrService.batchProcess(request);
return ResponseEntity.accepted().body(response);
}
}
这里有几个设计考虑:
- 异步处理:OCR识别比较耗时,所以采用"提交任务-查询结果"的异步模式,避免HTTP连接超时。
- 文件分离:先上传文件到存储服务,返回文件ID,后续用ID操作,避免重复传输大文件。
- 批量支持:企业场景经常需要批量处理文档,单独设计批量接口。
4.3 服务层实现
服务层是业务逻辑的核心。我采用策略模式,把不同的处理逻辑封装起来。
@Service
@Slf4j
public class OcrServiceImpl implements OcrService {
@Autowired
private TaskRepository taskRepository;
@Autowired
private FileService fileService;
@Autowired
private ModelClient modelClient;
@Autowired
private TaskExecutor taskExecutor;
@Override
public OcrResponse submitOcrTask(OcrRequest request) {
// 1. 验证文件是否存在
if (!fileService.fileExists(request.getFileId())) {
throw new BusinessException("文件不存在: " + request.getFileId());
}
// 2. 创建任务记录
OcrTask task = OcrTask.builder()
.taskId(UUID.randomUUID().toString())
.fileId(request.getFileId())
.status(TaskStatus.PENDING)
.options(request.getOptions())
.createdAt(LocalDateTime.now())
.build();
taskRepository.save(task);
log.info("创建OCR任务: {}", task.getTaskId());
// 3. 提交到线程池异步处理
taskExecutor.execute(() -> processTask(task));
// 4. 立即返回任务信息
return OcrResponse.builder()
.taskId(task.getTaskId())
.status(task.getStatus().name())
.createdAt(task.getCreatedAt())
.build();
}
private void processTask(OcrTask task) {
try {
log.info("开始处理任务: {}", task.getTaskId());
task.setStatus(TaskStatus.PROCESSING);
taskRepository.save(task);
// 1. 下载文件
byte[] fileContent = fileService.downloadFile(task.getFileId());
// 2. 根据文件类型选择处理器
OcrProcessor processor = getProcessor(fileContent);
// 3. 调用模型服务
String result = processor.process(fileContent, task.getOptions());
// 4. 保存结果
task.setResult(result);
task.setStatus(TaskStatus.COMPLETED);
task.setCompletedAt(LocalDateTime.now());
taskRepository.save(task);
log.info("任务处理完成: {}", task.getTaskId());
} catch (Exception e) {
log.error("处理任务失败: {}", task.getTaskId(), e);
task.setStatus(TaskStatus.FAILED);
task.setErrorMessage(e.getMessage());
taskRepository.save(task);
}
}
private OcrProcessor getProcessor(byte[] fileContent) {
// 根据文件内容判断类型
String mimeType = detectMimeType(fileContent);
if (mimeType.startsWith("image/")) {
return new ImageOcrProcessor(modelClient);
} else if (mimeType.equals("application/pdf")) {
return new PdfOcrProcessor(modelClient);
} else {
throw new BusinessException("不支持的文件类型: " + mimeType);
}
}
// 其他方法省略...
}
这里的关键点:
- 异步处理:用
TaskExecutor提交任务,避免阻塞HTTP线程。 - 策略模式:不同的文件类型(图片、PDF)用不同的处理器,方便扩展。
- 状态管理:完整记录任务生命周期,方便监控和排查问题。
- 异常处理:捕获所有异常,更新任务状态,不会因为单个任务失败影响系统。
5. 模型服务客户端
OCR服务需要调用后端的模型推理服务。我封装了一个客户端,处理连接、重试、熔断等逻辑。
@Component
@Slf4j
public class ModelClient {
private final RestTemplate restTemplate;
private final String modelEndpoint;
public ModelClient(@Value("${model.service.endpoint}") String endpoint) {
this.modelEndpoint = endpoint;
// 配置带超时和重试的RestTemplate
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(5000); // 5秒连接超时
factory.setReadTimeout(30000); // 30秒读取超时(OCR可能较慢)
this.restTemplate = new RestTemplate(factory);
// 添加重试拦截器
this.restTemplate.setInterceptors(Collections.singletonList(
new RetryInterceptor(3, 1000) // 重试3次,间隔1秒
));
}
public String recognizeImage(byte[] imageData, OcrRequest.ProcessOptions options) {
try {
// 1. 将图片转为Base64
String base64Image = Base64.getEncoder().encodeToString(imageData);
// 2. 构建OpenAI兼容的请求
Map<String, Object> request = new HashMap<>();
request.put("model", "lighton-ocr");
request.put("messages", Collections.singletonList(
Map.of("role", "user",
"content", Collections.singletonList(
Map.of("type", "image_url",
"image_url", Map.of("url",
"data:image/jpeg;base64," + base64Image))
))
));
// 3. 设置生成参数
Map<String, Object> generationParams = new HashMap<>();
generationParams.put("max_tokens", 4096);
generationParams.put("temperature", 0.2); // 低温度,输出更稳定
generationParams.put("top_p", 0.9);
request.put("generation_config", generationParams);
// 4. 发送请求
String url = modelEndpoint + "/v1/chat/completions";
log.debug("调用模型服务: {}", url);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);
// 5. 解析响应
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
Map<String, Object> body = response.getBody();
List<Map<String, Object>> choices = (List<Map<String, Object>>) body.get("choices");
if (!choices.isEmpty()) {
Map<String, Object> message = (Map<String, Object>) choices.get(0).get("message");
return (String) message.get("content");
}
}
throw new ModelServiceException("模型服务返回异常: " + response.getStatusCode());
} catch (Exception e) {
log.error("调用模型服务失败", e);
throw new ModelServiceException("模型服务调用失败: " + e.getMessage(), e);
}
}
// PDF处理(需要先转图片)
public String recognizePdf(byte[] pdfData, OcrRequest.ProcessOptions options) {
try {
// 用PDFBox或PyPDFium2将PDF转为图片
// 这里简化处理,实际需要调用Python服务或本地库
List<byte[]> pageImages = convertPdfToImages(pdfData);
StringBuilder result = new StringBuilder();
for (int i = 0; i < pageImages.size(); i++) {
log.info("处理PDF第 {} 页,共 {} 页", i + 1, pageImages.size());
String pageResult = recognizeImage(pageImages.get(i), options);
result.append("## 第 ").append(i + 1).append(" 页\n\n");
result.append(pageResult).append("\n\n");
// 每处理一页稍微休息一下,避免GPU过热
if (i < pageImages.size() - 1) {
Thread.sleep(100);
}
}
return result.toString();
} catch (Exception e) {
log.error("PDF处理失败", e);
throw new ModelServiceException("PDF处理失败: " + e.getMessage(), e);
}
}
// 重试拦截器
private static class RetryInterceptor implements ClientHttpRequestInterceptor {
private final int maxRetries;
private final long retryInterval;
public RetryInterceptor(int maxRetries, long retryInterval) {
this.maxRetries = maxRetries;
this.retryInterval = retryInterval;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
int retryCount = 0;
while (true) {
try {
return execution.execute(request, body);
} catch (IOException e) {
if (retryCount >= maxRetries) {
throw e;
}
log.warn("请求失败,第 {} 次重试: {}", retryCount + 1, e.getMessage());
retryCount++;
try {
Thread.sleep(retryInterval);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new IOException("重试被中断", ie);
}
}
}
}
}
}
这个客户端做了几件重要的事:
- 超时控制:连接超时5秒,读取超时30秒(OCR处理需要时间)。
- 自动重试:网络波动或服务暂时不可用时,自动重试3次。
- 错误处理:统一异常类型,方便上层处理。
- PDF支持:把PDF转成图片再处理,虽然多了一步,但兼容性好。
6. 微服务治理
6.1 服务注册与发现
单实例服务不够可靠,我们需要部署多个OCR服务实例,这时候就需要服务发现。
我用Nacos作为注册中心,配置很简单:
# application.yml
spring:
application:
name: ocr-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
namespace: public
group: DEFAULT_GROUP
loadbalancer:
nacos:
enabled: true
在启动类加上注解:
@SpringBootApplication
@EnableDiscoveryClient // 启用服务发现
public class OcrServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OcrServiceApplication.class, args);
}
}
模型服务那边也要注册:
# 模型服务的docker-compose添加Nacos客户端
model-service:
image: custom/model-service
environment:
- NACOS_SERVER_ADDR=nacos:8848
- SERVICE_NAME=model-service
- SERVICE_PORT=8000
这样,OCR服务要调用模型服务时,不需要写死IP地址,只需要写服务名:
@LoadBalanced // 启用负载均衡
@Bean
public RestTemplate loadBalancedRestTemplate() {
return new RestTemplate();
}
// 调用时用服务名
String url = "http://model-service/v1/chat/completions";
6.2 负载均衡策略
多个模型服务实例之间如何分配请求?Spring Cloud LoadBalancer提供了几种策略:
@Configuration
public class LoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> modelServiceLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RoundRobinLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name
);
}
}
我选择的是轮询策略,每个实例依次处理请求。如果某个实例性能更好,可以配置权重:
model-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule
6.3 熔断与降级
模型服务可能因为GPU内存不足、模型加载失败等原因暂时不可用,这时候不能让它拖垮整个系统。
我用Resilience4j实现熔断:
@Service
public class ModelServiceWithCircuitBreaker {
private final CircuitBreaker circuitBreaker;
private final ModelClient modelClient;
public ModelServiceWithCircuitBreaker(ModelClient modelClient) {
this.modelClient = modelClient;
// 配置熔断器
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 失败率阈值50%
.waitDurationInOpenState(Duration.ofSeconds(30)) // 熔断30秒
.slidingWindowSize(10) // 统计最近10次调用
.minimumNumberOfCalls(5) // 至少5次调用才开始统计
.build();
this.circuitBreaker = CircuitBreaker.of("modelService", config);
}
public String recognizeImageWithCircuitBreaker(byte[] imageData,
OcrRequest.ProcessOptions options) {
return circuitBreaker.executeSupplier(() ->
modelClient.recognizeImage(imageData, options)
);
}
// 降级方法
public String fallbackRecognizeImage(byte[] imageData,
OcrRequest.ProcessOptions options,
Exception e) {
log.warn("模型服务熔断,使用降级方案", e);
// 降级方案1:返回提示信息
if (options.isAllowFallback()) {
return "【系统提示】OCR服务暂时不可用,请稍后重试";
}
// 降级方案2:调用备用OCR服务
// return backupOcrService.recognize(imageData);
throw new ServiceUnavailableException("OCR服务暂时不可用");
}
}
熔断器的工作逻辑:
- 连续失败率达到阈值(比如50%)时,进入熔断状态,直接拒绝请求。
- 熔断一段时间(比如30秒)后,进入半开状态,尝试放行少量请求。
- 如果半开状态的请求成功,恢复闭合状态;如果失败,继续保持熔断。
6.4 配置中心
微服务多了,配置管理就成了问题。我把所有配置放到Nacos配置中心:
# nacos配置 dataId: ocr-service-dev.yml
model:
service:
endpoint: http://model-service:8000
timeout: 30000
max-retries: 3
ocr:
processor:
pdf:
dpi: 150
max-pages: 100
image:
max-size-mb: 10
supported-formats: jpg,png,jpeg,bmp
task:
executor:
core-pool-size: 10
max-pool-size: 50
queue-capacity: 1000
在代码中动态获取配置:
@RefreshScope // 配置更新时自动刷新
@Component
@Data
public class OcrConfig {
@Value("${model.service.endpoint}")
private String modelEndpoint;
@Value("${model.service.timeout:30000}")
private int modelTimeout;
@Value("${ocr.processor.pdf.dpi:150}")
private int pdfDpi;
}
这样修改配置后,不需要重启服务,配置自动生效。
7. 部署与监控
7.1 Docker容器化部署
每个服务都打包成Docker镜像,用Docker Compose或Kubernetes部署。
# Dockerfile for ocr-service
FROM openjdk:17-jdk-slim
WORKDIR /app
# 复制JAR文件
COPY target/ocr-service-*.jar app.jar
# 设置JVM参数
ENV JAVA_OPTS="-Xmx1g -Xms512m -XX:+UseG1GC"
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
Docker Compose编排:
version: '3.8'
services:
nacos:
image: nacos/nacos-server:latest
ports:
- "8848:8848"
environment:
- MODE=standalone
ocr-service:
build: ./ocr-service
ports:
- "8080:8080"
environment:
- SPRING_CLOUD_NACOS_SERVER_ADDR=nacos:8848
depends_on:
- nacos
deploy:
replicas: 3 # 启动3个实例
model-service:
build: ./model-service
ports:
- "8000:8000"
environment:
- NACOS_SERVER_ADDR=nacos:8848
deploy:
replicas: 2 # 启动2个实例
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
7.2 监控与日志
微服务架构下,监控特别重要。我用Spring Boot Actuator暴露健康指标:
management:
endpoints:
web:
exposure:
include: health,metrics,info,prometheus
metrics:
export:
prometheus:
enabled: true
health:
circuitbreakers:
enabled: true
关键监控指标:
- 服务健康:每个实例是否正常
- 请求量:QPS、响应时间、错误率
- 资源使用:CPU、内存、GPU显存
- 熔断器状态:打开、关闭、半开
- 任务队列:积压任务数
日志用ELK收集:
@Slf4j
@Service
public class OcrService {
public OcrResponse submitOcrTask(OcrRequest request) {
MDC.put("taskId", UUID.randomUUID().toString());
MDC.put("fileId", request.getFileId());
log.info("收到OCR处理请求,选项: {}", request.getOptions());
try {
// 处理逻辑...
log.info("任务提交成功");
return response;
} finally {
MDC.clear();
}
}
}
这样在日志中就能看到完整的请求链路,方便排查问题。
7.3 性能优化建议
在实际部署中,我总结了几点优化经验:
GPU利用率优化:
# vLLM配置优化
command: >
--model lightonai/LightOnOCR-2-1B
--gpu-memory-utilization 0.9 # 提高到90%
--max-num-batched-tokens 4096 # 增加批处理token数
--pipeline-parallel-size 1
--tensor-parallel-size 1
--block-size 16
SpringBoot优化:
server:
tomcat:
max-threads: 200 # 增加线程数
max-connections: 10000
spring:
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
task:
execution:
pool:
core-size: 20
max-size: 100
queue-capacity: 1000
数据库优化:
// 使用连接池
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
// 批量插入任务结果
@Transactional
public void batchSaveResults(List<OcrResult> results) {
for (int i = 0; i < results.size(); i += BATCH_SIZE) {
List<OcrResult> batch = results.subList(i,
Math.min(i + BATCH_SIZE, results.size()));
resultRepository.saveAll(batch);
resultRepository.flush();
}
}
8. 实际应用案例
最后分享一个真实的应用案例。某金融公司需要处理大量的贷款申请材料(身份证、银行流水、收入证明等),原来靠人工录入,效率低、错误率高。
我们帮他们部署了这套OCR微服务平台,效果很明显:
处理流程:
- 客户在APP上传材料照片
- 前端调用OCR服务的上传接口
- 系统异步处理,提取关键信息(姓名、身份证号、金额等)
- 结果自动填入申请表单,人工只需核对
性能数据(部署后一个月统计):
- 日均处理文档:15,000份
- 平均处理时间:3.2秒/页
- 识别准确率:98.7%(身份证、银行卡等标准文档)
- 系统可用性:99.95%
- 成本对比:比原来的人工录入降低85%
技术亮点:
- 弹性伸缩:白天业务高峰时自动扩容到5个实例,晚上缩容到2个。
- 故障隔离:某个模型实例GPU内存溢出,熔断器立即隔离,不影响其他请求。
- 灰度发布:新模型版本先部署到1个实例,验证无误后再全量更新。
客户反馈最满意的是稳定性和成本。原来用第三方服务,高峰期经常排队,现在自己掌控,随时可以扩容;原来每月OCR服务费好几万,现在主要是电费,一个月不到一千。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
更多推荐



所有评论(0)