为什么我喜欢数据库?没那么复杂和吓人

去年我在 Square 的工作中接触到了很多种数据库。包括:

  • 发现和解决数据库性能问题。
  • 为新应用设计数据模型和分片策略。
  • 评估和推行新的数据库。

起初是为需求所迫,但我很快就对数据库着迷了。数据库的交叉研究几乎横贯了计算科学的每个领域——它的理论和实现都非常复杂,而且富有挑战性。 然而,我很快意识到这并非所有的人都像我一样热衷于数据库。对于我的很多同事和朋友而言,数据库是一个具有魔力的黑盒子系统,太吓人太复杂了以至于不能理解。我想要改变一下这个现状。 当谈论到数据库时,分布式系统的话题是不能忽略的。大部分现代数据库都是分布式的,要么是隐式的(分布式集群数据库),要么是显式的(通过应用程序级的分片连接到多个数据库的单个应用程序)。 这篇文章是我喜欢数据库和分布式系统的告白。它主要针对向我一样的程序员,经常接触数据库的应用开发者。我们主要用 Java、Python、或是 Ruby 编码,用来写服务端的应用。本文会覆盖到以下话题:

  • 比较和评估不同的数据库。
  • 如何理解和充分利用你的数据库。
  • 从大的层面上去理解数据库是如何工作的。

首先,什么是数据库

在这篇文章中,任何接收并存储数据以备将来获取之用的软件就是数据库。这包括了传统的 RDBMS 和 NoSQL 数据库,以及如 Apache Zookeeper 和 Kafka 一样的系统。

CAP 理论

CAP 理论。这是继图灵的停机问题和 P≠NP (技术上不可解)后,我最喜欢的不可解问题。CAP 理论表明,任何分布式系统最多只能同时满足 CP(一致性 & 分区容忍性), AP (可用性 & 分区容忍性),或者介于这两者之间。因此,一致性和可用性之间有很有趣的权衡出现。 关于CAP定理几个重要的误解:

  • 传统的“三选二”争论是没有意义的。你不能抛弃分区容忍性,因为那意味着“在分区中执行的操作行为是不确定的”,而且在这种情况下,数据库并不是真正一致性的。
  • 到达 CAP 理论的限制并没有默认给定。有很多的数据库,它们既不是一致的,可用的,也不是分区容忍的。要实现 CAP 理论的限制需要精心的设计和实现。

分布式系统

如前所述,许多现代数据库都以某种方式实现了分布式。推动数据库的分布式化的两个因素:

  • 为了规模上超越单机 —— 在多节点上存储和处理数据。
  • 为了增加可用性 —— 确保数据库不会发生单点故障。

这两个目标是相互紧密关联的。一般来说,通过增加机器数量来扩展系统会对可用性产生负面影响,因为发生单个机器故障的机会增加了。所以,实现高可用性几乎是可扩展性的先决条件。

正确性和效率

正确性和效率两者都重要,而且在分布式数据库中两者也是紧密关联的。 在任何软件中正确性都是重要的,但是对于数据库来说它是必不可少的。因为(1)数据库存储数据,错误的数据在重启后仍然存在。(2)数据库被认为是软件栈(software stack)中最值得信赖的基础。 数据库是正确的是什么意思呢?许多分布式数据库的代码很难懂,有惊人一致的语义。在这里你可以做一个权衡。在一般情况下,对于效率和可用性的成本来说,更严格的一致性使得编写应用程序更容易。 除了理论之外,还有实现和业务挑战的正确性。分布式系统本质上是复杂的。像 Paxos 这种算法是很难理解和正确实现的。随着系统越来越复杂,更多隐蔽的故障场景会出现。像 Redis 和 ElasticSearch 就承受了由他们分布式系统的非常规设计带来的考验。 除了上述的权衡,效率也是很重要的,因为以前遇到的困难。我学到越多的底层编码,我就更了解一台机器美秒可以执行多少原始操作(raw operations)。在许多情况下,效率降低了复杂度,使整个系统更简单。事实是分布式系统比底层编码更激发我去追求效率。在给定相同负载的情况下,我更乐意选择需要更少机器的数据库。 最后,用于机器间协调的计算和延迟开销会是很显著的。总之:运行的部件越少越好。 为了从代码中优化出更多的性能和效率,深入更底层的抽象中是必须的旅程,包括:

  • 内存分配 和 垃圾收集器 [2]
  • 文件系统调度 和 IO 设备特性
  • 内核设置
  • 各种系统调用的实现细节(fork,execv,malloc)

它们中产生任何的不协调,都会导致性能不佳。你不必成为一个内核黑客(kernel hacker),但是你需要对这些组件之间的交互有一定高度的了解。

给应用授权

