基于Hystrix实现服务隔离与降级

Hystrix 产生背景

在今天,基于SOA的架构已经大行其道。伴随着架构的SOA化,相关联的服务熔断、降级、限流等思想,也在各种技术讲座中频繁出现。
伴随着业务复杂性的提高,系统的不断拆分,一个面向用户端的API,其内部的RPC调用层层嵌套,调用链条可能会非常长。这会造成以下几个问题:

API接口可用性降低

引用Hystrix官方的一个例子,假设tomcat对外提供的一个application,其内部依赖了30个服务,每个服务的可用性都很高,为99.99%。那整个applicatiion的可用性就是:99.99%的30次方 = 99.7%,即0.3%的失败率。
这也就意味着,每1亿个请求,有30万个失败;按时间来算,就是每个月的故障时间超过2小时。

服务保护:当服务产生堆积的时候,对服务实现保护功能。
服务隔离:保证每个服务互不影响,使用信号量和线程池方式
服务降级:当服务不可用的时候,不会被等待,直接返回一个友好的提示。
服务熔断:当服务器达到最大的承受能力之后,直接拒绝访问服务,最后会调用服务降级方法,返回友好提示,目的保证服务不被宕机掉。
堆积请求:假设默认 tomcat 最大线程数 50,tomcat底层 http+线程池,每个线程都是独立的请求。尝试第51个请求,第51个请求会阻塞。大量请求正在等待,如果堆积请求过多。可能会造成服务器瘫痪。

服务保护

Hystrix 针对微服务保护框架。涉及到服务之间 rpc 远程调用。订单与会员服务实现rpc远程调用。微服务中 rpc 远程调用使用什么技术通讯:http 采用协议 restful,底层可以使用 httpclient 实现rpc 远程通讯。

思考:如果现在大量请求访问订单 orderIndex 服务,orderIndex 服务调用会员 memberIndex 服务。假设memberIndex 服务现在响应大概 1.5 秒。几个服务会产生雪崩效应。
2 个服务。如果在微服务中没有处理好雪崩效应,那么可能会导致一串连依赖关系的服务产生等待。

服务隔离

当大多数人在使用Tomcat时,多个HTTP服务会共享一个线程池,假设其中一个HTTP服务访问的数据库响应非常慢,这将造成服务响应时间延迟增加,大多数线程阻塞等待数据响应返回,导致整个Tomcat线程池都被该服务占用,甚至拖垮整个Tomcat。因此,如果我们能把不同HTTP服务隔离到不同的线程池,则某个HTTP服务的线程池满了也不会对其他服务造成灾难性故障。这就需要线程隔离或者信号量隔离来实现了。
使用线程隔离或信号隔离的目的是为不同的服务分配一定的资源,当自己的资源用完,直接返回失败而不是占用别人的资源。

Hystrix实现服务隔离两种方案

Hystrix的资源隔离策略有两种,分别为:线程池和信号量。

线程池方式

1、 使用线程池隔离可以完全隔离第三方应用,请求线程可以快速放回。
2、 请求线程可以继续接受新的请求,如果出现问题线程池隔离是独立的不会影响其他应用。
3、 当失败的应用再次变得可用时,线程池将清理并可立即恢复,而不需要一个长时间的恢复。
4、 独立的线程池提高了并发性
缺点:
线程池隔离的主要缺点是它们增加计算开销(CPU)。每个命令的执行涉及到排队、调度和上 下文切换都是在一个单独的线程上运行的。
引入 Hystix 依赖包

<dependency>
			<groupId>com.netflix.hystrix</groupId>
			<artifactId>hystrix-metrics-event-stream</artifactId>
			<version>1.5.12</version>
		</dependency>
		<dependency>
			<groupId>com.netflix.hystrix</groupId>
			<artifactId>hystrix-javanica</artifactId>
			<version>1.5.12</version>
		</dependency>

线程池方式实现服务隔离

public class OrderHystrixCommand extends HystrixCommand<JSONObject> {
	@Autowired
	private MemberService memberService;

	/**
	 * @param group
	 */
	public OrderHystrixCommand(MemberService memberService) {
		super(setter());
		this.memberService = memberService;
	}

        //run 表示服务执行的代码
	protected JSONObject run() throws Exception {
		JSONObject member = memberService.getMember();
		System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",订单服务调用会员服务:member:" + member);
		return member;
	}

    //配置服务的隔离机制
	private static Setter setter() {

		// 服务分组
		HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey("orders");
		// 服务标识
		HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey("order");
		// 线程池名称 为什么指定线程池名称?因为每个服务有自己独立的线程池
		HystrixThreadPoolKey threadPoolKey = HystrixThreadPoolKey.Factory.asKey("order-pool");
		// #####################################################
		// 线程池配置 线程池大小为10,线程存活时间15秒 队列等待的阈值为100,超过100执行拒绝策略 配置服务熔断 最多同时概念
		HystrixThreadPoolProperties.Setter threadPoolProperties = HystrixThreadPoolProperties.Setter().withCoreSize(10)
				.withKeepAliveTimeMinutes(15).withQueueSizeRejectionThreshold(100);
		// ########################################################
		// 命令属性配置Hystrix 开启超时
		HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter()
				// 采用线程池方式实现服务隔离
				.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)
				// 禁止超时
				.withExecutionTimeoutEnabled(false);
		return HystrixCommand.Setter.withGroupKey(groupKey).andCommandKey(commandKey).andThreadPoolKey(threadPoolKey)
				.andThreadPoolPropertiesDefaults(threadPoolProperties).andCommandPropertiesDefaults(commandProperties);

	}

    //服务降级
	@Override
	protected JSONObject getFallback() {
		// 如果Hystrix发生熔断,当前服务不可用,直接执行Fallback方法
		System.out.println("系统错误!");
		JSONObject jsonObject = new JSONObject();
		jsonObject.put("code", 500);
		jsonObject.put("msg", "系统错误!");
		return jsonObject;
	}
}
信号量

