大型网站技术架构核心原理与案例分析 读书笔记
高性能架构
内容导读:
- 常见的性能测试指标和性能测试方法;
- Web前端性能优化主要手段;
- 应用服务器性能优化主要手段;
- 存储性能优化主要手段;
思考:
假设有两个网站高性能架构设计方案:
- A方案,小于100个并发用户访问时,每个请求响应时间是1秒,当并发请求达到200是,请求时间骤增到10秒;
- B方案,不管100还是200并发用户访问,每个请求时间都是1.5秒。
假如采用了A方案,老板不满意希望改进。老板的想法是希望不管100还是200都能达到1秒响应时间。而实际上,架构师经过改进往往最终设计出来的是B方案的效果。这是因为分布式的引入,高并发场景的延迟得到改进,但低并发的延迟却有所恶化。
一、网站性能测试
不同视角
用户、开发人员、运维人员对性能的关注点不同。
- 用户关注的是点击鼠标到最后呈现内容是否快速。使用前端优化手段,浏览器特性、缓存、CDN、反向代理等。
- 开发人员关注响应延迟、系统吞吐量、并发处理能力、系统稳定性等。优化手段:缓存、集群、异步消息、代码优化。
- 运维人员关注基础设施性能和资源利用率,如带宽、服务器配置、数据中心网络架构、服务器和网络带宽的资源利用率等。主要优化手段有建设优化骨干网、使用高性价比定制服务器、利用虚拟化技术优化资源利用等。
性能测试指标
1. 响应时间
响应时间是最重要的性能指标。
常用系统操作需要的响应时间:
操作 | 响应时间 |
---|---|
打开一个网站 | 几秒 |
数据库中查询一条记录(有索引) | 十几毫秒 |
机械磁盘中一次寻址定位 | 4毫秒 |
从机械磁盘顺序读取1MB数据 | 2毫秒 |
从SSD磁盘顺序读取1MB数据 | 0.3毫秒 |
从远程分布式缓存Redis读取一个数据 | 0.5毫秒 |
从内存读取1MB数据 | 十几微秒 |
Java程度本地方法调用 | 几微秒 |
网络传输2KB数据 | 1微秒 |
目标操作耗时很少时,测试程序很难获取到一次操作的响应时间。通常是重复请求,比如重复执行一万次,统计总耗时,然后计算得到单次响应时间。
2. 并发数
与网站并发数相对应的还有在线用户数和网站系统用户数(对多数网站而言就是注册用户数)。
三者的比较关系为:
网站系统用户数 > 在线用户数 > 并发数
测试程序通过多线程模拟并发用户的办法来测试系统的兵法处理能力,为了模拟真实用户行为,测试程序并不是启动多线程然后不停地发送请求,而是在两次请求之间加入一个随机等待时间,这个时间被称作思考时间。
3. 吞吐量
单位时间内系统处理的请求数量,体现系统的整体处理能力。对于网站,可以用“请求数/秒”或“页面数/秒”来衡量,也可以用“访问人数/天”或是“处理的业务数/小时”等来衡量。TPS(每秒事务数)是吞吐量的一个常用量化指标,此外还有HPS(每秒HTTP请求数)、QPS(每秒查询数)等。
4. 性能计数器
System Load、对象与线程数、内存使用、CPU使用、磁盘与网络IO等指标。
System Load即系统负载,指当前正在被CPU执行和等待被CPU执行的进程数目总和,是反映系统忙闲的重要指标。多核CPU的情况下,完美情况是所有CPU都在被使用,没有进程在等待处理,所以Load的理想值是CPU的数目。Load值低于CPU数目的时候,表示CPU有空闲;Load值高于CPU数目时表示进程正在排队等待CPU调度。
Linux中使用top命令查看,该值是3个浮点数,分别表示最近1分钟、10分钟、15分钟的运行队列平均进程数。
性能测试方法
具体细分为性能测试、负载测试、压力测试、稳定性测试。
性能测试曲线中的性能测试、负载测试、压力测试。
响应时间曲线
性能测试报告
性能优化策略
性能分析
检查日志;检查监控数据,分析影响性能的主要因素:内存、磁盘、网路、CPU;代码问题还是架构设计不合理或者系统资源确实不足。
性能优化
- Web前端性能优化
- 应用服务器性能优化
- 存储服务器性能优化
二、Web前端性能优化
Web前端指网站业务逻辑之前的部分,包括浏览器加载、网站视图模型、图片服务、CDN服务等,主要优化手段有优化浏览器访问、使用反向代理、CDN等。
浏览器访问优化
减少http请求
合并CSS、合并Javascript、合并图片。将浏览器一次需要的Javascript、CSS合并成一个文件,这样浏览器就只需要一次请求。
使用浏览器缓存
对一个网站而言,CSS、Javascript、Logo、图标这些静态资源文件的更新频率比较低,但这些文件又是每次HTTP请求都需要的。将这些文件缓存在浏览器中可以极好的改进性能。
设置HTTP头中的Cache-Control和Expires属性可以设定浏览器缓存。
某些时候,静态资源文件变化需要及时应用到客户端浏览器,这种情况可以通过更改文件名实现,不是更改JS文件内容,而是生成新的JS文件并更新HTML中的引用。
使用浏览器缓存策略的网站在更新静态资源时,应采用批量更新的方法。如更新10个图标,应逐个更新并有一定间隔时间,避免大量缓存突然失效、集中更新缓存,造成服务器负载骤增、网络堵塞。
启用压缩
HTML、CSS、Javascript启用GZip压缩。对服务器和浏览器有压力。
CSS放在页面最上方、Javascript放在最下方
减少Cookie传输
减少Cookie中的数据量;静态资源使用独立域名访问,避免请求静态资源时发送Cookie.
CDN加速
反向代理
反向代理服务器也可以实现负载均衡。通常反向代理和负载均衡是一起的。
三、应用服务器性能优化
分布式缓存
网站性能优化第一定律:优先考虑使用缓存优化性能。
缓存的基本原理
缓存存储在高速访问介质中,一方面减少数据访问时间,另一方面缓存的数据是计算过的,可以减少重复计算。
合理使用缓存
- 不适合频繁修改的数据。一般来说,数据的读写比在2:1以上缓存才有意义。实践中这个比例非常高,如热门微博缓存后可能会被读取数百万次。
- 不适合没有热点的数据访问。大部分数据访问没有集中在小部分数据上。
- 数据不一致与脏读
- 缓存雪崩 数据库习惯了缓存的存在,当缓存服务崩溃时,数据库不能承受压力而宕机。有的网站通过缓存热备来提高缓存的可用性,但这违背了缓存的初衷——缓存不应被当做可靠的数据源。通过集群可以在一定程度上提高可用性。
- 缓存预热,在缓存系统启动时就把热点数据加载好。有些元数据如城市地名列表等可以全部加载到缓存进行预热。
- 缓存穿透,因不恰当的业务或恶意攻击,持续高并发地请求某个不存在的数据,所有请求都会落到数据库上。一个简单的对策是,将不存在的数据也缓存起来(value值为null)。
分布式缓存架构
分布式缓存有两种架构,一种是JBoss Cache为代表的许更新同步的分布式缓存,另一种是Memcached为代表的不互相通信的分布式缓存。
JBoss Cache在集群所有服务器中保存相同的数据,某台服务器有数据更新时,会通知其他机器更新或清除缓存数据。JBoss Cache通常将应用程序和缓存部署在同一台服务器上,应用可以快速从本地获取数据,但缓存数量受限于单一服务器内存空间。当集群规模较大时,缓存更新需同步到所有机器,代价很大。因此这种方案多见于企业应用系统中,很少在大型网站使用。
Memcached采用集中式缓存集群管理,也称作互不通信式的分布式架构。缓存与应用分离部署,应用通过一致性Hash等路由算法选择缓存服务器远程访问缓存数据,缓存服务器之间不通信,缓存集群规模可以很容易实现扩容,具有良好的可伸缩性。
Memcached
- 简单的通信协议。使用TCP协议通信,序列化协议则是一套基于文本的自定义协议。
- 丰富的客户端
- 高性能网络通信。Memcached服务端通信模块基于Libevent, 一个支持事件触发的网络通信程序库。
- 高效的内存管理。内存管理中的问题是内存碎片管理。操作系统和虚拟机垃圾回收通过压缩或复制来解决。Memcached采用固定空间分配来解决。
- 互不通信的架构
异步操作
消息队列主要改善网站的扩展性。另外也可以改善系统性能。用户请求发送给消息队列后立即返回,消费者进程从消息队列中获取数据,写入数据库。
数据写入消息队列后立即返回用户,数据在后续的业务校验、写数据库等操作可能失败。因此需要修改业务流程配合。如提交订单后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要消息队列的订单消费者真正处理完订单,甚至商品出库后再通过电子邮件或SMS消息通知用户订单成功。
任何可以晚点做的事情都应该晚点再做。
使用集群
使用负载均衡技术改善性能
代码优化
多线程
大型网站并发用户数会达到数万,单台服务器并发用户也会达到数百。
CGI时代,每个用户请求都会创建一个独立的系统进程来处理。目前的主要的Web应用服务器都采用多线程的方式响应请求。
使用多线程的原因有两个:IO阻塞和多CPU.
不管是Web容器管理的线程还是应用程序自己创建的线程,一台服务器上启动多少线程合适?
一个简单的估算公式:
启动线程数 = [任务执行时间/(任务执行时间 - IO等待时间) x CPU内核数]
如果都是CPU计算任务,则线程数不能超过CPU内核数。相反如果任务需要等待磁盘操作,网络响应,那么多启动线程有助于提高并发度,提高吞吐量。
对网站而言,不管有没有进行多线程编程,工程师写的每一行代码都会被多线程执行,甚至每一个线程都可能被多线程并发访问。
线程安全问题:
- 无状态对象。如Servlet和贫血模型对象。不过从面向对象角度看,无状态对象是不好的。
- 使用局部对象。在方法内部创建对象。
- 使用锁。
资源复用
有些系统资源的创建和销毁开销比较大,如数据库连接、网络通信连接、线程、复杂对象等。
从编程角度,资源复用主要有两种模式:单利和对象池。
单例模式是GoF经典模式中被较多诟病的一个,但由于目前web开发中主要使用贫血模式,Service和Dao都是无状态对象,无需重复创建,使用单例也就自然而然了。Spring默认构造的对象都是单例,是Spring容器管理的单例,而不是单例模式构造的单例。
数据结构
Hash表的读写性能在很大程度上依赖HashCode的随机性。HashCode越随机散列,Hash表的冲突就越少,读写性能越高。
目前比较好的Hash散列算法有Time33算法,即对字符串逐字符迭代乘以33求Hash值。
Time33的缺点在于相似字符串的HashCode也比较接近。一个可行的方案是对字符串取指纹信息,再对指纹信息求HashCode.
垃圾回收
垃圾回收可能会对系统的性能产生巨大影响。
JVM分代垃圾回收机制。
应用程序可用堆空间分为Young Generation和Old Generation. Young Generation又分为Eden Space, From Space和To Space.
- 新建对象总是在Eden中创建,Eden空间已满时触发Young GC, 将还被使用的对象复制到From区。Eden释放。
- Eden再次用完时,又触发Young GC, Eden和From还在使用对象复制到To区
- 再一次触发Young GC时,Eden区和To区还被使用的对象复制到From区。
- 经过多次Young GC, 超过某个阈值还未被释放的对象会被复制到Old Generation.
- 如果Old Generation也用完,则触发Full GC. Full GC会对系统性能产生较大影响,因此应根据系统业务特点和对象生命周期合理设置Young Generation和Old Generation的大小,尽量减少Full GC. 事实上有些Web应用在整个运行期间可以做到从不进行Full GC.
四、存储性能优化
机械硬盘和固态硬盘
机械硬盘具有快速顺序读写,慢速随机读写的访问特性。
B+树和LSM树
为了改善数据访问特性,文件系统和数据库通常会对数据排序后存储。传统关系型数据库的做法是使用B+树。
RAID和HDFS
- RAID0:拆分数据并行写入多块磁盘;无备份
- RAID1:一份数据同时写入两块磁盘
- RAID10:RAID0和RAID1的结合
- RAID3:数据分为N-1份,第N个磁盘做校验。数据修改较多的场景中,第N个磁盘频繁重写校验数据,后果是比其他磁盘更容易损坏。因此RAID3很少使用。
- RAID5:比RAID3更多使用。与RAID3不同,校验数据不是写入第N个盘,而是螺旋写入所有盘。
- RAID6:与RAID5类似,但数据只写入N-2块磁盘,并螺旋在两块磁盘中写入校验信息。
性能对比图
RAID技术可以通过硬件实现,也可以通过软件实现。RAID技术在传统关系型数据库和文件系统中广泛使用,但大型网站比较喜欢用的NoSQL和分布式文件系统中,RAID不常用。
HDFS