性能优化实践分享

背景与挑战

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

  • 性能现状:单机播放控制场景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. 风险控制:保持系统稳定性的前提下进行优化
相关推荐
RoyLin2 小时前
TypeScript设计模式:状态模式
前端·后端·typescript
RoyLin2 小时前
TypeScript设计模式:观察者模式
前端·后端·typescript
RoyLin2 小时前
TypeScript设计模式:备忘录模式
前端·后端·typescript
白衣鸽子2 小时前
PageHelper:分页陷阱避免与最佳实践
后端
BingoGo2 小时前
PHP 和 Elasticsearch:给你的应用加个强力搜索引擎
后端·php
泉城老铁2 小时前
Spring Boot对接抖音获取H5直播链接详细指南
spring boot·后端·架构
武子康2 小时前
大数据-101 Spark Streaming 有状态转换详解:窗口操作与状态跟踪实战 附多案例代码
大数据·后端·spark
数据小馒头2 小时前
企业级数据库管理实战(五):多数据库与异构环境的统一管理
后端
天天摸鱼的java工程师2 小时前
SpringBoot + RabbitMQ + MySQL + XXL-Job:物流系统运单状态定时同步与异常订单重试
后端