关于paas:一文读懂-Serverless将配置化思想复用到平台系统中

41次阅读

共计 20040 个字符,预计需要花费 51 分钟才能阅读完成。

作者 | 春哥大魔王
起源 | Serverless 公众号

写在后面

在 SaaS 畛域 Salesforce 是佼佼者,其 CRM 的概念曾经扩大到了 Marketing、Sales、Service 等畛域。那么 Salesforce 靠什么变成了这三个行业的解决方案呢?得益于 Salesforce 弱小的 aPaaS 平台。

ISV、外部施行、客户均能够从本人的维度基于 aPaaS 平台构建本人的行业,实现业务定制,甚至是行业定制。因为在此之前只有在 Sales 方向有专门的 SaaS 产品,而 Marketing 和 Service 都是由本人的 ISV 在各自行业的解决方案。所以 Salesforce 曾经从一家 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 怎么做

很多人把 Serverless 看做是 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. 创立函数

有了服务之后就能够创立函数了,比方抉择基于 http 申请的函数。

  • 选择函数绑定的服务;
  • 设置函数名称;
  • 抉择 runtime 环境;
  • 是否要求函数实例弹性;
  • 函数入口(触发器间接调用的指标办法);
  • 函数执行内存;
  • 函数执行超时工夫;
  • 设置实例并发度。

配置触发器,比方抉择了 HTTP 触发器,而后在触发器上绑定函数名称,因为是 http 拜访,能够抉择拜访的鉴权、认证形式,以及申请形式 POST or GET。

3. 代码编写

当函数创立好了之后,进入函数,能够看到形容、代码执行历史、触发器类型、日志查问页等。
如果是 HTTP 触发器,须要配置 http 触发门路。

能够看到就如后面介绍的那种,相似于类外面的一个函数,上下文申请会打到这里,间接执行。

Python 代码为例:

# -*- 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')]

流程如下:

  • 前端传入代码片段,格局是字符串;
  • 在 FC 函数中获取到传入的代码字符串,截取 code 内容和 input 内容;
  • 将代码保留为一个 py 文件,以工夫戳为文件命名,保留在 FC 函数的 /tmp 目录下,每个函数有本人独立的 /tmp 目录;
  • import time 库代码;
  • 通过 subprocess 创立子流程,以 shell 形式通过 py 命令执行保留在 /tmp 目录下的 py 文件;
  • 最初读取执行后果返回给前端。

前端调用 FC 函数:

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

用 Serverless 协调工作流

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

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

零碎灵活性与扩展性的外围是服务可编排,所以咱们须要做的是将现有零碎外部用户心愿定制的性能进行梳理、拆分、抽离、联合 FC 提供的无状态能力,将这些性能点进行编排,实现业务流程的定制。

需灵便配置工作流的业务

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

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

下面的流程是用户侧的流程,接下来须要转换成程序侧的流程,通过束缚的 FDL 创立工作流,如图:

FDL 代码如下:

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 接口实现全流程:

如想应用流程引擎须要进行相干的备案鉴权:

@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 代码外面流程如何串联起来:

  • 输出要启动的流程名称,比方每次订单编号作为启动流程实例名称;
  • 流程启动后的流程实例名称;
  • 启动输出参数,比方业务参数,比方一个 json 外面有商品、商家、地址、订单等上下文信息。
@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:

@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);
    }
  • 第一局部是启动流程;
  • 第二局部是创立订单对下,并模仿入库。

前端如何调用?

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

  • fnfname:要启动的流程名称;
  • execuname:随机生成 uuid,作为订单的编号,也作为启动流程实例的名称;
  • input:将商品、商家、订单号、地址构建为 JSON 字符串传入流程。
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)
                    }
                })
            }

1. generateInfo 节点

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

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

接下来写函数代码:

# -*- 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 函数。

2. payment 节点

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

- 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 函数相似。

# -*- 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 函数代码:

# -*- 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 发送用户抉择的领取形式和金额。

3. paymentCombination 节点

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

- 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 节点通过判断领取形式流转到具体解决领取逻辑的节点和函数。

4. zhifubao 节点

看一个 zhifubao 节点:

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 函数:

# -*- 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,就是咱们日常基于微服务的平台化 / 中台化解决方案,都能够从中获取设计养分在工作中利用。

正文完
 0