新闻动态 news

逻辑的引擎

文章来源: 成都公司  研发部  吴琨
 
         
               最近公司项目组内部会议上,“...我们公司人员开发的程序,运行起来说占用内存过高、一会儿又被说占用线程数过多,而我们的人员束手无策,只有听别人摆布,我觉得太丢脸了,需要大力提高对运行态的监控与检测能力!”也许,我们更应该在平时枯燥的工作中体味一丝人文的情怀,体验一下逻辑的魅力;从而理解程序,理解逻辑,理解所谓的“运行态”。
 
姜饼人的故事
 
    关于五十块一模一样饼干的问题,你回答得如何呢?让我们假设你是从外大空来的,从来没有见过一位面包师傅。有一天你无意间走进一家香气扑鼻的面包店,看到架子上有五十个一模一样的姜饼人。我想你大概会搔搔头,奇怪它们怎么看起来都一个样子。
 
    事实上这些姜饼人可能有的少了一双胳臂,有的头上缺了一角,有的则是肚子上很滑稽的隆起了一块。不过你仔细想过之后,还是认为这些姜饼人都有一些共同点。虽然这些姜饼人没有一个是完美的,但你仍会怀疑它们是出自同一双手的杰作。你会发现这些饼干全部都是用同一个模子做出来的。更重要的是,苏菲,你现在开始有一股不可抗拒的念头,想要看看这个模子。因为很明显的,这个模子本身一定是绝对完美的,而从某个角度来看,它比起这些粗糙的副本来,也会更美丽。
 
 
    一个老人拿着一把榔头和锥子,在一块大石头边上敲敲打打。有个在附近玩耍的小男孩看见了,惊讶地问:“你在找什么呢?”老人笑了笑,“过两天你再来看就知道了”。几天后,小男孩回来,看见老人正对着一匹石头马,在用砂布打磨。小男孩更加惊讶了,“你是怎么知道那匹马藏在大石头里呢?”
 
    马一直藏在石头里,更是藏在老人的心灵里。灵魂渴望乘着爱的翅膀回家,回到理型世界。而老人的手,能将理型世界的马重新放逐到感官世界。
 
关于程序开发
 
    我是一名程序员,读过很多的代码,也看过一些书,有基础理论的,更多的是讲细节的。还写过不少的代码,五花八门的,不同的领域。我总结,所谓的程序设计,就是套路,(按老外的说法,叫Pattern)。按亚里士多德的说法,这叫三段式;按哥德尔的说法,这叫形式系统。或者,按象棋的说法,叫马后炮,叫仙人指路(一种象棋的开局模式,一个我儿时小朋友从棋谱上看的~,多有意境的名字)。套路,就是一种想要将死军,将棋子的布局向某个状态上靠,以形成杀局。程序也一样,要达到某个业务功能,将程序的布局向某个组织方式上靠,以实现功能。
 
    实现同一个目标的套路有多种,条条道路通罗马。比如递归(Recusion),可以通过函数嵌套调用实现,也可以采用for语句完成。不同的套路,虽然都达到目标,但其影响是不一样的,可能函数嵌套依赖于stack,而for语句需要数组,可以定义为需要heap。这样一来,根据递归层次的量级,以及stack,heap各自量级的限制,决定了其使用场景。比如,stack大小的限制和stack overflow。
 
    没有一种套路是完美的。就好象没有人是完美的一样。牛顿定律也有失效的场景不是。记得汤姆汉克斯演的电影《天使与魔鬼》里有句话,“宗教是有缺陷的,因为人是有缺陷的”。也许,宗教,也是人积累创造出来的一种套路!
 
    如何知道这世界上都有哪些套路,理解套路,使用套路,或者更牛的,创造新套路?我的理解是,在于和你所碰到的具体问题,进行决斗。在无数的决斗中,才能体会到无数的细节,从而形成所谓的经验,见多识广。这样,在你所熟悉的领域,你可以自豪的说,一切都逃不出我的魔爪。这个领域就是程序设计,或者缩小一点范围,就是我们各自所从事的实际领域。学习套路,就是和问题决斗。我弟弟跟我讲过一句话:英文“研究”的单词叫“re-search”;search,search,再 search。因此,本计划的目标是:和大家一起 search 。
 
关于JVM
 
    计算机之父冯诺依曼的计算机原理是存储程序和程序控制。JVM是虚拟机,它也遵循该原理,所以,我们要理解内存(程序存储)和线程(程序控制)。又因为它是虚拟机,所以,它又要受到OS中process(操作系统进程的概念)的限制,而process会受到OS本身的限制,当然,OS会受到真正的物理机器的限制。
 
    我写这段话的目的是,要知道JVM处于什么样的位置,及其限制的来源,这对理解和分析出现问题,指明了方向。当问题出现时,根据以往的经验,可能我们不曾见过,但,它一定会有答案的(又不是做什么前沿研究),如何找到,则需要方向和方法。
 
 
    我们将JVM体系结构和冯诺依曼的体系结构对比,发现基本是一致的。当然,它们都来自于同一套数学基础:阿兰.图灵(参考书《逻辑的引擎》);都需要Method Area,Heap,stack。对JVM而言,核心的概念就是内存和线程。
 
    由于工程上的限制,或历史的原因,或现实的需求,各种限制之后,导致在JVM的具体实现上,也有诸多优势和缺陷。我们只有在深入到JVM或OS的细节之后,才能了解那个姜饼人为什么会缺胳膊少腿。
 
JVM案例
 
 

前面提到,内存主要分heappermgen 。那用词就要注意是那个内存了。

比如heap,过高是问题吗?也许程序就是需要那么多,也许存在内存泄漏。

注意:也许根本就不是问题!!!

heap泄漏会带来什么影响?

