读【微服务设计】(七)规模化微服务

1. 故障无处不在

从统计学上来说,规模化后故障会成为必然事件。

所以我们在设计实现微服务系统时只需要尽可能把多的可能故障的因素考虑进去,就可以尽可能保证系统的可用性。

2. 功能降级

微服务系统是由多个服务协同在一起工作的,当某个服务宕机时,我们需要考虑系统的对外表现是怎样的,比如商城系统的购物车服务挂掉了,这时候我们是选择让用户可以继续浏览商品还是将商城主页设置为“系统维护中”呢?这需要结合业务上下文来做决策,通常一个服务挂掉后,我们不会将整个系统设置为不可用,而是适当的处理,部分功能仍然可用,当然,这需要我们在设计时考虑到。

3. 反脆弱方式

  • 超时,超时是容易被忽略的事情,在调用其他服务时,正确设置超时是重要的事情。
  • 断路器,当A服务调用B服务总是超时或失败时(达到一定次数),断路器会打开,后续的请求会快速失败;一段时间后,客户端发送一些请求查看下游服务是否已经恢复,如果正确响应,将重置断路器。
  • 舱壁模式,是把自己从故障中隔离出来的一种方式。在航运领域,舱壁是船的一部分,合上舱口后可以保护船的其他部分。在软件架构领域,有很多不同的“舱壁”可供我们使用,比如给每个服务使用不同的连接池,这样当一个连接池被用尽时,其他服务的调用请求不受影响,断路器也属于一种舱壁。

4. 幂等

对幂等操作来说,其多次执行所产生的影响均与一次执行的影响相等。如果操作是幂等的,我们可以对其重复调用,而不用担心会有不利影响。当我们不确定操作是否被执行,想要重新执行操作,幂等就是非常有用的。

比如HTTP的GET和PUT,在HTTP规范里被定义成幂等的,当然这需要你在定义这些API时,严格遵循规范。

5. 扩展

扩展的目的是提升系统整体性能和可靠性。

  • 使用更强大的主机
  • 拆分负载,也就是使用一个主机运行一个服务的方式,主机通常指的是虚拟主机
  • 不要放在一个篮子,即使你拆分服务运行多个虚拟主机,但是他们都在一台物理主机上,这仍然存在很大的风险。所以尽可能让你的服务运行在不同的数据中心
  • 负载均衡,比如Nginx,能够提升系统吞吐量和可靠性
  • 重新设计,一开始的系统不会是很完善的,能够应对的并发有限,当预见到更多的负载来临时,应该考虑重新设计。

6. 扩展数据库

  • 扩展读取,很多业务场景都是读取为主的,可以使用缓存或只读副本来扩展读取性能。Mysql和Postgresql就具有这个功能,不过你得清楚这种方式下副本的数据可能不是实时的,但具有最终一致性。
  • 扩展写入,这个比扩展读会困难一些,一种方法是使用分片,比如mongodb,使用分片的问题是添加分片时可能导致的数据库宕机时间,在这方面你需要提前进行大量完备的测试。

7. 缓存

是性能优化常用的一种方法,通过存储之前操作的结果,一遍后续请求可以重用这个存储的值,而不需要再花时间和资源重新计算该值。

  • 客户端缓存,由客户端决定何时和是否请求新的数据,服务端会给出相应的提示以帮助客户端决策。
  • 代理服务器缓存,增加一个代理服务器放在客户端和服务器之间。反向代理和CDN就是很好的例子。
  • 服务器缓存,比如redis和memcache,或者服务进程内缓存。

