如何為Serverless架構(gòu)做了一個(gè)Django的Component

之前有過(guò)朋友問(wèn)我Flask巷送、Express這些框架是如何在函數(shù)中運(yùn)行,他是怎么樣的一個(gè)機(jī)制蔓姚?還有人問(wèn)我如何做一個(gè)Component惕耕?看了一下騰訊云Serverless架構(gòu)現(xiàn)在支持的框架:

image

我發(fā)現(xiàn)雖然支持了很多躏嚎,但是我比較鐘愛(ài)的Django貌似沒(méi)有蜜自,正好想到了部分人的疑惑,所以在這里卢佣,我就簡(jiǎn)單的和大家說(shuō)一下重荠,我如何做一個(gè)Django的Component。

分析已有Component(Flask為例)

首先第一步虚茶,我們要知道其他的框架是怎么運(yùn)行的戈鲁,例如Flask等,我們先通過(guò)騰訊云的Flask-Component嘹叫,按照他的說(shuō)明部署一下:

image

非常簡(jiǎn)單輕松愉快的部署上線婆殿,然后在函數(shù)的控制臺(tái),我們把部署好的下載下來(lái)罩扇,研究一下:

image

下載解壓之后鸣皂,我們可以看這樣一個(gè)目錄結(jié)構(gòu):

image

藍(lán)色框起來(lái)的,是依賴包暮蹂,黃色的app.py是我們的自己寫的代碼寞缝,那么紅色圈起來(lái)的是什么?這兩個(gè)文件從哪里出來(lái)的仰泻?
api_server.py文件內(nèi)容:

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)

可以看到荆陆,這里面是將我們創(chuàng)建的app.py文件引入,并且拿到了app這個(gè)對(duì)象集侯,并且將event和context同時(shí)傳遞給severless_wsgi.py中的handle_reques方法中被啼,那么問(wèn)題來(lái)了,這個(gè)方法是什么棠枉?

image

這個(gè)方法內(nèi)容好多......看著有點(diǎn)眼暈浓体,但是,我們可以直接發(fā)現(xiàn)這一段代碼:

image

這一段是什么呢辈讶?這一段實(shí)際上就是將我們拿到的參數(shù)(event和context)進(jìn)行轉(zhuǎn)換命浴,轉(zhuǎn)換之后統(tǒng)一environ中,然后接下來(lái)通過(guò)werkzeug這個(gè)依賴贱除,將這個(gè)內(nèi)容變成request對(duì)象生闲,并且與我們剛才說(shuō)的app對(duì)象一起調(diào)用from_app方法。獲得到反饋:

image

并且按照API網(wǎng)關(guān)的響應(yīng)集成的格式月幌,將結(jié)果返回碍讯。
此時(shí)此刻,各位看官可能有點(diǎn)想法了扯躺,貌似有一丟丟靈感出現(xiàn)了捉兴,那么我們不妨看一下Flask/Django這些框架的實(shí)現(xiàn)原理:

image

通過(guò)這個(gè)簡(jiǎn)版的原理圖蝎困,和我剛才說(shuō)的內(nèi)容,我們可以想到倍啥,實(shí)際上正常用的時(shí)候要通過(guò)web_server难衰,進(jìn)入到下一個(gè)環(huán)節(jié),而我們?cè)坪瘮?shù)更多是一個(gè)函數(shù)逗栽,本不需要啟動(dòng)web server,所以我們就可以直接調(diào)用wsgi_app這個(gè)方法失暂,其中這里的environ就是我們剛才的通過(guò)對(duì)event/context等進(jìn)行處理后的對(duì)象彼宠,start_response可以認(rèn)為是我們的一種特殊的數(shù)據(jù)結(jié)構(gòu),例如我們的response結(jié)構(gòu)形態(tài)等弟塞。所以凭峡,如果我們自己想要實(shí)現(xiàn)這個(gè)過(guò)程,不使用騰訊云flask-component决记,可以這樣做:

import sys

try:
    from urllib import urlencode
