1. 程式人生 > 實用技巧 >二次元看過來!基於 Serverless 的舞萌音遊查分器

二次元看過來!基於 Serverless 的舞萌音遊查分器

前言

本文作者:遠哥製造

一、什麼是 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 專案

  1. 手動建立內容為 Flaskrequirements.txt

  2. 按照配置文件建立 serverless.yml,例如本專案實際使用的完整內容,初次使用可自行酌情簡化

  3. 將密匙寫入 .env(當然,部署的時候也可以選擇微信掃碼授權)

TENCENT_SECRET_ID=<rm>
TENCENT_SECRET_KEY=<rm>

這樣基於 ServerlessFlask Demo 就部署完成了,接下來繼續按照自己的方式寫剩下的程式碼。

三、maimai_DX

maimai 是一款街機音遊。

在這裡放一張動圖自行體會一下,原始素材來自「外錄 maimai」QZKago Requiem Re:MASTER ALLPERFECT Player: Ruri*R

在國內,只能從微信公眾號中檢視成績,而且每次進頁面都需要微信的授權登入,並且裡面儲存的記錄有條數限制,相簿只存最新 10 條,遊戲記錄只存最新 50 條(就是一個佇列,先進先出的那種)。這就是本專案的初衷,自己打出來的每一次成績都應該儲存好。

舞萌查分器

成果展示了,前端 Fomantic-UI,後端 Flask+MySQLgh 開源地址:https://github.com/yuangezhizao/maimai_DX_CN_probe,歡迎 watchstarfork & pr

目前實裝瞭如下功能:

  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 以下,不要以為十兆很大,很快就用光也是可能的

具體如何操作呢?那就是要將第三方庫資料夾直接打包並建立為層,則在函式程式碼中可直接通過 import 引用,畢竟有些特殊庫比如 Brotli,Windows 下沒有 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 雲主機開始操作

pip3 install -r requirements.txt -t ./layer --upgrade
zip -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 in dirs:
   print(file)

layer

這才恍然大悟,打包時需要在當前路徑直接打包。上傳之後“層”更新為版本 2,但是 ModuleNotFoundError: No module named '_brotli' 報錯依舊,並且確認 _brotli.cpython-38-x86_64-linux-gnu.so 檔案實際存在。

而在 CentOSmacOS 上本地匯入均沒有問題,這可就犯難了,又想到很有可能是 python 版本的問題,於是去尋找現成 3.6 的環境,比如這裡:

再再次上傳之後“層”更新為版本 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 InterfaceWeb 伺服器閘道器介面,它是為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 而非 httpsURL

在檢視函式中重定向到 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. 響應資料壓縮

不論是IISApache還是Nginx,都提供有壓縮功能。畢竟自己在用的雲主機外網上行只有1M頻寬,壓縮後對於縮短首屏時間的效果提升極為顯著。對於Serverless,響應資料是通過API Gateway傳輸到客戶端,那麼壓縮也應該是它所具備的能力(雖然外網速度大幅度提高,但是該壓縮還是得壓縮),然而並沒有找到……看到某些js框架原生有提供壓縮功能,於是打算新增Flask自行壓縮的功能。簡單來講,通過訂閱@app.after_request訊號並呼叫第三方庫brotlicompress方法即可(
在寫之前去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資料夾是對映到/路徑下的,所以再加上releaseprepubtest訪問就自然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 &lt;module&gt;
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 新使用者禮包