高性能Web站点优化

简介

根据《构建高性能Web站点(修订版)》一书 加上自己的一些经验和理解写出了这篇博客,并在附录中留下了自己画的脑图。强烈推荐对Web站点优化有兴趣的开发者、web运维、架构师等阅读这本书。书中几乎涵盖了Web站点性能优化的所有内容,包括数据的网络传输、服务器并发处理能力、动态网页缓存、动态网页静态化、应用层数据缓存、分布式缓存、Web服务器缓存、反向代理缓存、脚本解释速度、页面组件分离、浏览器本地缓存、浏览器并发请求、文件的分发、数据库I/O优化、数据库访问、数据库分布式设计、负载均衡、分布式文件系统、性能监控等。

目录

由前端到后端的方式讲解web架构中可以优化的地方

客户端优化

客户端指用户的浏览器 html页面最终由浏览器渲染为用户所看到的界面。常用的浏览器有Chrome、Firefox、IE/Edge、Opera等,通常使用更新版本的浏览器可以获得更高的性能和更好的用户体验。
浏览器一般都向下兼容,而由于历史遗留问题 依旧有大量用户使用较旧版本的浏览器 比如IE8/IE9 甚至还有IE6的用户,各浏览器的兼容性问题 也成了前端开发者要面临的严峻问题。

页面缓存

使用HTTP协议的缓存协议 将资源服务器资源缓存到浏览器本地,用户只需和服务器请求需要更新的数据即可,这样大大提升了页面的加载速度 也降低了服务器的带宽使用。下面看看HTTP协议中有关缓存的内容

Last-Modified

浏览器向服务器发起资源请求,服务器响应用户资源的同时在响应的HTTP头部中添加Last-Modified字段,字段的值是这个资源的最后修改时间。比如Last-Modified: Thu, 15 Feb 2018 08:04:16 GMT

当客户端再次向浏览器请求这个资源时 会在HTTP请求头添加If-Mondified-Since字段,字段的值也是这个资源的最后修改时间。这意味着浏览器向服务器问 我请求的这个资源在此之后还有更新吗?这时候浏览器会检查这个资源在此之后有更新,如果没有更新就会返回304 Not Mondified。并不会返回资源的实体内容,而是告诉浏览器没有更新 可以使用本地缓存的内容。

如果服务器的资源发生改变了 则会重新向客户端发送更新后的资源,或者浏览器使用Ctrl + F5强制从服务器获取资源而无论资源是否有更新。

ETag

服务端用一串编码对资源进行标记,这串编码就是ETag。ETag是服务器来生成的,生成的方式可以自定义,比如可以先计算哈希值 然后将这个哈希值作为ETag。

如果一个文件的Etag没有变化 那这个文件的内容也没有变化。浏览器向服务器请求某个资源,服务器响应这个资源时在相应HTTP头部中添加ETag标签 比如:ETag: 50d0e11a7d7a4d0b5

当浏览器再次请求这个资源 会在HTTP请求头部添加If-None-Match: 50d0e11a7d7a4d0b5来询问服务器这个资源的Etag值还是这个吗?如果没有改变 服务器则响应304 Not Mondified

ETag是在HTTP/1.1中支持的缓存协商方法 主要是为了解决Last-Modified的一些缺点,比负载均衡环境中 很难保证各节点文件时间戳是完全相同的,这样客户端在不同的服务端轮询请求 无论内容是否改变 服务端都会发送全部的内容,这个时候只要使用ETag则可以避免这个问题。

但是ETag的生成会比Last-Modified更消耗CPU资源,而Last-Modified则对HTTP/1.0也有支持,Nginx则会同时返回这两个字段。

Expires

HTTP缓存的存在就是为了消灭不必要的请求 而Last-ModifiedETag明显还是存在请求和响应的,接下来的这两个协议就是完全不会发送HTTP请求的。

Expires告诉浏览器资源什么时候过期,在资源过期期间 浏览器会直接使用已缓存的资源而不会向服务器发起任何询问。

