第07课:分布式服务架构——最核心的架构

第07课:分布式服务架构——最核心的架构

RPC 架构解决了分布式环境下两个独立进程之间通过网络进行方法调用和数据传输这一基础性问题,但光有 RPC 是不够的。当服务越来越多时,服务地址的配置管理会变得非常困难,单点系统的访问压力也越来越大。当服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,以至于无法描述应用的架构关系。同时,服务的调用量越来越大,服务的容量问题就暴露出来。这些场景都不包含在 RPC 的职能范围之内,我们需要通过引入更加全面和强大的架构体系来解决这些问题,这种架构体系就称为分布式服务架构。在本篇中,我们同样通过核心组件来对分布式服务架构进行具体展开,这些核心组件包括负载均衡、服务路由、服务发布与调用、服务监控和治理、服务可靠性。

负载均衡

所谓负载均衡(Load Balance),简单讲就是将请求分摊到多个操作单元上进行执行,如下图中来自客户端的请求通过负载均衡机制将被分发到各个服务器,根据分发策略的不同将产生该策略下对应的分发结果。负载均衡建立在现有网络结构之上,它提供了一种廉价有效透明的方法扩展服务器的带宽、增加吞吐量、加强网络数据处理能力、提高网络的灵活性。负载均衡实现上可以使用硬件、软件或者两者兼有,本文主要介绍基于软件的负载均衡机制。

负载均衡根据服务器地址列表所存放的位置可以分成两大类型,一类是服务器端负载均衡,一类是客户端负载均衡。另一方面,以各种负载均衡算法为基础的分发策略决定了负载均衡的效果。本节中我们将分别围绕负载均衡的类型以及相应的算法展开讨论。

服务器端负载均衡

在分布式服务架构中,下图为服务器端负载均衡机制示意图,客户端发送请求到负载均衡器,这台负载均衡器负责将接收到的各个请求转发到运行中的某台服务节点上,然后接收到请求的服务做响应处理。提供服务器端负载均衡的工具有很多,例如常见的 Apache、Nginx、HAProxy 等都实现了基于 HTTP 协议或 TCP 协议的负载均衡模块。

基于服务器端的负载均衡机制实现比较简单,只需要在客户端与各个微服务实例之间架设集中式的负载均衡器即可。负载均衡器与各个服务实例之间需要实现服务诊断以及状态监控,通过动态获取各个服务的运行时信息决定负载均衡的目标服务。如果负载均衡器检测到某个服务已经不可用时就会自动移除该服务。

通过上述分析,可以看到负载均衡器运行在一台独立的服务器上并充当代理(Proxy)的作用。所有的请求都需要通过负载均衡器的转发才能实现服务调用,这可能会是一个问题,因为当服务请求量越来越大时,负载均衡器将会成为系统的瓶颈。同时,一旦负载均衡器自身发生失败,整个服务的调用过程都将发生失败。因此,在分布式服务架构中,为了避免集中式负载均衡所带来的这种问题,客户端负载均衡同样也是一种常用的方式。

客户端负载均衡

客户端本身同样可以实现负载均衡,客户端负载均衡最基本的表现形式如下图所示。客户端负载均衡机制的主要优势就是不会出现集中式负载均衡所产生的瓶颈问题,因为每个客户端都有自己的负载均衡器,该负载均衡器的失败也不会造成严重的后果。另一方面,由于所有的运行时信息都需要在多个负载均衡器之间进行传递,会在一定程度上加重网络流量负载。

客户端负载均衡,简单的说就是在客户端程序里面,自己设定一个调度算法,在向服务器发起请求的时候,先执行调度算法计算出目标服务器地址。也就是说每个服务中包含着各个服务器的配置信息,然后通过负载均衡算法计算目标服务器实现负载均衡。

客户端负载均衡的另一种典型实现方式是把 Nginx 等能够实现代理功能的负载均衡器部署到运行服务的同一台机器上。当然,这种方式需要考虑实施成本和维护性问题。

客户端负载均衡比较适合于客户端具有成熟的调度库函数、算法以及 API 的工具和框架。一般可以选择为初期简单的负载均衡方案,也可以结合其他负载均衡方案进行架构。

负载均衡算法

无论是使用服务器端负载均衡还是客户端负载均衡,运行时的分发策略决定了负载均衡的效果。分发策略在软件负载均衡中的实现体现为一组分发算法,通常称为负载均衡算法。负载均衡算法可以分成两大类,即静态负载均衡算法和动态负载均衡算法。

