5.9 线程上下文和Hystrix

当一个@HystrixCommand被执行时,它可以使用两种不同的隔离策略——THREAD(线程)和 SEMAPHORE(信号量)来运行。在默认情况下,Hystrix 以 THREAD隔离策略运行。用于保护调用的每个Hystrix命令都在一个单独的线程池中运行,该线程池不与父线程共享它的上下文。这意味着Hystrix可以在它的控制下中断线程的执行,而不必担心中断与执行原始调用的父线程相关的其他活动。
    通过基于SEMAPHORE的隔离,Hystrix管理由@HystrixCommand注解保护的分布式调用,而不需要启动一个新线程,并且如果调用超时,就会中断父线程。在同步容器服务器环境(Tomcat)中,中断父线程将导致抛出开发人员无法捕获的异常。这可能会给编写代码的开发人员带来意想不到的后果,因为他们无法捕获抛出的异常或执行任何资源清理或错误处理。
    
要控制命令池的隔离设置,开发人员可以在自己的@HystrixCommand注解上设置commandProperties属性。例如,如果要在Hystrix命令中设置隔离级别以便使用SEMAPHORE隔离,则可以使用:
    
@HystrixCommand(commandProperties = {
@HystrixProperty(name="execution.isolation.strategy", value="SEMAPHORE")
})
    注意
    
    在默认情况下,Hystrix团队建议开发人员对大多数命令使用默认的THREAD隔离策略。这将保持开发人员和父线程之间更高层次的隔离。THREAD隔离比SEMAPHORE隔离更重,SEMAPHORE隔离模型更轻量级,SEMAPHORE隔离模型适用于服务量很大且正在使用异步I/O编程模型(假设使用的是像Netty这样的异步I/O容器)运行的情况。
 
5.9.1 ThreadLocal与Hystrix
    在默认情况下,Hystrix不会将父线程的上下文传播到由Hystrix命令管理的线程中。例如,在默认情况下,对被父线程调用并由@HystrixComman保护的方法而言,在父线程中设置为ThreadLocal值的值都是不可用的(再强调一次,这是假设当前使用的是THREAD隔离级别)。
    这听起来可能会有一点难以理解,所以让我们看一个具体的例子。通常在基于REST的环境中,开发人员希望将上下文信息传递给服务调用,这将有助于在运维上管理该服务。例如,可以在REST调用的HTTP首部中传递关联ID(correlation ID)或验证令牌,然后将其传播到任何下游服务调用。关联ID是唯一标识符,该标识符可用于在单个事务中跨多个服务调用进行跟踪。
    要使服务调用中的任何地方都可以使用此值,开发人员可以使用Spring过滤器类来拦截对REST服务的每个调用,并从传入的HTTP请求中检索此信息,然后将此上下文信息存储在自定义的UserContext对象中。然后,在任何需要在REST服务调用中访问该值的时候,可以从ThreadLocal存储变量中检索UserContext并读取该值。代码清单5-8展示了一个示例Spring 过滤器,读者可以在许可服务中使用它。读者可以在licensingservice/src/main/java/com/thoughtmechanix/licenses/utils/UserContextFilter.java中找到这段代码。
    
代码清单5-8 UserContextFilter解析HTTP首部并检索数据
 
package com.thoughtmechanix.licenses.utils; 
// 为了简洁,省略了一些代码 
@Component public class UserContextFilter implements Filter {     
private static final Logger logger =LoggerFactory.getLogger(UserContextFilter.class);    
 @Override     
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,  FilterChain filterChain)     
throws IOException, ServletException{
HttpServletRequest httpServletRequest =(HttpServletRequest) servletRequest;         
UserContextHolder.getContext() .setCorrelationId(  httpServletRequest.getHeader(UserContext.CORRELATION_ID) );  
⇽---  检索调用的HTTP首部中设置的值,将这些值赋给存储在UserContextHolder中的UserContext       
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);     
}
 
UserContextHolder类用于将UserContext存储在ThreadLocal类中。一旦存储在ThreadLocal中,任何为请求执行的代码都将使用存储在UserContextHolder中的UserContext对象。代码清单5-9展示了UserContextHolder类。这个类可以在licensing-service/src/main/ java/com/thoughtmechanix/licenses/utils/UserContextHolder.java中找到。
    
