整体架构目标
高可用、高性能、数据一致性
高可用性
对于小型用应用或是很久之前的互联网架构采用都是集中架构,即所有有的网站功能都部署在一起,一个war包走天下,优缺点都十分明显,优点在于成本低,缺点在于单点故障,这对一个秒杀系统而言是绝对不能允许的。
最简单的一种解决方式就是增加服务器数量(集群),多个服务器共同维护同一个工程,再细化一点,多个Tomcat共同维护一个业务,这就是最近大火的微服务的一个基本思想。
在系统优化的道路上,必然会产生新的问题,比如,增加了服务器数量,那么到底增加几个,这些服务器如何进行分工,将一个工程进行服务拆分,那么服务与服务之间的调用该怎么解决(单击版都是本地调用),服务与服务之间错综复杂的关系如何维护……
一、常见的集群方式
1.主从复制
像MySQL数据库、redis等支持这种集群策略
从原理上解读并不难理解,一个主服务器负责“写业务”,多个slave服务器负责"读业务",将读写业务进行分离,大大降低了单个服务器,单点故障率必定也降低。
带来的问题:
数据一致性,当主节点更新了数据之后,应该尽快将更新的数据到slave服务器上,保证用户读到的数据是最新的。
mysql的做法是将数据的变更信息记录到一个二进制文件中,主服务器将这些信息推送到其他服务器上,slave服务器会在后台监听这个事件,进而定位到哪些数据需要更新。这种做法在很多地方都有体现,比如redis中的主从复制、RabbitMQ中的topic工作模式、ROS通信机制等等都是类似的操作
还有一个问题,如果主服务器挂掉了之后呢,秒杀场景中高并发写的规模也不容小觑,因此引出了哨兵模式
2.哨兵模式
工作原理:能够后台监控Master库是否故障,如果故障了根据投票数自动将slave库转换为主库。一组哨兵能同时监控多个Master。超过半数的哨兵节点认为其中的一个master故障了,则判定为故障了了
哨兵通过心跳机制来获取节点运行状态,可以设置多长时间向哨兵发送一次信号,这种心跳机制在springcloud中也有体现。
作用:(主从切换)
- 监控:
- 不断的检查master和slave是否正常运行
- 通知:
- 当被监控的服务器出现问题后,向其他哨兵发送通知,超过半数的哨兵节点认为其中的一个master故障了,则判定为故障了了。
- 自动故障转移
- 发现问题
- 竞选领头的哨兵
- 领头的哨兵优选新的master
- 断开原master和salve连接,选取一个slave作为master,将其他salve连接到新的master上
注意:哨兵也是一个redis服务器,一般哨兵配置3个以上而且是单数的,因为双数在投票时可能会打个平手
3.分布式架构(去中心化)
尽管哨兵模式看起来能够在很大程度上避免单点故障发生,但是还是不够好,因为有大量的数据重复保存在不同服务器上了。
拿redis举例:
- 特点:
- 所有的redis节点彼此互联
- 节点的失败fail是通过集群中超过半数的节点检查失效时才生效
- 客户端只需要连接任意一个redis节点即可
- 将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N
- 一个集群至少要有三个主节点,即要有六个节点。
当采用分布式集群之后,数据的存储以及查找就要有讲究,如果随便存储那么就有可能找错数据。
redis采用了一种CRC16(key)%16384的计算方式来决定将数据存在哪个数据库,这里说存在哪个数据库不太准确,确切讲应该为存在哪个槽中,这个槽可以看作是将每一个redis服务器存储数据的一个中间媒介,设计这个槽的目的是为了动态扩展性。
当redis集群中需要添加或者是删除redis节点时,那么原来内存空间可能会有所变化,增加槽这个概念就可以很好的解决这个问题,将16384个槽均匀分布在所设置的redis服务器上,当有新的节点增加之后,只需将其他一部分槽分摊给新的redis服务器即可。
二、负载均衡(Nginx和Ribbon)
上述讲到利用增加服务器的方法降低故障率,那么前端秒杀请求到底给哪个服务器,因为每台服务器的处理请求能力不同,应该尽可能的充分发挥每台服务器的性能,所以在请求达到服务器端时,需要这样一个分发器。
当然nginx不仅仅只有负载均衡一个作用,根据百度百科定义:Nginx是一个高性能的http和反向代理web服务器(俄罗斯人研发),最大的特点是占用内存小同时并发能力强,据相关测试数据称,nginx能支持高达50000个并发连接请求。
正反向代理
(1)正向代理
如果吧局域网外的Internet想象成一个巨大的资源库,则局域网中的客户端要访问Internet,则需要通过代理服务器来访问,这种访问方式称作正向代理。在正向代理中需要客户端手动配置代理服务器地址以及客户需要知道目标IP。
(2)反向代理
反向代理,实际上客户端对代理是无感知的,因为客户端不需要任何配置就可以访问,我们只需要将请求发送给反向代理服务器,由反向代理服务器去选择目标代理服务器获取数据后,在返回给客户端,此时反向代理服务器和目标服务器对外实际上就是一台服务器很好的隐藏了真实的IP地址,安全性高。
反向代理服务器实际上扮演了分发请求的角色。
总结一下:正向代理对外暴露目标IP,反向代理则隐藏
负载均衡
将请求均匀分布在各个服务器上,算法:平衡加权算法
动静分离
为了加快网站的解析速度,可以把动态页面和静态页面由不同的服务器来解析,加快解析速度,降低原来单个服务器的压力。比如在一个秒杀详情页面,实际上很多内容都是静态不变的,变化的数据往往只是秒杀政策中的秒杀时间、库存数量等等,因此没必要用户每一次请求都重新刷一次页面,可以将静态资源缓存到CDN(内容分发器)或是浏览器上,动态数据通过Ajax响应实现异步局部性更新。
拓展:Nginx负载均衡策略
核心代码如下:
在这里插入代码片
public class Weight{
private String ip;
private Integer staticweight;
private Integer currentweight;
/**
构造函数、set、get方法省略
*/
}
public class WeightRoundFind{
private static Map<String,Weight> weigthMap = new LinkedHashMap<>();
public static String getServerIp(){
if(weigthMap.isEmpty()){//初始化weigthMap
for(String ip:ServerIps.Weight_map.keySet()){
Integer weight = ServerIps.Weight_map.get(ip);
weigthMap.put(ip,new Weigth(ip,weight,0));
}
}
for(Weight w:weigthMap.Values()){
w.setCurrentWeight(w.getCurrentWeight()+w.getWeight());
}
Weigth maxCurrentWeight = null;
for(Weight w:weigthMap.Values()){
if(maxCurrentWeight==null||w.getCurrentWeight()>maxCurrentWeight.getCurrentWeight())
maxCurrentWeight = w;
}
}
maxCurrentWeight.setCurrentWeight(maxCurrentWeight.getCurrentWeight()-sumOf(staticWeight));
return maxCurrentWeight.getIp();
}
实际上,提到负载均衡就不得不提到SpringCloud中的Ribbon组件,Ribbon也可以做负载均衡,那他们两个有什么区别呢?
答:放的位置不同,Nginx在ribbon前边。
Nginx是服务器负载均衡,客户端所有请求都先经由nginx,然后由其进行转发。
ribbon是本地负载均衡,在微服务架构中才存在,在服务与服务之间进行远程调用(RPC)时,会从Eureka注册中心上获取注册信息服务列表之后缓存到JVM本地,在本地进行选择调用地址过程。
一个形象的比喻:我想去县政府办一点事,首先要经过县政府门卫(nginx),之后到了大堂前台,“您想办什么业务”,前台小姐姐(ribbon)在了解你要办什么业务后带你去相关办公室,哪个办公室有空闲了,就进哪个去办理。
在微服务架构下,一个微服务同时会有多个服务器共同维护,同样是为了减少单点故障的发生。
三、限流熔断降级
SpringCloud提供的Hystrix组件可以实现服务的降级与熔断,虽然目前Hystrix停止维护了,但是后面的resilence4j以及sentinel(阿里巴巴)都借鉴了Hystrix的思想(有时间一定深入了解一下其实现原理)。
Hystrix(豪猪)
出现背景:
在分布式系统中,服务于服务之间的调用关系错综复杂,假设微服务A调用微服务B和微服务C,微服务B、C又调用了很多其他微服务,即形成了很长的链路,如果在这个调用链路中某个服务响应时间过长或者直接挂掉(增加服务器可减少挂掉几率),那么对微服务A的调用就会积压的越来越多,进而引起系统的崩溃,雪崩!
定义:
Hystrix是一个用于处理分布式系统的延迟和容错的开源库。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似保险丝但又有区别),向调用方返回一个符合预期的、可处理的备选响应。
三个重要功能:
服务降级:fallback
优先核心业务得到处理,非核心业务弱处理或不处理
哪些情况会发生降级呢?
程序运行异常、超时、服务熔断触发服务降级、线程池打满等
服务熔断:break
熔断机制原理比较容易理解,像家里的保险丝,如果发生短路后能立刻熔断电路,避免发生灾难。在分布式系统中应用这一模式之后,服务调用方可以自己进行判断某些服务反应慢或者存在超时情况选择是否进行主动熔断,防止整个系统被拖垮;不同于电路熔断的是,Hystrix可以实现弹性容错,当情况好转之后可以自动重连。
上图展示的是三种熔断状态:
1.close:闭合状态,所有请求都能正常访问
2.open:打开状态,所有请求都无法访问,当失败比例大于50%或者次数大于20次以上时触发(参数可调)
3.halfopen:半开状态,打开状态默认5s休眠期,在休眠期所有请求都无法正常访问,过了休眠期会进入半开状态,放部分请求通过。
服务限流:flowlimit
设置规定时间允许多少个请求能够被处理。
这一部分阿里的sentinel做的比较好,下面在介绍一下sentinel。
sentinel
定义:分布式系统的流量防卫兵。
Hystrix缺点:
- 需要程序员手工搭建监控平台
- 没有一套web界面可以供开发者更加细粒化的配置,比如流控、速率控制(这里有个漏斗算法,先占个坑)、服务熔断、服务降级…
sentinel特点:
这里截个官网图吧
流量控制规则:
资源名:唯一名字,默认请求路径
针对来源:sentinel可以针对调用者进行限流,填写微服务名,默认default
阈值类型、单机阈值:
- Qps(每秒钟请求数量):当调用该API的QPS达到 阈值时进行限流
- 线程数:线程数量达到阈值时进行限流
是否集群:不需要
流控模式:
- 直接:请求达到触发限流条件时,直接进行限流
- 关联:当关联的资源达到阈值时,就限流自己
- 链路:只对指定链路上的流量进行限流控制
流控效果:
- 快速失败:直接失败,刨出异常
- Warm up:根据codeFactor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才能达到设置的QPS阈值
- 排队等待:匀速排队,让请求匀速通过,阈值类型必须设置为QPS,否则无效。
小结:上面都是来自官网,真正理解可能还需要结合具体实践。
四、网关
zuul逐渐被替代了,未来的趋势是gateway。
作用:提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤功能,例如:熔断、限流、重试等等。
SpringCloud GateWay是基于高性能的Reactor模式通信框架Netty以及filter过滤链实现的。
而zuul之所以被替代是因为zuul1.x是基于阻塞IO实现的,性能影响严重。
在servlet3.1之后有了异步非阻塞的支持,Spring WebFlux基于此而设计的一个升级版的springMVC架构。(~ ~这框架也太多了吧!!)
存在的位置:
三个重要概念:
路由:路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
断言:java8中的函数式编程,开发人员可以匹配Http请求中的所有内容,如果相匹配则进行路由。
Java8中有四大函数式接口:他们是用来配合lamda表达式来使用的。当然我么也可以自己自定义接口,只不过上述四种以及它们的子类可以覆盖大多数情况,让开发人员专注lamda表达式的内容,本质上就是匿名内部类的另一种表示形式。
* Consumer<T>:消费型接口
* void accept(T t)
* Supplier<T> :供给型接口
* T get();
* Function(T,R):函数型接口
* R apply( T t);
* Predicate<T>:断言型接口
* boolean test(T t)
*
//举例:将满足条件的字符串过滤出来添加到集合中去,路由过滤过程也差不多
public List<String> dealWirhStr(List<String> strs, Predicate<String> predicate){
List<String> list =new ArrayList<>();
for(String s: strs){
if(predicate.test(s))
list.add(s);
}
return list;
}
@Test
public void test04(){
List<String> strs = Arrays.asList(
"12345678910","adsd","adsdasdasdadasd","qrqtd146511"
);
List<String> strings = dealWirhStr(strs, (s) -> s.length() >= 10);
for(String s: strings){
System.out.println(s);
}
}
过滤:
指的是Spring框架中GateWayFilter的实例,可以再请求被路由前或者之后对请求进行修改。在pre类型的过滤器可以做参数校验、权限校验、流量控制、日志输出、协议转换;在post类型过滤器中可以做响应内容、响应头修改、日志输出等。
不过上述这些功能,貌似SpringMVC和Strus2甚至原生servlet都可以做到,为什么又单独开发了这么一个网管,我本人能想到的一个原因是:gateway底层封装了Spring WebFlux,它是基于异步非阻塞IO实现的,性能大大提高了,这也是高并发系统高性能优化的重要一环。
这里稍微提一下servlet中的过滤器和SpringMVC中拦截器的区别:
-
过滤器是 servlet 规范中的一部分, 任何 java web 工程都可以使用。
-
拦截器是 SpringMVC 框架自己的,只有使用了 SpringMVC 框架的工程才能用。
-
过滤器在 url-pattern 中配置了/*之后,可以对所有要访问的资源拦截。拦截器它是只会拦截访问的控制器方法,如果访问的是 jsp, html,css,image 或者 js 是不会进行拦截的。
-
拦截器是基于Java的动态代理实现的(体现了AOP编程的思想),而过滤器是基于函数回调(定义一个接口,在一个流程中先写上接口方法,后面在通过子类实现相应函数体实现回调机制)
线程池中的拒绝策略实际上就利用函数回调机制。
总结
上述都是一些具体的保证高可用的解决方案,肯定还有其他的方案,欢迎补充,另外希望批判没事 ~~