花钱的年华

江南白衣,公众号:春天的旁边

Guava Cache小记

| Filed under 技术

review代码时看到的问题,小记一下。
 

1. 使用Guava Cache的四个理由

 

1.1 entry不存在或失效时,保证单线程回源

避免了所有读不到该entry的线程一起去后面的数据库或Redis访问,压力暴炸。虽然自己做也行,但不就怕普通程序员一不小心没写好么。

 

1.2 后台定时刷新数据

数据放入缓存后,定时刷新,而且在正确配置后(后详),在刷新的过程中,依然使用旧数据响应请求,不会造成卡顿,非常美好。

 

1.3 WeakHashMap的并发安全版

Map里的Key或Value是 WeakReference的好处,就是对象本身失效后被GC掉,而不会因为对象依然在Map里保存而GC不掉。

可惜JDK自身提供的WeakHashMap,并没有Concurrent的版本,此时就可以考虑GuavaCache了。

我就是不用Guva Cache呢,那就用一个有expireTime的本地缓存,让它在失效后再被GC掉吧。

 

1.4 本地缓存中传统的expiretime,maxSize限制

略。

 

2. 使用Guava Cache的正确使用

 

2.1 设置并发级别

Guava Cache 是JDK7 ConcurrentHashMap的思路,先分N个Segment加锁,Segment下面才是HashMap。这样就把原本一个锁,打散成N个锁。
但和ConcurrentHashMap默认16个锁不一样,GuavaCache默认是4个锁,自己看着设置。

 

2.2 设置后台刷新

如果你什么都不做,只是设置 CacheBuilder.newBuilder().refreshAfterWrite(30, TimeUnit.SECONDS),那CacheLoader巧妇难为无米之炊之炊,还是要在当前线程里执行加载,还是会造成卡顿的。君不见CacheLoader中reload()的实现:

return Futures.immediateFuture(load(key));

所以,要后台刷新,一定要重载reload函数,给它一条执行的线程。

CacheLoader里本身有一个静态函数,只要传给它原来的CacheLoader 和 一个executor,它会返回一个修饰器模式的包装类,重新实现reload()函数,以callable在executor里执行load()函数。

public static CacheLoader asyncReloading(final CacheLoader loader, final Executor executor)

但这也怕这包装类万一没被内联,白白多了层调用啊,所以还是不要这么懒,自己把它的reload()方法抄出来,抄到自己的CacheLoader 或者一个公用的CacheLoader父类里了。

另外,如果executor給多个CacheLoader共用,最好用一个core pool size为1的,CachedPool风格的池。

 

2.3 get()与uncheckGet()

get()抛的是 Checked的 ExecutionException,另外还要做一次getCause()才能获得真正的异常,主要是load()里发生的Checked异常。

getUnchecked()则抛的是UncheckedExecutionException,还是要做getCause()哈。当然如果严重错误Error,还是会抛ExecutionError的,但这不用在接口里声明。

by calvin | tags : | 5

Netty SSL性能调优

| Filed under 技术

​嗯,这篇不长的文章,是一个晚上工作到三点的血泪加班史总结而成。多一个读,可能就少一个人加班。

不知为什么发在这里老是被云盾墙,直接发到了微博头条文章,《Netty SSL 性能调优》

比较遗憾的是,微博头条一定要給封面图片,一不小心,发了一张怪怪的。

by calvin | tags : | 3

高性能场景下,Map家族的优化使用建议

| Filed under 技术

啥,HashMap都还能灌一篇?我从小学用到现在。
是呀,如果不追求性能,这篇文章可以不看的,JDK本身已写得足够优秀,大家随便用就好。
但在JMC监控下,又的确很多时间都花在Map操作上,从自家的代码里翻呀翻,还是找到了好几点可以优化的地方。

 

1. HashMap 在JDK 7 与 JDK8 下的差别

顺便理一下HashMap.get(Object key)的几个关键步骤,作为后面讨论的基础。

1.1 获取key的HashCode并二次加工

因为对原Key的hashCode质量没信心,怕会存在大量冲突,HashMap进行了二次加工。

JDK7的做法:

h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);

JDK8 因为对自己改造过的哈希大量冲突时的红黑树有信心,所以简单一些,只是把高16位异或下来。

return h ^ (h >>> 16);

所以即使Key比较均匀无哈希冲突,JDK8也比JDK7略快的原因大概于此。

