本文是“设计键值存储”系列文章的第二篇。如果您尚未阅读 第一篇文章,建议先参阅该篇以了解背景。

在上一篇文章中,我们主要探讨了键值存储的基本概念,尤其是单机方案。当面临扩展性问题时,我们需要依据特定规则将数据分发至多台计算机,并通过协调器(Coordinator)将客户端请求定向至持有目标资源的节点。

设计分布式系统时,需要考虑诸多因素。将数据拆分到多台计算机时,流量平衡至关重要,因此确保键(Key)的随机分布是最佳实践。

本文将继续深入讨论分布式键值存储系统,涵盖系统可用性、一致性等核心主题。

系统可用性

评估分布式系统时,一个关键指标是系统可用性。例如,假设集群中的某台计算机因硬件故障或程序错误而崩溃,这将如何影响键值存储系统?

显然,若用户请求的资源恰好位于该故障机器上,系统将无法返回正确响应。在构建小型项目时,您可能不会优先考虑此问题;但如果您需要为数百万用户提供服务且拥有大量服务器,硬件故障将成为常态。您无法每次都手动重启服务器,这就是为什么高可用性在当今每个分布式系统中都必不可少。那么,该如何解决这一问题?

当然,您可以通过编写更强大的测试用例来提升代码健壮性。但程序始终可能存在缺陷,且硬件问题更难完全规避。最常见的解决方案是引入 副本(Replica)。通过设置拥有重复资源的计算机,我们可以大幅减少系统停机时间。例如,若单台计算机每月崩溃概率为 10%,引入一台备份计算机后,两台计算机同时宕机的概率将降至 1%(假设故障相互独立)。

副本与分片

乍一看,副本与分片(Sharding)非常相似。那么,这两者之间是什么关系?在设计分布式键值存储时,如何在副本和分片之间进行选择?

首先,我们需要明确这两种技术的目的:

  • 分片:主要用于将数据拆分到多台计算机,因为单台计算机无法存储海量数据。
  • 副本:是一种保护系统免于停机的方法,旨在提高可用性。

考虑到这一点,如果单台计算机无法存储所有数据,仅使用副本将无济于事,必须结合分片策略。

一致性

通过引入副本,我们可以增强系统的健壮性。然而,这也带来了一个关于一致性的问题。假设对于机器 A1,我们设有副本 A2。如何确保 A1 和 A2 的数据相同?

例如,当插入新条目时,我们需要更新两台计算机。但有可能其中之一的写操作失败。随着时间的推移,A1 和 A2 之间可能会积累大量不一致的数据,这是一个严重问题。

这里有几种常见的解决方案:

  1. 协调器保留副本:将本地副本保留在协调器中。每当更新资源时,协调器都会保留更新版本的副本。因此,如果节点更新失败,协调器可以重新执行该操作。
  2. 提交日志(Commit Log):如果您使用过 Git,应该非常熟悉提交日志的概念。基本上,每个节点计算机都会保留每个操作的提交日志,类似于所有更新的历史记录。当我们要更新机器 A 中的条目时,首先将此请求存储在提交日志中。然后,一个单独的程序将按顺序(在队列中)处理所有提交日志。每当操作失败时,我们可以查找提交日志轻松恢复。
  3. 读取冲突解决:假设请求的资源位于 A1、A2 和 A3 中,协调器可以向所有三台机器进行询问。如果返回的数据不同,系统可以即时解决冲突(例如采用多数派原则)。

值得注意的是,所有这些方法都不是互斥的。您完全可以根据应用程序的具体需求组合使用多种策略。

读取吞吐量

我还想在这篇文章中简要提及读取吞吐量。通常,键值存储系统应该能够支持大量的读取请求。那么,您将使用哪些方法来提高读取吞吐量?

为了提高读取吞吐量,通常的方法是利用内存。如果数据存储在每个节点计算机的磁盘中,我们可以将热点数据移动到内存中。一个更通用的想法是使用缓存。鉴于《设计缓存系统》一文已对此主题进行了深入分析,因此我在这里不再赘述。

总结

不要将这里的分析视为标准答案。相反,这些常见的解决方案旨在为您提供启发,帮助您提出不同的想法。

没有适用于每个系统的通用解决方案,您应该始终根据特定场景调整方法。