二、Eureka服务注册与发现
1. 概念
SpringCloud封装了Netflix公司开发的Eureka模块来实现服务注册与发现(类似于Dubbo的注册中心,如Zookeeper)。
服务的注册与发现对于微服务架构来说非常重要,有了服务发现与注册功能,只需要使用服务的标识符,就可以访问到服务,而不需要修改服务调用的配置文件了。
2. 基本架构
Eureka采用了C-S的设计架构,包含两个组件,分别是Eureka Server和Eureka Client。
Eureka Server:作为服务注册功能的服务器,它是服务注册中心。各节点启动后,会在Eureka Server中进行注册,这样Eureka Server的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可在界面中直观的看到。
Eureka Client:是一个Java客户端,用于简化与Eureka Server的交互,客户端同时也具备一个内置的、使用轮询负载算法(round-robin)的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内未接收到某个节点的心跳,Eureka Server将会从服务注册表中将这个服务移除(默认90秒)。
一般情况下,实现服务间的调用,需要搭建注册中心即Eureka Server,各个微服务作为Eureka Client,服务提供者连接Eureka Server注册服务并维持心跳连接,服务调用者从Eureka Server拉取服务注册表,完成目标服务的调用,示意图如下:
Eureka Server:提供服务注册与发现
Service Consumer:服务消费方从Eureka获取注册服务列表,从而能够消费服务
Service Provider:服务提供方将自身服务注册到Eureka,从而使服务消费方能够找到
3. 服务注册入门案例
3.1 搭建注册中心(Eureka Server)
3.1.1 新建工程spring-cloud-demo
1)首先创建一个Maven工程,名为spring-cloud-demo,如下图:
2)下一步确认工程信息,点击完成:
3.1.2 添加工程Maven依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.RC1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
这里注意SpringCloud的版本以地铁站名称命名,本案例中cloud版本选用的是Dalston.RC1,根据SpringCloud官网,与之匹配的boot版本选用的是1.5.2.RELEASE。
3.1.3 新建模块eureka-server-8888
点击工程名称spring-cloud-demo,右键新建一个maven module,名称为eureka-server-8888。该服务访问端口为8888,为了方便后面我们添加Eureka Server的服务集群,名称加上端口8888的后缀。
下一步确认模块信息,然后点击完成:
3.1.4 添加模块eureka-server-8888的Maven依赖
这个模块作为Eureka的服务端,需要引入spring-cloud-starter-eureka-server的依赖。
<dependencies>
<!--eureka server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
<!-- spring boot test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.1.5 添加配置文件application.yml
这个服务作为Eureka Server即注册中心,端口设置为8888,由于在本地测试,方便起见,eureka服务端的实例名称设置为localhost,访问时http://localhost:8888即可。
server:
port: 8888
eureka:
instance:
hostname: localhost #Eureka服务端的实例名称
client:
registerWithEureka: false #false表示不向注册中心注册自己
fetchRegistry: false #false表示本端就是注册中心,职责就是维护服务实例,并不需要去检索服务
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ #设置与Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址
3.1.6 添加启动类
@EnableEurekaServer注解,表示这是EurekaServer服务端启动类,可接受其他微服务注册进来。
@SpringBootApplication
@EnableEurekaServer //表示EurekaServer服务端启动类,接受其他微服务注册进来
public class EurekaServer8888App {
public static void main(String[] args) {
SpringApplication.run(EurekaServer8888App.class, args);
}
}
3.1.7 效果测试
运行启动类,浏览器输入http://localhost:8888,页面可以访问,且可以发现当前注册到Eureka中的服务列表为空。
接下来,将创建两个微服务,分别作为服务提供者与服务消费者,并将服务提供者注册到Eureka。详见3.2及3.3节。
3.2 创建会员服务(Service Provider)
在上一节的基础上,Eureka作为注册中心,已搭建完成,现在创建一个会员服务,作为服务提供者,注册到Eureka中。
3.2.1 新建模块service-member
同3.1.3节,这里不再赘述。
3.2.2 添加依赖
与Eureka Server不同的是,service-member模块作为其中的一个微服务(服务提供者),需要引入spring-cloud-starter-eureka,作为一个Eureka Client。
<dependencies>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.2.3 添加配置文件application.yml
设置该服务应用名称为service-member,访问端口为8001。
eureka:
client: #将客户端注册进Eureka服务列表内
serviceUrl:
defaultZone: http://localhost:8888/eureka/
server:
port: 8001
spring:
application:
name: service-member
3.2.4 添加启动类
@EnableEurekaClient注解,表示本服务启动后会自动注册到Eureka服务列表中。
@SpringBootApplication
@EnableEurekaClient //本服务启动后会自动注册到Eureka服务列表中
public class MemberApp {
public static void main(String[] args) {
SpringApplication.run(MemberApp.class, args);
}
}
3.2.5 添加一个接口,获取会员列表
这里暂时不调Service层接口,直接在Controller中返回固定数据。
@Controller
@RequestMapping("/member")
public class MemberController {
@Autowired
private DiscoveryClient client;
@RequestMapping("/list")
@ResponseBody
public Map<String, String> getAllMember() {
Map<String, String> members = new HashMap<String, String>(4);
members.put("zhangsan", "v3");
members.put("lisi", "v4");
members.put("wangwu", "v5");
members.put("zhaoliu", "v6");
return members;
}
}
3.2.6 效果测试
- 首先在浏览器输入:http://localhost:8001/member/list,可以发现service-member服务正常启动并能请求成功。
- 再查看Eureka Server,输入:http://localhost:8888,可以看到会员服务service-member已成功注册到Eureka中。
3.3 创建订单服务(Service Consumer)
前面已经成功搭建了Eureka作为注册中心,会员服务service-member作为服务提供者,现创建一个订单服务service-order作为服务消费者,远程调用会员服务service-member。
3.3.1 新建模块service-order
同3.1.3节,这里不再赘述。
3.3.2 添加依赖
service-order作为一个服务消费者,不需要向Eureka中注册,而是从Eureka Server中获取服务列表,service-member进行调用。故而就是一个简单的SpringBoot项目。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.3.3 添加配置文件application.yml
设置该服务应用名称为service-order,访问端口为8002
server:
port: 8002
3.3.4 添加启动类
@SpringBootApplication
public class OrderApp {
public static void main(String[] args) {
SpringApplication.run(OrderApp.class);
}
}
3.3.5 添加请求接口,获取会员列表
该接口内部实现为从订单服务service-order远程调用会员服务service-member,获取会员列表信息。
创建OrderController、OrderService、OrderServiceImpl,主要代码如下:
@Controller
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@RequestMapping("/getMemberList")
@ResponseBody
public Map<String, String> getMemberList() {
return orderService.getMemberList();
}
}
@Service
public class OrderServiceImpl implements OrderService {
private static final String REST_URL_PREFIX = "http://localhost:8001";
@Autowired
private RestTemplate restTemplate;
@Override
public Map<String, String> getMemberList() {
return restTemplate.getForObject(REST_URL_PREFIX + "/member/list", Map.class);
}
}
这里有几点说明:
① 远程调用:这里暂时利用org.springframework.web.client.RestTemplate
,一般项目中会用到Feign客户端,后面会详述;
② RestTemplate编译报错:需要进行实例化,因此在启动类OrderApp中添加一个注入bean的方法即可:
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
3.3.6 效果测试
分别启动以下微服务:
Eureka Server:eureka-server-8888
Service Provider:service-member
Service Consumer:service-order
从微服务service-order远程调用service-member服务:http://localhost:8002/order/getMemberList,可正确获取响应信息:
3.4 Eureka Server页面相关信息完善
3.4.1 主机名称修改
在Eureka的主页,发现service-member服务的Status显示的主机名称很长,且携带了电脑本机的名称,现在修改为service-member。
在service-member的yml文件添加instance-id,修改别名。
instance:
instance-id: service-member #自定义Eureka微服务管理页面的显示别名
修改完成后,基于Eureka的高可用特性,自动触发Eureka的自我保护模式(详见3.5节)。可以发现一句提示语,status中也会保留之前的信息及更新的信息。
重启Eureka Server服务,即可正常显示,如图:
3.4.2 主机ip信息提示修改
当光标放在服务上时,左下方显示的链接信息,现将其修改为ip:port形式。
在service-member的yml文件中,修改prefer-ip-address属性值为true。
prefer-ip-address: true #访问路径可以显示IP地址,默认为false 不显示
修改完成重新启动项目,查看效果:
3.4.3 微服务info内容详细信息配置
点击实例名称service-member,跳转到新页面http://10.100.7.155:8001/info,默认为空内容。此时可以配置一些项目相关信息,展示在页面上。
① service-member项目添加maven依赖spring-boot-actuator:
<!-- 监控信息完善 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
</dependency>
② service-member项目的配置文件application.yml添加info标签:
info:
app.name: charver-springclouddemo-servicemember
company.name: www.chavaer.com
contact.name: chavaer
contact.phone: 188-3288-3388
contace.email: chavaer@super.com.cn
③ 点击页面上的实例名称service-member,跳转到新页面http://10.100.7.155:8001/info,以上配置的info信息可在页面上以json格式展示:
3.5 自我保护机制
3.5.1 概念
默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当网络分区故障发生时,微服务与EurekaServer之间无法正常通信,以上行为就变得非常危险,因为微服务本身是健康的,此时本不该注销该实例。Eureka通过”自我保护模式“来解决这个问题。
在自我保护模式中,Eureka Server会保护服务注册表中的信息,不再注销任何实例。当它收到的心跳重新恢复到阈值以上时,该Eureka Server节点就会自动退出自我保护模式。
自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。使用自我保护模式,可以使Eureka集群更加的健壮、稳定。
3.5.2 配置
在SpringCloud中,可以使用eureka.server.enable-self-preservation = false
禁用自我保护模式。
3.5.3 效果
- 修改service-member服务名称,超过预定时间,未向Eureka Server发送心跳,Eureka不会注销原服务,而是两个都保留:
- 修改service-member内容,超过预定时间,未向Eureka Server发送心跳,也会触发Eureka自我保护机制,出现红色警示语:
4. 服务发现
对于注册进Eureka中的微服务,可以通过服务发现来获得该服务的信息。
为了清晰看到服务发现的效果,在service-member服务中添加一个接口,展示当前服务的一些信息。
4.1 service-member开启服务发现
在service-member的主启动类MemberApp 中通过@EnableDiscoveryClient
注解开启服务发现。
@SpringBootApplication
@EnableEurekaClient //本服务启动后会自动注册到Eureka服务列表中
@EnableDiscoveryClient //服务发现
public class MemberApp {
public static void main(String[] args) {
SpringApplication.run(MemberApp.class, args);
}
}
实际上,这里不需要@EnableDiscoveryClient
也可以,因为使用了Eureka作为服务注册中心,此时已开启服务注册与发现,从@EnableEurekaClient
的源码中就能看到已经加上了@EnableDiscoveryClient
注解。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@EnableDiscoveryClient
public @interface EnableEurekaClient {
}
4.2 service-member服务添加接口
在service-member项目MemberController中添加映射/discovery,当接收到该请求时,返回org.springframework.cloud.client.discovery.DiscoveryClient
对象。
@Controller
@RequestMapping("/member")
public class MemberController {
@Autowired
private DiscoveryClient client;
@RequestMapping("/list")
@ResponseBody
public Map<String, String> getAllMember() {
Map<String, String> members = new HashMap<>(4);
members.put("zhangsan", "v3");
members.put("lisi", "v4");
members.put("wangwu", "v5");
members.put("zhaoliu", "v6");
return members;
}
@RequestMapping("/discovery")
@ResponseBody
public Object discovery() {
System.out.println("Current client : " + client.description());
List<String> services = client.getServices();
System.out.println("Current service list : " + services);
for (String service : services) {
List<ServiceInstance> instances = client.getInstances(service);
for (ServiceInstance instance : instances) {
System.out.println(instance.getServiceId() + "\t" + instance.getHost() + "\t"
+ instance.getPort() + "\t" + instance.getUri());
}
}
return client;
}
}
4.3 service-order服务添加接口
在service-order中添加远程访问方法:
@Controller
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@RequestMapping("/getMemberList")
@ResponseBody
public Map<String, String> getMemberList() {
return orderService.getMemberList();
}
@RequestMapping("/memberDiscovery")
@ResponseBody
public Object memberDiscovery() {
return orderService.discovery();
}
}
@Service
public class OrderServiceImpl implements OrderService {
private static final String REST_URL_PREFIX = "http://localhost:8001";
@Autowired
private RestTemplate restTemplate;
@Override
public Map<String, String> getMemberList() {
return restTemplate.getForObject(REST_URL_PREFIX + "/member/list", Map.class);
}
@Override
public Object discovery() {
return restTemplate.getForObject(REST_URL_PREFIX + "/member/discovery", Object.class);
}
}
4.4 效果测试
请求http://10.100.7.155:8002/order/memberDiscovery,调用会员服务service-member,获取会员服务相关信息,如下图:
同时,在service-member的控制台打印信息,可以从DiscoveryClient对象中获取到注册到Eureka中的服务列表,并根据实例名获取对应实例的名称、主机、端口、访问Url等信息。
5. Eureka集群配置
5.1 创建Eureka Server
新建module:eureka-server-7777、eureka-server-9999,项目中的内容与已存在的项目eureka-server-8888一样,这三个服务作为一个集群。
5.2 修改host配置
由于目前只有一台机器,因此在本机配置3条ip与域名对应关系,ip都为本机ip即127.0.0.1,模拟集群配置。
找到本地文件:C:\Windows\System32\drivers\etc\hosts
添加3条映射关系:
127.0.0.1 eureka8888.com
127.0.0.1 eureka9999.com
127.0.0.1 eureka7777.com
5.3 修改yml配置
5.3.1 修改Eureka Service配置文件
分别修改三个eureka-server项目的yml文件,以eureka-server-8888项目为例:
hostname:配置为映射的域名
defaultZone:配置为集群中的另外两台服务器的访问url
eureka-server-8888配置:
server:
port: 8888
eureka:
instance:
# hostname: localhost #Eureka服务端的实例名称
hostname: eureka8888.com
client:
registerWithEureka: false #false表示不向注册中心注册自己
fetchRegistry: false #false表示本端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
serviceUrl:
# defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ #设置与Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址
defaultZone: http://eureka7777.com:7777/eureka/,http://eureka9999.com:9999/eureka/
eureka-server-7777配置:
server:
port: 7777
eureka:
instance:
hostname: eureka7777.com #Eureka服务端的实例名称
client:
registerWithEureka: false #false表示不向注册中心注册自己
fetchRegistry: false #false表示本端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
serviceUrl:
defaultZone: http://eureka8888.com:8888/eureka/,http://eureka9999.com:9999/eureka/
eureka-server-9999配置:
server:
port: 9999
eureka:
instance:
hostname: eureka9999.com #Eureka服务端的实例名称
client:
registerWithEureka: false #false表示不向注册中心注册自己
fetchRegistry: false #false表示本端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
serviceUrl:
defaultZone: http://eureka8888.com:8888/eureka/,http://eureka7777.com:7777/eureka/
5.3.2 修改Eureka Client配置文件
修改Eureka Client即service-member的application.yml文件:
defaultZone:配置为3台集群服务器域名
eureka:
client: #将客户端注册进Eureka服务列表内
serviceUrl:
defaultZone: http://eureka7777.com:7777/eureka/,http://eureka8888.com:8888/eureka/,http://eureka9999.com:9999/eureka/
instance:
instance-id: service-member #自定义Eureka微服务管理页面的显示别名
prefer-ip-address: true #访问路径可以显示IP地址,默认为false 不显示
server:
port: 8001
spring:
application:
name: service-member
info:
app.name: charver-springclouddemo-servicemember
company.name: www.chavaer.com
contact.name: chavaer
contact.phone: 188-3288-3388
contace.email: chavaer@super.com.cn
5.4 效果测试
- 分别启动以下服务:
Eureka Server:eureka-server-7777、eureka-server-8888、eureka-server-9999
Eureka Client:service-member - 分别访问
http://eureka7777.com:7777/、http://eureka8888.com:8888/、http://eureka9999.com:9999/,可以发现集群搭建成功,且3台服务器Eureka中都成功注册了服务service-member。
6. Eureka与Zookeeper
6.1 CAP原则
Consistency 一致性
Availability 可用性
Partition tolerance 分区容错性
CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性、可用性和分区容错性这三个需求。因此,根据CAP原理,系统设计一般满足CA原则、CP原则、AP原则三大类,但分布式系统必须满足P即分区容错性。
CA原则:单点集群,满足一致性、可用性的系统,通常在可扩展性上不太强大;
CP原则:满足一致性、分区容忍性的系统,通常性能不是特别高;
AP原则:满足可用性、分区容错性的系统,通常可能对一致性要求低一些。
6.2 Eureka与Zookeeper对比
- Zookeeper设计遵循AP原则
服务注册功能强于一致性。但是zk会出现一种情况:当master节点因网络故障和其他节点失去联系时,剩余节点会重新进行leader选举,问题在于,选择leader时间太长,而且选举期间整个zk集群是不可用的,这就导致在选举期间注册服务瘫痪。尤其在云部署环境下,由于网络问题使得zk集群失去master节点是较大概率发生的事情。 - Eureka设计遵循CP原则
Eureka为了避免zk的缺陷,在设计时优先保证可用性。Eureka中每个节点都是平等的,任一节点故障,都不会影响正常节点的工作,剩余可用节点依然可以提供注册和查询服务。
Eureka的自我保护机制强调,如果在15min内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:
① Eureka不再从注册服务表中移除因长时间无心跳而应过期的服务;
② Eureka仍然可以接收新服务的注册与查询,但不会被同步到其他节点上(保证当前节点可用);
③ 当网络稳定时,当前实例新的注册信息会被同步到其他节点。
因此,Eureka可以很好的应对网络故障导致部分节点失去联系的情况,而不会像Zookeeper那样使整个注册服务瘫痪。
【上一篇】一、初识SpringCloud
【下一篇】三、Ribbon负载均衡