Skip to content

Latest commit

 

History

History
61 lines (38 loc) · 10.7 KB

数据密集型应用设计.md

File metadata and controls

61 lines (38 loc) · 10.7 KB

可靠性、可扩展性、可维护性

为了减少系统的故障率,第一反应通常都是增加单个硬件的冗余度,例如:磁盘可以组建 RAID,服务器可能有双路电源和热插拔CPU,数据中心可能有电池和柴油发电机作为后备电 源,某个组件挂掉时冗余组件可以立刻接管。这种方法虽然不能完全防止由硬件问题导致的 系统失效,但它简单易懂,通常也足以让机器不间断运行很多年。 如果在硬件冗余的基础上进一步引入软件容错机制,那么系统在容忍整个(单台)机器故障 的道路上就更进一步了。这样的系统也有运维上的便利,例如:如果需要重启机器(例如应 用操作系统安全补丁),单服务器系统就需要计划停机。而允许机器失效的系统则可以一次 修复一个节点,无需整个系统停机。

大规模的系统架构通常是应用特定的—— 没有一招鲜吃遍天的通用可扩展架构(不正式的叫 法:万金油(magic scaling sauce) )。应用的问题可能是读取量、写入量、要存储的数 据量、数据的复杂度、响应时间要求、访问模式或者所有问题的大杂烩。举个例子,用于处理每秒十万个请求(每个大小为1 kB)的系统与用于处理每分钟3个请求 (每个大小为2GB)的系统看上去会非常不一样,尽管两个系统有同样的数据吞吐量。

一个良好适配应用的可扩展架构,是围绕着假设(assumption)建立的:哪些操作是常见 的?哪些操作是罕见的?这就是所谓负载参数。如果假设最终是错误的,那么为扩展所做的 工程投入就白费了,最糟糕的是适得其反。在早期创业公司或非正式产品中,通常支持产品 快速迭代的能力,要比可扩展至未来的假想负载要重要的多。

软件的大部分开销并不在最初的开发阶段,而是在持续的维护阶段,包括修复漏 洞、保持系统正常运行、调查失效、适配新的平台、为新的场景进行修改、偿还技术债、添 加新的功能等等。

一个优秀运维团队的典型职责如下(或者更 多): 监控系统的运行状况,并在服务状态不佳时快速恢复服务 跟踪问题的原因,例如系统故障或性能下降 及时更新软件和平台,比如安全补丁 了解系统间的相互作用,以便在异常变更造成损失前进行规避。 预测未来的问题,并在问题出现之前加以解决(例如,容量规划) 建立部署,配置、管理方面的良好实践,编写相应工具 执行复杂的维护任务,例如将应用程序从一个平台迁移到另一个平台 当配置变更时,维持系统的安全性 定义工作流程,使运维操作可预测,并保持生产环境稳定。 铁打的营盘流水的兵,维持组织对系统的了解。

数据模型与查询语言

采用NoSQL数据库的背后有几个驱动因素,其中包括: 需要比关系数据库更好的可扩展性,包括非常大的数据集或非常高的写入吞吐量 相比商业数据库产品,免费和开源软件更受偏爱。 关系模型不能很好地支持一些特殊的查询操作 受挫于关系模型的限制性,渴望一种更具多动态性与表现力的数据模型

JSON表示比多表模式具有更好的局部性(locality)。如果在前面的关系型示例中 获取简介,那需要执行多个查询(通过 user_id 查询每个表),或者在User表与其下属表之 间混乱地执行多路连接。而在JSON表示中,所有相关信息都在同一个地方,一个查询就足够了。

存储ID还是文本字符串,这是个副本(duplication)问题。使用ID的好处是,ID对人类没有任何意义,因而永远不需要改变:ID可以保持不变,即使它 标识的信息发生变化。任何对人类有意义的东西都可能需要在将来某个时候改变——如果这 些信息被复制,所有的冗余副本都需要更新。

网络模型是层次模型的推广。在层次模型的树结构中,每条记录只有一个父节点;在网 络模式中,每条记录可能有多个父节点。网络模型中记录之间的链接不是外键,而更像编程语言中的指针(同时仍然存储在磁盘 上)。访问记录的唯一方法是跟随从根记录起沿这些链路所形成的路径,这就像在n维数据空间中进行导航。

相比之下,关系模型做的就是将所有的数据放在光天化日之下:一个关系(表)只是一个元 组(行)的集合,仅此而已。如果你想读取数据,它没有迷宫似的嵌套结构,也没有复杂的 访问路径。你可以选中符合任意条件的行,读取表中的任何或所有行。你可以通过指定某些 列作为匹配关键字来读取特定行。你可以在任何表中插入一个新的行,而不必担心与其他表的外键关系。

