使用 uWSGI 和 Nginx 部署 Flask 应用

Step 0: 准备工作

  • 安装并配置 Nginx,可以参考这篇教程
  • 在 DNS 配置里加入一条 A 记录,将你的域名指向你的 IP,适用于子域名配置;

Step 1: 安装 Pip 包管理器

Setp 2: 创建 Python 虚拟环境

Python 虚拟环境有助于将不同的应用之间的 python 环境隔离;用于创建和管理虚拟环境的模块为 venv

创建项目目录并为其创建虚拟化环境

激活上面创建的虚拟化环境

Step 3: 创建 Flask 应用

安装 wheel 包,以支持 wheel(.whl) 打包格式

然后,再安装 Flask 和 uWSGI

创建 Flask Demo 应用

测试新创建的 Flask 应用

通过浏览器访问 http://server-domain-or-ip:5000 将会看到页面

Flask sample app

在控制台 CTRL-C 可以结束上面的 Server

创建 WSGI 网关

Step 4: 配置 uWSGI

直接使用 python app.py 运行服务的方式只适合本地开发,线上运行时要保证更高的性能和稳定性,需要使用 uwsgi 进行部署。

使用 uwsgi 部署 Flask 只需要换一种命令来启动服务即可

  • –socket 0.0.0.0:5000:指定暴露端口号为 5000
  • –protocol=http:使用 http 协议
  • -w wsgi:app:-w 指明了要启动的模块,wsgi 就是项目启动文件 wsgi.py 去掉扩展名,app 是 wsgi.py 文件中的变量 app,即 Flask 实例

启动完成后,在任意网络连通的机器上打开浏览器,并访问 http://server-domain-or-ip:5000 将再次看到同样结果

Flask sample app

关掉控制台,并退出虚拟环境

创建 uWSGI配置文件

Step 5: 创建 Systemd Service 配置文件

启动上面 uWSGI 服务,并让其开机自动启动

Step 6: 配置 Nginx,反向代理 uWSGI 请求

新创建一个 nginx server 配置项,通过 uwsgi_pass 将请求传递给我们在 app.ini 中配置文件指定创建的 socket

要启用该 server 配置,只要需要将其符号连接到 sites-enabled 目录:

检查新加的 nginx 配置文件是否有语法错误

重启 nginx 服务项

重启完成后,由于 Nginx 本身监听的端口是 80 端口,因此我们可以直接访问域名或者机器 IP 进行访问。

Flask sample app

参考文章

How To Serve Flask Applications with uWSGI and Nginx on Ubuntu 18.04

Chromium 定制之 FFmpeg 裁剪

一、FFmpeg 的定制

在定制 Chromium 过程中,发现 WebAudio 模块依赖于 FFmpeg,但是只用到了里面的少许接口,因此需要对 FFmepg 做裁剪。

Chromium 源码中提供了对 FFMPEG 整个模块编译的支持,并提供了相关的开关来控制,但是默认该开关是关闭的。

发现 WebAudio 中用到的几个 FFmpeg 中的接口都是 RDFT 相关。接口的实现全都在 avfft.c 这个 C 源文件中。该源文件中还有 FFT、MDCT、DCT 部分的接口实现,这些函数都被预处理指令结合诸如 CONFIG_RDFT、CONFIG_MDCT、CONFIG_DCT 之类的宏定义来控制是否参与编译。

剩下的任务便是从 FFMPEG 中 截取 RDFT 相关接口来做最小化定制。即以 avfft.c 文件出发,编译出一个可用的最小动态库,以满足 WebAudio 模块的需求。

对于 CONFIG_RDFT、CONFIG_MDCT、CONFIG_DCT 等宏的定义,Chromium 针对不同的平台,以及针对 Chrome/Chromium 商标的区分,分别作了预设的配置:

针对ARM 平台,找到了相关头文件的路径:

因此便可通过修改该头文件中相关宏的定义,来控制编译,实现针对 RDFT 模块的最小化定制。

通过不断的尝试,最终完成了 ARM 平台下 FFmpeg 的最小化定制,并且能正常工作。相关技巧见本文第三部分。

二、FFmpeg Wrapper 改造

Chromium 提供了针对 FFmpeg 中相关接口的 wrapper。编译时 WebAudio 模块中关于 FFMPEG 中的 function 调用都链接的是 wrapper function。在 wrapper 中去通过 dlopen/dlsym 的方式去加载关于 FFMPEG 的动态链接库。

继续调查发现,该 wrapper 的源文件是通过 python 脚本根据一个符号列表自动生成的。相关过程粗略介绍如下:

