2 Web框架
2.1基本原理
开始Web框架!
正如我多次讨论过的,Web框架的作用是将HTTP请求转换为函数调用,将函数返回值转换为HTTP响应。框架的真正本质是一个层,它通过HTTP和相关协议将工作的业务逻辑连接到Web。该框架负责我们的会话管理,并将URL映射到函数,使我们能够专注于应用逻辑。
在HTTP服务的总体方案中,应该这样认识框架。框架提供的,比如访问数据库、模板引擎和其他系统的接口,都是一个附加功能。作为程序员,你可能会发现这个附加功能很有用,但原则上它并不是我们运用框架的根本原因,我们首批美国框架是因为它充当了业务逻辑和HTTP之间的一个层。
2.2 实施
多亏了Miguel Gringberg撰写的Flask超级教程,我可以非常快地学会Flask。我不会在这里介绍整个教程,因为你可以在他的网站上阅读。我只使用第一篇文章的内容(共23篇!)创建一个非常简单的“Hello,world”应用。另外,同样的应用也可以用Django实现,推荐书籍:《跟老齐学Python:Django实战》。
要运行下面的示例,你需要一个虚拟环境,并且必须使用pip install flask
安装。如果你需要更多关于这方面的细节,请阅读相关教程。
app/__init__.py
文件是:
from flask import Flask
application = Flask(__name__)
from app import routes
app/routes.py
文件是:
from app import application
@application.route('/')
@application.route('/index')
def index():
return "Hello, world!"
你已经在这里看到了一个框架的作用。我们定义了一个index
函数,并在3行Python中将它与两个不同的URL(/
和/index
)连接起来。这给我们留出了时间和精力来正确处理业务逻辑。在本例中,这是一个革命性的“Hello, world”。
最后,service.py
文件是:
from app import application
Flask附带了一个所谓的web开发服务器(这些词现在听起来熟悉了吗?),我们可以在终端上运行它
代码语言:javascript复制$ FLASK_APP=service.py flask run
* Serving Flask app "service.py"
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL C to quit)
现在,你可以使用浏览器访问给定的URL,并查看一切是否正常运行。请记住,127.0.0.1是指“这台计算机”的特殊IP地址;操作系统通常将名称localhost
作为此IP的别名,因此这两个名称可以互换。如你所见,Flask开发服务器的标准端口是5000,因此你必须明确地提到它,否则你的浏览器将尝试访问端口80(默认的HTTP端口)。当你连接到浏览器时,将看到关于HTTP请求的一些日志消息。
127.0.0.1 - - [14/Feb/2020 14:54:27] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [14/Feb/2020 14:54:28] "GET /favicon.ico HTTP/1.1" 404 -
现在你可以同时识别这两个,因为这与我们在文章前一部分中使用小服务器得到的请求是相同的。
2.3 参考资料
这些参考资料为本节讨论的主题提供更详细的信息
- Miguel Gringberg's amazing Flask mega-tutorial[1]
- What is localhost[2]
- 本示例代码[3]
2.4 问题
很显然,我们解决了所有的问题,很多程序员就到此为止。他们学会了如何使用框架(这是一个巨大的成就!),但正如我们将很快发现的,这对于生产系统是不够的。让我们仔细看看Flask服务器的输出。上面清楚地写着:
代码语言:javascript复制WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
我们在处理任何生产系统时所面临的主要问题是性能。当我们最小化代码时,考虑一下我们如何处理JavaScript:我们有意识地混淆代码以使文件更小,但这样做的唯一目的是使文件的读取速度更快。
对于HTTP服务器来说,情况并不是完全不同。Web框架通常提供一个Web开发服务器,正如Flask所做的那样,它正确地实现了HTTP,但是效率非常低。首先,这是一个阻塞框架,这意味着如果我们的请求需要几秒钟才能被服务(例如,客户端从一个非常慢的数据库检索数据),那么任何其他请求都必须排队等待服务。这最终意味着用户将在浏览器的选项卡中看到一个下拉列表,只是摇摇头,认为我们不能建立一个现代网站。其他性能问题可能与内存管理或磁盘缓存有关。但一般而言,我们可以有把握地说,此Web服务器无法处理任何生产负载(即多个用户同时访问web站点并期望良好的服务质量)。
这并不奇怪。毕竟,我们为了专注于自己的业务,不想处理TCP/IP连接,所以我们将此委托给了维护框架的其他编码人员。而框架的作者则希望关注中间件、路由、HTTP方法的正确处理等等。他们不想花时间去优化“多用户”体验的性能。在Python世界中尤其如此(但对于Node.js来说,这一点就不那么适用了):Python不是高度面向并发的,编程风格和性能都不利于快速、无阻塞的应用程序。最近,随着异步和解释器的改进,这种情况正在发生变化,但我将这个问题留在另一篇文章中阐述。
所以,现在我们有了一个成熟的HTTP服务,我们需要让它变得如此之快,以至于用户甚至不会注意到:它没有在他们的计算机上本地运行。
3 提高并发性
3.1 基本原理
如果每当你遇到性能问题,就求助于并发。现在你会有很多问题!(见此处)
是的,并发解决了许多问题,但它也是很多问题的根源,所以我们需要找到一种最安全、不那么复杂的方法来使用它。我们基本上可能希望添加一个层,这个层以某种并发方式运行框架,而不需要更改框架本身的任何内容。
每当你不得不使不同的事物同质化的时候,就创造出一个间接的层面。这几乎解决了任何问题,只有一个问题除外。(见此处)
因此,我们需要创建一个层,让它以并发方式运行我们的服务,但我们也希望将其与服务的特定实现分离,这与我们正在使用的框架或库无关。
3.2 实施
在这种情况下,解决方案是给出Web框架必须公开的API规范,以便让独立的第三方组件可以使用它。在Python世界中,这组规则被命名为WSGI,即Web服务器网关接口,对于其他语言(如Java或Ruby),也存在这样的接口。这里提到的“网关”是系统框架之外的部分,在本文中讨论的是处理生产性能的部分。通过WSGI,我们为框架定义了一种公开公共接口的方式,使得对并发感兴趣的人可以自由地独立实现一些东西。
如果框架与网关接口兼容,我们可以添加处理并发性的软件,并通过兼容层使用框架。这样的组件是一个可用于生产的HTTP服务器,在Python世界中有两个常见的选择是Gunicorn和uWSGI。
可用于生产的HTTP服务器意味着软件可以像开发服务器那样理解HTTP,但同时为了支持更大的工作负载而提高性能,正如我们之前所说的,这是通过并发完成的。
Flask与WSGI兼容,所以我们可以让它与Gunicorn一起工作。要在我们的虚拟环境中安装它,请运行pip install gunicorn
,并设置它。创建一个名为wsgi.py
的文件,其中包含以下内容
from app import application
if __name__ == "__main__":
application.run()
要运行Gunicorn,请指定并发实例的数目和外部端口
代码语言:javascript复制$ gunicorn --workers 3 --bind 0.0.0.0:8000 wsgi
[2020-02-12 18:39:07 0000] [13393] [INFO] Starting gunicorn 20.0.4
[2020-02-12 18:39:07 0000] [13393] [INFO] Listening at: http://0.0.0.0:8000 (13393)
[2020-02-12 18:39:07 0000] [13393] [INFO] Using worker: sync
[2020-02-12 18:39:07 0000] [13396] [INFO] Booting worker with pid: 13396
[2020-02-12 18:39:07 0000] [13397] [INFO] Booting worker with pid: 13397
[2020-02-12 18:39:07 0000] [13398] [INFO] Booting worker with pid: 13398
如你所见,Gunicorn有worker模式,它是实现并发性的一种通用方法。具体来说,Gunicorn实现了一个pre-fork worker模式,这意味着它为每个worker预先创建了一个不同的Unix进程。你可以用ps
命令查看进程。
$ ps ax | grep gunicorn
14919 pts/1 S 0:00 ~/venv3/bin/python3 ~/venv3/bin/gunicorn --workers 3 --bind 0.0.0.0:8000 wsgi
14922 pts/1 S 0:00 ~/venv3/bin/python3 ~/venv3/bin/gunicorn --workers 3 --bind 0.0.0.0:8000 wsgi
14923 pts/1 S 0:00 ~/venv3/bin/python3 ~/venv3/bin/gunicorn --workers 3 --bind 0.0.0.0:8000 wsgi
14924 pts/1 S 0:00 ~/venv3/bin/python3 ~/venv3/bin/gunicorn --workers 3 --bind 0.0.0.0:8000 wsgi
在Unix系统中,使用进程只是实现并发的两种方法之一,另一种是使用线程。然而,每种解决方案的优点和缺点都不在本文的讨论范围之内。目前只需记住,你正在应对多个worker,这些worker异步处理传入的请求,从而实现一个非阻塞服务器,准备接受多个连接。
3.3 参考资料
这些资源为本节讨论的主题提供了更详细的信息
- The WSGI official documentation[4] and the Wikipedia page[5]
- The homepages of Gunicorn[6] and uWSGI[7]
- A good entry point for your journey into the crazy world of concurrency: [multithreading](https://en.wikipedia.org/wiki/Multithreading_(computer_architecture "multithreading")).
- 本例代码[8]
3.4 问题
使用Gunicorn,我们现在已经有了一个用于生产的HTTP服务器,并且显然实现了我们需要的一切。不过,仍有许多考虑因素和缺失的部分。
再回到性能
3个worker是否足以支撑我们新的杀手级的移动应用程序的负载?预计每分钟有成千上万的访问者,所以也许我们应该增加一些线程。但是,当我们增加线程的数量时,必须记住,我们正在使用的机器具有有限的CPU功率和内存。因此,我们必须再次关注性能,特别是可伸缩性:如何在不停止应用程序的情况下继续添加线程,用更强大的电脑替换现有的电脑,还是重新启动服务?
积极迎接变化
这不是我们在生产中必须面对的唯一问题。技术的一个重要方面是:随着新的、(希望是)更好的解决方案的普及,它会随着时间的推移而改变。我们设计系统时,通常尽可能把它们划分为多个通信层,正是因为我们希望能够自由地用其他的东西来替换一个层,无论是用更简单的组件还是更高级的组件,一个性能更好的组件,或者可能只是一个更便宜的组件。所以,再一次,我们希望能够优化底层系统,但要保持相同的界面,就像我们在Web框架中所做的那样。
HTTPS
系统中缺少的另一部分是HTTPS。Gunicorn和uWSGI不支持HTTPS协议,因此我们需要另外有一些东西来处理协议的“S”部分,将“HTTP”部分留给内部层。
负载均衡器
一般来说,负载均衡只是系统中的一个组件,它将工作分配给一个线程池。Gunicorn已经可以在它的工作线程之间分配负载了,所以这不是一个新的概念,但是我们通常希望在更大的层次上,在机器之间或者整个系统之间这样做。负载均衡可以是分层的,并且可以在多个级别上进行结构化。我们还可以为系统的某些组件标记为准备接受更多的负载(例如,因为它们的硬件更好)。负载均衡在网络服务中是非常重要的,而且负载的定义在不同的系统之间可能有很大的不同:一般来说,在Web服务中,连接的数量是负载的标准度量,因为我们假设:平均来说,所有连接都会给系统带来相同的负荷。
反向代理
负载均衡是转发代理,因为它们允许客户端联系池中的任何服务器。同时,反向代理允许客户端通过同一入口点检索多个系统生成的数据。反向代理是一个完美的方法,它将HTTP请求转发到可以用不同技术实现的子系统,例如,你可能希望用Python、Django和Postgres实现系统的一部分,用Go语言中的AWS Lambda函数实现另一部分,并与非关系数据库(如DynamoDB)连接。通常,在HTTP服务中,这个选择是根据URL做出的(例如,路由以/api/开头的每个URL)。
逻辑层
我们还需要一个可以实现一定数量逻辑的层来管理简单规则,这些规则与我们实现的服务无关。一个典型的例子是HTTP重定向:如果用户访问服务时使用的前缀是http://而不是https://,会发生什么?正确的方法是通过HTTP 301代码来处理问题,但是你不希望这样的请求影响你的框架,这样简单的任务会浪费资源。
(未完,待续)
阅读链接
- 剖析Web技术栈(一)
- 剖析Web技术栈(二)
参考资料
[1]
Miguel Gringberg's amazing Flask mega-tutorial: https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world
[2]
What is localhost: https://en.wikipedia.org/wiki/Localhost
[3]
本示例代码: https://github.com/lgiordani/dissecting-a-web-stack-code/tree/master/2_web_framework
[4]
WSGI official documentation: https://wsgi.readthedocs.io/en/latest/index.html
[5]
Wikipedia page: https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface
[6]
Gunicorn: https://gunicorn.org/
[7]
uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/
[8]
本例代码: https://github.com/lgiordani/dissecting-a-web-stack-code/tree/master/3_concurrency_and_facades