伙伴云客服论坛»论坛 S区 S行业资讯 查看内容

0 评论

0 收藏

分享

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

简介: 搭建一个 aPaaS 平台是需要很长时间的,当然也可以基于一些公有云产品的 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 界面上,左侧写代码,右侧展示执行效果。
一文读懂 Serverless,将配置化思想复用到平台系统中-1.png


    写的是代码块,代码数量不会特别大;代码运行速度快;支持多种编程语言;可以支持不可估计的流量洪峰冲击。
以阿里云处置方案看下如何支持多语言架构:
一文读懂 Serverless,将配置化思想复用到平台系统中-2.png


笼统来说,前端只需要将代码片段和编程语言的标识传给 Server 端即可,等待响应结果。Server 端可以针对于不同编程语言停止 runtime 分类、预处置等工作。
Serverless 怎么做

很多人把 Serverless 看做是 FC(function compute:函数计算),使用函数计算,无需业务自己搭建 IT 根底设备,只需要编码并上传代码。函数计算会按需为你准备好计算资源,弹性、可靠地运行,并提供 trace、日志查询、监控告警等治理才干。
比如:
一文读懂 Serverless,将配置化思想复用到平台系统中-3.png


在 FC 中有效劳和函数之分。一个效劳可以包含多个函数。我们可以用微效劳理解,我们通过 golang 或 java 搭建了一个微效劳架构,而 FC 效劳就是其中的类,FC 函数是类中的一个方法:
一文读懂 Serverless,将配置化思想复用到平台系统中-4.png


区别在于 Java 搭建的微效劳只能运行 java 类代码,golang 的类只能运行 go 写的代码,而 FC 函数可以装置不同语言的 runtime,支持运行不同语言程序。
一文读懂 Serverless,将配置化思想复用到平台系统中-5.png


类比理解之后,我们再看下如何调用 FC 的函数,一般的 FC 处置方案里面都有一个触发器的概念。比如 HTTP 触发器、对象存储触发器、日志效劳触发器、定时任务触发器、CDN 触发器、消息队列触发器等。触发器是对于 FC 函数调用的笼统收口,比如 HTTP 触发器一般都类比网关的一个 http 恳求事件,或是指定对象存储途径下上传了一个图片,这些触发事件的入口都可以是触发器。
一文读懂 Serverless,将配置化思想复用到平台系统中-6.png


触发器产惹事件之后可以调用 FC 函数,函数执行的逻辑可以是下载一张图片或是注册一个用户。
这样从触发器到 FC 函数逻辑处置就是一个 FC 的生命周期了。
那么 FC 是如何实现高可用的呢?
其实每个函数底层代码都是运行在一套 IaaS 平台上,使用 IaaS 资源,我们可以为每个函数设置运行代码时需要的内存配置即可,比如最小 128M,最大 3G 等。研发人员不需要关心代码运行在什么样的效劳器上,不需要关心启动了多少函数实例支持当前场景,不需要关注背后的弹性扩缩问题,这些都被收敛在 FC 之后。
一文读懂 Serverless,将配置化思想复用到平台系统中-7.png


如图有两种高可用战略:
    给函数设置并发实例数,比如 3 个,那么当有三个恳求进来时,该函数只启动一个实例,但是会启动三个线程来运行逻辑;线程到达上限后,会再拉起一个函数实例。
类似于线程池的方案。
一文读懂 Serverless,将配置化思想复用到平台系统中-8.png


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

如何在云平台创建一个 FC

1. 创建效劳

    首先新建一个效劳名称;选定效劳部署的地区(背后协助你就近部署在目的机房);选择是否翻开调试日志(开发过程开启,线上运行时可关闭)。
2. 创建函数

有了效劳之后就可以创建函数了,比如选择基于 http 恳求的函数。
    选择函数绑定的效劳;设置函数名称;选择 runtime 环境;是否要求函数实例弹性;函数入口(触发器直接调用的目的方法);函数执行内存;函数执行超时时间;设置实例并发度。
一文读懂 Serverless,将配置化思想复用到平台系统中-9.png


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

当函数创建好了之后,进入函数,可以看到描绘、代码执行历史、触发器类型、日志查询页等。
假设是 HTTP 触发器,需要配置 http 触发途径。
一文读懂 Serverless,将配置化思想复用到平台系统中-10.png


