1.django、flask高并发部署
1.1 协程产生背景
由于Python有把大锁GIL,会将多个线程在同一时刻,只能有一个线程执行,变成'串行',所以一个多线程python进程,并不能充分使用多核CPU资源,所以对于Python进程,可采用多进程部署方式比较有利于充分利用多核的CPU资源,而uWSGI服务器就是这么一个东西,可以以多进程方式执行WSGI app,其工作模式为 1 master进程 + N worker进程+m个线程(N*m线程),主进程负责接收客户端请求,然后将请求转发给worker进程,因此最终是worker进程负责处理客户端请求,这样很方便的将WSGI app以多进程方式进行部署此方式是企业常用部署方式。
但是存在一个问题:客户端向uwsgi的一个master发起一个HTTP请求,master分发给不同的worker进程,worker进程拉起线程处理请求,进入web框架,直到该请求处理完,这个线程才能处理其他请求(现在还没有协程),所以wsgi app是同步的。假如一个请求处理花费时间比较久,客户端请求数量又比较多的情况下,所有的线程都会被占满,所以就处理不了更多的请求(最大连接数取决于进程N*线程M)
我们的处理方案:
1)增大N,即worker的数量:在增加进程的数量的时候,进程是要消耗内存的,并且如果进程数量太多的情况下(并且进程均处于活跃状态),进程间的切换会消耗系统资源的,所以N并不是越大越好。一般情况下,可能将进程数目设置为CPU数量的2倍
2)增大m,即worker的线程数量:线程创建也是有限度的,由于线程栈是要消耗内存的,线程数量太多也不行
于是我们想,一条线程能不能同时处理多个请求呢?可以使用IO多路复用的模型
于是,gevent登场了
gevent是用户态的'线程',也就是协程,单线程下实现并发
gevent的好处就是无需等待I/O,当发生I/O调用是,gevent会主动切换到另一gevent进行运行,这样可以充分利用CPU资源
同时gevent通过monkey patch(猴子补丁)替换掉了标准库中阻塞的模块。
gevent是用户态的'线程',也就是协程,单线程下实现并发
gevent的好处就是无需等待I/O,当发生I/O调用是,gevent会主动切换到另一gevent进行运行,这样可以充分利用CPU资源
根据以上操作可以显著提高并发。
详见:https://zhuanlan.zhihu.com/p/358163330
1.2 不能在框架中使用全局变量
一般在部署Django或者Flask时,我们为了利用多核优势,一般使用uwsgi部署,原理如下:
如果我们设定uwsgi进程数为3,那么操作系统是开启3个进程来运行python的web程序。
如果我们在web项目中使用全局变量,由于多进程间数据是隔离的,所以定义的全局变量,分别在3个进程中,每个进程修改的也只是自己局部的变量。
举个例子:我们统计访问量,全局有一个访问量a=0。第一次请求来时分配到了进程A,进程A拿到全局变量加1,此时a=1是进程A自己局部的。此时第二个请求B被分配到了进程B,拿到全局变量,此时全局的a是0,进程B拿到加1结果还是1。由于进程间数据是隔离的,每个进程修改的a也只是自己内的a而不是全局的。
2.信号(signal)
2.1 补充:信号量
信号量和信号无关。之前我们在执行任务的时候由于有gil锁的存在,若干个线程会依次执行。但是如果有了信号量,在信号量中传入数字,假如是5,相当于同时获得了5把锁,5条线程会优先执行,5个程序执行完毕之后其他线程会抢这5把锁继续执行:
from threading import Thread, Lock, Semaphore
import time
import random
sp = Semaphore(5)
class MyThread(Thread):
def run(self):
sp.acquire()
print(self.name)
time.sleep(random.randint(1, 3))
sp.release()
for i in range(10):
t = MyThread()
t.start()
2.2 信号
Flask框架中的信号基于blinker(模块需要安装),其作用是让开发者在flask中定制一些用户行为,比如在请求进来的时候到指定的某个位置执行某个函数。
比如用户表新增一条记录,就记录以下日志。我们可以直接在orm语句下执行操作日志的代码,但是这样代码的侵入性太高,也就是如果我们要删除只能去每个函数中找到该行代码进行删除。而使用信号我们直接写一个函数,绑定内置信号即可,只要函数执行到指定位置就会执行(同样请求扩展也可以完成该需求):
flask内置信号:
request_started = _signals.signal('request-started') # 请求到来前执行
request_finished = _signals.signal('request-finished') # 请求结束后执行
before_render_template = _signals.signal('before-render-template') # 模板渲染前执行
template_rendered = _signals.signal('template-rendered') # 模板渲染后执行
got_request_exception = _signals.signal('got-request-exception') # 请求执行出现异常时执行
request_tearing_down = _signals.signal('request-tearing-down') # 请求执行完毕后自动执行(无论成功与否)
appcontext_tearing_down = _signals.signal('appcontext-tearing-down')# 应用上下文执行完毕后自动执行(无论成功与否)
appcontext_pushed = _signals.signal('appcontext-pushed') # 应用上下文push时执行
appcontext_popped = _signals.signal('appcontext-popped') # 应用上下文pop时执行
message_flashed = _signals.signal('message-flashed')
使用内置信号:
1.写一个函数(调用时系统会自动传入参数,所以要用可变长形参接收)
2 绑定内置信号
3.等待被触发
from flask import Flask,render_template,signals
app = Flask(__name__)
def test(*args,**kwargs):
print(args)
print(kwargs)
print('test执行了')
# 2.绑定内置信号
signals.before_render_template.connect(test)
# 1.写一个函数
@app.route('/')
def hello():
return render_template('index.html',name='max')
if __name__ == '__main__':
app.run()
2.2 自定义信号
以上我们使用的都是内置信号,我们还可以自定义信号:
顺序:
1.定义出信号
2.写一个函数
3.绑定自定义的信号
4.触发信号的执行
代码实现:
from flask.signals import _signals
from flask import session, Flask, render_template
app = Flask(__name__)
app.secret_key = 'fhyrue6t78y34'
# 1.定义出信号
session_set = _signals.signal('session_set')
# 2.写一个函数
def index(*args, **kwargs):
print('111')
print(args)
print(kwargs)
print('session设置值了')
# 3.绑定自定义的信号
session_set.connect(index)
# session_set.send('max')
# 如果设置在函数外面项目一启动就会触发
@app.route('/')
def index1():
session['name'] = 'max'
# 4.触发信号的执行
session_set.send('max')
return 'index1页面'
if __name__ == '__main__':
app.run()
访问根目录就可以触发index函数执行:
3.django信号
3.1 内置信号
观察者模式,又叫发布-订阅模式,类似于单例模式是23种设计模式之一。当发生一些动作的时候,发出信号,然后监听了这个信号的函数就会执行。通俗来讲,就是一些动作发生的时候,信号允许特定的发送者去提醒一些接受者。用于在框架执行操作时解耦(代码尽量精简,分成多个方法写,也就是代码侵入性低)。
django内置信号:
Model signals
pre_init # django的model执行其构造方法前,自动触发
post_init # django的modal执行其构造方法后,自动触发
pre_save # django的modal对象保存前,自动触发
post_save # django的modal对象保存后,自动触发。为了保持双写一致性,可以在数据新增之后删除缓存
pre_delete # django的modal对象删除前,自动触发
post_delete # django的modal对象删除后,自动触发
m2m_changed # django的modal中使用m2m字段操作第三张表(add,remove,clear)前后,自动触发
class_prepared # 程序启动时,检测已注册的app中modal类,对于每一个类,自动触发
Management signals
pre_migrate # 执行migrate命令前,自动触发
post_migrate # 执行migrate命令后,自动触发
Request/response signals
request_started # 请求到来前,自动触发
request_finished # 请求结束后,自动触发
got_request_exception # 请求异常后,自动触发
Database Wrappers
connection_created # 创建数据库连接时,自动触发
3.2 使用信号
方式一:
将以下内容放到utils或一个包的__init__.py中:
from django.db.models.signals import pre_save
import logging
def callBack(sender, **kwargs):
print(sender)
print(kwargs)
# 创建对象写日志
logging.basicConfig(level=logging.DEBUG)
# logging.error('%s创建了一个%s对象'%(sender._meta.db_table,kwargs.get('instance').title))
logging.debug('%s创建了一个%s对象'%(sender._meta.model_name,kwargs.get('instance').title))
pre_save.connect(callBack)
方式二:
from django.db.models.signals import pre_save
from django.dispatch import receiver
@receiver(pre_save)
def my_callback(sender, **kwargs):
print("对象创建成功")
print(sender)
print(kwargs)
3.3 django自定义信号
1.定义信号(一般创建一个py文件)(toppings,size 是接受的参数)
import django.dispatch
pizza_done = django.dispatch.Signal(providing_args=["toppings", "size"])
2.注册信号
def callback(sender, **kwargs):
print("callback")
print(sender,kwargs)
pizza_done.connect(callback)
3.触发信号
from 路径 import pizza_done
pizza_done.send(sender='seven',toppings=123, size=456)
4.flask-script
在django中,启动项目可以使用命令:
python manage.py runserver
我们想把flask做成像django一样用命令启动:
1.安装:
pip3.10 install flask-script
# 注意:flask需要使用2.2.2版本,flask_script需要使用2.0.3版本
2.修改代码:
'''当前文件名修改为manage.py'''
from flask import Flask,render_template
from flask_script import Manager
app = Flask(__name__)
manager = Manager(app)
@app.route('/')
def hello():
return render_template('index.html',name='max')
if __name__ == '__main__':
# 需要改成Manager对象名点run()
manager.run()
首先我们执行命令的路径需要在当前文件的目录下,其次我们的文件名为什么,就需要执行 python 文件名.py runserver,文件名为manage111那么执行命令就为:
python manage111.py runserver
了解了命令的前半段,我们现在需要了解runserver的由来以及定义:
首先我们需要自定义命令需要使用@manager.command装饰器(manager是类Manager的对象,命名成任何名字都可以,代替这里的manager就可以),在装饰器下写一个函数,函数名就是替代runserver的关键字,并且该函数可以接受一个参数,该参数就是在命令行函数名后面传入的:
1.制作一个简单的命令:
from flask import Flask
from flask_script import Manager
app = Flask(__name__)
manager = Manager(app)
@manager.command
def func(arg):
print(arg)
@app.route('/')
def index():
return 'indexxxx'
if __name__ == '__main__':
manager.run()
我们的函数名为maxmax.py,这时我们的命令就变成了:
python maxmax.py func max
并且这时func函数的参数args就是我们在命令行输入的max:
此后我们可以根据命令进行一些操作。
2.复杂命令:
from flask import Flask
from flask_script import Manager
app = Flask(__name__)
manager = Manager(app)
@manager.option('-n','--name',dest='name')
@manager.option('-u','--url',dest='url')
def test(name,url):
print(name,url)
if __name__ == '__main__':
manager.run()
此后我们可以通过
python maxmax.py test -n max -u xxx
或者
python maxmax.py test --name max --url xxx
的方式来触发: