springgateway自定义负载均衡策略实现内网开

公司使用spring cloud微服务,网关使用的是spring gateway,配合nacos注册中心

日常开发测试有个需求,就是自己本地起微服务,不管是通过前端页面点击调试,还是工具如postman发送api请求,都希望自己本地IP发起的请求,都转发到自己本地的微服务上,gateway无论是开发环境还是测试环境都是共用的,另外有一整套配套的开发环境或者测试环境的微服务,这样本地就不需要额外起gateway和对应的和自己这次开发无关的微服务了。

我其实对spring gateway的源码也不熟,之前调试过一次gateway内存泄漏,是官方的bug,堆外内存的count忘记释放了,导致gateway每过一段时间就停止服务,原因是堆外内存的count达到最大值,这是另外一个话题了,现在就想重写lb的策略,通过自己自定义的策略来实现上述需求。

我们熟悉代码最快的方法是什么?我觉得是debug,那就先本地debug启动gateway,然后打上断点,来通过debug调试熟悉整个请求转发以及lb的策略是什么样的。

首先我知道gateway一定会走LoadBalancerClientFilter,别问我怎么知道的,要问就是随便在spring-cloud-gateway-core的源码里根据类名猜的,这个基类只有2个方法

public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
复制代码
protected ServiceInstance choose(ServerWebExchange exchange)

复制代码

都打上断点瞅一瞅

通过Paw给本地的gateway发请求       

这个里面和这次需求有关的代码为:

if (url != null && ("lb".equals(url.getScheme()) || "lb".equals(schemePrefix)))
复制代码

这块判断因为反编译所以是写死的lb字符串,以spring的技术规范,源码应该不至于写成这样。

只有配置成lb协议的才会走choose方法,而根据choose的返回值可以猜出来这个方法的作用就是选取一个服务提供者。

protected ServiceInstance choose(ServerWebExchange exchange) {
        return this.loadBalancer.choose(((URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR)).getHost());
    }
复制代码

而choose的方法实现调用了loadBalancer的choose方法。

而实现了choose接口方法的实现类有哪些呢?

我目前也不知道会走哪个实现类,都打上断点看看会到哪个实现类里

可以看到走的是RibbonLoadBalancerClient这个实现类。方法实现又转给了自己的choose方法

public ServiceInstance choose(String serviceId, Object hint) {
        Server server = this.getServer(this.getLoadBalancer(serviceId), hint);
        return server == null ? null : new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
    }
复制代码
protected ILoadBalancer getLoadBalancer(String serviceId) {
        return this.clientFactory.getLoadBalancer(serviceId);
    }
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
        return loadBalancer == null ? null : loadBalancer.chooseServer(hint != null ? hint : "default");
    }
复制代码

通过抽象工厂拿到对应的loadBalancer,并调用其chooseServer方法

真实的loadBalancer是谁呢?是ZoneAwareLoadBalancer继承DynamicServerListLoadBalancer,这个loadBalancer为什么是ZoneAwareLoadBalancer,可以配置么?我目前还不知道。先忽略工厂策略,继续往下走。

这个ZoneAwareLoadBalancer玩了个寂寞,因为我只有一个Zone所以直接调用父类BaseLoadBalancer的chooseServer方法,并传了个default的key。

public Server chooseServer(Object key) {
        if (this.counter == null) {
            this.counter = this.createCounter();
        }

        this.counter.increment();
        if (this.rule == null) {
            return null;
        } else {
            try {
                return this.rule.choose(key);
            } catch (Exception var3) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", new Object[]{this.name, key, var3});
                return null;
            }
        }
    }
复制代码

这里又转交给了rule的choose方法。这里的rule是IRule接口,实现类有如下几个:

实际实现类是ZoneAvoidanceRule继承PredicateBasedRule

public Server choose(Object key) {
        ILoadBalancer lb = this.getLoadBalancer();
        Optional<Server> server = this.getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
        return server.isPresent() ? (Server)server.get() : null;
    }
复制代码
    public abstract AbstractServerPredicate getPredicate();
复制代码

交给了实现了chooseRoundRobinAfterFiltering抽象方法的AbstractServerPredicate,而具体实现方法为:

private int incrementAndGetModulo(int modulo) {
        int current;
        int next;
        do {
            current = this.nextIndex.get();
            next = (current + 1) % modulo;
        } while(!this.nextIndex.compareAndSet(current, next) || current >= modulo);

        return current;
    }
复制代码

至此找到了一个轮询的Server,这是默认实现。

你觉得我还有机会吗?先理一下思路,有2个抽象点可以深入看下,一个是获取loadBalancer的时候,一个是loadBalancer的rule。

我们一个一个回溯再深入一下,首先我们还是要回答开头的一个问题

首先我知道gateway一定会走LoadBalancerClientFilter,别问我怎么知道的,要问就是随便在spring-cloud-gateway-core的源码里根据类名猜的