可以看到就如前面介绍的那种,类似于类里面的一个函数,上下文恳求会打到这里,直接执行。
Python 代码为例:
  1. # -*- coding: utf-8 -*-
  2. import logging
  3. import urllib.parse
  4. import time
  5. import subprocess
  6. def handler(environ, start_response):
  7.     context = environ['fc.context']
  8.     request_uri = environ['fc.request_uri']
  9.     for k, v in environ.items():
  10.       if k.startswith('HTTP_'):
  11.         pass
  12.     try:        
  13.         request_body_size = int(environ.get('CONTENT_LENGTH', 0))   
  14.     except (ValueError):        
  15.         request_body_size = 0   
  16.     # 获取用户传入的code
  17.     request_body = environ['wsgi.input'].read(request_body_size)  
  18.     codeStr = urllib.parse.unquote(request_body.decode("GBK"))
  19.     # 因为body里的对象里有code和input两个属性,这里分别获取用户code和用户输入
  20.     codeArr = codeStr.split('&')
  21.     code = codeArr[0][5:]
  22.     inputStr = codeArr[1][6:]
  23.     # 将用户code保管为py文件,放/tmp目录下,以时间戳为文件名
  24.     fileName = '/tmp/' + str(int(time.time())) + '.py'
  25.     f = open(fileName, "w")
  26.     # 这里预置引入了time库
  27.     f.write('import time \r\n')
  28.     f = open(fileName, "a")
  29.     f.write(code)
  30.     f.close()
  31.     # 创建子进程,执行方才保管的用户code py文件
  32.     p = subprocess.Popen("python " + fileName, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, encoding='utf-8')
  33.     # 通过规范输入传入用户的input输入
  34.     if inputStr != '' :
  35.         p.stdin.write(inputStr + "\n")
  36.         p.stdin.flush()
  37.     # 通过规范输出获取代码执行结果
  38.     r = p.stdout.read()
  39.     status = '200 OK'
  40.     response_headers = [('Content-type', 'text/plain')]
  41.     start_response(status, response_headers)
  42.     return [r.encode('UTF-8')]
复制代码
流程如下:
    前端传入代码片段,格式是字符串;在 FC 函数中获取到传入的代码字符串,截取 code 内容和 input 内容;将代码保管为一个 py 文件,以时间戳为文件命名,保管在 FC 函数的 /tmp 目录下,每个函数有自己独立的 /tmp 目录;import time 库代码;通过 subprocess 创建子流程,以 shell 方式通过 py 命令执行保管在 /tmp 目录下的 py 文件;最后读取执行结果返回给前端。
前端调用 FC 函数:
一文读懂 Serverless,将配置化思想复用到平台系统中-11.png


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

工作流可以用顺序、分支、并行等方式来编排任务执行,之后流程会依照设定好的步骤可靠地协调任务执行,跟踪每个任务的状态切换,并在必要时执行定义的重试逻辑,确保流程顺利执行。
工作流流程通过记录日志和审计方式来监视工作流的执行,便于流程的诊断与调试。
一文读懂 Serverless,将配置化思想复用到平台系统中-12.png


系统灵敏性与扩展性的核心是效劳可编排,所以我们需要做的是将现有系统内部用户希望定制的功能停止梳理、拆分、抽离、结合 FC 提供的无状态才干,将这些功能点停止编排,实现业务流程的定制。
需灵敏配置工作流的业务

举个例子,比如餐饮场景下不同商家可以配置不同的支付方式,可以走微信支付、银联支付、支付宝支付。可以同时支持三家,也可以某一家,可以到付,也可以积分兑换等。假设没有一个好的配置化流程处置方案的话,系统中会呈现大量硬编码规则判断条件,系统迭代疲于奔命,是个不可持续的过程。
有了 FC 搭建的工作流就可以很优雅地处置这种问题,比如规整流程如下:
一文读懂 Serverless,将配置化思想复用到平台系统中-13.png


上面的流程是用户侧的流程,接下来需要转换成程序侧的流程,通过约束的 FDL 创建工作流,如图:
一文读懂 Serverless,将配置化思想复用到平台系统中-14.png


