生产填坑的经历为什么要称之为全面回忆呢,因为恰巧笔者之前看了一部科幻电影,名字就叫《全面回忆》 Total Recall,该片于2012年10月20日在中国上映,豆瓣评分7.0。
全面回忆的剧情简介......
欢迎来到Rekall,它是一个能够把你的梦境变成现实的工厂。主人公道格拉斯·奎德(柯林·法瑞尔 Colin Farrell 饰)是一名普通的工厂工人。尽管他有一位漂亮的妻子(凯特·贝金赛尔 Kate Beckinsale 饰),两人看似恩爱,但他内心似乎依然不满足。思绪旅行听起来像是个完美的假期,让他从沮丧的生活中得到放松——作为一名特工的真实记忆可能正是他所需要的。但是当这个过程出现可怕的异常时,奎德变成了一个被追杀的逃犯。他发现自己正在躲避受考哈根长官(布莱恩·科兰斯顿 Bryan Cranston 饰)控制的警察,没有一个人是奎德可以信任的,除了一名叛军的女战士(杰西卡·贝尔 Jessica Biel 饰),她为地下抵抗组织的首领(比尔·奈伊 Bill Nighy 饰)工作。幻想和现实之间的界线变得模糊,他的命运凶吉难卜,因为奎德发现了他的真实身份,他的真爱,还有他真正的命运。
本片改编自菲利普·K·迪克的著名短篇小说《We Can Remember It For You Wholesale》。
说来也巧,笔者认为大多数的生产填坑经历就像这部电影的剧情一般,男主角在无数次的美好记忆转换过程中,由于程序出现了一个可怕的错误导致了很多无法预期的可怕事件发生,只是在生产填坑中这个男主角可能是程序员。
内存全面回忆
那么对于一个Java程序开发者经常会遇到哪些坑呢?笔者仔细全面回忆了一番自己踩过的坑,真的是数不胜数,苦不堪言呐,所以笔者认为有必要做一些简单的总结与反思。针对Java应用程序的生产故障,笔者认为可以归纳为如下几个方面,一是内存方面,二是高并发导致的数据不一致,三是其他方面包括但不限于数据库服务故障、网络故障、磁盘故障等。以上几个方面,内存方面是最常见也是最重要的,可以说笔者遇到过的生产坑有80%都是内存问题,java语言实现了内存自动回收可谓说是一大技术进步,但同时这也成了悬在每个java程序员头顶的一只靴子,指不定什么时候会掉下来把你砸吐血。关于内存的垃圾自动回收在《深入理解JVM虚拟机原理》一书中有一句话讲的特别经典:
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的”高墙“,墙外面的人想进去,墙里面的人却想出来
首先生产内存问题按解决问题的时间复杂度和精力成本从低到高依次为JVM内存不足、JVM内存泄漏、高并发下的FullGC这三座大山
- JVM内存不足这类问题比较简单,主要原因是由于JVM向操作系统申请不到新的可用内存导致,一般常常表现为Java应用程序在运行过程中经常会自动重启,导致业务服务暂时中断。一般可查看GC日志,结合业务量和主机实际物理内存来重新设定JVM最大堆内存,或者直接增加主机的物理内存。这类问题笔者一般倾向于直接加大主机的物理内存,因为相较而言增加硬件配置在一定程度上来说反而是付出成本最低的。(有这调优的精力,程序员可以去做更多其他有意义的事情,创造更多的价值,现在硬件加个内存成本真的挺低的)
- JVM内存泄漏这类问题笔者认为是生产最常见的一个问题,也是发生概率最大的。主要原因是这类问题大多是由于Java程序员自己埋下的坑导致,如应用代码不停的new新对象内存又从不释放、往List或者Map中一直添加对象又不清理、在大量循环遍历代码块中操作字符串直接定义变量相+等等之类。近来笔者刚处理了一个内存泄漏的问题,是一个已上线运行了好几年的业务系统,每天上班工作时间大概会有40-50号人登陆系统进行录入作业,用户都是外包录入人员,每天按录入任务数计件算绩效。某一天突然就接到外包录入人员反馈说系统这段时间时不时变的好卡,具体表现在获取录入任务和提交任务时经常出现卡顿。最开始笔者判断可能是不是最近业务量增长了很多导致的,因为通常情况下已经稳定运行了好几年的系统一般不会有内存泄漏的问题,所以一开始笔者通过远程查看应用日志和统计数据库记录来确认在最近时间段内业务量是不是有所增长,但发现最近的业务量比上年度反而下降了许多,这就奇怪了,于是笔者再跑去终端用户的客户端浏览器NetWork中抓取每个获取任务和提交任务的action响应时间,发现每固定间隔一段时间就会接连遇到提交任务action服务端响应时间耗时超过3~5秒的异常情形(正常的响应时间大都是百毫秒级别),且这段时间的流量是平稳的,由此来看不太像是高并发,那么笔者就又把问题原因怀疑上了内存方面,于是登陆应用服务器,使用命令kill -3 pid 查看生成的javacore文件发现此时JVM堆内存已几乎耗尽至能申请到的配置最大4G内存,同时也发现在应用程序bin目录下已生成Java HeapDump文件,于是笔者进一步使用工具分析Dump文件发现90%的内存都被一个HashMap使用掉了,于是经过结合项目源码找到了这个Map,最终发现写这个代码的程序员忘了即时清理Map缓存。因为每次put map中的数据量比较小,之前系统未遇到问题是由于项目之初生产运维经常有切换演练或者定期软件更新,应用经常会重启释放内存,这次是1年多来应用程序都没有重启过,于是这个问题就暴露出来了,最后增加map的清理机制,此问题得以解决。针对这类问题最麻烦的是要找到内存泄漏的点,这里就要用到”工欲善其事必先利其器“的方法论了。对于HotSpot虚拟机我们应该尽可能多的去了解JVM内存诊断的辅助工具,如可视化多合一故障分析监控诊断工具VisualVM、jstat虚拟机内存统计信息监视工具、jmap内存映像工具、jhat虚拟机堆转储快照分析工具等,如果是非Sun系列,比如IBM的j9vm,可了解HeapAnalyzer、Javacore Analyzer、Garbage Collector Analyzer工具。由于笔者能力知识范围有限目前接触过和解决过生产问题的项目只用到过以上2个系列的JVM,对于其他平台,如HP VM、JRockit VM只是知道有这些个东西存在。
- 高并发下的Full GC生产故障通常存在于追求高吞吐量和低停顿的高质量服务系统中。想象一下在高并发的场景下,JVM 发生Full GC回收垃圾,导致所有正在执行业务的处理线程突然全部跑到安全点之上卡住停顿下来,对业务来说这将是一场怎样的灾难。以上过程简称STW(Stop The World)。解决Full GC引起的STW需要Java开发者对JVM有着很深入的理解,了解JVM各项配置参数,了解垃圾回收器主流的收集器算法,如ParNew、Parallel Scavenge/Old、Serial、CMS、G1等收集器算法,同时能理解和分析GC日志,在高并发业务应用场景下选择最适合自身应用的垃圾收集器算法组合和最优JVM配置参数,使应用Full GC发生的频率尽可能的降到最低同时使单次Full GC的时间尽可能的短。这一块需要通过大量的性能测试和GC日志跟踪监控持续调优,才能优化虚拟机的内存使用率,解决这类问题知识、经验、数据支撑是关键。以上是笔者处理Java内存方面问题的一些心得体会及感受,因为体验过很多次切肤之痛,所以有些感触还是挺深。
其次关于高并发导致的数据不一致问题这次就先不谈,篇幅有限,以后再说吧,笔者也是能力有限,需要再积累下。不过笔者认为对于我们Java程序员来说,并发编程是技术含量最高的一部分,这块很难单独讲完,暂时想到的内容主要包括Java内存模型、主内存与工作内存、线程间内存交互共享、原子性、可见性与有序性、先行发生原则,锁机制等等以上每一项内容都至少可以花一篇文章的篇幅来讲吧。
最后再说下其他方面的问题吧,这里分享一个大概2个月前遇到的生产应用数据库CPU使用率突然100%问题解决案例吧。简单交代下业务系统的背景,线上运行的是一个自助交易类系统,目前的日交易流水在40W笔左右,当然终端用户数还在扩充,预计未来日交易流水在百万级别,数据库采用的是DB2,服务器配置为一台4C 16G内存的小机,平时交易繁忙期CPU利用率也不会超过20%,很难想象一个日交易不过百万流水的应用系统突然就能把4核的CPU吃完(最近也无什么大更新),通过机房运维监控,场外求助数据库专家,使用db2top命令查询数据库每个applicaion id 执行的sql发现是由于某一个sql执行效率异常低下形成慢查询导致,当即抓到这个sql,发现这个sql是个关联表查询,sql写法异常低级,存在not in中拼接大量排除条件。庆幸的是这涉及一个业务管理端的非关键功能,平时都没什么人会用这个功能,只是刚好这次业务上有个通知,导致这块功能同时使用的人数较多。为防止数据库CPU资源不足的情况下造成大量数据库正常操作读写超时影响正常交易,情节严重甚至可能发生数据库服务器宕机的风险,笔者当即采取了功能降级的操作,通过临时更改用户菜单权限,将该功能菜单暂时对用户屏蔽以解燃眉之急。后面再通过优化程序sql,对涉及业务功能做性能测试,验证通过之后再重新开放给用户,最终问题得以解决。对于其他方面的问题解决之道,笔者也没有很好的经验,总结起来就是开发人员需要尽可能的拓展自己的知识面,比如数据库、存储、网络等方面,很多时候你一头雾水,茫然不知所措,是源于自己对陌生事物的无知,解决这类问题知识储备是你最有力的武器。
填坑总结
工作这么多年以来生产填坑是真不少,上面列举的只是最近一年内遇到的,对于每次复盘处理之后的生产故障问题,笔者也是会经常反思一下,故而今天在这里总结一下遇事处理原则:
- 生产无小事原则,往大了说生产事故涉及到软件用户的根本利益,往小说一次严重的生产事故可能导致公司或者企业失信破产,员工失业。
- 弃卒保车原则,在尽可能多的熟悉和了解生产环境之后评估当前问题对整个系统运行影响面有多大,快速制定应急响应机制,如采取限流、降级、熔断等技术手段减少应用故障损伤面。
- 服务不重启原则即不到万不得已的情况之下不要想着重启服务,笔者也是遇到过生产故障到了山穷水尽的地步,没办法只能重启服务,但过来经验告诉我能不重启服务就尽量不要重启,原因在于重启之后可能故障依然存在,同时可能还会造成很多异常数据形成故障二次伤害。真要重启服务的话,应该详尽考虑有效安全的重启机制,尽可能的利用外部资源来配合,如通过主备切换流量控制逐台重启、负载均衡流量控制重启等手段尽可能的减少因重启造成的业务影响。
- 好心态原则,面对生产故障,开发人员应该要有一个积极良好的心态,你上来是来解决问题的,不要甩锅,不要抱怨,更不要BB,思想上不懈怠,大脑保持冷静,不慌不乱,积极沟通,了解情况,收集数据,制定最优应对之策。
- 全面复盘原则,对于生产应用故障原因一定要有追根结底的决心,做到及时复盘反思,杜绝类似问题再度发生。
写在最后
以上算是一遍填坑经历软文吧,任何应用都可能会发生生产故障,有软件原因也有硬件原因,有外部因素也有内部因素,当面对应用故障拦路虎时我们唯有迎难而上,拥有一个好心态是关键。计算机领域的问题都是有迹可寻的,我们能遇到的大多数问题都必定会留下蛛丝马迹,只要你肯花心思和精力去探寻,必定能找到你想要的答案,真的实在不行也会有替代方案,无非就是代价问题。
本文一些观点只是本人拙见,不一定都是对的,关于如何填坑解决问题大家有什么建议或任何疑问都可直接评论或者简信我,可共同探讨交流。
码字不易,如果你觉得本文对你的工作或者生活有帮助,正好你又比较慷慨,可以给个赞赏不在乎多少或者点个️。平时笔者有稳定的工作收入,写作只是用来学习和记录生活,非职业,但你的点赞会让我知道自己写的文章是否真的对du'zhe有用,这会给予我继续写作下去的动力,谢谢!