Java,Go,NodeJS性能测试

PS: 项目地址:github,这边有所有的代码 写在文章之前: rpctest文件夹下为测试程序,使用说明见文档,改写自网上淘宝的性能测试用代码 go java nodejs文件夹下为三个不同版本的rpc echo server. 线下测试看,go语言的效率和Nodejs差不多,java因为没有解决并发问题所以只有单线程,效率约为前两者的70~80%,后续将会使用netty进行测试比较 测试环境: Intel i5 4核4线程 内存8G UBuntu x64 文章开始 文章缘由,因为公司中真好有一个代码竞赛,就是比赛写一个rpc服务调用性能竞赛,不能使用现有的轮子,所以java的netty,mina都被毙了,于是就开始了坑跌的 码码。第一个反应肯定使用java,毕竟是最熟悉的语言,然后就开始选型,bio,nio,aio,各种东西都出现了。到底选哪个?因为是追求性能,所以bio第一个被毙了, 然后就是nio和aio了,前者是io复用,后者是epoll,明显后者占优,所以最后锁定了使用aio,然后就开始码码了。 Java Version 首先,肯定是上网找aio的资料,都是不靠谱的,都是抄来抄去,没有什么实质的,除了淘宝邓悟的文章,不过也有缺陷,就是在高压下,会产生pendingwriteException 这个原因就是重复写。如果进行代码调试,将线程数增加,通过我的压测程序就可以发现。如图: 对于这个问题,如果不引入锁概念,就没有办法解决,至少我没解决。但是当时,想到第一个思路就是,使用消息队列的方式对写数据进行缓存,底层使用一个线程进行读取写,确保一个channel只被一个线程写。这样就杜绝了重复写的问题。但是时间紧迫- -,代码没有完善,就提交了。 最后依旧是单线程的程序。不过qps也已经很高了,在100线程,100byte数据的压测下,qps依旧能到50000+.这边就是测试数据: Go Version 然后就是传说中的Go了,Go有一个很明显的优势,就是协程,不过在这个文件中,我并没有使用到协程。原因就是时间问题,协程的概念就是在线程上做了一个调度,类似于 NodeJs的事件驱动。这边还是使用一个线程一个连接的弱方法,不过因为是直接的2进制文件,运行起来,对服务器的资源消耗依旧是很少。这就是运行时的top截图: 对比之下,之前的那个Java,在内存占用上确实是一个不小的劣势。 而且,在QPS测试上,Go也明显领先于单线程的Java程序,Go的QPS数据如下: 脱脱的60000+,所以这一关上,Go胜出。 NodeJS Version 最后的永远是主角,传说中性能爆表的NodeJS出现了。这边我用了一个比较作弊的手法,就是建立多个进程进行处理,但是,也只有CPU核数进程,所以在Top上,会看到四个NodeJS 进程,这就是我用来提升性能的一个偏方。所以我们来看这边的压测数据: 也是脱脱的60000+,不过,top数据就不那么好看了。 可见,NodeJS将CPU发挥到了相对极致,不过因为分散,所以整体上还是比较平稳。 总结 其实,总体上看,语言发展至今,语言层面的性能已经相差不大。而且绝大多数的程序还跟本没到语言性能层面。所以,开发的时候,还是要按照项目组最擅长的角度进行技术选型 并不是,新的就是最好的。不过有一点可以透露,NodeJS用了20行代码,Go是50行代码,而Java则是100+,不过我进行编写时所花费的时间是差不多的,所谓的开发效率,则是在相同 熟悉度的情况下。如果一点都不知道NodeJS,而就听说开发效率高就选择了他,到最后,其实得不偿失,不仅效率没有太大的提高,反而导致程序的不稳定。 最后,当然是Go最好了拉。不过。之后我会用netty进行再一度的比较,看看java,到底是不是壮士暮年了。

关于容器的新的看法

