上一篇我们介绍了Flask框架,并在开发环境运行了一个简单的本地应用。而在实际生产环境中,Flask往往用来做独立的应用部署,例如Web网站,或者接口服务。我们这里,就尝试基于Docker部署一个Flask的服务容器。

1 准备工作

在CentOS下安装Flask和Docker,这里就不赘述了,我版本分别是:

Python 3.6
Flask 1.1.2
Werkzeug 1.0.1
Docker version 17.09.1-ce, build 19e2cf6

2 应用代码

创建一个目录,并在该目录下分别创建文件及子目录,结构如下:

./
├── Dockerfile        #docker制作文件
└── app
    ├── app
    │   ├── main.py   #应用主入口文件
    │   ├── static    #目录,用于存放静态文件
    │   └── templates #模板,用于存放视图模板
    └── uwsgi.ini     #uwsgi配置文件

我们分别来看一下

2.1 main.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Description

Date: 2021/3/15 11:50 上午
"""

app = Flask(__name__)


@app.route('/', methods=['GET'])
def hello():
    return 'Hello World From Flask!'


if __name__ == '__main__':
    # Only for debugging while developing
    app.run(host='0.0.0.0', port=8080)

这里很简单,就是返回一个字符串。

2.2 uwsgi.ini

[uwsgi]
module = app.main
callable = app
master = true
processes = 16

分别解释一下

  • module:加载一个WSGI模块,这里加载的是与uwsgi.ini文件同级的app目录下的main.py这个模块;
  • callable:uWSGI加载的模块()中哪个应用变量将被调用, 对应上面main.py文件中,Flask所创建的应用变量app;
  • master:是否启动主进程,来管理其他进程,其它的uwsgi进程都是master进程的子进程,如果kill master进程,相当于重启所有的uwsgi进程;
  • processes:启动的进程数;

其他常用配置

  • threads:每个进程启动的线程数。
  • workers:启动的worker数,和processes效果相同;
  • daemonize = /var/log/myapp_uwsgi.log  使进程在后台运行,并将日志打到指定的日志文件或者udp服务器
  • max-requests = 5000   为每个工作进程设置请求数的上限。当一个工作进程处理的请求数达到这个值,那么该工作进程就会被回收重用(重启)。你可以使用这个选项来默默地对抗内存泄漏
  • reload-mercy = 8:设置在平滑的重启(直到接收到的请求处理完才重启)一个工作子进程中,等待这个工作结束的最长秒数。这个配置会使在平滑地重启工作子进程中,如果工作进程结束时间超过了8秒就会被强行结束(忽略之前已经接收到的请求而直接结束)

更多参数,可详见官方文档

2.3 Dockerfile

# 基础镜像
FROM tiangolo/uwsgi-nginx-flask:python3.6-alpine3.7

COPY app /app

EXPOSE 8080

3 制作镜像

在项目目录(我这里是test),执行命令

docker build -t test_image .

输出

Sending build context to Docker daemon  14.34kB
Step 1/3 : FROM tiangolo/uwsgi-nginx-flask:python3.6-alpine3.7
python3.6-alpine3.7: Pulling from tiangolo/uwsgi-nginx-flask
48ecbb6b270e: Pull complete
692f29ee68fa: Pull complete
f75fc7ac1098: Pull complete
c30e40bb471c: Pull complete
51a8cc25b36b: Pull complete
074ffb62a7a7: Pull complete
4adf1a1570c9: Pull complete
b85c5544f47f: Pull complete
4b692052c830: Pull complete
6c397707523a: Pull complete
8f024010f4fc: Pull complete
71a2c0a6f4c0: Pull complete
84153fe0f140: Pull complete
2f415e7b7d03: Pull complete
861b676195c0: Pull complete
7a0bf11a12e0: Pull complete
39035b694307: Pull complete
b99491632f33: Pull complete
562bd63da354: Pull complete
270a925c68d3: Pull complete
e2a1178baba1: Pull complete
Digest: sha256:5e13ef9c28578290a825b42a863d223a4b02b8e8d101e18d0ea463bb45878103
Status: Downloaded newer image for tiangolo/uwsgi-nginx-flask:python3.6-alpine3.7
 ---> cdec3b0d8f20
Step 2/3 : COPY app /app
 ---> 57b55cb280d5
Step 3/3 : EXPOSE 8080
 ---> Running in 059033350a5a
 ---> 7c09a8fe084c
Removing intermediate container 059033350a5a
Successfully built 7c09a8fe084c
Successfully tagged test_image:latest

确认下

docker images

输出

REPOSITORY         TAG             IMAGE ID            CREATED             SIZE
test_image         latest          7c09a8fe084c        2 hours ago         189MB

4 测试

4.1 运行镜像

docker run -it --rm -p 8081:8080 test_image

这里将主机的8081端口的请求,转发映射到容器的8080端口。输出

Checking for script in /app/prestart.sh
Running script /app/prestart.sh
Running inside /app/prestart.sh, you could add migrations to this file, e.g.:

#! /usr/bin/env sh

# Let the DB start
sleep 10;
# Run migrations
alembic upgrade head

/usr/lib/python2.7/site-packages/supervisor/options.py:298: UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations (including its current working directory); you probably want to specify a "-c" argument specifying an absolute path to a configuration file for improved security.
  'Supervisord is running as root and it is searching '
2021-03-15 07:43:42,808 CRIT Supervisor running as root (no user in config file)
2021-03-15 07:43:42,808 INFO Included extra file "/etc/supervisor.d/supervisord.ini" during parsing
2021-03-15 07:43:42,815 INFO RPC interface 'supervisor' initialized
2021-03-15 07:43:42,815 CRIT Server 'unix_http_server' running without any HTTP authentication checking
2021-03-15 07:43:42,816 INFO supervisord started with pid 1
2021-03-15 07:43:43,818 INFO spawned: 'nginx' with pid 10
2021-03-15 07:43:43,820 INFO spawned: 'uwsgi' with pid 11
[uWSGI] getting INI configuration from /app/uwsgi.ini
[uWSGI] getting INI configuration from /etc/uwsgi/uwsgi.ini

;uWSGI instance configuration
[uwsgi]
cheaper = 2
processes = 16
ini = /app/uwsgi.ini
module = app.main
callable = app
master = true
plugin = python3
ini = /etc/uwsgi/uwsgi.ini
socket = /tmp/uwsgi.sock
chown-socket = nginx:nginx
chmod-socket = 664
hook-master-start = unix_signal:15 gracefully_kill_them_all
need-app = true
die-on-term = true
show-config = true
;end of configuration

*** Starting uWSGI 2.0.17 (64bit) on [Mon Mar 15 07:43:43 2021] ***
compiled with version: 6.4.0 on 27 March 2018 12:43:27
os: Linux-3.10.0.514.26.2.el7.x86_64 #4 SMP Wed Aug 16 17:09:53 CST 2017
nodename: 737e365a4225
machine: x86_64
clock source: unix
pcre jit disabled
detected number of CPU cores: 4
current working directory: /app
detected binary path: /usr/sbin/uwsgi
your processes number limit is 62742
your memory page size is 4096 bytes
detected max file descriptor number: 1024000
lock engine: pthread robust mutexes
thunder lock: disabled (you can enable it with --thunder-lock)
uwsgi socket 0 bound to UNIX address /tmp/uwsgi.sock fd 3
uWSGI running as root, you can use --uid/--gid/--chroot options
*** WARNING: you are running uWSGI as root !!! (use the --uid flag) ***
Python version: 3.6.9 (default, Oct 17 2019, 12:14:22)  [GCC 6.4.0]
*** Python threads support is disabled. You can enable it with --enable-threads ***
Python main interpreter initialized at 0x7fdba6baaf40
uWSGI running as root, you can use --uid/--gid/--chroot options
*** WARNING: you are running uWSGI as root !!! (use the --uid flag) ***
your server socket listen backlog is limited to 100 connections
your mercy for graceful operations on workers is 60 seconds
mapped 1239640 bytes (1210 KB) for 16 cores
*** Operational MODE: preforking ***
WSGI app 0 (mountpoint='') ready in 1 seconds on interpreter 0x7fdba6baaf40 pid: 11 (default app)
uWSGI running as root, you can use --uid/--gid/--chroot options
*** WARNING: you are running uWSGI as root !!! (use the --uid flag) ***
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI master process (pid: 11)
spawned uWSGI worker 1 (pid: 13, cores: 1)
spawned uWSGI worker 2 (pid: 14, cores: 1)
running "unix_signal:15 gracefully_kill_them_all" (master-start)...
2021-03-15 07:43:45,067 INFO success: nginx entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
2021-03-15 07:43:45,067 INFO success: uwsgi entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)

看到最后面,nginx 和 uwsgi 都启动成功了。确认一下

docker ps -a

CONTAINER ID   IMAGE         COMMAND                  CREATED         STATUS          PORTS                                     NAMES
5c5c761a8a0b   test_image    "/entrypoint.sh /s..."   5 seconds ago   Up 4 seconds    80/tcp, 443/tcp, 0.0.0.0:8081->8080/tcp   awesome_hodgkin

可以看到,这里的PORTS,80和443都暴露出来了,外面的8081指向容器内的8080端口。

4.2 请求

使用curl命令

curl -v GET 127.0.0.1:8081

输出

* About to connect() to 127.0.0.1 port 8081 (#0)
*   Trying 127.0.0.1... connected
* Connected to 127.0.0.1 (127.0.0.1) port 8081 (#0)
> PUT / HTTP/1.1
> User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.13.1.0 zlib/1.2.3 libidn/1.18 libssh2/1.2.2
> Host: 127.0.0.1:8081
> Accept: */*
> Content-Length: 16
> Content-Type: application/x-www-form-urlencoded
>
* Closing connection #0
* Failure when receiving data from the peer
curl: (56) Failure when receiving data from the peer

报CURL 56错误 ,接收网络数据错误。

4.3 排查

我们进到容器里面看看

docker exec -it 5c5c761a8a0b sh

发现app目录下,多了几个文件,目录结构变成了:

./
├── app
│   ├── app
│   │   ├── __pycache__               #应用生成的缓存文件目录
│   │   │   └── main.cpython-36.pyc   #应用主入口文件的缓存文件
│   │   └── main.py                   #应用主入口文件
│   ├──
├── main.py             #项目入口
├── prestart.sh
├── supervisord.pid
└── uwsgi.ini           #uwsgi配置文件

其中与app目录同级的,多了一个main.py文件,内容如下

import sys

from flask import Flask

app = Flask(__name__)


@app.route("/")
def hello():
    version = "{}.{}".format(sys.version_info.major, sys.version_info.minor)
    message = "Hello World from Flask in a uWSGI Nginx Docker container with Python {} (default)".format(
        version
    )
    return message


if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True, port=80)

看起来,这个文件把Dockerfile所指定暴露的8080端口给屏蔽了,只对外暴露80端口了。

我们改一下端口,重新启动试一下。

docker run -it --rm -p 8081:80 test_image

 查看容器

CONTAINER ID    IMAGE         COMMAND                  CREATED         STATUS          PORTS                                     NAMES
c2e2c0edbe4c    test_image    "/entrypoint.sh /s..."   22 seconds ago  Up 20 seconds   443/tcp, 8080/tcp, 0.0.0.0:8081->80/tcp   quizzical_jang

 发现,这里确实暴露了443和8080端口,同时把主机的8081端口指向到容器内部的80端口。

再请求一下

curl -v GET 127.0.0.1:8081

返回 

* About to connect() to 127.0.0.1 port 8081 (#0)
*   Trying 127.0.0.1... connected
* Connected to 127.0.0.1 (127.0.0.1) port 8081 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.13.1.0 zlib/1.2.3 libidn/1.18 libssh2/1.2.2
> Host: 127.0.0.1:8081
> Accept: */*
> Content-Length: 0
> Content-Type: application/text
>
< HTTP/1.1 200 OK
< Server: nginx/1.15.3
< Date: Mon, 15 Mar 2021 08:23:28 GMT
< Content-Type: application/text
< Content-Length: 24
< Connection: keep-alive
<
"Hello World From Flask!"

成功返回了main.py文件中return的字符串。同时镜像运行也输出

[pid: 19|app: 0|req: 1/1] 172.30.0.1 () {36 vars in 539 bytes} [Mon Mar 15 08:23:28 2021] GET / => generated 24 bytes in 4 msecs (HTTP/1.1 200) 2 headers in 71 bytes (1 switches on core 0)
172.30.0.1 - - [15/Mar/2021:08:23:28 +0000] "GET / HTTP/1.1" 200 24 "-" "curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.13.1.0 zlib/1.2.3 libidn/1.18 libssh2/1.2.2" "-"

说明我们启动容器的时候,uwsgi自动把对外的端口改成了80和443,外部端口只能映射到这两个端口,请求才能顺利到达代码层。而Dockerfile中的EXPOSE命令似乎没有起作用。

至此,我们就用Docker把一个简单的Flask应用作为服务容器进行了部署。

后续的关于镜像推送和部署的,请参考之前的docker相关文章

04-16 22:54