顺便科普一下,Integer的HashCode就是自己,Long要把高32位异或下来变成int, String则是循环累计结果*31+下一个字符,不过因为String是不可变对象,所以生成完一次就会自己cache起来。

 

1.2 落桶

index = hash & (array.length-1);

桶数组大小是2的指数的好处,通过一次&就够了,而不是代价稍大的取模。

 

1.3 最后选择Entry

判断Entry是否符合,都是首先哈希值要相等,但因为哈希值不是唯一的,所以还要对比key是否相等,最好是同一个对象,能用==对比,否则要走equals()。 比如String,如果不是同一个对象,equals()起来要一个个字符做比较也是挺累的。

if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;

更累的是存在哈希冲突的情况,比如两个哈希值取模后落在同一个桶上,或者两条不同的key有相同的哈希值。
JDK7的做法是建一条链表,后插入的元素在上面,一个个地执行上面的判断。
而JDK8则在链表长度达到8,而且桶数量达到64时,建一棵红黑树,解决严重冲突时的性能问题。

 

2. 很多人忽视的加载因子Load Factor

加载因子存在的原因,还是因为减缓哈希冲突,如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。所以加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。

2.1 考虑加载因子地设定初始大小

相比扩容时只是System.arraycopy()的ArrayList,HashMap扩容的代价其实蛮大的,首先,要生成一个新的桶数组,然后要把所有元素都重新Hash落桶一次,几乎等于重新执行了一次所有元素的put。

所以如果你心目中有明确的Map 大小,设定时一定要考虑加载因子的存在。

Map map = new HashMap(srcMap.size())这样的写法肯定是不对的,有25%的可能会遇上扩容。

Thrift里的做法比较粗暴, Map map = new HashMap( 2* srcMap.size()), 直接两倍又有点浪费空间。

Guava的做法则是加上如下计算

(int) ((float) expectedSize / 0.75F + 1.0F);

 

2.2 减小加载因子

在构造函数里,设定加载因子是0.5甚至0.25。
如果你的Map是一个长期存在而不是每次动态生成的,而里面的key又是没法预估的,那可以适当加大初始大小,同时减少加载因子,降低冲突的机率。毕竟如果是长期存在的map,浪费点数组大小不算啥,降低冲突概率,减少比较的次数更重要。

 

3. Key的设计

对于String型的Key,如果无法保证无冲突而且能用==来对比,那就尽量搞短点,否则一个个字符的equals还是花时间的。

甚至,对于已知的预定义Key,可以自己试着放一下,看冲不冲突。比如,像”a1”,”a2”,”a3” 这种,hashCode是个小数字递增,绝对是不冲突的:)

 

4. EnumMap

对于上面的问题,有些同学可能会很冲动的想,这么麻烦,我还是换回用数组,然后用常量来定义一些下标算了。其实不用自己来,EnumMap就是可读性与性能俱佳的实现。

EnumMap的原理是,在构造函数里要传入枚举类,那它就构建一个与枚举的所有值等大的数组,按Enum. ordinal()下标来访问数组,不就是你刚才想做的事情么?

美中不足的是,因为要实现Map接口,而 V get(Object key)中key是Object而不是泛型K,所以安全起见,EnumMap每次访问都要先对Key进行类型判断。在JMC里录得不低的采样命中频率。
所以也可以自己再port一个类出来,不实现Map接口,或者自己增加fastGet(),fastPut()的函数。

 

5. IntObjectHashMap

Netty以及其他FastUtils之类的原始类型map,都支持key是int或 long。但两者的区别并不仅仅在于int 换 Integer的那点空间,而是整个存储结构和Hash冲突的解决方法都不一样。

HashMap的结构是 Node[] table; Node 下面有Hash,Key,Value,Next四个属性。
而IntObjectHashMap的结构是int[] keys 和 Object[] values.

在插入时,同样把int先取模落桶,如果遇到冲突,则不采样HashMap的链地址法,而是用开放地址法(线性探测法)index+1找下一个空桶,最后在keys[index],values[index]中分别记录。在查找时也是先落桶,然后在key[index++]中逐个比较key。

所以,对比整个数据结构,省的不止是int vs Integer,还有每个Node的内容。
而性能嘛,IntObjectHashMap还是稳赢一点的,随便测了几种场景,耗时至少都有24ms vs 28ms的样子,好的时候甚至快1/3。

 

