11.8扩展数据库

 
扩展无状态的微服务是相对简单的。但如果我们把数据存储在一个数据库呢?我们也需要 知道如何扩展数据库。不同类型的数据库会提供不同形式的扩展,理解哪种形式最适合你 的使用场景,将确保从一开始你就选择了正确的数据库技术。
 
11.8.1服务的可用性和数据的持久性
 
更直接地说,重要的是你要区分服务的可用性和数据的持久性这两个概念。你需要明白这 是不同的两件事情,因此会有不同的解决方案。
 
例如,对于所有写人数据库的数据,我可以将一份副本存储到一个弹性文件系统。如果数 据库出现故障,数据不会丢失,因为有一个副本,但数据库本身是不可用的,这会使我们 的微服务也不可用。一个更常用的模式是使用副本。把写入主数据库的所有数据,都复制 到备用副本数据库。如果主数据库出现故障,我的数据是安全的,但如果没有一个机制让 主数据库恢复或提升副本为主数据库,即使数据是安全的,数据库依然不可用。
 
11.8.2扩展读取
 
很多服务都是以读取数据为主的。例如保存我们出售物品信息的目录服务。添加新物品记 录是相当不规律的,如果说每笔写入的目录数据都有100次以上的读取,这个数字不会让你感到惊讶。令人高兴的是,扩展读取要比扩展写入更容易。缓存的数据在这里可以发挥 很大的作用,我们稍后会进行更深入的讨论。另一种模式是使用只读副本。
 
在像 MySQL 或 Postgres 这样的 RDBMS (Relational Database Management System,关系 型数据库管理系统)中,数据可以从主节点复制到一个或多个副本。这样做通常是为了确 保有一份叙据的备份以保证安全,但我们也可以用它来分发读取。正如我们在图11-6中 看到的,服务可以在单个主节点上进行所有的写操作,但是读取被分发到一个或多个只读 副本。从主数据库复制到副本,是在写入后的某个时刻完成的,这意味着使用这种技术读 取,有时候看到的可能是失效的数据,但是最终能够读取到一致的数据,这样的方式被称 为最终一致性。如果你能够处理暂时的不一致,这是一个相当简单和常见的用来扩展系统 的方式。稍后我们在看CAP定理时,会深入讨论这个话题。
图11-6:使用只读副本来扩展读取
 
几年前,使用只读副本进行扩展风靡一时,不过现在我建议你首先看看缓存,因为它可以 提供更显著的性能改善,而且工作量往往更少。
 
11.8.3扩展写操作
 
扩展读取是比较容易的。那么扩展写操作呢? 一种方法是使用分片。采用分片方式,会存 在多个数据库节点。当你有一块数据要写人时,对数据的关键字应用一个哈希函数,并基 干这个函数的结果决定将数据发送到哪个分片。举一个非常简单的(实际上是很糟的)的 例子,你可以想象将客户记录A〜M写到一个数据库实例,而N〜Z写到另一个数据库实 例。你可以在应用程序里管理这部分逻辑,但一些数据库,例如Mongo,已经帮你处理了 很多。
 
分片写操作的复杂性来自于查询处理。查找单个记录是很容易的,因为可以应用哈希函数 找到数据应该在哪个实例上,然后从正确的分片获取它。但如果查询跨越了多个节点呢? 例如,查找所有年满18岁的顾客。如果你要查询所有的分片,要么需要查询每个分片,然后在内存里进行拼接,要么有一个替代的读数据库包含所有的数据集。跨分片查询往往 采用异步机制,将查询的结果放进缓存。例如,Mongo使用map/reduce作业来执行这些 查询。
 
使用分片系统会出现的问题之一是,如果我想添加一个额外的数据库节点该怎么办?在过 去,这往往需要大量的宕机时间(特别是对于大型集群),因为你需要停掉整个数据库, 然后重新分配数据。最近,越来越多的系统支持在不停机的情况下添加额外的分片,而重 新分配数据会放在后台执行;例如,Cassandra在这方面就处理得很好。不过,添加一个分 片到现有的集群依然是有风险的,因此你需要确保对它进行了充分的测试。
 
写入分片可能会扩展写容量,但不会提高弹性。如果客户记录A〜M总是去实例X,那 么当实例X不可用时,A~M的记录依然无法访问。Cassandra在这方面提供额外的功能, 可以确保数据在一个环(ring,Cassandra的术语,来描述它的节点集合)内复制到多个节点。
 
正如你可能已经推断出的,从上面这些简单的概述中我们发现,扩展数据库写操作非常棘 手,而各种数据库在这方面的能力开始真正分化。我经常看到,当人们无法轻松地扩展现 有的写容量时,才改变数据库技术。如果这发生在你身上,买一个大点的机器往往是快速 解决这个问题的方法,但长远来看,你可能需要看看像Cassandra、Mongo或者Riak这样 的数据库系统,它们不同的扩展模型能否给你提供一个长期的解决方案。
 
11.8.4共享数据库基础设施
 
某些类型的数据库,例如传统的RDBMS,在概念上区分数据库本身和模式(schema)。这 意味着,一个正在运行的数据库可以承载多个独立的模式,每个微服务一个。这可以有效 地减少需要运行系统的机器的数量,从这一点来说它很有用,不过我们也引入了一个重要 的单点故障。如果该数据库的基础设施出现故障,它会影响多个微服务,这可能导致灾难 性故障。如果你正以这样的方式配置数据库,请确保慎重考虑了风险,并且确定该数据库 本身具有尽可能髙的弹性。
 
11.8.5 CQRS
 
CQRS ( Command-Query Responsibility Segregation,命令查询职责分离)模式,是一个存
 
储和查询信息的替代模型。传统的管理系统中,数据的修改和查询使用的是同一个系统。 使用CQRS后,系统的一部分负责获取修改状态的请求命令并处理它,而另一部分则负责 处理查询。    :
 
收到的命令会请求状态的变化,如果这些命令验证有效,它们将被应用到模型。命令应该 包含与它们意图相关的信息。它们可以以同步或异步的方式处理,允许扩展不同的模型来 处理;例如,我们可以只是将请求排进队列,之后再处理它们。
 
