1概述
1.1背景
这是我曾经作为总架构师,参与规划设计的大型项目之一,以下项目名称简称A。
A微服务建设的最开始阶段,挑战最大的就是微服务拆分之后的稳定性问题,拆分之后遇到了链路复杂、上线质量难以保障,排查定位问题困难等一系列痛点,急需解决。因此我们在解决这些问题/痛点的过程中,制定了一系列解决方案,逐渐总结分析出相应的微服务实践经验,形成了体系化的稳定性保障机制。
1.2目标
微服务稳定性保障需要从事前、事中和事后全方位进行考虑。微服务架构下,应用程序、依赖服务、网络等都有可能出现故障,稳定性设计和保障的具体目标
如下:
1.事前:故障预防,尽可能减少故障的产生,绝大多数稳定性问题和稳定性故障发生都有一定的诱因,并且一般是在多种拦截手段均失灵的情况下故障才会
发生,如果我们在故障发生前制定完备的稳定性保障措施,可以最大限度地减少稳定性故障的发生。
2.事中:故障快速定位,完全不出故障的业务是不存在的,关键是出故障时能够快速发现故障,只有及时发现,才能在最短时间内采取相应的解决措施。
3.事后:故障快速止损,发生故障后第一时间要进行业务止损,恢复业务的正常运行,故障深层次的具体原因可以事后再分析和复盘解决。
2事前的故障预防
2.1架构层面的解决方案
应考虑从架构层面解决雪崩、突发流量、服务间接口调用不稳定等方面的问题。
2.1.1雪崩
服务化后,服务变多,调用链路变长,如果一个调用链上某个服务节点出问题,很可能引发整个调用链路崩溃,也就是所谓的雪崩效应。应从架构层面增加技术解决方案,避免出现雪崩效应。为了避免雪崩效应,
技术解决方案中包含的关键点主要是以下2点:
1)在服务间加熔断。解决服务间纵向连锁故障问题。比如在A服务加熔断,当B故障时,开启熔断,A调用B的请求不再发送到B,直接快速返回。这样就避免了线程等待的问题。当然快速返回什么,fallback方案是什么,也需要根据具体场景做调整,比如返回默认值或者调用其他备用服务接口。如果涉及的场景适合异步通信,可以采用消息队列,也会有效避免同步调用的线程等待问题。
2)服务内(JVM内)线程隔离,即解决横向线程池污染的问题。为了避免因为一个方法出问题导致线程等待最终引发线程资源耗尽的问题,可以对线程池进
行分组,每个线程组服务于不同的类或方法。一个方法出问题,只影响自己不影响其他方法和类。
2.1.2突发流量
面对突发流量的场景(比如积分大转盘活动),因访问量会短时间内猛增数倍,也会出现服务间接口调用超时、接口调用失败的问题,进而增加平台不稳定
因素。
针对这些情况,A微服务主要需要采取以下几个措施/步骤解决:
1)关键微服务(关键链路)的处理
需要清楚场景涉及的关键微服务具体是哪几个,对这几个微服务的并发访问量做预估,再根据业务需求评估场景流量需要的并发访问量,如果后者大于前者,
那么需要适当增加节点。尽量估值要准确,假如预估少了(实际访问量远大于预估的访问量),该场景涉及的几个关键微服务(关键链路)可能会扛不住压力而
出现故障。
2)非关键微服务(非关键链路)的处理
对于非关键微服务(非关键链路)要考虑做降级,如果访问量超出承载能力,可能要按照一定策略抛弃超出阈值的访问请求(也要注意用户体验,可以给用户
返回一个友好的页面提示),甚至关闭某些服务的接口(比如第三方代理服务)。同时,也需根据情况考虑增加限流处理。限流的两个主要目的:1,应对突
发流量,避免系统被压垮(全局限流和IP限流)2,防刷,防止机器人脚本等频繁调用服务(userID限流和IP限流)。限流的切入点可以从整个链路的多个环
节着手处理,比如前端流量入口(Nginx)、后端流量入口(网关)等。
3)对高频访问资源的处理
资源分静态资源和动态资源。对于高频访问的资源,如果不做特殊处理,可能因增加系统消耗,所以也需要采取一定技术手段降低系统消耗。对于静态资源,
尤其是音视频文件、分布式文件等可以通过CDN加速方式提供;对于动态资源,可以通过借助分布式缓存中间件解决,甚至建立多级缓存机制,缓解数据库压力。
2.1.3服务间接口调用不稳定
微服务间接口调用是非常普遍的情况,当出现微服务间接口调用频繁失败、调用超时等情况,均属于服务间接口调用不稳定的情况。针对这些情况,A微服务会从以下几方面着手解决:
1.持续维护微服务间接口调用矩阵
每次涉及新增微服务接口或微服务间接口调用关系改变的情况,均需要及时更新维护微服务间接口调用矩阵。通过查看微服务间接口调用矩阵,当每次接口有变更时,可以分析出各被调用接口的最大影响范围。
2.预先约定接口,接口联调前完成契约测试
通过在线接口说明文档(Showdoc)维护变更的接口定义说明,接口的开发方和调用方均已对接口定义达成一致,双方按约定进行各自的开发,且在双方进行接口联调前,各自完成契约测试。
3.做微服务接口调用的优化
有以下几种优化手段,可以做微服务接口调用的优化:
1)避免出现较多次数循环的微服务间接口调用,尽量改成分批次的循环调用
2)适当增加失败重试机制
3)对于数据时效性不高的接口,可以考虑对依赖接口返回的数据做缓存处理,或者通过引入消息队列实现异步调用。
4)对于微服务间出现双向耦合的情况,需要考虑微调微服务划分,由双向耦合改为单向耦合,或者借助消息队列实现上下游的解耦。
5)调优微服务的关键配置参数,比如JVM参数、线程数等,以提高微服务的并发承载能力。
随着微服务架构演进,也需要逐渐补充完善各类技术方案和机制,加强系统稳定性,比如定制化的灰度发布解决方案、多级缓存解决方案等。
2.2代码层面的解决方案
A目前了采取多种技术手段,尽最大可能避免微服务代码层面的质量问题,保障A微服务应用的上线质量。
微服务从对接需求、开发、到提测、上线的过程中,涉及到一系列规范的制定。
2.2.1对接需求环节
在对接需求环节,需要对业务设计的需求进行需求评审,在需求评审规范中应明确评审的关键点,合理的输入项范围和输出项范围。
2.2.2开发环节
需要制定一系列开发规范,约束代码风格,确保技术可控。开发规范主要包括Java开发规范、微服务开发规范、数据库开发规范、日志规范、代码评审规范等。
1)微服务开发规范中,包含接口规范、异常处理规范、异常返回值处理的规范、中间件使用规范(消息队列使用规范、缓存使用规范等)、各类命名规范等。
2)日志规范的制定,应考虑是否可以达到上线后快速排查定位的效果。
3)代码评审:在提测前,需要参照代码评审规范对新增微服务代码进行评审。
因此代码评审规范中应明确评审的关键点,比如慢SQL查询等内容。
2.2.3提测上线环节
在上线前,需要进行充分的自测与测试,包括功能性测试、接口间调用联调测试、前后端联调测试、覆盖率测试、性能测试、压力测试等,对于提测上线的
流程需要制定流程性规范。
应建立代码分支管理规范,明确分支合并、新功能开发、各环境bug修复的代码分支管理流程和使用规范;对于提测、上线的发版,也应建立版本发布规范,
在规范中应充分考虑版本升级、版本回退、发版失败情况的处理。以下几种情况,A微服务会在提测发版时格外注意:
1)代码搭车上线
非本次计划内上线的内容,被带到生产环境,搭顺风车上线。
2)服务回滚时遗漏回滚代码
因上线异常,某些服务需要做上线回滚,但回滚后没有第一时间把代码回滚掉,其他人上线时将未回滚的问题代码再次带上线,导致连续两天出现系统
故障。因此,服务回滚的时候,必须第一时间回滚代码,保证主线代码任何时候都是干净没有问题的。
3)服务启动或者回滚时间过长
某服务上线异常,回滚时因单个服务回滚时间太长,导致未能短时间内快速止损。经排查,回滚过程中部署系统和服务都存在耗时过长的现象,由于服务
回滚速度比较慢,产生了一定的线上服务故障。定期检查和优化服务的启动和回滚时间,保证出现故障时可以第一时间完成回滚操作。
4)配置文件缺少有效的校验机制
因配置文件的配置项填写错误,导致线上事故,因此需要建立严格的检查和校验机制。
5)小流量后的修改没有经过严格的测试和灰度验证
某服务经过小流量灰度后,代码又有少量修改,再次上线时未灰度,导致线上故障。再小的变更,都要进行测试、灰度和双重检查(doublecheck)。修改
一行代码,也可能导致线上的稳定性故障。
3事中的故障快速定位
当出现线上故障时,如何快速定位问题、找到问题的根本原因并且快速修复,也需要采取一系列的解决方案和技术手段。
3.1建立监控体系
A会将故障分析和定位时涉及的所有相关信息监控起来,构建完善的监控闭环,对系统层、应用层、服务访问层等维度进行监控收集和告警。
1)系统层的监控:系统层主要是指CPU、磁盘、内存、网络、带宽等的监控。
因为不是本文重点,所以暂不展开介绍。
2)应用层的监控:应用层的监控包括微服务、中间件、数据库、容器、容器编排等的监控。
3)服务访问层的监控:主要指的是API接口监控。
因为本文重点讲微服务的稳定性建设,所以只重点展开第2)点和第3)点。
3.1.1应用层的监控-微服务监控
关于应用层的监控,本文会重点介绍微服务的监控。
在A微服务体系建设中,需要应用服务治理完整解决方案,对微服务的服务注册情况、健康状况、微服务调用链路、接口间调用情况等均做监控。
1)微服务注册情况的监控
服务注册及发现为服务治理最为主要的环节,它支撑着服务之间调用规则。需要建立统一的注册中心,对微服务的注册与发现进行监控。
Eureka包含两个组件:EurekaServer和EurekaClient。
EurekaServer提供服务注册服务,各个节点启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。
EurekaClient是一个java客户端,用于简化与EurekaServer的交互,客户端同时也就别一个内置的、使用轮询(round-robin)负载算法的负载均衡器。
在应用启动后,将会向EurekaServer发送心跳,默认周期为30秒,如果EurekaServer在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除(默认90秒)。
EurekaServer之间通过复制的方式完成数据的同步,Eureka还提供了客户端缓存机制,即使所有的EurekaServer都挂掉,客户端依然可以利用缓存中的信息消费其他服务的API。综上,Eureka通过心跳检查、客户端缓存等机制,确保了系统的高可用性、灵活性和可伸缩性。
2)微服务健康状况的监控
为快速排查定位微服务运行时的问题,需要服务监控组件,能对注册于服务发现的所有微服务监控起来,收集并展示微服务运行时的各项指标,包括健康检查、JVM内存、INFO信息、获得线程栈和堆栈信息等。
3)微服务调用链路的监控
微服务化后,一次请求会跨多个服务,不容易快速排查定位出问题的接口,对于请求响应慢的链路也不容易排查定位,这时就需要分布式链路追踪解决
方案,能够追踪整个调用链路的工具,协助排查问题。
4)接口间调用情况的监控
微服务架构中为了保证程序的可用性,防止程序出错导致网络阻塞,出现了断路器模型。断路器的状况反应程序的可用性和健壮性,它是一个重要指标。
当微服务数量很多时,监控非常繁杂,监控内容也较为分散.为了同时监控多个服务接口调用的熔断状况,需要增加熔断监控组件对所有接口间调用情况进行
统一监控,并实时反馈接口调用状态,状态主要包含以下几种:
1>.执行成功,
2>.熔断
3>.请求失败
4>.超时
5>.线程池拒绝
6>.执行异常
3.1.2服务访问层的监控
对服务访问层的监控,主要指API接口的监控,A主要做的监控包括API接口的活性检测,API接口调用情况统计等。
1)API接口的活性检测
我们自研了自动化程序,定时(每5分钟)轮询对所有微服务接口做活性检测。做活性检测的过程,引入失败重试机制(重试3次),收集连不通的接口信息,并向运维人员发送告警邮件。
2)API接口调用情况统计
通过在网关增加接口调用过滤器,实现埋点解决方案等多种方式,对API接口的调用情况做统计。统计过程,也会区分PC端和APP端等多终端的接口调用
情况。
3.2定制化日志系统
因为微服务是分布式的,且相互隔离的,它们不共享日志文件,需要建立一套统一的日志系统集中存储各微服务的日志。
A在建设日志系统时,有以下几个最佳实践:
1)设计一套分布式日志架构
在充分考虑海量日志的并发量、日志系统稳定性情况,我们设计了一套适合A系统的分布式日志架构。
2)用唯一性ID来关联各个请求
每一次微服务调用的完整请求链可能涉及多个微服务的接口调用,每个微服务的接口调用都会打印好几条日志信息,每一个调用分配一个唯一性的ID,即
全局唯一请求ID,以便标识出每一次请求。当问题排查定位时,通过此全局唯一请求ID,可以查找到该次请求的所有日
志。
3)对日志数据做结构化处理
为了快速找到帮助排查定位的日志,需要为日志数据构建成一套统一的标准格式,如:JavaScriptObjectNotation(JSON),来简化解析日志的过程。JSON允
许您拥有多层次的数据。在必要的时候,也可以单个日志的事件中获取更多的语义信息。
4)对日志信息做索引拆分处理
A每日的日志量较大,线上运行时间一长,查询结构化日志时就会较慢,日志文件的处理也会非常缓慢。为此我们做了一定程度的优化,为每个微服务建立独立的日志索引,根据索引做微服务日志拆分。
3.3维护问题处理台账
对于线上出现的故障、问题,建立了“A线上问题(事件)处理台账”,记录“问题(事件)的描述”、“问题的应急解决/处理方案”、“问题的应急处理人员”等信息,在历次问题解决中不断补充完善。
从管理角度,也应建立应急保障机制,明确各相关方和职责,以及相关方的应急措施。4事后的故障快速止损。在每次线上故障、线上问题紧急解决后,A均对出现过的问题/故障做了充分分析和复盘:
1)对每次故障的原因做深层次分析,找出问题的关键点和根本解决方案
2)分析此问题是否是面积式的问题。如果存在,也要紧急修复并紧急上线
3)定期组织模拟演练,对识别到的薄弱环节也会紧急加固
5总结
A平台一直致力于平台稳定性建设,其中微服务的稳定性建设尤为关键。
我们最初也经历了各种难以言说的痛,可以说一步一个坑,一路走来,也摸索出一些稳定性建设的经验,分享出来。
现在A平台逐渐走向稳定,我们仍然会继续初心不改,砥砺前行。