通过前面的讲解,我们知道硬编码提供者地址的方式有不少问题。要想解决这些问题,服务消费者需要一个强大的服务发现机制,服务消费者使用这种机制获取服务提供者的网络信息。不仅如此,即使服务提供者的信息发生变化,服务消费者也无需修改配置文件。
服务发现组件提供这种能力。在微服务架构中,服务发现组件是一个非常关键的组件。
使用服务发现组件后的架构图,如下:
服务提供者、服务消费者、服务发现组件这三者之间的关系大致如下;
综上,服务发现组件应具备以下功能:
综上,使用服务发现的好处是显而易见的。SpringCloud提供了多种服务发现组件的支持,例如Eureka、Consul和ZooKeeper等。
Eureka是Netflix开源的服务发现组件,本身是一个基于REST的服务。它包含Server和Client两部分。SpringCloud将它集成在子项目Spring Cloud Netflix中,从而实现微服务的注册与发现。
- Eureka的GitHub:https://github.com/Netflix/Eureka。
- Netflix是一家在线影片租赁提供商。
在分析Eureka的原理之前,先来了解一下Region和Availability Zone,如下图所示:
Region和Availability Zone均是AWS的概念。其中,Region表示AWS中的地理位置,每个Region都有多个Availability Zone,各个Region之间完全隔离。AWS通过这种方式实现了最大的容错和稳定性。
SpringCloud默认使用的Region是us-east-1,在非AWS环境下,可以将Availability Zone理解成机房,将Region理解为跨机房的Eureka集群。
下面分析一下Eureka的原理,Eureka架构如下图所示。
由上图可知,Eureka包含两个组件:Eureka Server和Eureka Client,它们的作用如下:
综上,Eureka通过心跳检查、客户端缓存等机制,提高了系统的灵活性、可伸缩性和可用性。
本节来编写一个Eureka Server。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-eureka-serverartifactId>
dependency>
package com.itmuch.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
server:
port: 8761
eureka:
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://localhost:8761/eureka/
简要讲解一下application.yml中的赔指数型:
这样一个Eureka Server就编写完成了。
测试
启动Eureka Server,访问http://localhost:8761/,可看到如下图所示的界面。
由图可知,Eureka Server的首页展示了很多信息,例如当前实例的系统状态、注册到Eureka Server上的服务实例、常用信息、实例信息等。显然,当前还没有任何微服务实例被注册到Eureka Server上。
本节将之前编写的用户微服务注册到Eureka Server上。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-eureka-serverartifactId>
dependency>
spring:
application:
name: microservice-provider-user
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
其中,spring.application.name用于指定注册到Eureka Server上的应用名称;eureka.instance.prefer-ip-address = true表示将自己的IP注册到Eureka Server。如不配置该属性或将其设置为false,则表示注册微服务所在操作系统的hostname到Eureka Server。
package com.itmuch.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class ProviderUserApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderUserApplication.class, args);
}
}
也可以使用@EnableEurekaClient注解替代@EnableDiscoveryClient。在SpringCloud中,服务发现组件有多种选择,例如ZooKeeper、Consul等。@EnableDiscoveryClient为各种服务组件提供了支持,该注解是spring-cloud-commons项目的注解,是一个高度的抽象;而@EnableEurekaClient表明是Eureka的Client,该注解是spring-cloud-netfix项目中的注解,只能与Eureka一起工作。当Eureka在项目的classpath中时,两个注解没有区别。
这样就可以将用户微服务注册到Eureka Server上了。同理,将电影微服务也注册到Eureka Server上,配置电影微服务的spring.application.name为microservice-consumer-movie。
相关文件如下:
MovieController.java
package com.itmuch.cloud.study.user.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import com.itmuch.cloud.study.user.entity.User;
@RestControllerpublic class MovieController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/user/{id}")
public User findById(@PathVariable Long id) {
return this.restTemplate.getForObject("http://localhost:8000/" + id, User.class);
}
}
User.java
package com.itmuch.cloud.study.user.entity;
import java.math.BigDecimal;
public class User {
private Long id;
private String username;
private String name;
private Integer age;
private BigDecimal balance;
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return this.age;
}
public void setAge(Integer age) {
this.age = age;
}
public BigDecimal getBalance() {
return this.balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
}
ConsumerMovieApplication.java
package com.itmuch.cloud.study;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerMovieApplication {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerMovieApplication.class, args);
}
}
application.yml
server:
port: 8010
spring:
application:
name: microservice-consumer-movie
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.itmuch.cloudgroupId>
<artifactId>microservice-consumer-movieartifactId>
<version>0.0.1-SNAPSHOTversion>
<packaging>jarpackaging>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>1.4.3.RELEASEversion>
parent>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-eurekaartifactId>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>Camden.SR4version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
测试
由图可知,此时用户微服务、电影微服务已经被注册到Eureka Server上了。
有分布式应用开发经验的读者应该能够看出,前文编写的单节点的Eureka Server并不适合线上生产环境。Eureka Client会定时连接Eureka Server,获取服务注册表中的信息并缓存到本地。微服务在消费远程API时总是使用本地缓存中的数据。因此一般来说,即使Eureka Server发生宕机,也不会影响到服务之间的调用。但如果Eureka Server宕机时,某些微服务也出现了不可用的情况,Eureka Client中的缓存若不被更新,就可能会影响到微服务的调用,甚至影响到整个应用系统的高可用性。因此,在生产环境中,通常会部署一个高可用的Eureka Server集群。
Eureka Server可以通过运行多个实例并相互注册的方式实现高可用部署,Eureka Server实例会彼此增量地同步信息,从而确保所有节点数据一致。事实上,节点之间相互注册是Eureka Server的默认行为,还记得前文编写单节点Eureka Server时,额外配置了eureka.client.registerWithEureka=false、eureka.client.fetchRegistry=false吗?
本节在前文的基础上,构建一个双节点Eureka Server集群。
复制项目microservice-discovery-eureka,将ArtifactId修改为microservice-discovery-eureka-ha。
配置系统的hosts,Windows系统的hosts文件路径是C:\Windows\System32\drivers\etc\hosts;Linux及Mac OS等系统的文件路径是/etc/hosts。
127.0.0.1 peer1 peer2
将application.yml修改如下:让两个节点的Eureka Server相互注册。
spring:
application:
name: microservice-discovery-eureka-ha
---
spring:
# 指定profile=peer1
profiles: peer1
server:
port: 8761
eureka:
instance:
# 指定当profile=peer1时,主机名是peer1
hostname: peer1
client:
serviceUrl:
#将自己注册到peer2这个Eureka上面去
defaultZone: http://peer2:8762/eureka/
---
spring:
profiles: peer2
server:
port: 8762
eureka:
instance:
hostname: peer2
client:
serviceUrl:
defaultZone: http://peer1:8761/eureka/
注意:配置application.yml时,各节点的缩进不要使用tab,而应使用空格,否则可能解析配置信息失败
如上,使用连字符(—)将该application.yml文件分为三段。第二段和第三段分别为spring.profiles指定了一个值,该值表示它所在的那段内容应用在哪个Profile里。第一段由于并未指定spring.profiles,因此这段内容会对所有Profile生效。
经过以上分析,不难理解,我们定义了peer1和peer2这两个Profile。当应用以peer1这个Profile启动时,配置该Eureka Server的主机名为peer1,并将其注册到http://peer2:8762/eureka/;反之,当应用以profile=peer2时,Eureka Server会注册到peer1节点的Eureka Server。
测试
打包项目,并使用以下命令启动两个Eureka Server节点。
java -jar microservice-discovery-eureka-ha-1.0-SNAPSHOT.jar --spring.profiles.active=peer1
java -jar microservice-discovery-eureka-ha-1.0-SNAPSHOT.jar --spring.profiles.active=peer2
通过spring.profiles.active指定使用哪个profile启动。
访问http://peer1:8761/,会发现“registered-replicas”中已有peer2节点;同理,访问http://peer2:8762/,也能发现其中的“registered-replicas”有peer1节点。
将应用注册到Eureka Server集群上
以microservice-provider-user项目为例,只须修改eureka.client.serviceUrl.defaultZone,配置多个Eureka Server地址,就可以将其注册到Eureka Server集群了。示例:
eureka:
client:
serviceUrl:
defaultZone: http://peer1:8761/eureka/,http://peer2:8762/eureka/
这样就可以将服务注册到Eureka Server集群上了。
当然,微服务即使只配置Eureka Server集群中的某个节点,也能正常注册到Eureka Server集群,因为多个Eureka Server之间的数据会相互同步。例如:
eureka:
client:
serviceUrl:
defaultZone: http://peer1:8761/eureka/
正常情况下,这种方式与配置多个Server节点的效果是一样的。不过为适应某些极端场景,建议在客户端配置多个Eureka Server节点。
启动microservice-provider-user项目,可以看到其已注册到Eureka Server集群中了。
在前面的示例中,Eureka Server是允许匿名访问的,本节来构建一个需要登录才能访问的Eureka Server。
复制项目microservice-discovery-eureka,将ArtifactId修改为microservice-discovery-eureka-authenticating。
在pom.xml中添加spring-boot-starter-security的依赖,该依赖为Eureka Server提供用户认证的能力。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
在application.yml中添加以下内容:
security:
basic:
#开启基于HTTP basic的认证
enabled: true
user:
#配置登录账号
name: user
#配置登录密码
password: password123
这样就为Eureka Server添加了基于HTTP basic的认证。如果不设置这段内容,账号默认是user,密码是一个随机值,该值会在启动时打印出来。
测试
启动microservice-discovery-eureka-authenticating。
访问http://localhost:8761/,需要身份验证的对话框,如下图所示。
输入账号密码,就可登录并访问Eureka Server。
将微服务注册到需认证的Eureka Server
如何才能将微服务注册到需认证的Eureka Server上呢?答案非常简单,只须将eureka.client.serviceUrl.defaultZone配置为http://user:password@EUREKA_HOST:EUREKA_PORT/eureka/ 这种形式,就可以将HTTP basic认证添加到Eureka Client了。因此,只须稍作修改,就可以将应用注册到本例的Eureka Server了:
eureka:
client:
serviceUrl:
#defaultZone: http://peer1:8761/eureka/,http://peer2:8762/eureka/
defaultZone: http://user:password123@localhost:8761/eureka/
对于更复杂的需求,可创建一个类型为DiscoveryClientOptionalArgs的@Bean,并向其中注入ClientFilter。