现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

李林锋:Java在电信软件领域的技术实战

2015-05-22 06:16 工业·编程 ⁄ 共 6696字 ⁄ 字号 暂无评论

从广义上看电信软件的范围非常广,细分实际可以分为两大类:系统软件和业务应用软件。系统软件包括路由器底层的信令机软件、手机操作系统等,业务应用软件主要包括客户关系管理CRM、网上营业厅、融合计费OCS和各类消息网关,例如短信网关、彩信网关等。本文重点介绍Java在电信业务软件中的应用。

电信软件的技术演进

C和C++主导时代

在2005年之前,电信软件主要使用C和C++进行开发,由于C和C++开源框架非常少,加之那个时代开源社区并不成熟,大部分的软件系统都由各设备提供商自己研发,或者采购国际大厂的相关产品,例如Oracle、IBM的平台中间件和服务器软件。

电信软件大多数都部署在小机中,对外提供高性能、低时延、高并发的系统调用,协议栈大多数都是电信私有协议栈,对于部分有前台管理Portal的系统,往往基于原生的HTML或者Struts等Web框架开发,通过HTTP协议与后端进行交互,它的逻辑架构图1所示。

图1 华为电信软件V1版逻辑架构图

在那个时代,电信软件绝大多数都部署在高性能的服务器中,处理各种信令、电信私有协议的接入和解析、复杂业务逻辑处理,系统对处理性能、时延、多核处理的要求非常高。当时Java主流版本还是JDK1.4.2(1.4.X),它在传统的Web应用、电子商务网站和政企系统中得到了比较广泛的应用,但在电信领域并没有大的应用,主要原因如下:

  • 在JDK1.5之前的早期版本中,Java在多线程编程、并行处理等方面能力很差,无法在电信软件服务器端使用;
  • JDK1.4.X对非阻塞I/O的支持并不好,相关NIO编程的可参考资料和开源框架很少,传统的阻塞I/O模型在电信高性能、高可靠场景中力不从心;业界很少有Java高性能服务端处理成功的案例,大家普遍对Java支持电信级应用场景持怀疑态度;
  • 那个时代电信领域的开发者都是C/C++出身,大家对新技术和语言有种天生的排斥。2005年之后,随着Java在各领域的快速普及和应用,以及基于Java的各种开源框架井喷式增长,各电信软件提供商开始尝试将系统切换到Java上,以降低系统的开发和运维成本。

Java主导时代

2005-2008年间,Java开始逐渐替代C/C++,成为电信软件开发的首选语言,在这期间最显著的特点就是涌现出了一大批成熟的Java开源框架,它客观上也促进了Java语言的推广。

2009年至今,分布式、大数据和云计算开始兴起,尽管小众语言在此阶段开始百花齐放,但是Java语言依旧是主流。

随着业务的不断发展,硬件成本的下降,基于X86架构的廉价硬件 + 分布式软件的模式在互联网行业得到了大规模应用,分布式架构日趋成熟。

从运营商业务看,尽管高性能的小机仍然是标配,但是运营商业务向数字化转型和云化降成本逐渐成为一种趋势。

传统SOA架构中的一些缺陷逐步暴露,例如企业集成总线ESB是实体总线,性能线性扩展能力有限;硬件负载均衡器的压力越来越大,不断扩容导致硬件成本增加;随着业务规模的不断增长,传统的数据库、配置中心等逐渐成为单点瓶颈等。

我们需要通过新的分布式架构来解决电信软件面临的成本高、性能无法线性增长等问题,以分布式技术为核心构建的华为分布式中间件应用而生,它主要包括分布式服务框架、分布式数据访问框架、分布式缓存、分布式日志采集系统等组成。

自从亚马逊的云计算服务面世以来,云计算技术作为应对笨重的传统IT架构的战略,已经成为越来越多的政府和企业的选择, “云”已经成为ICT技术和服务领域的“常态”,运营商基础设施云化也逐渐成为趋势。

在整个技术演进过程中,出现过许多技术和开源框架,本文针对电信软件的特点,对典型Java技术在电信软件领域的应用进行深入剖析。

