设计数据密集型应用—— 数据系统的未来(12 下)

1. 写在最前面

断断续续的终于写到了这本书的最后一篇,真正学习到的知识点一时之间没有办法马上记忆起来。但是那又有什么关系呢?

学习的乐趣,除了结果应该还有过程的吖。

2. 将事情做正确

对于只读数据的无状态服务,出问题也没什么大不了的:可以修复该错误并重启服务,而一切都恢复正常。像数据库这样的有状态系统就没那么简单了:它们被设计为永远记住事务(或多或少),所以如果出现问题,这种错误也会永远持续下去,这意味着它们需要更仔细的被设计。

希望构建可靠且正确的应用——即面对各种故障,程序的语义也能被很好地定义与理解。约四十年来,原子性、隔离性和持久性等事务特性一致是构建正确应用的首选工具。

注:事务并没有描述的那般可靠,弱隔离性在使用时常常带来很多困惑

事务在某些领域被完全抛弃,并被提供更好性能与可伸缩性的模型取代,但其语义更复杂。一致性经常被谈起,但其定义并不明确。

注:有些人断言我们应当为了高可用而「拥抱弱一致性」,但却对这些概念实际上意味着什么缺乏清晰的日志。

对于如此重要的话题,我们的理解,以及我们的工程方法却是惊人地薄弱。例如,确定在特定事务隔离等级和复制配置下运行特定应用是否安全是非常困难的。通常简单的解决方案似乎在低并发的情况下工作正常,并且没有错误,但在要求更高的情况下却会出现许多微秒的错误。

注:Kyle Kingsbury 的 Jepsen 实验标出了一些产品声称的安全保证与其在网络问题与崩溃时的实际行为之间 的明显差异。即使像数据库这样的基础设施产品没有问题,应用代码仍然需要正确使用它们提供的功能才行,如果配置很难理解,这是很容易出错的(这种情况下指的是弱隔离级别,法定人数配置等)。

如果你的应用可以容忍偶尔的崩溃,以及不可预料的方式损坏或丢失数据,那生活就要简单得多。另一方面,如果需要更强的正确性保证,那么可串行化与原子提交就是久经考验的方法,但它们是有代价的:它们通常只在单个数据中心工作,并限制了系统能够实现的规模与容错性。

虽然传统的事务方法并没有走远,但使应用正确而灵活地处理错误方面上,事务也不是最后一个可以谈的。后面的小结将会讨论一些在数据流架构中考量正确性的方式。

2.1 数据库的端到端原则

因为一个应用程序使用了具有相对较安全属性的数据系统(例如可串行化的事务),并不意味着就可以保证没有数据丢失或损坏。例如,如果某个应用有个 Bug,导致它写入不正确的数据,或者从数据库中删除数据,那么可串行化的事务也救不了你。

虽然不变性很有用,但它本省并非万灵药。让我们来看一个可能发生的、非常微秒的数据损坏案例。

2.1.1 正好执行一次操作

在「容错」中,见到了**恰好一次(或等效一次)**语义的概念。如果在处理消息时出现问题,可以选择丢弃消息或重试。如果重试,就会有这种风险:第一次实际上成功了,只不过你没有发现。结果这个消息被处理了两次

处理两次是数据损坏的一种形式:为同样的服务向客户收费两次(收费太多)或增长计数器两次(夸大指标)都不是我们想要的。在这种情况下,恰好一次意味着安排计算,使得最终效果与没有发生错误的情况一样,即使操作实际上因为某种错误而重试。

最有效的方法之一是使操作幂等;即确保它无论是执行一次还是执行多次都具有相同的效果。但是,将不是天生幂等的操作变为幂等的操作需要一些额外的保证:

  • 维护一些额外的元数据(例如更新了值的操作 ID 集合)
  • 从一个节点故障切换至另一个节点时做好防护
2.1.2 抑制重复

除了流处理之外,其他许多地方也需要抑制重复的模式。例如,TCP 使用了数据包上的序列号,以便接收方可以将它们正确排序,并确定网络上是否有数据丢失或重复。在将数据交付引用前,TCP 协议栈会重新传输任何丢失的数据包,也会移除任何重复的数据包。

注:以上例子的重复抑制仅适用于单条 TCP 连接的场景中。

思考:假设 TCP 连接是一个客户端与数据库的连接,并且在执行事务。在许多数据库中,事务是绑定在客户端连接上的(如果客户端发送了多个查询,数据库就知道它们属于同一个事务,因为它们是在同一个 TCP 连接上发送的)。如果客户端在发送 COMMIT之后并在从数据库服务器收到响应之前遇到网络中断与连接超时,客户端时不知道事务是否已经被提交的。

BEGIN TRANSACTION;
    UPDATE accounts SET balance = balance + 11.00 WHERE account_id = 1234;
    UPDATE accounts SET balance = balance - 11.00 WHERE account_id = 4321;
COMMIT;

上述的例子中的事务不是幂等的,可能会发生转了 22 而不是期望的 11。因此,尽管上述是一个事务原子性的标准样例,但它实际上并不正确,而真正的银行并不会这样办事。

注:客户端可以重连到数据库并重试事务,但现在已经处于 TCP 重复抑制的范围之外了。

两阶段提交协议会破坏 TCP 连接与事务之间的 1:1 映射,因此它们必须在故障后允许事务协调器重连到数据库,告诉数据库将存疑的事务提交还是中止。这足以确保事务只被恰好执行一次吗?

不幸的是,并不能,即使可以抑制数据库客户端与服务器之间的重复事务,仍需要考虑终端用户设备与应用服务器之间的网络。例如,如果终端终于的客户端是 Web 浏览器,则它可能会使用 HTTP POST 请求向服务器提交指令。也许用户正处于一个信号微弱的蜂窝数据网络连接中,它们成功地发送了 POST,但却在能够从服务器接收响应之前没了信号。

在这种情况下,可能会向用户显示错误消息,而他们可能会手动重试。从 Web 服务器的角度来看,重试是一个独立的请求;从数据库的角度看,这是一个独立的事务。

在以上条件下除重机制无济于事。

2.1.3 操作标识符

