前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文读懂Serverless,配置化思想复用到平台系统中

一文读懂Serverless,配置化思想复用到平台系统中

作者头像
春哥大魔王
发布2021-01-21 09:51:35
7610
发布2021-01-21 09:51:35
举报

写在前面

在SaaS领域Saleforce是佼佼者,其CRM的概念已经扩展到了Marketing、Sales、Service等领域。那么Saleforce靠什么变成了这三个行业的解决方案呢?得益于Saleforce强大的aPaaS平台(如果想了解aPaaS平台可以搜索之前的文章)。

ISV、内部实施、客户均可以从自己的维度基于aPaaS平台构建自己的行业,实现了业务定制,甚至是行业定制。因为在此之前只有在Sales方向有专门的SaaS产品,而Marketing和Service都是由自己的ISV在各自行业的解决方案。所以Saleforce已经从一家SaaS公司变成了一家aPaaS平台公司了。

搭建一个aPaaS平台是需要很长时间的,当然也可以基于一些公有云产品的Serverless方案实现现有系统的灵活性与扩展性,从而实现针对于不同客户的定制。

什么是Serverless

Serverless由两部分组成,Server和Less。

  • 前者可以理解为其解决方案范围处在服务端;
  • 后者可以译为少量的;

组合起来就是较少服务端干预的服务端解决方案。

与Serverless相对的是Serverfull,比较下对应的概念可能更便于理解。

在Serverfull时代,研发交付流程一般有三个角色:RD,PM,QA。

RD根据PM的PRD进行功能开发,交付到QA进行测试,测试完成之后发布到服务器。由运维人员规划服务器规格、数量、机房部署、节点扩缩容等,这种更多由人力处理的时代就是Serverfull时代。

之后进入了DevOps时代。

这个时代运维自己开发一套运维控制台,可以让研发同学在控制台上自己进行服务观测,数据查询,运维处理等,运维同学的工作轻松了不少,这个阶段主要释放了运维同学的人力。

而到了Serverless时代,这套运维控制台能力越来越丰富,可以实现按配置的自动扩缩容、性能监控、DevOps流水线等,同时侵入到研发流程侧,比如自动发布流水线,编译打包,代码质量监测,灰度发布,弹性扩缩等流程基本不需要人力处理了,就是Serverless时代。

Serverless怎么用

详细你有过这样的经历,在一个Web界面上,左侧写代码,右侧展示执行效果。

  • 写的是代码块,代码数量不会特别大
  • 代码运行速度快
  • 支持多种编程语言
  • 可以支持不可预计的流量洪峰冲击

以阿里云解决方案看下如何支持多语言架构:

抽象来说,前端只需要将代码片段和编程语言的标识传给Server端即可,等待响应结果。Server端可以针对于不同编程语言进行runtime分类、预处理等工作。

Serverless怎么做

很多人把Serverlesss看做是FC(function compute:函数计算),使用函数计算,无需业务自己搭建IT基础设施,只需要编码并上传代码。函数计算会按需为你准备好计算资源,弹性、可靠的运行,并提供trace、日志查询、监控告警等治理能力。

比如:

在FC中有服务和函数之分。一个服务可以包含多个函数。

我们可以用微服务理解,我们通过golang或java搭建了一个微服务架构,而fc服务就是其中的类,fc函数是类中的一个方法:

区别在于Java搭建的微服务只能运行java类代码,golang的类只能运行go写的代码,而fc函数可以安装不同语言的runtime,支持运行不同语言程序。

类比理解之后,我们再看下如何调用FC的函数,一般的FC解决方案里面都有一个触发器的概念。比如HTTP触发器、对象存储触发器、日志服务触发器、定时任务触发器、CDN触发器、消息队列触发器等。

触发器是对于fc函数调用的抽象收口,比如http触发器一般都类比网关的一个http请求事件,或是指定对象存储路径下上传了一个图片,这些触发事件的入口都可以是触发器。

触发器产生事件之后可以调用fc函数,函数执行的逻辑可以是下载一张图片或是注册一个用户。

这样从触发器到fc函数逻辑处理就是一个fc的生命周期了。

那么FC如何实现高可用的呢?

