花钱的年华

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

Netty之有效规避内存泄漏

| Filed under 技术

有过痛苦的经历,特别能写出深刻的文章 —— 凯尔文. 肖

直接内存是IO框架的绝配,但直接内存的分配销毁不易,所以使用内存池能大幅提高性能,也告别了频繁的GC。但,要重新培养被Java的自动垃圾回收惯坏了的惰性。

Netty有一篇必读的文档 官方文档翻译:引用计数对象 ,在此基础上补充一些自己的理解和细节。

 

1.为什么要有引用计数器

Netty里四种主力的ByteBuf,
其中UnpooledHeapByteBuf 底下的byte[]能够依赖JVM GC自然回收;而UnpooledDirectByteBuf底下是DirectByteBuffer,如Java堆外内存扫盲贴所述,除了等JVM GC,最好也能主动进行回收;而PooledHeapByteBuf 和 PooledDirectByteBuf,则必须要主动将用完的byte[]/ByteBuffer放回池里,否则内存就要爆掉。所以,Netty ByteBuf需要在JVM的GC机制之外,有自己的引用计数器和回收过程。

一下又回到了C的冰冷时代,自己malloc对象要自己free。 但和C时代又不完全一样,内有引用计数器,外有JVM的GC,情况更为复杂。

 

2. 引用计数器常识

  • 计数器基于 AtomicIntegerFieldUpdater,为什么不直接用AtomicInteger?因为ByteBuf对象很多,如果都把int包一层AtomicInteger花销较大,而AtomicIntegerFieldUpdater只需要一个全局的静态变量。
  • 所有ByteBuf的引用计数器初始值为1。
  • 调用release(),将计数器减1,等于零时, deallocate()被调用,各种回收。
  • 调用retain(),将计数器加1,即使ByteBuf在别的地方被人release()了,在本Class没喊cut之前,不要把它释放掉。
  • 由duplicate(), slice()和order()所衍生的ByteBuf,与原对象共享底下的buffer,也共享引用计数器,所以它们经常需要调用retain()来显示自己的存在。
  • 当引用计数器为0,底下的buffer已被回收,即使ByteBuf对象还在,对它的各种访问操作都会抛出异常。

 

3.谁来负责Release

在C时代,我们喜欢让malloc和free成对出现,而在Netty里,因为Handler链的存在,ByteBuf经常要传递到下一个Hanlder去而不复还,所以规则变成了谁是最后使用者,谁负责释放。

另外,更要注意的是各种异常情况,ByteBuf没有成功传递到下一个Hanlder,还在自己地界里的话,一定要进行释放。

3.1 InBound Message

在AbstractNioByteChannel.NioByteUnsafe.read() 处创建了ByteBuf并调用 pipeline.fireChannelRead(byteBuf) 送入Handler链。

根据上面的谁最后谁负责原则,每个Handler对消息可能有三种处理方式

  • 对原消息不做处理,调用 ctx.fireChannelRead(msg)把原消息往下传,那不用做什么释放。
  • 将原消息转化为新的消息并调用 ctx.fireChannelRead(newMsg)往下传,那必须把原消息release掉。
  • 如果已经不再调用ctx.fireChannelRead(msg)传递任何消息,那更要把原消息release掉。

假设每一个Handler都把消息往下传,Handler并也不知道谁是启动Netty时所设定的Handler链的最后一员,所以Netty在Handler链的最末补了一个TailHandler,如果此时消息仍然是ReferenceCounted类型就会被release掉。
 

3.2 OutBound Message

要发送的消息由应用所创建,并调用 ctx.writeAndFlush(msg) 进入Handler链。在每个Handler中的处理类似InBound Message,最后消息会来到HeadHandler,再经过一轮复杂的调用,在flush完成后终将被release掉。

 

3.3 异常发生时的释放

多层的异常处理机制,有些异常处理的地方不一定准确知道ByteBuf之前释放了没有,可以在释放前加上引用计数大于0的判断避免释放失败;

有时候不清楚ByteBuf被引用了多少次,但又必须在此进行彻底的释放,可以循环调用reelase()直到返回true。

 

4. 内存泄漏检测

所谓内存泄漏,主要是针对池化的ByteBuf。ByteBuf对象被JVM GC掉之前,没有调用release()把底下的DirectByteBuffer或byte[]归还到池里,会导致池越来越大。而非池化的ByteBuf,即使像DirectByteBuf那样可能会用到System.gc(),但终归会被release掉的,不会出大事。

Netty担心大家不小心就搞出个大新闻来,因此提供了内存泄漏的监测机制。

Netty默认会从分配的ByteBuf里抽样出大约1%的来进行跟踪。如果泄漏,会有如下语句打印:

LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetectionLevel=advanced' or call ResourceLeakDetector.setLevel()

这句话报告有泄漏的发生,提示你用-D参数,把防漏等级从默认的simple升到advanced,就能具体看到被泄漏的ByteBuf被创建和访问的地方。

  • 禁用(DISABLED) - 完全禁止泄露检测,省点消耗。
  • 简单(SIMPLE) - 默认等级,告诉我们取样的1%的ByteBuf是否发生了泄露,但总共一次只打印一次,看不到就没有了。
  • 高级(ADVANCED) - 告诉我们取样的1%的ByteBuf发生泄露的地方。每种类型的泄漏(创建的地方与访问路径一致)只打印一次。对性能有影响。
  • 偏执(PARANOID) - 跟高级选项类似,但此选项检测所有ByteBuf,而不仅仅是取样的那1%。对性能有绝大的影响。

实现细节

每当各种ByteBufAllocator 创建ByteBuf时,都会问问是否需要采样,Simple和Advanced级别下,就是以113这个素数来取模(害我看文档的时候还在瞎担心,1%,万一泄漏的地方有所规律,刚好躲过了100这个数字呢,比如都是3倍数的),命中了就创建一个Java堆外内存扫盲贴里说的PhantomReference。然后创建一个Wrapper,包住ByteBuf和Reference。

simple级别下,wrapper只在执行release()时调用Reference.clear(),Advanced级别下则会记录每一个创建和访问的动作。

当GC发生,还没有被clear()的Reference就会被JVM放入到之前设定的ReferenceQueue里。

在每次创建PhantomReference时,都会顺便看看有没有因为忘记执行release()把Reference给clear掉,在GC时被放进了ReferenceQueue的对象,有则以 "io.netty.util.ResourceLeakDetector”为logger name,写出前面例子里的Error级别的日日志。顺便说一句,Netty能自动匹配日志框架,先找Slf4j,再找Log4j,最后找JDK logger。

值得说三遍的事

一定要盯紧log里有没有出现 "LEAK: "字样,因为simple级别下它只会出现一次,所以不要依赖自己的眼睛,要依赖grep。如果出现了,而且你用的是PooledBuf,那一定是问题,不要有任何的侥幸,立刻用"-Dio.netty.leakDetectionLevel=advanced" 再跑一次,看清楚它创建和访问的地方。

功能测试时,最好开着"-Dio.netty.leakDetectionLevel=paranoid"。

但是,怎么测试都可能存在没有覆盖到的分支。如果内存尚够,可以适当把-XX:MaxDirectMemorySize 调大,反正只是max,平时也不会真用了你的。然后监控其使用量,及时报警。

 
片末招聘广告:唯品会广州总部的基础架构部招人!! 如果你喜欢纯技术的工作,对大型互联网企业的服务化平台有兴趣,愿意在架构的成长期还可以大展拳脚的时候加盟,请电邮 calvin.xiao@vipshop.com

 
文章持续修订,转载请保留原链接: http://calvin1978.blogcn.com/articles/netty-leak.html

by calvin | tags : | 7

Netty之Java堆外内存扫盲贴

| Filed under 技术

