由于惯性思维,很多人会将传统 MVC架构/RPC架构的做法带入到分布式服务框架的架构设计中,其中有些思想存在误区,或者已经过时,它们会破坏分布式服务框架的架构品质。

1 几个误区

1.1 NIO 就是异步服务

NIO 只解决了通信层面的异步问题,跟服务调用的异步没有必然关系,也就是说,即便采用传统的 BIO 通信,依然可以实现异步服务调用,只不过通信效率和可靠性比较差。
下面对异步服务调用和通信框架的关系进行说明:

用户发起远程服务调用之后,经历层层业务逻辑处理、消息编码,最终序列化后的消息会被放入到通信框架的消息队列中。业务线程可以选择同步等待、也可以选择直接返回,通过消息队列的方式实现业务层和通信层的分离是比较成熟、典型的做法。
采用 NIO还是 BIO对上层的业务是不可见的,双方的汇聚点就是消息队列。业务线程将消息放入到发送队列中,可以选择主动等待或者立即返回,跟通信框架是否是 NIO 没有任何关系。

1.2 服务调用天生就是同步的

服务调用主要有两种模式:

  1. OneWay 模式:只有请求,没有应答,例如通知消息。
  2. 请求-应答模式:一请求,一应答的模式,这种模式最常用。

OneWay 模式的服务调用由于不需要返回应答,因此很容易被设计成异步的:消费者发起远程服务调用之后,立即返回,不需要同步阻塞等待应答。
对于请求-应答模式,可以利用 Future-Listener 机制来实现异步服务调用。从业务角度看,它的效果与同步等待等价,但是从技术角度来看,可以保证业务线程在不同步阻塞的情况下实现同步等待的效果,执行效率更高。

1.3 异步服务调用性能更高

复杂的场景,异步服务调用会更高,越复杂的场景,异步服务调用优势越大。

2 服务调用方式

2.1 同步服务调用

没什么可说的,只是需要注意设置用户线程等待超时时间。

2.2 异步服务调用

基于 JDK的 Future机制,异步服务调用的工作原理如下:

  1. 消费者调用服务端发布的接口,接口调用由分布式服务框架包装成动态代理,发起远程服务调用。
  2. 通信框架异步发送请求消息,如果没有发生 I/O异常,返回。
  3. 请求消息发送成功后,I/O 线程构造 Future 对象,设置到 RPC上下文中。
  4. 用户线程通过 RPC上下文获取 Future对象。
  5. 构造 Listener 对象,将其添加到 Future中,用于服务端应答异步回调通知。
  6. 用户线程返回,不阻塞等待应答,
  7. 服务端返回应答消息,通信框架负责反序列化等。
  8. I/O 线程将应答设置到 Future 对象的操作结果中。
  9. Future 对象扫描注册的监听器列表,循环调用监听器的 operationComplete方法,将结果通知给监听器,监听器获取到结果,执行后续业务,异步调用结束。

还有一种异步调用形式,就是不添加 Listener,用户连续发起 N次服务调用,然后依次从 RPC上下文中获取 Futrue对象,最终再主动 get结果,业务线程阻塞,相对比老的同步服务调用,它的阻塞时间更短,工作原理如下:

其串行到并行的优化原理如下图:

异步服务调用的代码示例如下:

1
2
3
4
5
6
XxxService1.xxxMethod(Req); // 立即返回 null
Future f1 = RpcContext.getContext().getFuture();
XxxService2.xxxMethod(Req);
Future f2 = RpcContext.getContext().getFuture();
Object xResult1 = f1.get(3000);
Object xResult2 = f2.get(3000);

第二种基于 Future-Listener 的纯异步服务调用示例如下:

1
2
3
4
XxxService1.xxxMethod(Req);
Future f1 = RpcContext.getContext().getFuture();
Listener l = new Listener();
f1.addListener(l);

2.3 并行服务调用

A服务->B服务->C服务->…
串行服务调用比较简单,但在一些业务场景中,需要采用并行服务调用来降低 E2E 的时延。

  1. 多个服务之间逻辑不存在互相依赖关系,执行先后顺序没有严格的要求,逻辑上可以被并行执行。
  2. 长流程业务,调用多个服务,对时延比较敏感,其中有部分服务逻辑上无上下文关联。
    目标主要有两个:
  3. 降低业务 E2E 时延。
  4. 提升整个系统的吞吐量。

2.4 泛化调用

主要用于客户端没有API 接口及数据模型的场景,使用 Map表示。

3 最佳实践

服务框架支持多种服务调用方式,在实现项目中中如何选择,建议从以下几个角度考虑:

  1. 降低业务 E2E时延:业务调用链是否太长、某些服务是否不太可靠,需要对服务调用流程进行梳理,看是否可以通过并行服务调用来提升调用效率,降低服务调用时延。
  2. 可靠性角度:某些业务调用链上的关键服务不太可靠,一旦出故障会导致大量线程资源被挂住,可以考虑使用异步服务调用防止故障扩展。
  3. 业务场景:对于测试,不想为每个测试用例都开发一个服务接口,能否做一个通用的测试框架,通过 Map等泛容器实现通用服务调用。
  4. 传统的 RPC调用:服务调用比较简单,对时延要求不高的场景,可以考虑同步服务调用。

4 个人总结

服务调用有多种形式,需要从业务和技术做出取舍。

集群服务调用失败后,服务框架需要能够在底层自动容错。

1 集群容错场景

在分布式服务框架中,业务消费者不需要了解服务提供者的具体未知,它发起的服务调用请求也不包含服务提供者具体地址信息。因此,某个服务提供者是否可用对消费者而言无关紧要,最终的服务调用成功才是最重要的。
经过服务路由之后,选定某个服务提供者进行远程服务调用,但是服务调用可能会出错,下面进行故障场景进行分析。

1.1 通信链路故障

这里的链路指的是消费者和服务提供者之间的链路(通常为长连接),可能导致链路中断的原因有:

  1. 通信过程中,对方突然宕机导致链路中断。
  2. 通信过程中,对方因为解码失败等原因 Rest 掉连接,导致链路中断。
  3. 通信过程中,消费者 write SocketChannel 发生 IOException 导致链路中断。
  4. 通信过程中,消费者 read SocketChannel 发生 IOException 导致链路中断。
  5. 通信双方因为心跳超时,主动 close SocketChannel 导致链路中断。
  6. 通信过程中,网络发生闪断故障。
  7. 通信过程中,交换机异常导致链路中断。
  8. 通信过程中,消费者或者服务提供者因为长时间 Full GC 导致链路中断。

1.2 服务端超时

  1. 服务端的 I/O 线程没有及时从网络中读取客户端请求消息,导致该问题的原因通过是 I/O 线程被意外阻塞或者执行长周期操作。
  2. 服务端业务处理缓慢,或者被长时间阻塞,例如查询数据库,由于没有索引导致全表查询,耗时较长。
  3. 服务端发生长时间 Full GC,导致所有业务线程暂停运行,无法及时返回应答给客户端。

1.3 服务端调用失败

  1. 服务端解码失败,返回消息解码失败异常。
  2. 服务端发生动态流控,返回流控异常。
  3. 服务端消息队列积压率超过最大阈值,返回系统阻塞异常。
  4. 访问权限校验失败,返回权限相关异常。
  5. 违反 SLA 策略,返回 SLA 控制相关异常。
  6. 其它系统异常。