Expires的值是允许资源缓存到什么时候的一个时间戳 比如:Expires: Thu, 15 Feb 2018 08:04:16 GMT,浏览器根据当前时间和资源的过期时间进行对比 如果资源没有过期的话则直接使用资源。如果资源过期 并且存在ETag或者Last-Modified的情况 浏览器则会根据这两个字段询问服务器。

Cache-Control

如果浏览器的时间跟服务器的时间不一致 比如浏览器比服务器时间快了十分钟,那对于缓存五分钟的资源来说 浏览器总是认为资源是过期的,这时候Expires就无法正常工作了。

幸运的是Cache-Control是用来补足Expires的不足,Cache-Control的格式是Cache-Control: max-age=<second>max-age指定了缓存过期的相对本地时间 单位是秒。比如这个资源可以缓存3600秒,那么在3600秒内 浏览器都不会询问资源是否有更新了。

使用Ctrl + F5的方式刷新页面 会忽略缓存协议 所有内容都直接向服务端重新请求
而使用F5或者点击浏览器的刷新按钮,它允许浏览器在请求中附加必要的缓存协议,但是不允许直接使用本地缓存,也就是说 它能让Last-ModifiedETag生效 但是会让ExpiresCache-Control失效

浏览器请求过程

代码优化

代码优化主要在提升用户体验上,这样即使用户网络质量不好的情况下 也经尽可能的提升用户体验。
比如当网络质量不好 页面加载缓慢的情况下 可以提供一个loading的画面给用户 也可以显著的提升用户体验。

HTML的渲染过程

想知道一个页面是怎么出现在用户面前的 就需要了解浏览器是如何渲染html的

浏览器在网络上获取到HTML的相应字节时就开始对HTML进行构建了,但是由于浏览器渲染的一些机制 遇到某些情况浏览器页面就被阻塞了,这也就是用户看到页面卡住而无法操作的的原因,这种情况是非常影响用户体验的。

当遇到以下情况时 浏览器的UI界面会被阻塞

  • HTML的数据流被阻塞在了网络中 或者网络质量太差导致数据包接收过慢
  • 未加载完的脚本或者脚本执行时间过长
  • 遇到了script节点 但是此时还有未加载完的样式文件

css样式文件放到HTML文档中head标签内,js脚本文件最好放到body标签中的最下面,这样可以一定程度的避免js脚本的执行过程中导致浏览器UI阻塞了。

懒加载技术

大家在查看百度图片的时候 会发现不断的滚动页面 就会有更多的图片加载出来,这个就是图片的懒加载技术。

当业务查询出的数据量特别大时,如果全部传输给浏览器 不仅对浏览器 对服务器也会照成很大的压力。
而采用按需加载的方式,用户选择某个单元的时候 再加载这个单元需要的数据,这样既节约了系统相应时间,还提升了系统性能,更提高了用户体验 所以懒加载技术是非常有价值的。

懒加载技术也同样适用于Vue, Angular2SPA框架(single page web application)。通过webpack打包的SPA应用是非常大的,浏览器第一次加载的时候可能会非常耗时,所以可以将不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

使用Ajax交换数据

大部分的HTML页面渲染过程是在服务端完成的,但是通过Ajax技术可以在浏览器页面加载之后异步向服务器发送HTTP请求,获取到动态的数据后由客户端的JavaScript来将数据渲染到HTML页面中。这样后端只提供一套Web接口,数据的渲染部分由前端完成,后端不仅拥有了更高的通用性,也省去了对HTML页面渲染的CPU开销。

加载动画

当不得不存在一些耗时的页面时,使用加载动画 增加页面的趣味性也不失是一个好方法。可以在需要用户等待的页面时提供一个加载动画,然后数据准备完毕后移除动画。有了加载动画的存在 可以降低用户看到一片空白的等待页面由于不耐烦而关闭页面的概率。

传输优化

HTML文档在到达浏览器这段中可以有哪些优化呢

