kv持久化数据库
Ⅰ 一般什么产品或者系统或网站会使用K/V数据库型数据库呢
KV型存储系统是最常用的Nosql存储系统之一。Memcached和Redis是其最具代表的两个产品。本文将详细介绍Memcached和Redis的常用场景及如何构建一个高可用和自动弹性伸缩的KV存储系统。
Cache加DB是最常见的存储层架构。时间局部性原理指出正在被访问的数据很可能会在近期再次被访问。根据这一原理应用程序将最近访问过的数据保存在Cache中,每次读取请求首先访问Cache,若Cache中保存有该数据则直接获取数据返回给前端。若Cache中该数据不存在则从DB获取数据并将该数据保存到Cache;若数据被更新或删除则将Cache中对应数据置为失效。使用Cache能够很好地缓解DB的读请求压力。KV存储系统既可以应用在Cache层也可以应用在DB层。
Memcached使用内存作为存储介质,因为内存数据的易失性Memcached主要应用在Cache层。Memcached常见的应用场景是存储一些读取频繁但更新较少的数据,如静态网页、系统配置及规则数据、活跃用户的基本数据和个性化定制数据、准实时统计信息等。并不是所有场景都适合Memcached加DB的架构,在某些场景下这一架构存在一些局限。例如这一架构不能提升写的性能,写数据时还是数据直接存储到DB,同时需要将Cache中数据置为失效,所以对以写请求为主的应用使用Cache提升性能的效果并不是很明显。如果应用的热点数据或者活跃用户分布较为分散也会降低Cache的命中率。如果遇到机器宕机,内存数据会丢失,那么机器重启后需要一段时间重新建立热点数据,建立热点数据的过程中会对DB会造成较大的压力,严重时会导致系统雪崩。
相比Memcached,Redis做了一些优化。首先,Redis对数据做了持久化,支持AOF和RDB两种持久化方式,机器重启后能通过持久化数据自动重建内存。其次,Redis支持主从复制,主机会自动将数据同步到从机,可以进行读写分离,主机负责写操作,从机负责读操作。那样既增加了系统的读写性能又提升了数据的可靠性。再次,Redis除了支持string类型的value外还支持string、hash、set、sorted set、list等类型的数据结构。因此,Redis既可以应用在Cache层,也可以替换或者部分替换DB存储持久化数据。使用Redis作为Cache时机器宕机后热点数据不会丢失,无须像Memcached一样重建热点数据。相比Cache加DB的架构方式,使用Redis存储持久化数据不仅能够提升读性能,还能提升写性能,而且不存在热点数据分布是否集中而影响命中率的问题。Redis丰富的数据结构也使其拥有更加丰富的应用场景。Redis的命令都是原子性的,可以简单地利用INCR和DECR实现计数功能。使用list可以实现获取最近N个数的操作。sort set支持对数据排序,可以应用在排行榜中。set集合可以应用到数据排重。Redis还支持过期时间设置,可以应用到需要设定精确过期时间的应用。只要可以使用Redis支持的数据结构表示的场景,就可以使用Redis进行存储。但Redis不是万能的,它不支持关系型数据库复杂的SQL操作。某些场景下,可结合Redis和关系型DB,将简单查询相关的数据保存在Redis中,复杂SQL操作由关系型DB完成。
虽然Redis集很多优点于一身,但在实际运营中也存在一些问题。首先,Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。如果主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。其次,Redis的主从复制采用全量复制,复制过程中主机会fork出一个子进程对内存做一份快照,并将子进程的内存快照保持为文件发送给从机,这一过程需要确保主机有足够多的空余内存。若快照文件较大,对集群的服务能力会产生较大的影响,而且复制过程是在从机新加入集群或者从机和主机网络断开重连时都会进行,也就是网络波动都会造成主机和从机间的一次全量的数据复制,这对实际的系统运营造成了不小的麻烦。最后,Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。
Ⅱ 刚刚问我,redis持久化数据到数据库是怎么操作的
1、 快照的方式持久化到磁盘
自动持久化规则配置
save 900 1
save 300 10
save 60 10000
上面的配置规则意思如下:
# In the example below the behaviour will be to save:
# after 900 sec (15 min) if at least 1 key changed
# after 300 sec (5 min) if at least 10 keys changed
# after 60 sec if at least 10000 keys changed
redis也可以关闭自动持久化,注释掉这些save配置,或者save “”
如果后台保存到磁盘发生错误,将停止写操作.
stop-writes-on-bgsave-error yes
使用LZF压缩rdb文件,这会耗CPU, 但是可以减少磁盘占用.
rdbcompression yes
保存rdb和加载rdb文件的时候检验,可以防止错误,但是要付出约10%的性能,可以关闭他,提高性能。
rdbchecksum yes
导出的rdb文件名
dbfilename mp.rdb
设置工作目录, rdb文件会写到该目录, append only file也会存储在该目录下.
dir ./
Redis自动快照保存到磁盘或者调用bgsave,是后台进程完成的,其他客户端仍然和可以读写redis服务器,后台保存快照到磁盘会占用大量内存。调用save保存内存中的数据到磁盘,将阻塞客户端请求,直到保存完毕。
调用shutdown命令,Redis服务器会先调用save,所有数据持久化到磁盘之后才会真正退出。
对于数据丢失的问题:
如果服务器crash,从上一次快照之后的数据将全部丢失。所以在设置保存规则的时候,要根据实际业务设置允许的范围。
如果对于数据敏感的业务,在程序中要使用恰当的日志,在服务器crash之后,通过日志恢复数据。
2、 Append-only file 的方式持久化
另外一种方式为递增的方式,将会引起数据变化的操作, 持久化到文件中, 重启redis的时候,通过操作命令,恢复数据.
每次执行写操作命令之后,都会将数据写到server.aofbuf中。
# appendfsync always
appendfsync everysec
# appendfsync no
当配置为always的时候,每次server.aofbuf中的数据写入到文件之后,才会返回给客户端,这样可以保证数据不丢,但是频繁的IO操作,会降低性能。
everysec每秒写一次,这可能会丢失一秒内的操作。
aof最大的问题就是随着时间append file会变的很大,所以我们需要bgrewriteaof命令重新整理文件,只保留最新的kv数据。
Ⅲ ios leveldb怎么打开数据库
下文例子中演示了如何插入、获取、删除一条记录
LevelDB 简介
一、LevelDB入门
LevelDB是Google开源的持久化KV单机数据库,具有很高的随机写,顺序读/写性能,但是随机读的性能很一般,也就是说,LevelDB很适合应用在查询较少,而写很多的场景。LevelDB应用了LSM (Log Structured Merge) 策略,lsm_tree对索引变更进行延迟及批量处理,并通过一种类似于归并排序的方式高效地将更新迁移到磁盘,降低索引插入开销,关于LSM,本文在后面也会简单提及。
根据LevelDB官方网站的描述,LevelDB的特点和限制如下:
特点:
1、key和value都是任意长度的字节数组;
2、entry(即一条K-V记录)默认是按照key的字典顺序存储的,当然开发者也可以重载这个排序函数;
3、提供的基本操作接口:Put()、Delete()、Get()、Batch();
4、支持批量操作以原子操作进行;
5、可以创建数据全景的snapshot(快照),并允许在快照中查找数据;
6、可以通过前向(或后向)迭代器遍历数据(迭代器会隐含的创建一个snapshot);
7、自动使用Snappy压缩数据;
8、可移植性;
限制:
1、非关系型数据模型(NoSQL),不支持sql语句,也不支持索引;
2、一次只允许一个进程访问一个特定的数据库;
3、没有内置的C/S架构,但开发者可以使用LevelDB库自己封装一个server;
LevelDB本身只是一个lib库,在源码目录make编译即可,然后在我们的应用程序里面可以直接include leveldb/include/db.h头文件,该头文件有几个基本的数据库操作接口,下面是一个测试例子:
#include <iostream>
#include <string>
#include <assert.h>
#include "leveldb/db.h"
using namespace std;
int main(void)
{
leveldb::DB *db;
leveldb::Options options;
options.create_if_missing = true;
// open
leveldb::Status status = leveldb::DB::Open(options,"/tmp/testdb", &db);
assert(status.ok());
string key = "name";
string value = "chenqi";
// write
status = db->Put(leveldb::WriteOptions(), key, value);
assert(status.ok());
// read
status = db->Get(leveldb::ReadOptions(), key, &value);
assert(status.ok());
cout<<value<<endl;
// delete
status = db->Delete(leveldb::WriteOptions(), key);
assert(status.ok());
status = db->Get(leveldb::ReadOptions(),key, &value);
if(!status.ok()) {
cerr<<key<<" "<<status.ToString()<<endl;
} else {
cout<<key<<"==="<<value<<endl;
}
// close
delete db;
return 0;
}
上面的例子演示了如何插入、获取、删除一条记录,编译代码:
g++ -o test test.cpp libleveldb.a -lpthread -Iinclude
执行./test后,会在/tmp下面生成一个目录testdb,里面包含若干文件:
------------------------------------------------------------
LevelDB是google开源的一个key-value存储引擎库,类似于开源的Lucene索引库一样。其他的软件开发者可以利用该库做二次开发,来满足定制需求。LevelDB采用日志式的写方式来提高写性能,但是牺牲了部分读性能。为了弥补牺牲了的读性能,一些人提议使用SSD作为存储介质。
对于本地化的Key-value存储引擎来说,简单的使用一般都分成三个基本的步骤:(1)打开一个数据库实例;(2)对这个数据库实例进行插入,修改和查询操作;(3)最后在使用完成之后,关闭该数据库。下面将详细讨论该三个步骤:
一、打开一个数据库实例
一个leveldb数据库有一个对应一个文件系统目录的名字。该数据库的所有内容都存储在这个目录下。下面的代码描述了怎样打开一个数据库或者建立一个新的数据库。
#include <assert.h>
#include "leveldb/db.h"
leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
leveldb::Status status = leveldb::DB::Open(options,"/tmp/testdb", &db);
assert(status.ok());
如果打开已存在数据库的时候,需要抛出错误。将以下代码插在leveldb::DB::Open方法前面:
options.error_if_exists = true;
二、对数据库的简单读、写操作
LevelDB提供了Put,Delete和Get三个方法对数据库进行修改和查询。例如,下面的代码片段描述了怎样将key1对应的value值,移到key2对应的值。
std::string value;
leveldb::Status s = db->Get(leveldb::ReadOptions(), key1, &value);
if(s.ok()) s = db->Put(leveldb::WriteOptions(), key2, value);
if(s.ok()) s = db->Delete(leveldb::WriteOptions(), key1);
三、关闭数据库
在对数据库进行了一系列的操作之后,需要对数据库进行关闭。该操作比较简单:
... open the db as described above...
... do something with db ...
delete db;
上面对levelDB的简单使用做了基本的介绍,接下来就是如何自己写一个完成并且能运行的例子。
1、下载源码 git clone https://code.google.com/p/leveldb/
2、编译源码 cd leveldb && make all
3、编写test.cpp
#include <assert.h>
#include <string.h>
#include <leveldb/db.h>
#include <iostream>
int main(){
leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
leveldb::Status status = leveldb::DB::Open(options,"/tmp/testdb", &db);
assert(status.ok());
//write key1,value1
std::string key="key";
std::string value = "value";
status = db->Put(leveldb::WriteOptions(), key,value);
assert(status.ok());
status = db->Get(leveldb::ReadOptions(), key, &value);
assert(status.ok());
std::cout<<value<<std::endl;
std::string key2 = "key2";
//move the value under key to key2
status = db->Put(leveldb::WriteOptions(),key2,value);
assert(status.ok());
status = db->Delete(leveldb::WriteOptions(), key);
assert(status.ok());
status = db->Get(leveldb::ReadOptions(),key2, &value);
assert(status.ok());
std::cout<<key2<<"==="<<value<<std::endl;
status = db->Get(leveldb::ReadOptions(),key, &value);
if(!status.ok()) std::cerr<<key<<" "<<status.ToString()<<std::endl;
else std::cout<<key<<"==="<<value<<std::endl;
delete db;
return 0;
}
4、编译链接 g++ -o test test.cpp ../leveldb/libleveldb.a -lpthread -I../leveldb/include
注意libleveldb.a 和leveldb include的路径。
5、运行结果./test:
value
key2===value
key NotFound:
Ⅳ B站分布式KV存储实践
在B站的业务场景中,存在很多种不同模型的数据,有些数据关系比较复杂像:账号、稿件信息。有些数据关系比较简单,只需要简单的kv模型即可满足。此外,又存在某些读写吞吐比较高的业务场景,该场景早期的解决方案是通过MySQL来进行数据的持久化存储,同时通过redis来提升访问的速度与吞吐。但是这种模式带来了两个问题,其一是存储与缓存一致性的问题,该问题在B站通过canal异步更新缓存的方式得以解决,其二则是开发的复杂度,对于这样一套存储系统,每个业务都需要额外维护一个任务脚本来消费canal数据进行缓存数据的更新。基于这种场景,业务需要的其实是一个介于Redis与MySQL之间的提供持久化高性能的kv存储。此外对象存储的元数据,对数据的一致性、可靠性与扩展性有着很高的要求。
基于此背景,我们对自研KV的定位从一开始就是构建一个高可靠、高可用、高性能、高拓展的系统。对于存储系统,核心是保证数据的可靠性,当数据不可靠时提供再高的可用性也是没用的。可靠性的一个核心因素就是数据的多副本容灾,通过raft一致性协议保证多副本数据的一致性。
分布式系统,如何对数据进行分片放置,业界通常有两种做法,一是基于hash进行分区,二是基于range进行分区,两种方式各有优缺点。hash分区,可以有效防止热点问题,但是由于key是hash以后放置的,无法保证key的全局有序。range分区,由于相邻的数据都放在一起,因此可以保证数据的有序,但是同时也可能带来写入热点的问题。基于B站的业务场景,我们同时支持了range分区和hash分区,业务接入的时候可以根据业务特性进行选择。大部分场景,并不需要全局有序,所以默认推荐hash分区的接入方式,比如观看记录、用户动态这些场景,只需要保证同一个用户维度的数据有序即可,同一个用户维度的数据可以通过hashtag的方式保证局部有序。
整个系统核心分为三个组件:
Metaserver用户集群元信息的管理,包括对kv节点的健康监测、故障转移以及负载均衡。
Node为kv数据存储节点,用于实际存储kv数据,每个Node上保存数据的一个副本,不同Node之间的分片副本通过raft保证数据的一致性,并选出主节点对外提供读写,业务也可以根据对数据一致性的需求指定是否允许读从节点,在对数据一致性要求不高的场景时,通过设置允许读从节点可以提高可用性以及降低长尾。
Client模块为用户访问入口,对外提供了两种接入方式,一种是通过proxy模式的方式进行接入,另一种是通过原生的SDK直接访问,proxy本身也是封装自c++的原生SDK。SDK从Metaserver获取表的元数据分布信息,根据元数据信息决定将用户请求具体发送到哪个对应的Node节点。同时为了保证高可用,SDK还实现了重试机制以及backoff请求。
集群的拓扑结构包含了几个概念,分别是Pool、Zone、Node、Table、Shard 与Replica。
基于不同的业务场景,我们同时支持了range分区和hash分区。对于range场景,随着用户数据的增长,需要对分区数据进行分裂迁移。对于hash分区的场景,使用上通常会根据业务的数据量做几倍的冗余预估,然后创建合适的分片数。但是即便是几倍的冗余预估,由于业务发展速度的不可预测,也很容易出现实际使用远超预估的场景,从而导致单个数据分片过大。
之所以不在一开始就创建足够的分片数有两个原因:其一,由于每一个replica都包含一个独立的engine,过多的分片会导致数据文件过多,同时对于批量写入场景存在一定的写扇出放大。其二,每一个shard都是一组raftgroup,过多的raft心跳会对服务造成额外的开销,这一点后续我们会考虑基于节点做心跳合并优化减少集群心跳数。
为了满足业务的需求场景,我们同时支持了range和hash两种模式下的分裂。两种模式分裂流程类似,下面以hash为例进行说明。
hash模式下的分裂为直接根据当前分片数进行倍增。分裂的流程主要涉及三个模块的交互。
metaserver
分裂时,metaserver会根据当前分片数计算出目标分片数,并且下发创建replica指令到对应的Node节点,同时更新shard分布信息,唯一不同的是,处于分裂中的shard状态为splitting。该状态用于client流量请求路由识别。当Node完成数据分裂以后上报metaserver,metaserver更新shard状态为normal从而完成分裂。
Node
node收到分裂请求以后,会根据需要分裂的分片id在原地拉起创建一个新的分片。然后对旧分片的数据进行checkpoint,同时记录旧分片checkpoint对应的logid。新分片创建完成后,会直接从旧分片的checkpoint进行open,然后在异步复制logid之后的数据保证数据的一致性。新分片加载完checkpoint后,原来的旧分片会向raftgroup提交一条分裂完成日志,该日志处理流程与普通raft日志一致。分裂完成后上报分裂状态到metaserver,同时旧分片开始拒绝不再属于自己分片的数据写入,client收到分片错误以后会请求metaserver更新shard分布。
完成分裂以后的两个分片拥有的两倍冗余数据,这些数据会在engine compaction的时候根据compaction_filter过滤进行删除。
Client
用户请求时,根据hash(key) % shard_cnt 获取目标分片。表分裂期间,该shard_cnt表示分裂完成后的最终分片数。以上图3分片的分裂为例:
hash(key) = 4, 分裂前shard_cnt为3,因此该请求会被发送到shard1. 分裂期间,由于shard_cnt变为6,因此目标分片应该是shard4, 但是由于shard4为splitting,因此client会重新计算分片从而将请求继续发送给shard1. 等到最终分裂完成后,shard4状态变更为Normal,请求才会被发送到shard4.
分裂期间,如果Node返回分片信息错误,那么client会请求metaserver更新分片分布信息。
类似于MySQL的binlog,我们基于raftlog日志实现了kv的binlog. 业务可以根据binlog进行实时的事件流订阅,同时为了满足事件流回溯的需求,我们还对binlog数据进行冷备。通过将binlog冷备到对象存储,满足了部分场景需要回溯较长事件记录的需求。
直接复用raftlog作为用户行为的binlog,可以减少binlog产生的额外写放大,唯一需要处理的是过滤raft本身的配置变更信息。learner通过实时监听不断拉取分片产生的binlog到本地并解析。根据learner配置信息决定将数据同步到对应的下游。同时binlog数据还会被异步备份到对象存储,当业务需要回溯较长时间的事件流的时候,可以直接指定位置从S3拉取历史binlog进行解析。
基于上述提到的binlog能力,我们还基于此实现了kv的多活。learner模块会实时将用户写入的数据同步到跨数据中心的其他kv集群。对于跨数据中心部署的业务,业务可以选择就近的kv集群进行读取访问,降低访问延时。
kv的多活分为读多活和写多活。对于读多活,机房A的写入会被异步复制到机房B,机房B的服务可以直接读取本机房的数据,该模式下只有机房A的kv可以写入。对于写多活,kv在机房A B 都能同时提供写入并且进行双向同步,但是为了保证数据的一致性,需要业务上做数据的单元化写入,保证两个机房不会同时修改同一条记录。通过将用户划分单元,提供了写多活的能力。通过对binlog数据打标,解决了双向同步时候的数据回环问题。
对于用户画像和特征引擎等场景,需要将离线生成的大量数据快速导入KV存储系统提供用户读取访问。传统的写入方式是根据生成的数据记录一条条写入kv存储,这样带来两个问题。其一,大批量写入会对kv造成额外的负载与写入带宽放大造成浪费。其次,由于写入量巨大,每次导入需要花费较长的时间。为了减少写入放大以及导入提速,我们支持了bulk load的能力。离线平台只需要根据kv的存储格式离线生成对应的SST文件,然后上传到对象存储服务。kv直接从对象存储拉取SST文件到本地,然后直接加载SST文件即可对外提供读服务。bulk load的另外一个好处是可以直接在生成SST后离线进行compaction,将compaction的负载offload到离线的同时也降低了空间的放大。
由于LSM tree的写入特性,数据需要被不断的compaction到更底层的level。在compaction时,如果该key还有效,那么会被写入到更底层的level里,如果该key已经被删除,那么会判断当前level是否是最底层的,一条被删除的key,会被标记为删除,直到被compaction到最底层level的时候才会被真正删除。compaction的时候会带来额外的写放大,尤其当value比较大的时候,会造成巨大的带宽浪费。为了降低写放大,我们参考了Bitcask实现了kv分离的存储引擎sparrowdb.
sparrowdb 介绍
用户写入的时候,value通过append only的方式写入data文件,然后更新索引信息,索引的value包含实际数据所在的data文件id,value大小以及position信息,同时data文件也会包含索引信息。与原始的bitcask实现不一样的是,我们将索引信息保存在 rocksdb。
更新写入的时候,只需要更新对应的索引即可。compaction的时候,只需将索引写入底层的level,而无需进行data的拷贝写入。对于已经失效的data,通过后台线程进行检查,当发现data文件里的索引与rocksdb保存的索引不一致的时候,说明该data已经被删除或更新,数据可以被回收淘汰。
使用kv存储分离降低了写放大的问题,但是由于kv分离存储,会导致读的时候多了一次io,读请求需要先根据key读到索引信息,再根据索引信息去对应的文件读取data数据。为了降低读访问的开销,我们针对value比较小的数据进行了inline,只有当value超过一定阈值的时候才会被分离存储到data文件。通过inline以及kv分离获取读性能与写放大之间的平衡。
在分布式系统中,负载均衡是绕不过去的问题。一个好的负载均衡策略可以防止机器资源的空闲浪费。同时通过负载均衡,可以防止流量倾斜导致部分节点负载过高从而影响请求质量。对于存储系统,负载均衡不仅涉及到磁盘的空间,也涉及到机器的内存、cpu、磁盘io等。同时由于使用raft进行主从选主,保证主节点尽可能的打散也是均衡需要考虑的问题。
副本均衡
由于设计上我们会尽量保证每个副本的大小尽量相等,因此对于空间的负载其实可以等价为每块磁盘的副本数。创建副本时,会从可用的zone中寻找包含副本数最少的节点进行创建。同时考虑到不同业务类型的副本读写吞吐可能不一样导致CPU负载不一致,在挑选副本的时候会进一步检查当前节点的负载情况,如果当前节点负载超过阈值,则跳过该节点继续选择其他合适的节点。目前基于最少副本数以及负载校验基本可以做到集群内部的节点负载均衡。
当出现负载倾斜时,则从负载较高的节点选择副本进行迁出,从集群中寻找负载最低的节点作为待迁入节点。当出现节点故障下线以及新机器资源加入的时候,也是基于均值计算待迁出以及迁入节点进行均衡。
主从均衡
虽然通过最少副本数策略保证了节点副本数的均衡,但是由于raft选主的性质,可能出现主节点都集中在部分少数节点的情况。由于只有主节点对外提供写入,主节点的倾斜也会导致负载的不均衡。为了保证主节点的均衡,Node节点会定期向metaserver上报当前节点上副本的主从信息。
主从均衡基于表维度进行操作。metaserver会根据表在Node的分布信息进行副本数的计算。主副本的数量基于最朴素简单的数学期望进行计算: 主副本期望值 = 节点副本数 / 分片副本数。下面为一个简单的例子:
假设表a包含10个shard,每个shard 3个replica。在节点A、B、C、D的分布为 10、5、6、9. 那么A、B、C、D的主副本数期望值应该为 3、1、2、3. 如果节点数实际的主副本数少于期望值,那么被放入待迁入区,如果大于期望值,那么被放入待迁出区。同时通过添加误差值来避免频繁的迁入迁出。只要节点的实际主副本数处于 [x-δx,x+δx] 则表示主副本数处于稳定期间,x、δx 分别表示期望值和误差值。
需要注意的是,当对raft进行主从切换的时候,从节点需要追上所有已提交的日志以后才能成功选为主,如果有节点落后的时候进行主从切换,那么可能导致由于追数据产生的一段时间无主的情况。因此在做主从切换的时候必须要检查主从的日志复制状态,当存在慢节点的时候禁止进行切换。
3.7 故障检测&修复
一个小概率的事件,随着规模的变大,也会变成大概率的事件。分布式系统下,随着集群规模的变大,机器的故障将变得愈发频繁。因此如何对故障进行自动检测容灾修复也是分布式系统的核心问题。故障的容灾主要通过多副本raft来保证,那么如何进行故障的自动发现与修复呢。
健康监测
metaserver会定期向node节点发送心跳检查node的健康状态,如果node出现故障不可达,那么metaserver会将node标记为故障状态并剔除,同时将node上原来的replica迁移到其他健康的节点。
为了防止部分node和metaserver之间部分网络隔离的情况下node节点被误剔除,我们添加了心跳转发的功能。上图中三个node节点对于客户端都是正常的,但是node3由于网络隔离与metaserver不可达了,如果metaserver此时直接剔除node3会造成节点无必要的剔除操作。通过node2转发心跳探测node3的状态避免了误剔除操作。
除了对节点的状态进行检测外,node节点本身还会检查磁盘信息并进行上报,当出现磁盘异常时上报异常磁盘信息并进行踢盘。磁盘的异常主要通过dmesg日志进行采集分析。
故障修复
当出现磁盘节点故障时,需要将原有故障设备的replica迁移到其他健康节点,metaserver根据负载均衡策略选择合适的node并创建新replica, 新创建的replica会被加入原有shard的raft group并从leader复制快照数据,复制完快照以后成功加入raft group完成故障replica的修复。
故障的修复主要涉及快照的复制。每一个replica会定期创建快照删除旧的raftlog,快照信息为完整的rocksdb checkpoint。通过快照进行修复时,只需要拷贝checkpoint下的所有文件即可。通过直接拷贝文件可以大幅减少快照修复的时间。需要注意的是快照拷贝也需要进行io限速,防止文件拷贝影响在线io.
过期数据淘汰
在很多业务场景中,业务的数据只需要存储一段时间,过期后数据即可以自动删除清理,为了支持这个功能,我们通过在value上添加额外的ttl信息,并在compaction的时候通过compaction_filter进行过期数据的淘汰。level之间的容量呈指数增长,因此rocksdb越底层能容纳越多的数据,随着时间的推移,很多数据都会被移动到底层,但是由于底层的容量比较大,很难触发compaction,这就导致很多已经过期的数据没法被及时淘汰从而导致了空间放大。与此同时,大量的过期数据也会对scan的性能造成影响。这个问题可以通过设置periodic_compaction_seconds 来解决,通过设置周期性的compaction来触发过期数据的回收。
scan慢查询
除了上面提到的存在大批过期数据的时候可能导致的scan慢查询,如果业务存在大批量的删除,也可能导致scan的时候出现慢查询。因为delete对于rocksdb本质也是一条append操作,delete写入会被添加删除标记,只有等到该记录被compaction移动到最底层后该标记才会被真正删除。带来的一个问题是如果用户scan的数据区间刚好存在大量的delete标记,那么iterator需要迭代过滤这些标记直到找到有效数据从而导致慢查询。该问题可以通过添加 CompactOnDeletionCollector 来解决。当memtable flush或者sst compaction的时候,collector会统计当前key被删除的比例,通过设置合理的 deletion_trigger ,当发现被delete的key数量超过阈值的时候主动触发compaction。
delay compaction
通过设置 CompactOnDeletionCollector 解决了delete导致的慢查询问题。但是对于某些业务场景,却会到来严重的写放大。当L0被compaction到L1时候,由于阈值超过deletion_trigger ,会导致L1被添加到compaction队列,由于业务的数据特性,L1和L2存在大量重叠的数据区间,导致每次L1的compaction会同时带上大量的L2文件造成巨大的写放大。为了解决这个问题,我们对这种特性的业务数据禁用了CompactOnDeletionCollector 。通过设置表级别参数来控制表级别的compaction策略。后续会考虑优化delete trigger的时机,通过只在指定层级触发来避免大量的io放大。
compaction限速
由于rocksdb的compaction会造成大量的io读写,如果不对compaction的io进行限速,那么很可能影响到在线的写入。但是限速具体配置多少比较合适其实很难确定,配置大了影响在线业务,配置小了又会导致低峰期带宽浪费。基于此rocksdb 在5.9以后为 NewGenericRateLimiter 添加了 auto_tuned 参数,可以根据当前负载自适应调整限速。需要注意的是,该函数还有一个参数 RateLimiter::Mode 用来限制操作类型,默认值为 kWritesOnly,通常情况该模式不会有问题,但是如果业务存在大量被删除的数据,只限制写可能会导致compaction的时候造成大量的读io。
关闭WAL
由于raft log本身已经可以保证数据的可靠性,因此写入rocksdb的时候可以关闭wal减少磁盘io,节点重启的时候根据rocksdb里保存的last_apply_id从raft log进行状态机回放即可。
降副本容灾
对于三副本的raft group,单副本故障并不会影响服务的可用性,即使是主节点故障了剩余的两个节点也会快速选出主并对外提供读写服务。但是考虑到极端情况,假设同时出现两个副本故障呢? 这时只剩一个副本无法完成选主服务将完全不可用。根据墨菲定律,可能发生的一定会发生。服务的可用性一方面是稳定提供服务的能力,另一方面是故障时快速恢复的能力。那么假设出现这种故障的时候我们应该如何快速恢复服务的可用呢。
如果通过创建新的副本进行修复,新副本需要等到完成快照拷贝以后才能加入raft group进行选举,期间服务还是不可用的。那么我们可以通过强制将分片降为单副本模式,此时剩余的单个健康副本可以独自完成选主,后续再通过变更副本数的方式进行修复。
RaftLog 聚合提交
对于写入吞吐非常高的场景,可以通过牺牲一定的延时来提升写入吞吐,通过log聚合来减少请求放大。对于SSD盘,每一次写入都是4k刷盘,value比较小的时候会造成磁盘带宽的浪费。我们设置了每5ms或者每聚合4k进行批量提交。该参数可以根据业务场景进行动态配置修改。
异步刷盘
有些对于数据一致性要求不是非常高的场景,服务故障的时候允许部分数据丢失。对于该场景,可以关闭fsync通过操作系统进行异步刷盘。但是如果写入吞吐非常高导致page cache的大小超过了 vm.diry_ratio ,那么即便不是fsync也会导致io等待,该场景往往会导致io抖动。为了避免内核pdflush大量刷盘造成的io抖动,我们支持对raftlog进行异步刷盘。
透明多级存储,和缓存结合,自动冷热分离,通过将冷数据自动搬迁到kv降低内存使用成本。
新硬件场景接入,使用SPDK 进行IO提速,使用PMEM进行访问加速。
参考文献
[1] Bitcask A Log-Structured Hash Table for Fast Key/Value Data
[2] Lethe: A Tunable Delete-Aware LSM Engine
Ⅳ 什么是kv数据库
kv数据库是指Key-value数据库,是一种以键值对存储数据的一种数据库,类似java中的map。可以将整个数据库理解为一个大的map,每个键都会对应一个唯一的值。
Key-value数据库代表的有redis,Redis是一个Key-Value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)和zset(有序集合)。
另外redis是一种内存型的数据库,所以可以对外提供很好地读写操作,但是同样也暴露出内存占用高,数据持久化不易等问题。
(5)kv持久化数据库扩展阅读
key-value数据库的优点(以redis为例):
1、Redis将数据库完全保存在内存中,仅使用磁盘进行持久化。
2、相比于其他键值数据库,Redis有相对丰富的数据结构。
3、Redis可以将数据复制到任意数量的从机中。
5、快。每秒可以执行大约110000次设置(set)操作,每秒大约可执行81000次读取(get)操作.
6、支持丰富的数据类型。Redis有5种数据类型。
7、操作具有原子性。所有Redis操作都是原子操作,这确保了两个客户端并发访问,Redis服务器能接收更新的值。
8、多实用工具。Redis可用于多种用例,如:缓存,消息队列(支持发布和订阅),应用程序中的任何短期数据,如:session等。
Ⅵ 怎样操作leveldb数据库,实现增删改查
下文例子中演示了如何插入、获娶删除一条记录 LevelDB 简介 一、LevelDB入门 LevelDB是Google开源的持久化KV单机数据库,具有很高的随机写,顺序读/写性能,但是随机读的性能很一般,也就是说,LevelDB很适合应用在查询较少,而写很多的场景。
Ⅶ 什么是kv数据库
一个解决方案是使用键值(Key-Value)存储数据库,这是一种NoSQL(非
关系型数据库
)模型,其数据按照键值对的形式进行组织、索引和存储。KV存储非常适合不涉及过多数据关系业务关系的业务数据,同时能有效减少读写磁盘的次数,比
SQL数据库
存储拥有更好的读写性能。
Ⅷ 什么是kv数据库
kv数据库是指Key-value数据库,是一种以键值对存储数据的一种数据库,类似java中的map。可以将整个数据库理解为一个大的map,每个键都会对应一个唯一的值。
key-value分布式存储系统查询速度快、存放数据量大、支持高并发,非常适合通过主键进行查询,但不能进行复杂的条件查询。
如果辅以实时搜索引擎进行复杂条件检索、全文检索,就可以替代并发性能较低的MySQL等关系型数据库,达到高并发、高性能,节省几十倍服务器数 量的目的。以MemcacheDB、Tokyo Tyrant为代表的key-value分布式存储,在上万并发连接下,轻松地完成高速查询。
(8)kv持久化数据库扩展阅读:
数据库的安全直接关系到整个数据库系统的安全,其防护手段主要有以下八点:
1、使用正版数据库管理系统并及时安装相关补丁。
2、做好用户账户管理,禁用默认超级管理员账户或者为超级管理员账户设置复杂密码;为应用程序分别分配专用账户进行访问;设置用户登录时间及登录失败次数限制, 防止暴力破解用户密码。
3、分配用户访问权限时,坚持最小权限分配原则,并限制用户只能访问特定数据库,不能同时访问其他数据库。
4、修改数据库默认访问端口,使用防火墙屏蔽掉对 外开放的其他端口,禁止一切外部的端口探测行为。
5、对数据库内存储的重要数据、敏感数据进行加密存储,防止数据库备份或数据文件被盗而造成数据泄露。
6、设置好数据库的备份策略,保证数据库被破坏后能迅速恢复。
7、对数据库内的系统存储过程进行合理管理,禁用掉不必要的存储过程,防止利用存储过程进行数据库探测与攻击。
8、启用数据库审核功能,对数据库进行全面的事件跟踪和日志记录。
参考资料来源:
网络-Key-Value
网络-数据库
Ⅸ 开源高性能KV数据库
功能支持
使用说明
快速上手
重打开或创建一个数据库
注册当TTL超时删除事件通知
插入一条记录,(当重复Put同key时操作等同于更新内容操作)
设置一条已存在记录并8秒后超时自动删除
删除一条记录
性能
插入队列压力测试
300,0005865ns/op516B/op9allocs/op
取出队列压力测试
200,00014379ns/op1119B/op20allocs/op
KET VALUE 集合操作
import
重打开或创建一个数据库
注册当TTL超时删除事件通知
插入一条记录,(当重复Put同key时操作等同于更新内容操作)
插入一条记录并设置3秒后超时自动删除
设置一条已存在记录并8秒后超时自动删除
删除一条记录
操量操作(事务) Op为put时操作插入或更新,Op为del时操作删除
指定key取一条记录
返回全库的Key数据
返回所有K,V数据
按key开始位返回后续所有数据
以时间范围查询数据示例
匹配正则表达式为开头的数据
struct对象的相关操作
指定key取一条记录
返回所有记录
按key开始过滤返回
按key范围取数据
插入一条记录struct对象以json保存
指定key取一条记录
返回所有记录
MIX 设计是基于原KV库只有单维度存储方式,从而缺失了二维度的存储方式,所以MIX式库被设计出来
写入 raw
取出一个
查询指定表的字段是否存在
以raw读出表数据
写入及取出object
删除指定表的指定字段
删除整个表所有数据
##创建支持分组的kvdb
写入数据到分组
删除分组
消息队列 (FIFO)[先进先出]原则
import
重打开或创建一个队列数据库
推一个字符串到队列中
推一个对象到队列中
推一个bytes切片到队列中
推一批bytes切片到队列中
取出一条记录,取出成功后记录会被删除
提取一条记录,但不删除原记录
根据偏移量提取记录
更新一个队列原记录bytes类型
更新一个队列原记录字符串类型
更新一个队列原记录对象类型
import
重打开或创建一个分组队列数据库
以对象存储到队列中
以切片存储到队列中
删除指定分组
性能指标
开源地址:https://github.com/jacoblai/yiyidb
wiki地址:https://github.com/jacoblai/yiyidb/wiki