选择哪种缓存方式取决于你想优化什么,客户端缓存可以大大减少网络调用次数,并且是减少下游服务负载的最快方法之一,但采用这种方式,你在想改变缓存的方式时,让大批的客户端全部变化时很困难的。对于代理服务器缓存的方式,一切对客户端和服务端都是不透明的,不过这通常是增加缓存到现有系统的一个非常简单的方法。对于服务端缓存的方式,一切对客户端都是不透明的,这种方式可以方便的跟踪和优化缓存命中率。

  • HTTP缓存,HTTP提供了非常有用的控制手段,帮助我们在客户端或服务端缓存。
    • 首先,我们可以在response的header中添加“cache-control”指令,告诉客户端是否应该缓存资源,以及缓存多久。一些标准的静态网站内容如CSS和图片就适合这种方式。
    • 在response的header中,还可以指定一个expires字段,它指定一个日期和时间,资源会在这个时间之后失效,客户端会重新获取。如果你知道某个资源在多久后会更新,这就是一种合适的方式。
    • Etag,也是放在response的header中的字段,不过要结合request的header使用。它表示资源是否改变。例如我们想获取一个资源,response返回的Etag是05td21d,稍后,cache-control告诉我们缓存到期了,我们会再次请求服务器,不过这个时候可以在request的header中加一个键值对,"If-None-Match:05td21d",服务器会判断这个字段的值,如果Etag不匹配就会返回新的资源+200OK+新的Etag值,否则返回304状态码(未修改)。
    • 注意,上面的几个字段作用会有一些重叠,请充分了解原理后再决定是否同时使用它们。
  • 写缓存
    • 也就是先写缓存/队列,稍后再写入数据库或最终的位置,这也可以提高写性能。
  • 避免缓存击穿击垮服务
    • 作者推荐的方式是在缓存未命中时也不要请求源服务,而是快速失败,但告诉源服务马上填充缓存(异步)。
  • 保持简单,如果在太多地方使用缓存,客户端看到的数据很大可能都是过期的数据,这会导致很多问题。

8. 自动伸缩

就是根据业务的高低峰时段自动扩缩/缩容你的业务集群,这个能力一般由服务托管平台提供,有两种方式可选,一是响应型伸缩,二是预测型伸缩;后者需要通过大量的数据观察得出业务的高低峰时段。

9. CAP定理

分布式系统中的著名定理之一,C(一致性)A(可用性)P(分区容错性),这个定理告诉我们一个系统最多拥有其中两个特点。作者用经典的数据库主从副本场景说明了这个定理,在这里我就直接说结论了。

分布式系统中是不存在CA系统的,因为牺牲了分区容错性就表示所部署的是一个单进程/节点系统,这就不是一个分布式系统了。所以我们最终实现的分布式系统一定是CP或AP的,选择哪一种需要根据业务场景权衡,比如对于一个品类服务,一个记录过时了5分钟,可以接受吗?答案是肯定的,但是对于银行余额来说可以是过时的数据吗?当然不行。

我们的系统作为一个整体,不需要全部是AP或CP的,目录服务可以是AP的,因为记录可以允许是过时的,但对于钱币账户或者积分服务只能是CP的,因为你不会希望用户可以用仅剩的10元重复消费两次10元的商品。所以这个微服务是CP还是CP的?事实上,我们所做的是把关于CAP定理的权衡,推到单独的每个功能中去。

作者最后提到,无论系统本身如何保持一致,它也无法知道所有可能发生的事情,特别是当我们保存的是现实世界的记录,这就是在许多情况下,AP系统都是最终正确选择的原因之一。除了构建CP系统的复杂性外,它本身也无法解决所有问题。

10. 服务发现

这提供了一种便利:一个新的服务实例注册/销毁后,客户端能快速的访问到它/请求别的可用实例。

  • DNS,最简单的方式。DNS将一个名称与多个IP关联,就如同你多次访问某个网址,背后响应的不总是一个IP。DNS的缺点也很明显,域名的DNS条目有一个TTL,客户端认为在这个时间内该条目是有效的。当我们想更改域名所指向的主机时,TTL会导致客户端不能及时访问到新的主机,并且DNS可以在多个地方缓存条目,被缓存的地方越多,这种延迟会越大。绕开这个问题的一个方法是将域名指向负载均衡器的IP,实例的上线/下线由负载均衡器负责。这就如同现在常用的Nginx建站架构。
  • Zookeeper,最初是作为hadoop的一部分开发的,它被用于很多场景,包括配置管理,服务间的数据同步,Leader选举,消息队列和命名服务;它需要部署至少3个节点。
  • Consul,同样支持配置管理和服务发现。它比Zookeeper更进一步,为这些场景提供了更多的支持。它的杀手级特性之一是提供了现成的DNS服务器,具体来说,对于指定的名字,它能提供一条包含IP+端口的SRV记录,这意味者如果系统已经在使用DNS,并且支持SRV记录,你就可以直接开始使用Consul;它还提供节点健康检查的能力。Consul从注册服务、键值读写和健康检查都是Restful HTTP接口。

11. 文档

  • Swagger,它让你描述API,有一个很友好的Web界面,可以通过这个界面直接调用API。实现这些,Swagger需要服务提供与其格式匹配的附属文件,它有大量不同语言的库可以帮你做这些。

你可能感兴趣的:(架构思考,读【微服务设计】,微服务)