`
deadguy
  • 浏览: 3138 次
  • 性别: Icon_minigender_1
  • 来自: 广州
最近访客 更多访客>>
社区版块
存档分类
最新评论

基于BME(APPLICATION SERVER)的性能设计

    博客分类:
  • java
阅读更多

性能问题一直是困扰着软件开发的一个大难题,每到产品即将发布之际,都会四处找来“专家”进行 “调优”,虽说这样往往可以达到性能目标,但是往往是代码出现一些很奇异处理:为了减少查询或者消息发送而破坏封闭性,增加代码耦合,最终经常会出现牵一发而动全身,改了一处代码都不知道会有什么后果;或者大量使用异步机制,把本应同步处理的功能,进行异步处理而增加处理的流量,这样往往会导致自己都不知道操作何时做完,最后是否成功。性能调优变成了一种裱糊工作,差东墙补西墙,这种性能调优是一种并不会减少系统问题的做法。

其实我们在软件设计中通过某些合理的取舍就可以达到一定的性能目标,本文主要和大家分享在我在OCS AIS版本,斯里兰卡版本和CBS1.2开发中通过研究和解决一些列问题,而获得的一些在BME上开发的经验,但是即使你不使用BME开发,也是有一定的借鉴意义。

       下文讨论问题的顺序是从高层设计,逐步向底层实现逐步过渡,首先我们从构架说起,然后会探讨一个有关数据库设计问题,最后讨论几个具体功能实现手段的取舍。

 

一、      分布式与集群

首先我们从构架的选择层面来来研究一下,不同的构架对于性能的影响。为了考虑系统的性能,保证可扩展我们对构架一般有两种选择,一种是分布式系统,分布式系统可在数据库层面的分布式和业务逻辑层面的分布式,我们这里指得就是业务逻辑层面而实现的分布式,通过把一个大系统分解为分离子系统,通过互相调用消息来完成一个功能,通过增加不同功能的节点来提供扩展的一种构架方式,另一种是非分布式,暂且叫他集群式,在这个系统中,每个节点都是对等的,一般情况下互相之间没有调用,每一个单独的节点都可以完成全部的功能。

1.      分布式的处理方法

                                                          

分布式由于子系统之间都是分离的,相对独立,互相直接影响较小,可以有比较强针对性的设计,缺点是事务分布比较多的节点上,通讯消息成本往往比较高,而且在对多节点上操作性能会比较差。在开发时构架的结构性比较好,比较容易的把不同模块分离开来,适合模块化得开发。实际开发中一般采用WebService(后文简称为WS),CORBAEJB或者某些内部协议实现,在不同组件直接约定一些必要的接口。但是在性能上,由于消息发送消耗比较高,如果一个请求跨越多个节点处理的时间一般都比较长,导致线程长时间占用,数据库事务也长时间无法提交,导致CPUIO,内存消耗都会相应提高,用户体验相对较差,而且一个请求往往事务不好控制(事务主要指的的WebService,在EJB的事务传播上是没有问题的)。这种结构往往用于必须要分离部署的两个模块,比如计费节点和管理节点,由于计费节点往往安全性和稳定性要求极高,所以与计费节点是使用分离的部署方式所以他们之间往往分布式的方式,这样可以比较好的分离两个模块,但是这样就会暗示,这种交互是应该尽量减少的。而且为了性能提升尽量要减少的是,消息套消息的调用方式,比如说A节点使用WebService调用一个B节点,然后B节点的处理过程中又调用了C节点WebService,这样A节点的事务处理时间会非常的长,导致大量的线程等待占用大量资源,严重影响系统性能。    

OCS/CBS的版本中都有过这样消息嵌套的设计,如下图BMP如果要同步数据到SCP的SMF进程,就必须通过SCPAgent的转发,消息出现了多层嵌套,性能很不理想,后续修改由BME直接发送消息给SMF,性能有了很大的提升,在CBS中也有类似的问题比如对BILLINGARBUS之间的调用,通过修改为本地调用,性能有一定改观,这里就不赘述了。

BMP

DAS

SMP

SMF

ScpAgent

SCP

SMF

BMP

SMF

SCP

SMF

2.      集群式的处理方法

集群是在一个节点上包含所有功能,实现相对复杂,但是根据大树原则这样可以更加有效利用机器资源,而且可以省去跨节点的功能调用,数据库可以是一个实例也可以多个,在多个数据库节点情况下只是把数据库事务做成分布式,由于数据库的分布式事务的性能代价远远小于WebServiceEJB之类远程调用,同时在开发上分布式数据库事务的复杂度也远远小于使用WebService的开发方式。而且这样的横向扩展能力也是比较强,可以通过增加节点来解决性能问题,但是,集群式的开发是一个相对集中的开发方式,各个模块之间往往容易有相关性的影响,容易增加模块之间的耦合,把多个模块做成一个模块,往往开发成本价高。目前BUS实现就是基于这种集群式的思想。

 

 

BME

 

负载平衡器

数据库

VPN

BUS

……….

 

 

BME

 

VPN

BUS

……….

BME框架下,给予可以不同业务通过不同WEB应用方式,部署在同一个节点上,大大提高了这种模式的可用性。

 

3.      二者的取舍

OCS的经验,如果从性能角度考虑,尽量把相同解决方案又有比较大关联的节点使用集群式实现,从而解决相互调用的问题,如果解决方案不同(比如一个C++一个JAVA,或者应用服务器不同),可以就处理分布式,减少相互之间的耦合限制。随着BME的逐步完善,Spring框架的不断提升,集群式的开发方式的成本已经越来越低。

二、      分库与表分区

OCSCBS中都存在一些海量数据表,为了解决这个问题OCSCBS都拿出自己的方案。在OCS中主要是使用了BME提供的分库的实现,在CBS中除了使用分库的实现以外,还采用了表分区的方式处理海量数据。

1.      BME的分库实现

BME中对于大量的数据为了有效提高性能,采取水平分库的方式平衡数据库负载。首先把多表分散到不同的DB中,然后把数据根据关键列,分布到不同的数据库中。要想做到数据的水平切分,在每一个表中都要有相冗余字符 作为切分依据和标记字段,通常的应用中我们选用某一个特定字段作为区分字段,基于此分库的方式和规则分库以后,系统的查询,io等操作都可以有多个机器组成的群组共同完成了。具体的讲就是我们假设用户每一个操作都是必须输入一个唯一主键(在OCS1.2中式用户号码或在CBS1.2中者账户键值),根据位置主键确认用户数据所在的数据库,对数据进行操作,对所有用户的公用数据单独放入一个数据库中。

这样实施的方式可以具有很好的横向扩展性,理论上几乎可以任意的扩大的用户量,同时在一定的用户基数之上性能指标也与用户数量关联不大,而且实施的时候数据库无关,有非常好的可移植性。但这样在大规模实施的时候,我们还是发现了一些问题。首先分库导致功能实现变得的复杂性,分库的路由代码渗入业务代码,导致业务逻辑代码中会出现控制使用哪个数据库的代码,增加了代码维护难度。而且在某些特定问题上,会出现某些功能难以实现,比如有的表上是没有由于确认数据归属的唯一主键的,所以无法直接维护这些数据,如果要强行操作这些数据,就必须针对所有的库进行查询,性能很差,而且如果分库要对两个库里的表进行联合查询就非常复杂,同时分库对大表的联合查询性能也没有帮助。这种设计方式比较死板,处理问题上不够灵活,在一条数据关联两个用户的情况下,把这条数据放在哪个库中的路由逻辑实际上就必然跟业务逻辑耦合在一起的,比如涉及用户关系的数据,造成实现逻辑复杂。

2.      表分区的处理

在其他一些场合,为了解决海量数据的问题,除了水平分库之外,还是可以采用垂直切分数据的策略,比如在ORACLE数据库中有一种表分区的方式解决这个问题。

分区功能能够将表、索引或索引组织表进一步细分为段。这些数据库对象的段叫做分区。每个分区有自己的名称,还可以选择自己的存储特性。应用程序的角度来看,分区后的表与非分区表完全相同,使用 SQL命令访问分区后的表时,无需任何修改。

通过数据库分区我们可以我可以在SQL层完成一种类型于拆表的功能,这样有效的降低了代码的复杂性。

分区可以分为:

范围分区 :每个分区都由一个分区键值范围指定。

列表分区 :每个分区都由一个分区键值列表指定。

散列分区 :将散列算法用于分区键来确定指定行所在的分区。
组合范围散列分区 :范围和散列分区技术的组合,通过该组合,首先对表进行范围分区,然后针对每个单独的范围分区再使用散列分区技术进一步细分。索引组织表只能进行范围分区。

组合范围列表分区 :范围和列表分区技术的组合,通过该组合,首先对表进行范围分区,然后针对每个单独的范围分区再使用列表分区技术进一步细分。索引组织表可以按范围、列表或散列进行分区。

CBS1.2中,我们就是针对几个数量在千万级的表使用分区技术,根据表的不同用途和使用场景进行规划,比如日志表,我们就采用业界通用根据时间进行分区,时间段在界面上是必须输入的数据,可有效提高查询操作的响应,同时对与一些用户的消费数据,采用根据用户的键值进行分区。这样可以根据不同的用户场景,制定不同的分区策略,十分灵活,可以充分应付各种不同的用户场景。

 

3.      二者比较

比较两种对于海量数据的处理,各有千秋。分库可以实现近似无限的横向扩展,而且不依赖特定的数据库。但是业务复杂的情况向很难把路由处理从业务处理中剥离如出来,如果强行从业逻辑中剥离路由逻辑,将会造成大量的数据和查询的冗余。使用分表或RAC等方式是同样可以解决海量数据的问题,实现起来很方便,不会业务产生过强的耦合,但是他们还说不上无限横向扩展,而且要基于特定的数据库实现。

我们在实际使用可以根据各自的实际场景进行选择,我自觉得真正需要无限的横向扩展的场景实际是很少的,大多数问题是可以在数据库这个层面进行控制的。

 

三、      Spring中动态代理的原理以及对于实例生成方式的选择

1.      动态代理的实现方式

BMESpring2.0/2.5AOP被广泛使用,这样在默认情况下就需要大量使用JDK自带动态代理生来获取对象(在2.5中可以配置为cglib),这样可以有效的使用Spring提供的拦截器和事务管理等功能,但是同时大量的使用拦截器将是对象反射调用次数大大增加而且实例的生成也是比较高的消耗,常常引起一些性能问题。

首先我们简单介绍一个动态代理的原理。首先,也是其核心就是使用sun.misc.ProxyGenerator动态代理的生成动态代理的接口和类的字节码,然后在内存中直接把生成的类文件加载类对象,然后生成并返回代理对象。在生成的代理类文件中是靠反射来调用源对象的方法,同时调用拦截器的。在这个过程中的每次调用都要使用反射,而且在生成代理的实例的时候会如果classLoader与上一次不一致就会生成一个新的代理类,这样这样在相同情况下就会比一般调用多使用很多的CPU,同时使用动态代理会大大增加反射方法调用次数,这两方面都是对性能不好的影响。所以对于动态代理实例的生成还是调用动态代理实例方法(即使这个方法你不需要拦截,他也会使用反射方式调用),都会大大消耗性能。

2.      实例生成方式的取舍

通过对动态代理原理的了解,由于bean的生成是通过动态代理生成的,我们可以知道无论是生成动态代理实例还是调用动态代理中的方法都要有很多的消耗,所以首先要尽量减少调用的方法数量,同时在bean生成时对于单例和原型的选择也是至关重要,由于单例实例只是生成一次可以减少生成和加载类消耗,但是原型每次都会生成新的实例,可能都会有重复多次的类字节码生成和加载为class文件的工作量,尤其是对某些高性能要求的功能,类加载和反射的消耗不容小视,单例方式生成业务处理实例要比使用原型有在CPU占用上有明显的改善。

AIS OCS版本中,我曾经把一个使用原型的业务类(类似于StrutsAction),,当我把这个原型修改为单例,同时去除了部分动态代理的使用,把他们直接修改为new方式生成实例测试时,修改之前的性能处理的速度大概是120Caps/s,测试结果是134Caps/s,虽然性提升不足15%,但是CPU占用率却明显下降了20%。说明动态代理对CPU的消耗是非常巨大而不容忽视的。

四、      同步与异步

软件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:同步调用、回调和异步调用。我们在开发过程中,在发送远程消息,或者操作本地文件,或者记录日志或定时任务,批量任务等操作时经常喜欢使用异步操作,对于界面操作往往采用同步处理。同步调用是一种阻塞式调用,调用方要等待对方执行完毕才返回,它是一种单向调用;回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;异步调用是一种类似消息或事件的机制,不过它的调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。回调和异步调用的关系非常紧密,通常我们使用回调来实现异步消息的注册,通过异步调用来实现消息的通知。同步调用是三者当中最简单的,而回调又常常是异步调用的基础,也是异步调用中最关键的一环,但是我们构架中回调过程往往会被异化为某一种其操作,例如重做,记录日志,等等。同步适合快速响应,异步适合大吞吐量,但是二者组合设计不当到会导致吞吐量也不高,响应也很慢,

AIS OCS版本中,当BMECBP(计费节点)发送请求时,在BME测是一个同步请求,由于线程数是一定的,直到响应才能发送下一个,所以只能定量的发送请求,但是转发请求过程中,为了保证系统的吞吐量平台的传输组件将消息放入一个的异步队列等待很长时间才会处理,导致响应时间很长,处理的响应速度也很慢,流量也很低。

除了性能问题,跟严重的有可能甚至会出现功能问题。

例如在AIS OCS版本中,我们曾经把开户向SCP节点同步数据的功能做成是异步处理,这样表面上可以提高响应的速度,但实际上在这个方案中存在这一个致命的缺陷,他把异步中的回调过程修改为如果失败就重做默认成功,由于同步数据是异步如果同步SCP失败或延迟,开会界面仍然会显示开户成功,实际上数据在那个时候并没有同步到SCP上。当用户继续使用这个用户做其它操作时就会发现用户数据不全,而导致用户操作失败。所以在实际考虑使用异步操作时要结合场景,不能随便使用。

五、      线程的使用

单线程就是一个任务一个线程完成,多线程就是程序有多个线程一起完成。我们现在在程序设计有个错误的观念,多线程一定快,在理论上实际不是这样,从理论计算,对于单一CPU一个线程运算效率是最高的,只是我们遇到了IO等待之类的问题,通过切换线程,来提高CPU的利用率。

 

 

阻塞状态:当线程处于阻塞状态时,java虚拟机不会给线程分配CPU,直到线程重新进入就绪状态,它才有机会转到运行状态。
阻塞状态分为三种情况:

1
位于对象等待池中的阻塞状态:当线程运行时,如果执行了某个对象的wait()方法,java虚拟机就回把线程放到这个对象的等待池中

2
位于对象锁中的阻塞状态,当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他的线程占用,JVM就会把这个线程放到这个对象的琐池中。

3
其它的阻塞状态:当前线程执行了sleep()方法,或者调用了其它线程的join()方法,或者发出了I/O请求时,就会进入这个状态中。

 

如果在单CPU的环境下,只有在有阻塞状态的线程无法使用CPU,使用多线程切换来提高CPU的使用才有意义。

在一台机器上有2个以上的CPU的环境上,如果一个任务的临界区域越小,能产生的并发效果越好。临界区就是我们使用synchronized控制的区域,这种区域越小,多CPU的并发效果越好。如果一个操作被同步的区域占10%,那么他最多用10CPU的机器就可以,再多的CPU也是浪费。所以我们在实际编程中,对于使用线程场景下的代码,要特别注意synchronized的使用,不小心就会造成性能的瓶颈。但是如果是一个单CPU处理的场景就无所谓了。

在某些公共操作即使处理花费很短的时间,但是如果在多CPU的处理环境中使用同步也很容易造成问题,例如在BME中在记录运行日志时,为了实现异步的读写文件,把日志存入一个集合对象,然后集中一次读写,看似不错的设计,但是还有了一个小小的疏忽,它使用的集合是一个同步集合,也是在读集合的时候是无法向集合中写入的,造成大量线程的等待向这个集合写入日志。

六、      面对问题我们应该如何下手

一般情况下,出现性能问题我们会开,问题是在IO还是CPU,数据库还是业务逻辑,然后再对代码进行一些开膛破肚的修改,让代码变得臭不可闻,最后发现问题已经越来越难以解决,最后决定铲倒重来。

当然对于一些意料之外的要求,这个过程是不可避免的,但是性能要求往往在规格成型之前就已经有了定论,完全可以在开发之前进行设计和验证的。例如在LDS切换开发中,采取的方式首先是完成某一个垂直的功能原型,进行测试,发现功能和性能都不能达到要求,重新修改,再测试,一直到达到要求为止,最终这个功能在性能上还是取得了满意的结果。

如果在后期我们碰见了始料未及的问题,我想我们也应该看看问题根源是什么,不要埋头拉车,而忘了抬头看路。例如在某局点,出现

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics