前言

本文作者:远哥制造

一、什么是 Serverless Framework

Serverless Framework 是业界非常受欢迎的无服务器应用框架,开发者无需关心底层资源即可部署完整可用的 Serverless 应用架构。Serverless Framework 具有资源编排、自动伸缩、事件驱动等能力,覆盖编码、调试、测试、部署等全生命周期,帮助开发者通过联动云资源,迅速构建 Serverless 应用

没错,就像几天前看到的《Serverless 之歌》里面所说 I'm gonna reduce your ops,它能大幅度减轻运维压力,那就开始动手吧!注意开发环境需 Node.js 10.0+,一键全局安装:npm install -g serverless

二、腾讯云 Flask Serverless Component 简介

腾讯云 Flask Serverless Component,支持 Restful API 服务的部署

按照惯例首先来部署 demo 吧

  1. 本地 PyCharm 创建一个新的 Flask 项目

二次元看过来!基于 Serverless 的舞萌音游查分器_Serverless

  1. 手动创建内容为 Flask 的 requirements.txt

  2. 按照配置文档创建 serverless.yml,例如本项目实际使用的完整内容,初次使用可自行酌情简化

  3. 将密匙写入 .env(当然,部署的时候也可以选择微信扫码授权)

TENCENT_SECRET_ID=TENCENT_SECRET_KEY=

二次元看过来!基于 Serverless 的舞萌音游查分器_Serverless_02

二次元看过来!基于 Serverless 的舞萌音游查分器_Serverless_03

这样基于 Serverless 的 Flask Demo 就部署完成了,接下来继续按照自己的方式写剩下的代码。

三、maimai_DX

maimai 是一款街机音游。

二次元看过来!基于 Serverless 的舞萌音游查分器_Serverless_04

在这里放一张动图自行体会一下,原始素材来自「外录 maimai」QZKago Requiem Re:MASTER ALLPERFECT Player: Ruri*R

二次元看过来!基于 Serverless 的舞萌音游查分器_Serverless_05

  • 日本官网
  • 海外官网

在国内,只能从微信公众号中查看成绩,而且每次进页面都需要微信的授权登录,并且里面存储的记录有条数限制,相册只存最新 10 条,游戏记录只存最新 50 条(就是一个队列,先进先出的那种)。这就是本项目的初衷,自己打出来的每一次成绩都应该保存好。

舞萌查分器

成果展示了,前端 Fomantic-UI,后端 Flask+MySQL。gh 开源地址:https://github.com/yuangezhizao/maimai_DX_CN_probe,欢迎 watch、star、fork & pr!

二次元看过来!基于 Serverless 的舞萌音游查分器_二次元_06

目前实装了如下功能:

  1. wechat_archive中包含 主页,游戏数据,相册 和 游戏记录:对原始网页进行了修改,并且添加了 Highcharts 库可视化曲线显示变化
  2. record包含 记录(分页) 和 差异(分页):即自写的快速预览页面,是查看历史记录和成绩变化的非常实用的功能
  3. info包含 铺面列表:即全部铺面基础信息,输出到一个页面中,方便页面内搜索

开发过程

接下来将按照时间的顺序,描述一下开发过程中遇到的问题以及如何解决

1. Serverless Framework Component 配置文件

Serverless Framework 现在是 V2 版本,也就是说不能沿袭之前版本的 serverless.yml 配置文件,需要重新对照文档修改。

a. 之前版本会根据 requirements.txt 自动下载第三方库到项目目录下的 .serverless 文件夹下的 requirements 文件夹以参加最终的依赖打包,压缩成 zip 文件再最终上传至云函数运行环境

b. 最新版本不再自动下载,需要自行处理。官方示例的参考用法:hook

  src:
    # TODO: 安装python项目依赖到项目当前目录
    hook: 'pip3 install -r requirements.txt -t ./requirements'
    dist: ./
    include:
      - source: ./requirements
        prefix: ../ # prefix, can make ./requirements files/dir to ./
    exclude:
      - .env
      - 'requirements/**'

注释写的很清楚,使用 hook 去根据 requirements.txt 下载第三方库到项目目录下的 requirements 文件夹,避免第三方库导致本地文件夹管理混乱。然后 include 中指定了项目目录下的 requirements 文件夹在云端的 prefix,即对于云端的云函数运行环境,requirements 文件夹中的第三方库和项目目录是同级的,可以正常导入使用。当然了,本地运行使用的是全局的第三方库,并未用到项目目录下的 requirements 文件夹。

2. 层管理概述

