Spring Cloud——服务调用与负载均衡

327 阅读9分钟

       微服务提倡将一个原本独立的系统分成众多小型服务系统,这些小型服务系统都在独立的进程中运行,通过各个小型服务系统之间的协作来实现原本独立系统的所有业务功能。 小型服务系统使用多种跨进程的方式进行通信协作,而RESTful风格的网络请求是最为常见的交互方式之一。 RESTful网络请求是指RESTful风格的网络请求,其中REST是Resource RepresentationalState Transfer的缩写,直接翻译即“资源表现层状态转移”。

声明式调用Fegin

       采用了声明式 API 接口的风格,将 Java Http 客户端绑定到它的内部。Feign 的首要目标是将 Java Http 客户端调用过程变得简单。 Feign 的源码地址:github.com/OpenFeign/f…

Feign简介

        Feign是一个声明式RESTful网络请求客户端。Feign会根据带有注解的函数信息构建出网络请求的模板,在发送网络请求之前,Feign会将函数的参数值设置到这些请求模板中。

       使用Feign的Spring应用架构一般分为三个部分,分别为服务注册中心、服务提供者和服务消费者。服务提供者向服务注册中心注册自己,然后服务消费者通过Feign发送请求时,Feign会向服务注册中心获取关于服务提供者的信息,然后再向服务提供者发送网络请求。

Feign的工作原理

       Feign是一个伪Java Http客户端,Feign不做任何的请求处理。Feign通过处理注解生成Request模板,从而简化了HttpAPI的开发。开发人员可以使用注解的方式定制 Request API模板。在发送Http Request请求之前,Feign通过处理注解的方式替换掉Request模板中的参数,生成真正的Request,并交给Java Htp客户端去处理。利用这种方式,开发者只需要关注Feign注解模板的开发,而不用关注Http请求本身,简化了Http请求的过程,使得Http请求变得简单和容易理解。

        首先Feign通过包扫描注入FeignClient的Bean,该源码在FeignClientsRegistrar类中。首先在程序启动时,会检查是否有@EnableFeignClients注解,如果有该注解,则开启包扫描,扫描被@FeignClient注解的接口。代码如下:

//FeignClientsRegistrar类
private void registerDefaultConfiguration(AnnotationMetadata metadata,
		BeanDefinitionRegistry registry) {
	Map<String, Object> defaultAttrs = metadata
			.getAnnotationAttributes(EnableFeignClients.class.getName(), true);

	if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
		String name;
		if (metadata.hasEnclosingClass()) {
			name = "default." + metadata.getEnclosingClassName();
		}
		else {
			name = "default." + metadata.getClassName();
		}
		registerClientConfiguration(registry, name,
				defaultAttrs.get("defaultConfiguration"));
	}
}

         接着通过包扫描将有 @FeignClient 注解修饰的接口连同接口名和注解的信息一起取出,赋给 BeanDefinitionBuilder, 然后根据 BeanDefinitionBuilder 得到 BeanDefinition,最后将 BeanDefinition 注入 IoC 容器中, 源码如下:

//FeignClientsRegistrar类private void registerFeignClient(BeanDefinitionRegistry registry,
		AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
	String className = annotationMetadata.getClassName();
	BeanDefinitionBuilder definition = BeanDefinitionBuilder
			.genericBeanDefinition(FeignClientFactoryBean.class);
	validate(attributes);
	definition.addPropertyValue("url", getUrl(attributes));
	definition.addPropertyValue("path", getPath(attributes));
	String name = getName(attributes);
	definition.addPropertyValue("name", name);
	definition.addPropertyValue("type", className);
	definition.addPropertyValue("decode404", attributes.get("decode404"));
	definition.addPropertyValue("fallback", attributes.get("fallback"));
	definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
	definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

	String alias = name + "FeignClient";
	AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();

	boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null

	beanDefinition.setPrimary(primary);

	String qualifier = getQualifier(attributes);
	if (StringUtils.hasText(qualifier)) {
		alias = qualifier;
	}

	BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
			new String[] { alias });
	BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}

        注入 BeanDefinition 之后,通过JDK 的代理,当调用 Feign Client 接口里面的方法时,该方法会被拦截,源码在 ReflectiveFeign 类,代码如下:

public <T> T newInstance(Target<T> target) {
        ...//省略代码
	for (Method method : target.type().getMethods()) {
	  if (method.getDeclaringClass() == Object.class) {
		continue;
	  } else if(Util.isDefault(method)) {
		DefaultMethodHandler handler = new DefaultMethodHandler(method);
		defaultMethodHandlers.add(handler);
		methodToHandler.put(method, handler);
	  } else {
		methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
	  }
	}
	InvocationHandler handler = factory.create(target, methodToHandler);
	T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);

	for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
	  defaultMethodHandler.bindTo(proxy);
	}
	return proxy;
}

      在 SynchronousMethodHandler 类进行拦截处理,会根据参数生成 RequestTemplate 对象, 该对象是 Http 请求的模板,代码如下:

