项目是一个在运营中的项目,在后续新功能开发的时候线上总是出现各种问题,让客户对项目的信任度降低很多,也对公司造成了经济损失。于是就有了一个任务,提高项目的可用性。经过一段时间的实践,总结出来了一个公式
提升系统的稳定性=减少故障的数量+提升发现速度+提升恢复速度
减少故障的数量
线上的故障主要分为依赖类故障、变更类故障、容量类故障、固件类故障
依赖类故障
在现在分布式架构盛行的背景下,一个微服务的直接和间接依赖是非常多的。下游的某个服务、缓存、DB如果挂了,自己就会被“牵连”,无法提供正常服务。
下游服务异常
下游服务异常的时候会造成服务异常,那我们可以通过降级和流控来处理
可以使用Sentinel来进行降级和流控,Sentinel是面向分布式服务框架的流量控制组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。
Sentinel具有以下特征:
(1)丰富的运用场景
(2)完备的实时监控
(3)广泛的开源生态
(4)完善的SPI扩展点
降级Degrade
所谓【降级】,就是对下游的依赖从强依赖变成弱依赖。
熔断策略
- 慢调用比例(平均响应时间):选择以慢调用比例作为阈值,需要设置允许的慢调用RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。档单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测回复状态(HALF-OPEN状态),若接下来的一个请求响应时间小于设置的慢调用RT则结束熔断,若大于设置的慢调用RT则会再次被熔断。
- 异常比例(ERROR_RATIO):当单位统计时长(satIntervalMs)内请求数且大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是[0.0,1.0],代表0%-100%。
- 异常数(ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
流控SentinelResource
流控又分为限流,以及控流。
隔离
限流
限流的定义就是只允许指定的流量通过,举个例子就是有50个苹果,一天只能吃10个,把剩下丢掉就是限流
常见的实现方式包括【令牌桶限流】【计数限流】【线程池控制】等。
- 令牌桶限流 每分钟生成多少个令牌,拿到令牌的请求就继续,没有获得到令牌的直接抛弃掉
- 计数限流 期初设置一个阈值比如5,来一个请求先判断这个值是否大于0,大于0则-1,等于0则抛弃请求,处理完了+1。集群计数就面临着网络开销,就算中间件再快,网络总在那里。作为一个计数服务,可想而知请求量是怎样的。
- 线程池控制 类似计数限流,用的时候取一个,用完还回来。相信大家对线程池都比较了解了,就不再往下细说。
【计数限流】和【线程池控制】本质上是控制并发。
假设每个请求平均是10ms返回,线程池设置的数量是5。1秒钟=100个10ms,每个10ms有5个线程可以处理5个请求,那1秒就可以处理100 * 5 = 500个请求,qps=500。
所以,控制计数和线程池本质上是控制并发,不要和QPS搞混了。
控流
控流的定义就是允许流量通过一定的速度通过。举个例子就是有50个苹果,一天只能吃10个,把剩下的存起来分5天吃,常见的队列就是这种形式
总结
这样就可以很明显的看到限流和控流的差别,限流直接将多余的流量抛弃,而流控是将多余的流量保存起来,慢慢处理。所以使用限流还是控流可以按照业务场景来进行选择,如果业务的请求可以直接抛弃就用限流,如果类似于转账之类的关乎到收入的业务还是选择控流更合适
缓存异常
限流防止穿透,集群防止雪崩。
DB宕机
对DB进行高可用建设,
变更类故障
变更不只是【代码变更】,还包括【配置变更】和【数据变更】。这些变更,无论是参数改错了,还是DB数据订正错了,都可能导致严重的故障。
在各种系统中有大部分是变更类故障,而变更类故障各式各样,解决起来也不能使用同一种方案处理,并且无法根本解决,下面从代码、配置、数据这三个方面讲一下我们要怎么做
代码
代码的变更基本上每个需求的更新都会产生,在刚到公司的时候最怕的就是变更,每次总是会出现各种的问题,代码版本问题,发布的代码版本不对,甚至出现过线上跑的版本落后现有版本1年的情况。通过观察发现出现这些情况的原因有手工发布代码,代码提交混乱,代码冲突,新旧版本兼容度问题。于是通过制定代码管理规范、自动发布,加强测试规范,对接口进行兼容性硬性规定来处理这些问题
代码管理规范
首先是对代码分支的管理,避免出现线上代码分支不对的情况,也防止测试代码和实际发布代码不一致的问题出现
自动发布
在比较常见的问题中有一个手工打包,复制到服务器并启动,每个人理解不一样及时有sh脚本辅助也难免会出现打包打错了,目录放错了等问题出现,为了避免出现类似的问题,引入Jenkins来做自动构建发布部署。
Jenkins可以最大程度的保证每次打包的环境一致性,以及可以提供快捷的回滚
加强测试规范
从测试目的来看,测试的目的有三块【测试新功能是否正常】【测试老功能是否正常】【测试服务的兼容性】,针对目的可以得出测试其实可以分为三大块
- 功能测试 针对这次新增或修改的功能。
- 回归测试 针对老功能的验证
- 兼容性测试 验证新旧功能同时存在时的正确性。为什么要做兼容性测试,当程序发布出现异常时需要回滚如果没有兼容性那就会在发布时出现异常,还有当程序发布时程序没有兼容性就需要按照固定的顺序来发布如果出现循环依赖那在发布时就会出现异常
代码审核
代码审核的目的是**找出安全、性能、依赖和兼容性等测试不易发现的问题。及时识别出代码设计的缺陷,找到需要重构的地方**
防止出现一些常见的错误写法导致泄漏问题的出现。
配置文件
是不是经常听到:测试环境就可以呀,为什么到了线上就不行了。当你听到这句话的时候大概率就是配置文件在作祟了 。
配置文件的问题在开发周期长的项目中尤其容易出现。在避免出现配置文件出现问题这个到现在也没有找到100%避免的方法,是能通过流程来尽量的避免问题的出现。
- 要求开发写需求的上线文档,在文档中标注上线服务的前后顺序,配置文件,需要执行的sql
- 在提测时要求测试按照开发写的文档对测试环境的配置文件进行配置
- 功能测试完毕后,预发布环境按照文档再次进行配置
- 上线的前一天开上线大会对上线文档进行复盘
通过多次的确认来防止配置异常的问题。
数据
主要是Flyway来做数据库管理,Flyway 是一款开源的数据库版本管理工具,它更倾向于规约优于配置的方式。Flyway 可以独立于应用实现管理并跟踪数据库变更,支持数据库版本自动升级,并且有一套默认的规约,不需要复杂的配置,Migrations 可以写成 SQL 脚本,也可以写在 Java 代码中,不仅支持 Command Line 和 Java API,还支持 Build 构建工具和 Spring Boot 等,同时在分布式环境下能够安全可靠地升级数据库,同时也支持失败恢复。主要步骤为
- 开发编写脚本,并分别拷贝到dev,test,prod三个配置文件中指定的文件夹下,并需要根据配置文件中的语句在上线文档中预留回滚脚本
- 测试在部署时会验证一次语句的正确性
- 在上线大会对prod进行一次确认
通过工具+人工验证来保证正确性
具体可以参考
容量类故障
容量指的是服务器的磁盘 cpu 内存 各种连接池因不同的原因满足不了业务的需求,比如业务流量突然增加,线上队列数据堆积,日志输出增加、图片数量堆积等都可以造成故障,使服务无法提供正常的服务,在主流程的接口或者方法需要修改时一定要注意兼容性的问题.
业务流量的突然增加:
日常中并不是很常见,一般只有做类似于秒杀场景的时候可能会遇到,遇到类似的情况我们需要及时的做好压测,做好容量评估。上线后对服务器进行监控
线上队列数据堆积:
线上数据堆积也可能会出现磁盘被占满的情况,可以调整队列配置,设置合理的磁盘空间,并监控队列的使用情况,并及时的报警。队列的堆积很大的可能是因为下游消费端出现了问题,早日报警由人员的介入可以早日解决问题。
日志输出增加、图片数量堆积:
当我们使用docker的时候如果docker的日志文件并未做限制的时候,经过时间的更迭,docker容器占用的磁盘会越来越多。要解决这个问题需要限制docker容器的日志大小,如果应用需要留存日志等文件,可以通过挂载的形式将文件挂载到宿主机。
图片或者上传文件的问题,可以通过分布式存储模式把文件分片存储防止出现类似问题
总结
容量类故障可以通过运维手段来进行缓解,但是主体还是需要做好监控告警,并对可以预想到的问题进行充足的压测,并预留好容量
固件类故障
在现实社会中总是充满着意外,网络有可能会中断,服务器硬件可能会损坏。多机备份是解决这类问题的办法,经验不多,这里就不深聊此类问题
提升发现速度
提升发现速度主要是通过监控,监控又可以分为运维监控和变更监控
运维监控
运维监控告警可以通过Prometheus+Grafana来实现,而做好监控要从不同的维度进行
机器维度:
机器维度的监控指标包括CPU、Load、内存、网络、IO、磁盘等相关指标,可以通过设定这些指标的异常值,通过触发来进行告警。
应用维度:
应用维度的监控指标包括JVM使用情况、线程池使用情况:JVM情况主要包括YGC次数、时间,FullGC次数、时间,新生代老年代占比;线程池情况主要包括的线程池大小、最大线程数、活跃线程数、队列大小等。
服务维度
服务维度的监控指标包括error日志报错情况、服务接口调用量、耗时、成功率,调用接口调用量、耗时、成功率,dal层操作调用量、耗时、成功率。
外部依赖维度
外部依赖维度主要指应用系统常见的外部依赖的监控情况,主要包括数据库、缓存、消息队列等,这些一般情况都会独立进行部署,对应的机器监控同上面列举的机器维度监控;另外数据库还需要关注连接数、内存使用、SQL调用量、耗时、成功率,慢SQL等;缓存需要关注调用量、成功率,命中率、内存使用等;消息队列需要关注调用量、成功率,队列积压情况、死信队列等
变更监控
变更监控可以更加细分一下分为【对旧:观察异常运行情况】、【对新:观察更新是够生效】
对旧:观察异常运行情况
实现观察异常运行情况主要是观察变更之后,没有变更的服务是否有心得异常发生,可以通过日志埋点,关键业务逻辑检测,接口埋点的情况,通过规则判断是否受到影响,如果有影响需要尽快的修复
对新:观察更新是够生效
观察更新是否生效可以从检测新接口的各项指标。新接口打点的日志输出情况来确认,因为功能的大小,无法有效的指定详细的规则,一般会通过人工对频率和异常进行分析最后得出结论
慢查询监控
在运维监控中其实是包含了慢查询监控的,但是这里还是需要强调一下,服务运行中,当数据慢慢增多出现慢查询的可能性逐步增大,日常中需要进行优化的地方多出现在这里。所以在日常中更需要对慢查询格外的关注,根据不同的数据库有不同的统计方法,mysql可以通过配置开启慢查询监控
注意
在监控告警中有一个问题需要特别的注意,就是告警疲劳,狼来了的故事告诉我们只有真正发生了问题才应该告警,而不是频繁的告警当真正的问题出现时无法正确的处理
提升恢复速度
借用蚂蚁的变更三板斧【变更可监控】【变更可回滚】【变更可灰度】,其中变更可监控是用来提升发现速度的,而提升恢复速度主要是变更可回滚。
提升恢复速度也可以分为运维期恢复,变更期恢复
运维期恢复
应急处理
运维期间的恢复在排除容量类问题,大部分是可以通过对应用进行重启来解决的,所以定时探测+脚本重启服务可以作为一种应急的处理方法,可以自己手写脚本,也可以使用k8s。
冗余
也可以启动多份服务,当一个服务因为一场原因宕机后,将流量打入其他存活的服务中
变更期恢复
变更可回滚
大家可能会问:回滚不就是用原来的代码重新部署一遍就行了吗?
回顾上面的更新故障解决部分的时候,可以看到很多回滚的字样,整理一下回滚中可能存在的问题,并思考一下怎么解决
发布依赖
当我们需要考虑发布顺序的时候,就会遇到发布依赖的问题。当服务A更新完之后才能更新服务B时,回滚就需要服务B回滚完毕然后再回滚服务A,这时服务A就无法快速回滚了。
数据异常
当变更有数据结构变更的时候,这时老的系统如果不兼容现有的数据结构就会阻碍回滚,在更新故障解决部分我们要求更新时提供数据库回滚语句的原因就是这个,但是回滚了数据结构后,在回滚期间还是会产生脏数据,这些脏数据可以处理但是需要时间,这就无法实现快速回滚的诉求了
解决
【新数据在旧代码中兼容】
更好的兼容性设计,在设计新功能的数据库变更的时候要考虑到旧版本对新版本的数据结构支持,比如新增功能与原来功能关联时新增关联表,需要修改表内容时增加列冗余
【使用开关控制回滚】
“部署”与“生效”分离。相比代码回滚,使用开关回滚会快很多。
此外,使用开关回滚的方案,也可以解决上线代码被多次覆盖后不可回滚的问题。
变更可灰度
在使用开关控制回滚提到了部署与生效隔离,灰度就是分离很有效的手段。当服务A和服务B上线时我们在灰度环境中部署一套新版本的服务A和服务B,这时我们可以设置公司内部环境为灰度环境,当我们测试有异常时我们可以将灰度关闭,这样流量就不会进入灰度环境,以实现快速回滚。同样的例子我们可以把灰度服务逐步开放,一旦发现服务异常可以直接关闭灰度环境实现快速回滚。
而且可灰度还可以分开观察新版本和老版本的差异,
多次修改后的系统也可以回滚,如果单纯的代码回滚如果更新多次的服务可能就无法回滚了