优化建议

  1. 考虑加载因子地设定初始大小
  2. 减小加载因子
  3. String类型的key,不能用==判断或者可能有哈希冲突时,尽量减少长度
  4. 使用定制版的EnumMap
  5. 使用IntObjectHashMap

在你的代码之外,服务时延过长的三个追查方向(下)

| Filed under 技术

本文含一些比较有趣的地方,所以和上一篇截开,免得大家看着swap,page cache没劲就把文章关了。

 

第二方面 : JVM篇 — 高IO波动下的JVM停顿

准备知识:JVM的Stop The World,安全点,黑暗的地底世界

在压测中非常偶然的超时,但CPU正常,网络正常,只有IO在刷盘时偶然写个几秒。那,就继续怀疑IO吧。

第一步,日志都异步写了吗?而且,异步队列满时会选择丢弃吗?

Logback在异步日志队列满时,默认并不是丢弃的而是阻塞等待的,需要自己配置。

或者干脆自己重新写一个更高效的异步Appender。

 

第二步,探究高IO 如何造成JVM停顿

有时GC日志都很正常,只有十几毫秒GC停顿,加上-XX:+PrintGCApplicationStoppedTime 后,真正的停顿才现形。

又或者像下面这样的,user+sys很低,但real很高,因为是多线程并发GC,所以real本来的时间应该是user+sys的十分之一不到的。


[ParNew: 1767244K->134466K(1887488K), 0.0331500 secs] 1767244K->134466K(3984640K), 0.0332520 secs] [Times: user=1.54 sys=0.00, real=1.04 secs]

但日志都异步了呀,JVM和IO还有什么关系?

来吧,用lsof彻底检查一下,去掉那些网络的部分,jar和so的文件

lsof -p $pid|grep /|grep -v ".jar"|grep -v ".so"

撇开熟悉的日志文件,发现这两个文件:


/tmp/hsperfdata_calvin/44337
/**我是马赛克**/**公司安全部门别找我**/myapp/logs/gc.log

 

第三步,GC日志

一拍脑袋,对哦,GC日志,如果它被锁住,GC的安全点就出不去哦......

但也不能不写GC日志啊,怎么办?

好在,早有知识储备,详见 Linux下tempfs及/dev/shm原理与应用。 简单说,/dev/shm 就是个默认存在,权限开放的,拿内存当硬盘用的分区,最大上限是总内存的一半,当系统内存不足时会使用swap。板着指头算了一下,由于每次JVM重启时GC日志都会被重置(所以你在启动脚本里总会先把GC日志备份下来),那以一秒写一条日志的速度,一点内存就够写几年了,于是放心的把日志指向它。

-Xloggc:/dev/shm/gc-myapplication.log

怎么好像从来没有人关心过GC日志被锁住的? 后来才发现,原来网上还是有Linkedin的同学关注过的:Eliminating Large JVM GC Pauses Caused by Background IO Traffic

 

第四步,hsperfdata文件

改完之后继续运行,超时虽然少了,还是有啊~~~

等等,还有一个文件什么鬼?

/tmp/hsperfdata_$user/$pid

涨知识的时刻到了。原来JVM在安全点里会将statistics写入这个文件,给jps,jstats命令用的。它本来已经是MMAP将文件映射到内存了,读写文件本不会锁,但无奈还是要更新inode元信息,在这里被锁了。在安全点里被锁。。。整个JVM就被锁了。

真不知道,JVM里原来还默默做了这事情。。。

解决办法,又把它指向/dev/shm? 可惜不行。所以,咬咬牙,把它禁止了,反正生产上都靠JMX,用不上jstats和jps。 有两种方式禁用它,据说-XX:-UsePerfData 会影响JVM本身做的优化,所以:

-XX:+PerfDisableSharedMem

又是哪个疑问,为什么很少听到有人说的,就我们全碰上了?其实有的,比如Cassandra就禁止了它,见JVM statistics cause garbage collection pauses

高IO可以造成各种问题,如果参考上篇,将Linux的刷盘阀值降低,可以治标不治本的缓解很多症状。
 

第三方面 : 后台辅助程序篇

把前面都都改完了,在线下压测也很棒了,在生产环境里以30ms为目标,还是会有非GC时段的超时,这是为什么呢?

再换个思路,是不是被其他程序影响了?虽然生产上每台机只会部署一个应用,但还有不少监控程序,日志收集程序在跑呀。

