Spring Cloud系列(二):Eureka

简介

Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的。Spring Cloud将它集成在其子项目spring-cloud-netflix中,以实现Spring Cloud的服务发现功能。在微服务中使用此服务中间件能够得到更好的支持。

服务发现这类组件的一个核心是服务注册表,服务注册表是一个记录当前可用服务实例的网络信息数据库,也是服务发现机制的核心。服务注册表提供查询(找到可用的服务实例)和管理(管理服务注册和注销)API。

服务发现的方式

有两种,一种是客户端发现(Eureka、Zookeeper等),还有一种是服务端发现(Consul + Nginx)。

使用Eureka进行服务的注册和发现(服务端代码)

之前学分布式架构时,采用的是dubbo + zookeeper来实现不同独立工程之间的通信,在微服务中或者说是在Spring Cloud中,还是介意使用Eureka。下面用IDEA来完成一个简单的Spring Cloud Eureka。

程序实现

第一步当然是少不了在pom.xml中添加相关的依赖:

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

其次在Sping Boot启动类上,添加@EnableEurekaServer注解。

/**
 * @author uncle
 */
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}

接着在application.yml中配置一些最基本的东西:

server:
  port: 8888

eureka:
  # eureka 的实例地址
  instance:
    hostname: localhost
  # 客户端的一些配置
  client:
    # 是否注册其他的eureka,这里是单机模式,选择false
    registerWithEureka: false
    # 是否合并其他eureka的数据
    fetchRegistry: false
    # 健康检查
        healthcheck:
          enabled: true
    # 服务地址
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  server:
    # 自我保护模式
    enable-self-preservation: false
    # 清理无效节点的时间间隔,默认60000,单位毫秒
    eviction-interval-timer-in-ms: 60000

默认情况下,如果Eureka Server在一定时间内没有接收到某个微服务实例的心跳,Eureka Server将会注销该实例(默认90秒)。但是当网络分区故障发生时,微服务与Eureka Server之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。
Eureka通过“自我保护模式”来解决这个问题——当Eureka Server节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。一旦进入该模式,Eureka Server就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。当网络故障恢复后,该Eureka Server节点会自动退出自我保护模式。

以上内容选自:简书。如果需要健康检查测话,就要添加一个依赖,IDEA是没有健康检查的单词提示的,需要自己敲:

<!-- 健康检查 -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

然后访问http://localhost:8888/就可以看到:
image
红色报错的是提醒我们关闭了Eureka的自我保护模式。

使用Eureka的理由

  • Eureka来自生产环境
  • Spring Cloud对Eureka支持的很好

Eureka的架构图和原理

先看看官方给的一个架构图:

image
从上图中可以看到,Eureka包含两个组件:Eureka Server 和 Eureka Client。也就是服务端和客户端。它的每个区域都有一个Eureka集群,并且每个区域至少有一个eureka服务器可以处理区域故障,以防服务器瘫痪。

Eureka Server提供服务注册服务,各个节点启动后,会在Eureka Server中进行注册,这样Eureka Server中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。启动之后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90秒)。各个服务之间,通过复制的方式来实现数据的同步。

Eureka Client是一个Java客户端,用于简化与Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。

另外Eureka还提供了客户端缓存(Guava)的机制,即使所有的Eureka Server都挂掉,客户端依然可以利用缓存中的信息消费其他服务的API。

架构图上的那些单词

Application Service:服务提供者

Application Client:服务消费者

Replicate:复制。实现数据同步

Register:服务注册。当Eureka客户端向Eureka Server注册时,它提供自身的元数据,比如IP地址、端口,运行状况指示符URL,主页等。

Renew:服务续约。Eureka客户会每隔30秒发送一次心跳来续约。 通过续约来告知Eureka Server该Eureka客户仍然存在,没有出现问题。 正常情况下,如果Eureka Server在90秒没有收到Eureka客户的续约,它会将实例从其注册表中删除。 建议不要更改续约间隔。

