提供服务的应用,可以是SpringBoot应用,也可以是其它任意技术实现,只要对外提供的是Rest风格服务即可。本例中就是我们实现的spring-provider。
- 服务消费者
消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。本例中
就是我们实现的spring-consumer。
有趣故事
Eureka的故事来源于人人追求真善美的古希腊,“Eureka”是希腊语,意思是“我发现了!”
这个有魔力的单词是来源于阿基米德。在公元前200多年,他在洗澡时发现了证明王冠是否纯金的方法(黄金密度),他激动地一边大喊“Eureka!”一边跳出澡盆奔去王宫,连衣服都忘了穿。后来人们用Eureka这个词来形容洞察浮现的瞬间。
SMS、库存、积分服务器,服务迁移变更等需要修改相应的URL地址,怎么不修改URL地址?
在微服务中,spring-provider对外提供服务,需要对外暴露自己的地址。而consumer(调用者)需要记录服务提供者的地址。将来地址出现变更,还需要及时更新。这在服务较少的时候并不觉得有什么,但是在现在日益复杂的互联网环境,一个项目肯定会拆分出十几,甚至数十个微服务。此时如果还人为管理地址,不仅开发困难,将来测试、发布上线都会非常麻烦
这就好比是 网约车出现以前,人们出门叫车只能叫出租车。一些私家车想做出租却没有资格,被称为黑车。而很多人想要约车,但是无奈出租车太少,不方便。私家车很多却不敢拦(没有人告诉哪些车私家车可以拉人),而且满大街的车,谁知道哪个才是愿意载人的。一个想要,一个愿意给,就是缺少引子,缺乏管理啊。
此时滴滴这样的网约车平台出现了,所有想载客的私家车全部到滴滴注册,记录你的车型(服务类型),身份信息(联系方式)。这样提供服务的私家车,在滴滴那里都能找到,一目了然。
此时要叫车的人,只需要打开APP,输入你的目的地,选择车型(服务类型),滴滴自动安排一个符合需求的车到你面前,为你服务,完美!
Eureka做什么?
Eureka就好比是滴滴中心,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。
同时,服务提供方与Eureka之间通过“心跳” 机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。
这就实现了服务的自动注册、发现、状态监控。
Eureka是啥?
Eureka简介
Spring Cloud Eureka 是对Netflix公司的Eureka的二次封装,它实现了服务治理的功能,Spring Cloud Eureka提供服务端与客户端,服务端即是Eureka服务注册中心,客户端完成微服务向Eureka服务的注册与发现。服务端和客户端均采用Java语言编写。
一个消费者和一个生产者
多个消费者与多个生产者
下图显示了Eureka Server与Eureka Client的关系
- Eureka Server是服务端,负责管理各个微服务结点的信息和状态。
- 在微服务上部署Eureka Client程序,远程访问Eureka Server将自己注册在Eureka Server。
- 微服务需要调用另一个微服务时从Eureka Server中获取服务调用地址,进行远程调用。
原理
- 服务提供方启动后将注册到注册中心,提供IP, 名字,什么服务等信息,
- 服务调用方作为客户端注册到注册中心后,拉取注册中心的服务列表,在通过负载均衡调用对应的服务提供方。
- 注册中心可以建立集群,生成多台eureka,注册中心为了监测各个服务的心跳,将在每30S 向所注册的服务发起请求判断
- 服务是否挂掉,如果挂掉90S后将会将服务从注册中心剔除。
- 一个服务可以监测多台服务实例,从而可实现均衡负载。
Eureka使用Hello案例
注册中心Eureka
引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
eureka的配置文件
server:
port: 9001 # eureka默认端口号为8761
eureka:
client:
# EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址
service-url:
defaultZone: http://127.0.0.1:${server.port}/eureka
# 不把自己注册到eureka服务列表
register-with-eureka: false
# 拉取eureka服务信息
fetch-registry: false #false表示自己就是注册中心,不需要从注册中心获取注册列表信息
instance:
#客户端在注册时使用自己的IP而不是主机名
prefer-ip-address: true
# 实例id
instance-id: ${spring.cloud.ip-address}:${spring.application.name}:${server.port}
logging:
level:
root: debug
配置说明:
register-with-eureka: false false表示不向注册中心注册自己
fetch-registry: false false表示自己就是注册中心,不需要从注册中心获取注册列表信息
service-url 设置eureka server交互的地址查询服务和注册服务都需要用到这个地址(单机用)
主启动类
在eureka-server的主启动类上开启eureka服务**@EnableEurekaServer**
package com.tianju.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer // 声明当前springboot应用是一个eureka服务中心
public class EurekaApp {
public static void main(String[] args) {
SpringApplication.run(EurekaApp.class);
}
}
浏览器访问
启动eureka-server子项目,在浏览器上访问localhost:9001
查看日志
搭建生产者Provider
引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 在provider的pom文件中添加监控依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
配置yml文件
server:
port: 10010
eureka:
client: #客户端注册到eureka列表中
service-url:
defaultZone: http://127.0.0.1:9001/eureka
instance:
prefer-ip-address: true #显示访问url 客户端在注册时使用自己的IP而不是主机名
# # 实例id
# instance-id: ${spring.cloud.ip-address}:${spring.application.name}:${server.port}
instance-id: provider-10010 #注册中心显示出来的微服务名称
# 应用名称
spring:
application:
name: springCloud-provider
info:
app.name: SpringCloud
company.name: tianju
build.artifactId: $project.artifactId$
build.version: $project.version$
主启动类
package com.tianju.provider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient // 或 @EnableDiscoveryClient
public class ProviderApp {
public static void main(String[] args) {
SpringApplication.run(ProviderApp.class);
}
}
@EnableEurekaClient和@EnableDiscoveryClient区别
在使用Spring Cloud feign使用中在使用服务发现的时候提到了两种注解:
一种为@EnableDiscoveryClient;
一种为@EnableEurekaClient,用法上基本一致
spring cloud中discovery service有许多种实现(eureka、consul、zookeeper等等),
@EnableDiscoveryClient基于spring-cloud-commons;
@EnableEurekaClient基于spring-cloud-netflix。
其实用更简单的话来说,就是如果选用的注册中心是eureka,那么就推荐@EnableEurekaClient, 如果是其他的注册中心,那么推荐使用@EnableDiscoveryClient。
注解@EnableEurekaClient上有@EnableDiscoveryClient注解,可以说基本就是EnableEurekaClient有@EnableDiscoveryClient的功能,另外上面的注释中提到,其实@EnableEurekaClient注解就是一种方便使用eureka的注解而已,可以说使用其他的注册中心后,都可以使用@EnableDiscoveryClient注解,但是使用@EnableEurekaClient的情景,就是在服务采用eureka作为注册中心的时候,使用场景较为单一。
查看日志
修改配置信息,变成了20s
lease-renewal-interval-in-seconds: 20 #心跳时间
server:
port: 10010
eureka:
client: #客户端注册到eureka列表中
service-url:
defaultZone: http://127.0.0.1:9001/eureka
instance:
prefer-ip-address: true #显示访问url 客户端在注册时使用自己的IP而不是主机名
# # 实例id
# instance-id: ${spring.cloud.ip-address}:${spring.application.name}:${server.port}
instance-id: provider-10010 #注册中心显示出来的微服务名称
lease-renewal-interval-in-seconds: 20 #心跳时间
lease-expiration-duration-in-seconds: 60 #下线时间
# 应用名称
spring:
application:
name: springCloud-provider
info:
app.name: SpringCloud
company.name: tianju
build.artifactId: $project.artifactId$
build.version: $project.version$
搭建消费者consumer
引入依赖配置等雷同
注册到eureka
主启动类
package com.tianju.consumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableDiscoveryClient
public class ConsumerApp {
public static void main(String[] args) {
SpringApplication.run(ConsumerApp.class);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
在生产端提供controller
在消费端调用生产者
package com.tianju.consumer.controller;
import com.netflix.appinfo.InstanceInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerController {
@Resource
private DiscoveryClient discoveryClient;
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/getId/{id}")
public String getMsg(@PathVariable("id") String id) {
List<ServiceInstance> instances = discoveryClient.getInstances("springCloud-provider");
ServiceInstance instance = instances.get(0);
String host = instance.getHost();
int port = instance.getPort();
log.debug("消费者拼出路径+端口:"+ host+":"+port);
// 获取ip和端口信息,拼接成服务地址
String baseUrl = "http://" + instance.getHost() + ":" +
instance.getPort() + "/provider/get/" + id;
String consumer = restTemplate.getForObject(baseUrl, String.class);
log.debug("采用restTemplate调用生产者:"+baseUrl);
return "消费者调用生产者获得消息:"+consumer;
}
}
心跳和续约策略
Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址
- 提供者:启动后向Eureka注册自己信息(地址,提供什么服务)
- 消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新
- 心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态
常见概念
Register 服务注册
当Eureka客户端向Eureka Server注册时,它提供自身的元数据,比如IP地址、端口,运行状况指示符URL,主页等。
Renew 服务续约(心跳机制)
Eureka客户会每隔30秒发送一次心跳来续约。通过续约来告知Eureka Server该Eureka客户仍然存在,没有出现问题。正常情况下,如果Eureka Server在90秒没有收到Eureka客户的续约,它会将实例从其注册表中删除。建议不要更改续约间隔。
- 心跳机制是每隔30秒发送一个自定义的结构体(心跳包),让对方知道自己还活着,以确保连接的有效性的机制。
- 心跳机制是每隔30秒发送一个固定信息给服务端,服务端收到后回复一个固定的信息。如果服务端90秒内没有收到客户端消息则视客户端断开。
- 发送方可以是客户端或服务端,根据实际情况,一般是客户端;因为一个服务端可能有很多客户端,服务端作为发送方的比较耗费性能。
Fetch Registries 获取注册列表信息
Eureka客户端从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每30秒钟)更新一次。每次返回注册列表信息可能与Eureka客户端的缓存信息不同, Eureka客户端自动处理。如果由于某种原因导致注册列表信息不能及时匹配,Eureka客户端则会重新获取整个注册表信息。 Eureka服务器缓存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同。Eureka客户端和Eureka 服务器可以使用JSON / XML格式进行通讯。在默认的情况下Eureka客户端使用压缩JSON格式来获取注册列表的信息。
服务的下线和剔除
Cancel 服务下线
Eureka客户端在程序关闭时向Eureka服务器发送取消请求,发送请求后,该客户端实例信息将从服务器的实例注册表中删除,该下线请求不会自动完成,它需要调用以下内容:
DiscoveryManager.getInstance().shutdownComponent();
服务进行正常关闭操作,会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求之后,将该服务置为下线状态。
Eviction 服务剔除
在默认的情况下,当Eureka客户端连续90秒没有向Eureka服务器发送服务续约(心跳),Eureka服务器会将该服务实例从服务注册列表删除,即服务剔除。
有些时候,我们的服务提供方并不一定会正常下线,可能因为内存溢出、网络故障等原因导致服务无法正常工作。Eureka Server需要将这样的服务剔除出服务列表。因此它会开启一个定时任务,每隔60秒对所有失效的服务(超过90秒未响应)进行剔除。
可以通过eureka.server.eviction-interval-timer-in-ms 参数对其进行修改,单位是毫秒。
eureka:
server:
# 每隔多久(ms)触发一次服务剔除
eviction-interval-timer-in-ms: 10000
自我保护
我们关停一个服务,就会在Eureka面板看到一条警告:
这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。
Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。
但是这给我们的开发带来了麻烦, 因此开发阶段我们都会关闭自我保护模式:(eureka-server)
eureka:
server:
enable-self-preservation: false # 如果为true,表示要保护实例,不被剔除,false关闭自我保护模式,剔除实例
eviction-interval-timer-in-ms: 10000 # 扫描失效服务的间隔时间(缺省为60\*1000ms)
如果保护实例不被剔除,而且配置了 eviction-interval-timer-in-ms: 10000,则eviction-intervaltimer-in-ms参数为准,实例还是会被剔除
eureka:
server:
enable-self-preservation: true # 如果为true,表示要保护实例,不被剔除,false关闭自我保护模式,剔除实例
#eviction-interval-timer-in-ms: 10000 # 扫描失效服务的间隔时间(缺省为60\*1000ms)
Eureka集群搭建
Eureka Server即服务的注册中心,在刚才的案例中,我们只有一个EurekaServer,事实上EurekaServer也可以是一个集群,形成高可用的Eureka中心。
多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。
因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。
允许多个实例
1.启动第一个
所谓的高可用注册中心,其实就是把EurekaServer自己也作为一个服务进行注册,这样多个EurekaServer之间就能互相发现对方,从而形成集群。因此我们做了以下修改:把service-url的值改成了另外一台EurekaServer的地址,而不是自己。
先启动一个
启动报错,很正常。因为另一个服务没有启动
2.再启动一个
启动第二个eurekaServer,再次修改eureka-server的配置