一文讀懂 Serverless,將配置化思想複用到平臺系統中

頭圖.jpg

做者 | 春哥大魔王
來源 | Serverless 公衆號前端

寫在前面

在 SaaS 領域 Salesforce 是佼佼者,其 CRM 的概念已經擴展到了 Marketing、Sales、Service 等領域。那麼 Salesforce 靠什麼變成了這三個行業的解決方案呢?得益於 Salesforce 強大的 aPaaS 平臺java

ISV、內部實施、客戶都可以從本身的維度基於 aPaaS 平臺構建本身的行業,實現業務定製,甚至是行業定製。由於在此以前只有在 Sales 方向有專門的 SaaS 產品,而 Marketing 和 Service 都是由本身的 ISV 在各自行業的解決方案。因此 Salesforce 已經從一家 SaaS 公司變成了一家 aPaaS 平臺公司了。python

搭建一個 aPaaS 平臺是須要很長時間的,固然也能夠基於一些公有云產品的 Serverless 方案實現現有系統的靈活性與擴展性,從而實現針對於不一樣客戶的定製。ios

什麼是 Serverless

Serverless 由兩部分組成,Server 和 Less。golang

  • 前者能夠理解爲其解決方案範圍處在服務端;
  • 後者能夠譯爲少許的;

組合起來就是較少服務端干預的服務端解決方案。shell

與 Serverless 相對的是 Serverfull,比較下對應的概念可能更便於理解。編程

Serverfull 時代,研發交付流程通常有三個角色:RD,PM,QA。json

RD 根據 PM 的 PRD 進行功能開發,交付到 QA 進行測試,測試完成以後發佈到服務器。由運維人員規劃服務器規格、數量、機房部署、節點擴縮容等,這種更多由人力處理的時代就是 Serverfull 時代。axios

以後進入了 DevOps 時代。這個時代運維本身開發一套運維控制檯,可讓研發同窗在控制檯上本身進行服務觀測、數據查詢、運維處理等,運維同窗的工做輕鬆了很多,這個階段主要釋放了運維同窗的人力。後端

而到了 Serverless 時代,這套運維控制檯能力愈來愈豐富,能夠實現按配置的自動擴縮容、性能監控、DevOps 流水線等,同時侵入到研發流程側,好比自動發佈流水線、編譯打包、代碼質量監測、灰度發佈、彈性擴縮等流程基本不須要人力處理了,這就是 Serverless 時代。

Serverless 怎麼用

相信你有過這樣的經歷,在一個 Web 界面上,左側寫代碼,右側展現執行效果。

1.png

  • 寫的是代碼塊,代碼數量不會特別大;
  • 代碼運行速度快;
  • 支持多種編程語言;
  • 能夠支持不可預計的流量洪峯衝擊。

以阿里雲解決方案看下如何支持多語言架構:

2.png

抽象來講,前端只須要將代碼片斷和編程語言的標識傳給 Server 端便可,等待響應結果。Server 端能夠針對於不一樣編程語言進行 runtime 分類、預處理等工做。

Serverless 怎麼作

不少人把 Serverless 看作是 FC(function compute:函數計算),使用函數計算,無需業務本身搭建 IT 基礎設施,只須要編碼並上傳代碼。函數計算會按需爲你準備好計算資源,彈性、可靠地運行,並提供 trace、日誌查詢、監控告警等治理能力。

好比:

3.png

在 FC 中有服務和函數之分。一個服務能夠包含多個函數。咱們能夠用微服務理解,咱們經過 golang 或 java 搭建了一個微服務架構,而 FC 服務就是其中的類,FC 函數是類中的一個方法:

4.png

區別在於 Java 搭建的微服務只能運行 java 類代碼,golang 的類只能運行 go 寫的代碼,而 FC 函數能夠安裝不一樣語言的 runtime,支持運行不一樣語言程序。

5.png

類比理解以後,咱們再看下如何調用 FC 的函數,通常的 FC 解決方案裏面都有一個觸發器的概念。好比 HTTP 觸發器、對象存儲觸發器、日誌服務觸發器、定時任務觸發器、CDN 觸發器、消息隊列觸發器等。觸發器是對於 FC 函數調用的抽象收口,好比 HTTP 觸發器通常都類比網關的一個 http 請求事件,或是指定對象存儲路徑下上傳了一個圖片,這些觸發事件的入口均可以是觸發器。

6.png

觸發器產生事件以後能夠調用 FC 函數,函數執行的邏輯能夠是下載一張圖片或是註冊一個用戶。

這樣從觸發器到 FC 函數邏輯處理就是一個 FC 的生命週期了。

那麼 FC 是如何實現高可用的呢?

其實每一個函數底層代碼都是運行在一套 IaaS 平臺上,使用 IaaS 資源,咱們能夠爲每一個函數設置運行代碼時須要的內存配置便可,好比最小 128M,最大 3G 等。研發人員不須要關心代碼運行在什麼樣的服務器上,不須要關心啓動了多少函數實例支持當前場景,不須要關注背後的彈性擴縮問題,這些都被收斂在 FC 以後。

7.png

如圖有兩種高可用策略:

  • 給函數設置併發實例數,好比 3 個,那麼當有三個請求進來時,該函數只啓動一個實例,可是會啓動三個線程來運行邏輯;
  • 線程達到上限後,會再拉起一個函數實例。

相似於線程池的方案。

8.png

