I wrote before a guide Howto install Nginx/PHP-FPM on Fedora 16/15, CentOS/RHEL 6.1/6/5.7, but this guide is just installation guide and many cases Nginx and PHP-FPM basic configuration is good enough, but if you want to squeeze all the juice out of your VPS or web server / servers and do your maintenance work little bit easier, then this guide might be useful. These tips are based entirely on my own experience, so they may not be an absolute truth, and in some situations, a completely different configuration may work better. It’s also good to remember leave resources for another services also if you run example, MySQL, PostgreSQL, MongoDB, Mail server, Name server and/or SSH server on same machine.
And yes here we go…

Nginx Configuration and Optimizing Tips and Tricks


http://www.if-not-true-then-false.com/2011/nginx-and-php-fpm-configuration-and-optimizing-tips-and-tricks/

[转]知名网站的技术实现

[不指定 2012/06/14 14:15 | by ipaddr ]

网站需要具有良好的可伸缩性,来应对不断增长的访问量和数据量。《程序员》杂志5月刊的《可伸缩性的10年探索:知名网站的技术发展历程》一文中介绍了一些Alexa排名较前的网站的技术发展历程,本文将结合提及的Google、Facebook、Twitter等网站的技术发展历程,总结它们在可伸缩性、可用性、高性能以及低成本四点上通常采用的技术。

可伸缩

可伸缩分为垂直伸缩和水平伸缩两类,垂直伸缩通过升级机器的硬件来解决问题,水平伸缩通过增加机器来解决问题。不同网站在可伸缩上采用了不同的策略。例如,Google完全依赖水平伸缩来解决问题,而其他网站多数是依赖垂直伸缩来解决数据存储问题。

垂直伸缩要求软件要能在硬件升级时,发挥出硬件的能力,例如可配置的并行数、内存等。硬件的发展速度非常迅猛,网站的机器配置自然也是每年都在升级,因此软件时刻都在被检验是否能垂直伸缩。

水平伸缩主要解决的是如何仅通过增加机器就能解决问题,一般通过应用层和存储层两个层次来解决。

在应用层做到水平伸缩,通常采用的策略是Share Nothing/Stateless,将状态信息放入客户端或存储层(例如用户的会话信息放入Cookie,或放入服务器端的缓存系统)。此时,在访问量上涨后增加机器即可。在应用系统由单台增加到多台组成集群时,需要引入负载均衡,可能是硬件的也可能是软件的。

我们发现,就应用层的结构演变而言,前面提到的Google、Facebook、Twitter等网站在应用层上最后形成的结构几乎完全一样,均为前端Web系统集群 + Services集群。通常到了一定阶段后,前端Web系统和Services又会按照一定的规则来进行拆分,如按业务领域等。可见,其实SOA在大网站中已经落地,而不像企业领域中宣传的那么虚。

存储层实现水平伸缩,难度就比应用层大多了。从Google、Facebook、Twitter等几家网站的发展历程也可看出,解决存储层问题需要花费大量的时间和精力,而且由于业务发展的压力较大,很多网站开始会采用垂直伸缩方式来解决存储层的问题。

实现存储层的可伸缩性,通常采用的方案是单点写(指在某个粒度的单点,对HBase而言,单行数据一定在同一台机器上进行写操作),但读可能是多点。读多点,主要需要考虑不一致的问题。无论读是单点还是多点,数据都会在软件层面做到写多份,策略主要有同步写多份、投票写多份、异步复制最终一致等(例如HDFS、Cassandra、ZooKeeper),采用自动分裂方法来实现数据量增长时的自动伸缩,采用一致性Hash或根据某种规则的自动均衡策略等来实现机器增减时的相应处理,同时也需要有感知机器增减的方法(例如采用ZooKeeper)。

可以看出,要在存储层上实现可伸缩,技术上的难点很多。这也是为什么大规模网站都不在数据库上做复杂的运算,而只是把其当做一种存储信息的方式来使用。存储过程等基本不会出现,以降低数据库的压力。

除了应用层和存储层需要做到可伸缩外,在设计系统时硬件层面的可伸缩也是需要考虑的。例如,硬件负载设备只能支撑到一定流量,同样也要考虑如何让其能够可伸缩。

除了尽可能做到系统可伸缩外,减少对系统的压力也是缓解系统的可伸缩所面临挑战的方法,例如批量提交、最终一致、YouTube提到的“欺骗”、适当的正确性等策略。

可伸缩性是网站从小到大要经历的第一关挑战,同时随着网站的不断发展,还要不断地做技术改造才能保证网站在每个阶段(如同机房阶段、同城多机房阶段、异地多机房阶段)都具备优秀的可伸缩性。不过,幸运的是不同阶段的可伸缩性方案都已经比较成熟了。

可用性

可伸缩性能保证网站在访问量和数据量不断增长的情况下生存下来,同时也从某种程度上保证了网站的可用性。但除此之外,要保障系统的可用性,还需付出很多努力。

设计高可用性系统时,避免单点是其中最重要的一点,一般采用主备或集群等方法来避免单点。例如,负载均衡设备、MySQL等通常采用主备方式。在BigTable里采取了一种比较特殊的方法来避免Tablet Server的单点,即通过Chubby来感知Tablet Server的健康状况,如发现Tablet Server挂掉了,则由Master将此Tablet Server负责的Tablet迁移到其他的Tablet Server上。