except ImportError:
    from urllib.parse import urlencode

from flask import Flask

try:
    from cStringIO import StringIO
except ImportError:
    try:
        from StringIO import StringIO
    except ImportError:
        from io import StringIO

from werkzeug.wrappers import BaseRequest

__version__ = '0.0.4'


def make_environ(event):
    environ = {}
    for hdr_name, hdr_value in event['headers'].items():
        hdr_name = hdr_name.replace('-', '_').upper()
        if hdr_name in ['CONTENT_TYPE', 'CONTENT_LENGTH']:
            environ[hdr_name] = hdr_value
            continue

        http_hdr_name = 'HTTP_%s' % hdr_name
        environ[http_hdr_name] = hdr_value

    apigateway_qs = event['queryStringParameters']
    request_qs = event['queryString']
    qs = apigateway_qs.copy()
    qs.update(request_qs)

    body = ''
    if 'body' in event:
        body = event['body']

    environ['REQUEST_METHOD'] = event['httpMethod']
    environ['PATH_INFO'] = event['path']
    environ['QUERY_STRING'] = urlencode(qs) if qs else ''
    environ['REMOTE_ADDR'] = 80
    environ['HOST'] = event['headers']['host']
    environ['SCRIPT_NAME'] = ''
    environ['SERVER_PORT'] = 80
    environ['SERVER_PROTOCOL'] = 'HTTP/1.1'
    environ['CONTENT_LENGTH'] = str(len(body))
    environ['wsgi.url_scheme'] = ''
    environ['wsgi.input'] = StringIO(body)
    environ['wsgi.version'] = (1, 0)
    environ['wsgi.errors'] = sys.stderr
    environ['wsgi.multithread'] = False
    environ['wsgi.run_once'] = True
    environ['wsgi.multiprocess'] = False

    BaseRequest(environ)

    return environ


class LambdaResponse(object):
    def __init__(self):
        self.status = None
        self.response_headers = None

    def start_response(self, status, response_headers, exc_info=None):
        self.status = int(status[:3])
        self.response_headers = dict(response_headers)


class FlaskLambda(Flask):
    def __call__(self, event, context):
        if 'httpMethod' not in event:
            print('httpMethod not in event')
            return super(FlaskLambda, self).__call__(event, context)

        response = LambdaResponse()

        body = next(self.wsgi_app(
            make_environ(event),
            response.start_response
        ))

        return {
            'statusCode': response.status,
            'headers': response.response_headers,
            'body': body
        }

這樣一個(gè)流程摧冀,就會(huì)變得更加簡(jiǎn)單,清楚系宫。整個(gè)實(shí)現(xiàn)過(guò)程索昂,可以認(rèn)為是對(duì)web server部分進(jìn)行了一種“截?cái)唷被蛘呤恰疤鎿Q”:

image

這就是對(duì)Flask-Component的基本分析思路,那么按照這個(gè)思路扩借,我們是否可以將Django框架部署上Serverless架構(gòu)呢椒惨?那么Flask和Django有什么區(qū)別呢?我這里的區(qū)別特指的是在運(yùn)行啟動(dòng)過(guò)程中潮罪。

拓展思路:實(shí)現(xiàn)Django-component

仔細(xì)想一下康谆,貌似并沒(méi)有區(qū)別,那么我們是不是可以直接用Flask這個(gè)轉(zhuǎn)換邏輯嫉到,將flask的app替換成django的app呢沃暗?
把:

from flask import Flask
app = Flask(__name__)

替換成:

import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mydjango.settings')
application = get_wsgi_application()

是否就能解決問(wèn)題呢?
我們不妨試一下:

image

建立好Django項(xiàng)目何恶,直接增加index.py:

# -*- coding: utf-8 -*-

import os
import sys
import base64
from werkzeug.datastructures import Headers, MultiDict
from werkzeug.wrappers import Response
from werkzeug.urls import url_encode, url_unquote
from werkzeug.http import HTTP_STATUS_CODES
from werkzeug._compat import BytesIO, string_types, to_bytes, wsgi_encoding_dance
import mydjango.wsgi