使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,当请求进来时先判断计数 器的数值,若超过设置的最大线程个数则拒绝该请求,若不超过则通行,这时候计数器+1,请求返 回成功后计数器-1。
与线程池隔离最大不同在于执行依赖代码的线程依然是请求线程
tips:信号量的大小可以动态调整, 线程池大小不可以

public class OrderHystrixCommand2 extends HystrixCommand<JSONObject> {
	@Autowired
	private MemberService memberService;

	/**
	 * @param group
	 */
	public OrderHystrixCommand2(MemberService memberService) {
		super(setter());
		this.memberService = memberService;
	}

	protected JSONObject run() throws Exception {

		// Thread.sleep(500);
		// System.out.println("orderIndex线程名称" +
		// Thread.currentThread().getName());
		// System.out.println("success");
		JSONObject member = memberService.getMember();
		System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",订单服务调用会员服务:member:" + member);
		return member;
	}

	private static Setter setter() {
		// 服务分组
		HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey("members");
		// 命令属性配置 采用信号量模式
		HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter()
				.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
				// 使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,当请求进来时先判断计数
				// 器的数值,若超过设置的最大线程个数则拒绝该请求,若不超过则通行,这时候计数器+1,请求返 回成功后计数器-1。
				.withExecutionIsolationSemaphoreMaxConcurrentRequests(50);
		return HystrixCommand.Setter.withGroupKey(groupKey).andCommandPropertiesDefaults(commandProperties);
	}

	@Override
	protected JSONObject getFallback() {
		// 如果Hystrix发生熔断,当前服务不可用,直接执行Fallback方法
		System.out.println("系统错误!");
		JSONObject jsonObject = new JSONObject();
		jsonObject.put("code", 500);
		jsonObject.put("msg", "系统错误!");
		return jsonObject;
	}
}
服务降级

当服务不可用(相当于当服务的时候、网络延迟、服务器响应慢),客户端一直等待,应该直接返回一个错误提示给客户端,不让客户端继续等待。使用 fallback 方法返回当前不可用提示。
目的:为了提高用户体验。(实例:错误提示:服务器忙,请稍后重试),防止服务雪崩效应。
比如:连接超时、网络延迟、服务器响应

服务熔断

熔断和保险丝,为了防止火灾发生,如果当电流过大的情况下,保险丝可能会烧掉,最终不能使用电。目的是为了防止火灾发生。保险丝烧掉的时候是什么原因造成的:电流过大。
微服务中服务熔断产生的原因就是服务发生请求过多(属于高并发),设置一个限制,比如最多同时只能访问 100 个请求(100 个线程),超出的请求存放在缓存队列中,如果缓存队列中满的情况下,直接拒绝访问服务,直接访问不了服务。(服务熔断)

服务熔断和服务降级一起使用。
服务熔断能实现什么效果:防止服务器不会被挂掉。

基于Hystrix实现服务隔离与降级项目搭建
订单工程
引入依赖
<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.0.RELEASE</version>
	</parent>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
		<dependency>
			<groupId>org.apache.httpcomponents</groupId>
			<artifactId>httpclient</artifactId>
		</dependency>
		<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.47</version>
		</dependency>
		<dependency>
			<groupId>com.netflix.hystrix</groupId>
			<artifactId>hystrix-metrics-event-stream</artifactId>
			<version>1.5.12</version>
		</dependency>
		<dependency>
			<groupId>com.netflix.hystrix</groupId>
			<artifactId>hystrix-javanica</artifactId>
			<version>1.5.12</version>
		</dependency>
	</dependencies>
OrderController
@RestController
@RequestMapping("/order")
public class OrderController {
	@Autowired
	private MemberService memberService;

	@RequestMapping("/orderIndex")
	public Object orderIndex() throws InterruptedException {
		JSONObject member = memberService.getMember();
		System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",订单服务调用会员服务:member:" + member);
		return member;
	}

	@RequestMapping("/orderIndexHystrix")
	public Object orderIndexHystrix() throws InterruptedException {
		return new OrderHystrixCommand(memberService).execute();
	}

	@RequestMapping("/orderIndexHystrix2")
	public Object orderIndexHystrix2() throws InterruptedException {
		return new OrderHystrixCommand2(memberService).execute();
	}

	@RequestMapping("/findOrderIndex")
	public Object findIndex() {
		System.out.println("当前线程:" + Thread.currentThread().getName() + ",findOrderIndex");
		return "findOrderIndex";
	}
}
MemberService
@Service
public class MemberService {