运行pidstat 1监控,这些程序不会吃满所有CPU啊。等等,pidstat 以秒来单位运行,见 从dstat理解Linux性能监控体系 ,那毫秒级的峰值它是监控不到的。

又到了发挥想象力乱猜的时刻,主要是看着收集日志的Flueme怎么都不顺眼,看久了忽然发现,Flueme的启动参数几乎是裸奔的,没有配什么GC参数。。。那么按ParallelGCThreads=8+( Processor - 8 ) ( 5/8 )的公式,24核的服务器,flueme 在Young GC时会有18个比较高优先级的线程在狂抢CPU,会不会是它把CPU给抢得太多了?

反正Flueme是后台收集日志的,停久一点也没所谓,所以加上-XX: ParallelGCThreads=4把它的并发GC线程数降低

参数修改前 15分钟内, 大于30ms的业务调用173次
参数修改后 246分钟内, 大于30ms业务调用 41次

应用被后台老王害了的典型例子。。。。。。。

 
本文内容薛院长亦有贡献,他基于strace的分析方法更工程化,这里就偷懒不写了。

调优路漫漫,超时还没彻底消失,比如偶然会出现SYS time较高的GC,系统又没有Swap ,未完待续。

继续女排的图片:

在你的代码之外,服务时延过长的三个追查方向(上)

| Filed under 技术

服务化体系里一般都有着严格的超时设定,为业务部门排查那些毛刺慢响应,也是基础架构部门的专家坐诊服务之一。

有时候,即使你的代码写的很努力了,但还是会出现慢响应,因为这本就是艰难的世界。 本文从三个方向上各举一些例子:

第一方面主要是热身,更加有趣的两方面见下集

 

第一方面,操作系统篇

准备知识《从Apache Kafka 重温文件高效读写》中Swap 和 PageCache的部分。

1. 禁用swap

Linux有个很怪的癖好,当内存不足时,看心情,有很大机率不是把用作IO缓存的Page Cache收回,而是把冷的应用内存page out到磁盘上(具体算法看准备知识)。当这段内存重新要被访问时,再把它重新page in回内存(所谓的主缺页错误),这个过程进程是停顿的。增长缓慢的老生代,池化的堆外内存,都可能被认为是冷内存,用 cat /proc/[pid]/status 看看 VmSwap的大小, 再dstat里看看监控page in发生的时间。

在 /etc/sysctl.conf 放入下面一句,基本可以杜绝swap。设成0会导致OOM,案例在此,有些同学就设成1,喜欢就好。

vm.swappiness = 10

 

2. 加快Page Cache Flush的频率

又是一个Linux自己的奇怪设置,Linux的Page Cache机制说来话长(还是看看准备知识), 简单说IO其实都默认不是先写磁盘,而是写进Page Cache内存,inode脏了30秒,或脏数据达到了10%可用内存(Free+PageCache -mmap),才开始起flusher线程写盘。

我们的生产机内存起码都剩20G的样子,想想要以普通硬盘100MB/s级别的速度,一次写2G文件的速度....好在一般都达不到这个条件,一般是由几个日志文件轮流30秒触发,一次写几百M,花个三秒左右的时间。

文章里说,后台刷盘线程不会阻塞应用程序的write(2)。但是,

应用的write 过程是这样的:
锁inode -> 锁page -> 写入page -> 解锁page -> 解锁inode -> 锁inode page -> 写inode page -> 解锁inode page

而flusher的过程是这样的:
锁page -> 将page放进IO队列,等待IO调度写盘完成返回 -> 解锁page

可见,还是有锁,IO调度器也不是绝对公平,当IO繁忙,应用还是会发生阻塞 。

我们的做法是用一个100MB的绝对值来代替可用内存百分比来作为阀值。

在 /etc/sysctl.conf 里加入

vm.dirty_background_bytes = 104857600

在第二篇举的JVM停顿的例子,全和IO相关,即使不做后面JVM的调优,光降低这个阀值,也能大大缓解。

当然什么值是最优,必须根据机器配置,应用特性来具体分析。

 

3. 网络参数

太多可配置的地方,可以参考阿里云团队的一个很好的文章 Linux TCP队列相关参数的总结。 还是那句,不能看着文章就开始设,必须根据自己的情况。

比如我们自己设置网卡软中断队列的CPU亲和度:

平时网卡中断可能只用一个核来响应,在大流量下那个核会跑满。
运行irqbalance,也只是用到了1个CPU,12个核。
最后自己设定24条网卡中断队列对应24个核,效果最佳。。。。。。但你的情况就不一定一样啊。

 
姑娘,漂亮:

JVM的Stop The World,安全点,黑暗的地底世界

| Filed under 技术

什么是安全点

GC时的Stop the World(STW)是大家最大的敌人。但可能很多人没留意,除了GC,JVM底下还会发生这样那样的停顿。

JVM里有一条特殊的线程--VM Threads,专门用来执行一些特殊的VM Operation,比如分派GC的STW,thread dump等,这些任务,都需要整个Heap,以及所有线程的状态是静止的,一致的才能进行。所以JVM引入了安全点(Safe Point)的概念,想办法在需要进行VM Operation时,通知所有的线程进入一个静止的安全点。 如何做到的见 聊聊JVM(九)理解进入safepoint时如何让Java线程全部阻塞

除了GC,其他触发安全点的VM Operation包括:

  • 1. JIT相关,比如Code deoptimization, Flushing code cache
  • 2. Class redefinition (e.g. javaagent,AOP代码植入的产生的instrumentation)
  • 3. Biased lock revocation 取消偏向锁
  • 4. Various debug operation (e.g. thread dump or deadlock check)

打开JDK7源码的vm_operations.hpp,有一个长长的列表,比如ReportJavaOutOfMemory等等不能尽录。

 

监控安全点

是不是看得心里发毛,马上就想监控下自己的JVM到底发生了什么?

最简单的做法,在JVM启动参数的GC参数里,多加一句:

-XX:+PrintGCApplicationStoppedTime

它就会把全部的JVM停顿时间(不只是GC),打印在GC日志里。

2016-08-22T00:19:49.559+0800: 219.140: Total time for which application threads were stopped: 0.0053630 seconds

这真是个很有用很有用的必配参数,真正忠实的打出,几乎一切的停顿。。。。

但是,在JDK1.7.40以前的版本,它居然没有打印时间戳,所以只能知道JVM停了多久,但不知道什么时候停的。此时一个土办法就是加多一句“ -XX:+PrintGCApplicationConcurrentTime”,打印JVM在两次停顿之间的正常运行时间(同样没有时间戳),但好歹能配合有时间戳的GC日志,反推出Stop发生的时间了。

2016-08-22T00:19:50.183+0800: 219.764: Application time: 5.6240430 seconds

 

怎么进的安全点?

好,我现在知道有什么停顿了,但怎么知道因为上面列的哪种原因而停顿呢?
特别是,怎么好像多了很多时间非常非常短的没有GC日志伴随的停顿?

那,再多加两个参数

-XX:+PrintSafepointStatistics -XX: PrintSafepointStatisticsCount=1

此时,在stdout中会打出类似的内容


vmop [threads: total initially_running wait_to_block]
1913.425: GenCollectForAllocation [ 55 2 0 ]

[time: spin block sync cleanup vmop] page_trap_count
[ 0 0 0 0 6 ] 0

此日志分两段,第一段是时间戳,VM Operation的类型,以及线程概况

  • total: 安全点里的总线程数
  • initially_running: 安全点时开始时正在运行状态的线程数
  • wait_to_block: 在VM Operation开始前需要等待其暂停的线程数

第二行是到达安全点时的各个阶段以及执行操作所花的时间,其中最重要的是vmop

  • spin: 等待线程响应safepoint号召的时间
  • block: 暂停所有线程所用的时间
  • sync: 等于 spin+block,这是从开始到进入安全点所耗的时间,可用于判断进入安全点耗时
  • cleanup: 清理所用时间
  • vmop: 真正执行VM Operation的时间

噢,原来那些很多但又很短的安全点,全都是RevokeBias,详见偏向锁实现原理, 高并发的应用一般会干脆在启动参数里加一句"-XX:-UseBiasedLocking"取消掉它。

另外还看到有些类型是no vm operation, 文档上说是保证每秒都有一次进入安全点(如果这秒已经GC过就不用了),给一些需要在安全点里进行,又非紧急的操作使用,比如一些采样型的Profiler工具,可用-DGuaranteedSafepointInterval来调整,不过实际看它并不是每秒都会发生,时间不定。

 

那长期打开安全点日志如何?

