二、SpringCloud--Eureka

x33g5p2x  于2021-12-30 转载在 Spring  
字(30.6k)|赞(0)|评价(0)|浏览(213)

1.服务注册Eureka基础

1.1微服务的注册中心

注册中心可以说是微服务架构中的”通讯录“,它记录了服务和服务地址的映射关系。在分布式架构中,服务会注册到这里,当服务需要调用其它服务时,就这里找到服务的地址,进行调用。

1.1.1注册中心的主要作用

服务注册中心(下称注册中心)是微服务架构非常重要的一个组件,在微服务架构里主要起到了协调者的一个作用。注册中心一般包含如下几个功能:

  1. 服务发现:
  • 服务注册/反注册:保存服务提供者和服务调用者的信息
  • 服务订阅/取消订阅:服务调用者订阅服务提供者的信息,最好有实时推送的功能
  • 服务路由(可选):具有筛选整合服务提供者的能力。
  1. 服务配置:
  • 配置订阅:服务提供者和服务调用者订阅微服务相关的配置;
  • 配置下发:主动将配置推送给服务提供者和服务调用者。
  1. 服务健康检测
  • 检测服务提供者的健康情况。

1.1.2常见的注册中心

Zookeeper:它是一个分布式服务框架,是Apache Hadoop的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。简单来说zookeeper=文件系统+监听通知机制。

Eureka:Eureka是在Java语言上,基于Restful Api开发的服务注册与发现组件,Springcloud Netflflix中的重要组件。

Consul:Consul是由HashiCorp基于Go语言开发的支持多数据中心分布式高可用的服务发布和注册服务软件,采用Raft算法保证服务的一致性,且支持健康检查。

Nacos:Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。简单来说Nacos就是注册中心 +配置中心的组合,提供简单易用的特性集,帮助我们解决微服务开发必会涉及到的服务注册 与发现,服务配置,服务管理等问题。Nacos还是Spring Cloud Alibaba组件之一,负责服务注册与****发现

最后我们通过一张表格大致了解Eureka、Consul、Zookeeper的异同点。选择什么类型的服务注册与发现组件可以根据自身项目要求决定。

| <br>组件名<br> | <br>语言<br> | <br>CAP<br> | <br>一致性算法<br> | <br>服务健康检查<br> | <br>对外暴露接口<br> |
| <br>Eureka<br> | <br>Java<br> | <br>AP<br> | <br>无<br> | <br>可配対寺<br> | <br>HTTP<br> |
| <br>Consul<br> | <br>Go<br> | <br>CP<br> | <br>Raft<br> | <br>気寺<br> | <br>HTTP/DNS<br> |
| <br>Zookeeper<br> | <br>Java<br> | <br>CP<br> | <br>Paxos<br> | <br>気寺<br> | <br>客户端<br> |
| <br>Nacos<br> | <br>Java<br> | <br>AP<br> | <br>Raft<br> | <br>気寺<br> | <br>HTTP<br> |

1.2Eureka的概述

1.2.1Eureka的基础知识

Eureka是Netflflix开发的服务发现框架,SpringCloud将它集成在自己的子项目spring-cloud-netflflix中,实现SpringCloud的服务发现功能。

上图简要描述了Eureka的基本架构,由3个角色组成:
        1、Eureka Server

  • 提供服务注册和发现。
            2、Service Provider

  • 服务提供方;

  • 将自身服务注册到Eureka,从而使服务消费方能够找到。
            3、Service Consumer

  • 服务消费方;

  • 从Eureka获取注册服务列表,从而能够消费服务。

1.2.2Eureka的交互流程与原理

图是来自Eureka官方的架构图,大致描述了Eureka集群的工作过程。图中包含的组件非常多,可能比 较难以理解,我们用通俗易懂的语言解释一下:

  • Application Service相当于本书中的服务提供者,Application Client相当于服务消费者;
  • Make Remote Call,可以简单理解为调用RESTful API;
  • us-east-1c、us-east-1d等都是zone,它们都属于us-east-1这个region;

由图可知,Eureka包含两个组件:Eureka ServerEureka Client,它们的作用如下:

  • Eureka Client是一个Java客户端,用于简化与Eureka Server的交互;
  • Eureka Server提供服务发现的能力,各个微服务启动时,会通过Eureka Client向Eureka Server进行注册自己的信息(例如网络信息),Eureka Server会存储该服务的信息;
  • 微服务启动后,会周期性地向Eureka Server发送心跳(默认周期为30秒)以续约自己的信息。如果Eureka Server在一定时间内没有接收到某个微服务节点的心跳,Eureka Server将会注销该微服务节点(默认90秒);
  • 每个Eureka Server同时也是Eureka Client,多个Eureka Server之间通过复制的方式完成服务注册表的同步;
  • Eureka Client会缓存Eureka Server中的信息。即使所有的Eureka Server节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者。
            综上,Eureka通过心跳检测、健康检查和客户端缓存等机制,提高了系统的灵活性、可伸缩性和可用性。

1.3搭建Eureka注册中心

1.3.1搭建Eureka服务中心

(1)创建shop_eureka_server****子模块

在shop_parent下创建子模块shop。
(2)引入maven****坐标

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

