去年我在 Square 的工作中接触到了很多种数据库。包括:
起初是为需求所迫,但我很快就对数据库着迷了。数据库的交叉研究几乎横贯了计算科学的每个领域——它的理论和实现都非常复杂,而且富有挑战性。 然而,我很快意识到这并非所有的人都像我一样热衷于数据库。对于我的很多同事和朋友而言,数据库是一个具有魔力的黑盒子系统,太吓人太复杂了以至于不能理 解。我想要改变一下这个现状。 当谈论到数据库时,分布式系统的话题是不能忽略的。大部分现代数据库都是分布式的,要么是隐式的(分布式集群数据库),要么是显式的(通过应用程序级的分 片连接到多个数据库的单个应用程序)。 这篇文章是我喜欢数据库和分布式系统的告白。它主要针对向我一样的程序员,经常接触数据库的应用开发者。我们主要用 Java、Python、或是 Ruby 编码,用来写服务端的应用。本文会覆盖到以下话题:
首先,什么是数据库在这篇文章中,任何接收并存储数据以备将来获取之用的软件就是数据库。这包括了传统的 RDBMS 和 NoSQL 数据库,以及如 Apache Zookeeper 和 Kafka 一样的系统。 CAP 理论CAP 理论。这是继图灵的停机问题和 P≠NP (技术上不可解)后,我最喜欢的不可解问题。CAP 理论表明,任何分布式系统最多只能同时满足 CP(一致性 & 分区容忍性), AP (可用性 & 分区容忍性),或者介于这两者之间。因此,一致性和可用性之间有很有趣的权衡出现。 关于CAP定理几个重要的误解:
分布式系统如前所述,许多现代数据库都以某种方式实现了分布式。推动数据库的分布式化的两个因素:
这两个目标是相互紧密关联的。一般来说,通过增加机器数量来扩展系统会对可用性产生负面影响,因为发生单个机器故障的机会增加了。所以,实现高可用性几乎是可扩展性的先决条件。 正确性和效率正确性和效率两者都重要,而且在分布式数据库中两者也是紧密关联的。 在任何软件中正确性都是重要的,但是对于数据库来说它是必不可少的。因为(1)数据库存储数据,错误的数据在重启后仍然存在。(2)数据库被认为是软件栈 (software stack)中最值得信赖的基础。 数据库是正确的是什么意思呢?许多分布式数据库的代码很难懂,有惊人一致的语义。在这里你可以做一个权衡。在一般情况下,对于效率和可用性的成本来说,更严格的一致性使得编写应用程序更容易。 除了理论之外,还有实现和业务挑战的正确性。分布式系统本质上是复杂的。像 Paxos 这种算法是很难理解和正确实现的。随着系统越来越复杂,更多隐蔽的故障场景会出现。像 Redis 和 ElasticSearch 就承受了由他们分布式系统的非常规设计带来的考验。 除了上述的权衡,效率也是很重要的,因为以前遇到的困难。 我学到越多的底层编码,我就更了解一台机器美秒可以执行多少原始操作(raw operations)。在许多情况下,效率降低了复杂度,使整个系统更简单。事实是分布式系统比底层编码更激发我去追求效率。在给定相同负载的情况下, 我更乐意选择需要更少机器的数据库。 最后,用于机器间协调的计算和延迟开销会是很显著的。总之:运行的部件越少越好。 为了从代码中优化出更多的性能和效率,深入更底层的抽象中是必须的旅程,包括:
它们中产生任何的不协调,都会导致性能不佳。你不必成为一个内核黑客(kernel hacker),但是你需要对这些组件之间的交互有一定高度的了解。 给应用授权由于不同的编程语言都有自己的优缺点,数据库也有自己独特的特点。完全理解它们是很重要的。它可以让你实现高效和复杂的应用程序,同时委托大部分复 杂易错的工作给数据库。 一般来说,如果你没有严格的性能或可用性要求,那么传统 RDBMS 是一个好选择。ACID 的保证是非常强大的,而且工具也很不错。分片 RDBMS 虽然痛苦,但它是很好理解的。MySQL 和 Postgres 是两个普遍的选择。 全文搜索引擎允许你构建高级的索引和搜索功能。集成这些系统后的最终一致性,很少是个问题,因为搜索本身就是一个模糊操作。 Lucene 和它的变种数据库(Solr,ElasticSearch)是普遍的选择。 消息队列和事件处理系统消除了那些很难正确和高效实现的代码。Kafka、Storm、Spark SQL、RabbitMQ 和 Redis 是普遍的选择。 具有跨域复制的数据库使得区域故障切换和高可用性容易地多。在这方面没有很多开源选择,但是 Cassandra 可能是最成熟的一个。 一致性,leader 选举(leader election),以及分布式锁都是很难实现和测试的。不要自己去实现。用 Zookeeper 等,或者 raft 类库。 现在走向细节。数据库本身是一个抽象泄露(leaky abstraction)。他们一般在隐藏底层的复杂性上做了很好的工作,但是忽视其局限性最终会伤到你自己。以下是一些重要的东西:
业务挑战一旦你的软件栈中的一部分,数据库和你的基础架构保证24*7提供不中断服务 。它就引入了独特的业务挑战。 操作数据库就像在海洋中航行。无论何时你遇到了问题,你都要不让数据库下沉的同时解决它,即使是在风暴中心。因此,数据库需要有:
在运行的同时具备以上所有。坦白说,所有数据库在这点上都有缺点。在完全运行的同时允许任何配置都可以修改,是一个艰难的挑战。许多操作需要一个数据库层面的互斥量,额外的系统资源,或者重新启动。例如包括:
此外,你不能简单的替换一个数据库。即使是在同一数据库内迁移数据的任务也不简单。迁移到另一个数据库中更是难上加难,如果不是不可行[4]。一个 应用程序的代码是非常容易逐步铺开并恢复(如果需要的话)的。数据比代码存活地更久。数据模式和存储的数据通常会在多个应用程序之间共享。因此,初始的数 据库系统和对应的数据模型的选择是非常重要的。 最后,数据库总会发生故障。不管你用的是什么平台/服务即架构,有些故障是避免不了的:
对于这些却没有单一的解决方法。操作数据库的艺术真的属于维护一个高 SLA 系统的艺术,但是如果我需要给出一些技巧:
PS:在 Square ,我们有一个超酷的在线数据存储(ODS)团队把这些问题从我们这里抽离出去。 基础构建模块通过数据库提供的抽象真的很神奇。数据提取(ingestion),查询,复制,以及故障恢复都在同一个包里?但是当你习惯了它,你就开始认识到有一些基础的构建模块 —— 在所有数据库之间共享的通用模式和组件。 首先,数据检索降低了以下中的一个:
在本文的最后,我想说的是,电脑并不懂 SQL、索引、联接、或是其他的花哨的装饰。上层的操作需要被翻译成机器能执行的东西。 对于可持久化数据结构,B-trees, hash tables,和 LSM树 (log-structured-merge-trees)都是很普遍的选择。很可能你的数据存储在其中的一个,除非它需要一些特定的查找(例如:地理空间查询)。LSM 树是一种流行的现代的选择,在BigTable,HBase, 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 有他们自己的问题 :
关于作者: 伍翀本文由 伯乐在线 - 伍翀 翻译,sunbiaobiao 校稿。 |