《架构师》期刊摘要(2015年)二

一、一个优秀的设计过程或者方法论定义了一组一致的、可以重复的步骤,可以在将一个服务端服务组件输出为一个可访问的、有用的WEB API时使用。那就是说,一个清晰的方法论可以由开发人员、设计师和软件架构师共享,以便在整个实现周期内帮助大家协同活动。一个成熟的方法论还可以随着时间的发展,随着每个团队不断发现改善和精简过程的方式而得到精炼,却不会对实现细节产生不利的影响。

    1、列出所有组成部分:列出客户端程序可能要从我们的服务中获取的,或要放到我们的服务中的所有的数据片段,我们将这些称为语义描述符。视角在客户端,而不是服务器端,将API设计成客户端使用的东西很重要。

    2、绘制状态图:根据API绘制状态图,指明每个API的协议方法,是否幂等和安全。

    3、调和魔法字符串:所谓魔法字符,就是描述符,表示客户端跟你的服务通讯时将要访问的动作或者数据元素。(没看懂,我个人理解可能是API的参数列表约定)

    4、选一个媒体类型:客户端与服务端小题传递的媒体类型,HTML、JSON等。(这种描述太官方的,我觉得就是选定数据交互的格式和协议类型)

    5、创建语义归档:基于文档描述API设计的所有内容。

    6、代码开发

    7、发布。

《Web API设计方法论》(2015年2月)

