application.yml 示例

独立开发路上
2025-06-19 08:32
阅读 761

架构不是设计出来的,是“长”出来的

架构不是设计出来的,是“长”出来的

五年前,我刚入行做后端开发的时候,公司还是一套部署在本地服务器的单体应用。那时我们每天的工作就是在同一个 Git 分支上改代码、打包发布、偶尔出点问题就重启一下 Tomcat。整个项目结构清晰,功能模块都在一个工程里,看起来很稳。

但随着业务快速扩展,用户量激增,这个曾经“能用”的架构开始频频暴露出各种问题:发版慢、故障频发、数据库压力大、系统响应延迟严重……最头疼的一次,因为一次数据库表字段变更导致服务挂掉,整整花了半天才恢复。那时候我才真正意识到——技术架构不是一开始就设计完美的,而是随着业务的成长一步步演进出来的。

今天我想和你聊聊这些年我们团队经历的架构演进过程:从单体到微服务,再到如今拥抱云原生。这期间遇到的问题、踩过的坑,以及一些实际的技术方案和思考,都拿出来分享一下。


一、问题:当单体遇上增长,崩溃只是时间问题

我参与过一个电商平台的重构项目。最开始,这个平台只有一个部署包,包括商品管理、订单系统、用户中心等几十个模块,全部跑在一个 Java 应用里,依赖同一个 MySQL 数据库实例。

几个显著的问题很快暴露出来:

  • 发版风险大:改一个小 bug 都得整体打包发布,一旦新版本有兼容性问题就得回滚,影响范围非常广。
  • 性能瓶颈明显:特别是高峰期,数据库连接数经常爆满,订单模块的慢 SQL 拖垮整个系统。
  • 横向扩容困难:虽然我们可以把服务部署多个实例,但由于状态耦合严重(比如本地缓存、共享 session),并没有起到很好的效果。
  • 研发效率低:多个人同时在一个项目上开发容易冲突,新人进来也很难快速定位模块位置。

这些问题在业务量不大的时候还可以靠加班维持,但一旦日均 UV 超过百万,这套架构就成了系统的“定时炸弹”。


二、第一步尝试:拆分 —— 从单体走向粗粒度解耦

既然整体太庞大了,那我们就试着把它“掰开”。一开始我们选择的是按业务领域划分的方式,将原来的单体应用拆分成:

  • 用户中心
  • 商品中心
  • 订单中心
  • 支付中心
  • 内容中心

每个服务独立部署,各自有自己的数据库,服务间通过 HTTP 接口通信。当然,为了控制成本,一开始我们还没引入注册中心,服务地址是写死在配置文件里的。

关键思路:

  1. 数据库分库:原来的大库拆成多个业务数据库,缓解单点压力;
  2. 接口抽象化:定义统一的 RESTful API 协议;
  3. 异步解耦:部分场景下使用 RabbitMQ 做消息队列处理通知类操作;
  4. 缓存下沉:将公共读操作下沉到 Redis 缓存中,提升响应速度;

举个例子,订单生成时需要调用商品库存扣减接口。这时候我们会先通过 HTTP 请求远程调用商品中心的服务,如果失败会记录日志并重试。

// 示例:服务间调用伪代码
OrderService {
    createOrder(userId, productId) {
        // 远程调用商品中心接口
        boolean success = productService.decreaseStock(productId);
        if (!success) {
            log.warn("库存不足或服务调用失败");
            return "库存扣减失败";
        }
        
        // 创建订单
        orderRepo.create(order);
    }
}

这种简单拆分带来了一些好处:比如某个服务出现异常不会影响其他服务,发版也可以单独进行。但也暴露了不少问题:

  • 服务发现和治理缺失,调用链不可控;
  • 数据一致性难以保障,例如订单扣库存失败后需补偿机制;
  • 日志分散,问题排查变得困难;
  • 性能没有根本提升,反而新增了网络开销。

三、第二阶段:微服务基础设施搭建

为了解决上述问题,我们决定引入更完整的微服务架构,搭建了一系列基础组件:

1. 注册中心 + 客户端负载均衡

我们选择了 Spring Cloud 生态下的 Eureka 作为注册中心,并基于 Ribbon 实现客户端负载均衡。这样,订单服务可以自动感知商品服务的不同实例,避免了 IP 硬编码。

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
spring:
  application:
    name: order-service

2. 配置中心 & 监控告警

我们引入了 Spring Cloud Config 来统一管理各个服务的配置,并结合 Prometheus 和 Grafana 做指标采集与监控报警。

3. API 网关统一入口

Zuul 被我们用作网关,集中处理权限验证、路由转发、限流熔断等功能:

// ZuulFilter 示例,用于鉴权
public class AuthFilter extends ZuulFilter {

    @Override
    public String filterType() { return "pre"; }

    @Override
    public int filterOrder() { return 1; }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        String token = request.getParameter("token");
        if (validateToken(token)) {
            ctx.setSendZuulResponse(true); 
        } else {
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            ctx.setResponseBody("Unauthorized");
        }
        return null;
    }
}

这些改进让我们的架构具备了基本的微服务能力。但真正的挑战还在后面。


四、第三阶段:向云原生转型