服务调用异常不包括业务层面的处理异常,例如数据库操作异常、用户记录不存在等异常。

2 容错策略

服务不同,容错策略往往也不同,下面是集群容错和服务路由的关系:

消费者根据配置的路由策略选择某个目标地址之后,发起远程服务调用,发起远程服务调用,在此期间如果发生了远程服务调用异常,则需要服务框架进行集群容错,重新进行选路和调用。集群容错是系统自动执行的,上层用户并不需要关心底层的服务调用过程。

2.1 失败自动切换(FailOver)

服务调用失败自动切换策略指的是当发生 RPC调用异常时,重新选路,查找下一个可用的服务提供者。
服务发布的时候,可以指定服务的集群容错策略。消费者可以覆盖服务提供者的通用配置,实现个性化的容错策略。

FailOver 策略的设计思路如下:消费者路由操作完成之后,获得目标地址,调用通信框架的消息发送接口发送请求,监听服务端应答。如果返回的结果是 RPC调用异常(超时、流控、解码失败等系统异常),根据消费者集群容错的策略进行容错路由,如果是 FailOver,则重新返回到路由 Handler 的入口,从路由节点继续执行。选录完成之后,对目标地址进行对比,防止重新路由到故障服务节点,过滤掉上次的故障服务提供者之后,调用通信框架的消息发送接口发送请求消息。

  • 读操作,因为通常它是幂等的。
  • 幂等性服务,保证调用1 次和 N 次效果相同。
    注意:失败重试会增加服务调用时延,因此框架必须对失败重试的最大次数做限制,通常默认为 3,防止无限制重试导致服务调用时延不可控。

2.2 失败通知(Failback)

适用于非幂等性的服务调用,通过对失败错误码等异常信息的判断,决定后续的执行策略。
Failback 的设计方案如下:服务框架获取到服务提供者返回的 RPC 异常响应之后,根据策略进行容错。将 RPC异常通知给消费者,由消费者捕获异常进行后续处理。

2.3 失败缓存(Failcache)

Failcache 策略是失败自动恢复的一种,应用场景如下:

  1. 服务有状态路由,必须定点发送到指定的服务提供者。当发生链路中断、流控等服务暂时不可用时,服务框架将消息临时缓存起来,等待周期T,重新发送,直到服务提供者能够正常处理该消息。
  2. 对时延要求不敏感的服务。系统服务调用失败,通常是链路暂时不可用、服务流控、GC 挂住服务提供者进程等,这种失败不是永久性的失败,它的恢复是可预期的。如果消费者对服务调用时延不敏感,可以考虑采用自动恢复模式,即先缓存,再等待,最后重试。
  3. 通知类服务。例如通知粉丝积分增长、记录接口日志等,对服务调用的实时性要求不高,可以容忍自动恢复带来的时延增加。

为了保证可靠性,Failcache 策略在设计的时候需要考虑如下几个要素:

  1. 缓存时间、缓存对象上限数等需要做出限制,防止内存溢出。
  2. 缓存淘汰算法的选择,是否支持用户配置。
  3. 定时重试的周期T、重试的最大次数等需要做出限制并支持用户指定。
    重试达到最大上限仍失败,需要丢弃消息,记录异常日志。

2.4 快速失败(Failfast)

在业务高峰期,对于一些非核心的服务,希望只调用一次,失败也不再重试,为重要的核心服务解约宝贵的运行资源。此时,快速失败是不错的选择。
原理在于,获取服务调用异常之后,直接忽略异常,记录异常日志。

2.5 容错策略扩展

无论默认支持多少种容错策略,在业务实际使用过程中都需要支持用户自定义扩展容错策略。

3 个人总结

集群容错虽然功能简单,设计也并不复杂(不复杂???),但是该特性却非常重要。

分布式服务框架上线运行时都是集群组网,这意味着急群众存在某个服务的多实例部署,消费者如何从服务列表中选择合适的服务提供者进行调用,这就涉及到服务路由。

1 透明化路由

1.1 基于服务注册中心的订阅发布

在分布式服务框架中,服务注册中心用于存储服务提供者地址信息、服务发布相关的属性信息,消费者通过主动查询和被动通知的方式获取服务提供者的地址信息,而不需要像之前那样在代码中硬编码服务提供者地址信息。消费者只需要知道当前系统发布了哪些服务,而不需要知道服务具体存在于什么位置,这就是透明化路由。它的工作原理就是基于服务注册中心(例如 ZooKeeper)的订阅发布机制。
服务注册中心的工作原理如下:

由于消费者可能由于服务提供者启动,或者系统运行过程中新增服务提供者,或者某个服务提供者宕机退出,就会导致注册中心发生服务提供者地址变更。注册中心检测到服务提供者列表变更之后,将变更内容主动推送到服务消费者,消费者根据变更列表,动态刷新本地缓存的服务提供者地址。

1.2 消费者缓存服务提供者地址

采用客户端缓存服务提供者地址的方案不仅仅能提升服务调用性能,还能保证系统的可靠性。当注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存的地址信息进行通信,只是影响新服务的注册和老服务的下线,不影响已经发布和运行的服务。

2 负载均衡

2.1 随机

缺点:

  1. 在一个截面上碰撞的概率较高。
  2. 非对等集群组网,或者硬件配置差异较大,会导致各节点负载均匀。

2.2 轮询

轮询,按公约后的权重设置轮询比率,到达边界之后,继续绕接。缺点:

  1. 存在慢的提供者累计请求问题,例如第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。

2.3 服务调用时延

消费者缓存所有服务提供者的服务调用时延,周期性的计算服务调用平均时延,然后计算每个服务提供者服务调用时延与平均时延的差值,根据差值大小动态调整权重,保证服务时延大的服务提供者接收更少的消息,防止消息堆积,

该策略的特点就是要保证处理能力强的服务提供者接收到更多的消息,通过动态自动权重调整消除服务调用时延的振荡范围,使所有服务提供者服务调用时延接近平均值,实现负载均衡。

2.4 一致性哈希

相同参数的请求总是发到同一个服务提供者,当某一台提供者宕机时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。平台提供默认的虚拟节点数,可以通过配置参数进行修改。
一致性 Hash 环工作原理如下:

2.5 粘滞连接

粘滞连接用于有状态服务,尽可能让客户端总是向同一提供者发起服务调用,除非该提供者宕机,再连接另一台。由于服务通常被强烈建议设计成无状态的,因此,粘滞连接在实际项目中很少使用。
粘滞连接的实现比较简单,客户端首次跟服务端创建链路时,将该链路标记为粘滞连接,每次路由时直接选择粘滞连接,不执行负载均衡路由接口,当链路中断时,更新粘滞连接为不可用,重新寻找下一个可用的连接,将其标记为粘滞连接。

3 本地路由优先策略

3.1 injvm 模式

在一些业务场景中,本地 JVM 内部也发布了需要消费的服务。该场景下,从性能、可靠性等角度考虑,需要优先调用本 JVM内部的服务提供者,这种本地优先的路由模式被称为 injvm模式。

3.2 innative 模式

如果物理机或者 VM配置较好,多个应用金城往往会选择合设。服务消费者和服务提供者可能会被部署到同一台机器上(VM)。服务路由时优先选择本机的服务提供者,如果找不到再重新发起远程服务调用,该模式被称为 innative模式。

  1. 首先看本进程 JVM内部是否有符合要求的服务提供者。
  2. JVM 内部没有,选择服务提供者 IP地址与本机 IP地址相同的本地合设的服务提供者进程,通过本地网卡回环调用服务提供则。
  3. 如果 VM内部没有,则发起远程调用。