FDL 代码如下:
  1. version: v1beta1
  2. type: flow
  3. timeoutSeconds: 3600
  4. steps:
  5.   - type: task
  6.     name: generateInfo
  7.     timeoutSeconds: 300
  8.     resourceArn: acs:mns:::/topics/generateInfo-fnf-demo-jiyuan/messages
  9.     pattern: waitForCallback
  10.     inputMappings:
  11.       - target: taskToken
  12.         source: $context.task.token
  13.       - target: products
  14.         source: $input.products
  15.       - target: supplier
  16.         source: $input.supplier
  17.       - target: address
  18.         source: $input.address
  19.       - target: orderNum
  20.         source: $input.orderNum
  21.       - target: type
  22.         source: $context.step.name
  23.     outputMappings:
  24.       - target: paymentcombination
  25.         source: $local.paymentcombination
  26.       - target: orderNum
  27.         source: $local.orderNum
  28.     serviceParams:
  29.       MessageBody: $
  30.       Priority: 1
  31.     catch:
  32.       - errors:
  33.           - FnF.TaskTimeout
  34.         goto: orderCanceled
  35.   -type: task
  36.     name: payment
  37.     timeoutSeconds: 300
  38.     resourceArn: acs:mns:::/topics/payment-fnf-demo-jiyuan/messages
  39.     pattern: waitForCallback
  40.     inputMappings:
  41.       - target: taskToken
  42.         source: $context.task.token
  43.       - target: orderNum
  44.         source: $local.orderNum
  45.       - target: paymentcombination
  46.         source: $local.paymentcombination
  47.       - target: type
  48.         source: $context.step.name
  49.     outputMappings:
  50.       - target: paymentMethod
  51.         source: $local.paymentMethod
  52.       - target: orderNum
  53.         source: $local.orderNum
  54.       - target: price
  55.         source: $local.price
  56.       - target: taskToken
  57.         source: $input.taskToken
  58.     serviceParams:
  59.       MessageBody: $
  60.       Priority: 1
  61.     catch:
  62.       - errors:
  63.           - FnF.TaskTimeout
  64.         goto: orderCanceled
  65.   - type: choice
  66.     name: paymentCombination
  67.     inputMappings:
  68.       - target: orderNum
  69.         source: $local.orderNum
  70.       - target: paymentMethod
  71.         source: $local.paymentMethod
  72.       - target: price
  73.         source: $local.price
  74.       - target: taskToken
  75.         source: $local.taskToken
  76.     choices:
  77.       - condition: $.paymentMethod == "zhifubao"
  78.         steps:
  79.           - type: task
  80.             name: zhifubao
  81.             resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
  82.             inputMappings:
  83.               - target: price
  84.                 source: $input.price            
  85.               - target: orderNum
  86.                 source: $input.orderNum
  87.               - target: paymentMethod
  88.                 source: $input.paymentMethod
  89.               - target: taskToken
  90.                 source: $input.taskToken
  91.       - condition: $.paymentMethod == "weixin"
  92.         steps:
  93.           - type: task
  94.             name: weixin
  95.             resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/weixin-fnf-demo
  96.             inputMappings:
  97.             - target: price
  98.               source: $input.price            
  99.             - target: orderNum
  100.               source: $input.orderNum
  101.             - target: paymentMethod
  102.               source: $input.paymentMethod
  103.             - target: taskToken
  104.               source: $input.taskToken
  105.       - condition: $.paymentMethod == "unionpay"
  106.         steps:
  107.           - type: task
  108.             name: unionpay
  109.             resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/union-fnf-demo
  110.             inputMappings:
  111.             - target: price
  112.               source: $input.price            
  113.             - target: orderNum
  114.               source: $input.orderNum
  115.             - target: paymentMethod
  116.               source: $input.paymentMethod
  117.             - target: taskToken
  118.               source: $input.taskToken
  119.     default:
  120.       goto: orderCanceled
  121.   - type: task
  122.     name: orderCompleted
  123.     resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/orderCompleted
  124.     end: true
  125.   - type: task
  126.     name: orderCanceled
  127.     resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/cancerOrder
复制代码
示例体现了基于 Serverless 的 FC 可实现灵敏工作流。
流程如何触发的呢?
一文读懂 Serverless,将配置化思想复用到平台系统中-15.png


在用户选择完商品、填完地址之后,通过拉取商品、订单上下文,可以自动化触发流程了。
在微效劳背景下,很多才干不是闭环在单体代码逻辑之内,很多时候是多个业务系统的连接,比如串联多个 OpenAPI 接口实现全流程:
一文读懂 Serverless,将配置化思想复用到平台系统中-16.png


如想使用流程引擎需要停止相关的备案鉴权:
  1. @Configuration
  2. public class FNFConfig {
  3.     @Bean
  4.     public IAcsClient createDefaultAcsClient(){
  5.         DefaultProfile profile = DefaultProfile.getProfile(
  6.                 "cn-xxx",          // 地域ID
  7.                 "ak",      // RAM 账号的AccessKey ID
  8.                 "sk"); // RAM 账号Access Key Secret
  9.         IAcsClient client = new DefaultAcsClient(profile);
  10.         return client;
  11.     }
  12. }
