1 定义
微服务的概念最早是在 2014 年由 MartinFowler 和 James Lewis 共同提出,他们定义了微服务是由单一应用程序构成的小服务,拥有自己的进程与轻量化处理,服务依业务功能设计,以全自动的方式部署,与其他服务使用HTTP API 通讯。同时,服务会使用最小规模的集中管理 (例如 Docker)技术,服务可以用不同的编程语言与数据库等。
1.1 单体应用的问题
单体应用技术栈早年有LAMP(Linux + Apache + MySQL +PHP)和 MVC(Spring + iBatis/Hibernate + Tomcat)两大流派。优点是学习成本低,开发上手快,测试、部署、运维也比较方便,甚至一个人就可以完成一个网站的开发与部署。
早期在业务规模不大、开发团队人员规模较小的时候,采用单体应用架构,团队的开发和运维成本都可控。
然而随着业务规模的不断扩大,团队开发人员的不断扩张,单体应用架构就会开始出现问题。
- 部署效率低下。项目变大,代码膨胀,依赖变多,编译部署花费很多时间。
- 团队协作开发成本高。团队变大,分支管理困难,集成测试成本变高。
- 系统高可用差。 重要功能和次要功能在一个服务里,没有隔离设计,如果次要功能出问题,整个系统可用性都受到影响。
1.2 服务化
用通俗的话来讲,服务化就是把传统的单机应用中通过JAR包依赖产生的本地方法调用,改造成通过 RPC 接口产生的远程方法调用。 将传统的一个单体应用拆分成一个个独立的服务,交给独立的团队进行维护。通过服务化,可以解决单体应用膨胀、团队开发耦合度高、协作效率低下的问题。
1.3 微服务的定义
得益于以 Docker 为代表的容器化技术的成熟以及 DevOps 文化的兴起,服务化的思想进一步演化,演变为今天我们所熟知的微服务。
微服务和服务化的区别?
- 服务拆分粒度更细。微服务可以说是更细维度的服务化,小到一个子模块,只要该模块依赖的资源与其他模块都没有关系,那么就可以拆分为一个微服务。
- 服务独立部署。每个微服务都严格遵循独立打包部署的准则,互不影响。比如一台物理机上可以部署多个 Docker 实例,每个 Docker 实例可以部署一个微服务的代码。
- 服务独立维护。每个微服务都可以交由一个小团队甚至个人来开发、测试、发布和运维,并对整个生命周期负责。
- 服务治理能力要求高。因为拆分为微服务之后,服务的数量变多,因此需要有统一的服务治理平台,来对各个服务进行管理。
2 服务拆分
2.1 微服务的时机
一旦单体应用同时进行开发的人员超过 10 人,就会遇到上面的问题,这个时候就该考虑进行服务化拆分了。太多人同时开发维护一个单体应用就会出现部署效率低下、团队协作开发成本高、系统高可用差的问题。
2.2 服务拆分的姿势
纵向拆分: 将不同的功能模块服务化,独立部署和运维。是从业务维度进行拆分。标准是按照业务的关联程度来决定,关联比较密切的业务适合拆分为一个微服务,而功能相对比较独立的业务适合单独拆分为一个微服务。
横向拆分: 是从公共且独立功能维度拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源独立不与其他业务耦合。
2.3 服务拆分的前置问题
- 服务如何定义。对于单体应用来说,不同功能模块之前相互交互时,通常是以类库的方式来提供各个模块的功能。对于微服务来说,每个服务都运行在各自的进程之中,应该以何种形式向外界传达自己的信息呢?答案就是接口,无论采用哪种通讯协议,是 HTTP 还是 RPC,服务之间的调用都通过接口描述来约定,约定内容包括接口名、接口参数以及接口返回值。
- 服务如何发布和订阅。单体应用由于部署在同一个 WAR 包里,接口之间的调用属于进程内的调用。而拆分为微服务独立部署后,服务提供者该如何对外暴露自己的地址,服务调用者该如何查询所需要调用的服务的地址呢?这个时候你就需要一个类似登记处的地方,能够记录每个服务提供者的地址以供服务调用者查询,在微服务架构里,这个地方就是注册中心。
- 服务如何监控。通常对于一个服务,我们最关心的是 QPS(调用量)、AvgTime(平均耗时)以及 P999(99.9% 的请求性能在多少毫秒以内)这些指标。这时候你就需要一种通用的监控方案,能够覆盖业务埋点、数据收集、数据处理,最后到数据展示的全链路功能。
- 服务如何治理。可以想象,拆分为微服务架构后,服务的数量变多了,依赖关系也变复杂了。比如一个服务的性能有问题时,依赖的服务都势必会受到影响。可以设定一个调用性能阈值,如果一段时间内一直超过这个值,那么依赖服务的调用可以直接返回,这就是熔断,也是服务治理最常用的手段之一。
- 故障如何定位。在单体应用拆分为微服务之后,一次用户调用可能依赖多个服务,每个服务又部署在不同的节点上,如果用户调用出现问题,你需要有一种解决方案能够将一次用户请求进行标记,并在多个依赖的服务系统中继续传递,以便串联所有路径,从而进行故障定位。
3 基本组件
微服务架构下,服务调用主要依赖下面几个基本组件:服务描述、注册中心、服务框架、服务监控、服务追踪、服务治理。
3.1 服务描述
常用的服务描述方式包括 RESTful API、XML 配置以及 IDL 文件三种。
XML 配置方式多用作 RPC 协议的服务描述,通过 *.xml 配置文件来定义接口名、参数以及返回值类型等。
IDL 文件方式通常用作 Thrift 和 gRPC 这类跨语言服务调用框架中,比如 gRPC 就是通过Protobuf 文件来定义服务的接口名、参数以及返回值的数据结构,示例如下:
3.2 注册中心
注册中心的工作流程
- 服务提供者在启动时,根据服务发布文件中配置的发布信息向注册中心注册自己的服务。
- 服务消费者在启动时,根据消费者配置文件中配置的服务信息向注册中心订阅自己所需要的服务。
- 注册中心返回服务提供者地址列表给服务消费者。
- 当服务提供者发生变化,比如有节点新增或者销毁,注册中心将变更通知给服务消费者。
3.3 服务框架
- 服务通信采用什么协议?就是说服务提供者和服务消费者之间以什么样的协议进行网络通信,是采用四层 TCP、UDP 协议,还是采用七层 HTTP 协议,还是采用其他协议?
- 数据传输采用什么方式?就是说服务提供者和服务消费者之间的数据传输采用哪种方式,是同步还是异步,是在单连接上传输,还是多路复用。
- 数据压缩采用什么格式?通常数据传输都会对数据进行压缩,来减少网络传输的数据量,从而减少带宽消耗和网络传输时间,比如常见的 JSON 序列化、Java 对象序列化以及Protobuf序列化。
3.4 服务监控
- 指标收集。就是要把每一次服务调用的请求耗时以及成功与否收集起来,并上传到集中的数据处理中心。
- 数据处理。有了每次调用的请求耗时以及成功与否等信息,就可以计算每秒服务请求量、平均耗时以及成功率等指标。
- 数据展示。数据收集起来,经过处理之后,还需要以友好的方式对外展示,才能发挥价值。通常都是将数据展示在 Dashboard 面板上,并且每隔 10s 等间隔自动刷新,用作业务监控和报警等。
3.4 服务追踪
服务追踪的工作原理大致如下:
- 服务消费者发起调用前,会在本地按照一定的规则生成一个 requestid,发起调用时,将requestid 当作请求参数的一部分,传递给服务提供者。
- 服务提供者接收到请求后,记录下这次请求的 requestid,然后处理请求。如果服务提供者继续请求其他服务,会在本地再生成一个自己的 requestid,然后把这两个 requestid都当作请求参数继续往下传递。
3.4 服务治理
服务治理就是通过一系列的手段来保证在各种意外情况下,服务调用仍然能够正常进行。
- 单机故障。通常遇到单机故障,都是靠运维发现并重启服务或者从线上摘除故障节点。然而集群的规模越大,越是容易遇到单机故障,在机器规模超过一百台以上时,靠传统的人肉运维显然难以应对。而服务治理可以通过一定的策略,自动摘除故障节点,不需要人为干预,就能保证单机故障不会影响业务。
- 单 IDC 故障。你应该经常听说某某 App,因为施工挖断光缆导致大批量用户无法使用的严重故障。而服务治理可以通过自动切换故障 IDC 的流量到其他正常 IDC,可以避免因为单 IDC 故障引起的大批量业务受影响。
- 依赖服务不可用。比如你的服务依赖依赖了另一个服务,当另一个服务出现问题时,会拖慢甚至拖垮你的服务。而服务治理可以通过熔断,在依赖服务异常的情况下,一段时期内停止发起调用而直接返回。这样一方面保证了服务消费者能够不被拖垮,另一方面也给服务提供者减少压力,使其能够尽快恢复。
4 服务发布和引用
服务提供者如何发布一个服务,服务消费者如何引用这个服务。具体来说,就是这个服务的接口名是什么?调用这个服务需要传递哪些参数?接口的返回值是什么类型?以及一些其他接口描述信息。 场景的服务发布和引用方式
- RESTful API
- XML 配置
- IDL 文件
5 服务注册与发现
在微服务架构下,主要有三种角色:服务提供者(RPC Server)、服务消费者(RPCClient)和服务注册中心(Registry)
- RPC server 提供服务,在启动时,根据服务发布文件 server.xml 中的配置的信息,向Registry 注册自身服务,并向 Registry 定期发送心跳汇报存活状态。
- RPC Client 调用服务,在启动时,根据服务引用文件 client.xml 中配置的信息,向Registry 订阅服务,把 Registry 返回的服务节点列表缓存在本地内存中,并与 RPCSever 建立连接。
- 当 RPC Server 节点发生变更时,Registry 会同步变更,RPC Client 感知后会刷新本地内存中缓存的服务节点列表。
- RPC Client 从本地缓存的服务节点列表中,基于负载均衡算法选择一台 RPC Sever 发起调用。
5.1 注册中心实现
5.1.1 注册中心API
注册中心必须提供以下最基本的 API。
- 服务注册接口:服务提供者通过调用服务注册接口来完成服务注册。
- 服务反注册接口:服务提供者通过调用服务反注册接口来完成服务注销。
- 心跳汇报接口:服务提供者通过调用心跳汇报接口完成节点存活状态上报。
- 服务订阅接口:服务消费者通过调用服务订阅接口完成服务订阅,获取可用服务提供者节点列表。
- 服务变更查询接口:服务消费者通过调用服务变更查询接口,获取最新的可用服务节点列表。
除此之外,为了便于管理,注册中心还必须提供一些后台管理的 API,例如:
- 服务修改接口: 修改注册中心中某一服务的信息。
- 服务查询接口: 查询注册中心当前注册了哪些服务信息。
5.1.2 注册中心集群部署
注册中心作为服务提供者和服务消费者之间沟通的桥梁,它的重要性不言而喻。所以注册中心一般都是采用集群部署来保证高可用性,并通过分布式一致性协议来确保集群中不同节点之间的数据保持一致。
以开源注册中心 ZooKeeper 为例,ZooKeeper 集群中包含多个节点,服务提供者和服务消费者可以同任意一个节点通信,因为它们的数据一定是相同的,这是为什么呢?这就要从ZooKeeper 的工作原理说起:
- 每个 Server 在内存中存储了一份数据,Client 的读请求可以请求任意一个 Server。
- ZooKeeper 启动时,将从实例中选举一个 leader(Paxos 协议)。
- Leader 负责处理数据更新等操作(ZAB 协议)。
- 一个更新操作成功,当且仅当大多数 Server 在内存中成功修改 。
6 RPC远程服务调用
- 在单体应用时,一次服务调用发生在同一台机器上的同一个进程内部,也就是说调用发生在本机内部,因此也被叫作本地方法调用。
- 在进行服务化拆分之后,服务提供者和服务消费者运行在两台不同物理机上的不同进程内,它们之间的调用相比于本地方法调用,可称之为远程方法调用,简称 RPC(Remote Procedure Call),那么RPC调用是如何实现的呢?
要成功完成RPC调用,需要解决以下问题
- 客户端和服务端如何建立网络连接?
- 服务端如何处理请求?
- 数据传输采用什么协议?
- 数据该如何序列化和反序列化?
6.1 建立连接
6.1.1 Http通信
经过三次握手建立连接,四次挥手过程断开连接。
除了http通信方式还有Socke通信方式
6.2 网络IO
- 同步阻塞方式(BIO),客户端每发一次请求,服务端就生成一个线程去处理。当客户端同时发起的请求很多时,服务端需要创建很多的线程去处理每一个请求,如果达到了系统最大的线程数瓶颈,新来的请求就没法处理了。
- 同步非阻塞方式 (NIO),客户端每发一次请求,服务端并不是每次都创建一个新线程来处理,而是通过 I/O 多路复用技术进行处理。就是把多个 I/O 的阻塞复用到同一个 select的阻塞上,从而使系统在单线程的情况下可以同时处理多个客户端请求。这种方式的优势是开销小,不用为每个请求创建一个线程,可以节省系统开销。
- 异步非阻塞方式(AIO),客户端只需要发起一个 I/O 操作然后立即返回,等 I/O 操作真正完成以后,客户端会得到 I/O 操作完成的通知,此时客户端只需要对数据进行处理就好了,不需要进行实际的 I/O 读写操作,因为真正的 I/O 读取或者写入操作已经由内核完成了。这种方式的优势是客户端无需等待,不存在阻塞等待问题。
这些分别适用的场景
- BIO 适用于连接数比较小的业务场景,这样的话不至于系统中没有可用线程去处理请求。这种方式写的程序也比较简单直观,易于理解。
- NIO 适用于连接数比较多并且请求消耗比较轻的业务场景,比如聊天服务器。这种方式相比 BIO,相对来说编程比较复杂。
- AIO 适用于连接数比较多而且请求消耗比较重的业务场景,比如涉及 I/O 操作的相册服务器。这种方式相比另外两种,编程难度最大,程序也不易于理解。
上面两个问题就是“通信框架”要解决的问题,你可以基于现有的Socket通信,在服务消费者和服务提供者之间建立网络连接,然后在服务提供者一侧基于 BIO、NIO 和 AIO 三种方式中的任意一种实现服务端请求处理,最后再花费一些精力去解决服务消费者和服务提供者之间的网络可靠性问题。这种方式对于 Socket 网络编程、多线程编程知识都要求比较高,感兴趣的话可以尝试自己实现一个通信框架。但我建议最为稳妥的方式是使用成熟的开源方案,比如 Netty、MINA 等,它们都是经过业界大规模应用后,被充分论证是很可靠的方案。
6.3 数据传输的协议
HTTP 协议、Dubbo 协议(私有定制协议)
6.4 序列化方式
一般数据在网络中进行传输前,都要先在发送方一端对数据进行编码,经过网络传输到达另一端后,再对数据进行解码,这个过程就是序列化和反序列化。
常用的序列化方式分为两类:文本类如 XML/JSON 等,二进制类如 PB/Thrift 等 采用一种序列化方式考虑的问题:
- 支持数据结构类型的丰富度。数据结构种类支持的越多越好,这样的话对于使用者来说在编程时更加友好,有些序列化框架如 Hessian 2.0 还支持复杂的数据结构比如 Map、List 等。
- 跨语言支持。序列化方式是否支持跨语言也是一个很重要的因素,否则使用的场景就比较局限。比如 Java 序列化只支持 Java 语言。
- 性能。主要看两点,一个是序列化后的压缩比,一个是序列化的速度。PB 序列化的压缩比和速度都要比 JSON 序列化高很多,所以对性能和存储空间要求比较高的系统选用 PB 序列化更合适;而 JSON 序列化虽然性能要差一些,但可读性更好,更适合对外部提供服务。
7 微服务监控
监控的对象。
- 用户端监控。监控用户端功能是否正常。
- 接口监控。 通常是指业务提供的功能所依赖的具体 RPC 接口的监控。
- 资源监控。 通常是指某个接口依赖的资源的监控。比如用户关注了哪些人的关系服务使用的是 Redis 来存储关注列表,对 Redis 的监控就属于资源监控。
- 基础监控。 CPU利用率、内存使用率、IO读写、网卡带宽。
监控指标。
- 请求量。请求量监控分为两个维度,一个是实时请求量,一个是统计请求量。实时请求qps。统计请求量一般用 PV(Page View)即一段时间内用户的访问量来衡量,类似的指标还有UV。
- 响应时间。平均耗时。三九线、五九线。P99 = 500ms,意思是 99% 的请求响应时间在 500ms 以内,它代表了请求的服务质量,即 SLA。
- 错误率。错误率的监控通常用一段时间内调用失败的次数占调用总次数的比率来衡量,比如对于接口的错误率一般用接口返回错误码为 503 的比率来表示。
监控维度 全局维度、分机房维度、单机维度、时间维度、核心维度(核心业务和非核心业务分开监控)。
7.1 监控落地
7.1.1 ELK
ELK架构需要在各个服务器上部署 Logstash 来从不同的数据源收集数据,所以比较消耗 CPU 和内存资源,容易造成服务器性能下降,此后来又在 Elasticsearch、Logstash、Kibana 之外引入了 Beats 作为数据收集器。比对Logstash,Beats所占CPU和内存几乎可以忽略不计,可以安装在每台服务器上做轻量型代理,从成百上千或成千上万台机器向 Logstash 或者直接向 Elasticsearch 发送数据。
7.1.1 Prometheus
Prometheus 主要包含下面几个组件:
- Prometheus Server:用于拉取 metrics 信息并将数据存储在时间序列数据库。
- Jobs/exporters:用于暴露已有的第三方服务的 metrics 给 Prometheus Server,比如StatsD、Graphite 等,负责数据收集。
- Pushgateway:主要用于短期 jobs,由于这类 jobs 存在时间短,可能在 PrometheusServer 来拉取 metrics 信息之前就消失了,所以这类的 jobs 可以直接向 PrometheusServer 推送它们的 metrics 信息。
- Alertmanager:用于数据报警。
- Prometheus web UI:负责数据展示。
Prometheus 的独特之处在于它采用了拉数据的方式,对业务影响较小,同时也采用了时间序列数据库存储,而且支持独有的 PromQL 查询语言,功能强大而且简洁。
8 服务追踪
应用微服务架构的系统错综复杂,这时候对外一个请求失败,排查具体是什么模块出了问题相对单体应用就比较棘手。这时候服务追踪技术应运而生。
服务追踪的作用:
- 优化系统瓶颈。通过记录调用经过的每一条链路上的耗时,我们能快速定位整个系统的瓶颈点在哪里。
- 优化链路调用。通过服务追踪可以分析调用所经过的路径,然后评估是否合理。比如一个服务调用下游依赖了多个服务,通过调用链分析,可以评估是否每个依赖都是必要的,是否可以通过业务优化来减少服务依赖。一般业务都会在多个数据中心都部署服务,以实现异地容灾,这个时候经常会出现一种状况就是服务 A 调用了另外一个数据中心的服务 B,而没有调用同处于一个数据中心的服务 B。
- 生成网络拓扑。通过服务追踪系统中记录的链路信息,可以生成一张系统的网络调用拓扑图,它可以反映系统都依赖了哪些服务,以及服务之间的调用关系是什么样的,可以一目了然。
- 透明传输数据。除了服务追踪,业务上经常有一种需求,期望能把一些用户数据,从调用的开始一直往下传递,以便系统中的各个服务都能获取到这个信息。比如业务想做一些 A/B 测试,这时候就想通过服务追踪系统,把 A/B 测试的开关逻辑一直往下传递,经过的每一层服务都能获取到这个开关值,就能够统一进行 A/B 测试。
8.1 追踪原理
服务追踪系统的鼻祖是Google 发布的一篇的论文 Dapper, a LargeScale Distributed Systems Tracing Infrastructure 里面详细讲解了服务追踪系统的实现原理。它的核心理念就是调用链:通过一个全局唯一的 ID 将分布在各个服务节点上的同一次请求串联起来,从而还原原有的调用关系,可以追踪系统问题、分析调用数据并统计各种系统指标。
可以说后面的诞生各种服务追踪系统都是基于 Dapper 衍生出来的,比较有名的有 Twitter的Zipkin、阿里的鹰眼、美团的MTrace等。
- traceId, 全局唯一,用于标识一次分布式请求,会在RPC调用的网络中传递。
- spanId, spanId,用于标识一次 RPC 调用在分布式请求中的位置。
- annotation, 用于业务自定义埋点数据,可以是业务感兴趣的想上传到后端的数据,比如一次请求的用户 UID。
traceId 是用于串联某一次请求在系统中经过的所有路径,spanId 是用于区分系统不同服务之间调用的先后关系.
8.2 追踪实现
务追踪系统分为三层
- 数据采集层,负责数据埋点并上报。
- 数据处理层,负责数据的存储与计算。
- 数据展示层,负责数据的图形化展示。
调用拓扑图
9 服务治理
微服务部署好后,运行时会出现各种预料之中或之外的事情。比如:
- 节点宕机 注册中心宕机;服务提供者 B 有节点宕机;
- 网络不通: 服务消费者 A 和注册中心之间的网络不通;服务提供者 B 和注册中心之间的网络不通;服务消费者 A 和服务提供者 B 之间的网络不通;
- 性能变慢, 服务提供者B性能变慢。
- 返回失败报错,不能正确返回
9.1 节点管理
- 注册中心主动摘除机制 服务提供者需要定时向注册中心上报心跳,超过一定时间,就认为服务出问题,把出问题的节点从服务节点列表摘除。
- 服务消费者摘除机制 虽然注册中心主动摘除机制可以解决服务提供者节点异常的问题,但如果因为注册中心与服务提供者之间的网络出现异常,最坏的情况是注册中心会把服务节点全部摘除,导致服务消费者没有可用的服务节点调用,但其实这时候服务提供者本身是正常的。所以,将存活探测机制用在服务消费者这一端更合理,如果服务消费者调用服务提供者节点失败,就将这个节点从内存中保存的可用服务提供者节点列表中移除。
9.2 负载均衡
- 随机算法 顾名思义就是从可用的服务节点中随机选取一个节点。一般情况下,随机算法是均匀的,也就是说后端服务节点无论配置好坏,最终得到的调用量都差不多。
- 轮询算法 Round Robin 就是按照固定的权重,对可用服务节点进行轮询。可以给某些硬件配置较好的节点的权重调大些,这样的话就会得到更大的调用量,从而充分发挥其性能优势,提高整体调用的平均性能。
- 最少活跃调用算法, 这种算法是在服务消费者这一端的内存里动态维护着同每一个服务节点之间的连接数,当调用某个服务节点时,就给与这个服务节点之间的连接数加 1,调用返回后,就给连接数减1。然后每次在选择服务节点时,根据内存里维护的连接数倒序排列,选择连接数最小的节点发起调用,也就是选择了调用量最小的服务节点,性能理论上也是最优的。
- 一致性 Hash 算法, 指相同参数的请求总是发到同一服务节点。当某一个服务节点出现故障时,原本发往该节点的请求,基于虚拟节点机制,平摊到其他节点上,不会引起剧烈变动。
一致Hash算法用的最多的场景,就是分配cache服务。将某一个用户的数据缓存在固定的某台服务器上,那么我们基本上就不用多台机器都缓存同样的数据,这样对我们提高缓存利用率有极大的帮助。
不过硬币都是有两面的,一致Hash也不例外。当某台机器出问题以后,这台机器上的cache失效,原先压倒这台机器上的请求,就会压到其他机器上。由于其他机器原先没有这些请求的缓存,就有可能直接将请求压到数据库上,造成数据库瞬间压力增大。如果压力很大的话,有可能直接把数据库压垮。
适用场景分析 这几种算法的实现难度也是逐步提升的,所以选择哪种节点选取的负载均衡算法要根据实际场景而定。如果后端服务节点的配置没有差异,同等调用量下性能也没有差异的话,选择随机或者轮询算法比较合适;如果后端服务节点存在比较明显的配置和性能差异,选择最少活跃调用算法比较合适。
9.3 服务路由
对于服务消费者而言,在内存中的可用服务节点列表中选择哪个节点不仅由负载均衡算法决定,还由路由规则确定。 指定路由规则的原因:
- 业务存在灰度发布的需求。服务提供者做了功能变更,但希望先只让部分人群使用,然后根据这部分人群的使用反馈,再来决定是否做全量发布。
- 多机房就近访问的需求。 大部分业务规模中等及以上的互联网公司,为了业务的高可用性,都会将自己的业务部署在不止一个 IDC 中。不同IDC之间的访问由于要跨IDC,通过专线访问,尤其是IDC相距比较远时延迟就会比较大比如北京和广州的专线延迟一般在30ms左右,这对于某些延时敏感性的业务是不可接受的,所以就要一次服务调用尽量选择同一个IDC内部的节点,从而减少网络耗时开销,提高性能。这时一般可以通过IP段规则来控制访问,在选择服务节点时,优先选择同一IP段的节点。
9.3.1 配置规则
- 静态配置。就是在服务消费者本地存放服务调用的路由规则,在服务调用期间,路由规则不会发生改变,要想改变就需要修改服务消费者本地配置,上线后才能生效。
- 动态配置。路由规则是存在注册中心的,服务消费者定期去请求注册中心来保持同步,要想改变服务消费者的路由配置,可以通过修改注册中心的配置,服务消费者在下一个同步周期之后,就会请求注册中心来更新配置,从而实现动态更新。
9.4 服务容错有以下手段
服务容错有以下手段:
- FailOver:失败自动切换。就是服务消费者发现调用失败或者超时后,自动从可用的服务节点列表总选择下一个节点重新发起调用,也可以设置重试的次数。这种策略要求服务调用的操作必须是幂等的,也就是说无论调用多少次,只要是同一个调用,返回的结果都是相同的,一般适合服务调用是读请求的场景。
- FailBack:失败通知。就是服务消费者调用失败或者超时后,不再重试,而是根据失败的详细信息,来决定后续的执行策略。比如对于非幂等的调用场景,如果调用失败后,不能简单地重试,而是应该查询服务端的状态,看调用到底是否实际生效,如果已经生效了就不能再重试了;如果没有生效可以再发起一次调用。
- FailCache:失败缓存。 服务消费者调用失败或者超时后,不立即发起重试,而是隔一段时间后再次尝试发起调用。比如后端服务可能一段时间内都有问题,如果立即发起重试,可能会加剧问题,反而不利于后端服务的恢复。如果隔一段时间待后端节点恢复后,再次发起调用效果会更好。
- FailFast:快速失败。就是服务消费者调用一次失败后,不再重试。实际在业务执行时,一般非核心业务的调用,会采用快速失败策略,调用失败后一般就记录下失败日志就返回了。 一般情况下对于幂等的调用,可以选择 FailOver 或者 FailCache,非幂等的调用可以选择 FailBack 或者FailFast。
10 微服务落地
10.1 开源注册中心实现
应用内实现,Eureka
- Eureka Server:注册中心的服务端,实现了服务信息注册、存储以及查询等功能。服务端的
- Eureka Client:集成在服务端的注册中心 SDK,服务提供者通过调用 SDK,实现服务注册、反注册等功能。客户端的
- Eureka Client:集成在客户端的注册中心 SDK,服务消费者通过调用 SDK,实现服务订阅、服务更新等功能。
应用外实现,Consul
- Consul:注册中心的服务端,实现服务注册信息的存储,并提供注册和发现服务。
- Registrator:一个开源的第三方服务管理器项目,它通过监听服务部署的Docker实例是否存活,来负责服务提供者的注册和销毁。
- Consul Template:定时从注册中心服务端获取最新的服务提供者节点列表并刷新LB配置(比如 Nginx 的 upstream),这样服务消费者就通过访问 Nginx 就可以获取最新的服务提供者信息。
两种解决方案的不同之处在于应用场景,应用内的解决方案一般适用于服务提供者和服务消费者同属于一个技术体系;应用外的解决方案一般适合服务提供者和服务消费者采用了不同技术体系的业务场景,比如服务提供者提供的是 C++ 服务,而服务消费者是一个 Java 应用,这时候采用应用外的解决方案就不依赖于具体一个技术体系。同时,对于容器化后的云应用来说,一般不适合采用应用内 SDK 的解决方案,因为这样会侵入业务,而应用外的解决方案正好能够解决这个问题。
10.2 注册中心高可用
注册中心实现高可用的方法
- 集群部署。 部署多个实例组成集群来保证高可用性,这样的话即使有部分机器宕机,将访问迁移到正常的机器上就可以保证服务的正常访问。
- 多IDC部署. 部署在不止一个机房,这样能保证即使一个机房因为断电或者光缆被挖断等不可抗力因素不可用时,仍然可以通过把请求迁移到其他机房来保证服务的正常访问。
以Consul为例子,一方面,在每个数据中心(DATACENTER)内都有多个注册中心 Server 节点可供访问;另一方面还可以部署在多个数据中心来保证多机房高可用性。
10.3 数据一致性问题
多个数据中心之间如何保证数据一致?如何确保访问数据中心中任何一台机器都能得到正确的数据? 这里就涉及分布式系统中著名的 CAP 理论,即同时满足一致性 Consistency、可用性 Availability、分区容错性Partition Tolerance这三者是不可能的。 总的来说,就是数据节点越多,分区容错性越高,但数据一致性越难保证。为了保证数据一致性,又会带来可用性的问题。
不同的注册中心解决方案选择的方向也就不同,大致可分为两种。
- CP 型注册中心,牺牲可用性来保证数据强一致性,最典型的例子就是 ZooKeeper,etcd,Consul 了。ZooKeeper 集群内只有一个 Leader,而且在 Leader 无法使用的时候通过 Paxos 算法选举出一个新的 Leader。这个 Leader 的目的就是保证写信息的时候只向这个 Leader 写入,Leader 会同步信息到 Followers,这个过程就可以保证数据的强一致性。但如果多个 ZooKeeper 之间网络出现问题,造成出现多个 Leader,发生脑裂的话,注册中心就不可用了。而 etcd 和 Consul 集群内都是通过 raft 协议来保证强一致性,如果出现脑裂的话, 注册中心也不可用。
- AP 型注册中心 牺牲一致性来保证可用性,最典型的例子就是 Eureka 了。对比Zookeeper,Eureka不用选举一个 Leader,每个 Eureka 服务器单独保存服务注册地址,因此有可能出现数据信息不一致的情况。但是当网络出现问题的时候,每台服务器都可以完成独立的服务。
对于注册中心来说,最主要的功能是服务的注册和发现,在网络出现问题的时候,可用性的需求要远远高于数据一致性。即使因为数据不一致,注册中心内引入了不可用的服务节点也可以通过其他措施来避免,比如客户端的快速失败机制等,只要实现最终一致性,对于注册中心来说就足够了。因此,选择 AP 型注册中心,一般更加合适。
11 开源RPC技术选型
| rpc实现 | 跨语言支持 | 开源时间和公司 | 通信协议 | 序列化格式 | 通信方式 | |
|---|---|---|---|---|---|---|
| Dubbo | java | 2011/阿里 | 私有Dubbo,MI,Hession,HTTP,Thrift | Dubbo、Hession、JSON、Kryo、FST | ||
| Motan | java | 2016/微博 | Hessian 2 | Netty NIO 的 TCP 长链接方 | ||
| Tars | c++ | 2017/腾讯 | ||||
| Spring Cloud | Java | 2014/Pivotal 公司 | Http | |||
| 以下支持多语言 | --- | --- | --- | --- | --- | |
| gRPC | 支持多语言 | 2015/ 谷歌 | HTTP/2 | ProtoBuf | ||
| Thrift | 支持多语言 | 2007/ 脸书 | Binary、Compact、JSON、Multiplexed | Socket、Framed、File、Memory、zlib |
11.1 Dubbo
服务消费者和服务提供者都需要引入 Dubbo 的 SDK 才来完成 RPC 调用,因为Dubbo 本身是采用 Java 语言实现的,所以要求服务消费者和服务提供者也都必须采用Java 语言实现才可以应用。
- 通信框架方面,Dubbo 默认采用了 Netty 作为通信框架。
- 通信协议方面,Dubbo 除了支持私有的 Dubbo 协议外,还支持 RMI 协议、Hession协议、HTTP 协议、Thrift 协议等。
- 序列化格式方面,Dubbo 支持多种序列化格式,比如 Dubbo、Hession、JSON、Kryo、FST 等。
11.2 Motan
- register:用来和注册中心交互,包括注册服务、订阅服务、服务变更通知、服务心跳发送等功能。Server 端会在系统初始化时通过 register 模块注册服务,Client 端会在系统初始化时通过 register 模块订阅到具体提供服务的 Server 列表,当 Server 列表发生变更时也由 register 模块通知 Client。
- protocol:用来进行 RPC 服务的描述和 RPC 服务的配置管理,这一层还可以添加不同功能的 filter 用来完成统计、并发限制等功能。
- serialize:将 RPC 请求中的参数、结果等对象进行序列化与反序列化,即进行对象与字节流的互相转换,默认使用对 Java 更友好的 Hessian 2 进行序列化。
- transport:用来进行远程通信,默认使用 Netty NIO 的 TCP 长链接方式。
- cluster:Client 端使用的模块,cluster 是一组可用的 Server 在逻辑上的封装,包含若干可以提供 RPC 服务的 Server,实际请求时会根据不同的高可用与负载均衡策略选择一个可用的 Server 发起远程调用。
11.3 Motan
- 请求统一通过 API 网关 Zuul 来访问内部服务,先经过 Token 进行安全认证。
- 通过安全认证后,网关 Zuul 从注册中心 Eureka 获取可用服务节点列表。从可用服务节点中选取一个可用节点,然后把请求分发到这个节点。
- 整个请求过程中,Hystrix 组件负责处理服务超时熔断,Turbine 组件负责监控服务间的调用和熔断相关指标,Sleuth 组件负责调用链监控,ELK 负责日志分析。