几天前淘宝量子恒道在博客上分析了HBase的Cache机制,本篇文章,结合LevelDB 1.7.0版本的源码,分析下LevelDB的Cache机制。

  • 概述

LevelDB是Google开源的持久化KV单机存储引擎,据称是HBase的鼻祖Bigtable的重要组件tablet的开源实现。针对存储面对的普遍随机IO问题,LevelDB采用merge-dump的方式,将逻辑场景的随机写请求转换成顺序写log和写memtable的操作,由后台线程根据策略将memtable持久化成分层的sstable。针对读请求,LevelDB会首先查找内存中的memtable和imm(不可变的memtable),然后逐层查找sstable。

为了加快查找速度,LevelDB在内存中采用Cache的方式,在sstable中采用bloom filter的方式,尽最大可能减少随机读操作。

LevelDB的Cache分为两种,分别是table cache和block cache。table cache缓存的是sstable的索引数据,类似于文件系统中对inode的缓存;block cache是缓存的block数据,block是sstable文件内组织数据的单位,也是从持久化存储中读取和写入的单位;由于sstable是按照key有序分布的,因此一个block内的数据也是按照key紧邻排布的(有序依照使用者传入的比较函数,默认按照字典序),类似于Linux中的page cache。

block默认大小为4k,由LevelDB调用open函数时传入的options.block_size参数指定;LevelDB的代码中限制的block最小大小为1k,最大大小为4M。对于频繁做scan操作的应用,可适当调大此参数,对大量小value随机读取的应用,也可尝试调小该参数;

block cache默认实现是一个8M大小的LRU cache,为了减少锁开销,该LRU cache还分成了16个shard。此参数由options.block_cache设定,即可改变缓存大小,也可根据自己的应用需求,提供新的缓存策略。注意,此处的大小是未压缩的block大小。针对大块文件的读写遍历等需求,为了避免读入的块把之前的热数据都淘汰掉,可以在ReadOptions里设置哪些读取不需要进cache,如以下代码所示:

  leveldb::ReadOptions options;
options.fill_cache = false;
leveldb::Iterator* it = db->NewIterator(options);
for (it->SeekToFirst(); it->Valid(); it->Next()) {
...
}

table cache默认大小是1000,注意此处缓存的是1000个sstable文件的索引信息,而不是1000个字节。table cache的大小由options.max_open_files确定,其最小值为20-10,最大值为50000-10。

  • 源码分析

1.默认的Cache是一个分Shard的LRU Cache,代码片段如下:

leveldb-1.7.0/util/cache.cc

136 class LRUCache {
137 public:
138 LRUCache();
139 ~LRUCache();
140
141 // Separate from constructor so caller can easily make an array of LRUCache
142 void SetCapacity(size_t capacity) { capacity_ = capacity; }
143
144 // Like Cache methods, but with an extra "hash" parameter.
145 Cache::Handle* Insert(const Slice& key, uint32_t hash,
146 void* value, size_t charge,
147 void (*deleter)(const Slice& key, void* value));
148 Cache::Handle* Lookup(const Slice& key, uint32_t hash);
149 void Release(Cache::Handle* handle);
150 void Erase(const Slice& key, uint32_t hash);
151
152 private:
153 void LRU_Remove(LRUHandle* e);
154 void LRU_Append(LRUHandle* e);
155 void Unref(LRUHandle* e);
156
157 // Initialized before use.
158 size_t capacity_;
159
160 // mutex_ protects the following state.
161 port::Mutex mutex_;
162 size_t usage_;
163 uint64_t last_id_;
164
165 // Dummy head of LRU list.
166 // lru.prev is newest entry, lru.next is oldest entry.
167 LRUHandle lru_;
168
169 HandleTable table_;
170 };

1) capacity_是Cache大小,其单位可以自行指定(如table cache,一个sstable文件的索引信息是一个单位,而block cache,一个byte是一个单位);

2)lru_是一个双向链表,如注释说明,lru_.prev是最新被访问的条目,lru_.next是最老被访问的条目。在访问cache中的一个数据时,会顺次执行LRU_Remove和LRU_Append函数,将条目移到lru_.prev的位置;

3)table_是LevelDB自己实现的一个hash_map,其实现也在cache.cc文件中,据作者说,在特定的编译环境下性能更优,如与g++ 4.4.3内置的hashtable相比,随机读性能可以提升5%;

4)insert操作会根据capacity_的大小,顺着lru_.next讲老的条目Release掉;

2. block 的读取逻辑,代码片段如下:

leveldb-1.7.0/table/table.cc

