使用Java开发高性能网站需要关注的那些事儿

目录 JVM, 后端架构, 性能, 数据库-Database, 数据库-MySQL, 缓存


近期各家IT媒体举办的业内技术大会让很多网站都在披露自己的技术内幕与同行们分享,大到facebook,百度,小到刚起步的网站。facebook,百度之类的大型网站采用的技术和超凡的处理能力的确给人耳目一新的感觉,但并不是每个网站都是像facebook,百度 有上亿的用户访问流量,有海量的数据需要存储,需要使用到mapreduce/并行计算,HBase/列存储这些技术不可。技术手段始终是运营的支撑,对于当前的运营环境适用就好,没有必要非要赶个时髦,一定要和某项流行的技术产生点关系才善罢甘休。

在最近的技术大会中我们更多的目光都聚焦在这些大型网站,其实中小型门户网站的技术体系也是值得去探讨和关注。全天下的攻城师们并不是都在为这些大型门户网站服务,更多的攻城师们正在默默无闻的为一些刚刚起步的中小型网站服务,而且占据了攻城师队伍中的60%以上的人群。在关注大型门户网站的时候,中小型门户网站的技术发展和实战经验更值得去分享。

无论大型门户网站还是中小型垂直类型网站都会对稳定性、性能和可伸缩性有所追求。大型网站的技术经验分享值得我们去学习和借用,但落实到更具体的实践上并不是对所有网站可以适用,其他语言开发的网站我还不敢多说,但Java开发的系统,我还是能您给插上几句话:

JVM
JEE容器中运行的JVM参数配置参数的正确使用直接关系到整个系统的性能和处理能力,JVM的调优主要是对内存管理方面的调优,优化的方向分为以下4点:
1.HeapSize             堆的大小,也可以说Java虚拟机使用内存的策略,这点是非常关键的。
2.GarbageCollector  通过配置相关的参数进行Java中的垃圾收集器的4个算法(策略)进行使用。
3.StackSize             栈是JVM的内存指令区,每个线程都有他自己的Stack,Stack的大小限制着线程的数量。
4.DeBug/Log           在JVM中还可以设置对JVM运行时的日志和JVM挂掉后的日志输出,这点非常的关键,根据各类JVM的日志输出才能配置合适的参数。
网上随处可见JVM的配置技巧,但是我还是推荐阅读Sun官方的2篇文章,可以对配置参数的其所依然有一个了解
1.Java HotSpot VM Options
http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html
2.Troubleshooting Guide for Java SE 6 with HotSpot VM http://www.oracle.com/technetwork/java/javase/index-137495.html
另外,我相信不是每个人攻城师都是天天对着这些JVM参数的,如果你忘记了那些关键的参数你可以输入Java -X(大写X)进行提示。

JDBC
针对MySQL的JDBC的参数在之前的文章中也有介绍过,在单台机器或者集群的环境下合理的使用JDBC中的配置参数对操作数据库也有很大的影响。
一些所谓高性能的 Java ORM开源框架也就是打开了很多JDBC中的默认参数:
1.例如:autoReconnect、prepStmtCacheSize、cachePrepStmts、useNewIO、blobSendChunkSize 等,
2.例如集群环境下:roundRobinLoadBalance、failOverReadOnly、autoReconnectForPools、secondsBeforeRetryMaster。
具体内容可以参阅MySQL的JDBC官方使用手册:
http://dev.mysql.com/doc/refman/5.1/zh/connectors.html#cj-jdbc-reference

