Serverless传统Web框架迁移

与其说Serverless架构是一个新的概念/架构,不如说它是一个全新的思路、一种新的编程范式。在这种新的架构或者说新的编程范式下,使用全新的思路来做Serverless应用是再好不过的了,但是实际上并不是这样。原生的Serverless开发框架是非常少的。以Web框架为例,目前主流的Web框架均不支持Serverless部署,所以将传统框架更简单、更快速、更科学地部署到Serverless架构上就是一个值得探讨的问题。

1 请求集成方案

请求集成方案实际上就是把真实的API网关请求直接传递给FaaS平台,而不在中途增加任何转换逻辑。以阿里云函数计算的HTTP函数为例,当想要把传统框架(例如Django、Flask、Express、Next.js等)部署到阿里云函数计算平台上,并且享受Serverless带来的按量付费、弹性伸缩等红利时,得益于阿里云函数计算的HTTP函数和HTTP触发器,使用者不仅可以快速、简单地将框架部署到阿里云函数计算上,还可以保持和传统开发一样的体验。以Python的Bottle框架为例,开发一个Bottle项目:

# index.py 
import bottle 
@bottle.route('/hello/<name>') 
def index(name):    
    return "Hello world" 

if __name__ == '__main__':    
    bottle.run(host='localhost', port=8080, debug=True)

当想要把该项目部署到阿里云函数计算上时,只需要增加一个default_app对象即可:

app = bottle.default_app()

整个项目实现如下:

# index.py 
import bottle 
@bottle.route('/hello/<name>') 
def index(name):    
    return "Hello world" 

app = bottle.default_app()

if __name__ == '__main__':    
    bottle.run(host='localhost', port=8080, debug=True)

当想在阿里云函数计算平台创建函数时,将函数入口设置为index.app即可。除了Bottle框架之外,其他的Web框架的操作方法是类似的。再以Flask为例:

# index.py 
from flask import Flask 
app = Flask(__name__) 
@app.route('/') 
def hello_world():    
    return 'Hello, World!' 

if __name__ == '__main__':    
    app.run(host="0.0.0.0", port=int("8001"))

在配置函数的时候设置入口函数为index.app,即可保证该Flask项目运行在函数计算平台上。

当然,除了使用已有的语言化的Runtime,还可以考虑使用Custom Runtime和Custom Container来实现。例如,一个Web项目完成之后,我们可以编写一个Bootstrap文件(在Bootstrap文件中写一些启动命令即可),要启动一个Express项目,把Express项目准备好之后,直接通过Bootstrap实现:

#!/usr/bin/env 
bash export PORT=9000 
npm run star

除了上面的方法,其实阿里云函数计算还提供了更简单的Web框架迁移方案,即直接将传统Web框架迁移到函数计算中。我们在函数计算控制台找到应用中心,可以看到Web应用框架,如下所示。

 

阿里云函数计算应用中心

选择好对应的环境之后,只需要上传代码、做好简单的配置,即可让传统Web项目运行在阿里云函数计算平台上。

如果通过开发者工具进行部署,以Serverless Devs为例,可以先创建index.py:

# -*- coding: utf-8 -*- 
from bottle import route, run 
@route('/') 
def hello():    
    return "Hello World!" 

run(host='0.0.0.0', debug=False, port=9000)

然后编写资源和行为描述文件:

edition: 1.0.0 
name: functionApp 
access: defaule services:    
bottleExample:                                           
#服务名称        
component: devsapp/bottle                            
#组件名称        
actions:            
pre-deploy:                                      
#在deploy之前运行                
- run: pip3 install -t . -r requirements.txt 
#要运行的命令行                    
path: ./src                              
#命令行运行的路径        
props:                                               
#组件的属性值            
region: cn-shenzhen           
service:                
name: serverless-devs-bottle                
description: Serverless 
#Devs示例程序            
function:                
name: bottle                
description: bottle项目                
memorySize: 256                
code:                    
src: ./src                
customContainerConfig:                    
command: '["python3"]'                    
args: '["./bottle/index.py"]'

完成之后,执行deploy指令:

s deploy

部署如下所示。

 

在Serverless Devs上部署bottle框架

根据返回的网址,可以看到如图3-51所示的结果。

 

Serverless Devs部署结果预览

综上所述,通过阿里云函数计算进行传统Web框架的部署和迁移是相对方便的,并且得益于HTTP函数与HTTP触发器,整个过程侵入性非常低。当然,将传统Web框架部署到阿里云函数计算时,可选方案也是比较多的。

  • 编程语言化的Runtime:只需要写好函数入口即可。
  • Custom Runtime:只需要写好Bootstrap即可。
  • Custom Container:直接按照规范上传镜像文件即可。

部署途径也是多种多样的,具体如下。

  • 直接在控制台创建函数。
  • 在应用中心处创建Web应用。
  • 使用开发者工具直接部署。

2 其他方案

相对于阿里云的HTTP函数以及HTTP触发器而言,AWS、华为云、腾讯云等FaaS平台需要借助API网关以及转换层来实现将传统Web框架部署到FaaS平台。

如下所示,通常情况下使用Flask等框架实际上要通过Web Server进入下一个环节,而云函数更多是一个函数,本不需要启动Web Server,所以可以直接调用wsgi_app方法。

 

传统WSGI Web Server工作原理示例

这里的environ就是需要对event/context等进行处理的对象,也就是所说的转换层要做的工作;start_response可以认为是一种特殊的数据结构,例如response结构形态等。以Flask项目为例,在腾讯云云函数上,转换层结构如下:

import sys 
import json 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 

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:            
            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        
            }

当然,转换在某些情况下还是比较麻烦的,所以在很多时候,我们可以借助常见的开发者工具进行传统Web框架的部署,例如借助开源的开发者工具Serverless Devs、Serverless Framework等。

 

posted @ 2023-01-26 11:00  muzinan110  阅读(31)  评论(0编辑  收藏  举报