Spring Cloud Hystrix 客戶端彈性模式

客戶端彈性模式的重點是,在遠程服務發生錯誤或表現不佳時保護遠程資源的客戶端免於崩潰。 有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/,查看註冊服務,如下圖所示:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章