在金融交易日结束后,券商必须在监管规定范围内将合同单(contract note)发送到每一位当天有交易的用户手中。这种文件不仅包含精确的成交明细,还需要进行数字签名与加密,且往往呈现复杂的排版需求。面对每日超过百万次交易量的现实,如何在短时间内高质量完成PDF生成、签名与邮件投递,成为可用性与合规性双重挑战。 传统做法通常由单台大机器串行完成从数据处理到HTML渲染、Chrome/Puppeteer生成PDF、再到签名和邮件发送的全流程。随着交易量增长,单机与垂直扩展的瓶颈愈发明显。Chromium渲染消耗大量CPU与内存,长期运行导致成本与耗时急剧上升,邮件发送系统也常常成为吞吐瓶颈。
面对这些挑战,根本性的架构重构显得必不可少。 架构转型的核心思想在于横向扩展与把复杂流程拆分为大量相互独立的任务单元。把数据处理、PDF生成、数字签名、邮件投递等工作视为若干相互串联但可以独立调度的工序,借助高并发的工作节点并行处理,从而在最短时间内把所有任务"烧尽"。为了实现这一目标,需要几项关键技术配合:高效的并发语言与队列、轻量的作业调度与编排、快速稳定的PDF渲染引擎、可承载高并发短时负载的临时计算实例、以及能够承受海量小文件读写的存储策略。 在编程语言层面,将原先的Python工作程序重写为Go带来了显著的性能提升。Go在并发模型、运行时资源占用和编译生成的单体二进制上具有天然优势,适合把数以百万计的独立作业以最低的开销分发到成百上千的CPU核上执行。
为了解决任务分发与链式任务管理,设计并实现了轻量级的Go消息与作业管理库,使得某个用户的合同单数据可以在若干工序之间以"任务链"的方式可靠传递与追踪。 PDF生成是整个流水线的重中之重。最初通过HTML + Puppeteer进行渲染,因Chrome进程开销与内存占用导致性能难以满足需求。探索替代方案时,从TeX家族着手取得了显著成果。pdflatex在复杂排版场景下性能优于浏览器渲染,但面对超大文档时受限于静态内存分配。lualatex可以动态分配内存,但在极大表格或长文档上出现稳定性问题且编译时间较长。
最终的突破来自于Typst,这是一款用Rust实现的现代排版系统,单二进制、启动快、错误提示友好。实测结果显示,Typst在小文档上比传统TeX快2到3倍,而在数千页的合同单场景下,生成时间可以从数十分钟降低到不到一分钟,同时显著减小了镜像体积与启动延迟。 数字签名部分由于PDF签名本身的复杂性,目前在开源社区中难以找到高性能的批量签名库。为兼顾性能与稳定性,采用了基于Java OpenPDF的签名服务,将签名逻辑封装为一个常驻JVM的HTTP服务,以并发请求方式对接签名工作节点。通过Sidecar模式将签名服务与工作进程同宿主机部署,避免频繁启动JVM带来的开销,并且能以并发连接池的方式高效利用签名证书与密钥材料。 海量中间文件的存储与传递是另一个关键难题。
整个流程产生的临时文件数量巨大,既有Typst标记文件、未签名PDF,也有签名后的最终PDF,日常规模可以达到数百万乃至千万级别的短期文件操作。网络共享文件系统在并发读写小文件时表现不佳。对比测试显示,EFS在默认配置下在大量小文件并发写入时出现操作数限制与高延迟,不同模式的延迟表现差异明显且成本高昂。相对而言,EBS对单实例性能友好但无法跨实例共享,而S3以其高可用、低成本的对象存储特性成为更合适的选择。 然而直接把所有临时文件丢到S3也并非毫无代价。S3采用分区机制来保证吞吐稳定性,针对每个前缀的请求速率有推荐上限。
如果所有对象都落在具有共同前缀的分区上,会出现"分区热点",从而导致503 Slow Down错误。通过与云存储支持团队沟通并基于实践经验,最终采用了固定分区键的前缀策略,把文件分散到预先设计的若干前缀集合中,避免使用单一可排序ID产生的共同前缀聚集效应。结合向云服务团队申请预热分区的做法,可以在批处理高峰时段保持稳定的S3吞吐,从而支撑起数百万次的并发GET/PUT请求。 在调度与编排方面,选择了与现有平台契合且操作简单的工具。基于Nomad的作业编排实现了对临时计算集群的快速部署与弹性伸缩。配合Terraform模块化模板,在每天夜间预先启动一套干净的Nomad集群与预置的工作节点组,按需启动不同角色的Auto Scaling Group。
通过在实例启动时注入元数据标签,Nomad能够以约束(constraints)机制把不同类型的工作分配到专用机池,例如专门用于PDF生成的高CPU实例池与用于签名或邮件发送的轻量实例池。这种将工作按资源需求隔离的方式,既能提高节点利用率,也便于精细化定价与资源控制。 整个批处理的控制由中控实例负责,借助RunDeck执行从基础设施创建、作业部署到结果汇总的全流程脚本。中控节点从Redis中读取全局任务状态,实时显示已完成、失败或待处理的合同单数,便于人工介入与重试。Redis在架构中既作为消息代理也作为任务状态存储,支持快速的任务写入与查询中心化视图。针对失败任务设计了再试策略和有针对性的补处理流程,保证在任何网络或外部依赖异常时能够进行局部恢复。
邮件投递体系的演进也非常重要。原有的Postal邮件服务器在高并发场景下无法横向伸缩并成为瓶颈,替换为Haraka集群后得以轻松扩展SMTP并发连接数。邮件工作节点维护到Haraka的连接池,并用自研的smtppool库实现高效并发投递。自研邮件栈的前提是长期维护良好的IP发信信誉,包括严格的退信策略、限速策略与密切监控,避免因为批量投递导致外部邮件服务商限流或封禁IP。 实际运行中,整个流程从夜间启动计算集群,到并发生成、签名并投递超过150万份PDF,端到端时间控制在约25分钟。作业完成后,通过脚本回收所有临时资源,自动缩容ASG、停止Nomad作业并销毁集群,确保仅在需要时付费,从而实现了成本最优化。
这样一来,极短的高并发计算窗口与临时资源池配合,既满足了监管对时间的严格要求,也将总体成本压缩到可忽略的水平。 从工程实践角度来看,有几条可借鉴的经验。将单一长流水线拆解为可并行的独立阶段,有利于横向扩展与容错。选择适合业务特性的工具非常关键:对于高并发CPU密集型任务,轻量二进制与高效并发语言更有优势;排版工具的选择会直接影响生成速度与稳定性;对象存储的前缀分区策略需要提前规划并与云服务方沟通分区预热策略以避免热点。调度器与基础设施即代码(IaC)工具的组合能实现可重复、可审计的临时集群生命周期管理。 面向未来,可进一步的优化方向包括增加工作节点的实时指标采集,以便在批量运行时按需动态扩容或收缩;将签名服务向更高并发优化或探索硬件加速的签名方案以缩短签名瓶颈;在邮件投递环节增强智能投递策略以提高到达率并减少重试成本。
总体而言,把复杂业务拆解、利用短时高密度的计算资源、并在关键环节选择具备高性价比的工具,是在有限预算下实现百万级文档即时生成与投递的可行路径。 对于任何面临类似问题的团队,核心原则是以业务的吞吐与时间窗口为导向,结合工程上的可扩展性与云服务的分区特性,设计既稳健又高效的流水线。通过典型案例可以看到,采用合适的技术栈与架构模式,可以把原本耗时数小时甚至数天的批量任务,压缩到数分钟级别,从而大幅提升业务能力并保持合规性与成本可控性。 。