大部分公司并不需要微服务

本来之前标题的名字是不要被技术buzzword误导,觉得还是得标题党一些,:),请大家谅解,技术圈时长会不断的产生一些新的buzzword,很容易被误导,最可怕的是一些技术团队在没搞明白的情况下,就按buzzword去做或者去靠拢,好像生怕如果自己做的技术和buzzword不相关或者不一样,就很low一样,感觉这现象在技术圈太常见了,有些看的不太爽,写篇文章来讲讲自己的观点。

作为技术圈的我们大家,对各种buzzword一定要慎重,了解它产生的背景,可执行的各种前提和条件,技术始终是为公司的战略而服务的,buzzword是不是真的给你的场景带来了帮助,要想清楚,拿时下比较流行的微服务、AI、AR/VR来说说吧。

说到微服务这个buzzword,必须承认到现在为止我都没搞明白和服务化的区别,我都搞不太清楚淘宝在2008年做的服务化改造后形成的SOA体系到底是不是和现在的这个buzzword就是一回事,在各种文章里,微服务简直就被宣传的像是技术界一些场景的救世主,直接误导了很多同学上来就必搞微服务体系,但不知道有多少同学仔细想过有没有必要,对业务发展来说采用微服务到底是帮助还是变成了阻碍,在互联网类型快速迭代的业务中,业务的迭代效率是核心问题,以我自己的认知,对服务化我的观点一直是如果能不进这个坑,最好不进,一个单一应用的复杂度远比N个应用组成的分布式系统简单、快速多了,一旦进入分布式的坑,在技术上就不得不有比较大的投入,而对于一些还处于中小规模的公司而言,我觉得完全没有必要,Google的Jeff Dean在一次分享时讲到他对于Google做服务化的观点:让Google具备了千人并行协作开发的能力,在看到这观点以前,我一直觉得服务化重点解的是水平伸缩能力的问题,其次是并行协作的问题,但我现在基本更加赞同服务化重点是让一家公司具备了百人以上的并行协作开发能力,我认为在几十个研发同学的情况下,并行协作开发不会成为太大问题,这个时候的并行协作上的一些投入会远比进入服务化后的投入小很多,所以以前有一些朋友问我公司到底要不要改变为服务化时,我都问两问题:1. 公司研发团队现在总共多少人? 2. 目前的水平伸缩瓶颈是? 如果在这两个问题上服务化并不是核心的瓶颈,或者只需要付出少量的人或机器代价就可以解决,我会强烈建议不要做服务化,所以拜托受微服务这个buzzword诱惑的同学们,请大家在采用这样的架构前一定,千万要慎重思考,策略应该是以尽量不采用去推导会产生的代价和问题,如果这个代价和问题并不是那么大,就不要用,除非真的万不得已,那就请做好组织、团队人员方面的布局,以真正的做好服务化,不要让这个东西最后变成业务发展的障碍。

说说AI这个buzzword,我拿运维这个领域来举例说吧,AI实在是太火了,同样导致了运维界很多的工作也恨不得赶紧和AI绑上关系,当然不可否认的是,在运维这个领域,AI绝对是可以产生巨大帮助的,但首先要想明白的是你的整个环境真的为AI做好了准备吗?没想清楚这个问题,很容易最后出现一个状况是,各种算法,智能动作等等都准备好了,结果是基础的技术层面或环境层面压根就不具备这个能力,一切白扯,例如在运维这个领域,我认为要引入AI让其发挥作用,前提是要先把数据化、自动化、无人化做好,如果连这些都没做好,千万别先跳进AI的坑,AI通常依赖大量的数据去智能化的执行动作,这种情况下没有数据,不能自动执行,自动执行过程中需要人介入,那都意味着没法玩,很典型的在运维领域的一个case,容量的弹性伸缩,如果连判断一个应用容量够不够的数据都不充分,连应用能自动部署和启动都做不到,那先做了一个弹性伸缩的系统又有什么用呢?所以我更赞同的是AI确实是前景,但首先要把AI需要的一些前提给做好了,然后再进坑,千万别走反了。

最后说说同样极度火爆的AR/VR,AR/VR是个非常复杂的话题,同样很多的业务一冲动就决定投入大量资源去玩这两个方向,觉得不玩就挂了,但在玩这两个方向前,同样要想清楚的核心问题是,对于你的业务场景而言,AR/VR的一些技术普及的条件是不是都具备了,例如你生产了AR/VR内容,但现在的AR/VR的用户数,AR/VR设备的情况是不是真的到了值得你投入大量资源去做,君不见很多创业公司在做app的时候都先只做ios版本。

所以总的来说,就是技术圈的各种流行buzzword呢,当然会有它一定的道理,但是不是真的要去采用,千万别纯粹跟风,或者纯粹从技术角度判断,仔细的思考如果要采用buzzword,会发生什么,要做好什么样的准备,能获得什么样的收益(这和每家公司的战略地位,业务发展情况直接相关),毕竟一家公司的资源都是有限的,进了一个坑就意味着另一个坑可投入的资源少了。

系统设计的核心:设计原则

各种系统设计文档中,都会有专门的设计原则这个篇章,我记得我在几年前写系统设计文档时,都会觉得这个部分没什么可写的,通常会随便写上几条类似松耦合的原则,并且在之后的概要、详细设计中也不会有什么和设计原则这个部分太相关的部分,这种情况下就会越来越觉得设计原则这个小章节可写可不写,随着近几年从做单一系统到更广的多团队协作的大系统设计后,对设计原则这个章节需要传达的信息有了更多的理解,这篇文章就来谈谈自己的一些感受。

设计原则并不是什么空话,我认为设计原则表述的是架构师对整个系统的核心设计思想,并且要求把这个设计思想贯穿到所有子系统的概要/详细设计中,所以在这些子系统的概要/详细设计中要充分体现出对设计原则的考虑。

对于系统而言,什么才是设计思想呢,每个架构师在做系统的设计之前一般是会有思考的,思考的内容基本就会是要实现需求核心的几个点是什么,这些核心的点就是设计思想,举两个我自己做的设计的例子来说下,会更容易理解一些。

1. 当年在做T4(基于LXC的“虚拟化”产品)时,T4相对以前的Xen,是一个全新的替代产品,在思考T4的设计时,除了一些技术选型外,很重要的一点我判断是对用户透明,对用户而言要做到用的是T4,还是Xen,都感受不到区别,这一点必须贯穿到T4所有部分的设计中,因此这一点就是我当时列入设计原则的。

2. 前几年在做异地多活的设计时,异地多活中最重要的特色是多个异地的机房的数据库都是可写的,在这种情况下在设计的时候要考虑的重点我认为是数据正确性的问题,怎么保证用户数据不会写错乱是关键,所以在设计原则上我写入了数据正确性这条,这样确保了后续在整个跨多个技术领域的子系统的设计中都能仔细考虑数据正确性如何去做到。

3. 还有在做某些系统的设计时,平滑迁移我认为是项目能成功的关键因素,所以在设计原则上我也会写上平滑迁移,确保各个子系统在设计时会把如何从现有迁移到新的结构上放入关键。

从上面的几个例子可以看到,设计原则不仅仅需要表达设计中的一些非常技术层面的共同点(例如关键路径/非关键路径的划分、非关键路径的异步化等),更重要的是表达风险控制要素和优先级,确保在多人合作时整个项目的技术、时间风险的可控,对于一个架构师而言,如何控制好项目在设计、实现时的技术/时间风险,是我一直认为的最为关键的能力,也是我在观察很多架构师时最为看重的。

对于一个大型的项目而言,由于是多团队合作,设计思想的有效传达是非常重要的,设计思想传达的是架构师对整个系统设计的核心思考(不仅仅是结果,更是思考过程),这个思考的表达会非常有助于确保在多个架构师合作的情况下整个系统设计的一致性,以及项目核心目标的完成(之前另外一篇文章的多个团队技术方案冲突的决策原则对系统设计也是非常关键的,相同的是都是把架构师做决定的背后的原因讲清楚,以便多人协作,仅传达结果没有过程的那种是很难真正达成一致和留下深刻印象的),不过即使是小的单一系统的设计,就算架构师是同一个人,设计原则这块也需要陈述清楚,以确保在做各子模块设计时能遵守,同时也是让实现各部分代码的同学能更容易理解设计。