	public JSONObject getMember() {

		JSONObject result = HttpClientUtils.httpGet("http://127.0.0.1:8081/member/memberIndex");
		return result;
	}

}
HttpClientUtils
public class HttpClientUtils {
	private static Logger logger = LoggerFactory.getLogger(HttpClientUtils.class); // 日志记录

	private static RequestConfig requestConfig = null;

	static {
		// 设置请求和传输超时时间
		requestConfig = RequestConfig.custom().setSocketTimeout(2000).setConnectTimeout(2000).build();
	}

	/**
	 * post请求传输json参数
	 * 
	 * @param url
	 *            url地址
	 * @param json
	 *            参数
	 * @return
	 */
	public static JSONObject httpPost(String url, JSONObject jsonParam) {
		// post请求返回结果
		CloseableHttpClient httpClient = HttpClients.createDefault();
		JSONObject jsonResult = null;
		HttpPost httpPost = new HttpPost(url);
		// 设置请求和传输超时时间
		httpPost.setConfig(requestConfig);
		try {
			if (null != jsonParam) {
				// 解决中文乱码问题
				StringEntity entity = new StringEntity(jsonParam.toString(), "utf-8");
				entity.setContentEncoding("UTF-8");
				entity.setContentType("application/json");
				httpPost.setEntity(entity);
			}
			CloseableHttpResponse result = httpClient.execute(httpPost);
			// 请求发送成功,并得到响应
			if (result.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
				String str = "";
				try {
					// 读取服务器返回过来的json字符串数据
					str = EntityUtils.toString(result.getEntity(), "utf-8");
					// 把json字符串转换成json对象
					jsonResult = JSONObject.parseObject(str);
				} catch (Exception e) {
					logger.error("post请求提交失败:" + url, e);
				}
			}
		} catch (IOException e) {
			logger.error("post请求提交失败:" + url, e);
		} finally {
			httpPost.releaseConnection();
		}
		return jsonResult;
	}

	/**
	 * post请求传输String参数 例如:name=Jack&sex=1&type=2
	 * Content-type:application/x-www-form-urlencoded
	 * 
	 * @param url
	 *            url地址
	 * @param strParam
	 *            参数
	 * @return
	 */
	public static JSONObject httpPost(String url, String strParam) {
		// post请求返回结果
		CloseableHttpClient httpClient = HttpClients.createDefault();
		JSONObject jsonResult = null;
		HttpPost httpPost = new HttpPost(url);
		httpPost.setConfig(requestConfig);
		try {
			if (null != strParam) {
				// 解决中文乱码问题
				StringEntity entity = new StringEntity(strParam, "utf-8");
				entity.setContentEncoding("UTF-8");
				entity.setContentType("application/x-www-form-urlencoded");
				httpPost.setEntity(entity);
			}
			CloseableHttpResponse result = httpClient.execute(httpPost);
			// 请求发送成功,并得到响应
			if (result.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
				String str = "";
				try {
					// 读取服务器返回过来的json字符串数据
					str = EntityUtils.toString(result.getEntity(), "utf-8");
					// 把json字符串转换成json对象
					jsonResult = JSONObject.parseObject(str);
				} catch (Exception e) {
					logger.error("post请求提交失败:" + url, e);
				}
			}
		} catch (IOException e) {
			logger.error("post请求提交失败:" + url, e);
		} finally {
			httpPost.releaseConnection();
		}
		return jsonResult;
	}

	/**
	 * 发送get请求
	 * 
	 * @param url
	 *            路径
	 * @return
	 */
	public static JSONObject httpGet(String url) {
		// get请求返回结果
		JSONObject jsonResult = null;
		CloseableHttpClient client = HttpClients.createDefault();
		// 发送get请求
		HttpGet request = new HttpGet(url);
		request.setConfig(requestConfig);
		try {
			CloseableHttpResponse response = client.execute(request);

			// 请求发送成功,并得到响应
			if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
				// 读取服务器返回过来的json字符串数据
				HttpEntity entity = response.getEntity();
				String strResult = EntityUtils.toString(entity, "utf-8");
				// 把json字符串转换成json对象
				jsonResult = JSONObject.parseObject(strResult);
			} else {
				logger.error("get请求提交失败:" + url);
			}
		} catch (IOException e) {
			logger.error("get请求提交失败:" + url, e);
		} finally {
			request.releaseConnection();
		}
		return jsonResult;
	}

}
会员工程
@RestController
@RequestMapping("/member")
public class MemberController {

	@RequestMapping("/memberIndex")
	public Object memberIndex() throws InterruptedException {
		Map<String, Object> hashMap = new HashMap<String, Object>();
		hashMap.put("code", 200);
		hashMap.put("msg", "memberIndex");
		Thread.sleep(1500);
		return hashMap;
	}

}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值