【1024,Serverless】maimai_DX 查分器

2020-10-26 11:33:38 浏览数 (1)

原文地址:https://www.yuangezhizao.cn/articles/python/flask/serverless/maimai_DX_CN_probe.html

因编辑器限制,推荐访问“原文地址”以获得更好的页面排版方便阅读

0x00.前言

下班回到家楼下等电梯时刷微信时看到了Serverless 有一百种玩法,比好玩更好玩这篇推送文章,正巧自己最近几个月断断续续在写音游的历史记录存档,趁着这个机会决定参加这次应用开发

0x01.Serverless Framework

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

没错,就像几天前看到的《Serverless之歌》里面所说I'm gonna reduce your ops,它能大幅度减轻运维压力(也不得不佩服aws

那就开始动手吧!注意开发环境需Node.js 10.0 ,一键全局安装:npm install -g serverless

<details><summary>点击此处 ← 查看终端</summary>

代码语言:txt复制
Last login: Tue Oct 20 18:34:41 on ttys000
MacPro:maimai_DX_CN_probe yuangezhizao$ node -v
v14.14.0
MacPro:maimai_DX_CN_probe yuangezhizao$ npm -v
6.14.8
MacPro:~ yuangezhizao$ cd Documents/GitHub/maimai_DX_CN_probe/
MacPro:maimai_DX_CN_probe yuangezhizao$ npm install -g serverless
npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142
npm WARN deprecated request-promise-native@1.0.9: request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142
npm WARN deprecated har-validator@5.1.5: this library is no longer supported
/usr/local/bin/serverless -> /usr/local/lib/node_modules/serverless/bin/serverless.js
/usr/local/bin/sls -> /usr/local/lib/node_modules/serverless/bin/serverless.js

> snappy@6.3.5 install /usr/local/lib/node_modules/serverless/node_modules/snappy
> prebuild-install || node-gyp rebuild


> protobufjs@6.10.1 postinstall /usr/local/lib/node_modules/serverless/node_modules/protobufjs
> node scripts/postinstall


> serverless@2.8.0 postinstall /usr/local/lib/node_modules/serverless
> node ./scripts/postinstall.js


   ┌───────────────────────────────────────────────────┐
   │                                                   │
   │   Serverless Framework successfully installed!    │
   │                                                   │
   │   To start your first project run 'serverless'.   │
   │                                                   │
   └───────────────────────────────────────────────────┘

  serverless@2.8.0
added 644 packages from 484 contributors in 522.774s
MacPro:maimai_DX_CN_probe yuangezhizao$ serverless -v
Framework Core: 2.8.0
Plugin: 4.1.1
SDK: 2.3.2
Components: 3.2.4

</details>

注:其中Python云函数运行环境仅仅支持2.73.6,本来想在本地安装一个3.6的最新版本Python 3.6.12 - Aug. 17, 2020

结果发现官网只提供源代码并没有release可执行文件,自己平日的开发环境是最新Python 3.9.0 - Oct. 5, 2020

后来想到其实只需要注意下大的语法变更就ok了,基本上应该问题不大

0x02.腾讯云 Flask Serverless Component

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

本项目中并未实际使用Flask Command,故相当于没有任何限制,按照惯例首先来部署demo

  1. 本地PyCharm创建一个新的Flask项目 FlaskFlask
  2. 手动创建内容为Flaskrequirements.txt
  3. 按照配置文档创建serverless.yml,例如本项目实际使用的完整内容,初次使用可自行酌情简化

<details><summary>点击此处 ← 查看折叠</summary>

代码语言:txt复制
component: flask # (必选) 组件名称,在该实例中为flask
name: maimai_DX_CN_probe # (必选) 组件实例名称.
org: yuangezhizao # (可选) 用于记录组织信息,默认值为您的腾讯云账户 appid,必须为字符串
app: yuangezhizao # (可选) 用于记录组织信息. 默认与name相同,必须为字符串
stage: dev # (可选) 用于区分环境信息,默认值是 dev

