Eureka 服务注册中心

Eureka架构下将服务分为以下两个角色:

  • 注册中心(Eureka Server,服务端)。
  • 客户端(Eureka Client)。

Eureka会将客户端(即服务)的信息进行注册,让客户端可以通过Eureka注册中心获取服务信息并进行调用。

Eureka的作用:

  • 注册服务信息:服务提供者启动时向Eureka注册自己的信息,Eureka保存这些服务信息到服务列表中。
  • 获取服务信息:服务消费者根据服务名称向Eureka拉取服务列表。
  • 负载均衡:有多个服务提供者时,服务消费者利用负载均衡算法,从Eureka注册的服务列表中挑选一个服务后发起远程调用。
  • 感知服务健康状态
    • 服务提供者会每隔30秒向Eureka Server发送心跳请求,报告自己的健康状态。
    • Eureka会更新记录服务信息列表,心跳不正常的服务会被剔除。

在Eureka架构中,角色有以下分工:

  • Eureka Server:记录服务信息;进行心跳监控,剔除心跳不正常的服务。
  • Eureka Client:
    • Provider:注册自己的信息到Eureka Server;每隔30秒向Eureka Server发送心跳。
    • Consumer:根据服务名称从Eureka Server拉取服务列表;基于服务列表做负载均衡,选中一个服务后发起远程调用。

Eureka 示例

依照上例,订单服务在获取订单信息时需要从用户服务中获取用户信息并一同发送:

  1. 首先需要创建一个新的Maven模块来运行Eureka Server。

    • Eureka Server的pom.xml如:

      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
          <parent>
              <groupId>asia.linner.demo</groupId>
              <artifactId>cloud-demo</artifactId>
              <version>1.0</version>
          </parent>
      
          <artifactId>eureka-server</artifactId>
      
          <properties>
              <maven.compiler.source>8</maven.compiler.source>
              <maven.compiler.target>8</maven.compiler.target>
              <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
          </properties>
      
          <dependencies>
              <!--Eureka服务端-->
              <dependency>
                  <groupId>org.springframework.cloud</groupId>
                  <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
              </dependency>
          </dependencies>
      </project>
      

      其中最主要的就是导入eureka-server起步依赖:

      <!--Eureka服务端-->
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
      </dependency>
      

      在Eureka中,服务端和客户端有不同的依赖,服务端的起步依赖就是spring-cloud-starter-netflix-eureka-server

  2. 编写EurekaApplication启动类,并在启动类上使用@EnableEurekaServer注解装配EurekaServer:

    @EnableEurekaServer     // EurekaServer开关,自动装配EurekaServer
    @SpringBootApplication
    public class EurekaApplication {
        public static void main(String[] args) {
            SpringApplication.run(EurekaApplication.class, args);
        }
    }
    
  3. eureka-server中,添加application.yml并加入以下配置信息:

    server:
      # 服务端口(视具体情况进行更改)
      port: 8180
    spring:
      application:
        # 服务名称
        name: eureka-server
    eureka:
      client:
        service-url:
          # Eureka的地址信息(如果有多个,则用逗号”,“隔开)
          defaultZone: http://localhost:8180/eureka
    
  4. 分别在order-serviceuser-service中添加Eureka客户端依赖:

    <!-- Eureka客户端 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    
  5. 分别在Eureka客户端的application.yml中添加如下配置信息:

    eureka:
      # 配置eureka-server的信息
      client:
        service-url:
          # defaultZone要与eureka-server中的defaultZone相同
          defaultZone: http://localhost:8180/eureka
    

    分别在Eureka客户端的application.yml中添加服务名称(spring.application.name),如order-service

    spring:
      application:
        # 服务名称
        name: order-service
    

    Eureka Server利用spring.application.name来找到对应模块的信息,并返回。利用spring.application.name也可以做负载均衡。

    由于所有模块都可能作为服务提供者被其他服务调用,所以项目中所有服务模块都应该在Eureka Server中进行注册。

  6. 在服务消费者中,需要在注册RestTemplate的方法上使用注解@LoadBalanced来开启Eureka的负载均衡。

    order-service中:

    @Bean
    @LoadBalanced   // 开启Eureka负载均衡
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    
  7. 服务消费者通过Eureka,使用RestTemplate调用其他模块。

    order-service调用user-service

    @Service
    public class OrderService {
    
        @Autowired
        private OrderMapper orderMapper;
        // 注入RestTemplate
        @Autowired
        private RestTemplate restTemplate;
    
        public Order queryOrderById(Long orderId) {
            // 1.查询订单
            Order order = orderMapper.findById(orderId);
            // 2.利用RestTemplate发起HTTP请求,查询用户信息
            // 2.1.URL路径(将服务的IP改为服务的名称)
            String url = "http://user-service/user/" + order.getUserId();
            // 2.2.发送HTTP(GET)请求,实现远程调用
            User user = restTemplate.getForObject(url, User.class);
            // 3.封装User到Order
            order.setUser(user);
            // 4.返回
            return order;
        }
    }
    

    这里面最主要的是,配置了Eureka后,使用RestTemplate调用其他模块时,URL中的IP可以改为服务提供者(被调模块)的服务名称,并且可以自动做负载均衡。即,原本是使用localhost:8081这个IP端口来调用user-service,但是现在可以直接使用http://user-service来调用user-service提供的接口。