代码清单5-9 所有UserContext数据都是由UserContextHolder管理的
public class UserContextHolder {
private static final ThreadLocal<UserContext> userContext
= new ThreadLocal<UserContext>();
←getContext()方法将检索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();
}
}
 
此时,可以向许可证服务添加一些日志语句。我们将添加日志记录到以下许可证服务类和方法。
    com/thoughtmechanix/licenses/utils/UserContextFilter.java中UserContextFilter类的doFilter()方法。
    com/thoughtmechanix/licenses/controllers/LicenseServiceController.Java中LicenseService Controller的getLicenses()方法。
    com/thoughtmechanix/licenses/services/LicenseService.java中LicenseService类的getLicensesByOrg()方法。此方法通过@HystrixCommand标注。
    
接下来,将使用名为tmx-correlation-id和值为TEST-CORRELATION-ID的HTTP首部来传递关联ID以调用服务。图5-10展示了在Postman中使用HTTP GET来访问 http://localhost: 8080/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/。
 
 
图5-10 向许可证服务调用的HTTP首部添加关联ID
    一旦提交了这个调用,当它流经UserContext、LicenseServiceController和LicenseServer类时,我们将看到3条日志消息记录了传入的关联ID:
 
UserContext Correlation id: TEST-CORRELATION-ID 
LicenseServiceController Correlation id: TEST-CORRELATION-ID 
LicenseService.getLicenseByOrg Correlation:
    
正如预期的那样,一旦这个调用使用了由Hystrix保护的LicenseService.getLicensesByOrg()方法,就无法得到关联ID的值。幸运的是,Hystrix和Spring Cloud提供了一种机制,可以将父线程的上下文传播到由Hystrix线程池管理的线程。这种机制被称为HystrixConcurrencyStrategy。
    
5.9.2 HystrixConcurrencyStrategy实战
    Hystrix允许开发人员定义一种自定义的并发策略,它将包装Hystrix调用,并允许开发人员将附加的父线程上下文注入由Hystrix命令管理的线程中。实现自定义HystrixConcurrencyStrategy需要执行以下3个操作。
    (1)定义自定义的Hystrix并发策略类。
    (2)定义一个Callable类,将UserContext注入Hystrix命令中。
    (3)配置Spring Cloud以使用自定义Hystrix并发策略。
    HystrixConcurrencyStrategy的所有示例可以在licensing-service/src/main/java/com/ thoughtmechanix/licenses/hystrix包中找到。
    1.自定义Hystrix并发策略类
 
我们需要做的第一件事,就是定义自己的HystrixConcurrencyStrategy。在默认情况下,Hystrix只允许为应用程序定义一个HystrixConcurrencyStrategy。Spring Cloud已经定义了一个并发策略用于处理Spring安全信息的传播。幸运的是,Spring Cloud允许将Hystrix并发策略链接在一起,以便我们可以定义和使用自己的并发策略,方法是将其“插入”到Hystrix并发策略中。
    Hystrix并发策略的实现可以在许可证服务hystrix包的ThreadLocalAwareStrategy. java中找到,代码清单5-10展示了这个类的代码。
    
代码清单5-10 定义自己的Hystrix并发策略
    package com.thoughtmechanix.licenses.hystrix; 
//  为了简洁,省略了import语句 
public class ThreadLocalAwareStrategy extends HystrixConcurrencyStrategy{  
⇽---  扩展基本的Hystrix ConcurrencyStrategy类     
private HystrixConcurrencyStrategy existingConcurrencyStrategy;    
 
⇽---  Spring Cloud 已经定义了一个并发类。将已存在的并发策略传入自定义的 HystrixConcurrencyStrategy的类构造器中     
 
public ThreadLocalAwareStrategy(HystrixConcurrencyStrategy existingConcurrencyStrategy) {  
this.existingConcurrencyStrategy = existingConcurrencyStrategy;     
}     
 
⇽---  有几个方法需要重写。要么调用existingConcurrencyStrategy方法实现,要么调用基类HystrixConcurrencyStrategy         
 
@Override     
public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize){  
return existingConcurrencyStrategy != null              
? existingConcurrencyStrategy.getBlockingQueue(maxQueueSize)  :              : super.getBlockingQueue(maxQueueSize);    
 }     
 
