高階程序員必備技能:Fizz網關的二次開發

1、概述

在使用 Fizz 過程當中,可能會碰到:java

  • 須要定製http server
  • 須要額外的http client
  • 須要自定義http filter
  • 須要訪問mysql、redis/codis、mongo、kafka 等

等問題,下面依次介紹解決辦法,同時其它二次開發問題亦可參考。mysql

2、定製http server

Fizz 採用 webflux 官方默認亦是最優的 http server 實現,並經過 WebFluxConfig 暴露,以方便外界進行細粒度的控制。react

不建議建立多個 http server,即便它們共享同一端口。git

webflux 默認基於 reactor-netty 實現 http server,可經過 NettyReactiveWebServerFactory 進行定製和擴展,包括 tcp、http、reactor-netty、netty、系統資源等層面,fizz 的 WebFluxConfig 含 NettyReactiveWebServerFactory bean,可修改或建立新的 NettyReactiveWebServerFactory bean 以定製 http server,下面是建立NettyReactiveWebServerFactory bean 的例子。github

@Bean
    public NettyReactiveWebServerFactory nettyReactiveWebServerFactory(ServerProperties serverProperties) {
        NettyReactiveWebServerFactory httpServerFactory = new NettyReactiveWebServerFactory();
        httpServerFactory.setResourceFactory(null);
        LoopResources lr = LoopResources.create("fizz-el", 1, Runtime.getRuntime().availableProcessors(), true);
        httpServerFactory.addServerCustomizers(
                httpServer -> (
                        httpServer.tcpConfiguration(
                                tcpServer -> {
                                    return (
                                            tcpServer
                                                    .runOn(lr, false) // 指定運行 server 的 eventloop 資源
                                                    .port(8080)       // server 監聽的端口
                                                    .bootstrap(
                                                            serverBootstrap -> (
                                                                    // 下面定製 netty 並調整 tcp 參數
                                                                    serverBootstrap
                                                                            .option(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT)
                                                                            .childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT)
                                                                            .childOption(ChannelOption.SO_KEEPALIVE, true)
                                                                            .childOption(ChannelOption.TCP_NODELAY,  true)
                                                            )
                                                    )
                                    );
                                }
                        )
                )
        );
        return httpServerFactory;
    }
複製代碼

netty 的默認配置適用絕大多數場景,儘可能少調整。web

3、建立額外的 http client

對外 http 交互,可直接使用 fizz 的 FizzWebClient 或 proxyWebClient,proxyWebClient 就是一個 org.springframework.web.reactive.function.client.WebClient, FizzWebClient 也是基於 proxyWebClient,提供了與 eureka 註冊中心服務交互的便利。redis

儘可能共享 FizzWebClient 或 proxyWebClient 進行 http 操做,不建議引入 apache httpclient、feign 等 http 客戶端,即便它們是異步、響應式的,確實須要建立額外的 WebClient 時,可參考 proxyWebClientConfig 的作法,而後儘可能共享新建的 WebClient,例如:spring

private ConnectionProvider getConnectionProvider() {
        return ConnectionProvider.builder("fizz-cp").maxConnections(2_000)
                                                    .pendingAcquireTimeout(Duration.ofMillis(6_000))
                                                    .maxIdleTime(Duration.ofMillis(40_000))
                                                    .build();
    }
    private LoopResources getLoopResources() {
        LoopResources lr = LoopResources.create("fizz-wc-el", Runtime.getRuntime().availableProcessors(), true);
        lr.onServer(false);
        return lr;
    }
    public WebClient webClient() {
        ConnectionProvider cp = getConnectionProvider(); // 客戶端鏈接池
        LoopResources lr = getLoopResources();           // 運行客戶端的 eventloop 資源
        HttpClient httpClient = HttpClient.create(cp).compress(false).tcpConfiguration(
                tcpClient -> {
                    return tcpClient.runOn(lr, false)
                                    // 定製客戶端底層 netty
                                    // .bootstrap(
                                    // bootstrap -> (
                                    // bootstrap.channel(NioSocketChannel.class)
                                    // )
                                    // )
                                    .doOnConnected(
                                            connection -> {
                                                connection.addHandlerLast(new ReadTimeoutHandler( 20_000, TimeUnit.MILLISECONDS))
                                                          .addHandlerLast(new WriteTimeoutHandler(20_000, TimeUnit.MILLISECONDS));
                                            }
                                    )
                                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 20_000)
                                    .option(ChannelOption.TCP_NODELAY,            true)
                                    .option(ChannelOption.SO_KEEPALIVE,           true)
                                    .option(ChannelOption.ALLOCATOR,              UnpooledByteBufAllocator.DEFAULT);
                }
        );
        return WebClient.builder().exchangeStrategies(ExchangeStrategies.builder().codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(-1)).build())
                                  .clientConnector(new ReactorClientHttpConnector(httpClient)).build();
    }
複製代碼

WebClient 的配置和 http server 是相似的。sql

4、自定義 http filter

若是須要在請求處理的流水線上加入邏輯,可經過插件機制實現,具體可參考插件章節,儘可能避免自定義 WebFilter,若是須要,應該繼承 ProxyAggrFilter:數據庫

