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%以上,表格和公式都还原得很好。下面这张图展示了处理前后的对比:

OCR处理效果对比

左边是原始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设计要兼顾易用性和扩展性。我设计了三个核心接口:

  1. 文件上传接口:接收用户上传的文档
  2. OCR处理接口:触发OCR识别
  3. 结果查询接口:获取处理结果

先看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);
    }
}

这里有几个设计考虑:

  1. 异步处理:OCR识别比较耗时,所以采用"提交任务-查询结果"的异步模式,避免HTTP连接超时。
  2. 文件分离:先上传文件到存储服务,返回文件ID,后续用ID操作,避免重复传输大文件。
  3. 批量支持:企业场景经常需要批量处理文档,单独设计批量接口。

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);
        }
    }
    
    // 其他方法省略...
}

这里的关键点:

  1. 异步处理:用TaskExecutor提交任务,避免阻塞HTTP线程。
  2. 策略模式:不同的文件类型(图片、PDF)用不同的处理器,方便扩展。
  3. 状态管理:完整记录任务生命周期,方便监控和排查问题。
  4. 异常处理:捕获所有异常,更新任务状态,不会因为单个任务失败影响系统。

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);
                    }
                }
            }
        }
    }
}

这个客户端做了几件重要的事:

  1. 超时控制:连接超时5秒,读取超时30秒(OCR处理需要时间)。
  2. 自动重试:网络波动或服务暂时不可用时,自动重试3次。
  3. 错误处理:统一异常类型,方便上层处理。
  4. 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服务暂时不可用");
    }
}

熔断器的工作逻辑:

  1. 连续失败率达到阈值(比如50%)时,进入熔断状态,直接拒绝请求。
  2. 熔断一段时间(比如30秒)后,进入半开状态,尝试放行少量请求。
  3. 如果半开状态的请求成功,恢复闭合状态;如果失败,继续保持熔断。

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

关键监控指标:

  1. 服务健康:每个实例是否正常
  2. 请求量:QPS、响应时间、错误率
  3. 资源使用:CPU、内存、GPU显存
  4. 熔断器状态:打开、关闭、半开
  5. 任务队列:积压任务数

日志用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微服务平台,效果很明显:

处理流程

  1. 客户在APP上传材料照片
  2. 前端调用OCR服务的上传接口
  3. 系统异步处理,提取关键信息(姓名、身份证号、金额等)
  4. 结果自动填入申请表单,人工只需核对

性能数据(部署后一个月统计):

  • 日均处理文档:15,000份
  • 平均处理时间:3.2秒/页
  • 识别准确率:98.7%(身份证、银行卡等标准文档)
  • 系统可用性:99.95%
  • 成本对比:比原来的人工录入降低85%

技术亮点

  1. 弹性伸缩:白天业务高峰时自动扩容到5个实例,晚上缩容到2个。
  2. 故障隔离:某个模型实例GPU内存溢出,熔断器立即隔离,不影响其他请求。
  3. 灰度发布:新模型版本先部署到1个实例,验证无误后再全量更新。

客户反馈最满意的是稳定性成本。原来用第三方服务,高峰期经常排队,现在自己掌控,随时可以扩容;原来每月OCR服务费好几万,现在主要是电费,一个月不到一千。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

更多推荐