其实每个函数底层代码都是运行在一套IaaS平台上,使用IaaS资源,我们可以为每个函数设置运行代码时需要的内存配置即可,比如最小128M,最大3G等。研发人员不需要关心代码运行在什么样的服务器上,不需要关心启动了多少函数实例支持当前场景,不需要关注背后的弹性扩缩问题,这些都被收敛在FC之后。

如图有两种高可用策略:

  • 给函数设置并发实例数,比如3个,那么当有三个请求进来时,该函数只启动一个实例,但是会启动三个线程来运行逻辑;
  • 线程达到上限后,会再拉起一个函数实例;

类似于线程池的方案。

那么Serverless如何提效呢?

  • 效率高:如果新加了语言,只需要创建一个对应的Runtime的FC函数即可;
  • 高可用:通过多线程、多实例两种方式保障高可用,且函数实例扩缩容完全由FC自助处理,不需要运维做任何配置;
  • 成本低:在没有触发器请求时,函数实例不会被拉起,也不会计费,所以在流量低谷期间或者夜间时,FC消耗的成本是非常低的;

如何在云平台创建一个FC

创建服务

  1. 首先新建一个服务名称
  2. 选定服务部署的地区(背后帮助你就近部署在目标机房)
  3. 选择是否打开调试日志(开发过程开启,线上运行时可关闭)

创建函数

有了服务之后就可以创建函数了,比如选择基于http请求的函数;

  1. 选择函数绑定的服务
  2. 设置函数名称
  3. 选择runtime环境
  4. 是否要求函数实例弹性
  5. 函数入口(触发器直接调用的目标方法)
  6. 函数执行内存
  7. 函数执行超时时间
  8. 设置实例并发度

配置触发器,比如选择了http触发器,然后在触发器上绑定函数名称,由于是http访问,可以选择访问的鉴权、认证方式,以及请求方式POST or GET。

代码编写

当函数创建好了之后,进入函数,可以看到描述、代码执行历史、触发器类型、日志查询页等。

如果是HTTP触发器,需要配置http触发路径。

可以看到就如前面介绍的那种,类似于类里面的一个函数,上下文请求会打到这里,直接执行。

Python代码为例:

代码语言:javascript
复制
# -*- coding: utf-8 -*-

import logging
import urllib.parse
import time
import subprocess

def handler(environ, start_response):
    context = environ['fc.context']
    request_uri = environ['fc.request_uri']
    for k, v in environ.items():
      if k.startswith('HTTP_'):
        pass
    try:        
        request_body_size = int(environ.get('CONTENT_LENGTH', 0))    
    except (ValueError):        
        request_body_size = 0   
    # 获取用户传入的code
    request_body = environ['wsgi.input'].read(request_body_size)  
    codeStr = urllib.parse.unquote(request_body.decode("GBK"))
    # 因为body里的对象里有code和input两个属性,这里分别获取用户code和用户输入
    codeArr = codeStr.split('&')
    code = codeArr[0][5:]
    inputStr = codeArr[1][6:]
    # 将用户code保存为py文件,放/tmp目录下,以时间戳为文件名
    fileName = '/tmp/' + str(int(time.time())) + '.py'
    f = open(fileName, "w")
    # 这里预置引入了time库
    f.write('import time \r\n')
    f = open(fileName, "a")
    f.write(code)
    f.close()
    # 创建子进程,执行刚才保存的用户code py文件
    p = subprocess.Popen("python " + fileName, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, encoding='utf-8')
    # 通过标准输入传入用户的input输入
    if inputStr != '' :
        p.stdin.write(inputStr + "\n")
        p.stdin.flush()
    # 通过标准输出获取代码执行结果
    r = p.stdout.read()
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [r.encode('UTF-8')]

流程如下:

  1. 前端传入代码片段,格式是字符串;
  2. 在FC函数中获取到传入的代码字符串,截取code内容和input内容;
  3. 将代码保存为一个py文件,以时间戳为文件命名,保存在fc函数的/tmp目录下,每个函数有自己独立的/tmp目录;
  4. import time库代码;
  5. 通过subprocess创建子流程,以shell方式通过py命令执行保存在/tmp目录下的py文件;
  6. 最后读取执行结果返回给前端;

前端调用fc函数:

