#286 add hystrix demo back, 最后一轮代码清理。

This commit is contained in:
Calvin
2013-10-18 00:19:33 +08:00
parent c415034fc6
commit cdaebd63a2
10 changed files with 92 additions and 72 deletions
@@ -11,7 +11,7 @@ import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.springframework.http.HttpStatus;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import org.springside.examples.quickstart.data.TaskData;
import org.springside.examples.quickstart.entity.Task;
@@ -92,7 +92,7 @@ public class TaskRestFT extends BaseFunctionalTestCase {
try {
restTemplate.getForObject(taskUri, Task.class);
fail("Get should fail while feth a deleted task");
} catch (HttpClientErrorException e) {
} catch (HttpStatusCodeException e) {
assertEquals(HttpStatus.NOT_FOUND, e.getStatusCode());
}
}
@@ -105,7 +105,7 @@ public class TaskRestFT extends BaseFunctionalTestCase {
try {
restTemplate.postForLocation(resoureUrl, titleBlankTask);
fail("Create should fail while title is blank");
} catch (HttpClientErrorException e) {
} catch (HttpStatusCodeException e) {
assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode());
Map messages = jsonMapper.fromJson(e.getResponseBodyAsString(), Map.class);
assertEquals(1, messages.size());
@@ -117,7 +117,7 @@ public class TaskRestFT extends BaseFunctionalTestCase {
try {
restTemplate.put(resoureUrl + "/1", titleBlankTask);
fail("Update should fail while title is blank");
} catch (HttpClientErrorException e) {
} catch (HttpStatusCodeException e) {
assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode());
Map messages = jsonMapper.fromJson(e.getResponseBodyAsString(), Map.class);
assertEquals(1, messages.size());
@@ -19,7 +19,7 @@ import org.springside.modules.utils.Threads;
*/
@Controller
public class DependencyResourceController {
public static final int TIMEOUT = 15000;
public static final int TIMEOUT = 30000;
public static String status = "normal";
@@ -27,7 +27,7 @@ public class DependencyResourceController {
private AccountEffectiveService accountService;
/**
* 根据控制器中的状态而演示不同的行为,如正常返回,15秒后返回或直接报错
* 根据控制器中的状态而演示不同的行为,如正常返回,30秒后返回或直接报错
*/
@RequestMapping(value = "/hystrix/resource/{id}", method = RequestMethod.GET)
@ResponseBody
@@ -37,7 +37,7 @@ public class DependencyResourceController {
return handleRequest(id);
}
// 演示超时,10秒后返回.
// 演示超时,30秒后返回.
if ("timeout".equals(status)) {
Threads.sleep(TIMEOUT);
return handleRequest(id);
@@ -3,13 +3,19 @@ package org.springside.examples.showcase.demos.hystrix.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import org.springside.examples.showcase.webservice.rest.UserDTO;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.exception.HystrixBadRequestException;
/**
* HystrixCommand的实现类, 将访问远程资源的逻辑封装在T run()函数内.
* 如需对异常, 超时, 短路等进行自行处理,返回默认值则实现T getFallback()函数.
*
* @author calvin
*/
public class GetUserCommand extends HystrixCommand<UserDTO> {
private static Logger logger = LoggerFactory.getLogger(GetUserCommand.class);
@@ -18,7 +24,7 @@ public class GetUserCommand extends HystrixCommand<UserDTO> {
private Long id;
/**
* 构造函数,注入配置,命令用到的资源访问类和命令参数.
* 构造函数,注入配置,用到的资源访问类和命令参数.
*/
protected GetUserCommand(Setter commandConfig, RestTemplate restTemplate, Long id) {
super(commandConfig);
@@ -34,15 +40,15 @@ public class GetUserCommand extends HystrixCommand<UserDTO> {
try {
logger.info("Access restful resource");
return restTemplate.getForObject("http://localhost:8080/showcase/hystrix/resource/{id}", UserDTO.class, id);
} catch (HttpClientErrorException e) {
} catch (HttpStatusCodeException e) {
throw handleException(e);
}
}
/**
* 处理异常,对于客户端本身的异常,抛出HystrixBadRequestException,不计算入短路统计内。
* 处理异常,对于客户端自己的异常,抛出HystrixBadRequestException,不算入短路统计内。
*/
protected Exception handleException(HttpClientErrorException e) {
protected Exception handleException(HttpStatusCodeException e) {
HttpStatus status = e.getStatusCode();
if (status.equals(HttpStatus.BAD_REQUEST)) {
throw new HystrixBadRequestException(e.getResponseBodyAsString(), e);
@@ -1,6 +1,6 @@
package org.springside.examples.showcase.demos.hystrix.service;
import java.util.Map;
import java.util.HashMap;
import javax.annotation.PostConstruct;
@@ -9,7 +9,6 @@ import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springside.examples.showcase.webservice.rest.UserDTO;
import com.google.common.collect.Maps;
import com.netflix.hystrix.HystrixCircuitBreaker;
import com.netflix.hystrix.HystrixCommand.Setter;
import com.netflix.hystrix.HystrixCommandGroupKey;
@@ -29,8 +28,7 @@ import com.netflix.hystrix.util.HystrixRollingNumberEvent;
@Service
public class UserService {
private boolean runInNewThread = true;
private boolean isolateThreadPool = true;
private Setter commandConfig;
private RestTemplate restTemplate;
@@ -45,8 +43,8 @@ public class UserService {
/**
* 演示获取Hystrix的Metrics.
*/
public Map<String, Object> getHystrixMetrics() {
Map<String, Object> metricsMap = Maps.newHashMap();
public MetricsMap getHystrixMetrics() {
MetricsMap metricsMap = new MetricsMap();
HystrixCommandKey key = HystrixCommandKey.Factory.asKey("GetUserCommand");
HystrixCommandMetrics metrics = HystrixCommandMetrics.getInstance(key);
@@ -67,19 +65,6 @@ public class UserService {
metricsMap.put("latency50", metrics.getTotalTimePercentile(50));
metricsMap.put("latency90", metrics.getTotalTimePercentile(90));
metricsMap.put("latency100", metrics.getTotalTimePercentile(100));
} else {
metricsMap.put("circuitOpen", false);
metricsMap.put("totalRequest", 0);
metricsMap.put("errorPercentage", 0);
metricsMap.put("success", 0);
metricsMap.put("timeout", 0);
metricsMap.put("failure", 0);
metricsMap.put("shortCircuited", 0);
metricsMap.put("threadPoolRejected", 0);
metricsMap.put("semaphoreRejected", 0);
metricsMap.put("latency50", 0);
metricsMap.put("latency90", 0);
metricsMap.put("latency100", 0);
}
return metricsMap;
@@ -89,7 +74,7 @@ public class UserService {
* 演示Hystrix Command的各种配置项, 并构造Thread-Safed的RestTemplate.
*/
@PostConstruct
private void init() {
public void init() {
// 初始化RestTemplate
restTemplate = new RestTemplate();
@@ -100,37 +85,46 @@ public class UserService {
HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter();
commandConfig.andCommandPropertiesDefaults(commandProperties);
// 设置短路计算规则 //
commandProperties
// 设置短路规则 //
// 设置短路后的保护时间 ,默认为5秒,改为20秒方便演示
commandProperties.withCircuitBreakerSleepWindowInMilliseconds(20000)
// 多少百分比的失败在rolling windows内发生,计算为短路。默认为50%,无改变.
.withCircuitBreakerErrorThresholdPercentage(50)
// 至少多少请求在rolling window内发生,才开始触发短路的计算,默认为20, 设为3方便演示.
.withCircuitBreakerRequestVolumeThreshold(3)
// rolling windows 长度,默认为20秒,改为180秒方便演示。同时相应改变桶的数量.
.withMetricsRollingStatisticalWindowInMilliseconds(180000)
.withMetricsRollingStatisticalWindowBuckets(180);
// 设置短路后的保护时间 ,默认为5秒,改为20秒方便演示//
commandProperties.withCircuitBreakerSleepWindowInMilliseconds(20000);
// rolling windows 长度,默认为20秒,改为120秒方便演示。同时相应改变桶的数量.
.withMetricsRollingStatisticalWindowInMilliseconds(120000)
.withMetricsRollingStatisticalWindowBuckets(120);
// 设置超时与并发控制 //
if (runInNewThread) {
if (isolateThreadPool) { // 使用隔离的Hystrix线程池
// 线程超时,默认为1秒,设为3秒方便演示.
commandProperties.withExecutionIsolationThreadTimeoutInMilliseconds(3000);
// 线程池属性, 线程池大小,默认为10,无改变。待执行队列的大小,默认为5,无改变.
commandConfig.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(10)
.withQueueSizeRejectionThreshold(5));
} else {
// RestTemplate设置为10秒超时.
((SimpleClientHttpRequestFactory) restTemplate.getRequestFactory()).setConnectTimeout(10000);
} else { // 使用原有的调用者线程
// 依靠RestTemplate本身的超时机制,设为10秒。
((SimpleClientHttpRequestFactory) restTemplate.getRequestFactory()).setReadTimeout(10000);
// 设置使用信号量模式而不是默认的线程池模式,设置并发,默认为10,无改变.
// 设置使用原有的调用者线程,设置并发,默认为10,无改变.
commandProperties.withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE)
.withExecutionIsolationSemaphoreMaxConcurrentRequests(10);
}
}
public void setRunInNewThread(boolean runInNewThread) {
this.runInNewThread = runInNewThread;
public void setIsolateThreadPool(boolean isolateThreadPool) {
this.isolateThreadPool = isolateThreadPool;
}
/**
* 简易Map,对不存在的统计项返回0.
*/
public static class MetricsMap extends HashMap {
@Override
public Object get(Object key) {
Object result = super.get(key);
return result != null ? result : "0";
}
}
}
@@ -31,7 +31,6 @@ public class HystrixDemoController {
model.addAttribute("allStatus", allStatus);
model.addAttribute("statusHolder", new StatusHolder(DependencyResourceController.status));
model.addAttribute("metrics", userService.getHystrixMetrics());
return "hystrix/hystrix";
}
@@ -53,6 +52,17 @@ public class HystrixDemoController {
return "redirect:/hystrix";
}
/**
* 从默认的Hystrix线程池模式切换为使用原调用者线程的模式.
*/
@RequestMapping(value = "/hystrix/disableIsolateThreadPool")
public String disableIsolateThreadPool() {
userService.setIsolateThreadPool(false);
userService.init();
return "redirect:/hystrix";
}
@PostConstruct
public void init() {
allStatus.put("normal", "正常");
@@ -62,10 +72,10 @@ public class HystrixDemoController {
}
/**
* 给Spring Form Tag使用的类
* 给Spring Form Tag使用的类.
*/
public static class StatusHolder {
public String value;
private String value;
public StatusHolder(String value) {
this.value = value;
@@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.springside.modules.utils.Exceptions;
import org.springside.modules.web.MediaTypes;
import com.netflix.hystrix.exception.HystrixBadRequestException;
@@ -22,7 +23,7 @@ import com.netflix.hystrix.exception.HystrixRuntimeException.FailureType;
@ControllerAdvice
public class HystrixExceptionHandler extends ResponseEntityExceptionHandler {
/**
* 处理Hystrix Runtime异常.
* 处理Hystrix Runtime异常, 异常分为两类,一类是Command内部抛出异常,一类是Hystrix自身的保护机制
*/
@ExceptionHandler(value = { HystrixRuntimeException.class })
public final ResponseEntity<?> handleException(HystrixRuntimeException e, WebRequest request) {
@@ -34,7 +35,7 @@ public class HystrixExceptionHandler extends ResponseEntityExceptionHandler {
// 对命令抛出的异常进行特殊处理
if (type.equals(FailureType.COMMAND_EXCEPTION)) {
status = HttpStatus.INTERNAL_SERVER_ERROR;
message = "HystrixRuntimeException:" + e.getCause().getMessage();
message = Exceptions.getErrorMessageWithNested(e);
}
logger.error(message, e);
@@ -49,13 +50,11 @@ public class HystrixExceptionHandler extends ResponseEntityExceptionHandler {
*/
@ExceptionHandler(value = { HystrixBadRequestException.class })
public final ResponseEntity<?> handleException(HystrixBadRequestException e, WebRequest request) {
Exception realException = (Exception) e.getCause();
logger.error("HystrixClientException:" + realException.getMessage(), realException);
String message = Exceptions.getErrorMessageWithNested(e);
logger.error(message, e);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType(MediaTypes.TEXT_PLAIN_UTF_8));
return handleExceptionInternal(realException, realException.getMessage(), headers, HttpStatus.BAD_REQUEST,
request);
return handleExceptionInternal(e, message, headers, HttpStatus.BAD_REQUEST, request);
}
}
@@ -1,5 +1,4 @@
<%@ page contentType="text/html;charset=UTF-8" %>
<%@ page import="org.springside.examples.showcase.demos.hystrix.dependency.DependencyResourceController,com.netflix.hystrix.*,com.netflix.hystrix.HystrixCommandMetrics.HealthCounts,com.netflix.hystrix.util.*" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springside.org.cn/tags/form" %>
@@ -19,7 +18,7 @@
$('#hystrixResponse').html('<strong>Status:</strong> '+jqXHR.status+', <strong>Content:</strong> ' + jqXHR.responseText );
}).fail( function(jqXHR, textStatus){
$('#hystrixResponse').removeClass("alert alert-success").addClass("alert alert-warning");
$('#hystrixResponse').html('<strong>Status:</strong> '+jqXHR.status+', <strong>Content:</strong> ' + jqXHR.statusText );
$('#hystrixResponse').html('<strong>Status:</strong> '+jqXHR.status+', <strong>Content:</strong> ' + jqXHR.responseText );
});
}
</script>
@@ -39,25 +38,26 @@
<h2>主要用户故事</h2>
<ul>
<li>在默认的正常状态,访问Hystrix服务和依赖资源,均返回正常结果。</li>
<li>将资源状态切换为"超时",依赖资源需要在15秒后才返回结果访问,此时访问Hystrix服务,3秒后超时,返回503。</li>
<li>三次超时后满足短路条件(60秒滚动窗口内起码有3个请求,50%失败),再次访问服务,立即返回503。</li>
<li>10秒短路保护期内,所有访问都立即返回503,不会访问依赖资源。</li>
<li>保护期过后,会放行一个请求,如果还是超时,则继续保持短路状态。如果成功则重置所有计数器。</li>
<li>将资源状态切换为"超时",依赖资源需要在15秒后才返回结果访问,此时访问Hystrix服务,3秒后超时返回503。</li>
<li>两次超时后满足短路条件(60秒滚动窗口内起码有3个请求,50%失败),再次访问服务,立即返回503。</li>
<li>20秒短路保护期内,所有访问立即返回503,日志内不会显示访问依赖资源。</li>
<li>保护期过后,放行一个请求,如果还是超时,则继续保持短路状态。如果成功则重置所有计数器。</li>
</ul>
<h2>其他用户故事</h2>
<ul>
<li>将资源状态切换为"服务端失败",访问 Hystrix服务,即时返回500错误。</li>
<li>将资源状态切换为"错误请求",依赖资源返回400错误,访问Hystrix服务,返回500错误,但错误率变化统计无变化。</li>
<li>切换为调用者线程模式。</li>
<li>使用JMeter高并发测Hystrix试线程池用光之后的保护.</li>
<li>启动后切换为<a href="${ctx}/hystrix/disableIsolateThreadPool">调用者线程模式</a>, 依靠RestTemplate自身的超时机制,延长为10秒。</li>
</ul>
<h2>监控结果 </h2>
<form><input type="submit" value="刷新" class="btn btn-success"/></form>
<table class="table table-bordered">
<tr><td>是否短路:${metrics['circuitOpen']}</td></tr>
<tr><td>3分钟窗口内,请求数:${metrics['totalRequest']}, 失败百分比:${metrics['errorPercentage']}%,成功:${metrics['success']}, 超时:${metrics['timeout']},失败:${metrics['failure']},短路:${metrics['shortCircuited']}, 线程池满:${metrics['threadPoolRejected']}</td></tr>
<tr><td>50%延时:${metrics['latency50']}ms,90%延时:${metrics['latency90']}ms,100%延时:${metrics['latency100']}ms </td></tr>
<tr><td>2分钟窗口内,请求数:${metrics['totalRequest']}, 失败百分比:${metrics['errorPercentage']}%,成功:${metrics['success']}, 超时:${metrics['timeout']},失败:${metrics['failure']},短路:${metrics['shortCircuited']}, 线程池满:${metrics['threadPoolRejected']}</td></tr>
<tr><td>最快的50%请求延时:${metrics['latency50']}ms,90%请求延时:${metrics['latency90']}ms,100%请求延时:${metrics['latency100']}ms </td></tr>
</table>
</body>
</html>
@@ -4,7 +4,7 @@ import static org.junit.Assert.*;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import org.springside.examples.showcase.functional.BaseFunctionalTestCase;
import org.springside.examples.showcase.webservice.rest.UserDTO;
@@ -33,7 +33,7 @@ public class UserJaxRsFT extends BaseFunctionalTestCase {
assertEquals(new Long(1), user.getTeamId());
try {
user = restTemplate.getForObject(resoureUrl + "/{id}.json", UserDTO.class, 1L);
} catch (HttpClientErrorException e) {
} catch (HttpStatusCodeException e) {
fail(e.getMessage());
}
assertEquals("admin", user.getLoginName());
@@ -19,7 +19,7 @@ import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import org.springside.examples.showcase.functional.BaseFunctionalTestCase;
import org.springside.examples.showcase.webservice.rest.UserDTO;
@@ -56,6 +56,7 @@ public class UserRestFT extends BaseFunctionalTestCase {
jdkTemplate = new RestTemplate();
// (optional)设置20秒超时
((SimpleClientHttpRequestFactory) jdkTemplate.getRequestFactory()).setConnectTimeout(20000);
((SimpleClientHttpRequestFactory) jdkTemplate.getRequestFactory()).setReadTimeout(20000);
// 设置使用HttpClient4.0
httpClientRestTemplate = new RestTemplate();
@@ -100,7 +101,7 @@ public class UserRestFT extends BaseFunctionalTestCase {
HttpEntity<String> xml = jdkTemplate.exchange(resoureUrl + "/{id}.xml", HttpMethod.GET, requestEntity,
String.class, 1L);
System.out.println("xml output is " + xml.getBody());
} catch (HttpClientErrorException e) {
} catch (HttpStatusCodeException e) {
fail(e.getMessage());
}
}
@@ -122,7 +123,7 @@ public class UserRestFT extends BaseFunctionalTestCase {
// 直接取出JSON串
String json = httpClientRestTemplate.getForObject(resoureUrl + "/{id}.json", String.class, 1L);
System.out.println("json output is " + json);
} catch (HttpClientErrorException e) {
} catch (HttpStatusCodeException e) {
fail(e.getMessage());
}
}
@@ -141,7 +142,7 @@ public class UserRestFT extends BaseFunctionalTestCase {
try {
jdkTemplate.exchange(resoureUrl + "/{id}.xml", HttpMethod.GET, requestEntity, UserDTO.class, 1L);
fail("Get should fail with error username/password");
} catch (HttpClientErrorException e) {
} catch (HttpStatusCodeException e) {
assertEquals(HttpStatus.UNAUTHORIZED, e.getStatusCode());
}
}
@@ -35,6 +35,16 @@ public class Exceptions {
return stringWriter.toString();
}
/**
* 获取组合本异常信息与底层异常信息的异常描述, 适用于本异常为统一包装异常类,底层异常才是根本原因的情况。
*/
public static String getErrorMessageWithNested(Exception e) {
Throwable nestedException = e.getCause();
return new StringBuilder().append(e.getMessage()).append(" nested exception is ")
.append(nestedException.getClass().getName()).append(":").append(nestedException.getMessage())
.toString();
}
/**
* 判断异常是否由某些底层的异常引起.
*/