简介:本篇是整个《如何流量无损的在线应用架构》系列的第二篇,这一系列共三篇,旨在使用最为朴素的语言将影响在线应用流量稳定性的技术问题做一个归类,这些问题的解决方案有的只是一些代码层面的细节,有的需要工具进行配合,有的则需要昂贵的解决方案,如果您的应用想在云上有一个【流量无损】的一站式体验,可以关注阿里云的《企业级分布式应用服务(EDAS)》这个云产品,EDAS 也将会持续向默认接入流量无损的方向演进.下一篇,我们将从数据服务交换的角度进行讲解,更重要的是下一章还会点出重点预防的两把钥匙。
作者 | 孤弋、十眠
前言
上一篇如何构建流量无损的在线应用架构 | 专题开篇 是我们基于下图,讲解了流量解析与流量接入两个位置做到流量无损的一些关键技术,这一篇我们主要从流量服务的维度,详细聊聊正式服务过程中会影响线上流量的一些技术细节。
服务
最近我们分析了某大型互联网公司最近一年的线上故障原因,其中,产品自身质量问题(设计不合理、BUG 等)占比最高,为 37%;由于线上发布、产品和配置变更引起的问题占比 36%;最后是依赖服务的高可用类的问题如:设备故障,上下游的依赖服务出现问题或者资源瓶颈等。
基于左侧的原因分析可以看出,如何治理好变更、产品质量、和高可用类的问题,是服务如何做到流量无损的关键所在。我们把这个几点原因结合应用的生命周期划分成为了几个关键阶段:
- 应用变更态:当我们的服务(应用)进行版本发布、配置变更的整个过程中,我们需要有确切的手段做保护。
- 运行服务态:当我们的变更完毕后,一开始是一个 “冷”的状态,如何在正常流量甚至超常规流量到来之前的,让系统平稳度过?
- 高可用依赖态:当我们服务的某些节点出现问题,或者我们外部的依赖(如 其他微服务、DB、缓存)等出现瓶颈时,是否有相对应的办法?
为了从容地应对以上这些场景,我们将分别列举相关例子来探讨相应的解决方案。
1、变更态:应用优雅下线
应用变更时有一步,是需要将原有的应用停止。而在生产环境中的在线应用停止之前,需要将服务流量的服务下线。我们常说的流量主要有两种:1)同步调用的流量,如:RPC、HTTP 请求等;2)另外一种是异步流量,如:消息消费、后台任务调度等。
以上两种流量,如果在服务端还存在未处理完的请求时将进程停止,都会造成流量损失。要想解决这一种情况,通常情况需要分两步进行:
1)将现有节点在相应的注册服务中摘除,场景有:将 RPC 服务在注册中心的节点中摘除、将 HTTP 服务在上游的负载均衡处摘除、将后台任务(消息消费等)的线程池尝试关闭,不再进行新的消费或服务。
2)停顿一段时间(依据业务情况而定),确保进程中已经进来的流量能得到很好的处理之后,再将进程关闭。
2、变更态:应用调度
变更过程中另外一个动作就是选择资源(机器或容器)之后发起一次部署,如何选择资源就是我们通常意义上理解的【调度】,如果是传统的物理机或者调度能力欠缺的虚拟机的情况,调度这一层没有太多的发挥空间,因为他的资源基本上都是固定的;但是容器技术(尤其后来的 Kubernetes 技术的普及)的出现除了给交付领域带来了诸多的变化之外,他也给调度领域带来了的不一样的故事,即他从传统的规划资源分配引领到了灵活调度的时代。
在 Kubernetes 中,默认可以根据应用使用的资源(CPU、内存、磁盘等)信息,来选择一个最为合适的节点进行调度。如果我们更进一步,也可以根据自身应用的流量特征定制一个调度器,比如尽量让流量大的应用不聚集在相同的节点上,这样就能避免因为抢占带宽而造成的流量损失。
3、变更态:应用优雅上线
当应用调度到了相应的资源之后,接下来是部署和启动应用。和应用停止的场景类似,应用在完全初始化完成之前,我们的节点很可能已经被注册,后台任务的线程池也很可能开始启动了。此时上游服务(如 SLB 开始路由、消息开始消费)就会有流量调度进来。但是在应用被完全初始化完成之前,流量的服务质量是无法得到保证的,比如一个 Java 应用启动之后的前几次请求,基本上是一个“卡顿”的状态。
如何解决这个问题呢?和应用的优雅下线动作序列相反,我们需要有意识的将服务注册、后台任务线程池、消息消费者线程池的初始化动作滞后。要确保等到应用完全初始化之后再进行。如果有外置的负载均衡路由流量的场景,还需要应用部署的自动化工具进行相应的配合。
4、变更态:应用服务预热
系统完成上线之后,有时如果遇到流量突增,可能会令系统水位瞬间升高进而导致崩溃。典型场景如大促时的零点,洪峰涌入时,应用实例会瞬间进入大量的流量,这些流量会触发诸如 JIT 编译、框架初始化、类加载等底层资源优化的问题,这些优化会在短时间之内给系统造成高负载的问题,进而造成业务流量损失。为了解决这个问题,我们需要控制流量缓慢增加,通过开启类加载器并行类加载,框架提前初始化,日志异步化等方式提升刚启动应用的业务容量,从而实现大流量场景下的扩容、上线等操作的流量无损。
5、变更态:Kubernetes 服务结合
从 2020 年开始,我们看到一个明显的趋势就是 Spring Cloud + Kubernetes 已经成为了微服务体系中最流行的配搭。而在一个基于 Kubernetes 构建的微服务体系中, 如何将微服务体系和 Kubernetes 进行有效的结合是很有挑战的一个点,Kubernetes 中的 Pod 生命周期管理本身就提供了两个探测点:
- RreadinessProbe,用于探测一个 Pod 是否就绪接受流量,探测失败将会在 Kubernetes Service 中摘取该节点,且该节点的状态为 NotReady 。
- LivenessProbe,用于探测 Pod 是否健康,如探测失败将会重启 Pod。
如果我们的应用没有配置 readinessProbe ,默认只会检查容器内进程是否启动运行,而对于进程中运行的业务是否真的健康是很难考量的。在发布的过程中,如果我们使用的是滚动发布策略,那么当 Kubernetes 发现新起的 Pod 中的业务进程已经启动了,Kubernetes 就会开始销毁老版本的 Pod,看起来这个过程是没有什么问题的。但我们仔细想一下,“新起的 pod 中的业务进程已经启动”,并不代表“业务已经启动就绪”,有的时候如果业务代码存在问题,那么我们的进程启动了,甚至业务端口也已经暴露了,但是由于业务代码等异常情况,导致进程起来后服务还没来得及注册。可此时老版本的 Pod 已经销毁。对于应用的消费者来说,可能会出现 No Provider 的问题,从而导致在发布的过程中出现大量的流量损失。
同样,如果我们的应用没有配置 livenessProbe ,Kubernetes 默认只会检查容器内进程是否存活,而当我们的应用的某个进程由于资源竞争、FullGc、线程池满或者一些预期外的逻辑导致其处于假死的状态时,进程虽然存活,但是服务质量低下甚至是为 0。那么此刻进入到当前应用的全部流量都会报错,出现大量的流量损失。此刻我们的应用应该通过 livenessProbe 告诉 Kubernetes 当前应用的 Pod 处于不健康且已经无法自己恢复的状态,需要对当前 Pod 进行重启操作。
readinessProbe 和 livenessProbe 的配置,目的是及时且灵敏地反馈当前应用的健康情况,以此来保证 Pod 内的各个进程都处于健康状态,从而保证业务的流量无损。
6、变更态:灰度
一次版本的迭代,我们很难保证新的代码经过测试后,在线上就没有任何问题。为什么大部分的故障和发布相关?因为发布是整体业务发布到线上的最后一个环节,一些研发过程中累计的问题,很多时候最后发布环节才会触发。换句话说,一个潜规则就是公认线上发布基本上不可能没有 BUG,只是大小而已,但是发布环节要解决的问题就是:既然肯定会有问题,那如何将问题的影响面降至最小?答案是灰度。如果有一些没有测试到的问题,恰巧我们线上也是全量一批发布的,那么错误将会因为全网铺开而被放大,出现大量且长时间的线上流量损失。如果我们系统具备灰度能力(甚至全链路灰度),那么我们就可以通过灰度发布的方式将问题的影响面控制到最低。如果系统具备完整的灰度过程中的可观测能力,那么发布就会稳定且安全得多。如果灰度能力可以打通全链路流程,那么即使是同时面对多个应用的发布都可以有效保证线上流量无损。
7、运行态:服务降级
当应用遇到业务高峰期,发现下游的服务提供者遇到性能瓶颈,甚至即将影响业务时。我们可以对部分的服务消费者进行服务降级操作,让不重要的业务方不进行真实地调用,直接返回Mock的结果甚至异常返回,将宝贵的下游服务提供者资源保留给重要的业务调用方使用,从而提升整体服务的稳定性。我们把这个过程叫做:服务降级。
当应用依赖的下游服务出现不可用的情况,导致业务流量损失。您可以通过配置服务降级能力,当下游服务出现异常时,服务降级使流量可以在调用端 "fail fast",有效防止雪崩。
8、运行态:自动离群摘除
与服务降级类似,自动离群摘除是在流量服务的过程中,遇到单个服务不可用时自动将节点进行摘除的能力,他区别于服务降级主要是体现在两点:
1)自动完成:服务降级是一种运维动作,需要通过控制台进行配置,并且指定对应的服务名才能做到相应的效果;而【自动离群摘除】能力是会主动探测上游节点的存活情况,在这条链路上整体做降级。
2)摘除粒度:服务降级降级的是(服务+节点 IP),以 Dubbo 举例子,一个进程会发布以服务接口名(Interface)为服务名的微服务,如果触发到这个服务的降级,下次将不再调用这个节点的此服务,但是还是会调用其他服务。但是【离群摘除】是整个节点都不会去尝试调用。
9、高可用:注册中心容灾
注册中心作为承担服务注册发现的核心组件,是微服务架构中必不可少的一环。在 CAP 的模型中,注册中心可以牺牲一点点数据一致性(C),即同一时刻每一个节点拿到的服务地址允许短暂的不一致,但必须要保证的是可用性(A)。因为一旦由于某些问题导致注册中心不可用,连接他的节点可能会因为无法获取服务地址而对整个系统出现灾难性的打击。除了常见的高可用手段,注册中心特有的容灾手段还有:
1)推空保护:数据中心网络抖动或者在发布的过程中,会经常出现批量闪断的情况,但这种情况其实不是业务服务的不可用,如果注册中心识别到这是一种异常情况(批量闪断或地址变空时),应该采取一种保守的策略,以免误推从而导致全部服务出现"no provider"的问题,所有微服务会因此导致大量的流量损失。
2)客户端缓存容灾:与推空保护一样,站在客户端的角度逻辑同样适用,我们经常遇见客户端和注册中心出现网络问题时将地址更新的情况,客户端也不能完全相信注册中心反馈的所有结果,只有明确告知的是正常的结果才能将内存中的地址更新,尤其遇到最后一个地址时采取的策略更要慎重;同时拿到地址之后,也不能完全相信,因为很可能注册中心推送下来的地址根本就不可达。此时要有类似于心跳保活的策略可动态调整对端服务是否可用,以免将服务直接发往无法连接的地址导致流量损失。
3)本地缓存容灾:注册中心容灾了,客户端也容灾了是否足够?通常情况如果不做变更是足够的,但是如果有一个应用在进行变更时注册中心不可用的话,会发生什么事情呢?一个是本身地址注册不上,另外一个就是如果有服务发生依赖调用时,流量进来后会出现"no provider"而导致流量损失,甚至根本就无法启动。如果我们把注册中心的依赖简化理解为就是对一个服务名字的地址解析的话,其实我们可以将这个解析结果保存在本地做成容灾备份,这样就能有效避免在变更过程中因为注册中心不可用而导致流量损失。
10、高可用:同城多机房容灾
同城的特点是 RT 一般处在一个比较低的延迟(< 3ms 以内),所以在默认情况下,我们可以基于同城的不同机房搭建起来一个大的局域网,然后把我们应用跨机房分布在多个机房中,以此来应对单机房出现故障时的流量受损风险。相比异地多活,这种基础设施的建设成本较小,架构变动也比较小。不过在微服务体系之下,应用之间的链路错综复杂,随着链路深度越来越深,治理的复杂度也会随之增加,如下图所示的场景就是前端流量很有可能因为在不同的机房相互调用而导致 RT 突增,最终导致流量损失。
要解决上面的问题,关键是在服务框架层面需要支持同机房优先路由的能力,即:如果目标服务和自己所在机房相同,则优先将流量路由至和我同机房的节点。要实现这个能力的方法大致是注册服务时将自身所在的机房信息上报,机房信息也当成元信息推送至调用方,调用方在路由时通过定制服务框架的 Router 的能力,优先选择和自己相同机房的地址作为目标地址路由。
结语
本篇是整个《如何流量无损的在线应用架构》系列的第二篇,这一系列共三篇,旨在使用最为朴素的语言将影响在线应用流量稳定性的技术问题做一个归类,这些问题的解决方案有的只是一些代码层面的细节,有的需要工具进行配合,有的则需要昂贵的解决方案,如果您的应用想在云上有一个【流量无损】的一站式体验,可以关注阿里云的《企业级分布式应用服务(EDAS)》这个云产品,EDAS 也将会持续向默认接入流量无损的方向演进.下一篇,我们将从数据服务交换的角度进行讲解,更重要的是下一章还会点出重点预防的两把钥匙。
原文链接
本文为阿里云原创内容,未经允许不得转载。