4 路由规则

4.1 条件路由规则

应用场景如下:

  1. 通过 IP条件表达式进行黑白名单访问控制,例如 comsumerIP != 192.168.1.1
  2. 流量引导,只暴露部分服务提供者,防止整个集群服务都被冲垮,导致其它服务也不可用,例如 providerIP = 192.168.3*
  3. 读写分离:method=find,list,get,query=>providerIP=192.168.1.*
  4. 前后台分离:app=web=>providerIP=192.168.1.,app=java=>providerIP=192.168.2.
  5. 灰度升级,将 Web前台应用路由到新的服务版本上:app=web=>providerIP=192.168.1.

4.2 脚本路由规则

使用脚本来实现路由规则,在于动态编译,修改实时生效,常见的脚本语言有 JavaScript、Groovy、MVEL 等。

5 路由策略定制

除了提供默认的路由策略之外,在架构上还需要支持业务扩展路由算法,实现业务自定义路由。

  1. 灰度升级,用户需要按照业务规则进行灰度路由:例如按照用户省份路由、按照请求来源中断类型(IOS、Android)、按照手机号段等;不同的用户按照规则路由到不同的集群环境中,例如没有同步升级的用户路由到升级前的环境,同步配套升级的消费者请求路由到灰度升级后的新版本中。
  2. 服务故障、业务高峰期的导流:通过自定义路由,将异常的峰值流量导流到几台或者1 台服务器上,防止整个集群负载过重导致整个生产系统雪崩。

路由扩展策略如下:

  1. 提供接口。
  2. 提供配置 XML Schema定义。
  3. 通过 Spring Bean 方式的服务发布、通过 JDK 的 SPI 方式扩展,即 META-INF/services。

6 配置化路由

  1. 本地配置:包括服务提供者和服务消费者、默认全局配置三种。
  2. 统一注册管理:无论是服务提供者还是消费者,本地配置的路由策略统一注册到服务注册中心,进行集中化配置管理。
  3. 动态下发:运维人员通过服务治理 Portal修改路由规则,更新后的路由规则被持久化到服务注册中心。

路由配置优先级:客户端配置>服务端配置>全局配置。

7 最佳实践————多机房路由

为了能够相互发现对方的服务,不同机房会共用同一个服务注册中心集群(异地容灾机房除外)。假如机房1 发布了服务A,机房2 同样也发布了服务A,此时服务注册中心就会将2 个不同机房的服务A 地址信息推送给消费者,无论是机房1 还是机房2 的消费者,都将看到两个不同机房的服务。

如果仅仅依靠随机、轮询等负载均衡策略,消息将会被路由到两个机房,达不到不跨机房调用的目标。此时可以使用配置条件路由策略,通过网段条件匹配来实现地址过滤。
也可以使用虚拟分组策略,将整个集群系统的服务提供者(跨机房)逻辑分成若干个组,某个消费者只访问一个虚拟分组的服务提供者,防止跨组服务调用。

8 个人总结

服务路由需要既具备丰富的路由策略,还要具备扩展能力,是非常重要的分布式下的基础功能。

1 关键技术点分析

1.1 是否必须支持多协议

分布式服务框架需要具备通过扩展的方式支持多协议的能力,协议栈应该作为一个架构扩展点开放出来。

1.2 公有协议还是私有协议

以 Web Service 公有协议为例,它的性能存在如下缺陷:

  1. SOAP 消息使用 XML 进行序列化,相比于 PB 等二进制序列化框架,性能低很多。
  2. SOAP 通常由 HTTP 协议承载,HTTP 1.0 不支持双向全工通信,而且一般使用短连接通信,性能比较差。

如果没有特殊需求,分布式服务框架默认使用性能更高、扩展性更好的私有协议(二进制)进行通信。对 HTTP/Restful 等公有协议进行扩展

1.3 集成开元还是自研

  1. 如果已经有可以满足需求的框架,优先选择继承开源框架。
  2. 如果使用到的功能不多,或者对性能要求极高,可以考虑基于 Netty 自研。

2 功能设计

2.1 功能描述

私有协议栈承载了业务内部各模块之间的消息交互和服务调用,主要功能如下:

  1. 定义了私有协议的通信模型和消息定义。
  2. 支持服务提供者和消费者之间采用点对点长连接通信
  3. 基于 Java NIO 通信框架,提供高性能的异步通信能力。
  4. 提供可扩展的编解码框架,支持多种序列化格式。
  5. 握手和安全认证机制。
  6. 链路的高可靠性。

2.2 通信模型

私有协议栈通信模型如下:

  1. 客户端发送握手请求消息,携带节点ID 等有效神风认证信息。
  2. 服务端对握手请求消息进行合法性校验,包括节点ID 有效性校验、节点重复登录校验和IP 地址合法性校验,校验通过后,返回登录成功的握手应答消息。
  3. 链路建立成功之后,客户端发送业务消息。
  4. 链路成功之后,服务端发送心跳消息。
  5. 链路建立成功之后,客户端发送心跳消息。
  6. 链路建立成功之后,服务端发送业务消息。
  7. 服务端退出时,服务端关闭连接,客户端感知对方关闭连接后,被动关闭客户端连接。

2.3 协议消息定义

通信协议栈的消息模型分为两部分,消息头和消息体。消息头存放协议公共字段和用户扩展字段,消息体则用于承载消息内容。以 HTTP 协议为例,请求消息头允许客户端向服务端传递请求的附加信息以及客户端自身的信息,常见的消息头关键字有 Accept、Authorization、Host 等。

2.4 协议消息的序列化和反序列化

消息的序列化分为两部分,消息头的序列化和消息体的序列化,两者采用的机制不一样。原因是协议栈可以由不同的序列化框架承载,标识序列化格式的字段在消息头中定义,因此我们必须首先对消息头做通用解码,获取序列化格式,然后根据类型再调用对应的解码器对消息体做解码。消息头是通用编解码。

2.5 链路创建

协议栈包括服务端和客户端,对于上层应用来说,一个节点可能既是服务端也是客户端。
考虑到安全,链路简历需要通过基于 IP 地址或者号段的黑白名单安全认证机制,以及通过秘钥等。

2.6 链路关闭

由于采用长连接通信,在正常的业务运行期间,双方通过心跳和业务消息维持链路,任何一方都不需要主动关闭连接。以下情况,客户端和服务端需要关闭连接:

  1. 当对方宕机或重启时,会主动关闭链路。
  2. 消息读写过程中,发生了 I/O 异常,需要主动关闭连接。
  3. 心跳消息读写过程中发生了 I/O 异常,需要主动关闭连接。
  4. 心跳超时,需要主动关闭连接。
  5. 发生编码异常或其它不可恢复错误时,需要主动关闭连接。

3 可靠性分析

3.1 客户端连接超时

客户端业务需要、以及资源的长时间占有等,需要设置超时时间。

3.2 客户端重连机制

客户端通过链路关闭监听器监听链路状态,如果链路中断,等待 INTERVAL 时间后,由客户端发起重连操作,如果重连失败,间隔周期 INTERVAL 后再次发起重连,直到重连成功。