复制代码
startFNF 代码里面流程如何串联起来:
    输入要启动的流程名称,比如每次订单编号作为启动流程实例名称;流程启动后的流程实例名称;启动输入参数,比如业务参数,比如一个 json 里面有商品、商家、地址、订单等上下文信息。
  1. @GetMapping("/startFNF/{fnfname}/{execuname}/{input}")
  2.     public StartExecutionResponse startFNF(@PathVariable("fnfname") String fnfName,
  3.                                            @PathVariable("execuname") String execuName,
  4.                                            @PathVariable("input") String inputStr) throws ClientException {
  5.         JSONObject jsonObject = new JSONObject();
  6.         jsonObject.put("fnfname", fnfName);
  7.         jsonObject.put("execuname", execuName);
  8.         jsonObject.put("input", inputStr);
  9.         return fnfService.startFNF(jsonObject);
  10.     }
复制代码
再看下 fnfService.startFNF:
  1. @Override
  2.     public StartExecutionResponse startFNF(JSONObject jsonObject) throws ClientException {
  3.         StartExecutionRequest request = new StartExecutionRequest();
  4.         String orderNum = jsonObject.getString("execuname");
  5.         request.setFlowName(jsonObject.getString("fnfname"));
  6.         request.setExecutionName(orderNum);
  7.         request.setInput(jsonObject.getString("input"));
  8.         JSONObject inputObj = jsonObject.getJSONObject("input");
  9.         Order order = new Order();
  10.         order.setOrderNum(orderNum);
  11.         order.setAddress(inputObj.getString("address"));
  12.         order.setProducts(inputObj.getString("products"));
  13.         order.setSupplier(inputObj.getString("supplier"));
  14.         orderMap.put(orderNum, order);
  15.         return iAcsClient.getAcsResponse(request);
  16.     }
复制代码
    第一部分是启动流程;第二部分是创建订单对下,并模仿入库。
前端如何调用?
在前端当点击选择商品和商家页面中的下一步后,通过 GET 方式调用 HTTP 协议的接口 /startFNF/{fnfname}/{execuname}/{input}。和上面的 Java 方法对应。
    fnfname:要启动的流程名称;execuname:随机生成 uuid,作为订单的编号,也作为启动流程实例的名称;input:将商品、商家、订单号、地址构建为 JSON 字符串传入流程。
  1. submitOrder(){
  2.                 const orderNum = uuid.v1()
  3.                 this.$axios.$get('/startFNF/OrderDemo-Jiyuan/'+orderNum+'/{\n' +
  4.                     '  "products": "'+this.products+'",\n' +
  5.                     '  "supplier": "'+this.supplier+'",\n' +
  6.                     '  "orderNum": "'+orderNum+'",\n' +
  7.                     '  "address": "'+this.address+'"\n' +
  8.                     '}' ).then((response) => {
  9.                     console.log(response)
  10.                     if(response.message == "success"){
  11.                         this.$router.push('/orderdemo/' + orderNum)
  12.                     }
  13.                 })
  14.             }
复制代码
1. generateInfo 节点

