一.Spring Cloud简介
Spring Cloud服务间是通过http协议进行通信的
region和zone
查看:https://www.cnblogs.com/junjiang3/p/9061867.html
分布式微服务的区别:
最大的区别是微服务是个组件化的,有自己独立的资源,可以
独立运行,可插拔。
一.Eureka
1.Eureka简介
(1)Eureka的基本原理
关于Eureka的原理参看:
https://blog.csdn.net/forezp/article/details/73017664
由图可知,Eureka包含两个组件,Eureka Server和Eureka Client
Eureka Server:提供服务注册服务,各个节点启动后会在Eureka Server进行注册。
Eureka Server之间通过复制的方式完成数据的同步。在应用启动
后,将会向Eureka Server发送心跳,默认周期为30秒,如果
Eureka Server在多个心跳周期内没有接收到某个节点的心跳,将
会重服务器注册表中把这个服务节点移除(开启自我保护除外)。
Eureka Client:是一个Java客户端,用于简化与Eureka Server的交互,该客户端
具备一个内置的使用轮询负载均算法的负载均衡器。Eureka提供客
户端缓存机制,即使所有的Eureka Server都挂掉,客户端依然可以
利用缓存中的信息消费其它服务中的Api。
客户端缓存的实现原理:
Eureka Client缓存机制很简单,设置了一个每30秒执行一次的定时
任务,定时去服务端获取注册信息。获取之后,存入本地内存。
综上,Eureka通过心跳检测、健康检查、客户端缓存等机制确保了系统的高可用性、
灵活性和可伸缩性。
(2)Eureka包含的核心功能
(a)Register:服务注册
当Eureka客户端向Eureka Server注册时,它提供自身的元数据,比如IP地
址、端口,运行状况指示符URL,主页等。
(b)Renew:服务续约
Eureka客户会每隔30秒发送一次心跳来续约。 通过续约来告知
Eureka Server该Eureka客户仍然存在,没有出现问题。 正常情况下,如果
Eureka Server在90秒没有收到Eureka客户的续约,它会将实例从其注册表
中删除。 建议不要更改续约间隔。
(c)Fetch Registries:获取注册列表信息
Eureka客户端从服务器获取注册表信息,并将其缓存在本地。客户端会使
用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每30
秒钟)更新一次。每次返回注册列表信息可能与Eureka客户端的缓存信息
不同, Eureka客户端自动处理。如果由于某种原因导致注册列表信息不能
及时匹配,Eureka客户端则会重新获取整个注册表信息。 Eureka服务器缓
存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩
内容和没有压缩的内容完全相同。Eureka客户端和Eureka 服务器可以使用
JSON / XML格式进行通讯。在默认的情况下Eureka客户端使用压缩JSON
格式来获取注册列表的信息。
(d)Cancel:服务下线
Eureka客户端在程序关闭时向Eureka服务器发送取消请求。 发送请求后,
该客户端实例信息将从服务器的实例注册表中删除。该下线请求不会自动
完成,它需要调用以下内容:
DiscoveryManager.getInstance().shutdownComponent();
2.Eureka的一些使用示例
(1)Eureka集群
Eureka服务器端application.yml配置(以两台为例):
server:
port: 8761
spring:
application:
name: eureka-servier
#Spring提供了profiles的功能,可以配置多套配置,用于区分不同的环境(开发、测试、生产)在运行时
#指定使用那套,这样代码只要一套,运行时加入不同参数就可以了。比如在UnitTest中,加入:
#@ActiveProfiles("dev"),即可使用dev的配置。也可以在运行jar的时候
#加入:-Dspring.profiles.active=release。
profiles: slave1
eureka:
client:
serviceUrl:
#服务注册中心的配置内容,指定服务注册中心的位置
defaultZone: http://localhost:8762/eureka
---
server:
port: 8762
spring:
application:
name: eureka-servier
profiles: slave2
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka
Eureka客户端配置:
spring:
application:
name: consumer-demo
server:
port: 9000
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka,http://localhost:8762/eureka
集群成功示例图:
(2)健康检查:
如果Eureka Server在一定时间内(默认90秒)没有接收到某个微服务实例的心跳,
Eureka Server将会移除该实例。
健康检查配置(下面代码是一个eureka客户端而不是eureka server):
spring:
application:
name: consume-demo-hertbeat
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
#eureka客户端发送给eureka服务器心跳的频率
lease-renewal-interval-in-seconds: 5
#表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance。
lease-expiration-duration-in-seconds: 10
同时应该在eureka server中关闭自我保护,示例代码如下(下例是一个eureka server
而不是eureka client):
spring:
application:
name: eurker-servier-heartbeat
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
server:
#自我保护配置(true 开启,false 关闭)
enable-self-preservation: false
#(清理服务列表的时间间隔)
上面代码中为了测试方便还增加了清理服务列表的时间,即使满足“10秒没收到请求但不满足到
了清理服务列表的时间,一样不会剔除该服务”。
(3)自我保护:
首先阐明存在的一个问题:
默认情况下,如果Eureka Server在一定时间内(默认90秒)没有接收到某个微服务实例的心跳,
Eureka Server将会移除该实例。但是当网络分区故障发生时,微服务与Eureka Server之间无法
正常通信,而微服务本身是正常运行的,此时不应该移除这个微服务,Eureka通过自我保护机制
来解决该问题的。
自我保护机制:当Eureka Server节点在短时间内丢失过多客户端时(15分钟内超过85%的客户端
节点都没有正常的心跳),那么这个节点就会进入自我保护模式。一旦进入该模式,
Eureka Server就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任
何微服务)。当网络故障恢复后,该Eureka Server节点会自动退出自我保护模式。
自我保护配置:
spring:
application:
name: eurker-servier-heartbeat
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
server:
#自我保护配置(true 开启,false 关闭)
enable-self-preservation: false
#(清理服务列表的时间间隔)
eviction-interval-timer-in-ms: 10000
(4)健康监控
默认情况下注册到eureka server的服务是通过心跳来告知自己是UP还是DOWN,并不是
通过spring-boot-actuator模块的/health端点来实现的,这样其实不是很合理。因为默认的心跳
实现方式可以有效的检查eureka客户端进程是否正常运作,但是无法保证客户端应用能够正常
提供服务(大多数微服务应用都会有一些其他的外部资源依赖,比如数据库,REDIS缓存等,
如果我们的应用与这些外部资源无法连通的时候,实际上已经不能提供正常的对外服务了,但
因为客户端心跳依然在运行,所以它还是会被服务消费者调用)。
健康状态有UP和DOWN两种,如果Eureka中的是UP,则该服务可以正常调用;如果
Eureka中的健康状态是DOWN则该服务不可调用
问题场景:如果一个服务并没有死掉,但是其本身是有问题的,例如访问数据库的服务无法连接
到数据库,这个时候需要使用健康监控。
注意:健康监控监控的是客户端,所以健康指示器和健康处理器的代码只能写在需要健康的客户端。
(a)引入jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>1.5.4.RELEASE</version>
</dependency>
(b)编写健康指示器
package app;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.Status;
import org.springframework.stereotype.Component;
@Component
public class MyHealthIndicator implements HealthIndicator {
public Health health() {
if(ProviderMonitorController.canVisitDB)
{
return new Health.Builder(Status.UP).build();
}else
{
return new Health.Builder(Status.DOWN).build();
}
}
}
(c)模拟数据库无法访问
package app;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProviderMonitorController {
public static boolean canVisitDB=true;
@RequestMapping(value = "/setDB/{can}",method = RequestMethod.GET)
public void setDB(@PathVariable boolean can)
{
canVisitDB=can;
}
}
(d)测试
(i)查看健康状态
在浏览器里输入http://localhost:8080/health,返回状态为UP
(ii)执行模拟数据库无法访问并查看健康状态
浏览器中输入:http://localhost:8080/setDB/false 设置数据库无法访问
浏览器中输入:http://localhost:8080/health 查看健康状态为 DOWN
查看Eureka中的健康状态仍然为UP,这时需要使用健康检查处理器来改变Eureka中的状
态(只有改变了Eureka中的状态,那么有问题的服务才不能被其它被访问)。
(e)健康处理器代码(默认情况下这个处理器30秒执行一次):
package app;
import com.netflix.appinfo.HealthCheckHandler;
import com.netflix.appinfo.InstanceInfo;
import jdk.net.SocketFlow;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.boot.actuate.health.Status;
@Component
public class MyHealthCheckHandle implements HealthCheckHandler {
@Autowired
private MyHealthIndicator myHealthIndicator;
public InstanceInfo.InstanceStatus getStatus(InstanceInfo.InstanceStatus instanceStatus) {
Status status= myHealthIndicator.health().getStatus();
if(status.equals(Status.UP))
{
return InstanceInfo.InstanceStatus.UP;
}else
{
return InstanceInfo.InstanceStatus.DOWN;
}
}
}
application.yml(主要关注健康处理器的执行频率)
spring:
application:
name: my-health-provider
endpoints:
sensitive: false
eureka:
client:
#健康处理器执行频率。默认30秒执行一次,这里改成10秒执行一次
instanceInfoReplicationIntervalSeconds: 10
serviceUrl:
defaultZone: http://localhost:8761/eureka/
(f)测试健康处理器
浏览器中执行:http://localhost:8080/setDB/false,可以看到health端口和Eureka 中的状态
全部变成DOWN
Ribbon
1.客户端负载均衡框架,支持可插拔式的负载均衡规则
2.支持多中协议,如HTTP、UDP
3.Ribbon支持的负载均衡规则
RoundRobinRule:轮询
AvailabilityFilteringRule:会过滤掉两类服务器。(1)短路状态(连续3次超时)(2)并发数过高
的服务器。
WeightedResponseTimeRule:为每个服务器设置一个权重值,服务器的响应时间越长权重值越小。
这里是根据权重值随机选择服务器。
ZoneAvoidanceRule:复合判断server所在区域的性能和server的可用性选择server,使用区域对服
务器进行选择。
BestAvailableRule:忽略短路的服务器并选择并发数最小的一个服务器
RandomRule:随机数
RetryRule:含有重试机制的选择逻辑。
NFLoadBalancerPingClassName:检查服务器是否存活
NFLoadBalancerClassName:负载均衡器的实现类
NIWSServerListClassName:服务器列表的处理类,用来维护服务器列表
NIWSServerListFilterClassName:实现服务器的拦截
Feign
1.Feign是一款rest客户端,底层使用了Ribbon的Api,天然具有负载均衡的功能;
Feign默认使用HttpURLConnection连接HTTP服务。
2.Feign的一些模块介绍
(1)编码器和解码器
(a)编码器:对请求内容进行处理。向服务发送请求的过程中,有些情况需要对请求的内容
进行处理。例如服务端发布的服务接收的是JSON格式参数,而客户端使用的
是对象,这种情况就可以使用编码器,将对象转换为JSON字符串。
JSON的编码器和解码器:GsonEncoder和GsonDecoder
示例代码:
服务器端:
/**
* 参数为JSON
*/
@RequestMapping(value = "/person/create", method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String createPerson(@RequestBody Person person) {
System.out.println(person.getName() + "-" + person.getAge());
return "Success, Person Id: " + person.getId();
}
客户端:
public interface PersonClient {
@RequestLine("POST /person/create")
@Headers("Content-Type: application/json")
String createPerson(Person person);
@Data
class Person {
Integer id;
String name;
Integer age;
String message;
}
}
/**
* 运行主类
* @author 杨恩雄
*
*/
public class EncoderTest {
public static void main(String[] args) {
// 获取服务接口
PersonClient personClient = Feign.builder()
.encoder(new GsonEncoder())
.target(PersonClient.class, "http://localhost:8080/");
// 创建参数的实例
Person person = new Person();
person.id = 1;
person.name = "Angus";
person.age = 30;
String response = personClient.createPerson(person);
System.out.println(response);
}
}
(b)解码器:对服务的响应内容进行处理。例如解析响应的JSON或者XML字符串,转换为
我们所需要的对象。
代码示例:
PersonClient personService = Feign.builder()
.decoder(new GsonDecoder())
.target(PersonClient.class, "http://localhost:8080/");
(c)XML风格的编码器和解码器
JAXBEncoder和JAXBDecoder
具体代码查看:https://my.oschina.net/JavaLaw/blog/1555037
(d)自定义编码器和解码器
实现接口Decoder和Encoder即可
(2)自定义Feign的客户端
(a)介绍:Feign的客户端默认使用HttpURLConnection连接HTTP服务
(b)实现方式及代码示例
需要实现接口feign.Client,实现思路是请求转新的请求响应转新的响应。
代码示例:
自定义客户端:
package org.crazyit.cloud;
import java.io.IOException;
import java.net.URI;
import java.util.Collection;
import java.util.HashMap;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import feign.Client;
import feign.Request;
import feign.Request.Options;
import feign.Response;
public class MyClient implements Client {
public Response execute(Request request, Options options)
throws IOException {
System.out.println("this is my client");
try {
// 创建一个默认的客户端
CloseableHttpClient httpclient = HttpClients.createDefault();
// 获取调用的HTTP方法
final String method = request.method();
// 创建一个HttpClient的HttpRequest
HttpRequestBase httpRequest = new HttpRequestBase() {
public String getMethod() {
return method;
}
};
// 设置请求地址
httpRequest.setURI(new URI(request.url()));
// 执行请求,获取响应
HttpResponse httpResponse = httpclient.execute(httpRequest);
// 获取响应的主体内容
byte[] body = EntityUtils.toByteArray(httpResponse.getEntity());
// 将HttpClient的响应对象转换为Feign的Response
Response response = Response.builder()
.body(body)
.headers(new HashMap<String, Collection<String>>())
.status(httpResponse.getStatusLine().getStatusCode())
.build();
return response;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
发送请求代码:
package org.crazyit.cloud;
import feign.Feign;
public class MyClientTest {
public static void main(String[] args) {
HelloClient client = Feign.builder()
.client(new MyClient())
.target(HelloClient.class,
"http://localhost:8080");
String result = client.hello();
System.out.println(result);
}
}
(2)注解翻译器
注解翻译器用来翻译注解,在spring cloud集成的时候会使用spring的注解,不要懵逼,其原
理是使用了注解翻译器可自定义注解翻译器,但是个人认为基本不需要自定义注解翻译器,
学习的目的只是要了解spring cloud能使用spring注解的原理。
自定义注解翻译器:
(a)定义注解
package org.crazyit.cloud.contract;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyUrl {
String url();//该出叫属性不叫方法
String method();//该出叫属性不叫方法
}
(b)定义注解翻译器
package org.crazyit.cloud.contract;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import feign.Contract.BaseContract;
import feign.MethodMetadata;
public class MyContract extends BaseContract {
@Override
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
// TODO Auto-generated method stub
}
@Override
protected void processAnnotationOnMethod(MethodMetadata data,
Annotation annotation, Method method) {
// 注解是MyUrl类型的,才处理
if(MyUrl.class.isInstance(annotation)) {
MyUrl myUrl = method.getAnnotation(MyUrl.class);
String url = myUrl.url();
String httpMethod = myUrl.method();
data.template().method(httpMethod);
data.template().append(url);
}
}
@Override
protected boolean processAnnotationsOnParameter(MethodMetadata data,
Annotation[] annotations, int paramIndex) {
// TODO Auto-generated method stub
return false;
}
}
(c)客户端调用
package org.crazyit.cloud.contract;
import org.crazyit.cloud.jaxrs.RsClient;
import feign.Feign;
import feign.jaxrs.JAXRSContract;
public class ContractMain {
public static void main(String[] args) {
ContractClient client = Feign.builder()
.contract(new MyContract())
.target(ContractClient.class,
"http://localhost:8080");
String result = client.hello();
System.out.println(result);
}
}
(3)请求拦截器
用户拦截请求,处理一些公共信息。
自定义请求拦截器:
(a)实现一个请求拦截器
package org.crazyit.cloud.interceptor;
import feign.RequestInterceptor;
import feign.RequestTemplate;
public class MyInterceptor implements RequestInterceptor {
public void apply(RequestTemplate template) {
template.header("Content-Type", "application/json");
System.out.println("这是自定义请求拦截器");
}
}
(b)客户端调用
package org.crazyit.cloud.interceptor;
import org.crazyit.cloud.HelloClient;
import feign.Feign;
public class InterceptorMain {
public static void main(String[] args) {
HelloClient client = Feign.builder()
.requestInterceptor(new MyInterceptor())
.target(HelloClient.class,
"http://localhost:8080");
String result = client.hello();
System.out.println(result);
}
}
(4)接口日志
默认情况下接口是不会记录接口日志的,如果想要了解接口的调用情况,需要配置接口日志
实现:
package org.crazyit.cloud.log;
import org.crazyit.cloud.HelloClient;
import org.crazyit.cloud.interceptor.MyInterceptor;
import feign.Feign;
import feign.Logger;
public class LogMain {
public static void main(String[] args) {
HelloClient client = Feign.builder()
.logLevel(Logger.Level.FULL)//日志级别说明 NONE:默认值,不记录日志;BASIC:记录请求方法、URL、响应状态代码和执行时间;HEADERS:除BASIC记录的日志外,还会记录请求头和响应头的信息;FULLL:在HEADERS的基础上,请求和响应的元数据,都会保存
.logger(new Logger.JavaLogger().appendToFile("logs/http.log"))
.target(HelloClient.class,
"http://localhost:8080");
String result = client.hello();
System.out.println(result);
}
}
3.Feign与Spring cloud整合
(1)看Demo整理知识点
(2)Feign的客户端中已经使用了Ribbon的Api,默认负载均衡配置是轮询,可以通过设置
Ribbon的负载均衡策略来定义Feign客户端的负载均衡策略。
(3)Feign的一些常用配置
解码器(Decoder):bean名称为feignDecoder,ResponseEntityDecoder类。
编码器(Encoder):bean名称为feignEecoder,SpringEncoder类。
日志(Logger): bean名称为feignLogger,Slf4jLogger类。
注解翻译器(Contract): bean名称为feignContract,SpringMvcContract类。
Feign实例的创建者(Feign.Builder):bean名称为feignBuilder,HystrixFeign.Builder类。
Hystrix框架将在后面章节中讲述。
Feign客户端(Client):bean名称为feignClient,LoadBalancerFeignClient类。
Logger.Level:接口日志的记录级别,相当于调用了Fiegn.Builder的logLevel方法,
请见5.2.9章节。
Retryer:重试处理器,相当于调用了Fiegn.Builder的retryer方法。
ErrorDecoder:异常解码器,相当于调用了Fiegn.Builder的errorDecoder方法。
Request.Options:设置请求的配置项,相当于调用了Fiegn.Builder的options方法。
Collection<RequestInterceptor>:设置请求拦截器,相当于调用了Fiegn.Builder的
requestInterceptors方法。下面
是配置多个拦截器的样例,并不全,只是为了便于理解,
只是补充需要百度
@Bean
public RequestInterceptor getRequestInterceptorsA() {
return new RequestInterceptor() {
public void apply(RequestTemplate template) {
System.out.println("这是第一个请求拦截器");
}
};
}
@Bean
public RequestInterceptor getRequestInterceptorsB() {
return new RequestInterceptor() {
public void apply(RequestTemplate template) {
System.out.println("这是第二个请求拦截器");
}
};
}
压缩配置:
feign.compression.request.enabled:
设置为true时,表示开启“请求”压缩。
feign.compression.response.enabled:
设置为true时,表示开启响应压缩。
feign.compression.request.mime-types:
数据类型列表,默认值为text/xml,application/xml,application/json。
feign.compression.request.min-request-size:
设置请求内容的最小阀值,默认值为2048。若大于2048则开启压缩。
Hystrix
1.Netflix提供的一个容错框架,它是为了解决雪崩效应而产生的。
在一个分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,如何能够保
证在一个依赖出问题的情况下,不会导致整体服务失败,这个就是Hystrix需要做的事情。
Hystrix提供了熔断、隔离、Fallback、cache、监控等功能,能够在一个、或多个依赖同
时出现问题时保证系统依然可用。
(1)雪崩效应:服务雪崩效应是一种因,服务提供者的不可用导致服务调用者的不可用,并
将不可用逐渐放大的过程。
上图中, A为服务提供者,B为A的服务调用者,C和D是B的服务调用者,当A的不可
用,引起B的不可用,并将不可用逐渐放大C和D时, 服务雪崩就形成了。
雪崩效应的产生过程分为如下三个阶段:
(a)服务提供者不可用
(b)重试加大流量
(c)服务调用者不可用
服务雪崩的每个阶段都可能由不同的原因造成,比如造成服务不可用的原因有:"硬件
故障”、“程序Bug”、“缓存击穿”、“用户大量请求”。硬件故障可能为硬件损坏造成的服
务器主机宕机,网络硬件故障造成的服务提供者的不可访问。缓存击穿一般发生在缓
存应用重启,所有缓存被清空时,以及短时间内大量缓存失效时。大量的缓存不命
中,使请求直击后端,造成服务提供者超负荷运行,引起服务不可用。在秒杀和大促
开始前,如果准备不充分,用户发起大量请求也会造成服务提供者的不可用。
重试加大流量的原因有:“用户重试”、“代码逻辑重试”。在服务提供者不可用后,
用户由于忍受不了界面上长时间的等待,而不断刷新页面甚至提交表单。服务调用端
的会存在大量服务异常后的重试逻辑。这些重试都会进一步加大请求流量。在服务提
供者不可用后,用户由于忍受不了界面上长时间的等待,而不断刷新页面甚至提交表
单。服务调用端的会存在大量服务异常后的重试逻辑,这些重试都会进一步加大请求
流量。
服务调用者不可用 产生的主要原因是:“同步等待造成的资源耗尽”。当服务调用者
使用同步调用时,会产生大量的等待线程占用系统资源。一旦线程资源被耗尽,服务调
用者提供的服务也将处于不可用状态, 于是服务雪崩效应产生了。
针对造成服务雪崩的不同原因, 可以使用不同的应对策略:“流量控制”,“改进缓存模
式服务”、“自动扩容服务调用者降级服务”。
针对造成服务雪崩的不同原因, 可以使用不同的应对策略:
(a)流量控制
(b)改进缓存模式
(c)服务自动扩容
(d)服务调用者降级服务
“流量控制”的具体操作包括:
网关限流
用户交互限流
关闭重试
因为Nginx的高性能, 目前一线互联网公司大量采用Nginx+Lua的网关进行流量控制,由此
而来的OpenResty也越来越热门,用户交互限流的具体措施有:
采用加载动画,提高用户的忍耐等待时间。
提交按钮添加强制等待时间机制。
“改进缓存模式”的措施包括:
缓存预加载
同步改为异步刷新
“服务自动扩容”的措施主要有:
AWS的auto scaling
“服务调用者降级服务”的措施包括:
资源隔离
对依赖服务进行分类
不可用服务的调用快速失败
资源隔离主要是对调用服务的线程池进行隔离。我们根据具体业务,将依赖服务分为:
强依赖和弱依赖.。强依赖服务不可用会导致当前业务中止,而弱依赖服务的不可用不会导
致当前业务的中止。不可用服务的调用快速失败一般通过“超时机制”、“熔断器”和熔断后的
降级方法来实现。.
(2)Hystrix
(a)Hystrix工作模式如下图:
(b)Hystrix的设计
(I)资源隔离
货船为了进行防止漏水和火灾的扩散,会将货仓分隔为多个,这种资源隔离减少风险
的方式被称为:Bulkheads(舱壁隔离模式)。Hystrix将同样的模式运用到了服务调
用者上。在一个高度服务化的系统中,我们实现的一个业务逻辑通常会依赖多个服务,
比如: 商品详情展示服务会依赖商品服务、价格服务,、商品评论服务.。如图所示:
调用三个依赖服务会共享商品详情服务的线程池。如果其中的商品评论服务不可
用,就会出现线程池里所有线程都因等待响应而被阻塞,从而造成服务雪崩。如
图所示:
Hystrix通过将每个依赖服务分配独立的线程池进行资源隔离,从而避免服务雪崩。
如下图所示,当商品评论服务不可用时,即使商品服务独立分配的20个线程全部
处于同步等待状态,也不会影响其他依赖服务的调用。
Hystrix的隔离策略:
两种隔离策略,线程池和信号量。
线程池隔离:对每个command创建一个自己的线程池,执行调用。通过线程
池隔离来保证不同调用不会相互干扰和每一个调用的并发限制。
信号量隔离:对每个command创建一个自己的计数器,当并发量超过计数器
指定值时,直接拒绝。信号量没有timeout机制。
二者比较:99%的情况使用的是线程池
线程池隔离 | 信号量隔离 | |
线程 | 与调用线程非相同线程() | 与调用线程相同(jetty线程) |
开销 | 排队、调度、上下文开销等 | 无线程切换,开销低 |
异步 | 支持 | 不支持 |
并发支持 | 支持(最大线程池大小) | 支持(最大信号量上限) |
区别消息描述 | 当用户请求服务时,设请求的是服务A和服务 I,tomcat的线程会将请求的任务交给服务A或的内部线程池里面的线程来执行tomcat的线程就可以去干别的事情去了,当服务A和服务I自己线程池里面的线程执行完任务之后,就会将调用的结果返回给tomcat的线程,从而实现资源的隔离,当有大量并发的时候,服务内部的线程池的数量就决定了整个服务的并发度,例如服务A的线程池大小为10个,当同时有12请求时,只会允许10个任务在执行,其他的任务被放在线程池队列中,或者是直接走降级服务,此时,如果服务A挂了,就不会造成大量的tomcat线程被服务A拖死,服务I依然能够提供服务。整个系统不会受太大的影响。 | 信号量的资源隔离只是起到一个开关的作用,例 如,服务X的信号量大小为10,那么同时只允许 10个tomcat的线程(此处是tomcat的线程,而不 是服务X的独立线程池里面的线程)来访问服务 X,其他的请求就会被拒绝,从而达到限流保护的作用。 |
注意,表格中最后一项“区别消息描述”中的“线程池”和“信号量”的主要区别是:
“线程池”是使用服务提供者内部的线程池,“信号量”是使用的服务调用者的
Tomcat线程。
线程池和信号量配置:
execution.isolation.strategy :该属性用来设置执行的隔离策略,有如下二个选
项:
HREAD:通过线程池隔离的策略,在独立线程上执行,并且他的并发限制受线
程池中线程数量的限制(默认)。
SEMAPHONE:通过信号量隔离的策略,在调用线程上执行,并且他的并发限
制受信号量计数的限制。
线程池例子:
package org.crazyit.cloud;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
public class MyCommand extends HystrixCommand<String> {
private int index;
public MyCommand(int index) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory
.asKey("TestGroupKey")));
this.index = index;
}
@Override
protected String run() throws Exception {
Thread.sleep(500);
System.out.println("执行方法,当前索引:" + index);
return "";
}
@Override
protected String getFallback() {
System.out.println("执行回退,当前索引:" + index);
return "";
}
}
package org.crazyit.cloud;
import com.netflix.config.ConfigurationManager;
public class ThreadMain {
public static void main(String[] args) throws Exception {
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.threadpool.default.coreSize", 4);
for(int i = 0; i < 6; i++) {
MyCommand c = new MyCommand(i);
c.queue();
}
Thread.sleep(5000);
}
}
信号量(SEMAPHORE)例子:
package org.crazyit.cloud;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
public class MyCommand extends HystrixCommand<String> {
private int index;
public MyCommand(int index) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory
.asKey("TestGroupKey")));
this.index = index;
}
@Override
protected String run() throws Exception {
Thread.sleep(500);
System.out.println("执行方法,当前索引:" + index);
return "";
}
@Override
protected String getFallback() {
System.out.println("执行回退,当前索引:" + index);
return "";
}
}
package org.crazyit.cloud;
import com.netflix.config.ConfigurationManager;
import com.netflix.hystrix.HystrixCommandProperties.ExecutionIsolationStrategy;
public class SemaphoreMain {
public static void main(String[] args) throws Exception {
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.command.default.execution.isolation.strategy", ExecutionIsolationStrategy.SEMAPHORE);
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests", 3);
for(int i = 0; i < 6; i++) {
final int index = i;
Thread t = new Thread() {
public void run() {
MyCommand c = new MyCommand(index);
c.execute();
}
};
t.start();
}
Thread.sleep(5000);
}
}
(II)熔断器
服务的健康状况 = 请求失败数 / 请求总数。
当熔断器开关关闭时,请求被允许通过熔断器。如果当前健康状况高于设定阈
值,开关继续保持关闭。如果当前健康状况低于设定阈值,,开关则切换为打开状
态。当熔断器开关打开时,请求被禁止通过。当熔断器开关处于打开状态,经过
一段时间后,熔断器会自动进入半开状态,这时熔断器只允许一个请求通过。当
该请求调用成功时,熔断器恢复到关闭状态。若该请求失败,熔断器继续保持打
开状态,接下来的请求被禁止通过。
(III)命令模式(参考文章:https://blog.csdn.net/sjyttkl/article/details/73536297)
首先,命令模式是一种设计模式。现在来具体讲解:举个例子,司令员下令让士
兵去干件事情,从整个事情的角度来考虑,司令员的作用是,发出口令,口令经
过传递,传到了士兵耳朵里,士兵去执行。这个过程好在,三者相互解耦,任何
一方都不用去依赖其他人,只需要做好自己的事儿就行,司令员要的是结果,不
会去关注到底士兵是怎么实现的。我们看看关系图:
Invoker是调用者(司令员),Receiver是被调用者(士兵),MyCommand是
命令,实现了Command接口,持有接收对象,看实现代码:
public interface Command
{
public void exe();
}
public class Invoker
{
private Command command;
public Invoker(Command command)
{
this.command = command;
}
public void action()
{
command.exe();
}
}
public class MyCommand implements Command
{
private Receiver receiver;
public MyCommand(Receiver receiver)
{
this.receiver = receiver;
}
@Override
public void exe()
{
receiver.action();
}
}
public class Receiver
{
public void action()
{
System.out.println("command received!");
}
}
public class Test
{
public static void main(String[] args)
{
Receiver receiver = new Receiver();
Command cmd = new MyCommand(receiver);
Invoker invoker = new Invoker(cmd);
invoker.action();
}
}
命令模式的目的就是达到命令的发出者和执行者之间解耦,实现请求和执行分开。
Hystrix使用命令模式(继承HystrixCommand类)来包裹具体的服务调用逻辑(run
方法),并在命令模式中添加了服务调用失败后的降级逻辑(getFallback)。在使用了
Command模式构建了服务对象之后,服务便拥有了熔断器和线程池的功能。
Hystrix的内部处理:
(i)构建Hystrix的Command对象, 调用执行方法。
(ii)Hystrix检查当前服务的熔断器开关是否开启,若开启,则执行降级服务
getFallback方法。
(iii)若熔断器开关关闭, 则Hystrix检查当前服务的线程池是否满载,若满载,
则执行降级服务getFallback方法;若线程池没有满载,Hystrix开始执行服
务调用具体逻辑run方法。
(iv)若服务执行失败或超时,则执行降级服务getFallback方法,并将执行结果
上报Metrics(Hystrix的Metrics中保存了当前服务的健康状况,包括服务调
用总次数和服务调用失败次数等。根据Metrics的计数,熔断器从而能计算
出当前服务的调用失率,用来和设定的阈值比较从而决定熔断器的状态切换
逻辑。)更新服务健康状况;若服务执行成功,返回正常结果。.
总结:
1.关于Hystrix执行回退,以下三种情况会执行回退:
(1)执行超时等失败情况
(2)断路器打开(在此处顺便将断路器的相关知识做个介绍)
断路器打开条件:
(a)默认情况下10秒产生20次请求
(b)在满足(a)的情况下,当请求错误百分比大于阈值,默认50%,
则会打开断器。下例为断路器打开的例子,其中条件(b)不变,条
件(a)为10秒中产生10次请求。
package org.crazyit.cloud.open;
import com.netflix.config.ConfigurationManager;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.hystrix.HystrixCommand.Setter;
public class OpenMain {
public static void main(String[] args) {
// 10秒内大于10次请求,满足第一个条件
ConfigurationManager
.getConfigInstance()
.setProperty(
"hystrix.command.default.circuitBreaker.requestVolumeThreshold",
10);
for(int i = 0; i < 15; i++) {
ErrorCommand c = new ErrorCommand();
c.execute();
// 输出断路器状态
System.out.println(c.isCircuitBreakerOpen());
}
}
static class ErrorCommand extends HystrixCommand<String> {
public ErrorCommand() {
super(Setter.withGroupKey(
HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(500)));
}
@Override
protected String run() throws Exception {
Thread.sleep(800);
return "";
}
@Override
protected String getFallback() {
return "fallback";
}
}
}
2.断路器关闭:短路器打开后会在之后的一段之间内处于打开状态(默认是
5秒),我们管这个叫休眠期,休眠期满后, 断路器会尝试
执行一次命令,如果执行成功断路器关闭,否则断路器仍然
处于打卡状态。下例中,是断路器打开后自动关闭的例子,
知识满足断路器打开的第一个条件是“10秒有3次请求”,满足
断路器打开的第二个条件不变。
package org.crazyit.cloud.open;
import com.netflix.config.ConfigurationManager;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandMetrics.HealthCounts;
import com.netflix.hystrix.HystrixCommandProperties;
public class CloseMain {
public static void main(String[] args) throws Exception {
// 10秒内大于3次请求,满足第一个条件
ConfigurationManager
.getConfigInstance()
.setProperty(
"hystrix.command.default.circuitBreaker.requestVolumeThreshold",
3);
boolean isTimeout = true;
for(int i = 0; i < 10; i++) {
TestCommand c = new TestCommand(isTimeout);
c.execute();
HealthCounts hc = c.getMetrics().getHealthCounts();
System.out.println("断路器状态:" + c.isCircuitBreakerOpen() + ", 请求数量:" + hc.getTotalRequests());
if(c.isCircuitBreakerOpen()) {
isTimeout = false;
System.out.println("============ 断路器打开了,等待休眠期结束");
Thread.sleep(6000);
}
}
}
static class TestCommand extends HystrixCommand<String> {
private boolean isTimeout;
public TestCommand(boolean isTimeout) {
super(Setter.withGroupKey(
HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(500)));
this.isTimeout = isTimeout;
}
@Override
protected String run() throws Exception {
if(isTimeout) {
Thread.sleep(800);
} else {
Thread.sleep(200);
}
return "";
}
@Override
protected String getFallback() {
return "fallback";
}
}
}
2.命令执行:
(1)共有四种命令执行方式:
HystrixCommand实现了execute()和queure();HystrixObervableCommand实现了observe()
和toObservable()。
execute():同步执行,重依赖的服务返回一个单一的结果对象,或是发生错误时抛出异常。
其实通过queue()返回的异步对象Future<R>的get()方法来实现同步执行的。该方
法会等待任务执行结束,然后获得R类型的结果进行返回。
queue():异步执行,直接返回一个Future对象,其中包含了服务执行结束时要返回的单一结
果对象。
observer():返回Observable对象,它代表了操作的多个结果,它是一个Hot Obervable。
toObservable():同样会返回Observable对象,代表了操作的多个结果,但是它返回的是一个
Cold Obervable。
Hot Obervable和Cold Obervable分别对应command.observe()和command.toObservable()的
返回对象。
Hot Obervable:它不论“事件源”是否有订阅者,都会在创建后对事件进行发布,所以对于
HOt Observable的每一个“订阅者”都可能是从“事件源”的中途开始的,并可
能只是看到了整个操作的局部过程。
Cold Obervable:在没有“订阅者”的时候并不会发布事件,而是进行等待,直到有“订阅者”之
后才发布事件,所以对于Cold Obervable的订阅者,它可以保证重一开始看
到整个操作的全部过程
(2)RxJava的观察者——订阅者模式。
Hystrix底层实现中大量的使用了RxJava,为了容易理解后续内容,在这里RxJava对观察者
——订阅者做一个简单介绍。.
上面提到的Observable对象称为“事件源“或“被观察者”,与其对应的Subscriber对象可以理解为
“订阅者”或者 “观察者”。
(a)Observable用来向订阅者Subscriber对象发布事件,Subscriber则在接收到事件后对其进
行处理。
(b)一个Observable可以发出多个事件,直到结束或者发生异常。
(c)Observable对象每发出一个事件,就会调用对应观察者Subscriber对象的onNext()方法。
(d)每一个Observable的执行,最后一定会调用Subscriber.onCompleted()或是
Subscriber.onError()来结束事件的操作流。
(1)HystrixCommand:用于返回单一响应。
(2)HystrixObservableCommand:用于返回多个可自定义的响应。
(1)execute():以同步堵塞方式执行run()。以demo为例,调用execute()后,hystrix先创建一个新
线程运行run(),接着调用程序要在execute()调用处一直堵塞着,直到run()运行完
成。
(2)queue():以异步非堵塞方式执行run()。以demo为例,一调用queue()就直接返回一个Future
对象,同时hystrix创建一个新线程运行run(),调用程序通过Future.get()拿到run()
的返回结果,而Future.get()是堵塞执行的。
(3)observe():以异步非堵塞方式执行run()。事件注册前执行run()/construct()。以demo为例,第
一步是事件注册前,先调用observe()自动触发执 行run()/construct()(如果继承的
是HystrixCommand,hystrix将创建新线程非堵塞执行run();如果继承的是
HystrixObservableCommand,将以调用程序线程堵塞执行construct()); 第二步
是从observe()返回后调用程序调 用subscribe()完成事件注册,如果run()/construct()
执行成功则触发onNext()和onCompleted(),如果执行异常则触发onError()。
(4)toObservable():以异步非堵塞方式执行run()。事件注册后执行run()/construct()。以demo为
例,第一步是事件注册前,一调用toObservable()就直接返回一个
Observable<String>对象,第二步调用subscribe()完成事件注册后自动触发
执行run()/construct()(如果继承的是HystrixCommand,hystrix将创建新线程
非堵塞执行run(),调用程序不必等待run();如果继承的是
HystrixObservableCommand,将以调用程序线程堵塞执行construct(),调用
程序等待construct()执行完才能继续往下走),如果run()/construct()执行成功
则触发onNext()和onCompleted(),如果执行异常则触发onError()
3.GroupKey、
断路器关闭:短路器打开后会在之后的一段之间内处于打开状态(默认是5秒),我们管这个
叫休眠期,休眠期满后, 断路器会尝试执行一次命令,如果执行成功断路器关
闭,否则断路器仍然处于打卡状态。下例中,是断路器打开后自动关闭的例子,
知识满足断路器打开的第一个条件是“10秒有3次请求”,满足断路器打开的第二
个条件不变。
package org.crazyit.cloud.open;
import com.netflix.config.ConfigurationManager;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandMetrics.HealthCounts;
import com.netflix.hystrix.HystrixCommandProperties;
public class CloseMain {
public static void main(String[] args) throws Exception {
// 10秒内大于3次请求,满足第一个条件
ConfigurationManager
.getConfigInstance()
.setProperty(
"hystrix.command.default.circuitBreaker.requestVolumeThreshold",
3);
boolean isTimeout = true;
for(int i = 0; i < 10; i++) {
TestCommand c = new TestCommand(isTimeout);
c.execute();
HealthCounts hc = c.getMetrics().getHealthCounts();
System.out.println("断路器状态:" + c.isCircuitBreakerOpen() + ", 请求数量:" + hc.getTotalRequests());
if(c.isCircuitBreakerOpen()) {
isTimeout = false;
System.out.println("============ 断路器打开了,等待休眠期结束");
Thread.sleep(6000);
}
}
}
static class TestCommand extends HystrixCommand<String> {
private boolean isTimeout;
public TestCommand(boolean isTimeout) {
super(Setter.withGroupKey(
HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(500)));
this.isTimeout = isTimeout;
}
@Override
protected String run() throws Exception {
if(isTimeout) {
Thread.sleep(800);
} else {
Thread.sleep(200);
}
return "";
}
@Override
protected String getFallback() {
return "fallback";
}
}
}
(3)线程池满载或信号量达到阈值
Hystrix默认使用的是线程池
(a)THREAD(线程池):由线程池决定命令的执行,如果线程池满载就不会执行命令,
从而执行回退逻辑,默认线程池大小为10。
(b)SEMAPHORE(信号量):当一个请求并发数高于设定的阈值时就不会执行该命令,
从而执行回退逻辑,默认最大并发数是10。
(c)THREAD和SEMAPHORE比较:SEMAPHORE资源较THREAD资源消耗量小,但是
不支持超时和异步。
线程池示例:
package org.crazyit.cloud;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
public class MyCommand extends HystrixCommand<String> {
private int index;
public MyCommand(int index) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory
.asKey("TestGroupKey")));
this.index = index;
}
@Override
protected String run() throws Exception {
Thread.sleep(500);
System.out.println("执行方法,当前索引:" + index);
return "";
}
@Override
protected String getFallback() {
System.out.println("执行回退,当前索引:" + index);
return "";
}
}
package org.crazyit.cloud;
import com.netflix.config.ConfigurationManager;
public class ThreadMain {
public static void main(String[] args) throws Exception {
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.threadpool.default.coreSize", 4);
for(int i = 0; i < 6; i++) {
MyCommand c = new MyCommand(i);
c.queue();
}
Thread.sleep(5000);
}
}
SEMAPHORE示例
package org.crazyit.cloud;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
public class MyCommand extends HystrixCommand<String> {
private int index;
public MyCommand(int index) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory
.asKey("TestGroupKey")));
this.index = index;
}
@Override
protected String run() throws Exception {
Thread.sleep(500);
System.out.println("执行方法,当前索引:" + index);
return "";
}
@Override
protected String getFallback() {
System.out.println("执行回退,当前索引:" + index);
return "";
}
}
package org.crazyit.cloud;
import com.netflix.config.ConfigurationManager;
import com.netflix.hystrix.HystrixCommandProperties.ExecutionIsolationStrategy;
public class SemaphoreMain {
public static void main(String[] args) throws Exception {
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.command.default.execution.isolation.strategy", ExecutionIsolationStrategy.SEMAPHORE);
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests", 3);
for(int i = 0; i < 6; i++) {
final int index = i;
Thread t = new Thread() {
public void run() {
MyCommand c = new MyCommand(index);
c.execute();
}
};
t.start();
}
Thread.sleep(5000);
}
}
5.回退模式
如果CommandA和CommandB中都有回退,那么MainCommand 中就不会执行回退;如果
CommandA和CommandB中没有回退,那么MainCommand中的回退才会执行。
6.缓存
感觉没什么用,生产环境使用Redis就可以了,不过还是简单记录一下,保持知识结构的完整性
缓存在一次请求中才有效。如下例(c1、c2、c3演示的是缓存机制,c4演示的是清空缓存):
package org.crazyit.cloud.cache;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixRequestCache;
import com.netflix.hystrix.HystrixThreadPoolKey;
import com.netflix.hystrix.HystrixCommand.Setter;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategyDefault;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;
public class CacheMain {
public static void main(String[] args) {
HystrixRequestContext ctx = HystrixRequestContext.initializeContext();
String key = "cache-key";
CacheCommand c1 = new CacheCommand(key);
CacheCommand c2 = new CacheCommand(key);
CacheCommand c3 = new CacheCommand(key);
c1.execute();
c2.execute();
c3.execute();
System.out.println("命令c1,是否读取缓存:" + c1.isResponseFromCache());
System.out.println("命令c2,是否读取缓存:" + c2.isResponseFromCache());
System.out.println("命令c3,是否读取缓存:" + c3.isResponseFromCache());
HystrixRequestCache cache = HystrixRequestCache.getInstance(HystrixCommandKey.Factory.asKey("MyCommandKey"),
HystrixConcurrencyStrategyDefault.getInstance());
cache.clear(key);
CacheCommand c4 = new CacheCommand(key);
c4.execute();
System.out.println("命令c4,是否读取缓存:" + c4.isResponseFromCache());
ctx.shutdown();
}
static class CacheCommand extends HystrixCommand<String> {
private String cacheKey;
public CacheCommand(String cacheKey) {
super(Setter.withGroupKey(
HystrixCommandGroupKey.Factory.asKey("TestGroupKey"))
.andCommandKey(
HystrixCommandKey.Factory.asKey("MyCommandKey")));
this.cacheKey = cacheKey;
}
@Override
protected String run() throws Exception {
System.out.println("执行方法");
return "";
}
@Override
protected String getFallback() {
System.out.println("执行回退");
return "";
}
@Override
protected String getCacheKey() {
return this.cacheKey;
}
}
}
8.Hystrix合并请求:
在调用通一个服务(URL相同,参数不同)时,可以将多个请求合并为一个请求
例子(该处例子是于spring cloud整合的):
package org.crazyit.cloud.collapser;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import org.crazyit.cloud.Member;
import org.springframework.stereotype.Service;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCollapser;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
@Service
public class CollService {
@HystrixCollapser(batchMethod = "getMembers", collapserProperties = {
@HystrixProperty(name = "timerDelayInMilliseconds", value = "1000")
})
public Future<Member> getMember(Integer id) {
System.out.println("执行单个查询的方法");
return null;
}
@HystrixCommand
public List<Member> getMembers(List<Integer> ids) {
List<Member> mems = new ArrayList<Member>();
for(Integer id : ids) {
System.out.println(id);
Member m = new Member();
m.setId(id);
m.setName("angus");
mems.add(m);
}
return mems;
}
}
package org.crazyit.cloud.collapser;
import java.util.concurrent.Future;
import org.crazyit.cloud.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CollController {
@Autowired
private CollService collService;
@RequestMapping(value = "/coll", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public String testCollapse() throws Exception {
// 连续执行3次请求
Future<Member> f1 = collService.getMember(1);
Future<Member> f2 = collService.getMember(2);
Future<Member> f3 = collService.getMember(3);
Member p1 = f1.get();
Member p2 = f2.get();
Member p3 = f3.get();
return "";
}
}
9.spring cloud整合Hystrix
(1)使用RestTemplate
(a)Service
package org.crazyit.cloud;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
@Service
//@DefaultProperties(defaultFallback = "getMemberFallback")
public class MemberService {
@Autowired
private RestTemplate restTpl;
@HystrixCommand(fallbackMethod = "getMemberFallback", groupKey = "MemberGroup", commandKey = "MemberCommandKey",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")
}, threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "2")
})
public Member getMember(Integer id) {
try {
Thread.sleep(2000);
} catch (Exception e) {
}
Member member = restTpl.getForObject(
"http://spring-hy-member/member/{id}", Member.class, id);
return member;
}
public Member getMemberFallback(Integer id) {
Member m = new Member();
m.setId(1);
m.setName("error member");
return m;
}
}
(b)controller
package org.crazyit.cloud;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class TestController {
@Autowired
private MemberService memberService;
@RequestMapping(value = "/router", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public Member router() {
return memberService.getMember(1);
}
}
10.Hystrix与Feign整合(重点)
直接看例子:其中hello是测试回退的方法;toHello是测试断路器的方法
package org.crazyit.cloud.feign;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient(name = "spring-hy-member", fallback = HelloClientFallback.class)
public interface HelloClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
public String hello();
@RequestMapping(method = RequestMethod.GET, value = "/toHello")
public String toHello();
}
package org.crazyit.cloud.feign;
import org.springframework.stereotype.Component;
@Component
public class HelloClientFallback implements HelloClient {
public String hello() {
return "fallback hello";
}
public String toHello() {
return "fallback timeout hello";
}
}
配置断路器的超时时间、和打开需要的请求次数(标红的部分),hello方法不需要该配置,hello
关于断路器的配置是写在代码中的(标签)。
配置指定方法(toHello())超过500毫秒则执行回退方法:
server:
port: 8081
spring:
application:
name: spring-hy-sale
feign:
hystrix:
enabled: true
hystrix:
command:
HelloClient#toHello():
execution:
isolation:
thread:
timeoutInMilliseconds: 500
circuitBreaker:
requestVolumeThreshold: 3
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
配置所有方法如执行超过500毫秒就执行回退方法:
server:
port: 8081
spring:
application:
name: spring-hy-sale
feign:
hystrix:
enabled: true
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 500
circuitBreaker:
requestVolumeThreshold: 3
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
Zuul
1.功能和特性:
(1)提供代理、过滤、路由等功能,不用暴露服务,提升安全性;
(2)与Ribbon是整合的,先天支持负载均衡
(3)Zuul可做负载均衡和反向代理
关于负载均衡说明:下面的例子中若有若干个服务,服务的serviceId的名称相同,Zull就
可以根据serviceId做负载均衡
zuul:
routes:
#Ribbon路由
consumer:
path: /consumer/**
serviceId: consumer1
2.工作原理如下面三张图:
上图中三类过滤器:“pre”过滤器,"routing"过滤器、"post"过滤器,代表的都是一个阶段,其由
若干个过滤器组成(由若干个过滤器组成),其中“routing”过滤器是真正转发请求的,具有路由
功能。
下图体现了过滤器的优先级,并代表@EnableZuulProxy。其中数字越小优先级越低,在
routing过滤阶段,只会选择一个过滤器去执行。
下图是@EnableZuulServer的执行流程,灰掉的是不执行的过滤器,所以@EnableZuulServer
是一个低配版的@EnableZuulProxy
3.加入Zuul后的集群
4.更换Http客户端
默认使用HttpClient
可更换OkHttpClient
ribbon.okhttp.enabled=true
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
OKHttpClient是一款http客户端,用来发送Http请求的。
5.路由配置
(1)路由的分类
(a)简单路由:以Http或Https开头的是简单路由,例子如下
zuul:
routes:
#简单路由第一种配置方式
testSimpleRoute:
path: /simpleRouteTest/163
url: http://www.163.com
#简单路由第二种配置方式(省略path)
simpleRouteTest163:
url: http://www.163.com
(b)跳转路由:以forward开头的,使用的是dispatcher的forward,响应请求的其实是
forward去的地址。
zuul:
routes:
#跳转路由
testForward:
path: /foo/**
url: forward:/testForward
(c)Ribbo路由
zuul:
routes:
#Ribbon路由
consumer:
path: /consumer/**
serviceId: consumer1
(d)自定义路由:没学明白
(2).请求头配置,可以指定请求头信息不被转发,默认accept-language, cookie是不转发的
如下例:
zuul:
sensitiveHeaders: accept-language, cookie
(3).路由端点:展示路由的映射关系
6.过滤器
(1)Zuu自带的过滤器及其优先级
其中数字越小优先级越高,routing阶段的过滤器只会选择一个去执行(通过外部的一个值来保证
过滤器执行的唯一性)、
(2)动态路由:没讲