非阻塞的Python web框架tornado

公司项目中需要使用长链接方式的获取后端数据库——主要是Redis的实时数据。

由于项目本身是PHP的初次看到这个项目,首先想到的是Apache + mod_php的方式,配合php的ob_start()方式直接调用,就如同我之前的一篇东西所说的那样。可问题不这么简单:

  1. 系统是nginx + php-fpm方式,php-fpm“hold不住”过多的Http请求,而nginx需要调整响应时间。
  2. 用户数量很多,Apache的消耗很大。本身功能点很小,实现成本不合算。

说到并发,Apache采用的方式是大量的fork进程,通过“人多力量大”的方式应对多个请求,这样的基于进程(线程)模型的并发,一旦调用sleep,进程只是休眠而已,仍然占用着内存,仍然需要进程调度,资源始终得不到释放,资源自然无法得到控制。这些年nginx的流行大多都是因为nginx采用的epoll方式有效的解决这个问题——直接挂掉进程,然后再指定时间内重新启用。这就是传说中的“非阻塞”(asynchronous IO  AIO)。

多并发、轻量级应用,我首先想到的是Python,加上“非阻塞”关键字,得到的结果就是Tornado

tornado原本是FriendFeed引擎的开源版本,本身就是为解决“C10K”问题而生的。

稍微研究了一下,coding也很简便:

    #!/usr/bin/env python  
    #By Litrin J   
    #http://www.litrin.net/  
    import tornado.httpserver  
    import tornado.ioloop  
    import tornado.options  
    import tornado.web  
    import time, redis, hashlib,random  
    from tornado.options import define, options  
      
    define("port", default=8888, help="run on the given port", type=int)  
      
    class LongPolling(tornado.web.RequestHandler):  
      
        minWaitTime = 15  
        maxWaitTime = 900  
      
        RedisHost   = "172.18.194.98"  
        RedisPrefix = "Q/MSG%s"  
      
        SignSalt    = "salt"  
      
        StopCode    = "{'stop':1}"  
     
        @tornado.web.asynchronous  
        def get(self):  
            sUid        = self.get_argument("uid", None)  
            sSign       = self.get_argument("sign", None)  
            sJsonCall   = self.get_argument("jsonCallback", None)  
      
            if (sUid is None or sSign is None   
                    or self.checkSign(sUid, sSign) == False):  
                raise tornado.web.HTTPError(404)  
                self.clear()  
      
            else:  
                self.doLongPolling(callback=self.onWaitting)  
      
        def checkSign(self, sUid, sSign):  
            return True  
      
        def doLongPolling(self, callback):  
            #Check if the client close the connection  
            if self.request.connection.stream.closed():  
                self.clear()  
      
            sKey        =   self.RedisPrefix % self.uid  
            res         =   redis.Redis(self.RedisHost)  
            sMessage    =   res.rpop(sKey)  
            del res #close the redis at 1st time  
      
            callback(sMessage)  
      
        def onWaitting(self, sMessage):  
            if (sMessage is not None):  
                self.onRespones(sMessage)  
            else:  
                iNextPollingTime = time.time() + self.minWaitTime  
                if self.minWaitTime < self.maxWaitTime:  
                    self.minWaitTime *= 2  
                    #Can't useing time.sleep for no-bloking mode  
                    tornado.ioloop.IOLoop.instance().add_timeout(  
                    iNextPollingTime ,   
                    lambda: self.doLongPolling(callback=self.onWaitting)  
                )  
                else:  
                    self.onRespones(self.StopCode)  
      
        def onRespones(self, sMessage):  
            sJsonCall   = self.get_argument("jsonCallback", None)  
            if sJsonCall is not None:  
                sMessage= sJsonCall + "( " + sMessage.decode('utf-8') + ")"  
      
            self.write(sMessage)  
            self.finish()  
      
    def main():  
        tornado.options.parse_command_line()  
        application = tornado.web.Application([  
            (r"/", LongPolling),  
        ])  
        http_server = tornado.httpserver.HTTPServer(application)  
        http_server.listen(options.port)  
        tornado.ioloop.IOLoop.instance().start()  
      
    if __name__ == "__main__":  
        main()  

非阻塞的Python web框架tornado

公司项目中需要使用长链接方式的获取后端数据库——主要是Redis的实时数据。

由于项目本身是PHP的初次看到这个项目,首先想到的是Apache + mod_php的方式,配合php的ob_start()方式直接调用,就如同我之前的一篇东西所说的那样。可问题不这么简单:

  1. 系统是nginx + php-fpm方式,php-fpm“hold不住”过多的Http请求,而nginx需要调整响应时间。
  2. 用户数量很多,Apache的消耗很大。本身功能点很小,实现成本不合算。

说到并发,Apache采用的方式是大量的fork进程,通过“人多力量大”的方式应对多个请求,这样的基于进程(线程)模型的并发,一旦调用sleep,进程只是休眠而已,仍然占用着内存,仍然需要进程调度,资源始终得不到释放,资源自然无法得到控制。这些年nginx的流行大多都是因为nginx采用的epoll方式有效的解决这个问题——直接挂掉进程,然后再指定时间内重新启用。这就是传说中的“非阻塞”(asynchronous IO  AIO)。

多并发、轻量级应用,我首先想到的是Python,加上“非阻塞”关键字,得到的结果就是Tornado

tornado原本是FriendFeed引擎的开源版本,本身就是为解决“C10K”问题而生的。

