客户端弹性模式的重点是,在远程服务发生错误或表现不佳时保护远程资源的客户端免于崩溃。 有4种客户端弹性模式,它们分别是:
- 客户端负载均衡(client load balance)模式;
- 断路器(circuit breaker)模式;
- 后备(fallback)模式;
- 舱壁(bulkhead)模式。
1 构建Hystrix服务器
修改许可证服务项目pom.xml文件来导入Spring Hystrix的Maven依赖项。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
LicensingServiceApplication.java
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableCircuitBreaker
public class LicensingServiceApplication {
@LoadBalanced
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(LicensingServiceApplication.class, args);
}
}
2 使用Hystrix断路器
Hystrix和Spring Cloud使用@HystrixCommand注解来将Java类方法标记为由Hystrix断路器进行管理。当Spring框架看到@HystrixCommand时,它将动态生成一个代理,该代理将包装该方法,并通过专门用于处理远程调用的线程池来管理对该方法的所有调用。 我们将包装licensingservice/src/main/java/com/example/licenses/services/License Service.java中的LicenseService类中的getLicensesByOrg()方法。
@HystrixCommand
public List<License> getLicensesByOrg(String organizationId) {
return licenseRepository.findByOrganizationId(organizationId);
}
通过让调用时间稍微超过1 s(每3次调用中大约有1次),让我们来模拟getLicensesByOrg()方法执行慢数据库查询。
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);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public List<License> getLicensesByOrg(String organizationId) {
randomlyRunLong();
return licenseRepository.findByOrganizationId(organizationId);
}
如果访问http://localhost:8080/organizations/442adb6e-fa58-47f3-9ca2-ed1fecdfe86c/licenses/端点的次数足够多,那么应该会看到从许可证服务返回的超时错误消息。
对组织微服务的调用超时
我们可以使用方法级注解使被标记的调用拥有断路器功能,其优点在于,无论是访问数据库还是调用微服务,它都是相同的注解。 例如,在许可证服务中,我们需要查找与许可证关联的组织的名称。如果要使用断路器来包装对组织服务的调用的话,一个简单的方法就是将RestTemplate调用分解到自己的方法,并使用@HystrixCommand注解进行标注:
@HystrixCommand
private Organization getOrganization(String organizationId) {
return organizationRestClient.getOrganization(organizationId);
}
public License getLicense(String organizationId, String licenseId) {
License license = licenseRepository.findByOrganizationIdAndLicenseId(organizationId, licenseId);
Organization org = getOrganization(organizationId);
if (license != null) {
return license
.withOrganizationName( org.getName())
.withContactName( org.getContactName())
.withContactEmail( org.getContactEmail() )
.withContactPhone( org.getContactPhone() )
.withComment(config.getExampleProperty());
}
return license;
}
访问http://localhost:8080/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/f3831f8c-c338-4ebe-a82a-e2fc1d1ff78a,查看注册服务,如下图所示:
定制断路器的超时时间
在与新的开发人员合作使用Hystrix进行开发时,我们经常遇到的第一个问题是,他们如何定制Hystrix中断调用之前的时间。这一点通过将附加的参数传递给@HystrixCommand注解可以轻松完成。
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "12000") })
public List<License> getLicensesByOrg(String organizationId) {
randomlyRunLong();
return licenseRepository.findByOrganizationId(organizationId);
}
访问http://localhost:8080/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/,查看注册服务,如下图所示:
修改@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "10000"),访问http://localhost:8080/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/,查看注册服务,如下图所示:
3 实现后备模式
断路器模式下,由于远程资源的消费者和资源本身之间存在“中间人”,因此开发人员有机会拦截服务故障,并选择替代方案。 在Hystrix中,这被称为后备策略(fallback strategy),并且很容易实现。让我们看看如何为许可数据库构建一个简单的后备策略,该后备策略简单地返回一个许可对象,这个许可对象表示当前没有可用的许可信息。
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "10000") }, fallbackMethod = "buildFallbackLicenseList")
public List<License> getLicensesByOrg(String organizationId) {
randomlyRunLong();
return licenseRepository.findByOrganizationId(organizationId);
}
private List<License> buildFallbackLicenseList(String organizationId) {
List<License> fallbackList = new ArrayList<>();
License license = new License().withId("0000000-00-00000").withOrganizationId(organizationId)
.withProductName("Sorry no licensing information currently available");
fallbackList.add(license);
return fallbackList;
}
现在我们拥有了后备方案,接下来继续访问端点。这一次,当我们访问这个端点并遇到一个超时错误(有1/3的机会)时,我们不会从服务调用中得到一个返回的异常,而是得到虚拟的许可证值。如下图所示:
4 实现舱壁模式
Hystrix提供了一种易于使用的机制,在不同的远程资源调用之间创建舱壁。 要实现隔离的线程池,我们需要使用@HystrixCommand注解的其他属性。 接下来的代码将完成以下操作。
- 为getLicensesByOrg()调用建立一个单独的线程池。
- 设置线程池中的线程数。
- 设置单个线程繁忙时可排队的请求数的队列大小。
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "10000") }, threadPoolKey = "licenseByOrgThreadPool", threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "30"),
@HystrixProperty(name = "maxQueueSize", value = "10") }, fallbackMethod = "buildFallbackLicenseList")
public List<License> getLicensesByOrg(String organizationId) {
randomlyRunLong();
return licenseRepository.findByOrganizationId(organizationId);
}
通常情况下,直到服务处于负载状态,开发人员才能知道它的性能特征。线程池属性需要被调整的关键指标就是,即使目标远程资源是健康的,服务调用仍然超时。
5 定制Hystrix断路器
commandPoolProperties = {
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="10"),
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="75"),
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="7000"), @HystrixProperty(name="metrics.rollingStats.timeInMilliseconds", value="15000"),
@HystrixProperty(name="metrics.rollingStats.numBuckets", value="5")
}
Hystrix 库是高度可配置的,可以让开发人员严格控制使用它定义的断路器模式和舱壁模式的行为。开发人员可以通过修改Hystrix断路器的配置,控制Hystrix在超时远程调用之前需要等待的时间。开发人员还可以控制Hystrix断路器何时跳闸以及Hystrix何时尝试重置断路器。 使用Hystrix,开发人员还可以通过为每个远程服务调用定义单独的线程组,然后为每个线程组配置相应的线程数来微调舱壁实现。这允许开发人员对远程服务调用进行微调,因为某些远程资源调用具有较高的请求量。
属性名称 | 默认值 | 描述 |
fallbackMethod | None | 标识类中的方法,如果远程调用超时,将调用该方法。 回调方法必须与@HystrixCommand注解在同一个类中,并且必须具有与调用类相同的方法签名。如果值不存在,Hystrix会抛出异常 |
threadPoolKey | None | 给予@HystrixCommand一个唯一的名称,并创建一个独立于默认线程池的线程池。 如果没有定义任何值,则将使用默认的Hystrix线程池 |
属性名称 | 默认值 | 描述 |
threadPoolProperties | None | 核心的Hystrix注解属性,用于配置线程池的行为 |
coreSize | 10 | 设置线程池的大小 |
maxQueueSize | −1 | 设置线程池前面的最大队列大小。如果设置为−1,则不使用队列,Hystrix将阻塞请求, 直到有一个线程可用来处理 |
circuitBreaker. requestVolumeThreshold | 20 | 设置Hystrix开始检查断路器是否跳闸之前滚动窗口中必须处理的最小请求数 注意:此值只能使用commandPoolProperties属性设置 |
属性名称 | 默认值 | 描述 |
circuitBreaker. errorThresholdPercentage | 50 | 在断路器跳闸之前,滚动窗口内必须达到的故障百分比 注意:此值只能使用commandPoolProperties属性设置 |
circuitBreaker. sleepWindowInMilliseconds | 5,000 | 在断路器跳闸之后,Hystrix尝试进行服务调用之前将要等待的时间(以毫秒为单位) 注意:此值只能使用commandPoolProperties属性设置 |
属性名称 | 默认值 | 描述 |
metricsRollingStats. timeInMilliseconds | 10,000 | Hystrix收集和监控服务调用的统计信息的滚动窗口 (以毫秒为单位) |
metricsRollingStats. numBuckets | 10 | Hystrix在一个监控窗口中维护的度量桶的数量。 监视窗口内的桶数越多, Hystrix在窗口内监控故障的时间越低 |
6 线程上下文和Hystrix
当一个@HystrixCommand被执行时,它可以使用两种不同的隔离策略——THREAD(线程)和 SEMAPHORE(信号量)来运行。 在默认情况下,Hystrix 以 THREAD隔离策略运行。用于保护调用的每个Hystrix命令都在一个单独的线程池中运行,该线程池不与父线程共享它的上下文。这意味着Hystrix可以在它的控制下中断线程的执行,而不必担心中断与执行原始调用的父线程相关的其他活动。
在默认情况下,Hystrix不会将父线程的上下文传播到由Hystrix命令管理的线程中。 通常在基于REST的环境中,开发人员希望将上下文信息传递给服务调用,这将有助于在运维上管理该服务。例如,可以在REST调用的HTTP首部中传递关联ID(correlation ID)或验证令牌,然后将其传播到任何下游服务调用。关联ID是唯一标识符,该标识符可用于在单个事务中跨多个服务调用进行跟踪。
UserContext.java
package com.example.licenses.utils;
import org.springframework.stereotype.Component;
@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();
public String getCorrelationId() {
return correlationId;
}
public void setCorrelationId(String correlationId) {
this.correlationId = correlationId;
}
public String getAuthToken() {
return authToken;
}
public void setAuthToken(String authToken) {
this.authToken = authToken;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getOrgId() {
return orgId;
}
public void setOrgId(String orgId) {
this.orgId = orgId;
}
}
UserContextFilter.java
package com.example.licenses.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@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));
UserContextHolder.getContext().setUserId(httpServletRequest.getHeader(UserContext.USER_ID));
UserContextHolder.getContext().setAuthToken(httpServletRequest.getHeader(UserContext.AUTH_TOKEN));
UserContextHolder.getContext().setOrgId(httpServletRequest.getHeader(UserContext.ORG_ID));
logger.debug("UserContextFilter Correlation id: {}", UserContextHolder.getContext().getCorrelationId());
filterChain.doFilter(httpServletRequest, servletResponse);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}
UserContextHolder.java
package com.example.licenses.utils;
import org.springframework.util.Assert;
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();
}
}
UserContextInterceptor.java
package com.example.licenses.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
public class UserContextInterceptor implements ClientHttpRequestInterceptor {
private static final Logger logger = LoggerFactory.getLogger(UserContextInterceptor.class);
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
HttpHeaders headers = request.getHeaders();
headers.add(UserContext.CORRELATION_ID, UserContextHolder.getContext().getCorrelationId());
headers.add(UserContext.AUTH_TOKEN, UserContextHolder.getContext().getAuthToken());
return execution.execute(request, body);
}
}
LicenseServiceController.java
private static final Logger logger = LoggerFactory.getLogger(LicenseServiceController.class);
logger.debug("LicenseServiceController Correlation id: {}", UserContextHolder.getContext().getCorrelationId());
LicenseService.java
private static final Logger logger = LoggerFactory.getLogger(LicenseService.class);
logger.debug("LicenseService.getLicensesByOrg Correlation id: {}", UserContextHolder.getContext().getCorrelationId());
application.yml
#Setting the logging levels for the service
logging:
level:
com.netflix: WARN
org.springframework.web: WARN
com.example: DEBUG
访问http://localhost:8080/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/,查看注册服务,如下图所示:
Hystrix允许开发人员定义一种自定义的并发策略,它将包装Hystrix调用,并允许开发人员将附加的父线程上下文注入由Hystrix命令管理的线程中。实现自定义HystrixConcurrencyStrategy需要执行以下3个操作。
- 定义自定义的Hystrix并发策略类。
- 定义一个Callable类,将UserContext注入Hystrix命令中。
- 配置Spring Cloud以使用自定义Hystrix并发策略。
DelegatingUserContextCallable.java
package com.example.licenses.hystrix;
import com.example.licenses.utils.UserContext;
import com.example.licenses.utils.UserContextHolder;
import java.util.concurrent.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;
}
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 DelegatingUserContextCallable<V>(delegate, userContext);
}
}
ThreadLocalAwareStrategy.java
package com.example.licenses.hystrix;
import com.netflix.hystrix.HystrixThreadPoolKey;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariable;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableLifecycle;
import com.netflix.hystrix.strategy.properties.HystrixProperty;
import com.example.licenses.utils.UserContextHolder;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadLocalAwareStrategy extends HystrixConcurrencyStrategy {
private HystrixConcurrencyStrategy existingConcurrencyStrategy;
public ThreadLocalAwareStrategy(HystrixConcurrencyStrategy existingConcurrencyStrategy) {
this.existingConcurrencyStrategy = existingConcurrencyStrategy;
}
@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) {
return existingConcurrencyStrategy != null ? existingConcurrencyStrategy.getRequestVariable(rv)
: super.getRequestVariable(rv);
}
@Override
public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, HystrixProperty<Integer> corePoolSize,
HystrixProperty<Integer> maximumPoolSize, HystrixProperty<Integer> keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
return existingConcurrencyStrategy != null
? existingConcurrencyStrategy.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime,
unit, workQueue)
: super.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
return existingConcurrencyStrategy != null
? existingConcurrencyStrategy
.wrapCallable(new DelegatingUserContextCallable<T>(callable, UserContextHolder.getContext()))
: super.wrapCallable(new DelegatingUserContextCallable<T>(callable, UserContextHolder.getContext()));
}
}
ThreadLocalConfiguration.java
package com.example.licenses.hystrix;
import com.netflix.hystrix.strategy.HystrixPlugins;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier;
import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook;
import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher;
import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
public class ThreadLocalConfiguration {
@Autowired(required = false)
private HystrixConcurrencyStrategy existingConcurrencyStrategy;
@PostConstruct
public void init() {
// Keeps references of existing Hystrix plugins.
HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier();
HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher();
HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance().getPropertiesStrategy();
HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins.getInstance().getCommandExecutionHook();
HystrixPlugins.reset();
HystrixPlugins.getInstance()
.registerConcurrencyStrategy(new ThreadLocalAwareStrategy(existingConcurrencyStrategy));
HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook);
}
}
访问http://localhost:8080/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/,查看注册服务,如下图所示:
如有疑问,请看视频:Spring Cloud 实战教程