(3)配置application.yml

server:
 port: 8761
eureka:
 instance:
   hostname: localhost
 client:
   registerWithEureka: false  
   fetchRegistry: false
   serviceUrl:
     defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

        registerWithEureka: 是否将自己注册到Eureka服务中,本身就是所有无需注册;
        fetchRegistry : 是否从Eureka中获取注册信息;

        serviceUrlEureka: 客户端与Eureka服务端进行交互的地址。

**(4)**配置启动类
在cn.itcast.eureka下创建启动类EurekaServerApplication

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
 public static void main(String[] args) {
 SpringApplication.run(EurekaServerApplication.class, args);
 }
}

1.3.2服务注册中心管理后台

打开浏览器访问http://localhost8761即可进入EurekaServer内置的管理控制台,显示效果如下。

1.4服务注册到Eureka

1.4.1商品服务注册

(1) 商品模块中引入坐标,在shop_service_product的pom文件中添加eureka。

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-commons</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>

(2)配置application.yml文件,在工程的application.yml中添加Eureka Server的主机地址。

eureka:
 client:
   serviceUrl: # eureka server的路径
     defaultZone: http://localhost:8761/eureka/
 instance:
   prefer-ip-address: true #使用ip注册

(3)修改启动类添加服务注册注解

@SpringBootApplication
//@EnableDiscoveryClient
//@EnableEurekaClient
public class UserApplication {
 public static void main(String[] args) {
 SpringApplication.run(UserApplication.class, args);
 }
}

从Spring Cloud Edgware版本开始,@EnableDiscoveryClient或@EnableEurekaClient

省略。只需加上相关依赖,并进行相应配置,即可将微服务注册到服务发现组件上。

1.4.2订单服务注册

和商品微服务一样,只需要引入坐标依赖,在工程的application.yml中添加Eureka Server的主机地址即可。

1.4.3用户服务注册

和商品微服务一样,只需要引入坐标依赖,在工程的application.yml中添加Eureka Server的主机地址即可。

1.5Eureka中的自我保护

微服务第一次注册成功之后,每30秒会发送一次心跳将服务的实例信息注册到注册中心。通知Eureka Server 该实例仍然存在。如果超过90秒没有发送更新,则服务器将从注册信息中将此服务移除。 Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,如果出现低于的情况 (在单机调试的时候很容易满足,实际在生产环境上通常是由于网络不稳定导致),Eureka Server会 将当前的实例注册信息保护起来,同时提示这个警告。保护模式主要用于一组客户端和Eureka Server 之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务) 验证完自我保护机制开启后,并不会马上呈现到web上,而是默认需等待5分钟(可以通过
eureka.server.wait-time-in-ms-when-sync-empty配置),即5分钟后你会看到下面的提示信息:

如果关闭自我保护:通过设置 eureka.enableSelfPreservation=false来关闭自我保护功能。

1.6Eureka中的元数据

Eureka的元数据有两种:标准元数据和自定义元数据。

标准元数据:主机名、IP地址、端口号、状态页和健康检查等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。
        自定义元数据:可以使用eureka.instance.metadata-map配置,符合KEY/VALUE的存储格式。这些元数据可以在远程客户端中访问。

        在程序中可以使用DiscoveryClient获取指定微服务的所有元数据信息。

@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class RestTemplateTest {
 
 @Autowired
 private DiscoveryClient discoveryClient;
 @Test
 public void test() {
 //根据微服务名称从注册中心获取相关的元数据信息
 List<ServiceInstance> instances = discoveryClient.getInstances("shopservice-product");
 for (ServiceInstance instance : instances) {
 System.out.println(instance);
 }
 }
}

2.服务注册Eureka高级

在上一个章节,实现了单节点的Eureka Server的服务注册与服务发现功能。Eureka Client会定时连接 Eureka Server,获取注册表中的信息并缓存到本地。微服务在消费远程API时总是使用本地缓存中的数 据。因此一般来说,即使Eureka Server发生宕机,也不会影响到服务之间的调用。但如果Eureka Server宕机时,某些微服务也出现了不可用的情况,Eureka Server中的缓存若不被刷新,就可能会影 响到微服务的调用,甚至影响到整个应用系统的高可用。因此,在生成环境中,通常会部署一个高可用的Eureka Server集群。

Eureka Server可以通过运行多个实例并相互注册的方式实现高可用部署,Eureka Server实例会彼此增量地同步信息,从而确保所有节点数据一致。事实上,节点之间相互注册是Eureka Server的默认行为。

 2**.1.1搭建Eureka Server****高可用集群**

1)修改本机host属性

**        **由于是在个人计算机中进行测试很难模拟多主机的情况,Eureka配置server集群时需要执行host地址。 所以需要修改个人电脑中host地址。

127.0.0.1 eureka1
127.0.0.1 eureka2

2)修改shop_eureka_server工程中的yml配置文件,添加如下配置属性

#指定应用名称
spring:
 application:
   name: shop-eureka-server
---
#执行peer1的配置信息
spring:
 profiles: eureka1
server:
 port: 8761
eureka:
 instance:
   hostname: eureka1
 client:
   service-url:
     defaultZone: http://eureka2:8762/eureka
