大家好,这里是架构资源栈!点击上方关注,添加“星标”,一起学习大厂前沿架构!

去年,我们团队面临着一个看似不可能的挑战:我们的 Spring Boot 应用程序需要处理增长了 20 倍的流量,从每秒 5 万个请求增加到惊人的 100 万个。由于只有三个月的交付时间,而且硬件预算有限,我不确定我们能否完成这个任务。

剧透预警:我们做到了。我们的应用程序现在可以轻松处理每秒 120 万个请求的峰值负载,响应时间低于 100 毫秒,而运行所需的基础设施成本与之前大致相同。

在本指南中,我将向您详细介绍我们如何实现这一目标,分享我们发现的真正瓶颈、产生最大影响的优化以及我们在此过程中学到的令人惊讶的经验教训。

测量起点⏱️

在做出任何改变之前,我都会设定清晰的绩效基准。这一步不容置疑;如果不知道起点,就无法衡量进度,也无法找到最大的改进机会。

我们的初始指标如下:

// Initial Performance Metrics
Maximum throughput: 50,000 requests/second
Average response time: 350ms
95th percentile response time: 850ms
CPU utilization during peak: 85-95%
Memory usage: 75% of available heap
Database connections: Often reaching max pool size (100)
Thread pool saturation: Frequent thread pool exhaustion

Enter fullscreen mode Exit fullscreen mode

我使用了多种工具来收集这些指标:

  • JMeter:用于负载测试和建立基本吞吐量数字
  • Micrometer + Prometheus + Grafana:用于实时监控和可视化
  • JProfiler:深入分析代码中的热点
  • 火焰图:识别 CPU 密集型方法

有了这些基准指标,我就可以确定优化的优先级并衡量其影响。

发现真正的瓶颈

初步分析发现了几个有趣的瓶颈:

  • 线程池饱和:默认的 Tomcat 连接器已达到极限
  • 数据库连接争用:HikariCP 配置未针对我们的工作负载进行优化
  • 序列化效率低下:Jackson 在请求/响应处理期间消耗了大量 CPU
  • 阻塞 I/O 操作:特别是在调用外部服务时
  • 内存压力:过多的对象创建导致频繁的 GC 暂停

让我们系统地解决每一个问题。

反应式编程:游戏规则改变者⚡

最有影响力的变化是采用 Spring WebFlux 的响应式编程。这不是一个临时的替代方案;它需要我们重新思考如何构建应用程序。

我首先识别具有大量 I/O 操作的服务:

// BEFORE: Blocking implementation
@Service
public class ProductService {
    @Autowired
    private ProductRepository repository;

    public Product getProductById(Long id) {
        return repository.findById(id)
                .orElseThrow(() -> new ProductNotFoundException(id));
    }
}

Enter fullscreen mode Exit fullscreen mode

并将它们转换为反应式实现:
// AFTER: Reactive implementation
@Service
public class ProductService {
    @Autowired
    private ReactiveProductRepository repository;

    public Mono<Product> getProductById(Long id) {
        return repository.findById(id)
                .switchIfEmpty(Mono.error(new ProductNotFoundException(id)));
    }
}

Enter fullscreen mode Exit fullscreen mode

控制器也进行了相应更新:
// BEFORE: Traditional Spring MVC controller
@RestController
@RequestMapping("/api/products")
public class ProductController {
    @Autowired
    private ProductService service;

    @GetMapping("/{id}")
    public ResponseEntity<Product> getProduct(@PathVariable Long id) {
        return ResponseEntity.ok(service.getProductById(id));
    }
}

Enter fullscreen mode Exit fullscreen mode

// AFTER: WebFlux reactive controller
@RestController
@RequestMapping("/api/products")
public class ProductController {
    @Autowired
    private ProductService service;

    @GetMapping("/{id}")
    public Mono<ResponseEntity<Product>> getProduct(@PathVariable Long id) {
        return service.getProductById(id)
            .map(ResponseEntity::ok)
            .defaultIfEmpty(ResponseEntity.notFound().build());
    }
}

Enter fullscreen mode Exit fullscreen mode

仅此一项更改就通过更高效的线程利用,使我们的吞吐量翻了一番。WebFlux 不再为每个请求分配一个线程,而是使用少量线程来处理大量并发请求。