除了避免单点外,降低耦合也是设计高可用性系统时应考虑的重点。通常采取异步化来降低耦合,将非关键逻辑和关键逻辑隔离开。例如,打开天猫的商品详情时,都会有相关商品的推荐。如果这个推荐系统出问题的话,就会导致商品详情也看不到了,因此这里可以异步载推荐系统,通常采用AJAX带超时的方式来实现。

Google、Facebook等网站在总结多年的可用性系统设计经验时,列入了同一条设计原则,即保持简单。简单的方案一般比较容易掌控,而复杂的方案一方面实现难度大,另一方面出现问题时很难排查。

可控性也是各网站强调的重点,可控性意味着一切代码都在掌控之下,并且最好是每个使用到的部分都由专业人员负责(对可用性要求越高,在这点上要求也就越高)。这样在出现问题时才能清楚是哪个地方造成的,并且可以自行排查和解决。如不可控,则意味着一旦出现问题,就得依赖第三方来排查,时间上非常不可控。这也是网站不愿意采用商用软件的重要原因。

编写高质量软件是保障系统高可用性的重要一环,可控性是其基础。Google、Facebook等网站在总结保障高可用的经验中均提到了监控/报警、容错、自我保护等策略。

监控/报警是软件自身能够保障高可用的重要策略,就像是汽车的仪表盘一样,可以告诉你油还剩多少、速度是多少、胎压是否正常等重要信息。对于软件而言,同样需要让外部获取到其运行的状况。例如,Google的软件都会提供一个HTML页面供使用者或开发人员访问(用过Hadoop的人也会发现这个特征)。在这个页面上可通过key/value的方式来获取系统的一些运行指标。对于RPC系统而言,Google会采集所有的正常请求、错误请求以及其消耗时间的分布状况(>0.05s、>0.1s等)。除了监控系统的运行状况外,也提供了其他一些方式以便外部能简单判断系统运行是否正常,例如cURL某页面等。对于不正常的现象,要及时报警,尽可能做到在故障尚未影响到用户时解决掉。

软件的正常运行需要依赖很多外部因素,例如机房、硬件、数据库、服务等,而所有依赖的部分都有可能出现故障(要坚信这点,互联网的特色是所有小概率事件都会发生)。在设计软件时需要考虑当依赖方出问题时,如何保障软件的可用性,因此一定要做一些容错处理。为了避免机房故障,各网站通常会租用或建设多机房。例如,Google采用IDE硬盘来存储文件,不做RAID,于是采用复制三份的策略来避免硬盘故障导致数据丢失。

软件一般会提供多种功能,有的重要、有的不重要的。而由不重要的功能异常导致重要的功能出现问题,显然不合算,因此在设计软件时需要充分考虑异常隔离,不互相影响。例如在Google的系统设计中会采用Prioritized Request等策略。

James Hamilton的那篇著名的论文《On Designing and Deploying Internet-Scale Services》中提到的Graceful Degradation,即为优雅降级。通常采用的方法是在故障将要出现或出现后,关闭系统的某些功能来降低故障产生的影响。例如,网站上有些操作可能特别耗资源,而这些资源的消耗又可能影响到核心功能,因此一旦出现影响,就可以关闭这些功能保障核心功能可用。降级可以帮助系统临时绕开故障,而产生故障的原因需要在事后排查。

交付具有高可用特征的软件是开发人员的重要职责。而对一个网站而言,软件不是一次性交付的,也不是好几年才升级一次,需要频繁交付,因此维护软件是保障高可用的重要环节。

多数情况下,系统的不可用是由变更造成的,因此如何降低变更对系统可用性造成的影响成为了各网站都关注的重点。Google在发布新产品时通常采取“滚木移石”方法,而Facebook则通常采用Dark Launch方法,降低变更带来的影响。

人工处理系统变更是故障产生的隐患,因此各网站基本都会推荐多种工具来实现系统变更的自动化,例如采用Puppet来实现自动化部署。

除了发布这个重要环节外,处理故障也是维护方面的重要工作。系统总有出现故障时,如何快速处理故障以降低对故障可用性的影响也成为网站一直关注的重点。前面已经讲述了可控性对解决故障的帮助。除可控性外,各网站也在研究其他方法。Facebook采用FBAR来自动处理部分故障,这显然可从某种程度上降低故障产生的影响。

性能

前面提到的可控性同样也是保证性能的重点,只有明确知道调用的每个API及所依赖环境(包括软硬件)的细节原理,才能编写出高性能软件。

从系统结构上来看,为了保障高性能,各大网站都采用了类似的方法。首先是前端Web系统这块,都采用了可编译为机器码的方式,即便Facebook采用的是PHP,其仍然研发了一个可自动转化为C++代码的产品来提升运行效率。

设计系统时,应考虑将没有前后依赖的逻辑并行化处理,或者将大的请求进行拆分。例如,Google会先将所搜索的内容进行分词,然后并行进行索引查询,从而提高响应速度。

基本上各大网站都极度依赖缓存,原因在于,内存的访问速度远快于磁盘。在依赖缓存的场景中,最需要做到的是数据一致性的合理保障。一个典型的场景是数据更新时保障缓存一致性的策略。要将缓存的更新和数据的更新做成一个事务显然有不小的难度,此时网站常采用一个简单的策略来保障,就是先失效缓存,再更新数据,等到下次系统去访问此数据时,才更新到内存。

除了缓存外,可以看到各大网站都采用了CDN,CDN可以让静态文件更靠近用户,便于用户快速获取。而除了让静态文件更靠近用户外,多IDC的建设除了提升可用性外,还可以让动态数据更靠近用户。当然,这在技术上的实现难度会比CDN高很多。