1.静态负载均衡算法

静态负载均衡算法的代表是是各种随机(Random)和轮询(Round Robin)算法。

采用随机算法进行负载均衡在集群中相对比较平均。随机算法实现也比较简单,使用 JDK 自带的 Random 相关随机算法就可指定服务提供者地址。随机算法的一种改进是加权随机(Weight Random)算法,在集群中可能存在部分性能较优服务器,为了使这些服务器响应更多请求,就可以通过加权随机算法提升这些服务器的权重。

加权轮循(Weighted Round Robin)算法同样按照权重,顺序循环遍历服务提供者列表,到达上限之后重新归零,继续顺序循环直到指定某一台服务器作为服务的提供者。普通的轮询算法实际上就是权重为1的加权轮循算法。

2.动态负载均衡算法

所有涉及到权重的静态算法都可以转变为动态算法,因为权重可以在运行过程中动态更新。例如动态轮询算法中权重值基于对各个服务器的持续监控并不断更新。基于服务器的实时性能分析分配连接(比如每个节点的当前连接数或者节点的最快响应时间)是常见的动态策略。类似的动态算法还包括最少连接数(Least Connection)算法和服务调用时延(Service Invoke Delay)算法,前者对传入的请求根据每台服务器当前所打开的连接数来分配;后者中服务消费者缓存并计算所有服务提供者的服务调用时延,根据服务调用和平均时延的差值动态调整权重。

源 IP 哈希(Source IP Hash)算法实现请求 IP 粘滞连接,尽可能让消费者总是向同一提供者发起调用服务。这是一种有状态机制,也可以归为动态负载均衡算法。

服务路由

在集群化环境中,当客户端请求到达集群,如何确定由某一台服务器进行请求响应就是服务路由(Routing)问题。从这个角度讲,负载均衡也是一种路由方案,但是负载均衡的出发点是提供服务分发而不是解决路由问题,常见的静态、动态负载均衡算法也无法实现精细化的路由管理。服务路由的管理也可以归为几个大类,包括直接路由、间接路由和路由规则。

直接路由

所谓直接路由就是服务的消费者需要感知服务提供者地址信息。服务消费者获取服务提供者地址的基本思路是通过配置中心或者数据库,当服务的消费者需要调用某个服务时,基于配置中心或者数据库中存储的目标服务的具体地址构建链路完成调用。这是常见的路由方案,但并不是一种好的方案,一方面服务的消费者直接依赖服务提供者的具体地址,一旦在运行时服务提供者地址发生改变时无法在第一时间通知消费者,可能会导致服务消费者的相应变动,从而增强服务提供者和消费者之间的耦合度。另一方面创建和维护配置中心或数据库持久化操作同样需要成本。

间接路由

间接路由体现了解耦思想并充分发挥了发布-订阅(Publish-Subscribe)模式的作用,发布-订阅机制参考下图,事件(Event)是整个结构能够运行所依赖的基本数据模型,围绕事件存在两个角色,即发布者和订阅者。发布者发布事件,订阅者关注自身所想关注的事件,发布者和订阅者并不需要感知对方的存在,两者之间通过传输事件的基础设施进行完全解耦。

在分布式服务架构中,实现间接路由的组件一般称为服务注册中心(Service Registration Center),服务注册中心从概念上讲就是发布-订阅模式中传输事件的基础设施,可以把服务的地址信息理解为事件的具体表现。

通过服务注册中心,服务提供者发布服务到注册中心,服务消费者订阅感兴趣的服务。服务消费者只需知道有哪些服务,而不需要知道服务具体在什么位置,从而实现间接路由。当服务提供者地址发生变化时,注册中心推送服务变化到服务消费者确保服务消费者使用最新的地址路由信息。同时,为了提高路由的效率和容错性,服务消费者可以配备缓存机制以加速服务路由,更重要的是当服务注册中心不可用时,服务消费者可以利用本地缓存路由实现对现有服务的可靠调用。服务注册中心的基本模型如下图所示:

路由规则

间接路由解决了路由解耦问题,面向全路由场景。在服务故障、高峰期导流、业务相关定制化路由等特定场景下,依靠间接路由提供的静态路由信息并不能满足需求,这就需要实现动态路由,动态路由可以通过路由规则(Routing Rule)进行实现。