前者(指 b)是一个很合理的设计,不过在实际环境中却发现了新的问题。完全一致的配置文件

  src:
    hook: 'pip3 install -r ./src/requirements.txt -t ./src/requirements'
    dist: ./src
    include:
      - source: ./requirements
        prefix: ../
    exclude:
      - .env

在 macOS 下成功部署之后,云端的云函数编辑器中看到 requirements 文件夹不存在,第三方库和项目目录是同级的,的确没问题。

不过在 Windows 下成功部署之后,云端的云函数编辑器中看到了 requirements 文件夹?也就是说第三方库和项目目录非同级,于是访问就会出现 no module found 的导入报错了……

反复尝试修改 prefix 等配置项到最后也没有调试成功,因此在这里提出两种解决方法:

a. 修改配置文件如下,让本地的第三方库和项目目录同级存在

  src:
    hook: 'pip3 install -r ./src/requirements.txt -t ./src'
    dist: ./src
    exclude:
      - .env

不过随着项目和第三方库的扩大文件夹会越来越多,非常不便于管理

b. 使用云函数提供的 层

虽然 sls deploy 部署的速度很快,但是如果可以在部署时只上传项目代码而不去处理依赖不就更好了嘛,这样跨终协作端开发只需要关心项目代码就 ok 了,再也不需要管理依赖!

并且还有一点,想在 SCF 控制台中在线编辑函数代码需要将部署程序包保持在 10MB 以下,不要以为十兆很大,很快就用光也是可能的

二次元看过来!基于 Serverless 的舞萌音游查分器_Serverless_07

具体如何操作呢?那就是要将第三方库文件夹直接打包并创建为层,则在函数代码中可直接通过 import 引用,毕竟有些特殊库比如 Brotli,Windows 下没有 vc++ 的话就只能去https://lfd.uci.edu/~gohlke/pythonlibs下载 wheel 安装。

macOS 下正常安装之后会得到 _brotli.cpython-39-darwin.so,brotli.py 中再以 import _brotli 的形式导入,不过又出新问题了,云端会导入报错ModuleNotFoundError: No module named '_brotli'"

当前 SCF 的执行环境建立在以下基础上:标准 CentOS 7.2

为了解决问题尝试在 linux 环境下打包,拿起手头的 CentOS 8.2 云主机开始操作

pip3 install -r requirements.txt -t ./layer --upgrade
zip -r layer.zip ./layer

然后就可以把打包的 layer.zip 下载到本地再传上去了,暂时可以一劳永逸了。

二次元看过来!基于 Serverless 的舞萌音游查分器_Serverless_08

对了,配置文件可以移除 hook 并添加 layers 了

  src:
    src: ./src
    exclude:
      - .env
      - '__pycache__/**'
  layers:
    - name: maimai_DX_CN_probe
      version: 3

已绑定层的函数被触发运行,启动并发实例时,将会解压加载函数的运行代码至 /var/user/ 目录下,同时会将层内容解压加载至 /opt 目录下。若需使用或访问的文件 file,放置在创建层时压缩文件的根目录下。则在解压加载后,可直接通过目录 /opt/file 访问到该文件。若在创建层时,通过文件夹进行压缩 dir/file,则在函数运行时需通过 /opt/dir/file 访问具体文件

体验更快的部署速度吧!因为第三方库已经打包在“层”中了

但是奇怪的是,在云端导入任意第三方库均会报错,于是调试着查看 path

for path in sys.path:
    print(path)

/var/runtime/python3
/var/user
/opt
/var/lang/python3/lib/python36.zip
/var/lang/python3/lib/python3.6
/var/lang/python3/lib/python3.6/lib-dynload
/var/lang/python3/lib/python3.6/site-packages
/var/lang/python3/lib/python3.6/site-packages/pip-18.0-py3.6.egg

再查看 opt

import os
dirs = os.listdir('/opt')

for file in dirs:
   print(file)

layer

这才恍然大悟,打包时需要在当前路径直接打包。上传之后“层”更新为版本 2,但是 ModuleNotFoundError: No module named '_brotli' 报错依旧,并且确认 _brotli.cpython-38-x86_64-linux-gnu.so 文件实际存在。

而在 CentOS 和 macOS 上本地导入均没有问题,这可就犯难了,又想到很有可能是 python 版本的问题,于是去寻找现成 3.6 的环境,比如这里:

二次元看过来!基于 Serverless 的舞萌音游查分器_Serverless_09

再再次上传之后“层”更新为版本 3,访问成功!课题终于解决,原来是需要相同版本的 Python 3.6 运行环境

