从0开始用SpringCloud搭建微服务系统【二】

服务注册与服务发现

  1. 背景:在传统应用中,组件之间的调用,通过有规范的约束的接口来实现,从而实现不同模块间良好的协作。但是被拆分成微服务后,每个微服务实例的网络地址都可能动态变化,数量也会变化,使得原来硬编码的地址失去了作用。需要一个中心化的组件来进行服务的登记和管理。

  2. 概念:实现服务治理,即管理所有的服务信息和状态。

  3. 注册中心好处:不用关心有多少提供方。

  4. 注册中心有哪些: Eureka,Nacos,Consul,Zookeeper等。

  5. 服务注册与发现包括两部分,一个是服务器端,另一个是客户端。

    1. 服务端(Server)是一个公共服务,为Client提供服务注册和发现的功能,维护注册到自身的Client的相关信息,同时提供接口给Client获取注册表中其他服务的信息,使得动态变化的Client能够进行服务间的相互调用。
    2. 客户端(Client) 将自己的服务信息通过一定的方式登记到 Server上,并在正常范围内维护自己信息一致性,方便其他服务发现自己,同时可以通过Server获取到自己依赖的其他服务信息,完成服务调用,还内置了负载均衡器,用来进行基本的负载均衡。

服务端和客户端功能

server注册中心功能

  1. 服务注册表:记录各个微服务信息,例如服务名称,ip,端口等。注册表提供查询 API(查询可用的微服务实例)和管理 API(用于服务的注册和注销)。
  2. 服务注册与发现:
    1. 注册:将微服务信息注册到注册中心。
    2. 发现:查询可用微服务列表及其网络地址。
  3. 服务检查:定时检测已注册的服务,如发现某实例长时间无法访问,就从注册表中移除。

client功能

  1. 注册:每个微服务启动时,将自己的网络地址等信息注册到注册中心,注册中心会存储(内存中)这些信息。
  2. 获取服务注册表:服务消费者从注册中心,查询服务提供者的网络地址,并使用该地址调用服务提供者,为了避免每次都查注册表信息,所以 client 会定时去 serve r拉取注册表信息到缓存到 client 本地。
  3. 心跳:各个微服务与注册中心通过某种机制(心跳)通信,若注册中心长时间和服务间没有通信,就会注销该实例。
  4. 调用:实际的服务调用,通过注册表,解析服务名和具体地址的对应关系,找到具体服务的地址,进行实际调用。

Eureka

  • Eureka 是 Netflix 开源的一个 Restful 服务,主要用于服务的注册发现。它由两个组件组成:Eureka 服务器 和Eureka 客户端。
  • Eureka 服务器用作服务注册服务器。各个节点启动后,会在 Eureka Server 中进行注册,这样 Eureka Server 中的服务注册表中将会存储所有可用服务节点的信息。
  • Eureka 客户端是一个 Java 客户端,用来简化与服务器的交互,作为轮询负载均衡器,发现相关的服务,并提供服务的故障切换。
  • Eureka 在设计时就优先保证可用性。即在 CAP 理论中,Eureka 满足 AP。Eureka 各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。
  • Eureka 的几个时间点:
    • 在应用启动后,将会向 Eureka Server 发送心跳,默认周期为 30 秒。
    • 如果 Eureka Server 在多个心跳周期内没有接收到某个节点的心跳,Eureka Server 将会从服务注册表中把这个服务节点移除(默认90秒)。
    • Eureka Client 对已经获取到的注册信息做了 30s 缓存。即服务通过 Eureka 客户端第一次查询到可用服务地址后会将结果缓存,下次再调用时就不会真正向 Eureka 发起 HTTP 请求了。
    • 负载均衡组件 Ribbon 也有 30s 缓存。Ribbon 会从上面提到的 Eureka Client 获取服务列表,然后将结果缓存 30s。
    • 最大可能出现2分钟的延迟:注册延迟30s + Eureka服务器响应延迟30s + Eureka客户端更新延迟30s + Ribbon服务列表更新延迟30s。
  • Eureka 启动保护机制:

    • 如果在 15 分钟内超过 85% 的节点都没有正常的心跳,那 么Eureka 就认为客户端与注册中心出现了网络故障。

    • 自我保护机制的触发条件:

      条件:当每分钟心跳次数( renewsLastMin ) 小于 numberOfRenewsPerMinThreshold 时,并且开启自动保护模式开关( eureka.server.enable-self-preservation = true ) 时,触发自我保护机制,不再自动过期续约。

      其中:

      1. numberOfRenewsPerMinThreshold = expectedNumberOfRenewsPerMin * 续租百分比( eureka.server.renewalPercentThreshold, 默认0.85 )

      2. expectedNumberOfRenewsPerMin = 当前注册的应用实例数 x 2 。为什么乘以 2: 默认情况下,注册的应用实例每半分钟续租一次,那么一分钟心跳两次,因此 x 2 。

        解释:服务实例数10个,期望每分钟续约数10 2=20,期望阈值200.85=17,自我保护少于17时 触发。】

    • Eureka 启动保护机制会出现以下情况:

      • Eureka 不再从注册列表中移除因为长时间没收到心跳而应该过期的服务。
      • Eureka 仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)。
      • 当网络稳定时,当前实例新的注册信息会被同步到其它节点中。