整个过程只需要前端将代码传入到FC函数里面,整个Server端各个环节都不需要研发与运维同学关心,体现了Serverless的精髓。

用Serverless协调工作流

工作流可以用顺序、分支、并行等方式来编排任务执行,之后流程会按照设定好的步骤可靠的协调任务执行,跟踪每个任务的状态切换,并在必要时执行定义的重试逻辑,确保流程顺利执行。

工作流流程通过记录日志和审计方式来监视工作流的执行,便于流程的诊断与调试。

系统灵活性与扩展性的核心是服务可编排,所以我们需要做的是将现有系统内部用户希望定制的功能进行梳理、拆分、抽离、结合FC提供的无状态能力,将这些功能点进行编排,实现业务流程的定制。

业务上有哪些需要灵活配置的工作流

举个例子,比如餐饮场景下不同商家可以配置不同的支付方式,可以走微信支付、银联支付、支付宝支付。可以同时支持三家,也可以某一家,可以到付,也可以积分兑换等。如果没有一个好的配置化流程解决方案的话,系统中会出现大量硬编码规则判断条件,系统迭代疲于奔命,是个不可持续的过程。

有了FC搭建的工作流就可以很优雅的解决这种问题,比如规整流程如下:

上面的流程是用户侧的流程,接下来需要转换成程序侧的流程,通过约束的FDL创建工作流,如图:

FDL代码如下:

代码语言:javascript
复制
version: v1beta1
type: flow
timeoutSeconds: 3600
steps:
  - type: task
    name: generateInfo
    timeoutSeconds: 300
    resourceArn: acs:mns:::/topics/generateInfo-fnf-demo-jiyuan/messages
    pattern: waitForCallback
    inputMappings:
      - target: taskToken
        source: $context.task.token
      - target: products
        source: $input.products
      - target: supplier
        source: $input.supplier
      - target: address
        source: $input.address
      - target: orderNum
        source: $input.orderNum
      - target: type
        source: $context.step.name
    outputMappings:
      - target: paymentcombination
        source: $local.paymentcombination
      - target: orderNum
        source: $local.orderNum
    serviceParams:
      MessageBody: $
      Priority: 1
    catch:
      - errors:
          - FnF.TaskTimeout
        goto: orderCanceled
  -type: task
    name: payment
    timeoutSeconds: 300
    resourceArn: acs:mns:::/topics/payment-fnf-demo-jiyuan/messages
    pattern: waitForCallback
    inputMappings:
      - target: taskToken
        source: $context.task.token
      - target: orderNum
        source: $local.orderNum
      - target: paymentcombination
        source: $local.paymentcombination
      - target: type
        source: $context.step.name
    outputMappings:
      - target: paymentMethod
        source: $local.paymentMethod
      - target: orderNum
        source: $local.orderNum
      - target: price
        source: $local.price
      - target: taskToken
        source: $input.taskToken
    serviceParams:
      MessageBody: $
      Priority: 1
    catch:
      - errors:
          - FnF.TaskTimeout
        goto: orderCanceled
  - type: choice
    name: paymentCombination
    inputMappings:
      - target: orderNum
        source: $local.orderNum
      - target: paymentMethod
        source: $local.paymentMethod
      - target: price
        source: $local.price
      - target: taskToken
        source: $local.taskToken
    choices:
      - condition: $.paymentMethod == "zhifubao"
        steps:
          - type: task
            name: zhifubao
            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
            inputMappings:
              - target: price
                source: $input.price            
              - target: orderNum
                source: $input.orderNum
              - target: paymentMethod
                source: $input.paymentMethod
              - target: taskToken
                source: $input.taskToken
      - condition: $.paymentMethod == "weixin"
        steps:
          - type: task
            name: weixin
            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/weixin-fnf-demo
            inputMappings:
            - target: price
              source: $input.price            
            - target: orderNum
              source: $input.orderNum
            - target: paymentMethod
              source: $input.paymentMethod
            - target: taskToken
              source: $input.taskToken
      - condition: $.paymentMethod == "unionpay"
        steps:
          - type: task
            name: unionpay
            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/union-fnf-demo
            inputMappings:
            - target: price
              source: $input.price            
            - target: orderNum
              source: $input.orderNum
            - target: paymentMethod
              source: $input.paymentMethod
            - target: taskToken
              source: $input.taskToken
    default:
      goto: orderCanceled
  - type: task
    name: orderCompleted
    resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/orderCompleted
    end: true
  - type: task
    name: orderCanceled
    resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/cancerOrder