3.自定义入口文件

components源码tencent-flask/src/_shims/中的文件每次都会被原封不动地重新打包上传到云端云函数中,目前有两个文件

a. severless_wsgi.py,作用是 converts an AWS API Gateway proxied request to a WSGI request.
WSGI的全称是Python Web Server Gateway Interface即Web 服务器网关接口,它是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口

b. sl_handler.py,就是默认的入口文件

import app  # Replace with your actual application
import severless_wsgi

# If you need to send additional content types as text, add then directly
# to the whitelist:
#
# serverless_wsgi.TEXT_MIME_TYPES.append("application/custom+json")

def handler(event, context):
    return severless_wsgi.handle_request(app.app, event, context)

针对于自己的项目,使用了 Flask 的 工厂函数,为了避免每次都要在云端云函数编辑器中重新修改,最好的方法是自定义入口文件:

import severless_wsgi

from maimai_DX_CN_probe import create_app  # Replace with your actual application


# If you need to send additional content types as text, add then directly
# to the whitelist:
#
# serverless_wsgi.TEXT_MIME_TYPES.append("application/custom+json")

def handler(event, context):
    return severless_wsgi.handle_request(create_app(), event, context)

再指定 执行方法 为 serverless_handler.handler,就 ok 了

4. url_for 输出 http 而非 https 的 URL

在视图函数中重定向到 url_for 所生成的链接都是 http,而不是 https……其实这个问题 Flask 的文档 Standalone WSGI Containers有描述到

说到底这并不是 Flask 的问题,而是 WSGI 环境所导致的问题,推荐的方法是使用中间件,官方也给出了 ProxyFix

from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)

但是是从X-Forwarded-Proto中取的值,apigw中其为http,因此并不能直接使用这个ProxyFix
因为Flask的社区还算完善,参考资料很多前人都铺好了路,所以直接去Stack Overflow搜解决方法,Flask url_for generating http URL instead of https
问题出现的原因如图:Browser ----- HTTPS ----> Reverse proxy(apigw) ----- HTTP ----> Flask
因为自己在apigw设置了前端类型仅https,也就是说Browser端是不可能使用http访问到的,通过打印environ可知

{
  "CONTENT_LENGTH": "0",
  "CONTENT_TYPE": "",
  "PATH_INFO": "/",
  "QUERY_STRING": "",
  "REMOTE_ADDR": "",
  "REMOTE_USER": "",
  "REQUEST_METHOD": "GET",
  "SCRIPT_NAME": "",
  "SERVER_NAME": "maimai.yuangezhizao.cn",
  "SERVER_PORT": "80",
  "SERVER_PROTOCOL": "HTTP/1.1",
  "wsgi.errors":,
  "wsgi.input":,
  "wsgi.multiprocess": False,
  "wsgi.multithread": False,
  "wsgi.run_once": False,
  "wsgi.url_scheme": "http",
  "wsgi.version": (1, 0),
  "serverless.authorizer": None,
  "serverless.event": "",
  "serverless.context": "",
  "API_GATEWAY_AUTHORIZER": None,
  "event": "",
  "context": "",
  "HTTP_ACCEPT": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
  "HTTP_ACCEPT_ENCODING": "gzip, deflate, br",
  "HTTP_ACCEPT_LANGUAGE": "zh-CN,zh;q=0.9,en;q=0.8",
  "HTTP_CONNECTION": "keep-alive",
  "HTTP_COOKIE": "",
  "HTTP_ENDPOINT_TIMEOUT": "15",
  "HTTP_HOST": "maimai.yuangezhizao.cn",
  "HTTP_SEC_FETCH_DEST": "document",
  "HTTP_SEC_FETCH_MODE": "navigate",
  "HTTP_SEC_FETCH_SITE": "none",
  "HTTP_SEC_FETCH_USER": "?1",
  "HTTP_UPGRADE_INSECURE_REQUESTS": "1",
  "HTTP_USER_AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
  "HTTP_X_ANONYMOUS_CONSUMER": "true",
  "HTTP_X_API_REQUESTID": "5bcb29af2ca18c1e6d7b1ec5ff7b5427",
  "HTTP_X_API_SCHEME": "https",
  "HTTP_X_B3_TRACEID": "5bcb29af2ca18c1e6d7b1ec5ff7b5427",
  "HTTP_X_QUALIFIER": "$LATEST"
}

HTTP_X_FORWARDED_PROTO对应apigw里的变量是HTTP_X_API_SCHEME,故解决方法如下:app.wsgi_app = ReverseProxied(app.wsgi_app)