inputs:
  region: ap-beijing # 云函数所在区域
  functionName: maimai_DX_CN_probe # 云函数名称
  serviceName: maimai_DX_CN_probe # api网关服务名称
  runtime: Python3.6 # 运行环境
  handler: serverless_handler.handler
    #  src: ./src # 第一种为string时,会打包src对应目录下的代码上传到默认cos上。
    #  src:
    # TODO: 安装python项目依赖到项目当前目录
    #    hook: 'pip3 install -r ./src/requirements.txt -t ./src/requirements'
    #    dist: ./src
  #    include:
  #      - source: ./requirements
  #        prefix: ../ # prefix, can make ./requirements files/dir to ./
  #    exclude:
  #      - .env
  #      - 'requirements/**'
  # serviceId: service-np1uloxw # api网关服务ID
  src: # 第二种,部署src下的文件代码,并打包成zip上传到bucket上
    src: ./src  # 本地需要打包的文件目录
    #       bucket: bucket01 # bucket name,当前会默认在bucket name后增加 appid 后缀, 本例中为 bucket01-appid
    exclude: # 被排除的文件或目录
      - .env
      - '__pycache__/**'
    # src: # 第三种,在指定存储桶bucket中已经存在了object代码,直接部署
    #   bucket: bucket01 # bucket name,当前会默认在bucket name后增加 appid 后缀, 本例中为 bucket01-appid
    #   object: cos.zip  # bucket key 指定存储桶内的文件
  layers:
    - name: maimai_DX_CN_probe #  layer名称
      version: 4 #  版本
  functionConf: # 函数配置相关
    timeout: 10 # 超时时间,单位秒
    eip: true # 是否固定出口IP
    memorySize: 128 # 内存大小,单位MB
    environment: #  环境变量
      variables: #  环境变量数组
        DEBUG: False
    vpcConfig: # 私有网络配置
      vpcId: 'vpc-mrg5ak88' # 私有网络的Id
      subnetId: 'subnet-hqwa51dh' # 子网ID
  apigatewayConf: #  api网关配置
    isDisabled: false # 是否禁用自动创建 API 网关功能
    enableCORS: false #  允许跨域
    customDomains: # 自定义域名绑定
      - domain: maimai.yuangezhizao.cn # 待绑定的自定义的域名
        certificateId: hMMBPdz0 # 待绑定自定义域名的证书唯一 ID
        # 如要设置自定义路径映射,请设置为 false
        isDefaultMapping: false
        # 自定义路径映射的路径。使用自定义映射时,可一次仅映射一个 path 到一个环境,也可映射多个 path 到多个环境。并且一旦使用自定义映射,原本的默认映射规则不再生效,只有自定义映射路径生效。
        pathMappingSet:
          - path: /
            environment: release
          - path: /prepub
            environment: prepub
          - path: /test
            environment: test
        protocols: # 绑定自定义域名的协议类型,默认与服务的前端协议一致。
          #          - http # 支持http协议
          - https # 支持https协议
    protocols:
      #      - http
      - https
    environment: release
    serviceTimeout: 15
    # usagePlan: #  用户使用计划
    #   usagePlanId: 1111
    #   usagePlanName: slscmp
    #   usagePlanDesc: sls create
    #   maxRequestNum: 1000
    # auth: #  密钥
    #   secretName: secret
    #   secretIds:
    #     - xxx