示例体现了基于Serverless的FC可实现灵活工作流。

流程如何触发的呢?

在用户选择完商品、填完地址之后,通过拉取商品、订单上下文,可以自动化触发流程了。

在微服务背景下,很多能力不是闭环在单体代码逻辑之内,很多时候是多个业务系统的连接,比如串联多个OpenAPI接口实现全流程:

如想使用流程引擎需要进行相关的备案鉴权:

代码语言:javascript
复制
@Configuration
public class FNFConfig {
 
    @Bean
    public IAcsClient createDefaultAcsClient(){
        DefaultProfile profile = DefaultProfile.getProfile(
                "cn-xxx",          // 地域ID
                "ak",      // RAM 账号的AccessKey ID
                "sk"); // RAM 账号Access Key Secret
        IAcsClient client = new DefaultAcsClient(profile);
        return client;
    }
 
}

startFNF代码里面流程如何串联起来:

  1. 输入要启动的流程名称,比如每次订单编号作为启动流程实例名称;
  2. 流程启动后的流程实例名称;
  3. 启动输入参数,比如业务参数,比如一个json里面有商品、商家、地址、订单等上下文信息;
代码语言:javascript
复制
   @GetMapping("/startFNF/{fnfname}/{execuname}/{input}")
    public StartExecutionResponse startFNF(@PathVariable("fnfname") String fnfName,
                                           @PathVariable("execuname") String execuName,
                                           @PathVariable("input") String inputStr) throws ClientException {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("fnfname", fnfName);
        jsonObject.put("execuname", execuName);
        jsonObject.put("input", inputStr);
        return fnfService.startFNF(jsonObject);
    }

再看下fnfService.startFNF:

代码语言:javascript
复制
  @Override
    public StartExecutionResponse startFNF(JSONObject jsonObject) throws ClientException {
        StartExecutionRequest request = new StartExecutionRequest();
        String orderNum = jsonObject.getString("execuname");
        request.setFlowName(jsonObject.getString("fnfname"));
        request.setExecutionName(orderNum);
        request.setInput(jsonObject.getString("input"));
 
        JSONObject inputObj = jsonObject.getJSONObject("input");
        Order order = new Order();
        order.setOrderNum(orderNum);
        order.setAddress(inputObj.getString("address"));
        order.setProducts(inputObj.getString("products"));
        order.setSupplier(inputObj.getString("supplier"));
        orderMap.put(orderNum, order);
 
        return iAcsClient.getAcsResponse(request);
    }
  1. 第一部分是启动流程
  2. 第二部分是创建订单对下,并模拟入库

前端如何调用:

在前端当点击选择商品和商家页面中的下一步后,通过 GET 方式调用 HTTP 协议的接口/startFNF/{fnfname}/{execuname}/{input}。和上面的 Java 方法对应。

  • fnfname:要启动的流程名称。
  • execuname:随机生成 uuid,作为订单的编号,也作为启动流程实例的名称。
  • input:将商品、商家、订单号、地址构建为 JSON 字符串传入流程。
代码语言:javascript
复制
            submitOrder(){
                const orderNum = uuid.v1()
                this.$axios.$get('/startFNF/OrderDemo-Jiyuan/'+orderNum+'/{\n' +
                    '  "products": "'+this.products+'",\n' +
                    '  "supplier": "'+this.supplier+'",\n' +
                    '  "orderNum": "'+orderNum+'",\n' +
                    '  "address": "'+this.address+'"\n' +
                    '}' ).then((response) => {
                    console.log(response)
                    if(response.message == "success"){
                        this.$router.push('/orderdemo/' + orderNum)
                    }
                })
            }

generateInfo节点:

先看下第一个FDL节点定义:

代码语言:javascript
复制
 - type: task
    name: generateInfo
    timeoutSeconds: 300
    resourceArn: acs:mns:::/topics/generateInfo-fnf-demo-jiyuan/messages
    pattern: waitForCallback
    inputMappings:
      - target: taskToken
        source: $context.task.token
      - target: products
        source: $input.products
      - target: supplier
        source: $input.supplier
      - target: address
        source: $input.address
      - target: orderNum
        source: $input.orderNum
      - target: type
        source: $context.step.name
    outputMappings:
      - target: paymentcombination
        source: $local.paymentcombination
      - target: orderNum
        source: $local.orderNum
    serviceParams:
      MessageBody: $
      Priority: 1
    catch:
      - errors:
          - FnF.TaskTimeout
        goto: orderCanceled
  • name:节点名称
  • timeoutSeconds:超时时间,节点等待时长,超过时间后跳转到goto分支指向的orderCanceled节点
  • pattern:设置为 waitForCallback,表示需要等待确认。inputMappings:该节点入参。
    • taskToken:Serverless 工作流自动生成的 Token。
    • products:选择的商品。
    • supplier:选择的商家。
    • address:送餐地址。
    • orderNum:订单号。
  • outputMappings:该节点的出参。
    • paymentcombination:该商家支持的支付方式。
    • orderNum:订单号。
  • catch:捕获异常,跳转到其他分支。

Serverless工作流支持多个云服务集成,将其他服务作为任务步骤的执行单元。服务集成方式通过FDL表达式实现,在任务步骤中,可以使用resourceArn来定义集成的目标服务,使用pattern定义集成模式。

在resourceArn中配置/topics/generateInfo-fnf-demo-jiyuan/messages 信息,就是集成了MNS消息队列服务,当generateInfo节点触发后会向generateInfo-fnf-demo-jiyuanTopic 中发送一条消息。消息的正文和参数在serviceParams对象中zhi'd指定。MessageBody是消息正文,配置$表示通过输入映射inputMappings产生消息正文。

generateInfo-fnf-demo 函数:

向 generateInfo-fnf-demo-jiyuanTopic 中发送的这条消息包含了商品信息、商家信息、地址、订单号,表示一个下订单流程的开始,既然有发消息,那么必然有接受消息进行后续处理。在函数计算控制台,创建服务,在服务下创建名为 generateInfo-fnf-demo 的事件触发器函数,这里选择 Python Runtime:

创建MNS触发器,选择监听generateInfo-fnf-demo-jiyuanTopic。

打开消息服务MNS控制台,创建generateInfo-fnf-demo-jiyuanTopic:

接下来写函数代码:

代码语言:javascript
复制
# -*- coding: utf-8 -*-
import logging
import json
import time
import requests
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
from aliyunsdkfnf.request.v20190315 import ReportTaskFailedRequest
 
 
def handler(event, context):
    # 1. 构建Serverless工作流Client
    region = "cn-hangzhou"
    account_id = "XXXX"
    ak_id = "XXX"
    ak_secret = "XXX"
    fnf_client = AcsClient(
        ak_id,
        ak_secret,
        region
    )
    logger = logging.getLogger()
    # 2. event内的信息即接受到Topic generateInfo-fnf-demo-jiyuan中的消息内容,将其转换为Json对象
    bodyJson = json.loads(event)
    logger.info("products:" + bodyJson["products"])
    logger.info("supplier:" + bodyJson["supplier"])
    logger.info("address:" + bodyJson["address"])
    logger.info("taskToken:" + bodyJson["taskToken"])
    supplier = bodyJson["supplier"]
    taskToken = bodyJson["taskToken"]
    orderNum = bodyJson["orderNum"]
    # 3. 判断什么商家使用什么样的支付方式组合,这里的示例比较简单粗暴,正常情况下,应该使用元数据配置的方式获取
    paymentcombination = ""
    if supplier == "haidilao":
        paymentcombination = "zhifubao,weixin"
    else:
        paymentcombination = "zhifubao,weixin,unionpay"
 
    # 4. 调用Java服务暴露的接口,更新订单信息,主要是更新支付方式
    url = "http://xx.xx.xx.xx:8080/setPaymentCombination/" + orderNum + "/" + paymentcombination + "/0"
    x = requests.get(url)
 
    # 5. 给予generateInfo节点响应,并返回数据,这里返回了订单号和支付方式
    output = "{\"orderNum\": \"%s\", \"paymentcombination\":\"%s\" " \
                         "}" % (orderNum, paymentcombination)
    request = ReportTaskSucceededRequest.ReportTaskSucceededRequest()
    request.set_Output(output)
    request.set_TaskToken(taskToken)
    resp = fnf_client.do_action_with_exception(request)
    return 'hello world'

