设计数据密集型应用第一二章

第一章

第一章主要讲解的是应用系统的可靠性,可拓展性,可维护性的概念信息

可靠性

造成错误的原因叫做 故障(fault),能预料并应对故障的系统特性可称为 容错(fault-tolerant)韧性(resilient)。“容错” 一词可能会产生误导,因为它暗示着系统可以容忍所有可能的错误,但在实际中这是不可能的。比方说,如果整个地球(及其上的所有服务器)都被黑洞吞噬了,想要容忍这种错误,需要把网络托管到太空中 —— 这种预算能不能批准就祝你好运了。所以在讨论容错时,只有谈论特定类型的错误才有意义。

注意 故障(fault) 不同于 失效(failure)故障 通常定义为系统的一部分状态偏离其标准,而 失效 则是系统作为一个整体停止向用户提供服务。故障的概率不可能降到零,因此最好设计容错机制以防因 故障 而导致 失效

描述性能

一旦系统的负载被描述好,就可以研究当负载增加会发生什么。我们可以从两种角度来看:

  • 增加负载参数并保持系统资源(CPU、内存、网络带宽等)不变时,系统性能将受到什么影响?
  • 增加负载参数并希望保持性能不变时,需要增加多少系统资源?

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

可维护性

在设计之初就尽量考虑尽可能减少维护期间的痛苦,从而避免自己的软件系统变成遗留系统。为此,我们将特别关注软件系统的三个设计原则:

  • 可操作性(Operability)

    便于运维团队保持系统平稳运行。

  • 简单性(Simplicity)

    从系统中消除尽可能多的 复杂度(complexity),使新工程师也能轻松理解系统(注意这和用户接口的简单性不一样)。

  • 可演化性(evolvability)

    使工程师在未来能轻松地对系统进行更改,当需求变化时为新应用场景做适配。也称为 可伸缩性(extensibility)可修改性(modifiability)可塑性(plasticity)

简单性:管理复杂度

用于消除 额外复杂度 的最好工具之一是 抽象(abstraction)。一个好的抽象可以将大量实现细节隐藏在一个干净,简单易懂的外观下面。一个好的抽象也可以广泛用于各类不同应用。比起重复造很多轮子,重用抽象不仅更有效率,而且有助于开发高质量的软件。抽象组件的质量改进将使所有使用它的应用受益。

例如,高级编程语言是一种抽象,隐藏了机器码、CPU 寄存器和系统调用。 SQL 也是一种抽象,隐藏了复杂的磁盘 / 内存数据结构、来自其他客户端的并发请求、崩溃后的不一致性。当然在用高级语言编程时,我们仍然用到了机器码;只不过没有 直接(directly) 使用罢了,正是因为编程语言的抽象,我们才不必去考虑这些实现细节。

抽象可以帮助我们将系统的复杂度控制在可管理的水平,不过,找到好的抽象是非常困难的。在分布式系统领域虽然有许多好的算法,但我们并不清楚它们应该打包成什么样抽象。

第二章

本章主要介绍了几种数据模型与其对应的查询语言

传统关系型数据库与文档模型

关系型数据库

对象关系不匹配

目前大多数应用程序开发都使用面向对象的编程语言来开发,这导致了对 SQL 数据模型的普遍批评:如果数据存储在关系表中,那么需要一个笨拙的转换层,处于应用程序代码中的对象和表,行,列的数据库模型之间。模型之间的不连贯有时被称为 阻抗不匹配(impedance mismatch)

像 ActiveRecord 和 Hibernate 这样的 对象关系映射(ORM object-relational mapping) 框架可以减少这个转换层所需的样板代码的数量,但是它们不能完全隐藏这两个模型之间的差异。

在一对多的情况中如果并不经常需要通过其中属性逆向查找文档,其场景会更加适用。如果应用程序中的数据具有类似文档的结构(即,一对多关系树,通常一次性加载整个树),那么使用文档模型可能是一个好主意。但如果你的应用程序确实会用到多对多关系,那么文档模型就没有那么诱人了。

例如用一个文档模型来表示一个人的简介

{
  "user_id": 251,
  "first_name": "Bill",
  "last_name": "Gates",
  "summary": "Co-chair of the Bill & Melinda Gates... Active blogger.",
  "region_id": "us:91",
  "industry_id": 131,
  "photo_url": "/p/7/000/253/05b/308dd6e.jpg",
  "positions": [
    {
      "job_title": "Co-chair",
      "organization": "Bill & Melinda Gates Foundation"
    },
    {
      "job_title": "Co-founder, Chairman",
      "organization": "Microsoft"
    }
  ],
  "education": [
    {
      "school_name": "Harvard University",
      "start": 1973,
      "end": 1975
    },
    {
      "school_name": "Lakeside School, Seattle",
      "start": null,
      "end": null
    }
  ],
  "contact_info": {
    "blog": "http://thegatesnotes.com",
    "twitter": "http://twitter.com/BillGates"
  }
}

数据查询语言

命令式语言告诉计算机以特定顺序执行某些操作。可以想象一下,逐行地遍历代码,评估条件,更新变量,并决定是否再循环一遍。

