Web2.0的兴起,掀起了互联网新一轮的网络创业大潮。以用户为导向的新网站建设概念,细分了网站功能和用户群,不仅成功的造就了一大批新生的网站,也极大的方便了上网的人们。但Web2.0以用户为导向的理念,使得新生的网站有了新的特点——高并发,高流量,数据量大,逻辑复杂等,对网站建设也提出了新的要求。
本文围绕高并发高流量的网站架构设计问题,主要研究讨论了以下内容:
首先在整个网络的高度讨论了使用镜像网站,CDN内容分发网络等技术对负载均衡带来的便利及各自的优缺点比较。然后在局域网层次对第四层交换技术,包括硬件解决方案F5和软件解决方案LVS,进行了简单的讨论。接下来在单服务器层次,本文着重讨论了单台服务器的Socket优化,硬盘级缓存技术,内存级缓存技术,CPU与IO平衡技术(即以运算为主的程序与以数据读写为主的程序搭配部署),读写分离技术等。在应用层,本文介绍了一些大型网站常用的技术,以及选择使用该技术的理由。最后,在架构的高度讨论了网站扩容,容错等问题。
本文以理论与实践相结合的形式,结合作者实际工作中得到的经验,具有较广泛的适用性。
1 引言
1.1 互联网的发展
最近十年间,互联网已经从一个单纯的用于科研的,用来传递静态文档的美国内部网络,发展成了一个应用于各行各业的,传送着海量多媒体及动态信息的全球网络。从规模上看,互联网在主机数、带宽、上网人数等方面几乎一直保持着指数增长的趋势,2006年7月,互联网上共有主机439,286,364台,WWW 站点数量达到 96,854,877个 [1]。全球上网人口在2004 年达到 7 亿 2900万 [2],中国的上网人数在 2006 年 12 月达到了约 1亿3700 万[3]。另一方面,互联网所传递的内容也发生了巨大的变化,早期互联网以静态、文本的公共信息为主要内容,而目前的互联网则传递着大量的动态、多媒体及人性化的信息,人们不仅可以通过 互联网阅读到动态生成的信息,而且可以通过它使用电子商务、即时通信、网上游戏等交互性很强的服务。因此,可以说互联网已经不再仅仅是一个信息共享网络,而已经成为了一个无所不在的交互式服务的平台。
1.2 互联网网站建设的新趋势
互联网不断扩大的规模,日益增长的用户群,以及web2.0[4]的兴起,对互联网网站建设提出了新的要求:
高性能和高可扩展性。2000 年 5 月,访问量排名世界第一(统计数据来源[5])的Yahoo [6]声称其日页浏览数达到 6 亿 2500 万,即每秒约 30,000 次HTTP 请求(按每个页面浏览平均产生 4 次请求计算) .这样大规模的访问量对服务的性能提出了非常高的要求。更为重要的是, 互联网受众的广泛性,使得成功的互联网服务的访问量增长潜力和速度非常大,因此服务系统必须具有非常好的可扩展性,以应付将来可能的服务增长。
支持高度并发的访问。高度并发的访问对服务的存储与并发能力提出了很高的要求,当前主流的超标量和超流水线处理器能处理的并发请求数是有限的,因为随着并发数的上升,进程调度的开销会很快上升。互联网广域网的本质决定了其访问的延迟时间较长,因此一个请求完成时间也较长,按从请求产生到页面下载完成 3 秒计算, Yahoo 在 2000 年 5 月时平均有 90,000 个并发请求。而且对于较复杂的服务,服务器往往要维护用户会话的信息,例如一个互联网网站如果每天有 100 万次用户会话,每次 20分钟的话,那平均同时就会有约 14000 个并发会话。
高可用性。互联网服务的全球性决定了其每天 24 小时都会有用户访问,因此任何服务的停止都会对用户造成影响。而对于电子商务等应用,暂时的服务中止则意味着客户的永久失去及大量的经济损失,例如ebay.com[7]1999 年 6 月的一次 22小时的网站不可访问,对此网站的 380万用户的忠诚度造成巨大影响,使得 Ebay 公司不得不支付了近500万美元用于补偿客户的损失,而该公司的市值同期下降了 40 亿美元[8]。因此,关键互联网应用的可用性要求非常高。
1.3 新浪播客的简介
以YouTube[9]为代表的微视频分享网站近来方兴未艾,仅2006年一年,国内就出现近百家仿YouTube的微视频分享网站[10],试图复制YouTube的成功模式。此类网站可以说是Web2.0概念下的代表网站,具有Web2.0网站所有典型特征:高并发,高流量,数据量大,逻辑复杂,用户分散等等。新浪[11]作为国内最大的门户网站,在2005年成功运作新浪博客的基础上,于2006年底推出了新浪播客服务。新浪播客作为国内门户网站中第一个微视频分享服务的网站,依靠新浪网站及新浪博客的巨大人气资源,在推出后不到半年的时间内,取得了巨大的成功:同类网站中上传视频数量第一、流量增长最快、用户数最多[12],所有这些成绩的取得的背后,是巨大的硬件投入,良好的架构支撑和灵活的应用层软件设计。
本文是作者在新浪爱问搜索部门实习及参与新浪播客开发的经验和教训的回顾,是作者对一般高并发高流量网站架构的总结和抽象。
2 网络层架构
2.1 镜像网站技术
镜像网站是指将一个完全相同的站点放到几个服务器上,分别有自己的URL,这些服务器上的网站互相称为镜像网站[13]。镜像网站和主站并没有太大差别,或者可以视为主站的拷贝。镜像网站的好处是:如果不能对主站作正常访问(如服务器故障,网络故障或者网速太慢等),仍能通过镜像服务器获得服务。不便之处是:更新网站内容的时候,需要同时更新多个服务器;需要用户记忆超过一个网址,或需要用户选择访问多个镜像网站中的一个,而用户选择的,不一定是最优的。在用户选择的过程中,缺乏必要的可控性。
在互联网发展的初期,互联网上的网站内容很少,而且大都是静态内容,更新频率底。但因为服务器运算能力低,带宽小,网速慢,热门网站的访问压力还是很大。镜像网站技术在这种情况下作为一种有效解决方案,被广泛采用。随着互联网的发展,越来越多的网站使用服务器端脚本动态生成内容,同步更新越来越困难,对可控性要求越来越高,镜像技术因为不能满足这类网站的需要,渐渐的淡出了人们的视线。但有一些大型的软件下载站,因为符合镜像网站的条件——下载的内容是静态的,更新频率较低,对带宽,速度要求又比较高,如国外的SourceForge (http://www.SourceForge.net,著名开源软件托管网站),Fedora(http://fedoraproject.org,RedHat赞助的Linux发行版),国内的华军软件园(http://www.onlinedown.net),天空软件站(http://www.skycn.com)等,还在使用这项技术(图1)。
在网站建设的过程中,可以根据实际情况,将静态内容作一些镜像,以加快访问速度,提升用户体验。
2.2 CDN内容分发网络
CDN的全称是Content Delivery Network,即内容分发网络。其目的是通过在现有的互联网中增加一层新的网络架构,将网站的内容发布到最接近用户的网络“边缘”,使用户可以就近取得所需的内容,分散服务器的压力,解决互联网拥挤的状况,提高用户访问网站的响应速度。从而解决由于网络带宽小、用户访问量大、网点分布不均等原因所造成的用户访问网站响应速度慢的问题[14]。
CDN与镜像网站技术的不同之处在于网站代替用户去选择最优的内容服务器,增强了可控制性。CDN其实是夹在网页浏览者和被访问的服务器中间的一层镜像或者说缓存,浏览者访问时点击的还是服务器原来的URL地址,但是看到的内容其实是对浏览者来说最优的一台镜像服务器上的页面缓存内容。这是通过调整服务器的域名解析来实现的。使用CDN技术的域名解析服务器需要维护一个镜像服务器列表和一份来访IP到镜像服务器的对应表。当一个用户的请求到来的时候,根据用户的IP,查询对应表,得到最优的镜像服务器的IP地址,返回给用户。这里的最优,需要综合考虑服务器的处理能力,带宽,离访问者的距离远近等因素。当某个地方的镜像网站流量过大,带宽消耗过快,或者出现服务器,网络等故障的时候,可以很方便的设置将用户的访问转到另外一个地方(图2)。这样就增强了可控制性。
图2 CDN原理示意图
CDN网络加速技术也有它的局限性。首先,因为内容更新的时候,需要同步更新多台镜像服务器,所以它也只适用于内容更新不太频繁,或者对实时性要求不是很高的网站;其次,DNS解析有缓存,当某一个镜像网站的访问需要转移时,主DNS服务器更改了IP解析结果,但各地的DNS服务器缓存更新会滞后一段时间,这段时间内用户的访问仍然会指向该服务器,可控制性依然有不足。
目前,国内访问量较高的大型网站如新浪、网易等的资讯频道,均使用CDN网络加速技术(图3),虽然网站的访问量巨大,但无论在什么地方访问,速度都会很快。但论坛,邮箱等更新频繁,实时性要求高的频道,则不适合使用这种技术。
图3 新浪网使用ChinaCache CDN服务。
ChinaCache的服务节点全球超过130个,
其中中国节点超过80个,
覆盖全国主要6大网络的主要省份[15]。
2.3 应用层分布式设计
新浪播客为了获得CDN网络加速的优点,又必须避免CDN的不足,在应用层软件设计上,采取了一个替代的办法。新浪播客提供了一个供播放器查询视频文件地址的接口。当用户打开视频播放页面的时候,播放器首先连接查询接口,通过接口获得视频文件所在的最优的镜像服务器地址,然后再到该服务器去下载视频文件。这样,用一次额外的查询获得了全部的控制性,而这次查询的通讯流量非常小,几乎可以忽略不计。CDN中由域名解析获得的灵活性也保留了下来:由接口程序维护镜像网站列表及来访IP到镜像网站的对应表即可。镜像网站中不需要镜像所有的内容,而是只镜像更新速度较慢的视频文件。这是完全可以承受的。
2.4 网络层架构小结
从整个互联网络的高度来看网站架构,努力的方向是明确的:让用户就近取得内容,但又要在速度和可控制性之间作一个平衡。对于更新比较频繁内容,由于难以保持镜像网站之间的同步,则需要使用其他的辅助技术。
3 交换层架构
3.1 第四层交换简介
按照OSI[16]七层模型,第四层是传输层。传输层负责端到端通信,在IP协议栈中是TCP和UDP所在的协议层。TCP和UDP数据包中包含端口号(port number),它们可以唯一区分每个数据包所属的协议和应用程序。接收端计算机的操作系统根据端口号确定所收到的IP包类型,并把它交给合适的高层程序。IP地址和端口号的组合通常称作“插口(Socket)”。
第四层交换的一个简单定义是:它是一种传输功能,它决定传输不仅仅依据MAC地址(第二层网桥)或源/目标IP地址(第三层路由),而且依据IP地址与TCP/UDP (第四层) 应用端口号的组合(Socket)[17]。第四层交换功能就像是虚拟IP,指向实际的服务器。它传输的数据支持多种协议,有HTTP、FTP、NFS、Telnet等。
以HTTP协议为例,在第四层交换中为每个服务器组设立一个虚拟IP(Virtue IP,VIP),每组服务器支持某一个或几个域名。在域名服务器(DNS)中存储服务器组的VIP,而不是某一台服务器的真实地址。
当用户请求页面时,一个带有目标服务器组的VIP连接请求发送给第四层交换机。第四层交换机使用某种选择策略,在组中选取最优的服务器,将数据包中的目标VIP地址用实际服务器的IP地址取代,并将连接请求传给该服务器。第四层交换一般都实现了会话保持功能,即同一会话的所有的包由第四层交换机进行映射后,在用户和同一服务器间进行传输[18]。
第四层交换按实现分类,分为硬件实现和软件实现。
3.2 硬件实现
第四层交换的硬件实现一般都由专业的硬件厂商作为商业解决方案提供。常见的有Alteon[19],F5[20]等。这些产品非常昂贵,但是能够提供非常优秀的性能和很灵活的管理能力。Yahoo中国当初接近2000台服务器使用了三四台Alteon就搞定了[21]。鉴于条件关系,这里不展开讨论。
3.3 软件实现
第四层交换也可以通过软件实现,不过性能比专业硬件稍差,但是满足一定量的压力还是可以达到的,而且软件实现配置起来更灵活。 软件四层交换常用的有Linux上的LVS(Linux Virtual Server),它提供了基于心跳(heart beat)的实时灾难应对解决方案,提高了系统的鲁棒性,同时提供了灵活的VIP配置和管理功能,可以同时满足多种应用需求[22]。
4 服务器优化
4.1 服务器整体性能考虑
对于价值昂贵的服务器来说,怎样配置才能发挥它的最大功效,又不至于影响正常的服务,这是在设计网站架构的时候必须要考虑的。常见的影响服务器的处理速度的因素有:网络连接,硬盘读写,内存空间,CPU速度。如果服务器的某一个部件满负荷运转仍然低于需要,而其他部件仍有能力剩余,我们将之称为性能瓶颈。服务器想要发挥最大的功效,关键的是消除瓶颈,让所有的部件都被充分的利用起来。
4.2 Socket优化
以标准的 GNU/Linux 为例。GNU/Linux 发行版试图对各种部署情况都进行优化,这意味着对具体服务器的执行环境来说,标准的发行版可能并不是最优化的[23]。GNU/Linux 提供了很多可调节的内核参数,可以使用这些参数为服务器进行动态配置,包括影响 Socket 性能的一些重要的选项。这些选项包含在 /proc 虚拟文件系统中。这个文件系统中的每个文件都表示一个或多个参数,它们可以通过 cat 工具进行读取,或使用 echo 命令进行修改。这里仅列出一些影响TCP/IP 栈性能的可调节内核参数[24]:
/proc/sys/net/ipv4/tcp_window_scaling “1”(1表示启用该选项,0表示关闭,下同) 启用 RFC[25] 1323[26] 定义的 window scaling;要支持超过 64KB 的窗口,必须启用该值。
/proc/sys/net/ipv4/tcp_sack “1”启用有选择的应答(Selective Acknowledgment),通过有选择地应答乱序接收到的报文来提高性能(这样可以让发送者只发送丢失的报文段);对于广域网通信来说,这个选项应该启用,但是这也会增加对 CPU 的占用。
/proc/sys/net/ipv4/tcp_timestamps “1” 以一种比重发超时更精确的方法(参阅 RFC 1323)来启用对 RTT 的计算;为了实现更好的性能应该启用这个选项。
/proc/sys/net/ipv4/tcp_mem “24576 32768 49152” 确定 TCP 栈应该如何反映内存使用;每个值的单位都是内存页(通常是 4KB)。第一个值是内存使用的下限。第二个值是内存压力模式开始对缓冲区使用应用压力的上限。第三个值是内存上限。超过这个上限时可以将报文丢弃,从而减少对内存的使用。
/proc/sys/net/ipv4/tcp_wmem “4096 16384 131072” 为自动调优定义每个 socket 使用的内存。第一个值是为 socket 的发送缓冲区分配的最少字节数。第二个值是默认值(该值会被 wmem_default 覆盖),缓冲区在系统负载不重的情况下可以增长到这个值。第三个值是发送缓冲区空间的最大字节数(该值会被 wmem_max 覆盖)。
/proc/sys/net/ipv4/tcp_westwood “1” 启用发送者端的拥塞控制算法,它可以维护对吞吐量的评估,并试图对带宽的整体利用情况进行优化;对于 WAN 通信来说应该启用这个选项。
与其他调优努力一样,最好的方法实际上就是不断进行实验。具体应用程序的行为、处理器的速度以及可用内存的多少都会影响到这些参数对性能作用的效果。在某些情况中,一些认为有益的操作可能恰恰是有害的(反之亦然)。因此,需要逐一试验各个选项,然后检查每个选项的结果,最后得出最适合具体机器的一套参数。
如果重启了 GNU/Linux 系统,设置的内核参数都会恢复成默认值。为了将所设置的值作为这些参数的默认值,可以使用 /etc/rc.local 文件,在系统每次启动时自动将这些参数配置成所需要的值。
在检测每个选项的更改带来的效果的时候,GNU/Linux上有一些非常强大的工具可以使用:
ping 这是用于检查主机的可用性的最常用的工具,也可以用于计算网络带宽延时。
traceroute 打印连接到特定网络主机所经过的一系列路由器和网关的路径(路由),从而确定每个 hop 之间的延时。
netstat 确定有关网络子系统、协议和连接的各种统计信息。
tcpdump 显示一个或多个连接的协议级的报文跟踪信息,其中包括时间信息,可以使用这些信息来研究不同协议的报文时间。
Ethereal 以一个易于使用的图形化界面提供 tcpump (报文跟踪)的信息,支持报文过滤功能。
iperf 测量 TCP 和 UDP 的网络性能;测量最大带宽,并汇报延时和数据报的丢失情况。
4.3 硬盘级缓存
硬盘级别的缓存是指将需要动态生成的内容暂时缓存在硬盘上,在一个可接受的延迟时间范围内,同样的请求不再动态生成,以达到节约系统资源,提高网站承受能力的目的。Linux环境下硬盘级缓存一般使用Squid[27]。
Squid是一个高性能的代理缓存服务器。和一般的代理缓存软件不同,Squid用一个单独的、非模块化的、I/O驱动的进程来处理所有的客户端请求。它接受来自客户端对目标对象的请求并适当地处理这些请求。比如说,用户通过浏览器想下载(即浏览)一个web页面,浏览器请求Squid为它取得这个页面。Squid随之连接到页面所在的原始服务器并向服务器发出取得该页面的请求。取得页面后,Squid再将页面返回给用户端浏览器,并且同时在Squid本地缓存目录里保存一份副本。当下一次有用户需要同一页面时,Squid可以简单地从缓存中读取它的副本,直接返回给用户,而不用再次请求原始服务器。当前的Squid可以处理HTTP, FTP, GOPHER, SSL和WAIS等协议。
Squid默认通过检测HTTP协议头的Expires和 Cache-Control字段来决定缓存的时间。在实际应用中,可以显式的在服务器端脚本中输出HTTP头,也可以通过配置apache的mod_expires模块,让apache自动的给每一个网页加上过期时间。对于静态内容,如图片,视频文件,供下载的软件等,还可以针对文件类型(扩展名),用 Squid 的 refresh_pattern 来指定缓存时间。
Squid 运行的时候,默认会在硬盘上建两层hash目录,用来存储缓存的Object.它还会在内存中建立一个Hash Table,用来记录硬盘中Object分布的情况。如果Squid配置成为一个Squid集群中的一个的话,它还会建立一个 Digest Table(摘要表),用来存储其它 Squid 上的Object摘要。当用户端想要的资料本地硬盘上没有时,可以很快的知道应该去集群中的哪一台机器获得。在硬盘空间快要达到配置限额的时候,可以配置使用某种策略(默认使用LRU:Least Recently Used-最近最少用)删除一些Object,从而腾出空间[28][29]。
集群中的Squid Server 之间可以有两种关系:第一种关系是:Child 和 Parent.当 Child Squid Server 没有资料时,会直接向 Parent Squid Server 要资料,然后一直等,直到 Parent 给它资料为止。 第二种关系是:Sibling 和 Sibling.当 Squid Server 没有资料时,会先向 Sibling 的 Squid Server 要资料,如果 Sibling 没资料,就跳过它向 Parent 要或直接上原始网站去拿。
默认配置的Squid,没有经过任何优化的时候,一般可以达到 50% 的命中率[30](图4)。如果需要,还可以通过参数优化,拆分业务,优化文件系统等办法,使得Squid达到 90% 以上的缓存命中率。 Squid处理TCP连接消耗的服务器资源比真正的HTTP服务器要小的多,当Squid分担了大部分连接,网站的承压能力就大大增强了。
4 某网站使用MRTG工具检测到的Squid命中率
蓝线表示Squid的流量,绿色部分表示Apache流量
4.4 内存级缓存
内存级别的缓存是指将需要动态生成的内容暂时缓存在内存里,在一个可接受的延迟时间范围内,同样的请求不再动态生成,而是直接从内存中读取。Linux环境下内存级缓存Memcached[31]是一个不错的选择。
Memcached是danga.com(运营Live Journal[32]的技术团队)开发的一套非常优秀的分布式内存对象缓存系统,用于在动态系统中减少数据库负载,提升性能。和 Squid 的前端缓存加速不同,它是通过基于内存的对象缓存来减少数据库查询的方式改善网站的性能,而其中最吸引人的一个特性就是支持分布式部署;也就是说可以在一群机器上建立一堆 Memcached 服务,每个服务可以根据具体服务器的硬件配置使用不同大小的内存块,这样,理论上可以建立一个无限大的基于内存的缓存系统。
Memcached 是以守护程序方式运行于一个或多个服务器中,随时接受客户端的连接操作,客户端可以由各种语言编写,目前已知的客户端 API 包括 Perl/PHP/Python/Ruby/Java/C#/C 等等[附录1].客户端首先与 Memcached 服务建立连接,然后存取对象。每个被存取的对象都有一个唯一的标识符 key,存取操作均通过这个 key 进行,保存的时候还可以设置有效期。保存在 Memcached 中的对象实际上是放置在内存中的,而不是在硬盘上。Memcached 进程运行之后,会预申请一块较大的内存空间,自己进行管理,用完之后再申请一块,而不是每次需要的时候去向操作系统申请。Memcached将对象保存在一个巨大的Hash表中,它还使用NewHash算法来管理Hash表,从而获得进一步的性能提升。所以当分配给Memcached的内存足够大的时候,Memcached的时间消耗基本上只是网络Socket连接了[33]。
Memcached也有它的不足。首先它的数据是保存在内存当中的,一旦服务进程重启(进程意外被关掉,机器重启等),数据会全部丢失。其次Memcached以root权限运行,而且Memcached本身没有任何权限管理和认证功能,安全性不足。第一条是Memcached作为内存缓存服务使用无法避免的,当然,如果内存中的数据需要保存,可以采取更改Memcached的源代码,增加定期写入硬盘的功能。对于第二条,我们可以将Memcached服务绑定在内网IP上,通过Linux防火墙进行防护。
4.5 CPU与IO均衡
在一个网站提供的所有功能中,有的功能可能需要消耗大量的服务器端IO资源,像下载,视频播放等,而有的功能则可能需要消耗大量的服务器CPU资源,像视频格式转换,LOG统计等。在一个服务器集群中,当我们发现某些机器上CPU和IO的利用率相差很大的时候,例如CPU负载很高而IO负责很低,我们可以考虑将该服务器上的某些耗CPU资源的进程换成耗IO的进程,以达到均衡的目的。均衡每一台机器的CPU和IO消耗,不仅可以获得更充分的服务器资源利用,而且还能够支持暂时的过载,遇到突发事件,访问流量剧增的时候, 实现得体的性能下降(Graceful performance degradation)[34],而不是立即崩溃。
4.6 读写分离
如果网站的硬盘读写性能是整个网站性能提升的一个瓶颈的话,可以考虑将硬盘的读,写功能分开,分别进行优化。在专门用来写的硬盘上,我们可以在Linux下使用软件RAID-0(磁盘冗余阵列0级)[35]。RAID-0在获得硬盘IO提升的同时,也会增加整个文件系统的故障率——它等于RAID中所有驱动器的故障率之和。如果需要保持或提高硬盘的容错能力,就需要实现软件RAID-1,4或5,它们能在某一个(甚至几个)磁盘驱动器故障之后仍然保持整个文件系统的正常运行[36],但文件读写效率不如RAID-0.而专门用来读的硬盘,则不用如此麻烦,可以使用普通的服务器硬盘,以降低开销。
一般的文件系统,会综合考虑各种大小和格式的文件的读,写效率,因而对特定的文件读或写的效率不是最优。如果有必要,可以通过选择文件系统,以及修改文件系统的配置参数来达到对特定文件的读或写的效率最大化。比如说,如果文件系统中需要存储大量的小文件,则可以使用ReiserFS[37]来替代Linux操作系统默认的ext3系统,因为ReiserFS是基于平衡树的文件系统结构,尤其对于大量文件的巨型文件系统,搜索速度要比使用局部的二分查找法的ext3快。 ReiserFS里的目录是完全动态分配的,因此不存在ext3中常见的无法回收巨型目录占用的磁盘空间的情况。ReiserFS里小文件(< 4K)可以直接存储进树,小文件读取和写入的速度更快,树内节点是按字节对齐的,多个小文件可共享同一个硬盘块,节约大量空间。ext3使用固定大小的块分配策略,也就是说,不到4K的小文件也要占据4K的空间,导致的空间浪费比较严重[38]。 但ReiserFS对很多Linux内核支持的不是很好,包括2.4.3、2.4.9 甚至相对较新的 2.4.16,如果网站想要使用它,就必须要安装与它配合的较好的2.4.18内核——一般管理员都不是很乐意使用太新的内核,因为在它上面运行的软件,都还没有经过大量的实践测试,也许有一些小的bug还没有被发现,但对于服务器来说,再小的bug也是不能接受的。ReiserFS还是一个较为年轻的,发展迅速的文件系统,它相对于ext3来说有一个很大的缺陷就是,每次ReiserFS文件系统升级的时候,必须完全重新格式化整个磁盘分区。所以在选择使用的时候,需要权衡取舍[39]。
5 应用程序层优化
5.1 网站服务器程序的选择
经统计[40],当前互联网上有超过50%的网站主机使用Apache[41]服务器程序。 Apache是开源界的首选Web服务器,因为它的强大和可靠,而且适用于绝大部分的应用场合。但是它的强大有时候却显得笨重,配置文件复杂得让人望而生畏,高并发情况下效率不太高。而轻量级的Web服务器Lighttpd[42]却是后起之秀,基于单进程多路复用技术,其静态文件的响应能力远高于Apache. Lighttpd对PHP的支持也很好,还可以通过Fastcgi方式支持其他的语言,比如Python等。 虽然Lighttpd是轻量级的服务器,功能上不能跟Apache比,某些复杂应用无法胜任,但即使是大部分内容动态生成的网站,仍免不了会有一些静态元素,比如图片、JS脚本、CSS等等,可以考虑将Lighttpd放在Squid的前面,构成 Lighttpd->Squid->Apache的一条处理链,Lighttpd在最前面,专门处理静态内容的请求,把动态内容请求通过Proxy模块转发给Squid,如果Squid中有该请求的内容且没有过期,则直接返回给Lighttpd.新请求或者过期的页面请求交由Apache中的脚本程序来处理。经过Lighttpd和Squid的两级过滤,Apache需要处理的请求大大减少,减少了Web应用程序的压力。同时这样的构架,便于把不同的处理分散到多台计算机上进行,由Lighttpd在前面统一分发。
在这种架构下,每一级都是可以进行单独优化的,比如Lighttpd可以采用异步IO方式,Squid可以启用内存来缓存,Apache可以启用MPM(Multi -Processing Modules,多道处理模块)等,并且每一级都可以使用多台机器来均衡负载,伸缩性好。
著名视频分享网站YouTube就是选择使用Lighttpd作为网站的前台服务器程序。
5.2 数据库选择
MySQL[43]是一个快速的、多线程、多用户和健壮的SQL数据库服务器,支持关键任务、重负载系统的使用,是最受欢迎的开源数据库管理系统,是Linux下网站开发的首选。它由MySQL AB开发、发布和提供支持。
MySQL数据库能为网站提供:
高性能。MySQL支持海量,快速的数据库存储和读取。还可以通过使用64位处理器来获取额外的一些性能,因为MySQL在内部里很多时候都使用64位的整数处理。
易用性。MySQL的核心是一个小而快速的数据库。它的快速连接,快速存取和安全可靠的特性使MySQL非常适合在互联网站上使用。
开放性。MySQL提供多种后台存储引擎的选择,如MyISAM, Heap, InnoDB,Berkeley Db等。缺省格式为MyISAM. MyISAM 存储引擎与磁盘兼容的非常好[44]。
支持企业级应用。MySQL有一个用于记录数据改变的二进制日志。因为它是二进制的,这一日志能够快速地将数据的更改从一台机器复制(replication)到另一台机器上。即使服务器崩溃,这一二进制日志也能够保持完整。这一特性通常被用来搭建数据库集群,以支持更大的流量访问要求[30](图5)。
5 MySQL主辅库模式集群示意
MySQL也有一些它自身的缺陷,如缺乏图形界面,缺乏存储过程, 还不支持触发器,参照完整性,子查询和数据表视图等,但这些功能都在开发者的TO-DO列表当中。这就是开源的力量:你永远可以期待更好。
国外的Yahoo!,国内的新浪,搜狐等很多大型商业网站都使用MySQL 作为后台数据库。对于一般的网站系统,无论从成本还是性能上考虑,MySQL应该是最佳的选择。
5.3 服务器端脚本解析器的选择
目前最常见的服务器端脚本有三种:ASP(Active Server Pages),JSP(Java Server Pages),PHP (Hypertext Preprocessor)[45][46]。
ASP全名Active Server Pages,以及它的升级ASP.NET,是微软公司出品的一个WEB服务器端的开发环境,利用它可以产生和运行动态的、交互的、高性能的WEB服务应用程序。ASP采用脚本语言VBScript(C#)作为自己的开发语言。 但因为只能运行在Windows环境下,这里我们不讨论它。
PHP是一种跨平台的服务器端的嵌入式脚本语言。它大量地借用C,Java和Perl语言的语法, 并耦合PHP自己的特性,使WEB开发者能够快速地写出动态生成页面。它支持目前绝大多数数据库。PHP也是开源的,它的发行遵从GPL开源协议,你可以从 PHP官方站点(http://www.php.net)自由下载到它的二进制安装文件及全部的源代码。如果在Linux平台上与MySQL搭配使用,PHP是最佳的选择。
JSP是Sun公司推出的新一代站点开发语言,是Java语言除Java应用程序和Java Applet之外的第三个应用。Jsp可以在Serverlet和JavaBean的支持下,完成功能强大的站点程序。 作为采用Java技术家族的一部分,以及Java 2(企业版体系结构)的一个组成部分,JSP技术拥有Java技术带来的所有优点,包括优秀的跨平台性,高度可重用的组件设计,健壮性和安全性等,能够支持高度复杂的基于Web的应用。
除了这三种常见的脚本之外,在Linux下我们其实还有很多其他的选择:Python(Google使用),Perl等,如果作为CGI调用,那么可选择范围就更广了。使用这些不太常见的脚本语言的好处是,它们对于某些特殊的应用有别的脚本所不具有的优势;不好的地方是,这些脚本语言在国内使用的人比较少,当碰到技术上的问题的时候,能找到的资料也较少。
5.4 可配置性
在大型网站开发过程中,不管使用什么技术,网站的可配置性是必须的。在网站的后期运营过程中,肯定会有很多的需求变更。如果每一次的需求变更都会导致修改源代码,那么,这个网站的开发可以说是失败的。
首先,也是最重要的一点,功能和展示必须分开。PHP和JSP都支持模板技术,如PHP的Smarty,Phplib,JSP的JSTL(JSP Standard Tag Library)等。核心功能使用脚本语言编写,前台展示使用带特殊标签的HTML,不仅加快了开发速度,而且方便以后的维护和升级[47]。
其次,对于前台模板,一般还需要将页面的头,尾单独提取出来,页面的主体部分也按模块或者功能拆分。对CSS,JS等辅助性的代码,也建议以单独的文件形式存放。这样不仅方便管理,修改,而且还可以在用户访问的时候进行缓存,减少网络流量,减轻服务器压力。
再次,对于核心功能脚本,必须将与服务器相关的配置内容,如数据库连接配置,脚本头文件路径等,与代码分离开。尤其当网站使用集群技术,CDN加速等技术的时候,每一台服务器上的配置可能都会不一样。如果不使用配置文件,则需要同时维护几份不同的代码,很容易出错。
最后,应该尽量做到修改配置文件后能实时生效,避免修改配置文件之后需要重启服务程序的情况。
5.5 封装和中间层思想
在功能块层次,如果使用JSP,基于纯面向对象语言Java的面向对象思想,类似数据库连接,会话管理等基本功能都已经封装成类了。如果使用PHP,则需要在脚本代码中显式的封装,将每一个功能块封装成一个函数,一个文件或者一个类。
在更高的层次,可以将网站分为表示层,逻辑层,持久层,分别进行封装,做到当某一层架构发生变化时,不会影响到其他层。比如新浪播客在一次升级的时候,将持久层的数据库由原来的集中式改为分布式架构,因为封装了数据库连接及所有操作[附录2],做到了不修改任何上层代码,平稳的实现了过渡。近来流行的MVC架构,将整个网站拆分成Model(模型/逻辑)、View(视图/界面)、Controller(控制/流程)三个部分,而且有很多优秀的代码框架可供选择使用, 像JSP的Structs,Spring,PHP的php.MVC, Studs 等。使用现成的代码框架,可以使网站开发事半功倍。
6 扩容、容错处理
6.1 扩容
一个大型网站,在设计架构的时候,必须考虑到以后可能的容量扩充。新浪播客在设计时充分地考虑了这一点。对于视频分享类网站来说,视频存储空间消耗是巨大的。新浪播客在主存储服务器上,采用配置文件形式指定每一个存储盘柜上存储的视频文件的ID范围。当前台服务器需要读取一个视频的时候,首先通过询问主存储服务器上的接口获得该视频所在的盘柜及目录地址,然后再去该盘柜读取实际的视频文件。这样如果需要增加存储用的盘柜,只需要修改配置文件即可,前台程序丝毫不受影响。
新浪播客采用MySQL数据库集群,在逻辑层封装了所有的数据库连接及操作。当数据库存储架构发生改变的时候,如增加一台主库,将某些数据表独立成库,增加读取数据用的从库等,都只需要修改封装了的数据库操作类,上层代码不用修改。
新浪播客的前台页面服务器使用F5公司的硬件第四层交换机,网通,电信分别导向不同的虚拟IP,每一个虚拟IP后面又有多个服务器提供服务。当访问流量增大的时候,可以很方便往虚拟IP后面增加服务器,分担压力。
6.2 容错
对于商业性网站来说,可用性是非常重要的。7*24的访问要求网站具有很强的容错能力。错误包括网络错误,服务器错误以及应用程序错误。
2006年12月27日台湾东部外海发生里氏7.6级地震,造成途径台湾海峡的多条海底电缆中断,导致许多国外网站,像MSN, NBA, Yahoo!(英文主站)等国内无法访问,但也有例外,以Google为代表的在国内建设有分布式数据节点的很多网站却仍然可以访问。虽然说地震造成断网是不可抗原因,但如果在这种情况下网站仍然可以访问,无疑能给网站用户留下深刻的印象。这件事情给大型商业网站留下的教训是:网站需要在用户主要分布区域保持数据存在,以防止可能的网络故障。
对于服务器错误,一般采取冗余设计的方法来避免。对于存储服务器(主要是负责写入的服务器),可以使用RAID(冗余磁盘阵列);对于数据库(主要是负责写入的主库),可以采用双主库设计[30];对于提供服务的前台,则可以使用第四层交换的集群,由多台服务器同时提供服务,不仅分担了流量压力,同时还可以互相作为备份。
在应用层程序中,也要考虑“用户友好”的出错设计。典型例子如HTTP 404 出错页面,程序内部错误处理,错误返回提示等,尽可能的做到人性化。
7 总结及展望
7.1 总结
对于一个高并发高流量的网站来说,任何一个环节的瓶颈都会造成网站性能的下降,影响用户体验,进而造成巨大的经济损失。在全互联网层面,应该使用分布式设计,缩短网站与用户的网络距离,减少主干网上的流量,以及防止在网络意外情况下网站无法访问的问题。在局域网层面,应该使用服务器集群,一方面可以支撑更大的访问量,另一方面也作为冗余备份,防止服务器故障导致的网站无法访问。在单服务器层面,应该配置操作系统,文件系统及应用层软件,均衡各种资源的消耗,消除系统性能瓶颈,充分发挥服务器的潜能。在应用层,可以通过各种缓存来提升程序的效率,减少服务器资源消耗(图6)。另外,还需要合理设计应用层程序,为以后的需求变更,扩容做好准备。
图6 典型高并发高流量网站的架构
在每一个层次,都需要考虑容错的问题,严格消除单点故障,做到无论应用层程序错误,服务器软件错误,服务器硬件错误,还是网络错误,都不影响网站服务。
7.2展望
当前Linux环境下有著名的LAMP(Linux+Apache+MySQL+PHP/PERL/PYTHON)网站建设方案,但只是针对一般的中小网站而言。对于高并发高流量的大型商业网站,还没有一个完整的,性价比高的解决方案。除去服务器,硬盘,带宽等硬件投资外,还需要花费大量的预算和时间精力在软件解决方案上。
随着互联网的持续发展,Web2.0的兴起,在可以预见的未来里,互联网的用户持续增多,提供用户参与的网站不断增加,用户参与的内容日益增长,越来越多的网站的并发量,访问量会达到一个新的高度,这就会促使越来越多的个人,公司以及研究机构来关注高并发高流量的网站架构问题。就像Web1.0成就了无数中小网站,成就了LAMP一样,Web2.0注定也会成就一个新的,高效的,成本较低的解决方案。这个方案应该包括透明的第三方CDN网络加速服务,价格低廉的第四层甚至更高层网络交换设备,优化了网络性能的操作系统,优化了读写性能,分布式,高可靠的文件系统,揉合了内存,硬盘等各个级别缓存的HTTP服务器,更为高效的服务器端脚本解析器,以及封装了大部分细节的应用层设计框架。
技术的进步永无止境。我们期待互联网更为美好的明天。
参考文献
[1]Robert Hobbes' Zakon, Hobbes' Internet Timeline v8.2 , available at http://www.zakon.org/robert/internet/timeline/
[2]GlobalReach Inc., Global Internet Statistics (by language), available at
http://www.glreach.com/globstats/index.php3
[3]中国互联网络信息中心,第十九次中国互联网络发展状况统计报告,available at: http://www.cnnic.net.cn/index/0E/index.htm
[4]Web2.0,Definition available at http://www.wikilib.com/wiki/Web2.0
[5]Alexa Internet, Inc. http://www.alexa.com/
[6]Yahoo! Inc. http://www.yahoo.com/
[7]eBay Inc. 著名的网上拍卖网站,http://www.ebay.com/
[8]Chet Dembeck, Yahoo! Cashes In On eBay's Outage, available at:
http://www.ecommercetimes.com/perl/story/545.html
[9]YouTube, Inc. http://www.youtube.com/
[10]数据来源:互联网周刊,2007年第3期
[11]新浪网技术(中国)有限公司,http://www.sina.com.cn/
[12]数据来源:新浪播客改版公告,available at:
http://games.sina.com.cn/x/n/2007-04-16/1427194553.shtml
[13]邓宏炎, 叶娟丽,网络参考文献初探,武汉大学学报: 人文社会科学版, 2000
[14]彭湘凯,CDN网络及其应用,微计算机信息,2005年02期
[15]数据来源:ChinaCache, http://www.chinacache.com/
[16]Open System Interconnect,开放式系统互联模型,1984年由国际标准化组织(ISO)提出的一个开放式网络互联参考模型,参考 http://www.iso.org/
[17]凌仲权,丁振国,基于第四层交换技术的负载均衡,中国数据通信,2003
[18]陈明锐,邱钊,黄曦,黄俊,智能负载均衡技术在高负荷网站上的应用,广西师范大学学报(自然科学版),2006年04期
[19]Alteon Inc. http://www.alteon.com/
[20]F5 Networks, Inc. http://www.f5.com.cn/
[21]数据来源:http://www.toplee.com/blog/archives/71.html
[22] 傅明,程晓恒,王玮,基于Linux的服务器负载均衡性访问的解决方案,计算机系统应用,2001年09期
[23]Ming-Wei Wu, Ying-Dar Lin, Open source software development: an overview, Computer, 2001 - ieeexplore.ieee.org
[24]王海花 , 杨斌,Linux TCP/IP协议栈的设计及实现特点,云南民族大学学报(自然科学版),2007年01期
[25]Requests for Comments(RFC),the publication vehicle for technical specifications and policy documents produced by the (IETF (Internet Engineering Task Force) , the IAB (Internet Architecture Board), or the IRTF (Internet Research Task Force),http://www.ietf.org/rfc.html
[26]RFC 1323,http://www.ietf.org/rfc/rfc1323.txt?number=1323
[27]Squid web proxy cache team, http://www.squid-cache.org/
[28]马俊昌 , 古志民,网络代理缓存Squid存储系统分析,计算机应用,2003年10期
[29]韩向春,郭婷婷,林星宇,丰保杰,集群缓存系统中代理缓存技术的研究,计算机工程与设计,2006年20期
[30]Brad Fitzpatrick, LiveJournal's Backend,A history of scaling, oscon 2005 ,http://www.danga.com/words/
[31]Danga Interactive, http://www.danga.com/memcached/
[32]LiveJournal,著名的博客托管商(BSP), http://www.livejournal.com/
[33]Brad Fitzpatrick,Distributed caching with memcached,Linux Journal ,Volume 2004,Issue 124,Page 5, August 2004
[34]周枫,面向 Internet 服务的可扩展集群对象 存储及磁盘日志缓存技术研究,清华大学硕士毕业论文,2002
[35]陈赟,杨根科,吴智铭,RAID系统中RAID级别的具体实现算法,微型电脑应用,2003年06期
[36]陈平仲,硬件实现RAID与软件实现RAID的比较,现代计算机(专业版),2005年01期
[37]NAMESYS,http://www.namesys.com/
[38]D Bobbins,Advanced file system implementor s guide: Journalling and ReiserFS,IBM's Developer Works Journal,June,2001
[39]刘章仪,Linux文件系统分析,贵州工业大学学报(自然科学版),2002年04期
[40]数据来源:http://news.netcraft.com/archives/2007/04/02/april_2007_web_server_survey.html
[41]The Apache Software Foundation ,http://httpd.apache.org/
[42]Lighttpd, http://www.lighttpd.net/
[43]MySQL AB,http://www.mysql.com/
[44]顾治华,忽朝俭,MySQL存储引擎与数据库性能,计算机时代,2006年10期
[45]The PHP Group,http://www.php.net/
[46]范云芝,动态网页制作技术ASP、PHP和JSP比较分析,电脑知识与技术(学术交流),2005年10期
[47]王耀希,王丽清,徐永跃,利用模板技术实现B/S 研发过程的分离与并行,计算机应用研究,2004
附 录
[附录1]
1. Memcache的客户端PHP 封装
class memcache_class
{
function memcache_class()
{
}
/**
* 用post方法,执行memcache的写入操作
* $data参数,允许是php的数组。
* exp参数是设定的超时时间,单位是秒。
*/
function p_memcache_write($key, $data, $exp=3600)
{
$mmPageStartTime = microtime();
$ip = MEMCACHE_SERVER_IP;
$port = MEMCACHE_SERVER_PORT;
$type = MEMCACHE_SERVER_TYPE;
//对$data进行序列化,允许$data是数组
$data = serialize($data);
//对$data进行压缩
//$data = gzcompress ($data);
$submit=array( type => $type,
cmd => "set",
key => $key,
data => $data,
exp => $exp
);
$ret = memcache_class::posttohost($query, $submit);
return $ret;
}
/**
* 用post方法,执行memcache的读出操作
*/
function p_memcache_read($key)
{
$mmPageStartTime = microtime();
$ip = MEMCACHE_SERVER_IP;
$port = MEMCACHE_SERVER_PORT;
$type = MEMCACHE_SERVER_TYPE;
$submit=array( type => $type,
cmd => "get",
key => $key
);
$res = memcache_class::posttohost($query, $submit);
//对$res进行解压缩
//$res = gzuncompress($res);
//对$res进行反序列化,允许$res是数组
$res = unserialize($res);
return $res;
}
/**
* 执行post的函数
*/
function posttohost($url, $data)
{
$mmPageStartTime = microtime();
$url = parse_url($url);
$encoded = "";
while (list($k,$v) = each($data))
{
$encoded .= ($encoded ? "&" : "");
$encoded .= rawurlencode($k)."=".rawurlencode($v);
}
for ($i = 0; $i < 3; $i ++)
{
$fp = @fsockopen($url['host'], $url['port'],$errno, $errstr, 1);
if ($fp)
break;
}
if (!$fp)
{
return "";
}
@stream_set_timeout($fp, 2);
@fputs($fp, sprintf("POST %s%s%s HTTP/1.0\n", $url['path'], $url['query'] ? "?" : "", $url['query']));
@fputs($fp, "Host: $url[host]\n");
@fputs($fp, "Content-type: application/x-www-form-urlencoded\n");
@fputs($fp, "Content-length: " . strlen($encoded) . "\n");
@fputs($fp, "Connection: close\n\n");
@fputs($fp, "$encoded\n");
$line = @fgets($fp,1024);
if (!eregi("^HTTP/1\.. 200", $line)) return;
$results = "";
$inheader = 1;
while(!feof($fp))
{
$line = @fgets($fp,1024);
if ($inheader && ($line == "\n" || $line == "\r\n"))
{
$inheader = 0;
}
elseif (!$inheader)
{
$results .= $line;
}
}
@fclose($fp);
return $results;
}
}
2.使用示例
$out="";
if (MEMCACHE_FLAG === true)
{
$memcache_key = md5(trim($key));
$time_before = getmicrotime();
$mdata = memcache_class::p_memcache_read($memcache_key);
$time_after = getmicrotime();
$memcache_read_time = $time_after - $time_before;
if (strlen($mdata) >= MIN_RESULT) {
$out = $mdata;
$memhit = 1;
memcached_log("CACHE_HIT");
}
else {
$memhit = 0;
memcached_log("CACHE_NOT_HIT");
}
}
if (!(strlen($out) >= MIN_RESULT))
{
$query = get_query();
$time_before=getmicrotime();
$out = http_read($MySQLHost,$MySQLPort,$query,&$errstr,10);
$time_after=getmicrotime();
}
$len = strlen($out);
if(MEMCACHE === true && $memhit <= 0)
{
$memcache_key = md5(trim($key));
$time_before = getmicrotime();
memcache_class::p_memcache_write($memcache_key, $out, MEMCACHE_TIME);
$time_after = getmicrotime();
$memcache_write_time = $time_after - $time_before;
memcached_log("CACHE_WRITE");
}
[附录2]
MySQL wrap class
<?php
class mysqlRpc
{
var $_hostWrite = '';
var $_userWrite = '';
var $_passWrite = '';
var $_hostRead = '';
var $_userRead = '';
var $_passRead = '';
var $_dataBase = '';
var $db_write_handle = null;
var $db_read_handle = null;
var $db_last_handle = null;
var $_cacheData = array();
var $mmtime = 60;
function mysqlRpc($database, $w_servername, $w_username, $w_password, $r_servername='', $r_username='', $r_password='') {}
function connect_write_db() {}
function connect_read_db() {}
function query_write($sql, $return = false) {}
function query_read($sql, $return = false) {}
function query_first($sql, $return = false) {}
function insert_id(){}
function affected_rows(){}
function escape_string($string){}
function fetch_array($queryresult, $type = MYSQL_ASSOC){}
}
?>