代码分五部分:

  • 构建 Serverless 工作流 Client。
  • event 内的信息即接受到 TopicgenerateInfo-fnf-demo-jiyuan 中的消息内容,将其转换为 Json 对象。
  • 判断什么商家使用什么样的支付方式组合,这里的示例比较简单粗暴,正常情况下,应该使用元数据配置的方式获取。比如在系统内有商家信息的配置功能,通过在界面上配置该商家支持哪些支付方式,形成元数据配置信息,提供查询接口,在这里进行查询。
  • 调用 Java 服务暴露的接口,更新订单信息,主要是更新支付方式。
  • 给予 generateInfo 节点响应,并返回数据,这里返回了订单号和支付方式。因为该节点的 pattern 是 waitForCallback,所以需要等待响应结果。

generateInfo-fnf-demo 函数配置了 MNS 触发器,当 TopicgenerateInfo-fnf-demo-jiyuan 有消息后就会触发执行 generateInfo-fnf-demo 函数。

payment 节点:

接下来是payment的FDL代码定义:

代码语言:javascript
复制
- type: task
    name: payment
    timeoutSeconds: 300
    resourceArn: acs:mns:::/topics/payment-fnf-demo-jiyuan/messages
    pattern: waitForCallback
    inputMappings:
      - target: taskToken
        source: $context.task.token
      - target: orderNum
        source: $local.orderNum
      - target: paymentcombination
        source: $local.paymentcombination
      - target: type
        source: $context.step.name
    outputMappings:
      - target: paymentMethod
        source: $local.paymentMethod
      - target: orderNum
        source: $local.orderNum
      - target: price
        source: $local.price
      - target: taskToken
        source: $input.taskToken
    serviceParams:
      MessageBody: $
      Priority: 1
    catch:
      - errors:
          - FnF.TaskTimeout
        goto: orderCanceled

当流程流转到payment节点后,用户就可以进入到支付页面。

payment 节点会向 MNS 的 Topicpayment-fnf-demo-jiyuan 发送消息,会触发 payment-fnf-demo 函数。

payment-fnf-demo 函数:

payment-fnf-demo 函数的创建方式和 generateInfo-fnf-demo 函数类似。

代码语言:javascript
复制
# -*- coding: utf-8 -*-
import logging
import json
import os
import time
import logging
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkcore.client import AcsClient
from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
from aliyunsdkfnf.request.v20190315 import ReportTaskFailedRequest
from mns.account import Account  # pip install aliyun-mns
from mns.queue import *
 
 
def handler(event, context):
    logger = logging.getLogger()
    region = "xxx"
    account_id = "xxx"
    ak_id = "xxx"
    ak_secret = "xxx"
    mns_endpoint = "http://your_account_id.mns.cn-hangzhou.aliyuncs.com/"
    queue_name = "payment-queue-fnf-demo"
    my_account = Account(mns_endpoint, ak_id, ak_secret)
    my_queue = my_account.get_queue(queue_name)
    # my_queue.set_encoding(False)
    fnf_client = AcsClient(
        ak_id,
        ak_secret,
        region
    )
    eventJson = json.loads(event)
 
    isLoop = True
    while isLoop:
        try:
            recv_msg = my_queue.receive_message(30)
            isLoop = False
            # body = json.loads(recv_msg.message_body)
            logger.info("recv_msg.message_body:======================" + recv_msg.message_body)
            msgJson = json.loads(recv_msg.message_body)
            my_queue.delete_message(recv_msg.receipt_handle)
            # orderCode = int(time.time())
            task_token = eventJson["taskToken"]
            orderNum = eventJson["orderNum"]
            output = "{\"orderNum\": \"%s\", \"paymentMethod\": \"%s\", \"price\": \"%s\" " \
                         "}" % (orderNum, msgJson["paymentMethod"], msgJson["price"])
            request = ReportTaskSucceededRequest.ReportTaskSucceededRequest()
            request.set_Output(output)
            request.set_TaskToken(task_token)
            resp = fnf_client.do_action_with_exception(request)
        except Exception as e:
            logger.info("new loop")
    return 'hello world'

