技术探索与实践踩坑记录:从线上故障说起

写码不秃头
2025-06-18 15:04
阅读 433

开篇:为什么要分享这个话题?

开篇:为什么要分享这个话题?

作为技术团队负责人,我经常跟团队成员讲:“写代码不可怕,可怕的是不知道问题出在哪。”在日常工作中,我们每天都在和各种各样的技术细节打交道。无论是开发新功能、部署服务,还是优化性能,总会遇到一些看似“小问题”却足以让人彻夜难眠的挑战。

这篇文章想结合一次真实经历,聊聊我在一个关键项目上线过程中踩过的那些坑。我会从项目背景出发,讲到实际中遇到的问题、我们是如何一步步排查并解决它们的,再到最终取得的效果和经验教训。希望通过这篇技术笔记,能让更多开发者少走弯路,也欢迎大家评论交流,一起探讨最佳实践。


项目背景:一次微服务架构升级

系统架构设计-1

项目背景:一次微服务架构升级

2023年年初,我们团队承接了一个重要任务:将原有的单体应用拆分为多个微服务,并迁移到 Kubernetes 集群上运行。原来的系统是一个基于 Spring Boot 的电商后台,涵盖用户管理、订单处理、支付接口、消息队列等多个模块。随着业务增长,单体架构已经明显影响到了系统的可维护性和扩展性,所以我们决定启动这一轮架构改造。

整体规划如下:

  • 使用 Spring Cloud + Dubbo 做微服务治理
  • 数据库按域拆分,使用 MySQL 分库分表方案
  • 消息中间件由 RabbitMQ 切换为 Kafka
  • 容器编排使用 Kubernetes
  • 引入 Prometheus 做监控告警

听起来很完美对吧?但在实施过程中,我们遇到了一系列意料之外的问题。


第一个坑:Kubernetes 服务发现异常导致调用失败

第一个坑:Kubernetes 服务发现异常导致调用失败

问题描述

项目拆分完成后,我们将第一个服务 order-service 推送到了 Kubernetes 环境运行,并通过 Spring Cloud Gateway 对外暴露接口。一切看起来都很顺利,直到 QA 同学反馈:调用 /order/create 接口时偶尔会报错,提示 User service not available

我们一看日志,确实是 order-service 调用了 user-service 失败,但奇怪的是 user-service 是正常运行的。而且并不是每次都失败,而是有一定的概率出现这个问题。

排查过程

  1. 初步分析:我们认为可能是服务注册/发现配置有问题。因为我们在使用 Eureka + Ribbon 做服务注册与负载均衡。
  2. 查看服务状态:kubectl get svc, pod 等命令显示所有服务都正常运行。
  3. 检查 Eureka 注册情况:user-service 已经成功注册到 Eureka Server 上。
  4. 日志追踪:order-service 在调用 user-service 时,有时候返回的是空实例列表,导致触发 LoadBalancerException

这说明 load balancer 拿不到可用实例。那问题来了:为什么服务明明运行着,却拿不到它的地址呢?

解决方案

进一步排查后我们发现问题出在服务健康检查机制上。Eureka 默认只依赖心跳来判断服务是否存活,而我们的 user-service 启动时间比较长(大约要3分钟),刚启动时虽然注册上了,但还没完全准备好接收请求。

解决方案:

  • 在 user-service 中增加自定义健康检查逻辑,并上报 /actuator/health 状态给 Eureka;
  • 修改 ribbon 的配置,让其只选择处于 UP 状态的服务实例:
ribbon:
  NIWSServiceInfoEnabled: true
  listOfServers: http://localhost:8761/eureka/
  DeploymentContextBasedVipAddresses: user-service
  • 同时调整了 health check 的 timeout 时间:
@Bean
public HealthCheckHandler healthCheckHandler() {
    return new CustomHealthCheck();
}

其中 CustomHealthCheck 实现了对数据库连接池、外部API连通性的检测逻辑。

做完这些修改后,调用成功率明显上升,基本解决了服务发现不稳定的问题。


第二个坑:Kafka 消费积压严重

第二个坑:Kafka 消费积压严重

问题描述

第二个棘手问题是 Kafka 消费者积压严重。我们在 order-service 内部通过 Kafka 发布订单创建事件,供其他服务消费(比如通知用户、更新库存等)。但是在压测环境中,我们发现消息堆积越来越多,消费者无法及时处理。

排查过程

我们先确认了生产端没有问题,每秒大约产生几百条消息。然后去看消费者的处理逻辑,发现每次消费都要去调用几个外部 API,耗时较长(平均每个消息处理时间超过500ms)。

与此同时,消费者线程数不足、拉取消息间隔设置过长,加剧了积压问题。

解决方案

我们做了如下优化:

1. 提高并发消费能力

在 Spring Kafka 配置中增加 concurrency 参数:

spring:
  kafka:
    consumer:
      concurrency: 5

这样相当于启动5个消费者线程同时工作,大大提升了吞吐量。

2. 批量消费 + 异步处理

我们改用了批量拉取的方式处理消息,并配合线程池做异步处理:

@Bean
public ConsumerFactory<String, String> orderConsumerFactory() {
    Map<String, Object> props = new HashMap<>();
    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker");
    props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    return new DefaultKafkaConsumerFactory<>(props);
}

