前言
在微服务架构中,项目服务化的拆分,必然存在多个服务之间的相互调用。而单个服务在考虑到高可用时,又需要对服务进行集群化部署,再通过负载均衡策略选择其中一个服务进行调用,以此来增加系统的可用性和扩展性。因此在服务之间相互调用时,如何保证负载均衡是个不得不去考虑的问题。Spring Cloud Ribbon作为Spring Cloud体系中的核心组件之一,提供了服务间调用及API网关转发提供负载均衡的功能,解决了服务之间调用存在的问题。下文将结合源码深入分析Ribbon这个组件的调用过程,让读者对Ribbon组件有更深的了解。
入口
在Spring Cloud体系架构下进行服务调用时,通常会通过RestTemplate发起服务调用,并在注入RestTemplate实例时指定@LoadBalanced注解,标识开启Ribbon负载均衡。以此作为入口,进行分析
@Autowired
private RestTemplate restTemplate;
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
@RequestMapping("/list")
public String getServerBInfo(){
return restTemplate.getForObject("http://order-service/server/list", String.class);
}
首先从getForObject方法进行源码跟踪进行分析,从调用链可以看到会执行到RibbonLoadBalancerClient类的execute方法
class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest
...
protected final ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
InterceptingRequestExecution requestExecution = new InterceptingRequestExecution();
//核心方法
return requestExecution.execute(this, bufferedOutput);
}
}
private class InterceptingRequestExecution implements ClientHttpRequestExecution
...
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
if (this.iterator.hasNext()) {
ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
return nextInterceptor.intercept(request, body, this);
}
...
return delegate.execute();
}
}
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
...
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null,
"Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName,
this.requestFactory.createRequest(request, body, execution));
}
}
public class RibbonLoadBalancerClient implements LoadBalancerClient
...
public Object execute(String serviceId, LoadBalancerRequest request, Object hint)
throws IOException
{
//核心,获取ILoadBalancer实现
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
//这里会根据负载均衡策略选择到一个服务
Server server = getServer(loadBalancer, hint);
if(server == null)
{
throw new IllegalStateException((new StringBuilder()).append("No instances available for ").append(serviceId).toString());
} else
{
RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server));
//服务调用实现
return execute(serviceId, ((ServiceInstance) (ribbonServer)), request);
}
}
}
RibbonLoadBalancerClient的execute方法是服务调用过程中的核心方法,主要负责:
(1)首先获取ILoadBalancer的实现,在它的实现类中会记录调用的目标服务列表(这里就是order-service的服务列表)
(2)然后根据ILoadBalancer实现类中记录的服务列表,通过Ribbon提供的负载均衡策略选择一个服务
(3)最后对目标服务发起HTTP请求进行调用,获取调用结果
接下来,基于这几个环节进行深入分析
创建上下文对象
首先查看调用getLoadBalancer方法调用链,可以看到它会执行到getInstance方法,而该方法就是整个Ribbon调用过程最为关键的一处。它会先通过getContext方法尝试获取目标服务对应的上下文对象,然后再根据该上下文对象找到ILoadBalancer类型的Bean并返回。
其中getContext方法在获取上下文对象时,会先根据contexts这个缓存通过调用的服务名来找到与之对应的上下文对象,找不到就会创建一个上下文对象,再设置到contexts缓存中。因此服务调用过程中,第一次调用都会更慢一些。
public abstract class NamedContextFactory
implements DisposableBean, ApplicationContextAware
//记录服务和上下文的映射关系
private Map<String,AnnotationConfigApplicationContext> contexts;
...
public Object getInstance(String name, Class type)
{
//获得服务对应的上下文对象
AnnotationConfigApplicationContext context = getContext(name);
if(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0)
//从容器中,获取类型为ILoadBalancer的bean
return context.getBean(type);
else
return null;
}
//获得上下文对象
protected AnnotationConfigApplicationContext getContext(String name)
{
if(!contexts.containsKey(name))
synchronized(contexts)
{
if(!contexts.containsKey(name))
//存在服务的上下文对象,则创建一个
contexts.put(name, createContext(name));
}
//直接从缓存中获取
return (AnnotationConfigApplicationContext)contexts.get(name);
}
}
当contexts缓存中不存在上下文对象时,此时就会调用到createContext方法进行上下文对象的创建,该方法最关键的是它会向创建好的上下文对象中注入了两个核心配置类EurekaRibbonClientConfiguration和RibbonClientConfiguration,而这两个配置类,是实现Ribbon负载均衡的核心,也是不同服务注册中心整合Spring Cloud Ribbon必须关注的点
总结一点:分析到这里,可以看到服务调用时会为每一个服务创建一个上下文对象,而创建上下文对象的目的就是注入一些核心配置类,后续再通过这些核心配置类完成目标服务列表的获取
protected AnnotationConfigApplicationContext createContext(String name) {
//为调用的服务创建一个上下文对象
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name)
.getConfiguration()) {
context.register(configuration);
}
}
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
//查找以default.的
if (entry.getKey().startsWith("default.")) {
for (Class<?> configuration : entry.getValue().getConfiguration()) {
//向上下文对象注册一个配置类,这里的配置类是EurekaRibbonClientConfiguration
context.register(configuration);
}
}
}
//这里向容器中注入defaultConfigType类型为:RibbonClientConfiguration
context.register(PropertyPlaceholderAutoConfiguration.class,
this.defaultConfigType);
context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
this.propertySourceName,
Collections.<String, Object>singletonMap(this.propertyName, name)));
if (this.parent != null) {
// Uses Environment from parent as well as beans
context.setParent(this.parent);
// jdk11 issue
// https://github.com/spring-cloud/spring-cloud-netflix/issues/3101
context.setClassLoader(this.parent.getClassLoader());
}
context.setDisplayName(generateDisplayName(name));
//容器的启动
context.refresh();
return context;
}
configurations这个Map中会记录以**default.**开头的key,其对应的值就为EurekaRibbonClientConfiguration配置类
defaultConfigType是在NamedContextFactory的子类SpringClientFactory创建时构造函数中指定的,并设置到NamedContextFactory类的defaultConfigType中
当目标服务对应的上下文对象被创建完成后,接下来就是通过调用refresh方法,进行容器的初始化,并解析注册的这两个核心配置类,下面对两个核心配置类的作用进行分析:
EurekaRibbonClientConfiguration
在解析EurekaRibbonClientConfiguration类时,其最关键是会通过@Bean方式向目标服务对应的上下文对象中注入了ServerList类型的Bean,这里的实例为DiscoveryEnabledNIWSServerList
说明一点:ServerList可以理解为一个规范,不同注册中心实现服务调用功能都需要去实现该接口,并且在这个接口内部有两个非常关键的方法getUpdatedListOfServers和getInitialListOfServers,这两个方法定义了服务列表获取。Eureka作为注册中心,提供服务调用功能,它就实现了该接口,并自定义了一个DiscoveryEnabledNIWSServerList类
public class EurekaRibbonClientConfiguration
...
@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(IClientConfig config, Provider<EurekaClient> eurekaClientProvider)
{
if (propertiesFactory.isSet(ServerList.class, serviceId)) {
return (ServerList)propertiesFactory.get(ServerList.class, config, serviceId);
}
//核心类
DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(config, eurekaClientProvider);
DomainExtractingServerList serverList = new DomainExtractingServerList(discoveryServerList, config, approximateZoneFromHostname);
return serverList;
}
}
public abstract interface ServerList<T extends Server>
{
public abstract List<T> getInitialListOfServers();
public abstract List<T> getUpdatedListOfServers();
}
DiscoveryEnabledNIWSServerList实现了ServerList接口提供的核心方法,内部都会调用obtainServersViaDiscovery方法,该方法就是完成了目标服务列表的获取。而获取的来源是在Eureka客户端定时进行服务拉取时,会将拉取到的服务列表信息维护在一个localRegionApps实例中,而获取服务列表就是从该实例中获取到的
服务拉取过程不懂的话,可以看之前写一篇文章<<Spring Cloud组件之深入分析Eureka服务注册与发现核心源码>>
public class DiscoveryEnabledNIWSServerList extends AbstractServerList<DiscoveryEnabledServer>{
...
public List<DiscoveryEnabledServer> getInitialListOfServers()
{
return obtainServersViaDiscovery();
}
public List<DiscoveryEnabledServer> getUpdatedListOfServers()
{
return obtainServersViaDiscovery();
}
}
private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
List<DiscoveryEnabledServer> serverList = new ArrayList();
if ((eurekaClientProvider == null) || (eurekaClientProvider.get() == null)) {
logger.warn("EurekaClient has not been initialized yet, returning an empty list");
return new ArrayList();
}
EurekaClient eurekaClient = (EurekaClient)eurekaClientProvider.get();
if (vipAddresses != null) {
for (String vipAddress : vipAddresses.split(","))
{
//核心方法,获取服务实例列表
List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
for (InstanceInfo ii : listOfInstanceInfo) {
if (ii.getStatus().equals(InstanceInfo.InstanceStatus.UP))
{
...
DiscoveryEnabledServer des = createServer(ii, isSecure, shouldUseIpAddr);
serverList.add(des);
}
}
if ((serverList.size() > 0) && (prioritizeVipAddressBasedServers)) {
break;
}
}
}
return serverList;
}
public List<InstanceInfo> getInstancesByVipAddress(String vipAddress, boolean secure) {
//获取实例列表
return getInstancesByVipAddress(vipAddress, secure, this.instanceRegionChecker.getLocalRegion());
}
public List<InstanceInfo> getInstancesByVipAddress(String vipAddress, boolean secure, @Nullable String region) {
Applications applications;
if (vipAddress == null)
throw new IllegalArgumentException("Supplied VIP Address cannot be null");
if (this.instanceRegionChecker.isLocalRegion(region)) {
//从localRegionApps变量中获取服务列表,localRegionApps我前面服务拉取时,会将拉取的服务列表记录在将变量中
applications = this.localRegionApps.get();
} else {
applications = this.remoteRegionVsApps.get(region);
if (null == applications) {
logger.debug("No applications are defined for region {}, so returning an empty instance list for vip address {}.", region, vipAddress);
return Collections.emptyList();
}
}
if (!secure)
return applications.getInstancesByVirtualHostName(vipAddress);
//获取服务列表
return applications.getInstancesBySecureVirtualHostName(vipAddress);
}
RibbonClientConfiguration
解析RibbonClientConfiguration类,它的核心作用也是通过@Bean的方法向Spring容器中注入了一个类型为ILoadBalancer的Bean,具体的实现为ZoneAwareLoadBalancer。前面分析DiscoveryEnabledNIWSServerList时我们知道它提供了获取服务列表的方法,那方法的总得有地方调用吧。实际上在ZoneAwareLoadBalancer的构造方法内部就会完成了getUpdatedListOfServers方法的调用,并将获取到的服务列表信息维护在allServerList和upServerList集合中
public class RibbonClientConfiguration {
...
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
return this.propertiesFactory.get(ILoadBalancer.class, config, name);
}
//创建负载均衡器ILoadBalancer 的实现
return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
serverListFilter, serverListUpdater);
}
}
public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping, ServerList serverList, ServerListFilter filter, ServerListUpdater serverListUpdater)
{
super(clientConfig, rule, ping);
isSecure = false;
useTunnel = false;
serverListUpdateInProgress = new AtomicBoolean(false);
updateAction = new _cls1();
serverListImpl = serverList;
this.filter = filter;
this.serverListUpdater = serverListUpdater;
if(filter instanceof AbstractServerListFilter)
((AbstractServerListFilter)filter).setLoadBalancerStats(getLoadBalancerStats());
//核心方法
restOfInit(clientConfig);
}
void restOfInit(IClientConfig clientConfig)
{
...
//更新服务列表
updateListOfServers();
....
}
public void updateListOfServers()
{
List servers = new ArrayList();
if(serverListImpl != null)
{
//这里就会调用到DiscoveryEnabledNIWSServerList类的getUpdatedListOfServers方法,来获取服务列表
servers = serverListImpl.getUpdatedListOfServers();
LOGGER.debug("List of Servers for {} obtained from Discovery client: {}", getIdentifier(), servers);
if(filter != null)
{
servers = filter.getFilteredListOfServers(servers);
LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}", getIdentifier(), servers);
}
}
//更新记录到allServerList和upServerList集合中
updateAllServerList(servers);
}
获取的ILoadBalancer信息如下:
负载均衡
完成目标服务列表获取后,接下来,就会根据Ribbon的负载均衡策略,从ZoneAwareLoadBalancer实例中维护的服务列表allServerList或者upServerList中获取一个服务实例。具体的负载均衡策略的实现,就不分析。
public class RibbonLoadBalancerClient implements LoadBalancerClient
...
public Object execute(String serviceId, LoadBalancerRequest request, Object hint)
throws IOException
{
//这里获取到的就是ZoneAwareLoadBalancer实例
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
//这里会根据负载均衡策略选择到一个服务
Server server = getServer(loadBalancer, hint);
...
}
}
public Server chooseServer(Object key)
{
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
}
try {
//根据负载均衡策略选择一个服务
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", new Object[] { name, key, e }); }
return null;
}
Ribbon提供的负载均衡如下:
- 线性轮训 RoundRobinRule
- 可以重试的轮训 RetryRule
- 根据运行响应时间来计算权重 WeightedResponseTimeRule
- 过滤掉故障实例,选择请求数最小的实例 BestAvailableRule
- 随机 RandomRule
经过负载均衡后获取到的Server服务信息如下:
服务调用
通过负载均衡找到目标Server后,最后就会发起对目标服务的HTTP请求调用,获取服务调用结果
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);
RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);
try {
//服务调用
T returnVal = request.apply(serviceInstance);
statsRecorder.recordStats(returnVal);
return returnVal;
}
// catch IOException and rethrow so RestTemplate behaves correctly
catch (IOException ex) {
statsRecorder.recordStats(ex);
throw ex;
}
catch (Exception ex) {
statsRecorder.recordStats(ex);
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
总结
这里只是分析了Ribbon在Eureka注册中心下的实现,在Nacos注册中心下Ribbon又有不同的实现的,后续分析Nacos时会对Ribbon在Nacos下的实现进行分析,想了解的,可以点下关注,会持续更新哦
关于Nacos服务调用的实现已更新《Spring Cloud Alibaba组件之深入分析Nacos服务调用源码实现》
由于本人能力有限,分析不恰当的地方和文章有错误的地方的欢迎批评指出,非常感谢!