上面代码核心思路是等待用户在支付页面选择某个支付方式确认支付。使用了 MNS 的队列来模拟等待。循环等待接收队列 payment-queue-fnf-demo 中的消息,当收到消息后将订单号和用户选择的具体支付方式以及金额返回给 payment 节点。

前端选择支付方式页面:

经过 generateInfo 节点后,该订单的支付方式信息已经有了,所以对于用户而言,当填完商品、商家、地址后,跳转到的页面就是该确认支付页面,并且包含了该商家支持的支付方式。

进入该页面后,会请求 Java 服务暴露的接口,获取订单信息,根据支付方式在页面上显示不同的支付方式。代码片段如下:

当用户选定某个支付方式点击提交订单按钮后,向 payment-queue-fnf-demo 队列发送消息,即通知 payment-fnf-demo 函数继续后续的逻辑。

使用了一个 HTTP 触发器类型的函数,用于实现向 MNS 发消息的逻辑,paymentMethod-fnf-demo 函数代码:

代码语言:javascript
复制
# -*- coding: utf-8 -*-
 
import logging
import urllib.parse
import json
from mns.account import Account  # pip install aliyun-mns
from mns.queue import *
HELLO_WORLD = b'Hello world!\n'
 
def handler(environ, start_response):
    logger = logging.getLogger() 
    context = environ['fc.context']
    request_uri = environ['fc.request_uri']
    for k, v in environ.items():
      if k.startswith('HTTP_'):
        # process custom request headers
        pass
    try:       
        request_body_size = int(environ.get('CONTENT_LENGTH', 0))   
    except (ValueError):       
        request_body_size = 0  
    request_body = environ['wsgi.input'].read(request_body_size) 
    paymentMethod = urllib.parse.unquote(request_body.decode("GBK"))
    logger.info(paymentMethod)
    paymentMethodJson = json.loads(paymentMethod)
 
    region = "cn-xxx"
    account_id = "xxx"
    ak_id = "xxx"
    ak_secret = "xxx"
    mns_endpoint = "http://your_account_id.mns.cn-hangzhou.aliyuncs.com/"
    queue_name = "payment-queue-fnf-demo"
    my_account = Account(mns_endpoint, ak_id, ak_secret)
    my_queue = my_account.get_queue(queue_name)
    output = "{\"paymentMethod\": \"%s\", \"price\":\"%s\" " \
                         "}" % (paymentMethodJson["paymentMethod"], paymentMethodJson["price"])
    msg = Message(output)
    my_queue.send_message(msg)
   
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [HELLO_WORLD]

函数的逻辑很简单,就是向 MNS 的队列 payment-queue-fnf-demo 发送用户选择的支付方式和金额。

paymentCombination 节点:

paymentCombination 节点是一个路由节点,通过判断某个参数路由到不同的节点,以 paymentMethod 作为判断条件:

代码语言:javascript
复制
- type: choice
    name: paymentCombination
    inputMappings:
      - target: orderNum
        source: $local.orderNum
      - target: paymentMethod
        source: $local.paymentMethod
      - target: price
        source: $local.price
      - target: taskToken
        source: $local.taskToken
    choices:
      - condition: $.paymentMethod == "zhifubao"
        steps:
          - type: task
            name: zhifubao
            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
            inputMappings:
              - target: price
                source: $input.price            
              - target: orderNum
                source: $input.orderNum
              - target: paymentMethod
                source: $input.paymentMethod
              - target: taskToken
                source: $input.taskToken
      - condition: $.paymentMethod == "weixin"
        steps:
          - type: task
            name: weixin
            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/weixin-fnf-demo
            inputMappings:
            - target: price
              source: $input.price            
            - target: orderNum
              source: $input.orderNum
            - target: paymentMethod
              source: $input.paymentMethod
            - target: taskToken
              source: $input.taskToken
      - condition: $.paymentMethod == "unionpay"
        steps:
          - type: task
            name: unionpay
            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/union-fnf-demo
            inputMappings:
            - target: price
              source: $input.price            
            - target: orderNum
              source: $input.orderNum
            - target: paymentMethod
              source: $input.paymentMethod
            - target: taskToken
              source: $input.taskToken
    default:
      goto: orderCanceled

