高可用编程

常见的稳定性问题

一、内存

1. 数据库返回内容过多

特别注意那些未限制返回条数的sql, 是否存在某天突然返回大批量数据的可能性,建议加防御性的分页或者limit限制,返回条数建议不要大于1000条。在实际的场景中,一般都会进行limit的配置化,如配置在阿波罗中,这样方便动态调节。

2. 上传的文件过大过多

上传的某个文件是否有变大的可能性, 是否存在很多人同时上传文件的可能性, 大文件,不要一次性全部读入内存后再逐一处理。建议一条条或一批批读取,处理完一批再读一批,最好采用异步任务处理。

3. 导出的文件过大过多

导出的文件是否存在数据量过大的可能性,是否存在很多人同时导出的可能性,大文件建议分批从数据库读取分批append到文件,不要把所有数据缓冲到内存中,最好采用异步任务处理。

4.ThreadLocal局部变量未释放、连接未在用完关闭等

大部分容器采用线程池的方式来实例化和管理线程,线程会被反复重用, 所以保存到线程局部变量里的内容在使用完后一定要释放,否则很容易导致内存溢出, 如logback的MDC。

5. 本地缓存

本地缓存是否有限制缓存总量或者有驱逐策略,否则未预料情况下容易导致内存溢出,存在较大数据量时建议优先考虑redis缓存。

二、线程

1.无限制的创建线程

特别注意程序中是否存在每次请求都新建一个线程的情况,并发量大的情况可能导致创建大量线程,拖慢整体应用,建议使用线程池,限制最大线程数,最大线程数建议不要超过500。

2.线程queue队列过大

注意不要设置过大的线程等待队列,或者无界的队列,当线程任务执行过慢时,这可能导致内存的溢出,系统强制重启时队列任务也会丢失。建议最大不要超过5000,线程池拒绝策略可适当考虑CallerRunsPolicy策略,该策略可将请求的接收速率慢慢降下来。

3.线程池被过度复用

建议系统里不要一个线程池用在所有类型任务上,比较重的任务尽量每类任务一个线程池,以防止一类任务运行时间较长导致的不同业务相互影响。

4.一个接口问题拖垮整个应用

当请求并发量较大时,如某个接口的处理很慢可能会导致整体应用变慢,主要原因为Tomcat, 远程服务调用框架默认情况下并未做线程池的隔离,此时如果一个接口较慢将耗尽池中所有线程,新进来的请求将无法处理。此类问题的排查可通过jstack命令查看线程状态来判断,另建议为重要接口和远程调用单独设置线程池,限制线程池大小,设计尽可能小的超时时间,或者框架层做降级和熔断处理。 如支付系统曾经出现过微众钱包渠道返回较慢导致整个收银台不可用的情况。

5.定时任务并发执行

如果业务是不能容忍定时任务并发执行的,要确保定时任务是否存在并发执行的可能性,特别要注意是否存在前一个任务还没跑完,下个时间点任务起来执行的情况, 此时可能出现同一批数据被捞取出来多次执行。

三、CPU

1.一般情况

上述内存,线程,数据库等很多问题也会导致CPU异常增高, JAVA中NIO的select方式,在IO阻塞时会导致大量的轮询,此时CPU也会高, 如等待慢SQL的返回,可借助Linux的 top -H命令查看具体线程

2.递归调用

如果存在递归调用的, 是否存在死循环不能退出的可能, 建议程序中尽量避免使用递归

3.循环处理事件过长

循环任务,特别是循环条件根据外部某个值来判断,循环体执行逻辑又较重的情况,要注意是否存在死循环或者循环次数特别多的可能,建议加一个静态值来控制此类循环的最大循环次数, 防止单个循环占用资源过多拖跨整个应用容器。

四、数据库

1.SQL语句不走索引

稳定性问题的罪魁祸首,特别是针对较大数据量的表查询&更新操作,一定要确保SQL在任何时候都能走到索引,少用Mybatis的动态条件SQL拼装,一个业务一个SQL配置。

2.索引扫描的数据量较大

针对较大的表,要确保索引扫描数据量不要太大,比如创建时间上面有索引,我们根据该时间来筛选,此时建议控制时间范围内的数据在10万条以内,放太大跟全表扫也没太大区别。