要在通过几跳的网络通信上使操作具有幂等性,仅仅依赖数据库提供的事务机制是不够的——需要考虑**端到端(end-to-end)**的请求流。例如,可以为操作生成一个唯一的标识符(UUID),并将其作为隐藏表单字段包含在客户端应用中,或通过计算所有表单相关字段的散列来生成 ID。如果 Web 浏览器提交了两次 POST 请求,这两个请求将具有相同的操作 ID。然后,可以将该操作 ID 一路传递到数据库,并检查是否曾经使用给定 ID 执行过一个请求。

使用唯一 ID 来抑制重复请求

ALTER TABLE requests ADD UNIQUE (request_id);

BEGIN TRANSACTION;
    INSERT INTO requests
        (request_id, from_account, to_account, amount) 
        VALUES('0286FDB8-D7E1-423F-B40B-792B3608036C', 4321, 1234, 11.00);
    UPDATE accounts SET balance = balance + 11.00 WHERE account_id = 1234;
    UPDATE accounts SET balance = balance - 11.00 WHERE account_id = 4321;
COMMIT;

依赖于 request_id 列上的唯一约束。如果一个事务尝试插入一个已经存在的 ID,那么 INSERT失败,事务被中止,使其无法生效两次。即使在较弱的隔离级别下,关系数据库也能正确地维护唯一性约束。

2.1.4 端到端的原则

抑制重复事务的这种情况只是一个更普遍的原则的一个例子,这个原则被称为 端到端原则,具体:

只有在通信系统两端应用的知识与帮助下,所讨论的功能才能完全地正确地实现。因此将这种质疑的功能做为通信系统本身的功能是不可能的。

在我们的例子中所讨论的功能是重复抑制。我们看到 TCP 在 TCP 连接层次抑制了重复的数据包,一些流处理器在消息处理层次提供了所谓的恰好一次语义,但这些都无法阻止当一个请求超时时,用户亲自提交重复的请求。 TCP、数据库事务、以及流处理器本身并不能完全排除这些重复。解决这个问题需要一个端到端的解决方案:从终端用户的客户端一路传递到数据库的事务标识符。

**端到端的原则也适用于检查数据的完整性:**以太网、TCP 和 TLS 中内置的校验和检测网络中数据包的损坏情况,但是它们无法检测到由连接两端发送/接收软件中 Bug 导致的损坏。或数据存储所在磁盘上的损坏。想要捕获数据所有可能的损坏来源,需要端到端的校验和。

类似的原则也适用于加密:家庭 WIFI 网络上的密码可以防止人们窃听你的 WIFI 流量,但无法组织互联网上其他地方攻击者的窥探;客户端与服务器之间的 TLS/SSL 可以组织网络攻击者,但无法阻止恶意服务器。

注:只有端到端的加密和认证可以防止所有这些事情。

2.1.5 在数据系统中应用端到端的思考

回到最初的论点:仅仅因为应用使用了提供相对较强安全属性的数据系统,例如可串行化的事务,并不意味着应用的数据就不会丢失或损坏了。应用本身也需要采取端到端的措施,例如除重。

这实在是一个遗憾,因为容错机制很难弄好。

低层次的可靠机制运行的相当好,因而剩下的高层级错误基本很少出现。如果能将这些剩下的高层级机制打包成抽象,而应用不需要再去操心,那该多好——但恐怕还没有找到这一正确的抽象。

长期以来,事务被认为是一个很好的抽象,相信它们确实是有用的。正如第七章导言中所讨论的,它们将各种可能的问题(并发写入、违背约束、崩溃、网络中断、磁盘故障)合并为两种结果:提交或中止。

注:这是对编程模型的巨大简化,但是恐怕还不够。因为事务的代价是昂贵的,尤其是涉及异构存储的分布式事务。

出于这些原因,探索对容错的抽象是很有价值的。它使提供应用特定的端到端的正确性属性变得简单,而且还能在大规模分布式环境中提供良好的性能与运维特性。

2.2 强制约束

让我们思考一下在分拆数据库上下文中的正确性?

端到端的除重可以通过客户端一路透传到数据库的请求 ID 实现,那么其他类型的约束呢?

比如:唯一性约束,需要强制实施唯一性的应用功能例子:用户名或邮件地址必须唯一标识用户,文件存储服务不能包含多个重名文件,两个人不能再航班或剧院预定同一个座位。

其他类似的约束也非常类似:例如,确保账户余额永远不变为负数,确保不会超卖库存,或者会议室没有重复的预定。

注:执行唯一性约束的技术通常也可以用于这些约束。

2.2.1 唯一性约束需要达成共识

在第九章中提到过,在分布式环境中,强制执行唯一性约束需要共识:如果存在多个具有相同值的并发请求,则系统需要决定冲突中的哪一个被接受,并拒绝其他违背约束的操作。

注:达成共识最常见方式是使单个节点作为领导,并使其负责所有决策。只要不介意所有请求都挤过单个节点——(比如:对于延迟敏感的程序这种就不可以),只要该节点没有失效,系统就能正常工作。

​ 如果需要容忍领导者失效,那么就又回到了共识问题。

唯一性检查可以通过对唯一性字段分区做横向伸缩。例如,如果需要通过请求 ID 确保唯一性,可以确保所有具有相同请求 ID 的请求都被路由到同一分区。如果需要让用户名是唯一的,则可以按用户名的散列值做分区。

注:异步多主复制排除在外,因为可能发生不同主库同时接受冲突写操作的情况,因此这些值不再是唯一的。如果想要立刻拒绝任何违背约束的写入,同步协调是无法避免的

2.2.2 基于日志消息传递中的唯一性

日志确保所有消费者以相同的顺序读取消息——这种保证在形式上被称为全序广播等价于共识。在使用基于日志的消息传递的分拆数据库方法中,可以使用非常类似的方法来执行唯一性约束。

注:流处理器在单个线程上依次消费单个日志分区中的所有消息。因此,如果日志是按需要确保唯一的值做的分区,则流处理器可以无歧义地、确定性地决定几个冲突操作中的哪一个先到达。

