低延迟系统的Java实践

时间:2023-01-02 21:35:03

在很久很久以前,如果有人让我用Java语言开发一个低延迟系统,我肯定会用迷茫的眼神望着他,然后说“are you kidding me?”。然而随着Java语言的日臻完善以及JVM性能的极速提升,使得用Java语言开发低延迟(不要和实时系统搞混)系统越来越成为可能,其中就包括最典型的交易(支付)系统。当然作为系统架构师,他们会尝试使用一些成熟分布式架构方案(通常是整合一些商业或开源项目),通过利用冗余计算资源以及异步通信方式提高应用程序的吞吐量和响应率,使其到达低延迟系统的标准,这在社区中有大量的实践案例,包括淘宝,京东、XX系等。然而我的兴趣爱好是研究Java语言本身能为低延迟应用开发带来什么,在开发低延迟系统中我们有哪些实践可以参照,这才是本文的讨论重点。关于低延迟系统和实时系统的区别不再赘述,作为架构师的你们应该比我清楚的多。

作为低延迟系统,比如交易系统,应该有2个比较重要的参数指标:吞吐量和响应率(当然还有其他重要指标)。吞吐量表达了系统在单位时间内所处理的请求量;而响应率则表达了单次请求所消耗的单位时间。这2个指标基本能判断出一个交易系统是否“足够快”。当我们在使用Java语言开发低延迟系统时,应该放弃一些我们之前约定俗成的规则,其中就包括了我们一直信奉的Java编程原则——面向对象。有人肯定会说我,“这不是扯蛋吗?那你还用Java干啥?”。其实我们并不会放弃Java面向对象的思维方式,而是在使用的方式上有所改变而已。Java设计之初就是纯面向对象的,记得之前所有Java入门书中都会有一句名言:“在Java世界,一切皆对象!”。有点跑题了,写这篇文章也是因为之前看到了一篇文章《Using Java in Low Latency Environments》,在这篇文章中几位大师讨论了有关于Java在低延迟环境中的使用方法,有些原则非常值得参考,再结合自己实践工作中的一些经验的积累,所以总结了三条最最重要的基本准则,以供同学们参考。

如果你是一个Java老手,肯定对JVM或是Java语言的各种特性了如指掌。JVM的内存释放是由GC自动完成的,程序员无法直接控制和干预GC的执行(有人会说,不是有System.gc()可以执行垃圾收集嘛,那就请你好好的去看一下Java Doc吧),这也是我对Java最大的诟病之一。我们都知道,当JVM在执行GC时,不管是YGC还是FULL GC,JVM都将阻塞其他所有正在执行的线程,虽然这个时间已经从分钟级别降低到了毫秒级别,但是作为低延迟系统还是会受其影响,从而降低系统的响应率。这种情况直到Oracle推出带有并行GC的JVM之前都会一直存在,为了避免这种情况,大师们的解决方案是降低GC的频率,将GC控制在每天一次或是几天一次,那到底这么做呢?大师们为我们指明了一条明路。那就是环保——尽量少产生“垃圾”或不产生“垃圾”,简单讲就是少使用堆对象(用new关键字实例化的对象),甚至包括String对象。好吧,小伙伴们都惊呆了,你是要我去写C代码嘛!!还好,我会C不会因此而失业——开个玩笑。其实大师们想表达的意思是对象复用技术,这种技术可以大量减少堆对象的产生。在我现在的交易系统开发中,基本不会关注对象的复用,字符串对象更是当做了基本类型来使用。其实作为交易系统,业务逻辑非常的复杂,各种逻辑判断,上百个交易业务属性,再加上对面向对象技术以及设计模型的迷恋,势必会引起堆对象的泛滥从而导致GC的频繁执行。所以为了减少“垃圾”的产生,我们必须在对象的设计和使用上做一些约束,例如,用基本类型(short、int、long、double等)替换包装对象、减小对象的规模(不要嵌套对象太多)、用数组替换Java集合、使用对象池复用对象(例如:commons-pool库)、减少第三方类库的使用等等。当然我们所做的一切都比不上来自GC自身的改进,所以真心希望oracle尽快的推出可以并行的垃圾收集器,使其不再成为我们既爱又恨的关注点。