由于不同的编程语言都有自己的优缺点,数据库也有自己独特的特点。完全理解它们是很重要的。它可以让你实现高效和复杂的应用程序,同时委托大部分复杂易错的工作给数据库。 一般来说,如果你没有严格的性能或可用性要求,那么传统 RDBMS 是一个好选择。ACID 的保证是非常强大的,而且工具也很不错。分片 RDBMS 虽然痛苦,但它是很好理解的。MySQL 和 Postgres 是两个普遍的选择。 全文搜索引擎允许你构建高级的索引和搜索功能。集成这些系统后的最终一致性,很少是个问题,因为搜索本身就是一个模糊操作。Lucene 和它的变种数据库(Solr,ElasticSearch)是普遍的选择。 消息队列和事件处理系统消除了那些很难正确和高效实现的代码。Kafka、Storm、Spark SQL、RabbitMQ 和 Redis 是普遍的选择。 具有跨域复制的数据库使得区域故障切换和高可用性容易地多。在这方面没有很多开源选择,但是 Cassandra 可能是最成熟的一个。 一致性,leader 选举(leader election),以及分布式锁都是很难实现和测试的。不要自己去实现。用 Zookeeper 等,或者 raft 类库。 现在走向细节。数据库本身是一个抽象泄露(leaky abstraction)。他们一般在隐藏底层的复杂性上做了很好的工作,但是忽视其局限性最终会伤到你自己。以下是一些重要的东西:

  • 理解数据库的正确性保证。一个失败的操作是指什么[3]? 哪些操作是完全一致的,哪些又不是?
  • 理解数据是如何被存储和获取的。哪些操作是有效的,哪些又是无效的?有没有一个查询规划(query planner),或者每个操作的详细统计信息?
  • 分片和集群架构。理解数据是如何在集群中分布的。你的分片策略均衡地分布数据了吗?或者有没有热点存在?
  • 数据建模模式和反模式。

业务挑战

一旦你的软件栈中的一部分,数据库和你的基础架构保证24*7提供不中断服务 。它就引入了独特的业务挑战。 操作数据库就像在海洋中航行。无论何时你遇到了问题,你都要不让数据库下沉的同时解决它,即使是在风暴中心。因此,数据库需要有:

  • 内省(introspect)和监视系统的方法。
  • 维护和管理系统的把手。
  • 复制,备份和恢复。丢失数据是极其糟糕的。机器会在某个点奔溃。由于数据库是有状态的,你不能简单地只部署代码。

在运行的同时具备以上所有。坦白说,所有数据库在这点上都有缺点。在完全运行的同时允许任何配置都可以修改,是一个艰难的挑战。许多操作需要一个数据库层面的互斥量,额外的系统资源,或者重新启动。例如包括:

  • 在 MySQL 5.6 以前,添加一列需要全表锁,而黑客们喜欢 pt-online-schema-change 的存在就是为了缓解这一问题。MySQL 现在支持在线模式迁移。
  • Cassandra 允许你简单地添加、删除、和修复节点。然而,这些操作给系统添加了额外的负载,而且需要容量空间。

此外,你不能简单的替换一个数据库。即使是在同一数据库内迁移数据的任务也不简单。迁移到另一个数据库中更是难上加难,如果不是不可行[4]。一个应用程序的代码是非常容易逐步铺开并恢复(如果需要的话)的。数据比代码存活地更久。数据模式和存储的数据通常会在多个应用程序之间共享。因此,初始的数据库系统和对应的数据模型的选择是非常重要的。 最后,数据库总会发生故障。不管你用的是什么平台/服务即架构,有些故障是避免不了的:

  • 应用程序代码弄脏或丢失数据的 Bug 。
  • 误解了数据库的安全性和一致性保证,丢失了写操作。
  • 数据模型和数据库不匹配 —— 例如,有一个数据集并不适合在一个独立 shard 上。期待原子切换(compare-and-swap)到最终一致性的数据库中工作。
  • 运行故障 —— 机器崩溃。硬盘损耗。操作系统升级。
  • 网络分区[6]。
  • 惊群效应 —— 单个系统故障,并级联到其他系统。
  • 而最糟糕的是 —— “突然慢下来了”。“随机尖峰延迟”。“每天一次偶发错误”。“这条记录应该存在但是没有了”。

对于这些却没有单一的解决方法。操作数据库的艺术真的属于维护一个高 SLA 系统的艺术,但是如果我需要给出一些技巧:

  • 应用程序开发人员理解的限制和故障模式的行为。
  • 编写一个有弹性的应用程序。多数据中心部署。自动故障转移。
  • 拥有一支由熟悉业务并且了解不同故障场景和恢复方法的工程师(网站可靠性工程师,DBAs)所组成的队伍。

PS:在 Square ,我们有一个超酷的在线数据存储(ODS)团队把这些问题从我们这里抽离出去。

基础构建模块

