数据的组织: 结构化、半结构化、非结构化
内容及其处理:文本、图像、音频、视频
存取:海量
使用:基于语义
运行环境及其管理:面向网络
memcached是一个高性能的分布式内存对象缓存系统,用于动态web应用以减轻数据库负载。
尽管是“分布式”缓存服务器。但是服务器端没有分布式功能,各个memecached之间不会相互通信以共享信息,如何进行分布式?取决于客户端的实现。
Mmcached采用的多线程模型:
主线程(main thread,单一)
工作线程(work thread,多个)
使用libevent作为底层的网络处理组件。
libevent:一个异步事件处理程序库,将Linux的epoll、BSD类操作系统的kqueue等事件处理功能封装成统一接口。使用双向链表保存所有注册的I/O和Signal事件,采用min_heap来管理timeout事件。主循环函数不断检测注册事件,如果有事件发生,则将其放入就绪链表,并调用事件的回调函数,完成业务逻辑处理。
早期的Memcached内存分配通过对所有记录进行malloc和free来进行。
(1)容易产生内存碎片;
(2)加重操作系统内存管理器的负担。
改进措施:默认采用Slab Allocator机制分配、管理内存。
Slab Allocator基本原理:
Chunk——按照预先规定的大小,将分配的内存分割成各种特定长度的块。
slab class——尺寸相同的块分成组(chunk的集合)。
分配到的内存不会释放,重复使用已分配的内存。只在get时查看记录的时间戳是否过期。
替换策略
(1)优先使用已超时的记录空间
(2)如果还存在追加记录时空间不足的情况,使用最近最少使用(LRU)机制替换已有缓存内容(引用计数非零则不替换)
(1)高扩展性
(2)简单的key-value存储查询
(3)高可用,提供“always on”的服务
(4)服务器级别的协议保证
客户端请求最终交给preference list中的一个节点处理,该节点称为coodinator 。Dynamo采用类似Quarum的方式保证数据正确,即W+R>N。
Put流程:
(1)coodinator生成新的数据版本,及vector clock分量 ;
(2)本地保存新数据 ;
(3)向preference list中的所有节点发送写入请求 ;
(4)收到W-1个确认后向用户返回成功 。
Get流程 :
(1)coodinator向preference list中所有节点请求数据版本 ;
(2)等到R-1个答复
(3)coodinator通过vector clock处理有因果关系的数据版本 ;
(4)将不相容的所有数据版本返回用户。
Hinted Handoff(暗示接力)技术:
例:N=3,某数据的preference list是节点A、B、C。若A节点失效,则对该数据的写请求将发送到节点B、C、D上。
D暂时取代A的角色,原本应该写到A上的数据存放到D的一个特定的文件夹(意味着这些数据不是D本该拥有的,而是其它节点的)。
D上会启动一个线程定期检查A的状态,当发现A恢复后,就将D上存放的这些A的数据写回到A。
该策略保证了节点失效时系统的高可用和数据持久性。
默认观点:节点失败无法恢复的情况并不常见,加入或离开集群都需要手动通过命令完成。
Dynamo集群中的每个节点都会维护当前集群的成员及节点不可达等信息,这些信息通过Gossip协议传播到整个集群;客户端可以通过任意一个节点获得并维护这些成员信息,从而找到自己要访问的数据。
Dynamo使用一个基于Gossip的协议传播成员变动,并维持成员的最终一致性:每个节点每隔一秒随机选择另一个节点,两个节点协调他们保存的成员变动历史。
新节点加入时选择自己负责的虚拟节点,并将其虚拟节点表保存到磁盘,之后与其他的节点通过Gossip协议交换协调他们的虚拟节点表。
↓
每个节点都知道全局的虚拟节点表。
**逻辑分裂错误:**如果A节点和B节点同时加入到集群,根据上述基于Gossip协议的加入机制,A和B会互相不知道对方的存在,这种错误称为逻辑分裂。
外部发现(种子节点避免逻辑分裂):
Dynamo中有一些种子节点,每个节点都知道种子节点,每个节点都与种子节点进行虚拟节点表的协调,从而避免了逻辑分裂错误。
种子的发现(discovered)是通过外部机制来实现的
Redis是一个开源的key-value存储系统,将大部分数据存储在内存中,使用C语言开发。
Redis内部使用一个redisObject对象来表示所有的key和value,与Memcached仅支持简单的key-value结构的数据记录不同,value支持五种数据类型及其相关操作:
字符串——String
哈希表——Hash(实现:数据较少时使用zipmap 一种一维数组,增大时转换为ht 真正的HashMap)
链表——List(实现:双向链表)
集合——Set(可以自动去重 ,实现:value永远为null的HashMap,通过计算hash来排重、判断存在)
有序集合——Sorted Set(实现:采用HashMap和跳跃表 SkipList来保证数据的存储和有序,HashMap记录成员到score的映射,跳跃表实现排序,HashMap里存的score作为排序依据。)
Skip List:一种随机化的数据结构,基于并联的链表,其效率相当于二叉查找树(对于大多数操作需要O(log n)平均时间)。
Skip List基本思想:有序的链表加上附加的前进链接。
一个跳表的结构特征:
(1)一个跳表由多个层(level)组成;
(2)每一层都是一个有序的链表;
(3)第1层包含所有的元素;
(4)如果元素x出现在第i层,则所有比i小的层都包含x;
(5)第i层的元素通过一个down指针指向下一层拥有相同值的元素;
(6)在每一层中都包含-1和1两个元素,分别表示INT_MIN和INT_MAX;
(7)Top指针指向最高层的第一个元素。
跳跃列表的增加是以随机化的方式进行的,所以在列表中的查找可以快速的跳过部分列表(因此得名)。
跳表的插入需要三个步骤:
(1)查找到在每层待插入位置;
(2)随机产生一个层数;
(3)从高层至下插入,插入时算法和普通链表的插入完全相同。
删除节点操作和插入类似,找到每层需要删除的位置,之后删除操作和普通链表一样。
注:如果该节点的level是最大的,则需更新跳表的level。
跳表的优点就是查找比普通链表快。
Skip List所有操作都以对数随机化的时间进行,较好的解决了有序链表查找特定值的困难。
Redis通过定义一个数组zmalloc_allocations[]来记录所有的内存分配情况。
这个数组的长度为ZMALLOC_MAX_ALLOC_STAT。
数组的每一个元素代表相应大小内存块被分配的个数(内存块的大小为该元素的下标),例如:zmalloc_allocations[16]代表已经分配的长度为16bytes的内存块的个数。
有一个静态变量 used_memory记录当前分配内存的总和。
Redis虽然是基于内存的存储系统,同时也提供内存数据持久化的机制,有两种持久化策略:
1)RDB快照:将当前数据的快照存成一个数据文件,从而持久化。
RDB的实现借助了fork命令的copy on write机制。在生成快照时,将当前进程fork出一个子进程,然后在子进程中循环所有的数据,将数据写成为RDB文件。
可靠性:当生成一个新的RDB文件时,Redis生成的子进程先将数据写到一个临时文件中,然后通过原子性系统调用rename将临时文件重命名为RDB文件,这样在任何时候出现故障,RDB快照文件总是可用的。
生成时机:可以通过save指令配置RDB快照生成的时机(例如当10分钟以内有100次写入就生成快照,或者1小时内有1000次写入就生成快照,或者多个规则一起实施)。
可用性:开启RDB的代价不高,但是RDB文件中的数据并不是全新的,从上次RDB文件生成到Redis停机这段时间的数据将丢失。
2)AOF日志(Append Only File):一个追加写入的日志文件,与一般数据库的bin log不同,AOF文件是可识别的纯文本,内容是一个个导致数据变化的Redis标准命令。生成过程类似于RDB。
↓
优化策略:AOF文件会越来越大,所以Redis提供了AOF rewrite功能,就是重新生成一份AOF文件,文件中一条记录的操作只会有一次,去掉之前叠加的操作。
Redis槽(slot):集群将key分成16384个slots(hash 槽),slot作为数据映射的单位。
Keys到slot的映射:
HASH_SLOT = CRC16(key) mod 16384。其中CRC16是一种冗余码校验和,将字符串转换成16位的数字。
每个节点持有16384个slots中的一部分。
↓
Redis Cluster最多支持16384个nodes(每个nodes持有一个slot)。
Redis集群中的各个节点通过Gossip协议来交换各自关于不同节点的状态信息,协议由三种消息实现:
MEET(握手)、
PING、
PONG(Ping的回应)。
每次发送MEET、PING、PONG消息时,发送者都从自己的已知节点列表中随机选出两个节点的信息(可以是主节点或者从节点) 保存到两个clusterMsgDataGossip结构中。
接收者收到消息时会访问消息正文中的两个结构,并根据自己是否认识结构中记录的被选中节点进行操作:
1)若发来的节点不在接收者的已知节点列表 → 接收者第一次接触到该节点,接收者将根据结构中记录的IP地址和端口号等信息与该节点进行握手(MEET)。
2)若发来的节点已经存在于接收者的已知节点列表 → 接收者之前已经与该节点接触过,接收者将根据结构记录的信息对该节点对应的clusterNode结构进行更新。
为了保证单点故障下的数据可用性,Redis Cluster引入了Master节点和Slave节点:每个Master节点有两个用于冗余的Slave节点。 → 集群中任意两个节点宕机都不会导致数据不可用。若Master节点退出,集群会自动选择一个Slave节点成为新的Master节点。
NWR理论(WernerVogels在讲“EventuallyConsistent”时提出)。设一个存储系统有如下属性:
N=每个数据的副本数
W=每次写操作时,必须同步确认写成功的副本数
R=每次读操作时,需要读取的副本数
则当W+R>N时,该存储系统可以提供强一致性。
强一致性等价于R中至少包含一个最新的副本,即(R-(N-W))>0,即W+R>N。
微软的图处理引擎,基于分布式内存的云系统,能够有效支持针对web规模图数据的在线和离线处理任务。
在分布式缓存的基础上实现了对图数据的全局寻址,可有效支持随机存取。
默认前提条件:内存成本足够低、网络速度足够高。
Slave节点:存储一部分图数据,执行图计算任务。图计算任务包括向其他各类节点收发消息。
Proxy节点:系统中的可选节点,不包含数据,只处理消息。作为client和slave节点的中间层,也用作消息聚集节点,可汇总来自多个slave的消息。
Client节点:用户接口层,通过API和slave以及proxy节点通讯。
数据空间的划分:分为2^p个内存块(trunk),分布于m个节点上( 2^p >m),通常一个节点容纳多个trunk。
分解成多个trunk的原因:
1)多个trunk有利于并发;
2)维持一个大型的哈希表将导致哈希冲突的概率增加。
底层存储:为保证容错一致性,这些trunk底层上采用TFS(Trinity File System)分布式文件系统存储,类似HDFS
Kademlia(简称Kad),一种典型的结构化P2P覆盖网络(应用层网络)。
信息的存储:以哈希表条目形式分散存储在各节点上。
↓
全网构成一张巨大的分布式哈希表
检索:通过Kademlia协议查询key值对应的value(不必关心value所在节点位置)。
应用:eMule、BitTorrent等P2P文件交换系统的检索协议。
网络集群存储、维护两张分布式哈希表:关键词字典、文件索引字典。
关键词字典:关键词→其所对应的文件名称及相关信息,key=关键词字符串的160比特SHA1散列,value为一个三元组列表 (文件名,文件长度,文件的SHA1校验值) 。
文件索引字典:文件信息→文件的拥有者(下载服务提供者),key=文件的SHA1校验值,value也是一个三元组列表 (拥有者IP,下载侦听端口,拥有者节点ID)。
存储和交换无需集中索引服务器参与优势:
1)提高了查询效率
2)提高了文件交换系统的可靠性。
每一个节点有一个专属ID(一个160bit的整数),由节点自己随机生成(可以认为ID具有唯一性)。
距离为两个ID的二进制异或值。
两个节点的ID分别为a与b,则有:
distance=a XOR b。
Kad网络规定:条目依据其key值被复制到目标节点ID距离最近的k个节点中。
k取值准则——任意选择至少k(典型取值20)个节点,它们在任意时刻同时不在线的几率几乎为0。
为了实现较短的查询响应延迟,在条目查询的过程中,任一条目可被cache到任意节点之上。
时效性:考虑条目在节点上存储的时效性,越接近目标结点保存的时间将越长。
为什么?
节点之间的距离取异或值
↓
对于同一个key值的所有查询都会逐步收敛到同一个路径上,而不管查询的起始节点位置如何。
↓
沿着查询路径上的节点都缓存相应的
每一个节点均维护160个list,每个list称为一个k-桶(k-bucket) 。
第i个k-桶的内容:记录当前节点已知的与自身距离为2i~2(i+1)的其他节点的网络信息(NodeID,IP地址,UDP端口)。
一个k-桶最多存放k个对端节点信息,桶中节点信息按访问时间排序(最早访问的在头部)。
List(k-桶)的更新原则:
1)目标节点信息已经在list中,将其移至队尾;
2)list未满,且目标节点不在其中,其信息将直接添入list队尾;
3)list已满,先检查队首节点是否仍有响应,如果有,则队首节点被移至队尾,目标节点被抛弃;如果没有,则抛弃队首节点,将最新访问的节点信息插入队尾。
K桶的设计初衷:维护最近最新见到的节点信息更新,对于某个需要查找的特定ID节点N,可以从当前节点的k桶中迅速的查出距离N最近的若干已知节点。
查找与目标节点网络距离最近的k个节点所对应的网络信息(NodeID,IP地址,UDP端口)。
1)发起者从自己的k-桶中选出若干距离目标ID最近的节点,并向它们同时发送异步查询请求;
2)被查询节点收到请求后,从自己的k-桶中找出自己所知的目标ID的若干近邻返回给发起者;
3)发起者收到返回信息后,再次从当前已知的近邻节点中选出若干未被请求的,并重复步骤1。
重复上述过程2)~3)直至无法获得k近邻的更新时停止。
在查询过程中没有及时响应的节点将立即被排除。
搜索发起方以迭代方式不断查询距离key较近的节点
↓
直至查询路径中的任一节点返回所需查找的value。
系统优化:
搜索成功后发起方可选择将条目作为cache存储到查询路径的多个节点中,条目cache的超时时间与节点的距离呈指数反比关系。
1)获知一个已经加入Kad网络的节点信息(记为节点I),并将其加入自己的k-buckets;
2)向I节点发起一次针对自己ID的节点查询请求,从而通过节点I获取一系列与自己邻近的其他节点信息;
3)刷新所有的k-bucket,保证自己获得最新的节点信息。
1)NAS是一台特殊的含有大硬盘空间的计算机,连接在以太网上,其它计算机通过网络映射硬盘使用该空间。
SAN是一种容易扩容的光纤通讯的磁盘阵列机,是多台服务器共享使用多台阵列机,可以安装各种软件,可跨平台。
2)SAN是光纤协议,NAS是TCP/IP协议。NAS是利用现有网络,SAN是在sever端再架设一个网络。
3)NAS以文件方式访问数据,而SAN以sectors方式访问数据。
SAN对于高容量块级数据传输有明显的优势,易扩展且管理高效,可运行关键应用(如数据库、备份等)。
NAS更加适合文件级别的数据处理。可作为日常办公中需要经常交换小文件的存储配置(如存储网页)。
4)SAN更多的是强调:范围+高效。
NAS主要强调:共享。
NAS使用的文件传输协议意味着:当把数据库建立在NAS上时,取得一条记录需要对整个数据文件进行传输(如果数据库不更改数据访问方式)。
一些公司推出了融合NAS与SAN的存储解决方案,可分为两类:“NAS头”与“统一存储系统”。
1)NAS头——由专为提供文件服务而优化的部件(文件管理器,filer)构成,NAS头连接到后端上的SAN存储上,以类似于利用SAN存储提供存储容量的方式为NAS头提供存储容量。
2)“统一存储系统”(如NetApp的FAS统一网络存储系统)——原有的NAS基础上增加对FCP协议的支持。
通过不同的接口卡完成对SAN和NAS的同时支持,如通过以太网卡提供NAS的访问服务,同时又可通过HBA卡提供SAN的访问服务。
由于NAS具有自己的操作系统和文件系统,因此增加的FCP和原有的NFS、CIFS、HTTP一样,仅是一个协议的支持。
NAS和SAN可以共同有效使用所有虚拟化的空间。
虽然数据库系统数据量十分庞大(可能几十亿、几百亿条甚至更多),但一段时间(例如一天)的修改量并不大(通常不超过几千万条到几亿条)
↓
增量数据(UpdateServer) + 基线数据(ChunkServer)
↓
增量数据: OceanBase使用单台服务器(UpdateServer)记录一段时间的修改增量,其存储以内存表(memtable)为主,SSD(固态盘)为辅。
基线数据:在增量数据时间段内保持不变的数据称为基线数据,存储于多台服务器(ChunkServer),类似于分布式文件系统。
RootServer:主控机(类似于GFS的master),进行机器故障检测、负载平衡计算、负载迁移调度等。
查询:由MergerServer把基线数据(Chunk-Server)和增量数据( UpdateServer )融合后返回调用者,分散在多台服务器上。
写事务:集中在UpdateServer进行,避免了复杂的分布式写事务,可实现跨行跨表事务,又有较好的扩展性。
UpdateServer开始总是以内存表(memtable)方式记录修改,当内存表达到一定阈值,就冻结当前内存表并将后续修改切换到新的内存表。
冻结内存表不再接受写入并被转换成一种紧凑格式保存到SSD盘,转换完成后,冻结内存表的内存即可回收。
使用主键(row key,类似于关系数据库的聚簇索引)对表中数据进行排序和存储。主键可包含多列并具有唯一性。
基线数据按主键排序并划分成sstable(存储一个或几个表的一段按主键连续的数据),一个或者多个sstable组合成数据量大致相等的块(tablet,缺省大小是256MB,可配置)并存储在ChunkServer上。
为了避免ChunkServer故障导致数据丢失,tablet通常保存2~3个副本(可配置)。
每隔一段时间(例如一天)把当前增量合并到原有基线数据并生成新的基线数据(称为每日合并),然后清除过期的修改增量和过期的基线数据。(数据的迁移)
合并过程:
1)UpdateServer冻结当前内存表并开启新内存表,此后新的修改写入新内存表;
2)ChunkServer融合当前基线数据与冻结的内存表,生成新基线数据;
3)当所有tablet的新基线数据生成后,UpdateServer冻结的内存表即可释放,其所占内存也被回收。
合并的调度时机:为了降低对用户访问的影响,合并被设置成低优先级任务,当机器负载(如CPU负载和I/O等待)高于一定阈值时,合并速度会减慢甚至暂停。
实际应用中,数据库管理员(DBA)通常把每日合并设定在业务的低峰期(后半夜) 。
1) 状态数据的持久化和迁移。
更新操作首先以事务提交日志(MySQL称为binlog, NOSQL称为commit log)写入到磁盘,为了保证可靠性,commit log需要复制多份。
机器宕机时需要通过commit log记录的状态修改信息将服务迁移到集群中的其它节点。
2) 子表的分裂和合并。
B+树实现的难点在于树节点的分裂与合并。
在分布式系统中,数据被顺序划分为大小在几十到几百MB的数据区间(子表),相当于B+树的叶子节点。
每个子表在系统中存储多份,需要保证多个副本的分裂点一致。
子表分裂时也有更新操作,增加了多副本一致的难度。