这里的关键是,内部用于处理命令和查询的模型本身是完全独立的。例如,我可能选择把 命令作为事件,只是将命令列表存储在一个数据存储中(这一过程称为事件溯源,event sourcing)。我的查询模型可以查询事件库,从存储的事件推算出领域对象的状态,或只是 从系统的命令部分获取一个聚合,来更新其他不同类型的存储。在许多方面,我们得到跟 之前讨论的只读副本方式同样的好处,但CQRS中的副本数据,不需要和处理数据修改的 数据存储相同。 .
 
这种形式的分离允许不同类型的扩展。我们系统的命令和查询部分可能是在不同的服务或 在不同的硬件上,完全可以使用不同类型的数据存储。这解锁了处理扩展的大量方法。你 甚至可以通过实现不同的查询方式来支持不同类型的读取格式,比如支持图形展示的数据 格式,或是基于键/值形式的数据格式。
 
但要提醒大家一句:相对于单一数据存储处理所有的CRUD操作的模式,这种模式是一个 相当大的转变。我见过不止一个经验丰富的开发团队在纠结如何正确地使用这一模式!
 
11.9缓存
 
缓存是性能优化常用的一种方法,通过存储之前操作的结果,以便后续请求可以使用这个 存储的值,而不需花时间和资源重新计算该值。通常情况下,缓存可以消除不必要的到数 据库或其他服务的往返通信,让结果返回得更快。如果使用得当,它可以带来巨大的性能 好处。HTTP在处理大量请求时,伸缩性如此良好的原因就是内置了缓存的概念。
 
即使对一个简单的单块Web应用程序来说,你也可以选择在很多不同的地方,使用多种不 同的方式进行缓存。在微服务的架构下,每个服务都有自己的数据源和行为,对干在何处 以及如何缓存,我们有更多的选择。对干一个分布式系统,通常认为缓存可以放在客户端 或服务端。但是,放在哪里最好呢?
 
11.9.1客户端、代理和服务器端缓存
 
使用客户端缓存的话,客户端会存储缓存的结果。由客户端决定何时(以及是否)获取最 新副本。理想情况下,下游服务将提供相应的提示,以帮助客户端了解如何处理响应,因 此客户端知道何时以及是否需要发送一个新的请求。代理服务器缓存,是将一个代理服务 器放在客户端和服务器之间。反向代理或CDN (Content Delivery Network,内容分发网 络),是很好的使用代理服务器缓存的例子。服务器端缓存,是由服务器来负责处理缓存, 可能会使用像Redis或Memcache这样的系统,也可能是一个简单的内存缓存。
 
哪种缓存最合理取决于你正在试图优化什么。客户端缓存可以大大减少网络调用的次数, 并且是减少下游服务负载的最快方法之一。但是使用由客户端负责缓存这种方式,如果你 想改变缓存的方式,让大批的消费者全都变化是很困难的。让过时的数据失效也比较棘手,尽管我们会在稍后讨论一些应对机制
 
使用代理服务器缓存时,一切对客户端和服务器都是不透明的。这通常是增加缓存到现有 系统的一个非常简单的方法。如果代理服务器被设计成对通用的流量进行缓存,它也可以 缓存多个服务。一个常见的例子是,反向代理Squid或Varnish,它们可以缓存任何HTTP 通信。在客户端和服务器间加入代理服务器,会引入额外的网络跳数(network hops),虽 然以我的经验来说,它很少会导致出现问题,因为缓存本身的性能优化已经超过了其他额 外的网络开销。
 
使用服务器缓存,一切对客户端都是不透明的,它们不需要关心任何事情。缓存在服务 器外围或服务器限界内时,很容易了解一些类似数据是否失效这样的事情,还可以跟踪 和优化缓存命中率。在你有多种类型客户端的情况下,服务器缓存可能是提高性能的最 快方式。
 
我工作过的每一个面向公众的网站,最终都是混合使用这三种方法。不过对干几个分布式 系统,我没有使用任何缓存。所有这些都取决于你需要处理多少负载,对数据及时性有多 少要求,以及你的系统现在能做什么。知道你有几个不同的工具,这只是一个开始而已。
 
11.9.2 HTTP 缓存
 
HTTP提供了一些非常有用的控制手段,帮助我们在客户端或服务器端缓存,即使你不使 用HTTP也值得了解一下。
 
首先,使用HTTP,我们可以在对客户端的响应中使用cache-cont「ol指令。这些指令告 诉客户他们是否应该缓存资源,以及应该缓存几秒。我们还可以设置Expires头部,它不 再指定一段内容应该缓存多长时间,而是指定一个日期和时间,资源在该日期和时间后被 认为失效,需要再次获取。你共享的资源本质,决定了哪一种方法最为合适。标准的静态 网站内容,像CSS和图片,通常很适合使用简单的cache-control TTL (Time To Live,生 存时间值)。另一方面,如果你事先知道什么时候会更新一个新版本的资源,设置Expires 头部将更有意义。以上两种方法都非常有用,客户端甚至无需发请求给服务器。
 
除了 cache-control和Expires,我们在HTTP的兵器库里还有另一种选择:实体标签 (Entity Tags)或称为Etag。ETag用于标示资源的值是否已改变。如果我更新了客户记录, 虽然访问资源的URI相同,但值已经不同,所以我会改变ETag。有一种非常强大的请求 方式叫作条件GET。当发送一个GET请求时,我们可以指定附加的头告诉服务器,只有 满足某些条件时才会返回资源。   
 
例如,假如我们想要获取一个客户的记录,其返回的ETag是05t6fkd2sa。稍后,也许因 为cache-corvtrol指令告诉我们这个资源可能已经失效,所以我们想确保得到最新的版本。 当发出后续的GET请求,我们可以发送一个If-None-Match:05t6fkd2sa。这个条件判断请求告诉服务器,如果ETag值不匹配则返回特定URI的资源。如果我们的已经是最新版本, 服务器会直接返回响应304 (未修改),告诉客户端缓存的已经是最新版本。如果有可用的 新版本,我们会得到响应200 0K、更新后的资源以及新的ETag。
 