为了保证服务端能够有充足的时间释放句柄资源,在首次断连时客户端需要等待 INTERVAL 时间之后再发起重连,而不是失败后就立即重连。

3.3 客户端重复握手保护

当客户端握手成功之后,在链路处于正常状态下,不允许客户端重复握手,以防止客户端在异常状态下反复重连导致句柄资源被耗尽。

服务端接收到客户端的握手请求消息之后,首先对IP 地址进行合法性检验,如果校验成功,在缓存的地址表中查看客户端是否已经登录,如果已经登录,则拒绝登录,返回错误码 -1,同时关闭 TCP 链路,并在服务端的日志中打印握手失败的原因。

客户端接收到握手失败的应答消息之后,关闭客户端的 TCP连接,等待 INTERVAL 时间之后,再次发起 TCP连接,直到认证成功。

3.4 消息缓存重发

无论客户端还是服务端,当发生链路中断之后,在链路恢复之前,缓存在消息队列中待发送的消息不能丢失,等链路恢复之后,重新发送这些消息,保证链路中断期间消息不丢失。

考虑到内存溢出的风险,建议消息缓存队列设置上限,当达到上限之后,应该拒绝继续向该队列添加新的消息。

3.5 心跳机制

在凌晨等业务低谷期时段,如果发生网络闪断、连接被 Hang 住等网络问题时,由于没有业务消息,应用金城很难发现。到了白天业务高峰期时,会发生大量的网络通信失败。为了解决这个问题,在网络空闲时采用心跳机制来检测链路的互通性,一旦发现网络网络故障,立即关闭链路,主动重连。

4 最佳实现–协议的前后兼容性

考虑到协议的前向兼容性,核心的设计原则有2 个:

  1. 消息头第一个字段中携带协议的版本号,用于标识消息协议版本。
  2. 消息头最后一个字段是 Map 类型的扩展字段,用于服务框架自身或者用户扩展消息头。

5 个人总结

协议栈描述了分布式服务框架的通信契约,序列化和反序列化框架用于协议消息对象和二进制数组之间的相互转换,通信框架在技术上承载协议,协议依赖通信。

1 几个关键概念澄清

通常我们习惯将序列化(Serialization)称为编码(Encode),它将对象序列化为字节数组,用于网络传输、数据持久化或其它用途。
反之,反序列化(Deserialization)/解码(Decode)把从网络、磁盘等读取的字节数组还原成原始对象(通常是原始对象的副本)。

1.1 序列化与通信框架的关系

序列化与通信框架不是强耦合的关系,通信框架提供的编解码框架可以非常方便地支持用户通过扩展实现自定义的序列化格式。通信框架的编解码接口作为可选插件,并不强制用户一定要在通信框架内部实现消息的序列化和反序列化。

1.2 序列化与通信协议的关系

序列化与通信协议是解耦的,同一种通信协议可能由多种序列化方式承载,同一种序列化方式也可以用在不同协议里。

以 HTTP 协议为例,承载消息体的可以是 XML、JSON 等文本类的协议,也可以是图片附件等二进制流媒体协议。
在设计分布式服务框架时,序列化和反序列化是一个独立的接口和插件,它可以被多种协议重用、替换和扩展,以实现服务框架序列化方式的多样性。

1.3 是否需要支持多种序列化方式

整体而言,序列化可以分为文本类和二进制类两种,不同的业务场景需求也不同,分布式服务框架面向的领域是多样化的,因此它的序列化/反序列化框架需要具备如下特性:

  1. 默认支持多种常用的序列化/反序列化方式,文本类例如 XML/JSON 等,二进制的如 PB(Protocol Buffer)/Thrift 等。
  2. 序列化框架可扩展,用户可以非常灵活、方便地扩展其它序列化方式。

2 功能设计

从功能、跨语言支持、兼容性、性能等多个角度进行综合考量。

  1. 功能丰富。
  2. 跨语言支持。
  3. 兼容性
  4. 性能。

3 扩展性设计

利用 Netty 提供的编解码框架,可以非常快速的实现序列化/反序列化框架的扩展。

3.1 内置的序列化/反序列化功能类

为了降低用户的开发难度,Netty 对常用的功能和 API 做了装饰,以屏蔽底层的实现细节。Netty 内置的编解码功能包括 base64、Protobuf、JBoss Marshalling、spdy 等。

3.2 反序列化扩展

  1. 业务发布服务的时候,可以指定协议类型和承载数据的序列化方式,例如将购买商品服务发布成 HTTP 服务,序列化格式采用 XML;同时允许用户指定新增的序列化格式发布服务。
  2. 序列化类库能够以插件的格式插入到通信调用链中,实现序列化格式的扩展。在这个过程中,需要考虑 TCP 的黏包和拆包等底层相关的技术细节。

我们看半包的处理,如果不处理半包,Netty 调用 decode 方法传递的 ByteBuf 对象可能就是个半包,我们拿半包做反序列化就会失败,因此在反序列化之前,我们需要保证调用解码方法时传递的是个完整的数据包。

了解 TCP 通信机制的渎职应该都知道 TCP 底层的黏包和拆包,当我们在接收消息的时候,不能认为读取到的报文就是个整包消息,特别是对于采用非阻塞 I/O 和长连接通信的程序。
如何区分一个整包消息,通常有如下四种做法:

  1. 固定长度,例如每 120 个字节代表一个整包消息,不足的前面补位。解码器在处理这类定长消息的时候比较简单,每次读到指定长度的字节后进行解码。
  2. 通过回车换行符区分消息,例如 HTTP 协议。这类区分消息的方式多用于文本协议。
  3. 通过特定的分隔符区分整包消息。
  4. 通过在协议头/消息头中设置长度字段来标识整包消息。

4 最佳实践

4.1 接口的前向兼容性规范

  1. 制定”分布式服务框架接口兼容性规范“,在规范中要明确服务框架支持哪些兼容性,例如新增字段、删除字段还是修改字段。
  2. 引导客户。

4.2 高并发下的稳定性

需要模拟现网高并发场景对序列化框架做压测和稳定性测试,如果序列化框架存在全局锁较激烈的线程竞争等问题,多线程、高并发压力测试就会出现问题。究其原因是因为一些序列化框架为了实现线程安全,使用了全局锁等,这从实用角度看确实简单,但是在高并发场景下就会出现性能下降、耗时不稳定等问题。

解决此类问题的方案很多,例如每个线程聚合一个序列化/反序列化类库,避免多线程竞争。

5 个人总结

序列化/反序列化是 RPC 框架的基础组成部分,从性能、业务考虑,而 Netty 非常适合去做扩展。当然,还是清楚其中的经典黏包、拆包。

恭喜 RNG,恭喜 UZI

六年
恋恋不忘
必有回响


1 关键技术点分析

1.1 长连接还是短连接

绝大多数的分布式服务框架(RPC框架)都推荐使用长连接进行内部通信,为什么选择长连接而不是短连接呢?具体原因如下:

  1. 相对比短连接,长连接更节省资源。长连接只会在首次创建时或者链路断连重连才创建链路,链路创建成功之后服务提供者和消费者会通过业务消息和心跳维系链路,实现多消息复用同一个链路节省资源。
  2. 远程通信是常态,调用时延是关键指标:服务化之后,本地 API 调用变成了远程服务调用,大量本地方法演进成了跨进程通信,网络时延称为关键指标之一。相比于一次简单的服务调用,链路的重建通常耗时更多,这就会导致链路层的时延消耗远远大于服务调用本身的损耗,这对于大型的业务系统而言是无法接受的。