=============================
欢迎关注微信公众号:hellojavacases

公众号上发布的消息都存放在http://hellojava.info上。

多个团队的技术方案冲突,怎么决策

作为一个架构师或技术Leader而言,技术方案的决策是常见的要做的事,毕竟很多时候并不会只有一条路能走到目的地,这个时候到底怎么决策很容易成为一个巨大的纠结点,在涉及多团队合作的情况下,甚至有可能会成为block整件事进展的关键因素,这篇文章就来聊聊这个。

自从我开始做一些比较大的跨多团队的基础技术项目后,就会经常面临一些技术方案的决策的事,从一开始的不知所措,痛苦无比,到现在,也算是积累了一些经验和方法,技术方案的决策上,最难的其实就在于可能多个技术方案都是可以走到目的地的,这个时候怎么选择,尤其是多个团队产生冲突的时候,怎么去选择就更加复杂化了。

对于一个由多个技术团队共同完成的大的技术方案而已,会出现两种情况。

一种情况比较简单,就是每个团队各尽其职,分工清晰,这个时候大的技术方案通常不会产生太多的冲突,可能会产生的就是A团队对B团队所负责的部分的技术方案有质疑,这种情况下通常其实不会太难办,大的技术方案是需要一个大架构师的,这个架构师需要定义出整个技术方案的设计原则(之后准备写一篇什么是看起来很虚但其实是核心的设计原则),只要在遵循了设计原则的情况下,我认为B团队所负责的部分的技术方案其他团队就不需要去质疑,谁负责哪块谁决定哪块的技术方案。

另外一种情况就会比较复杂,就是这个大的技术方案中,有所做的部分重叠的不同团队,这个时候就非常容易产生技术冲突,不同团队很有可能会给出不同的方案,这个时候作为整个大技术方案的owner或架构师,怎么去决策就是巨大挑战了,毕竟就算大技术方案的架构师是独裁的,但独裁的还是要有些道理的,不能完全靠行政手段,在这样的情况下,我的观点是这个时候的选择可以基于以下几点考虑去做:
1. 设计原则
多年前在写设计文档中的设计原则时,总是会觉得没什么值得写的,松耦合等等经常会成为常用词,但貌似然并卵,近几年才越来越明白其实设计原则是设计文档中的精髓所在,看起来通常设计原则会很短的几条,但好的架构师会通过这几条控制好整个项目的技术风险,确保各团队的技术方案不偏离关键航道,设计原则里写的点要在每部分的技术方案中贯穿,否则就和没写没什么区别,更形象一点说就是设计原则是在排技术方案细节中关注的重点的优先级,例如有些项目中平滑切换是最高优先级,有些项目中技术创新是最高优先级,设计原则这个部分很值得专门写篇文章,因此这里就不再展开了。
在多个冲突的技术方案选择时,设计原则是其中关键的衡量因素。

2. 团队分工、核心价值和定位
对于产生冲突的团队,需要站在公司角度来做一个判断,产生冲突的部分的技术要做成,核心成功的关键技术点到底掌握在哪个团队手里,还有是对于哪个团队而言是更为核心的价值,只有是那个团队的核心价值才能在人力投入,未来持续发展上有保障,每个团队在一个大的组织里都是有分工的,分工也对应到了其核心价值,在多个冲突的技术方案选择时,一定要考虑这个关键的衡量因素,显然在考虑这个因素的时候要非常中立,站在公司层面而不是小团队层面来考虑,以便确保最后的决策是符合组织对每个团队分工的期望的。
举我自己经历过的两个case来说说。
Case I: 在某个大的技术方案里,有两个团队在某个技术点上都有自己的实现方案,但我最后的决策是选择了当时相对而言反而更不成熟的一方,原因就是我的判断是这个技术点要做成,核心技术其实在这个当时还不是那么成熟的团队手里,
Case II: 在某个大的技术方案里,同样也是几个团队在同一个技术点上都有自己的方案,我最后选择的那个团队基于的判断是:我相信对于公司而言,那个团队才是应该掌控这项技术的组织。

3. 团队能力状况
除了团队分工外,团队的能力状况也是关键,有可能会出现一种状况是从组织层面考虑而言,是应该在B团队做最合适,但B团队的能力在当前可能不具备,反而是另外一个团队更具备,这个时候需要做的决策会是由另外一个团队承担起来,但同时逐步的操作组织层面的变化,确保在整个大的技术方案落地后,以后有持续发展的保障。

从上面这三个衡量因素可以看出,一个大技术方案的owner或架构师,光有技术是远远不够的,在多个冲突的技术方案选择时,更主要的不是评判技术方案的优劣,一旦陷入纯粹技术方案的优劣之争,其实是很难有结果的,越大的项目越是如此,通常来说其实方案可能没有优劣之分(当然,少数冲突的情况下是有明显的优劣之分的),而是哪个更适合的问题,甚至有些时候不同方案在最终多次迭代后走向的是同一方案,只是路径不同,和语言之争类似,所以作为大技术方案的决策者,首先要做的不是去评判技术方案的优劣,而是大家先一起对齐目标、设计原则,然后清晰阐述自己做选择的因素,最后做出决策,当然因为因素毕竟有主观性,要完全得到认可仍然是不容易的,但至少让参与大项目的大伙们清楚的知道你决策的原因,这样大家才能更好的理解整个技术方案,确保最终项目的实现和落地。

大家也来聊聊你碰到过的冲突?作为非决策者的感受?又或是作为决策者你最终的选择和感受?

=============================
欢迎关注微信公众号:hellojavacases

公众号上发布的消息都存放在http://hellojava.info上。

分布式领域架构师要掌握的技术

分布式系统无疑是持久的热门话题,但其实如果不是一定有必要,强烈建议不要进入分布式领域,在集中式的情况下很多问题都会简单不少,技术人员千万不要因为外界火热的例如微服务,就把自己的产品的也去做改造,一定要仔细判断是否有必要,不要为了技术而技术,那么在必须分布式的情况下(访问量、存储量或开发人数),一个分布式领域的合格的架构师要掌握哪些技术呢,这篇文章就聊聊这个话题。

简单重复下我对架构师的标准,一个架构师最重要的不是画几个框,连几条线(这是基本要求),而是控制技术风险,要控制技术风险显然不是看几个结构性的ppt就能学会的。

通信
既然是分布式系统,系统间通信的技术就不可避免的要掌握。
首先要掌握一些基础知识,例如网络通信协议(诸如TCP/UDP等等)、网络IO(Blocking-IO,NonBlocking-IO、Asyn-IO)、网卡(多队列等);更偏应用的层面,需要了解例如连接复用、序列化/反序列化、RPC、负载均衡等。
学了这些基本知识后,基本上可以写一个简单的分布式系统里的通信模块,但这其实远远不够,既然进入了分布式领域,对规模其实就已经有了不低的要求,通常也就意味着需要的是能支持大量连接、高并发、低资源消耗的通信程序。

大量的连接通常会有两种方式:
1. 大量client连一个server
在现如今NonBlocking-IO这么成熟的情况下,一个支持大量client的server已经不那么难写了,但在大规模,并且通常长连接的情况下,有一个点要特别注意,就是当server挂掉的时候,不能出现所有client都在一个时间点发起重连,那样基本就是灾难,在没有经验的情况下我看过好几起类似的case,到client规模上去后,server一重启基本就直接被冲进来的大量建连冲垮了(当然,server的backlog队列首先应该稍微设置大一些),通常可以采用的方法是client重连前都做随机时间的sleep,另外就是重连的间隔采取避让算法。