如果heap泄漏,长时间(比如几天)后,可能会coredump

如果heap泄漏,可能会引起GC反复执行,影响程序性能(GC会消耗CPU

怎么判断是否为heap泄漏?

通过jconsole工具,查看heap,看是否为锯齿形,如果顶点成上升趋势,则可能存在内存泄漏。(如果幅度不大,也可能是程序正常行为,例如缓存的数据越来越多)

如果顶点的上升趋势幅度很大,则可能真有泄漏。

如何定位heap泄漏的点呢?(困难模式)

可以通过查看heap中的实例数目,定期做对比。

有更好一点的工具,如mat,先通过jconsolevisualvmJVM-HEAP dump出来,然后加载到mat中,分析多个dump快照做对比。找出差一点,再结合程序代码,人工判断。

PermGen呢?

PermGen区是代码段,一般不执行GC,它一般呈阶梯上涨趋势,且最后会稳定在某个阶梯的台阶上。

如果PermGen的台阶太多,极有可能存在Perm泄漏,直到coredump

真出现Perm泄漏了呢?(地狱模式)

这是到了地狱了。

要定位Perm泄漏,真的很复杂。需要JVM的背景知识很多,且,需要知道整体代码里的各类细节,还,需要也许是运气。

不过,一般在框架中开发,是不会引起问题的。除非没按框架的规则瞎来的话。

 

 

线程多少才算高?对一般的LINUX而言,1~2K级别,认为是一个阀值。作为过高过低的依据。过高,导致thread swtich的消耗过高。(OS的限制)

WebLogic中,一般会设置为400左右。意思是去1K的一半左右,这是在并发能力和thread-switch之间做个平衡的结果。

Weblogic+Oracle中,和线程数息息相关的是JNDIjdbc连接数。比如thread=400,则jdbc=200100。比如设置jdbc=1,那么这400个线程白启动了,没有意义,等于起了1个线程。

jdbc数目的约束,来自两方面,太多,对Oracle服务器是个考验,太低,对Weblogic的服务能力是个考验。

一般经验值是线程数/2/4。即,认为,一个服务,1/21/4是时间区间消耗在数据库处理上,而其它时间就消耗在socket,框架上等其他处理上。

当然,对具体的应用,要根据非功能测试而选择自己的值。

 

 

影响TPS的因素很多。例如:
    如前所述,线程数目要合适
    jdbc数目要合适
    也许是其他的某种数目设置的不合适
    这是因为,OS的设计,就是按条目来的,到处都是条目
    也许是存在IO泄漏
    也许是umask不对
    还要看CPU占用                 
TPS少,且CPU少,则存在IO阻塞
    什么叫IO阻塞?
    也许是socket read/write wait
    也许是DB wait
    也许是 thread lock wait
    也许根本就是 dead lock
    不一而足
TPS少,而CPU高
    这个情况的核心意思是,代码一直在运行。
    至于代码为什么一直在运行呢?这,情况就太多了。
    但,它们都应该是很算法或程序结构有关系。
    也许是业务逻辑处理的不合适。
    也许该用map替换array迭代。
    也许不要将数据拷贝来,又拷贝去。
    也许。。。。。
    也许就这能力嘛,要不还要流控干嘛
 
 
一般建议为2G,不管是32bit还是64bit,这是因为,过高,会引起GC的时间更长(因为GC的阀值更好,需要回收的内存更多)。这也要看应用而定,比如,在应用中大量采用缓存(比如缓存可能达到1G),这时,可以设置为3G。这是因为,缓存是不回收的,它不影响GC。这也解释,内存高不高,不能一概而定。需要则高之,关键在于了解经验值的来历。
 
根据稳定性测试的观察来定。一般,512M是个经验值。多了又不会怀孕,只要物理机器的内存不是问题的话。
 
Perm的含义是持久化,代码段。在JVM中是class的集合。class的检索是基于hash算法的,多就多呗。只要不是泄漏,该多的,还得多。
 
Perm区可能存在class的重复加载问题。这是因为classloader的机制决定的。而class loader和线程又有很大的关系。Perm区的泄漏,一个重要原因,就是很线程的ThreadLocal有关。出现问题之后,真的很麻烦。
 
所以,对PermGen代码要注意避免:
 
    尽可能的不要启动临时线程。而用线程池。
    线程池也不要用可伸缩的池,而尽量用定值。
    在写ThreadLocal时,千万要小心。对会消失的线程,一定要清除ThreadLocal。
    会消失的线程是指:临时线程,或线程池中被伸缩掉的线程。
 
 
例如文件句柄泄漏。比如,某个逻辑A打开了文件,不关闭。而逻辑B打开后,关闭了。一个JVM打开的文件数目是有限的。比如1024(这是OS process的限制)。当A执行了1023次之后,开始并发的执行B,很明显,就并发不起来了:因为只剩下一个file slot了。
 
另一条路
 
我们实际学习,工作中,走的是主流的一条路,走的是图灵为我们指出的路。随着软件迈向并发时代,并发带来了太多的问题和难题。也许我们会走上一条阿隆佐·邱奇为我们指出的路:lambda演算一条没有内存变量的路。
 
理型世界的马一直藏在你我的心灵里,它将为逻辑的引擎提供强大的动力。
 
参考
 
    免费午餐的终结:软件迈向并发时代http://en.wikipedia.org/wiki/Herb_Sutter
概念性的介绍了并发带来的问题,顺着这条路下去,能找到很多相关的资料。
    《深入理解Java虚拟机:JVM高级特性与最佳实践》周志明
国人写的一本JVM相关的书籍,很值得一读。
    《苏菲的世界》
    《逻辑的引擎》http://book.douban.com/subject/1391740/  
作者号称是丘奇的学生