分布式缓存架构
什么是缓存Cache,高速缓冲存储器,是介于中央处理器和主存储器之间的高速小容量存储器,一个硬件或软件的组件用来存储将来会请求到的数据,而且能让数据获取更快。
Cache vs Buffer,高速缓存和缓冲区,缓存Cache可以是RAM的一部分,也可以是磁盘的一部分;缓冲区buffer主要用于输入/输出(I/O)过程。
常见的缓存:CPU缓存、操作系统缓存、数据库缓存、JVM编译缓存、CDN缓存、代理与反向代理缓存、应用程序缓存、分布式对象缓存。
缓存数据存储(Hash表)
缓存的关键指标:命中率
影响缓存命中率的主要指标:缓存键集合大小、缓存可使用内存空间、缓存对象生存时间。
缓存应用场景:代理缓存、反向代理缓存、多层反向代理缓存、内容分发网络(CDN)、CDN同时配置静态文件和动态内容、通读缓存、旁路缓存、浏览器对象缓存、本地对象缓存。
分布式缓存:
本地对象缓存构建分布式集群
远程分布式对象缓存
Memchached分布式对象缓存
Memchached分布式缓存访问模型
分布式对象缓存的一致性Hash算法
基于虚拟节点的一致性Hash算法
各种介质数据访问延迟
技术栈各个层次的缓存
缓存为什么能显著提升性能:内存构建比磁盘访问速度更快,缓存对象是最终结果,无需中间计算,减少CPU消耗;降低数据库、磁盘、网络压力,使这些IO设备获得更好响应。
缓存是系统性能优化的大杀器:技术简单、性能提升显著、应用场景多。但要合理使用缓存。
Redis VS Memchached
Redis和Memchached是互联网分层架构中,最常用的KV缓存。都是基于内存(in-memory)存储键值对映射的高性能缓存,彼此性能相差无几。
-
redis支持快照(dump)和aof两种方式持久化方式。
-
redis支持字符串、链表、集合、有序集合、散列表共5种数据结构,而memcached只能存储字符串。
-
redis支持丰富的命令,而memcached只能用APPEND命令将数据添加到已有字符串的末尾。
-
redis适合存储结构化的数据,memcached适合任意格式的数据,一般可用来缓存图片、视频等。
-
memcached的键最大长度为250字节,值默认最大1M字节。redis键长度最大512M字节。
Redis集群
redis最开始使用主从模式做集群,若master宕机需要手动配置slave转为master;后来为了高可用提出来哨兵模式,该模式下有一个哨兵监视master和slave,若master宕机可自动将slave转为master,但它也有一个问题,就是不能动态扩充;所以在3.x提出cluster集群模式。
Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。
消息队列与异步架构
同步调用VS异步调用
同步调用就是客户端等待调用执行完成并返回结果。异步调用就是客户端不等待调用执行完成返回结果,不过依然可以通过回调函数等接收到返回结果的通知。如果客户端并不关心结果,则可以变成一个单向的调用。这个过程有点类似于Java中的callable和runnable接口,我们进行异步执行的时候,如果需要知道执行的结果,就可以使用callable接口,并且可以通过Future类获取到异步执行的结果信息。如果不关心执行的结果,直接使用runnable接口就可以了,因为它不返回结果。
消息队列构建异步调用架构
使用异步调用架构的主要手段,就是通过消息队列构建。消息的生产者将消息发送到消息队列以后,由消息的消费者从消息队列中获取消息,然后进行业务逻辑的处理,消息的生产者和消费者是异步处理的,彼此不会等待阻塞。
消息队列的好处:解耦、异步、削峰;缺点:系统可用性降低、系统复杂性提高、一致性问题。
事件驱动架构EDA,事件驱动架构Event Driven Architecture (EDA) 是一种低耦合可分布式的架构,它通常处理异步信息流。
主要MQ产品比较:
RocketMQ,阿里系下开源的一款分布式、队列模型的消息中间件,原名Metaq,3.0版本名称改为RocketMQ,是阿里参照kafka设计思想使用java实现的一套mq。同时将阿里系内部多款mq产品(Notify、metaq)进行整合,只维护核心功能,去除了所有其他运行时依赖,保证核心功能最简化,在此基础上配合阿里上述其他开源产品实现不同场景下mq的架构,目前主要多用于订单交易系统。
RabbitMQ,使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP,STOMP,也正是如此,使的它变的非常重量级,更适合于企业级的开发。同时实现了Broker架构,核心思想是生产者不会将消息直接发送给队列,消息在发送给客户端时先在中心队列排队。对路由(Routing),负载均衡(Load balance)、数据持久化都有很好的支持。多用于进行企业级的ESB整合。
ActiveMQ,Apache下的一个子项目。使用Java完全支持JMS1.1和J2EE 1.4规范的 JMS Provider实现,少量代码就可以高效地实现高级应用场景。可插拔的传输协议支持,比如:in-VM, TCP, SSL, NIO, UDP, multicast, JGroups and JXTA transports。RabbitMQ、ZeroMQ、ActiveMQ均支持常用的多种语言客户端 C++、Java、.Net,、Python、 Php、 Ruby等。
Kafka,Apache下的一个子项目,使用scala实现的一个高性能分布式Publish/Subscribe消息队列系统,具有以下特性:
-
快速持久化:通过磁盘顺序读写与零拷贝机制,可以在O(1)的系统开销下进行消息持久化;
-
高吞吐:在一台普通的服务器上既可以达到10W/s的吞吐速率;
-
高堆积:支持topic下消费者较长时间离线,消息堆积量大;
-
完全的分布式系统:Broker、Producer、Consumer都原生自动支持分布式,依赖zookeeper自动实现复杂均衡;
-
支持Hadoop数据并行加载:对于像Hadoop的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。
负载均衡架构
负载均衡架
HTTP重定向负载均衡
DNS负载均衡
反向代理负载均衡
IP负载均衡
数据链路层负载均衡
负载均衡算法:
1、轮询法
将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。
2、随机法
通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多,
其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。
3、源地址哈希法
源地址哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。
4、加权轮询法
不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。
5、加权随机法
与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。
6、最小连接数法
最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。
应用服务器集群的Session管理:
在使用负载均衡的集群环境中,由于负载均衡服务器可能将请求发往集群任何一台应用服务器上,需要采取手段保证每次请求都能获得正确的Session。
-
Session复制
应用服务器开启Web容器的Session复制功能,在几台服务器之间同步Session对象,每台服务器都保存所有用户的Session信息。
优点:从本机上读取Session信息快速,任一机器宕机不影响使用。
缺点:集群规模较大时,服务器间需要大量的通信进行Session复制,占用服务器和网络的大量资源。只能在集群规模较小的情况下使用。
-
Session绑定(会话黏滞)
利用负载均衡的源地址Hash算法实现,负载均衡服务器总是将同一IP地址的请求分发给同一台服务器,这时负载均衡服务器必须工作在HTTP协议层。这样整个会话期间,用户所有的请求都在同一台服务器处理,Session绑定在某台特定服务器上。
缺点:不符合高可用需求,某台服务器宕机,该机器上Session就不存在了。
-
Cookie记录Session
浏览器Cookie记录Session。
优点:简单易用,支持应用服务器线性伸缩
缺点:受Cookie大小限制,能记录的信息有限;每次请求响应都要传输Cookie,影响性能;用户关闭Cookie,访问不正常。
-
Session服务器
利用独立部署的Session服务器(集群)统一管理Session,应用服务器读写Session时,都访问Session服务器,将应用服务器的状态分离,分为无状态的应用服务器和有状态的Session服务器(利用分布式缓存、数据库)。Cookie 里面记录一个Session ID,当Cookie被禁用时,使用URL重写技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。
优点:可用性高,伸缩性好,对信息大小无限制。
Mysql分布式数据库
Mysql主从复制
Mysql主主复制
主主失效恢复
主从复制注意点:
-
主主复制的两个数据库不能并发写入。
-
复制只是增加数据读并发处理能力,没有增加写并发能力和存储能力。
-
更新表结构会导致巨大的同步延迟。
数据分片
数据分片(segment,fragment, shard, partition),就是按照一定的规则,将数据集划分成相互独立、正交的数据子集,然后将数据子集分布到不同的节点上。
常见三种分片方式:hash方式,一致性hash(consistent hash),按照数据范围(range based)
分布式数据库中间件
Cobar:
阿里巴巴B2B开发的关系型分布式系统,管理将近3000个MySQL实例。 在阿里经受住了考验,后面由于作者的走开的原因cobar没有人维护 了,阿里也开发了tddl替代cobar。
MyCAT:
社区爱好者在阿里cobar基础上进行二次开发,解决了cobar当时存 在的一些问题,并且加入了许多新的功能在其中。目前MyCAT社区活 跃度很高,目前已经有一些公司在使用MyCAT。总体来说支持度比较高,也会一直维护下去,
TDDL:
Tddl是一个分布式数据库中间件,它在阿里内部被广泛的使用。
Sharding-Sphere:
Sharding-Sphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar这3款相互独立的产品组成。他们均提供标准化的数据分片、读写分离、柔性事务和数据治理功能,可适用于如Java同构、异构语言、容器、云原生等各种多样化的应用场景。
OneProxy:
数据库界大牛,前支付宝数据库团队领导楼总开发,基于mysql官方 的proxy思想利用c进行开发的,OneProxy是一款商业收费的中间件, 楼总舍去了一些功能点,专注在性能和稳定性上。有朋友测试过说在 高并发下很稳定。
Vitess:
这个中间件是Youtube生产在使用的,但是架构很复杂。 与以往中间件不同,使用Vitess应用改动比较大要 使用他提供语言的API接口,我们可以借鉴他其中的一些设计思想。
Kingshard:
Kingshard是前360Atlas中间件开发团队的陈菲利用业务时间 用go语言开发的,目前参与开发的人员有3个左右, 目前来看还不是成熟可以使用的产品,需要在不断完善。
Atlas:
360团队基于mysql proxy 把lua用C改写。原有版本是支持分表, 目前已经放出了分库分表版本。在网上看到一些朋友经常说在高并发下会经常挂掉。
MaxScale与MySQL Route:
这两个中间件都算是官方的吧,MaxScale是mariadb (MySQL原作者维护的一个版本)研发的,目前版本不支持分库分表。MySQL Route是现在MySQL 官方Oracle公司发布出来的一个中间件。
作业
-
用熟悉的编程语言实现一致性 hash 算法。
-
编写测试用例测试这个算法,测试 100 万 KV 数据,10 个服务器节点的情况下,计算这些 KV 数据在服务器上分布数量的标准差,以评估算法的存储负载不均衡性。
一致性Hash算法是对2^32取模。简单来说,一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-2^32-1(即哈希值是一个32位无符号整形),整个哈希环如下:
整个空间按顺时针方向组织,圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、5、6……直到2^32-1,也就是说0点左侧的第一个点代表2^32-1, 0和2^32-1在零点中方向重合,我们把这个由2^32个点组成的圆环称为Hash环。
一致性Hash算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题,例如系统中只有两台服务器,其环分布如下:
此时必然造成大量数据集中到Node A上,而只有极少量会定位到Node B上。为了解决这种数据倾斜问题,一致性Hash算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器IP或主机名的后面增加编号来实现。
代码_Hash算法:
public class HashUtils {
private static final long PSART = 16777619L;
private static final long PMULT = 2166136261L;
public static long getHash(String str) {
long p = PSART;
long hash = PMULT;
for (int i = 0; i < str.length(); i++)
hash = (hash ^ str.charAt(i)) * p;
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
if (hash < 0)
hash = Math.abs(hash);
return hash;
}
}
代码_标准差:
public class MathUtils {
public static double standardDiviation(int[] x) {
int m = x.length;
int sum = 0;
// 求和
for (int i = 0; i < m; i++) {
sum += x[i];
}
// 求平均值
int dAve = sum / m;
int dVar = 0;
for (int i = 0; i < m; i++) {// 求方差
dVar += Math.pow(x[i] - dAve, 2);
}
return Math.sqrt(dVar / m);
}
}
一致性HASH算法:
public class ConsistentHashing {
private List<String> realNodes = new LinkedList<String>();
private SortedMap<Integer, String> sortedMap = new TreeMap<Integer, String>();
private int virtualNum = 0;
public ConsistentHashing() {
}
public ConsistentHashing(List<String> nodes, int virtualNum) {
realNodes.addAll(nodes);
this.virtualNum = virtualNum;
init();
}
public ConsistentHashing(Map<String, Integer> nodes) {
realNodes.addAll(nodes.keySet());
}
public void init() {
sortedMap.clear();
for (String node : realNodes) {
String realNode = node + "_0";
sortedMap.put((int) HashUtils.getHash(realNode), realNode);
for (int i = 1; i <= virtualNum; i++) {
String virtualNode = node + "_" + String.valueOf(i);
sortedMap.put( (int) HashUtils.getHash(virtualNode), virtualNode);
}
}
}
public String getServer(String node) {
int hash = (int) HashUtils.getHash(node);
SortedMap<Integer, String> subMap = sortedMap.tailMap(hash);
if(!subMap.isEmpty()) {
Integer i = subMap.firstKey();
String virtualNode = subMap.get(i);
return virtualNode.substring(0, virtualNode.indexOf("_"));
}
return realNodes.get(0);
}
public int getVirtualNum() {
return virtualNum;
}
public void setVirtualNum(int virtualNum) {
this.virtualNum = virtualNum;
}
public List<String> getRealNodes() {
return realNodes;
}
public void addNode(String node) {
realNodes.add(node);
}
public void removeNode(String node) {
realNodes.remove(node);
}
}
测试代码:
public class AppTest extends TestCase {
private ConsistentHashing consistentHashing;
private static Map<String, Integer> nodes = new HashMap<String, Integer>();
public AppTest(String testName) {
super(testName);
for(int i=0;i<10;i++) {
nodes.put( "192.168.0."+(i+1), i);
}
consistentHashing = new ConsistentHashing(nodes);
}
public static Test suite() {
return new TestSuite(AppTest.class);
}
public void testHash() {
goHash(0);//无虚拟节点
goHash(10);//10虚拟节点
goHash(100);
goHash(1000);
}
public void goHash(int virNUm) {
consistentHashing.setVirtualNum(virNUm);
consistentHashing.init();
int[] counters = new int[nodes.size()];
for (int i = 0; i < 1000000; i++) {
String node = UUID.randomUUID().toString();
String serverIp = consistentHashing.getServer(node);
counters[nodes.get(serverIp)]++;
}
for (int i = 0; i < counters.length; i++) {
System.out.println("路由到结点[192.168.0."+(i+1)+"] "+ counters[i]+"次");
}
double dsv = MathUtils.standardDiviation(counters);
System.out.println(virNUm + "虚拟节点标准差:"+ dsv);
assertTrue(true);
}
}
测试结果:
路由到结点[192.168.0.1] 73711次
路由到结点[192.168.0.2] 88132次
路由到结点[192.168.0.3] 38560次
路由到结点[192.168.0.4] 81524次
路由到结点[192.168.0.5] 102842次
路由到结点[192.168.0.6] 213723次
路由到结点[192.168.0.7] 36849次
路由到结点[192.168.0.8] 53248次
路由到结点[192.168.0.9] 273023次
路由到结点[192.168.0.10] 38388次
0虚拟节点 标准差:14654.29507004687
路由到结点[192.168.0.1] 148980次
路由到结点[192.168.0.2] 95794次
路由到结点[192.168.0.3] 86326次
路由到结点[192.168.0.4] 36654次
路由到结点[192.168.0.5] 89828次
路由到结点[192.168.0.6] 110975次
路由到结点[192.168.0.7] 93610次
路由到结点[192.168.0.8] 84907次
路由到结点[192.168.0.9] 105570次
路由到结点[192.168.0.10] 147356次
10虚拟节点 标准差:14654.29507004687
路由到结点[192.168.0.1] 104468次
路由到结点[192.168.0.2] 105734次
路由到结点[192.168.0.3] 86492次
路由到结点[192.168.0.4] 95853次
路由到结点[192.168.0.5] 103345次
路由到结点[192.168.0.6] 97413次
路由到结点[192.168.0.7] 112574次
路由到结点[192.168.0.8] 87883次
路由到结点[192.168.0.9] 102369次
路由到结点[192.168.0.10] 103869次
100虚拟节点 标准差:7719.426986506188
路由到结点[192.168.0.1] 95187次
路由到结点[192.168.0.2] 97355次
路由到结点[192.168.0.3] 103488次
路由到结点[192.168.0.4] 97147次
路由到结点[192.168.0.5] 104259次
路由到结点[192.168.0.6] 97661次
路由到结点[192.168.0.7] 100685次
路由到结点[192.168.0.8] 106974次
路由到结点[192.168.0.9] 98206次
路由到结点[192.168.0.10] 99038次
1000虚拟节点 标准差:3568.2843216313354