---
#执行peer2的配置信息
spring:
 profiles: eureka2
server:
 port: 8762
eureka:
 instance:
   hostname: eureka2
 client:
   service-url:
     defaultZone: http://eureka1:8761/eureka

在配置文件中通过连字符(---)将文件分为三个部分,第一部分为应用名称,第二部分和第三部分是根据不同的profifiles选项动态添加,可以在IDEA启动时进行激活配置 。

使用IDEA启动历次EurekaServerApplicaion分别激活eureka1和eureka2配置。访问http://eureka1:8761和http://eureka1:8762/。会发现注册中心SHOP-EUREKA-SERVER已经有两个节点,并且registered-replicas(相邻集群复制节点)中已经包含对方。

2.1.2服务注册到EurekaServer****集群

如果需要将微服务注册到Eureka Server集群只需要修改yml配置文件即可

eureka:
 client:
   serviceUrl:
     defaultZone: http://eureka1:8761/eureka/,http://eureka1:8761/eureka/

以商品微服务为例,修改defaultZone配置添加多个Eureka Server的地址。

2.2 Eureka****中的常见问题

4.2.1****服务注册慢

**        **默认情况下,服务注册到Eureka Server的过程较慢。SpringCloud官方文档中给出了详细的原因 。

大致含义:服务的注册涉及到心跳,默认心跳间隔为30s。在实例、服务器、客户端都在本地缓存中具 有相同的元数据之前,服务不可用于客户端发现(所以可能需要3次心跳)。可以通过配置 eureka.instance.leaseRenewalIntervalInSeconds (心跳频率)加快客户端连接到其他服务的过 程。在生产中,最好坚持使用默认值,因为在服务器内部有一些计算,他们对续约做出假设。

2.2.2****服务节点剔除问题

**        **默认情况下,由于Eureka Server剔除失效服务间隔时间为90s且存在自我保护的机制。所以不能有效而迅速的剔除失效节点,这对开发或测试会造成困扰。解决方案如下:

**        Eureka Server**: 配置关闭自我保护,设置剔除无效节点的时间间隔。

instance:
   hostname: eureka1
 client:
   service-url:
     defaultZone: http://eureka2:8762/eureka
 server:
   enable-self-preservation: false  #关闭自我保护
   eviction-interval-timer-in-ms: 4000 #剔除时间间隔,单位:毫秒

Eureka Client: 置开启健康检查,并设置续约时间。

eureka:
 client:
   healthcheck: true #开启健康检查(依赖spring-boot-actuator)
   serviceUrl:
     defaultZone: http://eureka1:8761/eureka/,http://eureka1:8761/eureka/
 instance:
   preferIpAddress: true
   lease-expiration-duration-in-seconds: 10 #eureka client发送心跳给server端后,续
约到期时间(默认90秒)
   lease-renewal-interval-in-seconds: 5 #发送心跳续约间隔

2.2.3监控页面显示ip
**        **

        在Eureka Server的管控台中,显示的服务实例名称默认情况下是微服务定义的名称和端口。为了更好的对所有服务进行定位,微服务注册到Eureka Server的时候可以手动配置示例ID。配置方式如下。
        

eureka:
 instance:
   instance-id: ${spring.cloud.client.ip-address}:${server.port}
#spring.cloud.client.ip-address:获取ip地址

2.3 Eureka源码解析

2.3.1SpringBoot中的自动装载

ImportSelector

**        **ImportSelector接口是Spring导入外部配置的核心接口,在SpringBoot的自动化配置和@EnableXXX(功 能性注解)中起到了决定性的作用。当在@Confifiguration标注的Class上使用@Import引入了一个 ImportSelector实现类后,会把实现类中返回的Class名称都定义为bean。

public interface ImportSelector {
    String[] selectImports(AnnotationMetadata var1);
}

DeferredImportSelector接口继承ImportSelector,他和ImportSelector的区别在于装载bean的时机上,DeferredImportSelector需要等所有的@Confifiguration都执行完毕后才会进行装载。

public interface DeferredImportSelector extends ImportSelector {
 //...省略
}

接下来我们写一个小例子,看下ImportSelector的用法:
1**)定义Bean对象**

public class User {
 private String username;
 private Integer age;
 //省略..
}

2**)定义配置类****Confifiguration**

//定义一个configuration ,注意这里并没有使用spring注解,spring扫描的时候并不会装载该类
public class UserConfiguration {
 @Bean
 public User getUser() {
 return new User("张三",18);
 }
}

3 )定义ImportSelector

public class UserImportSelector implements ImportSelector {
 @Override
 public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        //获取配置类名称
 return new String[]{UserConfiguration.class.getName()};
 }
}

4**)定义EnableXXX****注解**

@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.TYPE)
@Import(UserImportSelector.class)
public @interface EnableUserBean {
}

**5 )**测试

/**
* 通过在类上声明@EnableUserBean,会自动的加载所有对象
*/
@EnableUserBean
public class TestApplication {
 public static void main(String[] args) {
 AnnotationConfigApplicationContext applicationContext = new
AnnotationConfigApplicationContext(TestApplication.class);
 User user = applicationContext.getBean(User.class);
 System.out.println(user);
 }
}

        由此可见,HelloWorldConfifiguration对象并没有使用Spring的对象对象创建注解声明 (@Controller,@Service,@Repostiroty),而是使用编程的方式动态的载入bean。