除了结构上的这几点外,技术创新是提升系统性能的重要方法。例如,Google提高了TCP的初始拥塞窗口值等。而要做到技术创新,显然可控性是基础。

成本

随着网站规模的不断扩大,系统的运行和维护成本将会成为公司中支出的重要部分。例如,有数据表明,腾讯每年支付给运营商的费用在总支出中占比排行第二(2010年为208亿)。网站规模越大,成本控制就越重要(潜台词是在网站规模不是很大时,也许支撑业务快速发展更重要)。例如,性价比是Google设计系统时重要考量的指标。有些网站会采用x元/每千次PV来计算成本。

有些性能优化需要增加成本(如缓存和CDN),而有些性能优化是可以降低成本的(如Google对索引结构的优化),因此性能优化通常也是降低成本的一种方法。网站规模大了以后,规模效应可以让有些性能优化带来的成本降低非常明显。

硬件不断升级,而软件系统层面上又更多的是靠集群来支撑,因此通常很难完全消耗硬件资源,虚拟化就成为一种不错的降低成本的方法。虚拟化具备很好的隔离机制,避免了应用间的互相影响,因此落地的难度不大。

除了依靠虚拟化来降低成本之外,Google采用了自行实现的一种Shared Environment方法来降低成本。Shared Environment可根据不同类型的资源消耗,动态组合(例如分时)部署到同一台机器上,充分利用资源。

如前所述,网站主要是靠可伸缩性存活下来的,因此随着规模的扩大,必然会有大量的机器。比如,Google有上百万台机器,Facebook有几十万台机器。在这么大规模下,自行根据应用特征设计机器,会带来很大的成本下降,因此Google、Facebook都自行设计机器和数据中心。从PUE上可以看出,Google、Facebook自行设计数据中心带来的成本降低非常可观。

各网站的情况不同,应对以上四点挑战所采用的方法不同,每个阶段都有自己适用的解决方案。例如,Google成立初期的主要业务是搜索,主要竞争的是技术,功能次之,而Facebook、eBay等网站的竞争压力主要在业务功能上,因此在成立之初必然会有不同的侧重点。不用想着一开始就把网站做成Google、Facebook等现在的结构,适合自己的就是好的。

很多开发人员在加入规模较大的网站后,会觉得系统结构已经稳定了,没有发展的空间,但从上面各网站的发展历程来看,可以看出网站对技术的要求是在不断演变的。通过观察大网站的发展历程,并结合公司的业务背景、知识结构等来判断其下一步的发展,对个人成长是有很大帮助的。同时可以借此储备一些技术,以把握技术演变时的机会,获得更大的成长。如果开发人员加入了规模尚小的网站,且自身技术储备不错的话,就有机会亲身经历网站从小到大的演变了。但这要看个人如何把握。

图1 发展到一定规模后的网站结构

围绕可伸缩性、可用性、性能和成本这四个方向,在网站发展到一定规模后,通常会演变成如图1所示的结构。

除了在可伸缩性、可用性、性能和成本这四方面的技术挑战外,网站还面临其他很多方面的挑战,例如海量数据分析和挖掘、网站安全、业务发展的灵活性支撑、人员增长后庞大的软件管理等,因此构建一个支撑大访问量、长期发展、低成本运行的网站是需要有坚实的技术背景作为支撑的。

作者林昊,目前就职于淘宝,2007-2010年负责设计和实现淘宝的服务框架,此服务框架在淘宝大面积使用,每天承担了150亿+的请求;2011年开始负责HBase在淘宝的落地,目前淘宝已有20个以上的在线项目在使用HBase。

升级RPM包的Spec文件调研

[不指定 2012/06/13 11:04 | by ipaddr ]

当在用户机器上安装或卸载程序时,能够执行命令将是很有用的。例如,可能需要编辑一个系统配置文件以启用新的服务,或者需要定义一个新用户以拥有正在安装的程序的所有权。

安装和卸载脚本的工作原理看起来很简单,但它们工作原理中的一些意外可能会引起大问题。这里是一些基本信息,可以将下列四节中的任意一个添加到.spec 文件,它列出了在包安装期间各个点上运行的shell 脚本:

%pre 在安装包之前运行

%post 在安装包之后运行

%preun 在卸载包之前运行

%postun 在卸载包之后运行

尤其要注意%install与这些节之间的差异。构建RPM 时,%install 在开发机器上运行;它应该将产品安装在开发机器上或安装到一个构建根目录中。另一方面,这些节指定当用户正在安装或卸载RPM包时将在用户的机器上运行什么。

一种好的技术是使用%pre脚本来检查安装前提条件,它们比RPM可以直接支持的更复杂。 如果不符合前提条件,那么脚本以非零状态退出,而且 RPM 不会继续安装。另外请注意,我们必须小心地使用卸载脚本来撤销安装脚本。

然而实际上没有那么简单:升级使每件事情都变得复杂,现在,让我们着手升级。如果用户只安装和删除自己的包,那么前面的指令将正常工作;但在升级期间,它们会完全失效。以下是 RPM 如何执行升级:

运行新包的 %pre

安装新文件

运行新包的 %post

运行旧包的 %preun

删除新文件未覆盖的所有旧文件

运行旧包的 %postun

如果我们使用5.3.2系列中现有SPEC文件中的脚本来升级,那么RPM最后将运行 %postun 脚本,它将除去我们在安装脚本中所做的所有工作。

