紧接上一篇,服务化框架落地的挑战和核心需求,那么基于这些核心诉求,我们整个的微服务框架的模型是如何?又该具备哪些核心的治理能力呢?通过本文来一一知晓!
随着互联网的发展和容器化的发展,更进一步的推动了微服务化的建设,在微服务体系下,我们的服务治理,首先要做的就是针对我们大量的服务怎么更好的进行管理,保证我们系统在运行过程中能够自动化的发现问题并自动解决一些问题,从而使我们的系统更加的稳定。而这些治理的策略,至少要包括服务的限流、降级、容错,以及服务的弹性伸缩、灰度发布,还有自动化的运维。
服务治理与管控包含几大块:服务的治理、服务的监控、服务的可用性保障
(资料图片)
服务的治理: 服务提供方的管理、服务使用方的管理、服务依赖的管理、服务的调用管理、服务的注册和发现、服务的部署和升级、服务的版本化管理服务的监控:数据采集、数据处理、链路跟踪、白盒化的报表展示、系统级的监控服务的可用性: 容错(Failover、Failfast)、负载均衡、过载保护、服务降级、频率限制服务化框架的核心能力,括:RPC、服务发现与注册、负载均衡、容错、熔断、限流、降级、权限、全链路日志跟踪也是属于服务治理的范畴之内,当然服务治理不仅仅包含这些核心能力,还包括各种管理,框架要做的就是全套服务,使用只管放心接入使用,所有治理相关的都由框架去实现。
在我前面一篇文章《微服务化框架落地的挑战和核心需求》中,我梳理了微服务化框架落地的一些挑战和核心需求,那么针对这些核心需求,我们看看微服务要实现那些核心能力,也就是将上述需求进行实现,实现的时候基于现有的技术方案来进行抽象和统一,最终就可以形成我们的微服务框架。我将我理解的微服务架构模型分为如下三部分:
核心能力,这个是框架必须要实现的,而且是任何一个服务化框架必备的能力扩展能力,这个是可以通过框架的插件化进行扩展的支持,当然,框架本身也可以支持,但是从我个人的理解上来看,这个通过外部组件进行扩展,会更加契合整体的设计管理平台,这个是围绕整个服务化框架、业务系统、开发流程所需要依赖的一些基础平台,这些平台可以更好的帮助我们进行服务的发布和上线,以及上线后的管理微服务需要提供的核心能力包括:微服务架构模型中通讯的基础协议RPC、服务发现与注册、负载均衡、容错、熔断、限流、降级、弹性扩缩容等
在微服务架构中,服务之间的通信,一般都是通过 远程访问的方式来进行。远程访问的通信方式有很多中,包括:
从通信风格上, 一般有 REST,RPC 和 定制协议这三种方式从交互方式上,按照交互对象的数量可以分为一对一和一对多,按照应答返回的方式分为同步和异步。RPC 是服务通讯的基础,如果没有统一的 RPC 框架,各个团队就需要实现自己的一套接口协议定义、序列化、反序列化、网络框架、服务治理等重复工作,因此可以说,微服务的核心就是要有一个统一的 RPC 框架,统一一套 RPC 是微服务化首先要解决的问题。通过统一的 RPC 框架协议,可以统一我们的接口定义,减少接入和学习的成本。同时,如果我们是基于已有的框架来实现,那么需要尽可能的保证兼容原有协议(比如基于 gRPC 的话,就要尽量保证兼容 gRPC 的接口协议)
业内可选的 RPC 框架有很多,比如 dubbo/dubbox,motan,thrift,grpc,Karyon/Ribbon等,在我之前的公司,我们推行服务化框架的时候,是选择了 gRPC 作为我们的基础框架,然后基于 gRPC 丰富了很多服务治理的策略,整体线上运行良好,并且有较多业务接入。
微服务架构下,我们的业务都是由一系列独立的微服务组成,那么这些微服务之间如何才能发现彼此?这就要求微服务之间存在一种发现的机制。这个机制,叫做服务发现,服务发现的核心功能就是服务注册表,注册表记录了所有的服务节点的配置和状态,每个微服务启动后都需要将自己的信息注册到服务注册表,然后由微服务或者负载均衡系统到服务注册表查询可用服务。
在设计上,服务发现机制的设计有服务端发现和客户端发现两种实现方式。
服务端发现模式(server-side):可以通过 DNS 或者带 VIP 的负载均衡来实现。优点是对客户端无侵入性,客户端只需要简单的向负载均衡或者服务域名发起请求,无需关系服务发现的具体细节,也不用引入服务发现的逻辑缺点是不灵活,不方便难异化处理;并且同时需要引入一个统一的负载均衡器。客户端发现模式(client-side):需要客户端服务注册中心中查询服务地址列表,然后再决定通过哪个地址请求服务。灵活性更高,可以根据客户端的诉求进行满足自身业务的负载均衡,但是客户端需要引入服务发现的逻辑,同时依赖服务注册中心常见服务注册组件包括:zookeeper、etcd、Consul在我曾经的项目中,我们是通过客户端发现模式来设计的,我们会把这个能力集成在微服务框架里面,然后微服务框架在启动的时候,会将自己服务的信息注册到注册中心,注册中心我们当时选用的是 ETCD。
服务注册和服务发现,在实现时根据对一致性要求的不同,分成两个流派:
强一致性比较常见的分布式一致性协议是 PAXOS 协议和 Raft 协议。相比 PAXOS 而言,Raft 协议易于理解和实现,因此最新的分布式一致性方案大都选择 Raft 协议。zookeeper 采用的是 PAXOS 协议,consul 和 etcd 采用的是 Raft 协议。弱一致性如果对一致性要求不高,可以选择以 DNS 为基础的方案。弱一致性方案比较少,一般多用于 REST 或者 HTTP + json / web service 等简单场合虽然方案众多,但是对于普通用户的大多数场景而言,一般互联网公司建议从 zookeeper、etcd、consul 这三个主流方案中选择,我之前的设计里面,我们选择的就是 etcd。
服务负载均衡,一般情况下,是会和服务发现放在一起设计的,因为服务发现后,紧接着,就是要对服务进行路由,也就是服务的负载均衡,他们一般是作为一个整体系统来设计。虽然服务发现机制有服务端发现和客户端发现两种实现方式,但是无论放在哪里实现,针对服务进行路由,也就是负载均衡的核心的功能就是路由算法。常见的路由算法有:随机路由、轮询路由、最小压力路由、最小连接数路由等。
目前微服务框架中,大都是在客户端发现模式(client-side) 来实现服务路和负载均衡,一般也都会支持常见的负载均衡策略,如随机,轮训,hash,权重,连接数【连接数越少,优先级越高】。
我们要对服务进行负载均衡的话,有一个先决条件,就是我们要路由的服务,一定是可用的,怎么保证服务可用,那么就需要进行心跳检测与摘除:进行心跳检测,每个 N 秒检测一次,超过 M 次心跳连不上就进行摘除。这样的话,就可以保证我们路由的服务都是正常的。
如果服务部署在多个不同的机房,那么我们路由的时候,一般的策略就是优先调用本地机房,失败重试也是本机房调用,如果本地机房没有可用节点则路由到其他机房。同时可以增加一个开关控制是否 failover 到远程机房。
负载均衡和容错是服务高可用的重要手段。服务容错的设计在业内有个基本原则,就是“Design for Failure”。常见的服务容错策略如请求重试、限流、降级、熔断等策略
超时是一种最常见的服务容错模式,只要涉及到网络调用,那么就有可能因为网络问题、依赖服务问题,导致调用的时候出现请求超时,一般情况下,我们都会对每个请求设置好超时时间(一般接口不应该超过 1s),如果超时未返回,那么就会断开连接,从而可以做对应的处理,比如返回失败、释放连接、释放资源等。如果不做超时处理,那么可能会导致请求一直卡在,然后一直占用系统资源,从而使得整个系统资源耗尽,或者用户一直刷不到数据。
重试,是指下游出错(不管是真的出错,还是超时)的时候进行使用,通过重试可以较好的保证数据的可靠性,尤其是当下游有网络波动导致超时的场景下,会比较有效。重试的设计,有几个要点:
重试的次数,一定要有一个最大值,经过一定的重试次数,还是拿不到正确的数据,那么就应该返回错误,而不能一直重试,重试的时间间隔,需要设置,不能不停的重试,避免因为重试过多、过快导致系统负担增大,重试的时间策略,我们一般都会引入Exponential Backoff 的策略,也就是所谓的 "指数级退避"。在这种情况下,每一次重试所需要的 sleep 时间都会指数增加。这其实和 TCP 的拥塞控制有点像。限流和降级都是用来保证核心服务稳定性的有效策略。限流是指限制每个服务(或者接口)的最大访问量,超过这个阈值就会被拒绝,是从用户访问的请求量量去考虑问题;降级是指高峰期对非核心的系统进行降级从而保证核心服务的可用性,是从系统功能的优先级角度考虑问题。
限流的目的是为了保证我们的系统不过载,当系统流量增大的时候,通过限流,可以保证能够在系统可承受的压力之下稳定运行,否则,一旦过载则会导致整个系统不可用。在我们实际应用中,到处都可以看到限流的一些策略场景,比如 Nginx 的 limit_conn 模块用来限制最大并发连接数、limit_req 模块用来限制每秒平均速率。
常见的限流方式可以分为两类:基于请求限流和基于资源限流。
基于请求限流。从外部系统的访问请求量的角度来考虑限流,常见的方式有:限制总量、限制时间量。基于资源限流。从内部系统的使用资源的角度来考虑限流,找到系统内部影响性能的关键资源,对其使用上限进行限制。常见的内部资源有:连接数、文件句柄、线程数、请求队列等。限流的维度和级别主要可以分为以下几类:服务级别维度的限流、接口级别维度的限流、业务细粒度级别维度的限流。
服务级别维度的限流、接口级别维度的限流主要目的是保护我们的系统,防止服务过载。业务细粒度级别维度的限流。主要目的是防刷,也是防止单用户或占用过多处理能力,影响其他用户。就是我们根据业务内部的具体维度去限流,比如我们可以通过 APPID 和 UID 这两个常见的业务维度。appid 就是对应某一类请求或者一个业务类型,UID 就是对应一个唯一的用户。首先,降级之前,一定是要先限流,限流之后,系统还是可能存在问题,才考虑降级。同时,降级不仅仅是开发人员的事情,还需要和产品人员沟通和协商降级后的处理以及对应的效果。
降级设计(Degradation)本质是为了解决资源不足和访问量过大的问题,当资源和访问量出现矛盾的时候,在有限的资源下,为了能够扛住大量的请求,我们就需要对系统进行降级操作。降级是指我们划分好系统的核心功能和非核心功能,然后当我们的系统超过最大处理能力之后,直接关闭掉非核心的功能,从而保障核心功能的可用。关闭掉非核心的功能后可以使我们的系统释放部分资源,从而可以有资源来处理核心功能。
当下游服务出现异常的时候(包括出错、超时),一般我们会进行短时间且少量的重试操作,但是如果重试几次依然无法解决,那么我们也不能一直重试,这样会给下游带来更大的压力。
熔断,就是在重试、限流等策略执行之后,还是无法解决的情况下的另外一种保护系统的策略手段,通过熔断,可以较好的保护好下游服务。熔断最重要的价值在于限制故障影响范围,通过熔断减少或拒绝请求从而有利于下游系统的恢复。
熔断系统设计的思路就是围绕熔断的三个状态来设计,一般我们通过三个模块(异常请求计算模块、熔断探测恢复模块、熔断记录和报警模块)来设计,主要的流程就是,在 Close 状态下,当我们的请求失败 N 次后,在 X 时间不再继续请求,实现熔断,进入 Half-Open 状态;与此同时,在 X 时间后恢复 M% 的请求,如果 M% 的请求都成功,则恢复到正常状态,进入 close 状态,否则再熔断 Y 时间,依此循环。如果一直无法恢复,那么可以直接进入 Open 状态。
熔断和降级这两个策略,看着比较像,字面的意思上来看都是要快速拒绝掉请求。但是他们是两个维度的设计,降级的目的是应对系统自身的故障,而熔断的目的是应对我们系统依赖的外部服务故障的情况。
微服务化后,服务可能会部署到多个不同的机房,也就是可能会部署到多个不同的集群上,为此,就有集群容错一说,集群容错里面,我们常见的两个策略是
failfast 策略,快速失败,只发起一次调用,失败立即报错。设计的时候需要注意控制超时时间的判断和记录:连接超时、响应超时failover 策略,失败自动切换,当出现失败,重试到其他集群的服务优先调用本地机房,失败也是本机房调用默认不 failover 到远程机房,开关开启的时候才启用一定比例的 failover 策略调用到远程机房需要根据错误码来区分不同的响应信息决定是否重试,比如404 400等情况是不需要重试的弹性扩缩容就要求应用服务设计为无状态模型,一般微服务框架无法实现弹性扩缩容,还需要配合其他的技术如容器技术(Kubernetes)、云服务技术等,这样才能实现弹性。
这里更多的是结合其他能力比如 K8s,特意单独提出来,是为了说明这个的重要性。
微服务需要一个统一的 API 网关,负责外部系统的访问操作。API 网关是外部系统访问的入口,所有的外部系统接⼊内部系统都需要通过 API 网关,我们一般的后端服务,外网是无法直接访问的,API 需要做一层处理,主要包括接入鉴权(是否允许接入)、权限控制(可以访问哪些功能)、传输加密、请求路由、流量控制等功能。一切 ok 后才能访问到我们的内网服务。
监控系统的开源代表作: Prometheus + Grafana,遵循 OpenMetrics 规范,基本数据格式分为 Gauge、Count、Summary、Histogram。在我们之前的实现中,公司也是采用普罗米修斯【prometheus】+ dashboard(grafana) 来实现整套监控系统的。
需要采集、监控的指标项有如下:
QPS组合查询 ,节点 ,服务,region,接口,时间 等latency组合查询 ,节点 ,服务,region,接口,时间 等SLA指定时间段内请求成功且耗时低于X毫秒的请求个数比例,API接口级别消费节点当前消费节点的状态,有多少个服务节点,健康状态,熔断状态等我们的一个业务服务,一般,都会有一个对应的配置文件,而且,我们需要尽可能的做到动态配置,而不是每次修改配置都要重新发布服务。比如说,我们定义好一个 Elasticserach 的账号密码,那么这个最好的方式,肯定是在配置文件里面,和代码隔离; 比如我们定义一个域名前缀,而这个前缀有可能会改变;比如我们定义一个请求的重试次数,这些,建议都是通过配置文件来管理。
每个服务一个配置文件,微服务化后,大量服务,就会有大量配置文件,那么这些配置文件怎么统一管理,怎么读取,怎么发布,就需要一个配置系统,配置系统也需要逐步演进为自动化治理方向,因此就需要一个完善的统一的自动化配置中心。
日志的重要性不言而喻,每次服务出问题,用户出问题,我们要查问题,必然要通过日志,而我们的服务这么多,部署的机器这么多,我们不能每次出问题之后,还要去每台机器上一个个的检索。为此,这个就必然需要能够将日志进行远程上报,然后统一收集和管理。
这里,远程日志组件的代表作是 ELK 系统:Elasticsearch, Logstash, Kibana。 通过引入 ELK,我们在代码里面,将日志上报,后续就可以通过 Kibana 去根据情况进行检索,然后拉取出对应的日志。
在微服务架构中,一个客户端请求的接入,往往涉及到后端一系列服务的调用,如何将这些请求串联起来?业界常用的方案是采用全局流水号【traceID】串联起来。通过全局流水号【traceID】,从日志里面可以拉出整条调用链路。
traceID 需要在所有日志里体现,包括访问日志、错误日志、警告日志等任何日志,唯一标识一个请求。调用链上每个环节的入口都先判断是否存在 traceID ,如果没有 traceID 则生成一个唯一值,存在请求上下文 context 中,输出日志的时候就可以把 traceID 打印并输出。然后调用其他服务的时候同时传递统一名称的header 给目标服务,目标服务处理请求是有获取到 traceID 就设置到自己的请求上线文,以此类推,整个请求链条就都使用统一的 traceID。同时 APP 客户端也可以传递一个自己的请求业务标识(可能不一定会唯一),服务端把传上来的请求标识作为 traceID 的一部分
一般公司的后端架构应用构建在不同的服务模块集上,可能是由不同的团队开发、使用不同的编程语言来实现、部署在多台服务器上。对于这种分布服务系统,传统查日志方式排查问题繁琐,用时较久。因此,需要一个工具能够梳理这些服务之间的关系,感知上下游服务的形态。比如一次请求的流量从哪个服务而来、最终落到了哪个服务中去?服务之间是RPC调用,还是HTTP调用?一次分布式请求中的瓶颈节点是哪一个,等等。如果我们需要跟踪某一个请求在微服务中的完整路径就要引入我们的分布式链路追踪系统了,目前业内分布式链路追踪系统基本上都是以 Google Dapper (中文版) 为蓝本进行设计与开发。
OpenTracing 制定了一套平台无关、厂商无关的Trace协议,使得开发人员能够方便的添加或更换分布式追踪系统的实现。在2016年11月的时候CNCF技术委员会投票接受OpenTracing作为Hosted项目,这是CNCF的第三个项目,第一个是Kubernetes,第二个是Prometheus,可见CNCF对OpenTracing背后可观察性的重视。比如大名鼎鼎的Zipkin、Jaeger都遵循OpenTracing协议。
然后时间走到 2022 年,OpenTracing 都已经快要过时, 现在最优的是 OpenTelemetry。OpenTelemetry 的终态就是实现 Metrics、Tracing、Logging 的融合,作为CNCF可观察性的终极解决方案,OpenTelemetry 可观测性领域的标准,OpenTelemetry 的三大数据模型如下:。
**Tracing:分布式追踪,观测请求调用链路。**提供了一个请求从接收到处理完毕整个生命周期的跟踪路径,通常请求都是在分布式的系统中处理,所以也叫做分布式链路追踪。**Metrics:指标监控,监控服务质量状况。**提供量化的系统内/外部各个维度的指标,一般包括Counter、Gauge、Histogram等。**Logging:服务日志,用来分析服务故障。**提供系统/进程最精细化的信息,例如某个关键变量、事件、访问记录等。在之前的文章里面,我们谈到,微服务和容器化结合是必然趋势,在当下,容器化平台基本都是采用 K8s ,那么基于 K8s 平台,我们的服务编排当然就需要围绕 K8s 来建设了。微服务框架 + K8s 容器平台 是当今互联网业务的黄金标准
如果是采用公有云的话,那么服务编排的管理平台,公有云已经帮你提供了。
如果是私有云的话,那么我们也需要基于 K8s 来搭建一套我们自己的服务编排的管理平台,用来帮助开发人员更好的去发布服务、线上管理服务。这个平台上的的功能可以包括:
服务发布和管理灰度发布服务重启/停止支持回滚服务登录登录到服务所在的容器里面服务扩缩容手动扩缩容自动扩缩容打通外部平台如监控平台、日志平台如服务限流和降级平台支持 debug 管理工具比如可以动态查看 go 相关的 runtime 信息,或者 pprof 等微服务体系下,有大量的服务需要进行运维管理,包括测试、发布上线、灰度放量、扩缩容等,那我们怎么合理的去处理好这些运维能力呢,这里就必须要有一套自动化运维管理体系,而这套东西,在业界,有一个叫做 DevOps 的文化,基于 DevOps 可以方便快捷的实现 CICD。CI 就是持续集成,是一种质量反馈机制,目的是尽快发现代码中的质量问题,CD 就是 持续部署,是指能够自动化、多批次、可控的实现软件部署,控制故障的影响范围或方便轻松的解决故障问题。通过 DevOps 的建设,可以实现软件生产流水线和软件运维自动化。
DevOps 平台这里,更多的集成代码管理平台、流水线构建、单测测试、接口测试判断等。比如我们开发完一个功能,那么代码首先要合入到 master,然后对代码进行一些检测,随后打 tag,构造镜像,最后发布。这一系列的流程,如果没有一个合适的管理平台,每个步骤都手动去处理的话,那么就会相当麻烦,而且耗时。
DevOps 平台的一些建议和实战经验:
代码分支合入 master 的时候,进行检测校验代码静态检查代码规范检查单测覆盖度检测接口测试检测检测通过后,可以合入 master 分支,然后自动打 tag,构造线上发布的镜像包镜像包构造完后,自动和服务编排管理平台打通,推送镜像到服务编排管理平台,自动触发发布流程发布流程需要有灰度的过程可随时中断和回滚微服务化会带来服务模块增多的问题,会在一定程度上带来开发成本的提升,所以需要通过配套的工具链来减少服务化后的开发成本。自动化测试平台的建设,可以有助于我们在开发测试阶段就尽可能的减少系统出现的问题,提供一层保障。自动化测试平台主要的目的是用来进行接口测试和接口拨测,后续也可以进一步去整合 流量录制和回放、全链路压测等相关功能。
自动化测试平台的主要的目的就是确保你上线发布的服务是正常运行的,而且尽量做到自动发现问题而不是通过人工反馈后才能知道问题。接口拨测这个事情,类似巡检,就是每个一段时间,自动调用你的后端接口,如果出现非预期的错误,那么就认为异常,可以进行告警。
由于这些都是基础功能,因此没有必要每个团队都各自搞一套,应该是公司内部统一搞一套这样的系统。
推荐阅读我的其他文章:
《高并发架构和系统设计经验》《TCP 长连接层的设计和 在 IM 项目中实战应用》《万字解读云原生时代,如何从 0 到 1 构建 K8s 容器平台的 LB(Nginx)负载均衡体系》《我终于统一了团队的技术方案设计模板》