以XML配置方式发布和引用服务。
一个服务包含了多个接口,可能有上行接口也可能有下行接口,每个接口都有超时控制以及是否重试等配置,如果有多个服务消费者引用这个服务,是不是每个服务消费者都必须在服务引用配置文件中定义?
解决方案:服务发布预定义配置,即使服务消费者没有在服务引用配置文件中定义,也能继承服务提供者的定义。
一个服务提供者发布的服务有上百个方法,并且每个方法都有各自的超时时间、重试次数等信息。服务消费者引用服务时,完全继承了服务发布预定义的各项配置。这种情况下,服务提供者所发布服务的详细配置信息都需要存储在注册中心中,这样服务消费者才能在实际引用时从服务发布预定义配置中继承各种配置。
当服务提供者发生节点变更,尤其是在网络频繁抖动的情况下,所有的服务消费者都会从注册中心拉取最新的服务节点信息,就包括了服务发布配置中预定的各项接口信息,这个信息不加限制的话可能达到 1M 以上,如果同时有上百个服务消费者从注册中心拉取服务节点信息,在注册中心机器部署为百兆带宽的情况下,很有可能会导致网络带宽打满的情况发生。
解决方案:服务引用定义配置。
服务配置升级的过程。由于引用服务的服务消费者众多,并且涉及多个部门,升级步骤就显得异常重要,通常可以按照下面步骤操作。
落地注册中心的过程中,需要解决一些列问题,包括:
服务信息,通常用JSON字符串来存储,包括:
服务一般会分成多个不同的分组,每个分组的目的不同。一般来说有下面几种分组方式:
所以注册中心存储的服务信息一般包含三部分内容:分组、服务名以及节点信息,节点信息又包括节点地址和节点其他信息。
具体存储的时候,一般是按照“服务 - 分组 - 节点信息”三层结构来存储,可以用下图来描述。Service 代表服务的具体分组,Cluster 代表服务的接口名,节点信息用 KV 存储。
注册中心具体是工作包括四个流程:
这里为什么服务消费者要把服务信息存在本机内存呢?
主要是因为服务节点信息并不总是时刻变化的,并不需要每一次服务调用都要调用注册中心获取最新的节点信息,只需要在本机内存中保留最新的服务提供者的节点列表就可以。
这里为什么服务消费者要在本地磁盘存储一份服务提供者的节点信息的快照呢?
这是因为服务消费者同注册中心之间的网络不一定总是可靠的,服务消费者重启时,本机内存中还不存在服务提供者的节点信息,如果此时调用注册中心失败,那么服务消费者就拿不到服务节点信息了,也就没法调用了。本地快照就是为了防止这种情况的发生,即使服务消费者重启后请求注册中心失败,依然可以读取本地快照,获取到服务节点信息。
getSign()
函数,从注册中心获取服务端该 Cluster 的 sign 值,并与本地保留的 sign 值做对比,如果不一致,就从服务端拉取新的节点信息,并更新 localcache 和 snapshot服务消费者同一个注册中心交互是最简单的。但是不可避免的是,服务消费者可能订阅了多个服务,多个服务只在不同的注册中心里有记录。这样的话,就要求服务消费者要具备在启动时,能够从多个注册中心订阅服务的能力。
通常一个服务消费者订阅了不止一个服务,一个服务消费者订阅了几十个不同的服务,每个服务都有自己的方法列表以及节点列表。
服务消费者在服务启动时,会加载订阅的服务配置,调用注册中心的订阅接口,获取每个服务的节点列表并初始化连接。
采用串行订阅的方式,每订阅一个服务,服务消费者调用一次注册中心的订阅接口,获取这个服务的节点列表并初始化连接,总共需要执行几十次这样的过程。
在某些服务节点的初始化连接过程中,出现连接超时的情况,后续所有的服务节点的初始化连接都需要等待它完成,导致服务消费者启动变慢,最后耗费了将近五分钟时间来完成所有服务节点的初始化连接过程。
采用并行订阅的方式,每订阅一个服务就单独用一个线程来处理,这样的话即使遇到个别服务节点连接超时,其他服务节点的初始化连接也不受影响,最慢也就是这个服务节点的初始化连接耗费的时间,最终所有服务节点的初始化连接耗时控制在了 30 秒以内。
通常一个服务提供者节点提供不止一个服务,所以注册和反注册都需要多次调用注册中心。在与注册中心的多次交互中,可能由于网络抖动、注册中心集群异常等原因,导致个别调用失败。
对于注册中心来说:
定时去清理注册中心中的“僵尸节点”。
通过优化反注册逻辑,对于下线机器、节点销毁的场景,调用注册中心提供的批量反注册接口,一次调用就可以把该节点上提供的所有服务同时反注册掉,从而避免了“僵尸节点”的出现。
服务消费者端启动时:
一般情况下,这个过程是没问题的,但是在网络频繁抖动时,服务提供者上报给注册中心的心跳可能会一会儿失败一会儿成功,注册中心就会频繁更新服务的可用节点信息,导致服务消费者频繁从注册中心拉取最新的服务可用节点信息,严重时可能产生网络风暴,导致注册中心带宽被打满。
为了减少服务消费者从注册中心中拉取的服务可用节点信息的数据量,这个时候可以通过增量更新(即版本号)的方式,注册中心只返回变化的那部分节点信息,尤其在只有少数节点信息变更时,可以大大减少服务消费者从注册中心拉取的数据量,从而最大程度避免产生网络风暴。
当下主流的服务注册与发现的解决方案,主要有两种:
采用应用内注册与发现的方式,最典型的案例要属 Netflix 开源的 Eureka,官方架构图如下。
主要由三个重要的组件组成:
采用应用外方式实现服务注册和发现,最典型的案例是开源注册中心 Consul,它的架构图如下。
Consul 实现应用外服务注册和发现主要依靠三个重要的组件:
解决方案 | 开源项目 | 应用场景 |
---|---|---|
应用内 | eureka | 适用于服务提供者和服务消费者同属于一个技术体系 |
应用外 | consul | 适合服务提供者和服务消费者采用了不同技术体系的业务场景 |
比如服务提供者提供的是 C++ 服务,而服务消费者是一个 Java 应用,这时候采用应用外的解决方案就不依赖于具体一个技术体系。
对于容器化后的云应用来说,一般不适合采用应用内 SDK 的解决方案,因为这样会侵入业务,而应用外的解决方案正好能够解决这个问题。
除了要考虑是采用应用内注册还是应用外注册的方式以外,还有两个最值得关注的问题,一个是高可用性,一个是数据一致性。
注册中心作为服务提供者和服务消费者之间沟通的纽带,它的高可用性十分重要。实现高可用性的方法主要有两种:
Consul 为例,通过这两种方法来保证注册中心的高可用性:
为了保证注册中心的高可用性,注册中心的部署都采用集群部署,并且还通常部署在不止一个数据中心,这样的话就会引出另一个问题,多个数据中心之间如何保证数据一致?
这里就涉及分布式系统中著名的 CAP 理论,即同时满足一致性、可用性、分区容错性这三者是不可能的,其中:
在一个分布式系统里面,包含了多个节点,节点之间通过网络连通在一起。正常情况下,通过网络,从一个节点可以访问任何别的节点上的数据。
分区:出现网络故障,导致整个网络被分成了互不连通的区域。
分区容错性:一旦出现分区,那么一个区域内的节点就没法访问其他节点上的数据了,最好的办法是把数据复制到其他区域内的节点,这样即使出现分区,也能访问任意区域内节点上的数据。
一致性:把数据复制到多个节点就可能出现数据不一致的情况。
可用性:要保证一致性,就必须等待所有节点上的数据都更新成功才可用。
总的来说,就是数据节点越多,分区容错性越高,但数据一致性越难保证。为了保证数据一致性,又会带来可用性的问题。
注册中心采用分布式集群部署,也面临着 CAP 的问题,所以注册中心大致可分为两种:
类型 | 说明 | 例子 |
---|---|---|
CP 型 | 牺牲可用性来保证数据强一致性 | ZooKeeper,etcd,Consul |
AP 型 | 牺牲一致性来保证可用性 | Eureka |
对于注册中心来说,最主要的功能是服务的注册和发现,在网络出现问题的时候,可用性的需求要远远高于数据一致性。即使因为数据不一致,注册中心内引入了不可用的服务节点,也可以通过其他措施来避免,比如客户端的快速失败机制等,只要实现最终一致性,对于注册中心来说就足够了。因此,选择 AP 型注册中心,一般更加合适。
在选择开源注册中心解决方案的时候,要看业务的具体场景。
一个完整的 RPC 框架主要有三部分组成:
业界应用比较广泛的开源 RPC 框架主要分为两类:
限定语言平台绑定的开源 RPC 框架:
名称 | 公司 | 年代 | 语言 |
---|---|---|---|
Dubbo | 阿里巴巴 | 2011 | Java |
Motan | 微博 | 2016 | Java |
Tars | 腾讯 | 2017 | C++ |
Spring Cloud | Pivotal | 2014 | Java |
从长远来看,支持多语言是 RPC 框架未来的发展趋势。正是基于此判断,各个 RPC 框架都提供了 Sidecar 组件来支持多语言平台之间的 RPC 调用。
跨语言平台的开源 RPC 框架:
名称 | 公司 | 年代 | 支持语言 |
---|---|---|---|
gRPC | 2015 | C++、Java、Python、Go、Ruby、PHP、Android Java、Objective-C | |
Thrift | 2007 年贡献给 Apache 基金 | C++、Java、PHP、Python、Ruby、Erlang |
Dubbo 的架构主要包含四个角色:
具体的交互流程:
服务消费者和服务提供者都需要引入 Dubbo 的 SDK 才来完成 RPC 调用,因为 Dubbo 本身是采用 Java 语言实现的,所以要求服务消费者和服务提供者也都必须采用 Java 语言实现才可以应用。
Dubbo 调用框架的实现:
Motan 与 Dubbo 的架构类似,都需要在 Client 端(服务消费者)和 Server 端(服务提供者)引入 SDK,其中 Motan 框架主要包含:
目前支持的开发语言如下:C++、Java、Nodejs、PHP、Go
Tars 的架构交互主要包括以下几个流程:
为了解决微服务架构中服务治理而提供的一系列功能的开发框架,它是完全基于 Spring Boot 进行开发的,Spring Cloud 利用 Spring Boot 特性整合了开源行业中优秀的组件,整体对外提供了一套在微服务架构中服务治理的解决方案。因为 Spring Boot 是用 Java 语言编写的,所以目前 Spring Cloud 也只支持 Java 语言平台,它的架构图可以用下面这张图来描述。
Spring Cloud 微服务架构是由多个组件一起组成的,各个组件的交互流程如下:
Spring Cloud 不仅提供了基本的 RPC 框架功能,还提供了服务注册组件、配置中心组件、负载均衡组件、断路器组件、分布式消息追踪组件等一系列组件,被技术圈的人称之为“Spring Cloud 全家桶”。如果不想自己实现以上这些功能,那么 Spring Cloud 基本可以满足你的全部需求。
而 Dubbo、Motan 基本上只提供了最基础的 RPC 框架的功能,其他微服务组件都需要自己去实现。由于 Spring Cloud 的 RPC 通信采用了 HTTP 协议,相比 Dubbo 和 Motan 所采用的私有协议来说,在高并发的通信场景下,性能相对要差一些,所以对性能有苛刻要求的情况下,可以考虑 Dubbo 和 Motan。
gRPC框架详解,出门右转看这里。
原理是通过 IDL(Interface Definition Language)文件定义服务接口的参数和返回值类型,然后通过代码生成程序生成服务端和客户端的具体实现代码,这样在 gRPC 里,客户端应用可以像调用本地对象一样调用另一台服务器上对应的方法。
它的主要特性包括三个方面。
HTTP/2
,因为 HTTP/2
提供了连接复用、双向流、服务器推送、请求优先级、首部压缩等机制,所以在通信过程中可以节省带宽、降低 TCP 连接次数、节省 CPU,尤其对于移动端应用来说,可以帮助延长电池寿命。Thrift 是一种轻量级的跨语言 RPC 通信方案,支持多达 25 种编程语言。Thrift 有一套自己的接口定义语言 IDL,可以通过代码生成器,生成各种编程语言的 Client 端和 Server 端的 SDK 代码,这样就保证了不同语言之间可以相互通信。
它的架构图可以用下图来描述。
Thrift RPC 框架的特性:
从成熟度上来讲,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 框架,就需要自己考虑注册中心、熔断、限流、监控、分布式追踪等功能的实现,不过好在大多数功能都有开源实现,可以直接采用。
一个监控系统的组成主要涉及四个环节:
不同的监控系统实现方案,在这四个环节所使用的技术方案不同,适合的业务场景也不一样。
比较流行的开源监控系统实现方案主要有两种:
ELK 是 Elasticsearch、Logstash、Kibana 三个开源软件产品首字母的缩写,通常配合使用,所以被称为 ELK Stack,架构如下图所示。
这种架构需要在各个服务器上部署 Logstash 来从不同的数据源收集数据,所以比较消耗 CPU 和内存资源,容易造成服务器性能下降,因此后来又在 Elasticsearch、Logstash、Kibana 之外引入了 Beats 作为数据收集器。
相比于 Logstash,Beats 所占系统的 CPU 和内存几乎可以忽略不计,可以安装在每台服务器上做轻量型代理,从成百上千或成千上万台机器向 Logstash 或者直接向 Elasticsearch 发送数据。
Beats 支持多种数据源,主要包括:
Beats 将收集到的数据发送到 Logstash,经过 Logstash 解析、过滤后,再将数据发送到 Elasticsearch,最后由 Kibana 展示,架构如下图所示。
Graphite 的组成主要包括三部分:
架构如下图。
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)
TICK 是 Telegraf、InfluxDB、Chronograf、Kapacitor 四个软件首字母的缩写,是由 InfluxData 开发的一套开源监控工具栈,因此也叫作 TICK Stack,架构如下图所示。
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
时间序数据库解决方案 Prometheus,它是一套开源的系统监控报警框架,受 Google 的集群监控系统 Borgmon 启发,2016 年正式加入 CNCF(Cloud Native Computing Foundation),成为受欢迎程度仅次于 Kubernetes 的项目,架构如下图所示。
Prometheus 主要包含下面几个组件:
工作流程:
alert.rules
,向 Alertmanager 推送警报。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
jobs/exporters
组件来获取 StatsD 等采集过来的 metrics 信息jobs/exporters
拉取数据前三种都是采用“推数据”的方式,而 Prometheus 是采取“拉数据”的方式,因此 Prometheus 的解决方案对服务端的侵入最小,不需要在服务端部署数据采集代理。
时间序列数据库的几种解决方案都支持多种功能的数据查询处理,功能也更强大。
// 查询所有匹配路径“stats.open.profile.*.API._comments_flow”的监控项之和,并且把监控项重命名为 Total QPS
alias(sumSeries(stats.openapi.profile.*.API._comments_flow.total_count,"Total QPS")
// 查询一分钟 CPU 的使用率
SELECT 100 - usage_idel FROM "
gen"."cpu" WHERE time > now() - 1m and "cpu"='cpu0'
// 查询一分钟 CPU 的使用率
100 - (node_cpu{job="node",mode="idle"}[1m])
Grafana 是一个开源的仪表盘工具,它支持多种数据源比如 Graphite、InfluxDB、Prometheus 以及 Elasticsearch 等。
实时性要求角度考虑,时间序列数据库的实时性要好于 ELK,通常可以做到 10s 级别内的延迟,如果对实时性敏感的话,建议选择时间序列数据库解决方案。
使用的灵活性角度考虑,几种时间序列数据库的监控处理功能都要比 ELK 更加丰富,使用更灵活也更现代化。所以如果要搭建一套新的监控系统,我建议可以考虑采用 Graphite、TICK 或者 Prometheus 其中之一。
Graphite 需要搭配数据采集系统比如 StatsD 或者 Collectd 使用,而且界面展示建议使用 Grafana 接入 Graphite 的数据源,它的效果要比 Graphite Web 本身提供的界面美观很多。
TICK 提供了完整的监控系统框架,包括从数据采集、数据传输、数据处理再到数据展示,不过在数据展示方面同样也建议用 Grafana 替换掉 TICK 默认的数据展示组件 Chronograf,这样展示效果更好。
Prometheus 因为采用拉数据的方式,所以对业务的侵入性最小,比较适合 Docker 封装好的云原生应用,比如 Kubernetes 默认就采用了 Prometheus 作为监控系统。
服务追踪系统的实现,主要包括三个部分:
Zipkin 主要由四个核心部分组成:
工作原理:
/foo
调用前,生成 trace 信息:TraceId:aa、SpanId:6b、annotation:GET /foo
,当前时刻的timestamp:1483945573944000
,Pinpoint 是 Naver 开源的一款深度支持 Java 语言的服务追踪系统,架构如下图所示:
四个部分组成:
TraceId:TomcatA^ TIME ^ 1、SpanId:10、pSpanId:-1(代表是根请求)
TraceId:TomcatA^ TIME ^1、新的 SpanId:20、pSpanId:10(代表是 TomcatA 的请求)
所以从探针支持的语言平台广泛性上来看,Zipkin 比 Pinpoint 的使用范围要广,而且开源社区很活跃,生命力更强。
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 简单。
在绘制链路拓扑图时:
从调用链路数据的精确度上看,Pinpoint 要比 Zipkin 精确得多。
除了 Zipkin 和 Pinpoint,业界还有其他开源追踪系统实现,比如 Uber 开源的 Jaeger,以及国内的一款开源服务追踪系统 SkyWalking。
在网络频繁抖动的情况下,注册中心中可用的节点会不断变化,服务消费者会频繁收到服务提供者节点变更的信息,不断请求注册中心来拉取最新的可用服务节点信息。当有成百上千个服务消费者,同时请求注册中心获取最新的服务提供者的节点信息时,可能会把注册中心的带宽给占满。
需要一种保护机制,即使在网络频繁抖动的时候,服务消费者也不至于同时去请求注册中心获取最新的服务节点信息。
一个可行的解决方案就是给注册中心设置一个开关,当开关打开时,即使网络频繁抖动,注册中心也不会通知所有的服务消费者有服务节点信息变更,比如只给 10% 的服务消费者返回变更,这样的话就能将注册中心的请求量减少到原来的 1/10。
打开开关会导致服务消费者感知最新的服务节点信息延迟到几分钟,在网络正常的情况下,开关并不适合打开;可以作为一个紧急措施,在网络频繁抖动的时候,才打开这个开关。
心跳开关保护机制,是为了防止服务提供者节点频繁变更导致的服务消费者同时去注册中心获取最新服务节点信息。
服务提供者在进程启动时,会注册服务到注册中心,并每隔一段时间,汇报心跳给注册中心,以标识自己的存活状态。如果隔了一段固定时间后,服务提供者仍然没有汇报心跳给注册中心,注册中心就会认为该节点已经处于“dead”状态,于是从服务的可用节点信息中移除出去。
如果遇到网络问题,大批服务提供者节点汇报给注册中心的心跳信息都可能会传达失败,注册中心就会把它们都从可用节点列表中移除出去,造成剩下的可用节点难以承受所有的调用,引起“雪崩”。但是这种情况下,可能大部分服务提供者节点是可用的,仅仅因为网络原因无法汇报心跳给注册中心就被“无情”的摘除了。
这个时候就需要根据实际业务的情况,设定一个阈值比例,即使遇到上述情况,注册中心也不能摘除超过这个阈值比例的节点。
这个阈值比例可以根据实际业务的冗余度来确定,通常会把这个比例设定在 20%,就是说注册中心不能摘除超过 20% 的节点。
因为大部分情况下,节点的变化不会这么频繁,只有在网络抖动或者业务明确要下线大批量节点的情况下才有可能发生。
而业务明确要下线大批量节点的情况是可以预知的,这种情况下可以关闭阈值保护;而正常情况下,应该打开阈值保护,以防止网络抖动时,大批量可用的服务节点被摘除。
服务节点摘除保护机制,是为了防止服务提供者节点被大量摘除引起服务消费者可以调用的节点不足。
上述两种情况下,因为注册中心里的节点信息是随时可能发生变化的,所以也可以把注册中心叫作动态注册中心。
服务消费者并不严格以注册中心中的服务节点信息为准,而是更多的以服务消费者实际调用信息来判断服务提供者节点是否可用。
这样的话,服务提供者节点就不需要向注册中心汇报心跳信息,注册中心中的服务节点信息也不会动态变化,称之为静态注册中心。
采用服务消费者端的保活机制,事实证明这种机制足以应对网络频繁抖动等复杂的场景。
静态注册中心中的服务节点信息并不是一直不变,当在业务上线或者运维人工增加或者删除服务节点这种预先感知的情况下,还是有必要去修改注册中心中的服务节点信息。
为什么要引入负载均衡算法呢?
算法 | 描述 | 场景 |
---|---|---|
随机算法 | 实现简单,在请求量远超可用服务节点数量的情况下,各个服务节点被访问的概率基本相同 | 应用在各个服务节点的性能差异不大的情况下 |
轮询算法 | 跟随机算法类似,各个服务节点被访问的概率也基本相同 | 应用在各个服务节点性能差异不大的情况下 |
加权轮询算法 | 在轮询算法基础上的改进,可以通过给每个节点设置不同的权重来控制访问的概率 | 应用在服务节点性能差异比较大的情况 |
最少活跃连接算法 | 与加权轮询算法预先定义好每个节点的访问权重不同,采用最少活跃连接算法,客户端同服务端节点的连接数是在时刻变化的,理论上连接数越少代表此时服务端节点越空闲,选择最空闲的节点发起请求,能获取更快的响应速度 | 尤其在服务端节点性能差异较大,而又不好做到预先定义权重时,采用最少活跃连接算法是比较好的选择 |
一致性 hash 算法 | 能够保证同一个客户端的请求始终访问同一个服务节点 | 适合服务端节点处理不同客户端请求差异较大的场景。如服务端缓存里保存着客户端的请求结果,可以一直从缓存中获取数据 |
在客户端本地维护一份同每一个服务节点的性能统计快照,并且每隔一段时间去更新这个快照。在发起请求时,根据“二八原则”,把服务节点分成两部分,找出 20% 的那部分响应最慢的节点,然后降低权重。这样的话,客户端就能够实时的根据自身访问每个节点性能的快慢,动态调整访问最慢的那些节点的权重,来减少访问量,从而可以优化长尾请求。
自适应最优选择算法是对加权轮询算法的改良,可以看作是一种动态加权轮询算法。它的实现关键之处就在于两点:
在具体实现时:
服务路由就是服务消费者在发起服务调用时,必须根据特定的规则来选择服务节点,从而满足某些特定的需求。
条件路由是基于条件表达式的路由规则。
// 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 的节点 |
脚本路由是基于脚本语言的路由规则,常用的脚本语言比如 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));
一般来讲,服务路由最好是存储在配置中心中,由配置中心来统一管理。这样的话,所有的服务消费者就不需要在本地管理服务路由,因为大部分的服务消费者并不关心服务路由的问题,或者说也不需要去了解其中的细节。通过配置中心,统一给各个服务消费者下发统一的服务路由,节省了沟通和管理成本。动态下发能够动态地修改路由规则,在某些业务场景下十分有用。
三种方式一起使用,服务消费者的判断优先级是本地配置 > 动态下发 > 配置中心管理。
单体应用改造成微服务的一个好处是可以减少故障影响范围,故障被局限在一个微服务系统本身,而不是整个单体应用都崩溃。
微服务系统可能出现故障的种类:
集群故障的产生原因:
应付集群故障的思路,主要有两种:限流和降级。
系统能够承载的流量根据集群规模的大小是固定的,称之为系统的最大容量。
当真实流量超过了系统的最大容量后,就会导致系统响应变慢,服务调用出现大量超时,用户的感觉就是卡顿、无响应。
应该根据系统的最大容量,给系统设置一个阈值,超过这个阈值的请求会被自动抛弃,这样的话可以最大限度地保证系统提供的服务正常。
通常微服务系统会同时提供多个服务,每个服务在同一时刻的请求量也是不同的,可能出现系统中某个服务的请求量突增,占用了系统中大部分资源,导致其他服务没有资源可用。
因此,要针对系统中每个服务的请求量也设置一个阈值,超过这个阈值的请求也要被自动抛弃。
用两个指标来衡量服务的请求量:
不过 QPS 因为不同服务的响应快慢不同,所以系统能够承载的 QPS 相差很大,因此一般选择工作线程数来作为限流的指标,给系统设置一个总的最大工作线程数以及单个服务的最大工作线程数。
降级是通过停止系统中的某些功能,来保证系统整体的可用性,是一种被动防御措施,一般是系统已经出现故障后所采取的一种止损措施。
降级的实现方式:通过开关来实现。
在系统运行的内存中开辟一块区域,专门用于存储开关的状态,也就是开启还是关闭。并且需要监听某个端口,通过这个端口可以向系统下发命令,来改变内存中开关的状态。当开关开启时,业务的某一段逻辑就不再执行,而正常情况下,开关是关闭的状态。
开关一般用在两种地方:
降级要按照对业务的影响程度分为三级:
采用多 IDC 部署的最大好处就是当有一个 IDC 发生故障时,可以把原来访问故障 IDC 的流量切换到正常的 IDC,来保证业务的正常访问。
流量切换的方式一般有两种:
比如访问“www.weibo.com”,正常情况下北方用户会解析到联通机房的 VIP,南方用户会解析到电信机房的 VIP,如果联通机房发生故障的话,会把北方用户访问也解析到电信机房的 VIP,只不过此时网络延迟可能会变长。
单机故障是发生概率最高的一种故障,只靠运维人肉处理不可行,要有某种手段来自动处理单机故障。
处理单机故障一个有效的办法就是自动重启。
通过设置阈值完成,如以某个接口的平均耗时为准,当监控单机上某个接口的平均耗时超过一定阈值时,就认为这台机器有问题,就需要把有问题的机器从线上集群中摘除掉,然后在重启服务后,重新加入到集群中。
要注意防止网络抖动造成的接口超时从而触发自动重启:
微服务相比于单体应用最大的不同之处在于,服务的调用从机器内部的本地调用变成了机器之间的远程方法调用,这个过程引入了两个不确定的因素:
一次用户调用可能会被拆分成多个系统之间的服务调用,任何一次服务调用如果发生问题都可能会导致最后用户调用失败。
在微服务架构下,一个系统的问题会影响所有调用这个系统的服务消费者,不加以控制会引起整个系统雪崩。
针对服务调用都要设置超时时间,以避免依赖的服务迟迟没有返回调用结果,把服务消费者拖死。
超时时间的设定:
根据正常情况下,服务提供者的服务水平来找到合适的超时时间。按照服务提供者线上真实的服务水平,取 P999( 99.9%) 或者 P9999(99.99%) 的调用都在多少毫秒内返回为准。
设置超时时间可以起到及时止损的效果,但服务调用的结果还是失败了,大部分情况下,调用失败都是因为偶发的网络问题或者个别服务提供者节点有问题导致的,如果能换个节点再次访问说不定就能成功。
从概率论的角度来讲,假如一次服务调用失败的概率为 1%,那么连续两次服务调用失败的概率就是 0.01%,失败率降低到原来的 1%。
在实际服务调用时,要设置服务调用超时后的重试次数。
某个服务调用的超时时间设置为 100ms,重试次数设置为 1,那么当服务调用超过 100ms 后,服务消费者就会立即发起第二次服务调用,而不会再等待第一次调用返回的结果了。
服务消费者发起服务调都同时发起两次服务调用:
这样的话,一次调用会给后端服务两倍的压力,所要消耗的资源也是加倍的,所以一般情况下,这种“鲁莽”的双发是不可取的。
优化的双发,即“备份请求”(Backup Requests),大致思想是服务消费者发起一次服务调用后,在给定的时间内(这个设定的时间通常要比超时时间短得多)如果没有返回请求结果,那么服务消费者就立刻发起另一次服务调用。
比如超时时间取的是 P999,那么备份请求时间取的可能是 P99 或者 P90,这是因为如果在 P99 或者 P90 的时间内调用还没有返回结果,那么大概率可以认为这次请求属于慢请求,再次发起调用理论上返回要更快一些。
注意,备份请求要设置一个最大重试比例,以避免在服务端出现问题的时,导致请求量几乎翻倍,给服务提供者造成更大的压力。最大重试比例可以设置成15%,一方面能体现备份请求的优势,另一方面不会给服务提供者额外增加太大的压力。
如果服务提供者出现故障,短时间内无法恢复时,无论是超时重试还是双发不但不能提高服务调用的成功率,反而会因为重试给服务提供者带来更大的压力,从而加剧故障。
针对这种情况,需要服务消费者能够探测到服务提供者发生故障,并短时间内停止请求,给服务提供者故障恢复的时间,待服务提供者恢复后,再继续请求。
这就好比一条电路,电流负载过高的话,保险丝就会熔断,以防止火灾的发生,所以这种手段就被叫作“熔断”。
断路器实现的关键就在于计算一段时间内服务调用的失败率, Hystrix 使用滑动窗口算法,如下图所示。
默认情况下,滑动窗口包含 10 个桶,每个桶时间宽度为 1 秒,每个桶内记录了这 1 秒内所有服务调用中成功的、失败的、超时的以及被线程拒绝的次数。
当新的 1 秒到来时,滑动窗口就会往前滑动,丢弃掉最旧的 1 个桶,把最新 1 个桶包含进来。
任意时刻,Hystrix 都会取滑动窗口内所有服务调用的失败率作为断路器开关状态的判断依据,这 10 个桶内记录的所有失败的、超时的、被线程拒绝的调用次数之和除以总的调用次数就是滑动窗口内所有服务的调用的失败率。
服务配置管理方案:
无论是把配置定义在代码里,还是把配置从代码中抽离出来,都相当于把配置存在了应用程序的本地。
如果需要修改配置,就要重新走一遍代码或者配置的发布流程,在线上业务中这是一个很重的操作,相当于一次上线发布过程,甚至更繁琐,需要更谨慎。
如果能有一个集中管理配置的地方,如果需要修改配置,只需要在这个地方修改一下,线上服务就自动从这个地方同步过去,不需要走代码或者配置的发布流程,这就是下面的配置中心。
配置中心的思路:把服务的各种配置(如代码里配置的各种参数、服务降级的开关甚至依赖的资源等)都在一个地方统一进行管理。服务启动时,可以自动从配置中心中拉取所需的配置,并且如果有配置变更的情况,同样可以自动从配置中心拉取最新的配置信息,服务无须重新发布。
一般来讲,配置中心存储配置是按照 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 值不同,客户端就会从配置中心拉取最新的配置信息。配置中心可以便于管理服务的配置信息,如果要修改配置信息,只需同配置中心交互,应用程序会通过订阅配置中心的配置,自动完成配置更新。
Spring Cloud Config 作为配置中心的功能比较弱,只能通过 git 命令操作,而且变更配置的话还需要手动刷新,如果不是采用 Spring Cloud 框架的话不建议选择。 而 Disconf 和 Apollo 的功能都比较强大,其中 Apollo 对 Spring Boot 的支持比较好,如果应用本身采用的是 Spring Boot 开发的话,集成 Apollo 会更容易一些。
单体应用改造为微服务架构后,服务调用从本地调用变成了远程方法调用后,面临的各种不确定因素变多:
微服务治理平台就是与服务打交道的统一入口,无论是开发人员还是运维人员,都能通过这个平台对服务进行各种操作:
通过微服务治理平台,可以调用注册中心提供的各种管理接口来实现服务的管理。
服务管理一般包括以下几种操作:
通过微服务治理平台,可以调用配置中心提供的接口,动态地修改各种配置来实现服务的治理。
服务治理手段包括以下几种:
微服务治理平台一般包括两个层面的监控:
微服务治理平台实现问题定位,可以从两个方面来进行:
微服务治理平台可以通过接入类似 ELK 的日志系统,能够实时地查询某个用户的请求的详细信息或者某一类用户请求的数据统计。
微服务治理平台可以调用容器管理平台,来实现常见的运维操作。
服务运维主要包括下面几种操作:
微服务治理平台关键在于:
一个微服务治理平台的组成主要包括三部分:
第一层:Web Portal。也就是微服务治理平台的前端展示层,包含以下功能界面:
第二层,API。也就是微服务治理平台的后端服务层,这一层提供接口给 Web Portal 调用,包含以下接口功能:
第三层,DB。也就是微服务治理平台的数据存储层,微服务治理平台不仅需要调用其他组件提供的接口,还需要存储一些基本信息,主要分为以下几种:
一个微服务框架是否成熟,除了要看它是否具备服务治理能力,还要看是否有强大的微服务治理平台。
因为微服务治理平台能够将多个系统整合在一起,无论是对开发还是运维来说,都能起到事半功倍的作用,这也是当前大部分开源微服务框架所欠缺的部分,所以对于大部分团队来说,都需要自己搭建微服务治理平台。