我这边是把所有资源密集型的操作从核心服务里拆了出来,单独起了一个微服务。做这个决定基于两个考虑:一个是系统稳定性,一个是微服务定位。
从稳定性的视角看
核心服务承载的是系统最关键的业务流程,审批、数据查询、业务流转这些。一旦核心服务不可用,整个系统就瘫了。
把资源密集型操作放在核心服务里,风险集中在三个方面。
线程池被占满。 一个PDF解析任务跑两三分钟,Tomcat默认的工作线程数是200。看起来一个任务只占了一个线程,影响不大。问题在于这个线程被锁住了两三分钟,相当于正常请求的几千倍。5个这样的任务并发来,5个线程被占住几分钟,期间如果正常请求量大一点,剩余的195个线程可能不够用,业务接口开始排队。
内存被吃光。 用PDFBox解析一个100MB的PDF,在内存里要展开好几倍。几个并发一来,堆内存直接打满,频繁Full GC,接口响应变得不稳定。严重的情况下直接OOM,核心服务整个进程挂掉,所有业务全停。
CPU被打满。 文本提取、规则匹配、图片处理这些操作是CPU密集型的。跑起来CPU占用率拉高,核心服务的正常请求响应时间跟着上升。
核心服务挂了,影响的是所有业务。 不是说PDF解析失败了没关系,而是这个PDF解析操作可能让你的审批接口、查询接口、业务流转接口全部不可用。一个辅助性的功能,把核心业务拉下水了。
可能有人会想:用异步线程处理不就行了?异步线程解决不了这个问题。异步线程仍然跑在同一个JVM里,内存和CPU还是同一份资源。PDF解析吃掉的内存不会因为你放到异步线程就消失了。OOM的时候整个进程一起挂,核心业务照样全停。
放在网关里也不行。网关是所有流量的入口,比核心服务还脆弱。一个大文件上传把网关线程占住了,所有服务的入口流量都受影响。
拆到独立的资源微服务之后,即使这个服务OOM了,核心服务完全不受影响。最多是某个解析任务失败了,重试一次就行。核心业务的可用性有了保障。
从服务定位的视角看
微服务拆分有个定位问题:每个服务存在的理由是什么?
大多数团队按业务领域拆。订单服务、用户服务、财务服务,每个服务对应一块业务。这个思路没问题,但它只覆盖了一个维度。
还有另一个维度值得考虑:按资源消耗特征拆。
资源微服务的定位是收口所有重IO、重CPU、重内存的操作,跟具体业务无关。它的划分维度不是业务领域,而是资源消耗特征。
看我们项目里实际放在这个服务里的功能:
- 邮件拉取和附件解析。运营那边的需求,需要通过IMAP协议拉取企业邮箱里的邮件,解析PDF附件。IMAP是长连接,附件解析吃内存
- PDF文档识别。物业那边的需求,需要对业主账单做OCR识别。PDFBox解析吃CPU和内存,调大模型做识别还有网络等待
- 大文件上传到第三方系统。财务那边的需求,需要把几百MB的文件通过multipart形式传到对方系统。HTTP连接要保持30分钟
- 银行回单解析。也是财务的,PDF文本提取加规则匹配加按关键字分页打包,CPU和IO双密集
这些功能分属运营、物业、财务三个不同的业务领域。如果按业务领域拆,它们应该分别待在各自的业务服务里。但它们有一个共同特征:执行时间长、内存占用高、CPU占用高、HTTP连接占用时间长。
把它们按资源特征聚合到一个服务里,每个服务的职责边界就清晰了:核心服务只处理轻量级的业务逻辑,快进快出;资源微服务专门处理重资源操作,允许慢、允许占资源。
代码结构上大致是这样:
Java
// 邮件拉取
MailFetchHandler.fetchUnread()
// 银行回单解析:提取文本、按规则分页、打包成ZIP、上传
ReceiptSplitHandler.splitAndPackage()
// 文件上传到第三方系统
ResourceUploader.upload()
// PDF文档识别
DocRecognizeService.recognize()
// 账单OCR识别任务
BillOcrService.createTask()
这些方法各自的业务语义不同,但运行时的资源消耗特征是一样的:耗时长、吃资源多。