减少数据传输量

浏览器减少http请求

在页面中引用一个js文件和将js代码写入到html文档中 对于页面渲染结果来说是相同的,但是对于页面中引用js文件 浏览器需要向后端发送两次HTTP请求,而HTTP的请求和响应头部数据也是数据流的一部分,如果能减少HTTP的请求数量 也可以提升一部分浏览器渲染速度。

但是将css和js甚至图片都写入到一个html文档 显然是非常降低文档的可读性的,对日后的维护等影响也比较大。但是可以使用webpack等打包工具,将前端页面写完后 利用webpack打包成单个或者多个文件,浏览器只需要加载打包后的文件就可以了。而且webpack在打包过程中还会对代码进行压缩 去掉代码中没必要的空格、换行符,将代码中的变量名用a, b, c等简短的变量代替,打包后的代码大小也会大大减少。

启用Keep-Alive

普通的一次HTTP请求 在服务器响应后就会关闭这次的TCP连接,下一个HTTP请求会继续开启和关闭一个TCP连接。如果能重用这个TCP连接就能省掉TCP连接和关闭的开销了,而Keep-Alive就是为此而生的。

只需要在HTTP头部加入Connection: Keep-Alive就可以启用Keep-Alive了,目前浏览器都支持HTTP/1.1协议,在HTTP/1.1协议中Keep-Alive是默认开启的。

gzip压缩数据

文本类型的数据拥有很大的压缩比,几乎可以达到80%以上 也就是说一个100KB的文档,通过gzip压缩后的大小可能只有20KB左右,在提升网络数据传输上效果是非常显著的。

gzip压缩需要web服务器开启压缩功能,浏览器也需要在请求头部设置接受gzip压缩的字段:Accept-Encoding: gzip, deflate,表明浏览器支持gzip和deflate这两种压缩方式。

服务器接受到请求后判断浏览器是否支持压缩 如果支持就将数据压缩后传输。
浏览器接受到数据后判断是否是已压缩的,如果是压缩的就先解压。

需要注意的是 有些数据类型即使压缩也不会有很大的压缩比的 比如大部分的图片和视频文件,对它们进行压缩不仅浪费宝贵的CPU资源 也获得不了很大的实际效果。所以需要在服务器配置排除掉对这些数据的压缩。

减少Cookie的大小

由于HTTP是无状态的协议,服务器需要知道用户的身份就将用户数据放入到Cookie中,Cookie也是HTTP请求头部的一个字段,Cookie的值的大小浏览器限制不得超过4KB。

浏览器在对某个域发起HTTP请求时,如果存在这个域的Cookie数据,那么就会携带上Cookie进行请求。如果Cookie过大的话 显然也会降低数据的传输效率,而且Cookie是不安全的数据存放方式 除非必须 应该尽量避免掉 换用更安全的Session技术。

CDN加速

CDN的全称是Content Delivery Network,即内容分发网络。CDN系统将数据分发到各CDN节点(其实就是缓存服务器),当用户访问时智能的选择离用户最近的CND节点。

CND对用户访问体验可以带来很大的提升,但是也要面临数据更新同步到各CDN节点再到用户浏览器中有一定的延迟问题。

选择更合适的宽带

总所周知联通的线路和电信的线路互通性并不是太好,BGP机房就是服务器租用商通过技术的手段,实现不同运营商能共同访问一个IP,并且不同运营商之间都能达到最快的接入速度的相关网络技术。BGP机房在一定程度上解决了各用户南北互通的问题,提高了用户的访问速度。

服务端优化

服务端优化指服务端的负载均衡器、Web静态资源服务器、真实后端服务器等程序的优化

应用服务器

这里将Web静态资源服务器和后端服务器都作为应用服务器

程序
多进程

使用多进程来提升服务器的并发能力和提高CPU的利用率。在Linux中和客户端每建立的一个TCP连接都作为一个进程打开的文件描述符,Linux对单个进程默认限制最大打开1024个文件描述符,能处理的Socket文件描述符就更少了。