Java的堆外内存本来是高贵而神秘的东西,只在一些缓存方案的收费企业版里出现。但自从用了Netty,就变成了天天打交道的事情,毕竟堆外内存能减少IO时的内存复制,不需要堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中;而且也没了烦人的GC。

好在,Netty所用的堆外内存只是Java NIO的 DirectByteBuffer类,通读一次很快。还有一些sun.misc.*的类木有源码,要自己跑去OpenJdk那看个明白。

 

1. 堆外内存的创建

在DirectByteBuffer中,首先向Bits类申请额度,Bits类有一个全局的 totalCapacity变量,记录着全部DirectByteBuffer的总大小,每次申请,都先看看是否超限 -- 堆外内存的限额默认与堆内内存(由-XMX 设定)相仿,可用 -XX:MaxDirectMemorySize 重新设定。

如果已经超限,会主动执行Sytem.gc(),期待能主动回收一点堆外内存。然后休眠一百毫秒,看看totalCapacity降下来没有,如果内存还是不足,就抛出大家最头痛的OOM异常。

如果额度被批准,就调用大名鼎鼎的sun.misc.Unsafe去分配内存,返回内存基地址,Unsafe的C++实现在此,标准的malloc。然后再调一次Unsafe把这段内存给清零。跑个题,Unsafe的名字是提醒大家这个类只给Sun自家用的,你们别用,不然哪天Sun把它藏起来了你们就哭死。果然,JDK9里就Oracle可能动手哦

JDK7开始,DirectByteBuffer分配内存时默认已不做分页对齐,不会再每次分配并清零 实际需要+分页大小(4k)的内存,这对性能应有较大提升,所以Oracle专门写在了Enhancements in Java I/O里。

最后,创建一个Cleaner,并把代表清理动作的Deallocator类绑定 -- 降低Bits里的totalCapacity,并调用Unsafe调free去释放内存。Cleaner的触发机制后面再说。

 

2. 堆外内存基于GC的回收

存在于堆内的DirectByteBuffer对象很小,只存着基地址和大小等几个属性,和一个Cleaner,但它代表着后面所分配的一大段内存,是所谓的冰山对象。通过前面说的Cleaner,堆内的DirectByteBuffer对象被GC时,它背后的堆外内存也会被回收。

快速回顾一下堆内的GC机制,当新生代满了,就会发生young gc;如果此时对象还没失效,就不会被回收;撑过几次young gc后,对象被迁移到老生代;当老生代也满了,就会发生full gc。

这里可以看到一种尴尬的情况,因为DirectByteBuffer本身的个头很小,只要熬过了young gc,即使已经失效了也能在老生代里舒服的呆着,不容易把老生代撑爆触发full gc,如果没有别的大块头进入老生代触发full gc,就一直在那耗着,占着一大片堆外内存不释放。

这时,就只能靠前面提到的申请额度超限时触发的system.gc()来救场了。但这道最后的保险其实也不很好,首先它会中断整个进程,然后它让当前线程睡了整整一百毫秒,而且如果gc没在一百毫秒内完成,它仍然会无情的抛出OOM异常。还有,万一,万一大家迷信某个调优指南设置了-DisableExplicitGC禁止了system.gc(),那就不好玩了。

所以,堆外内存还是自己主动点回收更好,比如Netty就是这么做的。

 

3. 堆外内存的主动回收

对于Sun的JDK这其实很简单,只要从DirectByteBuffer里取出那个sun.misc.Cleaner,然后调用它的clean()就行。

前面说的,clean()执行时实际调用的是被绑定的Deallocator类,这个类可被重复执行,释放过了就不再释放。所以GC时再被动执行一次clean()也没所谓。

在Netty里,因为不确定跑在Sun的JDK里(比如安卓),所以多废了些功夫来确定Cleaner的存在。

 

4. Cleaner如何与GC相关联?

涨知识的时间到了,原来JDK除了StrongReference,SoftReference 和 WeakReference之外,还有一种PhantomReference,Phantom是幻影的意思,Cleaner就是PhantomReference的子类。

当GC时发现它除了PhantomReference外已不可达(持有它的DirectByteBuffer失效了),就会把它放进 Reference类pending list静态变量里。然后另有一条ReferenceHandler线程,名字叫 "Reference Handler"的,关注着这个pending list,如果看到有对象类型是Cleaner,就会执行它的clean(),其他类型就放入应用构造Reference时传入的ReferenceQueue中,这样应用的代码可以从Queue里拖出这些理论上已死的对象,做佳节又重阳爱做的事情——这是一种比finalizer更轻量更好的机制。

 

5. 其实

专家们说,OpenJDK没有接受jemalloc(redis们在用)的补丁,直接用malloc在OS里申请一段内存,比在已申请好的JVM堆内内存里划一块出来要慢,所以我们在Netty一般用池化的 PooledDirectByteBuf 对DirectByteBuffer进行重用 ,《Netty权威指南》说性能提升了23倍,所以基本不需要头痛堆外内存的释放,顺便还告别了大数据流量下的频繁GC。

 
片末招聘广告:唯品会广州总部的基础架构部招人!! 如果你喜欢纯技术的工作,对大型互联网企业的服务化平台有兴趣,愿意在架构的成长期还可以大展拳脚的时候加盟,请电邮 calvin.xiao@vipshop.com

 
文章持续修订,转载请保留原链接: http://calvin1978.blogcn.com/articles/directbytebuffer.html

Apache Thrift设计概要

| Filed under 技术

最近把Apache Thrift 的Java版代码翻了一遍,尝试理解做一个RPC框架所要考虑的方方面面。

网上关于Thrift设计的文章好像不多,于是把自己的笔记整理了一下发上来。

加插招聘广告:唯品会广州总部的基础架构部招人!! 如果你喜欢纯技术的工作,对大型互联网企业的服务化平台有兴趣,愿意在架构的成长期还可以大展拳脚的时候加盟,请电邮 calvin.xiao@vipshop.com

1. Overview

Apache Thrift 的可赞之处是实现了跨超多语言(Java, C++, Go, Python,Ruby, PHP等等)的RPC框架,尽力为每种语言实现了相同抽象的RPC客户端与服务端。

简洁的四层接口抽象,每一层都可以独立的扩展增强或替换,是另一个可赞的地方。

最后,二进制的编解码方式和NIO的底层传输为它提供了不错的性能。

+-------------------------------------------+
| Server |
| (single-threaded, event-driven etc) |
+-------------------------------------------+
| Processor |
| (compiler generated) |
+-------------------------------------------+
| Protocol |
| (JSON, compact etc) |
+-------------------------------------------+
| Transport |
| (raw TCP, HTTP etc) |
+-------------------------------------------+

Transport层提供了一个简单的网络读写抽象层,有阻塞与非阻塞的TCP实现与HTTP的实现。

Protocol层定义了IDL中的数据结构与Transport层的传输数据格式之间的编解码机制。传输格式有二进制,压缩二进制,JSON等格式,IDL中的数据结构则包括Message,Struct,List,Map,Int,String,Bytes等。

Processor层由编译器编译IDL文件生成。
生成的代码会将传输层的数据解码为参数对象(比如商品对象有id与name两个属性,生成的代码会调用Protocol层的readInt与readString方法读出这两个属性值),然后调用由用户所实现的函数,并将结果编码送回。

在服务端, Server层创建并管理上面的三层,同时提供线程的调度管理。而对于NIO的实现,Server层可谓操碎了心。

在客户端, Client层由编译器直接生成,也由上面的三层代码组成。只要语言支持,客户端有同步与异步两种模式。

Facebook是Thrift的原作者,还开源有NiftySwift两个子项目, Cassandra是另一个著名用户,其跨语言的Client包就是基于Thrift的,这些在下一篇文章中展开讨论。

 

2. Transport层

2.1 Transport

TTransport除了open/close/flush,最重要的方法是int read(byte[] buf, int off, int len),void write(byte[] buf, int off, int len),读出、写入固定长度的内容。

