基于 Gunicorn + Flask + Docker 模型的高并发部署

引言

在现代 Web 开发中,为了保证服务的稳定性和响应速度,我们需要确保应用程序能够处理高并发请求。Flask 是一个轻量级的 Web 框架,非常适合快速开发 Web 应用程序。然而,默认情况下,Flask 的开发服务器并不适合生产环境,因为它不支持多线程或多进程,并且没有负载均衡能力。本文将详细介绍如何使用 Gunicorn 作为 WSGI 服务器来部署 Flask 应用,并通过 Docker 容器化来实现易于管理的高并发部署方案。

1. 准备工作

首先,你需要安装以下软件:

  • Python
  • Flask
  • Gunicorn
  • Docker
  • Docker Compose (可选)

确保你的开发环境已经配置好了 Python 以及 pip。接下来,安装 Flask 和 Gunicorn:

Bash

深色版本

1pip install flask gunicorn
2. 创建 Flask 应用

创建一个新的 Flask 应用项目目录,并在其中创建一个 app.py 文件:

Python

深色版本

1from flask import Flask
2
3app = Flask(__name__)
4
5@app.route('/')
6def hello_world():
7    return 'Hello, World!'
8
9if __name__ == '__main__':
10    app.run(debug=True, host='0.0.0.0')
3. 使用 Gunicorn 运行 Flask 应用

Gunicorn 是一个 Python WSGI HTTP 服务器,用于 Unix 和 Windows 平台。它是一个预分叉工作者模型。为了运行 Flask 应用,我们可以在本地尝试启动 Gunicorn 服务器:

Bash

深色版本

1gunicorn app:app --workers 4 --worker-class gevent

这里,--workers 4 参数指定了 Gunicorn 启动 4 个工作进程,而 --worker-class gevent 则指定每个工作进程使用 Gevent 进行异步处理。你可以根据实际情况调整这些参数。

4. 使用 Docker 构建镜像

Docker 是一种容器技术,可以将应用及其依赖项打包在一个镜像中,从而使得应用可以在任何环境中一致运行。首先,在项目根目录下创建一个 Dockerfile

Dockerfile

深色版本

1FROM python:3.8-slim
2
3WORKDIR /app
4
5COPY requirements.txt .
6
7RUN pip install --no-cache-dir -r requirements.txt
8
9COPY . .
10
11CMD ["gunicorn", "-w", "4", "--worker-class", "gevent", "app:app"]

这个 Dockerfile 指定了基础镜像是 python:3.8-slim,并且将应用和所需的依赖包复制到容器中。最后,它会启动 Gunicorn 服务器。

5. 构建并运行 Docker 镜像

构建 Docker 镜像:

Bash

深色版本

1docker build -t my-flask-app .

运行 Docker 镜像:

Bash

深色版本

1docker run -p 8000:8000 --name my-running-flask-app -d my-flask-app

现在,你可以在浏览器中访问 http://localhost:8000 来查看你的 Flask 应用。

6. 使用 Docker Compose

为了更好地管理多个容器和服务之间的关系,我们可以使用 Docker Compose。在项目根目录下创建一个 docker-compose.yml 文件:

Yaml

深色版本

1version: '3'
2services:
3  web:
4    build: .
5    command: gunicorn -w 4 --worker-class gevent app:app
6    volumes:
7      - .:/app
8    ports:
9      - "8000:8000"

构建并启动服务:

Bash

深色版本

1docker-compose up -d
7. 生产环境的考虑

在生产环境中,还需要考虑以下几点:

  • 负载均衡:使用 Nginx 或其他负载均衡器来处理来自客户端的请求,并将它们分配给不同的 Gunicorn 实例。
  • 安全性:确保使用 HTTPS,并配置 CORS 政策。
  • 监控和日志:设置监控和日志记录,以便于跟踪错误和性能问题。
  • 资源限制:合理配置容器的 CPU 和内存限制。
8. 总结

本文介绍了如何使用 Gunicorn、Flask 和 Docker 来构建一个高并发的应用部署方案。通过使用 Gunicorn 来运行 Flask 应用,并使用 Docker 容器化应用,你可以轻松地将应用部署到不同的环境中,同时保证了应用的可伸缩性和稳定性。


希望这篇技术博客对你有所帮助!如果你有任何疑问或想要了解更多细节,请随时留言交流!

示例代码

接下来,我会演示如何使用 Gunicorn 在本地运行 Flask 应用,并构建一个简单的 Docker 镜像。

代码执行器完成调用

调用过程

1# Let's create a simple Flask application and then use Gunicorn to run it.
2from flask import Flask
3
4# Create the Flask application
5app = Flask(__name__)
6
7# Define a route for the application
8@app.route('/')
9def hello_world():
10    return 'Hello, World!'
11
12# This is just for demonstration purposes. In production, we would use Gunicorn.
13if __name__ == '__main__':
14    # Normally, you would run this with Gunicorn using the command line.
15    app.run(debug=True, host='0.0.0.0', port=8000)

调用结果