电信领域技术实战

JavaNIO异步非阻塞通信

随着开源Web容器Tomcat、JBoss的逐渐成熟,以及商业Web容器的推广和发展,Java应用作为高性能服务器模式在电信领域也得到了认可。

尽管单节点Java应用服务的性能远远比不上C/C++开发的应用服务,但通过多JavaInstance集群部署+硬件扩展+负载均衡器,综合性能仍然可以和之前C/C++构建的系统相媲美。

切换到Java语言之后,硬件的成本有一定增加,但软件的开发效率、运维效率等却得到了极大提升,综合来看,切换到Java语言之后成本降低了很多。

在那个年代,无论是HTTP、SOAP等公有协议还是电信行业的私有协议栈,大多数都采用同步阻塞I/O通信,受限于Java底层的I/O类库,使用Java开发的通信程序都存在同步阻塞的问题。

传统同步阻塞通信面临的主要问题如下:

  • 性能问题:一连接一线程模型导致服务端的并发接入数和系统吞吐量受到极大限制;
  • 可靠性问题:由于I/O操作采用同步阻塞模式,当网络拥塞或者通信对端处理缓慢会导致I/O线程被挂住,阻塞时间无法预测;
  • 可维护性问题:I/O线程数无法有效控制、资源无法有效共享(多线程并发问题),系统可维护性差。

传统同步阻塞通信的处理模型图如图2所示。

图2 同步阻塞通信模型处理模型图

从图2我们可以看出,每当有一个新的客户端接入,服务端就需要创建一个新的线程(或者重用线程池中的可用线程),每个客户端链路对应一个线程。当客户端处理缓慢或者网络有拥塞时,服务端的链路线程就会被同步阻塞,也就是说所有的I/O操作都可能被挂住,这会导致线程利用率非常低,同时随着客户端接入数的不断增加,服务端的I/O线程不断膨胀,直到无法创建新的线程。

同步阻塞I/O在电信行业发生了很多问题,这些问题只能在业务层做规避,效果也不是很好,直到JDK1.4.2提供NIO类库之后,问题才开始逐步得到解决。

NIO类库最大的特点就是支持异步非阻塞通信,它的原理如下:在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。非阻塞I/O的模型如图3所示。

图3 I/O多路复用技术

JavaNIO类库的出现具有划时代的意义,总结如下:

  • 性能提升:采用I/O多路复用技术之后,单个I/O线程可以处理成百上千的链路;降低了I/O线程数,避免了由于海量客户端接入导致线程膨胀资源抢占问题;
  • 可靠性增强:支持非阻塞I/O操作,I/O线程不会因为网络繁忙、对方读写处理慢而被阻塞,保证了在业务高峰期、异常场景下系统的可靠性;
  • 其他Java异步协议栈的基础:基于JavaNIO类库,异步非阻塞通信开始逐步成为主流,Servlet3.X开始支持NIO、Tomcat5.5开始支持NIO、Jetty支持NIO等等。

目前在电信行业软件中,越来越多的Java应用程序开始使用JavaNIO进行异步非阻塞通信,例如使用NIO框架Netty开发RPC通信框架,使用Tomcat7.X的NIO通信模型来优化服务端的通信处理性能。

采用NIO通信的服务端线程模型如图4所示。

图4 NIO通信服务端线程模型

并发编程技术

硬件的发展和多任务处理

随着硬件特别是多核处理器的发展和价格的下降,多任务处理已经是所有操作系统必备的一项基本功能。在同一个时刻让计算机做多件事情,不仅仅是因为处理器的并行计算能力得到了很大提升,还有一个重要的原因是计算机的存储系统、网络通信等I/O性能与CPU的计算能力差距太大,导致程序的很大一部分执行时间被浪费在I/Owait上面,CPU的强大运算能力没有得到充分利用。

Java提供了很多类库和工具用于降低并发编程的门槛,提升开发效率,一些开源的第三方软件也提供了额外的并发编程类库方便Java开发者,使开发者将重心放在业务逻辑的设计和实现上,而不是处处考虑线程的同步和锁。