3.索引字段顺序不正确

组合索引需要注意跟筛选条件顺序保持一致, 否则将导致索引失效。 索引字段作为排序字段时,可将索引的Order值 (升序/降序)设置为一致。

4.热点数据

大并发情况下,要从设计上避免对热点数据加锁,如系统存在这样的热点将成为性能瓶颈, 如账户系统的中间账户。

5.太长的本地事务

本地事务尽量不要太长,大事务将导致数据库链接被长期hold住,这会降低系统的吐吞量,极端情况下可能导致数据库链接被耗尽, 如本地事务中包含一个大批量的请求处理。

6.本地事务包含远程调用

本地事务中尽量不要包含远程方法的调用,一来远程操作无法被回滚,而来远程调用存在网络的不可靠性, 如网络异常或执行时间较长容易导致数据库链接被耗尽。

五、异常处理

1. 网络异常

如网络连接超时,读超时,域名解析出错等,特别是针对较重的逻辑,遇到此类问题该如何恢复。

2.进程意外终止

进程被意外终止是不能避免的,如重启,掉电,进程crash等, 考虑系统里比较重要的接口在执行时如果出现这类情况将会怎么样, 特别是针对更新本地数据库→调远程服务→再更新本地数据库的操作?(一致性问题)

3.数据库异常

数据库异常也是一种网络异常,是网络就存在失败的可能性, 针对比较重的逻辑,建议能用本地事务保证的尽量开启事务,不能用事务保证提前记录下状态,事后定时恢复。

4.重复请求

大并发情况下,有很多种可能会导致重复的请求, 如负载均衡在第一次异常后可能会发起重试,MQ可能会重复投递消息,用户可能重复提交请求等等。 所以针对那些有副作用的接口(涉及到更新,删除,新增数据的) 需要幂等

异常处理

幂等性问题

一、常见场景

  • 重复提交
  • 异常重试
  • 消息重复投递
  • 网络重试
  • 定时任务重复执行
  • 表单重复提交

二、常见的幂等性解决方案

唯一索引 常用于创建数据库数据时,创建成功则表示OK,否则代表已创建。
悲观锁 处理请求时,通过for update,update 锁住是数据,然后检查状态,通过则执行,否则直接失败,热点数据会存在性能瓶颈。
乐观锁 update table_xxx set name=#name#,version=version+1 where version=#version#; 如果数据库中的版本跟提前读入的版本不一致,则请求并发或重复,适用于更新场景。
状态机 用于状态更新场景, 状态跃迁时必须是状态机中指定的状态, 如订单从待付款状态更新为已支付状态:
放重表 利用数据库唯一约束实现幂等,常用的一个思路是在表上构建一个业务标识的唯一性索引,请求到来时插入成功则正常处理,否则重复。
分布式锁 基于Redis,ZK等服务实现,需要有锁超时机制,此类引入了额外的稳定性因素,能不用则不要用。
Token 客户端先从服务端获取一个token, 附带在业务请求中, 当服务器接收到请求后,判断token是否已被处理过, 处理过则为重复请求, 常用于表单重复提交的场景。
makefile 复制代码
hints:
乐观锁和悲观锁的选择是看线程竞争的激烈程度,竞争激烈的情况下悲观锁的性能优于乐观锁。
相关推荐
程序员爱钓鱼8 小时前
Node.js 编程实战:即时聊天应用 —— WebSocket 实现实时通信
前端·后端·node.js
京东云开发者9 小时前
如何使用wireshark进行远程抓包
程序员
京东云开发者9 小时前
InheritableThreadLocal从入门到放弃
程序员
京东云开发者9 小时前
🔥1篇搞懂AI通识:大白话拆解核心点
程序员
Libby博仙9 小时前
Spring Boot 条件化注解深度解析
java·spring boot·后端
源代码•宸9 小时前
Golang原理剖析(Map 源码梳理)
经验分享·后端·算法·leetcode·golang·map
小周在成长10 小时前
动态SQL与MyBatis动态SQL最佳实践
后端
瓦尔登湖懒羊羊10 小时前
TCP的自我介绍
后端
小周在成长10 小时前
MyBatis 动态SQL学习
后端
子非鱼92110 小时前
SpringBoot快速上手
java·spring boot·后端