有段时间没有更新技术blog了,现在有空每天都写写围脖,记录生活和工作的点滴,但是有时候发现有些技术的想法和工作总结没有像过去那么完整的写很大一篇,但是也有零零散散的不少点滴,因此想着随意的写这么一个连续的片段分享。
为什么叫做代码背后的点滴呢,其实在现在互联网应用来说,其实用什么语言,用什么平台有些场景有影响,但已经不是绝对重要的因素的,其实代码被后的设计思想才是最重要的。而用最熟悉的方式去表现最自然的想法,那才能做到游刃有余,就好比我向华黎同学申请这次内部奖励的奖品希望是手写笔,因为不论什么画图工具用起来都会妨碍我的顺畅的表达,最终我把注意力集中到了画本身上,而丢失了应有的灵感(在没有拿到画笔前,最近先少画点图)。写代码也是一样,不要被流行,高性能,有潜力这些词搞丢了自己的目标,工具就是工具,能广泛的去学,但不必要广泛的用,把干活要用的那门搞熟练了再说。废话不多说,言归正传。
先说一下,以下的任何观点都必须“对症下药”,没啥万能灵丹妙药,怎么用,什么时候用是关键。
聚与散:
场景一,在分享时谈到Jetty7整个体系架构就一个线程资源池,为什么?整个jetty体系都是事件驱动模式(包括系统事件:NIO事件,包括业务事件:request
suspend),我们系统很多时候有很多资源池(线程,DB,服务)等等,有些是直接的,有些是间接的(依赖于第三方包引入的)。我们要求每个开发者都要给资源池设置上限,给队列限定长度,防止崩溃,但是当这些资源池散落在各个地方的时候,那么每一个资源达到边界以前,总和可能已经超过了系统负载能力,那么一样会奔溃。因此资源的统一规划看来有必要,那可以考虑将物理隔离变成逻辑隔离(防止业务干扰,同时可以根据权重模型来动态分配和预留资源),另一方面整个小团队到大团队,对于连接池的使用抽象出来以后,那么多个依赖都可以在抽象的基础上公用一个物理资源池,如果引入第三方缓存,都可以将资源策略应用到多机上。回过来,这里就要说聚和散的关系,聚能够在一定程度上有全局观,同时可以根据策略模型来调配资源,散最大的好处就是业务隔离,互相不会影响。最近在写一个线程资源分配的小东西,目标就是:资源可根据权重模型分配。(权重模型两条:设定某一类key获取资源可以保留一定资源独享,设定某一类key获取资源最大上限可控)其实这就是一个逻辑隔离资源的最基本的雏形。
场景二,有同学谈起他们的系统属于消耗CPU类型的应用,特别是对数据库获取到的数据作处理和渲染的时候,问有啥好的建议。我的建议是数据获取端没啥可优化的话,考虑数据存入端增加数据预处理功能,将数据处理的压力分摊到更多也数据存入方。当然这个代价是数据的可复用性降低,业务逻辑侵入到其他系统,数据存入方性能会有所下降(如果这三点不足以影响业务流程和数据存入方,那么可做)。其实这是聚和散的另一种表现,很多时候需要考虑的是全局优化,而不是局部优化。
场景三,我今天看了淘宝Tair的作者若海介绍关于tair的一些设计思路的文章,这和早先我做memcached的客户端思想是有共同点的。首先就是数据的获取从服务端中转路由变成了客户端获得配置本地hash选择目标服务器,这就将服务路由的功能客户端化了,避免了单点的瓶颈,也提高了访问性能。同时metadata的数据保存在configserver中,以弱依赖的方式主动推送,避免客户端受到影响。但他的设计里面数据同步是服务端做的,而我当年是客户端做的(队列中异步备份),当时一方面是没有办法去改服务端的memcached,另一方面服务端能有多少,客户端有多少,这些压力的分摊,天然的缓解服务端的压力,将压力分散在了客户端上。(代价就是客户端工作多一点,即时性依赖于各个客户端的能力)这也是一种散。
场景四,TOP大数据请求的处理设计,TOP的目标是什么?大坝。主要职能:限流,授权,路由。那么路由这件事情必须在TOP走么,数据交互和平台校验是否一定要在一个流程中完成,其实不然,安全校验通过以后可以颁发会话标识和目标服务地址,直接由客户端和服务端交互。(这其实就和很多分布式文件系统或者分布式缓存系统一样的考虑,校验通过后就直接建立数据通道进行数据交互,避免性能受到影响),这了的聚和散就涉及到业务的耦合性分析决定流程的聚散,改善性能和稳定性。
场景五,上面谈到过最近自己实现资源分配逻辑隔离的这样的资源池,现在就是做线程池,起初不想用jdk自带的线程池,原因是自带线程池启动线程后,线程只要在coresize内就不会被销毁,也没有调度来从池里面获取执行任务,完成任务放回到资源池,这些线程就轮训的去获取队列的数据。好处在于没有一个manager的管理性能和复杂度会提升,缺点就是资源分配就无法实现。其实这就是典型的一种资源分配有一个实例主导还是由多个消费者自己主导的设计。
强弱依赖
场景:Jetty的Continuation设计中,从调用suspend方法来悬挂起请求,避免退出容器service方法req和res被回收,到业务主动调用complete结束请求流程,回写数据到客户端,都采用产生事件,异步的由核心线程池去执行。这样业务线程池的线程生命周期就不会受到系统线程输出快慢或者容器本身压力的影响。这种弱依赖能够极大的解耦两个系统的服务能力和稳定性。
关键先生
场景一,有同学一个压力测试报告中做了这么一个测试,整个事务处理流程第一步骤消耗CPU,第二步骤消耗IO。整体事务时间=CPU
10 ms + IO 90 ms。他考虑是否能够通过缩短IO消耗时间或者CPU消耗时间来提升RT时间,就能够提升TPS。他的结果是IO提升一倍TPS没有任何变化,CPU消耗时间提升一倍,TPS翻番。这能说明啥,说明瓶颈在CPU上,提升一倍,那么处理能力增加一倍(就好比资源池内的资源生命周期缩短,被利用的次数更多),大部分请求都在CPU上排队等待处理,因此系统的TPS取决于瓶颈资源的处理能力而不是简单的缩短RT就可以改变的。(贴切的比喻就是漏斗,不论你大口朝上或者朝下,小口决定了水流量,小口就是那个关键先生)
场景二,异步化压力测试的结果中有如下一组结果:
Time.get是不向后中转服务的模拟请求,也就是服务端很快就响应给客户端了,而user.get是真实模拟服务向后请求,有一定消耗。但看这个结果,同样是同步和异步对比,前者性能相差很大,后者相差不大。原因?异步一定有消耗,但这个消耗占整体事务处理时间的比例是多少,这就会放大影响。而TOP大部分服务都要比user.get更加耗时,因此后面才是线上的真实状况。(因此场景模拟是压力测试最重要的条件),这里的关键先生还是要看整个事务处理的消耗点。衍生一下上次电话面试的一个北京的朋友,谈到了NIO,问了他是否做过BIO和NIO的测试比较,什么时候用NIO最好,他还真做过四类测试,有结果,但还是没搞明白为什么:1.传输数据量大,后端服务处理慢。2.传输数据量大,后端服务处理快。3.传输数据量小,后端服务处理慢。4.传输数据量小,后端处理速度快。结果是NIO在4处于劣势。其实BIO释放的够快,那么NIO的异步在整体事务处理消耗就形成了劣势。
生命周期
场景:在异步分享过程中,谈到了事件驱动的架构设计。原来的流程可以分成很多个步骤,每一个步骤由于处理的需要都会申请必要的资源,不同步骤有些是共享资源,有些是不共享的,那么从资源的生命周期角度来说,一个流程中所有资源的生命周期都是一样,就是整个业务事务的执行周期,对于在一个业务流程中各个步骤很少共享资源的状况,或者有等待外部返回内容的情况时,切割开流程步骤,降低资源生命周期,提升资源利用率,是事件驱动的最大优势。NIO如此,Servlet3规范如此。简单来说就是:按需分配。另一方面我们也能看到,处理都是无状态的,只有输入和输出,无状态的处理更容易复用。看起来这种设计很好,但其实背后还是有不少问题:1.流程复杂化,原来通过程序保证流程的顺序和依赖,现在切割以后,如何驱动,顺序如何保证,如何容错,都给系统增加很大的维护成本。2.异步化后消耗时间必然增大,不论是通过pull主动获取状态变更还是通过push被通知到,都在性能和时间消耗上会有一些付出。因此还是需要依环境而定。
瓶颈移动
场景一,还是一个同学的优化的案例,当时谈到有个叫做最佳线程数,但是觉得在优化过程中最佳线程数是在变化的,其实很多时候当你觉得任何的结论是不稳定的,那么你需要进一步深入挖掘下去。其实之所以最佳线程数这个值不稳定,关键在于我们如何去优化,找到问题所在。和前面举例一样,系统可以看成各种类型资源池的一个整体,然后由很多个步骤不断申请资源来完成各个步骤,最后返回结果。那么说白了,哪个资源池是流程中处理能力最弱的,他就是问题所在,就是瓶颈,但是通过优化后这个瓶颈可能有所改善,与此同时另一个资源成为漏斗的小口,这时候,瓶颈移动了,优化的预期结果和提升的瓶颈点性能就不吻合了。所以,把带宽,CPU,IO,外部服务,DB,Web线程池等等都看成资源池,找到瓶颈所在去优化,同时在优化过程中不断修正优化目标,达到最后全局优化的效果。
场景二,异步化分享中谈到一点,异步化要有全局观。例如A依赖与B系统,组成了一个完整的服务体系,通过异步化方式将A的资源生命周期缩短到只有A处理的时间,B的资源生命周期就是B的处理时间,此时如果A是原有整个系统的瓶颈,那么就会产生雪中送炭的效果,系统整体性能会有所提高(大部分情况下,原因看后面介绍)。如果B是瓶颈,那么就是雪上加霜,更多的水流被放进来,如果B自身没有流量控制,那么系统就会处于不健康的状况,甚至奔溃,系统的整体RT时间可能会增加(A节省的时间小于B增加的时间)。因此这就是系统间瓶颈移动产生的影响,同样也和一个系统一样,考虑优化一定是整体性的考虑,否则问题不会朝着预期的方向解决。 |