技术探索与实践优化:一次从“能用”到“好用”的性能优化实战
引言:为什么我们总是在做“优化”?

在我参与的很多项目中,最常听到的一句话就是:“先跑起来再说”。在产品快速迭代的互联网公司,这种思路无可厚非——只要功能实现了、用户能用上,就达到了第一阶段的目标。
但往往,在某个版本上线之后,大家开始发现:接口变慢了、页面加载卡顿、用户投诉响应延迟……这时候,团队才会意识到:能用 ≠ 好用。
今天我想跟大家分享一个真实项目中的优化案例。这不是那种教科书式的“理论推导”,而是一次从实际业务场景出发的技术探索与实践优化之旅。
项目背景:一个高并发搜索服务的诞生

这个故事发生在一个电商搜索后台系统中。我们的任务是重构原有的商品搜索服务,目标很明确:
- 支持高并发访问(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 频繁
这下可把我们吓坏了。紧急复盘后发现几个核心问题:
- Elasticsearch 查询过于复杂,聚合条件太多
- Java 客户端没有正确设置超时参数,导致线程阻塞
- 缺乏缓存机制,热点查询重复请求 ES
- Redis 缓存键设计不合理,缓存命中率低
- 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"}}
}
}

这样做的好处是利用了 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 频繁的根本原因有两个:
- JVM 内存太小,堆空间不足
- 日志写入方式阻塞主线程
调优手段:
- 升级堆内存至
-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