2021年开始,我们开始尝试从传统架构向云原生体系迁移。这次的动机很简单:想用更少的人力维护更多的服务,同时支持弹性扩缩容

技术选型上,我们做了如下升级:

物理机部署 Kubernetes + Docker
Zookeeper/Eureka Consul + Envoy 或 Istio
自建消息中间件 Kafka / RabbitMQ on K8s
Prometheus + AlertManager Prometheus Stack + Loki + Tempo

核心变化:

  • 容器化部署:所有服务打成 Docker 镜像,并部署在 Kubernetes 集群中;
  • 声明式配置管理:借助 Helm Charts 管理服务部署模板;
  • 服务网格加持:Istio 提供更强大的流量治理能力;
  • 统一日志与追踪:ELK Stack + OpenTelemetry 实现全链路追踪。

其中一个典型优化是在高并发下单场景引入了分布式锁+幂等设计。以创建订单为例:

@PostMapping("/order/create")
public ResponseEntity<?> createOrder(@RequestParam String userId,
                                    @RequestParam String productId,
                                    @RequestParam String requestId) {

    String lockKey = "lock:order:" + userId;
    
    try {
        // 获取分布式锁(Redis)
        boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 5, TimeUnit.SECONDS);
        if (!acquired) {
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("请勿重复提交");
        }

        // 幂等校验
        String existingOrderId = redisTemplate.opsForValue().get("request:" + requestId);
        if (existingOrderId != null) {
            return ResponseEntity.ok(existingOrderId);
        }

        // 扣库存、生成订单逻辑...
        
        String orderId = generateOrderId();
        redisTemplate.opsForValue().set("request:" + requestId, orderId, 24, TimeUnit.HOURS);

        return ResponseEntity.ok(orderId);
    } finally {
        redisTemplate.delete(lockKey);
    }
}

这一段代码背后其实隐藏了很多考量:

  • 高并发场景下必须加锁,否则会出现超卖;
  • 使用 Redis 锁要小心设置过期时间和释放逻辑;
  • 引入 requestId 可防止前端重复请求造成的多次创建;
  • 最终数据一致性的保障往往需要配合补偿任务或事件驱动架构。

五、踩过的坑 & 实战经验

在这几年的架构演进过程中,有几个坑让我记忆犹新:

1. 跨服务事务没做好,最终对账系统背锅

有一次我们在重构支付流程时,订单状态更新和支付流水记录这两个操作分别属于不同服务,原本以为可以依靠 MQ 的顺序性和消费重试来保证数据一致性。但实际上由于网络波动和消费幂等问题未处理好,导致部分订单状态已变更为“已支付”,但支付流水却迟迟没到账。

教训:跨服务事务不能依赖外部消息队列,必须设计本地事务表 + 状态核对任务,必要时手动干预。

2. 微服务间的循环依赖

刚开始拆服务时,商品中心需要查用户信息,用户中心反过来也要查用户收藏的商品信息。于是两个服务互相调用对方接口,结果上线后经常因为某一方服务异常导致雪崩效应。

解决方式:重新梳理数据归属边界,引入 CQRS 模式,把某些只读数据冗余到本地缓存表中。

3. Kubernetes 上线初期资源配额不合理

有一次灰度上线新服务时,Pod 启动特别慢,甚至出现 CrashLoopBackoff。后来才发现是因为内存限制设得太小,JVM 启动失败。另外还有一个服务因为 CPU 限制太低,在高峰期被系统频繁驱逐。

建议:生产环境一定要设置合理的 limits/request 值,并结合 HPA 做自动扩缩容。


六、效果对比与收益总结

从最初那个“能跑就行”的单体应用到现在基于 Kubernetes 的多集群部署架构,我们取得了以下成效:

指标 初期 当前
发版频率 每周 1~2 次 每天多次
故障隔离能力
系统可扩展性
排查效率
运维复杂度 简单 中高
成本投入 高但可控

更重要的是,现在我们可以通过 DevOps 工具链实现持续集成/持续交付(CI/CD),新功能的上线周期大幅缩短,故障也能快速定位和修复。


七、几点建议送给正在走这条路的你

  1. 不要一开始就追求“完美架构”
    架构是随着业务发展慢慢迭代出来的。一开始不必纠结于微服务还是事件驱动,先解决最痛的问题。

  2. 重视数据模型设计
    拆服务之前就要明确数据边界,避免后期反向调用泛滥,引发更多复杂度。

  3. 监控和日志是运维的生命线
    不管是单体还是微服务,如果没有完善的可观测体系,出了问题就像大海捞针。

  4. 别迷信任何框架或工具
    我们曾过度信任 Spring Cloud Feign 的重试机制,结果在一次服务雪崩中加重了问题。技术方案一定要结合自身业务场景评估。

  5. 学会用最小代价验证想法
    很多功能其实不需要上线就能验证可行性。我们做过很多 A/B 测试和压测实验,帮助决策是否值得投入改造。


架构不是纸上画出来的蓝图,而是在代码和日志中慢慢长出来的生命力。回顾这几年的经历,我最大的感悟就是:“好的架构不是设计出来的,是踩着坑走过来的。”希望这篇文章能让你少走些弯路,走得更快、更稳。

如果你也在经历架构演进的过程,欢迎留言交流,我们一起成长 💪。

评论 0

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