• 27 十 2009 /  互联网技术

    原文链接:Nginx & Comet: Low Latency Server Push

    作者:Ilya Grigorik

    翻译:孙绍轩

    服务器推送(Server Push)是高效的、延迟低的数据交换方式。如果数据发送端与接收端都在互联网中公开可见,可以使用PubSubHubbubsimpler Webhook等方法完成任务。但是如果数据接收方在防火墙内、在内网或它只是一个浏览器(只可以向外发送数据请求,无法处理传入的数据),则实现服务器推送就更难了。如果你有冒险精神,你可以建立一个反向HTTP服务器。如果你寻求可靠的解决方案,也许你要等待HTML5的WebSocket’s API特性了。但如果你需要即刻可以实现的解决方案,你可以妥协一下,使用异步推送模式来代替,你可以使用Comet,也被称为反向Ajax、HTTP服务器推送或HTTP流。

    早在2006年Alex Russel提出了一个不坏的技术思路,那就是长连接(Comet)概念:从客户端发起并保持一个连接直到数据出现并传送(long polling),或者永远保持一个连接,通过它推送数据到客户端(streaming)。这两种方法的好处是数据传送非常及时。因此长连接技术广泛用于聊天应用(Facebook, Google, Meebo等)以及实现即时触发的机制。

    Nginx变成一个长连接服务器

    实现长连接服务比较大的问题是特殊的隐形需求以及事件驱动web服务器能否高效处理众多的长连接。Friendfeed的Tornado服务器是一个标准应用级服务器的好例子。另外,感谢Leo Ponomarev的努力,你现在可以用nginx_http_push_module插件使你的Nginx服务器变身成为一台完全功能的长连接服务器。

    使用自定义的一套框架结构,Leo的插件只提供两个对外的接口:一个是订阅者,一个是发布者。客户端连接Nginx服务器,创建针对一个频道的long-polling长连接并等待数据。同时,发布者只是简单的将数据使用POST方法提交给Nginx,插件收到数据后将它一个个发给等待的客户端。这表明发布者不需要直接传递数据,它只是一个简单的事件产生器!没有比这个更简单的方法了。

    还有更高的功能是,客户端和发布端可以建立任意的频道,并且插件也提供消息队列功能,这表明Nginx服务器会在客户端断线的情况下临时保存消息。队列消息可以按照时间、等待列表长度或内存限制大小来失效释放。

    NginxRuby配置例子

    一开始,你需要从源代码编译一个Nginx。解压源代码包,从GitHub获取插件的源码并放入Nginx的源码目录,然后使用下面的参数编译(./configure –add-module=/path/to/plugin && make && make install)。下一步,参考readme文件和协议文件,了解所有的参数选项。一个多客户端接收信息的配置例子如下:

    > nginx-push.conf

    # 内部发布点(保证私有或不对外公开)

    location /publish {

    set $push_channel_id $arg_id;      #/?id=239aff3 或类似的文本标示

    push_sender;

    push_store_messages on;            # 打开消息队列

    push_message_timeout 2h;           # 2小时后消息失效

    push_max_message_buffer_length 10; # 保存10条消息

    push_min_message_recipients 0;     # 清除前最小接收人数目

    }

    # 公开的长连接接收点

    location /activity {

    push_listener;

    # 一个频道编号能有多少客户端同时连接

    # – last: 只有最频繁请求的客户端能保持,其它连接返回409

    # – first: 只有最早连接的那个客户端可以保持,其它连接返回409

    # – broadcast: 任何数量的客户端连接都会是长连接

    push_listener_concurrency broadcast;

    set $push_channel_id $arg_id;

    default_type  text/plain;

    }

    当你编译配置好你的Nginx服务器,并且启动它,我们可以建立一个简单的广播场景,一个数据发送广播方,几个订阅信息接收方来测试我们的长连接服务器。

    > comet-push-consume.rb

    require ‘rubygems’

    require ‘em-http’

    def subscribe(opts)

    listener = EventMachine::HttpRequest.new(‘http://127.0.0.1/activity?id=’+ opts[:channel]).get :head => opts[:head]

    listener.callback {

    # 打印所获取的内容,并重新订阅这个频道

    # 使用last-modified头去忽略之前已经获取的数据。

    puts “Listener recieved: ” + listener.response + “\\n”

    modified = listener.response_header['LAST_MODIFIED']

    subscribe({:channel => opts[:channel], :head => {‘If-Modified-Since’ => modified}})

    }

    end

    EventMachine.run {

    channel = “pub”

    # 每5秒钟发布一个新的消息

    EM.add_periodic_timer(5) do

    time = Time.now

    publisher = EventMachine::HttpRequest.new(‘http://127.0.0.1/publish?id=’+channel).post :body => “Hello @ #{time}”

    publisher.callback {

    puts “Published message @ #{time}”

    puts “Response code: ” + publisher.response_header.status.to_s

    puts “Headers: ” + publisher.response_header.inspect

    puts “Body: \\n” + publisher.response

    puts “\\n”

    }

    end

    # 打开两个客户端

    subscribe(:channel => channel)

    subscribe(:channel => channel)

    }

    nginx-push.zip (完整的Nginx配置和Ruby代码)

    在上面的代码中,每5秒钟数据发布端向Nginx服务器发出新的事件,服务器将数据通过长连接转发给两个订阅的客户端。当消息发送到客户端,服务器会断开他们的连接,客户端会立即重连并等待下一次数据的到来。结果就是使用Nginx实现了一个数据发布端到客户端的实时消息推送机制!

    期待生产环境下的长连接服务

    Leo的模块还在开发期,还需要时间来稳定下来,但它是一个需要关注的项目。最近的更新计划都着重于bug修复,未来的计划里有描述要加入流的模式:代替现在每次数据获取后都要重连的情况(long polling),Nginx会保持连接,将数据一段段的实时传送给客户端。拥有这个功能后你能很方便的部署你自己的信息触发式API(例如:Twitter流)。

    最后,不要忘记数字不断增长的其它Nginx模块,或者如果你感兴趣的话,可以参考Evan Miller编写的指引来开发自己的Nginx模块。

    Tags: ,

  • 12 六 2009 /  互联网技术

    通常长连接应用的结构思路网上都有很多介绍,但是很多细节并没有透露。最近实现具体的应用时探索了几点细节,记录下来。主要的问题是客户端浏览器要有实时的消息返回,但通常整个链路上都会出现一些设置问题导致的无法实时返回的情况。

    1、nginx服务器代理模式不会实时返回数据

    因为现在主流的web服务器还没有特别适合事件驱动的,所以长连接部分需要单独写server程序,但我们又希望用统一的80端口来做服务端口,所以我用nignx做了前面的代理,根据规则转发长连接请求到后台的独立服务器上。

    于是问题来了,通过nginx转发并不直接回复给客户端,需要数据传输后断开连接,客户端才能收到返回的所有内容。这是后需要将默认的proxy_pass的buffer关掉,有一个属性是proxy_buffering off;关掉以后nignx会直接返回结果而没有缓存等待所有内容都从后台服务器传送完成才返回给客户端。

    2、Chrome浏览器的实时处理数据机制

    Chrome和其他浏览器不一样,如果你一开始返回一个http头,里面表明有很长的数据需要下载,Chrome没有收到一定量数据前,是不会在浏览器中渲染或者执行这些已经下载的数据的,所以导致你无法通过长连接触发你想触发的javascript或一些你想做的操作。

    解决方案是在后台的服务器上判断如果是Chrome来访则要在内容body里面先输出标准的html头部head和body标签,然后后面跟随至少2048个字节的字符,可以随意,我用点符号来填充,然后才是正式的我要输出的内容。这样Chrome收够了2048字节的内容后就会触发对代码的处理工作,后续的代码也就变成实时触发的效果了。这算是一个针对Chrome的hack吧。

    Tags: , ,

  • 06 五 2009 /  互联网技术

    最近这两天一直在测试自己用python+twisted写的一个http服务器的效率,几度从心理崩溃的边缘闯了过来,残喘留下点墨迹,警醒后来者。

    这个服务程序没有什么业务逻辑,就是负责保持一大堆连接,等待服务器状态被更改了以后就通知所有的保持连接,返回一个信号值。如何维持这么多连接是这个应用的关键。之前选择了几种方案,感觉太消耗服务器资源,动不动就内存吃光了,又懒得用C从头写一个服务程序(太耗时间没有必要),所以选择了python的一个socket框架twisted。

    这个程序一开始用ab(Apache Benchmark)简单的压,没有发现什么问题,服务器内存和cpu占用率都不高,所以没在意就直接上线运行了,实际运行时好在流量不是太大,所以没有出什么问题,但实际监控到的数据是400左右的保持连接就要占用40%的cpu,这个数值有点高。如果这样计算的话,一台机器没有办法承载多少连接数。于是在测试环境开始第一次压力测试,希望能重现实际运营时的那个种状态。

    还是ab,并发500请求我的服务程序,发现有很多连接并不是预期时间返回的(每个连接最少保持25秒),很多连接是超出了很长时间才返回。调试程序,在子线程的部分输出一些调试信息,发现同时只有少数线程可以被执行,而大部分的线程都挂起了,印象中twisted有线程池的概念,找到了一个这样的参数 reactor.suggestThreadPoolSize(),默认值是10,也就是同时只有10个线程可以被处理,将这个值改大到我希望的并发数值以上,问题消失了。这个修改有用!

    继续ab,并发到1000,作为代理服务器的nginx报错,它停止响应请求了,这是为什么呢?nginx默认配置下是和系统下ulimit nofile数值相关的,突破的方法可以在nginx.conf配置文件中加一句话

    worker_rlimit_nofile 51200;

    生效了,为此我犯懒没有修改系统默认的ulimit -n的值,现在还是默认1024。

    还是ab,并发到2000,总是感觉ab并没有达到这样的并发数量,而是一点点增加连接数,另外服务器端cpu占用率也没有实测的那次那种很高的占用率,为了更真实的模拟,我用python写了一个模拟并发的程序代替ab

    同时通过查询资料确认twisted默认的reactor是selec()方法,该方法目前限定只能同时处理1024个请求,突破这种方法需要切换到epoll模式,在程序开头写如下的代码,激活epoll的reactor

    from twisted.internet import epollreactor
    epollreactor.install()

    自己写的压力测试脚本继续测,1000貌似没有问题,2000出现了问题,因为我的测试脚本是1千1千的往上加,加到2000的时候,服务基本停止响应了,有大量的连接在等待,服务器出现很多请求超时。什么原因呢?是不是ulimit的问题呢?很有可能,因为启动服务程序的用户ulimit -n才1024,只允许打开1024个文件句柄,nginx也许有其它的招数来绕过这个限制,毕竟它用root用户启动的,想干什么不行呀,但我这个程序是普通用户,我的程序包括twisted也不会像nginx这么优秀,自己什么都搞定了,所以要修改我这个用户默认的nofile值,加大!修改/etc/security/limits.conf文件,增加对应用户nofile的值,我设定的是65535,六万多的并发足够了。重新登陆用户重启服务程序后,缓慢的问题消失了,但新的问题又出现了!

    超过1000多并发后我写的程序主动停止响应,查询对应进程的/proc/xxx/fd目录下的句柄数,锁定在1000出头的一个数字,直接telnet服务端口,输入任何内容就主动断开连接了。怀疑twisted框架是不是有那个参数没有设置好,类似SuggestThreadPoolSize的问题呢?寻找,一直在寻找,都找到twisted源代码里面去了,无果。貌似大家都没有这个问题。

    给twisted社区的邮件列表发信,询问是否有限制或其它问题,等待之余怀疑的心态重新编译了一个新的python版本。原来的是2.4.3,更新到2.5.4后居然问题消失了!不禁问自己,这是为什么呢?

    很快得到了社区大侠的回复,说这是python的bug,不巧就是2.4.3这个版本有bug,2.4.2没有,2.4.4也没有,我真应该去买彩票了。

    一切都过去了,目前测试已经超过了4000保持连接,服务器状态良好,内存、cpu占用率都不高,之前实测cpu占用率高也许是thread pool设定问题以及select()模式的综合体现,目前都消失了,基本上维持20%的cpu占用率,python也是脚本语言,你也不能太苛刻的要求它嘛。

    总结:

    • 性能是压出来的,怀疑一切,尽情的压,总能找到瓶颈点,总能解决并提高
    • 社区力量很好,但不能什么事情都不走脑子就往上面问,详细描述问题会带来非常准确的回答
    • 崩溃边缘可以喘喘气,换换心情,换个角度也许问题就能解决了,切忌钻牛角尖

    Tags: , ,

  • 02 四 2009 /  互联网技术

    spawn-fcgi更新到1.6.0版本了,默认是不需要使用参数-C来设定开启php-cgi的数量了,沿用内置php-cgi的规则,使用env环境变量PHP_FCGI_CHILDREN来控制php-cgi的数量。我写了一个脚本,用于开启、关闭spawn-fcgi,大家可以直接使用。

     

     
    #! /bin/bash
     
    # This program write by yorgo(yorgo at ruisoft.com) on 2009/04/02
    # Last modified 2009/04/03
    # version 1.1
    # use this script to start or stop spawn-fcgi
     
    address="127.0.0.1"
    port="9000"
    pidpath="/tmp/spawn_phpcgi_${port}.pid"
    user="apache"
    group="apache"
    phpcgi="/usr/local/php-cgi/bin/php-cgi"
    PHP_FCGI_CHILDREN=16
    PHP_FCGI_MAX_REQUESTS=1000
     
    startspawn()
    {
    	env - PHP_FCGI_CHILDREN=${PHP_FCGI_CHILDREN} PHP_FCGI_MAX_REQUESTS=${PHP_FCGI_MAX_REQUESTS} /usr/local/php-cgi/bin/spawn-fcgi -a ${address} -p ${port} -u ${user} -g ${group} -f ${phpcgi} -P ${pidpath}
    	echo "success start."
    }
     
    if [ "$1" == "start" ]
    then
    	if [ ! -f $pidpath ]
    	then
    		startspawn
    	else
    		pidcount=`ps -ef |grep ${phpcgi}|wc -l`
    		if [ "$pidcount" -gt "1" ]
    		then
    			echo "service is already running."
    		else
    			rm -f ${pidpath}
    			startspawn
    		fi
    	fi
     
    elif [ "$1" == "stop" ]
    then
    	pid=`cat ${pidpath}`
    	kill ${pid}
    	rm -f ${pidpath}
    	echo "success stop."
    else
    	echo "spawnmanager.sh [start|stop]"
    	exit
    fi

    Tags: , ,

  • 19 二 2009 /  互联网技术

    最近做一个项目需要很高的并发长连接,apache下做了一个测试,连接数不多的情况下,负载已经开始冒头了,并且worker模式下php不是很稳定,于是乎转向尝试Nginx+FastCGI+PHP的组合方案。

    网上已经有很多关于这个组合的安装文档了,在这里我不重复,只是提一些我自认为比较迷惑的,网上文章没有唠叨的问题点。假设你们熟悉apache+php的组合,计划从那个平台迁移到Nginx下。Ok, Let’s go!

    • php源码包中的文档描述道,有关FastCGI模式,建议使用./php-cgi -b 127.0.0.1:9000的独立模式来接受web服务器转发过来的请求。既然是推荐模式,我就运行一把,结果这个程序一没有多进程模式,二不支持进程异常退出后的重新启动。并发请求上不去,稍微给一点压力,php-cgi崩溃或者异常退出了,系统就没有php解析服务了。于是乎转到lighttpd里的一个小工具spawn-fcgi。这个工具支持设定同时打开多个php-cgi来服务,并且任何子进程终止都会新建一个补上,可以说算是比较稳定的方案了。网上还有一个php-fpm,有空我看看怎么玩的。
    • 修改了php.ini配置信息,问题来了,如何重新启动这么多php-cgi呢?本以为spawn-fcgi是一个独立的进程,kill掉它就可以干掉其它呢,结果在进程列表中找不到spawn-fcgi,它隐身成php-cgi这个名字,并且是所有其他php-cgi的父进程。得,killall php-cgi,直接就杀掉所有的php-cgi包括那个父进程。重新启动spawn-fcgi,这时候就是新的配置信息了。有一点很不错的是,这时候Nginx没有重新启动,在没有php-cgi的时候,Nginx会显示一个界面友好的提示用户暂时无法访问。并且其它静态文件访问都是正常的。
    • 我测试的web项目要使用rewrite,正好研究一下如何切换rewrite代码,研究半天才发现有如下需要注意的点:
      1、Nginx不支持.htaccess文件,配置rewrite需要在nginx.conf里面写。
      2、apache中的RewriteRule需要改写成rewrite
      3、规则需要使用双引号引起来
      4、apache中[R] 要改写成redirect
      5、因为没有地方设置RewriteBase,所以所有的规则都要从/开始,跳转的程序地址也要从/开始
      6、RewriteCond用if代替
    • 调试上面的rewrite非常累人的kill掉nginx再重启,后来发现可以给nginx master一个信号kill -HUP xxx(nginx master的pid),这样服务没有停,就更新的配置。为了保证服务顺畅,执行这个之前要先执行nginx -t来测试配置文件是否有效

    使用ab压了一下,在压力下,访问网页响应速度还是不错的,并且太大的压力nginx会屏蔽请求,等待系统负载降下来了才会继续接受服务请求。这样不至于将服务器搞死。

    陆续我还会将测试发现的问题列在这里,对待新鲜事物总是会有这么几天不顺利,但是过去了以后自然就变成自己的经验,就不会有很恼人的问题了。坚持!

     

    后续使用nginx的心得,不开新的文章了,直接在这里添加内容

    1、日志翻滚:nginx不支持cronlog那样的管道模式,但是可以用kill -USR1 [pid]来给nginx信号来进行日志文件的刷新。将现有的日志文件改名后,给一下USR1信号,新的日志就产生了。

    2、一个站点下如果开多个虚拟主机,最好是单独写server {}这个配置块,不要在里面加不同的alias,否则很多跳转都会默认跳转到第一个站点的域名下,就算你访问的是它的别名站点也没用。网上有另外一个办法是做一个rewrite的规则,让这种跳转找到自己访问的源域名再跳转,感觉rewrite方案类似凑合事儿,因此还是用直接配置多个server的解决方案。多server配置时要加一个参数在http{}中 server_names_hash_bucket_size 128;否则服务器启动会有报错。

    3、spawn-fcgi -C的参数是代表开多少个后台的fcgi守候程序,一个程序同时只能服务一个请求,我的项目中有长连接的状态,所以这个参数开小了很容易导致因为后台没有足够的空闲fcgi而nginx停止响应。所以如果连接时间比较长,并发比较大的话,这里要开大点,开个几百个没有什么问题。

    4、怎么不暂停服务扩充spawn-fcgi的数量呢?先用spawn-fcgi启动另外一个新的端口例如9001服务,这个服务中fcgi的数量是要扩充到的数量,然后改nginx.conf里面的端口号到9001,然后kill -HUP [pid]更新nginx的配置,这会儿就使用新的spawn出来的fcgi了。然后将旧的spawn-fcgi的进程都杀掉释放资源就好了。

    5、上传文件大小在nginx的配置里也有体现,client_max_body_size 250m;可以将上传限制扩充到250M,加到http {}配置中或者server{}中都可以

    6、每个location属性都可以单独设定某些请求文件条件下的特殊属性,但要注意,设定必须包含root参数指定所请求资源的物理路径,否则回报404错误。

    7、可以使用expires 365d;属性来设定某些文件的缓存时间。

    Tags: , , ,