Bootstrap

SpringCloud Gateway 路由数量对性能的影响研究

SpringCloud Gateway系列文章共五篇,由我行开发工程师 提供,带大家深入剖析Gateway工作原理,及如何基于Gateway进行定制化开发以适应企业特定环境需求。

第一篇:

第二篇:

第三篇:

第四篇:

第五篇:SpringCloud Gateway 过滤器。

背景描述

近期在公司建设 API 治理平台过程中,以 SpringCloud Gateway 为基础,构建了一个 API 的 Mock 服务,以 API 的 URI 作为路由,根据服务端存储的 API DSL,验证请求信息,生成并返回 Mock 报文。

SpringCloud Gateway 具备很好的 支持功能,可以在 API DSL 创建的同时,创建一条 Mock 路由,这样 API DSL 创建后,开发人员就可以使用 Mock 服务进行开发调试工作。

但作为一个企业级应用,所管辖的 API 的数量众多,SpringCloud Gateway 在路由表急剧膨胀后的性能如何?目前没有查阅到明确说明的资料。翻阅其源码发现,断言命中其实是进行的遍历,这样路由表膨胀时,路由性能存疑,需要进行验证,根据验证结果制定近一步优化方案。

验证方法

保持其他所有变量不变,仅调整路由表大小,使用JMH工具进行基准测试,得出路由表膨胀与路由性能的关系。

需要编写三个服务:

配置信息

  CPU Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz

  基准速度:  3.60 GHz
  插槽:  1
  内核:  8
  逻辑处理器:  8
  虚拟化:  已启用
  L1 缓存:  512 KB
  L2 缓存:  2.0 MB
  L3 缓存:  12.0 MB

  测试期间CPU利用率约  50%

  SpringCloud Gateway 3.0.3, Java 1.8.0_251

测试过程需控制变量,所以CPU负债不能过高,下图是我执行测试时的CPU负载情况

路由定义示例

{
    "_id": {
        "$oid": "60c4f055588bca06132e44cb"
    },
    "predicates": [{
        "name": "Path",
        "args": {
            "_genkey_0": "/1999"
        }
    }],
    "filters": [{
        "name": "AddRequestHeader",
        "args": {
            "_genkey_0": "x-benchmark-routeId",
            "_genkey_1": "60c4f055588bca06132e44cb"
        }
    }, {
        "name": "PrefixPath",
        "args": {
            "_genkey_0": "/ok"
        }
    }],
    "uri": "http://localhost:9999",
    "metadata": {},
    "order": 1,
    "_class": "com.example.scgw.benchmark.route.MongoRouteDefinition"
}

测试结论

见上图,测试结果无论系统的吞吐量,还是响应时间指标,都随着路由表的膨胀变差,当路由表膨胀到10W级别时,服务基本不可用了!

这一结论印证了我的猜想。

优化思路一

保持路由网关的通用性(SpringCloud Gateway设计了很多路由断言手段,包括基于path、method、header、parameter等等),采用两级(多级)路由机制见下图:

优化思路二

Mock服务是一个专有场景,仅根据Path和指定Header进行转发,可以修改SpringCloud Gateway源码,提供一个根据Path+指定Header查找路由的HashMap,这样进行路由断言时就不需进行路由表遍历。

个人倾向于按照思路二进行优化,优化过程见

测试相关代码

路由网关

pom.xml


  org.springframework.boot
  spring-boot-starter-data-mongodb-reactive


  org.springframework.cloud
  spring-cloud-starter-gateway

MongoRouteDefinition.java

@Document
public class MongoRouteDefinition extends RouteDefinition {

  @Id
  private String id;

  @Override
  public String getId() {
    return this.id;
  }

  @Override
  public void setId(String id) {
    this.id = id;
  }

  public static MongoRouteDefinition from(RouteDefinition route) {
    MongoRouteDefinition newRoute = new MongoRouteDefinition();
    BeanUtils.copyProperties(route, newRoute);
    return newRoute;
  }
}

MongoRouteRepository.java