public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        return executeAndDecode(template);
      } catch (RetryableException e) {
        retryer.continueOrPropagate(e);
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }

        最后通过 RequestTemplate 生成 Request 请求对象,然后用 Http Client 获取 Response,即通过 Http Client 进行 Http 请求来获取响应。

在 Feign 中使用 HttpClient 和 OkHttp

       在 Feign 中,Client 是一个非常重要的组件,Feign 最终发送 Request 请求以及接收 Response 响应都是由 Client 组件完成的。Client 在 Feign 源码中是一个接口,在默认的情况下,Client 的实现类是 Client.Default,Client.Default 是由 HttpURLConnnection 来实现网络请求的。另外, Client 还支持 HttpClient 和 OkhHttp 来进行网络请求。 

public Response execute(Request request, Options options) throws IOException {
  HttpURLConnection connection = convertAndSend(request, options);
  return convertResponse(connection).toBuilder().request(request).build();
}

通过在配置文件 application.yml 中配置 

feign:
  httpclient:
    enabled: true

pom 文件加上 feign-httpclient 的依赖,Feign 就会采用 HttpClient 作为网络请求,而不是默认的 HttpURLConnection

<dependency>
	<groupId>com.netflix.feign</groupId>
	<artifactId>feign-httpclient</artifactId>
	<version>RELEASE</version>
</dependency>

使用 Okhttp 作为网络请求框架,则只需要在 pom 文件上加上 feign-okhttp 的依赖

<dependency>
	<groupId>com.netflix.feign</groupId>
	<artifactId>feign-okhttp</artifactId>
	<version>RELEASE</version>
</dependency>

总结

       Feign 的源码实现过程如下: 

(1)首先通过@EnableFeignClients 注解开启 FeignClient 的功能。只有这个注解存在,才 会在程序启动时开启对@FeignClient 注解的包扫描。 

(2)根据 Feign 的规则实现接口,并在接口上面加上@FeignClient 注解。 

(3)程序启动后,会进行包扫描,扫描所有的@ FeignClient 的注解的类,并将这些信息 注入 IoC 容器中。 

(4)当接口的方法被调用时,通过 JDK 的代理来生成具体的 RequestTemplate 模板对象。 

(5)根据 RequestTemplate 再生成 Http 请求的 Request 对象。 

(6)Request 对象交给Client 去处理,其中Client 的网络请求框架可以是HttpURLConnection、 HttpClient 和 OkHttp。 

(7)最后 Client 被封装到 LoadBalanceClient 类,这个类结合类 Ribbon 做到了负载均衡。

负载均衡Ribbon 

       负载均衡是指将负载分摊到多个执行单元上,常见的负载均衡有两种方式:

  • 一种是独立进程单元,通过负载均衡策略,将请求转发到不同的执行单元上,例如 Ngnix。
  • 另一种是将负载均衡逻辑以代码的形式封装到服务消费者的客户端上,服务消费者客户端维护了一份服务提供者的信息列表,有了信息列表,通过负载均衡策略将请求分摊给多个服务提供者,从而达到负载均衡的目的。 

Ribbon 简介

        Ribbon 是 Netflix 公司开源的一个负载均衡的组件,它属于上述的第二种方式,是将负载均衡逻辑封装在客户端中,并且运行在客户端的进程里。

        在 Spring Cloud 构建的微服务系统中,Ribbon 作为服务消费者的负载均衡器,有两种使用方式,一种是和 RestTemplate 相结合,另一种是和 Feign 相结合。Feign 已经默认集成了 Ribbon。

Ribbon负载均衡原理

负载均衡器的核心类为 LoadBalancerClient,它是一个 接口类,继承了 ServiceInstanceChooser,它的实现类为 RibbonLoadBalanceClient

LoadBalancerClient 是一个负载均衡的客户端,有如下 3 种方法。其中有 2 个 excute()方法, 均用来执行请求,reconstructURI()用于重构 Url

public interface LoadBalancerClient extends ServiceInstanceChooser {
    <T> T execute(String var1, LoadBalancerRequest<T> var2) throws IOException;

    <T> T execute(String var1, ServiceInstance var2, LoadBalancerRequest<T> var3) throws IOException;

    URI reconstructURI(ServiceInstance var1, URI var2);
}