这个接口在哪里调用呢?我们可以来看一下ConfifigurationClassParser这个类的processImports方法

private void processImports(ConfigurationClass configClass, SourceClass
currentSourceClass,
            Collection<SourceClass> importCandidates, boolean
checkForCircularImports) {
        if (importCandidates.isEmpty()) {
            return;
       }
        if (checkForCircularImports && isChainedImportOnStack(configClass)) {
            this.problemReporter.error(new CircularImportProblem(configClass, 
this.importStack));
       }
 else {
            this.importStack.push(configClass);
            try {
                for (SourceClass candidate : importCandidates) 
{ //对ImportSelector的处理
                    if (candidate.isAssignable(ImportSelector.class)) {
                        // Candidate class is an ImportSelector -> delegate to 
it to determine imports
                        Class<?> candidateClass = candidate.loadClass();
                        ImportSelector selector =
BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
                        ParserStrategyUtils.invokeAwareMethods(
                                selector, this.environment, this.resourceLoader, 
this.registry);
                        if (this.deferredImportSelectors != null && selector
instanceof DeferredImportSelector) { //如果为延迟导入处理
则加入集合当中
                            this.deferredImportSelectors.add(
                                    new
DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector));
                       }
                        else { //根据ImportSelector方法
的返回值来进行递归操作
                            String[] importClassNames =
selector.selectImports(currentSourceClass.getMetadata());
                            Collection<SourceClass> importSourceClasses =
asSourceClasses(importClassNames);
                            processImports(configClass, currentSourceClass, 
importSourceClasses, false);
                       }
                   }
                    else if
(candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
                        // Candidate class is an ImportBeanDefinitionRegistrar -
>
                        // delegate to it to register additional bean 
definitions
                        Class<?> candidateClass = candidate.loadClass();
                        ImportBeanDefinitionRegistrar registrar =
                                BeanUtils.instantiateClass(candidateClass, 
ImportBeanDefinitionRegistrar.class);
                        ParserStrategyUtils.invokeAwareMethods(
                                registrar, this.environment, 
this.resourceLoader, this.registry);
                        configClass.addImportBeanDefinitionRegistrar(registrar, 
currentSourceClass.getMetadata());
                   }
                    else { // 如果当前的类既不是
ImportSelector也不是ImportBeanDefinitionRegistar就进行@Configuration的解析处理
                        // Candidate class not an ImportSelector or 
ImportBeanDefinitionRegistrar ->
                        // process it as an @Configuration class
                        this.importStack.registerImport(
                                currentSourceClass.getMetadata(), 
candidate.getMetadata().getClassName());
                       
processConfigurationClass
(candidate.asConfigClass(configClass));
                   }
 }
           }
            catch (BeanDefinitionStoreException ex) {
                throw ex;
           }
            catch (Throwable ex) {
                throw new BeanDefinitionStoreException(
                        "Failed to process import candidates for configuration 
class [" +
                        configClass.getMetadata().getClassName() + "]", ex);
           }
            finally {
                this.importStack.pop();
           }
       }
   }

        在这里我们可以看到ImportSelector接口的返回值会递归进行解析,把解析到的类全名按照
@Confifiguration进行处理。

2springBoot自动装载

        SpringBoot开箱即用的特点,很大程度上归功于ImportSelector。接下来我们看下springBoot是如何在 spring的基础上做扩展的。 在SpringBoot中最重要的一个注解SpringBootApplication

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
 //...
}

在SpringBootApplication注解中声明了一个@EnableAutoConfiguration。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}

在EnableAutoConfifiguration中通过Import引入了SpringBoot定义的AutoConfigurationImportSelector 这个类内容比较多,我们只需看下最主要的逻辑代码即可。

public class AutoConfigurationImportSelector
 implements DeferredImportSelector, BeanClassLoaderAware, 
