服务端常见线上问题整理与解决措施
# 案例 1
案发现场
系统不断的查询某条特别慢 MySQL ,导致 MySQL CPU 密集,甚至飙到100%,直至整个 MySQL 集群不可用
事故现场解决方式:
- DBA 紧急用相应的工具 kill 该条查询请求
- 业务重启服务
- 业务在网关层(比如:kong),禁止该接口调用
反思总结:
- MySQL 慢查询日志告警,并且要不断治理
- 定期治理 MySQL 的所有执行语句,时刻关注 MySQL 的大表(超过500万条),对大表的所有查询语句要检查下是否有加索引。
- 引入 sentinel 流控平台,对 MySQL 的流量进行全局监控,每个 MySQL 方法都有设置默认超时、QPS以及相应的熔断机制
- 引入 sentinel 流控平台,对 HTTP 请求进行流控配置,每个 HTTP 请求都有默认超时、QPS以及相应的熔断机制
# 案例 2
案发现场
Elasticsearch 中的查询语句,使用 terms (类似 MySQL 的 IN)查询大量的商品 ID (或者订单 ID),导致慢查询耗时十几秒,进而引发调用的 Dubbo 线程池被打满(也有可能极大的增加 CPU 使用率)
事故现场解决方式:
- 重启业务服务
- 业务在网关层(比如:kong),禁止该接口调用
反思总结:
- 限制 terms 查询大量ID时,最大允许查询的ID个数
- 推荐用 multiGet(直接走文档索引,速度快) 的优化思路进行优化
- 每个 Dubbo 类级别设置默认的最大 dubbo 线程数(比如:30),Dubbo 整个线程池的默认大小是200,这样能够避免某个类的某个方法特别慢,导致整个 Dubbo 线程池被打满
- 引入 sentinel 流控平台,设置每个 Dubbo 方法的默认超时以及QPS,当然这里也可以设置并发线程数
# 案例 3
案发现场
业务依赖的非核心下沉服务挂掉了,导致业务一起抛异常
事故现场解决方式:
- 紧急修复代码,添加 try catch 逻辑,将异常捕获
- 催促调用的下层服务马上解决
反思总结:
- 对第三方的依赖没有梳理出,哪些是主干流程,哪些是枝干,主干流程中依赖或者使用的任务服务只要一出问题,整个服务都不能用,而枝干流程出问题,可能会降低用户体验,但是整个服务还是可用的,另外枝干流程还可以通过降级服务来进一步增加业务的高可用
- 引入 sentinel 流控平台
# 案例 4
案发现场
缓存redis(备注:只是做缓存,并未持久化)挂掉,导致业务不可用抛异常。
事故现场解决方式:
- 运维紧急重启或者排查缓存 Redis 的问题
反思总结:
- Redis 没有做高可用架构。(比如:使用 集群 + 主从结合模式)
- 业务设计上的缺陷,如果已经明确了Redis只是做为缓存使用,就应该在设计上要充分考虑缓存挂掉时,需要允许请求打到相应的持久层(比如:MySQL)
- 请求打到持久层(比如:MySQL),为了避免高吞吐的请求打挂 MySQL,需要在调用 MySQL 的DAO层上,加上流控(设置 超时、QPS、并发),从而确保 MySQL 不会挂
- 添加分布式锁保护持久层(注意:这里的分布式锁需要改造,大部分人容易严格按照超买超卖的逻辑实现,当出现较大并发时,并发中的后面的请求都超时了),正确的使用分布式锁的姿势:在并发过程中,当有一个线程持有该锁时,则去持久层获取数据,同时,其他的线程在锁超时前,不断的进行轮询缓存中是否有数据,一旦刚才那个持有锁的线程获取到持久层的数据,并且设置返回数据到缓存中,其他线程轮询到缓存也有数据,则所有并发的线程都马上返回数据,从而极大的减少并发过程中的请求超时问题。
# 案例 5
案发现场
利用canal 丢 binlog 到消息队列,业务消费消息队列,来更新业务缓存,业务强依赖缓存,当消息队列挂掉,业务的数据准确性马上出问题。
事故现场解决方式:
- 运维重启或者排查消息队列的问题,马上修复
反思总结:
- 运维这边需要重新评估消息队列的高可用方案
- 业务的设计架构有问题,业务想通过缓存来支撑高并发的场景,却没有考虑到业务依赖的组件哪些是核心的哪些是非核心的
- 添加动态开关,当消息队列挂掉后,将开关切换到,整个系统不使用缓存的状态(需要流控等对系统进行保护)
# 案例 6
案发现场
业务开发过程中,直接修改了旧的 dubbo api 的入参或者返回参数,业务先发下层服务,导致项目发布瞬间,上层的服务调用dubbo api 出错。
事故现场解决方式:
- 开发人员马上回滚发布上一版本的代码
反思总结:
- 对外暴露的接口,禁止修改入参,包括方法上的参数顺序以及参数类型
- 一般涉及到 dubbo api 的修改,都需要新增一个方法
# 案例 7
案发现场
某个对外提供服务的dubbo api由于数据库等原因,导致接口特别慢,此时请求的 QPS 仍然较大,导致 dubbo 线程池被打满。
事故现场解决方式: 与第一个案例类似。
反思总结: 与第一个案例类似。
# 案例 8
案发现场
更改了某个JAVA工程中缓存对象,并用这个缓存对象bean来设置和接收redis缓存,在发布服务的瞬间,由于线上的缓存还没过期,因此会用旧的缓存对象值来设置到相应的bean,导致数据不对。
事故现场解决方式:
- DBA 清理出问题的缓存 key 前缀
反思总结:
- 编码的时候,没有充分考虑线上的数据,以及上线的流程
- 跟缓存相关的bean,禁止进行修改,只允许新增字段,或者添加一个新的bean
# 案例 9
案发现场
某个dubo api 或者 http 对外暴露了一个整数值,正常情况下,业务只会传入小于100的值,系统正常,在某个时刻,业务突然传入1000000的值,导致系统cpu密集或者oom(这里面的数值:包括:分页等参数,以及一节对外暴露的数值参数)。
事故现场解决方式:
- 项目重启
反思总结:
- 本质问题,还是没有防御性编程的思想
- 对于 HTTP 以及 Dubbo 等对外提供服务的接口,你根本不知道调用方的参数会如何传,因此要将无界的参数控制在有界的范围内
# 案例 10
案发现场
某个dubo api 或者 http 对外暴露了一个字符串,正常情况下,业务只会传入小于5个字符,系统正常,在某个时刻,业务突然传入一个超过一万个字符的字符串,导致系统cpu密集或者oom(这里面的字符串:包括:名称、详细信息等)。
事故现场解决方式:
- 项目重启
反思总结:
- 本质问题,还是没有防御性编程的思想
- 对于 HTTP 以及 Dubbo 等对外提供服务的接口,你根本不知道调用方的参数会如何传,因此要将无界的参数控制在有界的范围内
# 案例 11
案发现场
某个核心业务由于线上的配置错误,比如把false 配置成fasle,导致Java代码层读取到的配置出错,进而引发线上事故。
事故现场解决方式:
- 将配置错误的 fasle 改成 false
反思总结:
- 核心重要的业务的发布流程一定要规范,任何细微的变更,都要在灰度环境(或者预上线环境)进行验证,然后才能在生产环境进行发布
# 总结
如何保证稳定
当一切不可控的时候,稳定性就无法保证。
一个稳定的系统,绝不是一蹴而就,而是不断地推销打磨,想尽各种可能地异常情况,自己给自己找问题,然后针对问题梳理出各种应对措施,要做到如何正确的处理这些问题,需要做到以下几点:
- 明确整个系统的各个业务的重要程度。
比如:订单系统中只有下单接口、下单日志接口两个接口,则下单接口重要程度 高于 单日志接口。在明确了重要程度后,需要通过各种措施保证,下单日志接口一定不要影响到下单接口,当系统要挂的时候,我们会让下单日志接口先挂掉。
- 明确项目中各个组件的重要程度。
比如:项目中有使用到 MySQL、ElasticSearch、Redis、Kafka,按照最小系统原则,我们明确了项目中只要 MySQL 不挂,只要我们能够确保在MySQL能够承受的流量范围内,此时,我们系统应该还是局部可用。