技术探索与实践优化:一次从“能用”到“好用”的性能优化实战

断点追踪者
2025-06-30 12:48
阅读 640

引言:为什么我们总是在做“优化”?

引言:为什么我们总是在做“优化”?

在我参与的很多项目中,最常听到的一句话就是:“先跑起来再说”。在产品快速迭代的互联网公司,这种思路无可厚非——只要功能实现了、用户能用上,就达到了第一阶段的目标。

但往往,在某个版本上线之后,大家开始发现:接口变慢了、页面加载卡顿、用户投诉响应延迟……这时候,团队才会意识到:能用 ≠ 好用

今天我想跟大家分享一个真实项目中的优化案例。这不是那种教科书式的“理论推导”,而是一次从实际业务场景出发的技术探索与实践优化之旅。

项目背景:一个高并发搜索服务的诞生

项目背景:一个高并发搜索服务的诞生

这个故事发生在一个电商搜索后台系统中。我们的任务是重构原有的商品搜索服务,目标很明确:

  • 支持高并发访问(QPS 要求达到5000以上)
  • 提升搜索结果准确性
  • 实现多维度筛选、排序和分页逻辑
  • 在不影响现有业务的前提下,逐步上线新架构

系统架构大致如下图所示:

前端 -> Gateway(鉴权、限流) -> SearchService -> ES + MySQL + Redis

一开始,为了赶进度,我们采用了比较常见的方案:使用 Elasticsearch 作为搜索引擎核心,通过 RestHighLevelClient(后来换成了 Java High Level REST Client)与 ES 通信,MySQL 主要是用来补全字段信息,Redis 缓存部分热搜词和用户行为数据。

刚上线时,一切都很顺利。直到某天大促活动开始……

问题描述:大促期间性能骤降

问题描述:大促期间性能骤降

上线后不到一周,正巧赶上年中大促。当天中午流量暴涨,监控数据显示 QPS 瞬间冲到了7000+,然而:

  • 接口平均响应时间从原来的200ms飙到了1s+
  • 部分查询出现超时甚至返回空白结果
  • 日志里频繁出现 ElasticsearchTimeoutException
  • CPU 使用率接近峰值,JVM Full GC 频繁

这下可把我们吓坏了。紧急复盘后发现几个核心问题:

  1. Elasticsearch 查询过于复杂,聚合条件太多
  2. Java 客户端没有正确设置超时参数,导致线程阻塞
  3. 缺乏缓存机制,热点查询重复请求 ES
  4. Redis 缓存键设计不合理,缓存命中率低
  5. JVM 参数设置不科学,GC 成为瓶颈

当时我负责搜索模块的核心调优,压力山大。老板一句话让我印象深刻:“现在用户买不了东西都在骂客服,你们得赶紧搞定。”

解决方案:逐层拆解痛点,针对性优化

一、ES 查询结构优化

我们之前的做法是把所有的查询条件一股脑都丢给 ES,包括大量聚合、filter、nested 查询,导致每次执行都非常耗资源。

做法:

  • 拆分复杂查询,将不必要的 filter 条件提前预处理成布尔值字段索引
  • 减少嵌套查询数量,尽量将 nested 查询转为父子文档关系
  • 对高频查询进行 profile 分析,找出耗时最长的部分

例如,原本的复杂查询结构简化后变为:

{
  "query": {
    "bool": {
      "must": [
        {"match": {"title": "手机"}},
        {"term": {"category_id": "123"}}
      ],
      "filter": [
        {"range": {"price": {"gte": 1000, "lte": 3000}}},
        {"terms": {"brand_ids": [1, 2, 3]}}
      ]
    }
  },
  "aggs": {
    "brands": {"terms": {"field": "brand_id"}}
  }
}

开发流程示意-1

这样做的好处是利用了 filter 的缓存特性,同时保持 query 的灵活性。

二、客户端超时控制与连接池配置

我们之前使用的是默认的 client 配置,没有任何超时限制,一旦某个查询卡住,整个线程都会被阻塞。

解决方法:

  • 设置合理的 connection timeout 和 socket timeout
  • 启用连接池并合理调整最大连接数
  • 加入断路器(Hystrix 或 Resilience4j)

代码示例(使用 Spring Boot + RestClient):

@Bean
public RestClient restClient() {
    return RestClient.builder(
            new HttpHost("es-host", 9200, "http"))
        .setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder
            .setConnectTimeout(2000)
            .setSocketTimeout(3000))
        .setMaxRetryTimeoutMillis(5000)
        .build();
}

这大大提升了系统的稳定性。