TSocket使用经典的JDK Blocking IO的Transport实现。1K的BufferedStream, TCP配置项:KeepAlive=true,TCPNoDelay=true,SoLinger=false,SocketTimeout与ConnectionTimeout可配。

TNonblockingSocket,使用JDK NIO的Transport实现,读写的byte[]会每次被wrap成一个ByteBuffer。TCP配置项类似。
其实这个NonBlockingSocket并没有完全隔离传输层,后面异步Client或NIO的Server还有很多事情要做。

相应的,TServerSocket和TNonblockingServerSocket是ServerTransport的BIO、NIO实现,主要实现侦听端口,Accept后返回TSocket或TNonblockingSocket。其他TCP配置项:ReuseAddress=true,Backlog与SocketTimeout可配。

2.2 WrapperTransport

包裹一个底层的Transport,并利用自己的Buffer进行额外的操作。

1. TFramedTransport,按Frame读写数据。

每Frame的前4字节会记录Frame的长度(少于16M)。读的时候按长度预先将整Frame数据读入Buffer,再从Buffer慢慢读取。写的时候,每次flush将Buffer中的所有数据写成一个Frame。

有长度信息的TFramedTransport是后面NonBlockingServer粘包拆包的基础。

2. TFastFramedTransport 与TFramedTransport相比,始终使用相同的Buffer,提高了内存的使用率。

TFramedTransport的ReadBuffer每次读入Frame时都会创建新的byte[],WriteBuffer每次flush时如果大于初始1K也会重新创建byte[]。

而TFastFramedTransport始终使用相同的ReadBuffer和WriteBuffer,都是1K起步,不够时自动按1.5倍增长,和NIO的ByteBuffer一样,加上limit/pos这样的指针,每次重复使用时设置一下它们。

3. TZlibTransport ,读取时按1K为单位将数据读出并调用JDK的zip函数进行解压再放到Buffer,写入时,在flush时先zip再写入。

4. TSaslClientTransport与TSaslServerTransport, 提供SSL校验。

 

3. Protocol层

3.1 IDL定义

Thrift支持结构见 http://thrift.apache.org/docs/types

* 基本类型: i16,i32,i64, double, boolean,byte,byte[], String。
* 容器类型: List,Set,Map,TList/TSet/TMap类包含其元素的类型与元素的总个数。
* Struct类型,即面向对象的Class,继承于TBase。TStruct类有Name属性,还含有一系列的Field。TField类有自己的Name,类型,顺序id属性。
* Exception类型也是个Struct,继承于TException这个checked exception。
* Enum类型传输时是个i32。
* Message类型封装往返的RPC消息。TMessage类包含Name,类型(请求,返回,异常,ONEWAY)与seqId属性。

相应的,Protocol 层对上述数据结构有read与write的方法。
对基本类型是直接读写,对结构类型则是先调用readXXXBegin(),再调用其子元素的read()方法,再调用readXXXEnd()。
在所有函数中,Protocol层会直接调用Transport层读写特定长度的数据。

3.2 TBinaryProtocol

如前所述,i16,i32, double这些原始类型都是定长的,String,byte[]会在前4个字节说明自己的长度,容器类,Strutct类所对应的TMap,TStruct,TField,TMessage里有如前所述的属性 (不过Struct与Field里的name属性会被skip),所以其实现实可以简单想象的。

3.3 TCompactProtocol

比起TBinaryProtocol,会想方设法省点再省点。

1. 对整数类型使用了 ZigZag 压缩算法,数值越小压的越多。比如 i32 类型的整数本来是4个字节, 可以压缩成 1~5 字节不等。而 i64类型的整数本来是8个字节,可以压缩成 1~10 字节不等。 因此,值小的i32和i64,和小的Collection和短的String(因为它们都有定义长度的int属性) 越多,它就能省得越多。

2. 它还会尝试将Field的fieldId和type挤在一个byte里写。原本field是short,type也占一个byte,合共三个byte。 它将1个byte拆成两组4bit,前4bit放与前一个field的Id的delta值(不能相差超过15,如果中间太多没持久化的optional field),后4bit放type(目前刚好16种type,把byte[]和String合成一种了)

3.4 TTupleProtocol

继承于TCompactProtocol,Struct的编解码时使用更省地方但IDL间版本不兼容的TupleScheme,见Processor层。

3.5 TJSONProtocol

与我们平时的Restful JSON还是有点区别,具体的定义看cpp实现的TJSONProtool.h文件

对于容器类和Message,会用数组的方式,前几个元素是元信息,后面才是value,比如List是[type,size,[1,2,3]],

对于Struct,会是个Map,Key是fieldId而不是name(为了节省空间?),Value又是一个Map,只有一个Key-Value Pair,Key是type,Value才是真正value。

 

4. Processor层

建议使用一个最简单的IDL文件,用Windows版的Generator生成一个来进行观察。

4.1 基础接口

TBase是大部分生成的Struct,参数类,结果类的接口,最主要是实现从Protocol层读写自己的函数。

TProcessFunction是生成的服务方法类的基类,它的process函数会完成如下步骤:

1. 调用生成的args对象的read方法从protocol层读出自己
2. 调用子类生成的getResult()方法:拆分args对象得到参数,调用真正的用户实现得到结果,并组装成生成的result对象。
3. 写消息头,
4. 调用生成的result对象的write方法将自己写入protocol层
5 调用transport层的flush()。

TBaseProcessor 只有 boolean process(TProtocol in, TProtocol out) 这个简单接口,会先调readMessageBegin(),读出消息名,再从processMap里找出相应的TProcessFunction调用。

TMultiplexedProcessor,支持一个Server支持部署多个Service的情况,在Processor外面再加一层Map,消息名从“add”会变为“Caculator:add”,当然,这需要需要客户端发送时使用 TMultiplexedProtocol修饰原来的Protocol来实现。

4.2 代码生成

1. IFace接口

接口里的函数名不能重名,即使参数不一样也不行。
如果参数是个Struct,则会直接使用继承于TBase的生成类,对客户代码有一定侵入性。
默认抛出TException 这个checked exception,可自定义继承于TException的其他Exception类。

2. Processor类

继承于TBaseProcessor,简单构造出processMap,构造时需要传入用户实现的IFace实现类。

3. 接口里所有方法的方法类

继承于TProcessFunction,见前面TProcessFunction的描述。

4. Struct类,方法参数类和方法结果类

继承于Base。

读取Struct时,遇上fieldId为未知的,或者类型不同于期望类型的field,会被Skip掉。所谓skip,就是只按传过来的type,读取其内容推动数据流往前滚动,但不往field赋值,这也是为什么有了生成的代码,仍然要传输元数据的原因。

为了保持不同服务版本间的兼容性,永远对方法的参数与Struct的field只增不减不改就对了。

因为不同版本间,Struct的filed的数量未知,而StructEnd又无特殊标志,所以在Struct最后会放一个type=Stop的filed,读到则停止Struct的读入。

写入Struct时,Java对象(如String,Struct)或者设为optional的原始类型(如int)会先判断一下这个值被设置没有。Java对象只要判断其是否为null,原始类型就要额外增加一个bitset来记录该field是否已设置,根据field的数量,这个bitset是byte(8个)或short(16个)。

以上是StandardScheme的行为,每个Struct还会生成一种更节约空间但服务版本间不兼容的TupleScheme,它不再传输fieldId与type的元数据,只在Struct头写入一个bitset表明哪几个field有值,然后直接用生成的代码读取这些有值的field。所以如果新版idl中filed类型改动将出错;新的field也不会被读取数据流没有往前滚动,接下来也是错。

5. Thrift二进制与Restful JSON的编码效率对比

