0.1. 单体应用 0.2. 服务化与微服务 0.3. 服务化拆分方式 0.3.1. 服务化拆分的前置条件 0.4. 微服务架构 0.4.1. 服务描述 0.4.1.1. RESTful API 0.4.1.2. XML配置 0.4.1.3. IDL文件 0.4.1.4. 小结 0.4.2. 注册中心 0.4.2.1. 注册中心实现方式 0.4.2.1.1. 问题1:注册中心访API 0.4.2.1.2. 问题2:集群部署 0.4.2.1.3. 问题3:目录存储 0.4.2.1.4. 问题4:服务健康状态检测 0.4.2.1.5. 问题5:服务状态变更通知 0.4.2.1.6. 问题6:白名单机制 0.4.3. 服务框架 0.4.3.1. 问题1:建立网络连接 0.4.3.2. 问题2:服务端处理请求 0.4.3.3. 问题3:数据传输协议 0.4.3.4. 问题4:数据序列化和反序列化 0.4.3.5. 综上 0.4.4. 服务监控 0.4.4.1. 监控对象 0.4.4.2. 监控指标 0.4.4.3. 监控维度 0.4.4.4. 监控系统原理 0.4.4.4.1. 数据采集 0.4.4.4.2. 数据传输 0.4.4.4.3. 数据处理 0.4.4.4.4. 数据展示 0.4.5. 服务追踪 0.4.5.1. 服务追踪的作用 0.4.5.2. 服务追踪原理 0.4.5.3. 服务追踪实现 0.4.5.3.1. 数据采集层 0.4.5.3.2. 数据处理层 0.4.5.3.3. 数据展示层 0.4.6. 服务治理 0.4.6.1. 常用服务治理手段 0.4.6.1.1. 节点管理 0.4.6.1.2. 负载均衡 0.4.6.1.3. 服务路由 0.4.6.1.4. 服务容错 0.5. 总结 微服务是由单一应用程序构成的小服务,拥有自己的进程与轻量化处理,服务依业务功能设计,以全自动的方式部署,与其他服务使用HTTP API通讯。同时,服务会使用最小规模的集中管理技术(如Docker),服务可以使用不用的编程语言和数据库。 0.1. 单体应用 单体应用技术栈: LAMP(Linux、Apache、MySQL、PHP) MVC(Spring、iBatis/Hibernate、Tomcat) 优点:学习成本低、开发上手快、测试部署运维方便 痛点: 部署效率低下 团队协作开发成本高 系统高可用性差 线上发布变慢(代码膨胀、服务启动时间变长) 0.2. 服务化与微服务 服务化:把传统单机应用中通过JAR包依赖产生的本地方法调用,改造成通过RPC接口产生的远程方法调用。对于通用的业务逻辑,抽象并独立成为专门的模块,对于代码复用和业务理解都有好处。 通过服务化,解决单体应用膨胀,团队开发耦合度高,协作效率低下的问题。 微服务与服务化相比: 服务拆分粒度更细:微服务是更细维度的服务化,只要该模块依赖的资源与其他模块没有关系,就可以拆分为一个微服务 服务独立部署:每个微服务都严格遵循独立打包部署的准则 服务独立维护 服务治理能力要求高:服务数量变多,需要有统一的服务治理平台,对各个服务进行管理 0.3. 服务化拆分方式 纵向拆分,从业务维度进拆分: 将不同的功能模块服务化,独立部署和运维,按照业务的关联程度来决定,关联紧密的服务拆分为一个微服务。 横向拆分,从公共且独立的功能维度拆分,按照是否有公共的被多个其他服务调用,且依赖的资源独立不与其他业务耦合。 0.3.1. 服务化拆分的前置条件 条件 单体应用 微服务 服务如何定义 以类库的方式提供各个模块的功能 以接口的形式向外传达信息,服务之间的调用都是通过接口描述来约定,约定内容包括接口名、接口参数和接口返回值 服务如何发布和订阅 接口之间的调用属于进程内调用 使用注册中心,服务提供者暴露位置,服务调用者查询服务地址 服务如何监控 调用量、平均耗时、99.9%请求性能在多少毫秒以内 全链路监控:业务埋点、数据收集、数据处理、数据展示 服务如何治理 服务数量多,依赖关系复杂,服务熔断 故障如何定位 一次调用依赖多个服务,每个服务部署在不同节点,将一次用户请求进行标记,并在多个依赖的服务系统中继续传递,以便串联所有路径,从而进行故障定位 0.4. 微服务架构 微服务架构模块图: 一次正常的服务调用流程: 服务提供者按照一定格式的服务描述,向注册中心注册服务,声明自己提供的服务以及服务的地址,完成服务发布。 服务消费者请求注册中心,查询所需调用的服务地址,然后以约定的通信协议向服务提供者发起请求,得到请求结果之后,再按照约定的协议解析结果。 在服务请求的过程中,服务的请求耗时,调用量,成功率等指标会被记录下来用作监控,调用经过的链路信息会被记录下来,用作故障定位和问题追踪。在这期间,如果调用失败,可以通过重试等服务治理手段来保证成功率。 微服务架构下,服务调用主要依赖的基本组件: 服务描述 注册中心 服务框架 服务监控 服务追踪 服务治理 0.4.1. 服务描述 服务如何对外描述,具体要解决如下几个问题: 服务名称? 调用服务需要提供的信息? 调用服务返回的结果格式? 如何解析结果? 常用的服务描述方式: RESTful API:常用于HTTP/HTTPS协议的服务描述,常用Wiki或Swagger进行管理 XML配置:常用于RPC协议的服务描述,通过*.xml配置文件来定义接口名、参数以及返回值类型 IDL文件:常用于Thrift和gRPC这类跨语言服务调用框架中,如gRPC通过Protobuf文件定义服务的接口名、参数以及返回值的数据结构 0.4.1.1. RESTful API 服务消费者通过HTTP协议调用服务(HTTP协议本身是一个公开的协议,对于服务消费者来说几乎没有学习成本),比较适合用作跨业务平台(业务部门内部,其他业务部门,外网)之间的服务协议。 0.4.1.2. XML配置 服务发布与引用的三个步骤: 服务提供者定义并实现接口 服务提供者进程启动时,通过加载server.xml配置文件将接口暴露出来 服务消费者进程启动时,通过加载client.xml配置文件来引入要调用的接口 通过在服务提供者和服务消费者之间维护一份对等的XML配置文件,来保证服务消费者按照服务提供者的约定来进行服务调用。如果变更接口,需要同时更新server.xml和client.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) 服务提供者将自己提供的服务以及地址登记到注册中心,服务消费者则从注册中心查询所需要调用的服务的地址,然后发起请求。 工作流程: 服务提供者在启动时,根据服务发布文件(server.xml)中配置的发布信息向注册中心注册自己的服务,并定期发送心跳汇报存活状态 服务消费者在启动时,根据消费者引用文件(client.xml)中配置的服务信息向注册中心订阅自己所需要的服务,把注册中心返回的服务列表缓存在本地内存中,并与服务提供者建立连接 注册中心返回服务提供者地址列表给服务消费者 当服务提供者发生变化(如,节点增删),注册中心将变更通知给服务消费者,服务消费者感知后会刷新本地内存中缓存的服务节点列表 服务调用者从本地缓存的服务节点列表中,基于负载均衡算法选择一个服务节点发起调用 0.4.2.1. 注册中心实现方式 涉及如下几个问题: 提供哪些接口? 如何部署? 如何存储服务信息? 如何监控服务提供者节点的存活? 如果服务提供者节点有变化如何通知服务消费者? 如何控制注册中心的访问权限? 0.4.2.1.1. 问题1:注册中心访API 注册中心必须提供以下最基本的API: 服务注册接口:服务提供者通过调用注册接口来完成服务注册 服务反注册接口:服务提供者通过调用反注册接口来完成服务注销 心跳汇报接口:服务提供者通过心跳汇报接口完成节点存活状态上报 服务订阅接口:服务消费者通过调用服务订阅接口完成服务订阅,获取可用的服务提供者节点列表 服务变更查询接口:服务消费者通过调用服务变更查询接口,获取最新的可用服务节点列表 为了便于管理,还需提供后台管理API: 服务查询接口:查询注册中心当前注册了哪些服务信息 服务修改接口:修改注册中心中某一服务的信息 0.4.2.1.2. 问题2:集群部署 注册中心作为服务提供者和消费者之间的沟通桥梁,采用集群部署来保证高可用性,并通过分布式一致性协议来保证集群中不同节点之间的数据一致性。 以Zookeeper为例(保证高可用性和数据一致性): 每个Server在内存中存储一份数据,Client的读取可以请求任意一个Server 启动时,基于Paxos协议从所有实例中选举一个Leader Leader基于ZAB协议处理数据更新等操作 一个更新操作成功,当且仅当大多数Server在内存中修改成功 0.4.2.1.3. 问题3:目录存储 以Zookeeper为例,注册中心存储服务信息一般采用层次化的目录结构: 每个目录称为一个znode,并且有一个唯一的路径标识 znode可以包含数据和子znode znode中的数据可以有多个版本,查询时需要带上版本信息 0.4.2.1.4. 问题4:服务健康状态检测 对服务提供者节点进行健康检测,才能保证注册中心里保存的服务节点都是可用的。 以Zookeeper为例,基于客户端(集群中的服务提供者)和服务端(注册中心)的长连接和会话超时控制机制,来实现服务健康状态检测。 客户端与服务端建立连接后,会话随之建立,并生成一个全局唯一的Session ID 客户端与服务端维持一个长连接,在SESSION_TIMEOUT周期内,服务端定时向客户端发送心跳信息(ping),服务器重置下一次SESSION_TIMEOUT时间 超过SESSION_TIMEOUT后,服务端都没有收到客户端的心跳消息,则认为Session结束,认为这个服务节点不可用,从注册中心删除 0.4.2.1.5. 问题5:服务状态变更通知 一旦注册中心探测到有服务提供者节点新增或删除,就必须立刻通知所有订阅该服务的消费者,消费者刷新本地缓存的服务节点信息,确保服务调用不会请求不可用的服务提供者。 以Zookeeper为例,基于Watcher机制,来实现服务状态变更通知: 消费者在调用Zookeeper的getData方法订阅服务时,还可以通过监听器Watcher的process方法获取服务的变更 然后调用getData方法来获取变更后的数据,刷新本地缓存的服务节点信息 0.4.2.1.6. 问题6:白名单机制 注册中心提供白名单机制,只有添加到白名单中的服务提供者才能够调用注册中的注册接口,这样可以避免测试环境中的节点意外跑到线上环境中。 0.4.3. 服务框架 通过注册中心,服务消费者获得服务提供者的地址,可以发起调用,但是在调用之前需要解决几个问题: 服务通信协议:四层TCP/UDP协议/七层HTTP协议/其他协议 数据传输方式:同步/异步/单连接传输/多路复用 数据压缩格式:通常数据传输都会压缩来减少网络传输的数据量,从而降低带宽和网络传输时间,JSON序列化/Java对象序列化/Protobuf序列化 相较于单体应用的本地方法调用,微服务进行的是远程方法调用(RPC)。在RPC中,把服务消费者称为客户端,把服务提供者称为服务端,两者位于网络上的不同位置,完成一次RPC需要建立连接,然后按照通信协议进行通信,正常通信后,服务端处理请求,客户端接收结果,为了减少传输的数据,对数据进行序列化。 完成RPC调用需要解决四个问题: 客户端和服务端如何建立网络连接? 服务端如何处理请求? 数据传输采用什么协议? 数据该如何序列化和反序列化? 0.4.3.1. 问题1:建立网络连接 基于TCP建立网络连接的两种最常用途径: HTTP通信:基于应用程序HTTP协议(HTTP协议基于传输层TCP协议),一次HTTP通信就是发起一次HTTP调用,一次HTTP调用就会建立一个TCP连接,经过三次握手过程来建立连接,完成请求后,再经过四次挥手过程断开连接。 Socket通信:基于TCP/IP协议的封装,建立一次Socket连接至少需要一对套接字,其中一个运行在客户端(ClientSocket),一个运行在服务端(ServerSocket)。Socket通信的过程分为四个步骤:服务器监听、客户端请求、连接确认、数据传输。 服务器监听: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中任一方式实现请求处理,最后在解决消费者和提供者之间的网络可靠性问题。可使用成熟开源的方案Netty,MINA。 0.4.3.3. 问题3:数据传输协议 最常用的HTTP协议,或者Dubbo协议,消费者和提供者之间基于协议的契约达成共识,消费者按照契约将数据编码后通过网络传输给提供者,提供者从网络上接收到数据后,按照契约对数据进行解码,然后处理请求,在将处理后的结果编码后通过网络传输给消费者,消费者对返回结果进行解码,得到提供者处理后的返回值。 协议契约包括两部分: 消息头:存放的是协议的公共字段以及用户扩展字段 消息体:存放的是传输数据的具体内容 以HTTP协议响应数据为例: 消息头中存放协议公共字段:Server代表服务端服务器类型、Content-Length代码返回数据的长度、Content-Type代表返回数据的类型 消息体中存放具体的返回结果,如一段HTML网页代码 0.4.3.4. 问题4:数据序列化和反序列化 数据在网络中传输前,在发送方对数据进行编码(序列化),经过网络传输到达接收方再对数据进行解码(反序列化)。 网络传输的耗时,取决于网络带宽和数据传输量。加快网络传输的方式,提高带宽或者减少数据传输量。对数据编码的目的是减小数据传输量。 序列化分为两类:文本类JSON/XML,二进制类PB/Thrift,决定序列化方式的三个因素: 支持数据结构的丰富度:数据结构种类支持的越多越好,对使用者来说编程更友好 跨语言支持:支持跨语言使用的场景更丰富 性能:序列化后的压缩比和序列化的速度 种类 压缩比 速度 适用场景 PB 高 快 对性能和存储空间要求高的系统 JSON 低 慢 可读性好,适合对外部提供服务 0.4.3.5. 综上 通信框架提供基础通信能力 通信协议描述通信契约 序列化和反序列化用于数据的编码和解码 一个通信框架可以适配多种通信协议,可以采用多种序列化和反序列化的格式。如Dobbo框架支持Dobbo协议、RMI协议、HTTP协议、支持JSON、Hession 2.0 、Java等序列化和反序列化格式。 0.4.4. 服务监控 服务监控在微服务改造中的重要性不言而喻,没有强大的监控能力,改造微服务架构后,就无法掌控各个不同服务的情况,在遇到调用失败时,如果不能快速发现系统的问题,对于业务来说就是异常灾难。 与单体应用相比,在微服务架构下,一次用户调用会因为服务化拆分后,变成多个不同服务之间的相互调用,一旦服务消费者与服务提供者之间能够发起服务调用,就需要对调用情况进行监控,以了解服务是否正常,通常,服务监控包括三个流程: 指标收集:每一次服务调用的请求耗时以及成功与否收集起来,上传到集中的数据处理中心 数据处理:根据收集的指标,计算每秒服务请求量,平均耗时以及成功率等 数据展示:处理后的数据以友好的方式展示,通常展示在Dashboard面板,并且每隔10s间隔自动刷新,用作服务监控和报警 0.4.4.1. 监控对象 对于微服务来说,监控对象可以分为四个层次,由上而下: 用户端监控:通常是指业务直接对用户提供的功能的监控。 接口监控:通常是指业务提供的功能所依赖的具体RPC接口的监控。 资源监控:通常是指某个接口依赖的资源的监控。 基础监控:通常是指对服务器本身的健康状况的监控,包括CPU利用率,内存使用量,I/O读写、网卡带宽等。 0.4.4.2. 监控指标 以下业务指标需要重点监控: 请求量:请求量监控分为两个维度,实时请求量和统计请求量。实时请求量(QPS,Queries Per Second)即每秒查询次数来衡量,它反映了服务调用的实时变化情况。统计请求量(PV,Page View)即一段时间内用户的访问量来衡量,比如一天的PV代表了服务一天的请求量,通常用来统计报表。 响应时间:用一段时间内所有调用的平均耗时来反应请求的响应时间。它只代表了请求的平均快慢情况。更关注慢请求的数量时,可以将响应时间划分为多个区间:0~10ms、10~50ms、50~100ms、100~500ms、500ms以上,最后一个区间内的请求数量就代表了慢请求量,正常情况下,这个区间内的请求应该为0。也可以从P90、P95、P99、P999角度来监控请求的响应时间,P99=500ms表示99%的请求的响应时间在500ms以内,它代表了请求的服务质量,即SLA。 错误率:用一段时间内调用失败的次数占调用总次数的比率来衡量,比如对于接口的错误率一般用返回错误码为503的比率表示。 0.4.4.3. 监控维度 全局维度:从整体角度监控对象的请求量,平均耗时以及错误率,全局维度的监控一般是为了对监控对象的调用情况有整体的了解。 分机房维度:为了业务的高可用性,服务通常部署在不同的机房,不同的地域,对同一个监控对象的各种指标可能会相差很大。 单机维度:同一机房,不同的服务器上,同一个监控对象的各种指标也会有很大差异。 时间维度:同一个监控对象,在每天的固定一个时刻各种指标通常也会不一样,这种差异可能有业务变更导致,或者活动运营导致。为了了解监控对象各种指标变化,通常需要与一天前,一周前,一个月前,甚至三个月前对比。 核心维度:业务一般会根据重要性程度对监控对象进行分级(核心/非核心业务),这两者在部署上必须隔离,分开监控,才能对核心业务做重点保障。 对于一个微服务来说,必须明确要监控哪些对象,哪些指标,并且从不同的维度进行监控,才能掌握微服务的调用情况。 0.4.4.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. 服务追踪 在微服务架构下,由于进行了服务拆分,一次请求往往需要涉及多个服务,每个服务可能是由不同团队开发,使用了不同的编程语言,还有可能部署在不同的机器上,分布在不同的数据中心。除了监控服务调用情况,还要记录服务调用经过的每层链路,以便进行问题追踪和故障定位。 服务追踪是分布式系统中必不可少的功能,可以帮助我们查询一次用户请求在系统中具体的执行路径,以及每一条路径的上下游的详细情况,对于追查问题十分有用。 工作原理: 服务消费者发起调用前,在本地按照规则生成一个requestid,发起调用时,将requestid当作请求参数的一部分传递给服务提供者 服务提供者接收到请求后,记录下requestid,然后处理请求。(如果服务提供者继续请求其他服务,会在本地生成自己的requestid,然后把两个requestid当作请求参数继续往下传递) 通过一层层往下传递,一次请求,依赖多少服务,经过多少服务节点,通过最开始生成的requestid串联所有节点,从而达到服务追踪的目的。 0.4.5.1. 服务追踪的作用 优化系统瓶颈:通过记录调用经过的每一条链路上的耗时,能快速定位整个系统的瓶颈点 优化链路调用:通过服务追踪可以分析调用所经过的路径,然后评估是否合理,评估每个依赖是否必要,是否可以通过业务优化来减少服务依赖;(跨数据中心部署的业务出现跨数据中心的调用,网络延迟可能有30ms,这对有些业务是不可接收的) 生成网络拓扑:通过服务追踪系统中记录的链路信息,可以生成一张系统的网络调用拓扑图,它可以反映系统都依赖了哪些服务,以及服务之间的调用关系,同时在网络拓扑上把服务调用的详细信息标出来,也能起到服务监控的作用 透明传输数据:除了服务追踪,业务上经常有一种需求,期望能把一些用户数据,从调用的开始一直往下传递,以便系统中的各个服务都能获取到这个信息;(如,业务进行A/B测试,通过服务追踪系统把测试的开关逻辑一直往下传递,经过的每一层服务都能获取到这个开关值,就能统一进行A/B测试) 0.4.5.2. 服务追踪原理 Google的论文,这个是中文译文,详细的讲解了服务追踪系统的实现原理,核心理念就是调用链:通过一个全局唯一的ID将分布在各个服务节点上的同一次请求串联起来,从而还原原有的调用关系,可以追踪系统问题,分析调用数据并统计各种系统指标。 服务追踪中的一些基本概念:traceId、spanId、annonation等。 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. 服务追踪实现 服务追踪系统架构通常可以分为三层: 数据采集层:负责数据埋点并上报 数据处理层:负责数据的存储和计算 数据展示层:负责数据的图形化展示 0.4.5.3.1. 数据采集层 在系统的各个不同模块中进行埋点,采集数据并上报给数据处理层进处理。数据埋点具体如下图所示: 以图中红框部分,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) 0.4.5.3.2. 数据处理层 把数据采集层上报的数据按需计算,然后落地存储供查询使用。数据处理一般分为: 实时计算:对计算效率要求高,对收集的链路数据能够在秒级别完成聚合计算,以供实时查询;一般采用实时流式计算来对链路数据进行实时聚合加工,存储一般使用OLTP数据仓库(HBase),使用traceId作为RowKey,能天然地把一整条调用链聚合在一起,提高查询效率 离线计算:对计算效率要求不高,在小时级别完成链路数据的聚合计算即可,一般用作数据汇总统计;一般采用批处理来对链路数据进行离线计算,存储一般使用Hive。 0.4.5.3.3. 数据展示层 将处理后的链路信息以图形化的方式展示给用户。主要展示两种图形: 调用链路图:反应服务整体情况(服务总耗时、服务调用的网络深度、每一层经过的系统以及多少次调用)和每一层的情况(每一层发生了几次调用以及每一层调用的耗时);在实际项目中,主要用来做故障定位,如一次用户调用失败,可以通过链路图查看到经过了哪些调用,在哪里失败了 调用拓扑图:反应系统中包含哪些应用,它们之间的关系以及依赖调用的QPS、平均耗时情况;拓扑图是一种全局视野图,在实际项目中,主要用作全局监控,用来发现系统中异常的点,从而快速做出决策,如某个服务突然异常,在拓扑图中可以看出这个服务的调用耗时情况 0.4.6. 服务治理 服务监控发现问题,服务追踪定位问题,服务治理解决问题。 服务治理通过一些手段保证意外情况下,服务调用仍然能够正常进行。生产环境中经常遇到以下情况: 单机故障,服务治理通过一定策略,自动移除故障节点,保证单机故障不会影响业务 单IDC故障,服务治理可以通过自动切换故障IDC的流量到其他正常IDC,可以避免因为单IDC故障引起大批量业务受影响 依赖服务不可用,服务治理通过熔断,在依赖服务异常的情况下,一段时间内停止发起调用而直接返回,保证服务消费者不被拖垮,服务提供者减少压力,从而能够尽快恢复 服务容量问题,增加自动扩缩容 将单体应用改造为微服务架构后,服务调用由本地调用变成远程调用,服务消费者A需要通过注册中心查询服务提供者B的地址,然后发起调用,这个过程中可能遇到以下情况: 注册中心宕机 服务提供者B有节点宕机 服务消费者A和注册中心之间网络不通 服务提供者B和注册中心之间网络不通 服务消费者A和服务提供者B之间网络不通 服务提供者B有些节点性能变慢 服务提供者B短时间内出现问题 一次服务调用,在三者之间都有可能会有问题,需要服务治理才能确保调用成功。 0.4.6.1. 常用服务治理手段 节点管理:从服务节点的健康状态角度来考虑 负载均衡和服务路由:从服务节点访问优先级角度来考虑 服务容错:从调用的健康状态角度来考虑 在实际的微服务架构实践中,上面这些服务治理手段一般都会在服务框架中默认集成了,不需要业务代码去实现。如果想自己实现服务治理的手段,可以参考这些开源服务框架(Dubbo、Motan)的实现。 0.4.6.1.1. 节点管理 服务调度失败一般由两类原因引起:服务提供者自身出现问题(服务器宕机,进程意外退出)或者服务提供者、注册中心、服务消费者任意两者之间的网络出现问题。有两种节点管理手段: 注册中心主动摘除机制:服务提供者定时向注册中心汇报心跳,根据上一次汇报心跳距离现在的时长与超时时间对比来把节点从服务列表中摘除,并把最近可用的服务节点列表推送给服务消费者 服务消费者摘除机制:注册中心主动摘除可以解决服务提供者异常的问题,如果注册中心和服务提供者之间的网络出现问题,则注册中心可能会把所有提供者节点都移除,但服务提供者本身是正常的。所以将存活探测机制用在服务消费者一端更合理,如果服务消费者调用服务提供者节点失败,就将这个节点从内存中报错的可用服务提供者节点列表中移除 0.4.6.1.2. 负载均衡 通常,服务提供者是以集群方式存在,同一服务提供者的不同实例可能处在不同配置的服务器上,对于服务消费者而言,在从服务器列表中选取可用节点时,让配置较高的服务器承担多一些流量,充分利用服务器性能。主要使用的负载均衡算法有: 随机算法:从可用服务列表中随机选取一个,一般情况下,随机算法是均匀的 轮询算法:按照固定权重,对可用服务节点进行论询,可根据服务器的配置修改不同的权重,从而充分发挥性能优势,提高整体调用的平均性能 最少活跃调用算法:在服务消费者的内存里动态维护着同每一个服务节点之间的连接数,当调用某个服务节点时,就给这个节点之间的连接数加1,调用返回则连接数减1,每次在选择服务节点时,根据内存里维护的连接数倒序排列,选择连接数最小的节点发起调用,理论上性能最优 一致性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. 总结 使用开源组件或者自研,必须吃透每个组件的工作原理并能在此基础上进行二次开发。
0.1. 服务发布与引用 0.1.1. 一号坑 0.1.2. 二号坑 0.1.3. 三号坑 0.2. 注册中心 0.2.1. 存储的服务信息 0.2.2. 注册中心工作流程 0.2.2.1. 注册节点 0.2.2.2. 反注册 0.2.2.3. 查询节点信息 0.2.2.4. 订阅服务变更 0.2.3. 注册中心的坑 0.2.3.1. 多注册中心 0.2.3.2. 并行订阅服务 0.2.3.3. 批量反注册服务 0.2.3.4. 服务变更信息增量更新 0.2.4. 注册中心选型 0.2.4.1. 应用内 0.2.4.2. 应用外 0.2.4.3. 选型考虑的问题 0.2.4.3.1. 高可用 0.2.4.3.2. 数据一致性 0.2.5. 总结 0.3. RPC框架 0.3.1. 限定语言平台的开源RPC框架 0.3.1.1. Dubbo 0.3.1.2. Motan 0.3.1.3. Tars 0.3.1.4. Spring Cloud 0.3.1.5. 对比选型 0.3.2. 跨语言平台的开源 RPC 框架 0.3.2.1. gRPC 0.3.2.2. Thrift 0.3.2.3. 跨语言框架对比选型 0.4. 监控系统 0.4.1. ELK 0.4.2. Graphite 0.4.3. TICK 0.4.4. Prometheus 0.4.5. 选型对比 0.4.5.1. 数据收集 0.4.5.2. 数据传输 0.4.5.3. 数据处理 0.4.5.4. 数据展示 0.4.5.5. 总体 0.5. 服务追踪系统 0.5.1. Zipkin 0.5.2. Pinpoint 0.5.2.1. 工作原理 0.5.3. 选型 0.5.3.1. 埋点探针支持平台的广泛性 0.5.3.2. 系统集成难易程度 0.5.3.3. 调用链路数据的精确度 0.6. 服务治理 0.6.1. 服务节点探活 0.6.1.1. 心跳开关保护机制 0.6.1.2. 服务节点摘除保护机制 0.6.1.3. 静态注册中心 0.6.2. 负载均衡算法 0.6.2.1. 适用场景 0.6.2.2. 自适应最优选择算法 0.6.3. 服务路由 0.6.3.1. 应用场景 0.6.3.2. 服务路由的规则 0.6.3.2.1. 条件路由 0.6.3.2.2. 脚本路由 0.6.3.3. 服务路由的获取方式 0.7. 服务端故障处理 0.7.1. 集群故障 0.7.1.1. 限流 0.7.1.2. 降级 0.7.2. 单 IDC 故障 0.7.3. 单机故障 0.8. 服务调用失败处理手段 0.8.1. 超时 0.8.2. 重试 0.8.3. 双发 0.8.4. 熔断 0.8.4.1. 熔断的工作原理 0.9. 管理服务配置 0.9.1. 本地配置 0.9.2. 配置中心 0.9.2.1. 典型场景 0.9.3. 技术选型 0.10. 微服务治理平台 0.10.1. 服务管理 0.10.2. 服务治理 0.10.3. 服务监控 0.10.4. 问题定位 0.10.5. 日志查询 0.10.6. 服务运维 0.10.7. 搭建微服务治理平台 0.10.8. 总结 0.1. 服务发布与引用 以XML配置方式发布和引用服务。 0.1.1. 一号坑 一个服务包含了多个接口,可能有上行接口也可能有下行接口,每个接口都有超时控制以及是否重试等配置,如果有多个服务消费者引用这个服务,是不是每个服务消费者都必须在服务引用配置文件中定义? 解决方案:服务发布预定义配置,即使服务消费者没有在服务引用配置文件中定义,也能继承服务提供者的定义。 0.1.2. 二号坑 一个服务提供者发布的服务有上百个方法,并且每个方法都有各自的超时时间、重试次数等信息。服务消费者引用服务时,完全继承了服务发布预定义的各项配置。这种情况下,服务提供者所发布服务的详细配置信息都需要存储在注册中心中,这样服务消费者才能在实际引用时从服务发布预定义配置中继承各种配置。 当服务提供者发生节点变更,尤其是在网络频繁抖动的情况下,所有的服务消费者都会从注册中心拉取最新的服务节点信息,就包括了服务发布配置中预定的各项接口信息,这个信息不加限制的话可能达到 1M 以上,如果同时有上百个服务消费者从注册中心拉取服务节点信息,在注册中心机器部署为百兆带宽的情况下,很有可能会导致网络带宽打满的情况发生。 解决方案:服务引用定义配置。 0.1.3. 三号坑 服务配置升级的过程。由于引用服务的服务消费者众多,并且涉及多个部门,升级步骤就显得异常重要,通常可以按照下面步骤操作。 各个服务消费者在服务引用配置文件中添加服务详细信息。 服务提供者升级两台服务器,在服务发布配置文件中删除服务详细信息,并观察是否所有的服务消费者引用时都包含服务详细信息。 如果都包含,说明所有服务消费者均完成升级,那么服务提供者就可以删除服务发布配置中的服务详细信息。 如果有不包含服务详细信息的服务消费者,排查出相应的业务方进行升级,直至所有业务方完成升级。 0.2. 注册中心 落地注册中心的过程中,需要解决一些列问题,包括: 如何存储服务信息 如何注册节点 如何反注册 如何查询节点信息 如何订阅服务变更 0.2.1. 存储的服务信息 服务信息,通常用JSON字符串来存储,包括: 节点信息(IP和端口) 请求失败时重试的次数 请求结果是否压缩 服务一般会分成多个不同的分组,每个分组的目的不同。一般来说有下面几种分组方式: 核心与非核心,从业务的核心程度来分 机房,从机房的维度来分 线上环境与测试环境,从业务场景维度来区分 所以注册中心存储的服务信息一般包含三部分内容:分组、服务名以及节点信息,节点信息又包括节点地址和节点其他信息。 具体存储的时候,一般是按照“服务 - 分组 - 节点信息”三层结构来存储,可以用下图来描述。Service 代表服务的具体分组,Cluster 代表服务的接口名,节点信息用 KV 存储。 0.2.2. 注册中心工作流程 注册中心具体是工作包括四个流程: 服务提供者注册流程 服务提供者反注册流程 服务消费者查询流程 服务消费者订阅变更流程 0.2.2.1. 注册节点 首先查看要注册的节点是否在白名单内?如果不在就抛出异常,在的话继续下一步 其次要查看注册的 Cluster(服务的接口名)是否存在?如果不存在就抛出异常,存在的话继续下一步 然后要检查 Service(服务的分组)是否存在?如果不存在则抛出异常,存在的话继续下一步 最后将节点信息添加到对应的 Service 和 Cluster 下面的存储中 0.2.2.2. 反注册 查看 Service(服务的分组)是否存在,不存在就抛出异常,存在就继续下一步 查看 Cluster(服务的接口名)是否存在,不存在就抛出异常,存在就继续下一步 删除存储中 Service 和 Cluster 下对应的节点信息 更新 Cluster 的 sign 值 0.2.2.3. 查询节点信息 首先从 localcache(本机内存)中查找,如果没有就继续下一步。 接着从 snapshot(本地快照)中查找,如果没有就继续下一步。 这里为什么服务消费者要把服务信息存在本机内存呢? 主要是因为服务节点信息并不总是时刻变化的,并不需要每一次服务调用都要调用注册中心获取最新的节点信息,只需要在本机内存中保留最新的服务提供者的节点列表就可以。 这里为什么服务消费者要在本地磁盘存储一份服务提供者的节点信息的快照呢? 这是因为服务消费者同注册中心之间的网络不一定总是可靠的,服务消费者重启时,本机内存中还不存在服务提供者的节点信息,如果此时调用注册中心失败,那么服务消费者就拿不到服务节点信息了,也就没法调用了。本地快照就是为了防止这种情况的发生,即使服务消费者重启后请求注册中心失败,依然可以读取本地快照,获取到服务节点信息。 0.2.2.4. 订阅服务变更 服务消费者从注册中心获取了服务的信息后,就订阅了服务的变化,会在本地保留 Cluster 的 sign 值 服务消费者每隔一段时间,调用 getSign() 函数,从注册中心获取服务端该 Cluster 的 sign 值,并与本地保留的 sign 值做对比,如果不一致,就从服务端拉取新的节点信息,并更新 localcache 和 snapshot 0.2.3. 注册中心的坑 0.2.3.1. 多注册中心 服务消费者同一个注册中心交互是最简单的。但是不可避免的是,服务消费者可能订阅了多个服务,多个服务只在不同的注册中心里有记录。这样的话,就要求服务消费者要具备在启动时,能够从多个注册中心订阅服务的能力。 对于服务消费者来说,要能够同时从多个注册中心订阅服务 对于服务提供者来说,要能够同时向多个注册中心注册服务 0.2.3.2. 并行订阅服务 通常一个服务消费者订阅了不止一个服务,一个服务消费者订阅了几十个不同的服务,每个服务都有自己的方法列表以及节点列表。 服务消费者在服务启动时,会加载订阅的服务配置,调用注册中心的订阅接口,获取每个服务的节点列表并初始化连接。 采用串行订阅的方式,每订阅一个服务,服务消费者调用一次注册中心的订阅接口,获取这个服务的节点列表并初始化连接,总共需要执行几十次这样的过程。 在某些服务节点的初始化连接过程中,出现连接超时的情况,后续所有的服务节点的初始化连接都需要等待它完成,导致服务消费者启动变慢,最后耗费了将近五分钟时间来完成所有服务节点的初始化连接过程。 采用并行订阅的方式,每订阅一个服务就单独用一个线程来处理,这样的话即使遇到个别服务节点连接超时,其他服务节点的初始化连接也不受影响,最慢也就是这个服务节点的初始化连接耗费的时间,最终所有服务节点的初始化连接耗时控制在了 30 秒以内。 0.2.3.3. 批量反注册服务 通常一个服务提供者节点提供不止一个服务,所以注册和反注册都需要多次调用注册中心。在与注册中心的多次交互中,可能由于网络抖动、注册中心集群异常等原因,导致个别调用失败。 对于注册中心来说: 偶发的注册调用失败对服务调用基本没有影响,其结果顶多就是某一个服务少了一个可用的节点 偶发的反注册调用失败会导致不可用的节点残留在注册中心中,变成“僵尸节点”,但服务消费者端还会把它当成“活节点”,继续发起调用,最终导致调用失败 定时去清理注册中心中的“僵尸节点”。 通过优化反注册逻辑,对于下线机器、节点销毁的场景,调用注册中心提供的批量反注册接口,一次调用就可以把该节点上提供的所有服务同时反注册掉,从而避免了“僵尸节点”的出现。 0.2.3.4. 服务变更信息增量更新 服务消费者端启动时: 查询订阅服务的可用节点列表做初始化连接 订阅服务的变更,每隔一段时间从注册中心获取最新的服务节点信息标记 sign,并与本地保存的 sign 值作比对,如果不一样,就会调用注册中心获取最新的服务节点信息 一般情况下,这个过程是没问题的,但是在网络频繁抖动时,服务提供者上报给注册中心的心跳可能会一会儿失败一会儿成功,注册中心就会频繁更新服务的可用节点信息,导致服务消费者频繁从注册中心拉取最新的服务可用节点信息,严重时可能产生网络风暴,导致注册中心带宽被打满。 为了减少服务消费者从注册中心中拉取的服务可用节点信息的数据量,这个时候可以通过增量更新(即版本号)的方式,注册中心只返回变化的那部分节点信息,尤其在只有少数节点信息变更时,可以大大减少服务消费者从注册中心拉取的数据量,从而最大程度避免产生网络风暴。 0.2.4. 注册中心选型 当下主流的服务注册与发现的解决方案,主要有两种: 应用内注册与发现:注册中心提供服务端和客户端的 SDK,业务应用通过引入注册中心提供的 SDK,通过 SDK 与注册中心交互,来实现服务的注册和发现 应用外注册与发现:业务应用本身不需要通过 SDK 与注册中心打交道,而是通过其他方式与注册中心交互,间接完成服务注册与发现 0.2.4.1. 应用内 采用应用内注册与发现的方式,最典型的案例要属 Netflix 开源的 Eureka,官方架构图如下。 主要由三个重要的组件组成: Eureka Server:注册中心的服务端,实现了服务信息注册、存储以及查询等功能 服务端的 Eureka Client:集成在服务端的注册中心 SDK,服务提供者通过调用 SDK,实现服务注册、反注册等功能 客户端的 Eureka Client:集成在客户端的注册中心 SDK,服务消费者通过调用 SDK,实现服务订阅、服务更新等功能 0.2.4.2. 应用外 采用应用外方式实现服务注册和发现,最典型的案例是开源注册中心 Consul,它的架构图如下。 Consul 实现应用外服务注册和发现主要依靠三个重要的组件: Consul:注册中心的服务端,实现服务注册信息的存储,并提供注册和发现服务 Registrator:一个开源的第三方服务管理器项目,它通过监听服务部署的 Docker 实例是否存活,来负责服务提供者的注册和销毁 Consul Template:定时从注册中心服务端获取最新的服务提供者节点列表并刷新 LB 配置(比如 Nginx 的 upstream),这样服务消费者就通过访问 Nginx 就可以获取最新的服务提供者信息 解决方案 开源项目 应用场景 应用内 eureka 适用于服务提供者和服务消费者同属于一个技术体系 应用外 consul 适合服务提供者和服务消费者采用了不同技术体系的业务场景 比如服务提供者提供的是 C++ 服务,而服务消费者是一个 Java 应用,这时候采用应用外的解决方案就不依赖于具体一个技术体系。 对于容器化后的云应用来说,一般不适合采用应用内 SDK 的解决方案,因为这样会侵入业务,而应用外的解决方案正好能够解决这个问题。 0.2.4.3. 选型考虑的问题 除了要考虑是采用应用内注册还是应用外注册的方式以外,还有两个最值得关注的问题,一个是高可用性,一个是数据一致性。 0.2.4.3.1. 高可用 注册中心作为服务提供者和服务消费者之间沟通的纽带,它的高可用性十分重要。实现高可用性的方法主要有两种: 集群部署,通过部署多个实例组成集群来保证高可用性,这样的话即使有部分机器宕机,将访问迁移到正常的机器上就可以保证服务的正常访问。 多 IDC 部署,就是部署在不止一个机房,这样能保证即使一个机房因为断电或者光缆被挖断等不可抗力因素不可用时,仍然可以通过把请求迁移到其他机房来保证服务的正常访问。 Consul 为例,通过这两种方法来保证注册中心的高可用性: 一方面,在每个数据中心内都有多个注册中心 Server 节点可供访问 另一方面,还可以部署在多个数据中心来保证多机房高可用性 0.2.4.3.2. 数据一致性 为了保证注册中心的高可用性,注册中心的部署都采用集群部署,并且还通常部署在不止一个数据中心,这样的话就会引出另一个问题,多个数据中心之间如何保证数据一致? 这里就涉及分布式系统中著名的 CAP 理论,即同时满足一致性、可用性、分区容错性这三者是不可能的,其中: C(Consistency)代表一致性 A(Availability)代表可用性 P(Partition Tolerance)代表分区容错性 在一个分布式系统里面,包含了多个节点,节点之间通过网络连通在一起。正常情况下,通过网络,从一个节点可以访问任何别的节点上的数据。 分区:出现网络故障,导致整个网络被分成了互不连通的区域。 分区容错性:一旦出现分区,那么一个区域内的节点就没法访问其他节点上的数据了,最好的办法是把数据复制到其他区域内的节点,这样即使出现分区,也能访问任意区域内节点上的数据。 一致性:把数据复制到多个节点就可能出现数据不一致的情况。 可用性:要保证一致性,就必须等待所有节点上的数据都更新成功才可用。 总的来说,就是数据节点越多,分区容错性越高,但数据一致性越难保证。为了保证数据一致性,又会带来可用性的问题。 注册中心采用分布式集群部署,也面临着 CAP 的问题,所以注册中心大致可分为两种: 类型 说明 例子 CP 型 牺牲可用性来保证数据强一致性 ZooKeeper,etcd,Consul AP 型 牺牲一致性来保证可用性 Eureka ZooKeeper 集群内只有一个 Leader,而且在 Leader 无法使用的时候通过 Paxos 算法选举出一个新的 Leader。这个 Leader 的目的就是保证写信息的时候只向这个 Leader 写入,Leader 会同步信息到 Followers,这个过程就可以保证数据的强一致性。但如果多个 ZooKeeper 之间网络出现问题,造成出现多个 Leader,发生脑裂的话,注册中心就不可用了。 etcd 和 Consul 集群内都是通过 raft 协议来保证强一致性,如果出现脑裂的话, 注册中心也不可用。 Eureka 不用选举一个 Leader,每个 Eureka 服务器单独保存服务注册地址,因此有可能出现数据信息不一致的情况。但是当网络出现问题的时候,每台服务器都可以完成独立的服务。 对于注册中心来说,最主要的功能是服务的注册和发现,在网络出现问题的时候,可用性的需求要远远高于数据一致性。即使因为数据不一致,注册中心内引入了不可用的服务节点,也可以通过其他措施来避免,比如客户端的快速失败机制等,只要实现最终一致性,对于注册中心来说就足够了。因此,选择 AP 型注册中心,一般更加合适。 0.2.5. 总结 在选择开源注册中心解决方案的时候,要看业务的具体场景。 如果业务体系都采用 Java 语言的话,Netflix 开源的 Eureka 是一个不错的选择,并且它作为服务注册与发现解决方案,能够最大程度的保证可用性,即使出现了网络问题导致不同节点间数据不一致,你仍然能够访问 Eureka 获取数据。 如果你的业务体系语言比较复杂,Eureka 也提供了 Sidecar 的解决方案;也可以考虑使用 Consul,它支持了多种语言接入,包括 Go、Python、PHP、Scala、Java,Erlang、Ruby、Node.js、.NET、Perl 等。 如果你的业务已经是云原生的应用,可以考虑使用 Consul,搭配 Registrator 和 Consul Template 来实现应用外的服务注册与发现。 0.3. RPC框架 一个完整的 RPC 框架主要有三部分组成: 通信框架 通信协议 序列化和反序列化格式 业界应用比较广泛的开源 RPC 框架主要分为两类: 限定语言平台绑定的开源 RPC 框架: 名称 公司 年代 语言 Dubbo 阿里巴巴 2011 Java Motan 微博 2016 Java Tars 腾讯 2017 C++ Spring Cloud Pivotal 2014 Java 从长远来看,支持多语言是 RPC 框架未来的发展趋势。正是基于此判断,各个 RPC 框架都提供了 Sidecar 组件来支持多语言平台之间的 RPC 调用。 Dubbo Mesh提供多语言支持 Motan-go,目前支持 PHP、Java 语言之间的相互调用 spring-cloud-netflix-sidecar,可以让其他语言也可以使用 Spring Cloud 的组件 跨语言平台的开源 RPC 框架: 名称 公司 年代 支持语言 gRPC Google 2015 C++、Java、Python、Go、Ruby、PHP、Android Java、Objective-C Thrift Facebook 2007 年贡献给 Apache 基金 C++、Java、PHP、Python、Ruby、Erlang 0.3.1. 限定语言平台的开源RPC框架 0.3.1.1. Dubbo Dubbo 的架构主要包含四个角色: Consumer 是服务消费者 Provider 是服务提供者 Registry 是注册中心 Monitor 是监控系统 具体的交互流程: Consumer 一端通过注册中心获取到 Provider 节点后,通过 Dubbo 的客户端 SDK 与 Provider 建立连接,并发起调用。 Provider 一端通过 Dubbo 的服务端 SDK 接收到 Consumer 的请求,处理后再把结果返回给 Consumer。 服务消费者和服务提供者都需要引入 Dubbo 的 SDK 才来完成 RPC 调用,因为 Dubbo 本身是采用 Java 语言实现的,所以要求服务消费者和服务提供者也都必须采用 Java 语言实现才可以应用。 Dubbo 调用框架的实现: 通信框架方面,Dubbo 默认采用了 Netty 作为通信框架 通信协议方面,Dubbo 除了支持私有的 Dubbo 协议外,还支持 RMI 协议、Hession 协议、HTTP 协议、Thrift 协议等 序列化格式方面,Dubbo 支持多种序列化格式,比如 Dubbo、Hession、JSON、Kryo、FST 等 0.3.1.2. Motan Motan 与 Dubbo 的架构类似,都需要在 Client 端(服务消费者)和 Server 端(服务提供者)引入 SDK,其中 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 发起远程调用。 0.3.1.3. Tars 目前支持的开发语言如下:C++、Java、Nodejs、PHP、Go Tars 的架构交互主要包括以下几个流程: 服务发布流程:在 web 系统上传 server 的发布包到 patch,上传成功后,在 web 上提交发布 server 请求,由 registry 服务传达到 node,然后 node 拉取 server 的发布包到本地,拉起 server 服务。 管理命令流程:web 系统上可以提交管理 server 服务命令请求,由 registry 服务传达到 node 服务,然后由 node 向 server 发送管理命令。 心跳上报流程:server 服务运行后,会定期上报心跳到 node,node 然后把服务心跳信息上报到 registry 服务,由 registry 进行统一管理。 信息上报流程:server 服务运行后,会定期上报统计信息到 stat,打印远程日志到 log,定期上报属性信息到 prop、上报异常信息到 notify、从 config 拉取服务配置信息。client 访问 server 流程:client 可以通过 server 的对象名 Obj 间接访问 server,client 会从 registry 上拉取 server 的路由信息(如 IP、Port 信息),然后根据具体的业务特性(同步或者异步,TCP 或者 UDP 方式)访问 server(当然 client 也可以通过 IP/Port 直接访问 server)。 0.3.1.4. Spring Cloud 为了解决微服务架构中服务治理而提供的一系列功能的开发框架,它是完全基于 Spring Boot 进行开发的,Spring Cloud 利用 Spring Boot 特性整合了开源行业中优秀的组件,整体对外提供了一套在微服务架构中服务治理的解决方案。因为 Spring Boot 是用 Java 语言编写的,所以目前 Spring Cloud 也只支持 Java 语言平台,它的架构图可以用下面这张图来描述。 Spring Cloud 微服务架构是由多个组件一起组成的,各个组件的交互流程如下: 请求统一通过 API 网关 Zuul 来访问内部服务,先经过 Token 进行安全认证。 通过安全认证后,网关 Zuul 从注册中心 Eureka 获取可用服务节点列表。 从可用服务节点中选取一个可用节点,然后把请求分发到这个节点。 整个请求过程中,Hystrix 组件负责处理服务超时熔断,Turbine 组件负责监控服务间的调用和熔断相关指标,Sleuth 组件负责调用链监控,ELK 负责日志分析。 0.3.1.5. 对比选型 如果语言平台是 C++,那么只能选择 Tars; 如果是 Java 的话,可以选择 Dubbo、Motan 或者 Spring Cloud Spring Cloud 不仅提供了基本的 RPC 框架功能,还提供了服务注册组件、配置中心组件、负载均衡组件、断路器组件、分布式消息追踪组件等一系列组件,被技术圈的人称之为“Spring Cloud 全家桶”。如果不想自己实现以上这些功能,那么 Spring Cloud 基本可以满足你的全部需求。 而 Dubbo、Motan 基本上只提供了最基础的 RPC 框架的功能,其他微服务组件都需要自己去实现。由于 Spring Cloud 的 RPC 通信采用了 HTTP 协议,相比 Dubbo 和 Motan 所采用的私有协议来说,在高并发的通信场景下,性能相对要差一些,所以对性能有苛刻要求的情况下,可以考虑 Dubbo 和 Motan。 0.3.2. 跨语言平台的开源 RPC 框架 0.3.2.1. gRPC gRPC框架详解,出门右转看这里。 原理是通过 IDL(Interface Definition Language)文件定义服务接口的参数和返回值类型,然后通过代码生成程序生成服务端和客户端的具体实现代码,这样在 gRPC 里,客户端应用可以像调用本地对象一样调用另一台服务器上对应的方法。 它的主要特性包括三个方面。 通信协议采用了HTTP/2,因为 HTTP/2 提供了连接复用、双向流、服务器推送、请求优先级、首部压缩等机制,所以在通信过程中可以节省带宽、降低 TCP 连接次数、节省 CPU,尤其对于移动端应用来说,可以帮助延长电池寿命。 IDL 使用了ProtoBuf,ProtoBuf 是由 Google 开发的一种数据序列化协议,它的压缩和传输效率极高,语法也简单,所以被广泛应用在数据存储和通信协议上。 多语言支持,能够基于多种语言自动生成对应语言的客户端和服务端的代码。 0.3.2.2. Thrift Thrift 是一种轻量级的跨语言 RPC 通信方案,支持多达 25 种编程语言。Thrift 有一套自己的接口定义语言 IDL,可以通过代码生成器,生成各种编程语言的 Client 端和 Server 端的 SDK 代码,这样就保证了不同语言之间可以相互通信。 它的架构图可以用下图来描述。 Thrift RPC 框架的特性: 支持多种序列化格式:如 Binary、Compact、JSON、Multiplexed 等 支持多种通信方式:如 Socket、Framed、File、Memory、zlib 等 服务端支持多种处理方式:如 Simple 、Thread Pool、Non-Blocking 等 0.3.2.3. 跨语言框架对比选型 从成熟度上来讲,Thrift 因为诞生的时间要早于 gRPC,所以使用的范围要高于 gRPC,在 HBase、Hadoop、Scribe、Cassandra 等许多开源组件中都得到了广泛地应用。 而且 Thrift 支持多达 25 种语言,这要比 gRPC 支持的语言更多,所以如果遇到 gRPC 不支持的语言场景下,选择 Thrift 更合适。 但 gRPC 作为后起之秀,因为采用了 HTTP/2作为通信协议、ProtoBuf 作为数据序列化格式,在移动端设备的应用以及对传输带宽比较敏感的场景下具有很大的优势,而且开发文档丰富,根据 ProtoBuf 文件生成的代码要比 Thrift 更简洁一些,从使用难易程度上更占优势,所以如果使用的语言平台 gRPC 支持的话,建议还是采用 gRPC 比较好。 gRPC 和 Thrift 虽然支持跨语言的 RPC 调用,但是因为只提供了最基本的 RPC 框架功能,缺乏一系列配套的服务化组件和服务治理功能的支撑,所以使用它们作为跨语言调用的 RPC 框架,就需要自己考虑注册中心、熔断、限流、监控、分布式追踪等功能的实现,不过好在大多数功能都有开源实现,可以直接采用。 0.4. 监控系统 一个监控系统的组成主要涉及四个环节: 数据收集 数据传输 数据处理 数据展示 不同的监控系统实现方案,在这四个环节所使用的技术方案不同,适合的业务场景也不一样。 比较流行的开源监控系统实现方案主要有两种: 以ELK为代表的集中式日志解决方案 以Graphite、TICK和Prometheus等为代表的时序数据库解决方案 0.4.1. ELK ELK 是 Elasticsearch、Logstash、Kibana 三个开源软件产品首字母的缩写,通常配合使用,所以被称为 ELK Stack,架构如下图所示。 Logstash 负责数据收集和传输,它支持动态地从各种数据源收集数据,并对数据进行过滤、分析、格式化等,然后存储到指定的位置。 Elasticsearch 负责数据处理,它是一个开源分布式搜索和分析引擎,具有可伸缩、高可靠和易管理等特点,基于 Apache Lucene 构建,能对大容量的数据进行接近实时的存储、搜索和分析操作,通常被用作基础搜索引擎。 Kibana 负责数据展示,通常和 Elasticsearch 搭配使用,对其中的数据进行搜索、分析并且以图表的方式展示。 这种架构需要在各个服务器上部署 Logstash 来从不同的数据源收集数据,所以比较消耗 CPU 和内存资源,容易造成服务器性能下降,因此后来又在 Elasticsearch、Logstash、Kibana 之外引入了 Beats 作为数据收集器。 相比于 Logstash,Beats 所占系统的 CPU 和内存几乎可以忽略不计,可以安装在每台服务器上做轻量型代理,从成百上千或成千上万台机器向 Logstash 或者直接向 Elasticsearch 发送数据。 Beats 支持多种数据源,主要包括: Packetbeat,用来收集网络流量数据 Topbeat,用来收集系统、进程的 CPU 和内存使用情况等数据 Filebeat,用来收集文件数据 Winlogbeat,用来收集 Windows 事件日志收据 Beats 将收集到的数据发送到 Logstash,经过 Logstash 解析、过滤后,再将数据发送到 Elasticsearch,最后由 Kibana 展示,架构如下图所示。 0.4.2. Graphite Graphite 的组成主要包括三部分: Carbon:负责数据处理 Whisper:负责数据存储 Graphite-Web:负责数据展示 架构如下图。 Carbon:主要作用是接收被监控节点的连接,收集各个指标的数据,将这些数据写入 carbon-cache 并最终持久化到 Whisper 存储文件中去。 Whisper:一个简单的时序数据库,主要作用是存储时间序列数据,可以按照不同的时间粒度来存储数据,比如 1 分钟 1 个点、5 分钟 1 个点、15 分钟 1 个点三个精度来存储监控数据。 Graphite-Web:一个 Web App,其主要功能绘制报表与展示,即数据展示。为了保证 Graphite-Web 能及时绘制出图形,Carbon 在将数据写入 Whisper 存储的同时,会在 carbon-cache 中同时写入一份数据,Graphite-Web 会先查询 carbon-cache,如果没有再查询 Whisper 存储。 Graphite 自身并不包含数据采集组件,可以接入StatsD等开源数据采集组件来采集数据,再传送给 Carbon。 Carbon 对写入的数据格式有一定的要求,比如: // key” + 空格分隔符 + “value + 时间戳”的数据格式 // “.”分割的 key,代表具体的路径信息 // Unix 时间戳 servers.www01.cpuUsage 42 1286269200 products.snake-oil.salesPerMinute 123 1286269200 [one minute passes] servers.www01.cpuUsageUser 44 1286269260 products.snake-oil.salesPerMinute 119 1286269260 Graphite-Web 对外提供了 HTTP API 可以查询某个 key 的数据以绘图展示,查询方式如下。 // 查询 key“servers.www01.cpuUsage // 在过去 24 小时的数据 // 要求返回 500*300 大小的数据图 http://graphite.example.com/render?target=servers.www01.cpuUsage& width=500&height=300&from=-24h // Graphite-Web 还支持丰富的函数 // 代表了查询匹配规则“products.*.salesPerMinute”的所有 key 的数据之和 target=sumSeries(products.*.salesPerMinute) 0.4.3. TICK TICK 是 Telegraf、InfluxDB、Chronograf、Kapacitor 四个软件首字母的缩写,是由 InfluxData 开发的一套开源监控工具栈,因此也叫作 TICK Stack,架构如下图所示。 Telegraf 负责数据收集 InfluxDB 负责数据存储 Chronograf 负责数据展示 Kapacitor 负责数据告警 InfluxDB 对写入的数据格式要求如下: // <measurement>[,<tag-key>=<tag-value>...] <field-key>=<field-value>[,<field2-key>=<field2-value>...] [unix-nano-timestamp] // host为serverA,region为us_west,cpu的值为0.64,时间戳为1434067467100293230,精确到nano cpu,host=serverA,region=us_west value=0.64 1434067467100293230 0.4.4. Prometheus 时间序数据库解决方案 Prometheus,它是一套开源的系统监控报警框架,受 Google 的集群监控系统 Borgmon 启发,2016 年正式加入 CNCF(Cloud Native Computing Foundation),成为受欢迎程度仅次于 Kubernetes 的项目,架构如下图所示。 Prometheus 主要包含下面几个组件: Prometheus Server:用于拉取 metrics 信息并将数据存储在时间序列数据库。 Jobs/exporters:用于暴露已有的第三方服务的 metrics 给 Prometheus Server,比如 StatsD、Graphite 等,负责数据收集。 Pushgateway:主要用于短期 jobs,由于这类 jobs 存在时间短,可能在 Prometheus Server 来拉取 metrics 信息之前就消失了,所以这类的 jobs 可以直接向 Prometheus Server 推送它们的 metrics 信息。 Alertmanager:用于数据报警。 Prometheus web UI:负责数据展示。 工作流程: Prometheus Server 定期从配置好的 jobs 或者 exporters 中拉取 metrics 信息,或者接收来自 Pushgateway 发过来的 metrics 信息。 Prometheus Server 把收集到的 metrics 信息存储到时间序列数据库中,并运行已经定义好的 alert.rules,向 Alertmanager 推送警报。 Alertmanager 根据配置文件,对接收的警报进行处理,发出告警。 通过 Prometheus web UI 进行可视化展示。 Prometheus 存储数据到时间序列数据库,格式如下 // <metric name>{<label name>=<label value>, …} // 集群:cluster1、节点IP:1.1.1.1、端口:80、访问路径:/a、http请求总和:100 http_requests_total{instance="1.1.1.1:80",job="cluster1",location="/a"} 100 0.4.5. 选型对比 0.4.5.1. 数据收集 ELK 是通过在每台服务器上部署 Beats 代理来采集数据 Graphite 本身没有数据采集组件,需要配合使用开源收据采集组件,比如 StatsD TICK 使用了 Telegraf 作为数据采集组件 Prometheus 通过 jobs/exporters 组件来获取 StatsD 等采集过来的 metrics 信息 0.4.5.2. 数据传输 ELK 是 Beats 采集的数据传输给 Logstash,经过 Logstash 清洗后再传输给 Elasticsearch Graphite 是通过第三方采集组件采集的数据,传输给 Carbon TICK 是 Telegraf 采集的数据,传输给 InfluxDB Prometheus 是 Prometheus Server 隔一段时间定期去从 jobs/exporters 拉取数据 前三种都是采用“推数据”的方式,而 Prometheus 是采取“拉数据”的方式,因此 Prometheus 的解决方案对服务端的侵入最小,不需要在服务端部署数据采集代理。 0.4.5.3. 数据处理 ELK 可以对日志的任意字段索引,适合多维度的数据查询,在存储时间序列数据方面与时间序列数据库相比会有额外的性能和存储开销。 时间序列数据库的几种解决方案都支持多种功能的数据查询处理,功能也更强大。 Graphite 通过 Graphite-Web 支持正则表达式匹配、sumSeries 求和、alias 给监控项重新命名等函数功能,同时还支持这些功能的组合,如下所示: // 查询所有匹配路径“stats.open.profile.*.API._comments_flow”的监控项之和,并且把监控项重命名为 Total QPS alias(sumSeries(stats.openapi.profile.*.API._comments_flow.total_count,"Total QPS") InfluxDB 通过类似 SQL 语言的 InfluxQL,能对监控数据进行复杂操作,如下所示: // 查询一分钟 CPU 的使用率 SELECT 100 - usage_idel FROM " gen"."cpu" WHERE time > now() - 1m and "cpu"='cpu0' Prometheus 通过私有的 PromQL 查询语言,PromQL 语句如下,看起来更加简洁,如下所示: // 查询一分钟 CPU 的使用率 100 - (node_cpu{job="node",mode="idle"}[1m]) 0.4.5.4. 数据展示 Graphite、TICK 和 Prometheus 自带的展示功能都比较弱,界面也不好看,不过好在它们都支持Grafana来做数据展示。 Grafana 是一个开源的仪表盘工具,它支持多种数据源比如 Graphite、InfluxDB、Prometheus 以及 Elasticsearch 等。 ELK 采用了 Kibana 做数据展示,Kibana 包含的数据展示功能比较强大,但只支持 Elasticsearch,而且界面展示 UI 效果不如 Grafana 美观。 0.4.5.5. 总体 ELK 的技术栈比较成熟,应用范围也比较广,除了可用作监控系统外,还可以用作日志查询和分析。 Graphite 是基于时间序列数据库存储的监控系统,并且提供了功能强大的各种聚合函数比如 sum、average、top5 等用于监控分析,而且对外提供了 API 也可以接入其他图形化监控系统如 Grafana。 TICK 的核心在于其时间序列数据库 InfluxDB 的存储功能强大,且支持类似 SQL 语言的复杂数据处理操作。 Prometheus 的独特之处在于它采用了拉数据的方式,对业务影响较小,同时也采用了时间序列数据库存储,而且支持独有的 PromQL 查询语言,功能强大而且简洁。 实时性要求角度考虑,时间序列数据库的实时性要好于 ELK,通常可以做到 10s 级别内的延迟,如果对实时性敏感的话,建议选择时间序列数据库解决方案。 使用的灵活性角度考虑,几种时间序列数据库的监控处理功能都要比 ELK 更加丰富,使用更灵活也更现代化。所以如果要搭建一套新的监控系统,我建议可以考虑采用 Graphite、TICK 或者 Prometheus 其中之一。 Graphite 需要搭配数据采集系统比如 StatsD 或者 Collectd 使用,而且界面展示建议使用 Grafana 接入 Graphite 的数据源,它的效果要比 Graphite Web 本身提供的界面美观很多。 TICK 提供了完整的监控系统框架,包括从数据采集、数据传输、数据处理再到数据展示,不过在数据展示方面同样也建议用 Grafana 替换掉 TICK 默认的数据展示组件 Chronograf,这样展示效果更好。 Prometheus 因为采用拉数据的方式,所以对业务的侵入性最小,比较适合 Docker 封装好的云原生应用,比如 Kubernetes 默认就采用了 Prometheus 作为监控系统。 0.5. 服务追踪系统 服务追踪系统的实现,主要包括三个部分: 埋点数据收集,负责在服务端进行埋点,来收集服务调用的上下文数据 实时数据处理,负责对收集到的链路信息,按照 traceId 和 spanId 进行串联和存储 数据链路展示,把处理后的服务调用数据,按照调用链的形式展示出来 0.5.1. Zipkin Zipkin 主要由四个核心部分组成: Collector:负责收集探针 Transport 埋点采集的数据,经过验证处理并建立索引 Storage:存储服务调用的链路数据,默认使用的是 Cassandra(Twitter内部使用),可以替换成 Elasticsearch 或者 MySQL API:将格式化和建立索引的链路数据以 API 的方式对外提供服务,比如被 UI 调用 UI:以图形化的方式展示服务调用的链路数据 工作原理: 在业务的 HTTP Client 前后引入服务追踪代码,这样在 HTTP 方法/foo调用前,生成 trace 信息:TraceId:aa、SpanId:6b、annotation:GET /foo,当前时刻的timestamp:1483945573944000, 调用结果返回后,记录下耗时 duration, 再把这些 trace 信息和 duration 异步上传给 Zipkin Collector 0.5.2. Pinpoint Pinpoint 是 Naver 开源的一款深度支持 Java 语言的服务追踪系统,架构如下图所示: 四个部分组成: Pinpoint Agent:通过 Java 字节码注入的方式,来收集 JVM 中的调用数据,通过 UDP 协议传递给 Collector,数据采用 Thrift 协议进行编码 Pinpoint Collector:收集 Agent 传过来的数据,然后写到 HBase Storage HBase Storage:采用 HBase 集群存储服务调用的链路信息 Pinpoint Web UI:通过 Web UI 展示服务调用的详细链路信息 0.5.2.1. 工作原理 请求进入 TomcatA,然后生成 TraceId:TomcatA^ TIME ^ 1、SpanId:10、pSpanId:-1(代表是根请求) TomatA 调用 TomcatB 的 hello 方法,TomcatB 生成 TraceId:TomcatA^ TIME ^1、新的 SpanId:20、pSpanId:10(代表是 TomcatA 的请求) 返回调用结果后将 trace 信息发给 Collector,TomcatA 收到调用结果后,将 trace 信息也发给 Collector Collector 把 trace 信息写入到 HBase 中,Rowkey 就是 traceId,SpanId 和 pSpanId 都是列。 通过 UI 查询调用链路信息 0.5.3. 选型 0.5.3.1. 埋点探针支持平台的广泛性 Zipkin 提供了不同语言的 Library,不同语言实现时需要引入不同版本的 Library。官方提供了 C#、Go、Java、JavaScript、Ruby、Scala、PHP 等主流语言版本的 Library,而且开源社区还提供了更丰富的不同语言版本的 Library Pinpoint 目前只支持 Java 语言。 所以从探针支持的语言平台广泛性上来看,Zipkin 比 Pinpoint 的使用范围要广,而且开源社区很活跃,生命力更强。 0.5.3.2. 系统集成难易程度 以 Zipkin 的 Java 探针 Brave 为例,它只提供了基本的操作 API,如果系统要想集成 Brave,必须在配置里手动添加相应的配置文件并且增加 trace 业务代码。具体来讲,需要修改工程的 POM 依赖,以引入 Brave 相关的 JAR 包。收集每一次 HTTP 调用的信息,可以使用 Brave 在 Apache Httpclient 基础上封装的 httpClient,它会记录每一次 HTTP 调用的信息,并上报给 Zipkin。 Pinpoint 是通过字节码注入的方式来实现拦截服务调用,从而收集 trace 信息的,所以不需要代码做任何改动。 Java 字节码注入的大致原理如下图。 JVM 在加载 class 二进制文件时,动态地修改加载的 class 文件,在方法的前后执行拦截器的 before() 和 after() 方法,在 before() 和 after() 方法里记录 trace() 信息。 应用不需要修改业务代码,只需要在 JVM 启动时,添加类似下面的启动参数: -javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar -Dpinpoint.agentId=<Agent's UniqueId> -Dpinpoint.applicationName=<The name indicating a same service (AgentId collection) 从系统集成难易程度上看,Pinpoint 要比 Zipkin 简单。 0.5.3.3. 调用链路数据的精确度 Zipkin 收集到的数据只到接口级别 Pinpoint 采用了字节码注入的方式实现 trace 信息收集,所以它能拿到的信息比 Zipkin 多得多。不仅能够查看接口级别的链路调用信息,还能深入到调用所关联的数据库信息。 在绘制链路拓扑图时: Zipkin 只能绘制服务与服务之间的调用链路拓扑图 Pinpoint 不仅能够绘制服务与服务之间,还能绘制与 DB 之间的调用链路拓扑图 从调用链路数据的精确度上看,Pinpoint 要比 Zipkin 精确得多。 除了 Zipkin 和 Pinpoint,业界还有其他开源追踪系统实现,比如 Uber 开源的 Jaeger,以及国内的一款开源服务追踪系统 SkyWalking。 0.6. 服务治理 0.6.1. 服务节点探活 0.6.1.1. 心跳开关保护机制 在网络频繁抖动的情况下,注册中心中可用的节点会不断变化,服务消费者会频繁收到服务提供者节点变更的信息,不断请求注册中心来拉取最新的可用服务节点信息。当有成百上千个服务消费者,同时请求注册中心获取最新的服务提供者的节点信息时,可能会把注册中心的带宽给占满。 需要一种保护机制,即使在网络频繁抖动的时候,服务消费者也不至于同时去请求注册中心获取最新的服务节点信息。 一个可行的解决方案就是给注册中心设置一个开关,当开关打开时,即使网络频繁抖动,注册中心也不会通知所有的服务消费者有服务节点信息变更,比如只给 10% 的服务消费者返回变更,这样的话就能将注册中心的请求量减少到原来的 1/10。 打开开关会导致服务消费者感知最新的服务节点信息延迟到几分钟,在网络正常的情况下,开关并不适合打开;可以作为一个紧急措施,在网络频繁抖动的时候,才打开这个开关。 心跳开关保护机制,是为了防止服务提供者节点频繁变更导致的服务消费者同时去注册中心获取最新服务节点信息。 0.6.1.2. 服务节点摘除保护机制 服务提供者在进程启动时,会注册服务到注册中心,并每隔一段时间,汇报心跳给注册中心,以标识自己的存活状态。如果隔了一段固定时间后,服务提供者仍然没有汇报心跳给注册中心,注册中心就会认为该节点已经处于“dead”状态,于是从服务的可用节点信息中移除出去。 如果遇到网络问题,大批服务提供者节点汇报给注册中心的心跳信息都可能会传达失败,注册中心就会把它们都从可用节点列表中移除出去,造成剩下的可用节点难以承受所有的调用,引起“雪崩”。但是这种情况下,可能大部分服务提供者节点是可用的,仅仅因为网络原因无法汇报心跳给注册中心就被“无情”的摘除了。 这个时候就需要根据实际业务的情况,设定一个阈值比例,即使遇到上述情况,注册中心也不能摘除超过这个阈值比例的节点。 这个阈值比例可以根据实际业务的冗余度来确定,通常会把这个比例设定在 20%,就是说注册中心不能摘除超过 20% 的节点。 因为大部分情况下,节点的变化不会这么频繁,只有在网络抖动或者业务明确要下线大批量节点的情况下才有可能发生。 而业务明确要下线大批量节点的情况是可以预知的,这种情况下可以关闭阈值保护;而正常情况下,应该打开阈值保护,以防止网络抖动时,大批量可用的服务节点被摘除。 服务节点摘除保护机制,是为了防止服务提供者节点被大量摘除引起服务消费者可以调用的节点不足。 上述两种情况下,因为注册中心里的节点信息是随时可能发生变化的,所以也可以把注册中心叫作动态注册中心。 0.6.1.3. 静态注册中心 服务消费者并不严格以注册中心中的服务节点信息为准,而是更多的以服务消费者实际调用信息来判断服务提供者节点是否可用。 如果服务消费者调用某一个服务提供者节点连续失败超过一定次数,可以在本地内存中将这个节点标记为不可用。 并且每隔一段固定时间,服务消费者都要向标记为不可用的节点发起保活探测,如果探测成功了,就将标记为不可用的节点再恢复为可用状态,重新发起调用。 这样的话,服务提供者节点就不需要向注册中心汇报心跳信息,注册中心中的服务节点信息也不会动态变化,称之为静态注册中心。 采用服务消费者端的保活机制,事实证明这种机制足以应对网络频繁抖动等复杂的场景。 静态注册中心中的服务节点信息并不是一直不变,当在业务上线或者运维人工增加或者删除服务节点这种预先感知的情况下,还是有必要去修改注册中心中的服务节点信息。 0.6.2. 负载均衡算法 为什么要引入负载均衡算法呢? 考虑调用的均匀性,要让每个节点都接收到调用,发挥所有节点的作用 考虑调用的性能,哪个节点响应最快,优先调用哪个节点 0.6.2.1. 适用场景 算法 描述 场景 随机算法 实现简单,在请求量远超可用服务节点数量的情况下,各个服务节点被访问的概率基本相同 应用在各个服务节点的性能差异不大的情况下 轮询算法 跟随机算法类似,各个服务节点被访问的概率也基本相同 应用在各个服务节点性能差异不大的情况下 加权轮询算法 在轮询算法基础上的改进,可以通过给每个节点设置不同的权重来控制访问的概率 应用在服务节点性能差异比较大的情况 最少活跃连接算法 与加权轮询算法预先定义好每个节点的访问权重不同,采用最少活跃连接算法,客户端同服务端节点的连接数是在时刻变化的,理论上连接数越少代表此时服务端节点越空闲,选择最空闲的节点发起请求,能获取更快的响应速度 尤其在服务端节点性能差异较大,而又不好做到预先定义权重时,采用最少活跃连接算法是比较好的选择 一致性 hash 算法 能够保证同一个客户端的请求始终访问同一个服务节点 适合服务端节点处理不同客户端请求差异较大的场景。如服务端缓存里保存着客户端的请求结果,可以一直从缓存中获取数据 0.6.2.2. 自适应最优选择算法 在客户端本地维护一份同每一个服务节点的性能统计快照,并且每隔一段时间去更新这个快照。在发起请求时,根据“二八原则”,把服务节点分成两部分,找出 20% 的那部分响应最慢的节点,然后降低权重。这样的话,客户端就能够实时的根据自身访问每个节点性能的快慢,动态调整访问最慢的那些节点的权重,来减少访问量,从而可以优化长尾请求。 自适应最优选择算法是对加权轮询算法的改良,可以看作是一种动态加权轮询算法。它的实现关键之处就在于两点: 第一点是每隔一段时间获取客户端同每个服务节点之间调用的平均性能统计 第二点是按照这个性能统计对服务节点进行排序,对排在性能倒数 20% 的那部分节点赋予一个较低的权重,其余的节点赋予正常的权重 在具体实现时: 针对第一点,需要在内存中开辟一块空间记录客户端同每一个服务节点之间调用的平均性能,并每隔一段固定时间去更新。这个更新的时间间隔太短容易受瞬时的性能抖动影响,导致统计变化太快,没有参考性;太长的话时效性就会大打折扣,效果不佳,1 分钟的更新时间间隔是个比较合适的值。 针对第二点,关键点是权重值的设定,即使服务节点之间的性能差异较大,也不适合把权重设置得差异太大,这样会导致性能较好的节点与性能较差的节点之间调用量相差太大,这样也不是一种合理的状态。在实际设定时,可以设置 20% 性能较差的节点权重为 3,其余节点权重为 5。 0.6.3. 服务路由 服务路由就是服务消费者在发起服务调用时,必须根据特定的规则来选择服务节点,从而满足某些特定的需求。 0.6.3.1. 应用场景 分组调用。为了保证服务的高可用性,实现异地多活的需求,一个服务会部署在不同的数据中心和不同的公有云环境,服务节点也会分成不同的分组,对于服务消费者来说,选择哪一个分组调用,就必须有相应的路由规则。 灰度发布(金丝雀部署)。在服务上线发布前,需要先在一小部分规模的服务节点上先发布服务,然后验证功能是否正常。如果正常的话就继续扩大发布范围;如果不正常的话,就需要排查问题,解决问题后继续发布。 流量切换。在业务线上运行过程中,经常会遇到一些不可抗力因素导致业务故障,服务都不可用。这个时候就需要按照某个指令,能够把原来调用异常服务的流量切换到其他正常的机房。 读写分离。对于大多数互联网业务来说都是读多写少,所以在进行服务部署的时候,可以把读写分开部署,所有写接口可以部署在一起,而读接口部署在另外的节点上。 0.6.3.2. 服务路由的规则 0.6.3.2.1. 条件路由 条件路由是基于条件表达式的路由规则。 // condition://”代表了这是一段用条件表达式编写的路由规则 // 具体的规则是host = 10.20.153.10 => host = 10.20.153.11 // 分隔符“=>”前面是服务消费者的匹配条件,后面是服务提供者的过滤条件 condition://0.0.0.0/dubbo.test.interfaces.TestService?category=routers&dynamic=true&priority=2&enabled=true&rule=" // + URL.encode(" host = 10.20.153.10=> host = 10.20.153.11") // 如果服务消费者的匹配条件为空,就表示对所有的服务消费者应用 => host != 10.20.153.11 // 如果服务提供者的过滤条件为空,就表示禁止服务消费者访问 host = 10.20.153.10 => 具体例子 功能 路由条件 说明 排除某个服务节点 => host != 172.22.3.91 所有的服务消费者都不会访问 IP 为 172.22.3.91 的服务节点 白名单 host != 10.20.153.10,10.20.153.11 => 除了 IP 为 10.20.153.10 和 10.20.153.11 的服务消费者可以发起服务调用以外,其他服务消费者都不可以 黑名单 host = 10.20.153.10,10.20.153.11 => 除了 IP 为 10.20.153.10 和 10.20.153.11 的服务消费者不能发起服务调用以外,其他服务消费者都可以 机房隔离 host = 172.22.3.* => host = 172.22.3.* IP 网段为 172.22.3.* 的服务消费者,才可以访问同网段的服务节点 读写分离 method = find*,list*,get*,is* => host =172.22.3.94,172.22.3.95;method != find*,list*,get*,is* => host = 172.22.3.97,172.22.3.98 find、get、is* 等读方法调用 IP 为 172.22.3.94 和 172.22.3.95 的节点,除此以外的写方法调用 IP 为 172.22.3.97 和 172.22.3.98 的节点 0.6.3.2.2. 脚本路由 脚本路由是基于脚本语言的路由规则,常用的脚本语言比如 JavaScript、Groovy、JRuby 等。 // “script://”就代表了这是一段脚本语言编写的路由规则 // 具体规则定义在脚本语言的 route 方法实现里,只有 IP 为 10.20.153.10 的服务消费者可以发起服务调用 "script://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=" + URL.encode("(function route(invokers) { ... } (invokers))") function route(invokers){ var result = new java.util.ArrayList(invokers.size()); for(i =0; i < invokers.size(); i ++){ if("10.20.153.10".equals(invokers.get(i).getUrl().getHost())){ result.add(invokers.get(i)); } } return result; } (invokers)); 0.6.3.3. 服务路由的获取方式 本地配置:路由规则存储在服务消费者本地。服务消费者发起调用时,从本地固定位置读取路由规则,然后按照路由规则选取一个服务节点发起调用 配置中心管理:所有的服务消费者都从配置中心获取路由规则,由配置中心来统一管理 动态下发:运维人员或者开发人员,通过服务治理平台修改路由规则,服务治理平台调用配置中心接口,把修改后的路由规则持久化到配置中心。服务消费者订阅了路由规则的变更,会从配置中心获取最新的路由规则,按照最新的路由规则来执行 一般来讲,服务路由最好是存储在配置中心中,由配置中心来统一管理。这样的话,所有的服务消费者就不需要在本地管理服务路由,因为大部分的服务消费者并不关心服务路由的问题,或者说也不需要去了解其中的细节。通过配置中心,统一给各个服务消费者下发统一的服务路由,节省了沟通和管理成本。动态下发能够动态地修改路由规则,在某些业务场景下十分有用。 三种方式一起使用,服务消费者的判断优先级是本地配置 > 动态下发 > 配置中心管理。 0.7. 服务端故障处理 单体应用改造成微服务的一个好处是可以减少故障影响范围,故障被局限在一个微服务系统本身,而不是整个单体应用都崩溃。 微服务系统可能出现故障的种类: 集群故障:一旦代码出现 bug,可能整个集群都会发生故障,不能提供对外提供服务 单 IDC 故障:业务部署在不止一个 IDC,某些不可抗力导致整个 IDC 脱网 单机故障:集群中的个别机器出现故障,导致调用到故障机器上的请求都失败,影响整个系统的成功率 0.7.1. 集群故障 集群故障的产生原因: 代码 bug 所导致,比如说某一段 Java 代码不断地分配大对象,但没有及时回收导致 JVM OOM 退出 突发的流量冲击,超出了系统的最大承载能力,比如“双 11”,超出系统的最大承载能力,把整个系统给压垮 应付集群故障的思路,主要有两种:限流和降级。 0.7.1.1. 限流 系统能够承载的流量根据集群规模的大小是固定的,称之为系统的最大容量。 当真实流量超过了系统的最大容量后,就会导致系统响应变慢,服务调用出现大量超时,用户的感觉就是卡顿、无响应。 应该根据系统的最大容量,给系统设置一个阈值,超过这个阈值的请求会被自动抛弃,这样的话可以最大限度地保证系统提供的服务正常。 通常微服务系统会同时提供多个服务,每个服务在同一时刻的请求量也是不同的,可能出现系统中某个服务的请求量突增,占用了系统中大部分资源,导致其他服务没有资源可用。 因此,要针对系统中每个服务的请求量也设置一个阈值,超过这个阈值的请求也要被自动抛弃。 用两个指标来衡量服务的请求量: QPS 即每秒请求量 工作线程数 不过 QPS 因为不同服务的响应快慢不同,所以系统能够承载的 QPS 相差很大,因此一般选择工作线程数来作为限流的指标,给系统设置一个总的最大工作线程数以及单个服务的最大工作线程数。 0.7.1.2. 降级 降级是通过停止系统中的某些功能,来保证系统整体的可用性,是一种被动防御措施,一般是系统已经出现故障后所采取的一种止损措施。 降级的实现方式:通过开关来实现。 在系统运行的内存中开辟一块区域,专门用于存储开关的状态,也就是开启还是关闭。并且需要监听某个端口,通过这个端口可以向系统下发命令,来改变内存中开关的状态。当开关开启时,业务的某一段逻辑就不再执行,而正常情况下,开关是关闭的状态。 开关一般用在两种地方: 新增的业务逻辑,因为新增的业务逻辑相对来说不成熟,往往具备一定的风险,所以需要加开关来控制新业务逻辑是否执行; 依赖的服务或资源,因为依赖的服务或者资源不总是可靠的,所以最好是有开关能够控制是否对依赖服务或资源发起调用,来保证即使依赖出现问题,也能通过降级来避免影响。 降级要按照对业务的影响程度分为三级: 一级降级:对业务影响最小的降级,在故障的情况下,首先执行一级降级,所以一级降级也可以设置成自动降级,不需要人为干预 二级降级:对业务有一定影响的降级,在故障的情况下,如果一级降级起不到多大作用的时候,可以人为采取措施,执行二级降级 三级降级:对业务有较大影响的降级,这种降级要么是对商业收入有重大影响,要么是对用户体验有重大影响,所以操作起来要非常谨慎,不在最后时刻一般不予采用 0.7.2. 单 IDC 故障 同城双活,在一个城市的两个 IDC 内部署 异地多活,在两个城市的两个 IDC 内部署 金融级别的应用“三地五中心”部署,部署成本高,但可用性的保障要更高 采用多 IDC 部署的最大好处就是当有一个 IDC 发生故障时,可以把原来访问故障 IDC 的流量切换到正常的 IDC,来保证业务的正常访问。 流量切换的方式一般有两种: 基于 DNS 解析:通过把请求访问域名解析的 VIP 从一个 IDC 切换到另外一个 IDC 基于 RPC 分组:多IDC部署服务,每个 IDC 就是一个分组。假如一个 IDC 故障,通过向配置中心下发命令,把原先路由到这个分组的流量全部切换到别的分组 比如访问“www.weibo.com”,正常情况下北方用户会解析到联通机房的 VIP,南方用户会解析到电信机房的 VIP,如果联通机房发生故障的话,会把北方用户访问也解析到电信机房的 VIP,只不过此时网络延迟可能会变长。 0.7.3. 单机故障 单机故障是发生概率最高的一种故障,只靠运维人肉处理不可行,要有某种手段来自动处理单机故障。 处理单机故障一个有效的办法就是自动重启。 通过设置阈值完成,如以某个接口的平均耗时为准,当监控单机上某个接口的平均耗时超过一定阈值时,就认为这台机器有问题,就需要把有问题的机器从线上集群中摘除掉,然后在重启服务后,重新加入到集群中。 要注意防止网络抖动造成的接口超时从而触发自动重启: 在收集单机接口耗时数据时,多采集几个点,比如每 10s 采集一个点,采集 5 个点,当 5 个点中有超过 3 个点的数据都超过设定的阈值范围,才认为是真正的单机问题,这时会触发自动重启策略。 为了防止某些特殊情况下,短时间内被重启的单机过多,造成整个服务池可用节点数太少,最好是设置一个可重启的单机数量占整个集群的最大比例,一般这个比例不要超过 10%,正常情况下,不大可能有超过 10% 的单机都出现故障。 0.8. 服务调用失败处理手段 微服务相比于单体应用最大的不同之处在于,服务的调用从机器内部的本地调用变成了机器之间的远程方法调用,这个过程引入了两个不确定的因素: 调用的执行是在服务提供者一端,即使服务消费者本身是正常的,服务提供者也可能由于诸如 CPU、网络 I/O、磁盘、内存、网卡等硬件原因导致调用失败,还有可能由于本身程序执行问题比如 GC 暂停导致调用失败。 调用发生在两台机器之间,要经过网络传输,网络的复杂性是不可控的,网络丢包、延迟以及随时可能发生的瞬间抖动都有可能造成调用失败。 0.8.1. 超时 一次用户调用可能会被拆分成多个系统之间的服务调用,任何一次服务调用如果发生问题都可能会导致最后用户调用失败。 在微服务架构下,一个系统的问题会影响所有调用这个系统的服务消费者,不加以控制会引起整个系统雪崩。 针对服务调用都要设置超时时间,以避免依赖的服务迟迟没有返回调用结果,把服务消费者拖死。 超时时间的设定: 时间太短,可能会导致有些服务调用还没有来得及执行完就被丢弃 时间太长,可能导致服务消费者被拖垮 根据正常情况下,服务提供者的服务水平来找到合适的超时时间。按照服务提供者线上真实的服务水平,取 P999( 99.9%) 或者 P9999(99.99%) 的调用都在多少毫秒内返回为准。 0.8.2. 重试 设置超时时间可以起到及时止损的效果,但服务调用的结果还是失败了,大部分情况下,调用失败都是因为偶发的网络问题或者个别服务提供者节点有问题导致的,如果能换个节点再次访问说不定就能成功。 从概率论的角度来讲,假如一次服务调用失败的概率为 1%,那么连续两次服务调用失败的概率就是 0.01%,失败率降低到原来的 1%。 在实际服务调用时,要设置服务调用超时后的重试次数。 某个服务调用的超时时间设置为 100ms,重试次数设置为 1,那么当服务调用超过 100ms 后,服务消费者就会立即发起第二次服务调用,而不会再等待第一次调用返回的结果了。 0.8.3. 双发 服务消费者发起服务调都同时发起两次服务调用: 一方面可以提高调用的成功率 另一方面两次服务调用哪个先返回就采用哪次的返回结果,平均响应时间也要比一次调用更快 这样的话,一次调用会给后端服务两倍的压力,所要消耗的资源也是加倍的,所以一般情况下,这种“鲁莽”的双发是不可取的。 优化的双发,即“备份请求”(Backup Requests),大致思想是服务消费者发起一次服务调用后,在给定的时间内(这个设定的时间通常要比超时时间短得多)如果没有返回请求结果,那么服务消费者就立刻发起另一次服务调用。 比如超时时间取的是 P999,那么备份请求时间取的可能是 P99 或者 P90,这是因为如果在 P99 或者 P90 的时间内调用还没有返回结果,那么大概率可以认为这次请求属于慢请求,再次发起调用理论上返回要更快一些。 注意,备份请求要设置一个最大重试比例,以避免在服务端出现问题的时,导致请求量几乎翻倍,给服务提供者造成更大的压力。最大重试比例可以设置成15%,一方面能体现备份请求的优势,另一方面不会给服务提供者额外增加太大的压力。 0.8.4. 熔断 如果服务提供者出现故障,短时间内无法恢复时,无论是超时重试还是双发不但不能提高服务调用的成功率,反而会因为重试给服务提供者带来更大的压力,从而加剧故障。 针对这种情况,需要服务消费者能够探测到服务提供者发生故障,并短时间内停止请求,给服务提供者故障恢复的时间,待服务提供者恢复后,再继续请求。 这就好比一条电路,电流负载过高的话,保险丝就会熔断,以防止火灾的发生,所以这种手段就被叫作“熔断”。 0.8.4.1. 熔断的工作原理 把客户端的每一次服务调用用断路器封装起来,通过断路器来监控每一次服务调用。 如果某一段时间内,服务调用失败的次数达到一定阈值,那么断路器就会被触发,后续的服务调用就直接返回,也就不会再向服务提供者发起请求了。 根据熔断中断路器的状态来恢复服务调用。 Closed 状态:正常情况下,断路器是处于关闭状态的,偶发的调用失败也不影响。 Open 状态:当服务调用失败次数达到一定阈值时,断路器就会处于开启状态,后续的服务调用就直接返回,不会向服务提供者发起请求。 Half Open 状态:当断路器开启后,每隔一段时间,会进入半打开状态,这时候会向服务提供者发起探测调用,以确定服务提供者是否恢复正常。如果调用成功了,断路器就关闭;如果没有成功,断路器就继续保持开启状态,并等待下一个周期重新进入半打开状态。 断路器实现的关键就在于计算一段时间内服务调用的失败率, Hystrix 使用滑动窗口算法,如下图所示。 默认情况下,滑动窗口包含 10 个桶,每个桶时间宽度为 1 秒,每个桶内记录了这 1 秒内所有服务调用中成功的、失败的、超时的以及被线程拒绝的次数。 当新的 1 秒到来时,滑动窗口就会往前滑动,丢弃掉最旧的 1 个桶,把最新 1 个桶包含进来。 任意时刻,Hystrix 都会取滑动窗口内所有服务调用的失败率作为断路器开关状态的判断依据,这 10 个桶内记录的所有失败的、超时的、被线程拒绝的调用次数之和除以总的调用次数就是滑动窗口内所有服务的调用的失败率。 0.9. 管理服务配置 单体应用只需要管理一套配置 微服务每个系统都有自己的配置,并且各不相同,因为服务治理的需要,有些配置还需要动态改变,以达到动态降级、切流量、扩缩容等目的 0.9.1. 本地配置 服务配置管理方案: 把配置当作代码看待,随着应用程序代码一起发布。 把配置都抽离到单独的配置文件当中,使配置与代码分离。 无论是把配置定义在代码里,还是把配置从代码中抽离出来,都相当于把配置存在了应用程序的本地。 如果需要修改配置,就要重新走一遍代码或者配置的发布流程,在线上业务中这是一个很重的操作,相当于一次上线发布过程,甚至更繁琐,需要更谨慎。 如果能有一个集中管理配置的地方,如果需要修改配置,只需要在这个地方修改一下,线上服务就自动从这个地方同步过去,不需要走代码或者配置的发布流程,这就是下面的配置中心。 0.9.2. 配置中心 配置中心的思路:把服务的各种配置(如代码里配置的各种参数、服务降级的开关甚至依赖的资源等)都在一个地方统一进行管理。服务启动时,可以自动从配置中心中拉取所需的配置,并且如果有配置变更的情况,同样可以自动从配置中心拉取最新的配置信息,服务无须重新发布。 一般来讲,配置中心存储配置是按照 Group 来存储的,同一类配置放在一个 Group 下,以 K, V 键值对存储。 配置中心一般包含下面几个功能: 配置注册功能:配置中心对外提供接口 /config/service?action=register 来完成配置注册功能,需要传递的参数包括配置对应的分组 Group,以及对应的 Key、Value 值。 配置反注册功能:配置中心对外提供接口 config/service?action=unregister 来完成配置反注册功能,需要传递的参数包括配置对象的分组 Group,以及对应的 Key。 配置查看功能:配置中心对外提供接口 config/service?action=lookup 来完成配置查看功能,需要传递的参数包括配置对象的分组 Group,以及对应的 Key。 配置变更订阅功能:配置中心对外提供接口 config/service?action=getSign 来完成配置变更订阅接口,客户端本地会保存一个配置对象的分组 Group 的 sign 值,同时每隔一段时间去配置中心拉取该 Group 的 sign 值,与本地保存的 sign 值做对比。一旦配置中心中的 sign 值与本地的 sign 值不同,客户端就会从配置中心拉取最新的配置信息。 配置中心可以便于管理服务的配置信息,如果要修改配置信息,只需同配置中心交互,应用程序会通过订阅配置中心的配置,自动完成配置更新。 0.9.2.1. 典型场景 资源服务化。在应用规模不大的时候,所依赖的资源对应的 IP 可以直接写在配置里。但当业务规模发展到一定程度后,所依赖的这些资源的数量也开始急剧膨胀,这个时候如果采用的是本地配置的话,就需要去更改本地配置,把不可用的 IP 改成可用的 IP,然后发布新的配置,这样的过程十分不便。 业务动态降级。微服务架构下,拆分的服务越多,出现故障的概率就越大,因此需要有对应的服务治理手段,比如要具备动态降级能力,在依赖的服务出现故障的情况下,可以快速降级对这个服务的调用,从而保证不受影响。为此,服务消费者可以通过订阅依赖服务是否降级的配置,当依赖服务出现故障的时候,通过向配置中心下达指令,修改服务的配置为降级状态,这样服务消费者就可以订阅到配置的变更,从而降级对该服务的调用。 分组流量切换。为了保证异地多活以及本地机房调用,一般服务提供者的部署会按照 IDC 维度进行部署,每个 IDC 划分为一个分组,这样的话,如果一个 IDC 出现故障,可以把故障 IDC 机房的调用切换到其他正常 IDC。为此,服务消费者可以通过订阅依赖服务的分组配置,当依赖服务的分组配置发生变更时,服务消费者就对应的把调用切换到新的分组,从而实现分组流量切换。 0.9.3. 技术选型 Spring Cloud Config。Spring Cloud 中使用的配置中心组件,只支持 Java 语言,配置存储在 git 中,变更配置也需要通过 git 操作,如果配置中心有配置变更,需要手动刷新。 Disconf。百度开源的分布式配置管理平台,只支持 Java 语言,基于 Zookeeper 来实现配置变更实时推送给订阅的客户端,并且可以通过统一的管理界面来修改配置中心的配置。 Apollo。携程开源的分布式配置中心,支持 Java 和.Net 语言,客户端和配置中心通过 HTTP 长连接实现实时推送,并且有统一的管理界面来实现配置管理。 Spring Cloud Config 作为配置中心的功能比较弱,只能通过 git 命令操作,而且变更配置的话还需要手动刷新,如果不是采用 Spring Cloud 框架的话不建议选择。 而 Disconf 和 Apollo 的功能都比较强大,其中 Apollo 对 Spring Boot 的支持比较好,如果应用本身采用的是 Spring Boot 开发的话,集成 Apollo 会更容易一些。 0.10. 微服务治理平台 单体应用改造为微服务架构后,服务调用从本地调用变成了远程方法调用后,面临的各种不确定因素变多: 需要能够监控各个服务的实时运行状态、服务调用的链路和拓扑图 需要在出现故障时,能够快速定位故障的原因并可以通过诸如降级、限流、切流量、扩容等手段快速干预止损 微服务治理平台就是与服务打交道的统一入口,无论是开发人员还是运维人员,都能通过这个平台对服务进行各种操作: 开发人员可以通过这个平台对服务进行降级操作 运维人员可以通过这个平台对服务进行上下线操作,而不需要关心这个操作背后的具体实现 0.10.1. 服务管理 通过微服务治理平台,可以调用注册中心提供的各种管理接口来实现服务的管理。 服务管理一般包括以下几种操作: 服务上下线。 节点添加 / 删除 服务查询 服务节点查询 0.10.2. 服务治理 通过微服务治理平台,可以调用配置中心提供的接口,动态地修改各种配置来实现服务的治理。 服务治理手段包括以下几种: 限流 降级 切流量 0.10.3. 服务监控 微服务治理平台一般包括两个层面的监控: 整体监控,比如服务依赖拓扑图,将整个系统内服务间的调用关系和依赖关系进行可视化的展示,可以使用服务追踪系统提供的服务依赖拓扑图 具体服务监控,比如服务的 QPS、AvgTime、P999 等监控指标,可以通过 Grafana 等监控系统 UI 来展示 0.10.4. 问题定位 微服务治理平台实现问题定位,可以从两个方面来进行: 宏观层面,即通过服务监控来发觉异常,比如某个服务的平均耗时异常导致调用失败 微观层面,即通过服务追踪来具体定位一次用户请求失败具体是因为服务调用全链路的哪一层导致的 0.10.5. 日志查询 微服务治理平台可以通过接入类似 ELK 的日志系统,能够实时地查询某个用户的请求的详细信息或者某一类用户请求的数据统计。 0.10.6. 服务运维 微服务治理平台可以调用容器管理平台,来实现常见的运维操作。 服务运维主要包括下面几种操作: 发布部署。当服务有功能变更,需要重新发布部署的时候,可以调用容器管理平台分批按比例进行重新部署,然后发布到线上。 扩缩容。在流量增加或者减少的时候,需要相应地增加或者缩减服务在线上部署的实例,这时候可以调用容器管理平台来扩容或者缩容。 0.10.7. 搭建微服务治理平台 微服务治理平台关键在于: 封装对微服务架构内的各个基础设施组件的调用,对外提供统一的服务操作 API 提供可视化的界面,以方便开发人员和运维人员操作 一个微服务治理平台的组成主要包括三部分: Web Portal 层 API 层 数据存储 DB 层 第一层:Web Portal。也就是微服务治理平台的前端展示层,包含以下功能界面: 服务管理界面,进行节点的操作,比如查询节点、删除节点 服务治理界面,进行服务治理操作,比如切流量、降级等,可查看操作记录 服务监控界面,查看服务的详细信息,比如 QPS、AvgTime、耗时分布区间以及 P999 等 服务运维界面,执行服务的扩缩容操作,可查看操作记录 第二层,API。也就是微服务治理平台的后端服务层,这一层提供接口给 Web Portal 调用,包含以下接口功能: 添加服务接口,这个接口会调用注册中心提供的服务添加接口来新发布一个服务 删除服务接口,这个接口会调用注册中心提供的服务注销接口来下线一个服务 服务降级 / 限流 / 切流量接口,这几个接口会调用配置中心提供的配置修改接口,来修改对应服务的配置,然后订阅这个服务的消费者就会从配置中心拉取最新的配置,从而实现降级、限流以及流量切换 服务扩缩容接口,这个接口会调用容器平台提供的扩缩容接口,来实现服务的实例添加和删除 服务部署接口,这个接口会调用容器平台提供的上线部署接口,来实现服务的线上部署 第三层,DB。也就是微服务治理平台的数据存储层,微服务治理平台不仅需要调用其他组件提供的接口,还需要存储一些基本信息,主要分为以下几种: 用户权限,微服务治理平台的功能十分强大,所以要对用户权限(浏览、更改【对应到具体的服务】以及管理员)进行管理。 操作记录,记录用户在平台上所进行的变更操作,比如降级记录、扩缩容记录、切流量记录等 元数据,用来把服务在各个系统中对应的记录映射到微服务治理平台中,统一进行管理。比如某个服务在监控系统里可能有个特殊标识,在注册中心里又使用了另外一个标识,为了统一就需要在微服务治理平台统一进行转换,然后进行数据串联。 0.10.8. 总结 一个微服务框架是否成熟,除了要看它是否具备服务治理能力,还要看是否有强大的微服务治理平台。 因为微服务治理平台能够将多个系统整合在一起,无论是对开发还是运维来说,都能起到事半功倍的作用,这也是当前大部分开源微服务框架所欠缺的部分,所以对于大部分团队来说,都需要自己搭建微服务治理平台。
传统开发流程 DevOps 是一种新型的业务研发流程,业务的开发人员: 负责业务代码的开发 负责业务的测试 负责上线发布 真正做到掌控服务全流程。 DevOps 实现 DevOps,就必须开发完成代码开发后,能自动进行测试,测试通过后,能自动发布到线上。对应的这两个过程就是 CI 和 CD,具体来讲就是: CI(Continuous Integration),持续集成。开发完成代码开发后,能自动地进行代码检查、单元测试、打包部署到测试环境,进行集成测试,跑自动化测试用例。 CD(Continuous Deploy),持续部署。代码测试通过后,能自动部署到类生产环境中进行集成测试,测试通过后再进行小流量的灰度验证,验证通过后代码就达到线上发布的要求了,就可以把代码自动部署到线上。 DevOps的关键是如何实现代码开发自测通过,自动部署到测试环境,验证通过后再自动部署到生产环境,小流量验证后再自动发布到线上去。 在传统的采用物理机部署服务的时代,代码环境的可移植性差,经常会出现开发环境中运行通过的代码,部署到测试环境就运行不了的问题。 而容器化正好解决了代码环境的可移植性的问题,使得 DevOps 取得了突飞猛进的发展,并成为业界推崇的开发模式。 业界主流方案:Jenkins或者GitLab。 持续集成 确保每一次代码的 Merge Request 都测试通过,可随时合并到代码的 Develop 分支,主要包括四个阶段: build 阶段(开发分支代码的编译与单元测试) package 阶段(开发分支代码打包成 Docker 镜像) deploy 阶段(开发分支代码部署到测试环境) test 阶段(开发分支代码集成测试) 持续交付 确保所有代码合并 Merge Request 到 Develop 分支后,Develop 分支的代码能够在生产环境中测试通过,并进行小流量灰度验证,可随时交付到线上。主要包括五个阶段: build 阶段(Develop 分支的代码编译与单元测试) package 阶段(Develop 分支的代码打包成 Docker 镜像) deploy 阶段(Develop 分支的代码部署到测试环境) test 阶段(Develop 分支的代码集成测试) canary 阶段(Develop 分支的代码的小流量灰度验证) 持续部署 合并 Develop 分支到 Master 主干,并打包成 Docker 镜像,可随时发布到线上。主要包括四个阶段: build 阶段(Master 主干的代码编译与单元测试) package 阶段(Master 主干的代码打包成 Docker 镜像) clear 阶段(Master 主干的代码 Merge 回 Develop 分支) production 阶段(Master 主干的代码发布到线上) DevOps关键 持续集成阶段 保证每一次开发的代码都没有问题,即使合并到主干也能正常工作,这里主要依靠三部分的作用: 代码检查。发现代码潜在的一些 bug,实际执行时可以在集成类似Sonarqube之类的工具实现代码检查。 单元测试。针对每个具体代码模块的,单元测试的覆盖度越高,各个代码模块出错的概率就越小。 集成测试。将各个代码的修改集成到一起,统一部署在测试环境中进行测试。为了实现整个流程的自动化,集成自测阶段主要的任务就是跑每个服务的自动化测试用例,所以自动化测试用例覆盖的越全,集成测试的可靠性就越高。这里就要求开发和测试能及时沟通,在新的业务需求确定时,就开始编写测试用例。 持续交付阶段 保证最新的业务代码,能够在类生产环境中可能够正常运行,一般做法是从线上生成环境中摘掉两个节点,然后在这两个节点上部署最新的业务代码,再进行集成测试,集成测试通过后再引入线上流量,来观察服务是否正常。通常需要解决两个问题: 如何从线上生产环境中摘除两个节点。接入线上的容器管理平台,从线上生产环境中摘除某个节点,然后部署最新的业务代码。 如何观察服务是否正常。由于这两个节点上运行的代码是最新的代码,在引入线上流量后可能会出现内存泄露等在集成测试阶段无法发现的问题,所以这个阶段这两个节点上运行最新代码后的状态必须与线上其他节点一致。观察节点本身的状态,如 CPU、内存、I/O、网卡等,观察业务运行产生的 warn、error 的日志量的大小。 持续部署阶段 把在类生产环境下运行通过的代码自动的发布到线上所有节点中去,关键点在于实际的线上发布阶段并不是想象中的那么直接。 实际发布的时候,考虑到线上服务的稳定性,并不是说按照一定的步长,自动把所有服务池都发布了。这个阶段,采用了手动发布的方式以控制风险,或者只做到持续交付阶段,对于持续部署并不要求自动化。