除了提升Linux对最大文件描述符的限制外 通过多进程同时对外提供服务显然是更合适的方法,可以更高效的利用多核处理器的处理能力。

在Linux上使用ulimit -n可以查看当前最大文件描述符限制,使用ulimit -n 4096可以将最大文件描述符设置为4096。可以通过检索/proc/<PID>/limits文件查看正在运行的进程的最大文件描述符限制

Nginx中 修改worker_processes的值可以配置Nginx启动多少个worker进程 一般建立配置为auto
Apache的prefork模型,就是采用大量的进程来处理用户请求,当用户接入时,fork一个进程去处理。这样的好处是每个用户的请求相对隔离,不会因为一个用户请求处理进程崩溃而影响到其他用户的请求。但是在高并发下 内存的大量消耗和频繁的上下文切换可能成为瓶颈。
php-fpm的fastcgi中 预先初始化多个cgi进程等待web服务器的动态页面请求,这样可以减少进程创建和销毁的开销。

Linux还有一个OOM机制(Out Of Memory killer)和进程的优先级机制(nice值)。

OOM机制会监控那些占用内存过大,尤其是瞬间很快消耗大量内存的进程,为了防止内存耗尽而内核会把该进程杀掉。为了防止重要的进程触发OOM机制而被杀死 可以设置进程/proc/<PID>/oom_adj文件的值为-17来临时关闭对这个进程的监控。但是最好还是不要对这些耗内存大的程序使用 否则照成sshd等重要的进程被杀死就得不偿失了。

优先级呢通过配置进程的nice值来更改进程占用CPU的百分比,nice值得范围是-20到+19,值越低 程序可以获取更多的CPU执行时间。使用nice -n -19 nginx 可以将Nginx进程的nice值设置为-19。Nginx派生的子进程也会继承这个nice值。对于已经存在的进程可以通过renice -n 10 <PID>将该进程的nice值设置为10,但是并不会对该进程的子进程生效。

多线程

和多进程不同的是当客户建立连接后,派生一个线程去处理用户请求。线程的创建和销毁开销是低于进程的,内存使用也会减少一些。比如Apache的worker模型 同时使用了多进程和多线程,所有的线程共享同一个进程内存空间,如果一个线程崩溃 可能会影响到整个进程。

协程

大家可能对进程和线程比较熟悉 而对协程比较陌生。原生支持协程的语言并不多 但是协程非常适合在高并发环境下使用。

协程没有抢占式调度,由代码内主动放弃执行权切换到其他协程执行 因此也不存在上下文切换的开销。协程始终是运行在单线程内 所以也不存在变量共享的锁竞争。

Go语言对协程有良好的支持 所以非常适合高并发的服务端网络编程,Python语言yield关键字提供了对协程的基本支持,Gevent、Tornado等库都对协程有良好的支持。Python3.5之后引入async/await关键字对协程则有了非常好的支持。

优化系统调用

使用strace命令对Nginx的worker进程进行系统调用跟踪,可以分析出用户一次请求共进行了多少次系统调用。比如如果存在日志记录,那一定会存在每处理一条用户请求 都会向日志写入一条记录。每次对日志进行一次write操作 都是一次系统调用,如果关闭日志记录功能 就可以省掉一次日志写入的系统调用。但是对于Web服务器 日志存在的价值显然是高于能节约的响应时间。

使用strace -c -p <PID>可以对已启动的进程进行系统调用统计,使用Ctrl+C结束统计

root@ubuntu:~# strace -c -p 18803
strace: Process 18803 detached
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.000200          50         4           stat
0.00    0.000000           0         1           write
0.00    0.000000           0         1           open
0.00    0.000000           0         2           close
0.00    0.000000           0         1           fstat
0.00    0.000000           0         1           writev
0.00    0.000000           0         1           sendfile
0.00    0.000000           0         2           recvfrom
0.00    0.000000           0         2           setsockopt
0.00    0.000000           0         2           epoll_wait
0.00    0.000000           0         1           epoll_ctl
0.00    0.000000           0         1           accept4
------ ----------- ----------- --------- --------- ----------------
100.00    0.000200                    19           total

