01-微服务平台概念

微服务是由单一应用程序构成的小服务,拥有自己的进程与轻量化处理,服务依业务功能设计,以全自动的方式部署,与其他服务使用HTTP API通讯。同时,服务会使用最小规模的集中管理技术(如Docker),服务可以使用不用的编程语言和数据库。

0.1. 单体应用

单体应用技术栈:

  • LAMP(Linux、Apache、MySQL、PHP)
  • MVC(Spring、iBatis/Hibernate、Tomcat)

优点:学习成本低、开发上手快、测试部署运维方便

痛点:

  1. 部署效率低下
  2. 团队协作开发成本高
  3. 系统高可用性差
  4. 线上发布变慢(代码膨胀、服务启动时间变长)

0.2. 服务化与微服务

服务化:把传统单机应用中通过JAR包依赖产生的本地方法调用,改造成通过RPC接口产生的远程方法调用。对于通用的业务逻辑,抽象并独立成为专门的模块,对于代码复用和业务理解都有好处。

通过服务化,解决单体应用膨胀,团队开发耦合度高,协作效率低下的问题。

微服务与服务化相比:

  • 服务拆分粒度更细:微服务是更细维度的服务化,只要该模块依赖的资源与其他模块没有关系,就可以拆分为一个微服务
  • 服务独立部署:每个微服务都严格遵循独立打包部署的准则
  • 服务独立维护
  • 服务治理能力要求高:服务数量变多,需要有统一的服务治理平台,对各个服务进行管理

0.3. 服务化拆分方式

  1. 纵向拆分,从业务维度进拆分: 将不同的功能模块服务化,独立部署和运维,按照业务的关联程度来决定,关联紧密的服务拆分为一个微服务。
  2. 横向拆分,从公共且独立的功能维度拆分,按照是否有公共的被多个其他服务调用,且依赖的资源独立不与其他业务耦合

0.3.1. 服务化拆分的前置条件

条件单体应用微服务
服务如何定义类库的方式提供各个模块的功能接口的形式向外传达信息,服务之间的调用都是通过接口描述来约定,约定内容包括接口名、接口参数和接口返回值
服务如何发布和订阅接口之间的调用属于进程内调用使用注册中心,服务提供者暴露位置,服务调用者查询服务地址
服务如何监控调用量、平均耗时、99.9%请求性能在多少毫秒以内全链路监控:业务埋点、数据收集、数据处理、数据展示
服务如何治理服务数量多,依赖关系复杂,服务熔断
故障如何定位一次调用依赖多个服务,每个服务部署在不同节点,将一次用户请求进行标记,并在多个依赖的服务系统中继续传递,以便串联所有路径,从而进行故障定位

0.4. 微服务架构

微服务架构模块图:

image

一次正常的服务调用流程:

  1. 服务提供者按照一定格式的服务描述,向注册中心注册服务,声明自己提供的服务以及服务的地址,完成服务发布
  2. 服务消费者请求注册中心,查询所需调用的服务地址,然后以约定的通信协议向服务提供者发起请求,得到请求结果之后,再按照约定的协议解析结果。

在服务请求的过程中,服务的请求耗时调用量成功率等指标会被记录下来用作监控,调用经过的链路信息会被记录下来,用作故障定位和问题追踪。在这期间,如果调用失败,可以通过重试等服务治理手段来保证成功率。

微服务架构下,服务调用主要依赖的基本组件:

  1. 服务描述
  2. 注册中心
  3. 服务框架
  4. 服务监控
  5. 服务追踪
  6. 服务治理

0.4.1. 服务描述

服务如何对外描述,具体要解决如下几个问题:

  • 服务名称?
  • 调用服务需要提供的信息?
  • 调用服务返回的结果格式?
  • 如何解析结果?

常用的服务描述方式:

  1. RESTful API:常用于HTTP/HTTPS协议的服务描述,常用Wiki或Swagger进行管理
  2. XML配置:常用于RPC协议的服务描述,通过*.xml配置文件来定义接口名、参数以及返回值类型
  3. IDL文件:常用于ThriftgRPC这类跨语言服务调用框架中,如gRPC通过Protobuf文件定义服务的接口名、参数以及返回值的数据结构

0.4.1.1. RESTful API

服务消费者通过HTTP协议调用服务(HTTP协议本身是一个公开的协议,对于服务消费者来说几乎没有学习成本),比较适合用作跨业务平台(业务部门内部,其他业务部门,外网)之间的服务协议。

0.4.1.2. XML配置