@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> batchOrderKafkaListenerContainerFactory(
        ConsumerFactory<String, String> orderConsumerFactory) {
    ConcurrentKafkaListenerContainerFactory<String, String> factory =
            new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(orderConsumerFactory);
    factory.setBatchListener(true); // 支持批量消费
    factory.getContainerProperties().setPollTimeout(3000);
    return factory;
}

并在 listener 中实现批量消费逻辑:

@KafkaListener(topics = "order-topic", containerFactory = "batchOrderKafkaListenerContainerFactory")
public void consume(List<ConsumerRecord<?, ?>> records) {
    logger.info("Received a batch of {} messages", records.size());
    
    ExecutorService executor = Executors.newFixedThreadPool(10);
    
    for (ConsumerRecord<?, ?> record : records) {
        executor.submit(() -> processMessage(record));
    }

    executor.shutdown();
}

3. 消费者自动提交改为手动提交

为了防止消息丢失或重复消费,我们关闭了自动提交:

enable.auto.commit: false

并在消费完成后手动提交 offset:

void processMessage(ConsumerRecord record) {
    try {
        // 处理业务逻辑
        ...
        acknowledgment.acknowledge(); // 手动提交
    } catch (Exception e) {
        log.error("Failed to process message: {}", record.value(), e);
    }
}

这些改动之后,积压问题得到了极大缓解,处理速度提升了将近3倍。


第三个坑:Prometheus 监控延迟过高

当我们引入 Prometheus 做服务监控时,原本是为了更早发现问题。结果反而被它“坑”了一把。

问题现象

在某个上线后的早晨,运维报警说大量服务 CPU 爆满。但我们登录进去看的时候,CPU 并不高,Prometheus 数据显示有延迟,可能数据是几小时前的。

排查过程

  1. 查看 Prometheus 日志,发现频繁超时;
  2. 检查抓取目标数量,发现已经超过了默认限制(默认是1000个);
  3. Prometheus 存储压力大,磁盘 IO 很高;
  4. 查询响应慢,前端 Grafana 加载图表卡顿。

解决方案

针对这些问题,我们做了以下几点优化:

1. 分片采集 + 调整 scrape 配置

将 Prometheus 拆分为多个实例,分别负责不同区域的服务,比如基础组件、业务服务、中间件等。并通过 relabeling 过滤掉不必要的指标。

scrape_configs:
  - job_name: 'business-services'
    static_configs:
      - targets: ['service-a', 'service-b']
    relabel_configs:
      - source_labels: [__address__]
        action: keep
        regex: .*

2. 调整存储策略

使用 Thanos 或 VictoriaMetrics 替代原生 Prometheus,支持长期存储和横向扩展。

3. 控制指标粒度

避免开启过多 metrics,例如可以过滤掉 HTTP 请求相关的详细指标(如 status code、method 等),只保留整体成功率和平均耗时即可。

management.metrics:
  enable:
    web.server.request: false

4. 增加缓存与限流机制

在 Prometheus 前面加了个反向代理(Nginx)做缓存和限流:

location /api/v1/query_range {
    proxy_cache prometheus_cache;
    proxy_cache_valid 200 302 10m;
}

这些措施大幅降低了 Prometheus 的压力,监控数据准确性和实时性也得到了保障。


总结与收获

这次项目的迁移过程对我们整个技术团队来说是一次难得的锻炼机会。虽然中间踩了很多坑,但也积累了不少宝贵的经验:

  • 服务发现不能仅靠心跳,健康检查必须做细;
  • Kafka 消费需要合理控制并发和拉取频率,异步处理是关键;
  • Prometheus 不是万能药,得根据规模灵活应对;
  • 技术选型要考虑后期的可观测性和扩展性;
  • 任何系统都不能照搬书本上的做法,一定要结合实际场景来做权衡。

最后一点很重要 —— 我们常常会因为某个新技术流行就急着上马,但实际上很多“高级玩法”背后都有隐藏的成本。比如服务网格 Istio,我们其实也在评估阶段犹豫了好久,但考虑到当前团队能力和运维负担,最终选择了更简单的方案。


给读者的一些建议

如果你正在做类似的架构升级或微服务改造项目,下面几点建议或许对你有所帮助:

  1. 不要盲目追求新技术:适合自己的才是最好的。比如 Spring Cloud Alibaba 和 Dubbo 虽然强大,但如果团队没经验,不如先用 Spring Cloud Netflix;
  2. 做好充分的灰度测试:上线前务必要做全链路压测,尤其要注意服务之间的依赖关系;
  3. 注重可观测性设计:日志、监控、追踪三位一体,缺一不可;
  4. 多做预案,少埋隐患:上线初期可以保守点,比如保留降级开关、熔断机制等;
  5. 定期复盘总结:每一个踩过的坑都是团队成长的机会。

写到这里,突然想起项目上线那天的情景。晚上十一点半,我们还在会议室里盯着 Grafana 的大盘看有没有异常。当时我就在想:“技术这条路,走得越远,越发现要做的东西越多。”不过也正是这份不断突破自我、解决问题的乐趣,让我一直热爱这个行业。

愿你也能在每一次技术实践中找到成就感和满足感。


如果大家对上述内容有任何疑问或感兴趣的技术方向,欢迎留言讨论,我也很乐意继续分享更多的实战经验。

评论 0

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