Eureka:
1. 注册中心
注册中心与生产者,消费者之间的原理图:
-
Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址
-
提供者:启动后向Eureka注册自己信息(地址,提供什么服务)
-
消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新
-
心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态
2. 服务端:
2.1 依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.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>
<spring-cloud.version>Finchley.SR2</spring-cloud.version>
</properties>
<!-- SpringCloud依赖管理 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
2.2配置文件:
server:
port: 10087 # 端口
spring:
application:
name: eureka-server # 应用名称,会在Eureka中显示
eureka:
client:
service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
defaultZone: http://127.0.0.1:${server.port}/eureka
# 不注册自己
# register-with-eureka: false
# 不拉取服务
# fetch-registry: false
(1)eureka.client.registerWithEureka :表示是否将自己注册到Eureka Server,默认为true。由于当前这个应用就是Eureka Server,故而设为false。
(2)eureka.client.fetchRegistry :表示是否从Eureka Server获取注册信息,默认为true。因为这是一个单点的Eureka Server,不需要同步其他的Eureka Server节点的数据,故而设为false。
2.3 修改引导类,在类上添加@EnableEurekaServer注解:
@SpringBootApplication
@EnableEurekaServer // 声明当前springboot应用是一个eureka服务中心
public class YhEurekaApplication {
public static void main(String[] args) {
SpringApplication.run(YhEurekaApplication.class, args);
}
}
3.注册到Eureka:
概念:注册服务,就是在服务上添加Eureka的客户端依赖,客户端代码会自动把服务注册到EurekaServer中。
其中还有一个重要的知识点,暂时先不讲:RestTemplate
流程:
-
在pom.xml中,添加springcloud的相关依赖。
-
在application.yml中,添加springcloud的相关依赖。
-
在引导类上添加注解,把服务注入到eureka注册中心。
3.1 依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.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>Finchley.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<-- 上面是springboot的基本依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
3.2 配置文件
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/lxw
username: root
password: root
driverClassName: com.mysql.jdbc.Driver
application:
name: service-provider # 应用名称,注册到eureka后的服务名称
mybatis:
type-aliases-package: cn.yh.service.pojo
eureka:
client:
service-url: # EurekaServer地址
defaultZone: http://127.0.0.1:10087/eureka
3.3 修改引导类
通过添加@EnableDiscoveryClient
来开启Eureka客户端功能,@EnableDiscoveryClient该注解是netflix公司提供的,后期推荐使用springcloud提供的注解@EnableDiscoveryClient,功能相同。
@SpringBootApplication
@EnableDiscoveryClient
public class YhServiceProviderApplication {
public static void main(String[] args) {
SpringApplication.run(YhServiceApplication.class, args);
}
}
4. 基本架构
Eureka架构中的三个核心角色:
-
服务注册中心
Eureka的服务端应用,提供服务注册和发现功能,就是刚刚我们建立的yh-eureka。
-
服务提供者
提供服务的应用,可以是SpringBoot应用,也可以是其它任意技术实现,只要对外提供的是Rest风格服务即可。本例中就是我们实现的yh-service-provider。
-
服务消费者
消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。本例中就是我们实现的yh-service-consumer。
5. 高可用的Eureka Server
Eureka Server即服务的注册中心,在刚才的案例中,我们只有一个EurekaServer,事实上EurekaServer也可以是一个集群,形成高可用的Eureka中心。
5.1 服务同步
多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。
5.2 动手搭建高可用的EurekaServer
操作流程:
1. 启动第一个eurekaServer,我们修改原来的EurekaServer配置中的端口号,
2. 复制一个启动器,然后在启动服务,就会有两个端口的客户端(都是生产者/消费者)
5.3客户端注册服务到集群
因为EurekaServer不止一个,因此注册服务的时候,service-url参数需要变化:
eureka:
client:
service-url: # EurekaServer地址,多个地址以','隔开
defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
5.3 服务提供者
服务提供者要向EurekaServer注册服务,并且完成服务续约等工作。
5.3.1 服务注册
服务提供者在启动时,会检测配置属性中的:eureka.client.register-with-eureka=true
参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka Server会把这些信息保存到一个双层Map结构中。
Map<serviceId,Map<服务实例名,实例对象instance>>
-
第一层Map的Key就是服务id,一般是配置中的
spring.application.name
属性 -
第二层Map的key是服务的实例id。一般host+ serviceId + port,例如:
locahost:service-provider:8081
-
值则是服务的实例对象,也就是说一个服务,可以同时启动多个不同实例,形成集群。
5.3.2 服务续约
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew);
有两个重要参数可以修改服务续约的行为:
eureka:
instance:
lease-expiration-duration-in-seconds: 90
lease-renewal-interval-in-seconds: 30
-
lease-renewal-interval-in-seconds:服务续约(renew)的间隔,默认为30秒
-
lease-expiration-duration-in-seconds:服务失效时间,默认值90秒
也就是说,默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。
但是在开发时,这个值有点太长了,经常我们关掉一个服务,会发现Eureka依然认为服务在活着。所以我们在开发阶段可以适当调小。
eureka:
instance:
lease-expiration-duration-in-seconds: 10 # 10秒即过期
lease-renewal-interval-in-seconds: 5 # 5秒一次心跳
5.4 服务消费者
获取服务列表
当服务消费者启动时,会检测eureka.client.fetch-registry=true
参数的值,如果为true,则会拉取Eureka Server服务的列表只读备份,然后缓存在本地。并且每隔30秒
会重新获取并更新数据。我们可以通过下面的参数来修改:
eureka:
client:
registry-fetch-interval-seconds: 5 #默认每隔30秒会从注册中心服务列表中重新拉取一次
生产环境中,我们不需要修改这个值。
但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点。
6. 失效剔除和自我保护
6.1 服务下线
当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求之后,将该服务置为下线状态。
6.2 失效剔除
有些时候,我们的服务提供方并不一定会正常下线,可能因为内存溢出、网络故障等原因导致服务无法正常工作。Eureka Server需要将这样的服务剔除出服务列表。因此它会开启一个定时任务,每隔60秒对所有失效的服务(超过90秒未响应)进行剔除。
可以通过eureka.server.eviction-interval-timer-in-ms
参数对其进行修改,单位是毫秒,生产环境不要修改。
这个会对我们开发带来极大的不便,你对服务重启,隔了60秒Eureka才反应过来。开发阶段可以适当调整,比如:10秒
6.3 自我保护
我们关停一个服务,就会在Eureka面板看到一条警告:
这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。
但是这给我们的开发带来了麻烦, 因此开发阶段我们都会关闭自我保护模式:(yh-eureka)
eureka:
server:
enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(缺省为60*1000ms)
负载均衡
1. 概念:
Ribbon是Netflix发布的负载均衡器,它有助于控制HTTP和TCP客户端的行为。为Ribbon配置服务提供者提供地址列表后,Ribbon就可基于某种负载均衡算法,自助地帮助服务消费者去请求。Ribbon默认为我们提供了很多的负载均衡算法,例如轮询,随机等。当然,我们也可为Ribbon实现自定义的负载均衡算法。
2. 开启负载均衡
因为Eureka中已经集成了Ribbon,所以我们无需引入新的依赖,直接修改代码。
修改yh-service-consumer的引导类,在RestTemplate的配置方法上添加@LoadBalanced
注解:
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
3. 修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用:
@Controller
@RequestMapping("consumer/user")
public class UserController {
@Autowired
private RestTemplate restTemplate;
//@Autowired
//private DiscoveryClient discoveryClient; // 注入discoveryClient,通过该客户端获取服务列表
@GetMapping
@ResponseBody
public User queryUserById(@RequestParam("id") Long id){
// 通过client获取服务提供方的服务列表,这里我们只有一个
// ServiceInstance instance = discoveryClient.getInstances("service-provider").get(0);
String baseUrl = "http://service-provider/user/" + id;
User user = this.restTemplate.getForObject(baseUrl, User.class);
return user;
}
}
Hystrix
1. 概念:
1. 原理:
Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。
用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理。
服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应。
2.触发Hystix服务降级的情况:
- 线程池已满
- 请求超时
3. 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
4. 注解:
注解:@EnableCircuitBreaker --->@SpringCloudApplication=@SpringBootApplication+@EnableDiscoveryClient+@EnableCircuitBreaker
5. 操作
1. 实现单个降级方法:@HystixCommond(fallbackMethod = "降级要执行的方法名")注到要降级的方法上,然后编写要降级的方法,参数和返回值要一致,方法名没有要求。
2.
2.1 实现多个降级方法:在Controller类上添加@DefaultProperties(defaultFallback = "降级要执行的方法名") // 指定一个类的全局熔断方法
2.2 接着在要降级的方法上添加 @HystixCommond注解,然后再编写要降级的方法,参数和返回值要一致,方法名没有要求
- @DefaultProperties(defaultFallback = "defaultFallBack"):在类上指明统一的失败降级方法
- @HystrixCommand:在方法上直接使用该注解,使用默认的降级方法。
- defaultFallback:默认降级方法,不用任何参数,以匹配更多方法,但是返回值一定一致
设置超时时间:请求在超过这个时间之后会报错,默认是1秒
我们可以通过`hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds`来设置Hystrix超时时间:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000 # 设置hystrix的超时时间为6000ms
2. 熔断
2.1 概念:
熔断器,也叫断路器,其英文单词为:Circuit Breaker
熔断是降级下的一个小的模块,当至少访问20次,且有一般的概率出错时,会触发熔断,此时熔断处于断开状态,不论是正确的请求还是错误的请求都无法通过,
默认会休眠5秒,5秒之后,熔断会进入半开状态,此时会关闭一部分请求路径,如果请求正确,那么就可以通过,拿去数据,之后就可以进入关闭状态,所有正确的请求都可以通过。
2.2 熔断状态机3个状态:
可以通过抛异常让熔断进入打开状态
- Closed:关闭状态,所有请求都正常访问。
- Open:打开状态,所有请求都会被降级。Hystix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全打开。默认失败比例的阈值是50%,请求次数最少不低于20次。
- Half Open:半开状态,open状态不是永久的,打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,
则会完全关闭断路器,否则继续保持打开,再次进行休眠计时
- requestVolumeThreshold:触发熔断的最小请求次数,默认20
- errorThresholdPercentage:触发熔断的失败请求最小占比,默认50%
- sleepWindowInMilliseconds:休眠时长,默认是5000毫秒
Feign
1. 概念
Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。
2. 操作流程
0. 在启动器application类中添加@EnableFeignClients注解
1. 创建一个feign在feign目录下的UserClient接口,接着添加@FeignClient(value = "service-provider")注解,value是生产者的服务名。
2. 定义抽象方法,返回值类型,参数类型与生产者的方法一致,@PathVariable注解也不能省略,方法名可以任意,RequestMapping与生产者的controller的uri一致
@FeignClient(value = "service-provider") // 标注该类是一个feign接口
public interface UserClient {
@GetMapping("user/{id}")
User queryById(@PathVariable("id") Long id);
}
- 首先这是一个接口,Feign会通过动态代理,帮我们生成实现类。这点跟mybatis的mapper很像
- `@FeignClient`,声明这是一个Feign客户端,类似`@Mapper`注解。同时通过`value`属性指定服务名称
- 接口中的定义方法,完全采用SpringMVC的注解,Feign会根据注解帮我们生成URL,并访问获取结果
然后在Controller里面
@Autowired
private UserClient userClient;
userClient.queryUserById(id);
3. 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Feign默认也有对Hystrix的集成:
只不过,默认情况下是关闭的。我们需要通过下面的参数来开启:(在Yh-service-consumer工程添加配置内容)
feign:
hystrix:
enabled: true # 开启Feign的熔断功能
这个时候就不可以用@SpringCloudApplication注解了,因为@SpringCloudApplication包含了@EnableCircuitBreaker,这个注解的意思就是开启熔断
首先,我们要定义一个类UserClientFallback,实现刚才编写的UserClient,作为fallback的处理类
@Component
public class UserClientFallback implements UserClient {
@Override
public User queryById(Long id) {
User user = new User();
user.setUserName("服务器繁忙,请稍后再试!");
return user;
}
}
然后在UserFeignClient中,指定刚才编写的实现类
@FeignClient(value = "service-provider", fallback = UserClientFallback.class) // 标注该类是一个feign接口
public interface UserClient {
@GetMapping("user/{id}")
User queryUserById(@PathVariable("id") Long id);
}
Zuul网关
1. 概念:
Zuul是Netflix开源的微服务网关,他可以和Eureka,Ribbon,Hystrix等组件配合使用。Zuul的核心是一系列的过滤器。
这些过滤器可以完成一下功能:
- 身份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求。
- 审查与监控:在边缘位置追踪有意义的数据和统计结果,从而带来精确的生产视图。
- 动态路由:动态地将请求路由到不同的后端集群。
- 压力测试:逐渐增加指向集群的流量,以了解性能。
- 负载分配:为每一种负载类型分配对应容量,并启用超出限定值的请求。
- 静态相应处理:在边缘位置直接建立部分响应,从而避免其转发到内部集群。
- 多区域弹性:跨越AWS Region进行请求路由,旨在实现ELB(ElasticSearch Load Balanceing) 使用多样化,以及让系统的边缘更贴近系统的使用者。
2. 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
3. 配置文件
server:
port: 10010 #服务端口
spring:
application:
name: api-gateway #指定服务名
4. 添加注解
给启动类添加@EnableZuulProxy注解,表示开启网关功能
5. 配置路由规则:
第一种方式:
zuul:
routes:
service-provider: # 这里是路由id,随意写
path: /service-provider/** # 这里是映射路径
url: http://127.0.0.1:8081 # 映射路径对应的实际url地址
第二种方式
zuul:
routes:
service-provider: # 这里是路由id,随意写
path: /service-provider/**
url: http://127.0.0.1:8081
service-id: service-provider #指定服务名称
第三种方式:推荐使用
zuul:
routes:
service-provider: /service-provider/**
service-consumer: /service-consumer/**
prefix: /api
第四种方式:
不配置,即默认配置,默认情况,每个微服务的服务名,即是访问映射地址
6. 过滤器
Zuul作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作我们往往是通过Zuul提供的过滤器来实现的
6.1 ZuulFilter抽象类
ZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法:
public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();// 来自IZuulFilter
Object run() throws ZuulException;// IZuulFilter
}
-
shouldFilter
:返回一个Boolean
值,判断该过滤器是否需要执行。返回true执行,返回false不执行。 -
run
:过滤器的具体业务逻辑。 -
filterType
:返回字符串,代表过滤器的类型。包含以下4种:-
pre
:请求在被路由之前执行 -
route
:在路由请求时调用 -
post
:在route和errror过滤器之后调用 -
error
:处理请求时发生错误调用
-
-
filterOrder
:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。
6.2 过滤器执行生命周期
6.2.1 正常流程
-
请求到达首先会经过pre类型过滤器,而后到达route类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。
6.2.2 异常流程
-
整个过程中,pre或者route过滤器出现异常,都会直接进入error过滤器,在error处理完毕后,会将请求交给POST过滤器,最后返回给用户。
-
如果是error过滤器自己出现异常,最终也会进入POST过滤器,将最终结果返回给请求客户端。
-
如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和route不同的是,请求不会再到达POST过滤器了。
6.3 使用场景
-
请求鉴权:一般放在pre类型,如果发现没有访问权限,直接就拦截了
-
异常处理:一般会在error类型和post类型过滤器中结合来处理。
-
服务调用时长统计:pre和post结合使用。
6.4 自定义过滤器
6.4.1 定义过滤器类
定义过滤器类,继承ZuulFilter抽象类,并重写抽象方法。
@Component
public class LoginFilter extends ZuulFilter {
/**
* 过滤器类型,前置过滤器
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 过滤器的执行顺序
* @return
*/
@Override
public int filterOrder() {
return 1;
}
/**
* 该过滤器是否生效
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 登陆校验逻辑
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
// 获取zuul提供的上下文对象
RequestContext context = RequestContext.getCurrentContext();
// 从上下文对象中获取请求对象
HttpServletRequest request = context.getRequest();
// 获取token信息
String token = request.getParameter("access-token");
// 判断
if (StringUtils.isBlank(token)) {
// 过滤该请求,不对其进行路由
context.setSendZuulResponse(false);
// 设置响应状态码,401
context.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
// 设置响应信息
context.setResponseBody("{\"status\":\"401\", \"text\":\"request error!\"}");
}
// 校验通过,把登陆信息放入上下文信息,继续向后执行
context.set("token", token);
return null;
}
}
7. 负载均衡和熔断
Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。因此建议我们手动进行配置:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000 # 设置hystrix的超时时间为6000ms