思考:在多个用户尝试宣告相同用户名的情况下?

  • 每个对用户名的请求都被编码为一条消息,并追加到按用户名散列值确定的分区。
  • 流处理器一次读取日志中的请求,并使用本地数据库来追踪哪些用户名已经被占用。
    • 对于可用的用户名,则向输出流发送一条成功的消息
    • 对于所有不可以用的用户名,则向输出流发送一条拒绝的消息
  • 请求用户名的客户端监视输出流,等待与其请求相对应的成功或拒绝消息

注:该算法基本上与「使用全序广播实现线性一致的存储」中的算法相同。它可以简单地通过增加分区数伸缩至较大的请求吞吐量,因为每个分区都可以被独立处理。

​ 该方法不仅适用于唯一性约束,而且适用于许多其他类型的约束。

2.2.3 多分区请求处理

当涉及多个分区时,确保操作以原子方式执行且同时满足约束就变得很有趣了。

思考:可能有三个分区:一个包含请求 ID,一个包含收款人账户,另一个包含付款人账户。没有理由把这三种东西放入同一个分区,因为它们都是互相独立的。

在数据库的传统方法中,执行此事务需要跨全部三个分区进行原子提交,就这些分区上的所有其他事务而言,这实质上是将该事务嵌入一个全序。而这样就要求跨分区协调,不同的分区无法再独立地进行处理,因此吞吐量很可能受到影响。

但事实证明,使用分区日志可以达到等价的正确性而无需原子提交:

  1. 从账户 A 向账户 B 转账的请求由客户端提供一个唯一的请求 ID,并按请求 ID 追加写入相应日志分区。

  2. 流处理器读取请求日志。对于每个请求消息,它向输出流发出两条消息:

    • 付款人账户 A 的借记指令(按 A 分区)
    • 收款人账户 B 的贷记指令(按 B 分区)

    被发出的消息中会带有原始的请求 ID

  3. 后续处理器消费借记/贷记指令流,按照请求 ID 除重,并将变更应用至账户余额。

步骤1、2 是必要的,如果客户直接发送借记/贷记指令,则需要在这两个分区之间进行原子提交,以确保两者要么都发生或都不发生。为了避免对分布式事务的需要:

  • 将请求持久化记录为单条消息
  • 从单条消息衍生出借记/贷记指令

在几乎所有数据系统中,单对象写入都是原子的,因此请求要么出现在日志中,要么就不出现,无需多分区原子提交。

注:如果流处理器在步骤 2 中崩溃,则它会从上一个存储点恢复处理。这样做,它不会跳过任何请求消息,但可能会多次处理请求并产生重复的借记/贷记指令。但由于它是确定性的,因此它只是再次生成相同的指令,而步骤 3 中处理器可以使用端到端请求 ID 轻松地对其除重。

我似乎好像大概可能懂得了银行转账的原理

通过将多分区事务分解为两个不同分区方式的阶段,并使用端到端的请求 ID,实现了分布式事务同样的目的,即使再出现故障时,也可容错。

2.3 及时性与完整性

事务的一个便利属性是,它们通常是线性一致的,写入者会等到事务提交,而之后写入立刻对所有读者可见。

思考:为了方便抽象化,我们把一个操作拆分为跨越多个阶段的流处理器时,但真实操作并非如此。

日志的消费者在设计上就是异步的,因此发送不会等其他消息被消费者处理完。但是,客户端等待特定输出流中的特定消息是可能的。这正是「基于消息传递中的唯一性」一节中检查唯一性约束时所做的事。

注:唯一性检查的正确性不取决于消息发送者是否等待结果。等待的目的仅仅是同步通知发送者唯一性检查是否成功。但该通知可以与消息处理的结果相解耦。

2.3.1 及时性

及时性意味着确保用户观察到系统的最新状态。

CAP 定理使用线性一致性意义上的一致性,这是实现及时性的强有力方法。像写后读这样及时性更弱的一致性也很有用。

注:前文提到过,如果用户从陈旧的数据副本中读取数据,它们可能会观察到系统处于不一致的状态。但这种不一致是暂时的,而最终会通过等待与重试简单地得到解决。

2.3.2 完整性

完整性意味着没有损坏:即没有数据丢失,也没有矛盾或错误的数据。尤其是如果某些衍生数据集是作为底层数据之上的视图而维护的。这种衍生必须是正确的。

注:数据库索引必须正确地反映数据库的内容——缺失某些记录的索引等于无用的索引

如果完整性被违背,这种不一致是永久的:在大多数情况下,等待与重试并不能修复数据库损坏。相反的是,需要显式地检查与修复。

注:在 ACID 事务的上下文中,一致性通常被理解为某种特定于应用的完整性概念。原子性和持久性是保持完整性的重要工具。

请记住口号,违反及时性,「最终一致性」;违反完整性,「永无一致性」。

注:在大多数应用中,完整性比及时性重要得多。违反及时性可能令人困惑,但违反完整性的结果可能是灾难的。

​ 比起银行扣款延迟来说,扣错款是不能被接受的。

2.3.3 数据流系统的正确性

ACID 事务通常既提供及时性(例如线性一致性)也提供完整性保证(例如原子提交)。因此如果从 ACID 事务的角度来看待应用的正确性,那么及时性与完整性的区别无关紧要。

另一方面,对于基于事件的数据流系统而言,它们的一个有趣特性就是将及时性与完整性分开。在异步处理事件流时不能保证及时性,除非显示构建一个在返回之前明确等待特定消息到达的消费者。但完整性实际上才是流处理系统的核心。

注:恰好一次或等效一次语义是一种保持完整性的机制。如果事件丢失或者生效两次,就有可能违背数据系统的完整性。因此在出现故障时,容错消息传递与重复抑制对于维护数据系统的完整性是很重要的。