ResourceLoaderAware,
 BeanFactoryAware, EnvironmentAware, Ordered {
 @Override
 public String[] selectImports(AnnotationMetadata annotationMetadata) {
 if (!isEnabled(annotationMetadata)) {
 return NO_IMPORTS;
 }
 AutoConfigurationMetadata autoConfigurationMetadata =
AutoConfigurationMetadataLoader
 .loadMetadata(this.beanClassLoader);
 //主要逻辑在getAutoConfigurationEntry这个方法
 AutoConfigurationEntry autoConfigurationEntry =
getAutoConfigurationEntry(
 autoConfigurationMetadata, annotationMetadata);
 return
StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
 }
 protected AutoConfigurationEntry getAutoConfigurationEntry(
 AutoConfigurationMetadata autoConfigurationMetadata,
 AnnotationMetadata annotationMetadata) {
 if (!isEnabled(annotationMetadata)) {
 return EMPTY_ENTRY;
 }
 AnnotationAttributes attributes = getAttributes(annotationMetadata);
 //通过getCandidateConfigurations方法获取所有需要加载的bean
 List<String> configurations =
getCandidateConfigurations(annotationMetadata,
 attributes);
 //去重处理
 configurations = removeDuplicates(configurations);
 //获取不需要加载的bean,这里我们可以通过spring.autoconfigure.exclude人为配置
 Set<String> exclusions = getExclusions(annotationMetadata, attributes);
 checkExcludedClasses(configurations, exclusions);
 configurations.removeAll(exclusions);
 configurations = filter(configurations, autoConfigurationMetadata);
 //发送事件,通知所有的AutoConfigurationImportListener进行监听
fireAutoConfigurationImportEvents(configurations, exclusions);
 return new AutoConfigurationEntry(configurations, exclusions);
 }
 //这里是获取bean渠道的地方,重点看SpringFactoriesLoader#loadFactoryNames
 protected List<String> getCandidateConfigurations(AnnotationMetadata
metadata,
 AnnotationAttributes attributes) {
 //这里的getSpringFactoriesLoaderFactoryClass()最终返回
EnableAutoConfiguration.class
 List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
 getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
 Assert.notEmpty(configurations,
 "No auto configuration classes found in METAINF/spring.factories. If you "
 + "are using a custom packaging, make sure that file is 
correct.");
 return configurations;
 }
}

从上面的逻辑可以看出,最终获取bean的渠道在SpringFactoriesLoader.loadFactoryNames。

public final class SpringFactoriesLoader {
    public static final String FACTORIES_RESOURCE_LOCATION = "METAINF/spring.factories";
    private static final Log logger =
LogFactory.getLog(SpringFactoriesLoader.class);
    private static final Map<ClassLoader, MultiValueMap<String, String>> cache =
new ConcurrentReferenceHashMap();
    public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable
ClassLoader classLoader) {
        String factoryClassName = factoryClass.getName();
        //通过factoryClassName获取相应的bean全称
 //上面传入的factoryClass是EnableAutoConfiguration.class
        return
(List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, 
Collections.emptyList());
   }
    private static Map<String, List<String>> loadSpringFactories(@Nullable
ClassLoader classLoader) {
        MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
        if (result != null) {
            return result;
       } else {
            try {
                //获取工程中所有META-INF/spring.factories文件,将其中的键值组合成Map
                Enumeration<URL> urls = classLoader != null ?
classLoader.getResources("META-INF/spring.factories") : 
ClassLoader.getSystemResources("META-INF/spring.factories");
                LinkedMultiValueMap result = new LinkedMultiValueMap();
                while(urls.hasMoreElements()) {
                    URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
                    Properties properties =
PropertiesLoaderUtils.loadProperties(resource);
                    Iterator var6 = properties.entrySet().iterator();
                    while(var6.hasNext()) {
                        Entry<?, ?> entry = (Entry)var6.next();
                        String factoryClassName =
((String)entry.getKey()).trim();
                        String[] var9 =
StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                        int var10 = var9.length;
                        for(int var11 = 0; var11 < var10; ++var11) {
                            String factoryName = var9[var11];
                            result.add(factoryClassName, factoryName.trim());
                       }
                   }
               }
                cache.put(classLoader, result);
                return result;
           } catch (IOException var13) {
                throw new IllegalArgumentException("Unable to load factories 
from location [META-INF/spring.factories]", var13);
           }
       }
   }
    private static <T> T instantiateFactory(String instanceClassName, Class<T>
factoryClass, ClassLoader classLoader) {
        try {
            Class<?> instanceClass = ClassUtils.forName(instanceClassName, 
classLoader);
            if (!factoryClass.isAssignableFrom(instanceClass)) {
                throw new IllegalArgumentException("Class [" + instanceClassName
+ "] is not assignable to [" + factoryClass.getName() + "]");
           } else {
                return ReflectionUtils.accessibleConstructor(instanceClass, new
Class[0]).newInstance();
           }
       } catch (Throwable var4) {
            throw new IllegalArgumentException("Unable to instantiate factory 
class: " + factoryClass.getName(), var4);
       }
   }
}

        每个jar都可以定义自己的META-INF/spring.factories ,jar被加载的同时spring.factories里面定义的 bean就可以自动被加载。

3.Eureka替换方案Consul

4.服务调用Ribbon入门

经过以上的学习,已经实现了服务的注册和服务发现。当启动某个服务的时候,可以通过HTTP的形式 将信息注册到注册中心,并且可以通过SpringCloud提供的工具获取注册中心的服务列表。但是服务之 间的调用还存在很多的问题,如何更加方便的调用微服务,多个微服务的提供者如何选择,如何负载均衡等。

4.1Ribbon****概述

4.2.1什么是Ribbon

**        **是 Netflflixfa发布的一个负载均衡器,有助于控制HTTP和TCP客户端行为。在SpringCloud中, Eureka一般配合Ribbon进行使用,Ribbon提供了客户端负载均衡的功能,Ribbon利用从Eureka中读取到的服务信息,在调用服务节点提供的服务时,会合理的进行负载。

在SpringCloud中可以将注册中心和Ribbon配合使用,Ribbon自动的从注册中心中获取服务提供者的列表信息,并基于内置的负载均衡算法,请求服务 。

4.2.2Ribbon****的主要作用

**        **1)服务调用:基于Ribbon实现服务调用, 是通过拉取到的所有服务列表组成(服务名-请求路径的)映射关系。借助RestTemplate 最终进行调用。

2)负载均衡:当有多个服务提供者时,Ribbon可以根据负载均衡的算法自动的选择需要调用的服务地址。

