text: 通过过去的数篇文章，我们分别介绍了微服务架构从设计、开发到实施等各个方面，并以业界的前沿应用给出了一定实践案例。然而世界总不是你我想象中的美好，面临千奇百怪的业务需求，随着架构的不断变化，我们总会遇到各式各样的冲突和风险。因此，微服务架构并不适用于一拥而上——循序渐进才是可靠途径。
  分布式系统领域存在一个经典的谬误理论：Fallaciesofdistributedcomputing，基本总结了不可靠通信给系统架构带来的影响。由于网络通信在分布式系统中的极端重要性，使得网络可靠性成为此类系统永恒的梦魇。任何网络的不可靠都会引发各种灾难，且难以被察觉、定位和修复。即使有能力购买先进的硬件设施，以及最可靠的第三方系统，也不能消除出错的可能。
  实际上，问题在于设置一个容错率。因为对跨功能的需求是永不枯竭的，根据业务特点设置容错率，然后按需堆叠架构，才是一个看起来合理的策略。在测试一文中我们已经提到跨功能需求，例如对于响应时间/延迟的需求，应根据现有设施进行合理评估，再和业务需求阈值比较，从而决定改进方案。对于可用性，是否必须是一个24/7类的业务？或者用户完全能够容忍某个偶尔的不可用？对于数据的持续性，业务需求的数据保存时限如何？只有业务需求明确，才有必要开始考虑进一步的演进。
  当需求明确时，我们就可以考虑演进现有架构了——但别忘了前面提到的：分布式系统总会带来不可靠。那么如何度量当前系统的可靠性呢？最有效的手段就是利用现有的监控系统，统计出错率。在正常情况下，系统运行数据能够反映出一定的可靠度，但对某些意外情况，例如网络或其它硬件设备损坏，就很难真的实现了。例如，当服务A需要调用服务B的API时，B开始发生某些本地错误，且不能及时断开链接。那么A将持续保持对B的链接，直到链接超时自动断开，但随着A继续接收上游请求并继续发起对B的链接，从而造成服务A维持一个大规模HTTP链接池，当A的系统资源耗尽，灾难就会随之发生了。
  上面提到的例子在分布式架构中已经非常常见，开发人员已经知道若干种解决方案，包括合理超时、改进链接池、以及开发断路器等。但首要问题是如何发现此类问题？
  Google每年会组织一次灾难恢复测试（DiRT，DisasterRecoveryTest）练习，练习内容包括了针对大规模不可抗拒灾难的演练（例如地震）。Netflix则更进一步，每天会通过工具随机破坏生产环境，用来测试系统的可靠性和恢复能力，此类工具被称作SimianArmy。ChaosMonkey是其中比较着名的一种，其基本功能是每天随机性关闭AWS上的某些节点；ChaosGorilla能够断开整个可用的数据中心；LatencyMonkey能够模拟节点间通信缓慢的情况；上述工具都已经被Netflix贡献给了开源社区。
  因此，在互联网分布式系统的世界中，首先需要改变的就是传统的思维模式：无论系统有多健壮，失败必然发生。那么接下来我们需要考虑的就是如何在失败发生时采取补救措施——大多数情况下这比减少失败来得更容易。
  超时是一项经常被人忽略的关键反馈，因为它预示着下游服务出现了不可预知的问题。但难点在于多长时间才算超时？其实也有简单解决办法，首先，可能引发超时的操作必须在进程外执行；超时时限应在全系统内保持唯一值；一旦发生超时，即记录进日志系统。定期查看超时部分的记录，进行有效改善。
  想象一下家中电表里的空气开关，当电流达到峰值时，空开自动关闭，从而避免对家中电器造成损坏。当屋内长期无人时，也可以手动关闭空开，防止意外发生。空气开关就是断路器的一种，而在《ReleaseIt!》中，断路器则成为提高系统可用性的关键手段。
  再思考一下超时的情况。一旦下游服务故障，服务调用将发生超时，由于时限是默认设置的，越来越多的超时将发生，并进一步拖慢系统，直至崩溃（前文已提过）。断路器的作用是，当超时发生次数超过一个阈值，就切断到某个服务的通信，即超时时限趋近于零。即“快速死亡”。
  断路器能够避免错误发生时，故障蔓延至整个系统。在无法确定分布式系统可靠性的情况下，也确实是一个十分有效的解决方案。但这里需要注意，断路器并不是什么万能神器，只不过是起到“壮士断腕”的作用——想象一下，万一断路器“过早”启动了呢？在实践中，如果服务调用是异步的，那么采用消息队列进行处理也没什么问题。而如果是同步调用，就需要考虑一下引入断路器提高可用性了。
  《ReleaseIt!》中提到一种隔板模式。这种模式可以解释成船舱中的隔板结构，当船舱进水时，为了避免整船沉没，通常会选择关闭已经进水的船舱。一个简单的例子是采用服务——连接池模式，即对于每一个下游服务都保持独立的连接池，即使某个下游服务出现问题，也只会导致对应的连接池拥塞，而不会影响整个系统。
  在实际应用时，隔板方法几乎是标配的做法，而超时和断路器则可根据情况添加，因为前者是预防问题，而后者更多是收拾残局。例如对于同步调用，配置断路器则是必须。上述模式在实现起来也有很多开源选择，例如Netflix的Hystrix，.Net的Polly，以及Ruby下的circuit_breaker。
  无论同一个操作执行多少遍（大于等于1），输出结果不变，这就被称为幂等操作。幂等性在构建Web应用时非常重要，因为消息可能因为某些原因被重放多次，而当这些消息都到达服务器时，不应把这些消息认为是不同的请求进行多次处理。保持幂等操作可以提高应用的可靠性，这在实践时非常重要。
  值得一提的是，HTTP协议天然定义了GET、PUT操作应是幂等性的，如果破坏了这一原则，则可能会给系统带来许多问题。
  扩展的目的无非有二：容错和高性能。最直观的扩展方法就是更换CPU和I/O设备，被称作纵向扩展，这种方法实际上成本非常高昂，特别是当前芯片研发和生产已经陷入烧钱的地步，即使不差钱，要让应用充分利用多核和高速I/O架构也非易事。因此实际中更多采用的是横向扩展，特别是当虚拟化技术普及，若干廉价服务器组成的集群就可以很好解决扩展的问题。微服务架构的扩展技术实际上与单一系统类似，但也有一些区别。
  之前讨论过单服务－单主机的微服务部署方式，在实际应用中，为了节省成本更多团队选择在同一台主机上运行多个服务，但是随着负载需求的增加，采用单对单部署则是一种相对简单有效的解决方法。
  另一方面，如果某个服务负载确实很高，单纯的主机数量增加不能直接解决微服务架构的问题，那就要进一步考虑做拆分了（这种拆分避免不了对业务的考量，如果是非核心功能，还是建议直接独立出来）。
  横向扩展能把风险平摊到不同主机，从而提高可用性，但具体操作起来需要考虑许多因素。首先，基于现有的虚拟化平台，即使实现单对单部署，所谓的“主机”也不过是一台虚拟主机，不能保证“主机”不在同一台物理机上，那也就不能实现平摊风险的效果，不过还好许多平台目前已经支持了跨不同主机运行服务的功能。
  在企业内部虚拟化平台中曾流行采用StorageAreaNetwork，即SAN。SAN是一种十分昂贵的存储区域网络，旨在达到无错运行。而一旦采用SAN且后者挂掉，则整个虚拟集群就会受到影响，因此实际上还是不能避免单点失败的问题。
  另一个常见做法是采用多数据中心运行，这其实和IaaS的提供商有关。例如AWS采用的多区域数据中心架构，其保证对EC2实现99.95%的正常服务概率每月每区域，为了实现更高可用性，在此基础上部署应用到不同区域即可实现该目的。
  无论如何，对服务商的ServiceLevelAgreement，SLA的熟悉也十分重要，当灾难真的发生，如何“维权”也就在于你对该协议的理解程度了。
  负载均衡是一种十分简单有效的横向扩展技术，目前基本成为多实例架构的标配。负载均衡组件能够接收来源请求，根据自身算法把该请求发送至多个平行实例中的某一个，从而实现负载分担。
  现在，负载均衡技术非常成熟，包括从硬件方案到软件方案（例如mod_proxy）。不过其功能都类似，当某个实例不可用时将其屏蔽，一旦恢复则将其重新开启。一些负载均衡设施提供更加高级的功能，例如SSL终端。我们已经讨论过采用HTTPS防止中间人攻击和消息泄密的问题，但对系统内部的服务间通信来说，HTTPS会影响其整体运行性能。因此SSL终端就起到了把HTTPS转换成HTTP从而在内网通信的作用。
  商业负载均衡，如AWS的ELBs就支持上述功能，同时还可以在系统内部构建VLAN，从而实现内部的高效通信。
  对于硬件负载均衡设备来说，自动化始终是一个问题。如果企业更信任硬件方案，也不妨在后端搭设一个软件负载均衡设施，从而提高自动化的能力。
  负载均衡并非分担系统负载的唯一选择，基于Worker的系统架构同样有效。Worker被用于如Hadoop批处理进程模型和共享队列的异步事件监听，擅长批处理和异步处理。如图片处理、发送邮件和生成报表等。一旦达到系统峰值，新的Worker能被添加进当前系统，从而增加吞吐量。
  对Worker架构来说，尽管每一个Worker可以是不可靠的，但组织Worker的中央系统应保证可靠性。例如采用持久消息广播Zookeeper。
  当目前的架构无法应对越来越多的用户，重新设计可能是必然的选择，因此初期预留扩展接口是有必要的。当然，如今许多人会选择一开始就预估一个巨大的访问量，从而在此基础上直接构建大规模架构。这是一件非常危险的事，甚至有可能引发灾难。因为在初期我们更强调精益化，而非大而全。后者明显会引发无用的付出。
  因此要时刻了解：架构的演进是必须的过程，我们能做的只是留个心眼儿。何况系统扩展的需求并非标志着失败，而是巨大的成功。
  前文提到的扩展，通常只适用于针对无状态的服务。而如果是操作数据库的服务，情况则不同。最为直接的方法就是，借助数据库产品自身提供的扩展特性，而这就需要在项目初期就按需确定数据库产品的选择。
  应当明确的是，这两者完全不是一回事，因此解决问题的思路并不一致。例如当产品数据库出现问题，由于数据本身存有备份，因此持久性没太大问题。但数据库本身不可用，整个依赖于此的应用服务也就不可用。用现代运维技术的观点来看，即使设置了Primary和Replica，当Primary出现问题，如果缺少同步Replica到Primary的机制，就还是缺少可用性。
  针对读操作密集的应用，其扩展复杂度要比针对写操作低得多。因为目前存在各种各样的缓存技术（下篇文章将讨论），这里给出另外一种数据库架构：读备库。
  在关系型数据中，例如MySQL和Postgres，数据可以从主节点被复制到多个备库节点，这样一来确保了数据的安全性，同样也可被用于分担读操作。一般来说，写操作由一个主节点负责，读操作则由更多备库节点负责。复制操作发生在写操作的之后，从而在读库上存在一定的脏数据时间，当然最终能够保证一致性。这种系统其实是所谓的“最终一致”，通过牺牲严格的一致性来达到系统的扩展（参见分布式系统的CAP定律）。
  然而随着缓存技术的发展，实现缓存要比搭建读库方便得多，成本也更低。一般也应该根据业务需求选择不同的扩展方式。
  前面提到写操作扩展相对不那么容易，但也存在一般方案：分片。分片可以在写数据时，将数据按照某种哈希规则分布至多个节点上，从而实现横向扩展。在现代数据库技术中，分片几乎成为标配。
  分片的难点在于处理查询问题。在单片数据库中，查询基本上是通过内存和索引一次完成的，但是对于分片数据库，查询可能分别发生在不同的节点中，而这种多次查询的方式最终用异步机制组织起来，并配置缓存。例如MongoDB，就采用Map/Reduce架构解决查询问题。
  分片系统的另一个问题是，当要增加新的数据库节点时，系统需要整体停机，特别是针对大规模的分片＋Replica系统。目前最新的数据库技术已经支持在线扩充节点，并且能在后台保证数据的重新平衡，例如Cassandra。
  分片能够实现读操作的横向扩展，但无法满足扩展的另一个需求：可靠性。Cassandra先行一步，实现了环上多节点备份以提高可靠性。
  CQRS指命令查询职责隔离，这是一种极端的数据解耦模式。在标准数据库应用中，通常采用同一系统执行数据操作和查询。但是在CQRS模式下，负责处理命令并操作数据的模块和查询模块应当是分离的。
  CQRS带来的隔离性使得扩展方式更加多样化，同时一些业务需求也确实符合操作/查询分离的情况。在CQRS中，一种更加极端的形式甚至是同一个数据仓库，分别存在基于图的表示和基于键值对的表示等各种读方案。然而需要提醒的是，CQRS的应用场景目前还比较狭窄，因为传统CRUD的方式与次存在很大程度的不同，对团队来说将是一个巨大的挑战。