2. 一个client连大量的server
有些场景也会出现需要连大量server的现象,在这种情况下,同样要注意的也是不要并发同时去建所有的连接,而是在能力范围内分批去建。
除了建连接外,另外还要注意的地方是并发发送请求也同样,一定要做好限流,否则很容易会因为一些点慢导致内存爆掉。

这些问题在技术风险上得考虑进去,并在设计和代码实现上体现,否则一旦随着规模上去了,问题一时半会还真不太好解。

高并发这个点需要掌握CAS、常见的lock-free算法、读写锁、线程相关知识(例如线程交互、线程池)等,通信层面的高并发在NonBlocking-IO的情况下,最重要的是要注意在整体设计和代码实现上尽量减少对io线程池的时间占用。

低资源消耗这点的话NonBlocking-IO本身基本已经做到。

伸缩性
分布式系统基本就意味着规模不小了,对于这类系统在设计的时候必须考虑伸缩性问题,架构图上画的任何一个点,如果请求量或者是数据量不断增大,怎么做到可以通过加机器的方式来解决,当然,这个过程也不用考虑无限大的场景,如果经历过从比较小到非常大规模的架构师,显然优势是不小的,同样也会是越来越稀缺的。

伸缩性的问题围绕着以下两种场景在解决:
1. 无状态场景
对于无状态场景,要实现随量增长而加机器支撑会比较简单,这种情况下只用解决节点发现的问题,通常只要基于负载均衡就可以搞定,硬件或软件方式都有;
无状态场景通常会把很多状态放在db,当量到一定阶段后会需要引入服务化,去缓解对db连接数太多的情况。
2. 有状态场景
所谓状态其实就是数据,通常采用Sharding来实现伸缩性,Sharding有多种的实现方式,常见的有这么一些:
2.1 规则Sharding
基于一定规则把状态数据进行Sharding,例如分库分表很多时候采用的就是这样的,这种方式支持了伸缩性,但通常也带来了很复杂的管理、状态数据搬迁,甚至业务功能很难实现的问题,例如全局join,跨表事务等。
2.2 一致性Hash
一致性Hash方案会使得加机器代价更低一些,另外就是压力可以更为均衡,例如分布式cache经常采用,和规则Sharding带来的问题基本一样。
2.3 Auto Sharding
Auto Sharding的好处是基本上不用管数据搬迁,而且随着量上涨加机器就OK,但通常Auto Sharding的情况下对如何使用会有比较高的要求,而这个通常也就会造成一些限制,这种方案例如HBase。
2.4 Copy
Copy这种常见于读远多于写的情况,实现起来又会有最终一致的方案和全局一致的方案,最终一致的多数可通过消息机制等,全局一致的例如zookeeper/etcd之类的,既要全局一致又要做到很高的写支撑能力就很难实现了。

即使发展到今天,Sharding方式下的伸缩性问题仍然是很大的挑战,非常不好做。

上面所写的基本都还只是解决的方向,到细节点基本就很容易判断是一个解决过多大规模场景问题的架构师,:)

稳定性
作为分布式系统,必须要考虑清楚整个系统中任何一个点挂掉应该怎么处理(到了一定机器规模,每天挂掉一些机器很正常),同样主要还是分成了无状态和有状态:
1. 无状态场景
对于无状态场景,通常好办,只用节点发现的机制上具备心跳等检测机制就OK,经验上来说无非就是纯粹靠4层的检测对业务不太够,通常得做成7层的,当然,做成7层的就得处理好规模大了后的问题。
2. 有状态场景
对于有状态场景,就比较麻烦了,对数据一致性要求不高的还OK,主备类型的方案基本也可以用,当然,主备方案要做的很好也非常不容易,有各种各样的方案,对于主备方案又觉得不太爽的情况下,例如HBase这样的,就意味着挂掉一台,另外一台接管的话是需要一定时间的,这个对可用性还是有一定影响的;
全局一致类型的场景中,如果一台挂了,就通常意味着得有选举机制来决定其他机器哪台成为主,常见的例如基于paxos的实现。

可维护性
维护性是很容易被遗漏的部分,但对分布式系统来说其实是很重要的部分,例如整个系统环境应该怎么搭建,部署,配套的维护工具、监控点、报警点、问题定位、问题处理策略等等。

从上面要掌握的这些技术,就可以知道为什么要找到一个合格的分布式领域的架构师那么的难,何况上面这些提到的还只是通用的分布式领域的技术点,但通常其实需要的都是特定分布式领域的架构师,例如分布式文件系统、分布式cache等,特定领域的架构师需要在具备上面的这些技术点的基础上还具备特定领域的知识技能,这就更不容易了。

随着互联网的发展,分布式领域的很多技术都在成熟化,想想在8年或9年前,一个大规模的网站的伸缩性是怎么设计的还是很热门的探讨话题,但是到了今天基本的结构大家其实都清楚,并且还有很多不错的系统开源出来,使得很多需要经验的东西也被沉淀下去了,在有了各种不错的开源产品的支撑下以后要做一个分布式系统的难度一定会越来越大幅降低,云更是会加速这个过程。

ps: 在写这篇文章的过程中,发现要判断一个技术人的功底有多厚,其实还真不难,就是请TA写或者讲自己觉得懂的所有技术,看看能写多厚或讲多久…要写厚或讲很久其实都不容易,尽管我也不否认要很简洁的写明白或讲清楚也不容易,但一定是先厚然后薄。

=============================
欢迎关注微信公众号:hellojavacases

公众号上发布的消息都存放在http://hellojava.info上。

Java问题排查工具箱

问题排查除了最重要的解决思路和逻辑推导能力外,工具也是不可缺少的一部分,一个好用的工具可以事半功倍,甚至在某些情况下会因为没有相应的工具而压根就没法继续进行下去,这篇文章就来讲讲在排查Java问题时通常要用到的一些工具(ps:这种文章值得收藏,看一遍其实很容易忘)。

日志相关工具
查问题的时候会非常依赖日志,因此看日志的相关工具非常重要,通常的话掌握好tail,find,fgrep,awk这几个常用工具的方法就可以,说到这个就必须说关键的异常和信息日志输出是多么的重要(看过太多异常的随意处理,例如很典型的是应用自己的ServletContextListener实现,很多的Listener实现都会变成往外抛RuntimeException,然后直接导致tomcat退出,而tomcat这个时候也不会输出这个异常信息,这种时候要查原因真的是让人很郁闷,尽管也有办法)。
日志的标准化也非常重要,日志的标准化一方面方便像我这种要查各种系统问题的人,不标准的话连日志在哪都找不到;另一方面对于分布式系统而言,如果标准化的话是很容易做日志tracing的,对问题定位会有很大帮助。

CPU相关工具
碰到一些CPU相关的问题时,通常需要用到的工具:
top (-H)
top可以实时的观察cpu的指标状况,尤其是每个core的指标状况,可以更有效的来帮助解决问题,-H则有助于看是什么线程造成的CPU消耗,这对解决一些简单的耗CPU的问题会有很大帮助。
sar
sar有助于查看历史指标数据,除了CPU外,其他内存,磁盘,网络等等各种指标都可以查看,毕竟大部分时候问题都发生在过去,所以翻历史记录非常重要。
jstack
jstack可以用来查看Java进程里的线程都在干什么,这通常对于应用没反应,非常慢等等场景都有不小的帮助,jstack默认只能看到Java栈,而jstack -m则可以看到线程的Java栈和native栈,但如果Java方法被编译过,则看不到(然而大部分经常访问的Java方法其实都被编译过)。
pstack
pstack可以用来看Java进程的native栈。
perf
一些简单的CPU消耗的问题靠着top -H + jstack通常能解决,复杂的话就需要借助perf这种超级利器了。
cat /proc/interrupts
之所以提这个是因为对于分布式应用而言,频繁的网络访问造成的网络中断处理消耗也是一个关键,而这个时候网卡的多队列以及均衡就非常重要了,所以如果观察到cpu的si指标不低,那么看看interrupts就有必要了。

