缓存主要用来存放那些读写比很高、很少变化的数据,如商品的类目信息,热门词 的搜索列表信息,热门商品信息等。应用程序读取数据时,先到缓存中读取,如果读取 不到或数据已失效,再访问数据库,并将数据写入缓存,如图4.8所示。
大型 站需要缓存的数据量一般都很庞大,可能会需要数TB的内存做缓存,这时候
就需要另一种分布式缓存,如图4.10所不。Memcached采用一种集中式的缓存集群管理, 也被称作互不通信的分布式架构方式。缓存与应用分离部署,缓存系统部署在一组专门 的服务器上,应用程序通过一致性Hash等路由算法选择缓存服务器远程访问缓存数据, 缓存服务器之间不通信,缓存集群的规模可以很容易地实现扩容,具有良好的可伸缩性。
- Memcached
Memcached曾一度是 站分布式缓存的代名词,被大量 站使用。其简单的设计、优异的性能、互不通信的服务器集群、海量数据可伸缩的架构令 站架构师们趋之若鹜。
在不使用消息队列的情况下,用户的请求数据直接写入数据库,在高并发的情况下,
会对数据库造成巨大的压力,同时也使得响应延迟加剧。在使用消息队列后,用户请求 的数据发送给消息队列后立即返回,再由消息队列的消费者进程(通常情况下,该进程 通常独立部署在专门的服务器集群上)从消息队列中获取数据,异步写入数据库。由于 消息队列服务器处理速度远快于数据库(消息队列服务器也比数据库具有更好的伸缩 性),因此用户的响应延迟可得到有效改善。
消息队列具有很好的削峰作用一即通过异步处理,将短时间高并发产生的事务消 息存储在消息队列中,从而削平高峰期的并发事务。在电子商务 站促销活动中,合理 使用消息队列,可有效抵御促销活动刚开始大量涌入的订单对系统造成的冲击。如图4.14所示。
三台Web服务器共同处理来自用户浏览器的访问请求,这样每台Web服务器需要处 理的http请求只有总并发请求数的三分之一,根据性能测试曲线,使服务器的并发请求 数目控制在最佳运行区间,获得最佳的访问请求延迟。
4代码优化
站的业务逻辑实现代码主要部署在应用服务器上,需要处理复杂的并发事务。合 理优化业务代码,可以很好地改善 站性能。不同编程语言的代码优化手段有很多,这 里我们概要地关注比较重要的几个方面。
- 多线程
多用户并发访问是 站的基本需求,大型 站的并发用户数会达到数万,单台服务
器的并发用户也会达到数百。CGI编程时代,每个用户请求都会创建一个独立的系统进程 去处理。由于线程比进程更轻量,更少占有系统资源,切换代价更小,所以目前主要的
Web应用服务器都采用多线程的方式响应并发用户请求,因此 站开发天然就是多线程
编程。
从资源利用的角度看,使用多线程的原因主要有两个:10阻塞与多CPUo当前线程 进行10处理的时候,会被阻塞释放CPU以等待10操作完成,由于10操作(不管是磁 盘IO还是 络IO)通常都需要较长的时间,这时CPU可以调度其他的线程进行处理。
前面我们提到,理想的系统Load是既没有进程(线程)等待也没有CPU空闲,利用多 线程10阻塞与执行交替进行,可最大限度地利用CPU资源。使用多线程的另一个原因是服务器有多个CPU,在这个连手机都有四核CPU的时代,除了最低配置的虚拟机,一 般数据中心的服务器至少16核CPU,要想最大限度地使用这些CPU,必须启动多线程。
站的应用程序一般都被Web服务器容器管理,用户请求的多线程也通常被Web服 务器容器管理,但不管是Web容器管理的线程,还是应用程序自己创建的线程,一台服 务器上启动多少线程合适呢设服务器上执行的都是相同类型任务,针对该类任务启 动的线程数有个简化的估算公式可供参考:
启动线程数=[任务执行时间/ (任务执行时间-IO等待时间)]xCPU内核数
最佳启动线程数和CPU内核数量成正比,和IO阻塞时间成反比。如果任务都是CPU
计算型任务,那么线程数最多不超过CPU内核数,因为启动再多线程,CPU也来不及调 度;相反如果是任务需要等待磁盘操作, 络响应,那么多启动线程有助于提高任务并 发度,提高系统吞吐能力,改善系统性能。
多线程编程一个需要注意的问题是线程安全问题,即多线程并发对某个资源进行修 改,导致数据混乱。这也是缺乏经验的 站工程师最容易犯错的地方,而线程安全Bug 又难以测试和重现, 站故障中,许多所谓偶然发生的“灵异事件”都和多线程并发问 题有关。对 站而言,不管有没有进行多线程编程,工程师写的每一行代码都会被多线 程执行,因为用户请求是并发提交的,也就是说,所有的资源——对象、内存、文件、 数据库,乃至另一个线程都可能被多线程并发访问。
编程上,解决线程安全的主要手段有如下几点。
将对象设计为无状态对象:所谓无状态对象是指对象本身不存储状态信息(对象无 成员变量,或者成员变量也是无状态对象),这样多线程并发访问的时候就不会岀现状态 不一致,Java Web开发中常用的Servlet对象就设计为无状态对象,可以被应用服务器多 线程并发调用处理用户请求。而Web开发中常用的贫血模型对象都是些无状态对象。不 过从面向对象设计的角度看,无状态对象是一种不良设计。
使用局部对象:即在方法内部创建对象,这些对象会被每个进入该方法的线程创建, 除非程序有意识地将这些对象传递给其他线程,否则不会出现对象被多线程并发访问的情形。
并发访问资源时使用锁:即多线程访问资源的时候,通过锁的方式使多线程并发操作转为顺序操作,从而避免资源被并发修改。随着操作系统和编程语言的进步,出现各种轻量级锁,使得运行期线程获取锁和释放锁的代价都变得更小,但是锁导致线程同 步顺序执行,可能会对系统性能产生严重影响。
- 资源复用
系统运行时,要尽量减少那些开销很大的系统资源的创建和销毁,比如数据库连接、 络通信连接、线程、复杂对象等。从编程角度,资源复用主要有两种模式:单例 (Singleton )和对象池(Object Pool )。
单例虽然是GoF经典设计模式中较多被诟病的一个模式,但由于目前Web开发中主 要使用贫血模式,从Service到Dao都是些无状态对象,无需重复创建,使用单例模式也 就自然而然了。事实上,Java开发常用的对象容器Spring默认构造的对象都是单例(需 要注意的是Spring的单例是Spring容器管理的单例,而不是用单例模式构造的单例)。
对象池模式通过复用对象实例,减少对象创建和资源消耗。对于数据库连接对象,
每次创建连接,数据库服务端都需要创建专门的资源以应对,因此频繁创建关闭数据库 连接,对数据库服务器而言是灾难性的,同时频繁创建关闭连接也需要花费较长的时间。因此在实践中,应用程序的数据库连接基本都使用连接池(Connection Pool)的方式。数据库连接对象创建好以后,将连接对象放入对象池容器中,应用程序要连接的时候,就 从对象池中获取一个空闲的连接使用,使用完毕再将该对象归还到对象池中即可,不需 要创建新的连接。
前面说过,对于每个Web请求(HTTP Request), Web应用服务器都需要创建一个独 立的线程去处理,这方面,应用服务器也采用线程池(Thread Pool)的方式。这些所谓的 连接池、线程池,本质上都是对象池,即连接、线程都是对象,池管理方式也基本相同。
- 数据结构
早期关于程序的一个定义是,程序就是数据结构+算法,数据结构对于编程的重要性
不言而喻。在不同场景中合理使用恰当的数据结构,灵活组合各种数据结构改善数据读 写和计算特性可极大优化程序的性能。
前面缓存部分已经描述过Hash表的基本原理,Hash表的读写性能在很大程度上依赖 HashCode的随机性,即HashCode越随机散列,Hash表的冲突就越少,读写性能也就越
高,目前比较好的字符串Hash散列算法有Time33算法,即对字符串逐字符迭代乘以33, 求得Hash值,算法原型为:
hash (i) = hash (i-1) * 33 + str [i]
Time33虽然可以较好地解决冲突,但是有可能相似字符串的HashCode也比较接近, 如字符串“AA”的HashCode是2210,字符串“AB”的HashCode是2211。这在某些应 用场景是不能接受的,这种情况下,一个可行的方案是对字符串取信息指纹,再对信息 指纹求HashCode,由于字符串微小的变化就可以引起信息指纹的巨大不同,因此可以获得较好的随机散列,如图4.16所示。
在JVM分代垃圾回收机制中,将应用程序可用的堆空间分为年轻代(Young Generation )和年老代(Old Generation ),又将年轻代分为 Eden 区(Eden Space )、From 区和To区,新建对象总是在Eden区中被创建,当Eden区空间已满,就触发一次YoungGC ( Garbage Collection,垃圾回收),将还被使用的对象复制到From区,这样整个Eden区都是未被使用的空间,可供继续创建对象,当Eden区再次用完,再触发一次Young GC, 将Eden区和From区还在被使用的对象复制到To区,下一次Young GC则是将Eden区和To区还被使用的对象复制到From区。因此,经过多次Young GC,某些对象会在From区和To区多次复制,如果超过某个阈值对象还未被释放,则将该对象复制到OldGenerationo如果0Id Generation空间也已用完,那么就会触发Full GC,即所谓的全量回收,全量回收会对系统性能产生较大影响,因此应根据系统业务特点和对象生命周期,合理设置Young Generation和Old Generation大小,尽量减少Full GCO事实上,某些Web 应用在整个运行期间可以做到从不进行Full GC。
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!