读构建可扩展分布式系统:方法与实践04应用服务

1. 应用服务

1.1. 任何系统的核心都在于实现应用需求的特定业务逻辑

1.2. 服务是可扩展软件系统的核心

  • 1.2.1. 它们将契约定义为一个API,向客户端声明它们的能力

1.3. 应用服务器高度依赖于编程语言,但通常都会提供多线程编程模型,允许服务同时处理许多请求

1.4. 多服务配置意味着应用程序可以容忍单个实例的故障

1.5. 无状态服务允许负载均衡器简单地将请求重新发送到响应目标,从而轻松实现扩展并简化故障场景

1.6. 大多数负载均衡器采用被称为粘性会话的特性来支持有状态服务,但有状态服务会使得负载均衡和处理故障更加复杂

  • 1.6.1. 不建议在高度可扩展的系统中采用有状态服务

1.7. Java企业版(JEE)是一种成熟且被广泛部署的服务器端技术,它在不同范围提供了高度抽象的规范,可用于构建丰富而强大的服务

2. 服务设计

2.1. API

  • 2.1.1. Application Programming Interface,应用程序编程接口

  • 2.1.2. 定义了客户端与服务器之间的契约

  • 2.1.3. 指定了可能的请求类型、请求所需携带的数据以及请求所将获得的结果

  • 2.1.4. 主要风格还是基于HTTP的,它们通常被归类为RESTful

  • 2.1.4.1. REST是Roy T. Fielding在他的博士论文中定义的一种架构风格

  • 2.1.4.2. HTTP中API的创建、读取、更新和删除(简称CRUD)模式

>  2.1.4.2.1. 有效利用了四个HTTP动词,即POST、GET、PUT和DELETE
  • 2.1.4.3. HTTP PATCH动词来更新资源的各个属性
>  2.1.4.3.1. PATCH允许对一个资源的部分属性进行修改,而PUT则是将一个资源完整地替换成新的资源
  • 2.1.4.4. 有效负载通常被格式化成JSON,当然XML和其他格式也是有可能的

  • 2.1.4.5. HTTP动词与URI组合在一起定义了API操作的语义

>  2.1.4.5.1. 用URI表示的资源在概念上类似于面向对象设计(OOD)中的对象或实体关系模型(ER模型)中的实体
  • 2.1.5. HTTP API可以采用OpenAPI来声明

  • 2.1.5.1. SwaggerHub工具是OpenAPI中声明API的事实标准

  • 2.1.5.2. 该规范是用YAML标记语言来定义的

  • 2.1.6. 一种常见的反模式是Chatty API,它使用多个API请求来执行一个逻辑操作

  • 2.1.7. 对传递大负载的HTTP API进行压缩

  • 2.1.7.1. 所有现代Web服务器和浏览器都支持采用Accept-Encoding和Content-Encoding的HTTP消息头的压缩内容

  • 2.1.7.2. 压缩可以将网络带宽和延迟减少50%甚至更多

  • 2.1.7.3. 要权衡的是压缩和解压缩内容过程中消耗的计算时间,与节省的网络传输时间相比一般都很小

2.2. 设计服务

  • 2.2.1. 应用服务器容器接收请求并将它们路由到适当的处理函数来处理请求

  • 2.2.2. Spring框架利用一组注解来简化服务代码,注解定义依赖关系和实现依赖注入

2.3. 状态管理

  • 2.3.1. 实现可扩展服务的底线是要避免存储会话的状态

  • 2.3.2. HTTP是无状态的协议

  • 2.3.2.1. 意味着每个请求都是独立执行的,对来自同一个客户端的早于它执行的请求一无所知

  • 2.3.2.2. 无状态意味着每个请求都需要是自包含的,不管客户端之前的行为如何,客户端要为Web服务器提供足够的信息来满足请求

  • 2.3.3. HTTP支持cookie,就是大家所知的HTTP状态管理机制

  • 2.3.4. HTTP/2支持流、压缩和加密,所有这些行为都需要状态管理

  • 2.3.5. 客户端和服务器之间底层的socket连接保持打开状态,这样可以分摊同一个客户端多个请求创建连接的开销

  • 2.3.5.1. HTTP/1及以上版本的默认行为

  • 2.3.6. 会话状态指的是在请求之间保留的所有信息,后续请求可以假定服务已保留有关先前交互的信息

  • 2.3.6.1. 如果你有多个客户端会话都要维护会话状态,这将占用可用的服务内存

>  2.3.6.1.1. 占用的内存量将与服务所维护的客户端数量成正比
  • 2.3.6.2. 还必须注意保持会话状态的时长