这么不负责任的回答,我现在再看都有点脸红,不瞒你了,直接上代码吧

 

自动加载机制,初始化了LoadBalancerClientFilter,并且依赖RibbonAutoConfiguration.而LoadBalancerClientFilter初始化也需要2个参数:LoadBalancerClient和LoadBalancerProperties,我们看下RibbonAutoConfiguration:

重点这两个Bean,至此LoadBalancerClientFilter初始化完毕,并加入gateway的Filter大军,执行

public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain)方法

现在回到之前留的一个疑问,

这个loadBalancer其实就是LoadBalancerClient,注入的实现类就是RibbonLoadBalancerClient,而

@ConditionalOnMissingBean({LoadBalancerClient.class})

是有机会替换loadBalancer的。

那现在我们再看rule是怎么初始化的。

而RibbonLoadBalancerClient里面的ILoadBalancer也是可以配置的

通过在gateway里配置properties文件,即可指定自定义的ILoadBalancer和IRule

现在我们回顾一下调用流程,发现有2个丧心病狂的地方:

  1. LoadBalancerClientFilter
protected ServiceInstance choose(ServerWebExchange exchange) {
        return this.loadBalancer.choose(((URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR)).getHost());
    }
复制代码

这里丢失了exchange,只有serviceId的信息即:

((URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR)).getHost()

2.RibbonLoadBalancerClient

public ServiceInstance choose(String serviceId) {
        return this.choose(serviceId, (Object)null);
    }
public ServiceInstance choose(String serviceId, Object hint) {
        Server server = this.getServer(this.getLoadBalancer(serviceId), hint);
        return server == null ? null : new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
    }
复制代码

这里直接传了个null

丢失了request的信息,又key默认值"default"

所以我们如果想要实现,必须重写LoadBalancerClientFilter的protected ServiceInstance choose(ServerWebExchange exchange)

而ILoadBalancer其实不需要自定义,只需要自定义IRule即可

代码如下:

/**
 * @author wangdengwu
 */
@Slf4j
public class SameIpBalanceRule extends ClientConfigEnabledRoundRobinRule {

    public SameIpBalanceRule(ILoadBalancer lb) {
        this.setLoadBalancer(lb);
    }

    public SameIpBalanceRule() {

    }

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {

    }

    @Override
    public Server choose(Object ip) {
        log.info("client ip:{}", ip);
        List<Server> servers = this.getLoadBalancer().getReachableServers();
        if (servers.isEmpty()) {
            return null;
        }
        if (servers.size() == 1) {
            return servers.get(0);
        }
        return sameIpChoose(servers, ip);
    }

    private Server sameIpChoose(List<Server> servers, Object ip) {
        for (int i = 0; i < servers.size(); i++) {
            Server server = servers.get(i);
            String host = server.getHost();
            if (StringUtils.equals((CharSequence) ip, host)) {
                return server;
            }
        }
        return super.choose(ip);
    }
}
复制代码
/**
 * @author wangdengwu
 */
@Component
public class SameIpLoadBalancerClientFilter extends LoadBalancerClientFilter {

    @Value("${xxx.same.ip.enable}")
    private Boolean enableSameIp = false;

    public SameIpLoadBalancerClientFilter(LoadBalancerClient loadBalancer, LoadBalancerProperties properties) {
        super(loadBalancer, properties);
    }

    @Override
    protected ServiceInstance choose(ServerWebExchange exchange) {
        //是否开启same ip策略
        if (!enableSameIp) {
            return super.choose(exchange);
        }
        //获取浏览器访问者IP
        String ip = getRealIp(exchange.getRequest());
        String serviceIp = exchange.getRequest().getHeaders().getFirst("serviceIp");
        //强制指定IP优先级最高
        if (serviceIp != null) {
            ip = serviceIp;
        }
        if (this.loadBalancer instanceof RibbonLoadBalancerClient) {
            RibbonLoadBalancerClient client = (RibbonLoadBalancerClient) this.loadBalancer;
            String serviceId = ((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost();
            //这里使用ip做为选择服务实例的key
            return client.choose(serviceId, ip);
        }
        return super.choose(exchange);
    }

    private String getRealIp(ServerHttpRequest request) {
        // 这个一般是Nginx反向代理设置的参数
        String ip = request.getHeaders().getFirst("X-Real-IP");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeaders().getFirst("X-Forwarded-For");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeaders().getFirst("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeaders().getFirst("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddress().getAddress().getHostAddress();
        }
        // 处理多IP的情况(只取第一个IP)
        if (ip != null && ip.contains(",")) {
            String[] ipArray = ip.split(",");
            ip = ipArray[0];
        }
        return ip;
    }

}
复制代码

至此,代码完成了需求。

这里只是gateway实现了自定义路由的功能,其实还有一个地方遗漏了,那就是服务之间使用@FeignClient调用的时候,这块如何实现同源IP功能,就留给你去思考了。