首先,有一个 ffmpegsumo.sigs 的文件,记录着 Chromium 使用到的 FFMPEG 中的全部接口,该文件其实就是一个 C 头文件。

编译时,通过调用 generate_stubs.py 这个脚本去逐行 parse 该文件中的符号,针对每一个 function 生成对应的 wrapper function。涉及到某些重复的逻辑会用到一些函数模板,一个典型的模板如下:

该模板根据上述符号列表文件中的每一行记录,经 parse 得到 return_type(返回值类型)、name(函数名)、params(参数)、export(可见性控制符)等符号,再去生成 wrapper function。具体输出如下:

然后这些 wrapper function 以及 dlopen、dlsym 等动作都会被组合并输出到同一个源文件中,最后编译器再去编译生成的源文件。

通过研究该模板发现一个严重的问题:在 dlopen、dlsym 失败的时候,wrapper 模块会将所有的函数指针都置空。该模板生成的 wrapper function 又没有去判断函数指针是否为空,此种情形下一旦被 call 到,就会发生 coredump。

由于 wrapper 模块的源码是由脚本生成的,要去修改相关的逻辑必须修改相应的 python 脚本中的函数模板。

首先,在 wrapper function 中 call 实际的 function 之前,必须要进行空指针的判断。否则返回一个与原返回值类型相同的默认值。根据返回值的不同,又将原有模板拆分为三个不同的模板,分别针对一般函数、void 型函数和指针函数来进行处理:

通过这三个模板生成的 wrapper function 举例如下:

经此调整,修复了 wrapper function 中原有的缺陷,避免了异常情况下的 coredump。

三、技巧总结

3.1、关于 shared libary 的编译

编译 executuble file 与 shared libary,编译器的策略是不同的。编译 executable file,编译器会从 main function 出发,只处理被使用到的所有符号;而编译 shared libary,编译器会处理参与编译的源文件中的所有符号。针对后者,只要没有语法错误与符号冲突,一般编译都会通过。但是在动态加载时(比如 dlopen ),如果目标文件中状态为 UNDEFINED(链接到其他动态库的不算)的并且作用域为 GLOBAL 的符号,就会返回错误。

如果每编译一次,就拿到开发板上测试,效率是相当低的。那么如何在编译之后,不用运行就能确认一个 shared libaray 是可被正常使用的呢?首先,编译通过是必须的。然后,通过查看其中所有对外可见的符号及其状态、作用域等信息,来进行判断。

首先可以通过 readelf 命令加 -s 参数去查找并列出目标文件 libffmpegsumo.so 中的符号。举例如下:

从以上输出中可以看到目标文件中所有符号的索引号、起始地址、Section 大小、类型、状态以及符号名等信息。在此基础上加上一些过滤条件可以找到那些会导致动态加载失败的符号。下面的命令组合可以有效完成此工作:

过滤条件“UND”表示只列出其中状态为“UNDEFINED”的符号;-v “@” 表示没有 “@” 符号,即列出没有在其他动态库中定义的符号。

最后,一个命令组合便可确定该动态库是否实际可用:

结论:输出为 1 即表示该动态库可用。

注:分号后一条指令输出前一条指令返的返回值;返回值为 1,即表示最后一条 grep 查找失败,也就是在目标文件中没有找到符合过滤条件(类型为 GLOBAL、状态为 UNDEFINED、并且没有链接到其他动态库中)的符号。

3.2、关于 LICENSE 的检查

编译中遇到的另外一个问题是源文件的 LICENSE 检查。一个直接的方法是查看源文件头部的版权声明。例如 avfft.h 这个头文件中的版权申明是这样的:

可以看到该文件使用了 LPGL(GNU Lesser General Public License)许可证。但是针对大量的源文件,这样逐个文件地检查是比较耗时的。无意间发现 Chromium 的源码中有一个 LICENCES 检查的工具 checklicense.py,它既可以检查单个文件,又可以遍历整个目录:

针对单个文件的 LICENSE 检查:

针对目录的 LICENSE 检查:

这个工具结合其他文本处理命令可以迅速完成针对第三方源码库的 LICENSE 分析。

Google Cloud VM 配置 SSH

创建实例后通过浏览器窗口打开 ssh 连接,为 root 设置密码:

修改实例的 ssh 配置文件 /etc/ssh/sshd_config

找到 PermitRootLogin 和 PasswordAuthentication ,修改如下:

重启 sshd 使配置生效:

在本地生成 ssh 公钥和私钥:

复制公钥(keyfile.pub)内容,进入谷歌云平台页面 -> 计算引擎 -> 元数据 -> SSH 密钥,粘贴保存。

接着便可以通过 ssh 客户端登录 VM 实例了:

Enjoy it~