public interface MongoRouteRepository extends
    ReactiveMongoRepository {

}

MongoRouteDefinitionRepository.java

@Component
public class MongoRouteDefinitionRepository implements RouteDefinitionRepository {

  private final MongoRouteRepository mongoRouteRepository;

  public MongoRouteDefinitionRepository(
      MongoRouteRepository mongoRouteRepository) {
    this.mongoRouteRepository = mongoRouteRepository;
  }

  @Override
  public Flux getRouteDefinitions() {
    return mongoRouteRepository.findAll().map(r -> r);
  }

  @Override
  public Mono save(Mono route) {
    return route.flatMap(
        r -> mongoRouteRepository.save(MongoRouteDefinition.from(r))
            .and(Mono.empty())
    );
  }

  @Override
  public Mono delete(Mono routeId) {
    return routeId.flatMap(mongoRouteRepository::deleteById);
  }

  public Mono save(MongoRouteDefinition route) {
    return mongoRouteRepository.save(route);
  }
  
  public Mono delete(String routeId) {
    return mongoRouteRepository.deleteById(routeId);
  }

  public Flux saveAll(Flux routes) {
    return mongoRouteRepository
        .saveAll(routes.map(MongoRouteDefinition::from));
  }

  public Flux saveAll(List routes) {
    return mongoRouteRepository.saveAll(routes);
  }

  public Mono deleteAll() {
    return mongoRouteRepository.deleteAll();
  }
}

MongoRouteDefinitionRepositoryTest.java

@SpringBootTest
class MongoRouteDefinitionRepositoryTest {

  public static final String UP_STREAM = "http://localhost:9999";
  @Autowired
  MongoRouteDefinitionRepository mongoRepository;

  @Test
  void save() {
    Mono route = mongoRepository.save(newRoute(-1));

    StepVerifier.create(route)
        .expectNextMatches(r -> r.getId() != null)
        .expectComplete()
        .verify();
  }

  @Test
  void saveAll() {
    this.deleteAll();
    int cycle = 100;
    while (cycle-- > 0) {
      int count = 100;
      List routes = new ArrayList<>(count);
      while (count-- > 0) {
        routes.add(newRoute(cycle * 100 + count));
      }
      mongoRepository.saveAll(routes).blockLast();
    }
  }

  @Test
  void deleteAll() {
    mongoRepository.deleteAll().block();
  }

  private MongoRouteDefinition newRoute(int path) {
    MongoRouteDefinition route = new MongoRouteDefinition();
    route.setId(ObjectId.get().toHexString());
    PredicateDefinition predicate = new PredicateDefinition(
        "Path=/mock/" + path);
    route.setPredicates(Collections.singletonList(predicate));
    List filters = new LinkedList<>();
    filters.add(new FilterDefinition("AddRequestHeader=x-benchmark-routeId," + route.getId()));
    filters.add(new FilterDefinition("PrefixPath=/ok"));
    route.setFilters(filters);
    route.setOrder(1);
    route.setUri(URI.create(UP_STREAM));
    return route;
  }

application.properties

server.port=9999

挡板服务

pom.xml


  org.springframework.boot
  spring-boot-starter-webflux

java

public class MockApplication {

  public static void main(String[] args) {
    SpringApplication.run(MockApplication.class, args);
  }
  
  @Bean
  RouterFunction defaultRouter() {
    return route(path("/ok/**"), this::success);
  }

  Mono success(ServerRequest request) {
    String routeId = request.headers().firstHeader("x-benchmark-routeId");
    return ok()
        .header("x-mock-server-routeId", routeId)
        .contentType(MediaType.TEXT_PLAIN)
        .body(Mono.just(String.valueOf(routeId)), String.class);
  }
}

application.properties

server.port=9999

JMH 代码

创建工程

mvn archetype:generate \
          -DinteractiveMode=false \
          -DarchetypeGroupId=org.openjdk.jmh \
          -DarchetypeArtifactId=jmh-java-benchmark-archetype \
          -DgroupId=com.example \
          -DartifactId=jmh \
          -Dversion=1.32

修改POM文件,添加http客户端依赖


