绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
Kafka is Database
2022-04-15 20:25:13

# Kafka is Database

## 前言

[Martin Kleppmann],《数据密集型应用系统设计》([Designing target="_blank" rel="nofollow noreferrer" style="color: inherit; text-decoration: none; cursor: pointer; border-bottom: 1px solid rgb(128, 128, 128);">一次演讲中],用了一个标题Is Kafka a Database,他的答案是:Yes

某种程度,BunnyRedis是这个思想的一个实践,因为BunnyRedis可以简化抽象为:

BunnyRedis = Redis + Kafka

所以,显然我是赞同这个思想的。那么我谈谈我对这个问题的理解。

## 分拆等式

我是学理工的,所以,对Kafka is Database,我的理解完全是一个公式

Log is Database, i.e., Log = Database
Kafka is for Log,i.e., Kafka = Log

所以,一个小学生都能通过上面的逻辑等式,得到下面的等式

Kafka = Database


## 什么是Log?

首先我们要了解什么是Log,以及它有什么特征。

Log具有下面的特性:

1. 它是尾部追加模式,即新的数据写在整个数据结构的尾部,即Append Mode
2. 它是不可改变的,即我们写了这些数据,我们就不指望再次修改它,即Immutable

再用简单的词来抽象上面两个特性,就是Append Only

那么,我们为什么需要这样一个数据结构,以及它有什么特点(好处和坏处)?

Log这个数据结构,可以用在内存,也可以用在磁盘,它具有如下的特点:

1. 从写的Big O看,它是O(1)的操作,不管是在内存上发生,还是在磁盘上发生
2. 从读的Big O看,它也是O(1)的操作,而且特别适合磁盘


### 先看写Write

内存上我们不说了,显而易见,是O(1)操作,而且,如果你用Java ArrayList,或C++的std::vector,它是内存紧凑的,对于CPU cache非常适宜。

磁盘要看两个,一个是HDD,磁力旋转的存储,一个是SSD,芯片并发的存储。

对于HDD,Log非常适合,因为HDD,一个大的cost就是移动磁头重新定位,我们称之为seek time,一般是几个ms的消耗。而如果用Log,磁头无需移动。

[有兴趣的同学可以看一下视频,HDD内部是如何工作的]

对于SSD,情况要复杂很多。SSD的效能的有效利用,来自内部电子单元的并发度,从数据上看,给与SSD越多的数据(以及嫡值合适),它的效果越好。从这个角度看,Log并没有太大的优势,实际测试也是如此。

[有兴趣的同学可以看一下演示,SSD内部是如何工作的]

但如果较真的话,SSD还是有稍微的不同。因为SSD内部就像一个电脑,它有内存高速缓存,它有类似CPU的逻辑芯片和一些算法(比如:GC算法),所以如果Log适合计算机的某些场景,理论上,它也适合SSD这个小型计算机。但Anyway,这个不是主要因素,主要因素还是上面那点,并发度,或者说,足够的数据供给。

SSD真正的麻烦在于写放大Write Amplification,因为SSD内部也是page为单位的,小4K,而且SSD的写不是overwrite,即并不是在原来的位置替换(in-place update),而是一个COW,copy on write,读出旧的page数据,在内存修改好了后,然后在一个新的page,写入修改的数据。如果涉及block的擦除(erase),则写放大更大(block一般以M为单位)。

所以,如果写入不是4K对齐和4K整倍数,比如,我们希望对于某个文件的某个位置修改1个字节,那么在SSD里,是实际需要写入4K大小的一个新的block,这就会有4000倍的写放大。

而对于Log的写,可以减少写放大的伤害。因为不管是用机器的内存,还是用SSD内部的缓存,我们都可以聚集足够的新数据(比如4K),再写入一个必须用到的新的4K block,我们就可以降低甚至避免写放大。这是因为Log的特性是:Append-Only,即没有overwrite的需求。

### 再看读Read

对于读,如果正好需求是一段数据(比如:文件里的1M连续数据),也是O(1)操作。

内存不用说了,我们来看磁盘。

对于HDD,同样不需要移动磁头,或者说,只要一次磁头定位即可。这就省了多次seek time这个大cost。

对于SSD,它效能的根本,仍是并发性。所以,如果从Log连续读1M的数据,还是随机读总共1M数据,差别不大,只要读取的请求足够多。

但是和写放大类似,假设我们读4K数据正好在一个block,和读4000个1字节分布在文件不同的位置(比如分布在4000个block上),显然读1个block的效能要高。由于Log一般是把近相关的一些数据写到同一个block里,所以Log还是有利于这个场景,至少可以保证读的blocK数足够少。

### Log不利的地方

任何数据结构都不是的,都有trade off。

对于Log不利的地方在于:如果我们希望通过一个key找到对应的数据,我们就不是O(1)操作,而是O(N)操作,不管这个Log是在内存上,还是在磁盘上。

所以,对于Log,我们想实现key的检索,我们需要添加其他数据结构,比如Hash或Tree。用Hash的O(1),以及Tree的O(lg(N))来避免Log的O(N)的key检索。

## 什么是Database?

这个题目太大,Martin Cleppmann的答案是:ACID

我的想法更简单,只要能放数据(写入write),然后能再次获得数据(读取read),那么,它就是Database。从这个意义上讲,一个csv文件,都是一个数据库,而且csv文件是没有索引的。

也许有人有不同的想法,不过没有关系,大家不过是广义或狭义之分,Database大致要提供哪些基本功能,其实我们大家都清楚。

## Log is Database

那么我们如何理解Log is Database这句话?

### 单机系统

我们来看一下单进程的MySQL。

MySQL有两个的Log,一个是redo log,一个是binlog。

#### redo log

redo log,是数据库将修改的结果,记录在一个Log文件上。它的好处在于,避免了对于整个dataset的修改和即时落盘(fsync)的要求。

假设我们数据库很大,有1TB,但一个Transaction修改的数据一般并不大,我们假设每个Transaction只修改了1K数据,每个Transaction修改的位置,对于整个1TB dataset而言,是随机的。

如果我们每次修改1K字节,我们都要落盘的话,这个同步fsync操作,是非常大的cost,对于HDD,它要不停地移动磁头,对于SSD,它有写放大。

而且,如果每次修改都先落盘,然后再给客户确认消息的话,时间Latency很长,会大大降低整个数据库的效能Throughput。

所以,一个优化的方案如下:

我们对于整个数据集的改变,先将要改的数据只修改对应的内存页page,并不马上刷盘。晚些时候再在后台,通过集中的方式写盘(比如15分钟一次刷盘,将修改的内存页page,即dirt page,写到对应的磁盘页page)。

同时,我们对于修改的数据,写到redo log里,并快速地刷盘(比如:1秒钟1次刷盘),这样,万一掉电,损失的数据较小。

这样的好处在于:

1. 如果同一内存页在后台刷盘前,被修改了两次,那么我们就省了一次刷盘

2. 对于HDD,如果两次修改的两个页,在磁盘上位置上很接近,我们可以省一次磁盘定位的cost

3. 对于SSD,我们可以将一次一次零碎的写盘请求,集中大批量发送,从而提高SSD的并发度

4. 同时,异步写盘降低了Latency和提高了Throughput

所以,redo log是个冗余。如果不发生掉电事件,它可以不存在。但为了避免掉电损失全部的数据,以及顾虑每次都对page进行及时同步式地刷盘所带来的性能惩罚,我们做了个优化,利用Log对于磁盘的友好,来提高整个系统的性能Throughput同时将数据丢失data loss降低到较小。


#### binlog

binlog和redo log,有点类似。但redo log一般循环使用,所以,redo log只保持很少一段时间的操作记录(这样就节省磁盘),而binlog是将很长一段时间(你可以认为是历史所有的操作记录)保存下来。

binlog是为另外一个目的服务的:它不是为当前这个机器上的数据库服务,而是为其他数据库服务的。

对于MySQL,它可以组建成master/slave的replication模式,那么这个slave,要形成和master一模一样的数据,它就需要binlog这个Log。

类似地,我们可以在binlog里记录SQL语句,这样binlog不仅可以让MySQL再创建一个同样数据的slave,而且可以让其他类型的数据库(比如:PostgreSQL),也创建一个同样数据的数据库。

所以,从这个意义上讲:binlog就是一个数据库的数据。


### 集群系统

#### Aurora范例

随着互联网和云时代的到来,我们不满足一台数据库系统,我们希望数据库上云,而且是集群的。

一个比较经典的案例是Amazon的Aurora,它基于MySQL的底层,实现了云上的集群化,而且成功实践了"日志即数据库"(Log is Database)这个口号(slogan)。

下面两个架构图都是源自AWS的论文



从上面两个图我们可以看出,Aurora在实现集群方案时,充分利用了Log

1. 计算和存储分离的核心,是Log

2. 多机集群同步的核心,还是Log,不管这个集群是计算集群,还是存储集群


#### 为什么Log适合集群

除了上面分析的Log的那些好处,我们看看集群还有什么特殊要求:

集群还有重大的要求,两个关键点:

1. 希望所有的data,在每个机器上都是一致的

2. 希望集群的任意一个组件,都不要只有一个

我们来看看Log,是如何适用这两个需求的

Log是Append-Only的,也就意味,我们不会对历史做出修改。不改的东西,非常容易保证一致性。

Log是高效的,不管是对内存,还是磁盘,而且特别适合磁盘。所以,我们可以让集群里的一个而且只有一个机器负责决定,如何wrie这个Log,这样,我们不仅高效,而且一致。

我们希望数据是多份,不要只有一个点才拥有数据,因此,我们可以让Log进行多机拷贝,从而避免单点的风险。


## Kafka is for Log

地球人(后端程序员)都知道,Kafka是为Stream服务的。而Stream,从其内部数据结构看,就是Log。

Kafka很庞大,我也无力对整个系统进行剖析,我只是做几个简单的总结:

1. Kafka内部的数据结构就是Log,而且Kafka只做好一件事,只为这个Log服务,因此高效

2. Kafka是分布式的(Distributed),因此,committed data会在所有集群的机器(node)上都有同样一份拷贝

3. 尽管Kafka只有一个master(或者叫leader),而且这个master可能变动,而且可能出现brain split现象(某个时刻有两个机器都认为自己是master),但是,它通过集群所有机器的共识(consensus),让committed data保值一致

4. Kafka这个共识(consensus),并不是通过Raft这种共识协议达成的,因此它还需要依赖Zookeeper这个底层来保证meta data的一致(Zookeeper内部的ZAB协议,类似Raft的基本原理,大道至同),这是个非常聪明的做法。因为Raft这种共识协议,并不高效(因为Raft做了两件事,即有Log,也有state machine,而且Raft必须依赖磁盘,而Kafka理论上可以不要磁盘)

如果想要详细了解这个不同,请参考我的另外一个文章:

分布式思考:少就是多,多就是少


## BunnyRedis向Kafka学习

不光BunnyRedis简单抽象为: Redis + Kafka

更重要的是:BunnyRedis依赖Kafka,从而实现了**Half Master/Master**这种架构,详细可参考:

1. [Redis的一致性问题](https://github.com/szstonelee/bunnyredis/wiki/Why-redis-consistency-problem)

2. [BunnyRedis的一致性](https://github.com/szstonelee/bunnyredis/wiki/BunnyRedis-consistency)

3. [BunnyRedis是什么](https://github.com/szstonelee/bunnyredis/wiki/Home)

4. [从Raft角度看Half Master/Master(两层解耦)](https://zhuanlan.zhihu.com/p/407603154)


分享好友

分享这个小栈给你的朋友们,一起进步吧。

分布式思考和实践
创建时间:2022-04-14 14:15:30
关于分布式在数据库领域的应用的一些思考以及一些程序应用的开发
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

栈主、嘉宾

查看更多
  • szstonelee
    栈主

小栈成员

查看更多
  • miemieMIA
  • LCR_
  • jinchuan
  • MrSun
戳我,来吐槽~