性能优化实践分享

背景与挑战

我们项目在上线初期面临严重的性能瓶颈:

  • 性能现状:单机播放控制场景QPS仅为15左右,远低于预期
  • 技术特点:采用流式输出、大量长连接的架构
  • 业务压力:计划将400-500万用户接入平台
  • 核心风险:性能瓶颈成为平台上线的关键阻碍

通过系统性的性能优化,最终实现了单机QPS从15提升至70,平台可稳定支撑500万用户并发访问的显著改善。

优化方法论

分析工具与手段

  • JMeter:压力测试与性能基准测试
  • JProfiler:运行时性能监控与热点分析
  • MAT (Memory Analyzer Tool) :内存泄漏检测与对象分析
  • 系统监控:GC日志分析、线程状态监控

优化策略

采用多维度系统性优化策略,包括JVM调优、中间件优化、代码重构、架构调整等。


核心优化措施

一、缓存策略优化

问题分析:项目中存在大量高频查询场景未使用缓存,导致数据库压力过大,查询性能低下。

优化方案

  • 识别高频查询接口和数据热点
  • 为核心查询场景添加多级缓存
  • 实施缓存预热和失效策略
  • 建立缓存命中率监控机制

效果:显著减少数据库查询压力,提升响应速度。

二、流式输出技术选型对比

SseEmitter vs Flux 技术对比

维度 SseEmitter Flux (WebFlux)
编程模型 Servlet阻塞式 响应式编程
线程模型 每连接占用线程 非阻塞事件驱动
并发性能 中低并发场景适用 高并发表现优秀
学习成本 低,传统MVC风格 高,需响应式编程经验
兼容性 老项目友好 最适合WebFlux+Netty

实践结论

通过构建对照实验,发现在固定文本返回场景下,WebFlux性能虽有提升但未达到数量级差异。考虑到:

  • 代码重构成本较高
  • 团队技术栈适配度
  • 项目交付时间约束

决策:继续使用SseEmitter,通过其他维度优化达成性能目标。

三、工作流引擎优化

问题识别:MAT内存分析显示工作流执行链占用大量内存。

3.1 WorkflowId生成策略优化

原始方案:使用UUID生成唯一标识

ini 复制代码
// 问题:每次生成全新ID,无法复用工作流结构
String workflowId = UUID.randomUUID().toString();
​

优化方案:基于EL表达式生成确定性ID

ini 复制代码
// 解决:相同EL表达式生成相同ID,支持工作流复用
String workflowId = DigestUtils.md5Hex(elExpression);
​

优化效果

  • 内存优化:相同工作流结构复用,避免重复创建
  • 性能提升:跳过重复解析和编译步骤
  • 缓存友好:提高工作流引擎缓存效率

3.2 异步执行线程池优化

问题:LiteFlow默认线程池配置(核心16,最大64)在高并发场景下不足。

优化方案

java 复制代码
public class LiteFlowConfig {
    // 定义自定义线程池
    @Bean(name = "customLiteFlowExecutor")
    public ExecutorService customLiteFlowExecutor() {
        // 自定义线程工厂,设置线程名称前缀
        ThreadFactory threadFactory = new ThreadFactory() {
            private final AtomicInteger threadNumber = new AtomicInteger(1);
​
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("LiteFlow-Worker-" + threadNumber.getAndIncrement());
                return thread;
            }
        };
        // 配置线程池参数
        return new ThreadPoolExecutor(
            32, // 核心线程数
            128, // 最大线程数
            60L, TimeUnit.SECONDS, // 空闲线程存活时间
            new ArrayBlockingQueue<>(200), // 有界队列,容量200
            threadFactory, // 自定义线程工厂
            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:回退到调用者线程
        );
    }
    // 配置 LiteFlow 执行器,注入自定义线程池
    @Bean
    public LiteflowExecutor liteflowExecutor(ExecutorService customLiteFlowExecutor) {
        LiteflowExecutor executor = LiteFlowProxyUtil.getInstance(LiteflowExecutor.class);
        executor.setExecutorService(customLiteFlowExecutor);
        return executor;
    }
}

四、线程池与并发优化

4.1 系统架构与资源配置

  • 硬件配置:4核8G云服务器
  • 部署架构:EA(入口服务) + SA(核心处理服务),容器化部署
  • 并发模式:大量SSE长连接 + 流式输出

4.2 串行处理改并行优化

问题:EA与SA任务串行执行,响应时间过长

优化方案:引入专用线程池实现任务并行

less 复制代码
// 并行处理,提升响应速度
            eaExecutor.execute(() ->{
             ......
            });