class ReverseProxied(object):
    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        scheme = environ.get('HTTP_X_FORWARDED_PROTO')
        if scheme:
            environ['wsgi.url_scheme'] = scheme
        return self.app(environ, start_response)

app = Flask(__name__)
app.wsgi_app = ReverseProxied(app.wsgi_app)

5. 响应数据压缩

不论是IIS、Apache还是Nginx,都提供有压缩功能。毕竟自己在用的云主机外网上行只有1M带宽,压缩后对于缩短首屏时间的效果提升极为显著。对于Serverless,响应数据是通过API Gateway传输到客户端,那么压缩也应该是它所具备的能力(虽然外网速度大幅度提高,但是该压缩还是得压缩),然而并没有找到……看到某些js框架原生有提供压缩功能,于是打算添加Flask自行压缩的功能。简单来讲,通过订阅@app.after_request信号并调用第三方库brotli的compress方法即可(
在写之前去gh上看看有没有现成的轮子拓展,果然有……刚开始用的是Flask-Zipper,后来换成Flask-Compress解决了问题
实测3.1 MB的数据采用brotli压缩算法减至76.1 kB

二次元看过来!基于 Serverless 的舞萌音游查分器_二次元_10

二次元看过来!基于 Serverless 的舞萌音游查分器_Serverless_11

二次元看过来!基于 Serverless 的舞萌音游查分器_二次元_12

6. apigw 三种环境不同路径所产生的影响

默认的映射如下:

ID环境名访问路径
1发布release
2预发布prepub
3测试test

因为配置的static_url_path为"",即static文件夹是映射到/路径下的,所以再加上release、prepub和test访问就自然404了
因此绑定了自定义域名,使用自定义路径映射,并将发布环境的访问路径设置成/,这样再访问发布环境就没有问题了

ID环境名访问路径
1发布/
2预发布prepub
3测试test

7. 同时访问私有网络和外网

云函数中可以利用到的云端数据库有如下几种

  • 云数据库CDB,需要私有网络访问,虽然可以通过外网访问但是能走内网就不走外网
  • PostgreSQL for Serverless(ServerlessDB),这个是官方给Serverless配的pg数据库
  • 云开发TCB中的MongoDB,没记错的话需要开通内测权限访问

因为自己是从旧网站迁移过来的,数据暂时还没有迁移,因此直接访问原始云数据库CDB,在云函数配置所属网络和所属子网即可。但是此时会无法访问外网,一种解决方法是开启公网访问和公网固定IP,就可以同时访问内网和外网资源了。关于配置文件,本项目是单实例应用也就是说项目中只引入一个组件,部署时只生成一个组件实例。但是如果想引入数据库的话,就得新增组件了,目前在Flask Components中并没有提供数据库相关的配置项,因此需要项目中引入多个组件,部署时生成多个组件实例。也很简单,创建一个含有serverless.yml的新文件夹,用来配置postgresql

component: postgresql # (必填) 组件名称,此处为 postgresql
name: maimai_DX_CN_probe # (必选) 组件实例名称.
org: yuangezhizao # (可选) 用于记录组织信息,默认值为您的腾讯云账户 appid,必须为字符串
app: yuangezhizao # (可选) 用于记录组织信息. 默认与name相同,必须为字符串
stage: dev # (可选) 用于区分环境信息,默认值是 dev

inputs:
  region: ap-beijing # 可选 ap-guangzhou, ap-shanghai, ap-beijing
  zone: ap-beijing-3 # 可选 ap-guangzhou-2, ap-shanghai-2, ap-beijing-3
  dBInstanceName: maimai_DX_CN_probe
  #  projectId: 0
  dBVersion: 10.4
  dBCharset: UTF8
  vpcConfig:
    vpcId: vpc-mrg5ak88
    subnetId: subnet-hqwa51dh
  extranetAccess: false

然后在终端cd到这个目录再执行sls deploy即可成功部署postgresql

yum install python3-devel postgresql-devel
pip install psycopg2

结果

import psycopg2
File "/opt/psycopg2/__init__.py", line 51, in <module>
from psycopg2._psycopg import (                     # noqa
ImportError: libpython3.6m.so.1.0: cannot open shared object file: No such file or directory

下列问题处于解决之中:

  • http 强制跳转 https
  • 测试环境推送至生产环境

至此,本文就结束了,欢迎交流!

One More Thing

立即体验腾讯云 Serverless Demo,领取 Serverless 新用户礼包 ???? serverless/start