1.2 BIO 还是 NIO

经典的 BIO 通信模型如下:

采用 BIO 通信模型的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接,它接收到客户端连接请求后为每个客户端创建一个新的线程进行链路处理,处理完成后,通过输出流返回给客户端,线程销毁,这就是典型的一请求一应答通信模型。
该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈 1:1 的正比关系。由于线程是 Java 虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。

在 I/O 编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或 I/O 多路复用技术进行处理。 I/O 多路复用技术通过把多个 I/O 的阻塞复用到同一个 select 的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型相比,I/O 多路复用的最大优势是系统开销小,系统不需要创建新的额外线程/进程,也不需要维护这些线程/进程的运行,节省系统资源。

JDK1.4 提供了对非阻塞I/O(NIO)的支持,JDK1.5 使用 epoll 替代了传统的 select/poll,极大提升了 NIO 通信的性能。
NIO 采用多路复用技术,一个多路复用器 Selector 可以同时轮询多个 Channel ,由于 JDK 使用了 epoll() 代替了传统的 select 实现,所以它并没有最大连接句柄 1024/2048 的限制。这就意味着只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。
采用多路复用器 Selector 实现的 Reactor 通信模型如下:

1.3 自研还是选择开源 NIO 框架

选择 Netty!

2 功能设计

分布式服务框架的底层通信框架首先是一个通用的通信框架,它不应该与具体的协议绑定。基于通信框架智商,可以构建私有协议栈和公有协议栈。
架构原理如下图:

2.1 服务端设计

通信框架服务端的职责如下:

  1. 提供上层 API,用于初始化服务端实例,设置服务端通信相关参数,包括服务端的 I/O 线程池、监听地址、TCP 相关参数、接收和发送缓冲区大小等。
  2. 提供可扩展的编解码插件,用户可以通过扩展的方式实现自定义协议的编码和解码。
  3. 提供拦截面,用于私有协议栈开发。例如通过新增鉴权插件实现服务端对客户端的安全认证。

2.2 客户端设计

客户端需要考虑网络连接超时、连接失败等异常场景。

3 可靠性设计

3.1 链路有效性检测

当网络发生单通、连接被防火墙 Hang 住,长时间 GC 或者通信线程发生非预期异常时,会导致链路不可用且不易被及时发现。特别是异常发生在凌晨业务低谷期间,当早晨业务高峰期到来时,由于链路不可用会导致瞬间的大批量业务失败或者超时。

从技术层面看,要解决链路的可靠性问题,必须周期性地对链路进行有效性检测。目前最流行和最通用的做法就是心跳检测。

  1. TCP 层面的心跳检测,即 TCP 的 Keep-Alive 机制,它的作用域是整个 TCP 协议栈。
  2. 协议层的心跳检测,主要存在于长连接协议中,例如 SMPP 协议。
  3. 应用层的心跳检测,主要由各业务产品通过约定方式定时给对方发送心跳消息实现。

心跳的检测原理如下:

不同的协议,心跳检测机制也存在差异,归类两类:

  1. Ping-Pong 型心跳:由通信一方定时发送 Ping 消息,对方接收到 Ping 消息之后,立即返回 Pong 应答消息给对方,属于请求-响应型心跳。
  2. Ping-Ping 型心跳:不区分心跳请求和应答,由通信双方按照约定定时向对方发送心跳 Ping 消息,属于双向心跳。

心跳检测策略如下:

  1. 连续 N 次心跳检测都没有收到对方的 Pong 应答消息或者 Ping 请求消息,则认为链路已经发生逻辑失效,这被称为心跳超时。
  2. 读取和发送心跳消息的时候如果直接发生了 I/O 异常,说明链路已经失效,这被称为心跳失败。

无论发生心跳超时还是心跳失败,都需要关闭链路,由客户端发起重连操作,保证链路能够恢复正常。
Netty 的心跳检测实际上是利用了链路空闲检测机制实现的,它的空闲检测机制分为三种:

  • 读空闲,链路持续时间 t 没有读取到任何消息。
  • 写空闲,链路持续时间 t 没有发送任何消息。
  • 读写空闲,链路持续时间 t 没有接受或者发送任何消息。

3.2 断连重连机制

为了保证服务端能够有充足的时间释放句柄资源,在首次断连时客户端需要等待 INTERVAL 时间之后再发起重连,而不是失败后就立即重连。
为了保证句柄资源能够及时释放,无论什么场景下的重连失败,客户端都必须保证自身的资源被及时释放,包括但不限于 SocketChannel、Socket等。

3.3 消息缓存重发

当我们调用消息发送接口的时候,消息并没有真正被写入到 Socket 中,而是先放入 NIO 通信框架的消息发送队列中,由 Reactor 线程扫描待发送的消息队列,异步地发送给通信对端。假如很不辛,消息队列中积压了部分消息,此时链路中断,这会导致部分消息并没有真正发送给通信对端,如下图:

发生此故障时,我们希望 NIO 框架能够自动实现消息缓存和重新发送,遗憾的是作为基础的 NIO 通信框架,无论是 Mina 还是 Netty,都没有提供该功能,需要通信框架自己封装实现。

并非所有场景都需要通信框架做重发,例如服务框架的客户端,如果某个服务提供者不可用,会自动切换到下一个可用的服务提供者之上。假定是链路中断导致的服务提供者不可用,即便链路重新恢复,也没有必要将之前积压的消息重新发送,因为消息已经通过 FailOver 机制切换到另一个服务提供者处理。所以,消息缓存重发只是一种策略,通信框架应该支持链路级重发策略。

3.4 资源优雅释放

Java 的优雅停机通常通过注册 JDK 的 ShutdownHook 来实现,当系统接收到退出指令后,首先标记系统处于退出状态,不再接收新的消息,然后将积压的消息处理完,最后调用资源回收接口将资源销毁,最后各线程退出执行。

通常优雅退出有个时间限制,例如 30s,如果到达执行时间仍然没有完成推出前的操作,则由监控监本直接 kill -9 pid,强制退出。

4 性能设计

分布式服务框架被广泛应用于大数据处理、互联网消息中间件、游戏和金融行业等。对通信框架有很高的性能要求。

4.1 性能差的三宗罪

  1. 网络传输方式:同步阻塞 I/O,采用 BIO 通信模型的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接,接收到客户端连接之后,为其创建一个新的线程处理请求消息,处理完成之后,返回应答消息给客户端,线程销毁,这就是典型的一请求一应答模型。该架构最大的问题就是不具备弹性伸缩能力,当并发访问量增加后,服务端的线程个数和并发访问数成线性正比,导致并发量的增加,发生句柄溢出。
  2. 序列化性能差:Java 序列化机制是 Java 内部的一种对象编解码技术,无法跨语言;相对其它开源的序列化框架,Java 序列化后的码流太大,导致额外的资源占用;序列化性能差,资源占用率高(主要是 CPU 资源占用高)。
  3. 线程模型问题:由于采用同步阻塞 I/O,导致每个 TCP 连接都占用了一个线程。

