想法
在使用gateway时gateway是这样处理请求的,当一个请求到达gateway时,gateway会从路由配置中选择一个Instance将这个请求转发过去(我们的服务都是通过lb://协议接入的),在默认情况下是随机选择Instance。所以有一个想法,能不能根据Instance的负载,选择负载最小的那个Instance进行转发。我们的服务接入了promethus或者actuator可以通过它们获取到一个衡量服务器负载的参数。
方案
首先确定一个参数来衡量Instance的负载,这里采用Prometheus的process_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转发请求
实现
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,将Instance和promethus的返回数据封装在一起 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
- 定时任务
@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接口,构造出serviceDataMap和instanceDataMap。- 在完成之后,
ClearInstanceThread删除掉已经down掉的Instance。 - 这里有2个异步的地方,第一个是获取所有Instance的地方,还有一个是访问
promethus接口的地方。第一个异步完成时ClearInstanceThread启动,但这里嵌套了http请求,所以额外使用了CountDownLatch确保所有的http完成时才能执行ClearInstanceThread的run函数。
- 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相关的知识
ReactorServiceInstanceLoadBalancer,Instance选择器Promethus的返回数据结构。- React的代码编写方式。
2368

被折叠的 条评论
为什么被折叠?



