KV系列1 - LMDB

注: 本文仅为笔记,不怎么通顺和严谨。 KV系列属于一个大规模场景的必备品,而且通常很多公司会选择自研,一方面是各种不同产品均有局限,而大家的需求都有差别。另一方面社区已经有一些比较好的 building blocks,可以方便地进行组装和修改。最常见的情境是,分布式KV的开发 = 选一个底层kv存储 + 一个分布式协议。比如TiDB,ETCD等等。最近看 cloudflare 的 blog, 也是类似的场景。他们选的是 LMDB + 自研的分布式策略(称不上算法,比较简单),也很好的满足了自己的需求。 LMDB 全称是 Lightning Memory-Mapped Database, 使用内存映射文件,读写性能比较高。 LMDB的一些特性: 支持APPEND模式,提高写操作的性能 支持多进程/线程同时访问。这种场景下读性能可以随着实例数增加而线性提升。 单独写不block读, 读也不 block 写。 不需要 transcation log, 提高了写性能。 实现上来讲,利用内存映射是一大特色。通常的文件读取操作,通过read系统调用,要先把数据从硬盘 copy 到内核,然后再拷贝到用户空间。而 mmap, 不直接进行数据拷贝,而是在缺页中断时进行处理。而且是直接拷贝到用户态,所以会比 read 效率高些。另外,这种内存映射是只读的,也避免了程序错误破坏存储结构。写操作则是通过 write 系统调用完成,由系统来保证数据一致性。其他细节: 使用 B+ tree. LMDB只允许单个写,性能有所降低,但是不再需要WAL日志,以及其他种种并发控制的冲突及代价。 LMDB中,数据的基本操作单元是页,COW也是以页为单位。 如果写操作比较多,那么数据版本也会很多,旧数据会占用大量空间。LMDB会将旧的页插入到一棵B+tree当中,然后等没有事物再用到它之后就可以重复利用。这样省去了定期清理操作,但是无法保证数据可以恢复到任意时刻了。 数据访问可以直接返回内存指针,避免内存拷贝。 COW保证存储结构一直是合法的。系统崩溃不会导致数据库处于一个不一致的状态。最坏的情况只是丢失了一些未提交的数据。根据一些学界的研究,尚没有发现因为使用 LMDB 导致数据损坏的案例。 LMDB中事物的实现思路如下: Atom(A): LMDB中通过txn数据结构和cursor数据结构的控制,通过将脏页列表放入 dirtylist中,当txn进行提交时再一次性统一刷新到磁盘中或者abort时都不提交保证事务要不全成功、要不全失败。对于长事务,若页面spill到磁盘,因为COW技术,这些页面未与整棵B-Tree的rootpage产生关联,因此后续的事务还是不能访问到这些页面,同样保证了事务的原子性。 Consistency(C): 有如上的操作,保证其数据就是一致的,不存在因为多线程同时写数据导致数据产生错误的情况。 Isolation(I):事务隔离通过锁控制(MUTEX),LMDB支持的锁互斥是进程级别/线程级别,支持的隔离方式为锁表支持,读读之间不锁,写等待读完成之后开始,读等待写完成后开始. Duration(D):LMDB中,没有使用WAL、undo/redo log等技术来保证系统崩溃时数据库的可用性,其保证数据持续可用的技术是COW技术和只有一线程写技术。假如LMDB或者系统崩溃时,只有读操作,那么数据本来就没有发生变化,因此数据将不可能遭到破坏。假如崩溃时,有一个线程在进行写操作,则只需要判断最后的页面号与成功提交到数据库中的页面号是否一致,若不一致则说明写操作没有完成,则最后一个事务写失败,数据在最后一个成功的页面前的是正确的,后续的属于崩溃事务的,不能用,这样就保证了数据只要序列化到磁盘则一定可用,要不其就是还没有遵循ACI原则序列化到磁盘 总结来看,LMDB是一个极为优秀的产品。即使作者声称它主要是为了读场景而不是写场景,但实测的结果都不错。BUG少,稳定性强。 Cloudflare 的实践 Cloudflare 需要一个分布式的 KV Storage 来存储用户配置信息,当用户做了改动之后,能很快地分发到所有的数据中心。最开始用的是Kyoto Tycoon datastore, 在使用过程中发现了不少问题,最终切换到了 LMDB. ...

2020-11-26 · 1 分钟 · 197 字 · 涯余

笔记: Postgresql里的事务实现

虽然日常工作需要涉及到数据库的底层部分并不多,但Postgresql作为一个数据库实现的范本是很值得研究的。可以通过它的实现来探索很多通用的数据库设计以及系统设计的理念。本文主要关注于事物设计方面。 MVCC pg底层使用MVCC,修改数据时会直接创建新版本,而不是直接修改旧数据。这部分有一点需要注意的是在pg中,所有的语句都是在事物中执行的,不管是不是明确地用了BEGIN/COMMIT。 Transactions, tuples, and snapshots 先看一下 Transaction 的主要数据结构: typedef struct PGXACT { TransactionId xid; /* id of top-level transaction currently being * executed by this proc, if running and XID * is assigned; else InvalidTransactionId */ TransactionId xmin; /* minimal running XID as it was when we were * starting our xact, excluding LAZY VACUUM: * vacuum must not remove tuples deleted by * xid >= xmin ! */ ... } PGXACT; 事物以xid作为标识.pg针对它做了很多优化,仅在真正开始写数据的时候才分配xid,如果是只读的事物就完全不分配。 xmin表示当这个事物开始的时候仍然处在运行中的事物列表中最小的那个xid ...

2020-10-27 · 4 分钟 · 825 字 · 涯余