关于Java中的容器,大家因该第一个想到的是Tomcat,毕竟这应该是学习Java中的第一个容器(如果玩Java Web的话)。在经历了第一个项目和第二个项目之后,我才开始体会到容器的 必要性。 在此之前,我之前的所有的Java程序都是指定main函数的runnable jar package。这样做确实简单。而且快捷。但是有一个很明显的缺点,就是你无法控制他的生命周期。举一个很 简单的例子,如果程序挂了,除了运气比较好去top或者ps一把发现没有这个进程之外,貌似没有好的办法了(此处不考虑心跳协议)。还有一点,程序的可装配性比较差。需要在 main函数里指定加载的顺序。凡此种种,都是因为我们将程序直接泡在了jvm上,而jvm并没有在java层面上给我们过多的api接口。所以,为了改变这个状况,我们在jvm上又加了一个 容器。 所以,简单地说,容器的作用就是一个脸盆,程序就像是一个一个土豆,然后程序的运行就像是在脸盆里煮土豆。如果土豆煮过头了,脸盆就可以最先发现,同时,土豆爆炸了,脸盆也最先知道。 就拿这次的爬虫程序来说,我们本来可以用一个简单的jar程序就可以实现,但是考虑到爬虫的状态获取和报警机制,还是将爬虫扔到了tomcat中,同时,由于有多个模块的组合,我们又 通过spring进行了粘合。所以,爬虫看起来就是这样的一个架构: 所有线程的生命周期都被tomcat控制着。在爬虫阵亡的时候,或者Tomcat OOM跪了的时候,只要我们继承了ServletContextListener这个接口,如下的方法必定执行。 /** ** Notification that the servlet context is about to be shut down. ** All servlets and filters have been destroy()ed before any ** ServletContextListeners are notified of context ** destruction. */ public void contextDestroyed ( ServletContextEvent sce ); 所以,在这边我们就可以加入报警的代码。 然后,对于不同的模块,我们如何进行拼装呢?那就更加方便了,因为Spring的特性,我们可以通过配置bean的方式,让spring去安排模块的启动,具体的配置就是spring的事情了。比如一个最简单的…

第一个任务 — 店铺爬虫

关于这个项目,首先需求比较简单.或者说单一吧.唯一要做的就是将PHP传来的任务给做完.没有多余的爬取,只需要爬取一个页面上的商品描述和商店的描述.唯一好玩的一点就是需要在一周时间内 爬取180万url左右的数据.同时还要考虑对面的防刷设置.分配给的测试资源有两台16核32G的服务器. 很显然.爬虫的关键在于VPS的分配,因为一个ip访问某个网站的频率有限制.在有限的时间,要想爬取更多的网页,就需要多个vps,要多少呢.理论上,服务器的出口带宽为1000Mb,购买的VPS带宽为2Mb, 所以,考虑充分利用带宽,则需要500个.但事实上,我们的需求并没有如此之高.在实际测试中,发现最终的瓶颈在cpu上,频繁的字符串匹配,导致了cpu的飙涨.因为考虑到之后的维护,我用了整页面的 正则进行匹配,这样,之后的修改会方便很多.所以代码里会有如下的正则表达式: 看上去很难维护,但是要想到,之前用doc方式进行解析需要70~80行代码,就释然了.修改解析函数的需求需要对方改变自己的页面展示,到时候,就算是用jsoup写的解析函数,也逃不过重写的事实,这样的 话,再写一个正则可以说要简单很多. 其次是,任务的获取,任务的分发者是PHP,而我的爬虫是消费者,这样的话,需要有一个传输消息的机制.有人会说用socket,简单暴力.但是,如果考虑到今后的水平拓展,则需要使用高大上的MQ,为了简化开发 我们这边选型了Redis的MQ,其实原理很简单,就是在Redis中存放一个列表,然后我顺序读取即可.当然,因为Redis没有提供类似与锁的机制,很可能会造成多爬虫的脏读.这个我们在Redis上又存了一个信号量. 前面也说了,选Redis就是为了更快的开发,所以没有选用RabitMQ,还有一个重要的原因,通过RedisMQ(就这么先叫着吧),PHP端可以读取队列的状况,从而写一个控制界面,这也是一个重要的因素. 说完了选型.之后就是我的爬虫的编写.首先,因为淘宝和美丽说对爬取搜索页有频率限制,所以我抽象了两个搜索类,一个为ShopListTask,ShopDetailsTask,两个的不同就在于,List任务完成后,会主动暂停2秒, 防止被封,而两个网站对店铺详情页则没有限制.两个接口都继承了Callable,方便之后使用线程池. 然后就是一个简单的生产者消费者了.这个不要太简单,一个分发线程,一个接收线程池,搞定了.但是在实际使用中,会发现,对于某些特定的url,正则匹配的效率非常低,而对cpu占用则到了恐怖的境界.两天测试下来 积累的错误造成了cpu占用1589%,同时,线程没有回池,又造成了系统的吞吐量严重退化,两天积累的任务数到了恐怖的8000,(正常情况,对面一分钟的任务,我10秒搞定)!然后就加上了自己之前设计的线程级监控,对 于超时线程进行回收.具体的代码如下: public void dispatchTask(Callable<String> task) { if (taskqueue.size() != 0) { logger.info(“\n目前线程执行状况: \n\t” + “完成的任务数:\t” + workers.getCompletedTaskCount() + “\n\t” + “正在运行的线程数:\t” + workers.getActiveCount() + “\n\t” + “待运行的任务数\t:” + workers.getQueue().size() + “\n\t”); } MonitorTask monitorTask = new MonitorTask(); monitorTask.setTask(task); monitors.submit(monitorTask); } 这是我分发任务的代码,并不是直接将任务提交到workers中,而是用monitors做了一个封装,将真实任务通过MonitorTask加入workers,这段逻辑如下:…