二、Netty问题案例

    智能家居MQTT消息服务中间件,保持10W用户在线长链接,2W用户并发做消息请求;程序在运行一段时间之后,发现内存泄露。

    排查过程我们就不再赘言,我们发现netty的ScheduleFutureTask增加了9076%,达到110W个左右的实例,通过对业务代码的分析发现用户使用IdleStateHandler用于在链路空闲时进行业务逻辑处理,但是空闲时间设置的比较大,为15分钟。netty的IdleStateHandler会根据用户的使用场景,启动三类定时任务,分别是ReaderIdleTimeoutTask、WriterIdleTimeoutTask和AllIdleTimeoutTask,它们都会被加入到NioEventLoop的Task队列中。

    由于超时时间过长,10W个长连接链路会创建10W个ScheduleFutureTask对象,每个对象还保存有业务的成员变量,非常消耗内存。用户持久带设置的比较大,一些定时任务被老化到持久带中,没有被JVM垃圾回收,内存一直在增长,用户误认为存在内存泄露。

    用户的超时时间设置的非常不合理,15分钟的超时达不到设计目标,重新设计之后将超时时间设置为45秒,内存可以正常回收,问题解决。

 

    Netty海量推送服务设计要点:

    1、最大句柄数修改:百万长连接,首先需要优化的就是linux内核参数,修改linux支持的最大文件句柄数,通过ulimit -a查看。如果单个linux上连接数达到阀值,则会在创建新连接时抛出“too many open files”错误。我们可以通过修改“limit.conf”文件来调整此值。(注意soft 和hard都需要调整)

    当然过大的句柄数也会带来问题,导致linux上打开过多的链接,链接处理变慢,性能消耗加剧;我们应该根据服务器的实际物理配置来适当调整。

 

    2、当心CLOSE_WAIT:

    客户端的重连间隔需要合理设置,防止链接过于频繁导致的连接失败(比如端口还没释放);服务端正确处理IO异常和解码异常等,防止句柄泄露。

    由于网络不稳定经常会导致客户端断链,如果服务端没有能够及时的关闭Socket,就会导致处于CLOSE_WAIT状态的链路过多。CLOSE_WAIT状态的链路并不会释放句柄和内存资源,如果挤压过多可能会导致系统句柄耗尽。

    CLOSE_WAIT是被动关闭连接形成的,根据TCP状态机,服务端收到客户端发送的FIN(即客户端主动close(),且发送一个FIN指令),TCP协议栈会自动发送ACK,链接进入CLOSE_WAIT状态。但如果服务器端不执行socket.close()操作,状态就不能从CLOSE_WAIT迁移到last_ack(即服务端ACK,此后客户端将处于TIME_WAIT状态,然后再次ACK,服务端则closed),则系统中会存在很多close_wait状态的连接。通常一个CLOSE_WAIT会维持2个小时(2个MSL时长)。如果服务端程序因为某个原因导致系统造成过量的CLOSE_WAIT消耗资源,那么等不到释放那一刻,系统就会崩溃。

 

    导致CLOSE_WAIT过多的可能原因

    1)程序处理Bug,导致接收到对方的FIN之后没有及时关闭socket,可能是Netty的bug,也可能是业务代码的BUG。

    2)关闭sockt不及时:例如IO线程被意外阻塞,或者IO线程执行的用户自定义Task比例过高,导致IO操作处理不及时,链路未能及时释放。

 

    解决此问题的几个注意点:

    1)不要在Netty的IO线程上处理业务(心跳发送和检测除外)。对于java进程,线程不能无限增长,这就意味着Netty的Reactor线程数必须收敛。Netty的默认值为CPU核数 * 2,通常情况下,IO密集型应用建议线程数尽可能设置大一些,但这主要是针对传统同步IO而言,对于非阻塞IO,线程数并不建议设置太大,尽管没有最优值,但是IO线程数经验值是CPU核数 * 2(或者CPU核数 + 1)。

    2)在IO线程上执行自定义Task要当心,Netty IO处理线程NioEventLoop执行用户自定义的Task(比如Runnable)意味着执行非IO操作类的业务逻辑,这些业务逻辑通常用消息报文的处理和协议管理相关。它们的执行会抢占NioEventLoop IO读写的CPU时间。如果用户自定的Task果断,或者单个Task执行周期过长,会导致IO读写操作被阻塞,这样也会间接导致close_wait堆积。(建议的方式是,Netty封包完毕后,将消息保存在公用队列中,业务部分使用线程池与队列协同即可。)

    3)IdleStateHandler使用要当心,很多用户会使用它做心跳发送和检测,这种用法值得提倡。相比于自己启用定时任务发送心跳,这种方式更加高效。但是在实际开发中需要注意的是,在心跳的业务逻辑处理中,无论是正常还是异常场景,处理时延要可控,防止时延不可控导致NioEventLoop被意外阻塞。例如,心跳超时是或者发生IO异常时,业务调用Email发送告警,有可能Email发送过程会超时,这会导致IO线程的阻塞。

 

    3、合理的心跳周期

    4、合理设置接收和发送缓冲区容量

    对于长连接,每个链路都需要维护各自的消息接收和发送缓存区。Netty提供的ByteBuf支持容量动态调整,对于接收缓冲区的内存分配器,Netty提供了两种:

    1)FixedRecvByteBufAllocator:固定长度的接收缓存区分配器,由它分配的byteBuf长度都是固定大小的,并不会根据实际数据包的大小动态收缩。但是如果容量不足,支持动态扩展。动态扩展是Netty ByteBuf的一项基本功能,与ByteBuf分配器的实现没有关系。(只是初次分配的大小时固定的)

    2)AdaptiveRecvByteBufAllocator:容量动态调整的接收缓冲区分配器,他会根据之前Channel接收到的数据包带下进行计算,如果连续填充满接收缓冲区的可写空间,则动态扩容。如果连续两次接收到的数据包都小于指定值,则收缩当前的容量,以节约内存。

    由此可见AdaptiveRecvByteBufAllocator更加合理,我们可以在创建BootStrap时通过option(ChannelOption.RCVBUF_ALLOLCATOR,AdaptiveRecvByteBufAllocator.DEFAULT)来指定,不过默认就是Adaptive。无论如何,ByteBuf的大小应该合理,建议设置为消息的平均大小(大概率),设置的过大或者过小都会导致内存浪费。

 

    5、内存池(PooledByteBuf)

    我们都知道每个连接在数据操作时都会创建ByteBuf,如果连接很多,那么这些ByteBuf也会占用巨大的内存,而且使用效率不高(比如心跳检测,ByteBuf朝生夕死)。解决此问题的有效策略就是使用内存池,每个NioEvenLoop线程处理N个链路,在线程内部,链路的处理时串行的。假如A链路首先被处理,它会创建接收缓冲区等对象,待解码完成后,构造的POJO对象被封装成Task后投递到后台的线程池中执行,然后接收缓冲区被释放,每条消息的接收和处理都会重复接收缓冲区的创建和释放。如果使用内存池,则当A链路接收到新的数据包之后,从NioEventLoop的内存池中申请空闲的ByteBuf,解码完成之后,调用release将ByteBuf释放到内存池中,供后续B链路继续使用。我们推荐使用Netty 4中的PooledByteBufAllocator,似乎在4.x版本默认就是内存池模式,还是基于DirectByteBuf,所以对内存池中的ByteBuf使用时,需要注意内存的申请和释放需要成对出现,即retain()和release应该成对出现,否则将会导致内存泄露。(因为ByteBuf的释放时机将有Netty自己控制,基于reffer计数器实现)

    6、TCP参数优化

    常用的TCP采纳数,比如TCP层面的接收和发送缓冲区大小的设置,在Netty中分别对应ChannelOption中的SO_SNDBUF和SO_RCVBUF,建议设置合理的值。另外一个比较常用的优化手段就是软终端,如果所有的软中断都运行在CPU0相应网卡的硬件中断上,那么始终都是CPU0在处理软中断,而此时其他CPU资源就被浪费了,因此无法并行的执行多个软中断。大于等于2.6.35版本的linux内核,开启RPS,网络通信性能提升20%以上。RPS的基本原理:根据数据包的源地址,目的地址以及目的和源端口,计算出一个hash值,然后根据这个hahs值来选择软中断运行的CPU。从上层来看,也就是说将每个连接和CPU绑定,并通过hash值来均衡软中断运行在多个CPU上,从而提升通讯性能。(RPS:Receive Packet Streering,主要针对单队列网卡多CPU环境,请参考

《Netty百万级推送服务设计要点》(2015年2月)

三、管理者的时间错配包括三种情况:

    1)沉迷解决技术问题。这一般发生在刚从技术岗位提拔为管理者的时候,忘记自己是管理者了。解决复杂技术问题,能带来愉悦感,否则就是挫折感。于是遇到技术问题时,非得死磕到底,然后一周过去了,而部门其他同学却放羊一周。

    2)一心扑在管理上。这又是一个极端了,忘记自己的技术身份。把自己变成一个项目经理,整天只关心时间节点,不关注技术人员的小情怀,不协助他们解决具体的技术问题。

    3)沉迷单个业务模块。这是另一个特例。一般发生在内部提拔时。例如某位同学,之前是DBA组的负责人,提拔为运维部经理后,还是习惯于抓其擅长的数据库工作,这也是不应该的,否则就没必要提拔了嘛。

 

    员工的资源错配主要体现在时间安排上。事情多了,分不清轻重缓急,没有一个合理的排序原则、指导思想;混淆技术进步和工作要求(有时过分追求技术进步),简单的问题复杂化,降低客户满意度。

    如何做到高效运维:

    1、明确分工/职能

    2、技术专业化:优化监控系统(系统监控、业务监控,涉及到风险管控能力)、减少人为事故(责任心,流程管理,风险控制,自动化)、运维自动化(对基础运维尽可能自动化,容器话,减少出错)、合理优化架构(配合架构师选择合理的应用架构)、代码持续部署(主要是自动化部署)。

 

    “专业”、“激情”、“方便”、“快”--高效运维七字诀。(本人比较认同作者的“服务意识”和“技术专业性”两个方面的思想)