架构原理图

eureka架构图.png

Eureka Server

引入依赖 spring-cloud-starter-netflix-eureka-server, 在启动类 Application 上,添加 @EnableEurekaServer 注解。

如果是单机服务,可以在 application.yml 中使用以下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
eureka:
environment: dev # 设置环境,可选
server:
enable-self-preservation: false # 中小规模下,自我保护模式坑比好处多,所以关闭它
renewal-threshold-update-interval-ms: 120000 # 心跳阈值计算周期,如果开启自我保护模式,可以改一下这个配置
eviction-interval-timer-in-ms: 5000 # 主动失效检测间隔,配置成5秒
use-read-only-response-cache: false # 禁用readOnlyCacheMap
wait-time-in-ms-when-sync-empty: 0 #在Eureka服务器获取不到集群里对等服务器上的实例时,需要等待的时间,单机模式设置为0
client:
healthcheck: true
service-url:
defaultZone: http://${webfuse-security.user.name}:${webfuse-security.user.password}@localhost:2000/eureka/
registry-fetch-interval-seconds: 5 # 定时刷新本地缓存时间
register-with-eureka: false #表示是否将自己注册到Eureka Server,默认为true。由于当前这个应用就是Eureka Server,故而设为false。
fetch-registry: false #表示是否从Eureka Server获取注册信息,默认为true。因为这是一个单点的Eureka Server,不需要同步其他的Eureka Server节点的数据,故而设为false。
instance:
hostname: ${hostname:localhost}
instance-id: ${spring.application.name}@${spring.cloud.client.ip-address}:${server.port} # 自定义实例ID
prefer-ip-address: true
lease-expiration-duration-in-seconds: 10 # 没有心跳的淘汰时间,10秒
lease-renewal-interval-in-seconds: 5 # 心跳间隔,5秒

启动服务,可在 http://localhost:2000 查看项目页面。

Eureka Client

EurekaClient 可以在客户端获取eureka服务器上的注册者信息。

引入依赖 spring-cloud-starter-netflix-eureka-client, 在启动类 Application 上,添加 @EnableDiscoveryClient 注解。

application.yml 中使用以下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
eureka:
environment: dev
client:
healthcheck:
enabled: true
service-url:
defaultZone: http://user:[email protected]:8010/eureka/
registry-fetch-interval-seconds: 5 # 定时刷新本地缓存时间
instance:
hostname: ${hostname:localhost}
instance-id: ${spring.application.name}@${spring.cloud.client.ip-address}:${server.port} # 自定义实例ID
prefer-ip-address: true
lease-expiration-duration-in-seconds: 10 # 没有心跳的淘汰时间,10秒
lease-renewal-interval-in-seconds: 5 # 心跳间隔,5秒

启动项目即可。可在 http://localhost:2000 中看到注册的状态。

Eureka 高可用

Eureka Server 之间是可以互相注册的。

举个例子,我们有 3 个 Eureka 注册中心,端口分别为 2001 、 2002 和 2003 。那么端口为 2001 的最基本的配置如下:

1
2
3
4
eureka:
client:
service-url:
defaultZone: http://localhost:2002/eureka/,http://localhost:2003/eureka/

端口 2002 和 2003 服务可以根据以上规则配置。

多网卡选择

服务器有多个网卡,eh0,eh1,eh2,只有eh0可以让外部其他服务访问进来,而Eureka client将eh1和eh2注册到Eureka server上,这样其他服务就无法访问该微服务了。有两种方式:

  • 指定IP注册

    1
    2
    3
    4
    eureka:
    instance:
    prefer-ip-address: true
    ip-address: 实际能访问到的IP
  • 使用spring.cloud.inetutils配置网卡选择

Eureka健康检查

由于server和client通过心跳保持 服务状态,而只有状态为UP的服务才能被访问。看eureka界面中的status。

比如心跳一直正常,服务一直UP,但是此服务DB连不上了,无法正常提供服务。

此时,我们需要将 微服务的健康状态也同步到server。只需要启动eureka的健康检查就行。这样微服务就会将自己的健康状态同步到eureka。配置如下即可。

在client端加入Actuator,并配置eureka.client.healthcheck.enabled=true,将自己真正的健康状态传播到server。

通过代码来改动服务的状态:

1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Data
public class HealthStatusHandler implements HealthIndicator {
private Boolean status = true;
@Override
public Health health() {
if (status) {
return new Health.Builder().up().build();
}
return new Health.Builder().down().build();
}
}

应用场景:比如说短信业务,欠费了等情况,可以暂时下线服务。

Eureka遇到的坑

available-replicas为空的问题

如果使用了eureka.instance.prefer-ip-address: true,然后eureka.client.service-url.defaultZone配置的IP与实例IP不一致,会出现available-replicas为空的问题。

解决方法:在eureka.instance.ip-address中强制设置IP,然后在eureka.client.service-url.defaultZone配置对应的IP。

Eureka 最佳实践

本人所在的公司由于体量小,生产环境直接使用 Eureka 的默认配置进行高可用性运行,目前也没有出现太大的问题。

以下是一些实践参考文章(注意:文章中的版本号不是最新的,可能在配置上会有调整):