在声明式查询语言(如 SQL 或关系代数)中,你只需指定所需数据的模式 - 结果必须符合哪些条件,以及如何将数据转换(例如,排序,分组和集合) - 但不是如何实现这一目标。数据库系统的查询优化器决定使用哪些索引和哪些连接方法,以及以何种顺序执行查询的各个部分。

MapReduce查询

MapReduce 是一个由 Google 推广的编程模型,用于在多台机器上批量处理大规模的数据【33】。一些 NoSQL 数据存储(包括 MongoDB 和 CouchDB)支持有限形式的 MapReduce,作为在多个文档中执行只读查询的机制。

MapReduce 既不是一个声明式的查询语言,也不是一个完全命令式的查询 API,而是处于两者之间:查询的逻辑用代码片段来表示,这些代码片段会被处理框架重复性调用。它基于 map(也称为 collect)和 reduce(也称为 foldinject)函数,两个函数存在于许多函数式编程语言中。

看了栗子后,觉得他类似一个框架,他将连接等进行了抽象,其具体实现由使用者编写

在 PostgreSQL 中,你可以像这样表述这个查询:

SELECT
  date_trunc('month', observation_timestamp) AS observation_month,
  sum(num_animals)                           AS total_animals
FROM observations
WHERE family = 'Sharks'
GROUP BY observation_month;

date_trunc('month',timestamp) 函数用于确定包含 timestamp 的日历月份,并返回代表该月份开始的另一个时间戳。换句话说,它将时间戳舍入成最近的月份。

这个查询首先过滤观察记录,以只显示鲨鱼家族的物种,然后根据它们发生的日历月份对观察记录果进行分组,最后将在该月的所有观察记录中看到的动物数目加起来。

同样的查询用 MongoDB 的 MapReduce 功能可以按如下来表述:

db.observations.mapReduce(function map() {
        var year = this.observationTimestamp.getFullYear();
        var month = this.observationTimestamp.getMonth() + 1;
        emit(year + "-" + month, this.numAnimals);
    },
    function reduce(key, values) {
        return Array.sum(values);
    },
    {
        query: {
          family: "Sharks"
        },
        out: "monthlySharkReport"
    });
  • 可以声明式地指定一个只考虑鲨鱼种类的过滤器(这是 MongoDB 特定的 MapReduce 扩展)。
  • 每个匹配查询的文档都会调用一次 JavaScript 函数 map,将 this 设置为文档对象。
  • map 函数发出一个键(包括年份和月份的字符串,如 "2013-12""2014-1")和一个值(该观察记录中的动物数量)。
  • map 发出的键值对按键来分组。对于具有相同键(即,相同的月份和年份)的所有键值对,调用一次 reduce 函数。
  • reduce 函数将特定月份内所有观测记录中的动物数量相加。
  • 将最终的输出写入到 monthlySharkReport 集合中。

图数据模型

如我们之前所见,多对多关系是不同数据模型之间具有区别性的重要特征。如果你的应用程序大多数的关系是一对多关系(树状结构化数据),或者大多数记录之间不存在关系,那么使用文档模型是合适的。

但是,要是多对多关系在你的数据中很常见呢?关系模型可以处理多对多关系的简单情况,但是随着数据之间的连接变得更加复杂,将数据建模为图形显得更加自然。

一个图由两种对象组成:顶点(vertices,也称为 节点,即 nodes,或 实体,即 entities),和 (edges,也称为 关系,即 relationships,或 ,即 arcs)。多种数据可以被建模为一个图形。典型的例子包括:

  • 社交图谱

    顶点是人,边指示哪些人彼此认识。

  • 网络图谱

    顶点是网页,边缘表示指向其他页面的 HTML 链接。

  • 公路或铁路网络

    顶点是交叉路口,边线代表它们之间的道路或铁路线。

Cypher 查询语言

Cypher 是属性图的声明式查询语言,为 Neo4j 图形数据库而发明。

他在查询图相关时更加简单。能够使用简洁命令即可进行相关查询

三元组存储

在三元组存储中,所有信息都以非常简单的三部分表示形式存储(主语谓语宾语)。例如,三元组 (吉姆, 喜欢, 香蕉) 中,吉姆 是主语,喜欢 是谓语(动词),香蕉 是对象。

三元组的主语相当于图中的一个顶点。而宾语是下面两者之一:

  1. 原始数据类型中的值,例如字符串或数字。在这种情况下,三元组的谓语和宾语相当于主语顶点上的属性的键和值。例如,(lucy, age, 33) 就像属性 {“age”:33} 的顶点 lucy。
  2. 图中的另一个顶点。在这种情况下,谓语是图中的一条边,主语是其尾部顶点,而宾语是其头部顶点。例如,在 (lucy, marriedTo, alain) 中主语和宾语 lucyalain 都是顶点,并且谓语 marriedTo 是连接他们的边的标签。

新的非关系型 “NoSQL” 数据存储在两个主要方向上存在分歧:

  1. 文档数据库 的应用场景是:数据通常是自我包含的,而且文档之间的关系非常稀少。
  2. 图形数据库 用于相反的场景:任意事物都可能与任何事物相关联。