数据库连接池(DataSource)
应用程序与数据库连接频繁的交互会给系统带来瓶颈和大量的开销会影响到系统的性能,JDBC连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而再不是重新建立一个连接,因此应用程序不需要频繁的与数据库开关连接,并且可以释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。这项技术能明显提高对数据库操作的性能。
在此我认为有一点需要说明:
连接池的使用也是需要关闭,因为在数据库连接池启动的时候就预先和数据库获得了相应的连接,之后不再需要应用程序直接的和数据库打交道,因为应用程序使用数据库连接池是一个“借”的概念,应用程序从数据库连接池中获得资源是“借出”,还需要还回去,就好比有20个水桶放在这里,需要拿水的人都可以使用这些木桶从水池里面拿水,如果20个人都拿完水,不将水桶还回原地,那么后面来的人再需要拿水,只能在旁边等待有人将木桶还回去,之前的人用完后需要放回去,不然后面的人就会一直等待,造成资源堵塞,同理,应用程序获取数据库连接的时候Connection连接对象的时候是从“池”中分配一个数据库连接出去,在使用完毕后,归还这个数据库连接,这样才能保持数据库的连接“有借有还”准则。
参考资料:
http://dev.mysql.com/doc/refman/5.1/zh/connectors.html#cj-connection-pooling

数据存取
数据库服务器的优化和数据的存取,什么类型的数据放在什么地方更好是值得去思考的问题,将来的存储很可能是混用的,Cache,NOSQL,DFS,DataBase 在一个系统中都会有,生活的餐具和平日里穿衣服需要摆放在家里,但是不会用同一种类型的家具存放,貌似没有那个人家把餐具和衣服放在同一个柜子里面的。这就像是系统中不同类型的数据一样,对不同类型的数据需要使用合适的存储环境。文件和图片的存储,首先按照访问的热度分类,或者按照文件的大小。强关系类型并且需要事务支持的采用传统的数据库,弱关系型不需要事务支持的可以考虑NOSQL,海量文件存储可以考虑一下支持网络存储的DFS,至于缓存要看你单个数据存储的大小和读写的比例。
还有一点值得注意就是数据读写分离,无论在DataBase还是NOSQL的环境中大部分都是读大于写,因此在设计时还需考虑 不仅仅需要让数据的读分散在多台机器上,还需要考虑多台机器之间的数据一致性,MySQL的一主多从,在加上MySQL-Proxy或者借用JDBC中的一些参数(roundRobinLoadBalance、failOverReadOnly、autoReconnectForPools、secondsBeforeRetryMaster)对后续应用程序开发,可以将读和写分离,将大量读的压力分散在多台机器上,并且还保证了数据的一致性。

缓存
在宏观上看缓存一般分为2种:本地缓存和分布式缓存
1.本地缓存,对于Java的本地缓存而言就是讲数据放入静态(static)的数据结合中,然后需要用的时候就从静态数据结合中拿出来,对于高并发的环境建议使用 ConcurrentHashMap或者CopyOnWriteArrayList作为本地缓存。缓存的使用更具体点说就是对系统内存的使用,使用多少内存的资源需要有一个适当比例,如果超过适当的使用存储访问,将会适得其反,导致整个系统的运行效率低下。
2. 分布式缓存,一般用于分布式的环境,将每台机器上的缓存进行集中化的存储,并且不仅仅用于缓存的使用范畴,还可以作为分布式系统数据同步/传输的一种手段,一般被使用最多的就是Memcached和Redis。
数据存储在不同的介质上读/写得到的效率是不同的,在系统中如何善用缓存,让你的数据更靠近cpu,下面有一张图你需要永远牢记在心里,来自Google的技术大牛Jeff Dean(Ref)的杰作,如图所示:
cache-speed
并发/多线程
在高并发环境下建议开发者使用JDK中自带的并发包(java.util.concurrent),在JDK1.5以后使用java.util.concurrent下的工具类可以简化多线程开发,在java.util.concurrent的工具中主要分为以下几个主要部分:
1.线程池,线程池的接口(Executor、ExecutorService)与实现类(ThreadPoolExecutor、 ScheduledThreadPoolExecutor),利用jdk自带的线程池框架可以管理任务的排队和安排,并允许受控制的关闭。因为运行一个线程需要消耗系统CPU资源,而创建、结束一个线程也对系统CPU资源有开销,使用线程池不仅仅可以有效的管理多线程的使用,还是可以提高线程的运行效率。
2.本地队列,提供了高效的、可伸缩的、线程安全的非阻塞 FIFO 队列。java.util.concurrent 中的五个实现都支持扩展的 BlockingQueue 接口,该接口定义了 put 和 take 的阻塞版本:LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue、PriorityBlockingQueue 和 DelayQueue。这些不同的类覆盖了生产者-使用者、消息传递、并行任务执行和相关并发设计的大多数常见使用的上下文。
3.同步器,四个类可协助实现常见的专用同步语句。Semaphore 是一个经典的并发工具。CountDownLatch 是一个极其简单但又极其常用的实用工具,用于在保持给定数目的信号、事件或条件前阻塞执行。CyclicBarrier 是一个可重置的多路同步点,在某些并行编程风格中很有用。Exchanger 允许两个线程在 collection 点交换对象,它在多流水线设计中是有用的。
4.并发包 Collection,此包还提供了设计用于多线程上下文中的 Collection 实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。当期望许多线程访问一个给定 collection 时,ConcurrentHashMap 通常优于同步的 HashMap,ConcurrentSkipListMap 通常优于同步的 TreeMap。当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList。