内存相关工具
碰到一些内存相关的问题时,通常需要用到的工具:
jstat
jstat -gcutil或-gc等等有助于实时看gc的状况,不过我还是比较习惯看gc log。
jmap
在需要dump内存看看内存里都是什么的时候,jmap -dump可以帮助你;在需要强制执行fgc的时候(在CMS GC这种一定会产生碎片化的GC中,总是会找到这样的理由的),jmap -histo:live可以帮助你(显然,不要随便执行)。
gcore
相比jmap -dump,其实我更喜欢gcore,因为感觉就是更快,不过由于某些jdk版本貌似和gcore配合的不是那么好,所以那种时候还是要用jmap -dump的。
mat
有了内存dump后,没有分析工具的话然并卵,mat是个非常赞的工具,好用的没什么可说的。
btrace
少数的问题可以mat后直接看出,而多数会需要再用btrace去动态跟踪,btrace绝对是Java中的超级神器,举个简单例子,如果要你去查下一个运行的Java应用,哪里在创建一个数组大小>1000的ArrayList,你要怎么办呢,在有btrace的情况下,那就是秒秒钟搞定的事,:)
gperf
Java堆内的内存消耗用上面的一些工具基本能搞定,但堆外就悲催了,目前看起来还是只有gperf还算是比较好用的一个,或者从经验上来说Direct ByteBuffer、Deflater/Inflater这些是常见问题。
除了上面的工具外,同样内存信息的记录也非常重要,就如日志一样,所以像GC日志是一定要打开的,确保在出问题后可以翻查GC日志来对照是否GC有问题,所以像-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc: 这样的参数必须是启动参数的标配。

ClassLoader相关工具
作为Java程序员,不碰到ClassLoader问题那基本是不可能的,在排查此类问题时,最好办的还是-XX:+TraceClassLoading,或者如果知道是什么类的话,我的建议就是把所有会装载的lib目录里的jar用jar -tvf *.jar这样的方式来直接查看冲突的class,再不行的话就要呼唤btrace神器去跟踪Classloader.defineClass之类的了。

其他工具
jinfo
Java有N多的启动参数,N多的默认值,而任何文档都不一定准确,只有用jinfo -flags看到的才靠谱,甚至你还可以看看jinfo -flag,你会发现更好玩的。
dmesg
你的java进程突然不见了? 也许可以试试dmesg先看看。
systemtap
有些问题排查到java层面是不够的,当需要trace更底层的os层面的函数调用的时候,systemtap神器就可以派上用场了。
gdb
更高级的玩家们,拿着core dump可以用gdb来排查更诡异的一些问题。

io类型的问题我排查的很少,所以尽管知道一些工具,还是不在这里写了。

暂时就写这些,尽管工具的使用多数都可以临时学,但首先知道有哪些工具是最重要的,然后呢还是建议大家可以玩一玩这些工具,这样以后真的要用的时候也不至于一点印象都没有。

ps: 发现我的微信公众号还是写Java的文章阅读量高一些呀,本来我一直天真的认为像Borg这种文章应该阅读量也不低才对….

=============================
欢迎关注微信公众号:hellojavacases

公众号上发布的消息都存放在http://hellojava.info上。

YGC越来越慢,为什么

近来被这个case折腾的很,现象是有个应用在压测时qps表现不太稳定,好和不好的时候差别还挺大的,对比好和不好的时候,看到的现象就是好的时候ygc时间会比较短,而不好的时候ygc时间比较长,然后看正常运行的时候,也看到一个现象就是ygc时间会越来越长,直到cms gc触发后才能降回到一个比较小的值,于是开始查为什么ygc会越来越慢。

YGC越来越慢,还真没什么太多信息可入手的,于是求助JVM团队的寒泉子(微信公众号:lovestblog,强烈推荐)帮忙输出一些ygc分阶段的耗时信息,以便来推测到底是哪部分造成的ygc慢,例如我有点怀疑的是oldgen碎片的问题造成的,但通常碎片问题呢cms gc后也很难有太大程度的缓解,所以和这个现象不太一样。

拿到有更多trace信息的JDK版本后,更新上线,根据ygc不断变慢的trace信息对比发现问题出在了StringTable部分,这是ygc速度正常的情况下StringTable部分的速度:
[StringTable::possibly_parallel_oops_do_21030, 0.0010919 secs]
而ygc越来越慢后,StringTable部分:
[StringTable::possibly_parallel_oops_do_11152162, 0.1101763 secs]
从输出信息来看,可以看到在ygc速度还正常的时候,StringTable去扫的一个桶里的item数才21030个,而到了ygc速度很慢的时候,item数增长到了11152162个,这样的数量增长StringTable处理的速度变慢也是正常的。