那麼 Serverless 如何提效呢?

  • 效率高:若是新加了語言,只須要建立一個對應的 Runtime 的 FC 函數便可;
  • 高可用:經過多線程、多實例兩種方式保障高可用,且函數實例擴縮容徹底由 FC 自助處理,不須要運維作任何配置;
  • 成本低:在沒有觸發器請求時,函數實例不會被拉起,也不會計費,因此在流量低谷期間或者夜間時,FC 消耗的成本是很是低的。

如何在雲平臺建立一個 FC

1. 建立服務

  • 首先新建一個服務名稱;
  • 選定服務部署的地區(背後幫助你就近部署在目標機房);
  • 選擇是否打開調試日誌(開發過程開啓,線上運行時可關閉)。

2. 建立函數

有了服務以後就能夠建立函數了,好比選擇基於 http 請求的函數。

  • 選擇函數綁定的服務;
  • 設置函數名稱;
  • 選擇 runtime 環境;
  • 是否要求函數實例彈性;
  • 函數入口(觸發器直接調用的目標方法);
  • 函數執行內存;
  • 函數執行超時時間;
  • 設置實例併發度。

9.png

配置觸發器,好比選擇了 HTTP 觸發器,而後在觸發器上綁定函數名稱,因爲是 http 訪問,能夠選擇訪問的鑑權、認證方式,以及請求方式 POST or GET。

3. 代碼編寫

當函數建立好了以後,進入函數,能夠看到描述、代碼執行歷史、觸發器類型、日誌查詢頁等。
若是是 HTTP 觸發器,須要配置 http 觸發路徑。

10.png

能夠看到就如前面介紹的那種,相似於類裏面的一個函數,上下文請求會打到這裏,直接執行。

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

11.png

整個過程只須要前端將代碼傳入到 FC 函數裏面,整個 Server 端各個環節都不須要研發與運維同窗關心,體現了 Serverless 的精髓。

用 Serverless 協調工做流

工做流能夠用順序、分支、並行等方式來編排任務執行,以後流程會按照設定好的步驟可靠地協調任務執行,跟蹤每一個任務的狀態切換,並在必要時執行定義的重試邏輯,確保流程順利執行。

工做流流程經過記錄日誌和審計方式來監視工做流的執行,便於流程的診斷與調試。

12.png

系統靈活性與擴展性的核心是服務可編排,因此咱們須要作的是將現有系統內部用戶但願定製的功能進行梳理、拆分、抽離、結合 FC 提供的無狀態能力,將這些功能點進行編排,實現業務流程的定製。

需靈活配置工做流的業務

舉個例子,好比餐飲場景下不一樣商家能夠配置不一樣的支付方式,能夠走微信支付、銀聯支付、支付寶支付。能夠同時支持三家,也能夠某一家,能夠到付,也能夠積分兌換等。若是沒有一個好的配置化流程解決方案的話,系統中會出現大量硬編碼規則判斷條件,系統迭代疲於奔命,是個不可持續的過程。

有了 FC 搭建的工做流就能夠很優雅地解決這種問題,好比規整流程以下:

13.png

上面的流程是用戶側的流程,接下來須要轉換成程序側的流程,經過約束的 FDL 建立工做流,如圖:

14.png

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 可實現靈活工做流。

流程如何觸發的呢?

15.png

在用戶選擇完商品、填完地址以後,經過拉取商品、訂單上下文,能夠自動化觸發流程了。

在微服務背景下,不少能力不是閉環在單體代碼邏輯以內,不少時候是多個業務系統的鏈接,好比串聯多個 OpenAPI 接口實現全流程:

16.png

如想使用流程引擎須要進行相關的備案鑑權:

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

17.png

建立 MNS 觸發器,選擇監聽 generateInfo-fnf-demo-jiyuanTopic:

18.png

打開消息服務 MNS 控制檯,建立 generateInfo-fnf-demo-jiyuanTopic:

19.png

接下來寫函數代碼:

# -*- 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 節點後,用戶就能夠進入到支付頁面。

20.png

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 服務暴露的接口,獲取訂單信息,根據支付方式在頁面上顯示不一樣的支付方式。

21.png

代碼片斷以下:

22.png

當用戶選定某個支付方式點擊提交訂單按鈕後,向 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 發送用戶選擇的支付方式和金額。

23.png

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 節點沒作什麼邏輯,流程以下:

24.png

從 Serverless 工做流中看到的節點流轉是這樣的:

25.png

寫在後面

以上是一個基於 Serverless 的 FC 實現的工做流,模擬構建了一個訂單模塊,規則包括:

  • 配置商家和支付方式的元數據規則;
  • 確認支付頁面的元數據規則。

在實際項目中,須要將可定製的部分抽象爲元數據描述,須要有配置界面供運營或商家定製支付方式也就是元數據規則,而後先後端頁面基於元數據信息展現相應的內容。

若是以後須要接入新的支付方式,只須要在 paymentCombination 路由節點中肯定好路由規則,以後增長對應的支付方式函數便可,經過增長元數據配置項,就能夠在頁面展現新加的支付方式,並路由到新的支付函數中。

通過整篇文章相信不少人對於 Serverless 的定義,以及如何基於現有的公有云系統的 Serverless 功能實現商業能力已經有了必定的瞭解,甚至基於此有實力的公司能夠自研一套 Serverless 平臺。固然思想是相同的,其實文中不少邏輯與理論不止適用於 Serverless,就是咱們平常基於微服務的平臺化/中臺化解決方案,均可以從中獲取設計養分在工做中應用。

相關文章
相關標籤/搜索