4.2 基于Ribbon****实现订单调用商品服务

**        **不论是基于Eureka的注册中心还是基于Consul的注册中心,SpringCloudRibbon统一进行了封装,所以对于服务调用,两者的方式是一样的。 

4.2.1 坐标依赖

在springcloud提供的服务发现的jar中以及包含了Ribbon的依赖。所以这里不需要导入任何额外的坐标。

4.2.2****工程改造

**        ****(1) 服务提供者:**修改shop_service_product模块中ProductController#findById()方法如下

@Value("${server.port}")
private String port;
@Value("${spring.cloud.client.ip-address}")
private String ip;
@GetMapping("/{id}")
public Product findById(@PathVariable Long id) {
    Product product = productService.findById(id);
    //设置端口
    product.setProductDesc("调用shop-service-product服务,ip:"+ip+",服务提供者端
口:"+port);
    return product; }

**(2) 服务消费者:**修改服务消费者 shop_service_order模块中的启动类OrderApplication,在创建RestTemplate方法上添加 @LoadBalanced注解。

@Autowired
 private RestTemplate restTemplate;
 @GetMapping("/buy/{id}")
 public Product order() {
 //通过restTemplate调用商品微服务
 //Product product = 
restTemplate.getForObject("http://127.0.0.1:9002/product/1", Product.class);
 Product product = restTemplate.getForObject("http://shop-serviceproduct/product/1", Product.class);
 return product;
 }

4.2.3代码测试

**        **浏览器中请求http://localhost:9001/order/buy/1查看展示效果如下,已经可以在订单微服务中已服务名称的形式调用商品微服务获取数据。

5.服务调用Ribbon高级

5.1负载均衡概述

5.1.1什么是负载均衡

在搭建网站时,如果单节点的 web服务性能和可靠性都无法达到要求;或者是在使用外网服务时,经常 担心被人攻破,一不小心就会有打开外网端口的情况,通常这个时候加入负载均衡就能有效解决服务问题。

负载均衡是一种基础的网络服务,其原理是通过运行在前面的负载均衡服务,按照指定的负载均衡算法,将流量分配到后端服务集群上,从而为系统提供并行扩展的能力。

负载均衡的应用场景包括流量包、转发规则以及后端服务,由于该服务有内外网个例、健康检查等功能,能够有效提供系统的安全性和可用性。

5.1.2****客户端负载均衡与服务端负载均衡

**        服务端负载均衡**:先发送请求到负载均衡服务器或者软件,然后通过负载均衡算法,在多个服务器之间选择一个进行访问;即在服务器端再进行负载均衡算法分配。

客户端负载均衡:客户端会有一个服务器地址列表,在发送请求前通过负载均衡算法选择一个服务器,然后进行访问,这是客户端负载均衡;即在客户端就进行负载均衡算法分配。

5**.2基于Ribbon****实现负载均衡**

5.2.1****搭建多服务实例

**        **修改shop_service_product的application.yml配置文件,已profifiles的形式配置多个实例。

spring:
 profiles: product1
 application:
   name: shop-service-product
 datasource:
   driver-class-name: com.mysql.jdbc.Driver
   url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf8
   username: root
   password: 111111
 jpa:
   database: MySQL
   show-sql: true
   open-in-view: true
 cloud:
   consul: #consul相关配置
     host: localhost #ConsulServer请求地址
     port: 8500 #ConsulServer端口
     discovery:
        #实例ID
       instance-id: ${spring.application.name}-1
        #开启ip地址注册
       prefer-ip-address: true
        #实例的请求ip
       ip-address: ${spring.cloud.client.ip-address}
server:
 port: 9002
---
spring:
 profiles: product2
 application:
   name: shop-service-product
 datasource:
   driver-class-name: com.mysql.jdbc.Driver
   url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf8
   username: root
   password: 111111
 jpa:
   database: MySQL
   show-sql: true
   open-in-view: true
 cloud:
   consul: #consul相关配置
     host: localhost #ConsulServer请求地址
     port: 8500 #ConsulServer端口
     discovery:
        #实例ID
       instance-id: ${spring.application.name}-2
        #开启ip地址注册
       prefer-ip-address: true
        #实例的请求ip
       ip-address: ${spring.cloud.client.ip-address}
server:
 port: 9004

分别启动两次服务器验证效果,并查看两个控制台发现已轮询的方式调用了商品服务。

5.2.2负载均衡策略
        Ribbon内置了多种负载均衡策略,内部负责复杂均衡的顶级接口为 com.netflix.loadbalancer.IRule ,实现方式如下

  • com.netflix.loadbalancer.RoundRobinRule:以轮询的方式进行负载均衡。
  • com.netflix.loadbalancer.RandomRule:随机策略
  • com.netflix.loadbalancer.RetryRule:重试策略。
  • com.netflix.loadbalancer.WeightedResponseTimeRule:权重策略。会计算每个服务的权 重,越高的被调用的可能性越大。
  • com.netflix.loadbalancer.BestAvailableRule:最佳策略。遍历所有的服务实例,过滤掉 故障实例,并返回请求数最小的实例返回。
  • com.netflix.loadbalancer.AvailabilityFilteringRule:可用过滤策略。过滤掉故障和请 求数超过阈值的服务实例,再从剩下的实力中轮询调用。

在服务消费者的application.yml配置文件中修改负载均衡策略。

##需要调用的微服务名称
shop-service-product:
 ribbon:
   NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

策略选择:
1、如果每个机器配置一样,则建议不修改策略(推荐)

2、如果部分机器配置强,则可以改为WeightedResponseTimeRule

5.3 Ribbon****中负载均衡的源码解析

5**.3.1 Ribbon****中的关键组件**

**

**

.ServerList:可以响应客户端的特定的服务器列表。

  • ServerListFilter:可以动态获得的具有所需特征的候选服务器列表的过滤器。
  • ServerListUpdater:用于执行动态服务器列表更新。
  • Rule:负载均衡策略,用于确定从服务器列表返回哪个服务器。
  • Ping:客户端用于快速检查服务器当时是否处于活动状态。

・LoadBalancer:负载均衡器,负责负载均衡调度的管理。

5.3.2@LoadBalanced****注解

**        **使用Ribbon完成客户端负载均衡往往是从一个注解开始的。

/**
* 基于Ribbon的服务调用与负载均衡
*/
@LoadBalanced
@Bean
public RestTemplate getRestTemplate() {
    return new RestTemplate();
}

这个注解的主要作用是什么呢,查看源码

/**
* Annotation to mark a RestTemplate bean to be configured to use a 
LoadBalancerClient
* @author Spencer Gibb
*/
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

通过注释可以知道@LoadBalanced注解是用来给RestTemplate做标记,方便我们对RestTemplate添加一个LoadBalancerClient,以实现客户端负载均衡。

5.3.3自动装配

**        **根据SpringBoot中的自动装配规则可以在spring-cloud-netflix-ribbon-2.1.0.RELEASE.jar中可以 找到 spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.springframew
ork.cloud.netflix.ribbon.RibbonAutoConfiguration

找到自动装配的类RibbonAutoConfifiguration

@Configuration
@Conditional({RibbonAutoConfiguration.RibbonClassesConditions.class})
@RibbonClients
@AutoConfigureAfter(
    name = {"org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration"} )
@AutoConfigureBefore({LoadBalancerAutoConfiguration.class, 
AsyncLoadBalancerAutoConfiguration.class})
@EnableConfigurationProperties({RibbonEagerLoadProperties.class, 
ServerIntrospectorProperties.class})
public class RibbonAutoConfiguration {
 @Bean
    public SpringClientFactory springClientFactory() {
        SpringClientFactory factory = new SpringClientFactory();
        factory.setConfigurations(this.configurations);
        return factory;
   }
    
    @Bean
    @ConditionalOnMissingBean({LoadBalancerClient.class})
    public LoadBalancerClient loadBalancerClient() {
        return new RibbonLoadBalancerClient(this.springClientFactory());
   }
    //省略
}

通过 RibbonAutoConfiguration引入了LoadBalancerAutoConfiguration配置类。

5.3.4****负载均衡调用

@Configuration
@ConditionalOnClass({RestTemplate.class})
@ConditionalOnBean({LoadBalancerClient.class})
@EnableConfigurationProperties({LoadBalancerRetryProperties.class})
public class LoadBalancerAutoConfiguration {
    @LoadBalanced
    @Autowired(
        required = false
   )
    private List<RestTemplate> restTemplates = Collections.emptyList();
    @Autowired(
        required = false
   )
    private List<LoadBalancerRequestTransformer> transformers =
Collections.emptyList();
 public LoadBalancerAutoConfiguration() {
   }
    @Bean
    public SmartInitializingSingleton
loadBalancedRestTemplateInitializerDeprecated(ObjectProvider<List<RestTemplateCu
stomizer>> restTemplateCustomizers) {
        return () -> {
            restTemplateCustomizers.ifAvailable((customizers) -> {
                Iterator var2 = this.restTemplates.iterator();
                while(var2.hasNext()) {
                    RestTemplate restTemplate = (RestTemplate)var2.next();
                    Iterator var4 = customizers.iterator();
                    while(var4.hasNext()) {
                        RestTemplateCustomizer customizer = (RestTemplateCustomizer)var4.next();
                        customizer.customize(restTemplate);
                   }
               }
           });
       };
   }
    @Bean
    @ConditionalOnMissingBean
    public LoadBalancerRequestFactory
loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {
        return new LoadBalancerRequestFactory(loadBalancerClient, 
this.transformers);
   }
    @Configuration
    @ConditionalOnClass({RetryTemplate.class})
    public static class RetryInterceptorAutoConfiguration {
        public RetryInterceptorAutoConfiguration() {
       }
        @Bean
        @ConditionalOnMissingBean
        public RetryLoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient
loadBalancerClient, LoadBalancerRetryProperties properties, 
LoadBalancerRequestFactory requestFactory, LoadBalancedRetryFactory
loadBalancedRetryFactory) {
            return new RetryLoadBalancerInterceptor(loadBalancerClient, 
properties, requestFactory, loadBalancedRetryFactory);
       }
        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer
restTemplateCustomizer(RetryLoadBalancerInterceptor loadBalancerInterceptor) {
            return (restTemplate) -> {
                List<ClientHttpRequestInterceptor> list = new
ArrayList(restTemplate.getInterceptors());
 list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
           };
       }
   }
    @Configuration
    @ConditionalOnClass({RetryTemplate.class})
    public static class RetryAutoConfiguration {
        public RetryAutoConfiguration() {
       }
        @Bean
        @ConditionalOnMissingBean
        public LoadBalancedRetryFactory loadBalancedRetryFactory() {
            return new LoadBalancedRetryFactory() {
           };
       }
   }
    @Configuration
   
@ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"})
    static class LoadBalancerInterceptorConfig {
        LoadBalancerInterceptorConfig() {
       }
        @Bean
        public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient
loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
            return new LoadBalancerInterceptor(loadBalancerClient, 
requestFactory);
       }
        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer
restTemplateCustomizer(LoadBalancerInterceptor loadBalancerInterceptor) {
            return (restTemplate) -> {
                List<ClientHttpRequestInterceptor> list = new
ArrayList(restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
           };
       }
   }
}