>  2.3.6.2.1. 如果你将此设置为较短的时长,客户端可能会出现非预期的会话状态丢失

>  2.3.6.2.2. 如果你将会话超时期限设置得太长,则可能会因为资源不足而降低服务性能
  • 2.3.7. 无状态服务不会假定之前调用的任何会话状态已被保留

  • 2.3.7.1. 服务不应该维护来自之前请求的任何信息,每一个请求都可以独立处理

  • 2.3.7.2. 无状态服务要求客户端提供所有必要的信息,以便服务处理请求并提供响应

  • 2.3.8. 任何可扩展的服务都基于无状态的API

  • 2.3.9. 最重要的设计准则是,对于需要保留与客户端会话相关的状态的服务,状态必须存储在服务的外部,即外部数据存储

3. 应用服务器

3.1. 应用服务器是可扩展应用程序的核心,它托管了组成应用程序的业务服务

3.2. 其基本作用是接收来自客户端的请求,将程序逻辑应用于请求,并将请求结果返回给客户端

3.3. Tomcat是JEE平台的一个技术子集的开源实现,即Java servlet、Java Server Page(JSP)、Java Expression Language(EL)和Java WebSocket技术

3.4. 如果收到的请求多于服务器可以排队和处理的请求,那么新的TCP/IP连接将被拒绝,客户端将看到一些错误

  • 3.4.1. 最终,超负荷的服务器可能会因为资源耗尽而抛出异常并崩溃

  • 3.4.2. 花时间调整配置参数以有效处理预期负载是值得的

  • 3.4.3. 系统在达到100%利用率之前往往会降低性能

  • 3.4.3.1. 一旦任何资源(CPU利用率、内存使用、网络、磁盘访问等)接近100%利用率,系统就会表现出难以预测的性能问题

4. 水平扩展

4.1. 扩展系统的核心原则是能够轻松添加新的处理能力来处理增加的负载

4.2. 部署多个无状态服务器资源实例,使用负载均衡器在实例之间分配请求

4.3. 无状态服务副本和负载均衡器都是水平扩展所必需的

4.4. 服务副本部署在它们自己的(虚拟)硬件上

  • 4.4.1. 增加的处理能力使系统能够处理增加的负载

  • 4.4.2. 水平扩展的目的是创建一个系统,它的处理能力是所有可用资源的总和

  • 4.4.3. 服务器需要是无状态的,任何请求都可以发送到任何服务副本来处理

  • 4.4.4. 发送到哪个服务副本是由负载均衡器决定的,它可以使用各种策略来分发请求

  • 4.4.5. 如果负载均衡器可以使每个服务副本保持同样繁忙,那么我们就能有效地利用服务副本提供的处理能力

4.5. 如果我们的服务是有状态的,那么负载均衡器需要始终将来自同一服务器的请求路由到同一个服务副本

  • 4.5.1. 由于客户端会话的持续时间是不确定的,这就可能导致某些服务副本的负载比其他副本高得多

4.6. 水平扩展也提高了可用性

  • 4.6.1. 单服务实例如果失效了,服务就不可用了

  • 4.6.1.1. 单点故障(SPoF)

  • 4.6.1.2. 在任何可扩展的分布式系统中都应避免的一个问题

  • 4.6.2. 多个副本提高了可用性。如果一个副本失败,可以将请求定向到其他任何副本

5. 负载均衡

5.1. 负载均衡旨在有效利用服务集合的容量来优化每个请求的响应时间

5.2. 目的是避免某些服务超载而其他的服务利用不足

5.3. 客户端向负载均衡器的IP地址发送请求,负载均衡器将请求重定向到目标服务,并将结果转发回客户端

  • 5.3.1. 客户端永远不会直接与目标服务通信,这也有利于安全,因为服务可以存在于安全边界之后并且不会暴露在互联网上

5.4. 负载均衡器可以是网络级别或应用级别的,通常被称为四层和七层负载均衡器,分别对应开放系统互连(OSI)参考模型中第4层的网络传输层和第7层的应用层

  • 5.4.1. 网络级别的负载均衡器在网络连接级别分发请求,对单个TCP或UDP数据包进行操作

  • 5.4.1.1. 路由决策是根据客户端IP地址做出的

  • 5.4.1.2. 负载均衡器就会使用一种被称为网络地址转换(NAT)的技术,将客户端请求数据包中的目标IP地址从负载均衡器的地址更改为所选目标的地址

  • 5.4.1.3. 当收到来自目标服务的响应时,负载均衡器将记录在数据包头中的源地址从目标服务的IP地址更改为自己的IP地址

  • 5.4.1.4. 除了选择目标服务和执行NAT功能之外,它们提供的功能很少

  • 5.4.2. 应用级别的负载均衡器会重新组装整个HTTP请求,并根据HTTP头的值和消息的实际内容做出路由决策

  • 5.4.2.1. 应用负载均衡器是复杂的反向代理

  • 5.4.2.2. 意味着它们比网络负载均衡器稍微慢一些,但它们提供的强大功能可以用来弥补所产生的开销

