压测实践案例之网关

业务服务提供的压测代码

/**
 * @Author liqiang
 * @Description 网关测试
 * @Date 2025/03/26/14:32
 */
@RestController
@RequestMapping("/c/gateway/testGateway")
public class TestGateWayQueryApi {

    @PostMapping("/1.0/testGateway")
    @ApiOperation(value = "测试gateway", notes = "测试gateway")
    public ResponseBase<String> testGateway(Long sleep) {
        if (Objects.nonNull(sleep) && sleep > 0) {
            try {
                Thread.sleep(sleep);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        return ResponseBase.success(String.valueOf(System.currentTimeMillis()));
    }


}

压测如何判断是依赖服务瓶颈

背景:优化网关代码,通过压测针对异常指标判断瓶颈

1.当发现响应曲线上升。

2.简单方式使用另外一个接口轮询服务在冒尖的时候判断是否是服务异常

第一组

 第二组

第三组

第四组

以上图可以看出来是业务服务瓶颈。

如果非业务服务瓶颈就需要关注网关CPU JVM 内存等信息,以及网关的IO模型,我的网关使用spring-cloud-gateway

整条链路采用响应式的,如依赖的redis使用ReactorReditTemplate。针对JWTtoken解析采用弹性线程池。避免阻塞netty的 event loop工作线程。

观察服务器指标

1.top pid  然后输入1 查看各个cpu利用情况

 

2.jvm内存占用和回收情况

jstat -gcutil 10755

 

 

 

关于网关和业务服务压测

网关是采用NIO模型,网关性能瓶颈主要在CPU,如果没有读取body的情况内存占用也及小。我们生产网关jvm设置2G

业务服务可以通过压测接口1万 2万QPS就能达到性能瓶颈点。

但是网关如果没有明显的代码缺陷在NIO IO模型下,理论上支持10万QPS以上,集群部署加Nginx可以支持水平扩容。

针对网关应该换个思维跟业务服务区分开来,网关压到他的瓶颈还是不好压,网关如果没有明显代码缺陷,压不出来。压测网关我觉得换个思维,我觉得能够满足现有业务,同时有几倍的容错空间就行了

网关压测结果

部分服务出现异常网关是否能够正常提供能力

压测准备

1.阻塞线程组压测线程组准备

2. 混合压测非阻塞

压测结果分析

响应时间分布

TPS分布

 

梯度压测分布

网关优化方案

优化项(减少高CPU占用和IO占用阻塞 work EventLoop工作线程)

1.将网关的大量的redis操作改为响应式的ReactorRedisTemplate

  if (!stringRedisTemplate.opsForSet().isMember(Const.EFFECTIVE_LOGIN_USER_ID, user.getUserId())) {
            return getVoidMono(serverWebExchange, new TokenForbiddenResponse("User Token Forbidden or Expired!"));
        }

修改后

    private Mono<TokenContext> validateUser(TokenContext tokenContext) {
        JWTInfo jwtInfo = tokenContext.getJwtInfo();
        StopWatch stopWatch = null;
        if (log.isDebugEnabled()) {
            stopWatch = new StopWatch();
            stopWatch.start("EFFECTIVE_LOGIN_USER_ID");
        }
        StopWatch finalStopWatch = stopWatch;
        return reactiveRedisTemplate.opsForSet()
            .isMember(Const.EFFECTIVE_LOGIN_USER_ID, jwtInfo.getUserId())
            .flatMap(isMember -> {
                if (log.isDebugEnabled()) {
                    finalStopWatch.stop();
                    log.debug("ReactorAccessFilter EFFECTIVE_LOGIN_USER_ID ={}",
                        finalStopWatch.getLastTaskTimeMillis());
                }
                if (!isMember) {
                    // 返回错误信号,由上层统一处理
                    return Mono.error(
                        new YxtRuntimeException(ResponseCodeType.BIZ_EXCEPTION, "User Token Forbidden or Expired!"));
                }
                return Mono.just(tokenContext);
            });
    }

2.关于JWT解析改为弹性线程池

public class UserAuthUtil {
    public UserAuthUtil(KeyConfiguration keyConfiguration){
        this.keyConfiguration=keyConfiguration;
    }

    private KeyConfiguration keyConfiguration;

    public JWTInfo getInfoFromToken(String token) throws Exception {
        try {
            return JWTHelper.getInfoFromToken(token, keyConfiguration.getUserPubKey());
        }catch (ExpiredJwtException ex){
            throw new UserTokenException("User token expired!");
        }catch (SignatureException ex){
            throw new UserTokenException("User token signature error!");
        }catch (IllegalArgumentException ex){
            throw new UserTokenException("User token is null or empty!");
        }
    }
}

修改后

    // 解析 JWT 并缓存到 Redis
    private Mono<JWTInfo> parseAndCacheJwt(String token) {
        return Mono.fromCallable(() -> userAuthUtil.getInfoFromToken(token))  // 阻塞操作封装
            .subscribeOn(Schedulers.boundedElastic()); // 切换到弹性线程池
    }

优化方案二

充分利用cpu资源,将默认的Work Event Loop改为cpu核数*2

/**
 * @Author liqiang
 * @Description ReactorNetty
 * @Date 2025/03/27/16:48
 */
@Configuration
public class ReactorNettyConfig {

    /**
     * @return
     * @See reactor.netty.tcp.TcpResources#create
     */
    @Bean
    public ReactorResourceFactory reactorClientResourceFactory() {
        /**
         * 定义 Netty 的Selector 线程数,负责监听网络事件(如连接建立、数据到达)
         * 默认值:-1(由 Netty 自动分配,通常为 CPU 核心数)。
         * 若为 CPU 密集型应用,设置为 1 可减少上下文切换;若为高并发 I/O 密集型应用,保留默认值或按需调整。
         */
        System.setProperty("reactor.netty.ioSelectCount", "1");
        /**
         * 定义 Netty 的I/O 工作线程数,负责处理网络读写、编解码等任务。
         * :通常设置为 CPU 核心数 * 2(需与容器 CPU 资源限制对齐)。
         */
        System.setProperty("reactor.netty.ioWorkerCount",
            String.valueOf(Runtime.getRuntime().availableProcessors() * 2));
        return new ReactorResourceFactory();
    }
}

总结经验记录一些小插曲

上线预发后无IO接口也有几百ms延迟

排查方案

1.进转发服务 执行curl看是否有延迟

2.网关ping转发服务是否正常

3.网关pod直调转发服务是否正常

4.在网关pod直接调用网关是否正常

ps:最终发现通过域名转发到下游有延迟(上游涉及 waf),PS:如果压测需要走内网压,走外网会有稳定100+ms延迟

本地压测资源 导致吞吐量上不去

关注本地客户端的带宽、cpu、内存等信息,避免因为本地导致压测不上去,建议公司多台服务器一起混合压(ps也要考虑带宽)。

posted @ 2025-03-26 14:12  意犹未尽  阅读(90)  评论(0)    收藏  举报