4.2 通信性能三原则

  1. 传输:用什么样的通道将数据发送给对方:BIO/NIO/AIO。I/O 模型在很大程度上决定了通信的性能。
  2. 协议:采用什么样的通信协议, HTTP 等公有协议或者内部私有协议。通常内部私有协议可以被设计得更优。
  3. 线程:数据报如何读取?读取之后的编解码在哪个线程进行?编解码后的消息如何派发?Reactor 线程模型的不同,对性能的影响也非常大。

4.3 高性能之道

Netty 支持高性能通信的架构特性进行总结:

  1. 异步非阻塞通信:Netty 的 I/O 线程 NioEventLoop 由于聚合了多路复用器 Selector,可以同时并发处理成百上千个客户端 SocketChannel。由于读写都是非阻塞的,这就可以充分提升 I/O 的运行效率,避免由频繁的 I/O 阻塞导致的线程挂起。另外,由于 Netty 采用了异步通信模式,一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
  2. 高效的 I/O 线程模型:Netty 支持 Reactor 单线程模型、Reactor 多线程模型和主从 Reactor 多线程模型,可以满足不同的容量和性能需求。
  3. 高性能的序列化框架:默认提供了 Protobuf 二进制序列化框架,其它二进制序列化框架可以进行编解码框架扩展实现。

还支持零拷贝、内存池等其它性能相关的特性。

5 最佳实践

  1. 服务端只负责客户端的接入,不处理 I/O 读写操作,因此服务端的 boosGroup 设置线程数为1:EventLoopGroup bossGroup = new NioEventLoopGroup(1);
  2. 客户端的 NioEventLoopGroup 由于服务提供者可能有 1000个或更多,导致创建了 1000个对象,可以使用大的线程池,或者创建一个包含 NioEventLoopGroup 的数组,将客户端连接按照 Hash 算法分组,将所有连接均匀的打散在 NioEventLoopGroup 中。

6 个人总结

通信框架的重点在于 BIO、NIO、AIO,以及序列化。
这就是为什么别人面试会问 BIO/NIO/AIO/序列化。

1 分布式服务框架诞生背景

1.1 集中式到分布式

  1. 业务发展、应用规模变大,大型复杂应用的开发维护成本高,部署效率低,应用数量膨胀,数据库连接数变高。
  2. 代码复用低:由于公共模块都是进程内部的本地 API 调用,开发者按需开发,导致大量相同功能的 API 被重复开发。一旦涉及到公共模块的功能变更,所有重复实现都需要重新修改、编译和测试。
  3. 敏捷持续交付:想要在一个架构师都无法理解的巨无霸业务中新增或者修改一个功能,难度是非常大的。业务模块之间的循环依赖、重复 API 定义和开发、不合理的调用、冗长复杂的业务流程对新特性的上线简直是梦魇。

大规模系统架构的设计一般就是尽可能的拆分,以达到更好的独立扩展、部署、开发效率等。
具体的拆分策略大体上可以分为横向拆分和纵向拆分。

纵向拆分:不同业务模块独立部署,例如一个 CRM 系统就可以根据客户域、产品域、资源域、营销管理域等拆分。由大变小、分而治之。

横向拆分:将核心的、公共的业务拆分出来,通过分布式服务框架对业务进行服务化,消费者通过标准的契约来消费这些服务。服务提供者独立打包、部署,与消费者解耦。

1.2 引入服务治理

使用 RPC 框架对业务进行拆分之后,随着服务数的增多,急需一个服务治理框架,有效管控服务,提升服务运行期质量,防止业务服务代码架构腐化。因为,服务治理的主要应用如下:

  1. 生命周期管理:服务上线下线通知机制规范化。
  2. 服务容量规划。
  3. 运行期治理:对非核心服务采取降级、限流措施;缓存失效时,系统压力转移到数据库,服务调用时延突然增大,业务失败率升高,需要在线调大服务调用超时时间,保证业务成功率。
  4. 服务安全。

典型的 SOA 服务治理生命周期如下:

  1. 计划:确定服务治理的重点。
  2. 定义:定义服务治理模型。
  3. 启用:实现并实施服务治理。
  4. 度量:根据实施效果,改进服务治理模型。

2 业务分布式服务框架介绍

2.1 阿里 Dubbo

架构图如下:

功能总结:

  1. 根据服务提供者配置的 XML 文件将服务按照指定协议发布,完成服务的初始化工作。
  2. 服务提供者根据配置的服务注册中心地址连接服务注册中心,将服务提供者信息发布到服务注册中心。
  3. 消费者根据服务消费者 XML 配置文件的服务引用信息,连接注册中心,获取指定服务的地址等路由信息。
  4. 服务注册中心根据服务订阅关系,动态地向指定消费者推送服务地址信息。
  5. 消费者调用远程服务时,根据路由策略,从本地缓存的服务提供者地址列表中选择一个服务提供者,然后根据协议类型建立链路,跨进程调用服务提供者。

原理图如下:

2.2 淘宝 HSF

架构图如下:

功能总结:

  1. 配置 XML 方式发布和消费服务。
  2. 插件管理体系:平台与应用分开部署,运行期依赖,外部采用与应用独立的 classloader 隔离,内部采用 OSGI 隔离。
  3. 异步 NIO 通信。多种序列化方式。服务提供者和消费者之间采用长连接通信。
  4. 灵活的路由能力:客户端软负载,随机、轮询等多种路由策略,支持容灾和失效恢复等。
  5. 多协议支持:WebService、PB(Protocol buffer)和 Hession(HTTP)等。

整体结构图如下:

2.3 亚马逊 Coral Service

3 分布式服务框架设计

本章介绍分布式服务框架的架构原理和概要设计。

3.1 架构原理


通常分布式服务框架的架构可以抽象为三层:

  1. RPC 层:包括底层通信框架(例如 NIO 框架的封装、公有协议的封装等)、序列化和反序列化框架、用于屏蔽底层通信协议细节和序列化方式差异的 Remoting 框架。
  2. Filter Chain 层:服务调用职责链,提供多种服务调用切面供框架自身和使用者扩展,例如负责均衡、服务调用性能统计、服务调用完成通知机制、失败重发等。
  3. Service 层:主要包括 Java 动态代理,消费者使用,主要用于将服务提供者的接口封装成远程服务调用:Java 反射,服务提供者使用,根据消费者请求消息中的接口名、方法名、参数列表反射调用服务提供者的接口本地实现类。

从功能角度看,分布式服务框架通常会包含另外两个重要功能:服务治理中心和服务注册中心。

3.2 功能特性

  1. 服务订阅发布之配置化发布和引用服务:支持通过 XML 配置的方式发布和导入服务,降低对业务代码的侵入。
  2. 服务订阅发布之服务自动发现机制:由注册中心推送服务地址,消费者不需要配置服务提供者地址,服务地址透明化。
  3. 服务订阅发布之服务在线注册和去注册:支持运行态注册新服务,也支持运行态取消某个服务的注册。
  4. 服务路由之默认提供随机路由、轮询、基于权重的路由策略等。
  5. 服务路由之粘滞连接:总是向同一个提供方发起请求,除非此提供方挂掉,再切换到另一台。
  6. 服务路由之路由定制:支持用户自定义路由策略。
  7. 集群容错之 FailOver:失败自动切换,常用读操作;也可用于幂等性写操作。
  8. 集群容错之 Failback:失败自动恢复,后台记录失败请求,定时重发,通常用于消息通知操作。
  9. 集群容错之 Failfast:快速失败,只发起一次调用,失败立即报错,通常用于非幂等性的写操作。
  10. 服务调用之同步调用:消费者发起服务调用之后,同步阻塞等待服务端响应。
  11. 服务调用之异步调用:消费者发起服务调用之后,不阻塞立即返回,由服务端返回应答后异步通知消费者。
  12. 服务调用之并行调用:消费者同时对多个服务提供者批量发起服务调用请求,批量发起请求,集中等待应答。
  13. 多协议之私有协议:支持二进制等私有协议,支持自定义。
  14. 多协议之公有协议:支持 Web Service 等公有协议,用于外部服务对象。
  15. 序列化方式之二进制类序列化:支持 Thrift、Protocol buffer 等二进制协议,提升序列化性能。
  16. 序列化方式之文本类序列化:支持 JSON、XML 等文本类型的序列化方式。
  17. 统一配置之本地静态配置:安装部署一次,运行态不修改的配置,可以存放到本地配置文件中。
  18. 统一配置之基于配置中心的动态配置:运行态需要调整的参数,统一放到配置中心,修改之后统一下发,实时生效。