这里是Nginx的worker进行一次用户请求处理的系统调用统计,可以看到Nginx使用sendfile调用向用户发送数据。使用sendfile等更高效的大文件传输方式,尽量将操作在内核中完成,减少数据在内核态和用户态的复制。

使用更高效的系统调用方式和减少不必要的系统调用 就是对系统调用的优化了。

尽量避免锁竞争

只要涉及到多进程或多线程的变量共享问题 就会涉及到锁竞争了。只要使用了锁,多进程或多线程就变成了线性的等待锁资源,对于高并发状态 锁竞争会严重降低系统性能。所以在设计后端的时候 开发者需要尽量避免变量共享问题。

更高效的IO模型

Nginx的高并发能力源自它采用了多路IO就绪通知机制,常见的机制有select, poll, epoll等 其中Linux下性能最好的是epoll,跨平台的是select

程序代码跟踪分析

很多语言都有代码性能分析工具,利用这些工具可以分析出一段代码运行各部分的耗时。通过优化这些耗时的代码 可以更好的提升性能和对用户的响应。

可以使用xhprof对PHP的程序进行性能分析,使用cProfile对Python的代码进行性能分析。

缓存

这个缓存是在服务端的缓存

字节码缓存

PHP、Python等动态语言的执行首先需要将源代码编译为字节码,比如PHP的字节码是在Zend虚拟机中运行,Python的字节码是在Python虚拟机中运行。

这些脚本在执行过程中会先将源代码编译为字节码,而直接将字节码缓存起来就可以省掉编译的过程了。比如PHP的XCache。而Python可以通过python -m compileall <PATH>将单个py文件或者目录中全部的py文件编译为.pyc文件(Python3之后是生成在__pycache__目录中)。

页面缓存

后端生成的html页面 也可以先缓存起来 这样就不用重新生成了。

页面缓存又分为三种

  • 静态内容缓存

在Web服务器上将html, css, js等静态页面缓存到内存中或者Memcached等分布式缓存中。总所周知内存的速度是非常快的,对比从内存中取数据,从磁盘上取数据可以说是非常满的了。

如果存在代理服务器,在代理服务器上完成缓存可以更快的返回给用户 这样都不需要经过后端程序了。

  • 动态内容缓存

动态页面的渲染后端可能需要从数据库中取数据 然后将数据渲染到html模板中 最后生成动态页面。对于一些时效性要求不高的页面,可以选择将页面缓存起来,提供一个缓存过期时间,在未过期之前就直接向用户返回缓存的页面。这样可以有效的减少对数据库的读写和CPU的页面渲染。

动态页面缓存也最好配置在代理服务器上。Redis的键值过期机制也非常适合做这种缓存服务器。

  • 局部无缓存

如果一个页面中只有一部分的数据是需要更新的 其他部分的数据都不变 可以采用这种缓存方式。

比如采用SSI技术 只更新页面中动态的内容。但是需要服务器读取shtml模板页面,匹配和修改更新的内容,需要较大的CPU开销。

数据库读写缓存

如果能将数据库的查询结果也缓存起来 就可以避免某些对数据库的查询了。

读缓存

可以将select语句的查询结果缓存到缓存服务器中,当再次遇到相同的sql查询语句时就可以直接从缓存服务器获取结果而不必再次查询数据库了。但是需要注意缓存的生命周期,如果数据已经更新 而缓存还在生效 就会导致页面显示更新不及时。

写缓存

将频繁的数据库写入操作尝试合并为一次写入,比如累计用户登陆次数,先将数据存入memcached中,累计登陆10次后写入数据库中。但是这样想要获取准确的用户登陆次数信息 就要同时获取数据库记录而缓存服务器记录数的相加结果了,而且还要承受缓存服务器意外宕机导致的数据丢失问题。