数据库优化:隐藏的乘数📊

数据库交互是我们面临的另一个最大瓶颈。我实施了三管齐下的方法:

1.查询优化

我使用 Spring Data 的 @Query 注释来替换低效的自动生成的查询:

// BEFORE: Using derived method name (inefficient)
List<Order> findByUserIdAndStatusAndCreatedDateBetween(
    Long userId, OrderStatus status, LocalDate start, LocalDate end);

Enter fullscreen mode Exit fullscreen mode

// AFTER: Optimized query
@Query("SELECT o FROM Order o WHERE o.userId = :userId " +
       "AND o.status = :status " +
       "AND o.createdDate BETWEEN :start AND :end " +
       "ORDER BY o.createdDate DESC")
List<Order> findUserOrdersInDateRange(
    @Param("userId") Long userId, 
    @Param("status") OrderStatus status,
    @Param("start") LocalDate start, 
    @Param("end") LocalDate end);

Enter fullscreen mode Exit fullscreen mode

我还使用 Hibernate 的 @BatchSize 优化了一个特别有问题的 N+1 查询:

@Entity
public class Order {
    // Other fields

    @OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
    @BatchSize(size = 30) // Batch fetch order items
    private Set<OrderItem> items;
}

Enter fullscreen mode Exit fullscreen mode

2. 连接池调优

HikariCP 的默认设置会导致连接争用。经过大量测试,我最终确定了以下配置:

spring:
  datasource:
    hikari:
      maximum-pool-size: 30
      minimum-idle: 10
      idle-timeout: 30000
      connection-timeout: 2000
      max-lifetime: 1800000

Enter fullscreen mode Exit fullscreen mode

关键的见解是,连接数并不总是越多越好;我们发现最佳连接数是 30 个,这样既减少了争用,又不会使数据库不堪重负。

3. 实施策略缓存

我为经常访问的数据添加了 Redis 缓存:

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10))
            .disableCachingNullValues();

        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(cacheConfig)
            .withCacheConfiguration("products", 
                RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofMinutes(5)))
            .withCacheConfiguration("categories", 
                RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofHours(1)))
            .build();
    }
}

Enter fullscreen mode Exit fullscreen mode

然后将其应用于适当的服务方法:

@Service
public class ProductService {
    // Other code

    @Cacheable(value = "products", key = "#id")
    public Mono<Product> getProductById(Long id) {
        return repository.findById(id)
            .switchIfEmpty(Mono.error(new ProductNotFoundException(id)));
    }

    @CacheEvict(value = "products", key = "#product.id")
    public Mono<Product> updateProduct(Product product) {
        return repository.save(product);
    }
}

Enter fullscreen mode Exit fullscreen mode

这使得读取密集型操作的数据库负载减少了 70%。

序列化优化:令人惊讶的 CPU 节省器

分析显示,15% 的 CPU 时间花在了 Jackson 序列化上。我切换到了更高效的配置:

@Configuration
public class JacksonConfig {
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();

        // Use afterburner module for faster serialization
        mapper.registerModule(new AfterburnerModule());

        // Only include non-null values
        mapper.setSerializationInclusion(Include.NON_NULL);

        // Disable features we don't need
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        return mapper;
    }
}

Enter fullscreen mode Exit fullscreen mode

对于我们最注重性能的端点,我用 Protocol Buffers 替换了 Jackson:

syntax = "proto3";
package com.example.proto;

message ProductResponse {
  int64 id = 1;
  string name = 2;
  string description = 3;
  double price = 4;
  int32 inventory = 5;
}

Enter fullscreen mode Exit fullscreen mode

@RestController
@RequestMapping("/api/products")
public class ProductController {
    // Jackson-based endpoint
    @GetMapping("/{id}")
    public Mono<ResponseEntity<Product>> getProduct(@PathVariable Long id) {
        // Original implementation
    }

    // Protocol buffer endpoint for high-performance needs
    @GetMapping("/{id}/proto")
    public Mono<ResponseEntity<byte[]>> getProductProto(@PathVariable Long id) {
        return service.getProductById(id)
            .map(product -> ProductResponse.newBuilder()
                .setId(product.getId())
                .setName(product.getName())
                .setDescription(product.getDescription())
                .setPrice(product.getPrice())
                .setInventory(product.getInventory())
                .build().toByteArray())
            .map(bytes -> ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(bytes));
    }
}