rpm为了解决此问题,在其英文文档中提到了可以向脚本来传递一个参数$1,这个参数传递的过程是隐藏的,你只需在%pre%post%preun%postun中使用$1即可($1shell中就是第一个参数的意思)。这个参数的含义是在执行完此次操作后系统中此软件包的剩余数量是多少,就目前我的理解应该只有012三种可能。

1.在执行rpm –ivh的安装过程中,如果有同类包存在,则会报错提示无法安装,存在相同的文件。如果没有同类包存在则会执行安装动作,过程如下:

运行新包的%pre $1=1

安装新文件

运行新包的%post $1=1

2.在执行rpm –U的升级过程中,如果没有同类低级包存在,则过程和传递的参数与安装时完全相同,如果有同类低级包存在则会执行升级操作,过程如下:

运行新包的%pre $1=2

安装新文件

运行新包的%post $1=2

运行旧包的%preun $1=1

删除新文件未覆盖的任何旧文件

运行旧包的%postun $1=1

3.在执行rpm –e的删除过程如下:

运行旧包的%preun $1=0

删除文件

运行旧包的%postun $1=0

因此我们可以用传递的参数来判断rpm究竟在进行什么工作,来在脚本内部通过$1进行判断来决定进行什么动作。例如在参数为0的时候才真的执行卸载所要进行的动作。

另外在升级的时候,除了注意几个脚本的执行顺序与结果外,还要注意配置文件的处理是否正确,RPM还有一项重要的工作要做,这就是妥善处理配置文件(CONFIG FILE)。若直接采用安装方式,则用户已配置好的配置文件就会被覆盖,不符合用户要求。

RPM对某个配置文件,通过比较三种不同的MD5检查和(checksum)来决定如何处理它。这三种不同的MD5检查和是:

1. 原检查和。它是旧版本软件包安装时配置文件的MD5检查和。

2. 当前检查和。它是升级时旧版本配置文件的MD5检查和。

3. 新检查和。它是新版本软件包中配置文件的MD5检查和。

RPM针对以下几种情况分别处理:

1. 当原检查和=X,当前检查和=X,新检查和=X:

这表明配置文件未曾修改过。此时,RPM会将新的配置文件覆盖掉原文件,而不是对原文件不作处理,原因在于: 虽然文件名和文件内容都没有变化,但文件别的方面的属性(如文件的属主,属组,权限等)却可能改变,所以有必要覆盖一下。

2. 当原检查和=X,当前检查和=X,新检查和=Y:
这表明原配置文件没有改动过,但是它与新软件包中的配置文件却有所不同。这种情况下,RPM将用新文件覆盖掉旧文件,并且旧文件不作保存(因为它不曾改动过,没有必要保存)

3. 当原检查和=X,当前检查和=Y,新检查和=X:

这表明新文件与旧文件内容相同,但当前文件已经作过修改,这些修改对于新版本来说应该是合法的,可以使用的。因此,RPM对当前文件予以保留。

4. 当原检查和=X,当前检查和=Y,新检查和=Y:

这表明原文件经过修改,现在已与新文件相同,这或许是用户用来修补安全上的漏洞,新版本也作了同样的修改。这种情况下,RPM将新文件覆盖当前文件,避免文件属性方面的不同。

5. 当原检查和=X,当前检查和=Y,新检查和=Z:

这表明用户已修改了原文件,并且当前内容与新文件内容不同。这种情况下,RPM无法保证新版本软件能正常使用当前的配置文件,所以采用了一个比较明智的办 法,既能保护用户的配置数据,又能保证新版本软件正常。这种作法就是将当前文件换名保存(给原文件名加个.rpmsave的后缀,如原文件名为ABC,则 换名后为ABC.rpmsave),同时安装新文件,并给出警告信息。

6. 当没有原检查和时:

此种情况下,当前检查和与新检查和已无关紧要,这表明没有安装过此配置文件。因为没有安装过此配置文件,所以RPM无法判断当前文件是否被用户修改过。这种情况下,RPM会将当前文件换名保存(原文件名后缀不是加个.rpmsave,而是.rpmorig),同时安装新文件,并给出警告信息。

因此在%files中可以用%config字段将配置文件标识出来,这样在升级的过程中配置文件将被按照上面所描述的方法处理。

%config 文件路径的形式来添加。

Jpegoptim是一个google建议的很好的JPG,JPEG图像压缩工具,目前支持系统有Linux,Solaris,Darwin/OSX

软件安装需求:

Independent JPEG Group’s jpeg library version 6a or later

安装方法:

下载附件jpegoptim-1.2.3.tar.gz,当然官方地址:http://freshmeat.net/projects/jpegoptim/

tar zxvf jpegoptim-1.2.3.tar.gz
cd jpegoptim-1.2.3
./configure
make
make strip
make install

(依赖于libjpeg-devel包)

使用方法:


jpegoptim --strip-com --strip-exif --strip-iptc tomzhou.jpg

更多用法,可使用jpegoptim --help查看

注:使用 jpegoptim 最常见的问题是,颜色会失真,比如浅蓝色总会变成淡紫色。解决这个的办法是,不要清除掉图片的 ICC 标记(实际上,图片的ICC标记也不大,清除它获得的尺寸减小很有限),即,不要使用 --strip-all ,也不要使用 --strip-icc。


在线代码高亮 CodeMirror

[不指定 2012/05/29 20:17 | by ipaddr ]

又一款“Online Source  Editor”,基于Javascript,短小精悍,实时在线代码高亮显示,他不是某个富文本编辑器的附属产品,他是许多大名鼎鼎的在线代码编辑器的基础库。