数据共享

后端服务器群中的数据共享问题,比如用户上传了头像或者照片,因为负载均衡的原因只上传到了一台服务器上,这样下次用户换个设备登陆的时候可能就找不到上传的图片了。这个时候就需要让所有服务器共享一个数据存储。

数据可以通过rsync或者scp进行同步 但是服务器如果比较多 而站点刚好是用户上传数据比较频繁的类型,这两种方式显然就不适合了。还可以采用NFS或者Samba共享等方式,有条件的话可以使用NAS网络存储,或者使用开源的分布式文件系统,比如FastDFSHDFS等。

Web组件分离

随着业务的扩展和服务器规模的扩大,就需要对业务组件进行拆分了,将不同的组件部署在不同的服务器上。

不同组件使用不同的域名

比如百度知道的域名为zhidao.baidu.com,而百度知道中的图片存放在域名为gss0.bdstatic.com的服务器上。不同的域名可以指定不同的IP,不同的业务便可以分散到不同的服务器上了。

另一个好处是可以避开浏览器对同一个域名的并发数限制,将资源分散到不同的域名也增快了页面最终呈现的速度。

数据库连接优化

大多数的后端程序都有数据库连接池的概念,由数据库连接池来维护与数据库的连接,程序只用从数据库连接池中获取和释放连接就可以。程序对数据库连接池的获取和释放连接是非常廉价的,而数据库连接池会保证池内拥有一定量可用的数据库连接。

反向代理和负载均衡

当单台主机无法承受业务访问量的时候 就需要多台服务器同时工作了。而如何将用户请求均衡的分发到各后端服务器 就是负载均衡器的工作了,一般负载均衡器也是反向代理服务器。

动静分离

将网站的静态资源和动态页面分离,一般PHP等后端服务并不擅长处理静态资源,这个时候就需要使用专业的静态资源服务器来处理网站的静态资源了。

比如以高并发著称的Nginx就非常适合做这件事情,由Nginx处理静态资源,动态内容则转发到后端的应用服务器处理。Nginx在这里也就是反向代理服务器了。

负载均衡

通常由负载均衡软件或者专业的负载均衡设备来将用户请求分发到不同的后端服务器。

负载均衡器通常会根据一些算法来对用户请求进行分发,比如常用的轮询加权轮询ip_hash等。

负载均衡器往往还会对后端服务器进行健康检查,剔除意外宕机的后端服务器 以免某个用户刚好访问到宕机的服务器。同时负载均衡器上还要注意用户的登陆状态保持功能。

还可以在负载均衡器上配置ssl 这样就可以免去配置所有的后端服务器而获得一个https的网站了。

负载均衡的方式
Nginx或者HAProxy

四层负载均衡只根据传输层协议内容进行转发,而不关心传输层以上的数据。
七层负载均衡则根据应用层协议内容进行转发,可以对应用层协议进行更精确的控制,比如可以判断用户的访问设备,然后根据设备发送不同的页面。

NginxHAProxy对则四层负载均衡和七层负载均衡都有较好的支持。

HTTP重定向

通过返回302状态码将用户请求按照算法重定向到其他的服务器域名。

优点是比较简单,容易实现,负载均衡器只做请求分发 所以不会是IO瓶颈。
缺点是浏览器需要两次请求服务器才能完成一次访问,性能略差,302状态码还有可能降低搜索引擎的排名。

DNS简单轮询

同一个域名添加多条A记录,用户进行DNS查询时依照轮询算法返回某一个服务器的IP地址。

优点是将负载均衡工作交给了DNS,省去维护负载均衡器的麻烦,同时DNS支持基于地理位置的解析,可以返回给用户最近的服务器地址。
缺点:DNS是多级解析,每一级都可能有缓存,修改了DNS可能不会立即生效,控制权在运营商手里。实际上大型网站经常利用DNS作为第一级的负载均衡。

LVS负载均衡器