随着Java的发展,并发编程类库也经历了由功能单一、使用复杂到功能多样、使用简单的蜕变。在高性能、低时延的电信软件中,多线程并发编程无处不在,下面让我们一起熟悉下并发编程在电信软件中的应用。

Java的线程

并发的实现可以通过多种方式来实现,例如:单进程-单线程模型,通过在一台服务器上启多个进程实现多任务的并行处理。但在Java语言中,是通过单进程-多线程的模型进行多任务的并发处理。因此,我们有必要熟悉一下Java的线程。大家都知道,线程是比进程更轻量级的调度执行单元,它可以把进程的资源分配和调度执行分开,各个线程可以共享内存、I/O等操作系统资源,但又能够被操作系统发的内核线程或者进程执行。各线程可以独立的启动、运行和停止,实现任务的解耦。

主流的操作系统都提供了线程实现,目前实现线程的方式主要有三种,分别是:

  • 内核线程(KLT)实现:这种线程由内核来完成线程切换,内核通过线程调度器对线程进行调度,并负责将线程任务映射到不同的处理器上;
  • 用户线程实现(UT):通常情况下,用户线程指的是完全建立在用户空间线程库上的线程,用户线程的创建、启动、运行、销毁和切换完全在用户态中完成,不需要内核的帮助,因此执行性能更高;
  • 混合实现:将内核线程和用户线程混合在一起使用的方式。

由于虚拟机规范并没有强制规定Java的线程必须使用哪种方式实现,因此,不同的操作系统实现的方式也可能存在差异。对于Sun的JDK,在Windows和Linux操作系统上采用了内核线程的实现方式,在Solaris版本的JDK中,提供了一些专有的虚拟机线程参数,用于设置使用哪种线程模型。

JDK1.4的Wait和Notify

JDK1.5之前,并没有线程池类库,开发者需要自己创建线程、停止线程和销毁线程。同样也没有线程安全的容器和数据结构,存在多线程并发操作的地方,需要开发者自己使用同步块synchronized、wait和notify。关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法或者代码块。同步的作用不仅仅是互斥,它的另一个作用就是共享可变性,当某个线程修改了可变数据并释放锁后,其他线程可以获取被修改变量的最新值。如果没有正确的同步,这种修改对其他线程是不可见的。

尽管synchronized很好用,但它是个重量级的锁,如果使用范围不当,会导致单点瓶颈,造成系统性能下降。电信软件往往需要启动较多数量的线程,如果系统存在不恰当的synchronized同步块或synchronized同步块过多,线程竞争越激烈,性能下降越厉害。

在JDK1.4时代,多线程并发编程的水平决定了软件的性能和质量,它对软件开发人员的技能要求非常高。

JDK1.5新增的并发编程类库

在JDK1.5的发行版本中,Java平台新增了java.util.concurrent类库,这个包中提供了一系列的线程安全集合、容器和线程池,利用这些新的线程安全类可以极大地降低Java多线程编程的难度,提升开发效率。

在JDK1.5的发行版本中,Java平台新增了java.util.concurrent类库,这个包中提供了一系列的线程安全集合、容器和线程池,利用这些新的线程安全类可以极大地降低Java多线程编程的难度,提升开发效率。新的并发编程包中的工具可以分为如下四类:

  • 线程池ExecutorFramework以及定时任务相关的类库,包括Timer等;
  • 并发集合,包括List、Queue、Map和Set等;
  • 新的同步器,例如读写锁ReadWriteLock等;
  • 新的原子包装类,例如AtomicInteger等。在实际编码过程中,我们建议通过使用线程池、Task(Runnable/Callable)、原子类和线程安全容器来代替传统的同步锁、wait和notify,提升并发访问的性能、降低多线程编程的难度。以JDK默认提供的线程池ThreadPoolExecutor为例,它提供如下几种重要功能:
  • 提供线程池配置参数,可以指定核心工作线程数、最大线程数、空闲线程存活时间、消息队列的类型、消息队列长度等;
  • 线程安全的Runnable执行接口;
  • 线程池强制关闭和优雅关闭接口,用于优雅停机和资源释放;
  • 差异化的执行策略,例如消息队列满之后是拒绝还是由调用者线程自己代为执行任务,满足不同的用户场景。