Fetch Registries:获取注册列表信息。Eureka客户端从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每30秒钟)更新一次。每次返回注册列表信息可能与Eureka客户端的缓存信息不同, Eureka客户端自动处理。如果由于某种原因导致注册列表信息不能及时匹配,Eureka客户端则会重新获取整个注册表信息。 Eureka服务器缓存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同。Eureka客户端和Eureka 服务器可以使用JSON / XML格式进行通讯。在默认的情况下Eureka客户端使用压缩JSON格式来获取注册列表的信息。

Cancel:服务下线。Eureka客户端在程序关闭时向Eureka服务器发送取消请求。 发送请求后,该客户端实例信息将从服务器的实例注册表中删除。该下线请求不会自动完成,它需要调用以下内容:

DiscoveryManager.getInstance().shutdownComponent();

Eviction:服务剔除 。在默认的情况下,当Eureka客户端连续90秒没有向Eureka服务器发送服务续约,即心跳,Eureka服务器会将该服务实例从服务注册列表删除,即服务剔除。

region-zone

region和zone(或者Availability Zone)均是AWS的概念。在非AWS环境下,我们可以简单地将region理解为地域,zone理解成机房。一个region可以包含多个zone,可以理解为一个地域内的多个不同的机房。不同地域的距离很远,一个地域的不同zone间距离往往较近,也可能在同一个机房内。

region可以通过配置文件进行配置,如果不配置,会默认使用us-east-1。同样Zone也可以配置,如果不配置,会默认使用defaultZone。

Eureka Server通过eureka.client.serviceUrl.defaultZone属性设置Eureka的服务注册中心的位置。

指定region和zone的属性如下:

  • eureka.client.availabilityZones.myregion=myzone# myregion是region
  • eureka.client.region=myregion
    Ribbon的默认策略会优先访问通客户端处于同一个region中的服务端实例,只有当同一个zone中没有可用服务端实例的时候才会访问其他zone中的实例。所以通过zone属性的定义,配合实际部署的物理结构,我们就可以设计出应对区域性故障的容错集群。

小结

综上,Eureka通过心跳检测、健康检查、客户端缓存等机制,确保了系统的高可用性、灵活性和可伸缩性。

客户端代码

写maven工程,一般的操作就是一加依赖 二加注解 三改配置 四运行。所以第一步就是添加依赖:

<properties>
   <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
   <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
   <java.version>1.8</java.version>
   <!-- 在顶部添加这行 -->
   <spring-cloud.version>Edgware.RELEASE</spring-cloud.version>
</properties>

<!-- eureka client -->
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

<!-- spring cloud依赖 -->
<dependencyManagement>
   <dependencies>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-dependencies</artifactId>
         <version>${spring-cloud.version}</version>
         <type>pom</type>
         <scope>import</scope>
      </dependency>
   </dependencies>
</dependencyManagement>

二加注解,注解这个有点东西说,我在输入注解的时候还发现了一个注解:@EnableEurekaClient,它跟@EnableDiscoveryClient差不太多,看名字其实也能猜到,@EnableEurekaClient意思更为清楚一些,让人一看就知道是Eureka的注解,所以它是Eureka服务发现组件专用,@EnableDiscoveryClient可以用在其他服务发现组件上:

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

yml:

eureka:
  client:
    service-url:
      # 这个地址要跟服务中心地址相同
      defaultZone: http://localhost:8888/eureka/
  instance:
    # 表示用IP地址注册到Eureka
#    prefer-ip-address: true
    # 主机名(鼠标放在服务的Status(如:UP (1) - localhost:8889)上, 左下角就会有主机名称, 这里就是zengxiaochen:8889/info)
    hostname: zengxiaochen
    # 实例名
    appname: user
    # 实例失效时间
    lease-expiration-duration-in-seconds: 10
server:
  port: 8889

启动之后的预览图,下面就有这个实例的信息:
image

健康检查

<!-- 健康检查 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

添加了此依赖之后,启动微服务可以在控制台看到各个页面的URL,比如/health(服务的状态页面):
image

获取服务提供者信息

目前我知道的两个对象,一个是EurekaClientDiscoveryClient,从测试来看,后者比前者的信息更为详细,看代码:

@RestController
@RequestMapping(value = "/user/")
public class UserController {

    private static Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private EurekaClient eurekaClient;

    @Autowired
    /* 服务发现客户端 */
    private DiscoveryClient discoveryClient;

    @Autowired
    private UserDao userDao;

    /**
     * 通过用户ID查询用户信息
     *
     * @param id
     * @return
     */
    @GetMapping("{id}")
    public User findById(@PathVariable(value = "id") Long id) {
        return userDao.findOne(id);
    }

    @GetMapping("eureka-instance")
    public String serviceUrl() {
        /* 获取实现信息 */
        InstanceInfo instance = eurekaClient.getNextServerFromEureka("service-user", false);
        /* 获取此应用名的 IP:PORT */
        return instance.getHomePageUrl();
    }

    @GetMapping("instance-info")
    public ServiceInstance serviceInstance() {
        /* 第一种 已经过时 */
//        return discoveryClient.getLocalServiceInstance();

        /* 此方法的信息更为详细 */
        List<ServiceInstance> instances = discoveryClient.getInstances("service-user");
        if (instances != null && instances.size() > 0) {
            /* 不为空 */
            for (ServiceInstance instance : instances) {
                logger.info("host: {}, port: {}, url: {}",
                        instance.getHost(), instance.getPort(), instance.getUri());
                return instance;
            }
            // return instances.get(0);
        }
        return null;
    }
}

Eureka的参数详情

实例参数

spring.application.name:指定实例面板上的Status名称

eureka.instance.appname:指定实例名称,默认取 spring.application.name 配置值,如果没有则为 unknown

eureka.instance.hostname:指定实例URL的host名字,假如这个值是zeng的话,原先的http://localhost:8080/ -> http://zeng:8080/

eureka.instance.prefer-ip-address:默认false。如果为true,hostname则是以主机ip显示,http://localhost:8080/ -> http://127.0.0.1:8080/

eureka.instance.ip-address:指定实例的IP地址

eureka.instance.lease-renewal-interval-in-seconds:实例心跳间隔。单位:秒,默认30

eureka.instance.lease-expiration-duration-in-seconds:实例失效时间。单位:秒,默认90

eureka.instance.status-page-url-path:状态页面的URL,相对路径,默认/info

eureka.instance.status-page-url:状态页面URL,绝对路径

eureka.instance.health-check-url-path:健康检查URL

eureka.instance.health-check-url:健康检查URL

eureka.instance.metadataMap:定义元数据

客户端参数

eureka.client.service-url:指定服务注册中心地址,类型为 HashMap,并设置有一组默认值,默认的Key为 defaultZone;默认的Value为 http://localhost:8761/eureka ,如果服务注册中心为高可用集群时,多个注册中心地址以逗号分隔。如果服务注册中心加入了安全验证,这里配置的地址格式为: http://<;username>:<password>@localhost:8761/eureka 其中 <username> 为安全校验的用户名;<password> 为该用户的密码

注意安全校验参数

实现校验得走这几步,一,在yml中修改或配置:

    serviceUrl:
        defaultZone: http://user:password@${eureka.instance.hostname}:${server.port}/eureka/ // 这里的user和password要与第二步的user password一致

二:

security:
  basic:
    enabled: true
  user:
    name: user
    password: password

三,添加安全校验依赖(同时客户端的URL也要改成与服务端的URL一致):

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

eureka.client.fatch-registrey:默认true,检索服务

eureka.client.registrey-fetch-interval-seconds:默认30秒,从Eureka服务端获取注册信息的间隔时间

eureka.client.register-with-eureka:默认true,启动服务注册(其他文章有介绍)

eureka.client.eureka-server-connect-timeout-seconds:默认5秒,连接Eureka server的超时时间。

eureka.client.eureka-server-read-timeout-seconds:默认8秒,读取Eureka server信息的超时时间

eureka.client.filter-only-up-instances:默认true,获取实例时是否过滤,只保留UP状态的实例