本站的在线LESS编译器Markdown编辑器就是采用这个组件开发。

可以看出,CodeMirror的作者是一个十分向往自由的人。但他的CodeMirror绝对不简单,看看下面这份清单:

上述的这些在线代码编辑器都是基于CodeMirror的,是不是感到惊讶,里面有你熟悉的JS Library。

CodeMirror本身的定位也很明确,短小精悍,但代码质量很高,在Google  Group的群里面,人们热烈的进行着用CodeMirror做各式各样改造的讨论,可见对他的欢迎。以下有各种不同语言的Demo演示:

假如你有项目需要在线代码编辑,还等什么?CodeMirror,绝对是你最好的选择。

为了尊重作者对自由的向往,请在使用前认真阅读以下License,并严格尊重作者的声明:

https://github.com/marijnh/CodeMirror2/blob/master/LICENSE


最近发现一个奇怪的问题,Chrome在Ajax请求某个cgi时失败,直接请求的话,返回正常。IE,FF下工作正常。

环境是前端是nginx做反向代理,请求后端Apache,Nginx开启了Gzip。

经过抓包发现:
1.  JS中使用HEAD方法请求了 www.eit.name/time.html来获取服务器时间
2.  Js异步加载 www.eit.name/cgi-bin/test/view来获取数据
3.  Nginx在处理time.html时,返回了 HTTP Header + Body(upstream返回的空Body 并进行gzip)
4.  Chrome在收到HTTP Header后,根据HTTP协议标准,认定HEAD请求后没有Body,所以直接在同一个HTTP连接里面,发起了/cgi-bin/test/view的请求
5.  Chrome将 time.html gzip body + view body做为view的body进行解析, 解析失败,所以请求view失败。
6.  IE,FF也是同样的请求逻辑,但在第5步时进行了容错处理,所以工作正常。

解决此问题的方法,需要针对HEAD方法特别配置:

if ( $request_method = HEAD ) {
    return 200;
}

[转]杂谈Nginx与HTTP协议

[不指定 2012/05/10 09:54 | by ipaddr ]

在项目中遇到一个问题,需要详细了解下HTTP协议及其Nginx中对HTTP协议的支持程度。今天一天收集了一些资料,也梳理出最终方案。记录到博客上,方便后续查阅。重点关注以下几个方面:1、Http交互中如何判定内容的长度及其HTTP协议中关于Content-Length的解读。2、Chunk和Gzip在Nginx中的实现及原理。3、Upstream如何和Chunked结合。

Http协议中关于Content-Length的解读

在HTTP协议中,有Content-Length的详细解读。Content-Length用于描述HTTP消息实体的传输长度the transfer-length of the message-body。在HTTP协议中,消息实体长度和消息实体的传输长度是有区别,比如说gzip压缩下,消息实体长度是压缩前的长度,消息实体的传输长度是gzip压缩后的长度。

在具体的HTTP交互中,客户端是如何获取消息长度的呢,主要基于以下几个规则:

  • 响应为1xx,204,304相应或者head请求,则直接忽视掉消息实体内容。
  • 如果有Transfer-Encoding,则优先采用Transfer-Encoding里面的方法来找到对应的长度。比如说Chunked模式。
  • “如果head中有Content-Length,那么这个Content-Length既表示实体长度,又表示传输长度。如果实体长度和传输长度不相等(比如说设置了Transfer-Encoding),那么则不能设置Content-Length。如果设置了Transfer-Encoding,那么Content-Length将被忽视”。这句话翻译的优点饶,其实关键就一点:有了Transfer-Encoding,则不能有Content-Length。
  • Range传输。不关注,没详细看了:)
  • 通过服务器关闭连接能确定消息的传输长度。(请求端不能通过关闭连接来指明请求消息体的结束,因为这样可以让服务器没有机会继续给予响应)。这种情况主要对应为短连接,即非keep-alive模式。
  • HTTP1.1必须支持chunk模式。因为当不确定消息长度的时候,可以通过chunk机制来处理这种情况。
  • 在包含消息内容的header中,如果有content-length字段,那么该字段对应的值必须完全和消息主题里面的长度匹配。
    “The entity-length of a message is the length of the message-body before any transfer-codings have been applied”
    也就是有chunk就不能有content-length 。

其实后面几条几乎可以忽视,简单总结后如下:

1、Content-Length如果存在并且有效的话,则必须和消息内容的传输长度完全一致。(经过测试,如果过短则会截断,过长则会导致超时。)

2、如果存在Transfer-Encoding(重点是chunked),则在header中不能有Content-Length,有也会被忽视。

3、如果采用短连接,则直接可以通过服务器关闭连接来确定消息的传输长度。(这个很容易懂)

结合HTTP协议其他的特点,比如说Http1.1之前的不支持keep alive。那么可以得出以下结论:

1、在Http 1.0及之前版本中,content-length字段可有可无。

2、在http1.1及之后版本。如果是keep alive,则content-length和chunk必然是二选一。若是非keep alive,则和http1.0一样。content-length可有可无。

Nginx在Http协议方面的处理

第一、Nginx的chunk模块

Nginx的Chunk模块是一个典型的Filter模块,它本身是内置必选的Nginx模块。在0.7.66版本之后,有一个配置项chunked_transfer_encoding可以开启或者关闭chunk模式,默认是开启的。