154 Iterator* Table::BlockReader(void* arg,
155 const ReadOptions& options,
156 const Slice& index_value) {
157 Table* table = reinterpret_cast<Table*>(arg);
158 Cache* block_cache = table->rep_->options.block_cache;
159 Block* block = NULL;
160 Cache::Handle* cache_handle = NULL;
161
162 BlockHandle handle;
......
168 if (s.ok()) {
169 BlockContents contents;
170 if (block_cache != NULL) {
......
175 cache_handle = block_cache->Lookup(key);
176 if (cache_handle != NULL) {
177 block = reinterpret_cast<Block*>(block_cache->Value(cache_handle));
178 } else {
179 s = ReadBlock(table->rep_->file, options, handle, &contents);
180 if (s.ok()) {
181 block = new Block(contents);
182 if (contents.cachable && options.fill_cache) {
183 cache_handle = block_cache->Insert(
184 key, block, block->size(), &DeleteCachedBlock);
185 }
186 }
187 }
188 } else {
189 s = ReadBlock(table->rep_->file, options, handle, &contents);
190 if (s.ok()) {
191 block = new Block(contents);
192 }
193 }
194 }
195
196 Iterator* iter;
197 if (block != NULL) {
198 iter = block->NewIterator(table->rep_->options.comparator);
199 if (cache_handle == NULL) {
200 iter->RegisterCleanup(&DeleteBlock, block, NULL);
201 } else {
202 iter->RegisterCleanup(&ReleaseBlock, block_cache, cache_handle);
203 }
204 } else {
205 iter = NewErrorIterator(s);
206 }
207 return iter;
208 }

1) 首先从block cache中查找block,如果找不到,直接从持久化存储中读取,获取到一个新的block,插入block cache;

2) 对于查到的block,返回对应的迭代器(LevelDB中,所有的get\merge操作均是抽象成iterator实现的);

3)如果仔细读代码,iter->RegisterCleanup函数实现会有点绕,这个函数在iter析构时被调用,执行注册的ReleaseBlock,ReleaseBlock调用cache_handle的Unref方法,对cache中缓存的block减少一个引用计数;cache执行insert函数时,会给所有的LRUHandle的引用计数设成2,其中1用于LRUCache自身,在执行cache的Release操作时被Unref,从而真正释放。

3.table cache的读取逻辑,代码片段如下:

leveldb-1.7.0/db/table_cache.cc

 45 Status TableCache::FindTable(uint64_t file_number, uint64_t file_size,
46 Cache::Handle** handle) {
47 Status s;
48 char buf[sizeof(file_number)];
49 EncodeFixed64(buf, file_number);
50 Slice key(buf, sizeof(buf));
51 *handle = cache_->Lookup(key);
52 if (*handle == NULL) {
53 std::string fname = TableFileName(dbname_, file_number);
54 RandomAccessFile* file = NULL;
55 Table* table = NULL;
56 s = env_->NewRandomAccessFile(fname, &file);
57 if (s.ok()) {
58 s = Table::Open(*options_, file, file_size, &table);
59 }
60
61 if (!s.ok()) {
62 assert(table == NULL);
63 delete file;
64 // We do not cache error results so that if the error is transient,
65 // or somebody repairs the file, we recover automatically.
66 } else {
67 TableAndFile* tf = new TableAndFile;
68 tf->file = file;
69 tf->table = table;
70 *handle = cache_->Insert(key, tf, 1, &DeleteEntry);
71 }
72 }
73 return s;
74 }

和block cache类似,首先查找cache,如果找不到,直接从硬盘中读取。注意代码70行Insert函数的第3个参数,1表示每个sstable的索引信息在cache总占用1个单位的capacity_。其他内容不再赘述。

  • 总结

LevelDB是Jeff Dean, Sanjay Ghemawat的作品,实在是值得大家仔细品读。Cache机制非常简单,相信大家通过这篇文章能够非常清楚的了解其cache实现,其思路其实和文件系统的cache是一样的。另外,淘宝已经在Tair线上环境中大量使用了LevelDB存储引擎,推荐那岩写的《LevelDB实现解析》,35页的文档,结合着读代码,会对理解代码,有非常大的帮助。