TEXT_MIME_TYPES = [
    "application/json",
    "application/javascript",
    "application/xml",
    "application/vnd.api+json",
    "image/svg+xml",
]


def all_casings(input_string):
    if not input_string:
        yield ""
    else:
        first = input_string[:1]
        if first.lower() == first.upper():
            for sub_casing in all_casings(input_string[1:]):
                yield first + sub_casing
        else:
            for sub_casing in all_casings(input_string[1:]):
                yield first.lower() + sub_casing
                yield first.upper() + sub_casing


def split_headers(headers):
    """
    If there are multiple occurrences of headers, create case-mutated variations
    in order to pass them through APIGW. This is a hack that's currently
    needed. See: https://github.com/logandk/serverless-wsgi/issues/11
    Source: https://github.com/Miserlou/Zappa/blob/master/zappa/middleware.py
    """
    new_headers = {}

    for key in headers.keys():
        values = headers.get_all(key)
        if len(values) > 1:
            for value, casing in zip(values, all_casings(key)):
                new_headers[casing] = value
        elif len(values) == 1:
            new_headers[key] = values[0]

    return new_headers


def group_headers(headers):
    new_headers = {}

    for key in headers.keys():
        new_headers[key] = headers.get_all(key)

    return new_headers


def encode_query_string(event):
    multi = event.get(u"multiValueQueryStringParameters")
    if multi:
        return url_encode(MultiDict((i, j) for i in multi for j in multi[i]))
    else:
        return url_encode(event.get(u"queryString") or {})


def handle_request(application, event, context):

    if u"multiValueHeaders" in event:
        headers = Headers(event["multiValueHeaders"])
    else:
        headers = Headers(event["headers"])

    strip_stage_path = os.environ.get("STRIP_STAGE_PATH", "").lower().strip() in [
        "yes",
        "y",
        "true",
        "t",
        "1",
    ]
    if u"apigw.tencentcs.com" in headers.get(u"Host", u"") and not strip_stage_path:
        script_name = "/{}".format(event["requestContext"].get(u"stage", ""))
    else:
        script_name = ""

    path_info = event["path"]
    base_path = os.environ.get("API_GATEWAY_BASE_PATH")
    if base_path:
        script_name = "/" + base_path

        if path_info.startswith(script_name):
            path_info = path_info[len(script_name) :] or "/"

    if u"body" in event:
        body = event[u"body"] or ""
    else:
        body = ""

    if event.get("isBase64Encoded", False):
        body = base64.b64decode(body)
    if isinstance(body, string_types):
        body = to_bytes(body, charset="utf-8")

    environ = {
        "CONTENT_LENGTH": str(len(body)),
        "CONTENT_TYPE": headers.get(u"Content-Type", ""),
        "PATH_INFO": url_unquote(path_info),
        "QUERY_STRING": encode_query_string(event),
        "REMOTE_ADDR": event["requestContext"]
        .get(u"identity", {})
        .get(u"sourceIp", ""),
        "REMOTE_USER": event["requestContext"]
        .get(u"authorizer", {})
        .get(u"principalId", ""),
        "REQUEST_METHOD": event["httpMethod"],
        "SCRIPT_NAME": script_name,
        "SERVER_NAME": headers.get(u"Host", "lambda"),
        "SERVER_PORT": headers.get(u"X-Forwarded-Port", "80"),
        "SERVER_PROTOCOL": "HTTP/1.1",
        "wsgi.errors": sys.stderr,
        "wsgi.input": BytesIO(body),
        "wsgi.multiprocess": False,
        "wsgi.multithread": False,
        "wsgi.run_once": False,
        "wsgi.url_scheme": headers.get(u"X-Forwarded-Proto", "http"),
        "wsgi.version": (1, 0),
        "serverless.authorizer": event["requestContext"].get(u"authorizer"),
        "serverless.event": event,
        "serverless.context": context,
        # TODO: Deprecate the following entries, as they do not comply with the WSGI
        # spec. For custom variables, the spec says:
        #
        #   Finally, the environ dictionary may also contain server-defined variables.
        #   These variables should be named using only lower-case letters, numbers, dots,
        #   and underscores, and should be prefixed with a name that is unique to the
        #   defining server or gateway.
        "API_GATEWAY_AUTHORIZER": event["requestContext"].get(u"authorizer"),
        "event": event,
        "context": context,
    }

    for key, value in environ.items():
        if isinstance(value, string_types):
            environ[key] = wsgi_encoding_dance(value)

    for key, value in headers.items():
        key = "HTTP_" + key.upper().replace("-", "_")
        if key not in ("HTTP_CONTENT_TYPE", "HTTP_CONTENT_LENGTH"):
            environ[key] = value

    response = Response.from_app(application, environ)

    returndict = {u"statusCode": response.status_code}

    if u"multiValueHeaders" in event:
        returndict["multiValueHeaders"] = group_headers(response.headers)
    else:
        returndict["headers"] = split_headers(response.headers)

    if event.get("requestContext").get("elb"):
        # If the request comes from ALB we need to add a status description
        returndict["statusDescription"] = u"%d %s" % (
            response.status_code,
            HTTP_STATUS_CODES[response.status_code],
        )

    if response.data:
        mimetype = response.mimetype or "text/plain"
        if (
            mimetype.startswith("text/") or mimetype in TEXT_MIME_TYPES
        ) and not response.headers.get("Content-Encoding", ""):
            returndict["body"] = response.get_data(as_text=True)
            returndict["isBase64Encoded"] = False
        else:
            returndict["body"] = base64.b64encode(response.data).decode("utf-8")
            returndict["isBase64Encoded"] = True

    return returndict