@Override     
public <T> HystrixRequestVariable<T> getRequestVariable(  
HystrixRequestVariableLifecycle<T> rv)
{//Code removed for conciseness }
 
 
@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
return existingConcurrencyStrategy != null
? existingConcurrencyStrategy.wrapCallable(
⇽---  注入Callable实现,它将设置UserContext       
new DelegatingUserContextCallable<T>(
callable,
UserContextHolder.getContext()))
: super.wrapCallable(
new DelegatingUserContextCallable<T>(
callable,
UserContextHolder.getContext()));
}
}
 
注意代码清单5-10中类实现中的几件事情。首先,因为Spring Cloud已经定义了一个HystrixConcurrencyStrategy,所以所有可能被覆盖的方法都需要检查现有的并发策略是否存在,然后或调用现有的并发策略的方法或调用基类的Hystrix并发策略方法。开发人员必须将此作为惯例,以确保正确地调用已存在的Spring Cloud的HystrixConcurrencyStrategy,该并发策略用于处理安全。否则,在受Hystrix保护的代码中尝试使用Spring安全上下文时,可能会出现难以解决的问题。
 
要注意的第二件事是代码清单5-10中的wrapCallable()方法。在此方法中,我们传递了Callable的实现DelegatingUserContextCallable,用来将UserContext从执行用户REST服务调用的父线程,设置为保护正在进行工作的方法的Hystrix命令线程。
    
2.定义一个Java Callable类,将UserContext注入Hystrix命令中
    将父线程的线程上下文传播到Hystrix命令的下一步,是实现执行传播的Callable类。对于本示例,这个Callable类DelegatingUserContextCallable类位于hystrix包的DelegatingUserContextCallable.java中。代码清单5-11展示了这个类的代码。
    
代码清单5-11 使用DelegatingUserContextCallable传播UserContext
//  为了简洁,省略了import语句
public final class DelegatingUserContextCallable<V> implements Callable<V> {
private final Callable<V> delegate;
private UserContext originalUserContext;
⇽---  原始Callable类将被传递到自定义的Callable类,自定义Callable将调用Hystrix保护的代码和来自父线程的UserContext 
public DelegatingUserContextCallable(Callable<V> delegate,UserContext userContext) {
this.delegate = delegate;
this.originalUserContext = userContext;
}
⇽---  call()方法在被@HystrixCommand注解保护的方法之前调用   
public V call() throws Exception {
⇽---  UserContext设置之后,在Hystrix保护的方法上调用call()方法,如LicenseServer.getLicenseByOrg()方法  
 
UserContextHolder.setContext( originalUserContext );
try {
return delegate.call();
}
finally {
this.originalUserContext = null;
}
}
public static <V> Callable<V> create(Callable<V> delegate,UserContext userContext) {
return new DelegatingUserContextCallable<V>(delegate, userContext);
}
}
    
当调用Hystrix保护的方法时,Hystrix和Spring Cloud将实例化DelegatingUser-ContextCallable类的一个实例,传入一个通常由Hystrix命令池管理的线程调用的Callable类。在代码清单5-11中,此Callable类存储在名为delegate的Java属性中。从概念上讲,可以将delegate属性视为由@HystrixCommand注解保护的方法的句柄。
    除了委托的Callable类之外,Spring Cloud也将UserContext对象从发起调用的父线程传递出去。这两个值在创建DelegatingUserContextCallable实例时设置,实际的操作将发生在类的call()方法中。
    在call()方法中要做的第一件事是通过UserContextHolder.setContext()方法设置UserContext。记住,setContext()方法将UserContext对象存储在ThreadLocal变量中,这个ThreadLocal变量特定于正在运行的线程。设置了UserContext之后,就会调用委托的Callable类的call()方法。调用delegate.call()会调用由@HystrixCommand注解保护的方法。
    3.配置Spring Cloud以使用自定义Hystrix并发策略
    我们已经通过ThreadLocalAwareStrategy类实现了 HystrixConcurrencyStrategy类,并通过DelegatingUserContextCallable类定义了Callable类,现在,需要将它们挂钩在Spring Cloud和Hystrix中。要做到这一点,则需要定义一个新的配置类ThreadLocalConfiguration,如代码清单5-12所示。
    