在这样广泛使用的规范里内置了这些控制手段,这意味着可以利用大量已存在的软件,来 帮助我们•处理缓存。像Squid或Varnish这样的反向代理服务器可以位于客户端和服务器 间的网络上,可以按需存储缓存内容和使内容过期。这些系统旨在快速处理大量的并发请 求,并且它们也是面向公众网站扩展的一种标准方式。像AWS的CloudFront或Akamai 这样的CDN,可以把请求路由到调用附近的缓存,以确保通信不会跨越半个地球。简单地 说,HTTP客户端库和客户端缓存可以帮我们做大量的工作。
 
ETag, Expires和cache-control会有一些重叠,如果你决定全部使用它们,那么最终有 可能会得到相互矛盾的信息!关于各种方式的优点的深入讨论,可以看一下《REST实 战》,或阅读 HTTP 1.1 规范的第 13 章( http://www.w3.org/Protocols/rfc2616/rfc2616-secl3. html#sec 13.3.3),它们描述了客户端和服务器应该如何实现这些不同的控制手段。
 
无论你是否决定使用HTTP作为服务间通信的协议,客户端缓存和减少客户端与服务器之 间不必要的通信,都是值得尝试的措施。如果你决定选择一个不同的协议,请了解何时以 及如何为客户提供提示,以帮助其理解可以缓存的时间。
 
11.9.3为写使用缓存
 
你会发现尽管自己经常在读取时使用缓存,但在一些用例中,为写使用缓存也是有意义 的。例如,如果你使用后写式(writebehind)缓存,可以先写入本地缓存中,并在之后的 某个时刻将缓存中的数据写入下游的、可能更规范化的数据源中。当你有爆发式的写操 作,或同样的数据可能会被写入多次时,这是很有用的。后写式缓存是在缓冲可能的枇处 理写操作时,进一步优化性能的很有用的方法。
 
使用后写式缓存,如果对写操作的缓冲做了适当的持久化,那么即使下游服务不可用,我 们也可以将写操作放到队列里,然后当下游服务可用时再将它们发送过去。
 
11.9.4为弹性使用缓存
 
缓存可以在出现故障时实现弹性。使用客户端缓存,如果下游服务不可用,客户端可以先 简单地使用缓存中可能失效了的数据。我们还可以使用像反向代理这样的系统提供的失效 数据。对一些系统来说,使用失效但可用的数据,比完全不可用的要好,不过这需要你自 己做出判断。显然,如果我们没有把请求的数据放在缓存中,那么可做的事情不多,但还 是有一些方法的。
我曾经在《卫报》中见过一种技术,随后在其他地方也见过,就是定期去爬(crawl)现有的工作的网站,生成一个可以在意外停机时使用的静态网站。(定期爬虫自己的网站)虽然这个爬下来的版本不比 工作系统的缓存内容新,但在必要时,它可以确保至少有一个版本的网站可以显示。
 
11.9.5隐藏源服务
 
使用普通的缓存,如果请求缓存失败,请求会继续从数椐源获取最新的数据,请求调用 会一直等到结果返回。在普通情况下,这是期窪的行为。但是,如果遭受大量的请求缓 存失败,也许是因为提供缓存的整个机器(或一组机器)宕掉,大量的清求会波发送到 源服务。
 
对T那些提供卨度可缓存数据的服务,从设计上来讲,源服务本身就只能处理一小部分的 流量,因为大多数请求已经波源服务前面的缓存处理f。如果我们突然得到一个晴天霹雳 的消息,由干整个缓存区消失了,源服务就会接收到远大于其处理能力的请求。
 
在这种情况下,保护源服务的一种方式是,在第一时间就不要对源服务发起请求。相反, 如图11-7所示,在需要时源服务本身会异步地填充缓存。如果缓存请求失败,会触发一个 给源服务的事件,提醒它需要重新填充缓存。所以如果整个分片消失了,我们可以在后台 重建缓存。可以阻塞请求直到区域被甫新填充,但这可能会使缓存本身的争用,从而导致 一些问题。更合适的是,如果想优先保持系统的稳定,我们可以让原始请求失败,但要快 速地失败。
图11-7:保护源服务,在后台异步重建缓存
 
在某些情况下这种方法可能没有意义,但当系统的一部分发生故障时,它是确保系统仍然 可用的一种方式。让请求快速失败,确保不占m资源成增加延迟,我们避免了级联下游服 务导致的缓存故障,并给自己一个恢复的机会。    一
 
11.9.6保持简单
 
避免在太多地方使用缓存!在你和数据源之间的缓存越多,数据就越可能失效,就越难 确定客户端最终看到的赴是最新的数据。这在一个涉及多个服务的微服务架构调用链中,很有可能产生问题。再强调一次,缓存越多,就越难评估任何数据的新鲜程度。所 以如果你认为缓存是一个好主意,请保持简单,先在一处使用缓存,在添加更多的缓存 前慎重考虑!
 
11.9.7缓存中毒:一个警示
 
使用缓存时,我们经常认为最糟糕的事情是,我们会在一段时间内使用到失效数据。但如 果发现你会永远使用失效数据,该怎么办?在之前提到的一个做过的项目中,我们使用了 一个绞杀者应用程序,来帮助拦截对多个遗留系统的调用,希望增量地替换它们。我们的 系统作为代理在有效地运行着。应用程序的流量会被路由到遗留系统。在流量返回时,我 们做了一些清理工作,例如我们会确保在遗留程序的响应中存在合适的HTTP缓存头。
 
有一天,在一个普通的例行发布后不久,发生了一件奇怪的事情。我们在插人缓存头的一 个条件逻辑代码中,引入了一个bug,导致一小部分页面的缓存头没有被改变。不幸的是, 这个下游的应用程序也在之前的某个时候,将HTTP头改成包含Expires: Never。这在以 前没有任何影响,因为我们重写过这个头,但现在不一样了。
 
我们的应用程序大量使用Squid来缓存HTTP流量,上述问题很快被发现,因为我们看到 越来越多的请求绕过Squid本身来访问应用程序服务器。修复缓存头代码后,我们发布了 一个新的版本,并且手动清除了 Squid缓存的相关区域,但这还不够。
 
如前所述,你可以在多个地方进行缓存。当考虑在一个面向公众的Web应用程序中提供内 容服务时,在你和客户间可能存在多个缓存。可能不仅你在网站上使用CDN,有些ISP也 会使用缓存。你可以控制这些缓存吗?即使你可以,还有一个缓存是你无法控制的:用户 浏览器中的缓存。
 
这些使用Expires: Nev/e「的页面,停留在很多用户的缓存里,永远不会失效,直到缓存已 满或者用户手动清理它们。显然,我们无法让上述任何事情发生。我们唯一的选择就是, 改变这些网页的URL,以便能够重新获取它们。
 
缓存可以很强大,但是你需要了解数据从数据源到终点的完整缓存路径,从而真正理解它 的复杂性以及使它出错的原因。
 
11.10自动伸缩
 
如果你足够幸运,可以完全自动化地创建虚拟主机以及部署你的微服务实例,那么你已经 具备了对微服务进行自动伸缩的基本条件。
 
例如,众所周知的趋势有可能会触发伸缩的发生。可能系统的负载髙峰是从上午9点到下 午5点,因此你可以在早上8: 45启动额外的实例,然后在下午5: 15关掉这些你不再需要的实例,以节省开支。你需要数据来了解负载是如何随着时间的推移而变化的,这些数据 统计需要跨好几天甚至是好几周的时间周期。一些企业也有明显的季节性周期,所以需要 数据帮你做出正确的判断。
 
另一方面,你可以响应式地进行负载调整,比如在负载增加或某个实例发生故障时,来增 加额外的实例,或在不需要时移除它们。关键是要知道一旦发现有上升的趋势,你能够多 快完成扩展。如果你只能在负载增加的前几分钟得到消息,但是扩展至少需要10分钟, 那么你需要保持额外的容量来弥合这个差距。良好的负载测试套件在这里是必不可少的。 你可以使用它们来测试自动伸缩规则。如果没有测试能够重现触发伸缩的不同负载,那么 你只能在生产环境上发现规则的错误,但这时的后果不堪设想!
 
新闻网站是一个很好的混合使用预测型伸缩和响应型伸缩的例子。在我上个工作过的新闻 网站上,能清楚地看到其日常趋势,从早晨一直到午餐时间负载上升,随后开始下降。这 种模式每天都在重复,但在周末流量波动则不太明显。这呈现给你相当明显的趋势,可以 据此对资源进行主动扩容(缩容)。另一方面,一个大新闻可能会导致意外的高峰,在短 时间内需要更多的容量。
 
事实上相比响应负载,自动伸缩被更多应用于响应故障。AWS允许你指定这样的规则: “这个组里至少应该有5个实例”,所以如果一个实例宕掉后,一个新的实例会自动启动。 当有人忘记关掉这个规则时,就会导致一个有趣的打鼹鼠游戏(whack-a-mole),即当试图 停掉一个实例进行维护时,它却自动启动起来了!
 
响应型伸缩和预测型伸缩都非常有用,如果你使用的平台允许按需支付所使用的计算资 源,它们可以节省更多的成本。但这也需要仔细观察你提供的数据。我建议,首先在故障 的情况下使用自动伸缩,同时收集数据。一旦你想要为负载伸缩,一定要谨慎不要太仓促 缩容。在大多数情况下,手头有多余的计算能力,比没有足够的计算能力要好得多!
 
11.11 CAP定理
 
我们想要拥有一切,但不幸的是我们做不到。当使用微服务架构构建的分布式系统时,一 个数学证明甚至就能证明我们做不到。你很有可能已经听说过CAP定理,尤其是在论各 种不同类型的数据存储的优缺点时。其核心是告诉我们,在分布式系统中有三方面需要彼 此权衡:一致性(consistency)、可用性(availability)和分区容忍性(partition tolerance)。 具体地说,这个定理告诉我们最多只能保证三个中的两个。
 
一致性是当访问多个节点时能得到同样的值。可用性意味着每个请求都能获得响应。分区 容忍性是指集群中的某些节点在无法联系后,集群整体还能继续进行服务的能力。
 
自从Eric Brewer发表了他的初始猜想后,这个想法得到了数学证明。我不打算深入数学证 明本身,因为这不是那一类的书,而且我肯定会把它弄错。相反,让我们用一些实例来帮助理解,CAP定理背后是一套严密的逻辑推理。
 
我们已经介绍过一些简单的数据库扩展技术。让我们使用其中一个技术,来探讨CAP定 理背后的思想。如图11-8所示,假设我们的库存服务部署在两个独立的数据中心。我们 在每个数据中心的服务实例都有一个数据库支持,并且这两个数据库通过彼此通信进行 数据同步〗读和写操作都通过本地数据库节点,然后使用副本对不同节点之间的数据进 行同步。
图11-8:使用双主数据库彼此通信来进行数据同步
 
现在让我们考虑一下,当出现失败后会发生什么。想象一个简单的场景,比如两个数据中 心之间的网络断开了。此时同步会失败,对主数据库DC1的写入操作不会传送到DC2上, 反之亦然。大多数数据库支持一些设置和某种队列技术,以确保之后我们可以恢复,但在 此期间会发生什么呢?
 
11.11.1 牺牲一致性
 
假设我们完全不停用库存服务。如果现在我更改了 DC1上的数据,DC2的数据库将看不 到它。这意味着,任何访问我们在DC2上库存节点的请求,看到的可能是已经失效的数 据。换句话说,我们的系统仍然可用,两个节点在系统分区之后仍然能够服务请求,但失 去了一致性。这通常被称为一个AP系统。我们无法保证所有的这三个方面。
 
在这种分区情况下,如果我们继续接受写操作,那就需要接受这样的一个事实,在将来的 某个时候它们不得不重新同步。分区持续的时间越长,这个重新同步就会越困难。
 
现实情况是,即使我们没有数据库节点之间的网络故障,数据复制也不是立即发生的。正如前面提到的,系统放弃一致性以保证分区容忍性和可用性的这种做法,被称为最终一致 性;也就是说,我们希望在将来的某个时候,所有节点都能看到更新后的数据,但它不会 马上发生,所以我们必须清楚用户将看到失效数据的可能性。
 
11.11.2牺牲可用性
 
如果我们需要保证一致性,相反想要放弃其他方面,会发生什么呢?好吧,为了保证一致 性,每个数据库节点需要知道,它所拥有的数据副本和其他数据库节点中的数据完全相 同。现在在分区情况下,如果数据库节点不能彼此通信,则它们无法协调以保证一致性。 由于无法保证一致性,所以我们唯一的选择就是拒绝响应请求。换句话说,我们牺牲了可 用性。系统是一致的和分区容忍的,即cp。在这种模式下,我们的服务必须考虑如何做 功能降级,直到分区恢复以及数据库节点之间可以重新同步。
 
保持多个节点之间的一致性是非常困难的。有些事情(可能所有的)在分布式系统中会更 困难。请想一下,假设我想从本地数据库节点读取一条记录,如何确定它是最新的?我必 须去询问另一个节点。但我不得不要求不允许更新数据库节点(也就是在读取该节点之后,但读取还没有完成之前,该节点的数据不能被更新,否则之前的读取是过期的,所以为了避免这种情况的发生,需要用到锁),直到读取完成;换句话 说,我需要启动一个事务,跨多个数据库节点读取以确保一致性。但是一般读取时不使用 事务,不是吗?因为事务性读取很慢。它们需要锁。一个读取可以阻塞整个系统。系统所 有的一致性都需要一定程度的锁才能完成。
 
我们已经讨论过,分布式系统一定会出现失败的情况。考虑跨一组一致性节点的事务性读 取。我要求在启动读取时,远程节点锁定给定的记录。等我完成读取后,告诉远程节点释 放锁,但在这时我发现节点之间的通信失败了,现在怎么办?即使是在单个进程的系统 中,锁都很容易出错,在分布式系统中当然就更难做好了。
 
还记得我们在第5章讨论的分布式事务吗?它们很具有挑战性,核心原因是需要确保多个 节点的一致性问题。
 
让多节点实现正确的一致性太难了,我强烈建议如果你需要它,不要试图自己发明使用的 方式。相反,选择一个提供这些特性的数据存储或锁服务。例如Consul (我们很快就会讨 论到),设计实现了一个强一致性的键/值存储,在多个节点之间共享配置。就像“人们不 会让好朋友实现自己的加密算法”,现在成为‘‘人们不会让好朋友实现自己的分布式一致 性数据存储”。如果你认为需要实现自己的CP数据存储,首先请阅读完所有相关的论文, 然后再拿一个博士学位,最后准备几年的时间来试错。与此同时,我会使用一些合适的、 现成的工具,或者放弃一致去努力构建一个最终一致性的AP系统。
 
11.11.3牺牲分区容忍性
 
我们要挑选CAP中的两点,对吗?所以,我们有最终一致的AP系统。我们有一致的,但很难实现和扩展的CP系统。为什么没有CA系统呢?嗯,我们应如何牺牲分区容忍性 呢?如果系统没有分区容忍性,就不能跨网络运行。换句话说,需要在本地运行一个单独 的进程。所以,CA系统在分布式系统中根本是不存在的。
 
11.11.4 AP 还是 CP
 
哪个是正确的,AP还是CP?好吧,现实中要视情况而定。因为我们知道,在人们构建系 统的过程中需要权衡。我们知道AP系统扩展更容易,而且构建更简单,而CP系统由干 要支持分布式一致性会遇到更多的挑战,需要更多的工作。但我们可能不了解这种权衡对 业务的影响。对于库存系统,如果一个记录过时了 5分钟,这可接受吗?如果答案是肯定 的,那么解决方案可以是一个AP系统。但对于银行客户的余额来说呢?能使用过时的数 据吗?如果不了解操作的上下文,我们无法知道正确的做法是什么。了解CAP定理只是 让你知道这些权衡的存在,以及需要问什么问题。
 
11.11.5这不是全部或全不
 
我们的系统作为一个整体,不需要全部是AP或CP的。目录服务可能是AP的,因为我们 不太介意过时的记录。但库存服务可能需要是CP的,因为我们不想卖给客户一些没有的 东西,然后不得不道歉。
 
个别服务甚至不必是CP或AP的。
 
让我们考虑一下积分账户服务,那里存储了客户已经积攒的忠诚度积分的记录。我们可以 不在乎显示给客户的余额是失效的,但当涉及更新余额时,我们必须保证一致性,以确保 客户不会使用比他们实际拥有的更多的积分。这个微服务是CP还是AP的,还是两个都 是?事实上,我们所做的是把关于CAP定理的权衡,推到单独服务的每个功能中去。
 
另一种复杂性是,即使对于一致性或可用性而言,也可以有选择地部分采用。许多系统允 许我们更精细地做权衡。例如,Cassandra允许为每个调用做不同的权衡。因此如果需要 严格的一致性,我可以在执行一个读取时,保持其阻塞直到所有副本回应确认数据是一致 的,或直到特定数量的副本做出回应,或仅仅是一个节点做出回应。显然,如果我保持阻 塞直到所有副本做出回应,那么当其中一个不可用时,我会被阻塞很长一段时间。但是如 果我满足干只需要一个节点做出回应,接受缺乏一些一致性,这样可以降低一个副本不可 用所导致的影响。
 
你会经常看到关于有人打破CAP定理的文章。其实他们并没有,他们所做的其实是创建 一个系统,其中有些功能是CP的,有些是AP的。CAP定理背后有相应的数学证明。尽 管在学校尝试过多次,但最终我不得不承认数学规律是无法打破的。
 
11.11.6真实世界
 
我们讨论过的大部分,是电子世界内存中存储的比特和字节。我们以近乎小孩子的方式谈 论一致性,想象在所构建系统的范围内,可以使世界停止,让一切都有意义。然而,我们 所构建的只是现实世界的一个映射,有些也是我们无法控制的,对吗?
 
让我们重新考虑一下库存系统,它会映射到真实世界的实体物品。我们在系统里记录了专 辑的数量,在一天开始时,有100张The Brakes的G/ve 专辑。卖了一张后,剩99 张。很简单,对吧?但如果订单在派送的过程中,有人不小心把一张专辑掉到地上并且被 踩坏了,现在该怎么办?我们的系统说99张,但货架上是98张。
 
如果我们要让库存系统保持AP,然后不得不偶尔需要与某个用户联系,告诉他已购买的 一个专辑实际上缺货,这种体验如何?这会是世界上最糟糕的事情吗?事实上,这样做更 容易构建系统及对其进行扩容,同时保证其正确性。
 
我们必须认识到,无论系统本身如何一致,它们也无法知道所有可能发生的事情,特别是 我们保存的是现实世界的记录。这就是在许多情况下,AP系统都是最终正确选择的原因 之一。除了构建CP系统的复杂性外,它本身也无法解决我们面临的所有问题。
 
11-12服务发现
 
一旦你已经拥有不少微服务,关注点就会不可避免地转向它们究竟在何处。也许你想知 道,在特定环境下有哪些微服务在运行,据此你才能知道哪些应该被监测。也许像了解你 的账户服务在哪里一样简单,以便其消费者知道在哪里能找到它。或许你只是想方便组织 里的开发人员了解哪些API可用,以避免他们重新发明轮子。从广义上来说,上述所有用 例都属于服务发现。与微服务世界中的其他问题类似,我们有很多不同的选项来处理它。
 
所有我见过的解决方案,都会把事情分成两部分进行处理。首先,它们提供了一些机制, 让一个实例注册并告诉所有人:“我在这里! ”其次,它们提供了一种方法,一旦服务被 注册就可以找到它。然后,当考虑在一个不断销毁和部署新实例的环境中,服务发现会变 得更复杂。理想情况下,我们希望无论选择哪种解决方案,它都应该可以解决这些问题。
 
让我们看一些最常见的服务发现解决方案,然后再考虑如何选择。
 
DNS
 
最好先从简单的开始。DNS让我们将一个名称与一个或多个机器的IP地址相关联。例如, 我们可以决定,总能在 accounts.musiccopr.com上发现账户服务。接着会将这个域名关联到 运行该服务的主机的IP地址上,或者关联到一个负载均衡器,然后给不同的实例分发负 载。这意味着,我们不得不把更新这些条目作为部署服务的一部分。
 
当处理不同环境中的服务实例时,我见过的一个很好的方式是使用域名模板。例如,我 们可以使用一个形如“< 服务名 >-< 环境 xmusiccorp.com”的模板,然后基于此模板生成 accounts-uat.musiccorp.comaccounts-dev.musiccorp.com 这样的域名项。
 
处理不同坏境的更先进的方式是,在不同的环境中使用不同的域名服务器。所以我可以假 定,总是可以通过 accounts.musiccorp.com找到账户服务,但根据其所处环境的不同,可能 会解析到不同的主机上。如果你已经将环境放进不同的网段,并且可以很容易地管理DNS 服务器和条目,这可能是相当简洁的解决方式,但如果你不能从这种设置中获取更多其他 的好处,相对来说这个投入就太大了。
 
DNS有许多优点,其中最主要的优点是它是标准的,并且大家对这个标准都非常熟悉,儿 乎所有的技术栈都支持它。不幸的是,尽管有很多服务可以管理组织内的DNS,但其中很 少是为处理这种高度可控制主机的场景而设计的,这使得更新DNS条目有些痛苦。亚马 逊的Roiite53服务在这方面确实做得不错,但在可选的自托管服务中,还没有像它一样好 的,虽然(我们很快就会讨论)Consul在这方面可能会提供一些帮助。除了更新DNS条目 存在的问题,DNS规范本身也会导致一些问题。
 
域名的DNS条目有一个TTL。客户端可以认为在这个时间内该条目是有效的。当我们想 要更改域名所指向的主机时,需要更新该条目,但不得不假定客户至少在TTL所指示的时 间内持有旧的IP。DNS可以在多个地方缓存条目(甚至JVM也会缓存DNS条目,除非你 告诉它不要这么做),它们被缓存的地方越多,条目就越可能会过时。
 
绕过这个问题的一种方法是,如图11-9所示,让你的域名条目指向负载均衡器,接着由它 来指向服务实例。当你部署一个新的实例时,可以从负载均衡器中移除旧的实例,并添加 新的实例。有些人使用DNS轮询调度,DNS条目会指向一组机器。这种技术存在很严重 的问题,因为底层主机对客户端是不可见的,因此当某个主机有问题时,很难停止对该主 机的请求路由。
图11-9:使用DNS指向负载均衡器,以避免失效DNS条目的问题
 
如前所述,DNS是广为人知并被广泛支持的。但它确实有一两个缺点。我建议在采用更复 杂的方案之前,调查一下它是否适合你。当你只有单个节点时,使用DNS直接引用主机 就可以了。但对于那些有多个主机实例的情况,应该将DNS条目解析到负载均衡器,它 可以正确地把单个主机移入和移出服务。
 
11.13动态服务注册
 
作为一种在高度动态的环境发现节点的方法,DNS存在一些缺点,从而催生了大量的替代 系统,其中大部分包括服务注册和一些集中的注册表,注册表进而可以提供查找这些服务 的能力。通常,这些系统所做的不仅仅是服务注册和服务发现,这可能也不是一件好事。 这是一个拥挤的领域,因此只看其中几个选项,让你大概了解一下有哪些选择可用。
 
11.13.1 Zookeeper
 
Zookeeper ( http://zookeeper.apache.org/)最初是作为Hadoop项目的一部分进行开发的。它
 
被用于令人眼花缭乱的众多使用场景中,包括配置管理、服务间的数据同步、leader选举、 消息队列和命名服务(对我们有用的)。
 
像许多相似类型的系统,Zookeeper依赖于在集群中运行大量的节点,以提供各种保障。 这意味着,你至少应该运行三个Zookeeper节点。Zookeeper的大部分优点,围绕在确保数 据在这些节点间安全地复制,以及当节点故障后仍能保持一致性上。
 
Zookeeper的核心是提供了一个用于存储信息的分层命名空间。客户端可以在此层次结构 中,插入新的节点,更改或查询它们。此外”它们可以在节点上添加监控功能,以便当信 息更改时节点能够得到通知。这意味着,我们可以在这个结构中存储服务位置的信息,并 且可以作为一个客户端来接收更改消息。Zookeeper通常被用作通用配置存储,因此你也 可以存储与特定服务相关的配置,这可以帮助你完成类似动态更改日志级别,或关闭正在 运行的系统特性这样的任务。我个人倾向于不使用Zookeerp这样的系统作为配置源,因为 我认为这使得在给定服务中定位变得更加困难。
 
Zookeeper本身所提供的特性是相当通用的,这就是它适用于这么多场景的原因。你可以 认为它只是信息树的一个副本,当它发生更改时对你做出提醒。这意味着,你通常会在它 上面构建一些功能,以适应你的特定场景。幸运的是,大多数语言都提供了客户端库。
 
在众多选项中,Zookeeper可以说是比较老的,而且对比新的替代品,在服务发现方面 没有提供很多现成的功能。即便如此,它还是被充分使用和测试过的,并得Pf了广泛使 用。Zookeepei•底层算法的正确实现相当困难。例如,我知道一个数据库供应商,只使用 Zookeeper作为leader选举,以确保在出现故障的情况下,能够正确提升主节点。这个客 户认为Zookeeper太重量级了,然后自己实现r PAX0S算法来替换Zookeeper,结果花费了大量时间来修复其中的缺陷。人们常说,你不应该实现自己的加密算法库。我想延 伸这个说法,你也不应该实现自己的分布式协调系统。使用已有的可工作的选择是非常 明智的。
 
11.13;2 Consul
 
和Zookeeper—样,Consul ( http://www.consul.io/)也支持配置管理和服务发现。但它比 Zookeeper更进一步,为这些关键使用场景提供了更多的支持。例如,它为服务发现提供 一个HTTP接口。Consul提供的杀手级特性之一是,它实际上提供了现成的DNS服务器。(也就是部署了Consul的机器可以作为一台DNS服务器) 具体来说,对于指定的名字,它能提供一条SRV记录,其中包含IP和端口。这意味着, 如果系统的一部分已经在使用DNS,并且支持SRV记录,你就可以直接开始使用Consul, 而无需对现有系统做任何更改。
 
Consul还内置了一些你可能觉得有用的其他功能,比如,对节点执行健康检查的能力。 这意味着,Consul提供的功能与其他专门的监测工具提供的有重叠,虽然你更可能使 用Consul作为这些信息的数据源,然后把它放到更全面的仪表板或报警系统中。不过, Consul的高容错设计,以及在处理大量使用临时节点系统方面的专注,确实让我好奇是否 在一些场景下,它最终可以取代像Nagios和Sensu这样的系统。(运维监控软件Sensu)
 
Consul从注册服务、查询键/值存储到插入健康检查,都使用的是RESTful HTTP接口, 这使集成不同技术栈变得非常简单。另一个让我非常喜欢的事情是,Consul背后的团队把 底层集群管理拆分了出来。Consul底层的Serf可以处理集群中的节点监测、故障管理和报 警。然后Consul在其之上添加了服务发现和配置管理。这种关注点分离的做法很吸引我, 这应该不会让你感到奇怪,因为这个主题贯穿本书!
 
Consul很新,鉴于它使用算法的复杂性,我通常犹豫是否要推荐用它来完成这种重要的工 作。尽管如此,Hashicorp,其背后的团队,确实在创建非常有用的开源技术方面有很好的 记录(Packer和Vagmnd),这个项目还在积极发展中,我也和几个在生产环境上使用过它 的人聊过。鉴于此,我认为它很值得一看。
 
Eureka
 
Netflix 的开源系统 Eureka ( https://github.com/Netflix/eureka),追随 Consul 和 Zookeeper 等系统的趋势,但它没有尝试成为一个通用配置存储。实际上,它有非常确定的目标使用 场景。
 
Eureka还提供了基本的负载均衡功能,它可以支持服务实例的基本轮询调度查找。它提供 了一个基于REST的接口,因此你可以编写自己的客户端,或者使用它自己提供的Java客 户端。Java客户端提供了额外的功能,如对实例的健康检查。很显然,如果你绕过Eureka 的客户端,直接使用REST接口,就可以自行实现了。
 
通过客户端直接处理服务发现,可以避免一个单独的进程。但每个客户端需要实现服务发 现。Netflix在JVM上进行了标准化,让所有的客户端都使用Eureka来达到这个目的。如 果你在一个多语言的环境中,挑战会更大。
 
11.13.4构造你自己的系统
 
我自己用过并且在其他地方见过的一种方法是,构造你自己的系统。(构建自己的服务发现功能)曾经在一个项目上, 我们大量使用AWS,它提供了将标签添加到实例的能力。当启动服务实例时,我使用标 签来帮助定义实例是做什么的。这允许你关联一些丰富的元数据到给定的主机,例如:
•服务=账户 
•环境=生产 
•版本=154
 
我可以使用AWS的API,来查询与给定AWS账户相关联的所有实例,找到所关心的机 器。在这里,AWS本身处理与每个实例相关联的元数据的存储,并为我们提供查询的能 力。然后我构建了命令行工具来实现与这些实例之间的交互,并构建仪表板使状态监控变 得更加容易,尤其是当你让每个服务实例都提供健康检查接口时。
 
上一次,我没有做到使用AWS的API来发现服务依赖关系这一步,但事实上我可以做到。 很明显,如果你希望当下游服务的位置发生变化时,上游服务能得到提醒,就需要自己构 建系统。
 
11.13.5别忘了人
 
到目前为止,我们看过的系统让服务实例注册自己并查找所需要通信的其他服务变得非常 容易。但是我们有时也想要这些信息。无论你选择什么样的系统,要确保有工具能让你在 这些注册中心上生成报告和仪表盘,显示给人看,而不仅仅是给电脑看。
 
11.14文档服务
 
通过将系统分解为更细粒度的微服务,我们希望以API的形式暴露出很多接缝,人们可以 用它来做很多很棒的事情。如果正确地进行了服务发现,就能够知道东西在哪里。但是我 们如何知道这些东西的用处,或如何使用它们? 一个明显的选择是API的文档。当然,文 裆往往会过时。理想情况下,我们会确保文档总是和最新的微服务API同步,并当大家需 要知道服务在哪里时,能够很容易地看到这个文档。两种不同的技术,Swagger和HAL, 试图使这成为现实,这两个都值得一看。
 
11.14.1    Swagger
 
Swagger让你描述API,产生一个很友好的Web用户界面,使你可以查看文档并通过Web 浏览器与API交互。能够直接执行请求是一个非常棒的特性。例如,你可以定义POST模 板,明确微服务期望的内容是什么样的。
 
要实现这些,Swagger需要服务提供与其格式相匹配的附属文件。Swagger有大量的不同 语言的库可以帮你做这些。例如,对于Java,你可以对方法使用注解来匹配API调用,然 后就可以生成相应的文件。
 
我喜欢Swagger提供的终端用户体验,但它为超媒体核心中的增量探索概念做得很少。尽 管如此,它仍是一个很好的公开服务文档的方法。
 
11.14.2    HAL 和 HAL 浏览器
 
HAL (Hypertext Application Language,超文本应用程序语言, http://stateless.co/hal_ specification.html)本身是一个标准,用来描述我们公开的超媒体控制的标准。正如我们 在第4章中提过的,超媒体控制是一种方法,它允许客户逐步探索我们的API来使用服 务,并且其耦合度比其他集成技术都低。如果你决定采用HAL的超媒体标准,那么不仅 可以利用广泛使用的客户端库消费API (在撰写本文时,HAL维基列出了多种不同语言 的50个支持库),也可以使用HAL的浏览器,它提供了一种通过Web浏览器探索API 的方式。
 
就像Swagger,这个用户界面不仅可以充当活文档,还可以对服务本身执行调用。虽然它 的执行调用并不是那么顺畅。使用Swagger时,你可以对像发一个POST请求这样的事情 定义模板,而使用HAL时你需要自己做更多。另一方面是,超媒体控制的内在能力能够 让你更有效地探索API公开的服务,因为就可以很容易地跟随链接。事实证明,Web浏览 器很擅长做这种事情!
 
跟Swagger不同,驱动这个文档的所有信息和沙箱都被嵌入在超媒体控制中。这是一把双 刃剑。如果你已经在使用超媒体控制,几乎可以毫不费力地提供一个HAL浏览器,让客 户探索你的API。然而,如果没有使用超媒体,那你要么不使用HAL,要么改造你的API 来使用超媒体,这个行为很可能会破坏现有的消费者。
 
HAL还描述了一些超媒体标准,并有相应的客户端支持库,这是一个额外的好处,也许 这就是在已使用超媒体控件的人中,使用HAL作为API文档比使用Swagger更多的原因。 如果你在使用超媒体,我更推荐使用HAL而不是Swaggei*。但是如果你没有使用超媒体, 也不能判断将来是否切换,我肯定会建议使用Swagger。
 
 
11.15自描述系统
 
在 SOA 的早期演化过程中,UDDI (Universal Description, Discovery, and Integration,通用 描述、发现与集成服务)标准的出现,帮助人们理解了哪些服务正在运行。这些方法都相 当重量级,并催生出一些替代技术去试图理解我们的系统。Martin Fowler提出了人文注册 表( http://martinfowler.com/bliki/HumaneRegistry.html)的概念,它是一个更轻量级的方法, 在这个方法中有一个地方,可以让人们记录组织中有关服务的信息,和维基一样简单。
 
有一个关于我们系统行为的全景图是非常重要的,特别是在规模化后。我们已经讨论了许 多不同的技术,它们会帮助我们理解系统。通过追踪下游服务的健康状态和使用关联标识, 可以帮助我们识别调用链,得到关于服务如何交互的真实数据。使用像Consul这样的服务 发现系统,可以看到我们的微服务在哪里运行。HAL让我们在任何给定的接口上查看有哪 些功能,同时健康检查页面和监控系统,让我们知道系统整体的和单个服务的健康状态。
 
所有这些信息都能以编程的方式使用。所有这些数据使我们的人文注册表,比一个毫无疑 问会过时的简单维基页面更强大。所以我们应该使用它来显示系统发出的所有信息。通过 创建自定义的仪表盘,我们可以将大量信息结合在一起,帮助我们理解生态系统。
 
无论如何,从活系统中抽取出一些数据,来形成静态Web页面或维基是一个很好的开始。 随着时间的推移,获取的信息越来越多。简化这些信息的获取,是系统运行规模化后管理 浮现出来的复杂性的关键工具。
 
11.16 小结
 
作为一种设计方法,微服务还相当年轻,所以虽然我们有一些很好的经验可以借鉴,但我 相信未来几年,会产生更多有用的模式来处理规模化。尽管如此,我希望本章列出的一些 步骤,可供你在规模化微服务的旅途中借鉴,并打下良好的基础。
 
除了本章所涵盖的内容,我推荐Nygard的优秀图书Release It!。在书里他分享了一系列关 于系统故障的故事,以及一些处理它们的模式。这本书很值得一读(事实上,我甚至认为 它应该成为构建任何规模化系统的必读书籍)。
 
我们已经讨论了很多,并已经接近尾声了。在下一章也就是最后一章中,我会综合所有内 容,总结本书所学的内容。
 
 
 
 
 
 
 
 
 
 
 

猜你喜欢

转载自www.cnblogs.com/mongotea/p/11992123.html
今日推荐