def main_handler(event, context):
    return handle_request(mydjango.wsgi.application, event, context)

然后我們部署到函數(shù)上孽锥,看一下效果:
函數(shù)信息:

from django.shortcuts import render
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

# Create your views here.
@csrf_exempt
def hello(request):
    if request.method == "POST":
        return HttpResponse("Hello world ! " + request.POST.get("name"))
    if request.method == "GET":
        return HttpResponse("Hello world ! " + request.GET.get("name"))

通過(guò)部署完成,并綁定apigw觸發(fā)器细层,然后在postman中進(jìn)行測(cè)試:
get:

image

post:

image

可以看到忱叭,通過(guò)我們對(duì)運(yùn)行原理的基本剖析和對(duì)django的改造,我們已經(jīng)通過(guò)增加一個(gè)文件和相關(guān)依賴的方法今艺,實(shí)現(xiàn)了Django上Serverless的過(guò)程韵丑。

接下來(lái),我們看一下虚缎,如何將這個(gè)代碼寫成一個(gè)Component:
首先Clone下來(lái)Flask-Component的代碼:

image

然后撵彻,我們按照Django的部分模式進(jìn)行修改:

image

第一部分钓株,是我們可能會(huì)依賴的一個(gè)依賴包,以及我們剛才放入的index.py文件陌僵。在用戶調(diào)用這個(gè)Component的時(shí)候轴合,我們會(huì)把這兩個(gè)文件,放入用戶的代碼中碗短,一并上傳受葛。
第二部分是Serverless.js部分,這里的一個(gè)基本格式:

const { Component } = require('@serverless/core')
class TencentDjango extends Component {
  async default(inputs = {}) {
  }
  async remove(inputs = {}) {
  }
}
module.exports = TencentDjango

用戶在執(zhí)行sls的時(shí)候偎谁,會(huì)默認(rèn)調(diào)用default的方法总滩,在執(zhí)行sls remove的時(shí)候會(huì)調(diào)用remove的方法,所以可以認(rèn)default的內(nèi)容是部署巡雨,而remove的內(nèi)容是移除闰渔。

