社畜下班时刷微信时看到了《Serverless 有一百种玩法,比好玩更好玩》这篇推送,正巧自己最近断断续续在写音游的历史记录存档,趁着这个机会决定参加这次应用开发。
Serverless Framework
是业界非常受欢迎的无服务器应用框架,开发者无需关心底层资源即可部署完整可用的Serverless
应用架构。Serverless Framework
具有资源编排、自动伸缩、事件驱动等能力,覆盖编码、调试、测试、部署等全生命周期,帮助开发者通过联动云资源,迅速构建Serverless
应用
没错,就像几天前看到的《Serverless 之歌》里面所说 I'm gonna reduce your ops
,它能大幅度减轻运维压力,那就开始动手吧!注意开发环境需 Node.js 10.0+
,一键全局安装:npm install -g serverless
腾讯云
Flask Serverless Component
,支持Restful API
服务的部署
按照惯例首先来部署 demo
吧
PyCharm
创建一个新的 Flask
项目Flask
的 requirements.txt
serverless.yml
,例如本项目实际使用的完整内容,初次使用可自行酌情简化.env
(当然,部署的时候也可以选择微信扫码授权)TENCENT_SECRET_ID=<rm>TENCENT_SECRET_KEY=<rm>
复制代码
这样基于 Serverless
的 Flask Demo
就部署完成了,接下来继续按照自己的方式写剩下的代码。
maimai 是一款街机音游:
日本官网:https://maimai.sega.jp海外官网:https://maimai.sega.com
在国内,只能从微信公众号中查看成绩,而且每次进页面都需要微信的授权登录,并且里面存储的记录有条数限制,相册
只存最新 10 条,游戏记录
只存最新 50 条(就是一个队列,先进先出的那种)。这就是本项目的初衷,自己打出来的每一次成绩都应该保存好。
成果展示了,前端 Fomantic-UI
,后端 Flask
+MySQL
。
gh 开源地址:https://github.com/yuangezhizao/maimai_DX_CN_probe,欢迎 watch、star、fork & pr!
https://maimai.yuangezhizao.cn
目前实装了如下功能:
主页
,游戏数据
,相册
和 游戏记录
:对原始网页进行了修改,并且添加了 Highcharts
库可视化曲线显示变化记录(分页)
和 差异(分页)
:即自写的快速预览页面,是查看历史记录和成绩变化的非常实用的功能铺面列表
:即全部铺面基础信息,输出到一个页面中,方便页面内搜索接下来将按照时间的顺序,描述一下开发过程中遇到的问题以及如何解决
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
以下,不要以为十兆很大,很快就用光也是可能的
具体如何操作呢?那就是要将第三方库文件夹直接打包并创建为层,则在函数代码中可直接通过 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 --upgradezip -r layer.zip ./layer
然后就可以把打包的 layer.zip
下载到本地再传上去了,暂时可以一劳永逸了。
对了,配置文件可以移除 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 indirs:
print(file)
layer
这才恍然大悟,打包时需要在当前路径直接打包。上传之后“层”更新为版本 2
,但是 ModuleNotFoundError: No module named '_brotli'
报错依旧,并且确认 _brotli.cpython-38-x86_64-linux-gnu.so
文件实际存在。
而在 CentOS
和 macOS
上本地导入均没有问题,这可就犯难了,又想到很有可能是 python
版本的问题,于是去寻找现成 3.6
的环境,比如这里:
3.6.8
再再次上传之后“层”更新为版本 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": <__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)
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
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
至此,本文就结束了,欢迎交流!
头图:Unsplash
作者:远哥制造
原文:https://mp.weixin.qq.com/s/7ScZQcYp2UT9aOG_sMXYqA
原文:二次元看过来!基于 Serverless 的舞萌音游查分器
来源:TencentServerless - 微信公众号 [ID:ServerlessGo]
转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
领取专属 10元无门槛券
私享最新 技术干货