之前有過朋友問我Flask、Express這些框架是如何在函數中運行,他是怎麼樣的一個機制?還有人問我如何做一個Component?看了一下騰訊雲Serverless架構現在支持的框架:
我發現雖然支持了很多,但是我比較鍾愛的Django貌似沒有,正好想到了部分人的疑惑,所以在這裡,我就簡單的和大家說一下,我如何做一個Django的Component。
首先第一步,我們要知道其他的框架是怎麼運行的,例如Flask等,我們先通過騰訊雲的Flask-Component,按照他的說明部署一下:
非常簡單輕鬆愉快的部署上線,然後在函數的控制檯,我們把部署好的下載下來,研究一下:
下載解壓之後,我們可以看這樣一個目錄結構:
藍色框起來的,是依賴包,黃色的app.py是我們的自己寫的代碼,那麼紅色圈起來的是什麼?這兩個文件從哪裡出來的?
api_server.py文件內容:
<code>import app # Replace with your actual applicationimport 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)/<code>
可以看到,這裡面是將我們創建的app.py文件引入,並且拿到了app這個對象,並且將event和context同時傳遞給severless_wsgi.py中的handle_reques方法中,那麼問題來了,這個方法是什麼?
這個方法內容好多......看著有點眼暈,但是,我們可以直接發現這一段代碼:
這一段是什麼呢?這一段實際上就是將我們拿到的參數(event和context)進行轉換,轉換之後統一environ中,然後接下來通過werkzeug這個依賴,將這個內容變成request對象,並且與我們剛才說的app對象一起調用from_app方法。獲得到反饋:
並且按照API網關的響應集成的格式,將結果返回。
此時此刻,各位看官可能有點想法了,貌似有一丟丟靈感出現了,那麼我們不妨看一下Flask/Django這些框架的實現原理:
通過這個簡版的原理圖,和我剛才說的內容,我們可以想到,實際上正常用的時候要通過web_server,進入到下一個環節,而我們雲函數更多是一個函數,本不需要啟動web server,所以我們就可以直接調用wsgi_app這個方法,其中這裡的environ就是我們剛才的通過對event/context等進行處理後的對象,start_response可以認為是我們的一種特殊的數據結構,例如我們的response結構形態等。所以,如果我們自己想要實現這個過程,不使用騰訊雲flask-component,可以這樣做:
<code>import systry: from urllib import urlencodeexcept ImportError: from urllib.parse import urlencodefrom flask import Flasktry: from cStringIO import StringIOexcept ImportError: try: from StringIO import StringIO except ImportError: from io import StringIOfrom 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 environclass 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 }/<code>
這樣一個流程,就會變得更加簡單,清楚。整個實現過程,可以認為是對web server部分進行了一種“截斷”或者是“替換”:
這就是對Flask-Component的基本分析思路,那麼按照這個思路,我們是否可以將Django框架部署上Serverless架構呢?那麼Flask和Django有什麼區別呢?我這裡的區別特指的是在運行啟動過程中。
仔細想一下,貌似並沒有區別,那麼我們是不是可以直接用Flask這個轉換邏輯,將flask的app替換成django的app呢?
把:
<code>from flask import Flaskapp = Flask(__name__)/<code>
替換成:
<code>import osfrom django.core.wsgi import get_wsgi_applicationos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mydjango.settings')application = get_wsgi_application()/<code>
是否就能解決問題呢?
我們不妨試一下:
建立好Django項目,直接增加index.py:
<code># -*- coding: utf-8 -*-import osimport sysimport base64from werkzeug.datastructures import Headers, MultiDictfrom werkzeug.wrappers import Responsefrom werkzeug.urls import url_encode, url_unquotefrom werkzeug.http import HTTP_STATUS_CODESfrom werkzeug._compat import BytesIO, string_types, to_bytes, wsgi_encoding_danceimport mydjango.wsgiTEXT_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_casingdef 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_headersdef group_headers(headers): new_headers = {} for key in headers.keys(): new_headers[key] = headers.get_all(key) return new_headersdef 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: >
然後我們部署到函數上,看一下效果:
函數信息:
<code>from django.shortcuts import renderfrom django.http import HttpResponsefrom django.views.decorators.csrf import csrf_exempt# Create your views here.@csrf_exemptdef 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"))/<code>
通過部署完成,並綁定apigw觸發器,然後在postman中進行測試:
get:
post:
可以看到,通過我們對運行原理的基本剖析和對django的改造,我們已經通過增加一個文件和相關依賴的方法,實現了Django上Serverless的過程。
接下來,我們看一下,如何將這個代碼寫成一個Component:
首先Clone下來Flask-Component的代碼:
然後,我們按照Django的部分模式進行修改:
第一部分,是我們可能會依賴的一個依賴包,以及我們剛才放入的index.py文件。在用戶調用這個Component的時候,我們會把這兩個文件,放入用戶的代碼中,一併上傳。
第二部分是Serverless.js部分,這裡的一個基本格式:
<code>const { Component } = require('@serverless/core')class TencentDjango extends Component { async default(inputs = {}) { } async remove(inputs = {}) { }}module.exports = TencentDjango/<code>
用戶在執行sls的時候,會默認調用default的方法,在執行sls remove的時候會調用remove的方法,所以可以認default的內容是部署,而remove的內容是移除。
部署這裡主要流程也蠻簡單的,首先將文件進行復制和處理,然後直接調用雲函數的組件,通過函數中的include參數將這些文件額外加入,再通過調用apigw的組件來進網關的管理,而用戶寫的yaml中inpust的內容,會在inputs中獲取,我們要做的就是對應的傳給不同的組件:
當然除了這兩部分對應放過去,上面的region等一些信息也要對應的進行處理。而調用底層組件方法也很簡單:
<code>const tencentCloudFunction = await this.load('@serverless/tencent-scf'const tencentCloudFunctionOutputs = await tencentCloudFunction(inputs)/<code>
處理好這裡之後,只需要修改一下package.json和readme就可以了。
目前,我已經完成了開源:https://github.com/gosls/tencent-django
也在NPM上進行了發佈:https://www.npmjs.com/package/@gosls/tencent-django
在使用的時候,只需要引入這個Component就好:
<code>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/<code>
至此,完成了Django Component的開發和測試。
閱讀更多 GoServerless 的文章