那就要查查为什么StringTable增长那么快了,StringTable增长基本都是String.intern搞的(关于StringTable和String.intern推荐看看这篇文章:http://java-performance.info/string-intern-in-java-6-7-8/),不过这个方法在native,btrace跟不了,所以寒泉子继续改了个JDK版本,采样的输出String.intern的栈信息,这个版本放上去跑后,很快看到这样的堆栈信息:
at java.lang.String.intern(Native Method)
at com.fasterxml.jackson.core.util.InternCache.intern(InternCache.java:45)
跟着这个栈信息翻对应的代码(话说不知道大家都用什么去查代码呢,我一般都用www.grepcode.com,觉得挺好用的),很容易明白问题所在,例如InternCache.intern部分的代码如下:
if (result == null) {
result = input.intern();
put(result, result);
}

jackson之所以用intern去处理,本来是想节省点cache的内存,没想到业务场景是每次都不一样的字符串,这样直接就导致了String.intern后StringTable的大小暴涨,所以在这种场景中,这样做反而得不偿失,还好jackson代码支持通过接口来把调用intern的部分关掉。

话说用String.intern不当造成的StringTable大,从而导致ygc速度慢这问题我好像碰过好几次了,而且到现在我都觉得StringTable这东西的设计不咋样,建议大家能不用String.intern还是别用好了,除非真的是重复量非常大的相同字符串处理。

=============================
欢迎关注微信公众号:hellojavacases

公众号上发布的消息都存放在http://hellojava.info上。

Borg:G家最重要的基础设施

即使是在Borg论文发表前,从各种渠道了解也都能知道G家有一个挺神秘,非常重要的系统叫Borg,在Borg论文发表后,更是让人可以确信,再加上近两年和G家的朋友们的接触,更加可以知道对于G家的研发人员而言,说Borg是G家最重要的基础设施应该不为过,如果有看过《Google SRE》书的同学,可以看出有一点是在G家对Borg的认识是深入各研发同学的,Google SRE书里讲到G家的编译打包这种临时性任务,其实都是提交一个临时性的任务到borg来完成,而我想在传统或者说目前多数的做法里都会采用固定的机器资源来做这些事。

对Borg带来的收益,我想只用摘取Borg论文里这一小段就足以说明:
“Since many other organizations run user-facing and batch jobs in separate clusters, we examined what would happen if we did the same. Figure 5 shows that segregating prod and non-prod work would need 20–30% more machines in the median cell to run our workload.”
G家的服务器数量是百万数量级,20-30%这数量级的节省简直了…

既然Borg这么的好,为什么现在在其他互联网公司还没看到采用一样的方式去做呢(要知道Borg差不多是2003年就开始做了),从我的接触来看,我了解到的原因主要是这两个:
1.技术难度,有一些声音是觉得Borg这条路线太难走了,里面要解决的技术问题太多;
2.离线任务的规模量还不够大,这个时候Borg采用的user-facing and batch jobs混合在一台机器上运行的方式的优势就不存在了。
从上面这两个原因可以看到,要去打造一套Borg这样的系统,基本上只有机器到达一定规模,以及大数据也发展到比较大规模的公司才能有足够的收益比,当然,以现在大数据、AI的火热程度,排行前十甚至前几十的互联网公司其实都已具备这样的前提条件。

那么要打造Borg这样的系统,要突破哪些技术难度呢?

1.容器化
Borg把所有的task都放在cgroup里运行,这种方式相比Docker基于的LXC的差别在于没有namespace的隔离,简单来说就是当你登录到机器上,看到的是整个物理机的状况,这对于运维来说其实是相当痛苦的,从Borg论文也可以看到Borg为了方便对task进行运维,做了很多辅助工具和系统,所以在容器化这个方面我会更加倾向于LXC,具备namespace的隔离,对运维来说就会友好非常多。
为什么一定要容器化呢,原因在于如果对资源的需求都是物理机,那资源的调度的灵活性将会大幅下降,所以容器化是非常重要的一个基础前提,容器化的难题一方面是对LXC的掌握,另一方面则是任何类型的软件都要放在LXC中跑,会碰到各种问题,无论是性能还是namespace隔离不完整带来的支持问题。

2.复杂的调度器
当一家大规模的公司的所有业务都要通过同一个调度层来分配资源时,这个调度层所要面临的业务复杂性是非常可怕的,再加上batch jobs的调度,各种调度需求混合在一起,以及要管理巨大的机器规模,如何在保证业务需求满足的同时的规模、效率、稳定,绝对不是一件容易的事,现在业界有的其他的例如swarm、k8s、mesos、yarn等等,离borg这种级别的调度器是有巨大差距的。

3.资源隔离
Borg需要把不同优先级的任务跑在同一台机器上,non-prod优先级的任务会share prod优先级任务的资源,这个时候如何保障好non-prod任务不影响到prod任务的性能等就非常非常重要了,而这个目前业界并没有什么很成熟的方案,无论是cpu、内存、IO(磁盘/网络),离要成熟的支撑这样的资源隔离都还有非常大的差距。

4.历史包袱
Borg在2003年开始做的话,我想那个时候G家的历史包袱应该是不大的,但现在其他的大的互联网公司要做的话,显然都有不小的历史包袱,例如通常都会需要面临多种不同的基础设施、运维模式等,怎么在背着巨大的历史包袱走到期望的那步,其实是一件非常不容易的事。

可以说,这四点技术难度都是世界级的,因为碰到这样的技术难度的机会其实本来就很少,显然只有巨大的收益才能产生这样的驱动力,所以说这种类型的系统只有大规模的公司才会投入力量去做,而现在阿里就在投入力量打造这样的系统,如果你正好对这些领域有兴趣,并且在这些领域有一定的积累,我想这样的好的难得的机会(就像一直说,只有少数几家公司有做这样的系统的动力)不容错过,应该毫不犹豫的来加入我们,直接发送消息来联系我吧,期待你的加盟!

=============================
欢迎关注微信公众号:hellojavacases

公众号上发布的消息都存放在http://hellojava.info上。

高质量的工程代码为什么难写

之所以想起写这篇文章,是因为最近看到的一个著名的开源项目在内部使用时的各种问题,不得不说,很多的开源的东西思想是不错的,但离真正工程化都有不小的距离,所以没什么商业公司采用的开源产品如果要引入的话一定要慎重,通常会有N多的坑等着你去填,而比较成功的开源项目的背后多数都会有商业公司在背后不断的改进。

遥想我00年开始学习写asp代码时,觉得写代码也不难呀,无非就是学学语法规则、库就可以写出来,记得有一次我实习面试的时候是让我在一个下午左右的时间写一个完整的留言板,那也就是刷刷刷就写好了,但随着后来工作,尤其是加入阿里以后,越来越明白高质量的工程代码为什么难写。

在写代码初期,最关注的是如何用代码实现需求,如果是仅仅实现业务需求的话,即使是刚上手的程序员,只要解题能力还OK,基本上都是可以写出代码来的,所以我自己一直认为数学成绩是程序员的一个非常重要的要求,数学好的人通常解题和逻辑思维能力是还不错的。

上面的这个基本的写代码的过程中,写的更好的同学的体现会在对业务的深刻理解以及抽象上,写出的代码会具备一定的复用能力,这个在这里不多加探讨。

但代码是不是实现了业务需求就结束了呢,其实远没有,这其实只是写代码的开始,除了正向的逻辑实现外,任何一个点的异常的分支逻辑怎么处理才是工程化的代码中更难处理的部分,这个问题在单机式的系统中会相对还好处理,在分布式的环境会变得非常的复杂,例如调用其他机器的功能超时了,出错了,到底该怎么处理,这也是为什么有了那么多的分布式的理论的东西出来,在增加了异常分支的处理逻辑后,通常会发现这个时候正向逻辑的代码在整个代码的占比中会大幅下降。

异常分支逻辑处理好后,通常还需要增加必要的日志信息,以便在出问题时方便排查,而不是到了要排查问题的时候,一点目前系统的状况都搞不清楚,所以吃掉重要的异常信息不抛出这种行为在写代码中是非常可耻的。

在处理好上面异常的相关动作后,代码的健壮性也要处理好,这个主要指:
1. 自我保护能力
对外提供的接口是否具备足够的自我保护能力,就是即使使用的人没仔细看API文档随便乱用也不会导致系统出问题,这种案例非常的多,例如对外提供了一个批量查询接口,结果用户一下传了一个里面有上千个用户id的数组,查询一下直接把内存耗光,像这种情况下不能怪使用的人,而应该怪实现API的这一端的保护做的不够好,按照这样的标准去看,会发现开源的很多东西的API都不太合格;
还有一种就是能力保护,如果超出了处理的并发量的能力,这个时候会发生什么;
2. 对资源的使用限制
这也是代码新手或一些开源产品中做的比较差的地方,很容易出现规模一上去,资源使用量也一直涨,没有限制,然后导致系统挂掉,很常见的案例是对线程池的使用,例如像Java中的Executors.newCachedThreadPool,这个接口很多人会用到,但很多用的人都没有仔细想过会不会在某种情况下这里创建出巨多的线程;还有例如用Map做cache,也没考虑大小限制的问题,结果就是随着数据量增长,某天突然就挂了;
健壮性是代码中比较复杂的部分,通常也是比较展现代码能力的部分,可能看起来就几行代码,但其实背后反映的差距是巨大的。

开源产品除了在健壮性上的差距外,通常还会出现的一个巨大差距就是整个系统的设计的伸缩能力,伸缩能力不够的话通常会导致结构性的重构,另外常见的就是在并发的处理上不够高效,例如锁的合理使用、无锁算法的引入等等,而这些需要非常强的系统设计和代码功底能力。

除了上面说的这些外,高质量的工程代码还需要考虑可维护(例如监控信息暴露)、安全性等,对我而言,我一直认为所谓的工程化其实就是把一些玩具性质的代码变成可在商业系统中真正健壮运行的代码。

上面的内容写的比较简略,不过应该也能看出,对于高质量的工程代码而言,其实实现业务逻辑只是其中占比很小的一部分,甚至花的时间是相对最少的一部分,所以我确实非常赞同面试的时候让同学写代码,这个时候很容易看出同学写代码的功力;有些时候为了考察同学写代码的熟练程度,我会问问IDE的快捷键,或者让手写一段不是太复杂的代码。

=============================
欢迎关注微信公众号:hellojavacases

公众号上发布的消息都存放在http://hellojava.info上。

从Netty到EPollSelectorImpl学习Java NIO

终于可以在写了几篇鸡汤文后,来篇技术文章了,:),题图是Trustin Lee,Mina/Netty都是他搞的,对Java程序员尤其是写通讯类的都产生了巨大影响,向他致敬!

在上周查一个内存OOM的问题之前,我一直觉得自己对Java NIO应该还是比较懂的,君不见N年前我曾经写过一篇《NFS-RPC框架优化过程(从37K到168K)》(尴尬的发现,上次导blog记录的时候竟然丢了一些文章,于是这文章link就不是自己的blog了),从那优化经历来说理论上对Netty的原理应该已经是比较清楚了才对,结果在查那个内存OOM的问题的时候,发现自己还是too young too navie,看到的现象是EPollSelectorImpl里的fdToKey有一大堆数据,就傻眼了,完全不知道这是为什么,当时就在公众号上发了个文本信息咨询大家,有不少同学给了我回复,另外滴滴打车的架构师欧阳康给了我一篇文章来说明EPollSelectorImpl这个部分的原理(强烈推荐,比我写的这篇会更深入到os层),本文就是综合了大家给我的点拨,来写写从Netty到EPollSelectorImpl的相关逻辑。