在实战中,我们利用安全点日志,发现过有程序定时调用Thread Dump等等情况。

但是,

1. 安全点日志默认输出到stdout,一是stdout日志的整洁性,二是stdout所重定向的文件如果不在/dev/shm,可能被锁。
2. 对于一些很短的停顿,比如取消偏向锁,打印的消耗比停顿本身还大。
3. 安全点日志是在安全点内打印的,本身加大了安全点的停顿时间。

所以并不建议默认长期打开,只在问题排查时打开。

 

把安全点日志打印到独立文件

如果在生产系统上要打开,再再增加下面四个参数:

-XX:+UnlockDiagnosticVMOptions -XX: -DisplayVMOutput -XX:+LogVMOutput -XX:LogFile=/dev/shm/vm.log

打开Diagnostic(只是开放了更多的flag可选,不会主动激活某个flag),关掉输出VM日志到stdout,输出到独立文件,我最喜欢的/dev/shm目录(内存文件系统)。

 

结论

  1. -XX:+PrintGCApplicationStoppedTime 很重要,一定要打开。
  2. 取消偏向锁
  3. 只在排查问题时打开安全点日志

 

参考资料

我部内置研究院的薛院长对本文亦有贡献,转载请保留原文链接: http://calvin1978.blogcn.com/articles/safepoint.html

Tim那天在群里说打猎去了,遥想英姿,配图一二。

记一次ClassLoader死锁,及近距离感受R大

| Filed under 技术

死锁,通常都是别人的故事,从未想过会发生在自己身上。

今天不光遇见了死锁,还第一次享受了JVM界顶级大佬R大(RednaxelaFX,知乎问题:R大是谁 ) 的手把手指导,特别有意义,所以给各位看官一记。

感受之一: R大乐于助人的程度,中美互联网加起来也没有几个能比得上。
感受之二: R大的脑容量和打字速度都是别人的七八倍,有人直接认为R大其实是代码成精变的。
感受之三: 在基础架构部这种地方,瞎蒙的运气,强大的观察力,专业的技能,都那么重要。

 

第一回合

今天把JMeter里的Netty也换成Netty Native模式,然后压测就停在那不动了。。。。。。

第一时间jstack,得到如下输出,看到Netty在加载Native库时, Object.wait() [0x00007fb274057000]被锁住了。 同时有一个在JMX里注册Metrics的线程成为了受害者,Netty的线程锁住了它所需要的”java.lang.Runtime<0x00000000ecee76e0>"

马上想看看这个Object.wait() [0x00007fb274057000]是啥。

同事建议用jstack -m pid再看看,结果那条线程的输出如下,还是没看出来所以然。

 

第二回合

此时就只好群里找R大求救。。。。R大在大洋彼岸晚饭中,让我用Sun的JDK自带的CLHSDB看看。

于是一边学习R大的文章借HSDB来探索HotSpot VM的运行时数据 ,一边生成CoreDump (先ulimit -c unlimited,再运行jmeter,再kill -7 pid, 就会在程序目录里有一个core.xxx文件)。

可惜真的不会用CLHSDB,whatis 0x00007fb274057000 说无效地址。

第三回合

于是决定蒙一下,虽然JMX那条线程在jstack看起来只是受害者,但还是尝试把它禁止掉。翻代码翻到一个启动参数,感谢那个留了后门的同事,加上去一试,不锁了。。。。

赶紧把这个结论告诉负责Metrics模块的同事,然后,他很快就找到了原因,注册JMX那线程的栈里有一句:

at sun.nio.ch.FileChannelImpl.(FileChannelImpl.java:1171)

然后,Netty的epoll代码里也有一句:

jclass fileChannelCls = (*env)->FindClass(env, "sun/nio/ch/FileChannelImpl")

应该就是这两句之间锁了。问同事怎么这么快定位到FileChannelImpl头上的,同事说先看Netty的对应代码,再人肉一句句对jstack的输出。

 

第四回合

把结论告诉R大,此时R大已经有空了,就开始手把手教学--如何用gdb来找到这个问题,避免下次还要靠瞎蒙与肉眼观察来解决。

1.用gdb打开coredump

gdb [path of Java] core.xxx

因为要用一模一样的executable(java),所以core.xxx拿回家玩没用,必须就地打开。

2.找到对应的线程