代码清单5-12 将自定义的 HystrixConcurrencyStrategy类挂钩到Spring Cloud中
 
//  为了简洁,省略了import语句
@Configuration
public class ThreadLocalConfiguration {
@Autowired(required = false)
⇽--- 当构造配置对象时,它将自动装配在现有的HystrixConcurrencyStrategy中   
private HystrixConcurrencyStrategy existingConcurrencyStrategy;
@PostConstruct
public void init() {
⇽--- 因为要注册一个新的并发策略,所以要获取所有其他的Hystrix组件,然后重新设置Hystrix插件
// 保留现有的Hystrix插件的引用      
HystrixEventNotifier eventNotifier = HystrixPlugins
.getInstance()
.getEventNotifier();
HystrixMetricsPublisher metricsPublisher = HystrixPlugins
.getInstance()
.getMetricsPublisher();
HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins
.getInstance()
.getPropertiesStrategy();
HystrixCommandExecutionHook commandExecutionHook =HystrixPlugins
.getInstance()
.getCommandExecutionHook();
HystrixPlugins.reset();
⇽--- 使用Hystrix插件注册自定义的Hystrix并发策略(ThreadConcurrency Strategy)
HystrixPlugins.getInstance()
.registerConcurrencyStrategy(
new ThreadLocalAwareStrategy(existingConcurrencyStrategy));
←然后使用Hystrix插件重新注册所有的Hystrix组件。
HystrixPlugins.getInstance()
.registerEventNotifier(eventNotifier);
HystrixPlugins.getInstance()
.registerMetricsPublisher(metricsPublisher);
HystrixPlugins.getInstance()
.registerPropertiesStrategy(propertiesStrategy);
HystrixPlugins.getInstance()
.registerCommandExecutionHook(commandExecutionHook);
}
   
 这个Spring配置类基本上重新构建了管理运行在服务中所有不同组件的Hystrix插件。在init()方法中,我们获取该插件使用的所有Hystrix组件的引用。然后注册自定义的Hystrix并发策略(ThreadLocalAwareStrategy)。
    
HystrixPlugins.getInstance().registerConcurrencyStrategy(     new ThreadLocalAwareStrategy(ex
 
记住,Hystrix只允许一个HystrixConcurrencyStrategy。Spring将尝试自动装配在现有的任何HystrixConcurrencyStrategy(如果它存在)中。最后,完成所有的工作之后,我们使用Hystrix插件把在init()方法开头获取的原始Hystrix组件重新注册回来。
    有了这些,现在可以重新构建并重新启动许可证服务,并通过之前图5-10所示的GET( http://localhost:8080/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/)来调用这个服务。当这个调用完成后,在控制台窗口中应该看到以下输出:
    
UserContext Correlation id: TEST-CORRELATION-ID 
LicenseServiceController Correlation id: TEST-CORRELATION-ID
LicenseService.getLicenseByOrg Correlation: TEST-CORRELATION-ID
    
为了产生一个小小的结果需要做很多工作,但是,当使用Hystrix的THREAD级别的隔离时,这些工作都是很有必要的。
    5.10 小结
    在设计高分布式应用程序(如基于微服务的应用程序)时,必须考虑客户端弹性。
    服务的彻底故障(如服务器崩溃)是很容易检测和处理的。
    一个性能不佳的服务可能会引起资源耗尽的连锁效应,因为调用客户端中的线程被阻塞,以等待服务完成。
    3种核心客户端弹性模式分别是断路器模式、后备模式和舱壁模式。
 
 
 
 

猜你喜欢

转载自www.cnblogs.com/mongotea/p/11973412.html
5.9