eureka.client.eureka-connection-idle-timeout-seconds:默认30秒,Eureka服务端连接空闲关闭时间

eureka.client.eureka-server-total-connections:默认200个,从Eureka客户端到所有Eureka服务端的连接总数

eureka.client.eureka-server-total-connections-per-host:默认50个,从Eureka客户端到每个Eureka服务端的连接总数

服务端参数

eureka.server.enable-self-preservation:自我保护模式。默认true

eureka.client.eviction-interval-timer-in-ms:清理无效节点的时间间隔,默认60000毫秒。

深入理解Eureka

自定义元数据

eureka:
  instance:
    metadata-map:
      zone: zxc  # 客户端可以理解的,会影响客户端的行为
      imain: imain.net # 客户端理解不了,不会影响客户端的行为

然后通过访问localhost:8761/eureka/apps/service-id就可以看到当前service-id的元数据信息。

为什么注册一个服务这么慢

当一个服务成为一个实例的时候,还涉及到了注册表的默认持续时间为30秒的周期性心跳。并且在实例中,服务端和客户端在本地缓存中具有相同的元数据之前,服务是不可用于客户端发现的,因此需要3次心跳周期,也就是90秒的时间,在生产环境中,最好使用默认值,因为在服务器内部有一些计算,它们对续约做出一些假设

eureka:
  instance:
    # 续约周期,默认30秒
    lease-renewal-interval-in-seconds: 30

如何实现Eureka的高可用

yml:

spring:
  application:
    name: EUREKA-HA
# 两个节点相互注册
---
spring:
  profiles: peer1
eureka:
  instance:
    hostname: peer1
  client:
    serviceUrl:
      defaultZone: http://peer2:8762/eureka/
server:
  port: 8761

---
spring:
  profiles: peer2
eureka:
  instance:
    hostname: peer2
  client:
    serviceUrl:
      defaultZone: http://peer1:8761/eureka/
server:
  port: 8762

然后配置本地的hosts文件

127.0.0.1 peer1 peer2

启动两次这个工程,访问peer1:8761:
image
访问peer2:8762:
image
Eureka实现高可用只需要两个节点的要求就行(第一个节点会报错,因为第一个节点需要连接其他的节点,此时其他的节点并未启动),上图之所以有两个节点,是因为节点和节点之间的数据会进行同步,1节点会把自身信息上报给其他节点。

可以做一个测试,启动另外一个不是高可用的工程,把此服务注册到高可用节点的其中一个上面(下文称此节点为“1”),然后刷新其他节点,看注册到1节点上的服务有没有被同步到其他节点上(答案是肯定的)。

不过介意服务在进行注册的时候,还是注册到所有节点上,而不是单独注册到一个节点,因为万一注册的这单独的节点突然挂了,然后当前这个服务又需要重新上报,此时就尴尬了,这个服务就上报不了了。

如何解决Eureka Server不踢出已经关闭的节点

server端:

eureka:
  server:
    # 自我保护模式,生产环境切勿关闭
    enable-self-preservation: false
    # 清理无效节点的时间间隔,默认60000,单位毫秒
    eviction-interval-timer-in-ms: 120000

client:

eureka:
  client:
    healthcheck:
      enabled: true # 健康检查
  instance:
    lease-renewal-interval-in-seconds: 10 # 租期更新时间间隔(默认30秒)
    lease-expiration-duration-in-seconds: 30 # 租期到期时间(默认90秒)

如果想在Eureka实例界面的Status上使用IP:PORT怎么办

eureka:
  instance:
    instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
其他的注意事项
  • (Ribbon)使用RestTemplate时,想要获取一个List应该使用数组的形式,而不是直接使用List。
  • (Feign)自定义配置时,@Configuration@ComponentScan包不能重叠
  • @FeignClient所在接口中,不支持@GetMapper等组合注解
  • 使用@PathVariable时,需要指定其value
  • Feign暂不支持复杂对象作为Controller参数
Last modification:January 12th, 2018 at 03:47 pm
If you think my article is useful to you, please feel free to appreciate

Leave a Comment