​
            saExecutor.execute(() ->{
             ......
            }
​

4.3 关键线程池参数调优

组件 原始配置 优化配置 调优理由
Tomcat最大线程 200 500 SSE长连接需要更多线程支撑
任务队列大小 10000 100 减少任务堆积,避免超时(>10s)
EA线程池 默认 核心100/最大200 匹配请求入口压力
SA线程池 默认 核心200/最大400 承载核心业务处理
HttpClient连接池 DefaultMaxPerRoute=100 400 避免EA->SA调用瓶颈

五、内存泄漏排查与修复

5.1 ShutdownHook内存泄漏

问题发现:压测结束后内存持续占用4G+,MAT分析发现大量shutdownHooks对象堆积。

根因分析

less 复制代码
// 问题代码:每次请求都注册新的ShutdownHook
Runtime.getRuntime().addShutdownHook(new Thread(eventSource::cancel));
​

问题机制

  • 高并发下每次执行都创建新Thread对象
  • Hook持有eventSource引用,阻止GC回收
  • JVM关闭前不会清理,造成内存泄漏

解决方案

less 复制代码
                // 删除ShutdownHook,在连接关闭时手动清理资源
                try {
                    // 业务逻辑处理
                } finally {
                    eventSource.cancel(); // 手动清理资源
                }
                @Override
                public void onFailure(EventSource eventSource, @Nullable Throwable t, @Nullable Response response) {
                    eventSource.cancel();
                }
​

5.2 OkHttpClient线程泄漏

问题:JProfiler显示压测期间线程数达到2000+,其中1000+由OkHttpClient创建。

根因

scss 复制代码
// 问题:每次调用都创建新的OkHttpClient实例
OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(timeout, TimeUnit.MILLISECONDS)
    .readTimeout(timeout, TimeUnit.MILLISECONDS)
    .writeTimeout(timeout, TimeUnit.MILLISECONDS)
    .build();
​

解决方案

scss 复制代码
// 复用现有OkHttpClient实例,避免重复创建
OkHttpClient client = okHttpClient.newBuilder()
    .connectTimeout(timeout, TimeUnit.MILLISECONDS)
    .build();
​

优化效果:峰值线程数从2000+降至600。

六、中间件性能调优

6.1 Kafka参数优化

问题:JProfiler显示大量Kafka阻塞线程,影响整体性能。

优化配置

ini 复制代码
# 提升吞吐量,降低延迟
kafka.producer.acks=0          # 原值:-1,降低可靠性要求
kafka.producer.linger.ms=1000  # 原值:1,批量发送提升效率
​

拒绝策略调整

  • 原策略:CallerRunsPolicy(主线程执行)
  • 新策略:DiscardPolicy(直接丢弃)
  • 理由:避免影响主线程性能,数据重要性相对较低

6.2 ID生成策略优化

问题:MongoDB串行获取ID导致加锁排队,效率低下。

解决方案:引入雪花算法

  • 避免数据库IO操作
  • 消除串行生成ID的阻塞
  • 保证ID全局唯一性
  • 提升高并发场景下的ID生成效率

七、连接管理优化

7.1 连接泄漏修复

问题发现:使用netstat命令检查发现大量连接未正常关闭

perl 复制代码
for %i in (ESTABLISHED LISTENING TIME_WAIT CLOSE_WAIT SYN_SENT) do @echo %i: & netstat -ano | findstr %i | find /c /v ""
​

根因:异常情况下SseEmitterUTF8连接未正确关闭。

解决方案:完善异常处理机制,确保连接在异常状态下也能正确释放。


压测实践经验

一、JMeter配置要点

关键参数设置

  • 线程数设置:从50开始逐步调整,避免设置过大(如400)
  • 原因分析:流式长连接项目响应较慢,过高并发可能导致测试不准确
  • 建议策略:渐进式压力测试,找到系统真实瓶颈点

环境隔离优化

单机压测问题:JMeter本身资源消耗可能影响被测服务性能

解决方案

  • 测试机与服务机分离部署
  • 独立的网络环境避免带宽竞争
  • 监控JMeter自身资源使用情况

二、熔断机制处理

压测期间熔断策略

临时关闭熔断的必要性

  1. 压测环境特殊性

    • 请求量远超正常负载
    • 服务响应时间自然增长
    • 熔断机制可能误判服务不可用
  2. 熔断对测试的影响

    • 拒绝部分请求,无法反映真实性能
    • 无法测试系统极限承载能力
    • 影响性能指标的准确性
  3. 临时关闭的好处

    • 确保请求完整到达目标服务
    • 避免测试结果偏差
    • 准确测量系统真实性能表现

线程池使用规范

核心原则

  1. 优先自定义ThreadPoolExecutor:避免Executors默认(如newFixedThreadPool的无界队列易OOM)。

理由:灵活控制队列/拒绝策略。

  1. 设置有界队列

理由:防积压,早拒绝。

  1. 自定义ThreadFactory:线程名前缀(如"EA-Task-")。

理由:便于JProfiler追踪/调试。

  1. 拒绝策略选择:非关键任务用DiscardPolicy;关键用CallerRunsPolicy。

理由:平衡性能与可靠性。

  1. 指定线程池: 在使用 @Async 或其他异步方式(如 CompletableFuture、LiteFlow 等)时,最好显式指定线程池,而不是依赖默认线程池。

理由:显式指定线程池能精确控制资源、优化性能、便于监控,避免默认配置的"黑盒"风险。


总结与展望

优化成果

  • 线程
  • 性能指标:单机QPS从15提升至70,提升366%
  • 用户支撑:平台稳定支撑500万用户并发访问
  • 系统稳定性:解决内存泄漏、连接泄漏等关键问题
  • 资源利用率:通过参数调优充分利用硬件资源

优化方法总结

  1. 系统性分析:结合多种工具进行全面性能诊断
  2. 分层优化:从JVM、框架、应用代码多层面优化
  3. 数据驱动:基于压测数据和监控指标进行决策
  4. 风险控制:保持系统稳定性的前提下进行优化
相关推荐
IT_陈寒15 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码16 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo16 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志17 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..17 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects17 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰17 小时前
一些基础和问题解决
后端
陈随易18 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN18 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp
Imchendiana18 小时前
《狂人日记NO.9》— 前后端一把梭,我的全栈实录
前端·后端