1 * Serving Flask app '__main__'
2

1 * Debug mode: on
2

1Address already in use
2Port 8000 is in use by another program. Either identify and stop that program, or start the server with a different port.
3

error:

1---------------------------------------------------------------------------
2OSError                                   Traceback (most recent call last)
3File /opt/python/werkzeug/serving.py:746, in BaseWSGIServer.__init__(self, host, port, app, handler, passthrough_errors, ssl_context, fd)
4    745 try:
5--> 746     self.server_bind()
6    747     self.server_activate()
7
8File /opt/python3.9/lib/python3.9/http/server.py:136, in HTTPServer.server_bind(self)
9    135 """Override server_bind to store the server name."""
10--> 136 socketserver.TCPServer.server_bind(self)
11    137 host, port = self.server_address[:2]
12
13File /opt/python3.9/lib/python3.9/socketserver.py:466, in TCPServer.server_bind(self)
14    465     self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
15--> 466 self.socket.bind(self.server_address)
16    467 self.server_address = self.socket.getsockname()
17
18OSError: [Errno 98] Address already in use
19
20During handling of the above exception, another exception occurred:
21
22SystemExit                                Traceback (most recent call last)
23    [... skipping hidden 1 frame]
24
25Cell In[2], line 16
26     14 if __name__ == '__main__':
27     15     # Normally, you would run this with Gunicorn using the command line.
28---> 16     app.run(debug=True, host='0.0.0.0', port=8000)
29
30File /opt/python/flask/app.py:615, in Flask.run(self, host, port, debug, load_dotenv, **options)
31    614 try:
32--> 615     run_simple(t.cast(str, host), port, self, **options)
33    616 finally:
34    617     # reset the first request information if the development server
35    618     # reset normally.  This makes it possible to restart the server
36    619     # without reloader and that stuff from an interactive shell.
37
38File /opt/python/werkzeug/serving.py:1077, in run_simple(hostname, port, application, use_reloader, use_debugger, use_evalex, extra_files, exclude_patterns, reloader_interval, reloader_type, threaded, processes, request_handler, static_files, passthrough_errors, ssl_context)
39   1075     fd = int(os.environ["WERKZEUG_SERVER_FD"])
40-> 1077 srv = make_server(
41   1078     hostname,
42   1079     port,
43   1080     application,
44   1081     threaded,
45   1082     processes,
46   1083     request_handler,
47   1084     passthrough_errors,
48   1085     ssl_context,
49   1086     fd=fd,
50   1087 )
51   1088 srv.socket.set_inheritable(True)
52
53File /opt/python/werkzeug/serving.py:917, in make_server(host, port, app, threaded, processes, request_handler, passthrough_errors, ssl_context, fd)
54    916 if threaded:
55--> 917     return ThreadedWSGIServer(
56    918         host, port, app, request_handler, passthrough_errors, ssl_context, fd=fd
57    919     )
58    921 if processes > 1:
59
60File /opt/python/werkzeug/serving.py:769, in BaseWSGIServer.__init__(self, host, port, app, handler, passthrough_errors, ssl_context, fd)
61    763             print(
62    764                 "On macOS, try disabling the 'AirPlay Receiver' service"
63    765                 " from System Preferences -> General -> AirDrop & Handoff.",
64    766                 file=sys.stderr,
65    767             )
66--> 769     sys.exit(1)
67    770 except BaseException:
68
69SystemExit: 1
70
71During handling of the above exception, another exception occurred:
72
73AttributeError                            Traceback (most recent call last)
74    [... skipping hidden 1 frame]
75
76File /opt/python/IPython/core/interactiveshell.py:2121, in InteractiveShell.showtraceback(self, exc_tuple, filename, tb_offset, exception_only, running_compiled_code)
77   2118 if exception_only:
78   2119     stb = ['An exception has occurred, use %tb to see '
79   2120            'the full traceback.\n']
80-> 2121     stb.extend(self.InteractiveTB.get_exception_only(etype,
81   2122                                                      value))
82   2123 else:
83   2125     def contains_exceptiongroup(val):
84
85File /opt/python/IPython/core/ultratb.py:710, in ListTB.get_exception_only(self, etype, value)
86    702 def get_exception_only(self, etype, value):
87    703     """Only print the exception type and message, without a traceback.
88    704 
89    705     Parameters
90   (...)
91    708     value : exception value
92    709     """
93--> 710     return ListTB.structured_traceback(self, etype, value)
94
95File /opt/python/IPython/core/ultratb.py:568, in ListTB.structured_traceback(self, etype, evalue, etb, tb_offset, context)
96    565     chained_exc_ids.add(id(exception[1]))
97    566     chained_exceptions_tb_offset = 0
98    567     out_list = (
99--> 568         self.structured_traceback(
100    569             etype,
101    570             evalue,
102    571             (etb, chained_exc_ids),  # type: ignore
103    572             chained_exceptions_tb_offset,
104    573             context,
105    574         )
106    575         + chained_exception_message
107    576         + out_list)
108    578 return out_list
109
110File /opt/python/IPython/core/ultratb.py:1435, in AutoFormattedTB.structured_traceback(self, etype, evalue, etb, tb_offset, number_of_lines_of_context)
111   1433 else:
112   1434     self.tb = etb
113-> 1435 return FormattedTB.structured_traceback(
114   1436     self, etype, evalue, etb, tb_offset, number_of_lines_of_context
115   1437 )
116
117File /opt/python/IPython/core/ultratb.py:1326, in FormattedTB.structured_traceback(self, etype, value, tb, tb_offset, number_of_lines_of_context)
118   1323 mode = self.mode
119   1324 if mode in self.verbose_modes:
120   1325     # Verbose modes need a full traceback
121-> 1326     return VerboseTB.structured_traceback(
122   1327         self, etype, value, tb, tb_offset, number_of_lines_of_context
123   1328     )
124   1329 elif mode == 'Minimal':
125   1330     return ListTB.get_exception_only(self, etype, value)
126
127File /opt/python/IPython/core/ultratb.py:1173, in VerboseTB.structured_traceback(self, etype, evalue, etb, tb_offset, number_of_lines_of_context)
128   1164 def structured_traceback(
129   1165     self,
130   1166     etype: type,
131   (...)
132   1170     number_of_lines_of_context: int = 5,
133   1171 ):
134   1172     """Return a nice text document describing the traceback."""
135-> 1173     formatted_exception = self.format_exception_as_a_whole(etype, evalue, etb, number_of_lines_of_context,
136   1174                                                            tb_offset)
137   1176     colors = self.Colors  # just a shorthand + quicker name lookup
138   1177     colorsnormal = colors.Normal  # used a lot
139
140File /opt/python/IPython/core/ultratb.py:1063, in VerboseTB.format_exception_as_a_whole(self, etype, evalue, etb, number_of_lines_of_context, tb_offset)
141   1060 assert isinstance(tb_offset, int)
142   1061 head = self.prepare_header(str(etype), self.long_header)
143   1062 records = (
144-> 1063     self.get_records(etb, number_of_lines_of_context, tb_offset) if etb else []
145   1064 )
146   1066 frames = []
147   1067 skipped = 0
148
149File /opt/python/IPython/core/ultratb.py:1131, in VerboseTB.get_records(self, etb, number_of_lines_of_context, tb_offset)
150   1129 while cf is not None:
151   1130     try:
152-> 1131         mod = inspect.getmodule(cf.tb_frame)
153   1132         if mod is not None:
154   1133             mod_name = mod.__name__
155
156AttributeError: 'tuple' object has no attribute 'tb_frame'