拆出来之后的配置差异
拆分之后有个直接的好处:两个服务可以用完全不同的配置,互不干扰。
资源微服务的JVM堆内存可以配到4G甚至8G,因为它要在内存里处理大文件。核心服务2G就够了,配太大反而GC停顿时间长。
Tomcat的超时配置也不一样。资源微服务需要支持500MB文件上传,HTTP连接超时得设到30分钟。核心服务的接口超时设在几秒就行,超过几秒的请求大概率已经有问题了。文件大小限制也是,资源微服务放开到500MB,核心服务保持默认的1MB。
这两套配置放在同一个服务里是矛盾的。你不可能给核心服务设30分钟的HTTP超时,那样一个慢请求会占住线程半小时。你也不可能给核心服务配8G堆内存来应对偶尔出现的大文件解析,平时都是浪费。
独立扩缩容也是一个实际的好处。月底银行回单集中处理的时候,只需要给资源微服务加机器或者加副本,核心服务不用动。
哪些操作应该放到资源微服务
给一个判断标准,遇到新需求的时候可以对照着看:
| 操作特征 | 典型场景 | 放核心服务的风险 | 建议 |
|---|---|---|---|
| 执行时间超过10秒 | 文件上传、大文件解析 | 占线程,拖慢核心接口 | 拆到资源服务 |
| 内存占用超过100MB | PDF解析、Excel大文件导入 | OOM风险,核心服务可能挂 | 拆到资源服务 |
| CPU密集型计算 | 图片处理、文本提取、规则批量匹配 | 核心接口响应变慢 | 拆到资源服务 |
| 长连接或长轮询 | IMAP邮件拉取、大模型API调用 | 占连接池资源 | 拆到资源服务 |
| 需要特殊超时配置 | 大文件上传(需要30分钟超时) | 和核心服务的短超时配置矛盾 | 拆到资源服务 |
| 偶发但不可控的负载 | 第三方回调触发的批量解析 | 突发负载冲击核心服务 | 拆到资源服务 |
| 毫秒级稳定执行 | CRUD、数据校验、状态流转 | 无风险 | 留在核心服务 |
小结
稳定性和定位,看起来是两个独立的理由,实际上指向同一个结论。微服务拆分不只有按业务领域拆这一条路,按资源消耗特征拆也是一个有效的维度。这跟之前聊过的出口入口网关微服务是同一个思路:网关收口所有第三方对接,资源微服务收口所有重资源操作。每个服务有明确的定位,系统就不容易乱。
这里多说一点。服务边界的划分、哪些操作放哪个服务里,这些决策如果你是团队的技术负责人或者架构师,是需要你来定的,不能任由团队成员自由发挥。如果没人管这件事,开发人员遇到一个新需求,最自然的做法就是往手头的业务服务里加代码。一个人加一点,一个人加一点,时间一长核心服务就变成了什么都干的大杂烩,稳定性风险在不知不觉中积累。等到某天出了故障才回头拆,成本比一开始就规划好要大得多。技术负责人应该在项目初期就把这个规矩定下来:资源密集型的操作统一走资源微服务,不往核心服务里放。规矩定得越早,后面越省心。
希望这篇内容可以帮到你。
最近在知乎出了秒杀专栏,感兴趣的可以订阅一下。至于知识星球的,可以搜:
- 老码头的技术浮生录
它是一个能实际帮你解决难题的星球。有问题的,找知心的Sam哥,支持无限次语音一对一解决你遇到的难题。另外后续我新写的所有对外的付费专栏,在星球内都是免费的,且可以拿到所有源代码。
我的知乎账号:
- SamDeepThinking