除了ThreadPoolExecutor,JDK还提供了专门用于执行定时任务的线程池ScheduledThreadPoolExecutor,利用它可以高效地调度和执行定时任务。相比于老版本JDK提供的Timer,性能更高、资源占用更少。

在电信软件中,往往需要对海量的消息进行超时控制,利用ScheduledThreadPoolExecutor可以同时执行海量定时器。

JDK1.5新的并发编程工具包中还新增了读写锁,它是个轻量级、细粒度的锁,合理地使用读写锁,相比于传统的同步锁,可以提升并发访问的性能和吞吐量,在读多写少的场景下,使用同步锁比同步块性能高一大截。

读写锁的使用总结如下:

  • 主要用于读多写少的场景,用来替代传统的同步锁,以提升并发访问性能;
  • 读写锁是可重入、可降级的,一个线程获取读写锁后,可以继续递归获取;从写锁可以降级为读锁,以便快速释放锁资源;
  • ReentrantReadWriteLock支持获取锁的公平策略,在某些特殊的应用场景下,可以提升并发访问的性能,同时兼顾线程等待公平性;
  • 读写锁支持非阻塞的尝试获取锁,如果获取失败,直接返回false,而不是同步阻塞,这个功能在一些场景下非常有用。例如多个线程同步读写某个资源,当发生异常或者需要释放资源时,由哪个线程释放是个挑战,因为某些资源不能重复释放或者重复执行,这样,可以通过tryLock方法尝试获取锁,如果拿不到,说明已经被其它线程占用,直接退出即可;
  • 获取锁之后一定要释放锁,否则会发生锁溢出异常。通常的做法是通过finally块释放锁。如果是tryLock,获取锁成功才需要释放锁。

CAS指令和原子类的应用:互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步被称为阻塞同步,它属于一种悲观的并发策略,我们称之为悲观锁。随着硬件和操作系统指令集的发展和优化,产生了非阻塞同步,被称为乐观锁。简单的说就是先进行操作,操作完成之后再判断下看看操作是否成功,是否有并发问题,如果有就进行失败补偿,如果没有就算操作成功,这样就从根本上避免了同步锁的弊端。

目前,在Java中应用最广泛的非阻塞同步就是CAS,在IA64、X86指令集中通过cmpxchg指令完成CAS功能,在sparc-TSO中由case指令完成,在ARM和PowerPC架构下,需要使用一对Idrex/strex指令完成。

从JDK1.5以后,可以使用CAS操作,该操作由sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等方法包装提供。通常情况下sun.misc.Unsafe类对于开发者是不可见的,因此,JDK提供了很多CAS包装类简化开发者的使用,例如AtomicInteger等。

开源第三方框架

在电信软件领域,Java开源第三方框架得到了非常广泛的应用,下面我们以NIO框架Netty为例,看下它在电信领域的行业应用总结:

  • 高并发:由于采用异步非阻塞模式,一个Netty服务端可以同时处理成千上万的客户端;
  • 高性能:Netty的综合性能在各个NIO框架中最高,它的单节点吞吐量非常高;
  • 安全性:支持HTTPS、SSL等,可以在传输层进行安全控制;
  • 定制性:可以方便地实现业务逻辑的定制;
  • 可靠性:内存保护、流量整形等。

从业务功能需求角度分析,主要使用Netty作为高性能、低时延的TCP通信框架,用于构建分布式消息中间件,例如MQ、分布式服务框架、分布式缓存等。同时基于Netty提供的CodeC能力,开发基于Netty的异步非阻塞应用层协议,例如HttpServer/Client、RestfulServer/Client等。

作者简介:李林锋,2007年毕业于东北大学,2008年进入华为公司从事高性能通信软件的设计和开发工作,有7年NIO设计和开发经验,精通Netty、Mina等NIO框架和平台中间件,现任华为软件平台架构部架构师,《Netty权威指南》作者。目前从事华为下一代中间件和PaaS平台的架构设计工作。

给我留言

留言无头像?