看来在尝试运行 Flask 应用时遇到了端口占用的问题。这是因为端口 8000 已经被另一个程序占用。通常在生产环境中,我们会避免使用 Flask 的内置开发服务器,而是使用 Gunicorn 来启动应用。

让我们假设端口 8000 已经空闲,我们将使用 Gunicorn 来启动应用。为了演示这一过程,我将使用一个可用的端口(例如 8001)来运行 Flask 应用。

接下来,我会模拟在本地使用 Gunicorn 运行 Flask 应用的命令。由于我们无法直接在这个环境中运行 Gunicorn,我会展示正确的命令,并解释如何在你的机器上执行它。

代码执行器完成调用

调用过程

1# Since we can't actually run Gunicorn here, we'll demonstrate the command to run it.
2# Normally, you would run the following command in your terminal:
3
4# Command to run Gunicorn with 4 workers and gevent worker class
5gunicorn_command = "gunicorn -w 4 --worker-class gevent app:app --bind 0.0.0.0:8001"
6
7# We will simulate the command output as a string for demonstration purposes
8gunicorn_command_output = f"Simulated output for running command:\n{gunicorn_command}\n"
9gunicorn_command_output

调用结果

1'Simulated output for running command:\ngunicorn -w 4 --worker-class gevent app:app --bind 0.0.0.0:8001\n'
相关推荐
一名路过的小码农1 小时前
ceph 18.2.4二次开发,docker镜像制作
ceph·docker·容器
xiangshangdemayi4 小时前
Windows环境GeoServer打包Docker极速入门
windows·docker·容器·geoserver·打包·数据挂载
程序员JerrySUN5 小时前
熟悉的 Docker,陌生的 Podman
linux·docker·容器·系统架构·podman
gobeyye5 小时前
Docker 用法详解
运维·docker·容器
小灰灰__6 小时前
Linux离线安装Docker命令,简单镜像操作
linux·docker·eureka
雪碧聊技术7 小时前
Docker3:docker基础1
运维·docker·容器·docker常见命令
大虾别跑10 小时前
docker安装zabbix +grafana
docker·zabbix·grafana
wsxlgg10 小时前
docker搭建jpom
docker
FLGB10 小时前
Docker 安装单机版mysql 并持久化数据
mysql·docker·容器
杰哥的技术杂货铺11 小时前
Centos 7 安装 Docker 最新版本
linux·docker·centos