路由规则常见的实现方案是白名单或黑名单,即把需要路由的服务地址信息(如服务 IP)放入可以控制是否可见的路由池中。更为复杂的场景可以使用 Python 等脚本语言实现各种定制化条件脚本(Condition Script),如针对某些请求 IP 或请求服务 URL 中的特定语义进行过滤,也可以细化到运行时具体业务参数控制路由效率。

下图对服务路由相关策略做了总结,我们可以看到负载均衡和直接路由、间接路由、路由规则一样都可以看作是一种路由方案,路由方案为服务的消费者提供服务目标地址,并通过网络完成远程调用。

服务发布与调用

服务注册中心是服务发布和引用的媒介,当我们把服务信息注册到注册中心,并能够通过主动或被动的方式从注册中心中获取服务调用地址时,需要考虑的问题就是如何进行有效的服务发布和调用。

服务发布

服务发布的目的是为了暴露(Export)服务访问入口,是一个通过构建网络连接并启动端口监听请求的过程。服务发布的整体流程参考下图,包含了服务发布过程中的核心组件,本节将对这些核心组件做一一展开。

1.发布启动器

发布启动器(Launcher)的作用是确定服务发布形式并启动发布平台。服务的发布形式常见有三种,即配置化、API调用和使用注解。通过以 XML 为代表的配置化工具,服务框架对业务代码零侵入,扩展和修改方便,同时配置信息修改能够实时生效;而通过 API 调用方式,服务框架对业务代码侵入性较强,修改代码之后需要重新编译才能生效;注解方式中,服务框架对业务代码零侵入,扩展和修改也比较方便,但修改配置需要重新编译代码。以上三种方式各有利弊,一般我们倾向于使用基于配置的方式,但在涉及到系统之间集成时,由于需要使用服务框架中较底层的服务接口,API 调用可能是唯一的选择。

发布平台的启动与所选择的发布方式密切相关。在使用配置化发布方式时,通常我们会借助于诸如 Spring 的容器进行服务实例的配置和管理,容器的正常启动意味着发布平台的启动,注解方式下的平台启动也类似。而对于 API 调用而言,简单使用 main 函数进行启动是通常的做法。

2.动态代理

在涉及到远程调用时,通常会在本地服务实现的基础上添加动态代理功能。通过动态代理实现对服务发布进行动态拦截,可以对服务发布行为本身进行封装和抽象,也便于扩展和定制化。JDK 自带的 Proxy 机制以及类如 Javassist 的字节码编辑库都可以实现动态代理。

3.发布管理器

发布管理器在整个服务发布流程中更像是一种承上启下的门户(Facade)。一方面,它获取协议服务器中生成的服务 URL 信息并发布到注册中心,另一方面,发布器也负责通知发布启动器本次发布是否成功。

4.协议服务器

协议服务器是真正实现服务器创建和网络通信的组件。协议服务器的主要作用在于确定发布协议以及根据该协议建立网络连接,并管理心跳检测、断线重连、端口绑定与释放。用于发布服务的常见协议包括 HTTP、RMI、Hessian等。

对于服务发布而言,注册中心的作用是保存和更新服务的地址信息,位于流程的末端。

服务调用

相较服务发布,服务的调用是一个导入(Import)的过程,整体流程如下图所示。图中我们可以看到服务调用流程与服务发布流程呈对称结构,所包含的组件包括:

1.调用启动器

调用启动器的作用就是确定服务的调用形式并启动调用平台,该组件使用的策略与发布启动器一样,不再重复介绍。

2.动态代理

动态代理完成本地接口到远程调用的转换。导入服务提供者接口 API 和服务信息并生成远程服务的本地动态代理对象,从而将本地 API 调用转换成远程服务调用并返回调用结果。

3.调用管理器

调用管理器具备缓存功能,保存着服务地址的缓存信息。当从注册中心获取服务提供者地址信息时,调用管理器根据需要更新本地缓存,确保在注册中心不可用的情况下,调用启动器仍然可以从本地缓存中获取服务提供者的有效地址信息。

4.协议客户端

协议客户端根据服务调用指定的协议类型创建客户端并发起连接请求,负责与协议服务器进行交互并获取调用结果。

在服务调用过程中,实现了从本地缓存获取服务路由、序列化请求消息封装成协议消息、发送协议请求并同步等待或注册监听器回调、反序列化应答消息并唤醒业务线程或触发监听器等分布式服务的基本步骤。如果调用超时或失败,将采用集群容错机制。至此,整个服务发布和调用过程形成闭环。

服务监控与治理

在分布式环境下,围绕某个业务链的所有服务之间的调用关系可能非常复杂,服务中间件、数据库、缓存、文件系统以及其它服务之间都可能存在依赖关系。为了确保系统运行时这些依赖关系的稳定性和可用性,服务调用路径、服务调用业务数据、服务性能数据都是需要监控的内容,以便进行系统故障的预防和定位。

服务监控的基本思路是日志埋点,即使用跟踪 Id 作为一次完整应用调用的唯一标识,然后将该次调用的详细信息通过日志的方式进行保存。日志埋点分为客户端埋点和服务器端埋点,前者关注于跟踪 Id、客户端 IP、调用方接口、调用时间等信息,而后者则记录跟踪 Id、调用方上下文、服务端耗时、处理结果。日志埋点会产生海量运行时数据,通常都需要专门的工具进行处理。基于 Hadoop、Storm、Spark 等技术的离线/实时批量处理框架,基于 Elastic Search、Solr 的垂直化搜索引擎以及专门的 Flume/ELK 等日志处理框架都被广泛应用于埋点数据处理。

服务可靠性

保障服务可靠性的手段也有很多,比较典型的就是集群容错和服务隔离。

集群容错

当服务运行在一个集群中,出现通信链路故障、服务端超时以及业务异常等场景都会导致服务调用失败。容错(Fault Tolerance)机制的基本思想是重试和冗余,即是当一个服务器出现问题时不妨试试其他服务器。集群的建立已经满足冗余的条件,而围绕如何进行重试就产生了几种不同的集群容错策略。

  • Failover

Failover即失效转移,当发生服务调用异常时,重新在集群中查找下一个可用的服务提供者。为了防止无限重试,通常对失败重试最大次数进行限制。典型的 Failover 结构如下图所示。

  • Failback

Failback可以理解为失败通知,当服务调用失败直接将远程调用异常通知给消费者,由消费者捕获异常进行后续处理。

  • Failsafe

失败安全策略中,当获取服务调用异常时,直接忽略。通常用于写入审计日志等操作,确保后续可以根据日志记录找到引起异常的原因并解决。该策略可以理解为一种简单的熔断机制(Circuit Breaker),为了调用链路的完整性,在非关键环节中允许出现错误而不中断整个调用链路。

  • Failfast

快速失败策略在获取服务调用异常时,立即报错。显然,Failfast 已经彻底放弃了重试机制,等同于没有容错。在特定场景中可以使用该策略确保非核心业务服务只调用一次,为重要的核心服务节约宝贵时间。

  • Forking

分支机制,使用该机制时会并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。

  • Broadcast

广播机制,调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息的业务场景,而不是简单的远程调用。

服务隔离

关于服务隔离,首先要介绍舱壁隔离模式(Bulkhead Isolation Pattern),该模式顾名思义就是像舱壁一样对资源或失败单元进行隔离,如果一个船舱破了进水,只损失一个船舱,其它船舱可以不受影响。舱壁隔离模式在分布式服务架构中的应用就是各种服务隔离思想。

服务隔离包括一些常见的隔离思路以及特定的隔离实现技术框架。所谓隔离本质上是对系统或资源进行分割,从而实现当系统发生故障时能限定传播范围和影响范围,即发生故障后只有出问题的服务不可用,保证其他服务仍然可用。隔离的基本思路如下图所示。

上图中的隔离媒介可以是线程、进程、集群、机房以及读写方式。其中关于线程隔离,目前业界也有像 Hystrix 这样优秀的框架可以直接使用,关于线程隔离和 Hystrix 我们将在下一篇《微服务:最热门的架构》有进一步介绍。

实现服务可靠性的其他手段还包括服务限流和服务降级,篇幅关系这里不再展开。

下篇导读

一个分布式系统需要实现远程通信的同时,还要考虑系统中各种服务的治理工作,包括负载均衡、服务路由、服务发布和调用、服务监控与治理、服务可靠性等,本篇对上述内容做了总结。在下一篇中,我们继续沿着分布式服务架构的方向,讨论目前非常流行的微服务架构。

上一篇
下一篇
目录