LevelDB Cache实现机制分析的更多相关文章

  1. LevelDB Cache机制

    [LevelDB Cache机制] 对于levelDb来说,读取操作如果没有在内存的memtable中找到记录,要多次进行磁盘访问操作.假设最优情况,即第一次就在level 0中最新的文件中找到了这个 ...

  2. Java 动态代理机制分析及扩展

    Java 动态代理机制分析及扩展,第 1 部分 王 忠平, 软件工程师, IBM 何 平, 软件工程师, IBM 简介: 本文通过分析 Java 动态代理的机制和特点,解读动态代理类的源代码,并且模拟 ...

  3. memcache redundancy机制分析及思考

    设计和开发可以掌控客户端的分布式服务端程序是件幸事,可以把很多事情交给客户端来做,而且可以做的很优雅.角色决定命运,在互联网架构中,web server必须冲锋在前,注定要在多浏览器版本以及协议兼容性 ...

  4. Java 动态代理机制分析及扩展,第 1 部分

    Java 动态代理机制分析及扩展,第 1 部分 http://www.ibm.com/developerworks/cn/java/j-lo-proxy1/ 本文通过分析 Java 动态代理的机制和特 ...

  5. Linux mips64r2 PCI中断路由机制分析

    Linux mips64r2 PCI中断路由机制分析 本文主要分析mips64r2 PCI设备中断路由原理和irq号分配实现方法,并尝试回答如下问题: PCI设备驱动中断注册(request_irq) ...

  6. IOS Table中Cell的重用reuse机制分析

    IOS Table中Cell的重用reuse机制分析 技术交流新QQ群:414971585 创建UITableViewController子类的实例后,IDE生成的代码中有如下段落: - (UITab ...

  7. 您还有心跳吗?超时机制分析(java)

    注:本人是原作者,首发于并发编程网(您还有心跳吗?超时机制分析),此文结合那里的留言作了一些修改. 问题描述 在C/S模式中,有时我们会长时间保持一个连接,以避免频繁地建立连接,但同时,一般会有一个超 ...

  8. Java 类反射机制分析

    Java 类反射机制分析 一.反射的概念及在Java中的类反射 反射主要是指程序可以访问.检测和修改它本身状态或行为的一种能力.在计算机科学领域,反射是一类应用,它们能够自描述和自控制.这类应用通过某 ...

  9. Linux 内核的文件 Cache 管理机制介绍

    Linux 内核的文件 Cache 管理机制介绍 http://www.ibm.com/developerworks/cn/linux/l-cache/ 1 前言 自从诞生以来,Linux 就被不断完 ...

随机推荐

  1. Memcached set 命令

    Memcached set 命令用于将 value(数据值) 存储在指定的 key(键) 中. 如果set的key已经存在,该命令可以更新该key所对应的原来的数据,也就是实现更新的作用. 语法: s ...

  2. 使用Entity Framework时遇到的各种问题总结

    在这里记录一下之前使用Entity Framework(4.3.1版本)遇到的问题. 更新没有设置主键的表 在默认情况下,EF不能对一个没有主键的表进行更新.插入和删除的动作.用xml方式查看edmx ...

  3. ImageView显示图像控件

    ImageView显示图像控件 一.简介 1. 2. ImageView,图像视图,直接继承自View类,它的主要功能是用于显示图片,实际上它不仅仅可以用来显示图片,任何Drawable对象都可以使用 ...

  4. python学习笔记(arange函数与linspace函数)

    上一篇提及到matplotlib模块.其中会涉及到numpy模块科学计数 这里总结两个数组生成函数 arange 与 linspace: #!/usr/bin/env python # -*- cod ...

  5. Shell 自定义函数

    语法: function fname() { 程序段} 例子: #!/bin/bash ## 定义函数,分子除以分母,算利润.占有率等## 参数1:分子## 参数2:分母 function divfu ...

  6. NLP(二)_汉语言分词技术-最大匹配法

    前述 词是自然语言中最小的有意义的构成单位.汉语文本是基于单字的文本,汉语的书面表达方式以汉字作为最小单元,词与词之间没有明显的界限标志,因此,分词是汉语文本分析处理中首先要解决的问题之一. 分词可能 ...

  7. 最大流EK算法/DINIC算法学习

    之前一直觉得很难,没学过网络流,毕竟是基础知识现在重新来看. 定义一下网络流问题,就是在一幅有向图中,每条边有两个属性,一个是cap表示容量,一个是flow 表示流过的流量.我们要求解的问题就是从S点 ...

  8. IOS-日期处理

    主要有以下类: NSDate -- 表示一个绝对的时间点NSTimeZone -- 时区信息NSLocale -- 本地化信息NSDateComponents -- 一个封装了具体年月日.时秒分.周. ...

  9. 条款11:记得在operator=中处理自赋值的情况。

    本来的版本是这样的: Widget & Widget::operator=(Widget rhs) { delete pb;//这里可能直接将rhs的pb删除了 pb = new (*rhs. ...

  10. ethtool常见命令使用方法

    查看网卡信息:ethtool DEVNAME Settings for eth6: Supported ports: [ FIBRE ] #可以看出网卡类型:光口或电口 Supported link ...