带着问题去看代码会比较的快和容易,我这次带着这几个问题:
1. EPollSelector里的fdToKey里的一大堆数据是怎么出现的;
2. Netty以及Java的EPoll部分代码是如何让N多连接的处理做到高效的,当然这主要是因为epoll,不过Java部分的相关实现也是重要的。

由于公众号贴代码不太方便,在这我就不贴大段的代码了,只摘取一些非常关键的代),代码部分我看的是Server部分,毕竟Server尤其是长连接类型的,通常会需要处理大量的连接,而且会主要是贴近我所关注的两个问题的相关代码。

Netty在初始化Server过程中主要做的事:
1. 启动用于处理连接事件的线程,线程数默认为1;
2. 创建一个EPollSelectorImpl对象;

在bind时主要做的事:
1. 开启端口监听;
2. 注册OP_ACCEPT事件;

处理连接的线程通过Selector来触发动作:

int selected = select(selector);

这个会对应到EPollSelectorImpl.doSelect,最关键的几行代码:

pollWrapper.poll(timeout);
int numKeysUpdated = updateSelectedKeys(); // 更新有事件变化的selectedKeys,selectedKeys是个Set结构

当numKeysUpdated>0时,就开始处理其中发生了事件的Channel,对连接事件而言,就是去完成连接的建立,连接建立的具体动作交给NioWorker来实现,每个NioWorker在初始化时会创建一个EPollSelectorImpl实例,意味着每个NioWorker线程会管理很多的连接,当建连完成后,注册OP_READ事件,注册的这个过程会调用到EPollSelectorImpl的下面的方法:

protected void implRegister(SelectionKeyImpl ski) {
SelChImpl ch = ski.channel;
fdToKey.put(Integer.valueOf(ch.getFDVal()), ski);
pollWrapper.add(ch);
keys.add(ski);
}

从这段代码就明白了EPollSelectorImpl的fdToKey的数据是在连接建立后产生的。

那什么时候会从fdToKey里删掉数据呢,既然放数据是建连接的时候,那猜测删除就是关连接的时候,翻看关连接的代码,最终会调到EPollSelectorImpl的下面的方法:

protected void implDereg(SelectionKeyImpl ski) throws IOException {
assert (ski.getIndex() >= 0);
SelChImpl ch = ski.channel;
int fd = ch.getFDVal();
fdToKey.remove(new Integer(fd));
pollWrapper.release(ch);
ski.setIndex(-1);
keys.remove(ski);
selectedKeys.remove(ski);
deregister((AbstractSelectionKey)ski);
SelectableChannel selch = ski.channel();
if (!selch.isOpen() && !selch.isRegistered())
((SelChImpl)selch).kill();
}

从上面代码可以看到,在这个过程中会从fdToKey中删除相应的数据。

翻代码后,基本明白了fdToKey这个部分,在Netty的实现下,默认会创建一个NioServerBoss的线程,cpu * 2的NioWorker的线程,每个线程都会创建一个EPollSelectorImpl,例如如果CPU为4核,那么总共会创建9个EPollSelectorImpl,每建立一个连接,就会在其中一个NioWorker的EPollSelectorImpl的fdToKey中放入SelectionKeyImpl,当连接断开时,就会相应的从fdToKey中删除数据,所以对于长连接server的场景而言,fdToKey里有很多的数据是正常的。

——————————-

第一个问题解决后,继续看第二个问题,怎么用比较少的资源高效的处理那么多连接的各种事件。

根据上面翻看代码的记录,可以看到在Netty默认的情况下,采用的是1个线程来处理连接事件,cpu * 2个NioWorker线程来处理读写事件。

连接动作因为很轻量,因此1个线程处理通常足够了,当然,客户端在设计重连的时候就得有避让机制,否则所有机器同一时间点重连,那就悲催了。

在分布式应用中,网络的读写会非常频繁,因此读写事件的高效处理就非常重要了,在Netty之上怎么做到高效也很重要,具体可以看看我之前写的那篇优化的文章,这里就只讲Netty之下的部分了,NioWorker线程通过
int selected = select(selector);
来看是否有需要处理的,selected>0就说明有需要处理,EPollSelectorImpl.doSelect中可以看到一堆的连接都放在了pollWrapper中,如果有读写的事件要处理,这里会有,这块的具体实现还得往下继续翻代码,这块没再继续翻了,在pollWrapper之后,就会updateSelectedKeys();,这里会把有相应事件发生的SelectionKeyImpl放到SelectedKeys里,在netty这层就会拿着这个selectedKeys进行遍历,一个一个处理,这里用多个线程去处理没意义的原因是:从网卡上读写的动作是串行的,所以再多线程也没意义。

所以基本可以看到,网络读写的高效主要还是ePoll本身做到,因为到了上层其实每次要处理的已经是有相应事件发生的连接,netty部分通过较少的几个线程来有效的处理了读写事件,之所以读写事件不能像连接事件一样用一个线程去处理,是因为读的处理过程其实是比较复杂的,从网卡cp出数据后,还得去看数据是否完整(对业务请求而言),把数据封装扔给业务线程等等,另外也正因为netty是要用NioWorker线程处理很多连接的事件,因此在高并发场景中保持NioWorker整个处理过程的快速,简单是非常重要的。

——————————

带着这两个问题比以前更往下的翻了一些代码后,确实比以前更了解Java NIO了,但其实还是说不到深入和精通,因为要更细其实还得往下翻代码,到OS层,所以我一如既往的觉得,其实Java程序员要做到精通,是比C程序员难不少的,因为技术栈更长,而如果不是从上往下全部打通技术栈的话,在查问题的时候很容易出现查到某层就卡壳,我就属于这种,所以我从来不认为自己精通Java的某部分。

最近碰到的另外一个问题也是由于自己技术栈不够完整造成排查进展一直缓慢,这问题现在还没结果,碰到的现象就是已经触发了netty避免ePoll bug的workaround,日志里出现:
Selector.select() returned prematurely 512 times in a row; rebuilding selector.
这个日志的意思是Selector.select里没有数据,但被连续唤醒了512次,这样的情况很容易导致一个cpu core 100%,netty认为这种情况出现时ePoll bug造成的,在这种情况下会采取一个workaround方法,就是rebuilding selector,这个操作会造成连接重建,对高并发场景来说,这个会造成超时等现象,所以影响还挺大的。
由于这个问题已经要查到os层,我完全无能为力,找了公司的一个超级高手帮忙查,目前的进展是看到有一个domain socket被close了,但epoll_wait的时候还是会选出这个fd,但目前还不知道为什么会出现这现象,所以暂时这问题还是存在着,有同学有想法的也欢迎给些建议。

=============================
欢迎关注微信公众号:hellojavacases

公众号上发布的消息都存放在http://hellojava.info上。

Java NIO之EPollSelectorImpl详解

这是滴滴的架构师欧阳康同学写的,非常赞,从EPollSelectorImpl到OS层面实现的详细解释,可以让大家对Java NIO的实现有更完整的理解,强烈推荐。

本文简述JDK1.7的NIO在linux平台上的实现,对java NIO的一些核心概念如Selector,Channel,Buffer等,不会做过多解释,这些请参考JDK的文档。JDK 1.7 NIO Selector在linux平台上的实现类是sun.nio.ch.EPollSelectorImpl,这个类通过linux下的epoll系列系统调用实现NIO,因此在介绍这个类的实现之前,先介绍一下linux的epoll。epoll是poll/select系统调用的一个改进版本,能以更高的性能实现IO事件的检测和分发(主要归功于epoll的事件回调机制,下文详述),主要包含以下3个系统调用:

#include
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

上述函数中,epoll_create函数负责创建一个检测IO事件的epoll实例,size参数用于“暗示”操作系统事件队列的长度,在linux-2.6.32内核中,此参数被忽略。epoll_ctl函数用于管理文件描述符的事件集,使用此函数可以注册、修改、删除一个或多个事件。epoll_wait负责检测事件,这三个函数的详细描,请参阅epoll的man文档。
Java类sun.nio.ch.EPollSelectorImpl主要的功能都委托给sun.nio.ch. EPollArrayWrapper实现 (下文所引java代码反编译自linux版jdk_1.7.0_17/lib/rt.jar):

package sun.nio.ch;
class EPollArrayWrapper{
private native int epollCreate();
private native void epollCtl(int paramInt1, int paramInt2, int paramInt3, int paramInt4);
private native int epollWait(long paramLong1, int paramInt1, long paramLong2, int paramInt2) throws IOException;
}

EPollArrayWrapper的三个native方法的实现代码可参阅openjdk7/jdk/src/solaris/native/sun/nio/ch/ EPollArrayWrapper.c,可看到这三个native方法正是对上述epoll系列系统调用的包装。(其他jdk的实现代码会有所不同,但归根结底都是对epoll系列系统调用的包装)。
EPollSelectorImpl. implRegister方法(Selector.register方法的具体实现),通过调用epoll_ctl向epoll实例中注册事件:

protected void implRegister(SelectionKeyImpl paramSelectionKeyImpl) {
if (this.closed)
throw new ClosedSelectorException();
SelChImpl localSelChImpl = paramSelectionKeyImpl.channel;
this.fdToKey.put(Integer.valueOf(localSelChImpl.getFDVal()), paramSelectionKeyImpl);
this.pollWrapper.add(localSelChImpl);
this.keys.add(paramSelectionKeyImpl);
}

上述方法中,除了向epoll实例注册事件外,还将注册的文件描述符(fd)与SelectionKey的对应关系添加到fdToKey中,这个map维护了文件描述符与SelectionKey的映射。每当向Selector中注册一个Channel时,向此map中添加一条记录,而当Channel.close、SelectionKey.cancel方法调用时,则从fdToKey中移除与Channel的fd相关联的SelectionKey,具体代码在EPollSelectorImpl.implDereg方法中:

protected void implDereg(SelectionKeyImpl paramSelectionKeyImpl) throws IOException {
assert (paramSelectionKeyImpl.getIndex() >= 0);
SelChImpl localSelChImpl = paramSelectionKeyImpl.channel;
int i = localSelChImpl.getFDVal();
this.fdToKey.remove(Integer.valueOf(i));
this.pollWrapper.release(localSelChImpl);
paramSelectionKeyImpl.setIndex(-1);
this.keys.remove(paramSelectionKeyImpl);
this.selectedKeys.remove(paramSelectionKeyImpl);
deregister(paramSelectionKeyImpl);
SelectableChannel localSelectableChannel = paramSelectionKeyImpl.channel();
if ((!localSelectableChannel.isOpen()) && (!localSelectableChannel.isRegistered()))
((SelChImpl)localSelectableChannel).kill();
}

EPollSelectorImpl. doSelect(Selector.select方法的实现),则通过调用epoll_wait实现事件检测:

protected int doSelect(long paramLong)
throws IOException
{
if (this.closed)
throw new ClosedSelectorException();
processDeregisterQueue();
try {
begin();
this.pollWrapper.poll(paramLong);
} finally {
end();
}
processDeregisterQueue();
int i = updateSelectedKeys();
if (this.pollWrapper.interrupted())
{
this.pollWrapper.putEventOps(this.pollWrapper.interruptedIndex(), 0);
synchronized (this.interruptLock) {
this.pollWrapper.clearInterrupted();
IOUtil.drain(this.fd0);
this.interruptTriggered = false;
}
}
return i;
}

此方法的主要流程概括如下:
1. 通过epoll_wait调用(this.pollWrapper.poll)获取已就绪的文件描述符集合
2. 通过fdToKey查找文件描述符对应的SelectionKey,并更新之,更新SelectionKey的具体代码在EPollSelectorImpl .updateSelectedKeys中:

private int updateSelectedKeys()
{
int i = this.pollWrapper.updated;
int j = 0;
for (int k = 0; k < i; k++) { int m = this.pollWrapper.getDescriptor(k); SelectionKeyImpl localSelectionKeyImpl = (SelectionKeyImpl)this.fdToKey.get(Integer.valueOf(m)); if (localSelectionKeyImpl != null) { int n = this.pollWrapper.getEventOps(k); if (this.selectedKeys.contains(localSelectionKeyImpl)) { if (localSelectionKeyImpl.channel.translateAndSetReadyOps(n, localSelectionKeyImpl)) j++; } else { localSelectionKeyImpl.channel.translateAndSetReadyOps(n, localSelectionKeyImpl); if ((localSelectionKeyImpl.nioReadyOps() & localSelectionKeyImpl.nioInterestOps()) != 0) { this.selectedKeys.add(localSelectionKeyImpl); j++; } } } } return j; }

关于fdToKey,有几个问题:
一、为何fdToKey会变得非常大?由上述代码可知,fdToKey变得非常大的可能原因有2个:
1.注册到Selector上的Channel非常多,例如一个长连接服务器可能要同时维持数十万条连接;
2.过期或失效的Channel没有及时关闭,因而对应的记录会一直留在fdToKey中,时间久了就会越积越多;
二、为何fdToKey总是串行读取?fdToKey中记录的读取,是在select方法中进行的,而select方法一般而言总是单线程调用(Selector不是线程安全的)。
三、tcp发包堆积对导致fdToKey变大吗?一般而言不会,因为fdToKey只负责管理注册到Selector上的channel,与数据传输过程无关。当然,如果tcp发包堆积导致IO框架的空闲连接检测机制失效,无法及时检测并关闭空闲的连接,则有可能导致fdToKey变大。

下面聊一聊epoll系统调用的具体实现,它的实现代码在(linux-2.6.32.65)fs/eventpoll.c中(下文所引内核代码,由于较长,所以只贴出主流程,省略了错误处理及一些相对次要的细节如参数检查、并发控制等),先看epoll_create 系统调用的实现:
fs/eventpoll.c

SYSCALL_DEFINE1(epoll_create, int, size)
{
if (size <= 0) return -EINVAL; return sys_epoll_create1(0); }

SYSCALL_DEFINE1是一个宏,用于定义有一个参数的系统调用函数,上述宏展开后即成为:
int sys_epoll_create(int size)
这就是epoll_create系统调用的入口。至于为何要用宏而不是直接声明,主要是因为系统调用的参数个数、传参方式都有严格限制,最多六个参数, SYSCALL_DEFINE2 -SYSCALL_DEFINE6分别用来定义有2-6个参数的系统调用。由上述代码可知,epoll_create函数最终调用sys_epoll_create1实现具体功能,同时也可以看出size参数被忽略了。sys_epoll_create1的主要代码如下(省略了错误处理及一些次要的细节如参数检查等):
fs/eventpoll.c

SYSCALL_DEFINE1(epoll_create1, int, flags)
{
int error, fd;
struct eventpoll *ep = NULL;
struct file *file;
error = ep_alloc(&ep);
file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep,
O_RDWR | (flags & O_CLOEXEC));
fd_install(fd, file);
ep->file = file;
return fd;
}

上述代码主要是分配一个struct eventpoll实例,并分配与此实例相关联的文件描述符,后续的epoll_ctl,epoll_wait调用通过此文件描述符引用此实例。struct eventpoll的结构如下:
fs/eventpoll.c

struct eventpoll {
spinlock_t lock;
struct mutex mtx;
wait_queue_head_t wq;
wait_queue_head_t poll_wait;
struct list_head rdllist;
struct rb_root rbr;
struct epitem *ovflist;
struct user_struct *user;
struct file *file;
int visited;
struct list_head visited_list_link;
}

