服务提供者
我们新建一个项目,对外提供查询用户的服务:
Spring脚手架创建工程
先创建父工程
配置依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
<relativePath/>
</parent>
<modules>
<module>user_service_demo</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.SR1</spring-cloud.version>
<mapper.starter.version>2.0.3</mapper.starter.version>
<mysql.version>5.1.32</mysql.version>
</properties>
<dependencyManagement>
<dependencies>
<!--SpringCloud的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
</dependency>
<!--通用Mapper启动器-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>${mapper.starter.version}}</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
创建服务提供方user_service
在父工程里创建一个Maven 的Module;
创建好之后
注入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper--spring-boot-starter</artifactId>
</dependency>
</dependencies>
编写程序
添加一个对外查询的接口
UserController.java
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id){
return userService.queryById(id);
}
}
UserService.java
@Service
public class UserService {
@Autowired
UserMapper userMapper;
public User queryById(Long id){
return userMapper.selectByPrimaryKey(id);
}
}
mapper
@Table(name = "tb_user")
public interface UserMapper extends Mapper<User> {
}
实体类:
@Data
//如果类的名字和数据表名字不一样,则通过@Table来指定
@Table(name = "tb_user")
public class User implements Serializable {
@Id
@KeySql(useGeneratedKeys = true)
private Long id;
private String userName;
private String password;
private String name;
private Integer age;
private Integer sex;
private Date birthday;
private String note;
private Date created;
private Date updated;
}
属性文件:
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/tb
username: root
password: westos
hikari:
maximum-pool-size: 20
minimum-idle: 10
mybatis:
type-aliases-package: com.example.pojo
启动并访问:
服务调用者
同样的创建一个子项目,并添加web启动器依赖:
编写代码
@Data
public class User implements Serializable {
private Long id;
private String userName;
private String password;
private String name;
private Integer age;
private Integer sex;
private Date birthday;
private String note;
private Date created;
private Date updated;
}
@Component
public class UserDao {
@Autowired
private RestTemplate restTemplate;
public User queryUserById(Long id){
String url="http://localhost:8081/user/"+id;
return restTemplate.getForObject(url,User.class);
}
}
@Service
public class UserService {
@Autowired
private UserDao userDao;
public List<User> queryByIds(List<Long> ids){
ArrayList<User> users = new ArrayList<>();
for (Long id : ids) {
users.add(userDao.queryUserById(id));
}
return users;
}
}
@RestController
@RequestMapping("/consumer")
public class ConsumerController {
@Autowired
private UserService userService;
@GetMapping
public List<User> consume(@RequestParam("ids") List<Long> ids){
return userService.queryByIds(ids);
}
}
访问客户端路径:
访问流程
存在的问题:
- 在consumer中,我们把url地址硬编码到了代码中,不方便后期维护;
- consumer需要记忆user-service的地址,如果出现变更,可能得不到通知,地址将失效;
- consumer不清除user-service的状态,服务宕机也不知道;
- user-service只有一台服务,不具备高可用性;
- 几遍user-service形成集群,consumer还需要自己实现负载均衡;
其实这些就是分布式服务必然要面临的问题:
-
服务管理:
- 如何实现自动注册和发现;
- 如何实现状态监管;
- 如何实现动态路由;
-
服务如何实现负载均衡;
-
服务如何解决容灾问题;
-
服务如何实现统一配置;
Eureka注册中心
认识Euraka
Eureka就好比滴滴,负责管理、记录服务提供者的信息,服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。
同时,服务提供方与Eureka之间通过“心跳“机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。这就实现了服务自动注册、发现、状态监控。
如果Eureka以集群模式部署,当集群中有分片出现故障时,那么Eureka就会转入自我保护模式,它允许在分片故障期间继续提供服务的发现和注册,当故障分片恢复运行时,集群中的其它分片会把它们的状态再次同步回来。不同可用区域的服务注册中心通过异步模式互相复制各自的状态,这意味着在任意给定时间点每个实例关于所有服务的状态有细微差别。
原理图
- Eureka:就是服务注册中心,可以是一个集群,对外暴露自己的地址;
- 提供者:启动后向Eureka注册自己的信息(地址、提供什么服务);
- 消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发给消费者,并且定期更新;
- 心跳:提供者定期通过http方式向Eureka刷新自己的状态;
Eureka客户端,主要处理服务的注册于发现,客户端服务通过注解参数配置的方式,嵌入在客户端应用程序的代码中,在应用程序运行时,Eureka客户端向注册中心注册自身提供的服务并周期性地发送心跳来更新它的服务租约,同时,它也能从服务端查询当前注册的服务信息并把它们缓存到本地并周期性地刷新服务状态。
添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-eureka-server</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
配置属性文件:
application.yml
server:
port: 10086 #指定访问端口
spring:
application:
name: EUREKA_SERVER #应用名称,会在Eureka中显示
eureka:
client:
register-with-eureka: true #是否注册自己的信息到EurekaServer,默认是true
fetch-registry: true # 是都拉取其他服务信息默认是true
service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其他server地址。
defaultZone: http://127.0.0.1:${server.port}/eureka
启动服务并访问:http://127.0.0.1:10086/
从日志中可以看到Eureka注册了自己的信息
配置依赖中出现的问题
做个标记纪念一下这个让我配了一天的依赖(微笑),开始连依赖包都下载不下来,在网上找的方法能试的都试了,最后连怎么好的都不知道。怎么说,最大的问题还是版本问题
防止以后出现这种问题: SpringCloud是Finchley.SR1版本,Eureka版本是2.0.1.RELEASE
链接:https://pan.baidu.com/s/1nW6f7ruMC7B1tXMpzF_wmw
提取码:12nw
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.cloud.netflix.eureka.server.EurekaServerInitializerConfiguration': Unsatisfied dependency expressed through field 'eurekaServerBootstrap'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'eurekaServerBootstrap' defined in class path resource [org/springframework/cloud/netflix/eureka/server/EurekaServerAutoConfiguration.class]: Unsatisfied dependency expressed through method 'eurekaServerBootstrap' parameter 1; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'eurekaServerContext' defined in class path resource [org/springframework/cloud/netflix/eureka/server/EurekaServerAutoConfiguration.class]: Unsatisfied dependency expressed through method 'eurekaServerContext' parameter 2; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'peerEurekaNodes' defined in class path resource [org/springframework/cloud/netflix/eureka/server/EurekaServerAutoConfiguration.class]: Post-processing of merged bean definition failed; nested exception is java.lang.IllegalStateException: Failed to introspect Class [org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration$RefreshablePeerEurekaNodes] from ClassLoader [sun.misc.Launcher$AppClassLoader@18b4aac2]
还是版本问题,添加如下依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
- Eureka服务启动之后访问不到页面
不知道为什么,我定义的访问地址是:http://127.0.0.1:10086/eureka/但是访问的时候总是报404,最后直接访问http://127.0.0.1:10086然后成功了。网上还有解决方法是给配置属性文件中添加spring.freemarker.prefer-file-system-access: false
,不过我试了还是没用。
将user-service注册到Eureka
注册服务,就是在服务上添加Eureka的客户端依赖,客户端代码会自动把服务注册到EurekaServer中。
在user-service-demo中添加Eureka客户端依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
在启动类上开启Eureka客户端功能
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan(basePackages = "com.example.mapper")
public class ServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceApplication.class,args);
}
}
编写配置
server:
port: 8081
spring:
application: #指定应用程序名字,到时候会显示在Eureka页面上
name: USER_SERVER
datasource:
url: jdbc:mysql://localhost:3306/tb
username: root
password: westos
hikari:
maximum-pool-size: 20
minimum-idle: 10
mybatis:
type-aliases-package: com.example.pojo
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance:
prefer-ip-address: true #调用getHostname获取实例的hostname时返回ip而不是host名称
ip-address: 127.0.0.1 #指定自己的ip信息,如果不指定会自动寻找浪费了性能
重启user-service-demo,并观察Eureka页面变化
启动过程中,从日志中可以看到服务提供端已经注册成功。
并且在Eureka页面的实例列表中也看到了
消费者从Eureka获取服务
接下来我们来修改consumer-demo,尝试从EurekaServer获取服务,方法与生产者类似;
添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<!--<version>2.0.1.RELEASE</version>-->
</dependency>
启动Eureka客户端
@SpringBootApplication
@EnableDiscoveryClient
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
修改配置
server:
port: 8080
spring:
application:
name: USER_CONSUMER
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance:
prefer-ip-address: true #调用getHostname获取实例的hostname时返回ip而不是host名称
ip-address: 127.0.0.1 #指定自己的ip信息,如果不指定会自动寻找浪费了性能
修改业务逻辑代码
@Service
public class ConsumerService {
@Autowired
private RestTemplate restTemplate;
//Eureka客户端,可以获取到服务实例信息
@Autowired
private DiscoveryClient discoveryClient;
public List<User> queryUsersByIds(List<Long> ids){
ArrayList<User> users = new ArrayList<>();
//根据服务名称获取服务实例
List<ServiceInstance> instances = discoveryClient.getInstances("USER_SERVER");
//因为只有一个Userserive所以直接获取第0个
ServiceInstance instanceInfo = instances.get(0);
//获取端口和id
String baseUrl="http://"+instanceInfo.getHost()+":"+instanceInfo.getPort()+"/user/";
ids.forEach(id->{
users.add(restTemplate.getForObject(baseUrl+id,User.class));
try {
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
});
return users;
}
}
运行服务消费者
看到注册成功的日志。
访问http://127.0.0.1/consumer?id=1,2得到结果
Eureka详解
基础架构
三个核心角色:
- 服务注册中心
Eureka的服务端应用,提供服务注册和发现功能,就是上述案例中的eureka_server_demo; - 服务提供者
提供服务的应用,可以使SpringBoot应用,也可以是其他任意技术实心,只要对外提供的是Rest风格服务即可,也就是案例中的user_server_demo; - 服务消费者
消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去那里调用服务方,本例中的user
_consumer_demo;
高可用的Eureka server
实际上EurekaServer也可以是一个集群,形成高可用的Eureka中心。
服务同步
多个Eureka Server之间也会互相注册服务,当服务提供者注册到EurekaServer集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。所以,无论Eureka客户端访问到的是哪个结点,都可以获取完整的服务列表信息。
搭建两个EurekaServer集群
直接复制上述案例中的Eureka配置,创建一个新的Eureka实例:
修改两个实例的配置,让他们互相注册
EurekaApplication1:
server:
port: 10087
spring:
application:
name: EUREKA_SERVER
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka/
EurekaApplication2:
server:
port: 10086
spring:
application:
name: EUREKA_SERVER
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10087/eureka/
所谓高可用注册中心,其实就是把EurekaServer自己也作为一个服务进行注册,这样多个EurekaServer之间就能互相发现对方,从而形成集群。因此做了下面的修改:
- 删除了
register-with-eureka=false
和fetch-registry=false
两个配置。因为默认值是true,这样就会把自己注册到注册中心了。
启动测试
可以看到出现了两个实例
客户端注册服务到集群
因为EurekaServer不止一个,因此注册服务的时候,service_url参数需要变化:
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka/,http://127.0.0.1:10086/eureka/
服务提供者
服务提供者要向EurekaServer注册服务,并且完成服务续约等工作;
服务注册
服务提供者在启动时,会检测配置属性中的:eureka.client.register-with-erueka=true
参数,如果为true(默认),则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,EurekaServer会把这些信息保存到一个双层Map结构中,第一层Map的Key就是服务名称,第二层Map的Key是服务的实例id。
服务续约
在注册服务完成后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:我还活着,这个过程称为服务续约。
参数配置:
eureka:
instance:
lease-expiration-duration-in-seconds: 90 #服务失效时间,默认90秒
lease-renewal-interval-in-seconds: 30 #服务续约间隔 默认30秒
也就是说默认情况下每隔30秒服务就会向注册中心发送一次心跳,证明自己还活着,如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务里表中移除,这两个值在生产环境中不要修改。
实例id
在status一列中显示以下信息:
- UP(1) 表示现在是启动了一个实例,没有集群;
- DESKTOP-2MVEC12:user-service:8081:是实例的名称(instence-id)
- 默认格式是:
${hostname} + ${spring.application.name} + ${server.port}
- instance-id是区分同一服务的不同实例的唯一标准,因此不能重复;
- 默认格式是:
可以通过:
eureka:
instance:
instance-id: ${spring.application.name}:${server.port}
服务消费者
获取服务列表
当消费者服务启动时,会检测eureka.client.fetch-registry=true
参数的值,如果为true则会从EurekaServer服务的列表只读备份,然后缓存在本地,并且每隔30秒会重新获取并更新数据。
eureka:
client:
registry-fetch-interval-seconds: 5
失效剔除和自我保护
失效剔除
有些时候,我们的服务提供方并不一定会正常下线,可能因为内存溢出、网络故障等原因导致服务无法正常运行。EurekaServer需要讲这样的服务剔除出服务列表。因此它会开启一个定时任务,每隔60秒对所有失效的服务(超过90秒未续约的)进行剔除。
可以通过eureka.server.eviction-interval-timer-in-ms
参数对其进行修改,时间单位为ms。
自我保护
我们关停一个服务,就会在Eureka面板看到一条警告:
这时触发了Eureka的自我保护机制,当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比重时都超过85%,在生产环境中,因为网咯延迟等原因,心跳失败实例的比例很可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不删除。
eureka:
server:
enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(缺省为60*1000ms)
负载均衡 Ribbon
实际情况中,我们往往会开启很多个user-service的集群,此时我们获取的服务列表中就会有很多个,到底该访问哪个呢?
一般这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。不过Eureka中已经帮我们集成了负载均衡组件:Ribbion.
什么是Ribbon?
Ribbon是Netflix发布的负载均衡器,它有助于控制HTTP和TCP客户端的行为。为Ribbon配置服务提供者地址列表后,Ribbon就可基于某种负载均衡算法,自动地帮助服务消费者去请求。Ribbon默认为我们提供了很多负载均衡算法。例如轮询、随机等。我们也可以为Ribbon自定义负载均衡算法。
启动两个服务实例
添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<!--<version>2.0.1.RELEASE</version>-->
</dependency>
开启负载均衡
- 在RestTemplate的配置方法上添加
@LoadBalance
注解:
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
@Service
public class ConsumerService {
@Autowired
private RestTemplate restTemplate;
public List<User> queryUsersByIds(List<Long> ids){
ArrayList<User> users = new ArrayList<>();
//根据服务名称获取服务实例
String baseUrl="http://user-server/user/";
ids.forEach(id->{
users.add(restTemplate.getForObject(baseUrl+id,User.class));
try {
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
});
return users;
}
}
注意
将服务名全改为小写,不然RestTemplate匹配不到路径。
源码跟踪
为什么我们只输入了服务名就可以访问了,之前还要获取ip和端口。显然有人帮我们根据service名称获取到了服务实例的ip和端口,它就是LoadBalancerIntercepter
拦截后处理url的方法
继续跟进execute方法,发现获取了8081端口服务:
负载均衡策略
Ribbon默认的负载均衡是简单的轮询,可以测试一下:
在源码中看到拦截中是使用RibbonLoadBalancerClient来进行负载均衡的,有一个choose方法:
配置负载均衡算法
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
格式是:{服务名称}.ribbon.NFLoadBalancerRuleClassName
,值就是IRule的实现类;
重试机制
Eureka的服务治理强调了CAP原则中的AP,即可用性和可靠性。它与Zookeeper这类强调CP(一致性、可靠性)的服务治理框架最大的区别在于:Eureka为了实现更高的服务可用性,牺牲了一定的一致性,极端情况下它宁愿接收故障实例也不原丢掉健康实例。正如上面说的自我保护机制。
但是,此时如果我们调用了这些不正常的服务,调用就会失败,从而导致其他服务不能正常工作,这显然是我们不愿意看到的。
当我们关闭一个user-service时,服务调用者再次访问会报错
因为服务剔除的延迟,consumer并不会立即得到最新的服务列表,此时再次访问会报错。
但是此时8082服务是正常的,因此Spring Cloud整合了Spring Retry来增强RestTemplate的重试能力,当一次服务调用失败之后,不会立即抛出一次,而是再次重试另一个服务。
配置Ribbon的重试
spring:
cloud:
loadbalancer:
retry:
enabled: true # 开启Spring Cloud的重试功能
user-service:
ribbon:
ConnectTimeout: 250 # Ribbon的连接超时时间
ReadTimeout: 1000 # Ribbon的数据读取超时时间
OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
MaxAutoRetries: 1 # 对当前实例的重试次数
添加依赖
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
根据如上配置,当访问到某个服务超时之后,它会再次尝试访问下一个服务实例,如果不行再换下一个,如果不行则返回失败。切换次数取决于MaxAutoRetriesNextServer
的值。