ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AWS EC2上部署ComPDF:从单机到可扩展的文档解析架构实践

AWS EC2上部署ComPDF:从单机到可扩展的文档解析架构实践 先说明一下背景我最近在公司内部主导做了一个文档解析中台底层核心就是 ComPDF部署在 AWS EC2 上支撑了多个业务线每天几万份 PDF、Word、图片的解析和转换需求。前期直接在一台大规格 EC2 上裸跑后面随着调用量上来踩了不少坑逐步改造成了可以横向扩展的服务架构。这篇博文就是把我从选型到落地、从单机到可扩展的完整过程写出来包括架构思路、部署步骤、自动扩缩容配置、监控告警以及几个比较隐蔽的坑。1. 为什么选 ComPDF EC2 这套组合1.1 先搞清楚 ComPDF 到底解决什么问题文档处理听起来是个通用需求但真正做起来远比想象的复杂。业务方给过来的文件千奇百怪有扫描件需要 OCR 识别、有 PDF 需要转 Word、有批量表格需要抽取成结构化数据、还有大量带水印或旋转角度的图片需要预处理。如果每个需求都自己调开源库去实现那工作量将是灾难级的尤其是 PDF 解析这种极其依赖排版细节的场景自己写的解析代码很容易在遇到复杂表格、公式、多栏布局时直接崩掉。ComPDF 的价值在于它把这一整套能力做成了标准化的接口和服务。你只需要把文件传上去指定要做的处理类型比如pdftoword、pdfocr、imageocr、pdfmerge它就能返回处理后的文件或者结构化结果。这大大降低了开发成本业务侧不需要关心底层算法是怎么实现的也不用去挨个适配不同来源、不同质量的 PDF 文件。但 ComPDF 本身只是一个服务组件真正让它稳定对外提供服务还需要一个可靠的运行环境。我选择 AWS EC2 而不是直接用 ComPDF 官方云服务核心考虑有三点一是数据安全很多文档涉及企业内部敏感信息不方便直接传到第三方 SaaS 平台二是成本控制当解析量达到一定规模后自建服务在成本上更有优势三是灵活调度自建环境下可以和内部其他服务的网络、存储、权限体系打通实现更紧密的集成。1.2 EC2 在部署这类服务时的优劣势EC2 是 AWS 最基础的 IaaS 服务本质上就是一台云上的虚拟机。相比直接用 ECS Fargate 或者 Kubernetes裸 EC2 部署 ComPDF 的好处是简单直接、排查问题容易服务挂了 SSH 上去就能看日志、改配置不需要在一堆容器编排概念里绕来绕去。缺点是运维成本稍高比如操作系统补丁、环境依赖、进程守护都需要自己处理。但从我的经验来看对于一个单机日处理量在几千到几万份文档的场景用 EC2 加 systemd 或 supervisor 管理进程完全够用而且可控性比容器化更高。还有一个关键因素——EC2 支持多种实例类型。文档处理是典型的 CPU 密集型任务尤其是 OCR 和 PDF 复杂排版解析CPU 核数直接决定处理速度。AWS 有 C 系列计算优化型实例比如 c6i、c7g也有通用型的 M 系列。我最终选的是 c6i.2xlarge 作为基准实例8 核 16GB 内存性价比很高。如果遇到突发高峰用 EC2 自带的能力做横向扩容非常方便这为后面的可扩展架构打下了基础。2. 架构设计可扩展性从哪几个维度入手2.1 有状态与无状态的拆分最开始部署 ComPDF 的时候我直接把它当成一个有状态服务来用结果发现很尴尬任务在执行中如果 EC2 重启正在处理的文件就会直接丢失而且无法把流量分摊到多台机器上。后来我重新设计了架构核心原则就是任务执行无状态化。具体做法是把接收请求和执行文档处理拆成两个层面。接收请求的节点不关心文档处理过程它只负责把任务丢进消息队列然后回调通知调用方。真正跑 ComPDF 的 worker 节点是无状态的任何一台 worker 挂掉消息队列里的任务都会被其他 worker 重新消费。EC2 的弹性伸缩正好能配合这个模型平时保持 2 台 worker 节点高峰期通过扩缩容组增加到 5 台甚至 10 台每台机器之间不需要同步状态也不存在数据迁移的问题极大地简化了扩容流程。2.2 数据存储的隔离文档处理服务绕不开文件存储。ComPDF 输出的结果文件是二进制文件不能直接放数据库里需要放在对象存储或共享文件系统上。在 AWS 生态里S3 是天然的选择。我把原始文档和输出文档都放在 S3 上EC2 本地只保留下载后的临时文件处理完立即上传、立即删除这个策略让整个系统的数据层变得非常干净。S3 还附带一个好处支持预签名 URL。调用方生成一个预签名 URL 上传原始文件后我只要在内部把文件的 S3 key 传给 workerworker 就能用最小权限的 IAM 角色直接读取文件不需要把文件内容通过 HTTP 传输中转减少了延迟和带宽消耗。2.3 消息队列做缓冲与削峰文档处理不像普通 API 请求那样秒级返回一份复杂的 PDF 转 Word 可能需要几秒到几十秒。如果直接把文档处理做成同步接口在文件高峰期EC2 的请求线程会被占满后续请求全部排队等待用户体验非常差。我引入了Amazon SQS简单队列服务做缓冲层。调用方提交任务后接口马上返回一个任务 ID实际处理过程在后台异步执行。SQS 的好处是无需自己维护消息中间件AWS 负责消息的高可用而且支持死信队列和消息延迟。任务处理完成后再通过 webhook 或轮询的方式通知调用方结果。这套架构的伸缩性体现在任务量增大时只需增加 worker EC2 的数量就能线性提升处理能力。消息队列本身是托管服务不用关心它能不能撑住。3. 部署 ComPDF 的完整实操过程3.1 从启动一台干净的 EC2 开始我倾向于用Ubuntu 22.04 LTS作为镜像因为它的生态最全网上能找到的所有依赖问题基本都是针对 Ubuntu 解决的遇到坑时搜索成本最低。首次启动 EC2 时有几个参数要特别注意否则后面会返工实例类型建议至少 c6i.2xlarge8 vCPU16GB 内存小规格实例跑 OCR 时会直接内存溢出。存储卷不要用默认的 8GB 通用型 SSD我建议至少 50GB因为 ComPDF 的 OCR 模型和字体文件体积不小加上系统依赖很容易超过默认空间。安全组只放开 22SSH和 8080ComPDF 服务端口其他端口一律不对外开放。IAM 角色这一步很多人会忽略但非常重要。给 EC2 分配一个 IAM 角色角色权限包含 S3 读写和 SQS 访问这样在机器上不需要配置任何 AccessKeyAWS SDK 会自动从实例元数据服务获取临时凭证。启动完机器后先用 SSH 登录然后做基础的系统更新sudo apt update sudo apt upgrade -y这一步建议执行虽然耗时几分钟但可以减少很多因为系统库太旧导致的编译或运行问题。3.2 安装 ComPDF 服务端ComPDF 的部署其实很直接它提供了 Linux 下的安装脚本。不同版本的安装方式可能略有区别但核心逻辑是先安装 Java 运行环境再部署 war 包或独立可执行 JAR。我这里用的是 Docker 方式原因是迁移和回滚时更干净不会把依赖留在宿主机里。先安装 Docker 和 Compose 插件sudo apt install -y docker.io docker-compose-v2 sudo systemctl enable docker sudo systemctl start docker然后从项目仓库拉取 ComPDF 的部署包检查里面是否包含 docker-compose.yml 或镜像文件。以我用的版本为例核心就是一个 Spring Boot 应用通过 8080 端口对外服务依赖一个 MySQL 数据库用来存任务和状态信息。为了简单我把 MySQL 也用 Docker 启动了但注意容器的数据卷要挂载到宿主机路径否则容器删掉后数据全丢。mkdir -p /opt/compdf cd /opt/compdf # 在这里放置 ComPDF 的 docker-compose.yml docker compose up -d mysql docker compose up -d compdf启动后用docker compose logs -f跟踪日志看到类似Started Application in xxx seconds的输出就说明服务已经起来了。这时先在本机用 curl 验证一下基础接口curl http://localhost:8080/api/health正常会返回一个包含状态码的 JSON。如果这一步没通过大概率是数据库连接配置有问题进去检查环境变量里数据库地址、用户名密码是否正确。3.3 配置 Nginx 反向代理ComPDF 服务本身监听 8080 端口我不打算把 8080 直接暴露出去而是用 Nginx 做一层反向代理统一处理 HTTPS 终止和访问日志。生产环境必须开启 HTTPS直接用 AWS 的Certificate ManagerACM申请免费证书绑定到你的域名上。Nginx 配置核心部分如下server { listen 443 ssl; server_name pdf-api.internal.example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; client_max_body_size 100m; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 300s; } }client_max_body_size这个参数很容易被忽略。文档处理场景下用户上传的 PDF 或图片经常有几十甚至上百兆Nginx 默认只允许 1MB 的请求体不调大这个参数文件传一半就会被 Nginx 直接拒绝而且报错信息是很笼统的 413排查起来很迷茫。proxy_read_timeout也要调大。ComPDF 对某些复杂文件处理超过 60 秒很正常默认 60 秒代理超时会中途断掉连接客户端收到的就是空响应。我直接设置成 300 秒给足时间。3.4 搭建 Auto Scaling 组可扩展架构的关键一步就是把单台 EC2 变成一个 Auto Scaling Group。做法是这样的先基于当前配置好的 EC2 创建AMIAmazon Machine Image然后用这个 AMI 定义启动模板最后创建 Auto Scaling Group。创建 AMI 的时机一定要等所有配置都完成后包括 Docker 镜像、Nginx 配置、环境变量、依赖库等。我一般会把 ComPDF 的 Docker 镜像先推到ECR上这样新的 worker 启动时只需要从 ECR 拉镜像即可AMI 本身小很多启动也更快。Auto Scaling Group 里关键配置是配置项设定值说明最小实例数2保证基础处理能力期望实例数2初始状态最大实例数10防止失控扩容扩容策略基于 SQS 队列深度队列中堆积消息越多扩容越激进缩容策略基于 CPU 使用率CPU 连续 15 分钟低于 20% 则缩容健康检查ELB 健康检查通过 HTTP/api/health检查用 SQS 队列深度做扩容指标比单纯 CPU 指标准确得多。因为文档处理任务是异步的可能在某个时刻 CPU 不高但队列里已经堆了一大堆任务等待处理。我用 CloudWatch 监控队列的ApproximateNumberOfMessagesVisible当这个值大于 100 时触发扩容每增加一台实例可以快速消化积压任务。3.5 创建负载均衡器有了多台 worker 之后需要把外部请求分散到它们上面。创建一个Application Load BalancerALB监听 443 端口请求转发到目标组。目标组成员就是 Auto Scaling Group 动态管理的实例。ALB 在这里不仅仅做流量分发还承担了一个重要角色——健康检查。我配置的健康检查路径为/api/health间隔 30 秒超时 5 秒不健康阈值 2 次。如果某台实例在两次检查后都没响应ALB 会自动把它从目标组摘除流量就不会再打到这台故障机器上。需要注意的是ComPDF 在处理任务时是长耗时的ALB 的默认空闲超时时间是 60 秒。如果超过了 60 秒哪怕 Nginx 没有断开ALB 这一层也已经把连接掐断了。所以要在 ALB 的属性里把空闲超时时间调大到 600 秒这是很多人在配置负载均衡时容易漏掉的一步。4. S3 对接与任务生命周期管理4.1 设计任务状态机一个文档处理任务从提交到完成我设计了以下几个状态PENDING已接收请求等待 worker 拉取PROCESSINGworker 正在处理SUCCEEDED处理成功结果文件已写入 S3FAILED处理失败记录错误信息每个状态都对应数据库表中的一行记录调用方通过任务 ID 查询状态。为了让调用方不需要频繁轮询我额外支持了 webhook 回调任务完成时自动向预设的 URL 发送 POST 请求携带结果文件地址。4.2 让 worker 访问 S3 文件之前的架构设计里提到用预签名 URL 让调用方直传 S3这里展开讲讲 worker 访问 S3 的完整链路。所有 EC2 实例都绑定了同一个 IAM 角色角色策略里有如下权限{ Version: 2012-10-17, Statement: [ { Effect: Allow, Action: [ s3:GetObject, s3:PutObject ], Resource: arn:aws:s3:::compdf-bucket/* } ] }有了这个策略worker 上的 Java SDK 不需要任何显式凭证就能直接读文件。实际处理步骤如下调用方提交任务时带一个sourceKey比如uploads/20250201/xxxx.pdf。worker 从 S3 把这个对象下载到本地临时目录。调用 ComPDF 的本地处理接口传入文件路径和处理参数。得到结果文件后上传到 S3 的outputs/前缀下。删除本地临时文件防止磁盘空间被耗尽。这个链路看起来简单但工程上有个细节值得注意S3 下载和上传的并发控制。如果多个 worker 同时处理大量任务每个任务都会占用带宽。我在 ComPDF 的配置里加了一个线程池限制最大并发处理数设为 CPU 核数的一半即 4 个并发避免同时下载太多文件导致带宽瓶颈也避免内存被多个大文件撑爆。4.3 生命周期配置S3 桶里日积月累会产生大量中间文件和结果文件如果不处理存储成本会慢慢爬升。我配置了 S3 生命周期规则临时文件tmp/前缀保留 1 天后自动删除输出结果文件outputs/前缀保留 30 天超过后自动归档到S3 Glacier再放 90 天删除。这样一来热数据始终控制在较小规模冷数据也不至于永久占据标准存储的费用。这个细节虽然和 EC2 部署本身关系不大但在一个完整的文档处理服务里存储成本往往比计算成本更容易被忽视。5. 需要特别留意的 6 个坑5.1 文件上传大小限制我在前面已经提到一次 Nginx 的client_max_body_size这里再重点说说全链路的限制。即使 Nginx 调大了ComPDF 自身的 Servlet 容器也有默认请求体限制Spring Boot 默认最大请求体就是 1MB实际取决于版本配置需要显式在配置文件中设置spring: servlet: multipart: max-file-size: 100MB max-request-size: 150MBALB 默认也限制 1MB 的请求体不过这个限制可以通过监听器规则调整。我实际遇到的情况是文件超过 10MB 时直接请求失败日志里同时出现 Nginx 413 和 Spring 报错顺着错误信息排查到max-file-size才解决。处理文档的服务一定要从客户端到 CDN 再到 Nginx 再到应用每一层都确认文件大小限制是放宽的。5.2 并发线程池与内存的平衡ComPDF 底层处理 PDF 时Java 堆内存占用非常高。我之前天真地把并发线程数调成 8结果跑了几十个任务后直接 OOMJVM 崩溃连基础状态查询接口都不可用。对比测试下来每个并发处理线程大约要占用 1-1.5GB 的堆内存。在 16GB 内存的实例上JVM 堆我设置为 8GB并发线程数设置为 4剩余的 8GB 留给操作系统和其他进程暂时够用。如果你的文件都是超大 PDF超过 100MB并发线程数建议再降到 2宁可稍微牺牲吞吐量也不要触发 OOM。5.3 时钟同步问题AWS 的 NTP 服务一般没问题但如果你在配置 Amazon Linux 或自建了时钟同步策略EC2 实例的时钟偏差会影响预签名 URL 的验证。有一次 S3 的上传一直报 403 签名不匹配排查了半天发现是实例时钟慢了两分钟预签名 URL 已过期。后来在 EC2 上统一装好chrony并确认同步正常问题再没出现过。5.4 SQS 消息可见性超时设置消息被 worker 拉取后并不会立刻从队列中消失而是进入不可见状态持续一段时间称为可见性超时。如果在这个超时时间内 worker 没有删除该消息这条消息会被重新投递到队列中被其他 worker 再次消费。我最初把可见性超时设置为 60 秒但实际处理一份大文件可能需要 2-3 分钟。结果就是同一个任务被反复消费生成多份重复结果而且白白浪费计算资源。建议把可见性超时设置为你处理任务的 P99 耗时的两倍比如 P99 是 150 秒就设置为 300 秒。宁可极端情况慢一点重新消费也不要让它频繁重试。5.5 镜像仓库拉取限流当 Auto Scaling 扩容后需要启动新实例所有新实例同时从 ECR 拉取镜像时遇到过 ECR 的限流导致部分实例启动失败。这个问题的规避方式是确保 AMI 里已经预置了所需的 Docker 镜像docker save打进去新实例启动后直接docker load本地镜像不需要现场拉取。或者用 ECR 的Pull Through Cache功能提前把镜像缓存到每个可用区的专属终端节点也能有效缓解。5.6 磁盘空间膨胀ComPDF 的 OCR 功能依赖大量语言模型数据处理过程中还会产生临时文件如果磁盘空间不足轻则任务失败重则整个服务失去响应。我在每台 EC2 上加了 CloudWatch 磁盘监控告警可用空间低于 20% 时触发报警。同时写了一个简单的定时清理脚本每 10 分钟扫描/tmp和 ComPDF 的临时目录删除超过 1 小时未访问的文件。这个看似不起眼的操作帮我避开了一次磁盘写满的严重事故。6. 扩展能力验证与效果数据6.1 压力测试看看系统到底能扛多少架构搭好后我做了两轮压测。第一轮是单 worker2 台实例跑 2000 份不同类型的文件包含 PDF 转 Word、OCR 识别、图片去阴影等任务。测试结果如下场景文件数平均耗时成功率PDF 转 Word文本型8003.2 秒/份99.8%PDF 转 Word扫描件60012.5 秒/份98.7%OCR 图片识别4008.9 秒/份99.2%复杂表格抽取20022.4 秒/份97.5%第二轮我模拟了高峰期一次性投递 10000 个任务到 SQSAuto Scaling 在 5 分钟内自动把 worker 从 2 台扩到了 6 台系统总吞吐量提升了约 2.6 倍任务积压量在 20 分钟后归零。整个过程没有人工干预完全由 CloudWatch 告警触发扩缩容策略完成。6.2 弹性伸缩的两次实战第一次实战是业务部门临时要做大批量历史合同归档一晚上需要处理 3 万份 PDF。我提前把 Auto Scaling 组的最大实例数临时调到 15并放宽了扩容阈值队列深度大于 50 就扩容。当天晚上系统稳定跑完平均每台实例处理约 2000 份文件单实例 CPU 稳定在 70%-85% 之间没有出现内存溢出。第二次实战是某个工作日突然来了一波营销活动上传的文件量比平峰高出 4 倍。我设置的自动扩容策略在 3 分钟内把实例数加到 8 台单份文件的处理时间并没有明显变长调用方基本无感知。次日凌晨流量回落后缩容策略自动把实例数降回 2 台整个过程只产生了合理的 EC2 费用。6.3 监控面板与告警CloudWatch 的告警配置我分成了三个维度服务可用性健康检查失败次数、5xx 错误率超过 1% 时告警任务积压SQS 队列中ApproximateNumberOfMessagesVisible超过 200 持续 5 分钟以上触发告警并扩容资源利用率CPU 使用率超过 85% 持续 10 分钟或者磁盘可用空间低于 20%通过 SNS 推送到钉钉群和邮件。其中 5xx 告警的阈值我特意调高了一点因为 2xx 成功率和 5xx 错误率在文档处理这种高耗时服务上会有一定波动阈值设太低反而容易产生大量误报让团队产生告警疲劳。实际运营中1% 这个阈值基本能精准捕获到真实故障同时不会频繁打扰。最后再分享几点个人经验部署 ComPDF 这套服务的整个过程让我对可扩展这三个字有了更具体的理解。可扩展不是说你用了 AWS 的某几个托管服务就自动获得了伸缩能力而是从任务的拆分、存储的隔离、消息队列的引入到扩缩容策略的配置每一步都在为无状态和可水平扩展服务。如果一开始就想直接上 K8s复杂度会指数级上升反而不利于聚焦在业务本身。如果你现在刚准备在 EC2 上跑 ComPDF我的建议是先在单机上把整个流程跑通包括上传、处理、结果返回确认没有内存和超时问题之后再逐步引入 SQS、S3 和 Auto Scaling。不要一上来就分布式否则出问题时定位会非常痛苦。最后再分享一个小技巧扩容策略不要只看 CPU 或者内存对于异步任务处理型的服务队列深度才是更直接的压力信号。把扩容触发的阈值设置成一个让你感到再不加机器就要来不及处理的值而不是等到任务已经堆积很多了才反应。这样既能保证服务体验也不会频繁扩缩容产生不必要的成本。
返回列表