流程是,用户选择支付方式后,通过消息发送给 payment-fnf-demo 函数,然后将支付方式返回,于是流转到 paymentCombination 节点通过判断支付方式流转到具体处理支付逻辑的节点和函数。

zhifubao节点:

看一个 zhifubao 节点:

代码语言:javascript
复制
choices:
      - condition: $.paymentMethod == "zhifubao"
        steps:
          - type: task
            name: zhifubao
            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
            inputMappings:
              - target: price
                source: $input.price            
              - target: orderNum
                source: $input.orderNum
              - target: paymentMethod
                source: $input.paymentMethod
              - target: taskToken
                source: $input.taskToken

节点的 resourceArn 和之前两个节点的不同,这里配置的是函数计算中函数的 ARN,也就是说当流程流转到这个节点时会触发 zhifubao-fnf-demo 函数,该函数是一个事件触发函数,但不需要创建任何触发器。流程将订单金额、订单号、支付方式传给 zhifubao-fnf-demo 函数。

zhifubao-fnf-demo 函数:

代码语言:javascript
复制
# -*- coding: utf-8 -*-
import logging
import json
import requests
import urllib.parse
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
from aliyunsdkfnf.request.v20190315 import ReportTaskFailedRequest
 
 
def handler(event, context):
  region = "cn-xxx"
  account_id = "xxx"
  ak_id = "xxx"
  ak_secret = "xxx"
  fnf_client = AcsClient(
    ak_id,
    ak_secret,
    region
  )
  logger = logging.getLogger()
  logger.info(event)
  bodyJson = json.loads(event)
  price = bodyJson["price"]
  taskToken = bodyJson["taskToken"]
  orderNum = bodyJson["orderNum"]
  paymentMethod = bodyJson["paymentMethod"]
  logger.info("price:" + price)
  newPrice = int(price) * 0.8
  logger.info("newPrice:" + str(newPrice))
  url = "http://xx.xx.xx.xx:8080/setPaymentCombination/" + orderNum + "/" + paymentMethod + "/" + str(newPrice)
  x = requests.get(url)
 
  return {"Status":"ok"}

代码逻辑很简单,接收到金额后,将金额打 8 折,然后将价格更新回订单。其他支付方式的节点和函数如法炮制,变更实现逻辑就可以。在这个示例中,微信支付打了 5 折,银联支付打 7 折。

完整流程

流程中的 orderCompleted 和 orderCanceled 节点没做什么逻辑,流程如下:

从 Serverless 工作流中看到的节点流转是这样的:

写在后面

以上是一个基于Serverless的fc实现的工作流,模拟构建了一个订单模块,规则包括:

  • 配置商家和支付方式的元数据规则
  • 确认支付页面的元数据规则

在实际项目中,需要将可定制的部分抽象为元数据描述,需要有配置界面供运营或商家定制支付方式也就是元数据规则,然后前后端页面基于元数据信息展示相应的内容。

如果之后需要接入新的支付方式,只需要在paymentCombination路由节点中确定好路由规则,之后增加对应的支付方式函数即可,通过增加元数据配置项,就可以在页面展示新加的支付方式,并路由到新的支付函数中。

经过整片文章相信很多人对于serverless的定义,以及如何基于现有的公有云系统的serverless功能实现商业能力了,甚至基于此有实力的公司可以自研一套serverless平台。当然思想是相同的,其实文中很多逻辑与理论不止适用于serverless,就是我们日常基于微服务的平台化/中台化解决方案,都可以从中获取设计营养在工作中应用。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-01-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 春哥talk 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 什么是Serverless
  • Serverless怎么用
  • Serverless怎么做
  • 如何在云平台创建一个FC
    • 创建服务
      • 创建函数
        • 代码编写
        • 用Serverless协调工作流
          • 业务上有哪些需要灵活配置的工作流
            • 完整流程
            • 写在后面
            相关产品与服务
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档