3.3 服务治理

  1. 服务运行态管控之服务路由:业务高峰期,通过动态修改路由策略实现导流。
  2. 服务运行态管控之服务限流:资源成为瓶颈时,服务端和消费者的动态流控。
  3. 服务运行态管控之服务迁入迁出:实现资源的动态分配。
  4. 服务运行态管控之服务降级:服务提供者故障时或者业务高峰期,进行服务强制或者容错降级,执行本地降级逻辑,保证系统平稳运行。
  5. 服务运行态管控之服务超时控制:动态调整超时时间,在业务高峰期保障业务调用成功率。
  6. 服务监控之性能统计:统计项包括服务调用时延、成功率、调用次数等。
  7. 服务监控之统计报表:提供多维度、实时和历史数据报表,同比、环比等性能对比数据,供运维、运营等使用。
  8. 服务监控之告警:指标异常,根据告警策略发送告警,包括但不限于短信、E-mail、记录日志等。
  9. 服务生命周期管理之上线审批:服务提供者不能随意线上发布服务,需要通过正规的审批流程批准之后才能上线。
  10. 服务生命周期管理之下线通知:服务提供者在下线某个服务之前一段时间,需要根据 SLA 策略,通知消费者。
  11. 服务生命周期管理之服务灰度发布:灰度环境划分原则、接口前向兼容性策略,以及消费者如何路由,都需要灰度发布引擎负责管理。
  12. 故障快速定界定位之分布式日志采集:支持在大规模分布式环境中实时采集容器、中间件和应用的各种日志。
  13. 故障快速定界定位之海量日志在线检索:支持分布式环境海量日志的在线检索,支持多维度索引和模糊查询。
  14. 故障快速定界定位之调用链可视化展示:通过分布式消息跟踪系统输出调用链,可视化、快速地进行故障定界。
  15. 故障快速定界定位之运行日志故障定位:通过调用链的故障关键字,在日志检索界面快速检索故障日志,用于故障的精确定位。
  16. 服务安全之敏感服务的授权策略:敏感服务如何授权,防止恶意调用。
  17. 服务安全之链路的安全防护“消费者和服务提供者之间的长连接,需要增加安全防护,例如基于 Token 的安全认证机制。

4 个人总结

分布式服务框架在原理、目标是类似的,因此不同的分布式服务框架原理也是相似的。

1 传统垂直应用架构

以经典的 MVC 垂直架构为例,通常分三层:

  1. View:视图展示层,使用 JSP/JS/HTML+CSS。
  2. Controller:调度控制层,请求的分发。
  3. Model:应用模型层,业务数据和业务执行逻辑,被多个视图重用。

标准的 MVC 模式并不包含数据访问层,但实际开发中需要专门的数据库连接池和统一的数据库访问接口对接数据库,于是 ORM 框架逐渐流行起来。

通常基于 MVC 架构开发的应用代码会统一打成一个 war 包,不同的应用功能之间通过本地 API 进行调用,基本不存在跨进程的远程服务调用。

通常的基于热双机备份,判断应用进程宕机或僵死后,应用切换备机,然后尝试重新拉起主机。
而在高并发、大流量的应用场景中,需要做集群,通常前端通过 F5 等负载均衡器做七层负载均衡,后端做对等集群部署。

2 RPC 架构

RPC 全称 Remote Procedure Call,它是一种进程间通信方式。允许像调用本地服务一样调用远程服务,具体实现可以不同。

2.1 RPC 框架原理

RPC 框架的目标就是让远程过程(服务)调用更加简单、透明,RPC 框架负责屏蔽底层的传输方式(TCP/UDP)、序列化方式(XML/JSON/二进制)和通信细节。
调用原理图如下:

  1. 远程服务提供者需要以某种形式提供服务调用相关的信息,包括但不限于服务接口定义、数据结构,或者中间态的服务定义文件,例如 Thrift 的 IDL 文件;服务调用者需要通过一定的途径获取远程服务调用相关信息,例如服务端接口定义 Jar 包导入,获取服务端 IDL 文件等。
  2. 远程代理对象:服务调用者调用的服务实际是远程服务的本地代理,对于 Java 语言,它的实现就是 JDK 的动态代理,通过动态代理的拦截机制,将本地调用封装成远程服务调用。
  3. 通信:RPC 框架与具体的协议无关。
  4. 序列化:远程通信,需要将对象转换成二进制码流进行网络传输,不同的序列化框架,支持的数据类型、数据包大小、异常类型及性能等都不同。有的甚至支持用户自定义序列化框架(Hadoop Avro)。

2.2 实现一个简单的 RPC 框架

  1. 分四个聚合项目:Provider、Consumer、RpcFramework、ServiceApi。为了简单期间,RPC 核心和 ServiceApi 合并一个项目 C。
  2. Provider 依赖 C 项目,Consumer 依赖 C 项目。这样 Provider 和 Consumer 都有了同一个顶级接口以及同版本的 RPC 框架。
  3. RPC 框架提供 export() 用于 Provider 发布自己的实现类、refer() 用于 Consumer 调用对应的顶级接口。
  4. refer() 方法用于生成代理,此代理每次执行方法都会调用 invoke() 方法,而 invoke() 方法实际通过 socket 连接 Provider ,将调用的方法名、参数等传递给 Provider。
  5. export() 方法用于获取 Consumer 传递的方法名,参数等,以及自己暴露的接口实现类,来反射执行获取结果,并返回给 Consumer。
  6. 代理对象获取到了结果,返回给调用者。
  7. Provider 使用了反射,用于将调用者的方法名、参数、自己暴露的服务调用执行获取结果。Consumer 使用了代理,每次执行方法,都会调用 invoke() 即将调用的方法名、参数、调用的接口传递给 Provider。实现远程服务的本地代理。
  8. 参考的 GITHUB 地址

2.3 业界主流 RPC 框架

  1. Facebook 的 Apache Thrift。
  2. Hadoop 的 Avro-RPC。
  3. caucho 的 Hession。
  4. Google 的 gRPC。

2.4 RPC 框架面临的挑战

  1. 提供服务的机器越来越多,服务 URL 配置管理变得非常困难(服务发布订阅中心)。
  2. 服务间依赖关系变得错综复杂,甚至分不清哪个应用要在哪个应用之前启动(链路追踪)。
  3. 某个服务调用量特别大,需要加机器。为了解决容量规划问题,需要采集服务调用 KPI 数据,进行汇总和分析,计算出服务部署实例数和服务器的配置规格。
  4. 服务上线容易下线难,下线通知,需要统一的服务生命周期管理流程进行管控,如何保证敏感服务不被误调用,服务的访问安全策略又如何制定?
  5. 服务治理问题。