5.5. 负载分配策略

  • 5.5.1. 决定了负载均衡器如何选择目标服务来处理请求

  • 5.5.2. 循环

  • 5.5.2.1. 负载均衡器以循环(round robin)方式将请求分发到可用的服务器

  • 5.5.3. 最少连接数

  • 5.5.3.1. 负载均衡器将新请求分发到打开连接数最少的一个服务器

  • 5.5.4. HTTP头字段

  • 5.5.4.1. 负载均衡器根据特定HTTP头字段的内容来引导请求

  • 5.5.5. HTTP操作

  • 5.5.5.1. 负载均衡器根据请求中的HTTP动词来引导请求

  • 5.5.6. 负载均衡器还可以为服务分配权重

5.6. 健康检测

  • 5.6.1. 负载均衡器会定期给负载均衡池中的每个服务发送ping命令并尝试连接以测试服务的健康状况

  • 5.6.1.1. 这些测试就被称为健康检查

  • 5.6.2. 如果与服务的连接只是发生暂时性故障,负载均衡器将会在服务变为健康可用后重新添加进来

5.7. 弹性

  • 5.7.1. 请求负载急剧增加会导致负载均衡器的服务容量达到饱和,导致更长的响应时间,并最终导致请求和连接失败

  • 5.7.2. 弹性指的是应用动态地提供新的服务容量以处理新增请求的一种能力

  • 5.7.3. 随着请求负载的增加,新的服务副本会被启动,负载均衡器将请求定向到新的副本

  • 5.7.4. 随着负载的减少,负载均衡器则会停止不再需要的服务副本

  • 5.7.5. 弹性要求负载均衡器与应用监控紧密集成,以便定义扩展策略来确定何时增加和减少服务容量

  • 5.7.6. AWS Auto Scaling组是弹性负载均衡的一个实例

  • 5.7.6.1. 定义了集合中服务实例数量的最大值和最小值

  • 5.7.6.2. 负载均衡器将确保组中始终具有最小数量的可用服务,并且永远不会超过最大数量

  • 5.7.6.3. 有两种方法可以控制一个组中的副本数量

>  5.7.6.3.1. 一种是基于时间计划表,相关用例的请求负载的增加和减少是可预测的

>  5.7.6.3.2. 如果无法预测增加的负载峰值,则可以根据应用指标(例如平均CPU和内存使用率以及队列中的消息数量)定义扩展策略来动态控制

  >   5.7.6.3.2.1. 如果超过策略的上限阈值,那么负载均衡器将启动一个或多个新的服务实例,直至性能下降到指标阈值以下
  • 5.7.7. 弹性是一个非常关键的特性,它允许服务随着需求的增长而动态扩展

  • 5.7.7.1. 对于负载频繁波动的高度可扩展系统,它几乎是一种以最低成本提供所需容量的必备能力

5.8. 会话保持(session affinity)

  • 5.8.1. 会话保持或粘性会话(sticky session)是有状态服务的负载均衡器特性

  • 5.8.2. 使用粘性会话,负载均衡器会将来自同一客户端的所有请求发送到同一服务实例

  • 5.8.3. 对于高度可扩展的系统来说,粘性会话可能会有问题

  • 5.8.3.1. 它们会导致负载不均衡的问题,随着时间的推移,客户端不会在服务之间均匀分布

  • 5.8.3.2. 发生负载不均衡的原因是客户端会话保持的时间不同

>  5.8.3.2.1. 在一个拥有百万级会话的系统中,持续不断地创建和销毁会话,负载不均衡是不可避免的

>  5.8.3.2.2. 这将导致一些服务副本不能被充分利用,而另一些则不堪重负,并可能由于资源耗尽而失败
  • 5.8.4. 通常,有状态服务器会产生在大规模系统中难以设计和管理的问题

  • 5.8.5. 如果服务由于短暂的网络中断而变慢,负载均衡器会将其从服务组中取出,直到它通过健康检查或失败

  • 5.8.6. 无状态服务提升了可扩展性,简化了故障场景,并减轻了服务管理的负担

5.9. 通过负载均衡扩展一组服务可能会导致负载均衡服务所依赖的下游服务或数据库不堪重负