服务发现:Eureka
服务发现是微服务架构中的一个重要概念。试想当系统服务之间的依赖越来越多,A服务可能需要调用B、C、D等服务,同时被调用方也就是服务提供方可能为了保证自身高可用,还需要同时以集群的模式部署B1、B2、C1、C2等,想象一下A的配置文件该有多复杂。将服务提供方的地址写死在配置文件中,那么服务提供方如果横向扩展增加实例,是不是还需要修改作为服务调用方的A的配置文件?这时我们就迫切需要一种服务发现的机制。所有的服务提供方启动时向注册中心报告自身的信息,包括自己的地址、端口,以及提供哪些服务等相关信息。当服务调用方需要调用服务时,只需要问注册中心是谁提供了相关的服务,注册中心返回哪些提供方提供了这些服务,调用方就可以自己根据注册中心返回的信息去请求了。Eureka就提供了这样一种能力,同时自身作为注册中心的同时也提供了高可用的支持,支持集群部署时各个节点之间的注册数据同步复制。
Eureka是Netflix开源的一款提供服务注册和发现的产品,提供了完整的服务注册和服务发现实现,也是Spring Cloud体系中最重要、最核心的组件之一。
通俗讲,Eureka就是一个服务中心,将所有可以提供的服务都注册到它这里来管理,其他各调用者需要的时候去注册中心获取,然后服务调用方再向服务提供方发起调用,避免了服务之间的直接调用,方便后续的水平扩展、故障转移等。
所以,服务中心这么重要的组件一旦宕机将会影响全部服务,因此需要搭建Eureka集群来保持高可用性,建议生产中最少配备两台。随着系统流量的不断增加,需要根据情况来扩展某个服务,Eureka内部提供均衡负载的功能,只需要增加相应的服务端实例即可。那么在系统的运行期间某个实例宕机怎么办?Eureka提供心跳检测机制,如果某个实例在规定的时间内没有进行通信则会被自动剔除掉,避免了某个实例挂掉而影响服务。
因此,使用Eureka就自动具有了注册中心、负载均衡、故障转移的功能。
它主要包括两个组件。
- Eureka Client:一个Java客户端,用于简化与Eureka Server的交互(通常就是微服务中的客户端和服务端)。
- Eureka Server:提供服务注册和发现的能力(通常就是微服务中的注册中心)。
各个微服务启动时,会通过Eureka Client向Eureka Server注册自己,Eureka Server会存储该服务的信息,如图2-1所示。
也就是说,每个微服务的客户端和服务端都会注册到Eureka Server,这就衍生出了微服务相互识别的话题。
- 同步:每个Eureka Server同时是Eureka Client(逻辑上的),多个Eureka Server之间通过复制的方式完成服务注册表的同步,从而实现Eureka的高可用。
- 识别:Eureka Client会在本地缓存Eureka Server中的信息。
即使所有Eureka Server节点都宕掉,服务消费方仍可使用本地缓存中的信息找到服务提供方。 - 续约:微服务会周期性(默认30s)地向Eureka Server发送心跳以续约(Renew)自己的信息(类似于heartbeat机制)。
- 续期:Eureka Server会定期(默认60s)执行一次失效服务检测功能,它会检查超过一定时间(默认90s)没有续约的微服务,发现则会注销该微服务节点。
当一个注册器客户端通过Eureka进行注册时,它会带上一些描述自己情况的元数据,如地址、端口、健康指示器地址、主页等。Eureka会接收每一个服务实例发送的心跳包。如果心跳包超过配置的间隔时间,则这个服务实例就会被移除。
2.1 使用Eureka
接下来尝试一个Eureka的示例。
新建一个Maven项目,在pom.xml中添加对Eureka服务端的依赖:
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
新建EurekaServerApplication.java,添加@EnableEurekaServer注解:
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
启动程序后,打开http://localhost:8080/ 就可以看到如图2-2所示的监控页面,图中展示了向Eureka注册的所有客户端。
2.1.1 Eureka服务提供方
启动一个服务提供方并注册到Eureka。新建一个项目并在pom.xml中添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
新建EurekaProviderApplication.java,并对外暴露一个sayHello的HTTP接口。
@SpringBootApplication
@EnableEurekaClient
@RestController
public class EurekaProviderApplication {
@RequestMapping("/sayHello")
public String sayHello(String name){
return "hello "+name;
}
public static void main(String[] args) {
new SpringApplicationBuilder(EurekaProviderApplication.class).web(true).run(args);
}
}
在application.yml配置Eureka服务端地址以及自身服务名称:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/ #在注册中心配置Eureka地址
spring:
application:
name: myprovider #自身名字
启动EurekaProviderApplication后,再去看Eureka的监控页面,应该能看到如图2-3所示信息,表明服务已经注册到Eureka。
还有一种方法就是直接使用原生的com.netflix.discovery.EurekaClient(对应Spring Cloud的DiscoveryClient)。通常,尽量不要直接使用原始的Netflix的Eureka Client,因为Spring已经对其进行封装抽象,应尽可能使用DiscoveryClient。
2.1.2 Eureka服务调用方
一旦在应用中使用了@EnableDiscoveryClient或者@EnableEurekaClient,就可以从Eureka Server中使用服务发现功能。
利用如下代码,通过DiscoveryClient可以从Eureka服务端获得所有提供myprovider服务的实例列表,并根据获得的ServiceInstance对象获取每个提供方的相关信息,如端口IP等,从中选取第一个提供方,通过Spring的RestTemplate发起HTTP请求进行调用。对于获取到的提供方集合,我们根据什么规则去选取哪个提供方?能否做到负载均衡呢?这将在第5章详细介绍。
@SpringBootApplication
@EnableDiscoveryClient
@Slf4j
@RestController
public class EurekaClientApplication {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
@Autowired
private DiscoveryClient client;
@Autowired
private RestTemplate restTemplate;
@RequestMapping(value = "/sayHello" ,method = RequestMethod.GET)
public String sayHello(String name) {
List<ServiceInstance> instances = client.getInstances("myprovider");
if (!instances.isEmpty()) {
ServiceInstance instance = instances.get(0);
log.info(instance.getUri().toString());
String result=restTemplate.getForObject(instance.getUri().toString()+ "/sayHello?name="+name,String.class);
return result;
}
return "failed";
}
public static void main(String[] args) {
SpringApplication.run(EurekaClientApplication.class, args);
}
}
此时,我们通过HTTP请求调用/sayHello接口,则可以看到服务调用方从Eureka获取服务提供方信息,并进行调用的日志信息了。
不要在@PostConstruct方法以及@Scheduled中(或者任何ApplicationContext还没初始完成的地方)使用EurekaClient。其需要等待SmartLifecycle(phase=0)初始化完成才可以。
2.2 进阶场景
尝试过Eureka的基本使用场景后,虽然功能能够实现,但是落实到具体的企业级应用场景时,必然会有许多需要自定义的配置以及一些生产环境可能需要用到的参数。下面列出一些场景供读者参考。
(1)Eureka的健康检查
默认情况下,Eureka通过客户端心跳包来检测客户端状态,并不是通过spring-boot-actuator模块的/health端点来实现的。默认的心跳实现方式可以有效地检查Eureka客户端进程是否正常运作,但是无法保证客户端应用能够正常提供服务。由于大多数微服务应用都会有一些外部资源依赖,比如数据库、Redis缓存等,如果应用与这些外部资源无法连通时,实际上已经不能提供正常的对外服务了,但因为客户端心跳依然在运行,所以它还是会被服务消费者调用,而这样的调用实际上并不能获得预期的效果。当然,我们可以开启Eureka的健康检查,这样应用状态就可以同步给Eureka了。在application.yml中添加如下配置即可:
eureka:
client:
healthcheck:
enabled:true
Eureka中的实例一共有如下几种状态:UP、DOWN、STARTING、OUT_OF_SERVICE、UNKNOWN。如果需要更多的健康检查控制,可以实现com.netflix.appinfo.Health-CheckHandler接口,根据自己的场景进行操作。
eureka.client.healthcheck.enabled=true只能在application.yml中设置,如果在bootstrap.yml中设置,会导致Eureka注册为UNKNOWN的状态。
(2)自我保护模式
Eureka在设计时,认为分布式环境的网络是不可靠的,可能会因网络问题导致Eureka Server没有收到实例的心跳,但是这并不能说明实例宕了,所以Eureka Server默认会打开保护模式,它主要是网络分区场景下的一种保护。
一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除里面的数据(即不会注销任何微服务)。在这种机制下,它仍然鼓励客户端再去尝试调用这个所谓down状态的实例,当网络故障恢复后,该Eureka Server节点会自动退出自我保护模式。若确实调用失败,熔断器就派上用场了。
关于熔断器,第6章会详细介绍并演示。
通过修改注册中心的配置文件application.yml,即可打开或关闭注册中心的自我保护模式:
eureka:
server:
enable-self-presaervation: false #关闭自我保护模式(默认为打开)
综上,自我保护模式是一种应对网络异常的安全保护措施。它的理念是宁可同时保留所有实例(健康实例和不健康实例都会保留),也不盲目注销任何健康的实例。使用自我保护模式,可以让Eureka集群更加健壮、稳定。
(3)踢出宕机节点
自我保护模式打开时,已关停节点是一直显示在Eureka首页的。
关闭自我保护模式后,由于其默认的心跳周期比较长等原因,要过一会儿才会发现已关停节点被自动踢出了。若想尽快踢出,就只能修改默认的心跳周期参数了。
注册中心的配置文件application.yml需要修改的地方如下所示:
eureka:
server:
enable-self-preservation: false # 关闭自我保护模式(默认为打开)
eviction-interval-timer-in-ms: 1000 # 续期时间,即扫描失效服务的间隔时间
(默认为60*1000ms)
客户端的配置文件application.yml需要修改的地方为:
eureka:
instance:
lease-renewal-interval-in-seconds: 5 # 心跳时间,即服务续约间隔时间(默认为30s)
lease-expiration-duration-in-seconds: 15 # 发呆时间,即服务续约到期时间(默认为90s)
client:
healthcheck:
enabled: true #开启健康检查(依赖spring-boot-starter-actuator)
更改Eureka Server的更新频率将打破注册中心的自我保护功能,不建议生产环境自定义这些配置。
(4)注册服务慢的问题
客户端去注册中心默认持续30s,直到实例自身、服务端、客户端各自元数据本地缓存同步完成后服务才可用(至少需要3次心跳周期)。可以通过eureka.instance.leaseRenewalIntervalInSeconds修改这个周期,改善客户端链接到服务的速度。不过,考虑短期的网络波动以及服务续期等情况,在生产环境最好用默认设定。
(5)服务状态显示UNKNOWN
如果在Eureka监控页面发现服务状态显示UNKNOWN,则很大可能是把微服务的eureka.client.healthcheck.enabled属性配置在bootstrap.yml里面的问题。比如,实际测试发现,Eureka首页显示的服务状态,本应是UP(1),却变成大红色的粗体UNKNOWN(1)。
(6)自定义InstanceId
两个相同的服务(端口不同),如果注册时设置的都是${spring.application.name},那么Eureka首页只会看到一个服务名字,而无法区分有几个实例注册上来了。于是,可以自定义生成InstanceId的规则。
Eureka服务名默认如下:
${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}}
可以在配置文件中通过eureka.intance.intance_id来自定义:
eureka:
instance:
#修改显示的微服务名为:IP:端口
instance-id: ${spring.cloud.client.ipAddress}:${server.port}
(7)自定义Eureka控制台服务的链接
既然微服务显示的名称允许修改,那么其对应的点击链接也是可以修改的。
同样,还是修改微服务的配置文件,如下所示:
eureka:
instance:
# ip-address: 192.168.6.66 #只有prefer-ip-address=true时才会生效
prefer-ip-address: true #设置微服务调用地址为IP优先(默认为false)
Eureka首页显示的微服务调用地址,默认是http://hostName:port/info。
而在设置prefer-ip-address=true之后,调用地址会变成http://ip:port/info。
这时若再设置ip-address=192.168.6.66,则调用地址会变成http://192.168.6.66:2100/info。
(8)健康度指示器
一个Eurake实例的状态页和健康指示器默认为/info和/health,这两个是由Spring Boot Actuator应用提供的访问端点。可以通过以下方式进行修改:
application.yml
eureka:
instance:
statusPageUrlPath: ${management.context-path}/info
healthCheckUrlPath: ${management.context-path}/health
这些地址会被用于Eureka对客户端元数据的获取,以及健康检测。
(9)就近原则
用户量比较大或者用户地理位置分布范围很广的项目,一般都会有多个机房。这个时候如果上线服务的话,我们希望一个机房内的服务优先调用同一个机房内的服务,当同一个机房的服务不可用时,再去调用其他机房的服务,以达到减少延时的作用。Eureka有Region和Zone的概念,可以理解为现实中的大区(Region)和机房(Zone)。Eureka Client在启动时需要指定Zone,它会优先请求自己Zone的Eureka Server获取注册列表。同样,Eureka Server在启动时也需要指定Zone。如果没有指定,其会默认使用defaultZone。
(10)高可用配置
Eureka Server也支持运行多实例,并以互相注册的方式(即伙伴机制)来实现高可用的部署,即每一台Eureka都在配置中指定另一个Eureka地址作为伙伴,它在启动时会向伙伴节点获取注册列表。如此一来,Eureka集群新加机器时,就不用担心注册列表的完整性。所以,只需要在Eureka Server里面配置其他可用的serviceUrl,就实现了注册中心的高可用。
我们新建两个Eureka服务端项。第1个服务端项为EurekaServer1项目中的配置文件
/src/main/resources/application.yml。
server:
port: 8989
eureka
serviceUrl:
defaultZone:
第2个服务端项为EurekaServer2项目中的配置文件/src/main/resources/application.yml。
server:
port: 9898
eureka
serviceUrl:
defaultZone:
启动Server1、Server2后,分别访问http://127.0.0.1:8989/eureka/、http://127.0.0.1:9898/eureka/ ,发现DS Replicas、General Info模块出现了对方的信息。读者可以自行测试,分别单独向Server1或者Server2进行服务注册时,都会自动同步给另外一个注册中心。
在生产环境中大于两台注册中心的条件下,可以同理将其配置成如图2-4所示的双向环形。
2.3 小结
本章详细讲解了Eureka组件的工作原理,并结合示例介绍了Eureka的服务提供方和服务调用方的使用步骤。在进阶场景章节中我们深入探究了各个场景下使用Eureka的各种解决方案与自定义场景的配置。下个章开始学习如何使用Config组件来对Eureka以及其他组件进行动态化配置。