ServiceInstanceChooser 接口有一个方法用于根据 serviceId 获取 ServiceInstance,即通过服务名来选择服务实例,代码如下:

public interface ServiceInstanceChooser {    ServiceInstance choose(String var1);}

LoadBalancerClient 的实现类为 RibbonLoadBalancerClient,最终的负载均衡的请求处理由它来执行。

在 RibbonLoadBalancerClient 的源码中,choose()方法用于选择具体服务实例。

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

该方法通过 getServer()方法去获取实例,最终交给 ILoadBalancer 类去选择服务实例。

public interface ILoadBalancer {
	//添加一个 Server 集合
    void addServers(List<Server> var1);
	//根据 key 去获取 Server
    Server chooseServer(Object var1);
	//标记某个服务下线
    void markServerDown(Server var1);
	//获取可用的Server 集合
    List<Server> getReachableServers();
	//获取所有的 Server 集合
    List<Server> getAllServers();
}

在ILoadBalancer 接口的实现类 DynamicServerListLoadBalancer构造函数中的initWithNiwsConfig()方法。在该方法中经过一系列的初始化配置。其中包括:

IClientConfig
用于配置负载均衡的客户端,IClientConfig 的默认实现类为 DefaultClientConfigImpl

IRule

       用于配置负载均衡的策略,IRule 有很多默认的实现类,这些实现类根据不同的算法和逻辑来处理负载均衡的策略。 IRule 的默认实现类有以下7种

  • BestAvailableRule:选择最小请求数。
  • ClientConfigEnabledRoundRobinRule:轮询。
  • RandomRule:随机选择一个 server。 
  • RoundRobinRule(默认):轮询选择 server。 
  • RetryRule:根据轮询的方式重试。 
  • WeightedResponseTimeRule:根据响应时间去分配一个 weight ,weight 越低,被选 择的可能性就越低。 
  • ZoneAvoidanceRule:根据 server 的 zone 区域和可用性来轮询选择。

IPing

     IPing 用于向 server 发送“ping”,来判断该 server 是否有响应,从而判断该 server 是否可 用。IPing 的实现类

  • PingUrl:真实地去 ping 某个 Url,判断其是否可用。
  • PingConstant:固定返回某服务是否可用,默认返回 true,即可用。 
  • NoOpPing:不去 ping,直接返回 true,即可用。 
  • DummyPing(默认):直接返回 true,并实现了 initWithNiwsConfig 方法。 
  • NIWSDiscoveryPing:根据 DiscoveryEnabledServer 的 InstanceInfo 的 InstanceStatus 去判断,如果为 InstanceStatus.UP,则可用,否则不可用。

ServerList

       ServerList 是定义获取所有 server 的注册列表信息的接口

ServerListFilter

       ServerListFilter 接口定义了可根据配置去过滤或者特性动态地获取符合条件的 server 列表的方法

负载均衡器是从 Eureka Client 获取服务列表信息的,并根据 IRule 的策略去路由,根据 IPing 去判断服务的可用性。

在 BaseLoadBalancer 类的 setupPingTask()方法中,开启了的 PingTask 任务, 在默认情况下,变量值为 10,即每 10 秒向 EurekaClient 发送一次心跳 “ping”。

void setupPingTask() {
	if (!this.canSkipPing()) {
		if (this.lbTimer != null) {
			this.lbTimer.cancel();
		}

		this.lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + this.name, true);
		this.lbTimer.schedule(new BaseLoadBalancer.PingTask(), 0L, (long)(this.pingIntervalSeconds * 1000));
		this.forceQuickPing();
	}
}

小结

       Ribbon 的负载均衡主要是通过 LoadBalancerClient 来实现的,而 LoadBalancerClient 具体交给了 ILoadBalancer 来处理,ILoadBalancer 通过配置 IRule、IPing 等,向 EurekaClient 获取注册列表的信息,默认每 10 秒向 EurekaClient 发送一次“ping”,进而检查是否需要更新服务的注册列表信息。如果服务的可用性发生了改变或者服务 数量和之前的不一致,则更新或者重新拉取。最后,在得到服务注册列表信息后,ILoadBalancer 根据 IRule 的策略进行负载均衡。

       RestTemplate 加上@LoadBalance 注解后,在远程调度时能够负载均衡,主要是维护了 一个被@LoadBalance 注解的 RestTemplate 列表,并给该列表中的 RestTemplate 对象添加了拦 截器。在拦截器的方法中,将远程调度方法交给了 Ribbon 的负载均衡器 LoadBalancerClient 去处理,从而达到了负载均衡的目的。

参考

深入理解Spring Cloud与微服务构建