  com.squareup.okhttp3
  okhttp
  4.9.1

编写测试案例

public class MyBenchmark {

  @Benchmark
  @Threads(2)
  @Fork(2)
  @BenchmarkMode(Mode.All)
  @Warmup(iterations = 1, time = 3)
  @Measurement(iterations = 10, time = 1)
  @Timeout(time = 300)
  public void testMethod() {
    // 根据路由总数设置随机路由path
    int path = (int) (Math.random() * 100000);
    testRoute(path);
  }

  /**
   * 待测试方法
   */
  public void testRoute(int path) {
    OkHttpClient client = new OkHttpClient();
    Request request = new Request.Builder()
        .url("http://127.0.0.1:8888/mock/" + path)
        .build();
    Call call = client.newCall(request);
    try {
      Response response = call.execute();
      assert response.body() != null;
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

执行性能基准测试

方式1,IDEA的JMH插件JMH plugin安装后,直接在IDEA里运行;

方式2,mvn 打包,执行jar包

mvn package
java -jar target/benchmarks.jar

收集测试结果

测试结果数据见下文,共6组。

测试数据

对照组(直连)

Benchmark                                    Mode    Cnt    Score     Error  Units
MyBenchmark.testMethod                      thrpt     20  899.152 ± 137.062  ops/s
MyBenchmark.testMethod                       avgt     20    0.002 ±   0.001   s/op
MyBenchmark.testMethod                     sample  17647    0.002 ±   0.001   s/op
MyBenchmark.testMethod:testMethod·p0.00    sample           0.001             s/op
MyBenchmark.testMethod:testMethod·p0.50    sample           0.002             s/op
MyBenchmark.testMethod:testMethod·p0.90    sample           0.003             s/op
MyBenchmark.testMethod:testMethod·p0.95    sample           0.003             s/op
MyBenchmark.testMethod:testMethod·p0.99    sample           0.003             s/op
MyBenchmark.testMethod:testMethod·p0.999   sample           0.012             s/op
MyBenchmark.testMethod:testMethod·p0.9999  sample           0.018             s/op
MyBenchmark.testMethod:testMethod·p1.00    sample           0.018             s/op
MyBenchmark.testMethod                         ss     20    0.003 ±   0.001   s/op

100条路由

Benchmark                                    Mode    Cnt    Score    Error  Units
MyBenchmark.testMethod                      thrpt     20  713.931 ± 97.302  ops/s
MyBenchmark.testMethod                       avgt     20    0.003 ±  0.001   s/op
MyBenchmark.testMethod                     sample  14090    0.003 ±  0.001   s/op
MyBenchmark.testMethod:testMethod·p0.00    sample           0.002            s/op
MyBenchmark.testMethod:testMethod·p0.50    sample           0.003            s/op
MyBenchmark.testMethod:testMethod·p0.90    sample           0.004            s/op
MyBenchmark.testMethod:testMethod·p0.95    sample           0.004            s/op
MyBenchmark.testMethod:testMethod·p0.99    sample           0.005            s/op
MyBenchmark.testMethod:testMethod·p0.999   sample           0.008            s/op
MyBenchmark.testMethod:testMethod·p0.9999  sample           0.011            s/op
MyBenchmark.testMethod:testMethod·p1.00    sample           0.011            s/op
MyBenchmark.testMethod                         ss     20    0.003 ±  0.001   s/op

1000条路由

Benchmark                                    Mode    Cnt    Score    Error  Units
MyBenchmark.testMethod                      thrpt     20  630.823 ± 41.301  ops/s
MyBenchmark.testMethod                       avgt     20    0.003 ±  0.001   s/op
MyBenchmark.testMethod                     sample  12617    0.003 ±  0.001   s/op
MyBenchmark.testMethod:testMethod·p0.00    sample           0.002            s/op
MyBenchmark.testMethod:testMethod·p0.50    sample           0.003            s/op
MyBenchmark.testMethod:testMethod·p0.90    sample           0.004            s/op
MyBenchmark.testMethod:testMethod·p0.95    sample           0.004            s/op
MyBenchmark.testMethod:testMethod·p0.99    sample           0.005            s/op
MyBenchmark.testMethod:testMethod·p0.999   sample           0.008            s/op
MyBenchmark.testMethod:testMethod·p0.9999  sample           0.009            s/op
MyBenchmark.testMethod:testMethod·p1.00    sample           0.009            s/op
MyBenchmark.testMethod                         ss     20    0.004 ±  0.001   s/op

5000条路由

Benchmark                                    Mode   Cnt    Score    Error  Units
MyBenchmark.testMethod                      thrpt    20  248.353 ±  4.634  ops/s
MyBenchmark.testMethod                       avgt    20    0.008 ±  0.001   s/op
MyBenchmark.testMethod                     sample  5007    0.008 ±  0.001   s/op
MyBenchmark.testMethod:testMethod·p0.00    sample          0.002            s/op
MyBenchmark.testMethod:testMethod·p0.50    sample          0.009            s/op
MyBenchmark.testMethod:testMethod·p0.90    sample          0.009            s/op
MyBenchmark.testMethod:testMethod·p0.95    sample          0.011            s/op
MyBenchmark.testMethod:testMethod·p0.99    sample          0.014            s/op
MyBenchmark.testMethod:testMethod·p0.999   sample          0.017            s/op
MyBenchmark.testMethod:testMethod·p0.9999  sample          0.019            s/op
MyBenchmark.testMethod:testMethod·p1.00    sample          0.019            s/op
MyBenchmark.testMethod                         ss    20    0.009 ±  0.001   s/op

1w条路由

Benchmark                                    Mode   Cnt    Score    Error  Units
MyBenchmark.testMethod                      thrpt    20  131.585 ±  1.799  ops/s
MyBenchmark.testMethod                       avgt    20    0.015 ±  0.001   s/op
MyBenchmark.testMethod                     sample  2671    0.015 ±  0.001   s/op
MyBenchmark.testMethod:testMethod·p0.00    sample          0.002            s/op
MyBenchmark.testMethod:testMethod·p0.50    sample          0.016            s/op
MyBenchmark.testMethod:testMethod·p0.90    sample          0.017            s/op
MyBenchmark.testMethod:testMethod·p0.95    sample          0.018            s/op
MyBenchmark.testMethod:testMethod·p0.99    sample          0.028            s/op
MyBenchmark.testMethod:testMethod·p0.999   sample          0.030            s/op
MyBenchmark.testMethod:testMethod·p0.9999  sample          0.030            s/op
MyBenchmark.testMethod:testMethod·p1.00    sample          0.030            s/op
MyBenchmark.testMethod                         ss    20    0.016 ±  0.001   s/op

10W条路由

Benchmark                                    Mode  Cnt   Score   Error  Units
MyBenchmark.testMethod                      thrpt   20  13.880 ± 0.364  ops/s
MyBenchmark.testMethod                       avgt   20   0.141 ± 0.002   s/op
MyBenchmark.testMethod                     sample  300   0.141 ± 0.002   s/op
MyBenchmark.testMethod:testMethod·p0.00    sample        0.005           s/op
MyBenchmark.testMethod:testMethod·p0.50    sample        0.141           s/op
MyBenchmark.testMethod:testMethod·p0.90    sample        0.142           s/op
MyBenchmark.testMethod:testMethod·p0.95    sample        0.143           s/op
MyBenchmark.testMethod:testMethod·p0.99    sample        0.144           s/op
MyBenchmark.testMethod:testMethod·p0.999   sample        0.270           s/op
MyBenchmark.testMethod:testMethod·p0.9999  sample        0.270           s/op
MyBenchmark.testMethod:testMethod·p1.00    sample        0.270           s/op
MyBenchmark.testMethod                         ss   20   0.141 ± 0.001   s/op