部署這里主要流程也蠻簡(jiǎn)單的,首先將文件進(jìn)行復(fù)制和處理铐望,然后直接調(diào)用云函數(shù)的組件冈涧,通過(guò)函數(shù)中的include參數(shù)將這些文件額外加入,再通過(guò)調(diào)用apigw的組件來(lái)進(jìn)網(wǎng)關(guān)的管理正蛙,而用戶寫的yaml中inpust的內(nèi)容督弓,會(huì)在inputs中獲取,我們要做的就是對(duì)應(yīng)的傳給不同的組件:

image

當(dāng)然除了這兩部分對(duì)應(yīng)放過(guò)去乒验,上面的region等一些信息也要對(duì)應(yīng)的進(jìn)行處理咽筋。而調(diào)用底層組件方法也很簡(jiǎn)單:

const tencentCloudFunction = await this.load('@serverless/tencent-scf'
const tencentCloudFunctionOutputs = await tencentCloudFunction(inputs)

處理好這里之后,只需要修改一下package.json和readme就可以了徊件。

image

目前奸攻,我已經(jīng)完成了開(kāi)源:https://github.com/gosls/tencent-django

也在NPM上進(jìn)行了發(fā)布:https://www.npmjs.com/package/@gosls/tencent-django

在使用的時(shí)候,只需要引入這個(gè)Component就好:

DjangoTest:
  component: '@serverless/tencent-django'
  inputs:
    region: ap-guangzhou
    functionName: DjangoFunctionTest
    djangoProjectName: mydjango
    code: ./
    functionConf:
      timeout: 10
      memorySize: 256
      environment:
        variables:
          TEST: vale
      vpcConfig:
        subnetId: ''
        vpcId: ''
    apigatewayConf:
      protocols:
        - http
      environment: release

至此虱痕,完成了Django Component的開(kāi)發(fā)和測(cè)試睹耐。


image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市部翘,隨后出現(xiàn)的幾起案子硝训,更是在濱河造成了極大的恐慌,老刑警劉巖新思,帶你破解...
    沈念sama閱讀 212,599評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窖梁,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡夹囚,警方通過(guò)查閱死者的電腦和手機(jī)纵刘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,629評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)荸哟,“玉大人假哎,你說(shuō)我怎么就攤上這事瞬捕。” “怎么了舵抹?”我有些...
    開(kāi)封第一講書人閱讀 158,084評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵肪虎,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我惧蛹,道長(zhǎng)扇救,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 56,708評(píng)論 1 284
  • 正文 為了忘掉前任香嗓,我火速辦了婚禮迅腔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘陶缺。我一直安慰自己,他們只是感情好洁灵,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,813評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布饱岸。 她就那樣靜靜地躺著,像睡著了一般徽千。 火紅的嫁衣襯著肌膚如雪苫费。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 50,021評(píng)論 1 291
  • 那天双抽,我揣著相機(jī)與錄音百框,去河邊找鬼。 笑死牍汹,一個(gè)胖子當(dāng)著我的面吹牛铐维,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播慎菲,決...
    沈念sama閱讀 39,120評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼嫁蛇,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了露该?” 一聲冷哼從身側(cè)響起睬棚,我...
    開(kāi)封第一講書人閱讀 37,866評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎解幼,沒(méi)想到半個(gè)月后抑党,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,308評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡撵摆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,633評(píng)論 2 327
  • 正文 我和宋清朗相戀三年底靠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片特铝。...
    茶點(diǎn)故事閱讀 38,768評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡苛骨,死狀恐怖篱瞎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情痒芝,我是刑警寧澤俐筋,帶...
    沈念sama閱讀 34,461評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站严衬,受9級(jí)特大地震影響澄者,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜请琳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,094評(píng)論 3 317
  • 文/蒙蒙 一粱挡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧俄精,春花似錦询筏、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,850評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至圾旨,卻和暖如春踱讨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背砍的。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,082評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工痹筛, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人廓鞠。 一個(gè)月前我還...
    沈念sama閱讀 46,571評(píng)論 2 362
  • 正文 我出身青樓帚稠,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親床佳。 傳聞我的和親對(duì)象是個(gè)殘疾皇子翁锡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,666評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容