LVS负载均衡器是所有软件负载均衡器中性能最高的了 甚至比肩专业的负载均衡器设备。这里就单独拿出来介绍下LVS的三种负载方式。

LVS-NAT

NAT模式 负载均衡工作在网络层 修改用户数据包的目标地址并转发到相应的后端服务器,后端服务器的默认网关应该指向NAT主机,LVS服务器收到回应后再将源地址改为自己的地址。但是此模式中 负载均衡器对所有的流量进行转发 所以随着后端服务器的增加,负载均衡器容易成为流量瓶颈。

LVS-DR

直接路由模式 负载均衡工作在数据链路层,通过修改数据包中的目标MAC地址,将数据包转发到后端服务器。最后数据直接由选中的后端服务器返回给用户,所以后端服务器也必须接入互联网。这时候LVS服务器不会是流量的瓶颈了 但是所有的后端服务器都需要有公网IP,而且必须和负载均衡器在同一个交换机上。

LVS-TUN

IP隧道模式 和直接路由工作原理相似,可以跨机房,服务器之间通过IP隧道连接 拥有直接路由模式模式的优点。但是IP隧道实际上是对数据包的又一层封装,IP隧道数据包的封装和解包都会有一定的开销。

硬件升级

可能的情况下 升级硬件设备对性能的提升是非常巨大的。当然在升级硬件的时候也要考虑主板是否支持这些硬件。

CPU

在选择CPU一般会考虑CPU的主频、核心数、制作工艺以及支持的指令集。

一般来说 CPU的主频越高 性能越强 但是功耗也会越高。核心数越多表示可以同时运行的进程越多,Intel的超线程技术 可以将一个物理核心数模拟为两个逻辑核心数,总性能大约可以提升20%左右。而随着CPU制作工艺的提升 芯片的体积可以越来越小 功耗也会更小。

内存

选购内存一般考虑类型和主频。现在内存已经进入了DDR4时代 但是服务器硬件更新迭代的比较慢 大部分服务器依旧使用DDR3内存,DDR4有着比DDR3更高的主频,更快的速度,和更低的功耗

硬盘
机械硬盘

机械硬盘的单位容量更为廉价,也是服务器采用最多的存储方式。但是机械硬盘属于损耗品 大约工作5年左右就会出现故障,一般企业会选择磁盘阵列技术来对硬盘的数据进行容错和提升性能。服务器硬盘的转速多为10000rpm或者15000rpm 转速越高 硬盘的读写性能越强。

下面看看几种常见的磁盘阵列方式

Raid 1: 至少需要两块硬盘 其中一块作为数据备份,读取性能高,且允许故障一块硬盘,但是空间利用率只有50%。

Raid 5: 性能 数据安全 和存储成本兼顾的方案 至少三块硬盘的话 允许故障一块,空间利用率为硬盘总数减1,因为有差不多一块盘的空间用于存储奇偶校验信息,并散列存储在所有的硬盘上。

Raid 10: 集合Raid0的高性能读写和Raid1的安全 至少需要4块硬盘 最多允许坏两块硬盘 空间利用率也只有50%。

固态硬盘

企业级固态硬盘的成本非常,但是性能也是非常强的。固态硬盘理论上支持无限次的读取,但是对擦写是由次数限制的。程序的运行很大一部分时间耗在了等待IO上,而使用固态硬盘可以大大提升文件的读写速度,程序的执行效率提升可以很明显的增强。通过对比使用固态硬盘的机器和机械硬盘的机器开机速度可以很明显的对比出固态硬盘对系统流畅度的提升。

几种常见的固态硬盘使用的闪存芯片

TLC闪存: 速度慢 寿命短 约500次擦写寿命 稳定性比较差 但是成本低。
MLC闪存: 速度一般 寿命一般 3千到1万次擦写寿命 稳定性适中 成本适中。
SLC闪存: 速度快 寿命长 10万次擦写寿命 稳定性强 企业级闪存 但是成本非常高。

网卡

