高质量的工程代码为什么难写

之所以想起写这篇文章,是因为最近看到的一个著名的开源项目在内部使用时的各种问题,不得不说,很多的开源的东西思想是不错的,但离真正工程化都有不小的距离,所以没什么商业公司采用的开源产品如果要引入的话一定要慎重,通常会有N多的坑等着你去填,而比较成功的开源项目的背后多数都会有商业公司在背后不断的改进。

遥想我00年开始学习写asp代码时,觉得写代码也不难呀,无非就是学学语法规则、库就可以写出来,记得有一次我实习面试的时候是让我在一个下午左右的时间写一个完整的留言板,那也就是刷刷刷就写好了,但随着后来工作,尤其是加入阿里以后,越来越明白高质量的工程代码为什么难写。

在写代码初期,最关注的是如何用代码实现需求,如果是仅仅实现业务需求的话,即使是刚上手的程序员,只要解题能力还OK,基本上都是可以写出代码来的,所以我自己一直认为数学成绩是程序员的一个非常重要的要求,数学好的人通常解题和逻辑思维能力是还不错的。

上面的这个基本的写代码的过程中,写的更好的同学的体现会在对业务的深刻理解以及抽象上,写出的代码会具备一定的复用能力,这个在这里不多加探讨。

但代码是不是实现了业务需求就结束了呢,其实远没有,这其实只是写代码的开始,除了正向的逻辑实现外,任何一个点的异常的分支逻辑怎么处理才是工程化的代码中更难处理的部分,这个问题在单机式的系统中会相对还好处理,在分布式的环境会变得非常的复杂,例如调用其他机器的功能超时了,出错了,到底该怎么处理,这也是为什么有了那么多的分布式的理论的东西出来,在增加了异常分支的处理逻辑后,通常会发现这个时候正向逻辑的代码在整个代码的占比中会大幅下降。

异常分支逻辑处理好后,通常还需要增加必要的日志信息,以便在出问题时方便排查,而不是到了要排查问题的时候,一点目前系统的状况都搞不清楚,所以吃掉重要的异常信息不抛出这种行为在写代码中是非常可耻的。

在处理好上面异常的相关动作后,代码的健壮性也要处理好,这个主要指:
1. 自我保护能力
对外提供的接口是否具备足够的自我保护能力,就是即使使用的人没仔细看API文档随便乱用也不会导致系统出问题,这种案例非常的多,例如对外提供了一个批量查询接口,结果用户一下传了一个里面有上千个用户id的数组,查询一下直接把内存耗光,像这种情况下不能怪使用的人,而应该怪实现API的这一端的保护做的不够好,按照这样的标准去看,会发现开源的很多东西的API都不太合格;
还有一种就是能力保护,如果超出了处理的并发量的能力,这个时候会发生什么;
2. 对资源的使用限制
这也是代码新手或一些开源产品中做的比较差的地方,很容易出现规模一上去,资源使用量也一直涨,没有限制,然后导致系统挂掉,很常见的案例是对线程池的使用,例如像Java中的Executors.newCachedThreadPool,这个接口很多人会用到,但很多用的人都没有仔细想过会不会在某种情况下这里创建出巨多的线程;还有例如用Map做cache,也没考虑大小限制的问题,结果就是随着数据量增长,某天突然就挂了;
健壮性是代码中比较复杂的部分,通常也是比较展现代码能力的部分,可能看起来就几行代码,但其实背后反映的差距是巨大的。

开源产品除了在健壮性上的差距外,通常还会出现的一个巨大差距就是整个系统的设计的伸缩能力,伸缩能力不够的话通常会导致结构性的重构,另外常见的就是在并发的处理上不够高效,例如锁的合理使用、无锁算法的引入等等,而这些需要非常强的系统设计和代码功底能力。

除了上面说的这些外,高质量的工程代码还需要考虑可维护(例如监控信息暴露)、安全性等,对我而言,我一直认为所谓的工程化其实就是把一些玩具性质的代码变成可在商业系统中真正健壮运行的代码。

上面的内容写的比较简略,不过应该也能看出,对于高质量的工程代码而言,其实实现业务逻辑只是其中占比很小的一部分,甚至花的时间是相对最少的一部分,所以我确实非常赞同面试的时候让同学写代码,这个时候很容易看出同学写代码的功力;有些时候为了考察同学写代码的熟练程度,我会问问IDE的快捷键,或者让手写一段不是太复杂的代码。

=============================
欢迎关注微信公众号:hellojavacases

公众号上发布的消息都存放在http://hellojava.info上。

高压力下表现不够好的Java应用

最近一个应用在做线上压测时,在压力比较高的情况下出现了恶化的现象,负载越来越高,响应时间越来越糟糕,并且无法恢复,而按照构建高可用系统的设计原则中的自我保护这个原则来说,这是一个设计的不够好的应用,来一起看看这个case。

这个应用之所以出现响应时间恶化的现象,现场排查的定位来看是由于GC变的很严重,基本上大部分的时间都在做GC,而GC严重的原因经过分析是由于活跃的线程比较多,每个线程占据一点内存,加起来的总数就导致了不断的GC,而不断的GC又导致了响应时间更糟糕,活跃的线程更多,占据的内存更多,GC更严重,这是一个典型的恶化的现象。

就这个case而言,要解决显然是要调小线程池的大小(后来确实也是调小了表现就正常了),避免创建的活跃线程太多,但活跃线程数到底应该控制在一个什么范围,这是一个很大的话题,并且如果每个应用都要配置,维护的成本将非常的高。

一个应用在高压力的状况下,最佳的表现应该是无论多高的压力都不至于导致应用出现恶化的现象,最多应该就是响应时间稍有下降,但在到达了一个值后应该就保持平稳,这样可以保障应用无论在多高压力下都是可以提供给部分请求正常服务的,对于其他超出处理能力范围的请求则拒绝。

自我保护说起来简单,但要做到确实不容易,意味着要考虑到各种异常情况下都得保障好应用本身还能尽可能的对外服务。

说到这个,顺带就说下另外两个自我保护做的不够好导致故障的case。

一个是某应用对外提供了批量接口,结果调用方一次性传入了上万个对象做批量查询,直接导致内存被耗光,fgc严重,从而影响到了正常的请求处理,这个case也是典型的自我保护做的差的现象。

另一个是Netty的API,对Netty熟悉的人会知道Netty Client的NioClientSocketChannelFactory是不能每次都new的,每次new会导致线程池重复的创建,但这个说起来我觉得也是自我保护做的不够好,导致在误用的情况下会耗光资源,影响正常服务。

其实这种case非常的多,mina/netty的WriteRequest队列其实也是的,一个没有限制大小的队列,在异常的情况下就会出现这个队列占据暴多内存,从而导致应用无响应的现象,而这些都必须靠使用者来避开。

所以其实在设计API的时候一定要考虑清楚怎么做好自我保护,而不是靠着文档,靠着代码的注释。

=============================
题图来源于:http://goo.gl/FdbRxm
欢迎关注微信公众号:hellojavacases

关于此微信号:
分享Java问题排查的Case、Java业界的动态和新技术、Java的一些小知识点Test,以及和大家一起讨论一些Java问题或场景,这里只有Java细节的分享,没有大道理、大架构和大框架。

公众号上发布的消息都存放在http://hellojava.info上。