稍微研究了一下,coding也很简便:

  1. #!/usr/bin/env python  
  2. #By Litrin J   
  3. #http://www.litrin.net/  
  4. import tornado.httpserver  
  5. import tornado.ioloop  
  6. import tornado.options  
  7. import tornado.web  
  8. import time, redis, hashlib,random  
  9. from tornado.options import define, options  
  10.   
  11. define("port", default=8888, help="run on the given port", type=int)  
  12.   
  13. class LongPolling(tornado.web.RequestHandler):  
  14.   
  15.     minWaitTime = 15  
  16.     maxWaitTime = 900  
  17.   
  18.     RedisHost   = "172.18.194.98"  
  19.     RedisPrefix = "Q/MSG%s"  
  20.   
  21.     SignSalt    = "salt"  
  22.   
  23.     StopCode    = "{'stop':1}"  
  24.  
  25.     @tornado.web.asynchronous  
  26.     def get(self):  
  27.         sUid        = self.get_argument("uid"None)  
  28.         sSign       = self.get_argument("sign"None)  
  29.         sJsonCall   = self.get_argument("jsonCallback"None)  
  30.   
  31.         if (sUid is None or sSign is None   
  32.                 or self.checkSign(sUid, sSign) == False):  
  33.             raise tornado.web.HTTPError(404)  
  34.             self.clear()  
  35.   
  36.         else:  
  37.             self.doLongPolling(callback=self.onWaitting)  
  38.   
  39.     def checkSign(self, sUid, sSign):  
  40.         return True  
  41.   
  42.     def doLongPolling(self, callback):  
  43.         #Check if the client close the connection  
  44.         if self.request.connection.stream.closed():  
  45.             self.clear()  
  46.   
  47.         sKey        =   self.RedisPrefix % self.uid  
  48.         res         =   redis.Redis(self.RedisHost)  
  49.         sMessage    =   res.rpop(sKey)  
  50.         del res #close the redis at 1st time  
  51.   
  52.         callback(sMessage)  
  53.   
  54.     def onWaitting(self, sMessage):  
  55.         if (sMessage is not None):  
  56.             self.onRespones(sMessage)  
  57.         else:  
  58.             iNextPollingTime = time.time() + self.minWaitTime  
  59.             if self.minWaitTime < self.maxWaitTime:  
  60.                 self.minWaitTime *= 2  
  61.                 #Can't useing time.sleep for no-bloking mode  
  62.                 tornado.ioloop.IOLoop.instance().add_timeout(  
  63.                 iNextPollingTime ,   
  64.                 lambdaself.doLongPolling(callback=self.onWaitting)  
  65.             )  
  66.             else:  
  67.                 self.onRespones(self.StopCode)  
  68.   
  69.     def onRespones(self, sMessage):  
  70.         sJsonCall   = self.get_argument("jsonCallback"None)  
  71.         if sJsonCall is not None:  
  72.             sMessage= sJsonCall + "( " + sMessage.decode('utf-8') + ")"  
  73.   
  74.         self.write(sMessage)  
  75.         self.finish()  
  76.   
  77. def main():  
  78.     tornado.options.parse_command_line()  
  79.     application = tornado.web.Application([  
  80.         (r"/", LongPolling),  
  81.     ])  
  82.     http_server = tornado.httpserver.HTTPServer(application)  
  83.     http_server.listen(options.port)  
  84.     tornado.ioloop.IOLoop.instance().start()  
  85.   
  86. if __name__ == "__main__":  
  87.     main()  

一旦建立http连接,系统会每隔一段时间去轮询用户的队列中是否有数据,有数据时才向客户端发送请求,否则就是“长时间小菊花”状态。很简单,仅仅用了不到100行代码的样子。当然,为了安全因素和节省资源,代码里添加了很多限制性的操作,否则可能会更简洁高效。

需要注意的是,tornado的理念就是建立在“非阻塞”基础上的,你当然可以选择time.sleep(n),但传统上的time.sleep(n)方式的休眠会导致IO阻塞,故只能采用tornado.ioloop.IOLoop.instance().add_timeout 的方法回调来回调去,将下次启动的时间转给系统。话说回来,这是我第一次在Java或JS以外使用回调函数……

初步测试了一下,借助Redis本身就很强的并发能力在台式机上可以每秒钟完成4K次请求以上,并可以保持同时4K以上的访问不掉线,此时的CPU load仍然保持在1以下,单是测试的结果就足以让Apache+Php组合汗颜。

部署的时候采用了单IP多端口方式,服务器有4个核心,决定开4个端口对应,分别是8885~8888,修改

define("port", default=8888, help="run on the given port", type=int)

即可

前端nginx用负责负载分发:

upstream backend {
        server 127.0.0.1:8888;
        server 127.0.0.1:8887;
        server 127.0.0.1:8886;
        server 127.0.0.1:8885;       
}
 server{
        listen  80;
        server_name message.test.com;
        keepalive_timeout 65;    #
        proxy_read_timeout 2000; #
        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;

    location / {
        proxy_pass_header Server;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Scheme $scheme;

        proxy_pass  http://backend;
        }
}


需要注意的是,这样的httpSocket方式很可能被内核认为是SYN攻击,而且对于大量的keep alive而言,很可能超过核心限制,还要修改sysctl参数。


#系统最大打开文件数
fs.file-max = 201510

#TCP连接的最长时间
net.ipv4.tcp_keepalive_time = 1800
#出现问题的尝试次数
net.ipv4.tcp_keepalive_probes = 15
#检查是否连接的等待时间
net.ipv4.tcp_keepalive_intvl = 60
#不进行SYN cookies防御
net.ipv4.tcp_syncookies = 0


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值