数据库系统设计概述
世界上只有两种开发人员,一种使用数据库系统的,一种开发数据库系统的。
数据是系统最重要的信息。大部分系统都是对数据的管理。应用系统通过数据模型来构建现实世界,通过算法操作对象或数据结构,来改变数据模型的状态。数据被组织在操作系统文件中,我们通过数据系统来组织,查询,搜索,处理数据。
本文将从数据库的发展、数据库的分类、常见数据库架构,数据库常见概念和技术等方面探讨这个我们接触最多的底层系统,并通过穿插不同数据库的实现原理,来了解数据库的具体实现。
本文分为五个大章节。探古溯源,从数据库的诞生,发展,现状和展望来了解数据库存在的意义,以及数据库设计的历史与现实原因。百家争鸣,本节从不同分类方式,讲解一些不同的数据库系统实现,有助于拓展我们的视野,在技术选型时可以作为参考(底层数据库系统的选型对整个系统的架构实在太重要了)。承上启下,本节是整篇文章的中间章节,前两章以兴趣点,纯理论展开,在本节中将对前两章做一个总结,有了前两章知识,我们已经可以去选择适合项目需求的数据库系统,对那些想更深入了解底层存储的同学也可以选择自己感兴趣的数据库类型和方案找到相应的实现,从而进入下一步学习。下面两章将讲解更多具体的技术点。知行合一,这一章节将讲解数据库的实现,分析一些数据库架构,分布式问题和解决方案,透析具体的数据库常见的技术点。
针对不同兴趣,大家可以按需取之,跳过不感兴趣的,看想关注的点。
一、探古溯源
疑今者察之古,不知来者视之往。——《管子》
数据库管理系统允许人员组织,存储和从计算机检索数据。在计算机的早期,使用“打孔卡”用于输入,输出和数据存储。打孔卡提供了一种快速的数据输入和检索方法。数据库在计算机的最新发展中起了非常重要的作用。第一批计算机程序是在 1950 年代初期开发的,几乎完全专注于编码语言和算法。当时,计算机基本上是大型计算器,数据(名称,电话号码)被认为是处理信息的残余物。当计算机开始商业化后,数据的重要性开始越来越被人重视。
题外话:穿越时间——笔者去了解一个东西,总喜欢追根溯源,从时间的起点,或从逻辑的深处开始探索。一个东西的逻辑原点往往是纯粹的简单的,之后随时间发展和广延的展开会逐渐复杂起来。所以从头开始了解一个东西,往往更容易理解。比如我们看一个系统的源码,可以从该系统的 1.0.0 版本开始,可以从这个系统最初想要解决的问题开始。
计算机数据库始于 1960 年代。此十年中,有两种流行的数据模型:称为 CODASYL
的网络模型和称为 IMS 的层次模型。SABER
系统被证明是商业上成功的一种数据库系统,该系统被 IBM 用来帮助美国航空管理其预订数据。
1970 年,大神 EF Codd 发表了一篇重要的论文:《大型共享数据库的数据关系模型》,提出了使用关系数据库模型的建议,他的想法改变了人们对数据库的看法。在他的模型中,数据库的架构或逻辑组织与物理信息存储断开连接,这成为数据库系统的标准原理。之后 UBC 开发了 Ingres 和在 IBM 开发了 SystemR。Ingres 使用一种称为 QUEL
的查询语言,引导而诞生了 Ingres Corp
,MS SQL Server
,Sybase
,PACE
和 Britton-Lee
之类的系统。另一方面,System R
使用 SEQUEL
查询语言,它有助于 SQL / DS
,DB2
,Allbase
,Oracle
和 Non-Stop SQL
的开发。关系数据库管理系统(RDBMS)已经成为公认的术语。
1976 年 P. Chen 提出了一个新的数据库模型,称为 Entity-Relationship
,即 ER
。该模型使设计人员可以专注于数据应用程序,而不是逻辑表结构。1980 年结构化查询语言或 SQL 成为标准查询语言。
RDBM系统
是存储和处理结构化数据的有效方法。然而,随着互联网的快速发展,“非结构化”数据(视频,照片,音乐等)变得更加普遍。非结构化数据既是非关系数据,又是无模式数据,而关系数据库管理系统根本就没有设计用于处理此类数据。21 世纪后,NoSql
模型进入人们的视野,NoSql 的出现是对互联网以及对更快的速度和对非结构化数据的处理需求的一种回应。一般而言,由于 NoSQL 数据库的速度和灵活性,它们在某些用例中比关系数据库更可取的。NoSQL模型
是非关系型的,并且采用“分布式”数据库系统。这个非关系系统速度很快,使用临时组织数据的方法,并且处理大量不同类型的数据。一般而言,NoSQL 相对于 RDBMS 系统有如下优势:
- 更高的可扩展性
- 分布式计算系统
- 低成本
- 灵活的架构
- 可以处理非结构化和半结构化数据
- 没有复杂的关系
在数据库的发展历程中,虽然只经历了短短半个世纪,却诞生了一批优秀的数据库系统,SystemR
、Postgresql
、Mysql
、DB2
、Oracle
、MongoDB
、HBase
、Neo4j
、Elasticsearch
等等,都在软件的发展中发挥了重要的。
二、百家争鸣
现在春天来了嘛,一百种花都让它开放,不要只让几种花开放,还有几种花不让它开放,这就叫百花齐放。—— 毛泽东
迄今为止,业界诞生的数据系统数不胜数。如果你打开DB-Engines 网站,可以看到几百个功能定位不同的数据库系统。查看DB-Engines
的分类排名,可以看出DB-Engines
将如此众多的系统大致分为以下几类(网址):
Willian Blair 在《Database Software Market:The Long-Awaited Shake-up》一文中以以下维度为数据库系统做了一个细致的分类:关系型/非关系型、操作型/分析型。
上图中的纵轴分类为 Relational Database
(关系型数据库,RDBMS)和 Nonrelational Database
(非关系型数据库,NoSQL),横轴的分类为 Operational(操作型,即 OLTP)和 Analytical(分析型,即 OLAP)。
非关系型的分类是一个比较笼统的划分,主要是针对传统关系型来区分的,与传统关系型系统模型不一致的都划分到了非关系型中。
非关系型(NoSQL)可以再进一步划分:Key-Value 型、列存储型、文档型、图数据库等。
- 文档存储:MongoDB、Elasticsearch、Amazon DocumentDB、Azure Cosmos DB 等。
- Key-Value 存储:Redis Labs、Oracle Berkeley DB、Amazon DynamoDB、Aerospike、LevelDB 等。
- 图数据库:Neo4j 等。
- 时序数据库:InfluxDB、Timescale 等。
- WideCloumn:DataStax、Cassandra、Apache HBase 和 Bigtable 等。
关系模型
关系型模型是大多数开发人员接触最早,接触最多的数据库模型。它基于集合理论,是最经典的数据库模式。关系型数据库采用行和列的二维表来建模数据。它适合于提前知道数据模型,并且数据模型比较固定,发生变化比较小,对查询比较灵活的场景,你只需要将数据以行和列的方式存储,在查询的时候以不同的需要组合数据。关系型不适合数据层次较多,记录与记录之间关联较多的场景,这种场景往往造成查询复杂度上升,查询性能下降。
关系型数据库主要用于大多数商业数据处理,其大多数是事务处理(如 ERP 系统、银行交易、航空公司订票、销售系统、金融财务管理系统等)和批处理场景(如客户发票、工资单、报告等)。
20 世纪 70 年代至今,关系型数据库经久不衰,其简洁的数据模型和经典的 SQL 查询语句支撑了当前大部分互联网系统,在线论坛、社交网络、电子商务等等,各式各样的系统背后,都隐藏着一个强大的关系数据库。
关系型数据库用的比较多的除了 Oracle
、Sql Server
等商业数据库外,就是 Mysql 了
,另外本人比较喜欢和推崇是 Postgresql
,被称为世界上功能最强大的开源数据库。
分析的世界
联机分析处理(Online analytical processing),简称OLAP,OLAP 是相对与传统的OLTP(联机事务处理,Online Transaction Processing)系统而言的,OLTP 是传统的关系型数据库的主要应用,侧重于基本的、日常的交互式的事务处理,例如银行交易。OLAP 是数据仓库系统的主要应用,支持复杂的分析操作,侧重分析决策支持,并且提供直观易懂的查询结果。OLAP 工具让用户能够从多个角度交互地分析多维数据。OLAP 由三个基本的分析操作组成:上卷(roll-up)、钻取(drill-down)、切片(slicing)和切块(dicing)。上卷涉及可以在一个或多个维度中累积和计算的数据的聚合。
OLAP 利于大数据量,数据更新少,经常使用大量数据做聚合统计的场景。OLTP 适合数据量小,频繁操作更新数据的场景。
OLAP 主要应用于商业智能、风控分析、智能报表等业务场景。
分析和事务是两个世界。在分析需求不大的时候,很多团队直接使用业务事务数据库做分析使用,这只能支持小数据量、分析需求变化不大,弱分析的场景。真正的数据分析场景,往往使用单独的数据仓库
。在不影响业务库的情况下,实时或周期批量地从中提取数据,转换成对分析友好的数据模式,执行必要的清理和转换,然后加载到数据仓库中。将数据导入仓库的过程称为提取-转换-加载
(Extract-Transform-Load, ETL)。
OLTP和OLAP没有明确的边界,它们的一些典型特性如下所示:
OLTP | OLAP | |
---|---|---|
用户 | 操作人员,底层管理人员 | 决策人员,高级管理人员 |
功能 | 日常操作处理 | 分析决策 |
DB 设计 | 面向应用 | 面向主题 |
数据 | 当前的,新的,细节的,二维的,分立的 | 历史的,聚集的,多维集成的,统一的 |
存取 | 读写数十上百条数据 | 读百万级数据 |
读特征 | 基于键,返回少量数据 | 基于大量数据汇总 |
写特征 | 随机访问,低延迟 | 批量或数据流 |
DB 大小 | 100MB~~GB | 100GB~~TB |
时间要求 | 实时性 | 对时间的要求不严格 |
主要应用 | 数据库 | 数据仓库 |
业界有许多优秀的开源的 OLAP 系统,比如:
- Druid:Metamarkets 公司开发的一个用于大数据实时处理的开源分布式系统。目前已经成为 Apache 的开源项目。官网 了解
- Kylin:Apache Kylin™ 是一个开源的、分布式的分析型数据仓库,提供 Hadoop/Spark 之上的 SQL 查询接口及多维分析(OLAP)能力以支持超大规模数据,最初由 eBay 开发并贡献至开源社区。它能在亚秒内查询巨大的表。官网
- Presto:Presto 是一个对 PB 级数据运行交互式分析的开源分布式 SQL 查询引擎。官网
- ClickHouse:ClickHouse 是由号称“俄罗斯 Google”的 Yandex 开发的一个列存储的 OLAP 系统。官网
列式存储
传统 OLTP 数据库通常采用行式存储
。以下图为例,所有的列依次排列构成一行,以行为单位存储,再配合以 B+ 树或 SS-Table 作为索引,就能快速通过主键找到相应的行数据。
行存储适用于 OLTP 场景,OLTP 的大多数操作都是以实体(Entity)为单位,即对每条记录的增删改查,因此将一行数据在物理上放在相邻的位置更利于操作,也更利于特定的优化。
在 OLAP 场景中,极少单独操作单条记录的情况。OLAP 分析往往针对大量的数据集,在大量的数据集的基础上对特定的列做分组、过滤、聚合操作。因此在物理上将每列数据放在相邻的位置。
这样如果针对某一列做分析聚合,只需要找到相应列的文件,或数据块的位置,比如,要计算上图数据的平均 Age,只需要获取 Age 列的数据集即可。但是,面向行的存储引擎仍然需要将所有行从磁盘加载到内存中、解析它们,并过滤出不符合所需条件的行。这可能需要很长的时间。
基于列模式的存储,天然就会具备以下几个优点:
-
自动索引
因为基于列存储,所以每一列本身就相当于索引。所以在做一些需要索引的操作时,就不需要额外的数据结构来为此列创建合适的索引。
-
利于数据压缩
利于压缩有两个原因。一来你会发现大部分列数据基数其实是重复的,拿上面的数据来说,因为同一个 author 会发表多篇博客,所以 author 列出现的所有值的基数肯定是小于博客数量的,因此在 author 列的存储上其实是不需要存储博客数量这么大的数据量的;二来相同的列数据类型一致,这样利于数据结构填充的优化和压缩,而且对于数字列这种数据类型可以采取更多有利的算法去压缩存储。
列式存储的概念其实很早就有,只是应时代所需,列式存储在近几年才火热起来,一时涌现了很多优秀的列式存储数据库,甚至很多之前的行存储系统,也有了列式存储的能力。
- Hbase:一个分布式的、面向列的开源数据库,该技术来源于 Fay Chang 所撰写的 Google 论文《Bigtable:一个结构化数据的[分布式存储系统]》。HBase 不同于一般的关系数据库,它是一个适合于非结构化数据存储的数据库。另一个不同的是 HBase 基于列的而不是基于行的模式。
- Cassandra:它最初由 Facebook 开发,用于改善电子邮件系统的搜索性能的简单格式数据,集 Google BigTable 的数据模型与
Amazon Dynamo
的完全分布式架构于一身。Facebook 于 2008 将Cassandra
开源,此后,由于Cassandra
良好的可扩展性其被许多知名网站所采用,成为了一种流行的分布式结构化数据存储方案。 - 其中上一章节提到的很多 OLAP 数据库大多数是面向列式存储的。如
Druid
、ClickHouse
等。
检索不再高深
曾几何时,全文检索是一个多么高深的技术,虽然如 Google 这样的全网搜索引擎背后的搜索算法和技术依然不是轻易就可以实现的。但现在大大小小的各种 App,网站的搜索功能的背后技术基本被一个强大的开源系统轻松就可以实现了。这个系统就是 Elasticsearch
,一个基于 Lucence
的分布式实时全文检索数据库。
伦敦的公寓内,Shay Banon 正在忙着寻找工作,而他的妻子正在蓝带 (Le Cordon Bleu) 烹饪学校学习厨艺。在空闲时间,他开始编写搜索引擎来帮助妻子管理越来越丰富的菜谱。
他的首个迭代版本叫做 Compass。第二个迭代版本就是 Elasticsearch(基于 Apache Lucene 开发)。他将 Elasticsearch 作为开源产品发布给公众,并创建了 #elasticsearch IRC 通道,剩下来就是静待用户出现了。
公众反响十分强烈。用户自然而然地就喜欢上了这一软件。由于使用量急速攀升,此软件开始有了自己的社区,并引起了人们的高度关注,尤其引发了 Steven Schuurman、Uri Boness 和 Simon Willnauer 的浓厚兴趣。他们四人最终共同组建了一家搜索公司。
一个程序员为帮助妻子管理菜谱开发的搜索工具最终称为一个强大的全文检索数据库。看来,面向对象依然是程序员创作的强大灵感源泉之一。
将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这部分从非结构化数据中提取出的然后重新组织的信息,称之索引。将这些索引与文档建立映射关联,通过索引检索出对应的文档数据,这种词汇到文档的映射被称之为倒排索引。先建立索引,再对索引进行搜索的过程就叫全文检索。
提到全文检索,不得不提到的一个技术就是 Lucene,Lucene 是 apache 下的一个开放源代码的全文检索引擎工具包。提供了完整的查询引擎和索引引擎,部分文本分析引擎。
Elastisearch 就是基于 Lucene 的一个分布式开源全文检索数据库。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful web 接口。Elasticsearch 是用 Java 开发的,并作为 Apache 许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。许多系统的搜索功能背后,其实就是一个强大的 Elastisearch 服务,Elasticsearch 也常由于日志检索,数据分析场景。
K-V 缓存霸主
在整个计算机系统中磁盘和网络是最慢的部分,一个系统中最重要的东西就是数据,而目前系统中的数据最终都存储在磁盘上。因此当前磁盘缓慢的读写速度和人民对系统响应数据和系统高并发之间的矛盾,就是目前系统需要解决的主要矛盾。将透彻了,所有的系统优化都是在缓解这个矛盾。
为提供系统响应数据和并发能力,一个最常见的手段就是缓存。在计算机系统中,CPU,内存,磁盘,网络的访问效率差着不同的数量级,为缓解这种数量级带来的访问效率问题,最常见的手段就是缓存。CPU 和内存之间有缓存,称之为 CPU 高效缓冲;内存和磁盘之间也自带缓存。
在分布式系统中,数据库访问的压力,我们常常使用分布式缓存系统来解决。
Redis 是一个高性能的 key-value 数据库。它支持存储的 value 类型相对更多,包括 string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和 hash(哈希类型)。Redis 支持缓存过期时间,原子操作,数据持久化,支持集群模式。
- K-V 缓存:将数据 K-V 化并缓存在 Redis 中,从而提高数据的访问效率,减小数据库的访问压力,这种常见的系统优化策略。
- 分布式锁:分布式锁,就是一个全局的临界资源,通过对这个临界资源的独占达到一种全局锁的功能,任何全局共享资源都可以实现分布式锁的功能,甚至 MySql,分布式文件系统。基于 Redis 的分布式锁,是常见的一种实现。
- Pub\Sub:发布订阅的管道功能本不应该是一个分布式缓存系统的功能,但 Redis 实现了这一部分功能,在一些简单的发布订阅场景下也可以很好的工作。
- 布隆过滤器:通过一个 bit 的 0 或 1 来表示 key 是否存在,通过 bit 集合来表示一组数据,这就是简单的布隆过滤器的实现。相对与用类似 Hash 的方式来存储 key 映射 boolean 值的方式,布隆过滤器可以节省大量的空间。Redis 就有布隆过滤器的实现。布隆过滤器常用来对大量数据做 True Or Flase 的判断,比如缓存是否存在,比如大量用户是否有权限。
- HyperLogLog:HyperLogLog 是用来快速计算基数的。基数,即不重复元素的个数(类似 SQL 的 count distinct)。
- 工具:介绍一些好用的 Java 技术栈的相关工具。Jetcache,阿里开源的一个基于注解的缓存框架。Redisson,一个强大的 Redis Java 客户端工具。
小而精
通常我们使用的数据库系统大多是 Client-Server 模式的,即数据库服务作为一个常驻进程运行在 Server 端,应用程序通过 TCP/IP 协议访问数据库系统。还有一种嵌入式的数据库,可以运行在本机中,这种数据库嵌入到应用程序中,随应用程序启动,数据存储在本地磁盘中。这种数据库是轻量的,一般占用内存少,代码精简。
- SQLite:遵守 ACID,实现了大多数 SQL 标准,支持 SQL 语法。支持 JDBC。
- H2:一个 Java 编写的关系型数据库,它可以被嵌入 Java 应用程序中使用,或者作为一个单独的数据库服务器运行。Spring Boot 内置的数据库。
- Berkeley DB:一个高效的嵌入式数据库和键-值数据库编程库。
- LevelDB:是 Google 开源的持久化 KV 单机数据库,具有很高的随机写,顺序读/写性能,LevelDB 应用了 LSM(Log Structured Merge) 策略。另一个 Facebook 基于 levelDB 开发的 RocksDB,也是一个高性能的 key-value 型内嵌式存储引擎。LevelDB 或 RocksDB 常常被当作存储引擎使用。比如强大的时间序列数据库 Influxdb 早期底层存储引擎就是用于的 LevelDB;RocksDB 是流式计算框架 Flink 的 Checkpoint 的底层存储引擎;著名的分布式 Actor 框架 Akka 也使用 RocksDB 作为默认的 checkpint 存储。由于其强大的顺序读写能力,也常常用来做 WAL(write-ahead-log)日志存储引擎。
这些小而精的嵌入式数据库,除了用在一些小型设备上,如手机客户端等。也常常被用于很多自研数据库系统的存储引擎。这些自研的数据库系统,以上面那些嵌入式数据库作为存储引擎,在上面实现自己特有功能,从而实现一个特殊的数据库系统,比如扩展分布式功能,基于其现实一个分布式存储系统;比如基于 LevelDB 等实现磁盘队列,和分布式队列;比如基于其存储特殊的模型的数据,如时间序列数据库;比如基于其实现本地操作日志记录和重试提交,实现最终一致性的分布式事务解决方案。
三、承上启下
前几章我们已经了解了数据库系统的发展,也从不同角度了解了数据库系统的不同分类,并且了解到了许多不同功能场景的数据库系统。为我们如何选择数据库系统已经增添了一份基础知识。我们应该如何选择一个适合的存储方案呢?
原则
- 选择是基于需求确定的。所以必须明确需求场景,然后按需求场景选择适合的存储方案。
- 没有调查就没有发言权。方案调研就是一个调查过程,需要先了解不同数据库的基本特性,才能选择合适的存储方案。
基本场景
和前章数据库系统的分类很相似。其实上面数据库系统的分类一方面就是基于不同的使用场景才设计的,从而有不同实现的数据库系统,从而有针对不同场景的特殊优化,从而逐渐形成了不同场景的特殊模型。
事务性,如 Mysql 这些是最常见的事务性系统使用的存储方案,满足 ACID,使用简单。支持千万级别数据级别的读写。分析性,适合 BI,数据报表、数据监控等数据服务系统。文档型,适合高度可变的数据模型,当你事先不知道你的数据看起来究竟像什么样子,文档型是一个不错的选择,文档型也适合点查询多余集合查询的场景。图数据库,图数据库是一种很特殊的,新兴的数据库类型,它侧重于分析解释数据之间的相互关系而不是数据值本身,它适合推荐引擎、权限访问控制和地理数据等场景。时序性,时序性数据库在数据分析,时序数据展示,监控领域使用比较多,它适合对大量时间型数据查询、过滤、组合、聚合分析等。K-V 型,缓存和固定 View 模式的数据展示,K-V 型的需要按查询组合好存储起来,这样查询时按 key 获取即可。
读写
- 是否需要写事务
- 顺序读写还是随机读写
- 偏点查询还是大量数据集分析查询
- 数据结构变化大,还是查询结构变化大
数据量
数据量,需要考虑数据的数量,也需要考虑数据数量的增长速度,这样就需要考虑数据库的量级承载能力以及水平扩展能力。
数据用途
对临时数据和重要的业务数据的存储可以采用相对侧重点不一致的方案。对数据的一致性要求的强弱也会影响数据存储系统的选型。对数据事务的要求,对数据保存时间的选择也会不一样。
可靠性
数据的可靠性即保证数据的可用的能力,可靠性与成本一般是权衡的一体两面,需要对数据可用性的要求选用不同的存储架构。
可扩展性
可扩展性表现在数据使用的可扩展和系统本身的可扩展上。
可维护性
- 可运维性:方便运营团队来保持系统平稳运行。
- 简单性:简化系统复杂性,使新工程师能够轻松理解系统。
- 可演化性:后续工程师能够轻松地对系统进行改进,并根据需求变化将其适配到非典型场景,也称为可延伸性、易于修改性或可塑性。
学习和了解数据底层存储,除了可以搭建良好的存储架构是提供思路上的帮助,也可以让我们学习到很多平时纯业务开发接触不多的底层技术实现。对底层技术的了解和掌握,又可以反过来让我们更加了解我们的整个业务系统,对系统的合理性优化做出重要的选择。也可以帮助我们实现自己的系统。
开源数据库系统的良好的分布式架构,优秀的网络通信,性能强劲的内存和磁盘访问优化以及更多经典的数据接口和算法都是值得我们学习和借鉴的。
四、知行合一
知是行的主意,行是知的工夫;知是行之始,行是知之成。—— 王阳明
这一章节将简单讲解一些数据库系统的常见技术点。
系统架构
Master-Slave
Master-slave 架构可以说是最常用的数据存储架构,关系型数据库如:mysql,postgreSql,oracle,Nosql 诸如:MongoDb,消息队列如:Kafka,RabbitMQ 等都使用了这种架构。
在整个系统中,Master 承担写任务,Slave 通过复制 Master 的数据保证与 Master 数据的一致性。Master 和 Slave 都可以承担读任务。Master 架构解决了数据的高可用问题(Slave 存储了数据副本),也扩展了数据读并发能力(多 Slave 同时通过读请求)。
在 Master-Slave 架构中,单 Master 如果出现故障,就会导致这个数据库系统不可用,这时就可以采用 Master-Master 架构,系统中同时存在多个 Master 节点,但是,多个 Mater 节点并不同时提供写服务,同时只会存在一个可写的 Master,另一个 Master 作为备机存在,只有当其他 Master 不可用时才会被选举称为 Master 节点提供写服务,作为备机的 Master 是可以提供读服务的。这种架构的只解决了单 Master 节点的高可用问题,并没有解决单 Master 负载过大的问题,这里之所以只有一个 Master 提供写服务,是为了保证写数据的一致性问题。
数据一致性
我们将同一份数据在不同数据节点上的存储称之为副本。只要系统中数据存在多个副本,就会有数据一致性问题。如何保证数据多副本的一致性,一直以来都是分布式系统的最大挑战。多节点数据同步,一般采用复制方式,从节点复制主节点的数据,多节点之间相互复制等等,但无论采用哪种方式,都无法避免不一致的情况。
数据一致性可以分为最终一致性和强一致性。强一致性模型能够允许你的单服务程序移植到分布式节点集群上并且不会发生任何错误。强一致性往往通过牺牲系统可用性来达到,在写入数据时,如无法保证多副本一致,则失败。最终一致性模型中,当停止改变数值的一段不确定的时间后,所有的复制集将会最终保持一致。这表明,在这段时间之前,数据副本在某种情形下是不一致的,但数据最终会达到一致,最终一致性意味着“收敛”,即预期所有的副本最终会收敛到相同的值。
在数据收敛过程中,为保证最终数据的一致性性,还有许多问题需要解决。如系统间的时序问题,原子提交问题,共识问题。
CAP 理论
定理:一个分布式系统不可能同时满足 consistency、availability、partition tolerance 这三个基本需求,最多同时满足两个。
- consistency 一致性:所有节点同一时刻看到相同数据
- availability 可用性:节点失败不阻止影响正在运行的节点的工作
- partition tolerance 分区容错:即使出现信息丢失或网络、节点失败,系统也能继续运行(通过复制)
这三种性质进行俩俩组合,可以得到下面三种情况:
- CA:完全严格的仲裁协议,例如 2PC(两阶段提交协议,第一阶段投票,第二阶段事物提交)
- CP:不完全(多数)仲裁协议,例如 Paxos、Raft
- AP:使用冲突解决的协议,例如 Dynamo、Gossip
CA 和 CP 系统设计遵循的都是强一致性理论。不同的是 CA 系统不能容忍节点发生故障。CP 系统能够容忍 2f+1 个节点中有 f 个节点发生失败。
分区
上面说副本只能保证数据的可用性。为提高大量数据集的读写能力,我们可以将数据拆分成不同的分区分开处理,这种技术称之为分片。
分片,即将数据集分割成相互独立的小数据集,减少因数据集增长而带来对单个节点的压力。数据分片有以下好处:
- 提高性能:限制分区中数据量的大小,降低数据压力
- 提高可用性:数据之间相互独立,不同分区之间失败互不影响,允许失败节点的存在
分区自然也会带来一些问题,首先需要考虑的就是如何分区的问题
。
- 基于关键字区间:将数据按关键字划分为不同区间,将相同区间的数据写入同一个节点。比如用户数据 id 分布在[1-1000000]之间,需将数据分布到 10 个节点上,可以将数据划分成十个区间:
- 关键字哈希分区:通过 Hash 算法计算分区号,将数据写入相应分区号的分区中。
数据分区带来的负载倾斜
和热点
问题:由于数据的不确定性,数据关键字计算出来的分区存储可能集中在某几个区间内,这样就可能导致某些节点数据明显多余其他节点,这种数据集中于某个节点的情况就是数据热点。由于数据热点的出现,整个系统的负载将倾斜到这些节点上,造成分区间的负载不均衡,这就是负载倾斜问题。
去中心化:Dynamo
Dynamo 是 Amazon 的一个分布式存储。Amazon 发表了一篇论文 Dynamo: Amazon’s Highly Available Key-value Store 讲解 Dynamo 架构,使得 Dynamo 称为许多数据存储系统借鉴的架构。
Dynamo 基于一些众所周知的技术实现了可扩展性和高可用性:
- 数据通过
一致性哈希
算法进行分区和复制(partitioned and replicated) - 通过
对象版本化
(object versioning)实现一致性 - 副本之间的一致性由一种
仲裁的技术
(quorum-like technique)和一个去中心化的副本同步协议
(replica synchroni protocol)来保证 - 基于 gossip 协议进行分布式故障检测和成员检测(membership)协议管理节点
Dynamo 是一个完全去中心化的系统。
向 Dynamo 添加或移除存储节点不需要人工 partition(调整哈希节点)或 redistribution(在节点之间重新平衡数据分布)
Dynamo 采用最终一致性方案。
生产级别的存储系统的架构是很复杂的。除了最终存储数据的组件之外,系统还要针对以下方面制定可扩展和健壮的解决方案:负载均衡、成员管理(membership)、故障检测、故障恢复、副本同步、过载处理(overload handling)、状态转移、并发和任务调度、请求 marshalling、请求路由(routing)、系统监控和告警,以及配置管理。
下表总结了 Dynamo 使用的这些技术及每项技术的好处。
Partition
- 技术:
一致性哈希
- 好处:增量可扩展性
写高可用
- 技术:读时协调(解决冲突)的
向量时钟
(vector clocks with reconciliation during reads) - 好处:version size 和更新频率(update rates)解耦
短时故障处理
- 技术:宽松的选举和 hinted handoff(移交给其他节点处理,附带提示信息)
- 好处:部分副本不可用时,仍然可以提供高可用性和持久性
持久(permanent)故障恢复
- 技术:基于
Merkle tree
的逆熵(anti-entropy) - 好处:后台同步版本不一致的副本
成员管理和故障检测
- 技术:基于
Gossip
的成员管理协议和故障检测 - 好处:保持了架构的对称性,无需一个中心组件(centralized registry)来存储成员和节点状态等信息
分布式数据库 Cassandra 就是 Dynamo 的典型实现。
有主架构:Bigtable
Bigtable 是 google 开源的数据库系统。Bigtable 是典型的有主架构。
Bigtable 主要由三个组件构成:
- 一个客户端库,会链接到每个客户端
- 一个 master server
- 多个 tablet server
master 负责:
- 将 tablet 分配给 tablet server
- 检测 tablet server 的过期(expiration)及新加(addition)事件
- 平衡 tablet server 负载
- 垃圾回收(GC)
- 处理 schema 变动,例如 table 和 column family 的创建
BigTable 的 Master 只负责元数据的管理,Table Server 负载自身管理的 Table 的读写功能,客户端只想 Master 同步元数据,数据不经过 Master 节点,直接和 Table Server 通信。因此,BigTable 中 Master 节点的负载很低。
有主架构中,Master 承担的能力也会不一致,比如在下图架构中,Master 只承担 Coordinate 功能,管理元数据和 Node 节点,Client 获取 Mata Data,直接和相应的数据节点通信。
在下面架构中,Client 不直接和 Data Node 节点通信,而是和 Master 通信,Master 更加相关元数据将请求转发给对应的 Data Node:
Coordinate-Worker
架构是很多分布式数据库采用的架构,有兴趣的同学可以看看笔者之前讲解的 《Druid 的架构设计》
索引
数据库系统的索引,就是用来提高数据检索效率的。数据库系统的数据记录存储在磁盘中,如果没有索引,要从磁盘中检索相应的记录,就需要扫描所有的数据段,这种 O(N) 的访问效率和全磁盘的扫描自然不可能在真正的数据库系统中使用。为提高数据检索能力,数据库系统引入索引技术,为磁盘上的数据记录做一个索引结构,这些索引放在内存中,或按块存储在磁盘上(但只需要少数几次磁盘读取就可以读入内存中),这样检索一个数据先从内存索引中查找到对应的 Key 或磁盘位置,然后从磁盘中读取记录。
这里索引做了两个事情:
- 将大量磁盘检索变成内存检索
- 通过特定的数据结构可以提高内存检索的效率,改变 O(N) 这种低效率的检索
HASH 索引
HASH 即哈希表,类似 Java 的 HashMap 数据结构,Key-Value 格式。假设我们在内存内维护一个 HashMap Index,key 为数据的键,Value 是数据在磁盘的存储偏移量。
- 获取数据时,先从内存 Map 获取对应数据的磁盘 offset,然后读取磁盘的数据。
- 写数据时,先将数据追加写入磁盘,然后更新内存 HashMap Index。
Hash 索引听起来过于简单,但确实是一种可行的索引方法。Hash 索引简单高效,查询性能 O(1),更新也高效,当时也有明显的缺点,比如:
- 需要将整个哈希表放入内存,这对于大数据量来说内存耗费将不可承受的。
- 只能进行精确查询。
- 不能实现范围查询。
B-Tree 索引
B-trees
索引始见于 1970 年,经受了长久的时间考验,时至今日,它仍然时几乎所有关系数据库中的标准索引实现,许多非关系型数据库也经常使用。
了解B-trees
索引先从二叉搜索树开始。二叉搜索树是一种特殊的二叉树,它满足以下条件:
- 左子树小于父节点
- 右子树大于父节点
上图是一个搜索二叉树,如果我要查找 208 这个 key:
- 先从根节点开始,即 136。比较 208 > 136,下一步应该从根节点的右子树查找
- 398 > 208,继续搜索 398 的左子树
- 250 > 208,继续搜索 250 的左子树
- 200 < 208,继续搜索 200 的右子树。
- 200 的右子树并不存在,因此数据中没有 208,查找结束
让我们再查找 40:
- 从根节点 136 开始,136 > 40,继续搜索左子树
- 80 > 40,继续搜索左子树
- 40 = 40,节点存在,从节点中获取数据 id,然后可以更加数据 id 查找对应的数据
在索引结构中,树的每个Node
包含一个 key 值,一个数据指针(或数据 id、磁盘 offset 等)
二叉搜索树的时间复杂度是 log(N)
,这是一个不错的结果。
二叉搜索树依旧只能获取特定的值,如果我需要进行范围查找,即查找两个数之间的所有数据,就需要去遍历树中的每一个节点,去判断节点是否在此范围内,这种情况下,时间复杂度又下降到了 O(N)
。因此我们需要改进上面的数据结构,现代大多数数据库都才有一种改进的二叉搜索树—— B+Tree。
B+Tree 在二叉搜索树的基础上添加如下特征:
- 仅仅在叶子节点存储索引信息(关联表数据的信息)
- 其余节点仅仅用于查找到最终的叶子节点(叶子节点包含了所有的 key)
在 B+Tree 中,每个 Key 会存在两个 Node,所有中间节点只用于辅助检索最终正确的叶子节点(叶子节点才包含关联数据的信息)。
让我们尝试从上面的 B+Tree 中搜索出[40, 100]之间的节点:
- 采用和二叉搜索树一样的方式,我们只需要搜索 40 这个节点(或搜索出最接近 40 的节点,当 40 的节点不存在时)
- 然后在叶子节点链表中往下追溯,知道超过 100
假设树中共有 N 个节点,追溯了 M 个叶子节点,那么可以得出,此次搜索的时间复杂度是:log(N) + M
。相对于之前的 O(N)
的二叉搜索树有以下好处:
- 不需要读取整棵树,这样可以减少读取磁盘的次数(索引数据一般按页存储在磁盘上)
- 大多数情况下 M (约等于检索范围)会远小于整个数据量 N,因此这里的
O(M)
时间复杂度大多数情况下远小于O(N)
。
任何事情都是双面的。B+Tree 索引带来的检索优势,必然会有其他方面的损失。这主要体现在删除数据时。因为叶子节点类似于链表结构,删除一个节点需要从 header 开始遍历,时间复杂度是 O(N)。
B+Tree 索引具有比较好的检索性能,为了减少磁盘访问次数,大多数索引系统的 B+tree 都只有 3- 4 层,因此 B+Tree 索引能够承载的节点数是有限的。B+Tree 在更新节点是需要自排序
和自平衡
,这需要额外的性能消耗,B+Tree 的插入和删除时间复杂度是 O(log(N))
。这就是为什么在使用数据库时不建议索引字段都添加索引,而是充分考虑具体情况,在需要的字段上添加索引,否则索引太多会影响表的insert\update\delete
操作性能。
LSM
B+Tree 是基于页的索引引擎,B+Tree 的数据存储本身是无序的,其建立索引的思想是在内存中维护一个 key 与数据磁盘位置的对应关系,并保证这个内存数据结构有序。有一种基于文件的存储引擎,它将数据划分成文件段,并保证数据在磁盘文件段中有序,因此,这种存储引擎并不需要在内存中维护所有数据的顺序表,只需要在内存中维护一个稀疏的索引结构,每次从内存索引中搜索到的数据并不是具体到每条数据,而是一个文件段(或文件块),然后将这些有序的数据读入内存,再按序获取具体的数据。(如何保证写入数据有序?)
LSM(Log-Structured Merge-Tree),就是这样一种索引结构。LSM 的架构如所示:
SSTable: LSM 的磁盘文件,称作SSTable(Sorted String Table)
。望文得意,LSM 存储在磁盘中的文件,数据也是按 Key 排序存储的,这样就可以解决上面讲到的数据量大了之后无法将数据全部索引到内存中的问题。如果磁盘文件也是有序的,那么内存索引可以采取”稀疏索引“(Sparse Index),可以每一段记录一个索引,将数据逻辑上分成多个block
,稀疏索引只需要记录每个block
的偏移量,每条数据通过遍历block
实现。这样索引量将大大减小。
Memtable: LSM 的内存结构叫做Memtable
。Memtable
是一个有序结构,同样可以采用树结构,可以用跳表
。LSM 写数据时,只需要写入内存中的Memtable
,当Memtable
到达一定量之后,会异步刷入磁盘,就是上面的SSTable
。
Immutable Memtable: 在数据从内存Memtable
刷入SSTable
时,为避免读写锁导致的性能问题,LSM 会在内存中 copy 一份immutable Memtable
表,顾名思义,这个数据结构不可改变,新写入的数据只会写入新的Memtable
,immutable Memtable
供刷盘线程读取,查询数据的请求也可以访问这个数据结构,这样如果数据在内存中,就不需要访问磁盘,可以提供数据查询的效率。
WAL: write ahead log,预写日志,关于 WAL,可以参考我之前的文章《你常听说的 WAL 到底是什么》。在 LSM 中,在数据刷入磁盘前,为防止异常导致数据丢失,LSM 会先将数据写入 WAL,然后写入 SSTable,系统重启时,LSM 会从 WAL 中回溯 SSTable,当写完一个 SSTable 时,LSM 会清理掉过期的 WAL 日志,防止 WAL 过量。
LSM 如何写入数据:
- 写入
WAL
- 写入
Memtable
-
Memtable
达到阈值时,复制Imutable Memtable
- 异步刷入磁盘
LSM 如何删除数据: 为保证顺序写磁盘,LSM 不会去直接删除数据,而是通过写一条 delete 标识来表示数据被删除,数据只有在被 Compact 时才会被真正删除。
LSM 如何读取数据: LSM 读取数据将从memtable
、imutable
、sstable
依次读取,直到读取到数据或读完所有层次的数据结构返回无数据。所以当数据不存在时,需要依次读取各层文件。LSM 可以通过引入布隆过滤器
来先判断一个数据是否存在,避免无效的扫文件。
密集索引(dense index) 和 稀疏索引(spare index):密集索引为每条数据对应一个索引记录,稀疏索引一般只索引数据块或文件,是跳跃式的。因此稀疏索引比密集索引更节省空间。
压缩
数据压缩对数据库系统中 I/O 性能的影响相当明显,它可以减少磁盘空间使用、降低带宽使用和提高吞吐量。数据库系统中的数据存储、索引存储、数据转换、数据备份和网络通信都会用到相应的压缩技术。当将数据库压缩引入实时数据库时。压缩算法必须提供高压缩比才能实现高数据存储,压缩算法必须足够快,才能在实时数据库中实现实时记录和查询功能。
压缩过程一般由两个独立的部分组成,建模
和编码
。建模定义输入流中不同符号的特征。模型存储有关符号在数据中出现的频率的信息,即符号概率。编码是压缩过程的第二部分,它根据模型提供的概率为不同符号创建一组代码,从而产生数据的压缩版本。将更频繁地出现的符号与较短的代码词和较长的稀有符号互换。数据的均匀性会影响大多数压缩算法的压缩比,但对压缩速度没有任何影响。因此,为了达到更好的压缩性能,压缩算法是专门为数据的每个部分设计的,因此不同的压缩算法对不同类型的,不同量级和不同组合的数据的压缩效果是不一致的。也因此大多数支持数据压缩的数据库系统都会提供多种不同的压缩算法让用户根据自身数据情况自由选择。
压缩算法可以分为以下两大类:
- 有损压缩:有损压缩会重构原始数据。所以读取的压缩后的数据是不完整的。这种压缩方式通常用在音频、视频等流文件的压缩中。
- 无损压缩:无损压缩不影响压缩数据的原始值。通常使用在文本,数字等数据的压缩中。
压缩应该考虑什么问题:
- 大小:压缩后的文件大小,即压缩比。当使用压缩时,本就是为了降低数据大小,所以压缩算法的压缩比是首要考虑的问题。
- 速度:压缩速度会影响数据的读写效率,这对实时系统来说尤为重要,速度和大小是 trade-off 的两面,必须充分考虑具体的使用场景。
- 资源:压缩节省了磁盘和宽带,但是会增加 CPU 和压缩时的内存使用。所以压缩时的资源耗损情况也需要考虑。
下面列举一些常见的压缩算法或方法(Gzip、Bzip2、LZMA、XZ、LZ4、LZO),并做相应的对比:
测试条件:
- Intel Core i5 CPU 750 at 2.67GHz
- 8GB of DDR3 memory
- tmpfs as ram disk
- Linux kernel 3.3.2, gentoo amd64
- CFLAGS: -pipe -O2 -g -floop-block -floop-interchange -fgraphite
- bzip2-1.0.6-r3, xz-utils-5.0.3, gzip-1.4
文件压缩对比结果(原数据: 445M):
压缩比对比:
各大数据库系统多多少少都会用到压缩技术来降低数据存储空间,来提高系统性能,以下列举一些数据库系统使用到的压缩技术:
- Google 在 BigTable 和 MapReduce 中使用 Snappy 压缩数据和网络传输。
- SQL Server 使用 XPRESS 算法压缩备份数据。
- Oracle 使用自实现的 Oracle Advanced Compression 算法压缩数据。
- MySQL 使用 LZ77 算法压缩 InnoDB 的表。
- Kafka 同时支持 gzip 和 snappy 和 lz4 算法,并对默认的 lz4 做了特定的优化。
- Druid 使用 lz4 压缩数据。
数值压缩:delta-of-delta
数值压缩经常用于压缩列式存储的数字列。前面我们讲到过,列式存储将每列的数据存储在相邻的位置。这样的存储结构利于压缩数据,下面我们讲一下在许多列式存储中使用的 Delta 数值压缩技术。
如图所示,假设有 6 个原始数值(73、300、302、332、343、372)。在未压缩之前,每个数值占用 4 个字节,6 * 4 = 24 共占用 24 个字节。Delta 压缩算法不存储原始的数值,而是先确定一个数字(一般取第一个数值),后面的数值是相对于第一个数值的差值,如图第二行所示得到的数据集为(73、227、3、30、11、29)。因为最大的差值是 227,因此只需要一个 byte 就可以表示,因此之前需要使用 4 个字节存储的每个数值,现在只需要使用 1 个字节。为了保存对应的差值相关元描述信息,需要额外的 1 字节保存这些信息,上图还将数据分块存储,因此最终需要的字节数是 7 个。这样相对于原始的 24 字节节约了将近 3 倍的空间。
其实上图就是 Elasticsearch 底层使用 Lucence 的原理。
delta-of-delta 适用于数值类型数据的压缩,且对数据量大并且数据集中的数据压缩才有效果。如果数据集比较小,且比较稀疏,数据的最大差值已经和数据值可以表示的最大值相差不大,那么压缩的意思便存在。
读写
数据存储系统就是一个与磁盘和网络打交道的系统,所以数据存储系统在这方面的优化可谓精益求精,比如异步IO
、缓冲批量读写
、append写数据
、按磁盘页读写数据
,预读数据
和磁盘内存映射技术
等等。
异步
与异步 IO 对应的是同步 IO,即每进行一次 IO 操作,需要等待此次操作结束才能继续接下来的操作,这样在大量并发请求情况下,IO 的效率将会大大降低,磁盘 IO 和网络 IO 在并发量大的情况下采用异步 IO 可以明显提供效率。
Mysql 的 InnoDB 也采用 AIO 提高效率,InnoDB1.1.x 之前,AIO 的实现通过 InnoDB 存储引擎中的代码来模拟实现,从 InnoDB1.1.x 开始,提供了内核级别的 AIO 支持,称为 Native AIO。在 InnoDB 存储引擎中,read ahead 方式的读取都是通过 AIO 完成,脏页的刷新,即磁盘的写入操作则全部由 AIO 完成。
在 Kafka 中,Broker 的数据磁盘落地,都是采用的 Java NIO 的方式处理的,这是 Java 的异步 IO 的实现,Java NIO 可以提供数据写入的并发性能。
缓冲
缓冲技术是为了协调吞吐速度相差很大的设备之间数据传送而采用的技术。
在数据到达与离去速度不匹配的地方,就应该使用缓冲技术。缓冲技术好比是一个水库,如果上游来的水太多,下游来不及排走,水库就起到“缓冲”作用,先让水在水库中停一些时候,等下游能继续排水,再把水送往下游。
将缓冲和批量发送结合,可以提高数据在在网络和磁盘中的写入速率。在数据写入网络或磁盘时,先设置一个缓冲池,当数据达到一定的数量或缓冲时间超时时,在将数据批量发送出去,可以减少请求并发,也可以减少请求额外数据带来的带宽和磁盘消耗。
在 Mysql 中,Innodb 在多个地方使用缓冲来提升写入性能。比如插入缓冲,将多个插入请求合并到一个操作中,这样可以将之前的一些非顺序写入变成相对的顺序写入,以提高写入效率。另一方面也可以按磁盘物理页写入数据,这样充分利用了磁盘的写入特性。
在 Elastisearch 和 Kafka 的客户端中,都采用了缓冲批量写入的功能来减少写入并发情况。
磁盘
在磁盘的读写优化上,经常可以看到以下技术:
按磁盘页读写数据:磁盘读写的单位是
页
。为了减少读写数据时磁盘访问频率,数据库系统通常都按页读写数据。预读数据:一些数据库系统认为用户访问了一部分数据,那么在它相邻的放的数据下次被访问的可能性也很大,所以会预先读取多个页的数据。
磁盘内存映射(MMP):即盘扇区映射到进程的虚拟内存空间的过程。MMP 读写数据时跨过了页缓存,减少了数据的拷贝次数;实现了用户空间和内核空间的高效交互方式;可以缓解系统内存不足的压力。
本文对各种技术浅尝辄止,其实每一个技术点都可以深入讲解,感兴趣的同学请持续关注我们后期的文章。
参考:
《Designing Data-Intensive Applications》
《Database Software Market:The Long-Awaited Shake-up》
《Distributed systems for fun and profit》
《How does a relational database work》
《七周七数据库》
《Mysql 技术内幕——InnoDB 存储引擎》
《数据库系统概念》
《Dynamo: Amazon's Highly Available Key-value Store》
http://arthurchiao.art/blog/amazon-dynamo-zh/
https://zhuanlan.zhihu.com/p/35622907
https://baike.baidu.com/
https://hbase.apache.org/
https://zh.wikipedia.org/
https://clickhouse.tech/
https://druid.apache.org/
https://www.elastic.co/
希望读者 「点赞」「分享」「在看」三连就是最大的鼓励。
后台回复 “加群” 进入专属技术群一起成长