上述数据结构的关键部分是:
1. 一个等待队列wq,epoll正是通过此等待队列实现的事件回调
2. 一个就绪列表rdllist,此列表以双链表的形式保存了已就绪的文件描述符
3. 一个红黑树rbr,用于保存已注册过的文件描述符,若重复注册相同的文件描述符,则会返回错误
等待队列是epoll系统调用的核心机制(不只是epoll,linux下事件的通知、回调等机制大都依赖于等待队列),在讲述epoll_ctl,epoll_wait的实现之前,先来看看等待队列。等待队列可以使一组进程/线程在等待某个事件时睡眠,当等待的事件发生时,内核会唤醒睡眠的进程/线程。注意,下文并不区分进程和线程,在linux下,进程和线程在调度这个意义下(调度就是指linux的进程调度,包括进程的切换、睡眠、唤醒等)并无差别。此机制可以类比java.lang.Object类的wait和notify/notifyAll方法,其中wait方法使线程睡眠,notify/notifyAll方法则唤醒睡眠的一个或全部线程。等待队列主要涉及两个数据结构:
include/linux/wait.h

struct __wait_queue_head {
spinlock_t lock;
list_head task_list;
};
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
void *private;
wait_queue_func_t func;
struct list_head task_list;
};

struct __wait_queue_head是队头结构,task_list 保存了添加到此队列上的元素,struct list_head是标准的linux双链表, 定义如下:
include/linux/list.h

struct list_head {
struct list_head *next, *prev;
};

注意,此结构既可以表示双链表的表头,也可以表示一个链表元素,且next,prev这两个指针可以指向任意数据结构。
struct __wait_queue是等待队列的元素结构,成员func是等待的进程被唤醒时执行的回调函数,其定义如下:
include/linux/wait.h

typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int flags, void *key);

struct __wait_queue的成员task_list是一个链表元素用于将此结构放置到struct __wait_queue_head中(这和此结构的task_list成员含义是不同的,此成员的含义为双链表的头),private成员一般指向等待进程的task_struct实例(该结构成员很多,在此就不贴出了,只需要知道linux下每个进程都对应一个task_struct 实例)。
在使用上,等待队列主要涉及以下函数(或者宏):
include/linux/wait.h
__add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
#define wait_event(wq, condition)
#define wake_up_xxx(x,…)
__add_wait_queue用于将一个进程添加到等待队列,wait_event是一个宏,它用于等待一个事件,当事件未发生时使等待的进程休眠,wake_up_xxx是一系列的宏,包括wake_up,wake_up_all,wake_up_locked,wake_up_interruptible等,负责唤醒休眠在某个事件上的一个或一组进程。关于等待队列的具体实现细节,由于牵涉较广(涉及到进程调度、中断处理等),这里不再详述,可以将add_wait_queue,wait_event类比java.lang.Object的wait方法,而wake_up则可以类比java.lang.Object的notify/notifyAll方法。
介绍完等待队列后,就可以进一步研究epoll_ctl的实现了,其代码实现中核心的部分是:
fs/eventpoll.c

SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
struct epoll_event __user *, event)
{
if (!tfile->f_op || !tfile->f_op->poll)
goto error_tgt_fput;
switch (op) {
case EPOLL_CTL_ADD:
error=ep_insert(ep, &epds, tfile, fd);
break;
case EPOLL_CTL_DEL:
error=ep_remove(ep, epi);
break;
case EPOLL_CTL_MOD:
error = ep_modify(ep, epi, &epds);
break;
}
return error;
}

什么样的文件描述符可以注册?从那个if判断可以看出,只有文件描述符对应的文件实现了poll方法的才可以,一般而言,字符设备的文件都实现了此方法,网络相关的套接字也实现了此方法,而块设备文件例如ext2/ext3/ext4文件系统文件,都没有实现此方法。实现了poll方法的文件,对应于java NIO的java.nio.channels.SelectableChannel,这也是为何只有 SelectableChannel 才能注册到Selector上的原因。ep_insert,ep_remove,ep_modify分别对应事件的注册、删除、修改,我们以ep_insert为例,看一下事件注册的过程,其关键代码如下:
fs/eventpoll.c

static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
struct file *tfile, int fd)
{
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
revents = tfile->f_op->poll(tfile, &epq.pt);
ep_rbtree_insert(ep, epi);
if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) {
list_add_tail(&epi->rdllink, &ep->rdllist);;
wake_up_locked(&ep->wq);
}
}

上述代码的主要做的事是:
1. 绑定等待队列的回调函数ep_ptable_queue_proc
2. 调用对应文件的实例的poll方法,此方法的具体实现差别非常大,但绝大多数都会调用wait_event相关方法,在没有事件发生时,使进程睡眠,例如socket对应的实现(代码在net/ipv4/af_inet.c的tcp_poll方法,在此不再详述);
3. 若注册的事件已经发生,则将已就绪的文件描述符插入到eventpoll实例的就绪列表(list_add_tail(&epi->rdllink, &ep->rdllist);),并唤醒睡眠的进程(wake_up_locked(&ep->wq))
第1步绑定的回调函数ep_ptable_queue_proc,会在等待的事件发生时执行,其主要功能是将就绪的文件描述符插入到eventpoll实例的就绪列表(具体是通过ep_ptable_queue_proc绑定的另一个回调函数ep_poll_callback实现的):
fs/eventpoll.c

static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key){
if (!ep_is_linked(&epi->rdllink))
list_add_tail(&epi->rdllink, &ep->rdllist);
}

最后看epoll_wait的实现,有了就绪队列,epoll_wait的实现就比较简单了,只需检查就绪队列是否为空,若为空,则在必要时睡眠或等待:
fs/eventpoll.c

SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
int, maxevents, int, timeout)
{
int error;
struct file *file;
struct eventpoll *ep;
file = fget(epfd);
ep = file->private_data;
error = ep_poll(ep, events, maxevents, timeout);
return error;
}

此函数最终调用ep_poll完成其主要功能:

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
int maxevents, long timeout)
{
retry:
if (list_empty(&ep->rdllist)) {
init_waitqueue_entry(&wait, current);
wait.flags |= WQ_FLAG_EXCLUSIVE;
__add_wait_queue(&ep->wq, &wait);

for (;;) {
set_current_state(TASK_INTERRUPTIBLE);
if (!list_empty(&ep->rdllist) || !jtimeout)
break;
if (signal_pending(current)) {
res = -EINTR;
break;
}

spin_unlock_irqrestore(&ep->lock, flags);
jtimeout = schedule_timeout(jtimeout);
spin_lock_irqsave(&ep->lock, flags);
}
__remove_wait_queue(&ep->wq, &wait);

set_current_state(TASK_RUNNING);
}
eavail = !list_empty(&ep->rdllist) || ep->ovflist != EP_UNACTIVE_PTR;

spin_unlock_irqrestore(&ep->lock, flags);
if (!res && eavail &&
!(res = ep_send_events(ep, events, maxevents)) && jtimeout)
goto retry;

return res;
}

上述代码主要是检查就绪队列是否为空,若为空时,则根据超时设置判断是否需要睡眠(__add_wait_queue)或等待(jtimeout = schedule_timeout(jtimeout);)。

综上所述,epoll系统调用通过等待队列,其事件检测(epoll_wait系统调用)的时间复杂度为O(n),其中n是“活跃”的文件描述符总数,所谓的活跃,是指在该文件描述符上有频繁的读写操作,而对比poll或select系统调用(其实现代码在fs/select.c中),其时间复杂度也是O(n),但这个n却是注册的文件描述符的总数。因此,当活跃的文件描述符占总的文件描述符比例较小时,例如,在长连接服务器的场景中,虽然同时可能需要维持数十万条长连接,但其中只有少数的连接是活跃的,使用epoll就比较合适。

=============================
欢迎关注微信公众号:hellojavacases

公众号上发布的消息都存放在http://hellojava.info上。