public abstract class ProxyAggrFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        String serviceId = WebUtils.getServiceId(exchange); // 即請求的 path 是以 /proxy 開頭
        if (serviceId == null) {
            return chain.filter(exchange);
        } else {
            return doFilter(exchange, chain);
        }
    }

    public abstract Mono<Void> doFilter(ServerWebExchange exchange, WebFilterChain chain);
}
複製代碼

實現 doFilter 方法便可,注意 filter 的執行順序,需在 fizz 的 PreFilter 和 RouteFilter 之間。

5、訪問 mysql、redis/codis、mongo、kafka 等

不建議在 fizz 中直接與 mysql 等傳統數據庫交互,由於它們沒有原生的異步客戶端,儘可能把數據轉移到分佈式或本地緩存中,如 redis。

對 redis/codis、mongo、kafka 等操做,應使用 spring 官方提供的響應式客戶端,注意客戶端版本要與 spring boot 版本一致, 客戶端使用可參官方文檔,至於與 fizz 的整合,包括涉及多服務端的場景等,可參考 fizz 內置的 redis 交互邏輯設計和實現,RedisReactiveConfig 及子類AggregateRedisConfig。

好比有個 biz0 redis 庫,在 fizz 中可按以下方式定義與其交互的邏輯:

在 application.yml 中加入:

biz0.redis.host: biz0  ip
biz0.redis.port: 6379
biz0.redis.password: 123456
biz0.redis.database: 0
複製代碼

定義對應 yml 的配置 bean,及與其交互的 redistemplate:

@Configuration
public class Biz0RedisConfig extends RedisReactiveConfig {
    
    static final String BIZ0_REACTIVE_REDIS_PROPERTIES         = "biz0ReactiveRedisProperties";
    static final String BIZ0_REACTIVE_REDIS_CONNECTION_FACTORY = "biz0ReactiveRedisConnectionFactory";
    static final String BIZ0_REACTIVE_REDIS_TEMPLATE           = "biz0ReactiveRedisTemplate";

    @ConfigurationProperties(prefix = "biz0.redis")
    @Configuration(BIZ0_REACTIVE_REDIS_PROPERTIES) // 此 bean 對應上面 yml 配置
    public static class biz0RedisReactiveProperties extends RedisReactiveProperties {
    }

    public Biz0RedisConfig(@Qualifier(BIZ0_REACTIVE_REDIS_PROPERTIES) RedisReactiveProperties properties) {
        super(properties);
    }

    @Override
    @Bean(BIZ0_REACTIVE_REDIS_CONNECTION_FACTORY)
    public ReactiveRedisConnectionFactory lettuceConnectionFactory() {
        return super.lettuceConnectionFactory();
    }

    @Override
    @Bean(BIZ0_REACTIVE_REDIS_TEMPLATE) // 與 biz0 redis 交互的 template
    public ReactiveStringRedisTemplate reactiveStringRedisTemplate( @Qualifier(BIZ0_REACTIVE_REDIS_CONNECTION_FACTORY) ReactiveRedisConnectionFactory factory) {
        return super.reactiveStringRedisTemplate(factory);
    }
}
複製代碼

RedisReactiveConfig 是 fizz 抽象的通用 redis/codis 配置,支持多服務端場景,並能在各對應客戶端間共享資源:

public abstract class RedisReactiveConfig {

    protected static final Logger log = LoggerFactory.getLogger(RedisReactiveConfig.class);

    // this should not be changed unless there is a truly good reason to do so
    private static final int ps = Runtime.getRuntime().availableProcessors();
    private static final ClientResources clientResources = DefaultClientResources.builder()
            .ioThreadPoolSize(ps)
            .computationThreadPoolSize(ps)
            .build();

    private RedisReactiveProperties redisReactiveProperties;

    // 子類覆蓋並定義配置
    public RedisReactiveConfig(RedisReactiveProperties properties) {
        redisReactiveProperties = properties;
    }

    // 子類覆蓋,建立與特定 redis/codis 交互的 template
    public ReactiveStringRedisTemplate reactiveStringRedisTemplate(ReactiveRedisConnectionFactory fact) {
        return new ReactiveStringRedisTemplate(fact);
    }

    // 子類覆蓋,指定客戶端鏈接池,一般子類不用調整此邏輯
    public ReactiveRedisConnectionFactory lettuceConnectionFactory() {

        log.info("connect to " + redisReactiveProperties);

        RedisStandaloneConfiguration rcs = new RedisStandaloneConfiguration(redisReactiveProperties.getHost(), redisReactiveProperties.getPort());
        String password = redisReactiveProperties.getPassword();
        if (password != null) {
            rcs.setPassword(password);
        }
        rcs.setDatabase(redisReactiveProperties.getDatabase());

        LettucePoolingClientConfiguration ccs = LettucePoolingClientConfiguration.builder()
                .clientResources(clientResources)
                .clientOptions(ClientOptions.builder().publishOnScheduler(true).build())
                .poolConfig(new GenericObjectPoolConfig())
                .build();

        return new LettuceConnectionFactory(rcs, ccs);
    }
}
複製代碼

介紹

做者:hongqiaowei Fizz Gateway開源地址:github.com/wehotel/fiz…

官方技術交流羣

Fizz官方技術交流①羣(已滿) Fizz官方技術交流②羣(已滿) Fizz官方技術交流③羣:512164278