python系列8:python的web工程化问题

1. 回调函数

简单来说就是把函数作为参数,声明的时候只是形式上执行了函数,只有在“回调”的时候才回去实际执行函数内容。来看一个简单的例子:

import time
def callback():
    print("这是一个callback函数")

def test_callback(call):
    print("进入函数")
    time.sleep(1)
    #开始回调
    call()
    print("调用完成")

test_callback(callback)

在web应用里面有多种请求方式,我们熟悉的两种调用方式:

  • 同步调用:阻塞式,一个请求的任务完成才能继续下一个请求
  • 异步调用:反向调用,服务器接口函数在收到消息后,调用客户端的接口返回结果。在这种方法中,服务器可以多进程处理任务,完成一个任务就返回结果,不会发生资源占用阻塞的问题。

回调是一种双向调用模式。比如上一节的例子,callback作为一个参数传入test_call,然后test_call回过头来调用callback的具体内容。我们来看网上的案例:

import random as rd

# -----------被调用方----------------------------
def server(fn):  # 生成10个[0,1)之间小数
    ns = []
    for i in range(10):
        n = round(rd.random(), 2)
        ns.append(n)

    # 不用直接 return, 因为调用方通知不接返回结果
    # 改成回调函数方式
    fn(ns)  # 调用是调用方函数,这一操作称之为回调。

# ----------------调用方------------------------
# 定义回调函数
def request(*args):
    # 进入到本函数内,意味着被调用方函数已执行完
    print('生成数据成功')
    print(args)

server(request)   

服务端形式化的执行了客户端的请求,执行的内容可以在客户端自定义。

2. 装饰器

装饰器用来实现一种AOP切面功能,即一些函数在调用前都必须实现的功能,比如用户是否登录,用户是否有权限, 数据读写操作前打开事务等之类需求,由装饰器来实现比较容易。来看个例子:

def logging_tool(func): 
      def wrapper(*arg, **kwargs): 
           logging.info('%s is running...' % func.__name__) 
           func() # 把today当作参数传递进来,执行func()就相当于执行today() 
      return wrapper
def today(): 
    print('2018-05-25') 

today = logging_tool(today) # 因为装饰器logging_tool(today)返回函数对象wrapper,故这条语句相当于today=wrapper 
today() # 执行today() 就相当于执行wrapper()

today虽然还是叫那个名字,但已经不是原来的today函数了,而是一个wrapper。上面的代码等价于:

def logging_tool(func):
    logging.info('%s is running...' % func.__name__)
    func()

def today():
    print('2018-05-25')
logging_tool(today)

之所以用上面的代码,是因为它在形式上保持了today函数。我们还有一个更加形式化的方法,把装饰器声明用@符号替换,如下:

def logging_tool(func): 
    def wrapper(*arg, **kwargs): 
        logging.info('%s is running...' % func.__name__) 
        func() # 把today当作参数传递进来,执行func()就相当于执行today() 
    return wrapper

@logging_tool 
def today(): 
    print('2018-05-25') 
today()

不需要对today() 函数做任何修改,只需在定义的地方加上装饰器,调用的时候还是和以前一样。
如果我们有其他的类似函数,可以继续调用装饰器来修饰函数,而不用重复修改函数或者增加新的封装。这样,提高程序可重复利用性,并增加程序的可读性。

如果想带参数的话,将wrapper再嵌套一层即可。外层参数是实际参数,内存参数放函数名。
还有一种是把类当做装饰器,涉及到回调

import time,random
class server(object): 
    def __init__(self, func): 
        print('启动server')
        self._func = func # 初始化装饰的函数 
    
    def __call__(self): 
        print ('开始回调') 
        self._func() # 调用装饰的函数 
        print ('完成回调') 
        
@server 
def request(): # 被装饰函数不带参数的情况 
    print ('执行回调...') 
    time.sleep(random.randint(3,10))

print('发出请求')
request()

下面我们来把同步改为异步:

import time,random
class server(object): 
    def __init__(self, func): 
        print('启动server')
        self._func = func # 初始化装饰的函数 
    
    def __call__(self): 
        print ('开始回调') 
        self._func() # 调用装饰的函数 
        print ('完成回调') 
        
@server 
def request(): # 被装饰函数不带参数的情况 
    print ('执行回调...') 
    time.sleep(random.randint(3,10))

print('发出多个请求')
import threading
threadlist = []
for i in range(5):
    threadlist.append(threading.Thread(target=request))

for t in threadlist:
    t.start()

3 python的web机制

我们首先来看flask的例子,app.route指定路由,使用装饰器的方式保存了一个URL和对应的函数的映射关系。app类一直运行,接收到请求消息后,执行路由对应的回调函数index(),并将其:

from flask import  Flask
app = Flask(__name__)

@app.route('/')
def index():
    return '

Hello World!

' if __name__ == '__main__': app.run(debug=True)