《高效运维最佳实践》(2015年2月)

四、关于有效的性能调优的一些建议:

    1、算法本身的优化:算法优化是性能局部优化的首选,并常采用各种性能监控软件来度量CPU时间、内存占用率、函数调用次数来定位问题,然后实施各种调优方法,比如优化循环、利用空间换时间、采用合适的数据结构等。但是算法本身的优化只能够帮助大家消除一些明显的编程细节引起的瓶颈,尤其单单通过算法优化的手段还不能完全解决性能问题,且具有非常大的难度。

    2、优化运行环境和资源:

    运行环境与资源包括各种软硬件平台,硬件包括CPU、内存、磁盘和网络等。最简单且最省事的调优方法是优化硬件资源,使用快速计算资源代替慢速计算资源,提升资源的计算能力。

    1)更快的CPU

    2)更快的本地IO设备,比如内存代理硬盘,SSD代替机械硬盘。(上述属于垂直优化)

    3)增加物理内存且调整内存页的SWAP。

    4)较快的网络IO设备,比如使用光纤、多队列多网卡等。

    5)快速计算资源代替慢速计算资源(比如更换为快速存储),本地计算换网络传输的优化最好采用压缩传输内容的优化手段,该方式尽管增加了CPU的解压缩时间但是减少了网络传输的时间(CPU更快而且通常性能基准稳定,易于扩展和监控;网络传输的环境因素复杂)

 

    软件环境包括操作系统、数据库、中间件等。软件环境条有的成本相对较高,并且工作量巨大,比如从windows迁移到linux平台、对数据库进行读写分离、从EJB迁移到Spring等。

    3、优化算法和资源间的交互

    当前各种调优实践最集中的领域是优化算法和资源间的交互,比如减少单台服务器的处理量、充分利用系统资源、减少不必要的计算、减少不必要的IO等。

    1)减少单台服务器的处理量:简而言之,单台服务器的处理能力是有限的,我们尽可能基于分布式计算的方式解决问题。总的优化原则是:分而治之,具体维度包括业务、组件边界、访问频率或对系统资源的消耗成都、瓶颈资源等。

    业务:把大应用按业务分成独立的相互合作的系统,如高层的采用SOA方式,低层的采用数据库分库方式。

    组件边界:WEB服务器、应用服务器、数据库服务器、文件服务器。

    对系统资源的消耗程度:采用读写分离方式。

    瓶颈资源:对数据库进行分表、分片等。

    4、充分利用系统资源:

    采用多进程、多线程、异步操作以及负载均衡都手段,其中负载均衡主要做到load(计算型、网络IO等)在多个服务器间均衡,避免某台服务器过载或者资源过于空闲。

    5、减少不必要的计算和IO:采用高效的缓存技术,比如分布式缓存、CDN缓存、客户端缓存、合并资源请求等方式,减少对网络IO、磁盘IO、API的调用次数等。

《关于有效的性能调优的一些建议》(2015年3月)

 

猜你喜欢

转载自shift-alt-ctrl.iteye.com/blog/2335410