3 SOA 服务化架构

SOA 是一种粗粒度、松耦合的以服务为中心的架构,接口之间通过定义明确的协议和接口进行通信。

3.1 面向服务设计的原则

  1. 服务可服用:不管是否存在即时复用的机会,服务均被设计为支持潜在的可复用。
  2. 服务共享一个标准契约:IDL 文件、Java 接口定义、甚至是一个接口说明文档。
  3. 服务是松耦合的:服务被设计为功能相对独立、尽量不依赖其它服务的独立功能提供者。
  4. 服务是底层逻辑的抽象:只有经服务契约所暴露的服务队外部世界可见,契约之外底层的实现逻辑是不可见的。
  5. 服务是可组合、可编排的:多个服务可能被编排组合成一个新的服务。
  6. 服务是可自治的:逻辑由服务所控制,并位于一个清晰的边界内,服务已经在边界内被控制,不依赖于其它服务。
  7. 服务是无状态的:这意味着要讲状态管理移至他处。
  8. 服务是可被自动发现的:服务发布上线后,允许被其它消费者自动发现。服务下线后,允许消费者接收服务下线通知。

3.2 服务治理

SOA 服务治理主要包括如下几个方面:

  1. 服务定义:SOA 治理最基础的方面就是监视服务的创建过程。
  2. 服务生命周期管理:服务需要进行规划、设计、实现、部署、维护、下线主要阶段。
  3. 服务版本治理:新版本的兼容性。
  4. 服务注册中心:服务提供者如何发布服务?服务消费者如何订阅服务?
  5. 服务监控:服务监控中心需要对服务的调用时延、成功率、吞吐率等数据进行实时采样和汇总。
  6. 运行期服务质量保障:包括服务限流、服务迁入迁出、服务升降级、服务权重调整和服务超时控制等,通过运行期的动态治理,可以在不重启服务的前提下达到快速提升服务运行质量的目标。
  7. 快速的故障定界定位手段:大规模分布式环境下海量业务/平台日志的采集、汇总和实时在线检索;分布式消息跟踪。
  8. 服务安全:是否允许任何人调用任何服务,数据敏感型服务是否允许所有用户访问所有数据,交互数据是否需要进行保护,服务的安全认证等。服务安全访问策略有多重,例如动态生成 Token 的方式做安全访问授权。

4 微服务架构

微服务架构(MSA)是一种服务化架构风格。
SOA 架构解决了应用服务化问题,但是随着服务规模越来越大、服务治理越来越多,微服务架构风格应运而生。微服务架构的主要特征如下:

  1. 原子服务。
  2. 高密度部署:利用LXC(例如 Docker)实现容器级部署。
  3. 敏捷交付:服务由小研发团队负责设计、开发、测试、部署、线上治理、灰度发布和下线,运维整个生命周期支撑,实现真正的 DevOps。
  4. 微自治:服务足够小,功能单一,可以独立打包、部署、升级、回滚和弹性伸缩,不依赖其它服务,实现局部自治。

相对于 SOA,主要差异如下:

  1. 服务拆分粒度。
  2. 服务依赖:SOA 尽量重用,微服务功能单一独立。
  3. 服务规模:SOA 服务粒度大,多数会采用多个服务合并打 war 包,因此服务实例数比较有限。微服务强调尽可能拆分,同时很多服务会独立部署。
  4. 架构差异。
  5. 服务治理。
  6. 敏捷交付。

5 个人理解总结

  1. MVC架构:在于多个功能部署同一个进程,一个 war 包,通过 HTTP 请求来实现互相的调用。重点在于前后端分离。
  2. RPC架构:将核心和公共业务抽取出来,独立运行进程,使用 RPC 调用服务屏蔽底层通信逻辑。重点在于业务复用以及通用拆分。
  3. SOA架构:服务生命周期管控和SOA服务治理是关键。
  4. 微服务架构:敏捷开发、持续交付、DevOps理论,基于 Docker 等轻量级容器。重点在于独立打包、部署和升级,小团队敏捷交付,交付周期短。

1. bash 手册

用于查看命令的具体详情

man xxx

2. ls 文件和目录列表

-a 显示隐藏文件
文件名支持 *? 符号过滤

3. 处理文件

-i 询问参数
touch
cp file1 file2:复制文件。参数 -R 用于递归复制文件
mv file1 file2:移动文件
rm file1:删除文件,文件名支持?*

4. 处理目录

mkdir:创建目录。-p 创建多个目录和子目录
rmdir:删除空目录
rm -rf:递归删除,-r 递归遍历,-f 删除不提示

5. 查看文件内容

file file1:获取文件的类型
cat file1:显示文本所有内容,参数 -n 加上行数,参数 -b 只给有内容的行加行数
more file1:
less file1:
tail file1:参数 -f,动态查看文件内容

6. 检测程序

ps: -ef
top:实时检测,q 退出
kill PID:-9 参数强制
killall Name:关闭进程名,可以使用通配符

7. 检测磁盘空间

mount:挂载媒体的
unmount:移除可移动设备
sort file:文件排序
grep pattern file:在 file 文件中查找 pattern 的行。-v 参数反向搜索
gzip/gunzip:压缩解压文件
tar:-A 追加归档,-x 提取文件

8. 理解 shell

&:将任务置入后台模式
which 命令:查看命令的对应路径
history:最近的使用过的命令列表

9. 使用Linux环境变量

查看环境全局变量:printenv/env
查看环境局部变量:set
export:将一个局部变量的key导出到全局环境中

10. 管理文件系统

  1. ext 文件系统:单文件不能超过2GB。
  2. ext2 文件系统:保存更多信息。
  3. 日志文件系统:先将数据直接写入存储设备再更新索引节点表->文件的更改写入到临时文件中,数据成功写到存储设备和索引节点表后再删除对应的日志条目。
  4. ext3 文件系统:在 ext2 基础上,给每个存储设备增加了一个日志文件。
  5. ext4 文件系统。

11. 安装软件程序

  1. Debian(Ubuntu):dpkg 命令。
  2. Red Hat:rpm 命令。yum 命令。

12. 使用编辑器

vim
nano
emacs

13. 参考

  1. 初识Linux shell:http://www.ituring.com.cn/book/tupubarticle/11430
  2. 走进shell:http://www.ituring.com.cn/book/tupubarticle/11431
  3. 基本的bash shell命令:http://www.ituring.com.cn/book/tupubarticle/11432
  4. 更多的bash shell命令: http://www.th7.cn/system/lin/201704/210752.shtml
  5. 理解shell:http://www.th7.cn/system/lin/201704/211006.shtml
  6. 使用Linux环境变量:http://www.voidcn.com/article/p-vizgjbtx-bmq.html
  7. 理解Linux文件权限:http://www.voidcn.com/article/p-whblgnni-bmq.html
  8. 管理文件系统:https://www.aliyun.com/jiaocheng/123749.html
  9. 安装软件程序:https://www.aliyun.com/jiaocheng/123748.html
  10. 使用编辑器:http://www.voidcn.com/article/p-fokuslvn-bnt.html