</details>

  1. 将密匙写入.env(这样就不用在部署时再拿起手机扫一扫了TENCENT_SECRET_ID=<rm> TENCENT_SECRET_KEY=<rm>

<details><summary>点击此处 ← 查看终端</summary>

</details>

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

0x03.maimai_DX

还是来简单地介绍一下maimai这款街机音游吧,外语水平好的可以直接去看日本官网&海外官网,介绍的还是比较专业的

到底是个什么样子的游戏呢?在这里放一张动图自行体会一下,原始素材来自【外录maimai】QZKago Requiem Re:MASTER ALLPERFECT Player : Ruri*R

在国内,只能从微信公众号中查看成绩(因为每次进页面都需要微信的授权登录

并且里面存储的记录有条数限制,相册只存最新十条,游戏记录只存最新五十条(就是一个队列,先进先出的那种

这就是本项目的初衷,自己打出来的每一次成绩都应该保存好

0x04.舞萌查分器

接下来就是成果展示了,gh开源地址:https://github.com/yuangezhizao/maimai_DX_CN_probe,后端Flask MySQL,前端Fomantic-UI,欢迎watchstarfork&pr

目前实装了如下功能:

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

0x05.发现&解决BUG

接下来将按照时间的顺序依次叙述一下开发过程中遇到的种种课题以及解决思路

1.Serverless Framework Component配置文件

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

之前版本的配置文件可以参考这里:[实验室站迁移 TencentCloud Serverless 之路#0x03.部署 Python Flask 框架

](/init.html#2-配置Serverless)

也正是因为这个版本变化,导致配置文件有一处需要注意的地方

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

②最新版本不再自动下载,需要自行处理。不过官方示例已经给写了参考用法:hook

代码语言:txt复制
  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.层管理概述

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

代码语言:txt复制
  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等配置项到最后也没有调试成功,因此在这里提出两种解决方法

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

代码语言:txt复制
  src:
    hook: 'pip3 install -r ./src/requirements.txt -t ./src'
    dist: ./src
    exclude:
      - .env

不过这样做自己是拒绝的,可想而知随着项目和第三方库的扩大文件夹会越来越多,非常不便于管理

②使用云函数提供的

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

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

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

macOS下正常安装之后会得到_brotli.cpython-39-darwin.sobrotli.py中再以import _brotli的形式导入

不过又出新问题了,云端会导入报错ModuleNotFoundError: No module named '_brotli'"

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

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

代码语言:txt复制
pip3 install -r requirements.txt -t ./layer --upgrade
zip -r layer.zip ./layer

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

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

代码语言:txt复制
  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访问具体文件

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

<details><summary>点击此处 ← 查看终端</summary>

代码语言:txt复制
MacPro:maimai_DX_CN_probe yuangezhizao$ sls deploy

serverless ⚡framework
Action: "deploy" - Stage: "dev" - App: "yuangezhizao" - Instance: "maimai_DX_CN_probe"

region: ap-beijing
apigw: 
  serviceId:     service-dn6wahj7
  subDomain:     service-dn6wahj7-1251901037.bj.apigw.tencentcs.com
  environment:   release
  url:           https://service-dn6wahj7-1251901037.bj.apigw.tencentcs.com/release/
  customDomains: 
    - 
      isBinded:  true
      created:   true
      subDomain: maimai.yuangezhizao.cn
      cname:     service-dn6wahj7-1251901037.bj.apigw.tencentcs.com
      url:       https://maimai.yuangezhizao.cn
scf: 
  functionName: maimai_DX_CN_probe
  runtime:      Python3.6
  namespace:    default
  lastVersion:  $LATEST
  traffic:      1

Full details: https://serverless.cloud.tencent.com/apps/yuangezhizao/maimai_DX_CN_probe/dev

15s › maimai_DX_CN_probe › Success

</details>

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

代码语言:txt复制
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

代码语言:txt复制
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文件实际存在

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

<details><summary>点击此处 ← 查看终端</summary>

代码语言:txt复制
[root@txy ~]# rm -rf layer
[root@txy ~]# mkdir layer && cd layer
[root@txy layer]# vim requirements.txt
[root@txy layer]# cat requirements.txt 
Flask
Flask-SQLAlchemy
Flask-Zipper
brotli
python-dotenv
pymysql
[root@txy layer]# python -V
Python 3.8.6
[root@txy layer]# pip -V
pip 9.0.3 from /usr/lib/python3.6/site-packages (python 3.6)
[root@txy layer]# pip3 -V
pip 20.2.4 from /usr/local/python3/lib/python3.8/site-packages/pip (python 3.8)
[root@txy layer]# pip install -r requirements.txt -t . --upgrade
……
[root@txy layer]# zip -r layer.zip . -x requirements.txt

</details>

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

3.自定义入口文件

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

severless_wsgi.py,作用是converts an AWS API Gateway proxied request to a WSGI request.

WSGI的全称是Python Web Server Gateway InterfaceWeb 服务器网关接口,它是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口

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

代码语言:txt复制
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工厂函数,为了避免每次都要在云端云函数编辑器中重新修改,最好的方法是自定义入口文件:

代码语言:txt复制
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而非httpsURL

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

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

代码语言:txt复制
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可知

代码语言:txt复制
{
  "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": <__main__.CustomIO object at 0x7feda2224630>,
  "wsgi.input": <_io.BytesIO object at 0x7fed97093410>,
  "wsgi.multiprocess": False,
  "wsgi.multithread": False,
  "wsgi.run_once": False,
  "wsgi.url_scheme": "http",
  "wsgi.version": (1, 0),
  "serverless.authorizer": None,
  "serverless.event": "<rm>",
  "serverless.context": "<rm>",
  "API_GATEWAY_AUTHORIZER": None,
  "event": "<rm>",
  "context": "<rm>",
  "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": "<rm>",
  "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)

代码语言:txt复制
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.响应数据压缩

不论是IISApache还是Nginx,都提供有压缩功能。毕竟自己在用的云主机外网上行只有1M带宽,压缩后对于缩短首屏时间的效果提升极为显著。对于Serverless,响应数据是通过API Gateway传输到客户端,那么压缩也应该是它所具备的能力(虽然外网速度大幅度提高,但是该压缩还是得压缩),然而并没有找到……看到某些js框架原生有提供压缩功能,于是打算添加Flask自行压缩的功能。简单来讲,通过订阅@app.after_request信号并调用第三方库brotlicompress方法即可(

在写之前去gh上看看有没有现成的轮子拓展,果然有……刚开始用的是Flask-Zipper,后来换成Flask-Compress解决了问题

实测3.1 MB的数据采用brotli压缩算法减至76.1 kB

<details><summary>点击此处 ← 查看折叠</summary>

</details>

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

默认的映射如下:

ID

环境名

访问路径

1

发布

release

2

预发布

prepub

3

测试

test

因为配置的static_url_path"",即static文件夹是映射到/路径下的,所以再加上releaseprepubtest访问就自然404

因此绑定了自定义域名使用自定义路径映射,并将发布环境的访问路径设置成/,这样再访问发布环境就没有问题了

ID

环境名

访问路径

1

发布

/

2

预发布

prepub

3

测试

test

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

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

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

因为自己是从旧网站迁移过来的,数据暂时还没有迁移,因此直接访问原始云数据库CDB,在云函数配置所属网络所属子网即可

但是此时会无法访问外网,一种解决方法是开启公网访问公网固定IP,就可以同时访问内网和外网资源了

下列问题处于解决之中:

  1. http强制跳转https
  2. 测试环境推送至生产环境

0x06.后记

昨晚出勤回来穿的少风又大,正好赶上今天1024嗓子疼略感冒……(然后还是尽可能地去写这篇文章

未完待续……

0 人点赞