负载均衡
负载均衡(Load Balancing)是一种计算机技术,用来在多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或其他资源中分配负责,以达到最优化资源使用、最大化吞吐量、最小化响应时间,同时避免出现过载的目的。
常见的负载均衡算法:
-
随机(
Random)算法:在实例列表中随机选择某个实例。 -
轮询(
RoundRobin)算法:循环取下一个。 -
最少连接数(
Least Connections)算法:每次取连接数最少的实例。 -
一致性(
Consistent Hashing)算法:基于一致性哈希算法总是将相同参数的请求落在同一实例上。 -
权重随机(
Weightd Random)算法:根据权重+随机选择某个实例。
Spring Cloud LoadBalancer负载均衡组件
SCL是新一代Spring Cloud客户端负载均衡的实现。2019年7月3日,在Hoxton.M1的发布公告上。Spring宣布更新该项目来代替Netflix Ribbon。
SCL相关的代码在spring-cloud-commons模块:
-
ServiceInstanceChooser:服务实例选择器,根据服务名获取一个服务实例(ServiceInstance)。 -
LoadBalancerClient:客户端负责均衡器,继承ServiceInstanceChooser,会根据ServiceInstance和Request请求信息执行最终结果。 -
BlockingLoadBalancerClient:基于Spring Cloud LoadBalancer的LoadBalancerClient默认实现。 -
RibbonLoadBalancerClient:基于Netflix Ribbon的LoadBalancerClient实现。
ServiceInstanceChooser
ServiceInstanceChooser服务实例选择器定义:
public interface ServiceInstanceChooser {
/**
* 根据服务名得到一个服务实例
* @param serviceId 服务名
* @return 对应服务名下的服务实例
*/
ServiceInstance choose(String serviceId);
}
自定义RandomServiceInstanceChooser(随机算法)获取ServiceInstance,再使用RestTemplate进行服务调用。
// 自定义服务实例选择器
public class RandomServiceInstanceChooser implements ServiceInstanceChooser {
private final DiscoveryClient discoveryClient;
private final Random random;
public RandomServiceInstanceChooser(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
random = new Random();
}
@Override
public ServiceInstance choose(String serviceId) {
List<ServiceInstance> serviceInstanceList =
discoveryClient.getInstances(serviceId);
return serviceInstanceList.get(random.nextInt(serviceInstanceList.size()));
}
}
// 方式一
@Bean
public RestTemplate normalRestTemplate() {
return new RestTemplate();
}
@Bean
public RandomServiceInstanceChooser randomServiceInstanceChooser(DiscoveryClient discoveryClient) {
return new RandomServiceInstanceChooser(discoveryClient);
}
@GetMapping("/customChooser")
public String customChooser() {
ServiceInstance serviceInstance = randomServiceInstanceChooser.choose(serviceName);
return normalRestTemplate.getForObject(
"http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/", String.class);
}
// 方式二
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
}
@GetMapping("/customChooser")
public String customChooser() {
return restTemplate.getForObject("http://" + serviceName + "/", String.class);
}
@LoadBalanced
spring-cloud-commons模块中的META-INF/spring-factories文件里存在LoadBalancerAutoConfiguration这个自动化配置类,根据工厂加载机制会被ApplicationContext加载。自动化配置类内部的Bean构造代码如下:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList(); //1
@Autowired(required = false)
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
//2
final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
return () -> restTemplateCustomizers.ifAvailable(customizers -> {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);//3
}
}
});
}
...
}
-
获取ApplicationContext中所有被
@LoadBalanced注解修饰的RestTemplate。 -
List<RestTemplateCustomizer>>是ApplicationContext存在的RestTemplateCustomizer Bean的集合。 -
遍历
RestTemplate集合,并使用RestTemplateCustomizer集合给每个RestTemplate定制。
//org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration.java
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate") // 1
static class LoadBalancerInterceptorConfig {
@Bean
public LoadBalancerInterceptor ribbonInterceptor( // 2
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer( // 3
final LoadBalancerInterceptor loadBalancerInterceptor) { // 4
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor); // 5
restTemplate.setInterceptors(list);
};
}
}
-
条件注解。
LoadBalancerInterceptorConfig配置类只有在ClassLoader不存在RetryTemplate时才会生效。 -
定义
LoadBalancerInterceptorBean,这个拦截器继承ClientHttpRequestInterceptor,可以被添加到RestTemplate拦截器列表中。 -
定义
RestTemplateCustomizerBean,会在LoadBalancerAutoConfiguration里的RestTemplateCustomizer列表中存在。 -
LoadBalancerInterceptor参数是代码2处创建的Bean。 -
使用lambda表达式在
RestTemplate的拦截器列表添加LoadBalancerInterceptor拦截器。
如果ClassLoader存在RetryTemplate,会触发另外一个配置类:
RetryInterceptorAutoConfiguration。该配置类内部的操作与LoadBalancerInterceptorConfig配置类唯一的区别就是构造RetryLoadBalancerInterceptor拦截器(跟LoadBalancerInterceptor相比,在RestTemplate调用失败的情况下会进行重试操作)。
LoadBalancerInterceptor拦截器:
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
private LoadBalancerClient loadBalancer;
private LoadBalancerRequestFactory requestFactory;
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer,
LoadBalancerRequestFactory requestFactory) { // 1
this.loadBalancer = loadBalancer;
this.requestFactory = requestFactory;
}
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
// for backwards compatibility
this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
}
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost(); // 2
Assert.state(serviceName != null,
"Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName, // 3
this.requestFactory.createRequest(request, body, execution));
}
}
-
LoadBalancerInterceptor构造器需要LoadBalancerClient和LoadBalancerRequestFactory参数(默认会在LoadBalancerAutoConfiguration里被构造,开发者可以进行覆盖)。前者需要根据负载均衡请求和服务名做真正的服务调用,后者构造负载均衡请求,构造过程中会使用LoadBalancerRequestTransformer对请求做一些自定义转换操作(默认情况下,LoadBalancerRequestTransformer接口无任何实现类,开发者可以根据业务构造Bean进行Request的转换操作)。 -
服务名使用URI中的host信息。
-
使用
LoadBalancerClient客户端负责均衡器做真正的服务调用。
LoadBalancerClient
LoadBalancerClient(客户端负载均衡器)会根据负载均衡器请求和服务名执行真正的负载均衡操作,该接口具体定义如下:
public interface LoadBalancerClient extends ServiceInstanceChooser {
/**
* 使用负载均衡得到的 ServiceInstance 为指定的服务执行请求
* @param serviceId 服务名
* @param request 负载均衡请求
* @param <T> type of the response
* @throws IOException in case of IO issues.
* @return 基于选中的服务实例 ServiceInstance 在 LoadBalancerRequest 回调中的返回结果
<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
/**
* 使用负载均衡得到的 ServiceInstance 为指定的服务执行请求
* @param serviceId 服务名
* @param serviceInstance 服务实例
* @param request 负载均衡请求
* @param <T> type of the response
* @throws IOException in case of IO issues.
* @return 基于选中的服务实例 ServiceInstance 在 LoadBalancerRequest 回调中的返回结果
*/
<T> T execute(String serviceId, ServiceInstance serviceInstance,
LoadBalancerRequest<T> request) throws IOException;
/**
* 使用服务实例的 ServiceInstance 中的host和port属性构造出真正的URI
* @param instance 服务实例
* @param original 带有服务名的URLI
* @return 重新构造的URI
*/
URI reconstructURI(ServiceInstance instance, URI original);
}
-
reconstructURI方法。这个方法用于重新构造URI。比如,要访问nacos-provider-lb服务下的“/”路径,这个URI为http://nacos-provider-lb/。nacos-provider-lb服务在注册中心有10个服务实例,某个服务实例ServiceInstance的IP为192.168.1.100,端口为8080.那么重新构造的真正的URI为http://192.168.1.100:8080/。 -
execute方法。有两个重载方法,其中一个方法比另外一个方法多了ServiceInstance服务实例参数。没有ServiceInstance参数的方法内部会通过choose方法(父接口ServiceInstanceChooser提供)使用负载均衡算法得到一个ServiceInstance,然后调用带有ServiceInstance参数的execute方法。
LoadBalancerClient默认实现类为基于SCL的BlockingLoadBalancerClient:
public class BlockingLoadBalancerClient implements LoadBalancerClient {
private final LoadBalancerClientFactory loadBalancerClientFactory;
public BlockingLoadBalancerClient(
LoadBalancerClientFactory loadBalancerClientFactory) {
this.loadBalancerClientFactory = loadBalancerClientFactory; // 1
}
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request)
throws IOException {
ServiceInstance serviceInstance = choose(serviceId); // 2
if (serviceInstance == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
return execute(serviceId, serviceInstance, request);
}
@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance,
LoadBalancerRequest<T> request) throws IOException {
try {
return request.apply(serviceInstance); // 3
}
catch (IOException iOException) {
throw iOException;
}
catch (Exception exception) {
ReflectionUtils.rethrowRuntimeException(exception);
}
return null;
}
@Override
public URI reconstructURI(ServiceInstance serviceInstance, URI original) {
return LoadBalancerUriTools.reconstructURI(serviceInstance, original); // 4
}
@Override
public ServiceInstance choose(String serviceId) {
ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory
.getInstance(serviceId); //5
if (loadBalancer == null) {
return null;
}
Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose())
.block();
if (loadBalancerResponse == null) {
return null;
}
return loadBalancerResponse.getServer();
}
}
-
BlockingLoadBalancerClient构造函数依赖LoadBalancerClientFactory。LoadBalancerClientFactory是一个用于创建ReactiveLoadBalancer的工厂类,LoadBalancerClientFactory内部维护着一个Map,该Map用于保存各个服务的ApplicationContext(Map的key是服务名)。每个ApplicationContext内部维护对应服务的一些配置和Bean。 -
没有
ServiceInstance参数的execute方法内部会调用choose方法获取ServiceInstance,然后调用另外一个重载的execute方法。 -
有
ServiceInstance参数的execute方法把负载均衡操作直接委托给LoadBalancerRequest负载均衡请求处理。 -
根据URI和找到的服务实例
ServiceInstance重新构造一个URI,这个过程被封装在LoadBalancerUriTools工具类里。 -
代码2处提到的choose方法会返回服务实例
ServiceInstance。choose方法首先会根据服务名和loadBalancerClientFactory得到的该服务名对应的ReactiveLoadBalancerBean,然后调用ReactiveLoadBalancer的choose方法得到服务实例ServiceInstance。
总结
-
@LoadBalanced注解修饰RestTemplate后,会根据RestTemplateCustomizer给RestTemplate做定制化操作。这个定制化操作一定含有一个添加LoadBalancerInterceptor负载均衡拦截器的操作。此外,我们还可以扩展添加符合业务需求的自定义定制化操作。 -
LoadBalancerInterceptor负载均衡拦截器拦截的背后会通过LoadBalancerClient的execute方法完成最终的调用。
Spring Cloud LoadBalancer还提供了@LoadBalancerClient注解用于进行自定义的配置操作。
@LoadBalancerClient注解有3个属性,分别是value:String、name:String和configuration:Class[],name和value属性表示同一个含义,即服务名,且只能设置一个属性。
@LoadBalancerClients注解的defaultConfiguration属性表示默认的配置类,所有的BlockingLoadBalancerClient都会使用这些配置类里的配置。
Netflix Ribbon负载均衡
Netflix Ribbon是Netflix开源的客户端负载均衡组件,在Spring Cloud LoadBalancer出现之前,它是Spring Cloud 生态里唯一的负载均衡组件。
RibbonLoadBalancerClient
Netflix Ribbon对应的LoadBalancerClient实现类为RibbonLoadBalancerClient:
public class RibbonLoadBalancerClient implements LoadBalancerClient {
private SpringClientFactory clientFactory; // 1
public RibbonLoadBalancerClient(SpringClientFactory clientFactory) {
this.clientFactory = clientFactory; // 2
}
@Override
public URI reconstructURI(ServiceInstance instance, URI original) {
Assert.notNull(instance, "instance can not be null");
String serviceId = instance.getServiceId();
RibbonLoadBalancerContext context = this.clientFactory
.getLoadBalancerContext(serviceId);
URI uri;
Server server; // 3
if (instance instanceof RibbonServer) {
RibbonServer ribbonServer = (RibbonServer) instance; // 4
server = ribbonServer.getServer();
uri = updateToSecureConnectionIfNeeded(original, ribbonServer);
}
else {
server = new Server(instance.getScheme(), instance.getHost(),
instance.getPort()); // 5
IClientConfig clientConfig = clientFactory.getClientConfig(serviceId);
// 6
ServerIntrospector serverIntrospector = serverIntrospector(serviceId);
uri = updateToSecureConnectionIfNeeded(original, clientConfig,
serverIntrospector, server);
}
return context.reconstructURIWithServer(server, uri); // 7
}
@Override
public ServiceInstance choose(String serviceId) {
return choose(serviceId, null);
}
public ServiceInstance choose(String serviceId, Object hint) {
Server server = getServer(getLoadBalancer(serviceId), hint); // 8
if (server == null) {
return null;
}
return new RibbonServer(serviceId, server, isSecure(server, serviceId),
serverIntrospector(serviceId).getMetadata(server)); // 9
}
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request)
throws IOException {
return execute(serviceId, request, null);
}
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
throws IOException {
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer, hint); // 10
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server,
isSecure(server, serviceId),
serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance,
LoadBalancerRequest<T> request) throws IOException {
Server server = null;
if (serviceInstance instanceof RibbonServer) {
server = ((RibbonServer) serviceInstance).getServer();
}
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonLoadBalancerContext context = this.clientFactory
.getLoadBalancerContext(serviceId);
// 11
RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);
try {
T returnVal = request.apply(serviceInstance); // 12
statsRecorder.recordStats(returnVal); // 13
return returnVal;
}
catch (IOException ex) {
statsRecorder.recordStats(ex);
throw ex;
}
catch (Exception ex) {
statsRecorder.recordStats(ex);
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
...
}
-
SpringClientFactory是一个与LoadBalancerClientFactory作用类似的工厂类,其内部维护着一个Map,这个Map用于保存各个服务的ApplicationContext(Map的key表示服务名)。每个ApplicationContext内部维护对应服务的一些配置和Bean。 -
SpringClientFactory在RibbonAutoConfiguration自动化配置类中被构造,可以通过构造器注入的方式注入。 -
reconstructURI方法与Spring Cloud LoadBalancer中的BlockingLoadBalancerClient实现完全不一样。-
BlockingLoadBalancerClient直接委托给LoadBalancerUriTools#reconstructURI方法实现,其内部使用ServiceInstance进行相应的属性替换; -
而
RibbonLoadBalancerClient内部基于新的类com.netflix.loadbalancer.Server(表示一个服务器实例,内部有host、port、schema、zone等属性)来实现。
-
-
RibbonServer是RibbonLoadBalancerClient的内部类,其实现了ServiceInstance接口,内部维护着一个Server属性,同时还有其他3个属性:serviceId:String(服务名)、secure:boolean(是否使用HTTPS)、metadata:Map<String,String>(服务器实例等元数据)。 -
基于
ServiceInstance构造Server。 -
ServerIntrospector接口可以根据Server调用isSecure和getMetadata方法获取secure和metadata信息。 -
使用
RibbonLoadBalancerContext#reconstructURIWithServer方法基于Server和老的URI重新构造新的URI。 -
choose方法返回负载均衡策略得到的最终实例,将负载均衡的操作委托给
ILoadBalancer接口的实现类,默认的实现是ZoneAwareLoadBalancer。 -
返回的服务实例
ServiceInstance使用RibbonServer,secure和metadata使用ServerIntrospector获取。 -
调用过程与choose方法选择服务实例的步骤一致
-
每次服务调用会使用
RibbonStatsRecorder内部的ServerStats对象进行数据统计。每个实例都有独立的ServerStats对象。 -
真正的服务调用操作,使用
LoadBalancerRequest完成。 -
服务调用成功,进行状态记录。
SCL和Netflix Ribbon对应功能对比
| 功能/组件 | Spring Cloud LoadBalancer | Netflix Ribbon |
|---|---|---|
| 负载均衡 | ReactiveLoadBalancer & ServerInstanceListSupplier | ILoadBalancer & IRule |
| 服务实例 | ServiceInstance | ServiceInstance & RibbonServer & Server & ServerIntrospector |
| 服务调用统计信息 | - | ServerStats & LoadBalancerStats |
RobbonServer和Server
Server表示一个服务器实例:
public class Server {
...
public static final String UNKNOWN_ZONE = "UNKNOWN";
private String host; // 域名
private int port = 80; // 端口,默认80
private String scheme; // schema,如http或https
private volatile String id; // 服务器实例ID
private volatile boolean isAliveFlag; // 是否还存活,如ping不通,则可能会false
private String zone = UNKNOWN_ZONE; // 服务器所在的zone
private volatile boolean readyToServe = true; // 是否可以对我提供服务
// meta信息,云厂商可以有不同的操作
private MetaInfo simpleMetaInfo = new MetaInfo() {
@Override
public String getAppName() {
return null;
}
@Override
public String getServerGroup() {
return null;
}
@Override
public String getServiceIdForDiscovery() {
return null;
}
@Override
public String getInstanceId() {
return id;
}
};
...
}
RibbonServer是RibbonLoadBalancerClient的内部类:
// RibbonLoadBalancerClient.java
public static class RibbonServer implements ServiceInstance {
private final String serviceId;
private final Server server;
private final boolean secure;
private Map<String, String> metadata;
public RibbonServer(String serviceId, Server server) {
this(serviceId, server, false, Collections.emptyMap());
}
public RibbonServer(String serviceId, Server server, boolean secure,
Map<String, String> metadata) {
this.serviceId = serviceId;
this.server = server;
this.secure = secure;
this.metadata = metadata;
}
@Override
public String getInstanceId() {
return this.server.getId();
}
@Override
public String getServiceId() {
return this.serviceId;
}
@Override
public String getHost() {
return this.server.getHost();
}
@Override
public int getPort() {
return this.server.getPort();
}
@Override
public boolean isSecure() {
return this.secure;
}
@Override
public URI getUri() {
return DefaultServiceInstance.getUri(this);
}
@Override
public Map<String, String> getMetadata() {
return this.metadata;
}
public Server getServer() {
return this.server;
}
@Override
public String getScheme() {
return this.server.getScheme();
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("RibbonServer{");
sb.append("serviceId='").append(serviceId).append('\'');
sb.append(", server=").append(server);
sb.append(", secure=").append(secure);
sb.append(", metadata=").append(metadata);
sb.append('}');
return sb.toString();
}
}
ServerIntrospector
ServerIntrospector接口基于Server对象确定服务实例是否是HTTPS协议,以及获取服务实例中的元数据信息:
public interface ServerIntrospector {
// 是否使用HTTPS协议
boolean isSecure(Server server);
// 获取元数据信息
Map<String, String> getMetadata(Server server);
}
各个注册中心都有具体的实现类:
-
Nacos:
NacosServerIntrospector; -
Eureka:
EurekaServerIntrospector; -
Consul:
ConsulServerIntrospector; -
Zookeeper:
ZookeeperServerIntrospector。
NacosServerIntrospector的实现:
public class NacosServerIntrospector extends DefaultServerIntrospector {
@Override
public Map<String, String> getMetadata(Server server) {
if (server instanceof NacosServer) {
return ((NacosServer) server).getMetadata();
}
return super.getMetadata(server);
}
@Override
public boolean isSecure(Server server) {
if (server instanceof NacosServer) {
return Boolean.valueOf(((NacosServer) server).getMetadata().get("secure"));
}
return super.isSecure(server);
}
}
ILoadBalancer
ILoadBalancer接口是Netflix Ribbon用于实现负载均衡的核心接口,其内部有一套完善的机制用于实现负载均衡:
-
维护所有的Server列表,可以添加、删除Server或更新Server的状态。
-
监听机制。当Server列表(
ServerListChangeListener)或Server状态(ServerStatusChangeListener)发送变化的时候,会产生相应事件。 -
可自定义的负载均衡策略IRule。
-
可自定义的服务实例健康状态检查方式IPing(针对单个Server如何检查是否健康)。
-
可自定义的服务实例健康状态检查策略IPingSteategy(针对单个Server如何检查)。
-
可自定义的Server列表过滤器(ServerListFilter),可以基于Server列表过滤出新的Server列表。
-
可自定义的Server列表获取方式(ServerList),用于获取注册中心服务对应实例。
-
可自定义的Server列表更新机制(ServerListUpdater),默认会使用一个调度线程池每30s从注册中心获取一次实例信息。
-
负载均衡器中各个服务实例当前的统计信息(LoadBalancerStats)。
Dubbo LoadBalancer负载均衡
Apache Dubbo是一款高性能Java RPC框架,其内部也拥有负载均衡功能。
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
@Adaptive("loadbalance")
<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}
LoadBalance接口只有一个select方法,会从一堆Invoker列表中根据负载均衡算法得到唯一的Invoker。
Dubbo Router接口定义:
public interface Router extends Comparable<Router> {
...
<T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
...
}
Spring Cloud与Apache Dubbo在路由和负载均衡侧的功能对比:
| 功能/框架 | Spring Cloud | Apache Dubbo |
|---|---|---|
| 负载均衡 | ReactiveLoadBalancer(SCL)或IRule(Ribbon) | LoadBalance |
| 路由 | ServiceInstanceListSupplier(SCL)或ILoadBalancer(Ribbon) | Router |
| 容错机制 | ServerStats(Ribbon)&ILoadBalancer | Cluster |
| 服务实例刷新机制 | DiscoveryClient(SCL)或ServerListUpdater(Ribbon) | NotifyListener |
| 健康检查 | Iping(Ribbon) | 不处理(注册中心实现),可以依靠fault tolerant机制过滤不健康的实例 |
服务调用
OpenFeign:声明式Rest客户端
RestTemplate的构造可以通过@LoadBalanced注解,使其拥有基于服务名进行服务调用的能力。 如果一个复杂的系统设计上百个服务名,如果使用RestTemplate发起服务,服务信息维护会非常麻烦。OpenFeign是一种声明式接口方式的Rest客户端,只需定义接口和对应的方式及注解,底层就会生成动态代理,发起Rest请求并获得最终结果。
概述
若要使用OpenFeign,需要在pom文件里加入对应的依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
引入依赖之后,启动类需要添加@EnableFeignClients注解。
@SpringBootApplication
@EnableDiscoveryClient(autoRegister = false)
@EnableFeignClients
public class NacosConsumer {
...
}
然后定义接口并使用@FeignClient注解修饰。@FeignClient注解需要指定name属性指明调用哪个服务,接口的方法声明映射服务对外暴露的方法:
@FeignClient(name = "nacos-provider")
public interface EchoService {
@GetMapping("/")
String echo();
}
最后通过@Autowired注解直接注入定义的接口,发起服务调用即可。
@EnableFeignClients注解的作用是扫描被@FeignClient注解修饰的类。比如basePackages:String[]、basePackageClasses:Class[]和clients:Class[],这些属性的目的只有一个,就是找出需要扫描的包路径,然后根据包名在扫描的类里找出被@FeignClient注解修饰的类。
@EnableFeignClients注解对外还暴露了一个defaultConfiguration:Class<?>[]属性,这个属性表示默认配置类。
@FeignClient注解对外也暴露一个configuration:Class<?>[]属性。这套加载机制跟@LoadBalancerClient和@LoadBalancerClients注解,以及@RibbonClient和@RibbonClients注解的作用是一样的,解决的都是配置类的优先级问题。
对JAX-RS的支持
@FeignClient注解接口内部的方法都是被SpringMVC相关参数所修饰的,OpenFeign接口声明的定义还支持JAX-RS注解,若要使用JAX-RS注解修饰OpenFeign接口,需要在pom中加上依赖:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jaxrs</artifactId>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
</dependency>
@FeignClient修饰的接口中的方法使用JAX-RS注解:
@FeignClient(name = "nacos-provider", configuration = MyLoadBalancerConfiguration.class, contextId = "jaxrs")
public interface EchoServiceJAXRS {
@GET
@Path("/")
String echo();
}
这里MyLoadBalancerConfiguration配置类内部的Contract实现类为JAXRSContract:
@Bean
public Contract myFeignContract(){
return new JAXRSContract();
}
OpenFeign底层执行原理
OpenFeign声明的接口不论是SpringMVC还是JAX-RS,其底层都会把接口解析成方法元数据(MethodMetadata),再通过动态代理生成接口的代理,并基于MethodMetadata进行Rest调用。
@EnableFeignClients注解提供的包名与@FeignClient注解修饰的接口找到所有的接口,并给予这些接口构造FeignClientFactoryBean这个FactoryBean。
FactoryBean内部真正构造的对象是一个Proxy,这个Proxy是通过Targeter#target构造出来的,Targeter内部构造通过Feign.Builder#build方法完成,build方法返回的是一个Feign对象。默认情况下返回的是ReflectiveFeign这个Feign对象的子类:
public class ReflectiveFeign extends Feign {
...
@SuppressWarnings("unchecked")
@Override
public <T> T newInstance(Target<T> target) {
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
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;
}
...
}
从这段代码可以看到,InvocationHandler、Proxy这些JDK内置的动态代理类完成了这个操作。
Dubbo Spring Cloud:服务调用的新选择
Dubbo Spring Cloud是Spring Cloud Alibaba项目内部提供的一个可以使用Spring Cloud客户端RestTemplate或OpenFeign调用Dubbo服务的模块。
Apache Dubbo和Spring Cloud是两套架构完全不同的开发框架。
-
Apache Dubbo暴露的服务都是接口级别的,
-
Spring Cloud暴露的服务是应用级别的。
RestTemplate或OpenFeign发起调用服务都会有对应的URL Path、Query Parameter、Header等内容(这是HTTP协议调用),如何让这些内容关联Dubbo服务呢?
针对上述问题Dubbo Spring Cloud实现了以应用为粒度的注册机制,每个Dubbo应用注册到注册中心后有且仅有一个服务。
Dubbo Spring Cloud定义了DubboMetadataService元数据服务的概念。这是一个专门存储Dubbo服务的元数据接口。DubboMetadataService接口定义如下:
public interface DubboMetadataService {
String VERSION = "1.0.0";
String getServiceRestMetadata();
Set<String> getAllServiceKeys();
Map<String, String> getAllExportedURLs();
String getExportedURLs(String serviceInterface, String group, String version);
}
核心方法getServiceRestMetadata获取Dubbo服务的Rest元数据是指:当一个Dubbo服务同时也被SpringMVC相关注解修饰时,SpringMVC相关注解修饰的内容就是这些Rest元数据,这些Rest元数据由RestMethodMetadata类修饰。当Dubbo服务自身也暴露Rest协议的时候,这些JAX-RS相关注解修饰的内容也会被解析成Rest元数据。
调用流程
使用RestTemplate或OpenFeign调用Dubbo服务会经历一下过程:
-
根据服务名得到注册中心的Dubbo服务
DubboMetadataService。 -
使用
DubboMetadataService里提供的getServiceRestMetadata方法获取要使用的Dubbo服务和对应的Rest元数据。 -
基于Dubbo服务和Rest元数据构造
GenericService。 -
服务调用过程中使用
GenericService发起泛化调用。
开发步骤
-
引入
spring-cloud-starter-dubbo依赖。<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-dubbo</artifactId> </dependency> -
Provider端接口加上SpringMVC相关注解或使用JAX-RS暴露Rest协议。
-
加上SpringMVC相关注解
@Service(version = "1.0.0") @RestController public class OrderServiceImpl implements OrderService { private static List<Order> orderList = new ArrayList<>(); static { orderList.add(Order.generate("jim")); orderList.add(Order.generate("jim")); orderList.add(Order.generate("test")); } @GetMapping("/allOrders") @Override public List<Order> getAllOrders(@RequestParam("userId") final String userId) { return orderList.stream().filter( order -> order.getUserId().equals(userId) ).collect(Collectors.toList()); } @GetMapping("/findOrder") @Override public Order findOrder(@RequestParam("orderId") String orderId) { return orderList.stream().filter( order -> order.getId().equals(orderId) ).findFirst().orElseGet(Order::error); } } -
使用JAX-RS暴露Rest协议
配置文件暴露rest协议
dubbo.protocols.rest.name=rest dubbo.protocols.rest.port=9090 dubbo.protocols.rest.server=netty接口使用JAX-RS注解修饰:
@Service(version = "1.0.0", protocol = {"dubbo", "rest"}) @Path("/') public class OrderServiceImpl implements OrderService { private static List<Order> orderList = new ArrayList<>(); static { orderList.add(Order.generate("jim")); orderList.add(Order.generate("jim")); orderList.add(Order.generate("test")); } @Path("/allOrders") @GET @Override public List<Order> getAllOrders(@RequestParam("userId") final String userId) { return orderList.stream().filter( order -> order.getUserId().equals(userId) ).collect(Collectors.toList()); } @Path("/findOrder") @GET @Override public Order findOrder(@RequestParam("orderId") String orderId) { return orderList.stream().filter( order -> order.getId().equals(orderId) ).findFirst().orElseGet(Order::error); } } -
Consumer客户端加上
@DubboTransported注解。RestTemplate和OpenFeign客户端都支持
@DubboTransported注解。@Bean @LoadBalanced @DubboTransported public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); return restTemplate; }@FeignClient("sc-dubbo-provider") @DubboTransported(protocol = "dubbo") public interface DubboFeignOrderService { @GetMapping("/allOrders") List<Order> getAllOrders(@RequestParam("userId") final String userId); @GetMapping("/findOrder") Order findOrder(@RequestParam("orderId") String orderId); } -
使用RestTemplate和OpenFeign调用Dubbo服务。