其次对低延迟系统具有影响的就是Java的内存模型,即JMM。Java内存模型定义了可见性和原子性,为了保证这两项实现,我们必须使用同步,在Java中,所有线程的同步必须争夺唯一的一把锁,因此在需要低延迟的环境中锁竞争会大大的影响吞吐量和响应率。在交易系统中,当请求量急剧上升时,锁的竞争将更加的激烈,从而导致大量的线程阻塞或是饿死。那如何来规避这种情况的发生呢?那就是使用无锁技术或无等待技术,具体而言就是在同步块中不加锁或是减小加锁的代码范围。我们可以避免使用synchronized或是使用ReentrantLock来自己控制锁的范围。ReentrantLock允许我们在代码块上加锁,但是必须要注意不要忘记释放锁。举一个例子,假设我们的交易系统中有一个共享的数据,每个写请求方法都需要用synchronized关键字加以同步,否则就会发生数据异常。现在我们用无锁技术来规避synchronized,实现很简单,就是使用一个队列,将所有写请求先放入队列中,然后由一个线程循环队列,将写请求写入共享数据中。其实这就是我们常说的“单一写原则”,另外异步处理也是一种“单一写原则”的具体化实现。

IO可以说是影响低延迟系统性能最为关键的因素之一,而网络IO更是各种IO调用的重中之重。在交易系统中网络IO无法避免,我们必须通过以太网从其他应用程序中获取资源,比如:数据库,消息系统等,同时我们又会通过以太网向其他应用系统输出服务,比如:交易通知等。所以,网络质量将直接影响到交易系统的吞吐量和响应时间。在广域网环境中,网络传输需要时间、为了保证TCP/IP可靠协议必须重新发送丢失的数据包,交换机或路由器也会产生网络阻塞,这些完全不可预知的问题都将影响到低延迟系统的性能,IO的延迟无时无刻的在考验着我们的忍耐底线。到目前为止,我们还没有一个绝对可行的方案来解决所有由IO引起的问题,但还是有一些指导建议值得我们去借鉴。我们可以通过预加载资源来最大限度的减少IO开销,例如,在应用程序启动的时候加载配置文件或其他资源文件等。这里需要注意的是,加载的资源不能在应用程序中被垃圾回收,让其存在于堆内存的P区中是一个不错的选择。另一方面,从JDK7开始,Java提供了SDP的支持,SDP协议可以大大的提升网络IO的性能,所谓SDP就是Sockets Direct Protocol,即套接字直联协议。它不同于传统TCP/IP协议,它需要硬件的支持,即InfiniBand网络设备。SDP可以直接访问远程主机的内存,不再需要通过ISO的7层模型来进行数据的传输,所以它的效率要比以太网的TCP/IP协议高很多。我们用一张图就可以非常清楚的对比SDP协议和以太网协议的本质区别.。(此图从infoq上摘录,非本人版权,特此声明)

低延迟系统的Java实践

从上图中可以看到,Java7提供的SDP协议是直接和物理层打交道,数据不再像之前的Java6那种以太网的方式要经过ISO的各层。当然,对于开发人员来说这一切都是是完全透明的,我们还是在使用非常熟悉的java.net.*包中各种API进行网络应用程序的开发,所有的一切全部交给JDK。是不是觉得很Cool呢!不过本人还未对此进行过尝试,因为我们公司目前还是以太网,并没有InfiniBand网络设备,所以SDP技术还有待验证。

综上所述,在用Java做低延迟系统开发时,我们应该从三个方面着手制定优化方案,第一,有效减少垃圾收集的执行频率;第二,有效的使用锁机制或根本不用锁;第三,减少IO(重点是网络IO)的等待处理时间。当然除此之外还有一些其他的小技巧,比如,不使用第三方库、不使用反射库(java.lang.reflect)、优化代码执行路径、用DirectByteBuffers创建数据结构等等。

好像写了那么多自我感觉干货不是太多,其实我只是想抛砖引玉,通过这样一篇文章能够激发出更多的碰撞和辩论,低延迟系统的开发是一个大课题,其复杂程度远远超过本文所讲述的内容,所以希望更多的人能参与进来,把砖头扔向我。