Enter fullscreen mode Exit fullscreen mode

这一变化将序列化 CPU 使用率降低了 80%,并将响应大小减少了 30%。

线程池和连接调优:配置魔法

使用 WebFlux,我们需要调整 Netty 的事件循环设置:

spring:
  reactor:
    netty:
      worker:
        count: 16  # Number of worker threads (2x CPU cores)
      connection:
        provider:
          pool:
            max-connections: 10000
            acquire-timeout: 5000

Enter fullscreen mode Exit fullscreen mode

对于我们应用程序中仍然使用 Spring MVC 的部分,我调整了 Tomcat 连接器:

server:
  tomcat:
    threads:
      max: 200
      min-spare: 20
    max-connections: 8192
    accept-count: 100
    connection-timeout: 2000

Enter fullscreen mode Exit fullscreen mode

这些设置使我们能够用更少的资源处理更多的并发连接。

使用 Kubernetes 进行水平扩展:最后的冲刺🚢

为了达到每秒 100 万个请求的目标,我们需要进行水平扩展。我将应用程序容器化,并将其部署到 Kubernetes 上。

FROM openjdk:17-slim
COPY target/myapp.jar app.jar
ENV JAVA_OPTS="-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+ParallelRefProcEnabled"
ENTRYPOINT exec java $JAVA_OPTS -jar /app.jar

Enter fullscreen mode Exit fullscreen mode

然后根据 CPU 利用率配置自动缩放:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: myapp-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  minReplicas: 5
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

Enter fullscreen mode Exit fullscreen mode

我们还通过 Istio 实现了服务网格功能,以实现更好的流量管理:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: myapp-vs
spec:
  hosts:
  - myapp-service
  http:
  - route:
    - destination:
        host: myapp-service
    retries:
      attempts: 3
      perTryTimeout: 2s
    timeout: 5s

Enter fullscreen mode Exit fullscreen mode

这使我们能够有效处理流量高峰,同时保持弹性。

衡量结果:证据📈

经过所有优化后,我们的指标得到了显着改善:

// Final Performance Metrics
Maximum throughput: 1,200,000 requests/second
Average response time: 85ms (was 350ms)
95th percentile response time: 120ms (was 850ms)
CPU utilization during peak: 60-70% (was 85-95%)
Memory usage: 50% of available heap (was 75%)
Database queries: Reduced by 70% thanks to caching
Thread efficiency: 10x improvement with reactive programming

Enter fullscreen mode Exit fullscreen mode

最令人满意的结果是什么?在黑色星期五促销期间,系统每秒处理 120 万个请求,没有任何警报,没有停机,只有满意的客户。

关键经验教训

  • 测量就是一切:如果没有适当的分析,我就会优化错误的东西。
  • 反应式并不总是更好:我们使用混合方法在 Spring MVC 上保留了一些更有意义的端点。
  • 数据库通常是瓶颈:缓存和查询优化为我们带来了一些最大的胜利。
  • 配置很重要:我们的许多改进都来自于简单地调整默认配置。
  • 不要过早扩展:我们首先优化应用程序,然后水平扩展,这节省了大量的基础设施成本。
  • 使用现实场景进行测试:我们最初使用综合测试的基准与生产模式不匹配,从而导致误导性的优化。
  • 针对 99% 进行优化:有些端点无法进一步优化,但它们仅占我们流量的 1%,因此我们将重点放在其他地方。
  • 平衡复杂性和可维护性:一些潜在的优化被拒绝,因为它们会使代码库过于复杂而难以维护。

性能优化并非指找到灵丹妙药,而是需要系统地识别并解决整个系统中的瓶颈。Spring Boot 的功能唾手可得,您只需要知道该如何利用。

你的 Spring 应用面临哪些性能挑战?

如果这篇文章对你有帮助的话,别忘了【在看】【点赞】支持下哦~

原文地址:https://mp.weixin.qq.com/s/jc-OT523ih-ITqwhrkEdAA
本文由博客一文多发平台 OpenWrite 发布!

吾日三省吾码
25 声望4 粉丝