通过数据库提供的抽象真的很神奇。数据提取(ingestion),查询,复制,以及故障恢复都在同一个包里?但是当你习惯了它,你就开始认识到有一些基础的构建模块 —— 在所有数据库之间共享的通用模式和组件。 首先,数据检索降低了以下中的一个:

  • 键值查找(哈希表)
  • 范围查找(树和 LSM 树)
  • 文件偏移量查找(Kafka,HDFS)

在本文的最后,我想说的是,电脑并不懂 SQL、索引、联接、或是其他的花哨的装饰。上层的操作需要被翻译成机器能执行的东西。 对于可持久化数据结构,B-trees, hash tables,和 LSM树 (log-structured-merge-trees)都是很普遍的选择。很可能你的数据存储在其中的一个,除非它需要一些特定的查找(例如:地理空间查询)。LSM 树是一种流行的现代的选择,在BigTableHBase, Cassandra, LevelDB, 以及 RocksDB 中都有使用,因为其一流的写入性能和合理的读取性能。 最后,还有流行的模式和算法用于整个不同的系统: Paxos, Raft一致性哈希Quorum 读/写Merkel 树, 以及 Vector Clocks 都是一些基本的构建模块。

总结

这篇文章是对一些话题的简单、高度概括。还有很多话题我没有涉及到,诸如对不同工作流的优化(OLAP、OLTP、批处理)和数据库的UX(查询语言、传输协议、客户端类库),这些都是同等重要的。不同语义一致性的影响,诸如sequential consistency, read your own write, at least once delivery 都是非常有趣的。 关于数据库最棒的事情就是它是一个非常成熟的抽象。它大部分都在工作,而作为一名应用开发者,你可以不用思考就能非常容易地保存和读取数据。这绝对是值得庆祝的,但是为了这足够先进的技术脱层皮也确实是值得的。 我希望有更多的人能被这个主题深深吸引,并充分利用它。

参考文献和旁注

[1] 你不能牺牲分区容忍性 —— http://codahale.com/you-cant-sacrifice-partition-tolerance/。Aphy’rs Jepsend 的文章是一个很好的入门资源 —— http://aphyr.com/tags/Jepsen [2] 现代数据库频繁地利用OS文件系统缓存显著地加速文件系统的访问。未使用的内存自动地被用作缓存。这样一个系统的推荐生产配置对于没经验的人来说是不寻常的,机器有90%的内存是空闲的。 [3] 在基于类似 dynamo 的 quorum 系统中的常见缺陷是写故障时没有给出任何信息。当没有按时写入到内部副本时,写操作可以在客户端外部失败。因此,失败的写操作是非常容易成功。最糟糕的是,在最后写成功(last-write-wins)解决策略和倾斜的系统时钟下,以后的写入操作可以被这次的写入故障覆盖。 [4] 在数据库之间迁移涉及到了在多个数据库之间同时做读写操作。数据的所有者(俗称“真相来源”)是不确定的,而在新旧系统之间做同步可能会有数据的丢失。 [5] 在初创公司的基础架构中,应用程序的 bug 可能是可靠性和可用性最大的罪犯。扩展和性能问题是可以被预测到的,而当你有一把服务器的时候,硬件故障通常不能被预见。然而,每天都会有新代码部署上线。 [6] 从网络是可靠的 —— 网络分区比你想象的还要常见。从根本上说,没有办法区分高延迟、网络分区、GC 停顿、和机器故障 —— 他们的表现都是低速连接。在 ElasticSearch 中这是一个普遍面临的问题。一个节点正在遭受一个大 GC 停顿,而整个集群认为这个节点挂了,然后试图重新分配数据,一连串的问题出现了。

多数据库

一旦一个系统与多个数据库有交互了,系统就是最终一致性了。你不能在同一时刻并发地修改多个数据库,除非你实现了两阶段提交协议(2PC)。这类似于“原子操作的成分是不是原子”。

关于删除

在任何的分布式系统中,删除数据是困难且危险的。数据到处都被复制,不管是数据库还是应用程序。没有一个适当的协调,被删除的数据被恢复回来是不可能的。一种典型的处理方法是写一个tombstones记录来代表删除,然而, tombstones 有他们自己的问题 :

  • Tombstones 占据硬盘空间。为了回收硬盘空间,tombstones 必须到期。如果 tombstones 在完全复制前过期且被删除了,已删除的记录可以被复制回来。
  • 你可以用一个旧的 tombstone 来删除一个将来的写操作。这被通俗地称为厄运石头(doomstone)。搞笑的是,这是一个现实存在的问题。
收藏 2 评论

关于作者:伍翀

微博:@黑桃夹克 个人主页 · 我的文章 · 10

相关文章

可能感兴趣的话题



直接登录
最新评论
  •   2015/01/30

    但是我怎么感觉数据库这块的职位除了dba,没别的了,
    面试处处碰壁。

    •   2015/01/30

      我也很喜欢数据库,,但是接触时间不太久,接触两年左右;
      还考虑要不要在这块继续发展下去。

跳到底部
返回顶部