在上一节看到的那样,可靠的流处理系统可以在无需分布式事务与原子提交协议的情况下保持完整性,这意味着它们有潜力达到与后者相当的正确性,同时还具备好得多的性能与运维稳健性。为了达到这种正确性,组合使用多种机制:

  • 将写入操作的内容表示为单条消息,从而可以轻松地被原子写入——(ps 与事件溯源搭配效果更佳
  • 使用与存储过程类似的确定性衍生函数,从这一消息中衍生出所有其他状态变更
  • 将客户端生成的请求 ID 传递通过所有的处理层次,从而允许端到端的除重,带来幂等性。
  • 使消息不可变,并允许衍生数据能被随时重新处理,这使从错误中恢复更加容易。

注:以上机制的组合,将会是构建容错应用的一个非常有前景的方向。

2.3.4 宽松地解释约束

如前所述,执行唯一性约束需要共识,通常通过在单个节点中汇集特定分区中的所有事件来实现。如果想要传统的唯一性约束形式,这种限制是不可避免的,流处理也不例外。

然而另一个需要了解的事实是,许多真实世界的应用实际上可以摆脱这种形式,接受弱得多的唯一性:

  • 如果两个人同时注册了相同的用户名或预定了相同的座位,可以给其中一个人发消息道歉,并要求他们选择一个不同的用户名。这种纠正错误的变化称为补偿性事务

  • 如果客户订购的物品杜宇仓库中的物品,可以下单补仓,并为延误向客户道歉。道歉工作流无论如何已经成为商业过程中的一部分,对于库存物品数目添加线性一致性的约束就没有必须要了

  • 与之类似,许多航空公司都会超卖机票,因为一些旅客可能会错过航班;许多旅馆也会超卖客房,因为部分客人可能会取消预定、在这些情况下,处于商业原因而故意违反「一人一座」的约束;当需要超过供给的情况出新时,就会进入补偿流程。

  • 如果有人从账户超额取款,银行可以向他们收取透支费用,并要求他们偿还欠款。通过限制每天的提款总额,银行的风险是有限的。

道歉的成本是否能 接受是一个商业决策。如果可以接受的话,在写入数据之前检查所有约束的传统模型反而会带来不必要的限制,而线性一致性的约束也不是必须的。乐观写入,时候检查可能是一种合理的选择。

注:可以在做一些万户成本高昂的事件前确保有相关的验证,但这并不意味着写入数据之前必须先进行验证。

2.3.5 无协调数据系统

现在已经有了两个结论:

  • 数据流系统可以维持衍生数据的完整性保证,而无需原子提交、线性一致性或同步的跨分区协调。
  • 虽然严格的唯一性约束要求及时性和协调,但许多应用实际上可以接受宽松的约束:只要整个过程保持完整性,这些约束可能会被违反并在稍后被修复。

数据流系统可以为许多应用提供无需协调的数据管理服务,且仍能给出很强的完整性保证。这种无协调的数据系统有着很大的吸引力:比起需要执行同步协调的系统,它们能达到更好的性能与更强的容错能力。

注:但是通过数据流的方式延迟似乎是不可控的。

举个例子,系统可以使用多个领导者配置运维,跨越多个数据中心,在区域间异步复制。任何一个数据中心都可以持续独立运行,因为不需要同步跨区域协调。这样的系统的及时性保证会很弱——如果不引入协调它是不可能线性一致的——但它仍然可以提供有力的完整性保证。

注:可串行化事务作为维护衍生状态的一部分是有用的,但它们只能在小范围内运行。异构分布式事务不是必需的。

​ 同步协调仍然可以在需要的地方引入,如果只是应用的一小部分需要它,没必要让所有操作都付出协调的代价。

2.4 信任但验证

所有关于正确性、完整性和容错的讨论都基于一些假设,假设某些事情可能会出错,但其他事情不会。

注:将这些假设称为系统模型

比如,我们应该假设进程可能会崩溃、机器可能突然断电、网络可能会任意延迟或丢弃消息。但是我们也能假设写入磁盘的数据在执行 fsync后不会丢弃,内存中的数据没有损坏,而 CPU 的乘法指令总是能返回正确的结果。

注:这些假设是相当合理的,因为大多数时候它们都是成立的,如果不得不经常担心计算机出错,那么基本上寸步难行。在传统上,系统模型采用二元方法处理故障:我们假设有些事情可能会发生,而其他事情永远不会发生。

​ 实际上,这更像是一个概率问题:有些事情更有可能,其他事情不太可能。问题在于违反我们假设的情况是否经常发生,以至于我们可能在实践中遇到它们。

​ 即使是随机为翻转在现代硬件上非常罕见,但也是有可能存在的

2.4.1 维护完整性,尽管软件有 Bug

除了这些硬件问题之外,总是存在软件 Bug 的风险,这些错误不会被较低层次的网络、内存或文件系统校验和所捕获。

注:即使广泛使用的数据库软件也有 Bug:即使像 MySQL 与 PostgreSQL 这样稳健、口碑良好、多年来被许多人充分测试过的软件,可能也存在 Bug。

​ 比如 MySQL 未能正确维护唯一约束,已经 PostgreSQL 的可串行化隔离等级存在特定的写偏斜异常。

对于应用代码,应该假设会有更多的错误,因为绝大多数应用代码经受的评审与测试远远无法与数据库的代码相比。许多应用甚至没有正确使用数据库提供的用于维持完整性的功能更,例如外键或唯一性约束。

注:数据库以一致的状态启动,而事务将其从一个一致状态转换至另一个一致的状态。因此,我们期望数据库始终处于一致状态。然而,只有当你假设事务没有 Bug 时,这种想法才有意义。

2.4.2 不要盲目信任承诺

由于硬件和软件并不总是符合我们的理想,所以数据损坏无可避免。

注:我们至少应该有办法查明数据是否已经损坏,以便我们能够修复它,并尝试追查错误的来源。检查数据完整性称为 审计

可审计性在财务中是非常非常重要的,因为每个人都知道错误总会发生,我们也认为能够检测和解决问题是合理的需求。审计不仅仅适用于财务应用程序。

注:成熟的系统同样倾向于考虑不太可能的事情出错的可能性,并管理这种风险。例如,HDFS 和 AWS S3 等大规模存储系统并不完全信任磁盘:它们运行后台进程持续回读文件,并将其与其他副本进行必将,并将文件从一个磁盘移动到另一个,以便降低静磨损坏的风险。

如果你想确保你的数据仍然存在,必须真正读取它并进行检查。大多数时候它们仍然会在那里,但如果不是这样,你一定想尽早知道答案,而不是更晚。

2.4.3 验证的文化

像 HDFS 和 S3 这样的系统仍然需要假设磁盘大部分时间都能正常工作——这是一个合理的假设。

注:这也它们始终能正常工作的假设并不相同。

然而目前还没有多少系统采用这种「信任但验证」的方式来持续审计自己。许多人认为正确性保证是绝对的,并且没有为罕见的数据结构损坏的可能性做准备。

注:想到了一个「以己之矛,攻己之盾」的想法,不严重的损失是可以接受的是不是就可以不验证了呢?见前文的飞机票超卖想法。

ACID 数据库的文化有可能会导致我们忽略可审计性。由于我们所信任的技术在大多数情况下工作的很好,通常会认为审计机制并不值得投资。

注:在 NoSQL 的旗帜下,更弱的一致性保证成为常态,更不成熟的存储技术越来越被广泛使用。但是由于审计机制还没有被开发出来,尽管这种方式越来越危险,我们仍然在过于乐观的基础上构建应用。

2.4.4 为可审计性而设计

如果一个事务在数据库中改变了多个对象,在这一事实发生后,很难说清这个事务到底意味着什么。即使你捕获了事务日志,各种表的插入、更新和删除操作并不一定能清楚地表明为什么要执行这个变更。

注:决定这些变更的是应用逻辑中的调用,而这一应用逻辑稍纵即逝,无法重现。

相比之下,基于事件的系统可以提供更好的可审计性。在事件溯源方法中,系统的用户输入被表示为一个单一不可变事件,而任何导致的状态变更都衍生自该事件。衍生可以实现为具有确定性与可重复性,因而相同的事件日志通过相同版本的衍生代码时,会导致相同的状态变更。

注:显示处理数据流可以使数据的「来龙去脉」更加清晰,从而使完整性检查更具可行性。

  1. 对于事件日志,我们可以使用散列来检查事件存储有没有被破坏。
  1. 对于任何衍生状态,我们可以重新运行从事件日志中衍生它的批处理器与流处理器,以检查是否获得相同的结果。

具有确定性且定义良好的数据流,也使调试与跟踪系统的执行变得容易。以便确定它为什么做了某些事情。如果出现意想之外的事情,那么重现导致意外的事件能够诊断故障原因。

2.4.5 端到端原则重现

如果我们不能完全相信系统的每个组件都不会损坏——每个硬件都没缺陷、每个软件都没有 Bug——那我们至少必须定期检查数据的完整性。

注:如果我们不检查,就不能发现损坏,直到无可挽回地导致对下游的破坏,那时候再去追中问题就要难得多。

检查数据系统的完整性,最好是以端到端的方式进行:我们能在完整性检查中涵盖的系统越多,某些处理阶段中出现不被察觉损坏的几率就越小。如果我们能检查整个衍生数据管道端到端的正确性,那么沿着这一路径的任何磁盘、网络、服务以及算法的正确性检查都隐含在其中了。

2.4.6 用于可审计数据系统的工具

目前,将可审计性作为顶层关注点的数据系统并不多。一些应用实现了自己的审计机制,例如将所有变更记录到单独的审计表中,但是确保审计日志与数据库状态的完整性仍然是很困难的。可以定期使用硬件安全模块对事务日志进行签名来防止篡改,但这无法保证正确的事务一开始就能进入日志中。

使用密码学工具来证明系统的完整性是十分有趣的,这种方式对于宽泛的硬件与软件问题,甚至是潜在的恶意行为都很稳健有效。加密货币,区块链,以及诸如比特币、以太坊、Ripple、Stellar 的分布式账本技术已经迅速出现在这一领域。

注:从数据系统的角度来看,它们包含了一些有趣的想法。实质上,它们是分布式数据库,具有数据模型与事务机制,而不同副本可以由互相不信任的组织托管。副本不断检查其他副本的完整性,并使用共识协议对应当执行的事务达成一致。

密码学审计与完整性检查通常依赖默克尔树,这是一颗散列值的树,能够用于高效地证明一条记录出现在一个数据集中

注:证书透明性也是依赖默克尔树的安全技术,用来检查 TLS/SSL 证书的有效性。

那些在证书透明度与分布式账本中使用的完整性检查和审计算法,将会在通用数据系统中得到越来越多的防范应用。要使得这些算法对于没有密码学审计的系统同样可伸缩,并尽可能降低性能损失还需要很多工作。

3. 做正确的事

在本书中,考察了各种不同的数据系统架构,评价了它们的优点与缺点,并探讨了构建可靠、可伸缩、可维护的应用的技术。但是,我们忽略了讨论中的一个重要而基础的部分。

每个系统都服务于一个目的;采用的每个举措都会同时产生期望的后果与意外的后果。

注:这个目的可能只是简单地赚钱,但其对世界的影响可能会远远超出最初的目的。我们建立这些系统的工程师,有责任去仔细考虑这些后果,并有意识地决定,我们希望生活在怎样的世界中。

我们将数据当做一种抽象的东西来讨论,但请记住,许多数据集都是关于人的:他们的行为,他们的兴趣,他们的身份。对待这些数据,必须怀着人性与尊重。用户也是人类,人类的尊严是至关重要的。

注:软件开发越来越多地涉及重要的道德抉择。有一些指导原则可以帮助软件工程师解决这些问题,例如 ACM 的软件工程道德规范与专业实践,但实践中很少会讨论这些,更不用说应用与强制执行。

技术本身并无好坏之分——关键在于它被如何使用,以及它如何影响人们。这对机械这样的武器是成立的,而搜索引擎这样的软件系统与之类似。软件工程师仅仅专注于技术而忽视其后果是不够的:道德责任也是我们的责任。

3.1 预测性分析

举个例子,预测性分析是「大数据」炒作的主要内容之一。

  • 使用数据分析预测天气或疾病传播是被允许的
  • 预测一个罪犯是否可能再犯,一个贷款申请人是否有可能违约,或者一个保险客户是否可能进行昂贵的索赔,则是另外一码事。这会直接影响到个人的生活。

随着算法决策变得越来越普遍,被某种算法(准确地或错误地)标记为有风险的某人可能会遭受大量这种「NO」的决策。系统性地被排除在工作、航旅、保险、租赁、金融服务,以及其他社会关键领域之外。这是一种对个体自由的极大约束,因此被称为「算法监狱」。

注:刑法司法系统可以为有罪的犯人做无罪推定(直到被证明是有罪的)。另一方面,自动化系统可以系统地,任意地将一个人排除在社会参与之外,不需要任何有罪的证明,而且几乎没有申诉的机会。

3.1.1 偏见与歧视

人们希望根据数据做出决定,而不是通过人的主观评价与直觉,希望这样能更加公平,并给予传统体制中经常被忽略的人更多的机会。

注:算法做出的决定不一定比人类更好或更差。每个人都可能有偏见,即使他们主动抗拒这一点。

当我们开发预测性分析系统时,不是仅仅用软件通过一系列 IF ELSE 规则将人类的决策过程自动化,那些规则本身甚至都是从数据中推断出来的。但这些系统学习的模型是个黑盒:即使数据中存在一些相关性,我们可能也压根不知道为什么。

注:如果算法的输入中存在系统性的偏见,则系统很有可能会在输出中学习并放大这种偏见。

预测性分析系统只是基于过去进行推断;如果过去是歧视性的,它们就会将这种歧视归纳为规律。如果我们希望未来比过去更好,那么就需要道德想象力,而这是只有人类才能提供的东西。数据与模型应该是我们的工具,而不是我们的主人。

3.1.2 责任与问责

自动决策引发了关于责任与问责的问题?

如果一人犯了错,他可以被追责,受决定影响的人可以申诉。算法也会犯错误,但是无果它们出错,谁来负责?

  • 当一辆自动驾驶汽车引发事故时,谁来负责?
  • 如果自动信用评分算法系统性地歧视特定种族或宗教的人,这些人是否有任何追索权?
  • 如果机器学习系统的决定要受到司法审查,你能向法官解释算法是如何做出决定的吗?

收集关于人的数据并进行决策,信用评级机构是一个很经典的例子。不良的信用评分会使生活变得更艰难,但至少信用分通常是基于个人实际的借款历史记录,而记录中的任何错误都能被纠正。然而,基于机器学习的评分算法通常会使用更宽泛的输入,并且更不透明;因而很难理解特定决策是怎样作出的,以及是否有人被不公正地,歧视性地对待。

很多数据本质上是统计性的,这意味着即使概率分布在总体上是正确的,对于个例也可能是错误的。例如,如果平均寿命是 80 岁,这并不意味着你在 80 岁生日时就会死掉。很难从平均值与概率分布中对某个特定个体的寿命作出什么判断,同样,预测系统的输出是概率性的,对于个例可能是错误的。

注:盲目相信数据决策至高无上,这不仅仅是一种妄想,而是有切实危险的。随着数据驱动的决策变得越来越普遍,我们需要弄清楚,如何使算法更负责任且更加透明,如何避免加强现有的偏见,以及如何在它们不可避免地出错加以修复。

​ 我们还需要想清楚,如何避免数据被用于害人,如何认识数据的积极潜力。

3.1.3 反馈循环

即使是那些对人直接影响比较小的预测应用,比如推荐系统,也有一些必须正视的难题。当服务变得善于预测用户想要看到什么内容时,它最终可能只会向用户展示他们已经同意的观点,将人们带入滋生刻板印象,误导信息,与极端思想的回音室。

注:我们在美国大选中,已经看到过社交媒体回音室对竞选的影响了。

当预测性分析影响人们的生活时,自我强化的反馈循环会导致非常有害的问题。例如,考虑雇主使用信用分来评估候选人的例子。你可能是一个信用不错的好员工,但因为不可抗力的意外而陷入财务困境。由于不能按期付账单,你的信用分会受到影响,进而导致找到工作更为困难。失业使你陷入贫困,这进一步恶化了你的分数,使你更难找到工作。

注:在数据与数学严谨性的伪装背后,隐藏的是由恶毒假设导致的恶性循环。

我们无法预测这种反馈循环何时发生。然而通过对整个系统(不仅仅是计算机化的部分,而且还有与人互动的人)进行整体思考,许多后果可以预测——一种称为系统思维的方法。

我们可以尝试理解数据分析如何响应不同的行为,结果或特性。

注:该系统是否加强或增大了人们之间现有的差异(比如,富者越富,贫者越贫),还是试图与不公做斗争。

​ 即使有着最好的动机,我们也必须当心意想不到的后果。

3.2 隐私和追踪

除了预测性分析——使用数据来做出关于人的自动决策,数据收集本身也存在道德问题。

思考:收集数据的组织,与被收集数据的人之间,到底属于什么关系?

当系统只存储用户明确输入的数据时,是因为用户希望系统以特定方式存储和处理这些数据,系统是在为用户提供服务:用户就是客户。但是,用户的活动被跟踪并记录,作为它们正在做的其他事情的副作用时,这种关系就没有那么清晰了。该服务不再仅仅完成用户想要它做的事情,而是服务与它自己的利益,而这可能与用户的利益相冲突。

注:追踪用户行为数据对于许多面向用户的在线服务而言,变得越来越重要:追踪用户点击了哪些搜索结果有助于改善搜索结果的排名;推荐「喜欢 X 的人也喜欢 Y」,可以帮助用户发现实用有趣的东西;A/B 测试和用户流量分析有助于改善用户界面。这些功能需要一定量的用户行为跟踪,而用户也可以从中受益。

不同的公司有着不同的商业模式,追踪并未止步于此。如果服务是通过广告盈利的,那么广告才是真正的客户,而用户的利益则屈居其次。跟踪的数据会变得详细,分析变得深入,数据会保留很长时间,以便为每个人建立画像,用于营销。

注:原来这个观点这么早就提出了,真·很有道理。

现在,公司与被收集数据的用户之间的关系,看上去就不太一样了。公司会免费服务用户,并设法让用户尽可能多的使用其服务。

3.2.1 监视

跟随本书的作者做一个小实验。

尝试用监视一词替换数据,再看看常见的短语是不是听起来还那么动听。比如:「在我们的监视驱动的组织中,我们收集实时监视流并将它们存储在我们监视仓库中。我们的监视科学家使用高级分析和监视处理来获得新的见解。」

注:对于本书《设计监视密集型 应用》(设计数据密集型应用)而言,这个思想实验是罕见的争议性内容,但作者仍认为需要激烈的言辞来强调这一点。在我们尝试制造软件「吞噬世界」的过程中,我们已经建立了世界上迄今为止所见过的最大规模的监视基础设施。

​ 我们正朝着万物互联迈进,我们正在迅速走近这样的一个世界:每个有人居住的空间至少包含一个带有互联网的麦克风,以智能手机、智能电视、语音控制助理设备、婴儿监控器甚至儿童玩具的形式存在,并使用具有云的语言识别。这些设备中的许多都有这可怕的安全记录。

3.2.2 同意与选择的自由

我们可能会断言用户是自愿选择使用了会跟踪其活动的服务,而且他们已经同意了服务条款与隐私策略,因此他们同意数据收集。我们甚至可以声称,用户在用所提供的数据来换取有价值的服务,并且为了提供服务,追踪是必要的。

注:毫无疑问,社交网络、搜索引擎以及各种其他免费的在线服务对于用户来说都是有价值的,但是这个说法却存在问题

用户几乎不知道他们提供给我们的是什么数据,哪些数据被放进数据库,数据又是怎样被保留与处理的——大多数隐藏政策都是模棱两可的,忽悠用户而不敢打开天窗说亮话。如果用户不了解他们的数据会发生什么,就无法给出任何有意义的同意

注:有时来自一个用户的数据还会提到一些关于其他人的事,而其他那些人既不是该服务的用户,也没有同意任何条款。我们本书这一部分讨论的衍生数据集——来自整个用户群的数据,加上行为追踪与外部数据源——就恰好是用户无法理解的数据类型。

而且从用户身上挖掘数据是一个单向过程,而不是真正的互惠关系,也不是公平的价值交互。用户对能用多少数据换来什么样的服务,即没有发言权也没有选择权:服务与用户之间的关系是非常不对称单边关系。这些条款是有服务提出的,而不是由用户提出的。

注:对于不同意监控的用户,唯一真正管用的备用选项,就是简单地不使用服务。

3.2.3 隐私与数据使用

有时候,人们声称「隐私已死」,理由是有些用户愿意把各种关于他们生活的事情发布到社交媒体上,有时是平凡俗套,但有时是高度私密的。但这种说法是错误的,而且是对「隐私」一次的误解。

拥有隐私并不意味着保密一切东西:它意味着拥有选择向谁展示哪些东西的自由,要公开什么,以及要保密什么。隐私权是一项决定权:在从保密到透明的光谱上,隐私使得每个人都能决定自己想要在什么地方于光谱上的哪些位置。

注:当通过监控基础设施从人身上提取数据时,隐私权不一定受到损害,而是转移到了数据收集者手中获取数据的公司实际上是说「相信我们会用你的数据做正确的事情」,这意味着,决定要透漏什么和保密什么的权利从个体手中转移到了公司手中。

从个体到公司的大规模隐私转移是历史上是史无前例的。监控一直存在,但它过去是昂贵的、手动的,不是可伸缩的、自动化的。信任关系一直存在,例如患者与其医生之间,或被告与其律师之间——但在这些情况下,数据的使用严格受到道德,法律和监管限制的约束。

注:互联网服务使得在未经有意义的同意下收集大量敏感信息变得容易得很多,而且无需用户理解他们的私人数据到底发生了什么。

3.2.4 数据资产与权力

由于行为数据是用户与服务交互的副产品,因此有时被称为「数据废气」——暗示数据是毫无价值的废料。从这个角度来看,行为和预测分析可以被看作是一种从数据中提取价值的回收形式,否则这些数据就会被浪费。

数据不仅仅是一种资产,而且是一种「有毒资产」,或者至少是「有害物质」。即使我们认为自己有能力组织数据滥用,但每当我们收集数据时,我们都需要平衡收益以及这些数据落入恶人手中的风险:计算机系统可能会被犯罪分子或敌国特务渗透,数据可能会被内鬼泄漏,公司可能会落入不择手段的管理层中,而这些管理者有着迥然不同的价值观。

3.2.5 回顾工业革命

工业革命是通过重大的技术与农业进步实现的,它带来了持续的经济增长,长期的生活水平显著提高。然而它也带来了一些严重的问题:空气污染(由于烟雾和化学过程)和水污染(工业垃圾和人类垃圾)是可怕的。工厂老板生活在纷奢之中,而城市工人经常居住在非常糟糕的住房中,并且在恶劣的条件下长时间工作。童工很常见,甚至包括矿井中危险而底薪的工作。

就像工业革命有着黑暗面需要应对一样,我们转向信息时代的过程中,也有需要应对与解决的重大问题。有理由相信数据的收集和使用就是其中的一个:

数据是信息时代的污染问题,保护隐私是环境挑战。几乎所有的电脑都能生产信息。它堆积在周围,开始溃烂。我们如何处理它——我们如何控制它,以及如何摆脱它——是信息经济健康发展的核心议题。正如我们今天回顾工业时代的早期年代,并想知道我们的祖先在忙于建设工业世界的过程时怎么能忽略污染问题;我们的孙辈在回望信息时代的早期年代时,将会就我们如何应对数据收集和滥用的挑战来评断我们。

我们应该设法让他们感到骄傲。

3.2.6 立法与自律

数据保护法可能有助于维护个人的权利。例如,1995 年「欧洲数据保护指示」规定,个人数据必须为特定的、明确的和合法的目的收集,而不是以与这些目的不相符的方式进一步处理。并且数据必须「就收集的目的而言适当、相关、不过分」。

但是,这个立法在今天的互联网环境下是否有效还是有疑问的。这些规则直接否定了大数据的哲学,即最大限度地收集数据,将其与其他数据结合起来进行实验和探索,以便产生新的洞察。

注:探索意味着将数据用于未曾预期的目的,这与用户同意的「特定于明确」目的相反。

那些收集了大量有关人的数据的公司反对监管,认为这是创新的负担与阻碍。在某种程度上,这种反对是有道理的。例如,分享医疗数据时,存在明显的隐私风险,但这也有潜在的机遇:如果数据分析能帮助我们实现更好的诊断或找到更好的治疗方法,能够组织多少人的死亡?

多度监管可能会组织这种突破。在这种潜在的机会与风险之间找出平衡是很困难的。

我们究竟能做到哪一步,是一个开放的问题。首先,我们不应该永久保留数据,而是一旦不再需要就立即清楚数据。清楚数据与不变性的想法背道而驰,但这是可以解决的问题。一种很有前景的方法是通过加密协议来实现访问控制,而不仅仅是通过策略。

注:总的来说,文化与态度的改变时必要的。

4. 碎碎念

从今年的四月份开始写了第一篇的总结,断断续续用了大半年的时间,终于在 2022 年的尾巴把所有的章节都总结完。

你问我真的记住了吗?真的有用吗?

我可以很肯定的回答,大部分的都没有记住。但是那又有什么关系呢,你学过的知识总会在未来的某个时间从脑子里蹦出来,跟你打招呼说:hi,我在这里哦,这个问题还可以这么这么解决的。

就这样吧,带着一点点的不舍和欣喜。

  • 我啊,可能没有很多优点,但是总是充满热爱。热爱日出时绚烂的朝霞,热爱夜晚漫天璀璨的星海,热爱阴天淅淅沥沥的雨声,热爱独处时的安静,热爱欢声笑语,热爱「咕嘟咕嘟」冒着热气的火锅,热爱甜甜的奶茶,热爱所有美好。
  • 我不觉得心智成熟是越来越宽容,什么都可以接受。相反,我觉得那应该是一个逐渐剔除的过程,知道自己最重要的是什么,知道不重要的是什么,然后做一个简单的人。
  • 我不是天生就自带光芒的人,只是我后来明白,现在这不是我想要的,所以通过不断的努力去提升自己。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 数据密集型应用系统设计涉及处理大量数据的系统,其中包括数据的存储、检索和处理。设计这种系统时需要考虑到数据的可靠性、可扩展性和性能。 在设计数据密集型应用系统时,首先需要选择合适的数据存储技术,例如关系型数据库、NoSQL数据库或分布式文件系统等。这些技术各有优势和适用场景,需要根据系统需求来选择。 其次,需要对数据进行分片和复制,以提高系统的可扩展性和可用性。分片将数据划分成多个部分,每个部分由不同的节点负责存储和处理;而复制则是将数据副本存储在不同的节点上,以防止单点故障。 此外,对于数据密集型应用系统,数据的一致性也是一个重要的考虑因素。可以通过使用分布式一致性协议(如Paxos或Raft)来确保数据的一致性。 在系统性能方面,可以采用多种技术来提高系统的吞吐量和响应时间。例如,可以使用缓存来减轻数据库的压力,使用异步消息队列来实现解耦和扩展,以及使用分布式计算框架来并行处理数据。 最后,在设计数据密集型应用系统时,还需要关注系统的监控和调优。通过监控系统的负载、资源使用情况和性能指标,可以及时发现问题并进行调优,以保证系统的稳定性和高效性。 综上所述,设计数据密集型应用系统需要考虑数据存储、分片和复制、一致性、性能优化以及监控和调优等方面。只有综合考虑这些因素,才能设计出满足系统需求的高效可靠的系统。 ### 回答2: 数据密集型应用系统设计是指设计和构建大量、复杂和敏感数据应用系统。这些系统通常需要高效地处理和存储大量数据,并能够提供快速的查询和分析功能。 在设计数据密集型应用系统时,需要考虑以下几个关键因素: 1. 数据需求分析:首先要理解应用系统的数据需求,包括数据类型、数据量和数据的使用频率等。这将有助于确定适合的数据库管理系统和存储架构。 2. 数据模型设计:根据数据需求,设计合适的数据模型,包括定义数据结构、关系和约束等。这将影响后续的数据库设计和查询性能。 3. 数据库选择:选择适合的数据库管理系统,如关系型数据库、NoSQL数据库或分布式数据库。根据数据量和访问模式来选择合适的存储方案,如磁盘存储、内存存储或混合存储。 4. 数据库优化:对数据库进行性能优化,包括索引设计、查询优化和缓存机制等。通过合理的数据库设计和优化,可提高系统的响应速度和负载能力。 5. 并发控制:数据密集型应用系统通常需要支持大量并发用户操作,因此需要实施有效的并发控制机制,如锁机制、事务管理和分布式事务处理。 6. 安全性设计:由于数据密集型应用系统通常处理敏感数据,因此需要对数据进行有效的安全保护。这包括数据加密、身份验证、访问控制和安全审计等。 设计数据密集型应用系统时,需综合考虑以上因素,并根据实际需求进行合理选择和设计。通过科学合理的架构和设计,可以提高系统的可靠性、性能和安全性,满足用户的数据处理和分析需求。 ### 回答3: 数据密集型应用系统的设计涉及到大量的数据的处理和管理。在设计这样的系统时,一个重要的方面是确定如何将数据存储和访问进行优化,以便在系统运行时能够快速高效地处理大量的数据。 对于数据的存储,可以考虑使用分布式存储系统,如Hadoop或Cassandra。这些系统能够将大量数据分散存储在多个节点上,以提高数据的可靠性和可扩展性。此外,还可以采用数据分片和数据复制的策略,以增加系统的容错能力和性能。 对于数据的访问,可以采用分布式计算框架,如MapReduce或Spark。这些框架能够将数据的计算任务分布到多个节点上,并通过数据并行的方式,提高系统的计算能力。同时,还可以使用缓存技术,如Redis或Memcached,来加快数据的访问速度。 另外,在数据密集型应用系统设计中,需要注意数据的安全性和隐私保护。可以采用数据加密和访问控制的措施,确保敏感数据不会被未经授权的人访问到。 最后,在设计数据密集型应用系统时,还要考虑系统的扩展性和可伸缩性。可以采用水平扩展的方式,通过增加服务器节点来增加系统的处理能力。同时,还要考虑系统的负载均衡和容灾机制,以防止单点故障和系统的不可用。 综上所述,数据密集型应用系统设计需要考虑多方面的因素,包括数据存储和访问的优化,数据的安全性和隐私保护,以及系统的扩展性和可伸缩性。只有综合考虑这些因素,才能设计出高效可靠的数据密集型应用系统。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值