使用千兆网卡,并使用多网卡做bond,数据链路层的负载均衡,提高网卡可用性和最大带宽。

数据库优化

数据库可以说是Web站点的最后一部分了,后端程序执行很可能很大一部分时间是等待数据库返回需要的数据,提高数据库的查询速度可以很明显的提升Web站点性能。

数据分区

当随着数据的增多 单台数据库实例已经无法承受站点日益增多的数据 就需要考虑对数据进行分区了。

垂直分区

如果数据库写操作频繁 从库则会将大量时间花在复制上 照成性能浪费。这时候就需要将写操作分散到不同的服务器上。最简单的就是将无关的库分散到不同的服务器上,然后不同的数据写入到不同的服务器上。为了方便日后数据库的垂直分区,设计数据库表结构的时候应该尽量减少外键约束和联合查询。

水平分区

当单表的数据了日益增多,也达到了写操作的极限,垂直分区就不适用了。这个时候就可以通过特定的算法将同一数据表的数据写入到不同的数据库中,这就是水平分区了。

比如说将id为奇数的数据放入数据库A,为偶数的放入数据库B。如果需要分散到更多的数据库中,可以对id进行取余计算。需要注意的是 程序也需要知道相应的算法而从正确的数据库取数据。

也可以使用分区反向代理,Spock Proxy可以帮助应用程序实现水平分区的访问调度,我们也不应该在程序中维护分区对应关系。

集群和读写分离

主从复制

多台数据库之间数据同步,查询可以从多台服务器上进行 减少单台服务器的压力。

读写分离

使用数据库反向代理 比如Mysql Proxy对后端数据库进行读写分离和负载平衡。读写分离的原理是让主数据库处理事务性查询,让从库处理数据查询,通过主从复制的方式将主库的数据更新到从库。

性能优化

对数据库软件的性能调优

选择合适的存储引擎

对于MySQL数据库 常用的数据引擎有MyISAMInnoDB,随着Innodb的不断更新改进,从MySQL5.5已经成为了默认存储引擎。

InnoDB: 行级锁 共享表空间 支持事务和外键 写性能比较好 支持热备份。
MyISAM: 表级锁 独立表空间 不支持事务和外键 读性能比较好 不支持热备份。

所以如果网站的写操作比较多适合选择InnoDB,读操作比较多的话适合选择MyISAM

Innodb缓存池大小

InnoDB会在内存中维护一个缓冲池,用于缓存数据和索引。当用户执行查询操作时 如果缓存池中有数据会直接返回结果,否则会从磁盘读取数据,然后放到缓冲池中。配置一个合理的缓存池大小 可以将尽量多的数据缓存到内存中。如果MySQL数据库独占单个服务器,可以将缓存池的大小配置为物理内存的80%。

建立索引

对经常作为查询条件的字段建立索引,这样查询的时候就不需要对表数据进行完整的扫描。但是索引有时候会比整体数据还要大。

减少表锁定等待

MyISAMUPDATE操作会排斥对当前表的所有其他操作,只有更新结束后才可以继续查询。InnoDB支持行级锁,在写操作比较多的情况下性能会好于MyISAM

使用查询缓存

select查询的结果缓存在内存中 当下次遇到相同的select语句将直接。默认情况下 mysql是没有开启查询缓存的,修改配置文件query_cache项开启。

但是需要注意,如果对这个表的数据进行了插入或更改 MySQL将会清除这个表所有的缓存。在对表读写频繁的情况下 查询缓存可能会添乱。但是如果有大量的相同或相似的查询,而且很少修改表中的数据就非常适合了。

缓存线程池

在配置文件中配置thread_cache_size,让MySQL创建的线程可以被重复利用,减少线程的创建和销毁的开销。

表结构设计和查询语句优化

设计合理的表结构和编写高效的查询语句,将相关的数据尽量不要使用多表查询,联合查询等方式。

使用NoSQL

放弃关系型数据库,根据项目选用更合适的非关系型数据库吧。。。

附录