文档数据库有时称为无模式(schemaless),但这具有误导性,因为读取数据的代码通常假 定某种结构——即存在隐式模式,但不由数据库强制执行。一个更精确的术语是读时 模式(schema-on-read)(数据的结构是隐含的,只有在数据被读取时才被解释),相应的 是写时模式(schema-on-write)(传统的关系数据库方法中,模式明确,且数据库确保所 有的数据都符合其模式)。读时模式类似于编程语言中的动态(运行时)类型检查,而写时模式类似于静态(编译时) 类型检查。就像静态和动态类型检查的相对优点具有很大的争议性一样,数据库中模 式的强制性是一个具有争议的话题,一般来说没有正确或错误的答案。

当引入关系模型时,关系模型包含了一种查询数据的新方法:SQL是一种声明式查询语言, 而IMS和CODASYL使用命令式代码来查询数据库。命令式语言告诉计算机以特定顺序执行某些操作。可以想象一下,逐行地遍历代码,评估条 件,更新变量,并决定是否再循环一遍。在声明式查询语言(如SQL或关系代数)中,你只需指定所需数据的模式 - 结果必须符合哪些 条件,以及如何将数据转换(例如,排序,分组和集合) - 但不是如何实现这一目标。数据库 系统的查询优化器决定使用哪些索引和哪些连接方法,以及以何种顺序执行查询的各个部分。

声明式查询语言是迷人的,因为它通常比命令式API更加简洁和容易。但更重要的是,它还隐 藏了数据库引擎的实现细节,这使得数据库系统可以在无需对查询做任何更改的情况下进行 性能提升。

关系模型可以处理多对多关系的简单情况, 但是随着数据之间的连接变得更加复杂,将数据建模为图形显得更加自然。一个图由两种对象组成:顶点(vertices)(也称为节点(nodes) 或实体(entities)), 和边(edges)( 也称为关系(relationships)或弧 (arcs) )。多种数据可以被建模为 一个图形。典型的例子包括: 社交图谱 顶点是人,边指示哪些人彼此认识。 网络图谱 顶点是网页,边缘表示指向其他页面的HTML链接。

存储与检索

索引是从主数据衍生的附加(additional)结构。许多数据库允许添加与删除索引,这不会影 响数据的内容,它只影响查询的性能。维护额外的结构会产生开销,特别是在写入时。

最简单的索引策 略就是:保留一个内存中的哈希映射,其中每个键都映射到一个数据文件中的字节偏移量, 指明了可以找到对应值的位置。当你想查找一个值时,使用哈希映射来查找数据文件中的偏移量,寻找(seek)该位置并读取 该值。这种存储策略非常适合每个键的值经常更新的情况。例如,键可能是视频的 URL,值可能是它播放的次数(每次有人点击播放按钮时递增)。在这种类型的工作负载 中,有很多写操作,但是没有太多不同的键——每个键有很多的写操作,但是将所有键保存 在内存中是可行的。

哈希表索引也有局限性: 散列表必须能放进内存,如果你有非常多的键,那真是倒霉。原则上可以在磁盘上保留一个哈希映射,不幸的是 磁盘哈希映射很难表现优秀。它需要大量的随机访问I/O,当它变满时增长是很昂贵的, 并且散列冲突需要很多的逻辑。

范围查询效率不高。例如,您无法轻松扫描kitty00000和kitty99999之间的所有键——您 必须在散列映射中单独查找每个键。

要求键值对的序列按键排序,我们把这个格式称为排序字符串表(Sorted String Table),简称SSTable。我们还要求每个 键只在每个合并的段文件中出现一次(压缩过程已经保证)。与使用散列索引的日志段相 比,SSTable有几个很大的优势:

1)这种方法就像归并排序算法中使用的 方法一样,读取输入文件,查看每个文件中的第一个键,复制最低键(根据排序顺序)到输出文件,并重复。这产生一个新的合并段文件,也按键排序。

2)为了在文件中找到一个特定的键,你不再需要保存内存中所有键的索引。假设你正在内存中寻找键 handiwork ,但是你不知道段文件中该关键字的确切偏移量。 然而,你知道 handbag 和 handsome 的偏移,而且由于排序特性,你知道 handiwork 必须出现在这两者之间。这意味着您可以跳到 handbag 的偏移位置并从那里扫描,直到 您找到 handiwork (或没找到,如果该文件中没有该键)。

基于 SSTable 储引擎工作如下: 1)写入时,将其添加到内存中的平衡树数据结构(例如,红黑树)。这个内存树有时被称 为内存表(memtable)。

2)当内存表大于某个阈值(通常为几兆字节)时,将其作为SSTable文件写入磁盘。这可以 高效地完成,因为树已经维护了按键排序的键值对。新的SSTable文件成为数据库的最新 部分。当SSTable被写入磁盘时,写入可以继续到一个新的内存表实例。 3)为了提供读取请求,首先尝试在内存表中找到关键字,然后在最近的磁盘段中,然后在 下一个较旧的段中找到该关键字。 有时会在后台运行合并和压缩过程以组合段文件并丢弃覆盖或删除的值。

这个方案效果很好。它只会遇到一个问题:如果数据库崩溃,则最近的写入(在内存表中,但尚未写入磁盘)将丢失。为了避免这个问题,我们可以在磁盘上保存一个单独的日志,每 个写入都会立即被附加到磁盘上。