再来看一个简单的webpy的例子,用一个字典指定映射关系

import web
 
urls = (
    "/.*", "Hello",
)
 
 
class Hello:
    def GET(self):
        """
        此方法,必须是大写。
        如果改成get,则报错:"HTTP/1.1 GET /" - 405 Method Not Allowed。
        :return:
        """
        return 'Hello, world!'
 
 
if __name__ == "__main__":
    app = web.application(urls, globals())
    app.run()

4. WSGI

WSGI (Web Server Gateway Interface),是为Python语言定义的Web服务器和Web应用程序之间的一种通用接口。
python系列8:python的web工程化问题_第1张图片

一个WSGI接口包括如下三个组件:

  • Server,处理请求,提供环境信息、以及一个callback 给appication,并接收web响应作为返回值;
  • Middleware,连接server和application两方,可以重写环境信息,根据目标URL,将请求路由到不同的应用对象;
  • Application,一个callable对象。
from wsgiref.simple_server import make_server

def simple_app(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [u"This is hello wsgi app".encode('utf8')]

httpd = make_server('', 8000, simple_app)
print "Serving on port 8000..."
httpd.serve_forever()

如果想要路由的话,需要使用Middleware

def exampleApplication(environ, start_response):  
    if environ['superSession'].has_key('visited'):  
        text = "You have already visited!"  
    else:  
        environ['superSession']['visited'] = 1  
        text = "This is your first visit."  
    start_response('200 OK', [('Content-type','text/plain')])  
    return [text]  
      
def session(application):  
    def app(environ, start_response):  
        if "superSession" not in environ:  
            import superSession  
            environ["superSession"] = superSession.session()  
        return application(environ, start_response)  
    return app  
      
application = session(exampleApplication)

5. Gunicorn

Gunicorn(绿色独角兽)是一个被广泛使用的高性能的Python WSGI UNIX HTTP服务器,简单来说可以利用多核的能力。Gunicorn 有一个master进程,以及几个的worker进程,master通过pre-fork的方式创建多个worker,跟Nginx的有点像。(Nginx简单来说是充分利用分布式的能力)
gunicorn 会启动一组 worker进程,所有worker进程公用一组listener,在每个worker中为每个listener建立一个wsgi server。每当有HTTP链接到来时,wsgi server创建一个协程来处理该链接,协程处理该链接的时候,先初始化WSGI环境,然后调用用户提供的app对象去处理HTTP请求。
下面是知乎的转载:https://www.zhihu.com/question/297267614/answer/505683007

Flask 作为一个 Web 框架,内置了一个 webserver, 但这自带的 Server 到底能不能用? 放官网的介绍: While lightweight and easy to use, Flask’s built-in server is not suitable for production as it doesn’t scale well. Some of the options available for properly running Flask in production are documented here.
很显然,内置的 webserver 是能用的。但不适合放在生产环境。这个 server 本来就是给开发者用的。框架本身并不提供生产环境的 web 服务器,SpringBoot 这种内置 Tomcat 生产级服务器 是例外。 我们查看 flask 代码的时候也可以看到这个 WebServer 的名称也叫做 run_simple , too simple 的东西往往不太适合生产。只有一个进程在跑所有的请求,而由于实现的简陋性,内置 webserver 很容易卡死。并且只有一个 Worker 在跑请求。在多核 CPU 下,仅仅占用一核。一言以蔽之,太弱,几个请求就打满了。
Gunicorn 作为 Server 相对而言可以有什么提升? gunicorn 的优点如下:帮我 scale worker;进程挂了帮我重启用;python 的框架 flask/django/webpy 配置起来都差不多;还有信号机制。可以支持多种配置。在管理 worker 上,使用了 pre-fork 模型,即一个 master 进程管理多个 worker 进程,所有请求和响应均由 Worker 处理。Master 进程是一个简单的 loop, 监听 worker 不同进程信号并且作出响应。比如接受到 TTIN 提升 worker 数量,TTOU 降低运行 Worker 数量。如果 worker 挂了,发出 CHLD, 则重启失败的 worker, 同步的 Worker 一次处理一个请求。 PS: 如果没有静态资源并且无需反向代理的话,抛弃 Nginx 直接使用 Gunicorn 和 Flask app 也能搞定。

这里我们说下 gunicorn 的用法,最简单的运行方式就是:
gunicorn code:application

其中code就是指code.py,application就是那个wsgifunc的名字。这样运行的话, gunicorn 默认作为一个监听 127.0.0.1:8000 的web server,可以在本机通过: http://127.0.0.1:8000 访问。

如果要通过网络访问,则需要绑定不同的地址(也可以同时设置监听端口):
gunicorn -b 10.2.20.66:8080 code:application
在多核服务器上,为了支持更多的并发访问并充分利用资源,可以使用更多的 gunicorn 进程:
gunicorn -w 8 code:application
这样就可以启动8个进程同时处理HTTP请求,提高系统的使用效率及性能。

另外, gunicorn 默认使用同步阻塞的网络模型(-k sync),对于大并发的访问可能表现不够好, 它还支持其它更好的模式,比如:gevent或meinheld。
gunicorn -k gevent code:application
gunicorn -k egg:meinheld#gunicorn_worker code:application

6. Nginx

直接通过uWSGI或gunicorn直接部署也是可以让外网访问的,那你可能会说,那要Nginx何用?Nginx作为一个高性能Web服务器,具有负载均衡、拦截静态请求、高并发…等等许多功能。
首先是负载均衡,它的作用是能够仅通过一个前端唯一的URL访问分发到后台的多个服务器,这对于并发量非常大的企业级Web站点非常有效。在实际应用中我们通常会让Nginx监听(绑定)80端口,通过多域名或者多个location分发到不同的后端应用。

其次是拦截静态请求,简单来说,Nginx会拦截到静态请求(静态文件,如图片),并交给自己处理。而动态请求内容将会通过WSGI容器交给Web应用处理;

Nginx还有其他很多的功能,这里便不一一介绍。那么前面说了,直接通过uWSGI或gunicorn也可以让外网访问到的,但是鉴于Nginx具有高性能、高并发、静态文件缓存、及以上两点、甚至还可以做到限流与访问控制,所以选择Nginx是很有必要的;

这里可以说明,如果你选择的架构是:Nginx + WSGI容器 + web应用,WSGI容器相当于一个中间件;如果选择的架构是uWSGI + web应用,WSGI容器则为一个web服务器。
关于Nginx的作用,可以看这篇测评:https://blog.csdn.net/Marcky/article/details/6624359。
测试命令为:ab -n 10000 -c 100 http://127.0.0.1:1988/ 。注意mac上跑不了这么多线程,要在linux机器上测试~

python系列8:python的web工程化问题_第2张图片
python系列8:python的web工程化问题_第3张图片

附录:压测工具

使用下面的语句压测:

ab -n 10000 -c 100 http://localhost:2000/

下面我们对这些参数,进行相关说明。如下:

-n即requests,用于指定压力测试总共的执行次数。
-c即concurrency,用于指定压力测试的并发数。
-t即timelimit,等待响应的最大时间(单位:秒)。
-b即windowsize,TCP发送/接收的缓冲大小(单位:字节)。
-p即postfile,发送POST请求时需要上传的文件,此外还必须设置-T参数。
-u即putfile,发送PUT请求时需要上传的文件,此外还必须设置-T参数。
-T即content-type,用于设置Content-Type请求头信息,例如:application/x-www-form-urlencoded,默认值为text/plain。
-v即verbosity,指定打印帮助信息的冗余级别。
-w以HTML表格形式打印结果。
-i使用HEAD请求代替GET请求。
-x插入字符串作为table标签的属性。
-y插入字符串作为tr标签的属性。
-z插入字符串作为td标签的属性。
-C添加cookie信息,例如:“Apache=1234”(可以重复该参数选项以添加多个)。
-H添加任意的请求头,例如:“Accept-Encoding: gzip”,请求头将会添加在现有的多个请求头之后(可以重复该参数选项以添加多个)。
-A添加一个基本的网络认证信息,用户名和密码之间用英文冒号隔开。
-P添加一个基本的代理认证信息,用户名和密码之间用英文冒号隔开。
-X指定使用的代理服务器和端口号,例如:“126.10.10.3:88”。
-V打印版本号并退出。
-k使用HTTP的KeepAlive特性。
-d不显示百分比。
-S不显示预估和警告信息。
-g输出结果信息到gnuplot格式的文件中。
-e输出结果信息到CSV格式的文件中。
-r指定接收到错误信息时不退出程序。

返回:

This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:        localhost
Server Hostname:        localhost
Server Port:            2000

Document Path:          /
Document Length:        28 bytes

Concurrency Level:      100 (并发数)
Time taken for tests:   35.138 seconds(压力测试总耗时)
Complete requests:      10000(压力测试总次数)
Failed requests:        0
Total transferred:      1030000 bytes(传输的总数据量)
HTML transferred:       280000 bytes(其中HTML文档的数量)
Requests per second:    284.59 [#/sec] (mean)(平均每秒的请求数)
Time per request:       351.379 [ms] (mean)(并发用户都请求一次的平均时间)
Time per request:       3.514 [ms] (mean, across all concurrent requests)(单个用户的请求时间,上面的数据除以并发数就行了)
Transfer rate:          28.63 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.5      0       7
Processing:     5  266 1142.7     19   30014
Waiting:        3  263 1142.7     16   30014
Total:          5  266 1142.7     19   30016

Percentage of the requests served within a certain time (ms)
  50%     19
  66%     24
  75%     28
  80%     33
  90%   1048
  95%   1276
  98%   3127
  99%   3543
 100%  30016 (longest request)

你可能感兴趣的:(python系列)