三、Redis 缓存设计升级

最初我们用了非常简单的 key 设计,比如 "search_result:keyword",但这显然不适合动态条件查询。

新的缓存策略:

  • 使用一致性哈希算法对关键词和参数进行 hash
  • 将组合条件映射为固定格式的 key,如 "search:kw=phone&cat=123&price=1000-3000"
  • 设置分级缓存 TTL(热点缓存更长,冷门更短)

另外,我们还加了一层本地缓存(Caffeine),用于存储一些极热查询的结果,进一步降低 Redis 压力。

四、JVM 调优与异步日志

线上 GC 频繁的根本原因有两个:

  1. JVM 内存太小,堆空间不足
  2. 日志写入方式阻塞主线程

调优手段:

  • 升级堆内存至 -Xms4g -Xmx6g
  • 使用 G1 垃圾回收器(CMS 已弃用)
  • 将 logback 的日志输出改为异步模式

logback.xml 示例片段:

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

<appender name="ASYNC_STDOUT" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="STDOUT"/>
</appender>

<root level="info">
    <appender-ref ref="ASYNC_STDOUT"/>
</root>

五、引入限流与熔断机制

最后,我们在网关层做了两件事:

  • 使用 Guava RateLimiter 进行本地限流
  • 使用 Resilience4j 在下游服务失败时自动熔断降级

这些措施让我们的搜索服务更加健壮,即使在高峰时段也能维持稳定。

开发过程中遇到的一些坑

坑1:ES 分片分布不均引发局部热点

上线初期,我们的索引只设置了3个主分片,但数据分布严重不均,某些 shard 大得离谱。后来我们将分片数增加到6,并配合 _reindex 重新打散数据,效果明显提升。

坑2:本地缓存占用过多内存,导致 OOM

我们最初直接用了 Caffeine 的 maximumSize 参数,但误以为只是限制条目数,实际上每个对象可能很大,导致堆内存暴增。后来改成了 maximumWeight 并配合权重函数来控制。

坑3:Redis Pipeline 未正确使用,导致性能下降

有段时间我们误以为使用 pipeline 必然提高性能,但实际上如果 batch 数据量太大或网络不稳定,反而会拖慢整体吞吐。最终我们采用“自动批处理 + pipeline”的混合方案,才真正发挥了作用。

效果总结:性能大幅提升,业务价值显现

经过一轮优化,整体效果如下:

指标 优化前 优化后
平均响应时间 ~1100ms ~220ms
P99 Latency ~2500ms ~500ms
GC 频率 每分钟 2~3 次 每小时几次
QPS 承载 ~2500 >7000
用户反馈 经常卡顿 几乎无卡顿

最关键的是:在接下来的大促中,搜索服务扛住了超过10倍以上的日常流量,没有出现任何重大故障。

我的经验总结与建议

如果你也遇到了类似的性能瓶颈,可以参考以下几个方向去尝试:

✅ 性能优化要抓住主要矛盾

  • 先看瓶颈在哪(CPU/IO/Memory/网络)
  • 不要一开始就盲目动手,分析监控数据才是王道
  • 优先关注 QPS 最高、响应最慢的接口

✅ 架构设计也要面向未来

  • 要考虑到扩容成本和技术债务
  • 有时候看似简单的实现可能会埋雷,比如聚合查询、深度分页等
  • 合理抽象接口层,便于后续升级替换(比如从旧版 client 换到新的)

✅ 工具很重要,但也别过度依赖

  • 监控系统(Prometheus + Grafana)、链路追踪(SkyWalking)、日志平台(ELK)一定要配齐
  • 但也要自己掌握基本的排查能力,不能光靠工具

✅ 多一点耐心,少一点急躁

性能优化不是一蹴而就的过程,尤其是在线上环境。我们当时整整调试了一个月才完成所有优化动作。中间无数次推翻重来、加班熬夜。不过当你看到系统越来越稳、用户反馈越来越好,那种成就感真的难以形容。

写在最后:技术探索的意义在于创造价值

回头看这次经历,其实它不仅仅是对搜索服务的优化,更是我对“技术如何服务于业务”的一次深刻理解。

很多时候,我们开发的功能“能用”只是一个起点。“好不好用”、“能不能抗压”、“有没有弹性”,才是真正决定用户体验的关键因素。

在这个高速变化的行业里,持续的技术探索和实践优化,是我们每一位开发者都应该坚持的方向。

希望这篇文章能给你带来一点点启发,也欢迎你在评论区分享你的优化经验,一起探讨更好的方案。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