为了服务版本兼容,Thrift仍然需要传输数字型的fieldId,但比JSON的fieldName省地方。
int与byte[],当然比JSON的数字字符串和BASE64编码字符串省地方。
省掉了’:’和’,'
省掉了””,[], {}, 但作为代价,字符串,byte[],容器们都要有自己的长度定义,Struct也要有Stop Field。
但比起JSON,容器和Field又额外增加了类型定义的元数据。

 

5. 服务端

基类TServer相当于一个容器,拥有生产TProcessor、TTransport、TProtocol的工厂对象。改变这些工厂类,可以修饰包裹Transport与Protocol类,改变TProcessor的单例模式或与Spring集成等等。

https://github.com/m1ch1/mapkeeper/wiki/Thrift-Java-Servers-Compared 这篇文章比较了各种Server

5.1 Blocking Server

TSimpleServer同时只能处理一个Client连接,只是个玩具。

TThreadPoolServer才是典型的多线程处理的Blocking Server实现。
线程池类似 Executors.newCachedThreadPool(),可设最小最大线程数(默认是5与无限)。
每条线程处理一个Client,如果所有线程都在忙,会等待一个random的时间重试直到设定的requestTimeout。
线程对于Client好像没有断开连接的机制,只靠捕获TTransportException来停止服务?

5.2 NonBlockingServer

TThreadedSelectorServer有一条线程专门负责accept,若干条Selector线程处理网络IO,一个Worker线程池处理消息。

THsHaServer只有一条AcceptSelect线程处理关于网络的一切,一个Worker线程池处理消息。

TNonblockingServer只有一条线程处理一切。

很明显TThreadedSelectorServer是被使用得最多的,因为在多核环境下多条Selector线程的表现会更好。所以只对它展开细读。

5.3 TThreadedSelectorServer

TThreadedSelectorServer属于 Half-Sync/Half-Async模式。

AcceptThread线程使用TNonblockingServerTransport执行accept操作,将accept到的Transport round-robin的交给其中一条SelectorThread。
因为SelectorThread自己也要处理IO,所以AcceptThread是先扔给SelectorThread里的Queue(默认长度只有4,满了就要阻塞等待)。

SelectorThread每个循环各执行一次如下动作
1. 注册Transport
2. select()处理IO
3. 处理FrameBuffer的状态变化

注册Transport时,在Selector中注册OP_READ,并创建一个带状态机(READING_FRAME_SIZE,READING_FRAME,READ_FRAME_COMPLETE, AWAITING_REGISTER_WRITE等)的FrameBuffer类与其绑定。

客户端必须使用FrameTransport(前4个字节记录Frame的长度)来发送数据以解决粘包拆包问题。

SelectorThread在每一轮的select()中,对有数据到达的Transport,其FrameBuffer先读取Frame的长度,然后创建这个长度的ByteBuffer继续读取数据,读满了就交给Worker线程中的Processor处理,没读够就继续下一轮循环。

当交给Processor处理时,Processor不像Blocking Server那样直接和当前的Transport打交道,而是实际将已读取到的Frame数据转存到一个MemoryTransport中,output时同样只是写到一个由内存中的ByteArray(初始大小为32)打底的OutputStream。

Worker线程池用newFixedThreadPool()创建,Processor会解包,调用用户实现,再把结果编码发送到前面传入的那个ByteArray中。FrameBuffer再把ByteArray转回ByteBuffer,状态转为AWAITING_REGISTER_WRITE,并在SelectorThread中注册该变化。

回到SelectorThread中,发现FrameBuffer的当前状态为AWAITING_REGISTER_WRITE,在Selector中注册OP_WRITE,等待写入的机会。在下一轮循环中就会开始写入数据,写完的话FrameBuffer又转到READING_FRAME_SIZE的状态,在Selector中重新注册OP_READ。

还有更多的状态机处理,略。

 

6. 客户端

6.1 同步客户端

同样通过生成器生成,其中Client类继承TClient基类实现服务的同步接口。