首先,先简单了解下在HTTP协议中Chunked相关的知识点。Chunked一种transfer coding方式,在HTTP1.0之前(包含http1.0)的版本是不支持的。在HTTP协议中定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Chunked-Body   = *chunk
                 last-chunk
                 trailer
                 CRLF
 
chunk          = chunk-size [ chunk-extension ] CRLF
                 chunk-data CRLF
chunk-size     = 1*HEX
last-chunk     = 1*("0") [ chunk-extension ] CRLF
 
chunk-extension= *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
chunk-ext-name = token
chunk-ext-val  = token | quoted-string
chunk-data     = chunk-size(OCTET)
trailer        = *(entity-header CRLF)

其中可以看到,每一个chunk都是可以大小自描述的。

在Nginx的Chunked模块中,header filter函数的流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
//如果没有content或者是head请求,则直接跳过。
if (r->headers_out.content_length_n == -1) {
       if (http版本 < http 1.1) {
            r->keepalive = 0;//关闭keep alive
       } else {
            if (开启chunk) {
                 r->chunked = 1;
            } else {
                 r->keepalive = 0;//也就是,如果关闭了chunk,则也关闭了keep alive。
            }
       }
  }

对应的body filter的流程,则更加简单,直接对当前输出的buf chain进行一个chunk封装。

第二、Nginx中Gzip模块和r->headers_out.content_length_n

r->headers_out.content_length_n :这个在Nginx内部用于表述请求返回内容的长度。但注意这不是完全相等的,只有在 r->headers_out.content_length_n >=0的时候,才有意义。比如说,通常后端的upstream(比如说PHP),如果没有在脚本中强制header输出content-length,则默认在nginx中 r->headers_out.content_length_n = -1。

Gzip模块也是一个典型的Filter模块。这里简单介绍下,后续可以详细描述。在header filter中会直接清空 r->headers_out.content_length_n和header中输出的content_length。为什么要清空呢?主要是因为gzip要对内容模块进行压缩处理,而在header filter的时候,gzip模块不可能计算出压缩后的内容长度(原因是在nginx中,header 输出和body的输出是完全两个不同的阶段),所以最好的办法就是在清空header中的content-length。这样结合之前的介绍的chunked模块,可以看出:在nginx中,如果采用gzip,如果是keep alive,则必然是chunked模式。

Upsteam如何和chunked结合

在前面介绍的Nginx Chunked模式实现中提到,Chunked模块的body filter是对当前处理的buf chain进行封装成一个Chunk。那么在Nginx中是如何实现多个Chunk的呢?这里要从 ngx_event_pipe_write_to_downstream这个函数说起,该函数是upstream处理中非常关键的一个函数,用于把upstream返回的数据直接进行输出。

分析该函数的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
for()
{
     if (p->downstream_error) {
            return ngx_event_pipe_drain_chains(p);//出错了
     }
     if (p->upstream_eof || p->upstream_error || p->upstream_done) {
          //是最后一个节点,或者已经读取upstream数据完毕
          //分类比的flush输出,这里面一般都含有bodyfilter比如
          rc = p->output_filter(p->output_ctx, p->in);
          //并且结束循环
          p->downstream_done = 1;
          break;
     }
     /*计算当前BUF的size*/
     if (bsize >= (size_t) p->busy_size) {
          /*超过大小则刷数据*/
          go flush;
     }
     for ( ;; ) {
          //循环拼装输出buf,如果超过busy_size同样进行flush
     }
flush:
     //flush输出
     rc = p->output_filter(p->output_ctx, out);
}

可以看到,有一个关键点,就是busy_size,它决定了一个chunk的大小。这个对应一个配置,追查后如下:在不同的upstream中有不同的定义,比如说proxy模式下,有proxy_busy_buffers_size,该值默认是proxy buffer size * 2 。(proxy buffer size根据操作系统,可以是4K或者8k)。fastcgi模式下,buffer_size默认是ngx_pagesize。配置项是fastcgi_busy_buffers_size。默认情况下是fastcgi的buffer size的2倍。(buffer size个是可配置的,buffer 个个数也是可以配置的,个数不能小于2)。

默认的bash设置中,在使用history命令查看历史命令的时候,不显示命令执行的时间,通过增加HISTTIMEFORMAT变量可以时间记录历史命令的功能。

设置方法:
在/etc/profile 里面加入下面2行就可以了,这样可以记录每个用户执行的命令了。

HISTTIMEFORMAT="%Y-%m-%d %H:%M:%S "
export HISTTIMEFORMAT

注:HISTTIMEFORMAT的格式你可以自己定义,定义成你想要的格式。具体格式可以参照date命令。例如用"%Y-%m-%d %H:%M:%S "格式按照我们中国人的时间格式,"%s " 按照unix时间戳的格式显示。

Amoeba是一个类似MySQL Proxy的分布式数据库中间代理层软件,是由陈思儒开发的一个开源的java项目。其主要功能包括读写分离,垂直分库,水平分库等,经过测试,发现其功能和稳定性都非常的不错,如果需要构架分布式数据库环境,采用Amoeba是一个不错的方案。目前Amoeba一共包括For aladdin,For MySQL和For Oracle三个版本,本文主要关注For MySQL版本的一个读写分离实现。实际上垂直切分和水平切分的架构也相差不大,改动几个配置就可以轻松实现。

下图是一个采用Amoeba的读写分离技术结合MySQL的Master-Slave Replication的一个分布式系统的架构:
amoeba_mysql

Amoeba处于在应用和数据库之间,扮演一个中介的角色,将应用传递过来的SQL语句经过分析后,将写的语句交给Master库执行,将读的语句路由到Slave库执行(当然也可以到Master读,这个完全看配置)。Amoeba实现了简单的负载均衡(采用轮询算法)和Failover。如果配置了多个读的库,则任何一个读的库出现宕机,不会导致整个系统故障,Amoeba能自动将读请求路由到其他可用的库上,当然,写还是单点的依赖于Master数据库的,这个需要通过数据库的切换,或者水平分割等技术来提升Master库的可用性。

Amoeba可以在不同机器上启动多个,并且做同样的配置来进行水平扩展,以分担压力和提升可用性,可以将Amoeba和MySQL装在同一台机器,也可以装在不同的机器上,Amoeba本身不做数据缓存,所以对于内存消耗很少,主要是CPU占用。对于应用来说,图中的三个Amoeba就是三台一模一样的MySQL数据库,连接其中任何一台都是可以的,所以需要在应用端有一个Load balance和Failover的机制,需要连接数据库时从三台中随机挑选一台即可,如果其他任何一台出现故障,则可以自动Failover到剩余的可用机器上。MySQL的JDBC驱动从connector-j 3.17版本起已经提供了这样的负载均衡和故障切换的功能,那么剩下的事情对于应用来说就很简单了,不需要做太多的改动就能搭建一套高可用的MySQL分布式数据库环境,何乐而不为?

参考链接:
Amoeba开发者博客
Amoeba下载
Amoeba文档
JavaEye上关于Amoeba的讨论贴

一、Unison简介

Unison是Windows、Linux以及其他Unix平台下都可以使用的文件同步工具,它能使两个文件夹(本地或网络上的)保持内容的一致。Unison拥有与其它一些同步工具或文件系统的相同的特性,但也有自身的特点:
1.跨平台使用;
2.对内核和用户权限没有特别要求;
3.Unison是双向的,它能自动处理两分拷贝中更新没有冲突的部分,有冲突的部分将会显示出来让用户选择更新策略;
4.只要是能连通的两台主机,就可以运行unison,可以直接使用socket连接或安全的ssh连接方式,对带宽的要求不高,使用类似rsync的压缩传输协议。

二、编译安装Unison

Linux下通过源码包编译安装Unison时,需要用到Objective Caml compiler。
通过以下方式安装
[root@server1 ~]# wget http://caml.inria.fr/pub/distrib/ocaml-3.12/ocaml-3.12.0.tar.gz
[root@server1 ~]# tar -xzvf ocaml-3.12.0.tar.gz
[root@server1 ~]# cd ocaml-3.12.0
[root@server1 ocaml-3.12.0]# ./configure
[root@server1 ocaml-3.12.0]# make world opt
[root@server1 ocaml-3.12.0]# make install

编译安装Unison
[root@server1 ~]# wget http://www.seas.upenn.edu/~bcpierce/unison//download/releases/stable/unison-2.40.63.tar.gz
[root@server1 ~]# tar -xzvf unison-2.40.63.tar.gz
[root@server1 ~]# cd unison-2.40.63
[root@server1 unison-2.40.63]# make UISTYLE=text
[root@server1 unison-2.40.63]# make install

在执行make install的过程中,可能会出现以下错误提示:
mv: cannot stat '/root/bin//unison': No such file or directory
make: [doinstall] Error 1 (ignored)
cp unison /root/bin/
cp: cannot create regular file '/root/bin/': Is a directory
make: *** [doinstall] Error 1

出现错误的原因在与Unison默认是将文件Copy到/root/bin目录,但Linux默认是没有该目录的,因此我们需要将生成的可执行文件unison复制到系统的PATH目录。
[root@server1 unison-2.40.63]# cp unison /usr/local/bin

将可执行文件unison上传到远程主机(假设远程主机IP为192.168.10.4)
[root@server1 unison-2.40.63]# scp unison root@92.168.10.4:/root/
通过SSH登陆到远程主机,再将unison复制到server2的PATH目录
[root@server2 ~]#cp unison /usr/local/bin

三、配置ssh key信任

建议通过普通用户进行操作,理由是通过root操作本身就危险,免密码登陆的root就更危险了。

在两台服务器上创建unison用户
[root@server1 ~]# useradd -m unison
[root@server1 ~]# passwd unison
[root@server2 ~]# useradd -m unison
[root@server2 ~]# passwd unison

在server1上创建key并配置server2的信任
[root@server1 ~]# su – unison
[unison@server1 ~]$ ssh-keygen -t rsa
在提示保存私钥(key)和公钥(public key)的位置时,使用默认值;
在提示是否需要私钥密码(passphrase)时,直接敲回车,即不使用私钥密码。
之后,将生成一对密钥,id_rsa(私钥文件)和id_rsa.pub(公钥文件),保存在/home/unison/.ssh/目录下。

将公钥添加到server2的 authorized_keys 文件中
将文件上传到server2(假设server2主机IP为192.168.10.4)
[unison@server1 ~]$ scp ~/.ssh/id_rsa.pub unison@192.168.10.4:/home/unison/

使用rsync用户SSH到登陆到远程主机,并将公钥添加到 authorized_keys 文件中
[unison@server2 ~]$ mkdir .ssh
[unison@server2 ~]$ chmod 700 .ssh
[unison@server2 ~]$ mv ~/id_rsa.pub ~/.ssh/authorized_keys

同理,执行以下步骤在server2上创建key并配置server1的信任
[root@server2 ~]# su – unison
[unison@server2 ~]$ ssh-keygen -t rsa