在该自动化配置类中,主要做了下面三件事:

  • 创建了一个LoadBalancerInterceptor的Bean,用于实现对客户端发起请求时进行拦截,以实现客户端负载均衡。
  • 创建了一个RestTemplateCustomizer的Bean,用于给RestTemplateLoadBalancerInterceptor 拦截器。
  • 维护了一个被@LoadBalanced注解修饰的RestTemplate对象列表,并在这里进行初始化,通过 调用 RestTemplateCustomizer的实例来给需要客户端负载均衡的RestTemplate 增加LoadBalancerInterceptor拦截器。
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
    private LoadBalancerClient loadBalancer;
    private LoadBalancerRequestFactory requestFactory;
    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, 
LoadBalancerRequestFactory requestFactory) {
        this.loadBalancer = loadBalancer;
        this.requestFactory = requestFactory;
   }
    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
        this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
   }
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, 
ClientHttpRequestExecution execution) throws IOException {
        URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid 
hostname: " + originalUri);
        return (ClientHttpResponse)this.loadBalancer.execute(serviceName, 
this.requestFactory.createRequest(request, body, execution));
   }
}

        通过源码以及之前的自动化配置类,我们可以看到在拦截器中注入了 LoadBalancerClient的实现。当 一个被 @LoadBalanced注解修饰的RestTemplate对象向外发起HTTP请求时,会被
LoadBalancerInterceptor类的intercept函数所拦截。由于我们在使用RestTemplate时候采用了

服务名作为host,所以直接从HttpRequest的URI对象中通过getHost()就可以拿到服务名,然后调用 execute 函数去根据服务名来选择实例并发起实际的请求。
分析到这里, LoadBalancerClient还只是一个抽象的负载均衡器接口,所以我们还需要找到它的具体 实现类来进一步分析。通过查看ribbon的源码,我们可以很容易的在org.springframework.cloud.netflix.ribbon 包下找到对应的实现类:RibbonLoadBalancerClient 。

public class RibbonLoadBalancerClient implements LoadBalancerClient {
    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));
   }
    
    public <T> T execute(String serviceId, LoadBalancerRequest<T> request) 
throws IOException {
        ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
        Server server = this.getServer(loadBalancer);
        if (server == null) {
            throw new IllegalStateException("No instances available for " +
serviceId);
       } else {

 RibbonLoadBalancerClient.RibbonServer ribbonServer = new
RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, 
serviceId), this.serverIntrospector(serviceId).getMetadata(server));
            return this.execute(serviceId, ribbonServer, request);
       }
   }
    
    protected Server getServer(String serviceId) {
        return this.getServer(this.getLoadBalancer(serviceId));
   }
    
    protected Server getServer(ILoadBalancer loadBalancer) {
        return loadBalancer == null ? null : 
loadBalancer.chooseServer("default");
   }
    
    protected ILoadBalancer getLoadBalancer(String serviceId) {
        return this.clientFactory.getLoadBalancer(serviceId);
   }
    
    //省略...
}
  • ServiceInstance choose(String serviceId):根据传入的服务id,从负载均衡器中为指定的服务选 择一个服务实例。
  • T execute(String serviceId, LoadBalancerRequest request):根据传入的服务id,指定的负载均衡器中的服务实例执行请求。
  • T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest request):根据传入的服务实例,执行请求。

从RibbonLoadBalancerClient代码可以看出,实际负载均衡的是通过ILoadBalancer来实现的。

@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server>
serverList, ServerListFilter<Server> serverListFilter, IRule rule, IPing ping, 
ServerListUpdater serverListUpdater) {
    return (ILoadBalancer)(this.propertiesFactory
   .isSet(ILoadBalancer.class, this.name) ? (ILoadBalancer)this.propertiesFactory
   .get(ILoadBalancer.class, config, this.name) : new
ZoneAwareLoadBalancer(config, rule, ping, serverList, serverListFilter, 
serverListUpdater));
}

相关文章

微信公众号

最新文章

更多