先看下第一个 FDL 节点定义:
  1. - type: task
  2.     name: generateInfo
  3.     timeoutSeconds: 300
  4.     resourceArn: acs:mns:::/topics/generateInfo-fnf-demo-jiyuan/messages
  5.     pattern: waitForCallback
  6.     inputMappings:
  7.       - target: taskToken
  8.         source: $context.task.token
  9.       - target: products
  10.         source: $input.products
  11.       - target: supplier
  12.         source: $input.supplier
  13.       - target: address
  14.         source: $input.address
  15.       - target: orderNum
  16.         source: $input.orderNum
  17.       - target: type
  18.         source: $context.step.name
  19.     outputMappings:
  20.       - target: paymentcombination
  21.         source: $local.paymentcombination
  22.       - target: orderNum
  23.         source: $local.orderNum
  24.     serviceParams:
  25.       MessageBody: $
  26.       Priority: 1
  27.     catch:
  28.       - errors:
  29.           - FnF.TaskTimeout
  30.         goto: orderCanceled
  31. ```
  32. - name:节点名称;
  33. - timeoutSeconds:超时时间,节点等待时长,超越时间后跳转到 goto 分支指向的 orderCanceled 节点;
  34. - pattern:设置为 waitForCallback,表示需要等待确认;
  35. - inputMappings:该节点入参;
  36.    - taskToken:Serverless 工作流自动生成的 Token;
  37.    - products:选择的商品;
  38.    - supplier:选择的商家;
  39.    - address:送餐地址;
  40.    - orderNum:订单号;
  41. - outputMappings:该节点的出参;
  42.    - paymentcombination:该商家支持的支付方式;
  43.    - orderNum:订单号;
  44. - catch:捕获异常,跳转到其他分支。
  45. Serverless 工作流支持多个云效劳集成,将其他效劳作为任务步骤的执行单元。效劳集成方式通过 FDL 表达式实现,在任务步骤中,可以使 用resourceArn 来定义集成的目的效劳,使用 pattern 定义集成形式。
  46. 在 resourceArn 中配置 /topics/generateInfo-fnf-demo-jiyuan/messages 信息,就是集成了 MNS 消息队列效劳,当 generateInfo 节点触发后会向 generateInfo-fnf-demo-jiyuanTopic 中发送一条消息。消息的正文和参数在 serviceParams 对象中 zhi'd 指定。MessageBody 是消息正文,配置 $ 表示通过输入映射 inputMappings 产生消息正文。
  47. generateInfo-fnf-demo 函数:
  48. 向 generateInfo-fnf-demo-jiyuanTopic 中发送的这条消息包含了商品信息、商家信息、地址、订单号,表示一个下订单流程的开端,既然有发消息,那么必然有接受消息停止后续处置。在函数计算控制台,创建效劳,在效劳下创建名为 generateInfo-fnf-demo 的事件触发器函数,这里选择 Python Runtime:
  49. ![17.png](https://ucc.alicdn.com/pic/developer-ecology/89d43991349e422e8bda26f905cce0c4.png)
  50. 创建 MNS 触发器,选择监听 generateInfo-fnf-demo-jiyuanTopic:
  51. ![18.png](https://ucc.alicdn.com/pic/developer-ecology/4612a6cd4ba54dc38a79cebf7cacc3ac.png)
  52. 翻开消息效劳 MNS 控制台,创建 generateInfo-fnf-demo-jiyuanTopic:
  53. ![19.png](https://ucc.alicdn.com/pic/developer-ecology/f51714a2ee594dcba4bbbc888c42288c.png)
  54. 接下来写函数代码:
复制代码
-- 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
  1. region = "cn-hangzhou"
  2. account_id = "XXXX"
  3. ak_id = "XXX"
  4. ak_secret = "XXX"
  5. fnf_client = AcsClient(
  6.     ak_id,
  7.     ak_secret,
  8.     region
  9. )
  10. logger = logging.getLogger()
  11. # 2. event内的信息即接受到Topic generateInfo-fnf-demo-jiyuan中的消息内容,将其转换为Json对象
  12. bodyJson = json.loads(event)
  13. logger.info("products:" + bodyJson["products"])
  14. logger.info("supplier:" + bodyJson["supplier"])
  15. logger.info("address:" + bodyJson["address"])
  16. logger.info("taskToken:" + bodyJson["taskToken"])
  17. supplier = bodyJson["supplier"]
  18. taskToken = bodyJson["taskToken"]
  19. orderNum = bodyJson["orderNum"]
  20. # 3. 判断什么商家使用什么样的支付方式组合,这里的示例比较简单粗暴,正常情况下,应该使用元数据配置的方式获取
  21. paymentcombination = ""
  22. if supplier == "haidilao":
  23.     paymentcombination = "zhifubao,weixin"
  24. else:
  25.     paymentcombination = "zhifubao,weixin,unionpay"
  26. # 4. 调用Java效劳暴露的接口,更新订单信息,主要是更新支付方式
  27. url = "http://xx.xx.xx.xx:8080/setPaymentCombination/" + orderNum + "/" + paymentcombination + "/0"
  28. x = requests.get(url)
  29. # 5. 给予generateInfo节点响应,并返回数据,这里返回了订单号和支付方式
  30. output = "{"orderNum": "%s", "paymentcombination":"%s" " \
  31.                      "}" % (orderNum, paymentcombination)
  32. request = ReportTaskSucceededRequest.ReportTaskSucceededRequest()
  33. request.set_Output(output)
  34. request.set_TaskToken(taskToken)
  35. resp = fnf_client.do_action_with_exception(request)
  36. return 'hello world'
复制代码
  1. 代码分五部分:
  2. - 构建 Serverless 工作流 Client;
  3. - event 内的信息即接受到 TopicgenerateInfo-fnf-demo-jiyuan 中的消息内容,将其转换为 Json 对象;
  4. - 判断什么商家使用什么样的支付方式组合,这里的示例比较简单粗暴,正常情况下,应该使用元数据配置的方式获取。比如在系统内有商家信息的配置功能,通过在界面上配置该商家支持哪些支付方式,形成元数据配置信息,提供查询接口,在这里停止查询;
  5. - 调用 Java 效劳暴露的接口,更新订单信息,主要是更新支付方式;
  6. - 给予 generateInfo 节点响应,并返回数据,这里返回了订单号和支付方式。因为该节点的 pattern 是 waitForCallback,所以需要等待响应结果。
  7. generateInfo-fnf-demo 函数配置了 MNS 触发器,当 TopicgenerateInfo-fnf-demo-jiyuan 有消息后就会触发执行 generateInfo-fnf-demo 函数。
  8. ### 2. payment 节点
  9. 接下来是 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.tokentarget: orderNum
      source: $local.orderNum - target: paymentcombination source: $local.paymentcombinationtarget: type
      source: $context.step.name outputMappings: - target: paymentMethod source: $local.paymentMethodtarget: orderNum
      source: $local.orderNum - target: price source: $local.pricetarget: taskToken
      source: $input.taskToken serviceParams: MessageBody: $
      Priority: 1
      catch:
    • errors:
        FnF.TaskTimeout
        goto: orderCanceled


当流程流转到 payment 节点后,用户就可以进入到支付页面。
一文读懂 Serverless,将配置化思想复用到平台系统中-17.png


payment 节点会向 MNS 的 Topicpayment-fnf-demo-jiyuan 发送消息,会触发 payment-fnf-demo 函数。
payment-fnf-demo 函数:
payment-fnf-demo 函数的创建方式和 generateInfo-fnf-demo 函数类似。
  1. # -*- coding: utf-8 -*-
  2. import logging
  3. import json
  4. import os
  5. import time
  6. import logging
  7. from aliyunsdkcore.client import AcsClient
  8. from aliyunsdkcore.acs_exception.exceptions import ServerException
  9. from aliyunsdkcore.client import AcsClient
  10. from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
  11. from aliyunsdkfnf.request.v20190315 import ReportTaskFailedRequest
  12. from mns.account import Account  # pip install aliyun-mns
  13. from mns.queue import *
  14. def handler(event, context):
  15.     logger = logging.getLogger()
  16.     region = "xxx"
  17.     account_id = "xxx"
  18.     ak_id = "xxx"
  19.     ak_secret = "xxx"
  20.     mns_endpoint = "http://your_account_id.mns.cn-hangzhou.aliyuncs.com/"
  21.     queue_name = "payment-queue-fnf-demo"
  22.     my_account = Account(mns_endpoint, ak_id, ak_secret)
  23.     my_queue = my_account.get_queue(queue_name)
  24.     # my_queue.set_encoding(False)
  25.     fnf_client = AcsClient(
  26.         ak_id,
  27.         ak_secret,
  28.         region
  29.     )
  30.     eventJson = json.loads(event)
  31.     isLoop = True
  32.     while isLoop:
  33.         try:
  34.             recv_msg = my_queue.receive_message(30)
  35.             isLoop = False
  36.             # body = json.loads(recv_msg.message_body)
  37.             logger.info("recv_msg.message_body:======================" + recv_msg.message_body)
  38.             msgJson = json.loads(recv_msg.message_body)
  39.             my_queue.delete_message(recv_msg.receipt_handle)
  40.             # orderCode = int(time.time())
  41.             task_token = eventJson["taskToken"]
  42.             orderNum = eventJson["orderNum"]
  43.             output = "{"orderNum": "%s", "paymentMethod": "%s", "price": "%s" " \
  44.                          "}" % (orderNum, msgJson["paymentMethod"], msgJson["price"])
  45.             request = ReportTaskSucceededRequest.ReportTaskSucceededRequest()
  46.             request.set_Output(output)
  47.             request.set_TaskToken(task_token)
  48.             resp = fnf_client.do_action_with_exception(request)
  49.         except Exception as e:
  50.             logger.info("new loop")
  51.     return 'hello world'
复制代码
上面代码核心思路是等待用户在支付页面选择某个支付方式确认支付。使用了 MNS 的队列来模仿等待。循环等待接收队列 payment-queue-fnf-demo 中的消息,当收到消息后将订单号和用户选择的详细支付方式以及金额返回给 payment 节点。
前端选择支付方式页面:
经过 generateInfo 节点后,该订单的支付方式信息已经有了,所以对于用户而言,当填完商品、商家、地址后,跳转到的页面就是该确认支付页面,并且包含了该商家支持的支付方式。
进入该页面后,会恳求 Java 效劳暴露的接口,获取订单信息,根据支付方式在页面上显示不同的支付方式。
一文读懂 Serverless,将配置化思想复用到平台系统中-18.png


代码片段如下:
一文读懂 Serverless,将配置化思想复用到平台系统中-19.png


当用户选定某个支付方式点击提交订单按钮后,向 payment-queue-fnf-demo 队列发送消息,即通知 payment-fnf-demo 函数继续后续的逻辑。
使用了一个 HTTP 触发器类型的函数,用于实现向 MNS 发消息的逻辑,paymentMethod-fnf-demo 函数代码:
  1. # -*- coding: utf-8 -*-
  2. import logging
  3. import urllib.parse
  4. import json
  5. from mns.account import Account  # pip install aliyun-mns
  6. from mns.queue import *
  7. HELLO_WORLD = b'Hello world!\n'
  8. def handler(environ, start_response):
  9.     logger = logging.getLogger()
  10.     context = environ['fc.context']
  11.     request_uri = environ['fc.request_uri']
  12.     for k, v in environ.items():
  13.       if k.startswith('HTTP_'):
  14.         # process custom request headers
  15.         pass
  16.     try:      
  17.         request_body_size = int(environ.get('CONTENT_LENGTH', 0))   
  18.     except (ValueError):      
  19.         request_body_size = 0  
  20.     request_body = environ['wsgi.input'].read(request_body_size)
  21.     paymentMethod = urllib.parse.unquote(request_body.decode("GBK"))
  22.     logger.info(paymentMethod)
  23.     paymentMethodJson = json.loads(paymentMethod)
  24.     region = "cn-xxx"
  25.     account_id = "xxx"
  26.     ak_id = "xxx"
  27.     ak_secret = "xxx"
  28.     mns_endpoint = "http://your_account_id.mns.cn-hangzhou.aliyuncs.com/"
  29.     queue_name = "payment-queue-fnf-demo"
  30.     my_account = Account(mns_endpoint, ak_id, ak_secret)
  31.     my_queue = my_account.get_queue(queue_name)
  32.     output = "{"paymentMethod": "%s", "price":"%s" " \
  33.                          "}" % (paymentMethodJson["paymentMethod"], paymentMethodJson["price"])
  34.     msg = Message(output)
  35.     my_queue.send_message(msg)
  36.     status = '200 OK'
  37.     response_headers = [('Content-type', 'text/plain')]
  38.     start_response(status, response_headers)
  39.     return [HELLO_WORLD]
复制代码
函数的逻辑很简单,就是向 MNS 的队列 payment-queue-fnf-demo 发送用户选择的支付方式和金额。
一文读懂 Serverless,将配置化思想复用到平台系统中-20.png


3. paymentCombination 节点

paymentCombination 节点是一个路由节点,通过判断某个参数路由到不同的节点,以 paymentMethod 作为判断条件:
  1. - type: choice
  2.     name: paymentCombination
  3.     inputMappings:
  4.       - target: orderNum
  5.         source: $local.orderNum
  6.       - target: paymentMethod
  7.         source: $local.paymentMethod
  8.       - target: price
  9.         source: $local.price
  10.       - target: taskToken
  11.         source: $local.taskToken
  12.     choices:
  13.       - condition: $.paymentMethod == "zhifubao"
  14.         steps:
  15.           - type: task
  16.             name: zhifubao
  17.             resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
  18.             inputMappings:
  19.               - target: price
  20.                 source: $input.price            
  21.               - target: orderNum
  22.                 source: $input.orderNum
  23.               - target: paymentMethod
  24.                 source: $input.paymentMethod
  25.               - target: taskToken
  26.                 source: $input.taskToken
  27.       - condition: $.paymentMethod == "weixin"
  28.         steps:
  29.           - type: task
  30.             name: weixin
  31.             resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/weixin-fnf-demo
  32.             inputMappings:
  33.             - target: price
  34.               source: $input.price            
  35.             - target: orderNum
  36.               source: $input.orderNum
  37.             - target: paymentMethod
  38.               source: $input.paymentMethod
  39.             - target: taskToken
  40.               source: $input.taskToken
  41.       - condition: $.paymentMethod == "unionpay"
  42.         steps:
  43.           - type: task
  44.             name: unionpay
  45.             resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/union-fnf-demo
  46.             inputMappings:
  47.             - target: price
  48.               source: $input.price            
  49.             - target: orderNum
  50.               source: $input.orderNum
  51.             - target: paymentMethod
  52.               source: $input.paymentMethod
  53.             - target: taskToken
  54.               source: $input.taskToken
  55.     default:
  56.       goto: orderCanceled
复制代码
流程是,用户选择支付方式后,通过消息发送给 payment-fnf-demo 函数,然后将支付方式返回,于是流转到 paymentCombination 节点通过判断支付方式流转到详细处置支付逻辑的节点和函数。
4. zhifubao 节点

看一个 zhifubao 节点:
  1. choices:
  2.       - condition: $.paymentMethod == "zhifubao"
  3.         steps:
  4.           - type: task
  5.             name: zhifubao
  6.             resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
  7.             inputMappings:
  8.               - target: price
  9.                 source: $input.price            
  10.               - target: orderNum
  11.                 source: $input.orderNum
  12.               - target: paymentMethod
  13.                 source: $input.paymentMethod
  14.               - target: taskToken
  15.                 source: $input.taskToken
复制代码
节点的 resourceArn 和之前两个节点的不同,这里配置的是函数计算中函数的 ARN,也就是说当流程流转到这个节点时会触发 zhifubao-fnf-demo 函数,该函数是一个事件触发函数,但不需要创建任何触发器。流程将订单金额、订单号、支付方式传给 zhifubao-fnf-demo 函数。
zhifubao-fnf-demo 函数:
  1. # -*- coding: utf-8 -*-
  2. import logging
  3. import json
  4. import requests
  5. import urllib.parse
  6. from aliyunsdkcore.client import AcsClient
  7. from aliyunsdkcore.acs_exception.exceptions import ServerException
  8. from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
  9. from aliyunsdkfnf.request.v20190315 import ReportTaskFailedRequest
  10. def handler(event, context):
  11.   region = "cn-xxx"
  12.   account_id = "xxx"
  13.   ak_id = "xxx"
  14.   ak_secret = "xxx"
  15.   fnf_client = AcsClient(
  16.     ak_id,
  17.     ak_secret,
  18.     region
  19.   )
  20.   logger = logging.getLogger()
  21.   logger.info(event)
  22.   bodyJson = json.loads(event)
  23.   price = bodyJson["price"]
  24.   taskToken = bodyJson["taskToken"]
  25.   orderNum = bodyJson["orderNum"]
  26.   paymentMethod = bodyJson["paymentMethod"]
  27.   logger.info("price:" + price)
  28.   newPrice = int(price) * 0.8
  29.   logger.info("newPrice:" + str(newPrice))
  30.   url = "http://xx.xx.xx.xx:8080/setPaymentCombination/" + orderNum + "/" + paymentMethod + "/" + str(newPrice)
  31.   x = requests.get(url)
  32.   return {"Status":"ok"}
复制代码
代码逻辑很简单,接收到金额后,将金额打 8 折,然后将价格更新回订单。其他支付方式的节点和函数如法炮制,变卦实现逻辑就可以。在这个示例中,微信支付打了 5 折,银联支付打 7 折。
完好流程

流程中的 orderCompleted 和 orderCanceled 节点没做什么逻辑,流程如下:
一文读懂 Serverless,将配置化思想复用到平台系统中-21.png


从 Serverless 工作流中看到的节点流转是这样的:
一文读懂 Serverless,将配置化思想复用到平台系统中-22.png


写在后面

以上是一个基于 Serverless 的 FC 实现的工作流,模仿构建了一个订单模块,规则包括:
    配置商家和支付方式的元数据规则;确认支付页面的元数据规则。
在实际项目中,需要将可定制的部分笼统为元数据描绘,需要有配置界面供运营或商家定制支付方式也就是元数据规则,然后前后端页面基于元数据信息展示相应的内容。
假设之后需要接入新的支付方式,只需要在 paymentCombination 路由节点中确定好路由规则,之后增加对应的支付方式函数即可,通过增加元数据配置项,就可以在页面展示新加的支付方式,并路由到新的支付函数中。
经过整篇文章相信很多人对于 Serverless 的定义,以及如何基于现有的公有云系统的 Serverless 功能实现商业才干已经有了一定的理解,甚至基于此有实力的公司可以自研一套 Serverless 平台。当然思想是相同的,其实文中很多逻辑与理论不止适用于 Serverless,就是我们日常基于微效劳的平台化/中台化处置方案,都可以从中获取设计营养在工作中应用。
作者:春哥大魔王
原文链接
本文为阿里云原创内容,未经允许不得借鉴

回复

举报 使用道具

全部回复
暂无回帖,快来参与回复吧
本版积分规则 高级模式
B Color Image Link Quote Code Smilies

余光
注册会员
主题 18
回复 19
粉丝 0
|网站地图
快速回复 返回顶部 返回列表