将文件上传到server1(假设server1主机IP为192.168.10.3)
[unison@server2 ~]$ scp ~/.ssh/id_rsa.pub unison@192.168.10.3:/home/unison/

使用rsync用户SSH到登陆到server1,并将公钥添加到 authorized_keys 文件中
[unison@server1 ~]$ mv ~/id_rsa.pub ~/.ssh/authorized_keys

重启SSH服务
[root@server1 ~]# /etc/init.d/sshd restart
[root@server2 ~]# /etc/init.d/sshd restart

四、Unison的配置与使用

在两台服务器上创建test目录,用于测试
[root@server1 ~]# su - unison
[unison@server1 ~]$ mkdir test
[root@server2 ~]# su - unison
[unison@server2 ~]$ mkdir test

在两台服务器上分别执行一次unison,如果出现提示确认,则直接敲回车选择默认值
[unison@server1 ~]$ unison /home/unison/test/ ssh://unison@192.168.10.4//home/unison/test/
[unison@server2 ~]$ unison /home/unison/test/ ssh://unison@192.168.10.3//home/unison/test/

修改两台服务器的unison配置文件,输入以下内容
[unison@server1 ~]$ vim /home/unison/.unison/default.prf
#Unison preferences file
root = /home/unison/test
root = ssh://unison@192.168.10.4//home/unison/test/
#force =
#ignore =
batch = true
#repeat = 1
#retry = 3
owner = true
group = true
perms = -1
fastcheck = false
rsync = false
sshargs = -C
xferbycopying = true
log = true
logfile = /home/unison/.unison/unison.log
[unison@server2 ~]$ vim /home/unison/.unison/default.prf
#Unison preferences file
root = /home/unison/test
root = ssh://unison@192.168.10.3//home/unison/test/
#force =
#ignore =
batch = true
#repeat = 1
#retry = 3
owner = true
group = true
perms = -1
fastcheck = false
rsync = false
sshargs = -C
xferbycopying = true
log = true
logfile = /home/unison/.unison/unison.log
相关注解如下:
force表示会以本地所指定文件夹为标准,将该目录同步到远端。这里需要注意,如果指定了force参数,那么Unison就变成了单项同步了,也就是说会以force指定的文件夹为准进行同步,类似与rsync。
Unison双向同步基本原理是:假如有A B两个文件夹,A文件夹把自己的改动同步到B,B文件夹也把自己的改动同步到A,最后A B两文件夹的内容相同,是AB文件夹的合集。
Unison双向同步的一个缺点是,对于一个文件在两个同步文件夹中都被修改时,unison是不会去同步的,因为unison无法判断以那个为准。
ignore = Path表示忽略指定目录,即同步时不同步它。
batch = true,表示全自动模式,接受缺省动作,并执行。
-fastcheck true 表示同步时仅通过文件的创建时间来比较,如果选项为false,Unison则将比较两地文件的内容。
log = true 表示在终端输出运行信息。
logfile 指定输出的log文件。

另外,Unison有很多参数,这里仅介绍常用的几个,详细的请参看Unison手册。
-auto //接受缺省的动作,然后等待用户确认是否执行。
-batch //batch mode, 全自动模式,接受缺省动作,并执行。
-ignore xxx //增加 xxx 到忽略列表中
-ignorecase [true|false|default] //是否忽略文件名大小写
-follow xxx //是否支持对符号连接指向内容的同步
owner = true //保持同步过来的文件属主
group = true //保持同步过来的文件组信息
perms = -1 //保持同步过来的文件读写权限
repeat = 1 //间隔1秒后,开始新的一次同步检查
retry = 3 //失败重试
sshargs = -C //使用ssh的压缩传输方式
xferbycopying = true"
-immutable xxx //不变目录,扫描时可以忽略
-silent //安静模式
-times //同步修改时间
-path xxx 参数 //只同步 -path 参数指定的子目录以及文件,而非整个目录,-path 可以多次出现。

PS:Windows下的unison配置文件默认位于C:\Documents and Settings\currentuser\.unison目录,默认的配置文件名是default.prf。

五、测试

首先分别在server1与server2的/home/unison/test目录下创建文件或目录,然后在server1上执行unison,接着如果在server1与server2上都能看到各自创建的文件,就说明同步成功。

分别在server1与server2上创建文件
[unison@server1 ~]$ cd test
[unison@server1 test]$ touch 1.txt touch 3.txt
[unison@server2 ~]$ cd test
[unison@server2 test]$ touch 2.txt touch 4.txt

在server1上执行unison
[unison@server1 ~]$ unison

在server1与server2上查看文件是否同步
[unison@server1 ~]$ cd test
[unison@server1 test]$ ls
1.txt 2.txt 3.txt 4.txt
[unison@server2 ~]$ cd test
[unison@server2 test]$ ls
1.txt 2.txt 3.txt 4.txt

均看到了“1.txt 2.txt 3.txt 4.txt”所有文件,说明文件同步已经成功!

注意:第一次SSH连接的时候可能需要输入一次密码,之后就不需要输入了。

六、定期或实时执行同步

如果想要定期执行,则通过crontab计划任务来实现,例如通过以下方式设置每5分钟执行一次
[root@server1 ~]# su - unison
[unison@server1 ~]$ crontab -e
*/5 * * * * /usr/local/bin/unison

分页: 6/57 第一页 上页 1 2 3 4 5 6 7 8 9 10 下页 最后页 [ 显示模式: 摘要 | 列表 ]