https://github.com/carnellj/spmia-chapter5
什么是客户端弹性模式?
客户端弹性软件模式的重点是,在远程服务发生错误或表现不佳时保护远程资源(另一个微服务调用或数据库查询)的客户端免于崩溃。这些模式的目标是让客户端“快速失败”,而不消耗注诸如数据库连接和线程池之类的宝贵资源,并且可以防止远程服务的问题向客户端的消费者进行“上游”传播。
有4中客户端弹性模式:
-
客户端负载均衡(client load balance)模式
-
断路器(circuit breaker)模式
-
后备(fallback)模式
-
舱壁(bulkhead)模式
1、客户端负载均衡模式
客户端负载均衡模式涉及让客户端从服务发现代理(Eureka)查找服务的所有实例,然后缓存服务实例的物理位置。每当服务消费者需要调用该服务实例时,客户端负载均衡器将从它维护的服务位置池返回一个位置。
负载均衡器可以检测服务实例是否抛出错误或表现不佳,如果客户端负载均衡器检测到问题,它可以从可用服务位置池中移除该服务实例,并防止将来的服务调用访问该服务实例。
简单说:负载均衡就是不需要每次调用都从服务发现Eureka获取服务物理位置,第一次访问后负载均衡器会缓存服务物理位置,再次访问时就不需要再向Eureka拿服务物理位置,直接从负载均衡器获取
2、断路器模式
当远程服务被调用时,断路器将监视这个调用。如果调用时间太长,断路器将会介入并中断调用。此外,断路器将监视所有对远程资源的调用,如果对某一个远程资源的调用失败次数足够多,那么断路器实现就会出现并采取快速失败,组织将来调用失败的远程资源。
简单说:一个服务被调用时会被断路器监听,如果调用时间过长会直接返回失败,并在规定时间内不允许其他服务消费者调用该服务直接返回失败;在某个时刻会放开部分服务消费者调用该服务,在规定次数内调用成功,则重新放开服务可正常调用
3、后备模式
当远程服务调用失败时,服务消费者将执行替代代码路径,并尝试通过其他方式执行操作,而不是生成一个异常。这通常涉及从另一数据源查找数据或将用户的请求进行排队以供将来处理。用户的调用结果不会显示为提示问题的异常,但用户可能会被告知,它们的请求要在晚些时候被满足。
简单说:断路器模式开启时,提供的后备服务提供调用
4、舱壁模式
通过使用舱壁模式,可以把远程资源的调用分到线程池中,并降低一个缓慢的远程资源调用拖垮整个应用程序的风险。线程池充当服务的“舱壁”。每个远程资源都是隔离的,并分配给线程池。如果一个服务响应缓慢,那么这种服务调用的线程池就会饱和并停止处理请求,而对其他服务的服务调用则不会变得饱和,因为它们被分配给了其他线程池。
简单说:服务消费者调用的服务牵涉到其他服务的调用,服务之间有各自有线程池隔离,在某个服务出现问题时不会影响其他服务的调用,避免了一个服务出问题出现连锁反应将其他应用程序服务拖垮
Hystrix断路器处理
Hystrix断路器检查流程
Hystrix集成
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<!-- 可选,因为某些版本的Hystrix默认带有的hystrix-javanica与SpringBoot版本不兼容会抛异常,该版本兼容Camden.SR5 -->
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
<version>1.5.9</version>
</dependency>
@SpringBootApplication
@EnableEurekaClient
@EnalbeCircuitBreaker // 集成Hystrix
public class Application {
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// @HystrixCommand将动态生成一个代理,该代理将包装该方法,并通过专门用于处理远程调用的线程池来管理对该方法的所有调用
// 如果断路器开启,调用该服务会抛出com.netflix.hystrix.exception.HystrixRuntimeException返回500
// 注意:当前是默认配置不使用其他属性,会导致所有远程服务调用都放在同一个线程池下,这可能会导致应用程序中出现问题,所以一般情况是根据服务需要配置对应属性
@HystrixCommand
public List<License> getLicenseByOrg(String organizationId) {
randomlyRunLong(); // 让调用该方法概率性出现延迟,表现断路器模式
return licenseRepository.findByOrganizationId(organizationId);
}
private void randomlyRunLong() {
Random rand = new Random();
int randomNum = rand.nextInt((3 - 1) + 1) + 1;
if (randomNum == 3)
sleep();
}
private void sleep() {
try {
Thread.sleep(11000); // Hystrix默认断路器时间为1s,超过1s会开启断路器
} catch(InterruptedException e) {
e.printStackTrace();
}
}
设置断路器超时时间
// commandPropreties接收一个@HystrixProperty数组,这里设置超时时间为12s
// 注意:开发人员应避免在Hystrix调用上增加默认超时的诱惑,除非实在无法解决运行缓慢的服务调用;如果确实遇到一些其他服务调用需要更长时间的服务调用,务必将这些服务调用隔离到单独的线程池中
@HystrixCommand(
commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value="12000")
}
)
public List<License> getLicenseByOrg(String organizationId) {
return licenseRepository.findByOrganizationId(organizationId);
}
后备处理
// 设置Hystrix断路器开启时,服务调用指定的后备方法buildFallbackLicenseList
// 注意:对后备方法有一定要求:
// 1、后备方法必须在所调用方法的类中
// 2、后备方法的参数要与调用服务的方法参数一致
@HystrixCommand(fallbackMethod = "buildFallbackLicenseList")
public List<License> getLicenseByOrg(String organizationId) {
return licenseRepository.findByOrganizationId(organizationId);
}
private List<License> buildFallbackLicenseList(String organizationId) {
List<License> fallbackList = new ArrayList<>();
License license = new License()
.withId("000000-00-0000")
.withOrganizationId(organizationId)
.withProductName("Sorry no licensing information currently available");
fallbackList.add(license);
return fallbackList;
}
实现舱壁模式
默认情况下,所有的Hystrix命令都将共享同一个线程池来处理请求。这个线程池将有10个线程来处理远程服务调用。
// threadPoolKey:提供一个自定义的线程池名称,线程池相关属性通过threadPoolProperties对象数组指定
// coreSize:定义线程池中线程最大数量
// maxQueueSize:定义一个位于线程池前的队列,它可以对传入的请求进行排队
// maxQueueSize的注意事项:
// 1、如果maxQueueSize=-1,则将使用Java SynchronousQueue来保存所有传入的请求。同步队列本质上会强制要求正在处理中的请求数量永远不能超过线程池中可用线程的数量;如果maxQueueSize>1,则将使用Java LinkedBlockingQueue,它允许开发人员即使所有线程都在忙于处理请求,也能对请求进行排队
// 2、maxQueue属性智能在线程池首次初始化时设置(例如应用程序启动时)。Hystrix允许通过queueSizeRejectionThreashold动态更改队列大小,但只有在maxQueue的值大于0时才能设置该属性
// maxQueueSize注意事项简单说:
// 1、maxQueueSize=-1,使用SynchronousQueue;maxQueueSize>1,使用LinkedBlockingQueue
// 2、maxQueueSize>0才能使用queueSizeREjectionThreashold属性动态更改队列大小
// 自定义线程池适当大小算法:
// 服务在健康状态时每秒支撑的最大请求数 x 第99百分位延迟时间(以秒为单位) + 用于缓冲的少量额外线程
@HystrixCommand(
fallbackMethod="buildFallbackLicenseList",
threadPoolKey = "licenseByOrgThreadPool"
threadPoolProperties = {
@HystrixProperty(name="coreSize", value="30")
@HystrixProperty(name="maxQueueSize" value="10")
}
)
public List<License> getLicenseByOrg(String organizationId) {
return licenseRepository.findByOrganizationId(organizationId);
}
Hystrix断路原理
Hystrix做的第一件事就是查看在10s内发生的调用数量。如果调用次数少于在这个窗口内需要发生的最小调用次数,那么即使有几个调用失败,Hystrix也不会采取行动。
在10s窗口内达到最少的远程资源调用次数时,Hystrix将开始查看整体故障的百分比。如果故障的总体百分比超过阀值,Hystrix将触发断路器,使将来几乎所有的调用都失败。错误阀值的默认值为50%;如果远程调用失败的百分比未达到要求的阀值,并且10s窗口已过去,Hystrix将重置断路器的统计信息。
当Hystrix在一个远程调用上“跳闸”断路器时,它将尝试启动一个新的活动窗口。每隔5s(该值可配置),Hystrix会让一个调用到达这个苦苦挣扎的服务。如果调用成功,Hystrix将重置断路器并重新开始让调用通过。如果调用失败,Hystrix将保持断路器断开,并在另一个5s里再次尝试上述步骤。
@HystricCommand(
fallbackMethod = "buildFallbackLicenseList",
threadPoolKey = "licenseByOrgThreadPool",
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "30")
@HystricProperty(name = "maxQueueSize", value = "10")
},
commandPoolProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10),
@HystricProperty(name = "circuitBreaker.errorThresholdPercentage", value = "75")
@HystricProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "7000"),
@HystricProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "15000"),
@HystricProperty(name = "metrics.rollingStats.numBuckets", value = "5")
}
)
public List<License> getLicensesByOrg(String organizationId) {
logger.debug("getLicensesByOrg Correlation id:{}", UserContextHolder.getContext().getCorrelationId());
randomlyRunLong(); // 测试概率出现断路器熔断
return licenseRepository.findByOrganizationId(organizationId);
}
-
circuitBreaker.requestVolumeThreshold
:控制Hystrix考虑将该断路器跳闸之前,在10s之内必须发生的连续调用数量**【即设置断路器启动要调用失败的次数,这里设置了10次】** -
circuitBreaker.errorThresholdPercentage
:超过circuitBreaker.requestVolumeThreshold
值之后在断路器跳闸之前必须达到的调用失败(由于超时、抛出异常或返回HTTP 500)百分比**【即设置故障阀值百分比,这里设置75%】** -
circuitBreaker.sleepWindowInMilliseconds
:在断路器跳闸之后,Hystrix允许另一个调用通过以便查看服务是否恢复健康之前Hystrix的休眠时间**【即设置恢复重置断路器放开服务调用的时间,这里设置7s一次】** -
metrics.rollingStats.timeInMilliseconds
:控制Hystrix用来监听服务调用问题的窗口大小,默认值为10s**【即设置断路器熔断检查时间,这里设为15s】** -
metrics.rollingStats.numBuckets
:在定义的滚动窗口中收集统计信息的次数。在这个窗口中,Hystrix在桶(bucket)中收集度量数据,并检查这些桶中的统计信息,以确定远程资源调用是否失败。给metrics.rollingStats.timeInMilliseconds
设置的值必须能被定义的桶的数量值整除
Hystrix配置
Hystrix有3个配置级别:
-
整个应用程序级别的默认值
-
类级别的默认值
-
在类中定义的线程池级别
每个Hystrix属性都有默认值,这些值将被应用程序中的每个@HystrixCommand注解所使用,除非这些属性值在Java类级别被设置,或者被类中单个Hystrix线程池级别的值覆盖。
Hystrix允许开发人员在类级别设置默认参数,以便特定类中的所有Hystrix命令共享相同的配置。
// @DefaultProperties类级属性设置Hystrix,@HystricCommand只能在方法中设置
@DefaultProperties(
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "10000")
}
)
class Service {....}
在生成环境中,最有可能需要调整的Hystrix数据(超时参数、线程池计数)将被外部化到Spring Cloud Config
线程上下文和Hystrix
当一个 @HystrixCommand
被执行时,它可以使用两种不同的隔离策略——THREAD(线程)和SEMAPHORE(信号量)来运行。在默认情况下,Hystrix以THREAD隔离策略运行。
-
THREAD:用于保护调用的每个Hystrix命令都在一个单独的线程池中运行,该线程池不与父线程共享它的上下文。这意味着Hystrix可以在它的控制下终端线程的执行,而不必担心中断与执行原始调用的父线程相关的其他活动。Hystrix团队建议开发人员对大多数命令使用默认的THREAD隔离策略
-
SEMAPHORE:Hystrix管理由@HystrixCommand注解保护的分布式调用,而不需要启动一个新线程,并且如果调用超时,就会中断父线程。在同步容器服务器环境(Tomcat)中,中断父线程将导致抛出开发人员无法捕获的异常。该隔离模式适用于服务量很大且正在使用异步I/O编程模型运行的情况
@HystrixCommand(
commandProperties = {
@HystricProperty(name = "execution.isolation.strategy", value = "SEMAPHORE")
}
)
线程上下文和Hystrix
在默认情况下,Hystrix不会将父线程的上下文传播到由Hystrix命令管理的线程中。通常在基于REST的环境中,开发人员希望将上下文信息传递给服务调用,这将有助于在运维上管理该服务。
例如,可以在REST调用的HTTP首部中传递关联ID(correlation ID)或验证令牌,然后将其传播到任何下游服务调用。关联ID是唯一标识符,该标识符可用于在单个事务中跨多个服务调用进行跟踪。
要使服务调用中的任何地方都可以使用此值,开发人员可以使用Spring过滤器类(这部分知识与Zuul路由有关)来拦截对REST服务的每个调用,并从传入的HTTP请求中检索此信息,然后将此上下文信息存储在自定义的UserContext对象中。然后,在任何需要在REST服务调用中访问该值的时候,可以从ThreadLocal存储变量中检索UserContext并读取该值。(简单说,就是拦截HTTP请求,将HTTP请求的上下文信息存储在外部,让其他地方都可以使用这个上下文获取相关信息)
// 存储上下文信息对象类
@Component
public class UserContext {
public static final String CORRELATION_ID = "tmx-correlation-id";
public static final String AUTH_TOKEN = "tmx-auth-token";
public static final String USER_ID = "tmx-user-id";
public static final String ORG_ID = "tmx-org-id";
private String correlationId = new String();
private String authToken = new String();
private String userId = new String();
private String orgId = new String();
getter and setter....
}
// 上下文信息管理类
public class UserContextHolder {
private static final ThreadLocal<UserContext> userContext = new ThreadLocal<UserContext>();
public static final UserContext getContext() {
UserContext context = userContext.get();
if (context == null) {
context = createEmptyContext();
userContext.set(context);
}
return userContext.get();
}
public static final void setContext(UserContext context) {
Assert.notNull(context, "Only non-null UserContext instances are permitted");
userContext.set(context);
}
public static final UserContext createEmptyContext() {
return new UserContext();
}
}
// 拦截HTTP请求,将HTTP的Header相关信息存储到自定义的上下文类中
@Component
public class UserContextFilter implements Filter {
private static final Logger logger = Logger.getLogger(UserContextFilter.class);
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
UsetContextHolder
.getContext()
.setCorrelationId(httpServletRequest.getHeader(UserContext.CORRELATION_ID));
UserContextHolder
.getContext()
.setUserId(httpServletRequest.getHeader(UserContext.USER_ID));
UserContextHolder
.getContext()
.setAuthTOken(httpServletRequest.getHeader(UserContext.AUTH_TOKEN));
UserContextHolder
.getContext()
.setOrgId(httpServletRequest.getHeader(UserContext.ORG_ID));
filterChain.doFilter(httpServletRequest, servletResponse);
}
}
HystrixConcurrencyStrategy
Hystrix允许开发人员定义一种自定义的并发策略,它将包装Hystrix调用,并允许开发人员将附加的父线程上下文注入由Hystrix命令管理的线程中。实现自定义HystrixConcurrencyStrategy需要执行以下3个步骤:
-
定义自定义的Hystrix并发策略类
-
定义一个Callable类,将UserContext注入Hystrix命令中
-
配置Spring Cloud以使用自定义Hystrix并发策略
在默认情况下,Hystrix只允许为应用程序定义一个HystrixConcurrencyStrategy。Spring Cloud已经定义了一个并发策略用于处理Spring安全信息的传播。Spring Cloud允许将Hystrix并发策略链接在一起,以便我们可以定义和使用自己的并发策略,方法是将其“插入”到Hystrix并发策略中。
public class ThreadLocalAwareStrategy extends HystrixConcurrencyStrategy {
// Spring Cloud提供的Hystrix并发策略类,在Spring Cloud提供的基础上扩展
private HystrixConcurrencyStrategy existingConcurrencyStrategy;
public ThreadLocalAwareStrategy(HystrixConcurrencyStrategy existingConcurrencyStrategy) {
this.existingConcurrencyStrategy = existingConcurrencyStrategy;
}
@Override
public BlockingQueue<Runnable> getBolckingQueue(int maxQueueSize) {
return existingConcurrencyStrategy != null
? existingConcurrencyStrategy.getBlockingQueue(maxQueueSize)
: super.getBlockingQueue(maxQueueSize);
}
@Override
public <T> HystrixRequestVariable<T> getRequestVariable(HystrixRequestVariableLifecycle<T> rv) {
....
}
@Override
public ThreadPoolExcecutor getThreadPool(
HystrixThreadPoolKey threadPoolKey,
HystrixProperty<Integer> corePoolSize,
HystrixProperty<Integer> maximumPoolSize,
HystrixProperty<Integer> keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue
) {
.....
}
// 需要自定义一个需求需要的Callable插入Spring Cloud提供的并发策略类HystrixConcurrencyStrategy
@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
Callable delegate = new DelegatingUserContextCallable<T>(callable, UserContextHolder.getContext());
return existingConcurrencyStrategy != null
? existingConcurrencyStrategy.wrapCallable(delegate)
: super.wrapCallable(delegate);
}
}
// 自定义Callable
public final class DelegatingUserContextCallable<V> implements Callable<V> {
private final Callable<V> delegate;
private UserContext originalUserContext;
public DelegatingUserContextCallable(Callable<V> delegate, UserContext userContext) {
this.delegate = delegate;
this.originalUserContext = userContext;
}
// call()方法在被@HystricCommand注解保护的方法之前调用,在此将UserContext注入
public V call() throws Exception {
UserContextHolder.setContext(originalUserContext);
try {
return delegate.call();
} finally {
this.originalUserContext = null;
}
}
public static <V> Callable<V> create(Callable<V> delegate, UserContext userContext) {
return new DeletagingUserContextCallable<V>(delegate, userContext);
}
}
@Configuration
public class ThreadLocalConfiguration {
@Autowired(required = false)
private HystrixConcurrencyStrategy existingConcurrencyStrategy;
@PostConstruct
public void init() {
// 将Hystrix原有的组件获取出来
HystrixEventNotifier eventNotifier = new HystrixPlugins
.getInstance().getEventNotifier();
HystrixMetricsPublisher metricsPublisher = new HystrixPlugins
.getInstance().getMetricsPublisher();
HystrixPropertiesStrategy propertiesStrategy = new HystrixPlugins
.getInstance().getPropertiesStrategy();
HystrixCommandExecutionHook commandExecutionHook = new HystrixPlugins
.getInstance().getCommandExecutionHook();
// 插入自定义的并发策略类
HystrixPlugins
.getInstance()
.registerConcurrencyStrategy(new ThreadLocalAwareStrategy(existingConcurrencyStrategy));
// 将Hystrix原有的组件重新注册
HystrixPlugins
.getInstance()
.registerEventNotifier(eventNotifier);
HystrixPlugins
.getInstance()
.registerMetricsPublisher(metricsPublisher);
HystricPlugins
.getInstance()
.registerPropertiesStrategy(propertiesStrategy);
HystricPlugins
.getInstance()
.registerCommandExecutionHook(commandExecutionHook);
}
}