int add(int num1,int num2)会调用生成的send_add(num1, num2) 与 int receive_add()。
send_add()构造add_args()对象,调用父类的sendBase(“add",args),sendBase()调用protocol的writeMessage写入消息头,然后调用args自己的write(protocol),然后调用transport的flush()发送。
receive_add()构造add_result,调用父类的receive_base(add_result),receive_base()调用protocol层的readMessage读出消息头,如果类型是Exception则 用TApplicationException类来读取消息,否则调用result类的read()函数。

注意这里的seq_id并不支持并发访问,在发送时简单的+1,在接收时再进行比较,如果不对则会报错。因此Client好像是非线程安全的。

6.2 异步客户端

异步客户端,需要传入一个CallBack实现,在收到返回结果或错误时调用。

使用NonblockingSocket,每个客户端会起一条线程,在这条线程里忙活所有消息Non blocking发送,接收,编解码及调用Callback类,还有超时调用的处理。
其状态机的写法,TThreadedSelectorServer有相似之处,略。

 

7. Http

走Http协议,主要是利用了Thrift的二进制编解码机制,而放弃了它的底层NIO传输与服务线程模型。

THttpClient,使用Apache HttpClient或JDK的HttpURLConnection Post内容。

TServlet,作为Servlet,将request与response的stream交给Processor处理即可,不用处理线程模型与NIO,很简单。

 

8. Generator

Generator用C++编写,有Parser类分析thrift文件元数据模型,然后每种语言有自己的生成模型。

为Java生成代码的t_java_generator.cc有五千多行,不过很规整很容易看。

除了Java,一般还会生成Html的API描述文档。

谢谢你看到这里,请再看一次招聘广告:

唯品会广州总部的基础架构部招人!! 如果你喜欢纯技术的工作,对大型互联网企业的服务化平台有兴趣,愿意在架构的成长期还可以大展拳脚的时候加盟,请电邮 calvin.xiao@vipshop.com

by calvin | tags : | 9

专访唯品会架构师肖桦:做编码的架构师

| Filed under 技术

给CSDN的Java 20周年专题写的采访稿,http://www.csdn.net/article/2015-05-19/2824712-Java

照例复制备份如下:

日前,CSDN特别采访了SpringSide发起人肖桦(网名江南白衣),请他分享了他眼中的Java以及从码农到架构师的经验之道。

CSDN:你是从什么时候开始接触Java的?是什么地方吸引了你?

肖桦:大概从2001年开始接触Java,之前使用C++,什么都要自己造轮子。进入Java世界,突然发现很多优秀的类库可以拿来就用。J2EE的一套东西,虽然后来大家都批评它做得不好,但EJB、JMS、JMX、JCA,这些东西对于还想着用C++来实现类似功能的衰人来说,有这么一套标准与实现实在太好了。即使在今天看来,J2EE的愿景还是很好的,只是一开始实现的重了,让后来的Spring们抢了风头。而在语言层面,突然就没有了指针,也不用管对象回收了。和PHP、ASP比,又仍然是个高大上的静态语言,对于我这种既喜欢优雅又喜欢简单的工程师来说是最好的。

CSDN:你怎么看待Java目前的现状?

肖桦:Oracle现在对Java还是不错的,Oracle是个务实的公司,Java的一切往务实的方向靠,比如它的发行计划,模块化争议太多,就把它搁置,从JDK7一直延到JDK9,把能上的菜先上了。又比如最近对JDK7也停止免费支持了,不再拖着长长的产品线,将精力集中起来。另外,OpenJDK也是另一个选择。但如果当初是Google得到了Java会怎样呢,谁知道呢?

CSDN:Java发展至今已有二十年了,你经历了哪些技术变迁呢?

肖桦:二十年的时间好像很长,但划分起来也就三四波的变迁。

语言层面,第一波是JDK5,泛型和新的并发包出来,要不要升到JDK5以支持泛型对很多项目是个困难的选择,最后是Spring之类几个核心的三方框架类库强行升级了,才带着整个Java社区跟随升级,那时候,每个升了级的项目都像走向了新生。很多项目也一直用JDK5到现在。

第二波是JDK6、7时,一波基于JVM的新语言出现,比如Scala、Cloujre,但对于不那么爱玩的项目来说,感觉不大。

第三波就是现在的JDK8,变动之大与JDK5相比并不小,所以现在也等着各个核心的框架如Spring先升级,可见Java社区并不是极客那么爱玩的地儿。

而在框架层面,J2EE无疑是第一波,Spring、Hibernate、Struts(SpringMVC)是第二波,老掉牙的故事也没什么好讲了。而第三波,我觉得是越来越多互联网应用使用Java,如果说J2EE和SSH更偏重于传统企业应用,第三波的技术变迁,更针对大规模、分布式、高可扩展性、高可靠性的互联网应用,比如新一波的服务化浪潮。

CSDN:近日,Oracle宣布停止发布JDK7安全补丁和升级包,此举,你怎么看?

肖桦:Oracle现在就宣布停止JDK7的安全补丁和升级包感觉有点早,因为据我所知,有些项目升级到JDK8之后,会因为JDK8内部一些实现的改进而造成不兼容,被迫又重新回退的。

但此举也是可以理解的,一来可以集中精力维护现有的JDK8,因为JDK8的改动太大,依然同等着力地维护两个版本可能有点吃力。二来,也是为了让更多用户主动地升级到JDK8来,如前所述,Java用户的JDK升级意愿一向不是很高,都靠着一些重要的开源框架类库升级了才会去升级,Oracle这也有推一把的意思。

CSDN:为何会想到创建SpringSide,有什么故事可以给我们分享吗?

肖桦:创建SpringSide是为了留下自己的痕迹,这是在写一本书和做一个开源项目之间的选择。现在我有点庆幸当初选择了后者,因为一个开源项目你可以坚持七八年不断地升级,你的用户也不会嫌你烦。而在做一个高大上的轮子,还是做个简单务实的封装与BestPractice的展示之间,我一点也不后悔当初选择了后者,因为个人精力、白天工作、公司变更等原因,以个人为主的轮子型项目很难坚持七八年,不断地维护它,可能还要大规模重构它的功能适应时代的变迁。当然,也有些项目做到了,向它们致敬。

CSDN:SpringSide目前有新版本更新吗?主要有哪些新特性?

肖桦:SpringSide项目有一阵没有大更新了,不过最近白天的工作从电信领域进入到互联网领域,会有更多的针对互联网、大数据的新特性。

CSDN:从一名码农到软件架构师,进阶之路是什么?有哪些方法呢?

肖桦:作为一个一直坚持写代码的架构师,从码农到软件架构师之间好像没有一条很清晰的界限,是一个知识面与项目经验的自然积累,以及在团队中不断体现出比个体程序员更强的责任后的渐进过程,所以有些敏捷团队说自己不需要一个架构师,但其实开发者已经做了一部分架构的工作,不论他们的职位名称是什么。没有谁能够恶补完一些书后,然后获得一个上级任命后就成为一个架构师了。再回过头来,如果要恶补,有些东西还是值得重点加强,比如分布式架构的基础与理论、抽象化问题、简化复杂架构的能力,比如架构文档化并说给别人听的能力,比如关于操作系统、网络的基本功保证你解决问题的能力等等。

CSDN:在日常生活中你是通过哪些方式来提升个人技能的?现在还会经常编码吗?

肖桦:除了拉书单看书,阅读文章这些日常的知识摄入手段外,最好白天找个牛人扎堆的地方工作,晚上在微信群里继续和整个互联网的牛人们讨论问题。另外,谈哲学可能太玄乎了,我自己也喜欢简单的东西,所以可以花点时间培养对美的直觉与诉求,代码之美,架构之美。我对自己职业的定位就是编码的架构师,也在百年老店爱立信那里,看到不少退休前仍在编码的程序员,所以这将一直是我的目标。

作者简介:肖桦(网名:江南白衣),开源项目SpringSide(http://springside.io)发起人,70后Java程序员,一个依然每天磨练自己Coding匠艺的架构师,喜欢平实、干净的设计。目前就职于唯品会平台与架构部。

Flickr前端工程师的情怀

| Filed under 技术

查看Flickr首页的源代码时被小小感动了:

在这什么都要合并、压缩、CDN,大家恨不得一个字节当两个字节用的时候,Flickr还继续在每页的Header里玩这么长一段,只能用情怀来解释了。

仔细看还藏着招聘广告,翻页面源码的都是工程师,正好是招聘对象。(全文完)

by calvin | tags : | 2

从Apache Kafka 重温文件高效读写

| Filed under 技术

0. Overview

卡夫卡说:不要害怕文件系统

它就那么简简单单地用顺序写的普通文件,借力于Linux内核的Page Cache,不(显式)用内存,胜用内存,完全没有别家那样要同时维护内存中数据、持久化数据的烦恼——只要内存足够,生产者与消费者的速度也没有差上太多,读写便都发生在Page Cache中,完全没有同步的磁盘访问。

整个IO过程,从上到下分成文件系统层(VFS+ ext3)、 Page Cache 层、通用数据块层、 IO调度层、块设备驱动层。 这里借着Apache Kafka的由头,将Page Cache层与IO调度层重温一遍,记一篇针对Linux kernel 2.6的科普文。

 

1. Page Cache

1.1 读写空中接力

Linux总会把系统中还没被应用使用的内存挪来给Page Cache,在命令行输入free,或者cat /proc/meminfo,"Cached"的部分就是Page Cache。

Page Cache中每个文件是一棵Radix树(又称PAT位树, 一种多叉搜索树),节点由4k大小的Page组成,可以通过文件的偏移量(如0x1110001)快速定位到某个Page。

当写操作发生时,它只是将数据写入Page Cache中,并将该页置上dirty标志。

当读操作发生时,它会首先在Page Cache中查找,如果有就直接返回了,没有的话就会从磁盘读取文件写入Page Cache再读取。

可见,只要生产者与消费者的速度相差不大,消费者会直接读取之前生产者写入Page Cache的数据,大家在内存里完成接力,根本没有磁盘访问。

而比起在内存中维护一份消息数据的传统做法,这既不会重复浪费一倍的内存,Page Cache又不需要GC(可以放心使用60G内存了),而且即使Kafka重启了,Page Cache还依然在。

 

1.2 后台异步flush的策略

这是大家最需要关心的,因为不能及时flush的话,OS crash(不是应用crash) 可能引起数据丢失,Page Cache瞬间从朋友变魔鬼。

当然,Kafka不怕丢,因为它的持久性是靠replicate保证,重启后会从原来的replicate follower中拉缺失的数据。

在内核3.2之前,由内核线程flusher(2.6蔡刚升级成pdflush,在2.6.32又再升级为flushser)负责将有dirty标记的页面,发送给IO调度层。内核会每5秒(/proc/sys/vm/dirty_writeback_centisecs)唤醒一次,根据下面三个参数来决定行为:

1. 如果page dirty的时间超过了30秒(/proc/sys/vm/dirty_expire_centisecs,单位是百分之一秒),就会被刷到磁盘,所以OS crash时最多丢30秒左右的数据。

2. 如果dirty page的总大小已经超过了10% (/proc/sys/vm/dirty_background_ratio) 的空余内存 ( Free+ Page Cache - mmap文件),则会在后台启动flusher 线程写盘,但不影响当前的write(2)操作(除非正好两个操作要同一个Page的锁了)。增减这个值是最主要的调优手段。

3. 如果wrte(2)的速度太快,比flusher还快,dirty page 迅速涨到 20% (/proc/sys/vm/dirty_ratio) 的总内存 (cat /proc/meminfo里的MemTotal),则此时所有进程的write(2)操作都会被block,各自在自己的时间片里去执行对自己文件的flush操作,因为操作系统认为现在已经来不及写盘了,如果crash会丢太多数据,要让大家都冷静点。这个代价有点大,要尽量避免。在Redis2.8以前,Rewrite AOF就经常导致这个大面积阻塞,现在已经改为Redis每32Mb先主动flush一下了。

详细的文章可以看: The Linux Page Cache and pdflush
 

1.3 主动flush的方式

对于重要数据,应用需要自己触发flush保证写盘。

1. 系统调用fsync() 和 fdatasync()

fsync(fd)将属于该文件描述符的所有dirty page的写入请求发送给IO调度层。

fsync()总是同时flush文件内容与文件元数据, 而fdatasync()只flush文件内容与后续操作必须的文件元数据。元数据含时间戳,大小等,大小可能是后续操作必须,而时间戳就不是必须的。因为文件的元数据保存在另一个地方,所以fsync()总是触发两次IO,性能要差一点。

2. 打开文件时设置O_SYNC,O_DSYNC标志或O_DIRECT标志

O_SYNC、O_DSYNC标志表示每次write后要等到flush完成才返回,效果等同于write()后紧接一个fsync()或fdatasync(),不过按APUE里的测试,因为OS做了优化,性能会比自己调write() + fsync()好一点,但与只是write()相比就慢很多了。注意此时write()还是先写到page cache的,read可以直接从page cache中获取数据。

O_DIRECT标志表示直接IO,完全跳过Page Cache。不过这也放弃了读文件时的Page Cache,必须每次读取磁盘文件,这是与O_SYNC的区别。而且要求所有IO请求长度,偏移都必须是底层扇区大小的整数倍。所以使用直接IO的时候一定要在应用层做好Cache。
 

1.4 Page Cache的清理策略

当内存满了,就需要清理Page Cache,或把应用占的内存swap到文件去。有一个swappiness的参数(/proc/sys/vm/swappiness)决定是swap还是清理page cache,值在0到100之间,设为0表示尽量不要用swap,这也是很多优化指南让你做的事情,因为默认值居然是60,Linux认为Page Cache更重要。Linux心情的量化公式如下:

swap_tendency = mapped_ratio / 2 + distress + vm_swappiness;

其中mapped_ratio是该区域内真正进程内存所占的比例,distress是回收pageCache的难度(取值在0-100), vm_swappiness是我们的设置值,三者相加大于100则进行swap out,否则回收Page Cache。

Page Cache的清理策略是LRU的升级版。如果简单用LRU,一些新读出来的但可能只用一次的数据会占满了LRU的头端。因此将原来一条LRU队列拆成了两条,一条放新的Page,一条放已经访问过好几次的Page。Page刚访问时放在新LRU队列里,访问几轮了才升级到旧LRU队列(想想JVM Heap的新生代老生代)。清理时就从新LRU队列的尾端开始清理,直到清理出足够的内存。

 

1.5 预读策略

根据清理策略,Apache Kafka里如果消费者太慢,堆积了几十G的内容,Cache还是会被清理掉的。这时消费者就需要读盘了。

内核这里又有个动态自适应的预读策略,每次读请求会尝试预读更多的内容(反正都是一次读操作)。内核如果发现一个进程一直使用预读数据,就会增加预读窗口的大小(最小16K,最大128K),否则会关掉预读窗口。连续读的文件,明显适合预读。

 

2. IO调度层

如果所有读写请求都直接发给硬盘,对传统硬盘来说太残忍了。IO调度层主要做两个事情,合并和排序。 合并是将相同和相邻扇区(每个512字节)的操作合并成一个,比如我现在要读扇区1,2,3,那可以合并成一个读扇区1-3的操作。排序就是将所有操作按扇区方向排成一个队列,让磁盘的磁头可以按顺序移动,有效减少了机械硬盘寻址这个最慢最慢的操作。

排序看上去很美,但可能造成严重的不公平,比如某个应用在相邻扇区狂写盘,其他应用就都干等在那了,pdflush还好等等没所谓,读请求都是同步的,耗在那会很惨。

所有又有多种算法来解决这个问题,其中内核2.6的默认算法是CFQ(完全公正排队),把总的排序队列拆分成每个发起读写的进程/线程 自己有一条排序队列,然后以时间片轮转调度每个队列,轮流从每个进程的队列里拿出若干个请求来执行(默认是4)。

在Apache Kafka里,消息的读写都发生在内存中,真正写盘的就是那条fluher内核线程,因为都是顺序写,即使一台服务器上有多个Partition文件,经过合并和排序后都能获得很好的性能,或者说,Partition文件的个数并不影响性能,不会出现文件多了变成随机读写的情况。

如果是SSD硬盘,没有寻址的花销,排序好像就没必要了,但合并的帮助依然良多,所以还有另一种只合并不排序的NOOP算法可供选择。

题外话

另外,硬盘上还有一块几十M的缓存,硬盘规格上的外部传输速率(总线到缓存)与内部传输速率(缓存到磁盘)的区别就在此......IO调度层以为已经写盘了,其实可能依然没写成,断电的话靠硬盘上的电池或大电容保命......

延伸阅读:Kafka文件存储机制那些事 by 美团技术团队

 
文章持续修订,转载请保留原链接: http://calvin1978.blogcn.com/articles/kafkaio.html

by calvin | tags : | 4

流式大数据处理的日子

| Filed under 技术

最近这个月好沉默,因为在看物联网与大数据的东西,刚入门没什么可说的......想起唐诺的一段话:“我们缺的不再是知识,过多过廉价的知识像大仓库般丧失了美感、珍罕感,再不复有魅惑力量。”

《后Hadoop时代的大数据架构》 -董飞,一个超完整大数据架构与选择的总结,看花眼,看哭人。

 

Storm/Spark Stream们

5. Spark Streaming的文章:
《Spark Streaming性能调优详解》
《Spark Streaming 1.3对Kafka整合的提升详解》

4. 《流式大数据处理的三种框架:Storm,Spark和Samza》: 各自的用例总结得不错,Samza的动态性看起来挺吸引:“如果你有大量的数据流处理阶段,且分别来自不同代码库的不同团队,那么Samza的细颗粒工作特性会尤其适用,因为它们可以在影响最小化的前提下完成增加或移除的工作。”

3. 我的《Storm笔记》

2. 上周在亚马逊的云上搭一套十台m3.large的大数据演示,演完立马卷堂大散,几小时下来只要六美刀,很经济实惠的样子。不过m3.large的性能有点low,下次得是m3.xlarge,价钱就要翻到十二美刀去了 。

1. 阿里的同学都是很好的翻译家。之前Storm以Clojure写成,翻代码时查问题都是连蒙带猜半明不白的,阿里的同学们用Java将它翻写成了JStorm。再之前,Scala写的Kafka也翻写一遍成了RocketMQ


 

Esper

《Pulsar:来自eBay的开源实时分析平台》,看进去发现它不单CEP用Esper来做,连数据的多佳节又重阳维度聚合也是用Esper——第一次看见活着的Esper重度使用者。

Esper的首页上写着如下字句,与时俱进的典范。

Esper runs well inside a Storm bolt, or a Samza stream task and inside a Spark Streaming operator

Storm + Esper,感觉很强大的流式大数据实时事件处理框架,但Esper那792页的用户手册.....找回三年前写的微博,““袜子穿上,毛衣穿上,一个人宅在家里,Esper长长的manual,深深伤害了我的感情,老了,nothing is easy to me。”

 

一些书

《大数据日知录》: 前几年参加各种技术会议,CAP,最终一致性,RWN,向量时钟,Paxos,一致性哈希,Gossip什么的能灌你一耳朵。而现在,你只要在家安安静静的看看《大数据日知录》就够了。即使你家项目没有PB级别的大数据要处理,只看前半部,对NoSQL和分布式系统的种种概念,其清晰的梳理,可见到书与文章、胶片的区别。

《Storm源码分析》:作为工具书在手边备一本,有疑问的时候可以翻一下。Clojure绝对是阻碍Storm进一步发展的原因。

《Storm分布式实时计算模式》:大概讲了下Storm与其他开源工具如何结合去实现一些功能,消遣的时候翻翻,不直接指导开发。

JDK数则

| Filed under 技术

7. 关于反射,两个最让人郁闷的地方都修正了

旧版JDK,反射时可能抛出ClassNotFoundException、NoSuchMethodException、IllegalAccessException还有InvocationTargetExcetpion,不知道别人怎样,反正我肯定会很偷懒的只捕捉或声明Exception类了,虽然可能有一百个理由说这样不好。JDK7之后,这堆异常有了叫ReflectiveOperationExcetpion的父类,抓它就行。

 
旧版JDK,还有个很莫名其妙的地方,就是所有反射,都拿不到参数名,无论名字叫啥,都返回arg0,arg1,所以在CXF,SpringMVC里,你都要把参数名字用annotation再写一遍:

Person getEmployee(@PathParam("dept") Long dept, @QueryParam("id") Long id)

现在,JDK8新提供的类java.lang.reflect.Parameter可以反射参数名了,编译时要加参数,如 javac -parameters xxx.java,或者Eclipse里设置。然后就可以写成:

Person getEmployee(@PathParam Long dept, @QueryParam Long id)

 

6. 比AtomicLong更好的高并发计数器

在超高并发的场景下,AtomicLong其实没有银弹,虽然没有锁,一样要通过不停循环的CAS来解决并发冲突。

for ( ; ; ) {
long current = get();
long next = current + 1;
if (compareAndSet(current, next))
return next;
}

可见,如果并发很高,每条线程可能要转好几轮的compareAndSet()才把自己的increment()做了。

那这时候,是不是会想起ConcurrentHashMap,分散开十六把锁来分散冲突概率的模式?

JDK8新增了一个LongAdder来实现这个思路,内部有多个计数器,每次increment()会落到其中一个计数器上,到sum()的时候再把它们的值汇总。

没有JDK8的同学也没所谓,Guava把LongAdder拷贝了一份。

但注意,此计数器适合高并发地increment(),到了某个时刻才sum()一次的统计型场景,如果要频繁、高并发地查询计数器的当前值,分散计数器带来的好处就抵消了。
另外,它的实现也比AtomicLong复杂不少,如果并发度不是那么高,继续用AtomicLong其实也挺好,简单就是好。
PS. 在酷壳有一篇更详细的讲解:<从LongAdder看更高效的无锁实现>


 

5. JDK7/8中排序算法的改进

面试季的同学背一脑袋的插入、归并、冒泡、快排,那,JDK到底看上了哪家的排序算法?

Colletions.sort(list) 与 Arrays.sort(T[])
Colletions.sort()实际会将list转为数组,然后调用Arrays.sort(),排完了再转回List。
PS. JDK8里,List有自己的sort()方法了,像ArrayList就直接用自己内部的数组来排,而LinkedList, CopyOnWriteArrayList还是要复制出一份数组。

而Arrays.sort(),对原始类型(int[],double[],char[],byte[]),JDK6里用的是快速排序,对于对象类型(Object[]),JDK6则使用归并排序。为什么要用不同的算法呢?

JDK7的进步
到了JDK7,快速排序升级为双基准快排(双基准快排 vs 三路快排);归并排序升级为归并排序的改进版TimSort,一个JDK的自我进化。

JDK8的进步
再到了JDK8, 对大集合增加了Arrays.parallelSort()函数,使用fork-Join框架,充分利用多核,对大的集合进行切分然后再归并排序,而在小的连续片段里,依然使用TimSort与DualPivotQuickSort。

结论
JDK团队的努力,从一些简单的New Features / Change List 根本看不到,所以没事升级一下JDK还是好的.....

 

4. 高并发的ThreadLocalRandom

JDK7的Concurrent包里有一个ThreadLocalRandom,伪随机数序列的算法和父类util.Random一样,遵照高德纳老爷子在《The Art of Computer Programming, Volume 2》里说的:

x(0)=seed;
x(i+1)=(A* x(i) +B) mod M;

区别是Random里的seed要用到AtomicLong,还要经常compareAndSet(current, next)来避免并发冲突,而ThreadLocalRandom用ThreadLocal模式来解决并发问题,seed用long就行了。

用法: int r = ThreadLocalRandom.current() .nextInt(1000);

没有JDK7的,可自行Copy Paste这个类,Netty和Esper都是这么干的。

ImportNews翻译了一篇更详细的文章: 多线程环境下生成随机数

 

3. JDK7

JDK6好像终于混不下去了,Spring Boot 1.2带的Tomcat和Jetty都要JDK7才能跑。没有用spring boot的Parent的话,要把Tomcat版本降下去挺麻烦的。另外JavaSimon的4.0版也需要JDK7了。

发现新JDK无论吸引人与否,比如JDK8y有了种种突破,最终还是靠Big Player的框架类库决然升级才能带动开发者的升级。而Big Player们又在担心如果升级了会丢失一些无法升级的用户.....麻杆打狼两头怕。

 

2. 《写给大忙人看的Java SE 8》

《写给大忙人看的Java SE 8》 :Java8的改进太大, 值得《Core Java》的作者再码一本, 事实上,为了保持兼容性,很多代码都保持在JDK5/6的水平上,这本书一次过将JDK7/JDK8的更新讲了,是本快捷的升级指南。

 

1. Nashorn

Nashorn——在JDK 8中融合Java与JavaScript之力: JDK8的新JavaScript引擎据说是JDK6时的2-10倍,另外Avatar.js可以在Java里跑Node.js及其类库, 然后Node.js里又再调用Java的类库,纸包鸡包纸。

 
文章持续修订,转载请保留原链接:http://calvin1978.blogcn.com/articles/jdk.html

by calvin | tags : | 9

关于架构设计的一切

| Filed under 技术

关于架构设计的文章收集,不定期更新。

软件设计杂谈 by 程序人生:从设计前到设计后的有趣的杂谈。 比如关于自造轮子的这段:

如果不知道这问题也许有现成的解决方案,自己铆足了劲写一个,大半会有失偏颇(比如说没做上游服务的health check,或者自己本身的high availability),结果bug不断,辛辛苦苦一个个都啃下来,费了大半天劲,做了某个开源软件的功能的子集。

代码一旦写出来,无论是5000行还是50行,都是需要有人去维护的,在系统的生命周期里,每一行自己写的代码都是一笔债务,需要定期不定期地偿还利息。

甚至对于直接购买非开源的商业软件,”工程师现在越来越贵,能用合理的价格搞定的功能,就不该雇人去打理。“

还有后面的"组成系统的必要服务", “把设计的成果讲给别人听”,“设计的改变不可避免” 都值得笔记,凯恩斯: "When the fact change, i change my mind, what do you do, sir?"

 
大规模业务服务器开发总结 by 54chen : 在普通架构师们眼里,微服务、异步消息队列、重用甚至开源是很酷的事情,但在一线架构师眼里......有趣的反常识。

 
架构腐化之谜 by 陈金洲: 喜欢里头那股苍凉的气息,"不超过1年的时间里,无论当初采用何种技术框架,应用何种架构,缓慢地混乱的过程似乎是不可抗拒的宿命......"
 
《软件架构模式》中文版, O'Reilly免费的电子书 Software Architecture Patterns,31页的薄书,如果还嫌长可以看鸟窝的笔记

 
InfoQ:Martin Fowler阐述“牺牲的架构”: 弃, 替换, 模块化; 马丁花总能说些大家敢做不敢说的话,和重构一样,为大家造一个高尚的名词出来-- 马丁的原文

by calvin | tags : | 0

Graphite的百万Metrics实践之路

| Filed under 技术

Graphite作为Metrics界的大哥

  • 它是RRDTool的Network Service版,和RRD一样支持Metrics的精度递减,比如一天之内10秒一条,7天之内聚合到1分钟一条,一年之内聚合到1小时一条。
  • 它支持丰富的查询函数,从简单的min/max/avg/sum 到 rate、top N等等,以Restful API提供。
  • 它有整个生态圈的插件支持,而且简单的TCP Plain Text协议,自己来插入数据也很简单。
  • 它有漂亮的DashBoard -- Grafana
  • 它有完整的HA和Scalable 方案 -- Carbon-Relay
  • 它有数据预聚合方案 -- Carbon-Aggregator

上面的一切,都只靠配置文件就能搞定, 不需要编一行代码。

但是,等你在服务器、应用的监控之外,真的将海量的业务Metrics也交给它,想不写一行代码就赚回一个看起来不错的报表系统时,问题来了。

 

百万Metrics......

每个Metrics在硬盘里都是一个文件。metrics的个数,最大估算值是所有维度大小的乘积,维度的增加让文件的个呈量级的增加。

写入的时候,依赖于Graphite自己很自豪的架构:Cluster Sharding分摊每台服务器上的Metric个数以及Carbon-Cache里的延时聚合批量写入同一个文件的机制持,对百万Metrics的支持并不困难。(唯一问题就是如果Carbon-Cache崩了,内存中未写入的数据会丢失)

但查询的时候......如果Dashboard里看的并不是某一个Metrics,而是某些维度下的一个合计,每次查询可能动不动都要打开上万个文件,就是件痛苦的事情。

解决方法之一是使用Carbon-Aggregator,像我们在报表系统里常做的那样,先做一些维度下的聚合。

比如,计数器只在每台Web服务器的内存里累加,metrics nam就是 traffic.server1.userlogin.count,因为我们只关心总用户登录数,就将20台server的数据聚合在一起,形成traffic.all.userlogin.count再发给carbon-cache,而原来属于每台server的记录则丢弃掉不再往后传,这样我们就少了20倍的metrics数量。

又比如 我们有时候会专门看某个app的使用情况,有时候又想看全部app的情况,则可以既新增一条allApp的聚合数据,也保留每一条app的数据。

好,铺垫完了。

 

读取百万Metrics时的真正问题

真正的问题1:carbon-aggregator是python写的基于Twisted的应用,见鬼的GIL问题,居然只能用到单核CPU。有时候单CPU核根本不够用。在Java里完全没想过会遇到的问题,别人说多核编程的时候总是很茫然,本来就多核的呀。。。。

真正的问题2:有些无法预先聚合的场景,比如要从2000个app中找出使用量最大的5个,则必须打开2000个app对应的全部文件。

真正的读取问题3: Sharding之后,查询时需要聚合多台服务器的结果,比如数据分布在三台机器上,第一台机命中了100个metrics,平均值是3;第二台命中了20个,平均值是2;第三台命中了10个,平均值是1。
首先决不能把三个平均值做平均。像MysqlCluster那种会做执行计划分析的,会只要求每台机上传sum 和 count 一共6条数据。但Graphite做不到这一点,只能让每台机把命中的metrics即130条数据都传上来。在0.9.12版中,如何批量上传数据还是个问题,在始终不肯发布的0.9.13 Snapshot版里此问题总算解决了。
但计算聚合时,再次遇到Python的单CPU核问题......

Graphite作者在宣布支持百万Metrics的时候,好像很少考虑这些问题。
 

要抛弃Graphite么?

现在,坚定一下继续留在Graphite的决心。能掀桌子把Graphite干掉吗? 一个选型的借鉴是Grafana作者合伙创业开的公司Raintank,它提供一个关于Metrics的SAAS平台,看the promising KairosDBinfluxdb: first impressions这两篇博客,也没太好的能下定决心的选择。

1. OpenTSDB
基于HBase,不支持RRD风格的数据精度递减,函数有限比如根本就没有Top N这种功能,运维复杂。

2. Kairosdb
基于Cassandra,8个月没更新了,似乎很不活跃。因为是从OpenTSDB fork出来的,所以不支持RRD与函数有限的问题依然在。

3. InfluxDB
用Go语言编写,是我最看好的一个alternative。但看好它快一年半了,还是没有成熟。最过份是,0.8版与还没发布的0.9版之间,居然几乎是重写。关于Cluster的文档也没写好让人不清楚底细。而且依然不支持RRD,只支持数据超过某个时间段就直接删除。

所以,近期还是留在Graphite吧,继续讨论优化吧。

 

可选的优化方法列表

两份有用的参考资料:

还有一个增强信心的案例,Booking.com(携程网的山寨对象),经过改造的Graphite,支持90台Server,50TB数据的方案。

1. 使用SSD

因为要写入和读取大量不同的文件,用SSD无疑会快很多。
但SSD的价格贵,而且Booking.com的视频里也说。在频繁读写下,一块SSD盘的寿命也就一年半。

2. 使用pypy代替python
看官方数据speed.pypy.org,twisted_tcp中比CPython快3倍,一快解千愁,可缓解很多问题了。

Python把py文件编译成字节码(pyc文件),再交给PVM执行,就像Java的JVM一样。但PVM没有JIT,每次都要把字节码翻译成机器码再执行,而JVM可以把解释后机器码保存下来。pypy提供了JIT,让Ruby社区一片羡慕。

3. 接受现实,减少数据精度

比如最初的一天之内就不要10秒一条记录了,降到30秒或一分钟,大大减少relay,aggregate, 写入的压力和文件的大小。

4. 使用Carbon-Agggrator方案做预聚合

Aggregator能减少不必要的存储,预聚合某些维度,见前。

5. 处理Carbon-Aggrator/Carbon-Relay的单CPU瓶颈

方法1: 前面提到的pypy应该有帮助。

方法2: 如果瓶颈是在carbon-replay,Booking.com用C重写的carbon-c-relay (但暂时用链表存储所有要聚合的Metrics,Metrics数量多时也会搞爆CPU的问题,作者在改),或者这个用Go重写的carbon-relay-ng,都是很活跃很值得尝试的项目。

方法3: 如果不想有任何改变,那多开几个carbon-aggregator,每个aggreator只负责少量的规则,甚至sharding出几个aggreator来完成同一条规则(如果该规则允许分区的话),aggregator会为每个要在聚合后吐出来的Metrics建立一个任务,用LoopingCall不断调用,所以要吐出的Metrics数量进行拆分。部署结构越来越复杂了。。。

6. Sharding

Sharding能减少每个节点上要读写文件的数量,Graphite作者预订的方案。

7. 处理Graphite-Web聚合Sharding时的单CPU瓶颈

暂时无法解决。只能期待前面pypy的提升。

Booking.com提供全套用go重写的方案,carbonserver, carbonzipper,carbonapi一起连用,它们都是重新实现了Graphite各部件的功能,但改动好像太大了,暂不推荐。

8. 最后一招,后台预查询预cache数据
可能有些慢查询要30秒才出结果,那就预先在后台查询一次,cache在Graphite-Web说连接的Memcached里。不过这时就只支持一些固定的时间段,不能支持last 15 minutes这种相对时间了。

其他优化

1. 将Whisper存储换成其他backend方案

用Cassandra的graphite-cyanite,Vimeo的同学写的用Influxdb的graphite-influxdb, 但看起来都不成熟也没多少用户,不敢试。

2. 将Graphite-Web换成Graphite-Api

Graphite-Api与Graphite-Web相比,少了用户管理,数据库与PNG渲染,只保留最核心的Query功能,而且有自己的一套plugin架构,比如raintank就写了个Kairosdb的backend

小结

InfluxDB真正超车之前,我们仍将尝试各种优化,继续实现我们不写一行代码,光靠配置获得一个不错的报表系统的愿望。

 
文章持续修订,转载请保留原链接:http://calvin1978.blogcn.com/articles/graphite.html

by calvin | tags : | 2