从一次线上故障说起:我的技术探索与实践之路

快乐鸟
2025-06-14 23:45
阅读 348

引言:问题总是来得猝不及防

那是去年初冬的一个深夜,我正准备上床休息,手机突然震动起来,钉钉上跳出一条红色的告警通知——核心业务接口响应超时报警。凌晨一点多,这个时间点发生服务异常,通常是大事。我立刻爬起来查看监控,发现某个关键的服务模块在持续报错,QPS(每秒请求量)骤降,用户反馈也开始增多。

这种场景,我想很多后端开发者都不陌生:突发性的性能瓶颈、日志里密密麻麻的异常、还有你面对问题时那种“说不出来但心里慌得一逼”的感觉。

后来复盘发现,这其实是一个典型的技术债爆发事件,而解决它的过程也让我对技术探索与实践有了更深层次的思考。


项目背景:微服务架构下的流量压测失败

我们公司是一家做电商导购类产品的互联网公司,主站日均UV超过百万级,后端使用的是Spring Cloud + Kubernetes的微服务架构体系。整个系统拆分成了几十个独立的服务模块,包括商品推荐、用户中心、积分系统、订单管理等。

2023年初,产品团队计划在年底大促前上线一个“限时拼团”新功能,并希望提前做好高并发压测准备。为了保证系统稳定性,我负责组织这次全链路压测工作。

当时我们用的是JMeter进行压测模拟,压测目标是达到1万TPS。但实际测试中,有一个核心服务在8000 TPS左右就开始频繁出现“Connection reset”和“Socket timeout”,甚至偶尔触发了Hystrix熔断。

更严重的是,我们还观察到数据库连接池被打爆,线程堆积严重,Redis缓存命中率急剧下降……


遇到的挑战:不只是性能问题那么简单

我们一开始怀疑是某个慢SQL导致的延迟,于是花了半天时间查慢查询日志,优化了几个表结构并添加了索引。可压测结果并没有太大改观。

接下来又怀疑是不是JMeter配置不当,比如并发数设置不合理、线程组策略不合适等。我们换了Gatling做压测验证,结果还是差不多。

进一步排查发现:

  • 数据库连接池(HikariCP)配置过小
  • Spring Boot中默认的Tomcat线程池配置太低
  • 接口内部存在大量串行调用,没有充分利用异步能力
  • Redis缓存未合理利用,部分热点数据重复获取
  • 微服务间调用链较长,缺乏合理的限流与熔断机制

这已经不是单纯的性能优化问题,而是架构层面的一些历史欠账开始显现。


技术选型与实现思路:一场多维度的协同改进

第一步:压测工具升级 + 基准指标标准化

我们决定使用Prometheus+Granfana搭建统一的监控体系,并引入SkyWalking作为APM工具来分析调用链。

最终我们选择了Apache Benchmark + Locust + SkyWalking组合拳的方式来做压测:

ab -n 10000 -c 500 http://api.service.com/v1/groupbuy/detail?groupId=1001

同时使用Locust写Python脚本来模拟真实用户行为路径:

from locust import HttpUser, task

class GroupBuyUser(HttpUser):
    @task
    def detail(self):
        self.client.get("/v1/groupbuy/detail?groupId=1001")
    
    @task(3)
    def list(self):
        self.client.get("/v1/groupbuy/list?page=1&pageSize=20")

技术原理图-1

这样既兼顾了高并发场景的压力生成,也模拟了真实用户的行为路径。

第二步:线程模型与异步化重构

原来的接口逻辑是典型的阻塞式调用方式,比如这样的代码:

public GroupBuyDetailVO getGroupDetail(String groupId) {
    Product product = productClient.getProduct(groupId);
    User user = userClient.getUserInfo(userId);
    List<Comment> comments = commentRepository.findByGroupId(groupId);
    
    return buildVO(product, user, comments);
}

三个子系统调用之间完全串行。我们通过引入Reactive编程,采用CompletableFuture进行任务编排,改造后如下:

public CompletableFuture<GroupBuyDetailVO> asyncGetGroupDetail(String groupId) {
    CompletableFuture<Product> productFuture = CompletableFuture.supplyAsync(() -> productClient.getProduct(groupId));
    CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userClient.getUserInfo(userId));
    CompletableFuture<List<Comment>> commentFuture = CompletableFuture.supplyAsync(() -> commentRepository.findByGroupId(groupId));

    return productFuture.thenCombine(userFuture, (product, user) -> buildVO(product, user, null))
                        .thenCombine(commentFuture, (vo, comments) -> {
                            vo.setComments(comments);
                            return vo;
                        });
}

效果非常明显,接口平均耗时从720ms降低到了340ms左右。

第三步:数据库与缓存优化

HikariCP连接池调优:

我们将原有的最小连接池从10提升至50,最大值根据预估负载设为200:

spring:
  datasource:
    hikari:
      minimum-idle: 50
      maximum-pool-size: 200
      idle-timeout: 600000
      max-lifetime: 1800000

Redis缓存穿透与击穿防护:

引入Guava本地缓存作为二级缓存,结合Redis一起使用:

Cache<String, Object> localCache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build();

