线上排查问题多了,慢慢有个很直观的感受。
刚入行的时候,遇到接口超时、服务卡顿、CPU飙升,第一反应就是翻代码。找逻辑漏洞、找死循环、找空指针,总觉得所有异常,一定是自己写的代码出了问题。
但踩过无数次坑之后才发现,生产环境百分之六十以上的性能劣化,和业务代码本身的bug,半毛钱关系都没有。
不是逻辑写错了,是姿势用错了。
是对中间件、JVM、服务器资源、并发模型的认知不全,导致写出了看似没问题,实则极其拖垮服务的代码。
最近迭代项目压测,就碰到一个特别典型的案例。
接口单线程跑完全正常,本地测毫无卡顿,一上压测并发直接雪崩,响应时间从20ms直接拉到1.2s。团队几个人对着代码抠了一上午,没查出任何逻辑错误,最后定位问题的时候,真的有点哭笑不得。
问题根源,居然是高频方法里,重复创建了SimpleDateFormat对象。
很多新手包括以前的我,都习惯性在方法内部new时间格式化工具。本地单测毫无感知,一旦到了高并发场景,频繁创建销毁对象,带来的GC开销巨恐怖,直接把接口吞吐量拖垮了。
这种问题,编译器不会报错,代码逻辑完全正确,测试用例也跑的过。可就是上了生产,随时会炸。
这也是我越来越觉得,业务代码写得对,只是入门;写得稳,才是进阶。
一、很多代码,只是“能跑”,并不“合格”
日常开发中,CRUD写多了,很容易陷入一个误区:只要功能实现,没有报错,迭代就算完成。
但生产环境和开发环境,完全是两个东西。
开发机流量小、并发低、资源充足,很多隐藏的问题根本暴露不出来。我们写的很多代码,都是典型的温室代码。
举几个工作中见过最多的,看似无害,实则隐患极大的写法。
第一个,集合遍历中频繁扩容。
很多人创建ArrayList,直接无脑new ArrayList(),不指定初始容量。
如果是少量数据无所谓,一旦接口批量查询、批量处理数据,动辄上千上万条数据,集合会不停的扩容、复制数组、创建新对象。频繁的内存迁移,在高并发下就是妥妥的性能杀手,而且你在日志里,根本看不到明显的报错。
第二个,try-catch 滥用。
为了避免程序抛出异常终止流程,不少同学喜欢把整块业务代码全部包在try-catch里,甚至循环体内部也嵌套try-catch。
异常对象的构建,本身是非常耗性能的,它会抓取完整的堆栈信息。高频并发场景下,哪怕是空异常,堆积起来也会严重拖慢执行速度。更离谱的是,很多人catch完直接空处理,连日志都不打,线上出问题完全无从排查。
第三个,日志打印不规范。
习惯性随意打日志,INFO级别输出超长报文、循环内重复打印无用日志、参数拼接不用占位符。
别小看日志,线上服务日志量过大,不仅占用磁盘IO,字符串拼接也会增加大量无用计算开销,高并发场景下,能直接让服务吞吐量腰斩,这点很多开发都忽略了。
这些问题,统统不算bug。
代码能跑,功能正常,测试通过,但是上线之后,只要流量稍微起来一点,各种卡顿、超时、GC频繁的问题就会接踵而至。
二、线上80%的卡顿,都是资源浪费堆出来的
做后端久了,越来越认可一个观点:线上性能问题,大部分都是资源利用不合理导致的。
不是机器配置不够,是我们的代码,在毫无意义的消耗资源。
就拿最常见的数据库问题来说。
很多接口慢,大家第一反应就是加索引。但实际排查下来,很多慢SQL,根本不是没加索引,而是查询姿势不对。
比如select * 查询,明明只需要两个字段,偏偏查回整表所有字段,增加网络传输和内存占用;比如分页查询深度过大,limit 10000,10这种写法,会先扫描一万多条无效数据再丢弃;比如事务范围过大,把查询、日志、甚至外部调用都塞进事务里,拉长事务耗时,造成锁等待堆积。
这些写法,单条SQL执行几乎无感,批量并发的时候,数据库连接池直接被打满,接口全线超时。
还有线程池的问题,也是重灾区。
很多项目直接用Executors创建线程池,要么不设置核心参数,要么队列、线程数配置不合理。
核心线程太少,任务堆积阻塞;核心线程太多,频繁上下文切换消耗CPU;队列设置过长,任务超时堆积。线上一出问题,就是连锁反应,一个服务拖垮整个链路。
我之前遇到过一次生产抖动,排查了整整一天。
最后发现是定时任务和业务接口共用了同一个线程池。白天业务高峰,定时任务抢占线程资源,导致正常业务请求没有线程可用,大批量超时。
这种问题,代码逻辑完全没问题,就是资源隔离没做好。
三、为什么新手很难发现这类问题?
很多刚入行的开发,包括我刚工作那会,排查问题只看报错日志。
没有error日志,没有异常抛出,接口能返回数据,就默认服务是正常的。
但实际上,性能问题,大多都是静默问题。
它不会主动报错,不会终止程序,只会悄悄的拖慢整体流程,堆积请求,慢慢耗尽服务器的CPU、内存、连接池资源,等到我们发现的时候,已经出现大面积服务降级了。
还有一个核心原因,本地开发环境太安逸。
本地测试,并发量为个位数,数据量百十条,哪怕代码写的再烂,也不会有任何性能压力。
只有真实的生产流量、海量数据、高并发场景,才能放大所有代码细节的问题。这也是为什么,很多人本地跑的好好的代码,一上线就各种翻车。
四、普通开发,该怎么规避这类隐形坑?
不用搞多高深的调优技巧,日常开发守住几个基础原则,就能避开绝大多数线上性能问题。
首先,拒绝无脑写法,养成基础优化习惯。
创建集合预估容量、高频工具类定义成静态常量、循环内部不创建对象、减少循环内数据库和IO操作。这些都是最基础的东西,但也是最有用的优化,坚持下来,代码性能会干净很多。
其次,一定要重视压测,不要相信本地自测。
任何涉及批量处理、高频调用、对外接口的代码,上线前简单跑一遍压测。不用精准,只看吞吐量、响应时间、GC次数有没有异常。很多隐形问题,压测一次就能暴露出来,比上线后熬夜排查靠谱太多。
然后,学会看基础监控,不要只看日志。
线上排查问题,优先看CPU、内存、GC频率、线程池状态、数据库慢日志。很多时候,监控曲线的异常波动,比日志更能快速定位问题根源。
最后,少写侥幸代码。
不要觉得流量小、数据少,就可以随便写。生产环境的流量,永远比你预估的更复杂,今天没问题,不代表下周、下个月不会出问题。所有靠运气能跑通的代码,迟早会在生产环境翻车。
写在最后
工作越久,越明白一个道理。
能实现功能,只是开发的最低标准。真正区分初级开发和中高级开发的,从来不是谁会的框架多、谁写的代码花哨,而是谁写的代码更稳,更少出隐形问题,更适配复杂的线上场景。
BUG好修,性能难调。
逻辑错误是显性的,性能问题是隐性的。显性问题靠细心,隐性问题靠积累和认知。
往后的开发路上,还是要沉下心,戒掉随手写代码的习惯,多思考一层,多校验一遍,把那些隐形的性能坑,提前堵在上线之前。