队列
关于队列可以分为:本地的队列 和 分布式队列 2类
本地队列:一般常见的用于非及时性的数据批量写入,可以将获取的数据缓存在一个数组中等达到一定数量的时候在进行批量的一次写入,可以使用BlockingQueue或者List/Map来实现。
相关资料:Sun Java API.
分布式队列:一般作为消息中间件,构建分布式环境下子系统与子系统之间通信的桥梁,JEE环境中使用最多的就是Apache的AvtiveMQ和Sun公司的OpenMQ。
轻量级的MQ中间件之前也向大家介绍过一些例如:Kestrel和Redis(Ref http://www.javabloger.com/article/mq-kestrel-redis-for-java.html),最近又听说LinkedIn的搜索技术团队推出了一个MQ产品-kaukaf(Ref http://sna-projects.com/kafka ),对此保持关注。
相关资料:
1.ActiveMQ http://activemq.apache.org/getting-started.html
2.OpenMQ  http://mq.java.net/about.html
3.Kafka       http://sna-projects.com/kafka      
4.JMS文章  http://www.javabloger.com/article/category/jms

NIO
NIO是在JDK1.4后的版本中出现的,在Java 1.4之前,Jdk提供的都是面向流的I/O系统,例如读/写文件则是一次一个字节地处理数据,一个输入流产生一个字节的数据,一个输出流消费一个字节的数据, 面向流的I/O速度非常慢,并且一个数据包要么整个数据报已经收到,要么还没有。Java NIO非堵塞技术实际是采取Reactor模式,有内容进来会自动通知,不必死等、死循环,大大的提升了系统性能。在现实场景中NIO技术多数运用两个方面,1是文件的读写操作,2是网络上数据流的操作。在NIO中有几个核心对象需要掌握:1选择器(Selector)、2通道(Channel)、3缓冲区(Buffer)。
我的废话:
1.在Java NIO的技术范畴中内存映射文件是一种高效的做法,可以用于缓存中存储的冷/热数据分离,将缓存中的一部分冷数据进行这样的处理,这种做法上比常规的基于流或者基于通道的I/O快的多,通过使文件中的数据出现为内存数组的内容来完成的,将文件中实际读取或者写入的部分才会映射到内存中,并不是将整个文件读到内存中。
2.在Mysql的jdbc驱动中也可以使用NIO技术对数据库进行操作来提升系统的性能。

长连接/Servlet3.0
这里说的长连接就是长轮询,以前浏览器(客户端)需要关注服务器端发生的数据变化需要不断的访问服务器,这样客户端的数量一多必然会给服务器端造成很大的压力,例如:论坛中的站内消息。现在Servlet3.0规范中提供了一个新的特性:异步IO通信;该特性会保持一个长连接。利用Servlet3异步请求的这项技术可以大大的缓解服务器端的压力。
Servlet3.0的原理就是将request的请求开启一个线程挂起,中间设置等待超时的时间,如果后台事件触发request请求,得到的结果返回给客户端的request请求,如果在设置等待超时的时间内没有任何事件发生也将请求返回给客户端,客户端将再次发起request请求,客户端与服务器端的交互可以与此往复。
就好比,你先过来跟我说如果有人找你,我就立马通知你你来见他,原先你需要不断的问我有没有要找你,而不管有没有人找你,你都需要不断的问我有没有人找你,这样的话不论问的人还是被问的人都会累死。

日志
Log4J是通常被人们使用的工具,系统在刚刚上线的时候日志一般都设置在INFO的级,真正上线后一般设置在ERROR级,但无论在任何时候,日志的输入内容都是需要关注的,开发人员一般可以依靠输出的日志查找出现的问题或者依靠输出的日志对系统的性能进行优化,日志也是系统运行状态的报告和排错的依据。
简单来说日志按照定义的不同策略和等级输出到不同的环境,那样便于我们分析和管理。相反你没有策略的输出,那么机器一多,时间一长,会有一大推乱糟糟的日志,会让你排错的时候无从下手,所以日志的输出策略是使用日志的关键点。
参考资料:http://logging.apache.org/log4j/1.2/manual.html

打包/部署
在代码设计的时候最好能将不同类型的功能模块在IDE环境中粗粒度的分为不同的工程,便于打成不同jar包部署在不同的环境中。有这样的一个应用场景:需要每天定时远程从SP那边获得当天100条新闻和部分城市的天气预报,虽然每天的数据量不多,但是前端访问的并发量很大,显然需要在系统架构上做到读写分离。
如果把web工程和定时抓取的功能模块完全集中在一个工程里打包,将导致需要扩展的时候每台机器上既有web应用也有定时器,因为功能模块没有分开,每台机器上都有定时器工作将会造成数据库里面的数据重复。
如果开发的时候就将web和定时器分为2个工程,打包的时候就可以分开部署,10台web对应一台定期器,分解了前端请求的压力,数据的写入也不会重复。
这样做的另一个好处就是可以共用,在上述的场景中web和定时器都需要对数据库进行读取,那么web和定时器的工程里都有操作数据库的代码,在代码的逻辑上还是感觉乱乱的。如果再抽出一个DAL层的jar,web和定时器的应用模块开发者只需要引用DAL层的jar,开发相关的业务逻辑,面向接口编程,无需考虑具体的数据库操作,具体的对数据库操作由其他开发者完成,可以在开发任务分工上很明确,并且互不干涉。

框架
所谓流行的SSH(Struts/Spring/Hiberanet)轻量级框架,对于很多中小型项目而言一点都不轻量级,开发者不仅需要维护代码,还需要维护繁琐的xml配置文件,而且说不定某个配置文件写的不对就让整个都工程无法运行。无配置文件可以取代SSH(struts/Spring/Hiberanet)框架的产品真的太多了,我之前就向大家介绍过一些个产品(Ref)。
这个我并不是一味的反对使用SSH(Struts/Spring/Hiberanet)框架,在我眼里SSH框架真的作用是做到了规范开发,而并不使用了SSH(Struts/Spring/Hiberanet)框架能提高多少性能。
SSH框架只是对于非常大的项目人数上百人的团队,还需要、继续增加团队规模的公司而言,是需要选择一些市面上大家都认可,并且熟悉的技术,SSH(Struts/Spring/Hiberanet)框架比较成熟所以是首先产品。
但是对于一些小团队中间有个把技术高人的团队而言完全可以选择更加简洁的框架,真正的做到提速你的开发效率,早日抛弃SSH框架选择更简洁的技术在小团队开发中是一种比较明知的选择。

1 条评论

  • 德里克
    2016年3月12日

    文章收益匪浅~请问一下,替代SSH框架的那个Ref链接好像失效了~想知道楼主认为替代SSH的好框架,谢谢~

发表评论

电子邮件地址不会被公开。 必填项已用*标注