Storm简介
有些热点数据相关的实施处理的方案,比如快速预热,热点数据的实时感知和快速降级,全部要用到Storm,因为我们要实时的计算出热点缓存数据,实时计算,高并发的情况。
这时要做一些实时的计算,必须涉及到分布式,分布式的技术才能处理高并发和大量的请求。
目前在实时计算领域,大数据的技术在做复杂的热数据的统计和分析,超大流量,高并发的,流式处理的分布式场景下,最成熟的就是Storm
Storm的特点
1 支撑各种实时类的项目场景:实时处理消息以及更新数据库,基于最基础的实时计算语义和API(实时数据处理领域):
对实时的数据流持续的进行查询和计算,同时将最新的计算结果持续的推送给客户端展示。
对耗时的查询进行并行化,基于DRPC,即分布式RPC调用,单表30天数据,并行化,每个进程查询一天数据,最后组装结果。
2 高度的可伸缩性:如果要扩容直接加机器,调整Storm计算作业的并行度即可,Storm会自动部署更多的线程和进程到其他的机器上去,无缝的快速扩容。
3 数据不丢失的保证:storm的消息可靠机制开启后,可以保证一条数据都不丢失
4 超强的健壮性:从历史经验来看,storm比hadoop,spark等大数据系统,健壮的多得多,因为元数据不是放在进程的内存中,而是全部放zookeeper,随便挂,不要紧
5 使用的便捷性:核心语义非常简单,开发起来效率很高
Storm集群架构及核心概念
Storm集群架构
Nimbus:Storm集群架构中的主节点,负责元数据的维护,资源调度,提交实时计算作业的入口
Supervisor:当在运行一个实时计算的作业,可以设置需要多少个进程和线程去执行,这个过程就是由Nimber来告诉Supervisor,然后Supervisor回启动worker进程来执行相应的任务。
Worker:每个Worker都在Supervisor所对应的服务器上面,收到Supervisor的命令后启动一定数量的线程来执行Supervisor分配的任务
Executor:他就是Worker启动的线程,有自己的调度Task的算法和执行机制
Task:一个Executor线程会执行多个task,task就是一段代码,会执行自己内部的代码处理一部分的数据
Storm核心概念
Topology:拓扑,务虚的一个概念
Spout:数据源的一个代码组件,我们可以写一个java类,实现Spout的接口,这段代码中我们可以自己尝试去数据源获取数据,比如从Kafka中消费数据
bolt:一个业务处理的代码组件Spout会将各种数据传输给bolt,各种bolt还可以串联成一个计算链条,java类实现了一个bolt接口,一堆spout + bolt就会组成一个Topology,就是一个拓扑,实时计算作业,bolt中包含着一个或多个task
tuple:就是一条数据,每条数据都会被封装在tuple中,在多个spout 和 bolt中传递
stream:就是一个流,务虚的一个概念,抽象的概念,源源不断过来的tuple就组成了一条数据流。
流分组
task和task之间的数据流向关系。每个bolt中包含多个task,当上游bolt中的task处理完数据以后,以什么策略将数据传递给下游的bolt
主要的有两种策略:
Shuffle Grouping:随机发射,负载均衡
Fields Grouping:按照task中的字段,分组发射
并行度
说白了对于一个拓扑来说并行度就是有多少个task,有人说executor是因为,默认情况下一个executor只有一个task,也就是说executor默认等于task的数量
热点缓存导致系统全盘崩溃的问题
前面有提到我们的系统是由三层架构:分发层的Nginx -> 对应用层的Nginx取模hash -> 转发到各个应用层Nginx -> 再转发到各个redis服务,这种架构存在一个问题:
当某一个商品突然变得非常热门时,因为超级热门的商品也就伴随着庞大的并发,因为个商品的路由转发是固定的,也就意味着对一个商品的请求只会落到一台固定的服务器上,那么很有可能将这台服务器直接干崩溃,当这台服务器崩溃后,重新路由到其他服务器上,其他服务器也崩溃,而导致整个系统全面崩溃。
包括其实redis3.0的集群也是通过hash slot存储数据的,也就是一个商品一定只会访问固定的一台redis服务,跟上述面临着同样的问题。
问题解决方案
1、在storm中,实时的计算出瞬间出现的热点
某个storm task,上面算出了1万个商品的访问次数,LRUMap
频率高一些,每隔5秒,去遍历一次LRUMap,将其中的访问次数进行排序,统计出往后排的95%的商品访问次数的平均值
1000
999
888
777
666
50
60
80
100
120
比如说,95%的商品,访问次数的平均值是100
然后,从最前面开始,往后遍历,去找有没有瞬间出现的热点数据1000,
95%的平均值(100)的10倍,这个时候要设定一个阈值,比如说超出95%平均值得n倍,5倍
我们就认为是瞬间出现的热点数据,判断其可能在短时间内继续扩大的访问量,甚至达到平均值几十倍,或者几百倍
当遍历,发现说第一个商品的访问次数,小于平均值的5倍,就安全了,就break掉这个循环
热点数据,热数据,不是一个概念
有100个商品,前10个商品比较热,都访问量在500左右,其他的普通商品,访问量都在200左右,就说前10个商品是热数据
统计出来
预热的时候,将这些热数据放在缓存中去预热就可以了
热点,前面某个商品的访问量,瞬间超出了普通商品的10倍,或者100倍,1000倍,热点
2、当出现热点数据时,storm这里,会直接发送http请求到nginx上,nginx上用lua脚本去处理这个请求
storm会将热点本身对应的productId,发送到流量分发的nginx上面去,放在本地缓存中
storm会将热点对应的完整的缓存数据,发送到所有的应用nginx服务器上去,直接放在本地缓存中
3、流量分发nginx的分发策略降级
流量分发nginx,加一个逻辑,就是每次访问一个商品详情页的时候,如果发现它是个热点,那么立即做流量分发策略的降级
hash策略,同一个productId的访问都同一台应用nginx服务器上
降级成对这个热点商品,流量分发采取随机负载均衡发送到所有的后端应用nginx服务器上去
瞬间将热点缓存数据的访问,从hash分发,全部到一台nginx,变成负载均衡,发送到多台nginx上去
避免说大量的流量全部集中到一台机器,50万的访问量到一台nginx,5台应用nginx,每台就可以承载10万的访问量
4、storm还需要保存下来上次识别出来的热点list
下次去识别的时候,这次的热点list跟上次的热点list做一下diff,看看可能有的商品已经不是热点了
热点的取消的逻辑,发送http请求到流量分发的nginx上去,取消掉对应的热点数据,从nginx本地缓存中删除
具体代码实现
1 在上述的ProductCountBolt类中加入以下内部类HotProductFindThread
加入热点缓存实时自动识别和感知的代码逻辑,以及向nginx反向推送缓存热点的ID和数据的代码逻辑
private class HotProductFindThread implements Runnable { public void run() { List<Map.Entry<Long, Long>> productCountList = new ArrayList<Map.Entry<Long, Long>>(); List<Long> hotProductIdList = new ArrayList<Long>(); while(true) { // 1、将LRUMap中的数据按照访问次数,进行全局的排序 // 2、计算95%的商品的访问次数的平均值 // 3、遍历排序后的商品访问次数,从最大的开始 // 4、如果某个商品比如它的访问量是平均值的10倍,就认为是缓存的热点 try { productCountList.clear(); hotProductIdList.clear(); if(productCountMap.size() == 0) { Utils.sleep(100); continue; } LOGGER.info("【HotProductFindThread打印productCountMap的长度】size=" + productCountMap.size()); // 1、先做全局的排序 for(Map.Entry<Long, Long> productCountEntry : productCountMap.entrySet()) { if(productCountList.size() == 0) { productCountList.add(productCountEntry); } else { // 比较大小,生成最热topn的算法有很多种 boolean bigger = false; for(int i = 0; i < productCountList.size(); i++){ Map.Entry<Long, Long> topnProductCountEntry = productCountList.get(i); if(productCountEntry.getValue() > topnProductCountEntry.getValue()) { int lastIndex = productCountList.size() < productCountMap.size() ? productCountList.size() - 1 : productCountMap.size() - 2; for(int j = lastIndex; j >= i; j--) { if(j + 1 == productCountList.size()) { productCountList.add(null); } productCountList.set(j + 1, productCountList.get(j)); } productCountList.set(i, productCountEntry); bigger = true; break; } } if(!bigger) { if(productCountList.size() < productCountMap.size()) { productCountList.add(productCountEntry); } } } } // 2、计算出95%的商品的访问次数的平均值 int calculateCount = (int)Math.floor(productCountList.size() * 0.95); Long totalCount = 0L; for(int i = productCountList.size() - 1; i >= productCountList.size() - calculateCount; i--) { totalCount += productCountList.get(i).getValue(); } Long avgCount = totalCount / calculateCount; // 3、从第一个元素开始遍历,判断是否是平均值得10倍 for(Map.Entry<Long, Long> productCountEntry : productCountList) { if(productCountEntry.getValue() > 10 * avgCount) { //缓存热点感知 hotProductIdList.add(productCountEntry.getKey()); // 将缓存热点反向推送到流量分发的nginx中 String distributeNginxURL = "http://192.168.31.227/hot?productId=" + productCountEntry.getKey(); HttpClientUtils.sendGetRequest(distributeNginxURL); // 将缓存热点,那个商品对应的完整的缓存数据,发送请求到缓存服务去获取,反向推送到所有的后端应用nginx服务器上去 String cacheServiceURL = "http://192.168.31.179:8080/getProductInfo?productId=" + productCountEntry.getKey(); String response = HttpClientUtils.sendGetRequest(cacheServiceURL); String[] appNginxURLs = new String[]{ "http://192.168.31.187/hot?productId=" + productCountEntry.getKey() + "&productInfo=" + response, "http://192.168.31.19/hot?productId=" + productCountEntry.getKey() + "&productInfo=" + response }; for(String appNginxURL : appNginxURLs) { HttpClientUtils.sendGetRequest(appNginxURL); } } } Utils.sleep(5000); } catch (Exception e) { e.printStackTrace(); } } } }
2 在流量分发 + 后端应用双层Nginx中加入接受热点缓存数据的接口
流量分发层
编辑引入主nginx.conf的product.conf
server {
listen 80;
server_name _;
#添加该路由路径
location /hot {
default_type 'text/html';
content_by_lua_file /usr/example/lua/hot.lua;
}
}编辑hot.lua脚本
#获取请求的URIlocal uri_args = ngx.req.get_uri_args()
#获取URI中的参数
local product_id = uri_args["productId"]
#获取Nginx中的缓存
local cache_ngx = ngx.shared.my_cache
#拼接热点数据id
local hot_product_cache_key = "hot_product_"..product_id
#将热点数据id放入分发层的缓存中
cache_ngx:set(hot_product_cache_key, "true", 60 * 60)
后端应用层
编辑引入主nginx.conf的product.conf
server {
listen 80;
server_name _;
#添加该路由路径
location /hot {
default_type 'text/html';
content_by_lua_file /usr/example/lua/hot.lua;
}
}在后端应用中编辑hot.lua脚本
local uri_args = ngx.req.get_uri_args()local product_id = uri_args["productId"]
local product_info = uri_args["productInfo"]
local product_cache_key = "product_info_"..product_id
local cache_ngx = ngx.shared.my_cache
#将热点数据和其数据信息,放入应用层的Nginx缓存中,过期时间60秒
cache_ngx:set(product_cache_key,product_info,60 * 60)
实现热点缓存自动降级为负载均衡流量分发策略
private class HotProductFindThread implements Runnable { public void run() { List<Map.Entry<Long, Long>> productCountList = new ArrayList<Map.Entry<Long, Long>>(); List<Long> hotProductIdList = new ArrayList<Long>(); List<Long> lastTimeHotProductList = new ArrayList<Long>(); while(true) { // 1、将LRUMap中的数据按照访问次数,进行全局的排序 // 2、计算95%的商品的访问次数的平均值 // 3、遍历排序后的商品访问次数,从最大的开始 // 4、如果某个商品比如它的访问量是平均值的10倍,就认为是缓存的热点 try { productCountList.clear(); hotProductIdList.clear(); if(productCountMap.size() == 0) { Utils.sleep(100); continue; } LOGGER.info("【HotProductFindThread打印productCountMap的长度】size=" + productCountMap.size()); // 1、先做全局的排序 for(Map.Entry<Long, Long> productCountEntry : productCountMap.entrySet()) { if(productCountList.size() == 0) { productCountList.add(productCountEntry); } else { // 比较大小,生成最热topn的算法有很多种 // 但是我这里为了简化起见,不想引入过多的数据结构和算法的的东西 // 很有可能还是会有漏洞,但是我已经反复推演了一下了,而且也画图分析过这个算法的运行流程了 boolean bigger = false; for(int i = 0; i < productCountList.size(); i++){ Map.Entry<Long, Long> topnProductCountEntry = productCountList.get(i); if(productCountEntry.getValue() > topnProductCountEntry.getValue()) { int lastIndex = productCountList.size() < productCountMap.size() ? productCountList.size() - 1 : productCountMap.size() - 2; for(int j = lastIndex; j >= i; j--) { if(j + 1 == productCountList.size()) { productCountList.add(null); } productCountList.set(j + 1, productCountList.get(j)); } productCountList.set(i, productCountEntry); bigger = true; break; } } if(!bigger) { if(productCountList.size() < productCountMap.size()) { productCountList.add(productCountEntry); } } } } // 2、计算出95%的商品的访问次数的平均值 int calculateCount = (int)Math.floor(productCountList.size() * 0.95); Long totalCount = 0L; for(int i = productCountList.size() - 1; i >= productCountList.size() - calculateCount; i--) { totalCount += productCountList.get(i).getValue(); } Long avgCount = totalCount / calculateCount; // 3、从第一个元素开始遍历,判断是否是平均值得10倍 for(Map.Entry<Long, Long> productCountEntry : productCountList) { if(productCountEntry.getValue() > 10 * avgCount) { hotProductIdList.add(productCountEntry.getKey()); if(!lastTimeHotProductList.contains(productCountEntry.getKey())) { // 将缓存热点反向推送到流量分发的nginx中 String distributeNginxURL = "http://192.168.31.227/hot?productId=" + productCountEntry.getKey(); HttpClientUtils.sendGetRequest(distributeNginxURL); // 将缓存热点,那个商品对应的完整的缓存数据,发送请求到缓存服务去获取,反向推送到所有的后端应用nginx服务器上去 String cacheServiceURL = "http://192.168.31.179:8080/getProductInfo?productId=" + productCountEntry.getKey(); String response = HttpClientUtils.sendGetRequest(cacheServiceURL); URLEncoder.encode(response,HTTP.UTF_8); String[] appNginxURLs = new String[]{ "http://192.168.31.187/hot?productId=" + productCountEntry.getKey() + "&productInfo=" + response, "http://192.168.31.19/hot?productId=" + productCountEntry.getKey() + "&productInfo=" + response }; for (String appNginxURL : appNginxURLs) { HttpClientUtils.sendGetRequest(appNginxURL); } } } } //4 实时感知热点数据的消失 //如果上次的热点数据列表为空,则直接将这次查询出的热点数据加入上次的热点数据列表 if(lastTimeHotProductList.isEmpty()){ if(!hotProductIdList.isEmpty()){ for(Long productId : hotProductIdList){ lastTimeHotProductList.add(productId); } } }else{ lastTimeHotProductList.forEach(pid->{ if(!hotProductIdList.contains(pid)){ //说明上次的热点数据消失了 //发送一个http请求到流量分发的nginx中,取消热点缓存的标识 String url = "http://192.168.31.227/cancel_hot?product="+pid; } }); if(!hotProductIdList.isEmpty()){ //清空上一次的热点数据列表,将这次的数据加入 lastTimeHotProductList.clear(); for(Long productId : hotProductIdList){ lastTimeHotProductList.add(productId); } }else{ lastTimeHotProductList.clear(); } } Utils.sleep(5000); } catch (Exception e) { e.printStackTrace(); } } } }
分发层lua脚本修改
#获取请求参数,比如 productId
local uri_args = ngx.req.get_uri_args()
local productId = uri_args["productId"]
local shopId = uri_args["shopId"]
local host = {"192.168.31.19", "192.168.31.187"}
local backend = ""
local hot_product__key = "hot_product_"..productId
#获取nginx缓存
local cache_nginx = ngx.shared.my_cache
local hot_product_flag = cache_nginx.get:(hot_product__key )
#如果hot_product_flag 有值说明是热点数据
if hot_product_flag == "true" then
math.randomseed(tostring((os.time)):reverse():sub(1,7))
#将分发策略改变成随机分发,负载均衡策略,两台服务器就随机取值1 ~ 2
local index = math.random(1 , 2)
backend = "http://"..host[hash]
#对productId进行hash
local hash = ngx.crc32_long(productId)
hash = (hash % 2) + 1
#hash值对应用服务器数量取模,获取到一个应用服务器
backend = "http://"..host[hash]
end
local requestBody = "/"..method.."?productId="..productId.."&shopId="..shopId
#利用http发送请求到应用层nginx
local http = require("resty.http")
local httpc = http.new()
local resp, err = httpc:request_uri(backend, {
method = "GET",
path = requestBody
})
if not resp then
ngx.say("request error :", err)
return
end
#获取响应后返回ngx.say(resp.body)
httpc:close()
在Storm中加入热点缓存消失实时自动识别和感知的代码
private class HotProductFindThread implements Runnable { public void run() { List<Map.Entry<Long, Long>> productCountList = new ArrayList<Map.Entry<Long, Long>>(); List<Long> hotProductIdList = new ArrayList<Long>(); List<Long> lastTimeHotProductList = new ArrayList<Long>(); while(true) { // 1、将LRUMap中的数据按照访问次数,进行全局的排序 // 2、计算95%的商品的访问次数的平均值 // 3、遍历排序后的商品访问次数,从最大的开始 // 4、如果某个商品比如它的访问量是平均值的10倍,就认为是缓存的热点 try { productCountList.clear(); hotProductIdList.clear(); if(productCountMap.size() == 0) { Utils.sleep(100); continue; } LOGGER.info("【HotProductFindThread打印productCountMap的长度】size=" + productCountMap.size()); // 1、先做全局的排序 for(Map.Entry<Long, Long> productCountEntry : productCountMap.entrySet()) { if(productCountList.size() == 0) { productCountList.add(productCountEntry); } else { // 比较大小,生成最热topn的算法有很多种 // 但是我这里为了简化起见,不想引入过多的数据结构和算法的的东西 // 很有可能还是会有漏洞,但是我已经反复推演了一下了,而且也画图分析过这个算法的运行流程了 boolean bigger = false; for(int i = 0; i < productCountList.size(); i++){ Map.Entry<Long, Long> topnProductCountEntry = productCountList.get(i); if(productCountEntry.getValue() > topnProductCountEntry.getValue()) { int lastIndex = productCountList.size() < productCountMap.size() ? productCountList.size() - 1 : productCountMap.size() - 2; for(int j = lastIndex; j >= i; j--) { if(j + 1 == productCountList.size()) { productCountList.add(null); } productCountList.set(j + 1, productCountList.get(j)); } productCountList.set(i, productCountEntry); bigger = true; break; } } if(!bigger) { if(productCountList.size() < productCountMap.size()) { productCountList.add(productCountEntry); } } } } // 2、计算出95%的商品的访问次数的平均值 int calculateCount = (int)Math.floor(productCountList.size() * 0.95); Long totalCount = 0L; for(int i = productCountList.size() - 1; i >= productCountList.size() - calculateCount; i--) { totalCount += productCountList.get(i).getValue(); } Long avgCount = totalCount / calculateCount; // 3、从第一个元素开始遍历,判断是否是平均值得10倍 for(Map.Entry<Long, Long> productCountEntry : productCountList) { if(productCountEntry.getValue() > 10 * avgCount) { hotProductIdList.add(productCountEntry.getKey()); if(!lastTimeHotProductList.contains(productCountEntry.getKey())) { // 将缓存热点反向推送到流量分发的nginx中 String distributeNginxURL = "http://192.168.31.227/hot?productId=" + productCountEntry.getKey(); HttpClientUtils.sendGetRequest(distributeNginxURL); // 将缓存热点,那个商品对应的完整的缓存数据,发送请求到缓存服务去获取,反向推送到所有的后端应用nginx服务器上去 String cacheServiceURL = "http://192.168.31.179:8080/getProductInfo?productId=" + productCountEntry.getKey(); String response = HttpClientUtils.sendGetRequest(cacheServiceURL); URLEncoder.encode(response,HTTP.UTF_8); String[] appNginxURLs = new String[]{ "http://192.168.31.187/hot?productId=" + productCountEntry.getKey() + "&productInfo=" + response, "http://192.168.31.19/hot?productId=" + productCountEntry.getKey() + "&productInfo=" + response }; for (String appNginxURL : appNginxURLs) { HttpClientUtils.sendGetRequest(appNginxURL); } } } } //4 实时感知热点数据的消失 //如果上次的热点数据列表为空,则直接将这次查询出的热点数据加入上次的热点数据列表 if(lastTimeHotProductList.isEmpty()){ if(!hotProductIdList.isEmpty()){ for(Long productId : hotProductIdList){ lastTimeHotProductList.add(productId); } } }else{ lastTimeHotProductList.forEach(pid->{ if(!hotProductIdList.contains(pid)){ //说明上次的热点数据消失了 //发送一个http请求到流量分发的nginx中,取消热点缓存的标识 String url = "http://192.168.31.227/cancel_hot?product="+pid; } }); if(!hotProductIdList.isEmpty()){ //清空上一次的热点数据列表,将这次的数据加入 lastTimeHotProductList.clear(); for(Long productId : hotProductIdList){ lastTimeHotProductList.add(productId); } }else{ lastTimeHotProductList.clear(); } } Utils.sleep(5000); } catch (Exception e) { e.printStackTrace(); } } } }
编辑nginx的server配置文件
server {
listen 80;
server_name _;
#添加该路由路径
location /cancel_hot {
default_type 'text/html';
content_by_lua_file /usr/example/lua/cancel_hot .lua;
}
}
编辑cancel_hot .lua
local uri_args = ngx.req.get_uri_args()
local product_id = uri_args["productId"]
local product_cache_key = "product_info_"..product_id
local cache_ngx = ngx.shared.my_cache
#将缓存中热点数据id对应的值置为false,过期时间60秒
cache_ngx:set(product_cache_key, "false" ,60)
local host = {"192.168.31.19", "192.168.31.187"}
Hystrix简介
1、Hystrix是什么?
在分布式系统中,每个服务都可能会调用很多其他服务,被调用的那些服务就是依赖服务,有的时候某些依赖服务出现故障也是很正常的。
Hystrix可以让我们在分布式系统中对服务间的调用进行控制,加入一些调用延迟或者依赖故障的容错机制。
Hystrix通过将依赖服务进行资源隔离,进而组织某个依赖服务出现故障的时候,这种故障在整个系统所有的依赖服务调用中进行蔓延,同时Hystrix还提供故障时的fallback降级机制
总而言之,Hystrix通过这些方法帮助我们提升分布式系统的可用性和稳定性
2、Hystrix的历史
hystrix,就是一种高可用保障的一个框架,类似于spring(ioc,mvc),mybatis,activiti,lucene,框架,预先封装好的为了解决某个特定领域的特定问题的一套代码库
框架,用了框架之后,来解决这个领域的特定的问题,就可以大大减少我们的工作量,提升我们的工作质量和工作效率,框架
hystrix,高可用性保障的一个框架
Netflix(可以认为是国外的优酷或者爱奇艺之类的视频网站),API团队从2011年开始做一些提升系统可用性和稳定性的工作,Hystrix就是从那时候开始发展出来的。
在2012年的时候,Hystrix就变得比较成熟和稳定了,Netflix中,除了API团队以外,很多其他的团队都开始使用Hystrix。
时至今日,Netflix中每天都有数十亿次的服务间调用,通过Hystrix框架在进行,而Hystrix也帮助Netflix网站提升了整体的可用性和稳定性
3、初步看一看Hystrix的设计原则是什么?
hystrix为了实现高可用性的架构,设计hystrix的时候,一些设计原则是什么???
(1)对依赖服务调用时出现的调用延迟和调用失败进行控制和容错保护
(2)在复杂的分布式系统中,阻止某一个依赖服务的故障在整个系统中蔓延,服务A->服务B->服务C,服务C故障了,服务B也故障了,服务A故障了,整套分布式系统全部故障,整体宕机
(3)提供fail-fast(快速失败)和快速恢复的支持
(4)提供fallback优雅降级的支持
(5)支持近实时的监控、报警以及运维操作
调用延迟+失败,提供容错
阻止故障蔓延
快速失败+快速恢复
降级
监控+报警+运维
完全描述了hystrix的功能,提供整个分布式系统的高可用的架构
4、Hystrix要解决的问题是什么?
在复杂的分布式系统架构中,每个服务都有很多的依赖服务,而每个依赖服务都可能会故障
如果服务没有和自己的依赖服务进行隔离,那么可能某一个依赖服务的故障就会拖垮当前这个服务
举例来说,某个服务有30个依赖服务,每个依赖服务的可用性非常高,已经达到了99.99%的高可用性
那么该服务的可用性就是99.99%的30次方,也就是99.7%的可用性
99.7%的可用性就意味着3%的请求可能会失败,因为3%的时间内系统可能出现了故障不可用了
对于1亿次访问来说,3%的请求失败,也就意味着300万次请求会失败,也意味着每个月有2个小时的时间系统是不可用的
在真实生产环境中,可能更加糟糕
上面也就是说,即使你每个依赖服务都是99.99%高可用性,但是一旦你有几十个依赖服务,还是会导致你每个月都有几个小时是不可用的
画图分析说,当某一个依赖服务出现了调用延迟或者调用失败时,为什么会拖垮当前这个服务?以及在分布式系统中,故障是如何快速蔓延的?
5、再看Hystrix的更加细节的设计原则是什么?
(1)阻止任何一个依赖服务耗尽所有的资源,比如tomcat中的所有线程资源
(2)避免请求排队和积压,采用限流和fail fast来控制故障
(3)提供fallback降级机制来应对故障
(4)使用资源隔离技术,比如bulkhead(舱壁隔离技术),swimlane(泳道技术),circuit breaker(短路技术),来限制任何一个依赖服务的故障的影响
(5)通过近实时的统计/监控/报警功能,来提高故障发现的速度
(6)通过近实时的属性和配置热修改功能,来提高故障处理和恢复的速度
(7)保护依赖服务调用的所有故障情况,而不仅仅只是网络故障情况
调用这个依赖服务的时候,client调用包有bug,阻塞,等等,依赖服务的各种各样的调用的故障,都可以处理
6、Hystrix是如何实现它的目标的?
(1)通过HystrixCommand或者HystrixObservableCommand来封装对外部依赖的访问请求,这个访问请求一般会运行在独立的线程中,资源隔离
(2)对于超出我们设定阈值的服务调用,直接进行超时,不允许其耗费过长时间阻塞住。这个超时时间默认是99.5%的访问时间,但是一般我们可以自己设置一下
(3)为每一个依赖服务维护一个独立的线程池,或者是semaphore,当线程池已满时,直接拒绝对这个服务的调用
(4)对依赖服务的调用的成功次数,失败次数,拒绝次数,超时次数,进行统计
(5)如果对一个依赖服务的调用失败次数超过了一定的阈值,自动进行熔断,在一定时间内对该服务的调用直接降级,一段时间后再自动尝试恢复
(6)当一个服务调用出现失败,被拒绝,超时,短路等异常情况时,自动调用fallback降级机制
(7)对属性和配置的修改提供近实时的支持
Hystrix线程池隔离技术与信号量隔离技术的区别
1、线程池隔离技术与信号量隔离技术的区别
hystrix里面,核心的一项功能,其实就是所谓的资源隔离,要解决的最最核心的问题,就是将多个依赖服务的调用分别隔离到各自自己的资源池内避免说对某一个依赖服务的调用,因为依赖服务的接口调用的延迟或者失败,导致服务所有的线程资源全部耗费在这个服务的接口调用上
一旦说某个服务的线程资源全部耗尽的话,可能就导致服务就会崩溃,甚至说这种故障会不断蔓延
hystrix,资源隔离,两种技术,线程池的资源隔离,信号量的资源隔离
信号量,semaphore
信号量跟线程池,两种资源隔离的技术,区别到底在哪儿呢?
线程池:适合绝大多数的场景,99%的,线程池,对依赖服务的网络请求的调用和访问,timeout这种问题
信号量:适合,你的访问不是对外部依赖的访问,而是对内部的一些比较复杂的业务逻辑的访问,但是像这种访问,系统内部的代码,其实不涉及任何的网络请求,那么只要做信号量的普通限流就可以了,因为不需要去捕获timeout类似的问题,算法+数据结构的效率不是太高,并发量突然太高,因为这里稍微耗时一些,导致很多线程卡在这里的话,不太好,所以进行一个基本的资源隔离和访问,避免内部复杂的低效率的代码,导致大量的线程被hang住
3、在代码中加入从本地内存获取地理位置数据的逻辑
业务背景里面, 比较适合信号量的是什么场景呢?
比如说,我们一般来说,缓存服务,可能会将部分量特别少,访问又特别频繁的一些数据,放在自己的纯内存中
一般我们在获取到商品数据之后,都要去获取商品是属于哪个地理位置,省,市,卖家的,可能在自己的纯内存中,比如就一个Map去获取
对于这种直接访问本地内存的逻辑,比较适合用信号量做一下简单的隔离
优点在于,不用自己管理线程池拉,不用care timeout超时了,信号量做隔离的话,性能会相对来说高一些
Hystrix线程池+服务+接口划分以及资源池容量大小控制
资源隔离,两种策略,线程池隔离,信号量隔离对资源隔离这一块东西,做稍微更加深入一些的讲解,告诉你,除了可以选择隔离策略以外,对你选择的隔离策略,可以做一定的细粒度的一些控制
1、execution.isolation.strategy
指定了HystrixCommand.run()的资源隔离策略,THREAD或者SEMAPHORE,一种是基于线程池,一种是信号量
线程池机制,每个command运行在一个线程中,限流是通过线程池的大小来控制的
信号量机制,command是运行在调用线程中,但是通过信号量的容量来进行限流
如何在线程池和信号量之间做选择?
默认的策略就是线程池
线程池其实最大的好处就是对于网络访问请求,如果有超时的话,可以避免调用线程阻塞住
而使用信号量的场景,通常是针对超大并发量的场景下,每个服务实例每秒都几百的QPS,那么此时你用线程池的话,线程一般不会太多,可能撑不住那么高的并发,如果要撑住,可能要耗费大量的线程资源,那么就是用信号量,来进行限流保护
一般用信号量常见于那种基于纯内存的一些业务逻辑服务,而不涉及到任何网络访问请求
netflix有100+的command运行在40+的线程池中,只有少数command是不运行在线程池中的,就是从纯内存中获取一些元数据,或者是对多个command包装起来的facacde command,是用信号量限流的
// to use thread isolation
HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(ExecutionIsolationStrategy.THREAD)
// to use semaphore isolation
HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE)
2、command名称和command组
线程池隔离,依赖服务->接口->线程池,如何来划分
你的每个command,都可以设置一个自己的名称,同时可以设置一个自己的组
private static final Setter cachedSetter =
Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"));
public CommandHelloWorld(String name) {
super(cachedSetter);
this.name = name;
}
command group,是一个非常重要的概念,默认情况下,因为就是通过command group来定义一个线程池的,而且还会通过command group来聚合一些监控和报警信息
同一个command group中的请求,都会进入同一个线程池中
3、command线程池
threadpool key代表了一个HystrixThreadPool,用来进行统一监控,统计,缓存
默认的threadpool key就是command group名称
每个command都会跟它的threadpool key对应的thread pool绑定在一起
如果不想直接用command group,也可以手动设置thread pool name
public CommandHelloWorld(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("HelloWorldPool")));
this.name = name;
}
command threadpool -> command group -> command key
command key,代表了一类command,一般来说,代表了底层的依赖服务的一个接口
command group,代表了某一个底层的依赖服务,合理,一个依赖服务可能会暴露出来多个接口,每个接口就是一个command key
command group,在逻辑上去组织起来一堆command key的调用,统计信息,成功次数,timeout超时次数,失败次数,可以看到某一个服务整体的一些访问情况
command group,一般来说,推荐是根据一个服务去划分出一个线程池,command key默认都是属于同一个线程池的
比如说你以一个服务为粒度,估算出来这个服务每秒的所有接口加起来的整体QPS在100左右
你调用那个服务的当前服务,部署了10个服务实例,每个服务实例上,其实用这个command group对应这个服务,给一个线程池,量大概在10个左右,就可以了,你对整个服务的整体的访问QPS大概在每秒100左右
一般来说,command group是用来在逻辑上组合一堆command的
举个例子,对于一个服务中的某个功能模块来说,希望将这个功能模块内的所有command放在一个group中,那么在监控和报警的时候可以放一起看
command group,对应了一个服务,但是这个服务暴露出来的几个接口,访问量很不一样,差异非常之大
你可能就希望在这个服务command group内部,包含的对应多个接口的command key,做一些细粒度的资源隔离
对同一个服务的不同接口,都使用不同的线程池
command key -> command group
command key -> 自己的threadpool key
逻辑上来说,多个command key属于一个command group,在做统计的时候,会放在一起统计
每个command key有自己的线程池,每个接口有自己的线程池,去做资源隔离和限流
但是对于thread pool资源隔离来说,可能是希望能够拆分的更加一致一些,比如在一个功能模块内,对不同的请求可以使用不同的thread pool
command group一般来说,可以是对应一个服务,多个command key对应这个服务的多个接口,多个接口的调用共享同一个线程池
如果说你的command key,要用自己的线程池,可以定义自己的threadpool key,就ok了
4、coreSize
设置线程池的大小,默认是10
HystrixThreadPoolProperties.Setter()
.withCoreSize(int value)
一般来说,用这个默认的10个线程大小就够了
5、queueSizeRejectionThreshold
控制queue满后reject的threshold,因为maxQueueSize不允许热修改,因此提供这个参数可以热修改,控制队列的最大大小
HystrixCommand在提交到线程池之前,其实会先进入一个队列中,这个队列满了之后,才会reject
默认值是5
HystrixThreadPoolProperties.Setter()
.withQueueSizeRejectionThreshold(int value)
6、execution.isolation.semaphore.maxConcurrentRequests
设置使用SEMAPHORE隔离策略的时候,允许访问的最大并发量,超过这个最大并发量,请求直接被reject
这个并发量的设置,跟线程池大小的设置,应该是类似的,但是基于信号量的话,性能会好很多,而且hystrix框架本身的开销会小很多
默认值是10,设置的小一些,否则因为信号量是基于调用线程去执行command的,而且不能从timeout中抽离,因此一旦设置的太大,而且有延时发生,可能瞬间导致tomcat本身的线程资源本占满
HystrixCommandProperties.Setter()
.withExecutionIsolationSemaphoreMaxConcurrentRequests(int value)
代码实现
package com.roncoo.eshop.cache.ha.hystrix.command; import com.alibaba.fastjson.JSONObject; import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.HystrixCommandGroupKey; import com.netflix.hystrix.HystrixCommandKey; import com.netflix.hystrix.HystrixThreadPoolKey; import com.netflix.hystrix.HystrixThreadPoolProperties; import com.roncoo.eshop.cache.ha.http.HttpClientUtils; import com.roncoo.eshop.cache.ha.model.ProductInfo; /** * 获取单条商品信息 * @author Administrator * */ public class GetProductInfoCommand extends HystrixCommand<ProductInfo> { private Long productId; public GetProductInfoCommand(Long productId) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService")) .andCommandKey(HystrixCommandKey.Factory.asKey("GetProductInfoCommand")) .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("GetProductInfoPool")) .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter() .withCoreSize(15) .withQueueSizeRejectionThreshold(10)) ); this.productId = productId; } @Override protected ProductInfo run() throws Exception { String url = "http://127.0.0.1:8082/getProductInfo?productId=" + productId; String response = HttpClientUtils.sendGetRequest(url); return JSONObject.parseObject(response, ProductInfo.class); } }
一次性获取多条商品信息
/** * 一次性批量查询多条商品数据的请求 */ @RequestMapping("/getProductInfos") @ResponseBody public String getProductInfos(String productIds) { HystrixObservableCommand<ProductInfo> getProductInfosCommand = new GetProductInfosCommand(productIds.split(",")); Observable<ProductInfo> observable = getProductInfosCommand.observe(); // observable = getProductInfosCommand.toObservable(); // 还没有执行 observable.subscribe(new Observer<ProductInfo>() { // 等到调用subscribe然后才会执行 public void onCompleted() { System.out.println("获取完了所有的商品数据"); } public void onError(Throwable e) { e.printStackTrace(); } public void onNext(ProductInfo productInfo) { System.out.println(productInfo); } }); return "success"; }
package com.roncoo.eshop.cache.ha.hystrix.command; import rx.Observable; import rx.Subscriber; import rx.schedulers.Schedulers; import com.alibaba.fastjson.JSONObject; import com.netflix.hystrix.HystrixCommandGroupKey; import com.netflix.hystrix.HystrixObservableCommand; import com.roncoo.eshop.cache.ha.http.HttpClientUtils; import com.roncoo.eshop.cache.ha.model.ProductInfo; /** * 批量查询多个商品数据的command * @author Administrator * */ public class GetProductInfosCommand extends HystrixObservableCommand<ProductInfo> { private String[] productIds; public GetProductInfosCommand(String[] productIds) { super(HystrixCommandGroupKey.Factory.asKey("GetProductInfoGroup")); this.productIds = productIds; } @Override protected Observable<ProductInfo> construct() { return Observable.create(new Observable.OnSubscribe<ProductInfo>() { public void call(Subscriber<? super ProductInfo> observer) { try { for(String productId : productIds) { String url = "http://127.0.0.1:8082/getProductInfo?productId=" + productId; String response = HttpClientUtils.sendGetRequest(url); ProductInfo productInfo = JSONObject.parseObject(response, ProductInfo.class); observer.onNext(productInfo); } observer.onCompleted(); } catch (Exception e) { observer.onError(e); } } }).subscribeOn(Schedulers.io()); }
基于Hystrix的request cache请求缓存技术优化批量商品数据查询接口
1、创建command,2种command类型
2、执行command,4种执行方式
3、查找是否开启了request cache,是否有请求缓存,如果有缓存,直接取用缓存,返回结果
首先,有一个概念,叫做reqeust context,请求上下文,一般来说,在一个web应用中,hystrix
我们会在一个filter里面,对每一个请求都施加一个请求上下文,就是说,tomcat容器内,每一次请求,就是一次请求上下文
然后在这次请求上下文中,我们会去执行N多代码,调用N多依赖服务,有的依赖服务可能还会调用好几次
在一次请求上下文中,如果有多个command,参数都是一样的,调用的接口也是一样的,其实结果可以认为也是一样的
那么这个时候,我们就可以让第一次command执行,返回的结果,被缓存在内存中,然后这个请求上下文中,后续的其他对这个依赖的调用全部从内存中取用缓存结果就可以了
不用在一次请求上下文中反复多次的执行一样的command,提升整个请求的性能
HystrixCommand和HystrixObservableCommand都可以指定一个缓存key,然后hystrix会自动进行缓存,接着在同一个request context内,再次访问的时候,就会直接取用缓存
用请求缓存,可以避免重复执行网络请求
多次调用一个command,那么只会执行一次,后面都是直接取缓存
对于请求缓存(request caching),请求合并(request collapsing),请求日志(request log),等等技术,都必须自己管理HystrixReuqestContext的声明周期
在一个请求执行之前,都必须先初始化一个request context
HystrixRequestContext context = HystrixRequestContext.initializeContext();
然后在请求结束之后,需要关闭request context
context.shutdown();
一般来说,在java web来的应用中,都是通过filter过滤器来实现的
public class HystrixRequestContextServletFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try {
chain.doFilter(request, response);
} finally {
context.shutdown();
}
}
}
@Bean
public FilterRegistrationBean indexFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean(new HystrixRequestContextServletFilter());
registration.addUrlPatterns("/*");
return registration;
}
结合咱们的业务背景,我们做了一个批量查询商品数据的接口,在这个里面,我们其实通过HystrixObservableCommand一次性批量查询多个商品id的数据
但是这里有个问题,如果说nginx在本地缓存失效了,重新获取一批缓存,传递过来的productId都没有进行去重,1,1,2,2,5,6,7
那么可能说,商品id出现了重复,如果按照我们之前的业务逻辑,可能就会重复对productId=1的商品查询两次,productId=2的商品查询两次
我们对批量查询商品数据的接口,可以用request cache做一个优化,就是说一次请求,就是一次request context,对相同的商品查询只能执行一次,其余的都走request cache
/** * 一次性批量查询多条商品数据的请求 */ @RequestMapping("/getProductInfos") @ResponseBody public String getProductInfos(String productIds) { for(String productId : productIds.split(",")) { GetProductInfoCommand getProductInfoCommand = new GetProductInfoCommand( Long.valueOf(productId)); ProductInfo productInfo = getProductInfoCommand.execute(); System.out.println(productInfo); System.out.println(getProductInfoCommand.isResponseFromCache()); } return "success"; }
package com.roncoo.eshop.cache.ha.hystrix.command; import com.alibaba.fastjson.JSONObject; import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.HystrixCommandGroupKey; import com.netflix.hystrix.HystrixCommandKey; import com.netflix.hystrix.HystrixThreadPoolKey; import com.netflix.hystrix.HystrixThreadPoolProperties; import com.roncoo.eshop.cache.ha.http.HttpClientUtils; import com.roncoo.eshop.cache.ha.model.ProductInfo; /** * 获取商品信息 * @author Administrator * */ public class GetProductInfoCommand extends HystrixCommand<ProductInfo> { private Long productId; public GetProductInfoCommand(Long productId) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService")) .andCommandKey(HystrixCommandKey.Factory.asKey("GetProductInfoCommand")) .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("GetProductInfoPool")) .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter() .withCoreSize(15) .withQueueSizeRejectionThreshold(10)) ); this.productId = productId; } @Override protected ProductInfo run() throws Exception { String url = "http://127.0.0.1:8082/getProductInfo?productId=" + productId; String response = HttpClientUtils.sendGetRequest(url); System.out.println("调用接口,查询商品数据,productId=" + productId); return JSONObject.parseObject(response, ProductInfo.class); } @Override protected String getCacheKey() { return "product_info_" + productId; }
Hystrix的fallback机制
1、创建command2、执行command
3、request cache
4、短路器,如果打开了,fallback降级机制
1、fallback降级机制
hystrix调用各种接口,或者访问外部依赖,mysql,redis,zookeeper,kafka,等等,如果出现了任何异常的情况比如说报错了,访问mysql报错,redis报错,zookeeper报错,kafka报错,error
对每个外部依赖,无论是服务接口,中间件,资源隔离,对外部依赖只能用一定量的资源去访问,线程池/信号量,如果资源池已满,reject
访问外部依赖的时候,访问时间过长,可能就会导致超时,报一个TimeoutException异常,timeout
上述三种情况,都是我们说的异常情况,对外部依赖的东西访问的时候出现了异常,发送异常事件到短路器中去进行统计
如果短路器发现异常事件的占比达到了一定的比例,直接开启短路,circuit breaker
上述四种情况,都会去调用fallback降级机制
fallback,降级机制,你之前都是必须去调用外部的依赖接口,或者从mysql中去查询数据的,但是为了避免说可能外部依赖会有故障
比如,你可以再内存中维护一个ehcache,作为一个纯内存的基于LRU自动清理的缓存,数据也可以放入缓存内
如果说外部依赖有异常,fallback这里,直接尝试从ehcache中获取数据
比如说,本来你是从mysql,redis,或者其他任何地方去获取数据的,获取调用其他服务的接口的,结果人家故障了,人家挂了,fallback,可以返回一个默认值
两种最经典的降级机制:纯内存数据,默认值
run()抛出异常,超时,线程池或信号量满了,或短路了,都会调用fallback机制
给大家举个例子,比如说我们现在有个商品数据,brandId,品牌,一般来说,假设,正常的逻辑,拿到了一个商品数据以后,用brandId再调用一次请求,到其他的服务去获取品牌的最新名称
假如说,那个品牌服务挂掉了,那么我们可以尝试本地内存中,会保留一份时间比较过期的一份品牌数据,有些品牌没有,有些品牌的名称过期了,Nike++,Nike
调用品牌服务失败了,fallback降级就从本地内存中获取一份过期的数据,先凑合着用着
public class CommandHelloFailure extends HystrixCommand<String> {
private final String name;
public CommandHelloFailure(String name) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.name = name;
}
@Override
protected String run() {
throw new RuntimeException("this command always fails");
}
@Override
protected String getFallback() {
return "Hello Failure " + name + "!";
}
}
@Test
public void testSynchronous() {
assertEquals("Hello Failure World!", new CommandHelloFailure("World").execute());
}
HystrixObservableCommand,是实现resumeWithFallback方法
2、fallback.isolation.semaphore.maxConcurrentRequests
这个参数设置了HystrixCommand.getFallback()最大允许的并发请求数量,默认值是10,也是通过semaphore信号量的机制去限流
如果超出了这个最大值,那么直接被reject
HystrixCommandProperties.Setter()
.withFallbackIsolationSemaphoreMaxConcurrentRequests(int value)
双层嵌套command开发商品服务接口的多层降级机制
多级降级先降一级,尝试用一个备用方案去执行,如果备用方案失败了,再用最后下一个备用方案去执行
command嵌套command
尝试从备用服务器接口去拉取结果
给大家科普一下,常见的多级降级的做法,有一个操作,要访问MySQL数据库
mysql数据库访问报错,降级,去redis中获取数据
如果说redis又挂了,然后就去从本地ehcache缓存中获取数据
hystrix command fallback语义,很容易就可以实现多级降级的策略
商品服务接口,多级降级的策略
command,fallback,又套了一个command,第二个command其实是第一级降级策略
第二个command的fallback是第二级降级策略
第一级降级策略,可以是
storm,我们之前做storm这块,第一级降级,一般是搞一个storm的备用机房,部署了一套一模一样的拓扑,如果主机房中的storm拓扑挂掉了,备用机房的storm拓扑定顶上
如果备用机房的storm拓扑也挂了
第二级降级,可能就降级成用mysql/hbase/redis/es,手工封装的一套,按分钟粒度去统计数据的系统
第三级降级,离线批处理去做,hdfs+spark,每个小时执行一次数据统计,去降级
特别复杂,重要的系统,肯定是要搞好几套备用方案的,一个方案死了,立即上第二个方案,而且要尽量做到是自动化的
商品接口拉取
主流程,访问的商品服务,是从主机房去访问的,服务,如果主机房的服务出现了故障,机房断电,机房的网络负载过高,机器硬件出了故障
第一级降级策略,去访问备用机房的服务,至关重要的一点:在做多级降级时,要将降级的command线程池单独做一个出来,
如果主流程的command都失败了,可能线程池都占满了,所以降级的command必须用自己独立的线程池
第二级降级策略,用stubbed fallback降级策略,比较常用的,返回一些残缺的数据回去
代码演示
package com.roncoo.eshop.cache.ha.hystrix.command; import java.text.SimpleDateFormat; import java.util.Date; import com.alibaba.fastjson.JSONObject; import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.HystrixCommandGroupKey; import com.netflix.hystrix.HystrixCommandKey; import com.netflix.hystrix.HystrixThreadPoolKey; import com.roncoo.eshop.cache.ha.cache.local.BrandCache; import com.roncoo.eshop.cache.ha.cache.local.LocationCache; import com.roncoo.eshop.cache.ha.http.HttpClientUtils; import com.roncoo.eshop.cache.ha.model.ProductInfo; /** * 获取商品信息 * @author Administrator * */ public class GetProductInfoCommand extends HystrixCommand<ProductInfo> { public static final HystrixCommandKey KEY = HystrixCommandKey.Factory.asKey("GetProductInfoCommand"); private Long productId; public GetProductInfoCommand(Long productId) { super(HystrixCommandGroupKey.Factory.asKey("ProductInfoService")); this.productId = productId; } @Override protected ProductInfo run() throws Exception { if(productId.equals(-1L)) { throw new Exception(); } if(productId.equals(-2L)) { throw new Exception(); } String url = "http://127.0.0.1:8082/getProductInfo?productId=" + productId; String response = HttpClientUtils.sendGetRequest(url); return JSONObject.parseObject(response, ProductInfo.class); } @Override protected ProductInfo getFallback() { return new FirstLevelFallbackCommand(productId).execute(); } private static class FirstLevelFallbackCommand extends HystrixCommand<ProductInfo> { private Long productId; public FirstLevelFallbackCommand(Long productId) { // 第一级的降级策略,因为这个command是运行在fallback中的 // 所以至关重要的一点是,在做多级降级的时候,要将降级command的线程池单独做一个出来 // 如果主流程的command都失败了,可能线程池都已经被占满了 // 降级command必须用自己的独立的线程池 super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService")) .andCommandKey(HystrixCommandKey.Factory.asKey("FirstLevelFallbackCommand")) .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("FirstLevelFallbackPool")) ); this.productId = productId; } @Override protected ProductInfo run() throws Exception { // 这里,因为是第一级降级的策略,所以说呢,其实是要从备用机房的机器去调用接口 // 但是,我们这里没有所谓的备用机房,所以说还是调用同一个服务来模拟 if(productId.equals(-2L)) { throw new Exception(); } String url = "http://127.0.0.1:8082/getProductInfo?productId=" + productId; String response = HttpClientUtils.sendGetRequest(url); return JSONObject.parseObject(response, ProductInfo.class); } @Override protected ProductInfo getFallback() { // 第二级降级策略,第一级降级策略,都失败了 ProductInfo productInfo = new ProductInfo(); // 从请求参数中获取到的唯一条数据 productInfo.setId(productId); // 从本地缓存中获取一些数据 productInfo.setBrandId(BrandCache.getBrandId(productId)); productInfo.setBrandName(BrandCache.getBrandName(productInfo.getBrandId())); productInfo.setCityId(LocationCache.getCityId(productId)); productInfo.setCityName(LocationCache.getCityName(productInfo.getCityId())); // 手动填充一些默认的数据 productInfo.setColor("默认颜色"); productInfo.setModifiedTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); productInfo.setName("默认商品"); productInfo.setPictureList("default.jpg"); productInfo.setPrice(0.0); productInfo.setService("默认售后服务"); productInfo.setShopId(-1L); productInfo.setSize("默认大小"); productInfo.setSpecification("默认规格"); return productInfo; } } }
Hystrix的手动降级
手动降级你写一个command,在这个command它的主流程中,根据一个标识位,判断要执行哪个流程
可以执行主流程,command,也可以执行一个备用降级的command
一般来说,都是去执行一个主流程的command,如果说你现在知道有问题了,希望能够手动降级的话,动态给服务发送个请求
在请求中修改标识位,自动就让command以后都直接过来执行备用command
3个command,套在最外面的command,是用semaphore信号量做限流和资源隔离的,因为这个command不用去care timeout的问题,嵌套调用的command会自己去管理timeout超时的
商品服务接口的手动降级的方案
主流程还是去走GetProductInfoCommand,手动降级的方案,比如说是从某一个数据源,自己去简单的获取一些数据,尝试封装一下返回
手动降级的策略,就比较low了,调用别人的接口去获取数据的,业务逻辑的封装
主流程有问题,那么可能你就需要立即自己写一些逻辑发布上去,从mysql数据库的表中获取一些数据去返回,手动调整一下降级标识,做一下手动降级
代码演示
package com.roncoo.eshop.cache.ha.hystrix.command; import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.HystrixCommandGroupKey; import com.netflix.hystrix.HystrixCommandKey; import com.netflix.hystrix.HystrixCommandProperties; import com.netflix.hystrix.HystrixCommandProperties.ExecutionIsolationStrategy; import com.roncoo.eshop.cache.ha.degrade.IsDegrade; import com.roncoo.eshop.cache.ha.model.ProductInfo; public class GetProductInfoFacadeCommand extends HystrixCommand<ProductInfo> { private Long productId; public GetProductInfoFacadeCommand(Long productId) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService")) .andCommandKey(HystrixCommandKey.Factory.asKey("GetProductInfoFacadeCommand")) .andCommandPropertiesDefaults(HystrixCommandProperties.Setter() //使用信号量 .withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE) .withExecutionIsolationSemaphoreMaxConcurrentRequests(15))); this.productId = productId; } @Override protected ProductInfo run() throws Exception { if(!IsDegrade.isDegrade()) { //如果不降级所执行的command return new GetProductInfoCommand(productId).execute(); } else { //手动降级之后所执行的command return new GetProductInfoFromMySQLCommand(productId).execute(); } } @Override protected ProductInfo getFallback() { return new ProductInfo(); } }
public class IsDegrade { private static boolean degrade = false; public static boolean isDegrade() { return degrade; } public static void setDegrade(boolean degrade) { IsDegrade.degrade = degrade; } }
生产环境中的线程池大小以及timeout超时时长优化经验总结
生产环境里面,一个是线程池的大小怎么设置,timeout时长怎么不合理的话,问题还是很大的
在生产环境中部署一个短路器,一开始需要将一些关键配置设置的大一些,比如timeout超时时长,线程池大小,或信号量容量
然后逐渐优化这些配置,直到在一个生产系统中运作良好
(1)一开始先不要设置timeout超时时长,默认就是1000ms,也就是1s
(2)一开始也不要设置线程池大小,默认就是10
(3)直接部署hystrix到生产环境,如果运行的很良好,那么就让它这样运行好了
(4)让hystrix应用,24小时运行在生产环境中
(5)依赖标准的监控和报警机制来捕获到系统的异常运行情况
(6)在24小时之后,看一下调用延迟的占比,以及流量,来计算出让短路器生效的最小的配置数字
(7)直接对hystrix配置进行热修改,然后继续在hystrix dashboard上监控
(8)看看修改配置后的系统表现有没有改善
下面是根据系统表现优化和调整线程池大小,队列大小,信号量容量,以及timeout超时时间的经验
假设对一个依赖服务的高峰调用QPS是每秒30次
一开始如果默认的线程池大小是10
我们想的是,理想情况下,每秒的高峰访问次数 * 99%的访问延时 + buffer = 30 * 0.2 + 4 = 10线程,10个线程每秒处理30次访问应该足够了,每个线程处理3次访问
此时,我们合理的timeout设置应该为300ms,也就是99.5%的访问延时,计算方法是,因为判断每次访问延时最多在250ms(TP99如果是200ms的话),再加一次重试时间50ms,就是300ms,感觉也应该足够了
因为如果timeout设置的太多了,比如400ms,比如如果实际上,在高峰期,还有网络情况较差的时候,可能每次调用要耗费350ms,也就是达到了最长的访问时长
那么每个线程处理2个请求,就会执行700ms,然后处理第三个请求的时候,就超过1秒钟了,此时会导致线程池全部被占满,都在处理请求
这个时候下一秒的30个请求再进来了,那么就会导致线程池已满,拒绝请求的情况,就会调用fallback降级机制
因此对于短路器来说,timeout超时一般应该设置成TP99.5,比如设置成300ms,那么可以确保说,10个线程,每个线程处理3个访问,每个访问最多就允许执行300ms,过时就timeout了
这样才能保证说每个线程都在1s内执行完,才不会导致线程池被占满,然后后续的请求过来大量的reject
对于线程池大小来说,一般应该控制在10个左右,20个以内,最少5个,不要太多,也不要太少
大家可能会想,每秒的高峰访问次数是30次,如果是300次,甚至是3000次,30000次呢???
30000 * 0.2 = 6000 + buffer = 6100,一个服务器内一个线程池给6000个线程把
如果你一个依赖服务占据的线程数量太多的话,会导致其他的依赖服务对应的线程池里没有资源可以用了
6000 / 20 = 300台虚拟机也是ok的
虚拟机,4个cpu core,4G内存,虚拟机,300台
物理机,十几个cpu core,几十个G的内存,5~8个虚拟机,300个虚拟机 = 50台物理机
你要真的说是,你的公司服务的用户量,或者数据量,或者请求量,真要是到了每秒几万的QPS,
3万QPS,60 * 3 = 180万访问量,1800,1亿8千,1亿,10个小时,10亿的访问量,app,系统
几十台服务器去支撑,我觉得很正常
QPS每秒在几千都算多的了
生产环境中的线程池自动扩容与缩容的动态资源分配经验
可能会出现一种情况,比如说我们的某个依赖,在高峰期,需要耗费100个线程,但是在那个时间段,刚好其他的依赖的线程池其实就维持一两个就可以了但是,如果我们都是设置死的,每个服务就给10个线程,那就很坑,可能就导致有的服务在高峰期需要更多的资源,但是没资源了,导致很多的reject
但是其他的服务,每秒钟就易一两个请求,结果也占用了10个线程,占着茅坑不拉屎
做成弹性的线程资源调度的模式
刚开始的时候,每个依赖服务都是给1个线程,3个线程,但是我们允许说,如果你的某个线程池突然需要大量的线程,最多可以到100个线程
如果你使用了100个线程,高峰期过去了,自动将空闲的线程给释放掉
(1)coreSize
设置线程池的大小,默认是10HystrixThreadPoolProperties.Setter()
.withCoreSize(int value)
(2)maximumSize
设置线程池的最大大小,只有在设置allowMaximumSizeToDivergeFromCoreSize的时候才能生效
默认是10
HystrixThreadPoolProperties.Setter()
.withMaximumSize(int value)
(5)keepAliveTimeMinutes
设置保持存活的时间,单位是分钟,默认是1
如果设置allowMaximumSizeToDivergeFromCoreSize为true,那么coreSize就不等于maxSize,此时线程池大小是可以动态调整的,可以获取新的线程,也可以释放一些线程
如果coreSize < maxSize,那么这个参数就设置了一个线程多长时间空闲之后,就会被释放掉
HystrixThreadPoolProperties.Setter()
.withKeepAliveTimeMinutes(int value)
(6)allowMaximumSizeToDivergeFromCoreSize
允许线程池大小自动动态调整,设置为true之后,maxSize就生效了,此时如果一开始是coreSize个线程,随着并发量上来,那么就会自动获取新的线程,但是如果线程在keepAliveTimeMinutes内空闲,就会被自动释放掉
默认是false
HystrixThreadPoolProperties.Setter()
.withAllowMaximumSizeToDivergeFromCoreSize(boolean value)
生产环境中,这块怎么玩儿的
也是根据你的服务的实际的运行的情况切看的,比如说你发现某个服务,平时3个并发QPS就够了,高峰期可能要到30个那么你就可以给设置弹性的资源调度
因为你可能一个服务会有多个线程池,你要计算好,每个线程池的最大的大小加起来不能过大,30个依赖,30个线程池,每个线程池最大给到30,900个线程,很坑的
还有一种模式,就是说让多个依赖服务共享一个线程池,我们不推荐,多个依赖服务就做不到资源隔离,互相之间会影响的
么体系
metric统计相关的各种高阶配置
1、为什么需要监控与报警?HystrixCommand执行的时候,会生成一些执行耗时等方面的统计信息。这些信息对于系统的运维来说,是很有帮助的,因为我们通过这些统计信息可以看到整个系统是怎么运行的。hystrix对每个command key都会提供一份metric,而且是秒级统计粒度的。
这些统计信息,无论是单独看,还是聚合起来看,都是很有用的。如果将一个请求中的多个command的统计信息拿出来单独查看,包括耗时的统计,对debug系统是很有帮助的。聚合起来的metric对于系统层面的行为来说,是很有帮助的,很适合做报警或者报表。hystrix dashboard就很适合。
2、hystrix的事件类型
对于hystrix command来说,只会返回一个值,execute只有一个event type,fallback也只有一个event type,那么返回一个SUCCESS就代表着命令执行的结束
对于hystrix observable command来说,多个值可能被返回,所以emit event代表一个value被返回,success代表成功,failure代表异常
(1)execute event type
EMIT observable command返回一个value
SUCCESS 完成执行,并且没有报错
FAILURE 执行时抛出了一个异常,会触发fallback
TIMEOUT 开始执行了,但是在指定时间内没有完成执行,会触发fallback
BAD_REQUEST 执行的时候抛出了一个HystrixBadRequestException
SHORT_CIRCUITED 短路器打开了,触发fallback
THREAD_POOL_REJECTED 线程成的容量满了,被reject,触发fallback
SEMAPHORE_REJECTED 信号量的容量满了,被reject,触发fallback
(2)fallback event type
FALLBACK_EMIT observable command,fallback value被返回了
FALLBACK_SUCCESS fallback逻辑执行没有报错
FALLBACK_FAILURE fallback逻辑抛出了异常,会报错
FALLBACK_REJECTION fallback的信号量容量满了,fallback不执行,报错
FALLBACK_MISSING fallback没有实现,会报错
(3)其他的event type
EXCEPTION_THROWN command生命自周期是否抛出了异常
RESPONSE_FROM_CACHE command是否在cache中查找到了结果
COLLAPSED command是否是一个合并batch中的一个
(4)thread pool event type
EXECUTED 线程池有空间,允许command去执行了
REJECTED 线程池没有空间,不允许command执行,reject掉了
(5)collapser event type
BATCH_EXECUTED collapser合并了一个batch,并且执行了其中的command
ADDED_TO_BATCH command加入了一个collapser batch
RESPONSE_FROM_CACHE 没有加入batch,而是直接取了request cache中的数据
3、metric storage
metric被生成之后,就会按照一段时间来存储,存储了一段时间的数据才会推送到其他系统中,比如hystrix dashboard
另外一种方式,就是每次生成metric就实时推送metric流到其他地方,但是这样的话,会给系统带来很大的压力
hystrix的方式是将metric写入一个内存中的数据结构中,在一段时间之后就可以查询到
hystrix 1.5x之后,采取的是为每个command key都生成一个start event和completion event流,而且可以订阅这个流。每个thread pool key也是一样的,包括每个collapser key也是一样的。
每个command的event是发送给一个线程安全的RxJava中的rx.Subject,因为是线程安全的,所以不需要进行线程同步
因此每个command级别的,threadpool级别的,每个collapser级别的,event都会发送到对应的RxJava的rx.Subject对象中。这些rx.Subject对象接着就会被暴露出Observable接口,可以被订阅。
5、metric统计相关的配置
(1)metrics.rollingStats.timeInMilliseconds
设置统计的rolling window,单位是毫秒,hystrix只会维持这段时间内的metric供短路器统计使用
这个属性是不允许热修改的,默认值是10000,就是10秒钟
HystrixCommandProperties.Setter()
.withMetricsRollingStatisticalWindowInMilliseconds(int value)
(2)metrics.rollingStats.numBuckets
该属性设置每个滑动窗口被拆分成多少个bucket,而且滑动窗口对这个参数必须可以整除,同样不允许热修改
默认值是10,也就是说,每秒钟是一个bucket
随着时间的滚动,比如又过了一秒钟,那么最久的一秒钟的bucket就会被丢弃,然后新的一秒的bucket会被创建
HystrixCommandProperties.Setter()
.withMetricsRollingStatisticalWindowBuckets(int value)
(3)metrics.rollingPercentile.enabled
控制是否追踪请求耗时,以及通过百分比方式来统计,默认是true
HystrixCommandProperties.Setter()
.withMetricsRollingPercentileEnabled(boolean value)
(4)metrics.rollingPercentile.timeInMilliseconds
设置rolling window被持久化保存的时间,这样才能计算一些请求耗时的百分比,默认是60000,60s,不允许热修改
相当于是一个大的rolling window,专门用于计算请求执行耗时的百分比
HystrixCommandProperties.Setter()
.withMetricsRollingPercentileWindowInMilliseconds(int value)
(5)metrics.rollingPercentile.numBuckets
设置rolling percentile window被拆分成的bucket数量,上面那个参数除以这个参数必须能够整除,不允许热修改
默认值是6,也就是每10s被拆分成一个bucket
HystrixCommandProperties.Setter()
.withMetricsRollingPercentileWindowBuckets(int value)
(6)metrics.rollingPercentile.bucketSize
设置每个bucket的请求执行次数被保存的最大数量,如果再一个bucket内,执行次数超过了这个值,那么就会重新覆盖从bucket的开始再写
举例来说,如果bucket size设置为100,而且每个bucket代表一个10秒钟的窗口,但是在这个bucket内发生了500次请求执行,那么这个bucket内仅仅会保留100次执行
如果调大这个参数,就会提升需要耗费的内存,来存储相关的统计值,不允许热修改
默认值是100
HystrixCommandProperties.Setter()
.withMetricsRollingPercentileBucketSize(int value)
(7)metrics.healthSnapshot.intervalInMilliseconds
控制成功和失败的百分比计算,与影响短路器之间的等待时间,默认值是500毫秒
HystrixCommandProperties.Setter()
.withMetricsHealthSnapshotIntervalInMilliseconds(int value)
生产环境中的hystrix分布式系统的工程运维经验总结
如果发现了严重的依赖调用延时,先不用急着去修改配置,如果一个command被限流了,可能本来就应该限流在netflix早期的时候,经常会有人在发现短路器因为访问延时发生的时候,去热修改一些配置,比如线程池大小,队列大小,超时时长,等等,给更多的资源,但是这其实是不对的
如果我们之前对系统进行了良好的配置,然后现在在高峰期,系统在进行线程池reject,超时,短路,那么此时我们应该集中精力去看底层根本的原因,而不是调整配置
为什么在高峰期,一个10个线程的线程池,搞不定这些流量呢???代码写的太烂了,异步,更好的算法
千万不要急于给你的依赖调用过多的资源,比如线程池大小,队列大小,超时时长,信号量容量,等等,因为这可能导致我们自己对自己的系统进行DDOS攻击
疯狂的大量的访问你的机器,最后给打垮
举例来说,想象一下,我们现在有100台服务器组成的集群,每台机器有10个线程大小的线程池去访问一个服务,那么我们对那个服务就有1000个线程资源去访问了
在正常情况下,可能只会用到其中200~300个线程去访问那个后端服务
但是如果再高峰期出现了访问延时,可能导致1000个线程全部被调用去访问那个后端服务,如果我们调整到每台服务器20个线程呢?
如果因为你的代码等问题导致访问延时,即使有20个线程可能还是会导致线程池资源被占满,此时就有2000个线程去访问后端服务,可能对后端服务就是一场灾难
这就是断路器的作用了,如果我们把后端服务打死了,或者产生了大量的压力,有大量的timeout和reject,那么就自动短路,一段时间后,等流量洪峰过去了,再重启访问
简单来说,让系统自己去限流,短路,超时,以及reject,直到系统重新变得正常了
就是不要随便乱改资源配置,不要随便乱增加线程池大小,等待队列大小,异常情况是正常的
缓存雪崩出现的原因
缓存雪崩这种场景,缓存架构中非常重要的一个环节,应对缓存雪崩的解决方案,避免缓存雪崩的时候,造成整个系统崩溃,带来巨大的经济损失
1、redis集群彻底崩溃
2、缓存服务大量对redis的请求hang住,占用资源
3、缓存服务大量的请求打到源头服务去查询mysql,直接打死mysql
4、源头服务因为mysql被打死也崩溃,对源服务的请求也hang住,占用资源
5、缓存服务大量的资源全部耗费在访问redis和源服务无果,最后自己被拖死,无法提供服务
6、nginx无法访问缓存服务,redis和源服务,只能基于本地缓存提供服务,但是缓存过期后,没有数据提供
7、网站崩溃
行业里真实的缓存雪崩的经验和教训
某电商,之前就是出现过,整个缓存的集群彻底崩溃了,因为主要是集群本身的bug,导致自己把自己给弄死了,虽然当时也是部署了双机房的,但是还是死了
电商大量的,几乎所有的应用都是基于那个缓存集群去开发的
导致各种服务的线程资源全部被耗尽,然后用在了访问那个缓存集群时的等待、超时和报错上了
然后导致各种服务就没有资源对外提供服务咯
然后各种降级措施也没做好,直接就是整体系统的全盘崩溃
导致网站就没法对外出售商品咯,导致了很大数额的经济的损失
redis缓存雪崩的应对解决方案
相对来说,考虑的比较完善的一套方案,分为事前,事中,事后三个层次去思考怎么来应对缓存雪崩的场景
1、事前解决方案
发生缓存雪崩之前,事情之前,怎么去避免redis彻底挂掉
redis本身的高可用性,复制,主从架构,操作主节点,读写,数据同步到从节点,一旦主节点挂掉,从节点跟上
双机房部署,一套redis cluster,部分机器在一个机房,另一部分机器在另外一个机房
还有一种部署方式,两套redis cluster,两套redis cluster之间做一个数据的同步,redis集群是可以搭建成树状的结构的
一旦说单个机房出了故障,至少说另外一个机房还能有些redis实例提供服务
2、事中解决方案
redis cluster已经彻底崩溃了,已经开始大量的访问无法访问到redis了
(1)ehcache本地缓存
所做的多级缓存架构的作用上了,ehcache的缓存,应对零散的redis中数据被清除掉的现象,另外一个主要是预防redis彻底崩溃
多台机器上部署的缓存服务实例的内存中,还有一套ehcache的缓存
ehcache的缓存还能支撑一阵
(2)对redis访问的资源隔离
(3)对源服务访问的限流以及资源隔离
3、事后解决方案
(1)redis数据可以恢复,做了备份,redis数据备份和恢复,redis重新启动起来
(2)redis数据彻底丢失了,或者数据过旧,快速缓存预热,redis重新启动起来
redis对外提供服务
缓存服务里,熔断策略,自动可以恢复,half-open,发现redis可以访问了,自动恢复了,自动就继续去访问redis了
基于hystrix的高可用服务这块技术之后,先讲解缓存服务如何设计成高可用的架构
缓存架构应对高并发下的缓存雪崩的解决方案,基于hystrix去做缓存服务的保护
事中,ehcache本身也做好了
源服务如果自己本身不知道什么原因出了故障,我们怎么去保护,调用商品服务的接口大量的报错、超时
限流,资源隔离,降级
解决具体代码实现
redis这一块,全都用hystrix的command进行封装,做资源隔离,确保说,redis的访问只能在固定的线程池内的资源来进行访问
哪怕是redis访问的很慢,有等待和超时,也不要紧,只有少量额线程资源用来访问,缓存服务不会被拖垮
package com.roncoo.eshop.cache.service.impl; import javax.annotation.Resource; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import redis.clients.jedis.JedisCluster; import com.roncoo.eshop.cache.hystrix.command.GetProductInfoFromReidsCacheCommand; import com.roncoo.eshop.cache.hystrix.command.GetShopInfoFromReidsCacheCommand; import com.roncoo.eshop.cache.hystrix.command.SaveProductInfo2ReidsCacheCommand; import com.roncoo.eshop.cache.hystrix.command.SaveShopInfo2ReidsCacheCommand; import com.roncoo.eshop.cache.model.ProductInfo; import com.roncoo.eshop.cache.model.ShopInfo; import com.roncoo.eshop.cache.service.CacheService; /** * 缓存Service实现类 * @author Administrator * */ @Service("cacheService") public class CacheServiceImpl implements CacheService { public static final String CACHE_NAME = "local"; @Resource private JedisCluster jedisCluster; /** * 将商品信息保存到本地缓存中 * @param productInfo * @return */ @CachePut(value = CACHE_NAME, key = "'key_'+#productInfo.getId()") public ProductInfo saveLocalCache(ProductInfo productInfo) { return productInfo; } /** * 从本地缓存中获取商品信息 * @param id * @return */ @Cacheable(value = CACHE_NAME, key = "'key_'+#id") public ProductInfo getLocalCache(Long id) { return null; } /** * 将商品信息保存到本地的ehcache缓存中 * @param productInfo */ @CachePut(value = CACHE_NAME, key = "'product_info_'+#productInfo.getId()") public ProductInfo saveProductInfo2LocalCache(ProductInfo productInfo) { return productInfo; } /** * 从本地ehcache缓存中获取商品信息 * @param productId * @return */ @Cacheable(value = CACHE_NAME, key = "'product_info_'+#productId") public ProductInfo getProductInfoFromLocalCache(Long productId) { return null; } /** * 将店铺信息保存到本地的ehcache缓存中 * @param productInfo */ @CachePut(value = CACHE_NAME, key = "'shop_info_'+#shopInfo.getId()") public ShopInfo saveShopInfo2LocalCache(ShopInfo shopInfo) { return shopInfo; } /** * 从本地ehcache缓存中获取店铺信息 * @param productId * @return */ @Cacheable(value = CACHE_NAME, key = "'shop_info_'+#shopId") public ShopInfo getShopInfoFromLocalCache(Long shopId) { return null; } /** * 将商品信息保存到redis中 * @param productInfo */ public void saveProductInfo2ReidsCache(ProductInfo productInfo) { SaveProductInfo2ReidsCacheCommand command = new SaveProductInfo2ReidsCacheCommand(productInfo); command.execute(); } /** * 将店铺信息保存到redis中 * @param productInfo */ public void saveShopInfo2ReidsCache(ShopInfo shopInfo) { SaveShopInfo2ReidsCacheCommand command = new SaveShopInfo2ReidsCacheCommand(shopInfo); command.execute(); } /** * 从redis中获取商品信息 * @param productInfo */ public ProductInfo getProductInfoFromReidsCache(Long productId) { GetProductInfoFromReidsCacheCommand command = new GetProductInfoFromReidsCacheCommand(productId); return command.execute(); } /** * 从redis中获取店铺信息 * @param productInfo */ public ShopInfo getShopInfoFromReidsCache(Long shopId) { GetShopInfoFromReidsCacheCommand command = new GetShopInfoFromReidsCacheCommand(shopId); return command.execute(); } }
所有redis服务中与redis-cluster交互的方法都以HystrixCommand封装,如下示例
package com.roncoo.eshop.cache.hystrix.command; import redis.clients.jedis.JedisCluster; import com.alibaba.fastjson.JSONObject; import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.HystrixCommandGroupKey; import com.roncoo.eshop.cache.model.ProductInfo; import com.roncoo.eshop.cache.spring.SpringContext; public class GetProductInfoFromReidsCacheCommand extends HystrixCommand<ProductInfo> { private Long productId; public GetProductInfoFromReidsCacheCommand(Long productId) { super(HystrixCommandGroupKey.Factory.asKey("RedisGroup")); this.productId = productId; } @Override protected ProductInfo run() throws Exception { JedisCluster jedisCluster = (JedisCluster) SpringContext.getApplicationContext() .getBean("JedisClusterFactory"); String key = "product_info_" + productId; String json = jedisCluster.get(key); if(json != null) { return JSONObject.parseObject(json, ProductInfo.class); } return null; } @Override protected ProductInfo getFallback() { return null; } }
为redis集群崩溃时的访问失败增加fail silent容错机制
资源隔离,避免说redis访问频繁失败,或者频繁超时的时候,耗尽大量的tomcat容器的资源去hang在redis的访问上
限定只有一部分线程资源可以用来访问redis你是不是说,如果redis集群彻底崩溃了,这个时候,可能command对redis的访问大量的报错和timeout超时,进而熔断(短路)
这是调用降级机制,fallback
使用fail silent模式,fallback里面直接返回一个空值,比如一个null,最简单了,缓存服务依然保持可用不被redis集群拖死
在外面调用redis的代码(CacheService类),是感知不到redis的访问异常的,只要你把timeout、熔断、熔断恢复、降级,都做好了
可能会出现的情况是,当redis集群崩溃的时候,CacheService获取到的是大量的null空值
根据这个null空值,我们还可以去做多级缓存的降级访问,nginx本地缓存,redis分布式集群缓存,ehcache本地缓存,CacheController
为redis集群崩溃时的场景部署定制化的熔断策略
缓存雪崩的解决方案,事中,发生缓存雪崩的时候,解决方案redis集群崩溃的时候,会怎么样?
(1)首先大量的等待,超时,报错
(2)如果是短时间内报错,会直接走fallback降级,直接返回null
(3)超时控制,你应该判断说redis访问超过了多长时间,就直接给timeout掉了
不推荐说用默认的值,一般不太精准,redis的访问你首先自己先统计一下访问时长的百分比,hystrix dashboard,TP90 TP95 TP99
一般来说,redis访问,假设说TP99在100ms,那么此时,你的timeout稍微多给一些,100ms
1、timeout超时控制
HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(int value)
意义在于哪里,一旦说redis出现了大面积的故障,此时肯定是访问的时候大量的超过100ms,大量的在等待和超时
就可以确保说,大量的请求不会hang住过长的时间,比如说hang住个1s,500ms,100ms直接就报timeout,走fallback降级了
2、熔断策略
(1)circuitBreaker.requestVolumeThreshold
设置一个rolling window,滑动窗口中,最少要有多少个请求时,才触发开启短路
举例来说,如果设置为20(默认值),那么在一个10秒的滑动窗口内,如果只有19个请求,即使这19个请求都是异常的,也是不会触发开启短路器的
HystrixCommandProperties.Setter()
.withCircuitBreakerRequestVolumeThreshold(int value)
我们应该根据我们自己的平时的访问流量去设置,而不是用默认值,比如说,我们认为平时一般的时候,流量也可以在每秒在QPS 100,10秒的滑动窗口就是1000
一般来说,你可以设置这样的一个值,根据你自己的系统的流量去设置
假如说,你设置的太少了,或者太多了,都不太合适
举个例子,你设置一个20,结果在晚上最低峰的时候,刚好是30,可能晚上的时候因为访问不频繁,大量的找不到缓存,可能超时频繁了一些,结果直接就给短路了
(2)circuitBreaker.errorThresholdPercentage
设置异常请求量的百分比,当异常请求达到这个百分比时,就触发打开短路器,默认是50,也就是50%
HystrixCommandProperties.Setter()
.withCircuitBreakerErrorThresholdPercentage(int value)
我们最好还是自己定制,自己设置,你说如果是要50%的时候才短路的话,会有什么情况呢,10%短路,也不太靠谱,90%异常才短路,又已经接近了调用服务崩溃的边缘
我觉得这个值可以稍微高一些,redis集群彻底崩溃,那么基本上就是所有的请求,100%都会异常,60%,70%比较适合
也有可能偶然出现网络的抖动,导致比如说就这10秒钟,访问延时高了一些,其实可能并不需要立即就短路,可能下10秒马上就恢复了
金融支付类的接口,可能这个比例就会设置的很低,因为对异常系统必须要很敏感,可能就是10%异常了,就直接短路了,不让继续访问了
比如金融支付类的接口,正常来说,是很重要的,而且必须是很稳定,我们不能容忍任何的延迟或者是报错
一旦支付类的接口,有10%的异常的话,我们基本就可以认为这个接口已经出问题了,再继续访问的话,也许访问的就是有问题的接口,可能造成资金的错乱,等给公司造成损失
熔断,不让访问了,走降级策略
就是对整个系统,是一个安全性的保障
(3)circuitBreaker.sleepWindowInMilliseconds
设置在短路之后,需要在多长时间内直接reject请求,然后在这段时间之后,再重新导holf-open状态,尝试允许请求通过以及自动恢复,默认值是5000毫秒
HystrixCommandProperties.Setter()
.withCircuitBreakerSleepWindowInMilliseconds(int value)
如果redis集群崩溃了,默认时间的话会在5s内就直接恢复,设置为1分钟比较合适
代码示例
package com.roncoo.eshop.cache.hystrix.command; import redis.clients.jedis.JedisCluster; import com.alibaba.fastjson.JSONObject; import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.HystrixCommandGroupKey; import com.netflix.hystrix.HystrixCommandProperties; import com.roncoo.eshop.cache.model.ProductInfo; import com.roncoo.eshop.cache.spring.SpringContext; public class GetProductInfoFromReidsCacheCommand extends HystrixCommand<ProductInfo> { private Long productId; public GetProductInfoFromReidsCacheCommand(Long productId) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RedisGroup")) .andCommandPropertiesDefaults(HystrixCommandProperties.Setter() //超时响应时间 .withExecutionTimeoutInMilliseconds(100) //十秒内有多少个请求异常达到打开断路器的要求 .withCircuitBreakerRequestVolumeThreshold(1000) //异常请求达到多少比例打开断路器,结合上述设置一起生效 .withCircuitBreakerErrorThresholdPercentage(70) //设置在短路之后,需要在多长时间内直接reject请求,然后在这段时间之后, //再重新导holf-open状态,尝试允许请求通过以及自动恢复,默认时间5000毫秒 .withCircuitBreakerSleepWindowInMilliseconds(60 * 1000)) ); this.productId = productId; } @Override protected ProductInfo run() throws Exception { JedisCluster jedisCluster = (JedisCluster) SpringContext.getApplicationContext() .getBean("JedisClusterFactory"); String key = "product_info_" + productId; String json = jedisCluster.get(key); if(json != null) { return JSONObject.parseObject(json, ProductInfo.class); } return null; } @Override protected ProductInfo getFallback() { return null; }
}
基于hystrix限流完成源服务的过载保护以避免流量洪峰打死MySQL
redis集群彻底崩溃的时候,一个是对redis本身做资源隔离、超时控制、熔断策略大量的请求,高并发会去访问源服务,商品服务(提供商品数据),QPS 10000去访问商品服务,基于mysql去查询
QPS 10000去访问mysql,会怎么样,mysql打死,商品服务也会死掉
就是要对商品服务这种源服务的访问施加限流的措施
限流怎么限,hystrix本身就是提供了两种机制,线程池(内部做了异步化处理,可以处理超时),semaphore(信号量,让tomcat线程执行运行逻辑,没有内部的异步化处理,一旦超时,会导致tomcat线程就hang住了)
一般推荐的是,线程池用来做有网络访问的这种资源隔离,因为涉及到网络,就很容易超时;sempahore是用来做对服务纯内存的一些复杂业务逻辑的操作,进行限流,因为不涉及网络访问,就是纯粹为了避免说对内存内的复杂业务逻辑进行太高并发的访问,造成系统本身的故障
semaphore是很合适的,比如一些推荐、搜索,有部分算法,复杂的算法,是放在服务内部纯内存去运行的,一个服务暴露出来的就是某个算法的执行
这个时候,就很适合用semaphore
访问外部的商品服务,所以还是用线程池做限流了。。。
算一下,要限多少,怎么限
假设说,每次商品服务的访问性能在200ms,1个线程一秒可以执行5次访问,假设说我们一个缓存服务实例对这个商品服务的访问每秒在150次
所以这个时候,我们就需要30个线程,每个线程每秒可以访问5次,总共每秒30个线程可以访问150次
这个时候呢,我们限流,要做得事情是这样子的,我们算的这个每秒150次访问时正常情况下,如果是非正常情况下,每秒1000次,甚至1w次,此时就可以自然限流
因为我们的线程池就30个。。。,还要设置等待队列
非正常情况下,直接线程池+等待队列全满,此时就会会出现大量的reject操作,然后就会去调用降级逻辑
接下来,我们要做限流,设置的就是线程池的大小,还有等待队列的大小,30个线程可以每秒处理150个请求,但是偶尔会多一些出来,同时30个线程处理150个请求会快一些,不用花费1秒钟,等待队列给一些buffer,不要偶尔1秒钟来了200条请求,50条直接给reject掉,等待队列,150个,30个线程直接500ms处理完了,等待队列中的50个请求依然可以继续处理
为源头服务的限流场景增加stubbed fallback降级机制
我们上一讲讲到说,限流,计算了一下线程池的最大的大小,和这个等待队列,去限制了每秒钟最多能发送多少次请求到商品服务避免大量的请求都发送到商品服务商去
限流过后,就会导致什么呢,比如redis集群崩溃了,雪崩,大量的请求涌入到商品服务调用的command中,是线程池不够
reject,被reject掉的请求就会去执行fallback降级逻辑
理清楚一些前提,首先一个请求都发送到这里来了,那么nginx本地缓存肯定就没了,redis已经崩溃了,ehcache中找不到这条数据对应的缓存
只能从源头的商品服务里面去查询,但是被限流了,这个请求只能走降级方案
都是用之前讲解的一些技术,stubbed fallback降级机制,残缺的降级
一般这种情况下,就是说,用请求参数中少量的数据,加上纯内存中缓存的少量的数据来提供残缺的数据服务
就给大家举个例子,我们之前讲解的stubbed fallback,是从内存中加载了部分品牌数据,加载了部分城市地理位置的数据啦。。。
方案,可以做,冷热分离
冷数据,也就是说你可以这么认为,将一些过时的数据,比如一个商品信息一周前的版本,放入大数据的在线存储中,比如比较合适做冷数据存放的是hbase
hadoop,离线批处理,hdfs分布式存储,yarn分布式资源调度(跟hbase没关系),mapreduce分布式计算
hbase,基于hdfs分布式存储基础之上,封装了一个系统,叫做hbase,分布式在线存储,分布式NoSQL数据库,里面可以放大量的冷数据
hbase,可以做商品服务热数据是放mysql,可以将一周前,一个月前的数据快照,做一份冷备放到hbase来备用
你本来正常情况下是直接去访问商品服务,去拉取热数据
发送请求去访问hbase,去加载冷数据,hbase本身是分布式的,所以也是可以承载高并发的访问的(分布式的特性比mysql),即使这个时候大量并发到了hbase,如果你集群运维够好的话,也开始以撑住的,加载到一条冷数据的话,那么此时就是过期的数据,商品一周前或者一个月前的一个快照版本
但是至少有数据,还可以显示一下
多级降级机制,先走hbase冷备,然后再走stubbed fallback
缓存雪崩的回顾
1、事前,redis高可用性,redis cluster,sentinal,复制,主从,从->主,双机房部署
2、事中,ehcache可以抗一抗,redis挂掉之后的资源隔离、超时控制、熔断,商品服务的访问限流、多级降级,缓存服务在雪崩场景下存活下来,基于ehcache和存活的商品服务提供数据
3、事后,快速恢复Redis,备份+恢复,快速的缓存预热的方案
缓存穿透
如果所查询的数据在各级缓存,以及mysql中都没有,相当于前面所有的缓存都无法起到拦截的作用,请求会直接打到mysql中,这样就产生了缓存穿透。如果有大量这样的请求进入可能会直接干掉mysql。
解决方案
1 做防盗链,防止有人恶意刷我们的接口
2 每次如果从源服务(商品服务)查询到的数据是空,就说明这个数据根本就不存在
那么如果这个数据不存在的话,我们不要不往redis和ehcache等缓存中写入数据,我们呢,给写入一个空的数据,比如说空的productInfo的json串
给nginx也是,返回一个空的productInfo的json串
因为我们有一个异步监听数据变更的机制在里面,也就是说,如果数据变更的话,某个数据本来是没有的,可能会导致缓存穿透,所以我们给了个空数据
但是现在这个数据有了,我们接收到这个变更的消息过后,就可以将数据再次从源服务中查询出来
然后设置到各级缓存中去了
缓存失效
缓存失效
还记得,我们在nginx中设置本地的缓存的时候,会给一个过期的时间,比如说10分钟
10分钟以后自动过期,过期了以后,就会重新从redis中去获取数据
这个10分钟到期自动过期的事情,就叫做缓存的失效
如果缓存失效以后,那么实际上此时,就会有大量的请求回到redis中去查询
缓存失效的问题。。。。
如果说同一时间来了1000个请求,都将缓存cache在了nginx自己的本地,缓存失效的时间都设置了10分钟
那么是不是可能导致10分钟过后,这些数据,就自动全部在同一时间失效了
如果同一时间全部失效,会不会导致说同一时间大量的请求过来,在nginx里找不到缓存数据,全部高并发走到redis上去了
加重大量的网络请求,网络负载也会加重
解决方案是什么呢?
解决方案
将nginx中数据的过期时间由固定10分钟更改为随机范围
增加这两行即可:
math.randomseed(tostring(os.time()):reverse():sub(1, 7))
local expireTime = math.random(600, 1200)
应用层nginx更改后的lua脚本
local uri_args = ngx.req.get_uri_args()
local productId = uri_args["productId"]
local shopId = uri_args["shopId"]
local cache_ngx = ngx.shared.my_cache
local productCacheKey = "product_info_"..productId
local shopCacheKey = "shop_info_"..shopId
local productCache = cache_ngx:get(productCacheKey)
local shopCache = cache_ngx:get(shopCacheKey)
if productCache == "" or productCache == nil then
local http = require("resty.http")
local httpc = http.new()
local resp, err = httpc:request_uri("http://192.168.31.179:8080",{
method = "GET",
path = "/getProductInfo?productId="..productId
})
productCache = resp.body
math.randomseed(tostring(os.time()):reverse():sub(1, 7))
local expireTime = math.random(600, 1200)
cache_ngx:set(productCacheKey, productCache, expireTime)end
if shopCache == "" or shopCache == nil then
local http = require("resty.http")
local httpc = http.new()
local resp, err = httpc:request_uri("http://192.168.31.179:8080",{
method = "GET",
path = "/getShopInfo?shopId="..shopId
})
shopCache = resp.body
cache_ngx:set(shopCacheKey, shopCache, 10 * 60)
end
local cjson = require("cjson")
local productCacheJSON = cjson.decode(productCache)
local shopCacheJSON = cjson.decode(shopCache)
local context = {
productId = productCacheJSON.id,
productName = productCacheJSON.name,
productPrice = productCacheJSON.price,
productPictureList = productCacheJSON.pictureList,
productSpecification = productCacheJSON.specification,
productService = productCacheJSON.service,
productColor = productCacheJSON.color,
productSize = productCacheJSON.size,
shopId = shopCacheJSON.id,
shopName = shopCacheJSON.name,
shopLevel = shopCacheJSON.level,
shopGoodCommentRate = shopCacheJSON.goodCommentRate
}
local template = require("resty.template")
template.render("product.html", context)
整体总结
1、亿级流量电商网站的商品详情页系统架构面临难题:对于每天上亿流量,拥有上亿页面的大型电商网站来说,能够支撑高并发访问,同时能够秒级让最新模板生效的商品详情页系统的架构是如何设计的?
解决方案:异步多级缓存架构+nginx本地化缓存+动态模板渲染的架构
2、redis企业级集群架构
面临难题:如何让redis集群支撑几十万QPS高并发+99.99%高可用+TB级海量数据+企业级数据备份与恢复?
解决方案:redis的企业级备份恢复方案+复制架构+读写分离+哨兵架构+redis cluster集群部署
3、多级缓存架构设计
面临难题:如何将缓存架构设计的能够支撑高性能以及高并发到极致?同时还要给缓存架构最后的一个安全保护层?
解决方案:nginx抗热点数据+redis抗大规模离线请求+ehcache抗redis崩溃的三级缓存架构
4、数据库+缓存双写一致性解决方案
面临难题:高并发场景下,如何解决数据库与缓存双写的时候数据不一致的情况?
解决方案:异步队列串行化的数据库+缓存双写一致性解决方案
5、缓存维度化拆分解决方案
面临难题:如何解决大value缓存的全量更新效率低下问题?
解决方案:商品缓存数据的维度化拆分解决方案(按照店铺维度,商品维度,品牌维度,城市地理位置维度,等各种维度)
6、缓存命中率提升解决方案
面临难题:如何将缓存命中率提升到极致?
解决方案:双层nginx部署架构+lua脚本实现一致性hash流量分发策略
7、缓存并发重建冲突解决方案
面临难题:如何解决高并发场景下,缓存重建时的分布式并发重建的冲突问题?
解决方案:基于zookeeper分布式锁的缓存并发重建冲突解决方案
8、缓存预热解决方案
面临难题:如何解决高并发场景下,缓存冷启动导致MySQL负载过高,甚至瞬间被打死的问题?
解决方案:基于storm实时统计热数据的分布式快速缓存预热解决方案
9、热点缓存自动降级方案
面临难题:如何解决热点缓存导致单机器负载瞬间超高?
解决方案:基于storm的实时热点发现+毫秒级的实时热点缓存负载均衡降级
10、高可用分布式系统架构设计
面临难题:如何解决分布式系统中的服务高可用问题?避免多层服务依赖因为少量故障导致系统崩溃?
解决方案:基于hystrix的高可用缓存服务,资源隔离+限流+降级+熔断+超时控制
11、复杂的高可用分布式系统架构设计
面临难题:如何针对复杂的分布式系统将其中的服务设计为高可用架构?
解决方案:基于hystrix的容错+多级降级+手动降级+生产环境参数优化经验+可视化运维与监控
12、缓存雪崩解决方案
面临难题:如何解决恐怖的缓存雪崩问题?避免给公司带来巨大的经济损失?
解决方案:全网独家的事前+事中+事后三层次完美缓存雪崩解决方案
13、缓存穿透解决方案
面临难题:如何解决高并发场景下的缓存穿透问题?避免给MySQL带来过大的压力?
解决方案:缓存穿透解决方案
14、缓存失效解决方案
面临难题:如何解决高并发场景下的缓存失效问题?避免给redis集群带来过大的压力?
解决方案:基于随机过期时间的缓存失效解决方案
硬件规划
每日上亿流量,高峰QPS过1万
nginx部署,负载很重,16核32G,建议给3~5台以上,就非常充裕了,每台抗个几千QPS
缓存服务部署,4核8G,按照每台QPS支撑500,部署个10~20台
redis部署,给redis本身的内存不要超过10个G每台给8核16G,根据数据量以及并发读写能力来看,部署5~10个master,每个master挂一个slave,主要是为了支撑更多数据量,1万并发读写肯定没问题了