DELTALOCK 2014-06-13
真正有资格谈Web高性能的,一定是具有丰富实战经验、经过很多次高并发压力考验的架构师。到目前为止我没有这种经验,个人作品访问量几近于零,接外包做的网站访问压力也不大。虽然没有机会披甲上阵,但纸上谈兵总是可以的吧!国内有2本非常不错的Web架构技术书籍,一本是来自阿里巴巴技术专家李智慧的《 大型网站技术架构 》,另一本则是监控宝CTO郭欣写的《 构建高性能Web站点 》。
《大型网站技术架构》所涉及的范围非常广,从5个方面(高性能、高可用、伸缩性、可扩展、安全)谈到了大型网站的架构。高屋建瓴,一目了然,唯一的缺点就是细节不够。而《构建高性能Web站点》则集中精力谈高性能,给出了很多实战操作示例。总的来说,这2本书非常适合搭配着看。
高性能可以分别对Web前端和Web后端来谈,本文是在我草草翻了翻这2本书后,针对如何提高Web后端性能做的一点读书笔记。
要知道Web系统的性能怎么样,首先必须要有评价指标和测试方法。
(1)评价指标
一般有下面几个:
这里稍微提一下CPU Load,它是指一段时间内CPU正在处理以及等待CPU处理的进程数之和的统计信息。如果服务器是8核,若Load=1,表示只有1个任务正在被处理,此时负载很小;Load=8,表示每个核都在处理一个任务,满负载;Load>8,表示需要处理的任务太多,CPU已经超负荷工作啦!可以用top命令查看CPU Load。
(2)测试方法
如何得到上面的指标呢?可以用Apache2附带的工具ab发起测试请求,用法很简单:
ab -n10000 -c10 http://localhost:5000
-n10000
表示总共发起10000次请求, -c10
表示并发用户数为10,也就是说同一时刻有10个用户访问了网站 http://localhost:5000
,一共访问了10000次(假设每个用户访问网站时仅发起1次请求)。执行完毕后就有blabla一大堆数据出来。
引起评价指标波动的关键因素在于 并发数 ,所以一般会在不同并发数下发起测试,得到指标数据。然后以每个评价指标作为y轴,并发数作为x轴进行描点绘图。一般y=f(x)有一个最大值,这个最大值就是系统能够承受的最大并发数了,高于此并发,系统性能会急剧下降,直至崩溃。下面是吞吐量随并发数的变化曲线示例:
Web系统一般是由不同组件构成的,必须着眼不同的组件来考虑性能优化。以最简单的Web架构来说吧,如图:
从左往右看:用户发出request,服务器收到后派发给App(就是指网站程序,一般基于某Web开发框架),App解析request,然后根据业务逻辑可能会访问数据库、读写文件等等,把信息整合为response交给Server,Server再传回用户。
用一个隐喻说明上面的关系:Server就像Boss,负责接活/将产品交付用户;App是干活的码农,但他在干活中可能还需要测试部门、安全部门等的支持。很简单吧!下面针对不同组件来分析性能优化方法。
关键:如何提升网络I/O吞吐率。
(1)并发策略
要想同一时间处理更多请求,自然要考虑并发了。就我的理解来看,并发策略中主要有2个自由度:I/O模型、执行模型。
在这里,I/O模型主要指服务器如何处理网络I/O。《UNIX网络编程》6.2节对此有非常精彩的描述,一般有5种:阻塞、非阻塞、I/O多路复用、信号驱动、异步。前四种都属于同步I/O,就是说用户进程在I/O处理中存在阻塞,而异步I/O则是在底层完成I/O处理后再通过信号机制通知用户进程。
执行模型的话,一般有这些:单进程/线程、多进程/线程(不考虑协程的话)。多进程/线程一般比单进程/线程性能要好,这样可以充分利用多核的优势。
与I/O模型组合起来说就是:到底是用哪一种执行模型,来运行哪一种I/O模型。举一些例子:NodeJS是单线程+异步I/O、Gunicorn是多进程+同步I/O等等。
完全追求I/O性能的话,多线程/进程+异步I/O应该是最牛的吧,比如用 pm2 开启cluster 模式,使用多进程在多核机器上跑NodeJS,性能有多强悍,还没有做过测试...
(2)集群与负载均衡
一个扛不住?那就加更多的服务器一起上吧!还需要使用某种负载均衡技术将网络请求均衡地分配到各个机器上,一般有:HTTP重定向、DNS轮询、反向代理、IP负载均衡、数据链路层负载均衡。均衡算法一般有:轮询、加权轮询、随机、最少连接、源地址散列等。以上这些在《大型网站技术架构》中都有精彩的阐述,就不展开说明了。
值得一提的是,集群和负载均衡是一种非常通用的技术,可以用于很多Web组件中。
(3)持久链接
HTTP是建立在TCP基础之上的,浏览器是一种HTTP软件,它在与服务器通信时首先需要建立TCP连接,这个过程有一定的开销。如果每次请求后都断掉的话,那是不是太浪费资源了?好在现代浏览器和服务器一般都支持长连接(Keep-Alive)技术。服务器在开启Keep-Alive后,会在HTTP应答中加上Connection: Keep-Alive
这个header,然后浏览器就知道需要长时间保持连接不关掉啦!
关键:在接收到request后,如何更快地做出response。
(1)执行速度
不同语言的运行速度会相当不一样,但是运行速度和开发效率是需要相互权衡的。让我用汇编去写Web,打死都不愿意!一般根据业务需求、团队喜好来选择,Java/Python/NodeJS/Go/PHP/Ruby都不错。选定后,可以采用某些措施加速代码的运行,比如使用更好的编译器/解释器、使用C/C++编译的第三方库等等。
(2)算法与数据结构
如果业务逻辑中涉及到一定复杂度的运算,那就可以考虑从算法和数据结构层面进行优化。这个可能对单次request效果不明显,但如果访问量特别大的话,微小的性能提升也会被放大很多倍哦!
(3)I/O模型
你可能比较好奇,不是在Server中已经提了I/O模型了嘛,怎么这里又来一个?请注意,Server中的I/O一般指网络I/O,具体来说是对TCP Socket的读写。而在App代码中也会和各种I/O设备打交道,举例:
内存I/O等待一般是可以忽略的(除非是分布式缓存,或者某块内存忽然坏掉-_-),其他I/O则不容忽视。那么从I/O请求到读写完毕之间,到底该怎么处理?一般的Web开发框架都是同步I/O模型,但是也有采用异步模型的,比如NodeJS、Tornado。在App中采用异步模型,必须采用支持异步调用的库。NodeJS中的File、HTTP、Net、Stream等module都是为Node量身打造的,完全异步。Tornado也可以使用到很多第三方库的 异步版 。值得一提的是,在App层面采用的异步I/O库,一般都是在Server的异步I/O机制的基础上打造的:Node自然不用说了,都是基于Node的事件机制;Tornado的异步库也都是基于 tornado.ioloop
的。
天下没有免费的午餐,由于执行模式是异步的,会给编程和调试带来巨大挑战。我们需要采取额外的措施来提升开发体验,事件机制、Promise等都是NodeJS中比较流行的应对方法。
BTW,在App层面采用异步I/O模型,只能解决I/O性能问题。如果业务逻辑是偏CPU运算密集型,提升效果就不明显啦!
(4)缓存
缓存真的是万金油。哪里卡哪里抹一点,从前端到后端,能抹的地方太多啦。对于App来说,下面是几种典型的应用场景:
一般使用Redis/Memcached来做缓存。
(5)计算外包
为了保证较高的请求吞吐率,业务逻辑代码中不适合出现特别耗时的操作(比如图像处理、群发邮件等),这个时候就可以把这些耗时的计算过程从App中外包出去,交给独立的计算模块完成。计算外包(也就是分布式计算啦)可以有2种策略:异步计算、并行计算。
异步计算 是将计算任务转移出去的一种方法,这种架构中一般会有3种角色:生产者、任务队列、消费者。App在这里充当了生产者的角色,不断地将新的计算任务插入队列中;可以用Redis、MySQL、第三方消息队列(比如RabbitMQ)等存储计算任务;消费者就是真正执行计算的了,一般会开启多个后台worker,不断地从队列中取出任务来执行。
举个例子,GitHub仓库的语言构成的计算是比较耗时的,每一次commit都执行计算是非常划不来的,所以GitHub采用了异步计算,每周以一定频率计算并更新各仓库的语言构成。
之前的异步计算只是把计算任务转移,但并没有减少总体的处理时间。怎么办?能不能把计算任务先分割为小块儿,然后一起算,算好了再汇总呢?这就是 并行计算 的原理啦!这里不得不提一下Google的Map/Reduce分布式并行计算框架。
简单地说,App这边负责拆分计算任务,然后会有多个worker对每一个计算任务都并发调用Map函数来运算,运算结果根据某参照(比如城市编号、班级号)存放在不同的结果集中。所有的Map都运算完毕后,再由worker针对每一个Map结果集都并发调用Reduce函数进行汇总计算,得出最终结果。这篇 文章 还不错,可以说明Map/Reduce的大概原理。
(6)资源复用
之前说过,既然Server可以复用与客户端的TCP连接,那么App能不能复用与各种I/O设备的连接呢?当然可以!比如MySQL、Redis等都可以用线程池的形式存放连接,减少开销。
关键:如何更快地读/写数据。
这里的数据库特指MySQL,在介绍数据库性能优化方法之前,提几个用于性能分析的MySQL命令行工具:
mysqlreport
:第三方MySQL状态报告工具,分析结果一目了然explain
:用于分析SQL语句的执行细节(比如是否用到了索引)mysqlsla
:用于查询哪些SQL操作的耗时超过了预设的阈值,使用此工具之前需要在 my.cnf
开启慢查询日志,即增加 long_query_time=1
和log-slow-queries = /data/var/mysql_slow.log
这2行配置项(1)索引
索引就像是一本书的目录,好的索引可以极大地提升select操作的效率,但会增加delete/update/insert的开销。如果你的数据库的读远多于写,那么索引是非常奏效的。
where A=1 and C=2
是无法使用到 A, B, C
组合索引的(2)冗余设计
数据库的表设计一般遵循所谓的第三范式(3NF),即要求非主键字段之间不能存在依赖关系。但是如果完全按照这样来做的话,SQL语句中会包含大量的join操作。在设计时我们可以保留适量的冗余,比如一个user的blogs数目,可以直接在user表中增加一个blogs_count字段,每次增加/删除blog时就相应地+/-此字段。以后查询起来就快了。
(3)读写分离
单个数据库读写扛不住?那就把读写分离开吧,使用MySQL的主从复制功能,多个从数据库保持与主数据库的同步,然后update/delete/insert全部走主数据库,select则使用负载均衡技术分摊到多个从数据库上。
(4)垂直分区 & 水平分区
读写分离了还是不行,怎么办?可以考虑把相对独立的数据表存放在不同的服务器上,然后每一个都采用读写分离技术,这就是垂直分表,可以进一步将读写压力分摊到更多的服务器上。
如果垂直分表了还不行,那就考虑将单表进一步拆分(分表),然后每n(n>=1)个表部署到独立的服务器上(水平分区)。分表和水平分区的方法大概有:哈希算法、范围分区、映射关系,具体见《构建高性能Web站点》P370页。
(5)NoSQL
如果以上的方法还不奏效,或者虽然奏效但维护成本太高,那就可以考虑抛弃关系数据库,转投NoSQL阵营了。需要注意的是,关系数据库和NoSQL都有各自的适用场景,谁也无法完全取代谁。但NoSQL实在是太多啦,如何选择呢?这里推荐一些比较不错的博文:Robbin的 文章 对NoSQL有一个概览式的分类,草屋主人的NoSQL系列文章 涉及到了各类应用场景,值得一读。
关键:如何更快地写入和读取。
(1)存储介质
换过SSD的同学一般都会体会到操作体验的巨大提升,我所用过的Digital Ocean的主打亮点就是SSD。不过SSD的性价比和稳定性尚待考验,但未来应该是SSD的!
(2)分布式存储
了解了很多眼花缭乱的名词:RAID、HDFS、MongoDB GridFS...但完全不知道它们的关系和脉络到底如何,至于适用场景的话也没怎么看懂TAT,所以就不说了...以后如果有了这方面的实战经验,我想可以单独讨论一下。留几个链接以后再看吧: [1] 、 [2] 、 [3] 、 [4] 。