jstack里的线程号是16进制的,先转回10进制,再对应回gdb里的线程号,这里Netty的线程号是17, jmx是56。

3. 进入Netty线程

(gdb) thread 17
[Switching to thread 17 (Thread 0x7fac9ce2c700 (LWP 35366))]#0 0x00007fad2932b5bc in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0

4.打印栈帧

jstack -m 里看到的在这里展开了。

5.进入第7层 Netty Epoll的JNI_OnLoad()函数

(gdb) frame 7
#7 0x00007fac9c404b23 in JNI_OnLoad () from /apps/svr/jmeter/native/libnetty-transport-native-epoll3448157366930317836.so

6. 列出20条指令

厉害的R大这里继续让我输入
(gdb) p (char*) 0x7fac9c4057ac
$2 = 0x7fac9c4057ac "sun/nio/ch/FileChannelImpl"

证明的确是加载"sun/nio/ch/FileChannelImpl”这个类锁了。

一头雾水的问,怎么知道要打印0x7fac9c4057ac??
R大解析,有个箭头的地方表明指令执行到这里,是调用方法的指令,再之前就是传递参数的语句,rsi是传递第二个参数的寄存器,所以# 0x7fac9c4057ac的注释就是char*的地址,遂打印之。

如果有debug info,可以采用更简单的方法查看,但明显发行版的JDK里不会有,No symbol table is loaded。

收获满满,下次遇到Natvie里的ClassLoader死锁问题也能GDB之了。

 

结论

原来因为程序里的两句代码:1. Netty异步建立连接,2.同步注册一个连接池Metrics的MBean。此时Netty因为要加载Native Lib,hold住了java.runtime,然后在JNI代码里尝试去加载FileChannelImpl类。同一时刻,JMX注册的进程先加载了FileChannelImp类,然后在某一步又尝试去获得java.runtime,传说中的死锁就这样发生了。

改进的方法很简单,尽早把JMX的MBean Server简单加载一下就好。

再次感谢R大和我的同事国忠。

转载请保留链接: http://calvin1978.blogcn.com/articles/classloader-deadlock.html

这么枯草的文章,配个图吧。

by calvin | tags : | 5

Netty资料皆阵列在前

| Filed under 技术

自己收集的一些Netty资料,不断更新的大仓库,大家就别原文复制黏贴转载了。

 

体系化资料

 

自己码的文章

 

精挑细选

 

Netty项目

by calvin | tags : | 3

Java应用调优之-总览导航

| Filed under 技术

1. 目标

调优之前,先认清自己的目标。

1. QPS
地球人都懂的指标。

2. 平均值,中值响应时间
其实,中值比平均值更好,因为平均值可能被少量极高的最高值拉高,而中值是所有结果值里位置排在中间的那个值。

3. 99 . 99%响应时间
99. 99%延时=30ms的意思就是,99 . 99%的请求,都能在30ms毫秒以内完成。
对于服务SLA的评估,毛刺率也很重要,一个服务可能平均响应时间是2ms,但遇上GC等Java绕不过去的坎,20ms也可以接受,但如果是200ms,就要找原因了。

4. CPU消耗
CPU能省就省,特别是docker时代,省了自己就方便了别人。
但CPU也要能压上去,否则就是哪里有锁了。

5. 内存、网络IO、磁盘IO消耗
特定场景特定需求。
 

2. 为什么说调优是艺术

说调优是艺术,因为它源于深厚的知识,丰富的经验和敏锐的直觉 -《Java性能权威指南》
所有网上的文章,都只能拓展你知识的深度。

《认清性能问题》 by 瞬息之间翻译

 

3. 文章一览

3.1 基本的准备

 

3.2 面向GC编程

 

3.3 并发与锁

 

3.4 JVM优化

 

3.5 其他技巧

 

4. 资料

4.1 书籍

《Java性能权威指南》
比起多年前那部调优圣经,讲得更加深入,也更加贴近现在的JDK。

《深入理解 Java 虚拟机 第2版》
理解虚拟机并不是那么难,Java程序员来说,很多知识其实是必须的。另外还有几本类似主题的书,忽然一下子都出来了。

《大话Java性能优化》
把书列于此不代表我完全同意书中的所有观点,仅供参考,辩证阅读。

 

4.2 网站

 

Tomcat线程池,更符合大家想象的可扩展线程池

| Filed under 技术

因由