Eureka 服务注册配置总结

按照角色配置Eureka,至少需要做如下配置:

  • Eureka Server:

    依赖:

    <!--Eureka服务端-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    

    配置(application.yml):

    server:
      # Eureka服务端口(视具体情况进行更改)
      port: 8180
    spring:
      application:
        # 服务名称
        name: eureka-server
    eureka:
      client:
        service-url:
          # Eureka的地址信息(如果有多个,则用逗号”,“隔开)
          defaultZone: http://localhost:8180/eureka
    
  • Eureka Client:

    依赖:

    <!-- Eureka客户端 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    

    配置(application.yml):

    spring:
      application:
        # 服务名称
        name: clinet-1-service
    eureka:
      client:
        service-url:
          # defaultZone要与eureka-server中的defaultZone相同
          defaultZone: http://localhost:8180/eureka
    
  • 服务消费者:

    开启负载均衡:

    @Bean
    @LoadBalanced   // 开启Eureka负载均衡
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    

    使用RestTemplate调用其他服务时,URL中的IP可以改为服务提供者(被调模块)的服务名称。


Idea 配置模块的多个实例

  1. 复制现有的配置:

    复制现有的配置

  2. 使用VM Options修改端口,避免与原有的模块产生端口冲突:

    修改端口


Ribbon 负载均衡

Ribbon是一款负载均衡组件,而Eureka的负载均衡则是基于Ribbon实现的。如上例(order-service)中:

@Bean
@LoadBalanced   // 开启Eureka负载均衡
public RestTemplate restTemplate() {
    return new RestTemplate();
}

负载均衡流程

Eureka中使用注解@LoadBalanced标识RestTemplate,从而实现拦截RestTemplate发起的请求,并获取相应的服务信息和执行负载均衡策略。

@LoadBalanced注解使用LoadBalancerInterceptor来执行请求的拦截。LoadBalancerInterceptor实现了ClientHttpRequestInterceptor接口。而ClientHttpRequestInterceptor接口主要用于拦截客户端HTTP请求。

ClientHttpRequestInterceptor接口:

@FunctionalInterface
public interface ClientHttpRequestInterceptor {
	ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
			throws IOException;
}

拦截流程:

  1. 使用RestTemplate发送请求时,Eureka会使用LoadBalancerInterceptorintercept()来执行拦截:

    public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
    
        private LoadBalancerClient loadBalancer;    // 负载均衡客户端
    
        @Override
        public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
                final ClientHttpRequestExecution execution) throws IOException {
            /*
                获取请求发出的URL
                例如order-service中获取订单信息时,会发出[http://user-service/user/{userId}]这个请求
                这里的request.getURI()会获取到[http://user-service/user/{userId}]这个请求的URI
            */
            final URI originalUri = request.getURI();
            /*
                通过URI获取请求的Host
                如:user-service
            */
            String serviceName = originalUri.getHost();
            Assert.state(serviceName != null,
                    "Request URI does not contain a valid hostname: " + originalUri);
            /*
                将Host作为Eureka客户端名称,获取相应的服务信息并执行负载均衡策略
            */
            return this.loadBalancer.execute(serviceName,
                    this.requestFactory.createRequest(request, body, execution));
        }
    }
    
  2. LoadBalancerClient是一个客户端执行负载均衡的接口,当实际执行时,使用的是RibbonLoadBalancerClient这个实现类:

    public class RibbonLoadBalancerClient implements LoadBalancerClient {
    
        /* ... */
    
        /**
         * 实际执行的是这个方法
         */
        public <T> T execute(
                String serviceId, // 传入的是serviceName,如 user-service
                LoadBalancerRequest<T> request, 
                Object hint) throws IOException {
    
            /*
                根据serviceId获取负载均衡器
                loadBalancer中有这么两个属性:
                - AllServerList
                - UpServerList
                它们记录了获取到的Eureka客户端信息
                实际执行时,loadBalancer的类型是ZoneAwareLoadBalancer
                ZoneAwareLoadBalancer继承了DynamicServerListLoadBalancer
                在loadBalancer中,使用IRule定义负载均衡策略
            */
            ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
            /*
                根据LoadBalancer,执行负载均衡策略,获取Eureka客户端的真实服务地址
                如,获取到 localhost:8081
            */
            Server server = getServer(loadBalancer, hint);
    
            /* ... */
        }
    
        /* ... */
    }
    

负载均衡策略

Ribbon的负载均衡策略是使用IRule这个接口来定义。在Eureka中给出了几个负载均衡策略的实现,它们的继承关系图如下:

负载均衡策略继承关系图

Eureka中负载均衡默认的实现是ZoneAvoidanceRule,它们的含义如下:

内置负载均衡规则类 规则描述
RoundRobinRule 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。
AvailabilityFilteringRule 对以下两种服务器进行忽略:
  • 短路的服务器:
    在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。
    短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。
  • 并发数过高的服务器:
    如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。
    并发连接数的上限,可以由客户端的<clientName>.<clientConfigNameSpace>.ActiveConnectionsLimit属性进行配置。
WeightedResponseTimeRule 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。
这个规则会随机选择服务器,这个权重值会影响服务器的选择。
ZoneAvoidanceRule 以区域可用的服务器为基础进行服务器的选择。
使用Zone对服务器进行分类(这个Zone可以理解为一个机房、一个机架等)。而后再对Zone内的多个服务做轮询。
在配置服务注册时,Zone的值可以被设置(按照服务器所在的地理位置进行设置)。使用此规则,服务消费者会优先选择跟自己在同一个Zone内的服务,然后再做轮询。
BestAvailableRule 忽略那些短路的服务器,并选择并发数较低的服务器。
RandomRule 随机选择一个可用的服务器。
RetryRule 重试机制的选择逻辑

配置负载均衡规则

配置负载均衡规则有两种方式:

  • 配置全局默认的负载均衡规则:

    在任何一个配置类或新建一个配置类,然后添加一个新的Bean。

    @Bean
    public IRule getLoadBalancerRule() {
        // 此处以RandomRule为例
        return new RandomRule();
    }
    

    使用Bean的方式配置负载均衡规则,无论当前服务远程调用哪个服务提供者,都是采用这个Bean里面配置的规则。

  • 指定的服务提供者配置单独的负载均衡规则:

    可以在application.yml中使用serverName.ribbon.NFLoadBalancerRuleClassName配置指定服务的负载均衡规则,它的值是具体规则的实现类的全类名。

    例如在order-service中配置user-service的负载均衡规则:

    user-service:
      ribbon:
        # 负载均衡规则
        NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
    

Ribbon 饥饿加载

  • 懒加载:第一次访问时才加载。
  • 饥饿加载:在项目启动时就开始加载。

Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。而饥饿加载则会在项目启动时创建,降低第一次访问的耗时。

可以在application.yml中,通过下面配置开启Ribbon的饥饿加载:

ribbon:
  eager-load: # 饥饿加载
    enabled: true # 开启饥饿加载
    clients: user-service # 指定对userservice这个服务饥饿加载

ribbon.eager-load.clients是一个集合类型,如果要对多个服务提供者开启饥饿加载,可以这样配置:

ribbon:
  eager-load: # 饥饿加载
    enabled: true # 开启饥饿加载
    # 对多个服务提供者开启饥饿加载
    clients:
      - clinet-1-service
      - clinet-2-service
      - clinet-3-service