然后在获取数据的方法中优先查本地缓存:

Object data = localCache.getIfPresent(key);
if (data == null) {
    data = redisTemplate.opsForValue().get(key);
    if (data != null) {
        localCache.put(key, data);
    }
}

对于热点数据加锁访问,防止雪崩和击穿问题。

第四步:服务治理增强

我们使用Spring Cloud Alibaba的Sentinel组件做服务限流和熔断控制,在Nacos中集中管理熔断规则:

spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080
      eager: true

并通过Sentinel控制台设置了资源级别的QPS阈值和异常比例熔断策略。


踩过的坑:那些痛彻心扉的记忆

在整个过程中,我们也踩了不少坑,有几个印象特别深刻的教训值得分享:

坑1:压测环境没隔离好

有一次我们在非生产环境压测,结果不小心把数据库服务器CPU打满,连带影响了其他服务。原因是测试数据量太少,导致索引失效,全表扫描导致CPU飙升。

解决方案:后续我们在压测环境中建立了专门的测试数据集,使用mockaroo生成几百万条高质量测试数据,并严格限制压测范围。

坑2:Redis长链接泄漏

在优化异步逻辑时,我们忘记关闭某些Redis操作的自动重试机制,导致连接泄露,最后连接池被占满。

解决方案:增加Redis客户端连接状态的健康检查,设置合理的超时时间和自动释放机制。

坑3:线程池配置不当引起OOM

刚开始尝试使用FixedThreadPool,结果在高峰期因任务队列满了导致拒绝任务,后来切换成CachedThreadPool又因为创建线程过多导致OOM。

最终方案:改为使用ThreadPoolTaskExecutor,并配合自定义拒绝策略处理溢出任务。

@Bean("asyncExecutor")
public ExecutorService asyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(50);
    executor.setMaxPoolSize(200);
    executor.setQueueCapacity(1000);
    executor.setThreadNamePrefix("ASYNC-");
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    executor.initialize();
    return executor;
}

成果展示:性能提升显著,业务稳定落地

经过一轮完整的技术改造和压测优化后,我们的核心接口表现如下:

指标 改造前 改造后
平均响应时间(P95) 1.2s 340ms
QPS峰值 6800 14000
熔断次数/小时 5次 无熔断
Redis缓存命中率 65% 92%
日均错误日志数量 2000+ 条 <200 条

这些数据直接支撑了当年“双十二”的顺利上线,拼团活动期间未出现任何重大故障,订单转化率提升了22%,GMV增长17%,技术保障得到了产品团队的一致认可。


经验总结:技术探索要“知其然,更要知其所以然”

在这次项目中,我有几点非常深刻的体会,想和大家分享一下:

1. 技术探索不能脱离业务场景

很多新技术听起来很炫酷,但在实际业务中并不是每个都适用。比如此次我们考虑过引入Apache Pulsar作为消息中间件,但考虑到Kafka已经在使用中,并且能满足当前业务需求,最后就没有贸然更换。

建议:选型要基于现有团队的能力、系统的成熟度、未来的扩展性综合评估。

2. 技术债必须早清理

很多性能问题都是老代码埋的雷,比如早期没有设计缓存层、异步化不足、数据库设计不规范等等。等到业务规模上去了才想起优化,代价往往很高。

建议:每次迭代都要预留一些技术债务修复的时间,做到可持续发展。

3. 监控永远比优化更重要

没有监控就谈不上真正的稳定性保障。我们之前有个API响应忽快忽慢,怎么也查不出问题,后来接入SkyWalking才发现是某个外部服务调用引起的毛刺。

建议:尽早引入分布式追踪、统一日志收集、链路分析等基础设施。

4. 实践比理论更能锻炼人

再多的设计模式、架构理论,不如一次线上问题的实战排查。每一次“救火”背后,都是成长的机会。

感悟:不要怕遇到问题,就怕没有解决问题的能力。


写在最后:做有温度的技术人

技术应用场景-2

这篇文章写到这里,其实也是一次自我梳理和复盘。从最初的焦虑、无助,到最后看到性能报告上那个满意的数字,整个过程就像一次“程序员的心灵修行”。

我始终相信一句话:

技术是手段,业务是目的,用户体验才是价值所在。

作为一名技术负责人,我不仅要关注一行行代码的优雅,更要看清它背后的业务价值和用户影响。

希望这篇来自一线战场的真实经验分享,能给正在阅读的你带来一些启发和思考。

如果你也有类似的经历或者想法,欢迎留言交流,一起成长。


附录:相关开源工具推荐

工具 地址 用途
Prometheus https://prometheus.io/ 监控采集
Grafana https://grafana.com/ 可视化看板
SkyWalking https://skywalking.apache.org/ 分布式追踪
Locust https://locust.io/ 压力测试
Nacos https://nacos.io/ 配置中心
Sentinel https://sentinelguard.io/ 流量控制

作者微信公众号:CodeClimber

如需获取文中涉及的源码或配置文件,请关注公众号回复【tech-explore】获取。


作者简介
某大型电商平台技术负责人,多年Java后端研发及架构经验,专注于高并发系统优化、分布式系统设计与DevOps体系建设。

评论 0

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