说起线程池,大家可能受连接池的印象影响,天然的认为,它应该是一开始有core条线程,忙不过来了就扩展到max条线程,闲的时候又回落到core条线程,如果还有更高的高峰,就放进一个缓冲队列里缓冲一下。

有些整天只和SSH打交道的同学,可能现在还是这样认为的。

无情的现实就是,JDK只有两种典型的线程池,FixedPool 与 CachedPool:

  • FixedPool固定线程数,忙不过来的全放到无限长的缓冲队列里。
  • CachedPool,忙不过来时无限的增加临时线程,闲时回落,没有缓冲队列。

Java ThreadPool的正确打开方式里,建议了如何设置,避免上面两句吓人的“无限”。但无论怎么配,都无法用现成的东西,配出文章一开始的想象图来。

但不得不说,这幅想象图还是比较美好的,特别对于偶有卡顿,偶有不可预测高峰的业务线程池来说。

当然,也有人说请求积压在最后的缓冲队列里不好控制,看具体业务场景了,缓冲队列也有不同的玩法(后详)。

 

原理

我们的同事老王,研究了一番ThreadPool的机制后,提出了自己实现队列的方式。碰巧,Tomcat也正是这么做的,比起Jetty完全自己写线程池,Tomcat基于JDK的线程池稍作定制,要斯文一些。

JDK线程池的逻辑很简单( 更详细描述还是见Java ThreadPool的正确打开方式

- 前core个请求,来一个请求就创建一个线程。
- 之后,把请求插入缓冲队列里让所有的线程去抢;如果插入失败则创建新线程。
- 如果达到max条线程了,抛出拒绝异常。

貌似控制的枢纽都在第2句那里--队列插入的结果。JDK也是通过使用LinkedBlockingQueue 与 特殊的SynchronousQueue,实现自己的控制效果。

那我可不可以自己封装一个Queue,在插入时增加以下逻辑呢?

  • 如果当前有空闲线程等待接客,则把任务加入队列让孩儿们去抢。
  • 如果没有空闲的了,总线程数又没到达max,那就返回false,让Executor去创建线程。
  • 如果总线程数已达到max,则继续把任务加入队列缓冲一下。
  • 如果缓冲队列也满了,抛出拒绝异常。

说白了就是这么简单。

 

Tomcat的实现

Tomcat的TaskQueue实现:

public class TaskQueue extends LinkedBlockingQueue {

@Override

public boolean offer(Runnable o) {

if (parent . getPoolSize() == parent . getMaximumPoolSize()) return super . offer(o);

if (parent . getSubmittedCount() < parent.getPoolSize()) return super . offer(o);

if (parent . getPoolSize() < parent . getMaximumPoolSize()) return false;

return super.offer(o);

}

}

非常简单的代码,唯一要说明的是,如何判断当前有没有空闲的线程等待接客。JDK的CachedPool是靠特殊的零长度的SynchronousQueue实现。而Tomcat则靠扩展Executor ,增加一个当前请求数的计数器,在execute()方法前加1,再重载afterExecute()方法减1,然后判断当前线程总数是否大于当前请求总数就知道有咩有围观群众。

 

更进一步

因为相信Tomcat这种百年老店,我们就不自己写这个池了,把Tomcat实现里一些无关需求剥掉即用。

但Tomcat就完美了吗?

首先,TaskQueue的offer()里,调用了executor.getPoolSize(),这是个有锁的函数,这是最遗憾的地方,在大家都在嫌线程池里一条队列锁得太厉害,像ForkJoinPool或Netty的设计都是一个线程一个队列时,这个有锁的函数相当碍眼。而且,最过分的是,Tomcat居然一口气调了三次(在Tomcat9 M9依然如此)。反复看了下,不求那么精准的话貌似一次就够了,真的有并发的变化的情况,executor里还有个处理RejectException,把任务重新放回队列的保险。

最后,说说缓冲队列的两种玩法:

一种是队列相对比较长,比如4096,主线程把任务丢进去就立刻返回了,如果队列满了就直接报拒绝异常。

一种是队列相对比较短的,比如512,如果满了,主线程就以queue.force(command, timeout)等在那里等队列有空,等到超时才报拒绝异常。

Tomcat的机制支持这两种玩法,自己设置就好。

 
文章可能还要修改,转载请保留原文链接,否则视为侵权:
http://calvin1978.blogcn.com/articles/tomcat-threadpool.html

一只Tomcat: