性能突破:星图平台架构优化
引言
开篇文章:API灵活定义+极速驱动:货拉拉星图平台技术架构与优化实践
平台概述
星图平台:全方位的“接口管家”,集配置化、高复用和自定义能力于一体,为业务接口的调用与调试提供了一站式解决方案。
其核心功能包括支持所有HTTP/SOA/SSE/MCP接口的配置、调试及调用,并且支持自定义流程编排,可针对请求参数、请求头与响应数据进行二次加工,极大丰富了业务接口场景,从“规则化使用”拓展到“定制化管理”,实现接口配置的灵活性与高效性。
项目背景
在现代业务场景中,各模块之间需要通过API接口进行数据与服务的高效联通。面对快速变更的业务需求,仅依赖定制化开发成本过高且扩展性不足。因此,亟需一个集中管理API接口的解决方案,使业务人员能够在无需反复对接研发的情况下,快速选择接口并完成配置化绑定。这不仅优化了接口管理与复用效率,还有效降低维护成本,提升整体运营能力。
星图平台便是在这一背景下孕育而生,作为一款功能全面且灵活的“接口管家”,它以配置化、高复用与自定义能力为核心,提供了一站式的接口调用与调试解决方案。
竞品分析与产品对比
为了更全面地评估星图平台在市场中的竞争力,我们选取了代表性的第三方工具(例如 Post***、Api***),并结合主流开源类引擎(Flowable、Activiti等),进行了功能、场景适配性等多维度的对比分析。
架构设计
良好的架构是系统稳定高效运行的基石。优秀的架构设计不仅奠定了开发迭代和功能扩展的坚实基础,同时有效降低了重复开发与维护成本。在面对快速变化的业务需求时,灵活的架构设计还能助力系统性能的持续优化,使开发过程更具弹性,同时保障系统后续维护、扩展和高性能运行得以平稳实现。
星图平台的架构设计以模块化、轻量化以及可扩展性为核心原则,致力于构建一套兼具灵活性与高性能的技术架构。
整体架构图
设计原则
模块化
模块化设计将系统划分为多个相对独立的功能模块,有效减少模块间相互依赖,提升运行稳定性。其中,各模块的职责划分明确,有助于降低开发复杂度,并为业务灵活扩展提供可靠支撑。关键模块设计如下:
• 配置模块
独立部署的配置服务,能够支持在高业务峰值期间快速新增与调整配置,并在配置修改后即时调试,为运营团队提供更灵活的操作空间。
• 运行模块
运行模块承载了核心业务调用,通过将运行模块拆分独立部署,有效降低其他模块对系统运行时的干扰,确保接口调用的高效与持续稳定。
• 执行流水模块
执行流水的写入和展示对实时性要求不高,通过异步消息的解耦,隔离接口调用和流水数据存盘,进一步提升系统运行效率。
轻量化
为了降低复杂度、提升整体性能,星图平台的架构遵循轻量化设计理念,在实现核心功能的同时尽量减少不必要的逻辑负担,使系统更为简洁高效:
• 核心功能优先
轻量化的体系仅聚焦于核心功能,包括参数校验、预处理、数据组装以及接口请求,避免额外冗余逻辑,从而确保平台整体运行的高效性和简洁性。
• 自主研发的轻量级流程引擎
星图平台采用自主研发的高性能轻量级流程引擎,与市面上现有的成熟方案相比更为精简,可高效支持高并发场景。该引擎与星图平台的接口流程完美兼容,能够显著提升复杂业务流程的运行效率。
流程引擎运行机制
平台的流程引擎具备以下特性:
• 内核轻量级,可支持高并发并行处理;
• 嵌合星图平台接口,保障流程接口配置与运行的高效协同。
可扩展
为了满足未来场景的多样化需求,星图平台底层架构采用插件化设计,具备极高的灵活性与扩展性。通过类似SPI(Service Provider Interface)的机制,底层引擎统一提供标准接口,并提供默认实现,业务方可通过扩展机制快速接入特定功能。
扩展组件设计:
•监听器(Listener)
系统启动时检索并注册实现的插件,通过业务线和业务类型完成归类,确保扩展逻辑可快速加载与分发。
• 执行器(Executor)
定义统一的执行流程与规范,支持前置预处理和后置处理的钩子接口,便于定制化的业务扩展需求。
• 执行引擎(Execution Engine)
提供对外统一的执行器入口,隐藏底层复杂的寻址逻辑,开发者可直接调用,简化了调用操作,提高了功能复用性。
挑战&痛点
在星图平台的架构设计和早期运行过程中,我们面临了诸多复杂的技术挑战。尽管系统的目标是构建一个高效、可拓展且稳定的架构,但在实际业务开发和系统实现中,依然有许多关键问题需要解决。一个优秀、稳定运行的系统绝非一蹴而就,以下是我们在平台早期开发与优化过程中所面临的核心痛点总结:
项目工程腐化
随着平台功能的不断迭代和业务的飞速变化,开发团队在架构设计和日常开发中遇到如下问题:
• 开发效率低
面对业务需求的频繁调整和日益复杂的业务逻辑,开发人员需要花费大量时间对代码进行修正和维护。低效的开发流程极大地影响了项目的交付效率。
• 代码管理困难
由于业务迭代迅速,代码库规模大幅膨胀,导致模块之间的耦合度逐步增加。在代码可维护性和模块可扩展性方面,开发人员面临着更大的管控难度。
• 稳定性问题
一些问题被暴露在高并发场景中,例如由于缺乏严谨的设计规范导致的接口不一致性、模块间调用异常等,这种不稳定性在面对大规模请求时尤为显著。
• 重复劳动多
业务场景多样且灵活,开发人员常常需要编写大量重复性代码,以应对各类接口调用和参数适配需求。这不仅造成了时间和资源的浪费,还增加了工程出错的风险。
性能与稳定性压力
在系统运行过程中,随着流量的增长和业务高峰期的到来,我们发现了一些显著的性能短板:
• 数据库压力大
高并发的流程接口请求会产生成千上万的运行数据,流程执行流水的并发写入对数据库造成了极大的存储和写入压力。
• CPU占用高
业务在高峰期流量激增,CPU占用率随之显著上升,服务负载长期维持在高水平。为缓解此问题,短期内只能通过增加机器配置(升配)或扩容集群来进行临时救急。
• 内存消耗过大
在高流量场景下,服务对内存的需求激增,部分机器的内存使用接近上限,威胁到服务的稳定性。
• 接口RT(响应时间)过慢
平台整体接口响应时间(RT)普遍偏高,部分流程接口RT甚至达到 5秒,接近不可用状态(特别是在对实时性要求较高的业务场景中,影响尤为严重)。
• 性能不稳定
系统在高频调用场景下存在性能波动,偶尔会出现请求响应时间的突刺现象(RT超高)。这种性能上的不稳定性导致用户体验不可预测,严重影响服务可靠性。
性能优化之路
在星图平台的性能优化过程中,我们从问题入手,通过对架构、执行逻辑和插件化管理的全面优化,有效缓解了高并发和复杂业务下的系统性能压力,并构建了一套灵活、可扩展的运行机制。以下是详细的实践分享:
应用指标分析
• CPU: 业务高峰期流量快速增长,CPU使用率维持在高水平,服务负载无法有效下降,短期只能通过升配和扩容来缓解。
• JVM:GC次数、GC 回收时间等指标均不太健康
• 接口响应延迟(RT):整体服务的RT普遍偏高,影响业务体验
发现问题
• CPU负载居高不下
业务高峰期流量快速增长,CPU使用率维持在高水平,服务负载无法有效下降,短期只能通过升配和扩容来缓解。
• 内存消耗大
系统的接口调用逻辑复杂,因业务高峰期内存消耗长时间居高不下,部分服务甚至濒临内存耗尽的边缘。
• 接口响应延迟(RT)过慢
整体服务的RT普遍偏高,个别流程接口的响应时间(RT)在高峰时段甚至达到 5秒,逼近不可用状态,严重影响业务体验。
• 数据库压力大
高并发的流程接口请求生成了大量的运行数据,频繁的并发写入使数据库成为性能瓶颈,增加了系统总体延迟风险。
• 性能波动大
服务性能缺乏稳定性,时而出现RT突刺现象,导致用户请求响应时间产生明显波动,可靠性受到威胁。
自动注册插件
为了有效应对上述性能挑战,我们提出并实现了“自动注册执行器”框架。它以自动化和标准化为核心,通过约定优于配置的原则优化系统的执行逻辑,并在开发与运行过程中提升开发效率与系统稳定性。
核心特点
• 自动注册
无需手动操作,业务逻辑能够按照约定自动注册到框架中,避免冗繁步骤,同时减少人工疏漏带来的风险。
• 插件化设计
框架采用插件化的扩展方式,核心框架只提供基础功能,通过插件实现具体的业务逻辑,使系统无障碍支持新增模块和业务扩展。
• 模块化解耦
框架通过标准化的接口与依赖管理,将复杂业务模块划分为独立的功能单元,模块间耦合度大幅降低,开发维护更灵活。
• 统一管理与监控
对所有注册插件、执行器的运行状态进行统一埋点和监控,方便快速定位性能问题,助力系统运维优化。
方案设计:核心架构与分层实现
为了更好地落地“自动注册执行器”,我们采用了以下设计方案:
底层插件化架构
底层采用插件化设计,框架通过类似 SPI(Service Provider Interface) 的自动注册机制,提供了以下关键组件:
• 监听器(Listener)
在服务启动时,自动扫描插件实现,并按照业务线和业务类型对其注册归类。
• 执行器(Executor)
定义标准化的业务执行流程,支持钩子函数(前置处理与后置处理),业务开发人员可按需扩展具体逻辑。
• 执行引擎(Engine)
对外暴露执行器入口,无需了解内部寻址细节,开发调用更加便捷高效。
使用场景举个🌰:流程引擎通过添加不同的节点类型实现类,完成自定义扩展,并且自动注册可向上延伸,应用层通过追加实现,完成更上层的自定义业务,从而覆盖复杂业务场景,每个实现相互解耦,提升可读性。
类UML
• Processor Register:服务启动时扫描所有注册的实现,通过业务线和业务类型标识归类。
• Auto Register Processor:定义实现规范,比提供前置预处理和后置处理等钩子,方便业务自定义动作。
• Auto Register Processor Engine:对外统一暴露的执行器入口,使用方直接调用,无需感知内部寻址实现。
流程引擎设计
流程引擎底层广泛使用自动注册插件,使其具有良好的扩展性,并且大大降低维护成本,应有的核心模块有:
• 流程节点定义&运行:所有节点以及子类型节点,通过自动注册插件挂载到流程引擎中,使每个节点逻辑解耦,扩展实现新节点类型更方便。
• 条件节点条件匹配:条件节点的条件项取值来源不尽相同,数据类型也有多样,通过自动注册插件可实现相互之间解耦。
• 流程运行数据持久化:不同的运行时数据,需要落到不同的数据表。
良好的系统架构设计是提升性能的“底座”,其较好的扩展性和弹性,可以让我们更友好地开展性能优化工作。
数据存储
数据生产与存储解耦
星图管理端服务(web):提供接口配置
星图运行服务(svc):提供接口调用服务
星图异步处理服务(task):提供支持异步消息、数据持久化、异步任务等服务
调用记录
• 星图运行服务(svc)服务将接口调用记录数据发送到kafka
• 星图异步处理服务(task)服务消费kafka消息,将调用记录写入ES
流程运行时流水
• 星图运行服务(svc)将流程节点执行流水数据发送到kafka
• 星图异步处理服务(task)服务消息kafka消息,将流程流水写入MySQL
通过将接口调用产生的数据和将数据落库存储解耦,将写入数据任务交由task(异步)服务处理,可以减轻svc(运行)服务负担,提升服务性能。
数据压缩
流程接口每个节点执行完毕后,随即将生产的数据发送到kafka,多个节点存在更新同个临时变量场景,并发量大时,会导致MySQL出现许多更新锁等待,最终导致MySQL集群CPU、RT等上涨。
MySQL锁等待【案例】
解决方案:将流程以及所有节点产生的数据进行合并压缩,完成接口调用时再一次性发生MQ消息,解决MySQL锁等待问题,而且MQ消息量大大降低,减轻MQ集群和svc服务消费的压力。
缓存
良好的架构设计,实现多级缓存时,也无需调整上层代码。
缓存设计
星图底层流程引擎支持二级缓存(本地缓存+分布式缓存),兜底查询MySQL,缓存预热后,所有运行相关的数据都可以从缓存获取。
缓存框架设计:利用模板方法设计模式,抽象实现所有缓存操作方法,特殊场景覆盖方法实现即可。
缓存key设计:流程定义和运行分开使用不同的存储key,其中
流程定义根据场景划分不同的key,比如流程版本定义、节点定义、节点连线等
流程运行时根据实例以及类型划分不同的key,比如流程实例、流程运行节点、流程变量等。
内存缓存
缓存框架选型
在分布式应用场景中,选用高效的内存缓存框架是打造高性能、高可用系统的重要组成部分。针对主流内存缓存框架 Guava 和 Caffeine,我们基于高并发环境特点及线程模型进行了深入对比测试,分析其清理机制的差异对性能(尤其是响应时间 RT 波动)的影响,并提供选型及优化建议。
特性对比
特性 | Guava | Caffeine |
清理机制 | 惰性清理:在缓存访问或容量达到限制时同步触发清理。 | 异步清理:借助后台线程定期清理过期缓存,实现清理与业务逻辑解耦。 |
过期数据处理 | 惰性触发,只有在触发访问或缓存达到容量限制时,才会清理过期数据。 | 异步触发,可利用定期任务及时清理过期数据,避免缓存中遗留过时记录。 |
线程争抢 | 实现简单,清理逻辑与访问或写入同步,线程争抢相对较少。 | 在高并发场景下,异步清理线程可能与核心业务线程发生资源争用(如 GC 或锁争抢)。 |
响应时间(RT)波动 | 表现稳定,由于清理操作是同步触发,RT 波动较小,适合对延迟敏感的业务场景。 | 清理由后台线程异步执行,在高频更新场景中可能引发 RT 波动,尤其压测场景下更为明显。 |
全局性能 | 性能稳定,但由于惰性清理机制,过期数据在被清理前会持续占用内存空间,存在潜在的内存浪费。 | 性能优越,异步清理可以减少主线程负担,但存在后台清理线程引发的资源争用风险。 |
适用场景 | 数据更新频率较低或分布均匀、访问压力适中的场景 | 高并发访问的场景,且能接受一定的 RT 波动,用以换取更高缓存命中率和更低内存占用。 |
基于场景选择 | ✅【全局较优&稳定&降低复杂度】 | ❌ |
内存策略
为了提升内存缓存的性能表现和资源利用效率,我们建议在框架配置中优化以下内存策略:
缓存容量限制 (maximumSize)
通过合理设置 maximumSize 参数,限制缓存最大存储数量。例如设置为 5 万条数据,减少因缓存过多导致的内存溢出(OOM)风险。
写入后过期时间 (expireAfterWrite)
根据缓存场景设置过期时间。例如对于运行时生成的临时数据,配置生命周期为 40 秒,这样在数据使用后能够及时释放资源,有效避免内存滞留。
监控缓存命中率
定期通过指标监控缓存命中率,动态调整策略优化缓存性能:
• hitRate(命中率):表示缓存命中比例,建议监控以确保命中率始终保持在理想水平(如 >90%)。
• missRate(未命中率):用于监测缓存未命中的场景以进行容量或失效策略调整。
• loadCount(加载次数):累计分析缓存加载频次,排查高频场景带来的性能瓶颈。
• loadExceptionRate(加载异常率):监控加载操作异常频率,优化上游资源表现。
我们的实践表明,星图运行时仅采用一级内存缓存(Guava)设计,相较传统的二级缓存架构(如内存+Redis组合),避免了跨网络通信的开销,将缓存层读取响应降低至 2ms 内【相比 Redis 平均 RT 5ms 的性能显著提升】
多线程
在不同应用场景下,合理设计线程池、优化资源争抢问题、避免线程锁引起的死锁风险以及正确使用异步任务不仅能够提升系统性能,还能显著改善延迟与吞吐表现。
划分线程池
• 独立线程池: 根据不同场景(如 I/O 密集、CPU 密集)配置独立的线程池,避免互相干扰。例如:
○ 核心业务: 高优先级接口、低延迟需求场景专用线程池。
○ 异步耗时操作: 较低优先级的耗时操作使用独立池隔离。
合理线程池配置
• 核心业务线程池:
○ 核心线程数、最大线程数与 CPU 核心数 成比例配置(如 >= 2 * CPU 核心数)。
○ 配置较小队列,减少任务队列中堆积,保证系统低延迟。
○ 避免线程过多增加上下文切换成本。
• 非核心任务线程池:
○ 减少核心线程数、最大线程数,调大队列容量。
○ 允许一定的延迟,保证主业务流程的资源优先级。
线程资源争抢案例与优化
在高并发请求下,进线优先级流程接口内多个并行流程嵌套,本地调用模式直接触发运行方法,导致在 CPU 核数较低 且 线程数配置不足 时,出现过多线程阻塞等待的情况,整体请求耗时高达 30 秒。
进线优先级接口:
价值标签【省略部分策略】:
jstack 堆栈信息分析:
• 堆栈快照中出现大量 WAITING 和 TIMED_WAITING 状态,这表明线程严重阻塞。
代码块:
"function_kefu--2" #267 prio=5 os_prio=31 tid=0x00007f8cace88800 nid=0x1854b waiting on condition [0x000000031682b000]java.lang.Thread.State: WAITING (parking)at sun.misc.Unsafe.park(Native Method)- parking to wait for <0x00000007bd893270> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)at cn.lalaframework.dtp.common.VariableLinkedBlockingQueue.take(VariableLinkedBlockingQueue.java:411)at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)at java.lang.Thread.run(Thread.java:748)"function_kefu--1" #266 prio=5 os_prio=31 tid=0x00007f8cad651000 nid=0x6b1f waiting on condition [0x0000000316219000]java.lang.Thread.State: WAITING (parking)at sun.misc.Unsafe.park(Native Method)- parking to wait for <0x00000007bd893270> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)at cn.lalaframework.dtp.common.VariableLinkedBlockingQueue.take(VariableLinkedBlockingQueue.java:411)at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)at java.lang.Thread.run(Thread.java:748)"OpsFlowOperateLogAsync--2" #265 prio=5 os_prio=31 tid=0x00007f8cace8a000 nid=0x14137 waiting on condition [0x0000000316013000]java.lang.Thread.State: WAITING (parking)at sun.misc.Unsafe.park(Native Method)- parking to wait for <0x00000006c4e72578> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)at cn.lalaframework.dtp.common.VariableLinkedBlockingQueue.take(VariableLinkedBlockingQueue.java:411)at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)at java.lang.Thread.run(Thread.java:748)"http-nio-20482-exec-3" #193 daemon prio=5 os_prio=31 tid=0x00007f8cacdd3800 nid=0x16c0f waiting on condition [0x0000000314edb000]java.lang.Thread.State: TIMED_WAITING (parking)at sun.misc.Unsafe.park(Native Method)- parking to wait for <0x00000007770f3708> (a java.util.concurrent.CompletableFuture$Signaller)at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)at java.util.concurrent.CompletableFuture$Signaller.block(CompletableFuture.java:1709)at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3334)at java.util.concurrent.CompletableFuture.timedGet(CompletableFuture.java:1788)at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1928)at cn.huolala.ops.flow.service.flow.run.gateway.subType.RunOpsGatewayParallelBeginServiceImpl.process(RunOpsGatewayParallelBeginServiceImpl.java:87)at cn.huolala.ops.flow.service.flow.run.BaseOpsRunNodeServiceImpl.processBySubNodeType(BaseOpsRunNodeServiceImpl.java:90)at cn.huolala.ops.flow.service.flow.run.gateway.RunOpsGatewayNodeServiceImpl.process(RunOpsGatewayNodeServiceImpl.java:39)at cn.huolala.auto.register.processor.AbsAutoRegisterProcessor.exec(AbsAutoRegisterProcessor.java:45)at cn.huolala.auto.register.processor.AutoRegisterProcessorEngine.exec(AutoRegisterProcessorEngine.java:122)at cn.huolala.auto.register.processor.AutoRegisterProcessorEngine$$FastClassBySpringCGLIB$$a05e36f5.invoke(<generated>)at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)at cn.huolala.kf.common.trace.report.aop.BaseAspect.getAspectBO(BaseAspect.java:35)at cn.huolala.kf.common.trace.report.aop.TraceReportAspect.around(TraceReportAspect.java:72)at sun.reflect.GeneratedMethodAccessor314.invoke(Unknown Source)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)"FlowTaskParallelAsync--2" #255 prio=5 os_prio=31 tid=0x00007f8cdb501000 nid=0x19e0b waiting on condition [0x0000000312f81000]java.lang.Thread.State: TIMED_WAITING (parking)at sun.misc.Unsafe.park(Native Method)- parking to wait for <0x0000000778709990> (a java.util.concurrent.CompletableFuture$Signaller)at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)at java.util.concurrent.CompletableFuture$Signaller.block(CompletableFuture.java:1709)at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3334)at java.util.concurrent.CompletableFuture.timedGet(CompletableFuture.java:1788)at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1928)at cn.huolala.ops.flow.service.flow.run.gateway.subType.RunOpsGatewayParallelBeginServiceImpl.process(RunOpsGatewayParallelBeginServiceImpl.java:87)at cn.huolala.ops.flow.service.flow.run.BaseOpsRunNodeServiceImpl.processBySubNodeType(BaseOpsRunNodeServiceImpl.java:90)at cn.huolala.ops.flow.service.flow.run.gateway.RunOpsGatewayNodeServiceImpl.process(RunOpsGatewayNodeServiceImpl.java:39)at cn.huolala.auto.register.processor.AbsAutoRegisterProcessor.exec(AbsAutoRegisterProcessor.java:45)at cn.huolala.auto.register.processor.AutoRegisterProcessorEngine.exec(AutoRegisterProcessorEngine.java:122)at cn.huolala.auto.register.processor.AutoRegisterProcessorEngine$$FastClassBySpringCGLIB$$a05e36f5.invoke(<generated>)at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)at cn.huolala.kf.common.trace.report.aop.BaseAspect.getAspectBO(BaseAspect.java:35)at cn.huolala.kf.common.trace.report.aop.TraceReportAspect.around(TraceReportAspect.java:72)at sun.reflect.GeneratedMethodAccessor314.invoke(Unknown Source)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)
根因:
• CPU 核数不足: 无法充分调度多线程。
•线程池配置不足: 核心线程数和最大线程数设置过低,阻塞流程中的并行子任务。
解决方案:
• 硬件提升:
○ 将服务实例升级至 至少 4 核心 CPU,为线程的并行调度提供足够的计算资源。
• 线程池优化:
○ 调大核心线程数和最大线程数,使其能够充分发挥多线程性能(如 2 * CPU 核数)。
○ 结合流程嵌套情况,设置合适的任务队列长度,避免任务过长时间排队。
线程锁
在 流程运行时,并行节点赋值变量或采集词槽可能更新相同的缓存 key。由于 并发更新 导致的问题:
根因:
• 数据不一致:
○ 并发线程 T1、T2 同时操作:
▪ T1 更新了缓存,但还未完成操作时,T2 直接覆盖 T1 的数据。
○ 导致 T1 更新的数据丢失,最终值错误。
解决方案:使用线程锁,设置超时等待时间(比如10ms),避免长时间线程阻塞。
技术考量:
• 需要加锁控制更新,但因更新前查询结果要求一致性,
不适用ReadWriteLock或ReentrantReadwritelock。并且
○ 使用 Redisson:
▪ Redisson 提供分布式锁,但因操作是本地完成,不涉及多节点,且网络 IO 带来性能开销,选择实现为原生锁。
• 最终选型:
○ 基于公平队列的 ReentrantLock:
▪ 适合高并发更新,且支持超时时等待,解决长期阻塞风险。
▪ 避免使用 synchronized,它无法超时且可能阻塞主线程。
同时,我们会根据不同的业务场景(读写流程变量、运行节点等)分配不同的锁实例,减少锁争抢。
• 对并行业务划分不同场景,分配 独立实例锁,例如:
○ 读写不同的流程变量使用互相隔离的锁,降低锁争用。
○ 对高频更新场景,动态调整锁竞争与性能的权衡。
公平锁 vs. 非公平锁:
• 公平锁(ReentrantLock(true)):
○ 按照线程等待的时间顺序获得锁,避免线程 “饥饿”。
○ 适合高优先级任务,尤其读写频繁、延迟敏感的业务场景。
• 非公平锁(ReentrantLock(false)):
○ 线程请求无序,性能较高,但可能导致部分线程长时间无法获得锁。
○ 可用于对时序性要求不严格的批量更新。
异步任务设计规范
在系统中,异步任务通过多线程并发处理,提高吞吐性能,但设计中需避免滥用异步。
异步任务场景
• 场景分析:将耗时长、可延迟处理或不太影响主业务链路的操作转为异步,例如:
a.条件节点中复杂格式的流水条件判断并持久化。
b.发送运行流水 Kafka 消息(网络 IO 占主流程时间)。
设计原则
• 任务是否适合异步:
○ 避免一些短周期、快速执行、延迟敏感任务异步化,如短时简单计算。
○ 频繁创建、销毁线程带来上下文切换耗时可能比任务本身耗时更大。
• 主线程与异步线程资源抢占:
○ 配置异步线程任务的专属线程池,隔离核心流程的线程资源。
○ 合理控制异步任务数量,避免主线程因资源争抢而延迟。
“异步不是越多越好”,比如一些频繁短小、简单的、时间敏感的操作,没必要使用异步线程,线程反复创建/销毁、上下文切换成本可能都比任务开销大,且还会与核心线程争抢资源,得不偿失。
代码优化
针对性能优化场景,我们可以从性能诊断、定位瓶颈以及解决方案入手,通过工具辅助分析、埋点验证和代码层优化,提升系统的整体性能与稳定性。
性能分析工具
火焰图
火焰图是一种通过采样分析生成的方法调用堆栈图,帮助定位性能热点。
• 特点:
○ 方法层次结构清晰,支持快速分析调用链路径。
○ 适合快速定位“大块头”性能瓶颈的模块或方法。
• 不足:
○ 只能定位到方法的大范围内,无法深入到每个请求的细节。
• 手动埋点
手动埋点是在代码关键点添加定制的耗时记录方法,输出相关日志作为性能瓶颈诊断依据。
优点:
○ 灵活性好,可以在任意代码块中埋点
○ 可控性强,根据实际需要输出各种指标
○ 成本低,使用java提供的时间方法,可以获取毫秒、微秒等
缺点:
○ 侵入性强,调整程序代码
○ 维护成本高,埋点代码太多,后面可能看不懂
应用场景:
适合分析特定功能模块的执行时间。例如判断复杂的条件逻辑、特定循环处理的耗时情况。
Arthas
Arthas 是强大的线上诊断工具,可以实时分析应用性能,且不修改代码直接排查问题。
支持常用命令:
•monitor: 查看方法的调用次数、平均耗时、失败次数等。
• trace: 追踪方法调用过程,查看参数、返回值及方法时间消耗。
优点:
• 非侵入式,代码无需修改。
• 实时性强,适合线上复杂场景的性能优化。
• 核心功能包括内存、GC、线程监控,能全方位诊断常见性能问题。
资源优化
资源优化是性能优化的核心,通过预处理、预加载、以及资源复用降低系统开销。
数据预处理
• 目标:避免循环中重复 I/O 操作
○ 尽量在进入循环体前将数据统一加载到内存中。
○ 即使数据使用了内存缓存,但高频访问仍会造成性能瓶颈。
• 案例场景:复杂条件节点的判断
○ 原逻辑:每个条件项判断时重复读取所需数据。
○ 优化方案:
▪将条件数据在进入条件遍历前统一预加载。
▪减少缓存高频访问。
资源预加载
• 函数预编译:
○ 对频繁调用的 Groovy 函数可以采取预编译机制,将编译后的函数缓存到本地以提升执行效率。【特别适用于频繁方法调用的复杂场景。】
对象资源复用
• 案例:javax.validation.Validation数据校验耗时高
• 根因:通过 trace 分析,底层工具类由于每次调用时重复初始化验证器工厂实例,导致耗时稳定在 10ms 以上。
• 优化方案:通过复用验证器工厂,避免重复初始化
优化前:
优化后:
优化前代码演示
public class ValidationUtils {//数据校验方法public static void validateThrowException(@Valid Object object) throws OpsParamValidateException {Set<ConstraintViolation<@Valid Object>> validateSet = Validation.buildDefaultValidatorFactory().getValidator().validate(object, new Class[0]);...}}
优化后代码演示
public class ValidationUtils {private static final Validator validator;static {validator = Validation.buildDefaultValidatorFactory().getValidator();}// 数据校验方法public static void validateThrowException(@Valid Object object) throws ServiceException {Set<ConstraintViolation<@Valid Object>> validateSet = validator.validate(object, new Class[0]);}}
• 效果:验证器初始化时间减少至微秒级,整体性能显著提升。
网络IO优化
通过使用内存缓存,我们已经优化替代了Redis、MySQL大部分读写场景,是不是可实现无DB网络IO呢?
• 场景1:检查接口调用权限(Redis 操作)
• 原逻辑:
○ 通过 Redis 查询接口权限列表,存在重复判断多次访问 Redis 的情况。
• 优化方案:
○ 在更新权限时通过 服务注册中心 通知调用服务刷新内存缓存。
○ 避免通过 Redis 频繁查询权限。
• 场景2:流程实例的初始化(MYSQL操作)
优化方案:
• 通过本地构建流程实例数据,采用雪花算法生成唯一 流程实例 Id。
• 整个运行时流程记录均关联至内存缓存中的实例数据,无需频繁访问 MySQL。
异步任务优化
通过以上的优化,剩下只有持久化流水产发送的MQ消息网络IO,通过异步任务方式发送MQ消息,使调用主链路无额外网络IO消耗(不包含处理节点请求接口)
• 原逻辑:
○ 流程运行产生持久化的流水记录,通过 MQ 发送。
○ 消息发送过程中产生网络 I/O,附带主链路的性能开销。
• 优化方案:
○ 消息发送任务通过异步线程方式处理,将网络 I/O 从主链路中完全隔离。
○ 主链路仅负责逻辑处理,网络操作的消耗转移至异步任务线程池中。
数据序列化
为什么使用了内存缓存,还是比较慢,操作耗时偶现2-6ms,通过trace分析链路,发现读写缓存数据时,数据序列化和反序列化占了不少时间。因此我们考虑了使用更高效率的二进制格式替代JSON格式。
二进制格式
• Protobuf简介
Protobuf是由Google开发的一种高效、结构化的序列化协议,用于在不同系统之间进行数据交换和存储。它属于一种“二进制序列化”格式,比传统的文本格式(如JSON、XML)更紧凑、更快速。
• Protostuff简介
Protostuff是一套基于Protobuf的高性能序列化框架,由国内开发者维护,可动态序列化和反序列化Java对象,不用事先定义.proto文件、无需代码生成。
Java类:
package cn.huolala.ops.flow.service.flow.run.model;public class FlowRunProcNodeBO {/*** 运行实例ID*/private Long runExecutionId;/*** 流程版本ID, flow 表 ID字段*/private Long versionId;/*** 当前运行节点ID,flow_re_proc_node 表 ID 字段*/private Long currentNodeId;/*** 当前运行节点名称*/private String currentNodeName;/*** 当前运行节点类型(SYS:系统, PEOPLE:人工, CONDITION:条件, START:开始, END:结束, CHAIN:链条)*/private String currentNodeType;/*** 当前运行子节点类型【扩展预留】*/private String currentSubNodeType;}
protobuf
syntax = "proto3";package cn.huolala.ops.flow.service.flow.run.model;message FlowRunProcNodeBO {// 运行实例IDint64 runExecutionId = 1;// 流程版本ID,flow表ID字段int64 versionId = 2;// 当前运行节点ID,flow_re_proc_node表ID字段int64 currentNodeId = 3;// 当前运行节点名称string currentNodeName = 4;// 当前运行节点类型(SYS:系统, PEOPLE:人工, CONDITION:条件, START:开始, END:结束, CHAIN:链条)string currentNodeType = 5;// 当前运行子节点类型【扩展预留】string currentSubNodeType = 6;}
• 性能测试
示例1:样本数据(字符长度=194)
处理方式 | 序列化(10w次) | 反序列化(10w次) | 序列化后字符长度(byte) |
Jackson | 7.8 s | 3.9 s | 235 b |
Jackson(复用ObjectMapper) | 446 ms | 585 ms | 235 b |
Fastjson | 270 ms | 640 ms | 194 b |
Protostuff | 110 ms | 150 ms | 78 b |
示例2:样本数据(字符长度=8533)
处理方式 | 序列化(10w次) | 反序列化(10w次) | 序列化后字符长度(byte) |
Jackson | 11.3 s | 12.3 s | 10 kb |
Jackson(复用ObjectMapper) | 4.4 s | 7.1 s | 10 kb |
Fastjson | 2.4 s | 12.3 s | 8.4 kb |
Protostuff | 2.1 s | 1.9 s | 5 kb |
为什么Jackson序列化后的内容比原始内容还长?
Jackson序列化默认带空格/换行,可调整 SerializationFeature.INDENT_OUTPUT 输出为紧凑JSON
objectMapper.configure(SerializationFeature.INDENT_OUTPUT, false);
最终方案:搭配使用,配置及运行时数据使用二进制格式存储在内存中,最后发送持久化数据时使用Jackson序列化,涉及日期时间格式化,Jackson会更加灵活方便。
Protostuff工具类
public class ProtostuffSerializer {// 序列化对象public static <T> byte[] serialize(T obj) {...};// 反序列化对象public static <T> T deserialize(byte[] data, Class<T> clazz) {...};// 序列化列表public static <T> byte[] serializeArray(List<T> list) {...};// 反序列化列表public static <T> List<T> deserializeArray {...};}
• 除了缓存数据格式化,还能做什么?
○ 复杂对象深拷贝:将对象格式化为二进制,再将二进制反序列化为新对象
流程接口节点主要运行步骤:
• 节点前置处理:RunNodeServiceImpl#processStart
• 节点逻辑处理:AbsAutoRegisterProcessor#process
• 节点后置处理:RunNodeServiceImpl#processFinish
优化前(JSON):
优化后(Protobuf):
优化效果:单个节点平均耗时(processFinish-processStart)降至2ms以内
二进制额外使用场景
使用场景:条件节点每个条件项执行流水,只需要追加记录,不需要修改或删除。
设计思路
以上方式序列化对象列表与普通方式的格式是不兼容的
deserializeArrayWithAppend ≠ deserializeArray
代码示例
将对象元素追加到列表末尾
public static <T> byte[] appendObject(byte[] existingBytes, T obj) {// 序列化新对象byte[] objBytes = serialize(obj);int len = objBytes.length;// 准备4字节长度字节byte[] lenBytes = new byte[ConstantPool.NUMBER_FOUR];lenBytes[ConstantPool.NUMBER_ZERO] = (byte) ((len >> 24) & 0xFF);lenBytes[ConstantPool.NUMBER_ONE] = (byte) ((len >> 16) & 0xFF);lenBytes[ConstantPool.NUMBER_TWO] = (byte) ((len >> 8) & 0xFF);lenBytes[ConstantPool.NUMBER_THREE] = (byte) (len & 0xFF);// 拼接:已有字节 + 4字节长度 + 新对象字节byte[] newBytes = new byte[existingBytes.length + ConstantPool.NUMBER_FOUR + len];System.arraycopy(existingBytes, ConstantPool.NUMBER_ZERO, newBytes, ConstantPool.NUMBER_ZERO, existingBytes.length);System.arraycopy(lenBytes, ConstantPool.NUMBER_ZERO, newBytes, existingBytes.length, ConstantPool.NUMBER_FOUR);System.arraycopy(objBytes, ConstantPool.NUMBER_ZERO, newBytes, existingBytes.length + ConstantPool.NUMBER_FOUR, len);return newBytes;}
将二进制数据反序列化为对象列表
public static <T> List<T> deserializeArrayWithAppend(byte[] data, Class<T> clazz) {List<T> resultList = new ArrayList<>();int index = ConstantPool.NUMBER_ZERO;int totalLen = data.length;while (index + ConstantPool.NUMBER_FOUR <= totalLen) {// 读取对象长度int objLen = ((data[index] & 0xFF) << 24) |((data[index + ConstantPool.NUMBER_ONE] & 0xFF) << 16) |((data[index + ConstantPool.NUMBER_TWO] & 0xFF) << 8) |(data[index + ConstantPool.NUMBER_THREE] & 0xFF);index += ConstantPool.NUMBER_FOUR;if (index + objLen > totalLen) {break;}byte[] objBytes = Arrays.copyOfRange(data, index, index + objLen);T obj = deserialize(objBytes, clazz);resultList.add(obj);index += objLen;}return resultList;}
性能测试
示例数据:样本数据(字符长度=8533)
处理方式 | 序列化(10w次) | 反序列化(10w次) | 追加对象(10w次) |
Protostuff | 2.1 s | 1.9 s | 1s |
效果 | 相比使用Protostuff反复序列化和反序列化,使用直接追加数据的方式,更节省时间(性能可提升4倍)。 | ||
JVM微调
JVM启动参数调优在服务性能优化过程中,更像是“锦上添花”,不能依赖调整JVM参数达到大幅性能提升,应该是针对性地微调,减小GC的影响,使服务处于更健康状态,比如:
• 通过调小-XX:MaxGCPauseMillis,比如50ms,缓解GC时间突刺情况。
• 通过调大-XX:G1HeapRegionSize,减少G1 Humongous Allocation出现次数。
性能优化结果
进线优先级接口:
RT优化过程: 5s以上 => 3s => 500ms => 160ms => 100ms(下降98%)【仅剩无法避免的 IO 耗时】
串行流程接口(50个节点,无外部API调用)
优化前:2.6s(单个节点平均执行耗时30ms)
优化后:90ms(单个节点平均执行耗时2ms以内)
机器数量
通过完成以上优化手段,星图平台从最开始基本无法提供有效的服务,到目前稳定高效地运行,服务性能得到质的提升,服务机器数减少60+%,支撑更高请求处理量,RT(p95)下降70+%,并且服务状况明显更稳定健康。
CPU使用情况
GC平均耗时与异常情况
JVM堆内存使用情况
平台功能介绍
核心功能
接口管理
• 全域覆盖:支持公司内部所有HTTP/SOA/SSE/MCP接口配置接入,无须担心遗漏,实现了快捷关联导入,操作更便捷。
• 版本管理:接口支持多版本管理,调用权限和引用版本都由自己把控,安全又有序。
• 函数管理:内置Groovy脚本编辑功能(在白名单范围内),可以随心所欲地自定义函数,比如单位转换、数据提取,是用于处理请求和响应的“妙招”。
• 字段衍生:使用数据字典或函数方式,对接口已有的字段进行异构生成新的结果字段,特别适合针对某些字段做解析处理。
• 租户隔离:不同业务线之间数据互不干扰,保证数据安全性。
流程编排
• 流程自定义:通过流程编排方式,组装一个或多个接口结果,或者对接口结果二次处理后,再作为参数传入下一个业务接口,甚至无需调用业务接口,单纯使用函数处理并返回结果。
• 并行处理:流程支持多节点并行处理,再将结果进行合并,提高整体效率。
• 黑盒封装:流程对外表现为普通接口,上游无需感知具体实现逻辑,就跟普通接口调用一样得心应手。
• 流程嵌套:已发布的流程接口可以被复用,和使用普通接口无异。
接口调用
• 简化对接:提供简洁的公司内部标准协议接口,能快捷完成配置对接。
• 统一调用:无论是普通接口还是流程接口,都是使用统一的调用入口,方便业务维护管理。
排障支持
• 全链路调用追溯:借助TraceId可追踪历史调用情况,查看请求参数、响应结果等信息,便于问题排查。
• 统计看板:业务租户独立看板,提供单个接口、不同版本和时间范围等纬度统计,一目了然,便于业务分析。
性能实践思考
良好代码习惯:性能优化的核心是设计与实现
程序的设计和实现决定了性能优化的上限,良好的代码习惯是性能优化的核心基石。
• 程序设计与算法优化是关键
○ 合理选择数据结构:确保数据访问的高效性,如使用HashMap代替List进行查找,减少时间复杂度。
○ 减少不必要的计算:缓存重复计算的结果,以空间换时间。
○ 优化算法:针对场景选择最优算法,避免低效遍历或嵌套循环。
○ 合理设计数据流:简化逻辑设计,减少多余的中间操作。
• 避免明显的性能陷阱
○ 过度同步:减少过多的锁竞争和锁等待问题。
○ 频繁I/O操作:优先使用批量读写、异步I/O,避免逐条或频繁处理。
○ 反射滥用:反射机制慢、耗资源,仅在必要情况下使用。
○ 对象创建过多:减少对象的频繁创建和销毁,尽量重用对象(如使用对象池)。
科学分析性能问题:弄清楚具体为什么慢
面对性能问题时,科学分析是优化的前提,需要综合直观指标和辅助工具定位问题。
直观指标:发现性能瓶颈
通过服务的关键性能监控指标,快速判断可能导致性能下降的原因:
• 服务层监控
○ CPU 使用率:高 CPU 使用率可能与线程消耗、大量计算相关。
○ 内存占用:检查内存是否持续增长,是否有内存泄漏风险。
○ 线程池状态:线程是否耗尽,线程等待队列是否过长。
• 中间件监控
○ 数据库层:MySQL 查询时间、慢查询统计,以及并发连接量。
○ 缓存层:Redis 命中率、查询延迟、内存占用等指标。
○ 队列系统:MQ 堵塞、消费延迟等。
辅助工具:追踪问题根源
• 火焰图:对热点代码进行分析,定位耗时最多的函数或方法。
• 线程堆栈(jstack):分析线程阻塞、死锁或高 CPU 消耗来源。
• 动态诊断工具(如 Arthas):实时查看方法耗时、线程状态和运行情况。
• 日志埋点:通过在关键业务逻辑中添加耗时日志,收集并分析性能数据。
性能优化利弊权衡:优化不只是技术任务,更是权衡艺术
性能优化不仅需要技术实践,也需要慎重评估优化的 成本、收益 和 风险。
• 人力成本:性能优化可能需要大量的时间、人力投入,尤其是高复杂度的优化任务。 • 维护成本:一些性能手段可能带来复杂的代码逻辑,增加后续的开发和维护工作量。 | • 性能提升:优化后是否让问题明显改善,如服务响应时间是否显著缩短,QPS(每秒查询量)是否提升。 • 降本增效:性能优化是否能带来计算和存储资源的节约,间接减少运营成本。 | • 架构破坏:优化手段是否破坏了系统原有架构的简洁性,导致系统可维护性下降。 • 复杂性增加:一些深度优化可能引入更复杂的逻辑,例如复杂的多线程协调或缓存一致性问题,埋下新的隐患。 • 收益有限:某些优化可能提升效果不明显,而引入了很多技术债。 |
优化需从全局视角着眼
• 性能优化不是一味追求“更快”,而应平衡 性能 和 可维护性、开发成本 的关系。
• 优化的终点在于 充分满足业务需求即可,避免过度优化带来的技术债和复杂度膨胀。
• 不盲目优化,而是通过科学分析问题用“刀刃上的努力”做到效益最大化。
项目收益
星图平台已成功接入超过100个业务方服务,业务接口700+,自定义流程接口100+,日均调用量500w+,业务落地场景较灵活涵盖常规业务、大模型(SSE/MCP)等场景。
业务收益
通过星图平台对接实现的业务场景有很多,我们简单看下几个典型业务案例。
研发效能
性能优化导向
实现性能提升,应遵循 精准定位问题、科学优化策略、权衡利弊平衡 的原则,注重以下几点:
核心理念:设计与实现是根本
• 高效代码:优化算法、精简逻辑,合理选择数据结构,减少冗余操作和锁竞争。
• 避免性能陷阱:减少频繁I/O、大量反射和不必要的资源消耗。
优化步骤:由外向内,层层深入
1. 改同步为异步:释放阻塞,提升服务效率。
2. 优化并发模型:利用多线程提高资源使用率。
3. 利用缓存机制:结合分布式缓存和内存缓存减少重复查询。
4. 精简代码逻辑:优化程序算法、数据流和资源管理。
科学诊断:精准识别慢点
• 监控指标分析:通过 CPU、内存、线程池、数据库、缓存等发现瓶颈。
• 工具辅助排查:使用火焰图、堆栈分析(jstack)、动态诊断工具(Arthas)深入定位问题。
权衡利弊:适度优化,避免过度
• 成本:考虑开发时间与后续维护代价。
• 收益:确保优化效果显著,能降本增效。
• 风险:避免复杂度增加和隐患引入,保持系统清晰性和可维护性。
漏斗式优化策略
总结:科学定位、逐级优化、不盲目追求极致,重点关注性能需求与性价比的平衡。
稳定性
值得一提的是,星图平台已稳定运行两年,其系统稳定性达到了“4个9”(即99.99%),为业务的连续性提供了坚实保障。这一切助力于业务发展、成本降低及效率提升。
平台规划
结语