Gateway自定义Instance选择器

想法

在使用gateway时gateway是这样处理请求的,当一个请求到达gateway时,gateway会从路由配置中选择一个Instance将这个请求转发过去(我们的服务都是通过lb://协议接入的),在默认情况下是随机选择Instance。所以有一个想法,能不能根据Instance的负载,选择负载最小的那个Instance进行转发。我们的服务接入了promethus或者actuator可以通过它们获取到一个衡量服务器负载的参数。

方案

首先确定一个参数来衡量Instance的负载,这里采用Prometheusprocess_cpu_usage这个参数,为了获取这个参数,需要每个Instance都加载promethus的插件。gateway通过访问promethus的接口获取到process_cpu_usage的数值,我们将相同Service的这个参数放在一个list中,然后做好排序,每次转发时选取负载最少的那个,将请求转发到这个Instance。这个list每10秒左右维护一次,重新获取下process_cpu_usage,并且排序。但这里有一个小问题,不能保证此时Instance还在正常工作中,所以,在转发之前先发送一个请求到Instance,看看是不是正常工作中,如果不工作了,那么按照顺序选取下一个Instance。如果promethus的接口出问题了,或者返回延迟了,那么就随机一个Instance转发。

方案总结

  • 建立一个Instance的list。这个list按照system.load.average.1m排序。
  • 建立一个定时任务维护这个list
  • 自定义一个LoadBalancerClientFilter类,用来替换默认的Filter,这个Filter按照list排序选择Instance转发请求

实现

  1. promethus的相关处理
@Data
public class InstanceData {
    private Boolean death;
    private ServiceInstance serviceInstance;
    private Map<String, PrometheusData> prometheusDatas;
}

@Data
public class PrometheusData {
    private Long timestamp;
    private String name;
    private String type;
    private String description;
    private List<MetricData> datas;

    public void addMetricData(MetricData data){
        if(datas==null)
            datas=new ArrayList<>();
        datas.add(data);
    }

    @Data
    public static class MetricData{
        private Float value;
        private Map<String,String> metric;
    }
}


private Map<String,List<InstanceData>> serviceDataMap=new ConcurrentHashMap<>();

private Map<String, Map<String,InstanceData>> instanceDataMap=new ConcurrentHashMap<>();

public List<InstanceData> getInstanceDataList(String servicdId){
        return serviceDataMap.get(servicdId);
    }

  • 定义一个全局变量instanceDataMap保存Instance的数据(按照Service分),
  • 定义一个全局变量serviceDataMap保存Service的数据,
  • 定义一个类InstanceData,将Instancepromethus的返回数据封装在一起
  • promethus的返回结构处理成PrometheusData,这样基本的数据结构定义完成。
  • 通过List<InstanceData> getInstanceDataList(String servicdId)函数获取Instance的List。这个List的第一个元素就是我们转发的Instance。

@Slf4j
public class PrometheusHandler implements Function<String, List<PrometheusData>> {
    @Override
    public List<PrometheusData> apply(String m) {
        String[] ms=m.split("[\n]");
        final String[] data=new String[2];
        List<PrometheusData> prometheusDatas=new ArrayList<>();
        PrometheusData prometheusData=null;
        for(String s:ms){
            if(s.startsWith("#")){
                String[] sarray=s.split("[ ]");
                if(sarray[1].equals("HELP")){
                    prometheusData=new PrometheusData();
                    prometheusData.setTimestamp(System.currentTimeMillis());
                    prometheusDatas.add(prometheusData);
                    prometheusData.setName(sarray[2]);
                    String description=s.replace("#","").replace("HELP","").replace(sarray[2],"").trim();
                    prometheusData.setDescription(description);
                    data[0]=sarray[2];
                }else if(sarray[1].equals("TYPE")) {
                    data[1]=sarray[3];
                    prometheusData.setType(sarray[3]);
                }
            }else {
                PrometheusData.MetricData md=dueMetricData(s);
                if(md!=null)
                    prometheusData.addMetricData(md);
            }
        }
        return prometheusDatas;
    }

    public static PrometheusData.MetricData  dueMetricData(String d){
        PrometheusData.MetricData data=new PrometheusData.MetricData();
        String[] ds=split(d);
        if(StringUtils.isBlank(ds[2]))
            return null;
        data.setValue(Float.parseFloat(ds[2]));
        Map<String,String> metricMap=new HashMap<>();

        for(String s:ds[1].split("[,]")){
            String[] ms=s.split("[=]");
            metricMap.put(ms[0].replace("\"","").trim(),ms[1]);
        }
        data.setMetric(metricMap);

        return data;
    }

    public static String[] split(String s){
        int start = s.indexOf("{");
        int end=s.lastIndexOf("}");
        String name="";
        String metirc="";
        String value="";
        try {
            name = s.substring(0, start);
            metirc=s.substring(start+1,end-1);
            value=s.substring(end+2);
        }catch (StringIndexOutOfBoundsException e){
            log.error("s={}",s);
        }

        return new String[]{name,metirc,value};
    }
}

这段代码是处理promethus的返回数据,将返回的字符串处理成PrometheusData.MetricData

  1. 定时任务
@Scheduled(fixedDelay = 1000*10)
    private void refleshInstance(){
        Set<String> instanceids=new HashSet<>();
        ClearInstanceThread clear=new ClearInstanceThread(instanceids,instanceDataMap,serviceDataMap);
        AtomicInteger total= new AtomicInteger();
        routeLocator.getRoutes().doOnNext(r-> discoveryClient.getInstances(r.getUri().getHost()).stream().forEach(i->total.incrementAndGet())).subscribe();
        log.info("instance total {}",total.get());
        instanceLatch=new CountDownLatch(total.get());
        routeLocator.getRoutes().map(r->r.getUri().getHost()).subscribe(serviceid-> discoveryClient.getInstances(serviceid).stream().forEach(i->{

            String url="http://"+i.getHost()+":"+i.getPort()+"/actuator/prometheus";
            log.info("promethus:{}",url);
            HttpUtils.getText(url,null).filter(s->!s.equals("fail")).subscribe(s -> {

                List<PrometheusData> prometheusDatas = prometheusHandler.apply(s);
                Map<String, PrometheusData> prometheusDataMap = new HashMap<>();
                prometheusDatas.stream().forEach(d -> prometheusDataMap.put(d.getName(), d));
                Map<String, InstanceData> instanceMap = instanceDataMap.get(i.getServiceId());

                instanceMap = (instanceMap == null ? new ConcurrentHashMap<>() : instanceMap);
                instanceDataMap.put(i.getServiceId(), instanceMap);
                InstanceData instanceData = instanceMap.get(i.getInstanceId());
                if (instanceData == null) {
                    instanceData = new InstanceData();
                    instanceData.setServiceInstance(i);
                    instanceMap.put(i.getInstanceId(), instanceData);
                }
                instanceData.setPrometheusDatas(prometheusDataMap);
                instanceids.add(i.getInstanceId());
            }, e -> log.error("", e), () -> instanceLatch.countDown());
        }), error->log.error("",error),new ClearInstanceThread(instanceids,instanceDataMap,serviceDataMap));
    }
    

public class ClearInstanceThread extends Thread{
        private Set<String> instanceIds;
        private Map<String, Map<String,InstanceData>> instanceDataMap;
        private Map<String,List<InstanceData>> serviceDataMap;
        public ClearInstanceThread(Set<String> instanceIds, Map<String, Map<String,InstanceData>> instanceDataMap, Map<String,List<InstanceData>> serviceDataMap){
            this.instanceIds=instanceIds;
            this.instanceDataMap=instanceDataMap;
            this.serviceDataMap=serviceDataMap;
        }

        @Override
        public void run() {
            try {
                instanceLatch.await(10,TimeUnit.SECONDS);
                log.info("instanceIds count {}", instanceIds.size());
                for (Map.Entry<String, Map<String, InstanceData>> entry : instanceDataMap.entrySet()) {
                    String servceid = entry.getKey();
                    log.info("clear before  {},{}", servceid, entry.getValue().size());
                    Iterator<Map.Entry<String, InstanceData>> instanceDataIterable = entry.getValue().entrySet().iterator();
                    while (instanceDataIterable.hasNext()) {
                        String instanceid = instanceDataIterable.next().getValue().getServiceInstance().getInstanceId();
                        log.info("{}", instanceid);
                        if (!instanceIds.contains(instanceid)) {
                            instanceDataIterable.remove();
                        }
                    }

                    List<InstanceData> instanceDatas = entry.getValue().entrySet().stream().map(e -> e.getValue()).collect(Collectors.toList());
                    log.info("clear after  {},{}", servceid, instanceDatas.size());
                    instanceDatas.sort(instanceCompareByPrometheusMetricCPU);
                    serviceDataMap.put(servceid, instanceDatas);
                }
            }
            catch (Exception e){
                log.error("",e);
            }
        }

    }

  • 这是一个定时任务
  • routeLocator.getRoutes().map(r->r.getUri().getHost()).subscribe(serviceid-> discoveryClient.getInstances(serviceid).stream().forEach(i->{ …… }获取所有的route,并且遍历所有的Instance的promethus接口,构造出serviceDataMapinstanceDataMap
  • 在完成之后,ClearInstanceThread删除掉已经down掉的Instance。
  • 这里有2个异步的地方,第一个是获取所有Instance的地方,还有一个是访问promethus接口的地方。第一个异步完成时ClearInstanceThread启动,但这里嵌套了http请求,所以额外使用了CountDownLatch确保所有的http完成时才能执行ClearInstanceThread的run函数。
  1. Filter

@Component
@Slf4j
public class PrometheusLoadBalancerClientFilter implements GlobalFilter, Ordered {
    private final LoadBalancerClientFactory clientFactory;

    private final GatewayLoadBalancerProperties properties;

    private final LoadBalancerProperties loadBalancerProperties;

    @Autowired
    private InstanceService instanceService;

    private Map<String,PrometheusLoadBacancer> prometheusLoadBacancerMap=new ConcurrentHashMap<>();

    public PrometheusLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory,
                                              GatewayLoadBalancerProperties properties,
                                              LoadBalancerProperties loadBalancerProperties){
        this.clientFactory = clientFactory;
        this.properties = properties;
        this.loadBalancerProperties = loadBalancerProperties;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        Route route=exchange.getAttribute(GATEWAY_ROUTE_ATTR);
        String serviceid=route.getUri().getHost();
        PrometheusLoadBacancer prometheusLoadBacancer=prometheusLoadBacancerMap.get(serviceid);
        if(prometheusLoadBacancer==null){
            prometheusLoadBacancer=new PrometheusLoadBacancer(instanceService,serviceid);
            prometheusLoadBacancerMap.put(serviceid,prometheusLoadBacancer);
        }

        return prometheusLoadBacancer.choose(ReactiveLoadBalancer.REQUEST).
                doOnNext(r->{
                    if(r.getServer()!=null) {
                        log.info("prometheus loadbalancer choose {}",r.getServer().getInstanceId());
                        URI uri = exchange.getRequest().getURI();
                        URI requestUrl = LoadBalancerUriTools.reconstructURI(r.getServer(), uri);
                        exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
                    }else{
                        log.info("Server is null:{}",route.getUri());
                    }
                }).then(chain.filter(exchange)).doOnError(er->log.debug("",er));
    }

    @Override
    public int getOrder() {
        return 10151;
    }

    @Slf4j
    public static class PrometheusLoadBacancer implements ReactorServiceInstanceLoadBalancer{
        private InstanceService instanceService;
        private String serviceId;

        public PrometheusLoadBacancer(InstanceService instanceService,String serviceId){
            this.instanceService=instanceService;
            this.serviceId=serviceId;
        }

        @Override
        public Mono<Response<ServiceInstance>> choose(Request request) {
            List<InstanceData> instanceData=instanceService.getInstanceDataList(this.serviceId);
            if(instanceData==null){
                log.warn("choose null {}",this.serviceId);
                return Mono.just(new DefaultResponse(null));
            }else {
                return Flux.fromIterable(instanceData).filter(s -> instanceService.isLiveAnsy(s.getServiceInstance())).next().
                        map(s -> new DefaultResponse(s.getServiceInstance()));
            }
        }

        @Override
        public Mono<Response<ServiceInstance>> choose() {
            return ReactorServiceInstanceLoadBalancer.super.choose();
        }
    }

}

  • 定一个PrometheusLoadBacancer类,并且实现ReactorServiceInstanceLoadBalancer这个接口,这里有2个重载函数Mono<Response<ServiceInstance>> choose(Request request)Mono<Response<ServiceInstance>> choose()第一个函数是我们需要的,从instanceData获取第一个可用的Instance。第二个函数是默认的获取Instance的方法。
  • Route route=exchange.getAttribute(GATEWAY_ROUTE_ATTR)获取到route,从这里获取到serviceid,通过serviceid获取到PrometheusLoadBacancer对象,再通过PrometheusLoadBacancer进行Instance选择。

这样我们就实现了一个自定义的Instance选择的功能。

知识点

完成以上功能需要了解几个gateway相关的知识

  1. ReactorServiceInstanceLoadBalancer,Instance选择器
  2. Promethus的返回数据结构。
  3. React的代码编写方式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值