服务发布与引用的三个步骤:

  1. 服务提供者定义并实现接口
  2. 服务提供者进程启动时,通过加载server.xml配置文件将接口暴露出来
  3. 服务消费者进程启动时,通过加载client.xml配置文件来引入要调用的接口

通过在服务提供者和服务消费者之间维护一份对等的XML配置文件,来保证服务消费者按照服务提供者的约定来进行服务调用。如果变更接口,需要同时更新server.xmlclient.xml

一般私有RPC框架会选择XML配置方式来描述接口,私有RPC协议的性能要比HTTP协议高,在对性能要求比较高的场景下,采用XML配置的方式比较合适。这种方式对代码侵入性高,适合公司内部比较紧密的业务之间采用。

0.4.1.3. IDL文件

接口描述语言,通过一种中立的方式来描述接口,使得在不同的平台上运行的对象和不同语言编写的程序可以相互通信交流。IDL主要用作跨语言平台的服务之间的调用

以gRPC为例,通过proto文件来定义接口,再使用protoc来生成不同语言平台的客户端和服务端代码,从而具备跨语言服务调用能力。

注意,在描述接口定义时,IDL文件需要对接口返回值进行详细定义。如果接口返回值比较多,且经常变化,采用IDL文件的接口定义就不太合适。可能造成IDL文件过大难以维护,IDL文件中定义的接口返回值有变更,需要同步所有服务消费者都更新,成本太高。

0.4.1.4. 小结

服务描述方式使用场景缺点
RESTful API跨语言平台,组织内外皆可使用HTTP作为通信协议,比TCP协议性能差
XML配置Java平台,一般用作组织内部不支持跨语言平台
IDL文件跨语言平台,组织内外皆可修改或删除PB字段不能向前兼容

0.4.2. 注册中心

在微服务架构中,主要有三种角色:

  • 服务提供者(RPC Server)
  • 服务消费者(RPC Client)
  • 服务注册中心(Registry)

服务提供者将自己提供的服务以及地址登记到注册中心,服务消费者则从注册中心查询所需要调用的服务的地址,然后发起请求。

