SOA (面向服务的架构)
微服务
就是将庞杂臃肿的单体应用拆分成细粒度的服务,独立部署,并交给各个中小团队来负责开发、测试、上线和运维整个生命周期。
那么服务化拆分具体该如何实施呢?一个最有效的手段就是将不同的功能模块服务化,独立部署和运维。以前面提到的社交 App 为例,你可以认为首页信息流是一个服务,评论是一个服务,消息通知是一个服务,个人主页也是一个服务。
这种服务化拆分方式是纵向拆分,是从业务维度进行拆分。标准是按照业务的关联程度来决定,关联比较密切的业务适合拆分为一个微服务,而功能相对比较独立的业务适合单独拆分为一个微服务。
还有一种服务化拆分方式是横向拆分,是从公共且独立功能维度拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源独立不与其他业务耦合。
- 服务如何发布和订阅.
单体应用由于部署在同一个 WAR 包里,接口之间的调用属于进程内的调用。而拆分为微服务独立部署后,服务提供者该如何对外暴露自己的地址,服务调用者该如何查询所需要调用的服务的地址呢?这个时候你就需要一个类似登记处的地方,能够记录每个服务提供者的地址以供服务调用者查询,在微服务架构里,这个地方就是注册中心。
服务监控能够发现问题,服务追踪能够定位问题所在,而解决问题就得靠服务治理了。服务治理就是通过一系列的手段来保证在各种意外情况下,服务调用仍然能够正常进行。
服务发布和引用
服务提供者如何发布一个服务,服务消费者如何引用这个服务
RESTful API
XML 配置
IDL
具体采用哪种服务描述方式是根据实际情况决定的,通常情况下,如果只是企业内部之间的服务调用,并且都是 Java 语言的话,选择 XML 配置方式是最简单的。如果企业内部存在多个服务,并且服务采用的是不同语言平台,建议使用 IDL 文件方式进行描述服务。如果还存在对外开放服务调用的情形的话,使用 RESTful API 方式则更加通用。
注册中心原理
在微服务架构下,主要有三种角色:服务提供者(RPC Server)、服务消费者(RPC Client)和服务注册中心(Registry),三者的交互关系请看下面这张图,我来简单解释一下。
RPC Server 提供服务,在启动时,根据服务发布文件 server.xml 中的配置的信息,向 Registry 注册自身服务,并向 Registry 定期发送心跳汇报存活状态。
RPC Client 调用服务,在启动时,根据服务引用文件 client.xml 中配置的信息,向 Registry 订阅服务,把 Registry 返回的服务节点列表缓存在本地内存中,并与 RPC Sever 建立连接。
当 RPC Server 节点发生变更时,Registry 会同步变更,RPC Client 感知后会刷新本地内存中缓存的服务节点列表。
RPC Client 从本地缓存的服务节点列表中,基于负载均衡算法选择一台 RPC Sever 发起调用。
以开源注册中心 ZooKeeper 为例,ZooKeeper 集群中包含多个节点,服务提供者和服务消费者可以同任意一个节点通信,因为它们的数据一定是相同的,这是为什么呢?这就要从 ZooKeeper 的工作原理说起:
每个 Server 在内存中存储了一份数据,Client 的读请求可以请求任意一个 Server。
ZooKeeper 启动时,将从实例中选举一个 leader(Paxos 协议)。
Leader 负责处理数据更新等操作(ZAB 协议)。
一个更新操作成功,当且仅当大多数 Server 在内存中成功修改 。
RPC 调用是如何实现的呢?
RPC 调用的原理与此类似,我习惯把服务消费者叫作客户端,服务提供者叫作服务端,两者通常位于网络上两个不同的地址,要完成一次 RPC 调用,就必须先建立网络连接。建立连接后,双方还必须按照某种约定的协议进行网络通信,这个协议就是通信协议。双方能够正常通信后,服务端接收到请求时,需要以某种方式进行处理,处理成功后,把请求结果返回给客户端。为了减少传输的数据大小,还要对数据进行压缩,也就是对数据进行序列化。
客户端和服务端如何建立网络连接?
-
HTTP 通信
HTTP 通信是基于应用层 HTTP 协议的,而 HTTP 协议又是基于传输层 TCP 协议的。一次 HTTP 通信过程就是发起一次 HTTP 调用,而一次 HTTP 调用就会建立一个 TCP 连接,经历一次下图所示的"三次握手”的过程来建立连接。
完成请求后,再经历一次“四次挥手”的过程来断开连接。
- Socket 通信
Socket 通信是基于 TCP/IP 协议的封装,建立一次 Socket 连接至少需要一对套接字,其中一个运行于客户端,称为 ClientSocket ;另一个运行于服务器端,称为 ServerSocket 。就像下图所描述的,Socket 通信的过程分为四个步骤:服务器监听、客户端请求、连接确认、数据传输。
-服务器监听:ServerSocket 通过调用 bind() 函数绑定某个具体端口,然后调用 listen() 函数实时监控网络状态,等待客户端的连接请求。
-客户端请求:ClientSocket 调用 connect() 函数向 ServerSocket 绑定的地址和端口发起连接请求。
-服务端连接确认:当 ServerSocket 监听到或者接收到 ClientSocket 的连接请求时,调用 accept() 函数响应 ClientSocket 的请求,同客户端建立连接。
-数据传输:当 ClientSocket 和 ServerSocket 建立连接后,ClientSocket 调用 send() 函数,ServerSocket 调用 receive() 函数,ServerSocket 处理完请求后,调用 send() 函数,ClientSocket 调用 receive() 函数,就可以得到得到返回结果。
服务端如何处理请求?
BIO 适用于连接数比较小的业务场景,这样的话不至于系统中没有可用线程去处理请求。这种方式写的程序也比较简单直观,易于理解。
NIO 适用于连接数比较多并且请求消耗比较轻的业务场景,比如聊天服务器。这种方式相比 BIO,相对来说编程比较复杂。
AIO 适用于连接数比较多而且请求消耗比较重的业务场景,比如涉及 I/O 操作的相册服务器。这种方式相比另外两种,编程难度最大,程序也不易于理解。
数据传输采用什么协议?
最常用的有 HTTP 协议,它是一种开放的协议,各大网站的服务器和浏览器之间的数据传输大都采用了这种协议。还有一些定制的私有协议,比如阿里巴巴开源的 Dubbo 协议,也可以用于服务端和客户端之间的数据传输。无论是开放的还是私有的协议,都必须定义一个“契约”,以便服务消费和服务提供者之间能够达成共识。服务消费者按照契约,对传输的数据进行编码,然后通过网络传输过去;服务提供者从网络上接收到数据后,按照契约,对传输的数据进行解码,然后处理请求,再把处理后的结果进行编码,通过网络传输返回给服务消费者;服务消费者再对返回的结果进行解码,最终得到服务提供者处理后的返回值。
数据该如何序列化和反序列化?
一般数据在网络中进行传输前,都要先在发送方一端对数据进行编码,经过网络传输到达另一端后,再对数据进行解码,这个过程就是序列化和反序列化。
为什么要对数据进行序列化和反序列化呢?要知道网络传输的耗时一方面取决于网络带宽的大小,另一方面取决于数据传输量。要想加快网络传输,要么提高带宽,要么减小数据传输量,而对数据进行编码的主要目的就是减小数据传输量。比如一部高清电影原始大小为 30GB,如果经过特殊编码格式处理,可以减小到 3GB,同样是 100MB/s 的网速,下载时间可以从 300s 减小到 30s。
常用的序列化方式分为两类:文本类如 XML/JSON 等,二进制类如 PB/Thrift 等,而具体采用哪种序列化方式,主要取决于三个方面的因素。
服务追踪系统原理
这就不得不提到服务追踪系统的鼻祖:Google 发布的一篇的论文<a href="http://bigbully.github.io/Dapper-translation/"><code>Dapper, a Large-Scale Distributed Systems Tracing Infrastructure</code></a>,里面详细讲解了服务追踪系统的实现原理。它的核心理念就是调用链:通过一个全局唯一的 ID 将分布在各个服务节点上的同一次请求串联起来,从而还原原有的调用关系,可以追踪系统问题、分析调用数据并统计各种系统指标。
可以说后面的诞生各种服务追踪系统都是基于 Dapper 衍生出来的,比较有名的有 Twitter 的Zipkin、阿里的鹰眼、美团的MTrace
traceId,用于标识某一次具体的请求 ID。当用户的请求进入系统后,会在 RPC 调用网络的第一层生成一个全局唯一的 traceId,并且会随着每一层的 RPC 调用,不断往后传递,这样的话通过 traceId 就可以把一次用户请求在系统中调用的路径串联起来。
spanId,用于标识一次 RPC 调用在分布式请求中的位置。当用户的请求进入系统后,处在 RPC 调用网络的第一层 A 时 spanId 初始值是 0,进入下一层 RPC 调用 B 的时候 spanId 是 0.1,继续进入下一层 RPC 调用 C 时 spanId 是 0.1.1,而与 B 处在同一层的 RPC 调用 E 的 spanId 是 0.2,这样的话通过 spanId 就可以定位某一次 RPC 请求在系统调用中所处的位置,以及它的上下游依赖分别是谁。
annotation,用于业务自定义埋点数据,可以是业务感兴趣的想上传到后端的数据,比如一次请求的用户 UID。