微服务设计笔记

微服务设计

  • 微服务的代码库大小的标准:一个微服务应该可以在两周内完全重写
  • 服务之间均通过网络调用进行通信,从而加强了服务之间的隔离性,避免紧耦合。这些服务应该可以彼此间独立进行修改,并且某一个服务的部署不应该引起该服务消费方的变动
  • 微服务可以帮助我们更快地采用新技术,并且理解这些新技术的好处
  • 如果使用较小的多个服务,则可以只对需要扩展的服务进行扩展,这样就可以把那些不需要扩展的服务运行在更小的、性能稍差的硬件
  • 如果使用共享代码来做服务之间的通信的话,那么它会成为一个耦合点
  • 架构师必须改变那种从一开始就要设计出完美产品的想法,相反我们应该设计出一个合理的框架,在这个框架下可以慢慢演化出正确的系统,并且一旦我们学到了更多知识,应该可以很容易地应用到系统
  • 未来的变化很难预见,所以与其对所有变化的可能性进行预测,不如做一个允许变化的计划。为此,应该避免对所有事情做出过于详尽的设计
  • 必须保证每个服务都可以应对下游服务的错误请求.即使你使用的不是HTTP,也应该注意类似的问题。对以下几种请求做不同的处理可以帮助系统及时失败,并且也很容易追溯问题:
    • 正常并且被正确处理的请求
    • 错误请求,并且服务识别出了它是错误的,但什么也没做
    • 被访问的服务宕机了,所以无法判断请求是否正常
  • 聚在一起,就如何做事情达成共识是一个好主意。但是,花时间保证人们按照这个共识来做事情就没那么有趣了,因为在各个服务中使用这些标准做法会成为开发人员的负担。我坚信应该使用简单的方式把事情做对。我见过的比较奏效的两种方式是,提供范例和服务代码模板
    • 如果你有一些很好的实践希望别人采纳,那么给出一系列的代码范例会很有帮助。这样做的一个初衷是:如果在系统中人们有比较好的代码范例可以模仿,那么他们也就不会错得很离谱。
    • 针对自己的开发实践裁剪出一个服务代码模板,不但可以提高开发速度,还可以保证服务的质量
    • 理想情况下,应该可以选择是否使用服务代码模板,但是如果你强制团队使用它,一定要确保它能够简化开发人员的工作,而不是使其复杂化。
    • 有时候我们会决定针对某个规则破一次例,然后把它记录下来。如果这样的例外出现了很多次,就可以通过修改原则和实践的方式把我们的理解固化下来
  • 把相关的行为聚集在一起,把不相关的行为放在别处。为什么呢?因为如果你要改变某个行为的话,最好能够只在一个地方进行修改,然后就可以尽快地发布。如果需要在很多不同的地方做这些修改,那么可能就需要同时发布多个微服务才能交付这个功能。在多个不同的地方进行修改会很慢,同时部署多个服务风险也很高,这两者都是我们想要避免的
  • 避免破坏性修改.有时候,对某个服务做的一些修改会导致该服务的消费方也随之发生改变。但是我们希望选用的技术可以尽量避免这种情况的发生。比如,如果一个微服务在一个响应中添加了一个字段,那么已有的消费方不应该受到影响。
  • 保证APl的技术无关性.保证微服务之间通信方式的技术无关性是非常重要的。
  • 使你的服务易于消费方使用.理想情况下,消费方应该可以使用任何技术来实现,从另一方面来说,提供一个客户端库也可以简化消费方的使用
  • 隐藏内部实现细节.我们不希望消费方与服务的内部实现细节绑定在一起,因为这会增加耦合。与细节绑定意味着,如果想要改变服务内部的一些实现,消费方就需要跟着做出修改。这会增加修改的成本,
  • 服务之间很容易通过数据库集成来共享数据,但是无法共享行为。内部表示暴露给了我们的消费方,而且很难做到无破坏性的修改,进而不可避免地导致不敢做任何修改,所以无论如何都要避免这种情况。
  • 对于请求/响应来说,客户端发起一个请求,然后等待响应。这种模式能够与同步通信模式很好地匹配,但异步通信也可以使用这种模式。我可以发起一个请求,然后注册一个回调,当服务端操作结束之后,会调用该回调。这适用于想要请求/响应风格的语义,又想避免其在耗时业务上的困境
  • 对于使用基于事件的协作方式来说,情况会颠倒过来。客户端不是发起请求,而是发布一个事件,然后期待其他的协作者接收到该消息,并且知道该怎么做。我们从来不会告知任何人去做任何事情。基于事件的系统天生就是异步的.基于事件的协作方式耦合性很低。客户端发布一个事件,但并不需要知道谁或者什么会对此做出响应,这也意味着,你可以在不影响客户端的情况下对该事件添加新的订阅者。这恰恰就是我们为了能独立发布服务而追求的特性
  • 更现代的一些RPC机制,比如protocol buffers或者Thrift,会通过避免对客户端和服务端的lock-step发布来消除客户端和服务器的部署无法分离的问题。
  • 如果你决定要选用RPC这种方式的话,需要注意一些问题:不要对远程调用过度抽象,以至于网络因素完全被隐藏起来;确保你可以独立地升级服务端的接口而不用强迫客户端升级
  • 如果你想要使用客户端库,一定要保证其中只包含处理底层传输协议的代码,比如服务发现和故障处理等。千万不要把与目标服务相关的逻辑放到客户端库中
  • 考虑这样一个例子:发货之后需要请求邮件服务来发送一封邮件。一种做法是,把客户的邮件地址、姓名、订单详情等信息发送到邮件服务。但是邮件服务有可能会将这个请求放入队列,然后在将来的某个时间再从队列中取出来,在这个时间差中,客户和订单的信息有可能就会发生变化。更合理的方式应该是,仅仅发送表示客户资源和订单资源的URI,然后等邮件服务器就绪时再回过头来查询这些信息
  • 如果在获取资源的同时,可以得到资源的有效性时限(即该资源在什么时间之前是有效的)信息的话,就可以进行相应的缓存,从而减小服务的负载
  • 客户端尽可能灵活地消费服务响应这一点符合Postel法则,也叫作[鲁棒性原则](https://tools.ietf.org/html/rfc761)。该法则认为,系统中的每个模块都应该“宽进严出”,即对自己发送的东西要严格,对接收的东西则要宽容
  • 语义化版本管理的每一个版本号都遵循这样的格式:MAJOR.MINOR.PATCH。其中MAJOR的改变意味着其中包含向后不兼容的修改;MINOR的改变意味着有新功能的增加,但应该是向后兼容的;最后,PATCH的改变代表对已有功能的缺陷修复
  • 微服务事务失败
    • 我们可以把这部分操作放在一个队列或者日志文件中,之后再尝试对其进行触发。对于某些操作来说这是合理的,但要保证重试能够修复这个问题。很多地方会把这种形式叫作最终一致性
    • 另一个选择是拒绝整个操作。在这种情况下,我们需要把系统重置到某种一致的状态。发起一个补偿事务来抵消之前的操作
  • 微服务分布式事务:如果现在有一个业务操作发生在跨系统的单个事务中,那么问问自己是否真的需要这么做。是否可以简单地把它们放到不同的本地事务中,然后依赖于最终一致性的概念
  • 巨大的修改代价意味着风险的增大。如何才能控制这些风险?我的方式是在影响最小的地方犯错误
  • CI的理解
    • 你是否每天签入代码到主线?你应该保证代码能够与已有代码进行集成。如果你的代码和其他人的代码没被频繁地放在一起,那么将来的集成就会非常困难。即使你只使用生命周期很短的分支来管理这些修改,也要尽可能频繁地把代码检入到单个主线分支中
    • 你是否有一组测试来验证修改?如果没有测试,我们只能知道集成后没有语法错误,但无法知道系统的行为是否已经被破坏。没有对代码行为进行验证的CI不是真正的CI。
    • 当构建失败后,团队是否把修复CI当作第一优先级的事情来做?
  • 与打桩相比,mock还会进一步验证请求本身是否被正确调用。如果与期望请求不匹配,测试便会失败
  • 保障频繁发布软件的关键是基于这样的一个想法:尽可能频繁地发布小范围的改变
  • 如果我们掉进陷进,为每一个新添加的功能增加一个新的端到端测试,那么这种情况会加剧恶化。当你给我展示每实现一个新的故事便添加一个新的端到端测试的代码库时,我将向你展示一个臃肿的测试套件、很长的反馈周期和巨大的重叠测试覆盖率。解决这个问题的最佳方法是。把测试整个系统的重心放到少量核心的场景上来。把任何在这些核心场景之外的功能放在相互隔离的服务测试中覆盖
  • 有一种不需要使用真正的消费者也能达到同样目的的方式,它就是CDC(Consumer-Driven Contract,消费者驱动的契约)。当使用CDC时,我们会定义服务(或生产者)的消费者的期望。这些期望最终会变成对生产者运行的测试代码。如果使用得当,这些CDC应该成为生产者CI流水线的一部分,这样可以确保,如果这些契约被破坏了的话,生产者就无法部署
  • 微服务部署
    • 蓝/绿部署:我们想要部署一个新版本v456。v123正常工作的同时,我们部署v456版本,但先不直接接受请求。相反,我们先对新部署的版本运行一些测试。等测试没有问题后,我们再切换生产负荷到新部署的v456版本的客户服务。通常情况下,我们会保留旧版本一小段时间,这样如果我们发现任何错误,能够快速恢复到旧的版本
    • 金丝雀发布是指通过将部分生产流量引流到新部署的系统,来验证系统是否按预期执行.金丝雀发布与蓝/绿发布的不同之处在于,新旧版本共存的时间更长,而且经常会调整流量。
  • 微服务监控:
    • 对服务
      • 最低限度要跟踪请求响应时间。做好之后,可以开始跟踪错误率及应用程序级的指标。
      • 最低限度要跟踪所有下游服务的健康状态,包括下游调用的响应时间,最好能够跟踪错误率
      • 标准化如何收集指标以及存储指标。
      • 监控底层操作系统,这样你就可以跟踪流氓进程和进行容量规划
    • 对系统
      • 聚合CPU之类的主机层级的指标及应用程序级指标。
      • 确保你选用的指标存储工具可以在系统和服务级别做聚合,同时也允许你查看单台主机的情况。
      • 确保指标存储工具允许你维护数据足够长的时间,以了解你的系统的趋势。
      • 使用单个可查询工具来对日志进行聚合和存储。
      • 了解什么样的情况需要行动,并根据这些信息构造相应的警报和仪表盘
  • 在试图阻止不可避免的故障上少花一点时间,而花更多时间去优雅地处理它.想想如何更加容易地在第一时间从故障中恢复过来
  • 规模化后,即使你买最好的工具,最昂贵的硬件,也无法避免它们会发生故障的事实。因此,你需要假定故障会发生。如果以这种想法来处理你做的每一件事情,为其故障做好准备,那么就会做出不同的权衡。如果你知道一个服务器将会发生故障,系统也可以很好地应对,那么又何必在阻止故障上花很多精力呢
  • 微服务系统分布在多台机器上(它们会发生故障),通过网络(它也是不可靠的)通信,这些都会使你的系统更脆弱,而不是更健壮
  • 请求在断路器打开的状态下,会快速地失败。一段时间后,客户端发送一些请求查看下游服务是否已经恢复,如果它得到了正常的响应,将重置断路器
  • 舱壁(bulkhead)是把自己从故障中隔离开的一种方式。在航运领域,舱壁是船的一部分,合上舱口后可以保护船的其他部分。所以如果船板穿透之后,你可以关闭舱壁门。如果失去了船的一部分,但其余的部分仍完好无损.舱壁是三个模式里最重要的。超时和断路器能够帮助你在资源受限时释放它们,但舱壁可以在第一时间确保它们不成为限制
  • 对幂等操作来说,其多次执行所产生的影响,均与一次执行的影响相同。如果操作是幂等的,我们可以对其重复多次调用,而不必担心会有不利影响。当我们不确定操作是否被执行,想要重新处理消息,从而从错误中恢复时,幂等会非常有用
  • 在大多数情况下,现在的主机实际上是一个虚拟的概念。如果所有的服务都在不同的主机上,但这些主机实际上都是运行在一台物理机上的虚拟主机呢?如果物理机宕掉,同样也会失去多个服务
  • 使用客户端缓存,如果下游服务不可用,客户端可以先简单地使用缓存中可能失效了的数据。我们还可以使用像反向代理这样的系统提供的失效数据。对一些系统来说,使用失效但可用的数据,比完全不可用的要好,不过这需要你自己做出判断
  • 保护源服务的一种方式是,在第一时间就不要对源服务发起请求。相反,在需要时源服务本身会异步地填充(go singleFlight)
  • 分区容忍性是指集群中的某些节点在无法联系后,集群整体还能继续进行服务的能力
  • 个别服务甚至不必是CP或AP的。让我们考虑一下积分账户服务,那里存储了客户已经积攒的忠诚度积分的记录。我们可以不在乎显示给客户的余额是失效的,但当涉及更新余额时,我们必须保证一致性,以确保客户不会使用比他们实际拥有的更多的积分
  • Cassandra允许为每个调用做不同的权衡。因此如果需要严格的一致性,我可以在执行一个读取时,保持其阻塞直到所有副本回应确认数据是一致的,或直到特定数量的副本做出回应,或仅仅是一个节点做出回应。显然,如果我保持阻塞直到所有副本做出回应,那么当其中一个不可用时,我会被阻塞很长一段时间。但是如果我满足于只需要一个节点做出回应,接受缺乏一些一致性,这样可以降低一个副本不可用所导致的影响。
  • 服务发现:让你的域名条目(服务域名)指向负载均衡器,接着由它来指向服务实例。当你部署一个新的实例时,可以从负载均衡器中移除旧的实例,并添加新的实例
  • 当需要做不兼容更改时,我们也应该同时提供新旧两个版本,允许消费者慢慢迁移到新版本
  • 你可以更改单个服务,然后把它部署到生产环境,无需联动地部署其他任何服务,这应该是常态,而不是例外。你的消费者应该自己决定何时更新,你需要适应他们。