工作流程:

  1. 服务提供者在启动时,根据服务发布文件(server.xml中配置的发布信息向注册中心注册自己的服务,并定期发送心跳汇报存活状态
  2. 服务消费者在启动时,根据消费者引用文件(client.xml中配置的服务信息向注册中心订阅自己所需要的服务,把注册中心返回的服务列表缓存在本地内存中,并与服务提供者建立连接
  3. 注册中心返回服务提供者地址列表给服务消费者
  4. 当服务提供者发生变化(如,节点增删),注册中心将变更通知给服务消费者,服务消费者感知后会刷新本地内存中缓存的服务节点列表
  5. 服务调用者从本地缓存的服务节点列表中,基于负载均衡算法选择一个服务节点发起调用

image

0.4.2.1. 注册中心实现方式

涉及如下几个问题:

  1. 提供哪些接口?
  2. 如何部署?
  3. 如何存储服务信息?
  4. 如何监控服务提供者节点的存活?
  5. 如果服务提供者节点有变化如何通知服务消费者?
  6. 如何控制注册中心的访问权限?
0.4.2.1.1. 问题1:注册中心访API

注册中心必须提供以下最基本的API:

  1. 服务注册接口:服务提供者通过调用注册接口来完成服务注册
  2. 服务反注册接口:服务提供者通过调用反注册接口来完成服务注销
  3. 心跳汇报接口:服务提供者通过心跳汇报接口完成节点存活状态上报
  4. 服务订阅接口:服务消费者通过调用服务订阅接口完成服务订阅,获取可用的服务提供者节点列表
  5. 服务变更查询接口:服务消费者通过调用服务变更查询接口,获取最新的可用服务节点列表

为了便于管理,还需提供后台管理API:

  1. 服务查询接口:查询注册中心当前注册了哪些服务信息
  2. 服务修改接口:修改注册中心中某一服务的信息
0.4.2.1.2. 问题2:集群部署

注册中心作为服务提供者和消费者之间的沟通桥梁,采用集群部署来保证高可用性,并通过分布式一致性协议来保证集群中不同节点之间的数据一致性。

以Zookeeper为例(保证高可用性和数据一致性):

  • 每个Server在内存中存储一份数据,Client的读取可以请求任意一个Server
  • 启动时,基于Paxos协议从所有实例中选举一个Leader
  • Leader基于ZAB协议处理数据更新等操作
  • 一个更新操作成功,当且仅当大多数Server在内存中修改成功

image

0.4.2.1.3. 问题3:目录存储

以Zookeeper为例,注册中心存储服务信息一般采用层次化的目录结构:

  • 每个目录称为一个znode,并且有一个唯一的路径标识
  • znode可以包含数据和子znode
  • znode中的数据可以有多个版本,查询时需要带上版本信息

image

0.4.2.1.4. 问题4:服务健康状态检测

对服务提供者节点进行健康检测,才能保证注册中心里保存的服务节点都是可用的。

以Zookeeper为例,基于客户端(集群中的服务提供者)和服务端(注册中心)的长连接和会话超时控制机制,来实现服务健康状态检测。

  1. 客户端与服务端建立连接后,会话随之建立,并生成一个全局唯一的Session ID
  2. 客户端与服务端维持一个长连接,在SESSION_TIMEOUT周期内,服务端定时向客户端发送心跳信息(ping),服务器重置下一次SESSION_TIMEOUT时间
  3. 超过SESSION_TIMEOUT后,服务端都没有收到客户端的心跳消息,则认为Session结束,认为这个服务节点不可用,从注册中心删除
0.4.2.1.5. 问题5:服务状态变更通知

一旦注册中心探测到有服务提供者节点新增或删除,就必须立刻通知所有订阅该服务的消费者,消费者刷新本地缓存的服务节点信息,确保服务调用不会请求不可用的服务提供者。

以Zookeeper为例,基于Watcher机制,来实现服务状态变更通知:

  1. 消费者在调用Zookeeper的getData方法订阅服务时,还可以通过监听器Watcher的process方法获取服务的变更
  2. 然后调用getData方法来获取变更后的数据,刷新本地缓存的服务节点信息
0.4.2.1.6. 问题6:白名单机制

注册中心提供白名单机制,只有添加到白名单中的服务提供者才能够调用注册中的注册接口,这样可以避免测试环境中的节点意外跑到线上环境中。

0.4.3. 服务框架

通过注册中心,服务消费者获得服务提供者的地址,可以发起调用,但是在调用之前需要解决几个问题:

  1. 服务通信协议:四层TCP/UDP协议/七层HTTP协议/其他协议
  2. 数据传输方式:同步/异步/单连接传输/多路复用
  3. 数据压缩格式:通常数据传输都会压缩来减少网络传输的数据量,从而降低带宽和网络传输时间,JSON序列化/Java对象序列化/Protobuf序列化

相较于单体应用的本地方法调用,微服务进行的是远程方法调用(RPC)。在RPC中,把服务消费者称为客户端,把服务提供者称为服务端,两者位于网络上的不同位置,完成一次RPC需要建立连接,然后按照通信协议进行通信,正常通信后,服务端处理请求,客户端接收结果,为了减少传输的数据,对数据进行序列化。

完成RPC调用需要解决四个问题:

  1. 客户端和服务端如何建立网络连接?
  2. 服务端如何处理请求?
  3. 数据传输采用什么协议?
  4. 数据该如何序列化和反序列化?

0.4.3.1. 问题1:建立网络连接

基于TCP建立网络连接的两种最常用途径:

  • HTTP通信:基于应用程序HTTP协议(HTTP协议基于传输层TCP协议),一次HTTP通信就是发起一次HTTP调用,一次HTTP调用就会建立一个TCP连接,经过三次握手过程来建立连接,完成请求后,再经过四次挥手过程断开连接。

image
image

  • Socket通信:基于TCP/IP协议的封装,建立一次Socket连接至少需要一对套接字,其中一个运行在客户端(ClientSocket),一个运行在服务端(ServerSocket)。Socket通信的过程分为四个步骤:服务器监听、客户端请求、连接确认、数据传输。

image

  • 服务器监听:ServerSocket通过调用bind()函数绑定某个具体端口,然后调用listen()函数实时监控网络状态,等待客户端的连接请求
  • 客户端请求:ClientSocket调用connect()函数向ServerSocket绑定的地址和端口发起连接请求
  • 服务端连接确认:当ServerSocket监听或者接收到ClientSocket的连接请求时,调用accept()函数响应ClientSocket的请求,同客户端建立连接
  • 数据传输:建立连接后,ClientSocket调用Send()函数,ServerSocket调用receive()函数,ServerSocket处理完请求后,调用send()函数,ClientSocket调用receive()函数,就可以返回结果了

网络经常出现闪断、连接超时、服务端宕机等各种异常,通常的处理手段有两种:

  • 链路存活检测:客户端定时发送心跳检测信息(一般通过ping请求)给服务端,客户端连续n次检测或超过规定时间没有回复,则认为链路失效,客户端重新与服务端建立连接
  • 断连重试:客户端在等待固定的时间间隔后发起重连,避免客户端连接回收不及时,客户端瞬间重连请求太多而把服务端连接数占满

0.4.3.2. 问题2:服务端处理请求

处理方式描述适用场景优缺点举例
同步阻塞(BIO)客户端发起一次请求,服务端生成一个线程处理,请求过多到达系统最大线程数,新请求无法被处理连接数比较小的业务场景这种方式写的程序简单直观易于理解
同步非阻塞(NIO)客户端发起一次请求,服务端不会每次都创建一个新线程,而是通过I/O多路复用技术处理,把多个I/O的阻塞复用到同一个select的阻塞上,使单线程也能同时处理多个客户端请求连接数较多且请求消耗比较轻的业务场景不用每个请求创建线程,节省系统开销,相对BIO编程比较复杂聊天服务器
异步非阻塞(AIO)客户端发起I/O请求后立即返回,等I/O请求真正被完成后,客户端得到操作完成的通知,此时客户端再对数据进行处理,不需要进行实际的I/O操作(由内核完成)连接数多且请求消耗比较重的业务场景客户端无需等待,不存在阻塞等待问题,但是编程难度最大,程序不易理解涉及I/O操作的相册服务

问题1和2都是通信框架要解决的问题,我们可以基于Socket通信在服务消费者和提供者之间建立网络连接,然后在提供者一侧基于BIO、NIO、AIO中任一方式实现请求处理,最后在解决消费者和提供者之间的网络可靠性问题。可使用成熟开源的方案NettyMINA

0.4.3.3. 问题3:数据传输协议

最常用的HTTP协议,或者Dubbo协议,消费者和提供者之间基于协议的契约达成共识,消费者按照契约将数据编码后通过网络传输给提供者,提供者从网络上接收到数据后,按照契约对数据进行解码,然后处理请求,在将处理后的结果编码后通过网络传输给消费者,消费者对返回结果进行解码,得到提供者处理后的返回值。

协议契约包括两部分:

  1. 消息头:存放的是协议的公共字段以及用户扩展字段
  2. 消息体:存放的是传输数据的具体内容

以HTTP协议响应数据为例:

  1. 消息头中存放协议公共字段:Server代表服务端服务器类型、Content-Length代码返回数据的长度、Content-Type代表返回数据的类型
  2. 消息体中存放具体的返回结果,如一段HTML网页代码

0.4.3.4. 问题4:数据序列化和反序列化

数据在网络中传输前,在发送方对数据进行编码(序列化),经过网络传输到达接收方再对数据进行解码(反序列化)。

网络传输的耗时,取决于网络带宽和数据传输量。加快网络传输的方式,提高带宽或者减少数据传输量。对数据编码的目的是减小数据传输量。

序列化分为两类:文本类JSON/XML,二进制类PB/Thrift,决定序列化方式的三个因素:

  1. 支持数据结构的丰富度:数据结构种类支持的越多越好,对使用者来说编程更友好
  2. 跨语言支持:支持跨语言使用的场景更丰富
  3. 性能:序列化后的压缩比和序列化的速度
种类压缩比速度适用场景
PB对性能和存储空间要求高的系统
JSON可读性好,适合对外部提供服务

0.4.3.5. 综上

  • 通信框架提供基础通信能力
  • 通信协议描述通信契约
  • 序列化和反序列化用于数据的编码和解码

一个通信框架可以适配多种通信协议,可以采用多种序列化和反序列化的格式。如Dobbo框架支持Dobbo协议、RMI协议、HTTP协议、支持JSON、Hession 2.0 、Java等序列化和反序列化格式。

0.4.4. 服务监控

服务监控在微服务改造中的重要性不言而喻,没有强大的监控能力,改造微服务架构后,就无法掌控各个不同服务的情况,在遇到调用失败时,如果不能快速发现系统的问题,对于业务来说就是异常灾难。

与单体应用相比,在微服务架构下,一次用户调用会因为服务化拆分后,变成多个不同服务之间的相互调用,一旦服务消费者与服务提供者之间能够发起服务调用,就需要对调用情况进行监控,以了解服务是否正常,通常,服务监控包括三个流程:

  1. 指标收集:每一次服务调用的请求耗时以及成功与否收集起来,上传到集中的数据处理中心
  2. 数据处理:根据收集的指标,计算每秒服务请求量平均耗时以及成功率
  3. 数据展示:处理后的数据以友好的方式展示,通常展示在Dashboard面板,并且每隔10s间隔自动刷新,用作服务监控和报警

0.4.4.1. 监控对象

对于微服务来说,监控对象可以分为四个层次,由上而下:

  • 用户端监控:通常是指业务直接对用户提供的功能的监控。
  • 接口监控:通常是指业务提供的功能所依赖的具体RPC接口的监控。
  • 资源监控:通常是指某个接口依赖的资源的监控。
  • 基础监控:通常是指对服务器本身的健康状况的监控,包括CPU利用率,内存使用量,I/O读写、网卡带宽等。

0.4.4.2. 监控指标

以下业务指标需要重点监控:

  • 请求量:请求量监控分为两个维度,实时请求量和统计请求量。实时请求量(QPS,Queries Per Second)即每秒查询次数来衡量,它反映了服务调用的实时变化情况。统计请求量(PV,Page View)即一段时间内用户的访问量来衡量,比如一天的PV代表了服务一天的请求量,通常用来统计报表。
  • 响应时间:用一段时间内所有调用的平均耗时来反应请求的响应时间。它只代表了请求的平均快慢情况。更关注慢请求的数量时,可以将响应时间划分为多个区间:0~10ms10~50ms50~100ms100~500ms500ms以上,最后一个区间内的请求数量就代表了慢请求量,正常情况下,这个区间内的请求应该为0。也可以从P90、P95、P99、P999角度来监控请求的响应时间,P99=500ms表示99%的请求的响应时间在500ms以内,它代表了请求的服务质量,即SLA。
  • 错误率:用一段时间内调用失败的次数占调用总次数的比率来衡量,比如对于接口的错误率一般用返回错误码为503的比率表示。

0.4.4.3. 监控维度

  • 全局维度:从整体角度监控对象的请求量,平均耗时以及错误率,全局维度的监控一般是为了对监控对象的调用情况有整体的了解。
  • 分机房维度:为了业务的高可用性,服务通常部署在不同的机房,不同的地域,对同一个监控对象的各种指标可能会相差很大。
  • 单机维度:同一机房,不同的服务器上,同一个监控对象的各种指标也会有很大差异。
  • 时间维度:同一个监控对象,在每天的固定一个时刻各种指标通常也会不一样,这种差异可能有业务变更导致,或者活动运营导致。为了了解监控对象各种指标变化,通常需要与一天前,一周前,一个月前,甚至三个月前对比。
  • 核心维度:业务一般会根据重要性程度对监控对象进行分级(核心/非核心业务),这两者在部署上必须隔离,分开监控,才能对核心业务做重点保障。

对于一个微服务来说,必须明确要监控哪些对象,哪些指标,并且从不同的维度进行监控,才能掌握微服务的调用情况。

0.4.4.4. 监控系统原理

对服务调用进行监控:

  1. 数据采集,收集每一次调用的详细信息,包括调用的响应时间、调用是否成功、调用的发起者和接收者
  2. 数据传输,把采集的数据通过一定的方式传输给数据处理中心进行处理
  3. 数据处理,数据中心对采集的数据按照服务的维度进行聚合,计算出不同服务的请求量、响应时间以及错误率等信息,并存储起来
  4. 数据展示,通过接口或者Dashboard的形式对外展示服务的调用情况
0.4.4.4.1. 数据采集
  • 服务主动上报,通过在业务代码或者服务框架里加入数据收集代码逻辑,每一次服务调用完成后,主动上报服务的调用信息
  • 代理采集,通过服务调用后把调用的详细信息记录到本地日志文件中,然后再通过代理去解析本地日志文件,然后再上报服务的调用信息

无论那种方式,首先考虑的问题是采样率,即采集数据的频率。采样率决定了监控的实时性和精确度。高采样率,实时性和精确度都高,消耗的资源也多,尤其是磁盘的I/O过高会影响正常的服务调用。

设置合理的采样率是数据采集的关键,最好可以动态控制。系统空闲时,提高采样率来追求监控的实时性和精确度,系统高负载时,降低采样率来追求系统的可用性和稳定性。

0.4.4.4.2. 数据传输
  • UDP传输,数据处理单元提供服务的请求地址,数据采集后通过UDP协议与服务器建立连接,然后把数据发送过去
  • Kafka传输,数据采集后发送到指定的Topic,然后数据处理单元再订阅对应的Topic,从Kafka消息队列中读取到对应的数据

数据格式非常重要,尤其是对带宽敏感以及解析性能要求比较高的场景,一般传输时采用的数据格式有:

  • 二进制协议,如PB对象,高压缩比、高性能、减少传输带宽并且序列化和反序列化效率高
  • 文本协议,如JSON字符串,可读性高,相对PB对象,占用带宽高,并且解析性能差一些
0.4.4.4.3. 数据处理

对收集的数据进行聚合并存储,聚合通常有两个维度:

  • 接口维度聚合,把实时收到的数据按照接口名维度实时聚合,可以得到每个接口的实时请求量、平均耗时等信息
  • 机器维度聚合,把实时收到的数据按照调用节点维度聚合,可以从单机维度查看每个接口的实时请求量、平均耗时等信息

聚合后的数据需要持久化到数据库中存储,一般选择的数据库有两种:

  • 索引数据库,如ElasticSearch,以倒排索引的数据结构存储,需要查询时,根据索引来查询
  • 时序数据库,如OpenTSDB,以时序序列数据的方式存储,查询的时候按照时序如1min、5min等维度来查询
0.4.4.4.4. 数据展示

把处理后的数据以Dashboard的方式展示给用户,如曲线图(监控趋势变化)、饼状图(监控占比分布)、格子图(细粒度监控)等。

0.4.5. 服务追踪

在微服务架构下,由于进行了服务拆分,一次请求往往需要涉及多个服务,每个服务可能是由不同团队开发,使用了不同的编程语言,还有可能部署在不同的机器上,分布在不同的数据中心。除了监控服务调用情况,还要记录服务调用经过的每层链路,以便进行问题追踪和故障定位。

服务追踪是分布式系统中必不可少的功能,可以帮助我们查询一次用户请求在系统中具体的执行路径,以及每一条路径的上下游的详细情况,对于追查问题十分有用。

工作原理:

  1. 服务消费者发起调用前,在本地按照规则生成一个requestid,发起调用时,将requestid当作请求参数的一部分传递给服务提供者
  2. 服务提供者接收到请求后,记录下requestid,然后处理请求。(如果服务提供者继续请求其他服务,会在本地生成自己的requestid,然后把两个requestid当作请求参数继续往下传递)

通过一层层往下传递,一次请求,依赖多少服务,经过多少服务节点,通过最开始生成的requestid串联所有节点,从而达到服务追踪的目的。

0.4.5.1. 服务追踪的作用

  1. 优化系统瓶颈:通过记录调用经过的每一条链路上的耗时,能快速定位整个系统的瓶颈点
  2. 优化链路调用:通过服务追踪可以分析调用所经过的路径,然后评估是否合理,评估每个依赖是否必要,是否可以通过业务优化来减少服务依赖;(跨数据中心部署的业务出现跨数据中心的调用,网络延迟可能有30ms,这对有些业务是不可接收的)
  3. 生成网络拓扑:通过服务追踪系统中记录的链路信息,可以生成一张系统的网络调用拓扑图,它可以反映系统都依赖了哪些服务,以及服务之间的调用关系,同时在网络拓扑上把服务调用的详细信息标出来,也能起到服务监控的作用
  4. 透明传输数据:除了服务追踪,业务上经常有一种需求,期望能把一些用户数据,从调用的开始一直往下传递,以便系统中的各个服务都能获取到这个信息;(如,业务进行A/B测试,通过服务追踪系统把测试的开关逻辑一直往下传递,经过的每一层服务都能获取到这个开关值,就能统一进行A/B测试)

0.4.5.2. 服务追踪原理

Google的论文,这个是中文译文,详细的讲解了服务追踪系统的实现原理,核心理念就是调用链:通过一个全局唯一的ID将分布在各个服务节点上的同一次请求串联起来,从而还原原有的调用关系,可以追踪系统问题,分析调用数据并统计各种系统指标。

服务追踪中的一些基本概念:traceIdspanIdannonation等。

image

  • 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
名称用途
traceId串联某一次请求在系统中经过的所有路径
spanId区分系统不同服务之间调用的先后关系
annotation用于业务自定义感兴趣的数据

0.4.5.3. 服务追踪实现

服务追踪系统架构通常可以分为三层:

  • 数据采集层:负责数据埋点并上报
  • 数据处理层:负责数据的存储和计算
  • 数据展示层:负责数据的图形化展示

image

0.4.5.3.1. 数据采集层

在系统的各个不同模块中进行埋点,采集数据并上报给数据处理层进处理。数据埋点具体如下图所示:

image

以图中红框部分,A调用B的过程为例,一次RPC请求可以分为四个阶段:

  • CS(Client Send):客户端发起请求,并生成调用上下文
  • SR(Server Receive):服务端接收请求,并生成上下文
  • SS(Server Send):服务端返回请求,这个阶段会将服务端上下文数据上报,如下图,上报的数据有(traceId=123456,spanId=0.1,appKey=B,method=B.method,start=103,duratuon=38)
  • CR(Client Receive):客户端接收返回结果,这个阶段会将客户端上下文数据上报,如下图,上报的数据有(traceId=123456,spanId=0.1,appKey=A,method=B.method,start=103,duratuon=38)

image

0.4.5.3.2. 数据处理层

把数据采集层上报的数据按需计算,然后落地存储供查询使用。数据处理一般分为:

  • 实时计算:对计算效率要求高,对收集的链路数据能够在秒级别完成聚合计算,以供实时查询;一般采用实时流式计算来对链路数据进行实时聚合加工,存储一般使用OLTP数据仓库(HBase),使用traceId作为RowKey,能天然地把一整条调用链聚合在一起,提高查询效率
  • 离线计算:对计算效率要求不高,在小时级别完成链路数据的聚合计算即可,一般用作数据汇总统计;一般采用批处理来对链路数据进行离线计算,存储一般使用Hive。
0.4.5.3.3. 数据展示层

将处理后的链路信息以图形化的方式展示给用户。主要展示两种图形:

  • 调用链路图:反应服务整体情况(服务总耗时、服务调用的网络深度、每一层经过的系统以及多少次调用)和每一层的情况(每一层发生了几次调用以及每一层调用的耗时);在实际项目中,主要用来做故障定位,如一次用户调用失败,可以通过链路图查看到经过了哪些调用,在哪里失败了
  • 调用拓扑图:反应系统中包含哪些应用,它们之间的关系以及依赖调用的QPS、平均耗时情况;拓扑图是一种全局视野图,在实际项目中,主要用作全局监控,用来发现系统中异常的点,从而快速做出决策,如某个服务突然异常,在拓扑图中可以看出这个服务的调用耗时情况

0.4.6. 服务治理

服务监控发现问题,服务追踪定位问题,服务治理解决问题。

服务治理通过一些手段保证意外情况下,服务调用仍然能够正常进行。生产环境中经常遇到以下情况:

  1. 单机故障,服务治理通过一定策略,自动移除故障节点,保证单机故障不会影响业务
  2. 单IDC故障,服务治理可以通过自动切换故障IDC的流量到其他正常IDC,可以避免因为单IDC故障引起大批量业务受影响
  3. 依赖服务不可用,服务治理通过熔断,在依赖服务异常的情况下,一段时间内停止发起调用而直接返回,保证服务消费者不被拖垮服务提供者减少压力,从而能够尽快恢复
  4. 服务容量问题,增加自动扩缩容

将单体应用改造为微服务架构后,服务调用由本地调用变成远程调用,服务消费者A需要通过注册中心查询服务提供者B的地址,然后发起调用,这个过程中可能遇到以下情况:

  1. 注册中心宕机
  2. 服务提供者B有节点宕机
  3. 服务消费者A和注册中心之间网络不通
  4. 服务提供者B和注册中心之间网络不通
  5. 服务消费者A和服务提供者B之间网络不通
  6. 服务提供者B有些节点性能变慢
  7. 服务提供者B短时间内出现问题

一次服务调用,在三者之间都有可能会有问题,需要服务治理才能确保调用成功。

0.4.6.1. 常用服务治理手段

  • 节点管理:从服务节点的健康状态角度来考虑
  • 负载均衡和服务路由:从服务节点访问优先级角度来考虑
  • 服务容错:从调用的健康状态角度来考虑

在实际的微服务架构实践中,上面这些服务治理手段一般都会在服务框架中默认集成了,不需要业务代码去实现。如果想自己实现服务治理的手段,可以参考这些开源服务框架(DubboMotan)的实现。

0.4.6.1.1. 节点管理

服务调度失败一般由两类原因引起:服务提供者自身出现问题(服务器宕机,进程意外退出)或者服务提供者、注册中心、服务消费者任意两者之间的网络出现问题。有两种节点管理手段:

  1. 注册中心主动摘除机制:服务提供者定时向注册中心汇报心跳,根据上一次汇报心跳距离现在的时长与超时时间对比来把节点从服务列表中摘除,并把最近可用的服务节点列表推送给服务消费者
  2. 服务消费者摘除机制:注册中心主动摘除可以解决服务提供者异常的问题,如果注册中心和服务提供者之间的网络出现问题,则注册中心可能会把所有提供者节点都移除,但服务提供者本身是正常的。所以将存活探测机制用在服务消费者一端更合理,如果服务消费者调用服务提供者节点失败,就将这个节点从内存中报错的可用服务提供者节点列表中移除
0.4.6.1.2. 负载均衡

通常,服务提供者是以集群方式存在,同一服务提供者的不同实例可能处在不同配置的服务器上,对于服务消费者而言,在从服务器列表中选取可用节点时,让配置较高的服务器承担多一些流量,充分利用服务器性能。主要使用的负载均衡算法有:

  1. 随机算法:从可用服务列表中随机选取一个,一般情况下,随机算法是均匀的
  2. 轮询算法:按照固定权重,对可用服务节点进行论询,可根据服务器的配置修改不同的权重,从而充分发挥性能优势,提高整体调用的平均性能
  3. 最少活跃调用算法:在服务消费者的内存里动态维护着同每一个服务节点之间的连接数,当调用某个服务节点时,就给这个节点之间的连接数加1,调用返回则连接数减1,每次在选择服务节点时,根据内存里维护的连接数倒序排列,选择连接数最小的节点发起调用,理论上性能最优
  4. 一致性Hash算法:相同参数的请求总是发送到同一服务节点,当某一个服务节点出现故障时,原本发往该节点的请求,基于虚拟节点机制,平摊到其他节点上,不会引起剧烈变动

这几种算法实现难度逐步提高,选择哪种算法根据实际场景而定:

  • 后端服务器的配置没有差异,同等调用量下性能也没有差异,随机选择或者轮询比较好
  • 后端服务节点存在比较明显的配置和性能差异,最少活跃调用算法比较好
0.4.6.1.3. 服务路由

对于服务消费者,在内存中的可用服务节点列表中选择哪个节点不仅由负载均衡算法决定,还由路由规则(通过一定的规则如条件表达式或者正则表达式来限定服务节点的选择范围)决定。

制定路由规则的原因:

  • 业务存在灰度发布的情况:服务提供者做了功能变更,但希望先只让部分人群使用,然后根据这部分人群的使用反馈,再来决定是否做全量发布。这个时候,就可以通过类似按尾号进行灰度的规则限定只有一定比例的人群才会访问新发布的服务节点
  • 多机房就近访问需求:为了业务的高可用性,会将业务部署在不止一个 IDC 中。这就存在一个问题,不同 IDC 之间的访问由于要跨 IDC,通过专线访问,尤其是 IDC 相距比较远时专线延迟一般在 30ms 左右,这对于某些延时敏感性的业务是不可接受的,所以就要一次服务调用尽量选择同一个 IDC 内部的节点,从而减少网络耗时开销,提高性能。这时一般可以通过 IP 段规则来控制访问,在选择服务节点时,优先选择同一 IP 段的节点。

配置路由规则的方式:

  • 静态配置:在服务消费者本地存放服务调用的路由规则,在服务调用期间,路由规则不会发生改变,要想改变就需要修改服务消费者本地配置,上线后才能生效
  • 动态配置:路由规则是存在注册中心的,服务消费者定期去请求注册中心来保持同步,要想改变服务消费者的路由配置,可以通过修改注册中心的配置,服务消费者在下一个同步周期之后,就会请求注册中心来更新配置,从而实现动态更新
0.4.6.1.4. 服务容错

服务调用并不总是一定成功的,对于服务调用失败的情况,需要有手段自动恢复,来保证调用成功。常用的手段主要有以下几种。

手段名称描述场景
FailOver(幂等调用)失败自动切换服务消费者发现调用失败或者超时后,自动从可用的服务节点列表中选择下一个节点重新发起调用,也可以设置重试的次数这种策略要求服务调用的操作必须是幂等的,也就是说无论调用多少次,只要是同一个调用,返回的结果都是相同的,一般适合服务调用是读请求的场景
FailBack(非幂等调用)失败通知服务消费者调用失败或者超时后,不再重试,而是根据失败的详细信息,来决定后续的执行策略对于非幂等的调用场景,如果调用失败后,不能简单地重试,而是应该查询服务端的状态,看调用到底是否实际生效,如果已经生效了就不能再重试了;如果没有生效可以再发起一次调用
FailCache(幂等调用)失败缓存服务消费者调用失败或者超时后,不立即发起重试,而是隔一段时间后再次尝试发起调用后端服务可能一段时间内都有问题,如果立即发起重试,可能会加剧问题,反而不利于后端服务的恢复。隔一段时间待后端节点恢复后,再次发起调用效果会更好
FailFast(非幂等调用)快速失败服务消费者调用一次失败后,不再重试实际在业务执行时,一般非核心业务的调用,会采用快速失败策略,调用失败后一般就记录下失败日志就返回了

0.5. 总结

使用开源组件或者自研,必须吃透每个组件的工作原理并能在此基础上进行二次开发。

上次修改: 14 April 2020