标签: Serverless

如何通过 Serverless 轻松识别验证码?

Serverless 概念自被提出就倍受关注,尤其是近些年来 Serverless 焕发出了前所未有的活力,各领域的工程师都在试图将 Serverless 架构与自身工作相结合,以获取到 Serverless 架构所带来的“技术红利”。

验证码(CAPTCHA)是“Completely Automated Public Turing test to tell Computers and Humans Apart”(全自动区分计算机和人类的图灵测试)的缩写,是一种区分用户是计算机还是人的公共全自动程序。可以防止恶意破解密码、刷票、论坛灌水,有效防止某个黑客对某一个特定注册用户用特定程序暴力破解方式进行不断地登陆尝试。实际上验证码是现在很多网站通行的方式,我们利用比较简易的方式实现了这个功能。CAPTCHA 的问题由计算机生成并评判,但是这个问题只有人类才能解答,计算机是无法解答的,所以回答出问题的用户就可以被认为是人类。说白了,验证码就是用来验证的码,验证是人访问的还是机器访问的“码”。

那么人工智能领域中的验证码识别与 Serverless 架构会碰撞出哪些火花呢?本文将通过 Serverless 架构和卷积神经网络(CNN)算法,实现验证码识别功能。

%title插图%num

浅谈验证码

验证码的发展,可以说是非常迅速的,从开始的单纯数字验证码,到后来的数字+字母验证码,再到后来的数字+字母+中文的验证码以及图形图像验证码,单纯的验证码素材已经越来越多了。从验证码的形态来看,也是各不相同,输入、点击、拖拽以及短信验证码、语音验证码……

Bilibili 的登录验证码就包括了多种模式,例如滑动滑块进行验证:

%title插图%num

例如,通过依次点击文字进行验证:

%title插图%num

而百度贴吧、知乎、以及 Google 等相关网站的验证码又各不相同,例如选择正着写的文字、选择包括指定物体的图片以及按顺序点击图片中的字符等。

验证码的识别可能会根据验证码的类型而不太一致,当然*简单的验证码可能就是*原始的文字验证码了:

%title插图%num

即便是文字验证码,也是存在很多差异的,例如简单的数字验证码、简单的数字+字母验证码、文字验证码、验证码中包括计算、简单验证码中增加一些干扰成为复杂验证码等。

%title插图%num

验证码识别

1. 简单验证码识别

验证码识别是一个古老的研究领域,简单说就是把图片上的文字转化为文本的过程。*近几年,随着大数据的发展,广大爬虫工程师在对抗反爬策略时,对验证码的识别要求也越来越高。在简单验证码的时代,验证码的识别主要是针对文本验证码,通过图像的切割,对验证码每一部分进行裁剪,然后再对每个裁剪单元进行相似度对比,获得*可能的结果,*后进行拼接,例如将验证码:

%title插图%num

进行二值化等操作:

%title插图%num

完成之后再进行切割:

%title插图%num

切割完成再进行识别,*后进行拼接,这样的做法是,针对每个字符进行识别,相对来说是比较容易的。

但是随着时间的发展,在这种简单验证码逐渐无法满足判断“是人还是机器”的问题时,验证码进行了一次小升级,即验证码上面增加了一些干扰线,或者验证码进行了严重的扭曲,增加了强色块干扰,例如 Dynadot 网站的验证码:

%title插图%num

不仅有图像扭曲重叠,还有干扰线和色块干扰。这个时候想要识别验证码,简单的切割识别就很难获得良好的效果了,这时通过深度学习反而可以获得不错的效果。

2. 基于 CNN 的验证码识别

卷积神经网络(Convolutional Neural Network,简称 CNN),是一种前馈神经网络,人工神经元可以响应周围单元,进行大型图像处理。卷积神经网络包括卷积层和池化层。

%title插图%num

如图所示,左图是传统的神经网络,其基本结构是:输入层、隐含层、输出层。右图则是卷积神经网络,其结构由输入层、输出层、卷积层、池化层、全连接层构成。卷积神经网络其实是神经网络的一种拓展,而事实上从结构上来说,朴素的 CNN 和朴素的 NN 没有任何区别(当然,引入了特殊结构的、复杂的 CNN 会和 NN 有着比较大的区别)。相对于传统神经网络,CNN 在实际效果中让我们的网络参数数量大大地减少,这样我们可以用较少的参数,训练出更加好的模型,典型的事半功倍,而且可以有效地避免过拟合。同样,由于 filter 的参数共享,即使图片进行了一定的平移操作,我们照样可以识别出特征,这叫做 “平移不变性”。因此,模型就更加稳健了。

1)验证码生成

验证码的生成是非常重要的一个步骤,因为这一部分的验证码将会作为我们的训练集和测试集,同时*终我们的模型可以识别什么类型的验证码,也是和这部分有关。

  1. # coding:utf-8
  2. import random
  3. import numpy as np
  4. from PIL import Image
  5. from captcha.image import ImageCaptcha
  6. CAPTCHA_LIST = [eve for eve in “0123456789abcdefghijklmnopqrsruvwxyzABCDEFGHIJKLMOPQRSTUVWXYZ”]
  7. CAPTCHA_LEN = 4  # 验证码长度
  8. CAPTCHA_HEIGHT = 60  # 验证码高度
  9. CAPTCHA_WIDTH = 160  # 验证码宽度
  10. randomCaptchaText = lambda char=CAPTCHA_LIST, size=CAPTCHA_LEN: “”.join([random.choice(char) for _ in range(size)])
  11. def genCaptchaTextImage(width=CAPTCHA_WIDTH, height=CAPTCHA_HEIGHT, save=None):
  12.     image = ImageCaptcha(width=width, height=height)
  13.     captchaText = randomCaptchaText()
  14.     if save:
  15.         image.write(captchaText, ‘./img/%s.jpg’ % captchaText)
  16.     return captchaText, np.array(Image.open(image.generate(captchaText)))
  17. print(genCaptchaTextImage(save=True))

通过上述代码,可以生成简单的中英文验证码:

%title插图%num

2)模型训练

模型训练的代码如下(部分代码来自网络)。

util.py 文件,主要是一些提取出来的公有方法:

  1. # -*- coding:utf-8 -*-
  2. import numpy as np
  3. from captcha_gen import genCaptchaTextImage
  4. from captcha_gen import CAPTCHA_LIST, CAPTCHA_LEN, CAPTCHA_HEIGHT, CAPTCHA_WIDTH
  5. # 图片转为黑白,3维转1维
  6. convert2Gray = lambda img: np.mean(img, -1if len(img.shape) > 2 else img
  7. # 验证码向量转为文本
  8. vec2Text = lambda vec, captcha_list=CAPTCHA_LIST: .join([captcha_list[int(v)] for v in vec])
  9. def text2Vec(text, captchaLen=CAPTCHA_LEN, captchaList=CAPTCHA_LIST):
  10.     “”
  11.     验证码文本转为向量
  12.     ““”
  13.     vector = np.zeros(captchaLen * len(captchaList))
  14.     for i in range(len(text)):
  15.         vector[captchaList.index(text[i]) + i * len(captchaList)] = 1
  16.     return vector
  17. def getNextBatch(batchCount=60, width=CAPTCHA_WIDTH, height=CAPTCHA_HEIGHT):
  18.     “”
  19.     获取训练图片组
  20.     ““”
  21.     batchX = np.zeros([batchCount, width * height])
  22.     batchY = np.zeros([batchCount, CAPTCHA_LEN * len(CAPTCHA_LIST)])
  23.     for i in range(batchCount):
  24.         text, image = genCaptchaTextImage()
  25.         image = convert2Gray(image)
  26.         # 将图片数组一维化 同时将文本也对应在两个二维组的同一行
  27.         batchX[i, :] = image.flatten() / 255
  28.         batchY[i, :] = text2Vec(text)
  29.     return batchX, batchY
  30. # print(getNextBatch(batch_count=1))

model_train.py 文件,主要是进行模型训练。在该文件中,定义了模型的基本信息,例如该模型是三层卷积神经网络,原始图像大小是 60*160,在*次卷积后变为 60*160, *池化后变为 30*80;第二次卷积后变为 30*80 ,第二次池化后变为 15*40;第三次卷积后变为  15*40 ,第三次池化后变为7*20。经过三次卷积和池化后,原始图片数据变为 7*20 的平面数据,同时项目在进行训练的时候,每隔 100 次进行一次数据测试,计算一次准确度:

  1. # -*- coding:utf-8 -*-
  2. import tensorflow.compat.v1 as tf
  3. from datetime import datetime
  4. from util import getNextBatch
  5. from captcha_gen import CAPTCHA_HEIGHT, CAPTCHA_WIDTH, CAPTCHA_LEN, CAPTCHA_LIST
  6. tf.compat.v1.disable_eager_execution()
  7. variable = lambda shape, alpha=0.01: tf.Variable(alpha * tf.random_normal(shape))
  8. conv2d = lambda x, w: tf.nn.conv2d(x, w, strides=[1111], padding=‘SAME’)
  9. maxPool2x2 = lambda x: tf.nn.max_pool(x, ksize=[1221], strides=[1221], padding=‘SAME’)
  10. optimizeGraph = lambda y, y_conv: tf.train.AdamOptimizer(1e-3).minimize(
  11.     tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(labels=y, logits=y_conv)))
  12. hDrop = lambda image, weight, bias, keepProb: tf.nn.dropout(
  13.     maxPool2x2(tf.nn.relu(conv2d(image, variable(weight, 0.01)) + variable(bias, 0.1))), keepProb)
  14. def cnnGraph(x, keepProb, size, captchaList=CAPTCHA_LIST, captchaLen=CAPTCHA_LEN):
  15.     “”
  16.     三层卷积神经网络
  17.     ““”
  18.     imageHeight, imageWidth = size
  19.     xImage = tf.reshape(x, shape=[-1, imageHeight, imageWidth, 1])
  20.     hDrop1 = hDrop(xImage, [33132], [32], keepProb)
  21.     hDrop2 = hDrop(hDrop1, [333264], [64], keepProb)
  22.     hDrop3 = hDrop(hDrop2, [336464], [64], keepProb)
  23.     # 全连接层
  24.     imageHeight = int(hDrop3.shape[1])
  25.     imageWidth = int(hDrop3.shape[2])
  26.     wFc = variable([imageHeight * imageWidth * 641024], 0.01)  # 上一层有64个神经元 全连接层有1024个神经元
  27.     bFc = variable([1024], 0.1)
  28.     hDrop3Re = tf.reshape(hDrop3, [-1, imageHeight * imageWidth * 64])
  29.     hFc = tf.nn.relu(tf.matmul(hDrop3Re, wFc) + bFc)
  30.     hDropFc = tf.nn.dropout(hFc, keepProb)
  31.     # 输出层
  32.     wOut = variable([1024, len(captchaList) * captchaLen], 0.01)
  33.     bOut = variable([len(captchaList) * captchaLen], 0.1)
  34.     yConv = tf.matmul(hDropFc, wOut) + bOut
  35.     return yConv
  36. def accuracyGraph(y, yConv, width=len(CAPTCHA_LIST), height=CAPTCHA_LEN):
  37.     “”
  38.     偏差计算图,正确值和预测值,计算准确度
  39.     ““”
  40.     maxPredictIdx = tf.argmax(tf.reshape(yConv, [-1, height, width]), 2)
  41.     maxLabelIdx = tf.argmax(tf.reshape(y, [-1, height, width]), 2)
  42.     correct = tf.equal(maxPredictIdx, maxLabelIdx)  # 判断是否相等
  43.     return tf.reduce_mean(tf.cast(correct, tf.float32))
  44. def train(height=CAPTCHA_HEIGHT, width=CAPTCHA_WIDTH, ySize=len(CAPTCHA_LIST) * CAPTCHA_LEN):
  45.     “”
  46.     cnn训练
  47.     ““”
  48.     accRate = 0.95
  49.     x = tf.placeholder(tf.float32, [None, height * width])
  50.     y = tf.placeholder(tf.float32, [None, ySize])
  51.     keepProb = tf.placeholder(tf.float32)
  52.     yConv = cnnGraph(x, keepProb, (height, width))
  53.     optimizer = optimizeGraph(y, yConv)
  54.     accuracy = accuracyGraph(y, yConv)
  55.     saver = tf.train.Saver()
  56.     with tf.Session() as sess:
  57.         sess.run(tf.global_variables_initializer())  # 初始化
  58.         step = 0  # 步数
  59.         while True:
  60.             batchX, batchY = getNextBatch(64)
  61.             sess.run(optimizer, feed_dict={x: batchX, y: batchY, keepProb: 0.75})
  62.             # 每训练一百次测试一次
  63.             if step % 100 == 0:
  64.                 batchXTest, batchYTest = getNextBatch(100)
  65.                 acc = sess.run(accuracy, feed_dict={x: batchXTest, y: batchYTest, keepProb: 1.0})
  66.                 print(datetime.now().strftime(‘%c’), ‘ step:’, step, ‘ accuracy:’, acc)
  67.                 # 准确率满足要求,保存模型
  68.                 if acc > accRate:
  69.                     modelPath = “./model/captcha.model”
  70.                     saver.save(sess, modelPath, global_step=step)
  71.                     accRate += 0.01
  72.                     if accRate > 0.90:
  73.                         break
  74.             step = step + 1
  75. train()

当完成了这部分之后,我们可以通过本地机器对模型进行训练,为了提升训练速度,我将代码中的 accRate 部分设置为:

  1. if accRate > 0.90:
  2.     break

也就是说,当准确率超过 90% 之后,系统就会自动停止,并且保存模型。

接下来可以进行训练:

%title插图%num

训练时间可能会比较长,训练完成之后,可以根据结果绘图,查看随着 Step 的增加,准确率的变化曲线:

%title插图%num

横轴表示训练的 Step,纵轴表示准确率

3. 基于 Serverless 架构的验证码识别

将上面的代码部分进行进一步整合,按照函数计算的规范进行编码:

  1. # -*- coding:utf-8 -*-
  2. # 核心后端服务
  3. import base64
  4. import json
  5. import uuid
  6. import tensorflow as tf
  7. import random
  8. import numpy as np
  9. from PIL import Image
  10. from captcha.image import ImageCaptcha
  11. # Response
  12. class Response:
  13.     def __init__(selfstart_responseresponseerrorCode=None):
  14.         self.start = start_response
  15.         responseBody = {
  16.             ‘Error’: {“Code”: errorCode, “Message”: response},
  17.         } if errorCode else {
  18.             ‘Response’: response
  19.         }
  20.         # 默认增加uuid,便于后期定位
  21.         responseBody[‘ResponseId’] = str(uuid.uuid1())
  22.         print(“Response: “, json.dumps(responseBody))
  23.         self.response = json.dumps(responseBody)
  24.     def __iter__(self):
  25.         status = ‘200’
  26.         response_headers = [(‘Content-type’‘application/json; charset=UTF-8’)]
  27.         self.start(status, response_headers)
  28.         yield self.response.encode(“utf-8”)
  29. CAPTCHA_LIST = [eve for eve in “0123456789abcdefghijklmnopqrsruvwxyzABCDEFGHIJKLMOPQRSTUVWXYZ”]
  30. CAPTCHA_LEN = 4  # 验证码长度
  31. CAPTCHA_HEIGHT = 60  # 验证码高度
  32. CAPTCHA_WIDTH = 160  # 验证码宽度
  33. # 随机字符串
  34. randomStr = lambda num=5“”.join(random.sample(‘abcdefghijklmnopqrstuvwxyz’, num))
  35. randomCaptchaText = lambda char=CAPTCHA_LIST, size=CAPTCHA_LEN: “”.join([random.choice(char) for _ in range(size)])
  36. # 图片转为黑白,3维转1维
  37. convert2Gray = lambda img: np.mean(img, -1if len(img.shape) > 2 else img
  38. # 验证码向量转为文本
  39. vec2Text = lambda vec, captcha_list=CAPTCHA_LIST: .join([captcha_list[int(v)] for v in vec])
  40. variable = lambda shape, alpha=0.01: tf.Variable(alpha * tf.random_normal(shape))
  41. conv2d = lambda x, w: tf.nn.conv2d(x, w, strides=[1111], padding=‘SAME’)
  42. maxPool2x2 = lambda x: tf.nn.max_pool(x, ksize=[1221], strides=[1221], padding=‘SAME’)
  43. optimizeGraph = lambda y, y_conv: tf.train.AdamOptimizer(1e-3).minimize(
  44.     tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(labels=y, logits=y_conv)))
  45. hDrop = lambda image, weight, bias, keepProb: tf.nn.dropout(
  46.     maxPool2x2(tf.nn.relu(conv2d(image, variable(weight, 0.01)) + variable(bias, 0.1))), keepProb)
  47. def genCaptchaTextImage(width=CAPTCHA_WIDTH, height=CAPTCHA_HEIGHT, save=None):
  48.     image = ImageCaptcha(width=width, height=height)
  49.     captchaText = randomCaptchaText()
  50.     if save:
  51.         image.write(captchaText, save)
  52.     return captchaText, np.array(Image.open(image.generate(captchaText)))
  53. def text2Vec(text, captcha_len=CAPTCHA_LEN, captcha_list=CAPTCHA_LIST):
  54.     “”
  55.     验证码文本转为向量
  56.     ““”
  57.     vector = np.zeros(captcha_len * len(captcha_list))
  58.     for i in range(len(text)):
  59.         vector[captcha_list.index(text[i]) + i * len(captcha_list)] = 1
  60.     return vector
  61. def getNextBatch(batch_count=60, width=CAPTCHA_WIDTH, height=CAPTCHA_HEIGHT):
  62.     “”
  63.     获取训练图片组
  64.     ““”
  65.     batch_x = np.zeros([batch_count, width * height])
  66.     batch_y = np.zeros([batch_count, CAPTCHA_LEN * len(CAPTCHA_LIST)])
  67.     for i in range(batch_count):
  68.         text, image = genCaptchaTextImage()
  69.         image = convert2Gray(image)
  70.         # 将图片数组一维化 同时将文本也对应在两个二维组的同一行
  71.         batch_x[i, :] = image.flatten() / 255
  72.         batch_y[i, :] = text2Vec(text)
  73.     return batch_x, batch_y
  74. def cnnGraph(x, keepProb, size, captchaList=CAPTCHA_LIST, captchaLen=CAPTCHA_LEN):
  75.     “”
  76.     三层卷积神经网络
  77.     ““”
  78.     imageHeight, imageWidth = size
  79.     xImage = tf.reshape(x, shape=[-1, imageHeight, imageWidth, 1])
  80.     hDrop1 = hDrop(xImage, [33132], [32], keepProb)
  81.     hDrop2 = hDrop(hDrop1, [333264], [64], keepProb)
  82.     hDrop3 = hDrop(hDrop2, [336464], [64], keepProb)
  83.     # 全连接层
  84.     imageHeight = int(hDrop3.shape[1])
  85.     imageWidth = int(hDrop3.shape[2])
  86.     wFc = variable([imageHeight * imageWidth * 641024], 0.01)  # 上一层有64个神经元 全连接层有1024个神经元
  87.     bFc = variable([1024], 0.1)
  88.     hDrop3Re = tf.reshape(hDrop3, [-1, imageHeight * imageWidth * 64])
  89.     hFc = tf.nn.relu(tf.matmul(hDrop3Re, wFc) + bFc)
  90.     hDropFc = tf.nn.dropout(hFc, keepProb)
  91.     # 输出层
  92.     wOut = variable([1024, len(captchaList) * captchaLen], 0.01)
  93.     bOut = variable([len(captchaList) * captchaLen], 0.1)
  94.     yConv = tf.matmul(hDropFc, wOut) + bOut
  95.     return yConv
  96. def captcha2Text(image_list):
  97.     “”
  98.     验证码图片转化为文本
  99.     ““”
  100.     with tf.Session() as sess:
  101.         saver.restore(sess, tf.train.latest_checkpoint(‘model/’))
  102.         predict = tf.argmax(tf.reshape(yConv, [-1, CAPTCHA_LEN, len(CAPTCHA_LIST)]), 2)
  103.         vector_list = sess.run(predict, feed_dict={x: image_list, keepProb: 1})
  104.         vector_list = vector_list.tolist()
  105.         text_list = [vec2Text(vector) for vector in vector_list]
  106.         return text_list
  107. x = tf.placeholder(tf.float32, [None, CAPTCHA_HEIGHT * CAPTCHA_WIDTH])
  108. keepProb = tf.placeholder(tf.float32)
  109. yConv = cnnGraph(x, keepProb, (CAPTCHA_HEIGHT, CAPTCHA_WIDTH))
  110. saver = tf.train.Saver()
  111. def handler(environ, start_response):
  112.     try:
  113.         request_body_size = int(environ.get(‘CONTENT_LENGTH’0))
  114.     except (ValueError):
  115.         request_body_size = 0
  116.     requestBody = json.loads(environ[‘wsgi.input’].read(request_body_size).decode(“utf-8”))
  117.     imageName = randomStr(10)
  118.     imagePath = “/tmp/” + imageName
  119.     print(“requestBody: “, requestBody)
  120.     reqType = requestBody.get(“type”, None)
  121.     if reqType == “get_captcha”:
  122.         genCaptchaTextImage(save=imagePath)
  123.         with open(imagePath, ‘rb’as f:
  124.             data = base64.b64encode(f.read()).decode()
  125.         return Response(start_response, {‘image’: data})
  126.     if reqType == “get_text”:
  127.         # 图片获取
  128.         print(“Get pucture”)
  129.         imageData = base64.b64decode(requestBody[“image”])
  130.         with open(imagePath, ‘wb’as f:
  131.             f.write(imageData)
  132.         # 开始预测
  133.         img = Image.open(imageName)
  134.         img = img.resize((16060), Image.ANTIALIAS)
  135.         img = img.convert(“RGB”)
  136.         img = np.asarray(img)
  137.         image = convert2Gray(img)
  138.         image = image.flatten() / 255
  139.         return Response(start_response, {‘result’: captcha2Text([image])})

在这个函数部分,主要包括两个接口:

• 获取验证码:用户测试使用,生成验证码

• 获取验证码识别结果:用户识别使用,识别验证码

这部分代码,所需要的依赖内容如下:

  1. tensorflow==1.13.1
  2. numpy==1.19.4
  3. scipy==1.5.4
  4. pillow==8.0.1
  5. captcha==0.3

另外,为了更加简单的来体验,提供测试页面,测试页面的后台服务使用 Python Web Bottle 框架:

  1. # -*- coding:utf-8 -*-
  2. import os
  3. import json
  4. from bottle import route, run, static_file, request
  5. import urllib.request
  6. url = “http://” + os.environ.get(“url”)
  7. @route(‘/’)
  8. def index():
  9.     return static_file(“index.html”, root=‘html/’)
  10. @route(‘/get_captcha’)
  11. def getCaptcha():
  12.     data = json.dumps({“type”“get_captcha”}).encode(“utf-8”)
  13.     reqAttr = urllib.request.Request(data=data, url=url)
  14.     return urllib.request.urlopen(reqAttr).read().decode(“utf-8”)
  15. @route(‘/get_captcha_result’, method=‘POST’)
  16. def getCaptcha():
  17.     data = json.dumps({“type”“get_text”“image”: json.loads(request.body.read().decode(“utf-8”))[“image”]}).encode(
  18.         “utf-8”)
  19.     reqAttr = urllib.request.Request(data=data, url=url)
  20.     return urllib.request.urlopen(reqAttr).read().decode(“utf-8”)
  21. run(host=‘0.0.0.0’, debug=False, port=9000)

该后端服务,所需依赖:

  1. bottle==0.12.19

前端页面代码:

  1. <!DOCTYPE html>
  2. <html lang=“en”>
  3. <head>
  4.     <meta charset=“UTF-8”>
  5.     <title>验证码识别测试系统</title>
  6.     <link href=“https://www.bootcss.com/p/layoutit/css/bootstrap-combined.min.css” rel=“stylesheet”>
  7.     <script>
  8.         var image = undefined
  9.         function getCaptcha() {
  10.             const xmlhttp = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject(“Microsoft.XMLHTTP”);
  11.             xmlhttp.open(“GET”‘/get_captcha’false);
  12.             xmlhttp.onreadystatechange = function () {
  13.                 if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
  14.                     image = JSON.parse(xmlhttp.responseText).Response.image
  15.                     document.getElementById(“captcha”).src = “data:image/png;base64,” + image
  16.                     document.getElementById(“getResult”).style.visibility = ‘visible’
  17.                 }
  18.             }
  19.             xmlhttp.setRequestHeader(“Content-type”“application/json”);
  20.             xmlhttp.send();
  21.         }
  22.         function getCaptchaResult() {
  23.             const xmlhttp = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject(“Microsoft.XMLHTTP”);
  24.             xmlhttp.open(“POST”‘/get_captcha_result’false);
  25.             xmlhttp.onreadystatechange = function () {
  26.                 if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
  27.                     document.getElementById(“result”).innerText = “识别结果:” + JSON.parse(xmlhttp.responseText).Response.result
  28.                 }
  29.             }
  30.             xmlhttp.setRequestHeader(“Content-type”“application/json”);
  31.             xmlhttp.send(JSON.stringify({“image”: image}));
  32.         }
  33.     </script>
  34. </head>
  35. <body>
  36. <div class=”containerfluid” style=”margintop: 10px“>
  37.     <div class=”rowfluid“>
  38.         <div class=”span12“>
  39.             <center>
  40.                 <h3>
  41.                     验证码识别测试系统
  42.                 </h3>
  43.             </center>
  44.         </div>
  45.     </div>
  46.     <div class=”rowfluid“>
  47.         <div class=”span2“>
  48.         </div>
  49.         <div class=”span8“>
  50.             <center>
  51.                 <img src=”” id=”captcha“/>
  52.                 <br><br>
  53.                 <p id=”result“></p>
  54.             </center>
  55.             <fieldset>
  56.                 <legend>操作:</legend>
  57.                 <button class=”btn” onclick=”getCaptcha()”>获取验证码</button>
  58.                 <button class=”btn” onclick=”getCaptchaResult()” id=”getResult” style=”visibilityhidden“>识别验证码
  59.                 </button>
  60.             </fieldset>
  61.         </div>
  62.         <div class=”span2“>
  63.         </div>
  64.     </div>
  65. </div>
  66. </body>
  67. </html>

准备好代码之后,开始编写部署文件:

  1. Global:
  2.   Service:
  3.       Name: ServerlessBook
  4.       Description: Serverless图书案例
  5.       Log: Auto
  6.       Nas: Auto
  7. ServerlessBookCaptchaDemo:
  8.   Component: fc
  9.   Provider: alibaba
  10.   Access: release
  11.   Extends:
  12.     deploy:
  13.       – Hook: s install docker
  14.         Path: ./
  15.         Pre: true
  16.   Properties:
  17.     Region: cn-beijing
  18.     Service: ${Global.Service}
  19.     Function:
  20.       Nameserverless_captcha
  21.       Description: 验证码识别
  22.       CodeUri:
  23.         Src: ./src/backend
  24.         Excludes:
  25.           – src/backend/.fun
  26.           – src/backend/model
  27.       Handlerindex.handler
  28.       Environment:
  29.         – KeyPYTHONUSERBASE
  30.           Value: /mnt/auto/.fun/python
  31.       MemorySize: 3072
  32.       Runtimepython3
  33.       Timeout: 60
  34.       Triggers:
  35.         – NameImageAI
  36.           TypeHTTP
  37.           Parameters:
  38.             AuthTypeANONYMOUS
  39.             Methods:
  40.               – GET
  41.               – POST
  42.               – PUT
  43.             Domains:
  44.               – DomainAuto
  45. ServerlessBookCaptchaWebsiteDemo:
  46.   Componentbottle
  47.   Provideralibaba
  48.   Accessrelease
  49.   Extends:
  50.     deploy:
  51.       – Hookpip3 install –r requirements.txt –t ./
  52.         Path: ./src/website
  53.         Pretrue
  54.   Properties:
  55.     Regioncnbeijing
  56.     CodeUri: ./src/website
  57.     Appindex.py
  58.     Environment:
  59.       – Keyurl
  60.         Value: ${ServerlessBookCaptchaDemo.Output.Triggers[0].Domains[0]}
  61.     Detail:
  62.       Service: ${Global.Service}
  63.       Function:
  64.         Nameserverless_captcha_website

整体的目录结构:

  1.  | – src # 项目目录
  2.  |   | – backend # 项目后端,核心接口
  3.  |       | – index.py # 后端核心代码
  4.  |       | – requirements.txt # 后端核心代码依赖
  5.  |   | – website # 项目前端,便于测试使用
  6.  |       | – html # 项目前端页面
  7.  |           | – index.html # 项目前端页面
  8.  |       | – index.py # 项目前端的后台服务(bottle框架)
  9.  |       | – requirements.txt # 项目前端的后台服务依赖

完成之后,我们可以在项目目录下,进行项目的部署:

  1. s deploy

部署完成之后,打开返回的页面地址:

%title插图%num

点击获取验证码,即可在线生成一个验证码:

%title插图%num

此时点击识别验证码,即可进行验证码识别:

%title插图%num

由于模型在训练的时候,填写的目标准确率是 90%,所以可以认为在海量同类型验证码测试之后,整体的准确率在 90% 左右。

%title插图%num

总结

Serverless 发展迅速,通过 Serverless 做一个验证码识别工具,我觉得这是一个非常酷的事情。在未来的数据采集等工作中,有一个优美的验证码识别工具是非常必要的。当然验证码种类很多,针对不同类型的验证码识别,也是一项非常有挑战性的工作。

%title插图%num

Serverless Devs

Serverless Devs 是一个开源开放的 Serverless 开发者平台,致力于为开发者提供强大的工具链体系。通过该平台,开发者可以一键体验多云 Serverless 产品,*速部署 Serverless 项目。

  • Github 地址:

    https://github.com/serverless-devs

  • Gitee 地址:

    https://gitee.com/organizations/serverless-devs/projects

  • Serverless Devs 官网:

    https://www.serverless-devs.com

Serverless 在大规模数据处理中的实践

前言

当您*次接触 Serverless 的时候,有一个不那么明显的新使用方式:与传统的基于服务器的方法相比,Serverless 服务平台可以使您的应用快速水平扩展,并行处理的工作更加有效。这主要是因为 Serverless 可以不必为闲置的资源付费,不用担心预留的资源不够。而在传统的使用范式中,用户必须预留成百上千的服务器来做一些高度并行化但执行时长较短的任务,而且必须为每一台服务器买单,即使有的服务器已经不再工作了。

以阿里云 Serverless 产品——函数计算为例,便可以完美解决您上述所有顾虑:

  • 如果您的任务本身计算量不是很大,但是有大量的并发任务请求需要并行处理, 比如多媒体文件处理、文档转换等;
  • 一个任务本身计算量很大,要求单个任务很快处理完,并且还能支持并行处理多个任务。

在这种场景下,用户唯一关注的就是:您的任务是可以分治拆解并且子任务是可以并行处理的,一个需要一个小时才能处理完的长任务,可以分解成 360 个独立的 10 秒长的子任务并行处理,这样,以前您要花一个小时才能处理完的任务,现在只需要 10 秒就可以搞定。由于采用的是按量计费的模型,完成的计算量和成本是大致相当的,而传统模型则因为预留资源肯定会存在浪费,浪费的费用也是需要您去承担的。

接下来,将详细阐述 Serverless 在大规模数据处理上的实践。

%title插图%num

*致弹性扩缩容应对计算波动

在介绍相关的大规模数据处理示例之前, 这里先简单介绍一下函数计算。

1. 函数计算简介

%title插图%num

  • 开发者使用编程语言编写应用和服务,函数计算支持的开发语言请参见开发语言列表;
  • 开发者上传应用到函数计算;
  • 触发函数执行:触发方式包括 OSS、API 网关、日志服务、表格存储以及函数计算 API、SDK 等;
  • 动态扩容以响应请求:函数计算可以根据用户请求量自动扩容,该过程对您和您的用户均透明无感知;
  • 根据函数的实际执行时间按量计费:函数执行结束后,可以通过账单来查看执行费用,收费粒度精确到 100 毫秒。

详情:函数计算官网

至此,您大约可以简单理解到函数计算是怎么运作的,接下来以大量视频并行转码的案例来阐述:假设一家在家教育或娱乐相关的企业,老师授课视频或者新的片源一般是集中式产生,而您希望这些视频被快速转码处理完以便能让客户快速看到视频回放。比如在当下疫情中,在线教育产生的课程激增,而出课高峰一般是 10 点、12 点、16 点、18 点等明显的峰值段,特定的时间内(比如半个小时)处理完所有新上传的视频是一个通用而且普遍的需求。

2. 弹性高可用的音视频处理系统

  • OSS 触发器

%title插图%num

如上图所示,用户上传一个视频到 OSS,OSS 触发器自动触发函数执行,函数计算自动扩容,执行环境内的函数逻辑调用 FFmpeg 进行视频转码,并且将转码后的视频保存回 OSS。

  • 消息触发器

%title插图%num

如上图所示,应用只需要发一个消息,自动触发函数执行音视频处理的任务即可,函数计算自动扩容,执行环境内的函数逻辑调用 FFmpeg 进行视频转码, 并且将转码后的视频保存回 OSS。

  • 直接手动调用 SDK 执行音视频处理任务

以 python 为例,大致如下:

  1. 1python
  2. 2    # -*- coding: utf-8 -*-
  3. 3    import fc2
  4. 4    import json
  5. 5
  6. 6    client = fc2.Client(endpoint=“http://123456.cn-hangzhou.fc.aliyuncs.com”,accessKeyID=“xxxxxxxx”,accessKeySecret=“yyyyyy”)
  7. 7
  8. 8    # 可选择同步/异步调用
  9. 9    resp = client.invoke_function(“FcOssFFmpeg”“transcode”, payload=json.dumps(
  10. 10    {
  11. 11        “bucket_name” : “test-bucket”,
  12. 12        “object_key” : “video/inputs/a.flv”,
  13. 13        “output_dir” : “video/output/a_out.mp4”
  14. 14    })).data
  15. 15
  16. 16    print(resp)

从上面我们也可以看出,触发函数执行的方式也很多,同时简单配置下 SLS 日志,就可以很快实现一个弹性高可用、按量付费的音视频处理系统,同时能提供免运维、具体业务数据可视化、强大自定义监控报警等超强功能的 dashboard。

%title插图%num

目前已经落地的音视频案例有 UC、语雀、躺平设计之家、虎扑以及几家在线教育的头部客户等,其中有些客户高峰期间,弹性使用到了万核以上 CPU 计算资源,并行处理的视频达到 1700+,同时提供了*高的性价比。

详情可以参考:

  • simple-video-processing
  • fc-oss-ffmpeg

%title插图%num

任务分治,并行加速

这种将任务分而治之的思想应用在函数计算上是一件有趣的事情,在这里举一个例子,比如您有一个超大的 20G 的 1080P 高清视频需要转码,即使您使用一台高配机器,需要的时间可能还是要按小时计,如果中途出问题中断转码,您只能重新开始再重复一遍转码的过程,如果您使用分治的思想+函数计算,转码的过程衍变为 分片-> 并行转码分片-> 合并分片,这样就可以解决您上述的两个痛点:

  • 分片和合成分片是内存级别的拷贝,需要的计算量*小,真正消耗计算量的转码,拆分成了很多子任务并行处理,在这个模型中,分片转码的*大时间基本等同于整个大视频的转码时间;
  • 即使中途某个分片转码出现异常,只需要重试这个分片的转码即可,不需要整个大任务推倒重来。

通过将大任务合理的分解,配合使用函数计算,编写一点 code,就可以快速完成一个弹性高可用、并行加速、按量付费的大型数据处理系统。

在介绍这个方案之前,我们先简单介绍一下 Serverless 工作流,Serverless 工作流可以很好地将函数和其他云服务和自建服务有组织地编排起来。

1. Serverless 工作流简介

Serverless 工作流(Serverless Workflow)是一个用来协调多个分布式任务执行的全托管云服务。在 Serverless 工作流中,您可以用顺序、分支、并行等方式来编排分布式任务,Serverless 工作流会按照设定好的步骤可靠地协调任务执行,跟踪每个任务的状态转换,并在必要时执行用户定义的重试逻辑,以确保工作流顺利完成。Serverless 工作流简化了开发和运行业务流程所需要的任务协调、状态管理以及错误处理等繁琐工作,让您聚焦业务逻辑开发。

详情:Serverless 工作流官网

接下来以一个大视频快速转码的案例来阐述 Serverless 工作编排函数,实现大计算任务的分解,并行处理子任务,*终达到快速完成单个大任务的目的。

2. 大视频的快速多目标格式转码

%title插图%num

如下图所示,假设用户上传一个 mov 格式的视频到 OSS,OSS 触发器自动触发函数执行,函数调用 FnF 执行,FnF 同时进行 1 种或者多种格式的转码(由 template.yml 中的 DST_FORMATS 参数控制),假设配置的是同时进行 mp4 和 flv 格式的转码。

  • 一个视频文件可以同时被转码成各种格式以及其他各种自定义处理,比如增加水印处理或者在 after-process 更新信息到数据库等;
  • 当有多个文件同时上传到 OSS,函数计算会自动伸缩,并行处理多个文件,同时每次文件转码成多种格式也是并行;
  • 结合 NAS + 视频切片,可以解决超大视频的转码,对于每一个视频,先进行切片处理,然后并行转码切片,*后合成,通过设置合理的切片时间,可以大大加快较大视频的转码速度;
  • fnf 可以跟踪每一步执行情况,并且可以自定义每一个步骤的重试,提高任务系统的鲁棒性,如:retry-example

详情可以参考:fc-fnf-video-processing

在任务分治,并行加速具体的案例中,上面分享的是 CPU 密集型任务分解,但也可以进行 IO 密集型任务分解,比如这个需求:上海的 region 的 OSS bucket 中的一个 20G 大文件,秒级转存回杭州的 OSS Bucket 中。这里也可以采用分治的思路,Master 函数在接到转存任务之后,将超大文件进行分片的 range 分配给每个 Worker 子函数,Worker 子函数并行转存属于自己那部分的分片,Master 函数待所有子 Worker 运行完毕之后,提交合并分片请求,完成整个转存任务。

%title插图%num

详情可以参考:利用函数计算多实例并发实现秒级转存超大文件

%title插图%num

总结

本文探讨了 Serverless 服务平台可以使您的应用快速水平扩展,并行处理的工作更加有效,并给出了具体的实践案例,无论在 CPU 密集型还是 IO 密集型场景,函数计算 + Serverless 都能完美解决您以下顾虑:

  • 不必为闲置的资源付费
  • 不用担心计算资源预留不够
  • 大计算量的任务需要快速处理完毕
  • 更好的任务流程跟踪
  • 完善的监控报警、免运维、业务数据可视化等

从 Serverfull 到 Serverless ,发生了什么

Serverfull 到 Serverless 的演变

%title插图%num

上图是 MVC 架构的 Web 应用部署之后的典型情况。上图中的整个蓝色部分就是服务端的边界,它是负责应用或代码的线上运维。而 Serverless 要解决的问题的边界就是服务端的边界,也就是服务端运维。

那么下面我们先来看一下服务端运维的发展史,也就是从一开始到 Serverless 的发展史。假设有一个 Web 应用,这个 Web 应用的研发涉及到两个角色:研发工程师和运维工程师。

研发工程师只关心应用的业务逻辑。具体来说就是,整个 MVC 架构 Web 应用的开发都归他负责,也就是从服务端界面 View 层,到业务逻辑 Control 层,再到数据存储 Model 层,整个 Web 应用的版本管理和线上 bug 修复都归研发工程师。

运维工程师则只关心应用的服务端运维事务。他负责部署上线小程的 Web 应用,绑定域名以及日志监控。在用户访问量大的时候,他要给这个应用扩容;在用户访问量小的时候,他要给这个应用缩容;在服务器挂了的时候,他还要重启或者换一台服务器。

  • Serverfull 时代。*开始的时候,研发工程师不用关心任何部署相关的事情。研发工程师每次发布新的应用后,运维工程师都负责部署上线*新的代码。运维工程师需要管理好迭代版本的发布,分支合并,将应用上线,遇到问题回滚。如果线上出了故障,还需要抓取日志发给研发工程师。

    Serverfull 时代将研发和运维完全隔离开来了。这种完全隔离开来的好处很明显:研发工程可以专心做好自己的业务,但是运维工程师就成了工具人了,就困在大量的运维工作中,处理大量琐碎的杂事。

  • DevOps 时代。运维工程师发现有很多事情都是重复性的工作,线上出故障了还得自己抓日志发给研发工程师,效率很低。因此运维工程师就开发了一套运维控制台,将部署上线和日志抓取的工作让研发工程师处理。

    这样,运维工程师可以稍微轻松点了,但是优化架构和扩缩容资源方案还是得负责。而研发工程师除了开发的任务,还要自己通过运维控制台发布新版本和解决线上故障。这个时候是研发兼运维 DevOps,研发工程师兼任了部分运维工程师的工作,但是这部分的工作就应该是研发工程负责的(比如版本控制、线上故障等),而且运维工程师将这部分工作工具化了,更加高效了,有 less 的趋势了。

  • 工业时代。运维工程师又基于研发工程师的开发流程,将运维控制台进一步提升,可以实现代码自动发布:代码扫描-测试-灰度验证-上线。这样一来,研发工程师只需要将*新的代码合并到 Git 仓库指定的 develop 分支,剩下的就由代码自动发布的流水线来负责了。这个时候研发工程师也不需要运维了,免运维 NoOps,研发工程师也就回到了当初,只需要关心自己的应用业务就可以了。

    同时,运维工程师发现资源优化和扩缩容方案也可以利用性能监控+流量估算解决。这样运维工程师的运维工作也全都自动化了。那么对于研发工程师来说,运维工程师的存在感越来越弱,需要运维工程师干的事情越来越少,都由自动化工具替代了。这就是 Serverless。

  • 未来。实现了免运维之后,运维工程师要转型去做更底层的服务,做基础架构的建设,提供更加智能、更加节省资源、更加周到的服务。而研发工程师可以完全不被运维的事情困扰,专注做好自己的业务,提升用户体验,思考业务价值。

    免运维 NoOps 并不是说服务端运维就不存在了,而是通过全知全能的服务,覆盖研发部署需要的所有需求,让研发工程师对它的感知越来越少。另外,NoOps 是理想状态,因为我们只能无限逼近 NoOps,所以说是 less,而不是 ServerZero。

Serverless 的 Server 限定了 Serverless 解决问题的边界,即服务端运维;less 说明了 Serverless 解决问题的目的,即免运维 NoOps。所以,Serverless 应该叫做服务端免运维,这也就是 Serverless 要解决的问题。

%title插图%num

什么是 Serverless

Serverless 要解决的就是将运维工程师的工作彻底透明化;而研发工程师只关心业务逻辑,不用关心部署运维和上线的各种问题。而要实现这种状态,那么就意味要对整个互联网服务端的运维工作进行*端抽象。而越抽象的东西,由于蕴含的信息量越大,所以越难定义。

但是,总的来说 Serverless 的含义有这两种:

  • 狭义 Serverless(*常见)是指 Serverless computing 架构 = FaaS 架构 = Trigger(事件驱动)+FaaS(Function as a Service,函数即服务)+BaaS(Backend as a Service,后端即服务,持久化或第三方服务)=FaaS + BaaS。
  • 广义 Serverless 是指服务端免运维,也就是具有 Serverless 特性的云服务。

%title插图%num

  狭义的 Serverless

我们日常工作提到的 Serverless 都是指狭义的 Serverless。

而这主要是因为历史原因,2014 年 11 月份,亚马逊推出了真正意义上的*款 Serverless FaaS 服务:Lambda。从此,Serverless 的概念才进入大多数人的视野,因此 Serverless 曾经一度就等于 FaaS。FaaS,函数即服务,它还有个名称叫作 Serverless Computing,它可以让我们随时随地创建、使用、销毁一个函数。

通常函数的使用过程:需要先从代码加载到内存,也就是实例化,然后被其他函数调用时执行。FaaS 中也是一样的,函数也需要实例化,然后被触发器 Trigger 调用。这两个*大的区别就是在 Runtime,也就是函数的上下文。FaaS 的 Runtime 是预先设置好的,都是云服务商提供的,我们可以使用但是无法控制。并且 FaaS 的 Runtime 是临时的,当 FaaS 的函数调用完之后,云服务商就会销毁这个实力,回收资源,也就意味着这个临时的 Runtime 会和函数一起销毁。因此,FaaS 推荐无状态的函数,也就是一个函数只要参数固定,那么返回的结果也必须是固定的。

那么将一开始的 MVC 架构的 Web 应用变成 Serverless 的话,那应该是怎样的呢?View 层是客户端展示的内容,通常并不需要函数算力;Control 层,就是函数的典型使用场景。在 MVC 架构中,一个 HTTP 的数据请求往往对应着一个 Control 函数,因此这个 Control 函数完全可以被 FaaS 函数代替。在 HTTP 的数据请求量大的时候,FaaS 函数会自动扩容多实例同时运行;在 HTTP 的数据请求量小的时候,又会自动缩容;当没有 HTTP 请求的时候,还会缩容至 0 实例。如下图所示:

%title插图%num

Control 函数变成了无状态的,并且函数的实例在不停地扩容缩容,那么此时想要持久化一些数据怎么办?当然 Control 函数中还是可以以操作数据库的命令方式来实现。但是,这种方式并不合理,因为 Control 层的方式变了,假如 Model 层还是以之前的那种方式,那么这种架构肯定是要散架。此时,就需要 BaaS 了,也就是将 Model 层进行 BaaS 化,BaaS 就是专门配合 FaaS 用的。下面 Model 层以 MySQL 为例,Model 层*好将操作数据库的命令封装成 HTTP 的 OpenAPI,提供给 FaaS 调用,自己控制这个 API 的请求频率以及限流降低等。这个 Model 层本身则可以通过连接池、MySQL 集群等方式去优化。如下图所示:

%title插图%num

至此,基于 Serverless 架构,传统的 MVC 架构完完全全被转化为了 View + FaaS + BaaS 的组合了。Serverless 毋庸置疑是因为 FaaS 架构才流行起来的。我们常见的 Serverless 都是指 Serverless Computing 架构,也就是由 Trigger、FaaS、BaaS 架构组成的应用。

  广义的 Serverless

广义的 Serverless 其实就是指服务端免运维,也是未来的趋势。要想达到 NoOps,需要具备:

  • 无需用户关心服务端的事情(容错、容灾、安全验证、自动扩缩容、日志调试)
  • 按使用量(调用次数、时长等)付费,低费用和高性能并行,大多数场景下节省开支。
  • 快速迭代&试错能力(多版本控制、灰度、CI&CD 等等)。

%title插图%num

为什么需要 Serverless 呢

在 2009 年的时候,有两种相互竞争的云虚拟化方法:

  • Amazon EC2,EC2 实例看起来很像物理硬件,用户可以从内核向上控制整个软件栈。
  • Google App Engine,是另一个针对特定领域的应用平台,它是一种将 stateless 计算层和 stateful 的存储层分类开来的一种应用程序结构。

*终市场使用了 Amazon 这种针对云计算的 low-level 虚拟机方式,而这种  low-level 虚拟机成功的主要原因是,早起的云计算用户希望在云中可以重新创建一个与本地计算机上相同的计算环境,以简化将其负载迁移到云上的工作。很明显,这种实际需求比为云重新编写新的程序更重要,尤其是在当时云计算能否成功尚不明确的情况下。

然后这种方式的缺点是,开发人员必须自己管理虚拟机,所以要么成为系统管理员,要么与它们一起设置环境。这些促使那些只使用简单化应用的客户向云服务商提出新要求,希望能有更简单的方式来运行这些简单应用。例如,假设应用希望将图片从手机端应用发送到云上,这需要创建*小的图片并将其放在 web 上,完成这个任务可能只需要几十行 JavaScript 代码,这与设置适当的服务器环境来运行这段代码相比,这个代码的开发是很微不足道的。

在这些需求的驱使下,Amazon 在 2015 年推出了一个名为 AWS Lambda service 的新服务。用户只需要编写代码,服务器供应和任务管理问题都由服务提供商来负责。编写的代码被打包为 FaaS(Function as a service),代表了 Serverless 计算的核心,但是云平台还提供了专门的 Serverless 框架,以满足特定的程序需求,如 BaaS(Backend as a Service)。简单地说,无服务计算定义如下:Serverless Computing = FaaS  + BaaS。同时,服务被视为无服务的话,那么必须能够自动扩缩容,并且根据实际使用情况计费。

★Cloud functions (i.e., FaaS) provide general compute and are complemented by an ecosystem of specialized Backend as a Service (BaaS) offfferings such as object storage, databases, or messaging.

%title插图%numServerless VS Serverful

在 Serverless 中,用户只需要使用高级语言编写云函数,选择触发云函数运行的事件就可以了。例如,加载一个图像到云存储中,或者向数据库添加一个很小的图片时,用户只需要编写相应的代码,而剩下的全都由 Serverless 系统来处理,比如选择实例、扩缩容、部署、容错、监控、日志、安全补丁等等。下面,总结了 Serverless 和传统方式的差异,我们将传统方式称为 Serverful。

%title插图%num

Serverless 和 Serverful 计算*关键的三个不同之处在于:

  1. **将计算与存储解耦。**存储和计算资源是分开提供的,相当于这两种资源的分配和计价都是独立的,通常来说存储资源是由一个独立的云服务来提供的,并且计算是无状态的。
  2. **执行代码而不需要管理资源分配。**与传统云计算用户需要请求资源的方式不同,serverless 是用户提交一段代码,云会自动给这段代码分配资源并执行。
  3. **以实际使用的资源量付费,而不是根据分配的资源数。**serverless 计费是根据一系列与执行相关的因素来计算的,例如代码的执行时间,而不实根据云平台,例如分配的 VM 的大小和数量

假如使用汇编语言和高级语言来形容的话,Serverful 计算类似于使用低级汇编语言进行编程,而 Serverless 计算类似于使用高级语言(例如 python)进行编程。例如,c = a + b 的简单表达式的汇编程序员必须显示选择一个或者多个寄存器,将值加载到这些寄存器中,执行运算,然后存储结果。这跟 Serverful 云编程的几个步骤是类似的:首先提供资源或者标识可用的资源,然后用必要的代码和数据加载这些资源,执行计算,返回或者存储结果,*终管理资源释放。而 Serverless 则提供了类似于高级编程语言的便捷性,Serverless 和高级编程语言也很相似性。比如,高级语言的自动内存管理不用再管理内存资源,而  Serverless 计算使程序员也不用再管理服务器资源。

%title插图%numAttractiveness of Serverless Computing

  对云服务提供商来说

  • Serverless 可以促进业务的增长,因为它使得云计算更容易编程,进而有助于吸引新客户并帮助现有客户更多地使用云计算。例如,*近的调查发现,大约 24% 的 Serverless 计算用户是云计算的新用户,30% 现有的 serverful 用户也使用了 Serverless 计算。
  • 短的运行时间、较小的内存占用和无状态特性使得云提供商更容易找到那哪些未使用的资源来运行这些任务,从而改进了资源复用。
  • 可以利用不太流行的计算机(实例类型由云提供商决定),比如对 serverful 云客户吸引较小的旧服务器。

★后面的两点可以*大化现有的资源并提高收益。

  对用户来说

  • 从编程效率的提高中获益,对于新手来说不需要理解云基础设施的前提下部署函数,老用户可以节省出部署的时间并聚焦于应用本身的问题。
  • 节约成本,因为云服务提供商将底层服务器的利用率提高了,并且函数只有在事件发生时才会计费,而且是细粒度的计费(通常是 100 毫秒),那么也就意味着只需要支付他们实际使用的部分而不是为他们预留的部分。

%title插图%numFaaS 是怎么运行的

在 Serverless 出现之前,我们要部署这样一个”Hello World”应用得何等繁琐。

  1. 我们要购买虚拟机服务;
  2. 初始化虚拟机运行环境,安装我们需要的应用运行环境,尽量和本地开发环境保持一致;
  3. 紧接着为了让用户能够访问我们刚刚启动的应用,我们需要购买域名,用虚拟机 IP 注册域名,配置 Nginx,启动 Nginx;
  4. *后我们还需要上传应用代码;
  5. 启动应用;

%title插图%num

采用 Serverless 之后,只需要简单的 3 步。Serverless 相当于对服务端运维体系进行了*端的抽象(抽象意味着用户请求 HTTP 数据请求的全链路,并没有质的改变,只是将全链路的模型简化了)。

  1. 之前在服务端构建代码的运行环境—函数服务
  2. 之前需要负载均衡和反向代理— HTTP 函数触发器
  3. 上传代码和启动应用—函数代码

整个启动过程如下图所示:

  1. 用户*次访问 HTTP 函数触发器时,函数触发器会 Hold 住用户的 HTTP 请求,并产生一个HTTP Request 事件通知函数服务;
  2. 函数服务检查有没有闲置的函数实例,如果没有函数实例,则去函数代码仓库拉取你的代码,初始化并启动一个函数实例;之后再传入 HTTP Request 对象作为函数的参数,执行函数。
  3. 函数执行的结果 HTTP Response 返回函数触发器,函数触发器再将结果返回给等待的用户客户端。

%title插图%num

★FaaS 和 PaaS 平台对比,*大的区别在于资源利用率。这也是 FaaS *大的创新点,FaaS 的应用实例可以缩容到 0,而 PaaS 平台至少要维持一台服务或容器。这主要是因为 FaaS 可以做到*速启动函数实例,而 PaaS 创建实例通常需要几十秒,为了保证你的服务可用性,必须一直维持至少一台服务器运行你的应用实例。

%title插图%num

FaaS 的*速启动

FaaS 中的冷启动是指从调用函数开始到函数实例准备完成的整个过程。冷启动关注的是启动时间,启动时间越短,对资源的利用率就越高。现在的云服务商,基于不同的语言特性,冷启动平均耗时基本在 100~700 毫秒之间。

下面这张图是 FaaS 应用冷启动的过程。其中,蓝色部分是云服务商负责的,红色部分是用户负责的。云服务商会不停地优化自己负责的部分,毕竟启动速度越快对资源的利用率就越高,例如冷启动过程中耗时较长的是下载函数代码。所以一旦你更新代码,云服务商就会偷偷开始调度资源,下载你的代码构建函数实例的镜像。请求*次访问时,云服务商就可以利用构建好的缓存镜像,直接跳过冷启动的下载函数代码步骤,从镜像启动容器,这个也叫预热冷启动。除此之外,还有预留实例策略也可加速或绕过冷启动时间。

%title插图%num

★FaaS 服务从 0 开始,启动并执行完一个函数,只需要 100 毫秒。这也是为什么 FaaS 敢缩容到 0 的主要原因。通常我们打开一个网页有个关键指标,响应时间在 1 秒以内,都算优秀。这么一对比,100 毫秒的启动时间,对于网页的秒开率影响真的*小。

为什么应用托管平台 PaaS 做不到*速启动呢?因为应用托管平台 PaaS 为了适应用户的多样性,必须支持多语言兼容,还要提供传统后台服务,例如 MySQL、Redis。这也就意味着,PaaS 在初始化环境时,有大量依赖和多语言版本需要兼容,而且兼容多种用户的应用代码往往也会增加应用构建过程的时间。

而 FaaS 设计之初就牺牲了用户的可控性和应用场景,来简化代码模型,并且分层结构进一步提升了资源的利用率。

%title插图%num

FaaS 的分层

FaaS 实例执行时,就如下图所示,至少是 3 层结构:容器、运行时 runtime、具体的函数代码。

  • 目前的 FaaS 实现方案中,容器方案可能是 Docker 容器、VM 虚拟机,甚至 Sandbox 沙盒环境。
  • 运行时 Runtime,就是你的函数执行时的上下文 context。Runtime 的信息包括代码运行的语言和版本,例如 Node.js v10,Python3.6;可调用对象,例如 aliyun SDK;系统信息,例如环境变量等等。

%title插图%num

这样分层的好处就是,容器层适用性更广,云服务商可以预热大量的容器实例,将物理服务器的计算碎片化。Runtime 的实例适用性较低,可以少量预热。容器和 Runtime 固定后,下载你的代码就可以执行了。通过分层,我们就可以做到资源统筹优化,让你的代码快速低成本地被执行。

另外,一旦容器 & Runtime 启动后,就会维持一段时间,这段时间内的这个函数实例就可以直接处理用户数据请求。当一段时间内没有用户请求事件发生(各个云服务商维持实例的时间和策略不同),则会销毁这个函数实例。

%title插图%num

%title插图%numFaaS 进程模型

从运行函数实例的进程角度来看,有两种模型:

  • 用完即毁型:函数实例准备好后,执行完函数就直接结束。FaaS *纯正的用法。
  • 常驻进程型:函数实例准备好后,执行完函数不结束,而是返回继续等待下一次函数被调用。即使 FaaS 是常驻进程型,如果一段时间没有事件触发,函数实例还是会被云服务商销毁。

★从下面这张图其实可以看到触发器就是一个常驻进程模型,只不过这个触发器由云服务商处理罢了。

%title插图%num

假设我们要部署的是一个 MVC 架构的 Web 服务,那么:

  • 在之前,假设没有 FaaS,我们要将应用部署到托管平台 PaaS 上;启动 Web 服务时,主进程初始化连接 MongoDB,初始化完成后,持续监听服务器的 80 端口,直到监听端口的句柄关闭或主进程接收到终止信号;当 80 端口和客户端建立完 TCP 链接,有 HTTP 请求过来,服务器就会将请求转发给 Web 服务的主进程,这时主进程会创建一个子进程来处理这个请求。
  • 而在 FaaS 常驻进程型模式下,首先我们要改造一下代码,Node.js 的 Server 对象采用 FaaS Runtime 提供的 Server 对象;然后我们把监听端口改为监听 HTTP 事件;启动 Web 服务时,主进程初始化连接 MongoDB,初始化完成后,持续监听 HTTP 事件,直到被云服务商控制的父进程关闭回收。

    当 HTTP 事件发生时,我们的 Web 服务主进程跟之前一样,创建一个子进程来处理这个请求事件。主进程就如我们上图中绘制的那个蓝色的圆点,当 HTTP 事件发生时,它创建的子进程就是蓝色弧形箭头,当子进程处理完后就会被主进程回收。

通过上面的例子,可以看到:常驻进程型就是为了传统 MVC 架构部署上 FaaS 专门设计的(显得很不自然,FaaS 原生的还是用完即毁型)。当然也可以使用用完即毁型来部署 MVC 架构的 Web 服务,但是不推荐这么做,因为用完即毁型对传统 MVC 改造的成本太大。

★从可控性和改造成本角度来看 Web 服务,服务端部署方案*适合的还是托管平台 PaaS 或者自己搭服务跑在 IaaS 上。正如我上一讲所说,使用 FaaS 就必须在 FaaS 的条件限制内使用,*佳的做法应该是一开始就选用 FaaS 开发。

用完即毁型适用的场景:数据编排和服务编排。

数据编排

目前*成功*广泛的设计模式就是 MVC 模式。但随着前端 MVVM 框架越来越火,前端 View 层逐渐前置,发展成 SPA 单页应用;后端 Control 和 Model 层逐渐下沉,发展成面向服务编程的后端应用。这种情况下,前后端更加彻底地解耦了,前端开发可以依赖 Mock 数据接口完全脱离后端限制,而后端的同学则可以面向数据接口开发,但这也产生了高网络 I/O 的数据网关层。

Node.js 的异步非阻塞和 JavaScript 天然亲近前端工程师的特性,自然地接过数据网关层。因此诞生了 Node.js 的 BFF 层 (Backend For Frontend),BFF 层充当了中间胶水层的角色,粘合前后端。将后端数据和后端接口编排,适配成前端需要的数据结构,提供给前端使用。

★未经加工的数据,我们称为元数据 Raw Data,对于普通用户来说元数据几乎不可读。所以我们需要将有用的数据组合起来,并且加工数据,让数据具备价值。对于数据的组合和加工,我们称之为数据编排。

%title插图%num

BFF 层通常是由善于处理高网络 I/O 的 Node.js 应用负责。传统的服务端运维 Node.js 应用还是比较重的,需要我们购买虚拟机,或者使用应用托管 PaaS 平台。但是,由于 BFF 层只是做无状态的数据编排,所以我们完全可以用 FaaS 用完即毁型模型替换掉 BFF 层的 Node.js 应用,也就是*近圈子里老说的 SFF(Serverless For Frontend)。

现在我们再串下新的请求链路逻辑。前端的一个数据请求过来,函数触发器触发我们的函数服务;我们的函数启动后,调用后端提供的元数据接口,并将返回的元数据加工成前端需要的数据格式;我们的 FaaS 函数完全就可以休息了。

%title插图%num

服务编排

服务编排和数据编排很像,主要区别是服务编排是对云服务商提供的各种服务进行组合和加工(也就是说服务商提供了一些 API ,我们对这些 API 进行整合来实现我们想要的功能)。

在 FaaS 出现之前,就有服务编排的概念,但服务编排受限于服务支持的 SDK 语言版本。我们要使用这些服务或 API,都要通过自己熟悉的编程语言去找对应的 SDK,在自己的代码中加载 SDK,使用秘钥调用 SDK 方法进行编排。如果没有 SDK,则需要自己根据平台提供的接口或协议实现 SDK。

但是,有了 FaaS 之后,我们就方便很多了。假如我们服务商没有给我们提供我们熟悉的语言的 SDK,那么我们可以使用其他语言编写一个编排的程序,这个编排的程序会对服务商的服务进行编排。之后,我们再去调用这个编排的程序即可,而这个编排的程序就可以使用用完即毁的方式。比如,我们的 Web 服务需要发送验证码邮件。我们查看阿里云的邮件服务文档,发现阿里云只提供了 Java、PHP 和 Python 的 SDK,而没有 Node.js 的 SDK。这个时候,我们可以参考邮件服务的 PHP 文档,就用 PHP 的 SDK 创建一个 FaaS 服务来发送邮件(发送邮件的功能是很单一的)。

这个也是 FaaS 的一个亮点:语言无关性。它意味着你的团队不再局限于单一的开发语言了,你们可以利用 Java、PHP、Python、Node.js 各自的语言优势,混合开发出复杂的应用。FaaS 服务编排被云服务商特别关注正是因为它具备的这种开放性。使用 FaaS 可以创造出各种各样复杂的服务编排场景,而且还与语言无关,这大大增加了云服务商各种服务的使用场景。当然,这对开发者也提出了要求,它要求开发者去更多地了解云服务商提供的各种服务。

Serverless 在 SaaS 领域的*佳实践

随着互联网人口红利逐渐减弱,基于流量的增长已经放缓,互联网行业迫切需要找到一片足以承载自身持续增长的新蓝海,产业互联网正是这一宏大背景下的新趋势。我们看到互联网浪潮正在席卷传统行业,云计算、大数据、人工智能开始大规模融入到金融、制造、物流、零售、文娱、教育、医疗等行业的生产环节中,这种融合称为产业互联网。

a而在产业互联网中,有一块不可小觑的领域是 SaaS 领域,它是 ToB 赛道的中间力量,比如 CRM、HRM、费控系统、财务系统、协同办公等等。

%title插图%num

SaaS 系统面临的挑战

在消费互联网时代,大家是搜索想要的东西,各个厂商在云计算、大数据、人工智能等技术基座之上建立流量*大化的服务与生态,基于海量内容分发与流量共享为逻辑构建系统。而到了产业互联网时代,供给关系发生了变化,大家是定制想要的东西,需要从供给与需求两侧出发进行双向建设,这个时候系统的灵活性和扩展性面临着前所未有的挑战,尤其是 ToB 的 SaaS 领域。

%title插图%num

特别对于当下的经济环境,SaaS 厂商要明白,不能再通过烧钱的方式,只关注在自己的用户数量上,而更多的要思考如何帮助客户降低成本、增加效率,所以需要将更多的精力放在自己产品的定制化能力上。

%title插图%num

%title插图%num

如何应对挑战

SaaS 领域中的佼佼者 Salesforce,将 CRM 的概念扩展到 Marketing、Sales、Service,而这三块领域中只有 Sales 有专门的 SaaS 产品,其他两个领域都是各个 ISV 在不同行业的行业解决方案,靠的是什么?毋庸置疑,是 Salesforce 强大的 aPaaS 平台。ISV、内部实施、客户均可以在各自维度通过 aPaaS 平台构建自己行业、自己领域的 SaaS 系统,建立完整的生态。所以在我看来,现在的 Salesforce 已经由一家 SaaS 公司升华为一家 aPaaS 平台公司了。这种演进的过程也印证了消费互联网和产业互联网的转换逻辑以及后者的核心诉求。

然而不是所有 SaaS 公司都有财力和时间去孵化和打磨自己的 aPaaS 平台,但市场的变化、用户的诉求是实实在在存在的。若要生存,就要求变。这个变的核心就是能够让自己目前的 SaaS 系统变得灵活起来,相对建设困难的 aPaaS 平台,我们其实可以选择轻量且有效的 Serverless 方案来提升现有系统的灵活性和可扩展性,从而实现用户不同的定制需求。

%title插图%num

Serverless工作流

Serverless 工作流是一个用来协调多个分布式任务执行的全托管云服务。在 Serverless工作流中,可以用顺序、分支、并行等方式来编排分布式任务,Serverless 工作流会按照设定好的步骤可靠地协调任务执行,跟踪每个任务的状态转换,并在必要时执行您定义的重试逻辑,以确保工作流顺利完成。Serverless 工作流通过提供日志记录和审计来监视工作流的执行,可以轻松地诊断和调试应用。

下面这张图描述了 Serverless 工作流如何协调分布式任务,这些任务可以是函数、已集成云服务API、运行在虚拟机或容器上的程序。

%title插图%num

看完 Serverless 工作流的介绍,大家可能已经多少有点思路了吧。系统灵活性和可扩展性的核心是服务可编排,无论是以前的BPM还是现在的 aPaaS。所以基于 Serverless 工作流重构SaaS系统灵活性方案的核心思路,是将系统内用户*希望定制的功能进行梳理、拆分、抽离,再配合函数计算(FC)提供无状态的能力,通过 Serverless 工作流进行这些功能点的编排,从而实现不同的业务流程。

%title插图%num

通过函数计算 FC 和 Serverless 工作流搭建灵活的订餐模块

订餐场景相信大家都不会陌生,在家叫外卖或者在餐馆点餐,都涉及到这个场景。当下也有很多提供点餐系统的 SaaS 服务厂商,有很多不错的 SaaS 点餐系统。

随着消费互联网向产业互联网转换,这些 SaaS 点餐系统面临的定制化的需求也越来越多,其中有一个需求是不同的商家在支付时会显示不同的支付方式,比如从A商家点餐后付款时显示支付宝、微信支付、银联支付,从B商家点餐后付款时显示支付宝、京东支付。突然美团又冒出来了美团支付,此时B商家接了美团支付,那么从B商家点餐后付款时显示支付宝、京东支付、美团支付。

诸如此类的定制化需求越来越多,这些 SaaS 产品如果没有 PaaS 平台,那么就会疲于不断的通过硬代码增加条件判断来实现不同商家的需求,这显然不是一个可持续发展的模式。

那么我们来看看通过函数计算 FC 和 Serverless 工作流如何优雅的解决这个问题。先来看看这个点餐流程:

%title插图%num

通过Serverless工作流创建流程

首选我需要将上面用户侧的流程转变为程序侧的流程,此时就需要使用 Serverless 工作流来担任此任务了。

打开 Serverless 控制台,创建订餐流程,这里 Serverless 工作流使用流程定义语言 FDL 创建工作流,如何使用FDL创建工作流请参阅文档。流程图如下图所示:

%title插图%num

FDL 代码为:

  1. 1version: v1beta1
  2. 2type: flow
  3. 3timeoutSeconds: 3600
  4. 4steps:
  5. 5  – type: task
  6. 6    name: generateInfo
  7. 7    timeoutSeconds: 300
  8. 8    resourceArn: acs:mns:::/topics/generateInfo-fnf-demo-jiyuan/messages
  9. 9    pattern: waitForCallback
  10. 10    inputMappings:
  11. 11      – target: taskToken
  12. 12        source: $context.task.token
  13. 13      – target: products
  14. 14        source: $input.products
  15. 15      – target: supplier
  16. 16        source: $input.supplier
  17. 17      – target: address
  18. 18        source: $input.address
  19. 19      – target: orderNum
  20. 20        source: $input.orderNum
  21. 21      – target: type
  22. 22        source: $context.step.name
  23. 23    outputMappings:
  24. 24      – target: paymentcombination
  25. 25        source: $local.paymentcombination
  26. 26      – target: orderNum
  27. 27        source: $local.orderNum
  28. 28    serviceParams:
  29. 29      MessageBody: $
  30. 30      Priority: 1
  31. 31    catch:
  32. 32      – errors:
  33. 33          – FnF.TaskTimeout
  34. 34        goto: orderCanceled
  35. 35  -type: task
  36. 36    name: payment
  37. 37    timeoutSeconds: 300
  38. 38    resourceArn: acs:mns:::/topics/payment-fnf-demo-jiyuan/messages
  39. 39    pattern: waitForCallback
  40. 40    inputMappings:
  41. 41      – target: taskToken
  42. 42        source: $context.task.token
  43. 43      – target: orderNum
  44. 44        source: $local.orderNum
  45. 45      – target: paymentcombination
  46. 46        source: $local.paymentcombination
  47. 47      – target: type
  48. 48        source: $context.step.name
  49. 49    outputMappings:
  50. 50      – target: paymentMethod
  51. 51        source: $local.paymentMethod
  52. 52      – target: orderNum
  53. 53        source: $local.orderNum
  54. 54      – target: price
  55. 55        source: $local.price
  56. 56      – target: taskToken
  57. 57        source: $input.taskToken
  58. 58    serviceParams:
  59. 59      MessageBody: $
  60. 60      Priority: 1
  61. 61    catch:
  62. 62      – errors:
  63. 63          – FnF.TaskTimeout
  64. 64        goto: orderCanceled
  65. 65  – type: choice
  66. 66    name: paymentCombination
  67. 67    inputMappings:
  68. 68      – target: orderNum
  69. 69        source: $local.orderNum
  70. 70      – target: paymentMethod
  71. 71        source: $local.paymentMethod
  72. 72      – target: price
  73. 73        source: $local.price
  74. 74      – target: taskToken
  75. 75        source: $local.taskToken
  76. 76    choices:
  77. 77      – condition: $.paymentMethod == “zhifubao”
  78. 78        steps:
  79. 79          – type: task
  80. 80            name: zhifubao
  81. 81            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
  82. 82            inputMappings:
  83. 83              – target: price
  84. 84                source: $input.price
  85. 85              – target: orderNum
  86. 86                source: $input.orderNum
  87. 87              – target: paymentMethod
  88. 88                source: $input.paymentMethod
  89. 89              – target: taskToken
  90. 90                source: $input.taskToken
  91. 91      – condition: $.paymentMethod == “weixin”
  92. 92        steps:
  93. 93          – type: task
  94. 94            name: weixin
  95. 95            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/weixin-fnf-demo
  96. 96            inputMappings:
  97. 97            – target: price
  98. 98              source: $input.price
  99. 99            – target: orderNum
  100. 100              source: $input.orderNum
  101. 101            – target: paymentMethod
  102. 102              source: $input.paymentMethod
  103. 103            – target: taskToken
  104. 104              source: $input.taskToken
  105. 105      – condition: $.paymentMethod == “unionpay”
  106. 106        steps:
  107. 107          – type: task
  108. 108            name: unionpay
  109. 109            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/union-fnf-demo
  110. 110            inputMappings:
  111. 111            – target: price
  112. 112              source: $input.price
  113. 113            – target: orderNum
  114. 114              source: $input.orderNum
  115. 115            – target: paymentMethod
  116. 116              source: $input.paymentMethod
  117. 117            – target: taskToken
  118. 118              source: $input.taskToken
  119. 119    default:
  120. 120      goto: orderCanceled
  121. 121  – type: task
  122. 122    name: orderCompleted
  123. 123    resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/orderCompleted
  124. 124    end: true
  125. 125  – type: task
  126. 126    name: orderCanceled
  127. 127    resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/cancerOrde

在解析整个流程之前,我先要说明的一点是,我们不是完全通过 Serverless 函数计算和 Serverless 工作流来搭建订餐模块,只是用它来解决灵活性的问题,所以这个示例的主体应用是Java编写的,然后结合了 Serverless 函数计算和 Serverless 工作流。下面我们来详细解析这个流程。

启动流程

按常理,开始点餐时流程就应该启动了,所以在这个示例中,我的设计是当我们选择完商品和商家、填完地址后启动流程:

%title插图%num

这里我们通过 Serverless 工作流提供的 OpenAPI 来启动流程。

%title插图%num

  • Java 启动流程

这个示例我使用 Serverless 工作流的 Java SDK,首先在 POM 文件中添加依赖:

  1. 1<dependency>
  2. 2<groupId>com.aliyun</groupId>
  3. 3<artifactId>aliyun-java-sdk-core</artifactId>
  4. 4<version>[4.3.2,5.0.0)</version>
  5. 5</dependency>
  6. 6<dependency>
  7. 7<groupId>com.aliyun</groupId>
  8. 8<artifactId>aliyun-java-sdk-fnf</artifactId>
  9. 9<version>[1.0.0,5.0.0)</version>
  10. 10</dependency>

然后创建初始化 Java SDK 的 Config 类:

  1. 1@Configuration
  2. 2public class FNFConfig {
  3. 3
  4. 4@Bean
  5. 5public IAcsClient createDefaultAcsClient(){
  6. 6        DefaultProfile profile = DefaultProfile.getProfile(
  7. 7“cn-xxx”,          // 地域ID
  8. 8“ak”,      // RAM 账号的AccessKey ID
  9. 9“sk”); // RAM 账号Access Key Secret
  10. 10        IAcsClient client = new DefaultAcsClient(profile);
  11. 11return client;
  12. 12    }
  13. 13
  14. 14}

再来看 Controller 中的 startFNF 方法,该方法暴露 GET 方式的接口,传入三个参数:

1、fnfname:要启动的流程名称。

2、execuname:流程启动后的流程实例名称。

3、input:启动输入参数,比如业务参数。

  1. 1@GetMapping(“/startFNF/{fnfname}/{execuname}/{input}”)
  2. 2public StartExecutionResponse startFNF(@PathVariable(“fnfname”String fnfName,
  3. 3@PathVariable(“execuname”String execuName,
  4. 4@PathVariable(“input”String inputStr) throws ClientException {
  5. 5        JSONObject jsonObject = new JSONObject();
  6. 6        jsonObject.put(“fnfname”, fnfName);
  7. 7        jsonObject.put(“execuname”, execuName);
  8. 8        jsonObject.put(“input”, inputStr);
  9. 9return fnfService.startFNF(jsonObject);
  10. 10    }}

再来看 Service 中的 startFNF 方法,该方法分两部分,*个部分是启动流程,第二部分是创建订单对象,并模拟入库(示例中是放在 Map 里了):

  1. 1 @Override
  2. 2public StartExecutionResponse startFNF(JSONObject jsonObject) throws ClientException {
  3. 3        StartExecutionRequest request = new StartExecutionRequest();
  4. 4        String orderNum = jsonObject.getString(“execuname”);
  5. 5        request.setFlowName(jsonObject.getString(“fnfname”));
  6. 6        request.setExecutionName(orderNum);
  7. 7        request.setInput(jsonObject.getString(“input”));
  8. 8
  9. 9        JSONObject inputObj = jsonObject.getJSONObject(“input”);
  10. 10        Order order = new Order();
  11. 11        order.setOrderNum(orderNum);
  12. 12        order.setAddress(inputObj.getString(“address”));
  13. 13        order.setProducts(inputObj.getString(“products”));
  14. 14        order.setSupplier(inputObj.getString(“supplier”));
  15. 15        orderMap.put(orderNum, order);
  16. 16
  17. 17return iAcsClient.getAcsResponse(request);
  18. 18    }

启动流程时,流程名称和启动流程实例的名称是需要传入的参数,这里我将每次的订单编号作为启动流程的实例名称。至于 Input,可以根据需求构造 JSON 字符串传入。这里我将商品、商家、地址、订单号构造了 JSON 字符串在流程启动时传入流程中。

另外,创建了此次订单的 Order 实例,并存在 Map 中,模拟入库,后续环节还会查询该订单实例更新订单属性。

  • VUE 选择商品/商家页面

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

1、fnfname:要启动的流程名称。

2、execuname:随机生成 uuid,作为订单的编号,也作为启动流程实例的名称。

3、input:将商品、商家、订单号、地址构建为 JSON 字符串传入流程。

  1. 1            submitOrder(){
  2. 2const orderNum = uuid.v1()
  3. 3this.$axios.$get(‘/startFNF/OrderDemo-Jiyuan/’+orderNum+‘/{\n’ +
  4. 4‘  “products”: “‘+this.products+‘”,\n’ +
  5. 5‘  “supplier”: “‘+this.supplier+‘”,\n’ +
  6. 6‘  “orderNum”: “‘+orderNum+‘”,\n’ +
  7. 7‘  “address”: “‘+this.address+‘”\n’ +
  8. 8‘}’ ).then((response) => {
  9. 9                    console.log(response)
  10. 10if(response.message == “success”){
  11. 11this.$router.push(‘/orderdemo/’ + orderNum)
  12. 12                    }
  13. 13                })
  14. 14            }

generateInfo 节点

*个节点 generateInfo,先来看看 FDL 的含义:

  1. 1 – type: task
  2. 2name: generateInfo
  3. 3timeoutSeconds: 300
  4. 4resourceArn: acs:mns:::/topics/generateInfo-fnf-demo-jiyuan/messages
  5. 5pattern: waitForCallback
  6. 6inputMappings:
  7. 7      – target: taskToken
  8. 8source: $context.task.token
  9. 9      – target: products
  10. 10source: $input.products
  11. 11      – target: supplier
  12. 12source: $input.supplier
  13. 13      – target: address
  14. 14source: $input.address
  15. 15      – target: orderNum
  16. 16source: $input.orderNum
  17. 17      – target: type
  18. 18source: $context.step.name
  19. 19outputMappings:
  20. 20      – target: paymentcombination
  21. 21source: $local.paymentcombination
  22. 22      – target: orderNum
  23. 23source: $local.orderNum
  24. 24serviceParams:
  25. 25MessageBody: $
  26. 26Priority: 1
  27. 27catch:
  28. 28      – errors:
  29. 29          – FnF.TaskTimeout
  30. 30goto: orderCanceled

1、name:节点名称。

2、timeoutSeconds:超时时间。该节点等待的时长,超过时间后会跳转到 goto 分支指向的 orderCanceled 节点。

3、pattern:设置为 waitForCallback,表示需要等待确认。inputMappings:该节点入参。

  • taskToken:Serverless 工作流自动生成的 Token。
  • products:选择的商品。
  • supplier:选择的商家。
  • address:送餐地址。
  • orderNum:订单号。

4、outputMappings:该节点的出参。

  • paymentcombination:该商家支持的支付方式。
  • orderNum:订单号。

5、catch:捕获异常,跳转到其他分支。

这里 resourceArn 和 serviceParams 需要拿出来单独解释。Serverless 工作流支持与多个云服务集成,即将其他服务作为任务步骤的执行单元。服务集成方式由 FDL 语言表达,在任务步骤中,可以使用 resourceArn 来定义集成的目标服务,使用 pattern 定义集成模式。所以可以看到在 resourceArn 中配置 acs:mns:::/topics/generateInfo-fnf-demo-jiyuan/messages 信息,即在 generateInfo 节点中集成了 MNS 消息队列服务,当 generateInfo 节点触发后会向 generateInfo-fnf-demo-jiyuanTopic 中发送一条消息。那么消息正文和参数则在 serviceParams 对象中指定。MessageBody 是消息正文,配置$表示通过输入映射 inputMappings 产生消息正文。

看完*个节点的示例,大家可以看到,在 Serverless 工作流中,节点之间的信息传递可以通过集成 MNS 发送消息来传递,也是使用比较广泛的方式之一。

generateInfo-fnf-demo 函数

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

%title插图%num

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

%title插图%num

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

%title插图%num

做好函数的准备工作,我们来开始写代码:

  1. 1# -*- coding: utf-8 -*-
  2. 2import logging
  3. 3import json
  4. 4import time
  5. 5import requests
  6. 6from aliyunsdkcore.client import AcsClient
  7. 7from aliyunsdkcore.acs_exception.exceptions import ServerException
  8. 8from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
  9. 9from aliyunsdkfnf.request.v20190315 import ReportTaskFailedRequest
  10. 10
  11. 11
  12. 12def handler(event, context):
  13. 13# 1. 构建Serverless工作流Client
  14. 14    region = “cn-hangzhou”
  15. 15    account_id = “XXXX”
  16. 16    ak_id = “XXX”
  17. 17    ak_secret = “XXX”
  18. 18    fnf_client = AcsClient(
  19. 19        ak_id,
  20. 20        ak_secret,
  21. 21        region
  22. 22    )
  23. 23    logger = logging.getLogger()
  24. 24# 2. event内的信息即接受到Topic generateInfo-fnf-demo-jiyuan中的消息内容,将其转换为Json对象
  25. 25    bodyJson = json.loads(event)
  26. 26    logger.info(“products:” + bodyJson[“products”])
  27. 27    logger.info(“supplier:” + bodyJson[“supplier”])
  28. 28    logger.info(“address:” + bodyJson[“address”])
  29. 29    logger.info(“taskToken:” + bodyJson[“taskToken”])
  30. 30    supplier = bodyJson[“supplier”]
  31. 31    taskToken = bodyJson[“taskToken”]
  32. 32    orderNum = bodyJson[“orderNum”]
  33. 33# 3. 判断什么商家使用什么样的支付方式组合,这里的示例比较简单粗暴,正常情况下,应该使用元数据配置的方式获取
  34. 34    paymentcombination = “”
  35. 35if supplier == “haidilao”:
  36. 36        paymentcombination = “zhifubao,weixin”
  37. 37else:
  38. 38        paymentcombination = “zhifubao,weixin,unionpay”
  39. 39
  40. 40# 4. 调用Java服务暴露的接口,更新订单信息,主要是更新支付方式
  41. 41    url = “http://xx.xx.xx.xx:8080/setPaymentCombination/” + orderNum + “/” + paymentcombination + “/0”
  42. 42    x = requests.get(url)
  43. 43
  44. 44# 5. 给予generateInfo节点响应,并返回数据,这里返回了订单号和支付方式
  45. 45    output = “{\”orderNum\”: \”%s\”, \”paymentcombination\”:\”%s\” “ \
  46. 46“}” % (orderNum, paymentcombination)
  47. 47    request = ReportTaskSucceededRequest.ReportTaskSucceededRequest()
  48. 48    request.set_Output(output)
  49. 49    request.set_TaskToken(taskToken)
  50. 50    resp = fnf_client.do_action_with_exception(request)
  51. 51return ‘hello world’

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

整个代码分五部分:

1、构建 Serverless 工作流 Client。

2、event 内的信息即接受到 TopicgenerateInfo-fnf-demo-jiyuan 中的消息内容,将其转换为 Json 对象。

3、判断什么商家使用什么样的支付方式组合,这里的示例比较简单粗暴,正常情况下,应该使用元数据配置的方式获取。比如在系统内有商家信息的配置功能,通过在界面上配置该商家支持哪些支付方式,形成元数据配置信息,提供查询接口,在这里进行查询。

4、调用Java服务暴露的接口,更新订单信息,主要是更新支付方式。

5、给予 generateInfo 节点响应,并返回数据,这里返回了订单号和支付方式。因为该节点的 pattern 是 waitForCallback,所以需要等待响应结果。

payment节点

我们再来看第二个节点 payment,先来看 FDL 代码:

  1. 1– type: task
  2. 2name: payment
  3. 3timeoutSeconds: 300
  4. 4resourceArn: acs:mns:::/topics/payment-fnf-demo-jiyuan/messages
  5. 5pattern: waitForCallback
  6. 6inputMappings:
  7. 7      – target: taskToken
  8. 8source: $context.task.token
  9. 9      – target: orderNum
  10. 10source: $local.orderNum
  11. 11      – target: paymentcombination
  12. 12source: $local.paymentcombination
  13. 13      – target: type
  14. 14source: $context.step.name
  15. 15outputMappings:
  16. 16      – target: paymentMethod
  17. 17source: $local.paymentMethod
  18. 18      – target: orderNum
  19. 19source: $local.orderNum
  20. 20      – target: price
  21. 21source: $local.price
  22. 22      – target: taskToken
  23. 23source: $input.taskToken
  24. 24serviceParams:
  25. 25MessageBody: $
  26. 26Priority: 1
  27. 27catch:
  28. 28      – errors:
  29. 29          – FnF.TaskTimeout
  30. 30goto: orderCanceled

当流程流转到 payment 节点后,意味着用户进入了支付页面。

%title插图%num

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

payment-fnf-demo函数

payment-fnf-demo 函数的创建方式和 generateInfo-fnf-demo 函数类似,这里不再累赘。我们直接来看代码:

  1. 1# -*- coding: utf-8 -*-
  2. 2import logging
  3. 3import json
  4. 4import os
  5. 5import time
  6. 6import logging
  7. 7from aliyunsdkcore.client import AcsClient
  8. 8from aliyunsdkcore.acs_exception.exceptions import ServerException
  9. 9from aliyunsdkcore.client import AcsClient
  10. 10from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
  11. 11from aliyunsdkfnf.request.v20190315 import ReportTaskFailedRequest
  12. 12from mns.account import Account  # pip install aliyun-mns
  13. 13from mns.queue import *
  14. 14
  15. 15
  16. 16def handler(event, context):
  17. 17    logger = logging.getLogger()
  18. 18    region = “xxx”
  19. 19    account_id = “xxx”
  20. 20    ak_id = “xxx”
  21. 21    ak_secret = “xxx”
  22. 22    mns_endpoint = “http://your_account_id.mns.cn-hangzhou.aliyuncs.com/”
  23. 23    queue_name = “payment-queue-fnf-demo”
  24. 24    my_account = Account(mns_endpoint, ak_id, ak_secret)
  25. 25    my_queue = my_account.get_queue(queue_name)
  26. 26# my_queue.set_encoding(False)
  27. 27    fnf_client = AcsClient(
  28. 28        ak_id,
  29. 29        ak_secret,
  30. 30        region
  31. 31    )
  32. 32    eventJson = json.loads(event)
  33. 33
  34. 34    isLoop = True
  35. 35while isLoop:
  36. 36try:
  37. 37            recv_msg = my_queue.receive_message(30)
  38. 38            isLoop = False
  39. 39# body = json.loads(recv_msg.message_body)
  40. 40            logger.info(“recv_msg.message_body:======================” + recv_msg.message_body)
  41. 41            msgJson = json.loads(recv_msg.message_body)
  42. 42            my_queue.delete_message(recv_msg.receipt_handle)
  43. 43# orderCode = int(time.time())
  44. 44            task_token = eventJson[“taskToken”]
  45. 45            orderNum = eventJson[“orderNum”]
  46. 46            output = “{\”orderNum\”: \”%s\”, \”paymentMethod\”: \”%s\”, \”price\”: \”%s\” “ \
  47. 47“}” % (orderNum, msgJson[“paymentMethod”], msgJson[“price”])
  48. 48            request = ReportTaskSucceededRequest.ReportTaskSucceededRequest()
  49. 49            request.set_Output(output)
  50. 50            request.set_TaskToken(task_token)
  51. 51            resp = fnf_client.do_action_with_exception(request)
  52. 52except Exception as e:
  53. 53            logger.info(“new loop”)
  54. 54return ‘hello world’

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

VUE选择支付方式页面

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

%title插图%num

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

%title插图%num

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

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

  1. 1# -*- coding: utf-8 -*-
  2. 2
  3. 3import logging
  4. 4import urllib.parse
  5. 5import json
  6. 6from mns.account import Account  # pip install aliyun-mns
  7. 7from mns.queue import *
  8. 8HELLO_WORLD = b’Hello world!\n’
  9. 9
  10. 10def handler(environ, start_response):
  11. 11    logger = logging.getLogger()
  12. 12    context = environ[‘fc.context’]
  13. 13    request_uri = environ[‘fc.request_uri’]
  14. 14for k, v in environ.items():
  15. 15if k.startswith(‘HTTP_’):
  16. 16# process custom request headers
  17. 17pass
  18. 18try:
  19. 19        request_body_size = int(environ.get(‘CONTENT_LENGTH’0))
  20. 20except (ValueError):
  21. 21        request_body_size = 0
  22. 22    request_body = environ[‘wsgi.input’].read(request_body_size)
  23. 23    paymentMethod = urllib.parse.unquote(request_body.decode(“GBK”))
  24. 24    logger.info(paymentMethod)
  25. 25    paymentMethodJson = json.loads(paymentMethod)
  26. 26
  27. 27    region = “cn-xxx”
  28. 28    account_id = “xxx”
  29. 29    ak_id = “xxx”
  30. 30    ak_secret = “xxx”
  31. 31    mns_endpoint = “http://your_account_id.mns.cn-hangzhou.aliyuncs.com/”
  32. 32    queue_name = “payment-queue-fnf-demo”
  33. 33    my_account = Account(mns_endpoint, ak_id, ak_secret)
  34. 34    my_queue = my_account.get_queue(queue_name)
  35. 35    output = “{\”paymentMethod\”: \”%s\”, \”price\”:\”%s\” “ \
  36. 36“}” % (paymentMethodJson[“paymentMethod”], paymentMethodJson[“price”])
  37. 37    msg = Message(output)
  38. 38    my_queue.send_message(msg)
  39. 39
  40. 40    status = ‘200 OK’
  41. 41    response_headers = [(‘Content-type’‘text/plain’)]
  42. 42    start_response(status, response_headers)
  43. 43return [HELLO_WORLD]

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

VUE代码片段如下:

%title插图%num

paymentCombination 节点

paymentCombination 节点是一个路由节点,通过判断某个参数路由到不同的节点,这里自然使用 paymentMethod 作为判断条件。FDL 代码如下:

  1. 1– type: choice
  2. 2    name: paymentCombination
  3. 3    inputMappings:
  4. 4      – target: orderNum
  5. 5source: $local.orderNum
  6. 6      – target: paymentMethod
  7. 7source: $local.paymentMethod
  8. 8      – target: price
  9. 9source: $local.price
  10. 10      – target: taskToken
  11. 11source: $local.taskToken
  12. 12    choices:
  13. 13      – condition: $.paymentMethod == “zhifubao”
  14. 14        steps:
  15. 15          – type: task
  16. 16            name: zhifubao
  17. 17            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
  18. 18            inputMappings:
  19. 19              – target: price
  20. 20source: $input.price
  21. 21              – target: orderNum
  22. 22source: $input.orderNum
  23. 23              – target: paymentMethod
  24. 24source: $input.paymentMethod
  25. 25              – target: taskToken
  26. 26source: $input.taskToken
  27. 27      – condition: $.paymentMethod == “weixin”
  28. 28        steps:
  29. 29          – type: task
  30. 30            name: weixin
  31. 31            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/weixin-fnf-demo
  32. 32            inputMappings:
  33. 33            – target: price
  34. 34source: $input.price
  35. 35            – target: orderNum
  36. 36source: $input.orderNum
  37. 37            – target: paymentMethod
  38. 38source: $input.paymentMethod
  39. 39            – target: taskToken
  40. 40source: $input.taskToken
  41. 41      – condition: $.paymentMethod == “unionpay”
  42. 42        steps:
  43. 43          – type: task
  44. 44            name: unionpay
  45. 45            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan.LATEST/functions/union-fnf-demo
  46. 46            inputMappings:
  47. 47            – target: price
  48. 48source: $input.price
  49. 49            – target: orderNum
  50. 50source: $input.orderNum
  51. 51            – target: paymentMethod
  52. 52source: $input.paymentMethod
  53. 53            – target: taskToken
  54. 54source: $input.taskToken
  55. 55    default:
  56. 56      goto: orderCanceled

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

zhifubao节点

我们具体来看一个 zhifubao 节点:

  1. 1choices:
  2. 2      – condition: $.paymentMethod == “zhifubao”
  3. 3        steps:
  4. 4          – type: task
  5. 5            name: zhifubao
  6. 6            resourceArn: acs:fc:cn-hangzhou:your_account_id:services/FNFDemo-jiyuan/functions/zhifubao-fnf-demo
  7. 7            inputMappings:
  8. 8              – target: price
  9. 9source: $input.price
  10. 10              – target: orderNum
  11. 11source: $input.orderNum
  12. 12              – target: paymentMethod
  13. 13source: $input.paymentMethod
  14. 14              – target: taskToken
  15. 15source: $input.taskToken

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

zhifubao-fnf-demo函数

现在我们来看zhifubao-fnf-demo函数的代码:

  1. 1# -*- coding: utf-8 -*-
  2. 2import logging
  3. 3import json
  4. 4import requests
  5. 5import urllib.parse
  6. 6from aliyunsdkcore.client import AcsClient
  7. 7from aliyunsdkcore.acs_exception.exceptions import ServerException
  8. 8from aliyunsdkfnf.request.v20190315 import ReportTaskSucceededRequest
  9. 9from aliyunsdkfnf.request.v20190315 import ReportTaskFailedRequest
  10. 10
  11. 11
  12. 12def handler(event, context):
  13. 13  region = “cn-xxx”
  14. 14  account_id = “xxx”
  15. 15  ak_id = “xxx”
  16. 16  ak_secret = “xxx”
  17. 17  fnf_client = AcsClient(
  18. 18    ak_id,
  19. 19    ak_secret,
  20. 20    region
  21. 21  )
  22. 22  logger = logging.getLogger()
  23. 23  logger.info(event)
  24. 24  bodyJson = json.loads(event)
  25. 25  price = bodyJson[“price”]
  26. 26  taskToken = bodyJson[“taskToken”]
  27. 27  orderNum = bodyJson[“orderNum”]
  28. 28  paymentMethod = bodyJson[“paymentMethod”]
  29. 29  logger.info(“price:” + price)
  30. 30  newPrice = int(price) * 0.8
  31. 31  logger.info(“newPrice:” + str(newPrice))
  32. 32  url = “http://xx.xx.xx.xx:8080/setPaymentCombination/” + orderNum + “/” + paymentMethod + “/” + str(newPrice)
  33. 33  x = requests.get(url)
  34. 34
  35. 35return {“Status”:“ok”}

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

完整流程

流程中的 orderCompleted 和 orderCanceled 节点没做什么逻辑,大家可以自行发挥,思路和之前的节点一样。所以完整的流程是这样:

%title插图%num

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

%title插图%num

%title插图%num

总结

到此,我们基于 Serverless 工作流和 Serverless 函数计算构建的订单模块示例就算完成了,在示例中,有两个点需要大家注意:

1. 配置商家和支付方式的元数据规则。

2. 确认支付页面的元数据规则。

因为在实际生产中,我们需要将可定制的部分都抽象为元数据描述,需要有配置界面制定商家的支付方式即更新元数据规则,然后前端页面基于元数据信息展示相应的内容。

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

关于 Serverless 函数计算的字体安装

前言

首先介绍下在本文出现的几个比较重要的概念:

函数计算(Function Compute):函数计算是一个事件驱动的服务,通过函数计算,用户无需管理服务器等运行情况,只需编写代码并上传。函数计算准备计算资源,并以弹性伸缩的方式运行用户代码,而用户只需根据实际代码运行所消耗的资源进行付费。函数计算更多信息 参考%title插图%num

Fun:Fun 是一个用于支持 Serverless 应用部署的工具,能帮助您便捷地管理函数计算、API 网关、日志服务等资源。它通过一个资源配置文件(template.yml),协助您进行开发、构建、部署操作。Fun 的更多文档 参考%title插图%num

备注: 本文介绍的技巧需要 Fun 版本大于等于 3.6.7。

函数计算运行环境中内置一些常用字体,但仍不满足部分用户的需求。如果应用中需要使用其它字体,需要走很多弯路。本文将介绍如何通过 ‍Fun 工具将自定义字体部署到函数计算,并正确的在应用中被引用。%title插图%num

1. 你需要做什么

  • 在代码(CodeUri)目录新建一个 fonts 目录;
  • 将字体复制到 fonts 目录;
  • 使用 fun deploy 进行部署。

2. 工具安装

建议直接从这里下载二进制可执行程序,解压后即可直接使用。‍下载地址

执行 fun –version 检查 Fun 是否安装成功。

  1. 1$ fun –version
  2. 23.6.7

3. 示例

demo 涉及的代码,托管在 ‍github 上。项目目录结构如下:

  1. 1$ tree -L -a 1
  2. 2
  3. 3├── index.js
  4. 4├── package.json
  5. 5└── template.yml

index.js 中代码:

  1. 1‘use strict’;
  2. 2
  3. 3var fontList = require(‘font-list’)
  4. 4
  5. 5module.exports.handler = async function (request, response, context{
  6. 6    response.setStatusCode(200);
  7. 7    response.setHeader(‘content-type’‘application/json’);
  8. 8    response.send(JSON.stringify(await fontList.getFonts(), null4));
  9. 9};

index.js 中借助 node 包 ‍font-list 列出系统上可用的字体。

template.yml:

  1. 1ROSTemplateFormatVersion: ‘2015-09-01’
  2. 2Transform: ‘Aliyun::Serverless-2018-04-03’
  3. 3Resources:
  4. 4  fonts-service: # 服务名
  5. 5    Type: ‘Aliyun::Serverless::Service’
  6. 6    Properties:
  7. 7      Description: fonts example
  8. 8    fonts-function: # 函数名
  9. 9      Type: ‘Aliyun::Serverless::Function
  10. 10      Properties:
  11. 11        Handlerindex.handler
  12. 12        Runtimenodejs8
  13. 13        CodeUri: ./
  14. 14        InstanceConcurrency: 10
  15. 15      Events:
  16. 16        httptest:
  17. 17          TypeHTTP
  18. 18          Properties:
  19. 19            AuthTypeANONYMOUS
  20. 20            Methods:
  21. 21              – GET
  22. 22              – POST
  23. 23              – PUT
  24. 24
  25. 25  tmp_domain: # 临时域名
  26. 26    Type: ‘Aliyun::Serverless::CustomDomain
  27. 27    Properties:
  28. 28      DomainNameAuto
  29. 29      ProtocolHTTP
  30. 30      RouteConfig:
  31. 31        Routes:
  32. 32          /:
  33. 33            ServiceNamefontsservice
  34. 34            FunctionNamefontsfunction
  1. template.yml 中定义了名为 fonts-service 的服务,此服务下定义一个名为 fonts-functionhttp trigger 函数。tmp_domain 中配置自定义域名中路径(/)与函数(fontsservice/fontsfunction)的映射关系。

1)下载字体

你可以通过 ‍这里 下载自定义字体 Hack,然后复制字体到 fonts 目录。

此时 demo 目录结构如下:

  1. 1$ tree -L 2 -a
  2. 2
  3. 3├── fonts(+)
  4. 4│   ├── Hack-Bold.ttf
  5. 5│   ├── Hack-BoldItalic.ttf
  6. 6│   ├── Hack-Italic.ttf
  7. 7│   └── Hack-Regular.ttf
  8. 8├── index.js
  9. 9├── package.json
  10. 10└── template.yml
  11. 2)安装依赖
  1. 1$ npm install
  2. 3)部署到函数计算

可以通过 fun deploy 直接发布到远端。

%title插图%num

4)预览线上效果

fun deploy 部署过程中,会为此函数生成有时效性的临时域名:

%title插图%num

打开浏览器,输入临时域名并回车:

%title插图%num

可以看到字体 Hack 已生效!!!

%title插图%num

原理介绍

  • fun deploy 时,如果检测到 CodeUri 下面有 fonts 目录,则为用户在 CodeUri 目录生成一个 .fonts.conf 配置文件。在该配置中,相比于原来的 /etc/fonts/fonts.conf 配置,添加了 /code/fonts 作为字体目录。
  • 自动在 template.yml 中添加环境变量,FONTCONFIG_FILE = /code/.fonts.conf,这样在函数运行时就可以正确的读取到自定义字体目录。

如果依赖过大,超过函数计算的限制(50M)则:

  • 将 fonts 目录添加到 .nas.yml;
  • 将 fonts 对 nas 的映射目录追加到 .fonts.conf 配置。

fun deploy 对大依赖的支持可参考 ‍《开发函数计算的正确姿势——轻松解决大依赖部署》

%title插图%num

小结

只需要在代码(CodeUri)目录新建一个 fonts 目录,然后复制所有字体到该目录即可。Fun 会自动帮你处理配置文件(.fonts.conf),环境变量以及大依赖场景的情况。

妥了!微服务治理的困难,用 Serverless 来解决

微服务治理面临的挑战

在业务初期,因人手有限,想要快速开发并上线产品,很多团队使用单体的架构来开发。但是随着公司的发展,会不断往系统里面添加新的业务功能,系统越来越庞大,需求不断增加,越来越多的人也会加入到开发团队,代码库也会增速的膨胀,慢慢的单体应用变得越来越臃肿,可维护性和灵活性逐渐降低,维护成本越来越高。

%title插图%num

这个时候很多团队会把单体应用架构改为微服务的架构,解决单体应用的问题。但随着微服务越来越多,运维投入会越来越大,需要保证几十甚至几百个服务正常运行与协作,这给运维带来了很大的挑战,下面从软件生命周期的角度来分析这些挑战:

  • 开发测试态
    • 如何实现开发、测试、线上环境隔离?
    • 如何快速调试本地变更?
    • 如何快速部署本地变更?
  • 发布态
    • 如何设计服务发布策略?
    • 如何无损下线旧版本服务?
    • 如何实现对新版本服务灰 度测试?
  • 运行态
    • 线上问题如何排查?有什么工具可以利用呢?
    • 对于服务质量差的节点如何处理?
    • 对于完全不工作的实例我们如何恢复?

面对以上问题,Serverless 应用引擎在这方面都做了哪些工作?

%title插图%num

Serverless 应用引擎

%title插图%num

如上图所示,Serverless 应用引擎(SAE)基于神龙 + ECI + VPC + SLB + NAS 等 IaaS 资源,构建了一个 Kubernetes 集群,在此之上提供了应用管理和微服务治理的一些能力。它可以针对不同应用类型进行托管,比如 Spring Cloud 应用、Dubbo 应用、HSF 应用、Web 应用和多语言应用。并且支持 Cloudtoolkit 插件、云效 RDC / Jenkins 等开发者工具。在 Serverless 应用引擎上,零代码改造就可以把 Java 微服务的应用迁移到 Serverless。

总的来说,Serverless 应用引擎能够提供成本更优、效率更高的一站式应用托管方案,零门槛、零改造、零容器基础,即可享受 Serverless + K8s + 微服务带来的技术红利。

%title插图%num

微服务治理实践

1. 开发态实践

1)多环境管理

%title插图%num

  • 多租户共有一个注册中心,通过不同的租户对流量进行隔离;更进一步可以通过网络 VPC 进行环境隔离;
  • 提供环境级别的运维操作,比如一键停止和拉起整个环境的功能;
  • 提供环境级别的配置管理;
  • 提供环境级别的网关路由流量管理。

2)云端联调

Serverless 应用引擎(SAE)基于 Alibaba CloudToolkit 插件+ 跳板机可以实现:

  • 本地服务订阅并注册到云端 SAE 内置的注册中心;
  • 本地服务可以和云端 SAE 服务互相调用。

%title插图%num

如上图所示,在实现的时候用户需要有一个 ECS 代理服务器,实际注册的是 ECS 代理服务器到 SAE 的注册中心,IDEA 在安装 Cloudtoolkit 插件以后,在启动进程时,会在本地拉起一个通道服务,这个通道服务会连上 ECS 代理服务器,本地所有的请求都会转到 ECS 代理服务器上,云端对服务的调用也会通过 ECS 代理转到本地,这样就可以以*新的代码在本地断点调试,这就是云端联调的实现。

3)构建快速开发体系

%title插图%num

代码在本地完成联调以后,要能快速地通过 Maven 插件和 IDEA-plugin,可以很快地一键部署到云端的开发环境。

2. 发布态实践

1)应用发布三板斧

%title插图%num

%title插图%num

  • 可灰度:应用在发布的过程中,运维平台一定要有发布策略,包括单批、分批、金丝雀等发布策略;同时还要支持流量的灰度;批次间也要允许自动/手动任选。
  • 可观测:发布过程可监控,白屏化实时查看发布的日志和结果,及时定位问题。
  • 可回滚:允许人工介入控制发布流程:异常中止、一键回滚。

通过这三点可以让应用发布做到可灰度、可观测、可回滚。

2)微服务无损下线

在版本更换的过程中,SAE 是如何保证旧版本的微服务流量可以无损地下线掉?

%title插图%num

上图是微服务注册和发行的整个流程,图中有服务消费者和服务提供者,服务提供者分别有 B1、B2 两台实例,服务消费者分别有 A1、A2 两台实例。

B1、B2 把自己注册到注册中心,消费者从注册中心刷新服务列表,发现服务提供者 B1、B2,正常情况下,消费者开始调用 B1 或者 B2,服务提供者 B 需要发布新版本,先对其中一个节点进行操作,如 B1,首先停止 Java 进程,服务停止过程又分为主动销毁和被动销毁,主动销毁是准实时的,被动销毁的时间由不同的注册中心决定,*差的情况可能需要一分钟。

如果应用是正常停止,Spring Cloud 和 Dubbo 框架的 ShutdownHook 能正常被执行,这一步的耗时基本上是可以忽略不计的。如果应用是非正常停止,比如说直接 Kill-9 的一个停止,或者是 Docker 镜像构建的时候,Java 进程不是一号进程,且没有把 Kill 信号传递给应用的话,那么服务提供者不会主动去注销节点,它会等待注册中心去发现、被动地去感知服务下线的过程。

当微服务注册中心感知到服务下线以后,会通知服务消费者其中一个服务节点已下线,这里有两种方式:注册中心的推送和消费者的轮巡。注册中心刷新服务列表,感知到提供者已经下线一个节点,这一步对于 Dubbo 框架来说不存在,但对于 Spring Cloud 来说,它*差的刷新时间是 30 秒。等消费者的服务列表更新以后,就不再调用下线节点 B。从第 2 步到第 6 步的过程中,注册中心如果是 Eureka,*差的情况需要消耗两分钟;如果是 Nacos,*差的情况需要消耗 50 秒。

在这个时间内请求都有可能出现问题,所以发布的时候会出现各种报错。

%title插图%num

经过上面的分析,在传统的发布流程中,客户端有一个服务端调用报错期,这是由于客户端没有及时感知到服务端下线的实例造成的,这种情况主要是因为服务提供者借助微服务,通知消费者来更新服务提供的列表造成的。

%title插图%num

那能否绕过注册中心,服务提供者直接通知服务消费者?答案是肯定的。SAE 做了两件事情,*,服务提供者在应用发布前,会主动向服务注册中心注销应用,并将应用标记为已下线状态,将原来停止进程阶段的注销变成了 preStop 阶段注销进程。

在接收到服务消费者的请求时,首先会正常处理本次请求,并且通知服务消费者此节点已经下线,在此之后消费者收到通知后,会立即刷新自己的服务列表,在此之后服务消费者就不会再把请求发到服务提供者 B1 的实例上。

通过上面这个方案,就使得下线感知时间大大缩短,从原来的分钟级别做到准实时的,确保你的应用在下线时能够做到业务无损。

3)基于标签的灰度发布

%title插图%num

发布策略分为分批发布和灰度发布,如何实现流量的灰度?从上面的架构图中可以看到,在应用发布之前,要配置一个灰度规则,比如按 uid 的取模余值 =20 来作为灰度流量的规则,当应用发布的时候,会对已发布的节点标识为一个灰度的版本,在这样的情况下,当有流量进来时,微服务网关和消费者都会通过配置中心拿到在治理中心配置的灰度规则。

消费者的 Agent 也会从注册中心拉取它所依赖的服务的一些信息,当一个流量进到消费者时,会按照灰度规则来做匹配,如果是灰度的流量,它会转化到灰度的机器上;如果是正常流量,它会转到正常的机器上,这是基于标签实现的灰度发布的具体逻辑。

3. 运行态实践

1)强大的应用监控 & 诊断能力

%title插图%num

运行态的实例,服务的运行过程中会出现这样或者那样的问题,怎么去排查和解决它?

排查和解决的前提是必须具有强大的应用监控能力和诊断能力,SAE 集成了云产品 ARMS,能够让跑在上面的 Java 微服务看到应用的调用关系拓扑图,可以定位到你的 MySQL 慢服务方法的调用堆栈,进而定位到代码级别的问题。

比如一个请求响应慢,业务出现问题,它可以定位到是哪个请求、哪个服务、服务的哪行代码出现了问题,这样就能为解决问题带来很多便利。总的来说,就是我们要先有监控报警的能力,才能帮助我们更好地诊断服务运营过程中的问题。

2)故障隔离和服务恢复

上面说到我们通过监控、报警来排查、解决遇到的问题,那我们的系统能否主动去做一些事情呢?SAE 作为一个 Serverless 平台,具备很多自运维的能力,下图中有两个场景:

%title插图%num

  • 场景1:某应用运营过程中,某几台机器由于磁盘满或者宿主机资源争抢,导致 load 很高或网络状态差,客户端出现调用超时或者报错。

面对这种情况,SAE 提供了服务治理能力,即离群摘除,它可以配置,当网络超时严重或者后端服务 5xx 报错达到一定比例时,可以选择把该节点从消费端服务列表中摘除,从而使得有问题的机器不再响应业务的请求,很好地保证业务的 SLA。

  • 场景2:某应用运行过程中,因突发流量导致内存耗尽,触发 OOM。

这种情况下,通过 SAE 这种 Serverless 应用引擎,节点在配置健康检查以后,节点里的容器是可以重新拉起的,可以做到快速对进程进行恢复。

3)精准容量+限流降级+*致弹性

%title插图%num

基于 Serverless Paas 平台 SAE 和其他产品的互动,来达到整个运维态的闭环。

用户在使用的时候,可以运用 PTS 压测工具构造场景,然后得出来一些阈值。比如可以对流量高峰所需要消耗的资源进行预估,这时就可以根据这些阈值设计弹性策略。当业务系统达到请求比例时,就可以按照所设置的弹性策略来扩缩容自己的机器。

扩缩容在时间上,有可能还跟不上处理大批量的请求,这时可以通过和 AHAS 的互动,配置限流降级的能力。当有突发大流量时,首先可以用 AHAS 的能力把一些流量挡在门外,然后同时触发 SAE 上应用的扩容策略去扩容实例,当这些实例扩容完成之后,整个机器的平均负载会下降,流量又重新放进来。从突发大流量到限流降级再到扩容,*后到流量达到正常状态,这就是“精准容量+限流降级+*致弹性”的*佳实践模型。

%title插图%num

总结

本文首先按照提出问题、解决问题的思路,阐述微服务在开发、发布和运行态是如何解决问题的;再介绍如何通过 Serverless 产品和其他产品的互动,从而实现精准流量、限流降级和*致弹性。

  • 开发测试态
    • 通过注册中心多租户和网络环境的隔离,并提供环境级别的能力;
    • 通过云端联调技术来快速调式本地变更;
    • 如果 IDE 插件快速部署本地变更。
  • 发布态
    • 运维平台针对应用发布需要具备可灰度、可观测、 可回滚;
    • 通过 MSE agent 能力实现服务无损下线;
    • 通过标签路由提供了线上流量灰度测试的能力。
  • 运行态
    • 建立强大应用监控和诊断能力;
    • 对服务质量差的节点具备离群摘除能力;
    • 对已经不工作的实例通过配置健康检查能够做到实例重启恢复业务;
    • 提供了精准容量+限流降级+*致弹性模型。

从 0 到 1,高德 Serverless 平台建设及实践

导读:高德从 FY21 财年开始启动 Serverless 建设,至今一年了,高德 Serverless 业务的峰值超过十万 qps 量级,平台从 0 到 1,qps 从零到十万,成为阿里集团内 Serverless 应用落地规模*大的 BU,这中间的过程是怎么样的?遇到过哪些问题?高德为什么要搞 Serverless/Faas?是如何做 Serverless/Faas 的?技术方案是什么样的?目前进展怎么样?后续又有哪些计划?本文将和大家做一个简单的分享。

%title插图%num

Why-高德为什么要搞 Serverless

高德为什么要搞 Serverless?背景原因是高德 FY21 财年启动了一个客户端上云项目。客户端上云项目的主要目的是为了提升客户端的开发迭代效率。

以前客户端业务逻辑都在端上,产品需求的变更需要走客户端发版才能发布,而客户端发版需要走各种测试流程、灰度流程,解决客户端崩溃等问题,目前的节奏是一个月一个版本。

客户端上云之后,某些易变的业务逻辑放到云上来。新的产品需求在云端来开发,不用走月度的版本发布,加快了需求的开发迭代效率,离产研同频的理想目标又近了一步(为什么要说“又”,是因为高德之前也做了一些优化往产研同频的方向努力,但是我们希望云端一体化开发可以是其中*有效的一个技术助力)。

%title插图%num

1. 目标:客户端开发模式–端云一体

虽然开发模式从以前的端开发转变为现在的云+端开发,开发同学应该还是原来负责相应业务的同学,但是大家知道,服务端开发和客户端开发显然是有差异的,客户端开发是面向单机模式的开发,服务端开发通常是集群模式,需要考虑分布式系统的协调、负载均衡、故障转移降级等各种复杂问题。如果使用传统的服务端模式来开发,这个过渡风险就会比较大。

Faas 很好地解决了这一问题。我们结合高德客户端现有的 xbus 框架(一套客户端上的本地服务注册、调用的框架),扩展了 xbus-cloud 组件,使得云上的开发就像端上开发一样,目标是一套代码、两地运行,一套业务代码既能在客户端上运行,也能在服务端上运行。

高德客户端主要有三个端:IOS、android、车机(类 Linux 操作系统)。主要有两种语言:C++ 和 Node.js。传统地图功能:如地图显示、导航路径显示、导航播报等等,由于需要跨三个端,采用 C++ 语言来开发。地图导航基础之上的一些地图应用功能,如行前/行后卡片、推荐目的地等,主要用 Node.js 来开发。

FY20 财年淘系前端团队开发了 Node.js Faas runtime。高德客户端上云项目,Node.js 的部分就采用了现有的淘系的 Node.js runtime,来接入集团的 Faas 平台,完成 Node.js 这部分的一些业务上云。2020 年十一期间很好地支撑了高德的十一出行节业务。

C++ Faas 没有现有的解决方案,因此我们决定在集团的基础设施之上做加法,新建 C++ Faas 基础平台,来助力高德客户端上云。

  • 端云一体的*佳实践关键:客户端和 Faas 之间的接口抽象

原本客户端的逻辑移到 Faas 服务端上来,或者新的需求一部分在 Faas 服务端上开发,这里的成败关键点在于:客户端和 Faas 的接口协议定义,也就是 Faas 的 API 定义,好的 API 定义除了对系统的可维护性有好处以外,对后续支撑业务的迭代开发也很重要,好的 API 定义请参考谷朴大神的文档:《API 设计*佳实践的思考》。

理想情况下:客户端做成一个解析 Faas 返回结果数据的一个浏览器。浏览器协议一旦定义好,就不会经常变换,你看 IE、Chrome 就很少更新。

当然我们的浏览器会复杂一些,它是地图浏览器。如何检验客户端和 Faas 之间的接口定义好不好,可以看后续的产品需求迭代,如果有些产品需求迭代只需要在 Faas 上完成,不需要客户端的任何修改,那么这个接口抽象就是成功的。

2. BFF 层开发提效

提到高德,大家首先想到的应该是其工具属性:高德是一个导航工具(这个说法现在已经不太准确了,因为高德这几年在做工具化往平台化的转型,我们要做万能的高德,高德的交易类业务正在兴起,高德打车、门票、酒店等业务发展很迅猛)。

针对高德导航来说,相比集团其他业务(如电商)来说,有大量的只读场景是高德业务的一大技术特点。这些只读场景里,大量的需求是 BFF(Backend For Frontend)类型的只读场景。为什么这么说?因为导航的*核心功能,例如 routing、traffic、eta 等都是相对稳定的,这部分的主要工作在持续不断地优化算法,使得高德的交通更准,算出的路径更优。这些核心功能在接口和功能上都是相对比较稳定的,而前端需求是多变的,例如增加个路径上的限宽墩提示等。

%title插图%num

Faas 特别适合做 BFF 层开发,在 Faas 上调用后端相对稳定的各个 Baas 服务,Faas 服务来做数据和调用逻辑封装、快速开发、发布。在业界,Faas 用的*多的场景也正是 BFF 场景(另外一个叫法是 SFF 场景,service for frontend)。

3. Serverless 是云时代的高级语言

FY21,高德是集团内*个全面上云的 BU,虽然高德已经全面上云了,但是这还不是云时代的终局,目前主要是全面 pouch 化并上云,容器方面做了标准化,在规模化、资源利用率方面可以全面享受云的红利,但是业务开发模式上基本上还和以前一样,仍是一个大型的分布式系统的写法。对于研发模式来说还并没有享受云的红利,可以类比为我们现在是在用汇编语言的方式来写跑在云上的服务。而 Serverless、云原生可以理解为云时代的高级语言,真正做到了 Cloud as a computer,只需要关注于业务开发,不需要考虑大型分布式系统的各种复杂性。

4. Go-Faas 补充 Go 语言生态

前面讲到了因为客户端上云项目,我们在阿里云 FC(函数计算)团队之上做加法,开发了 C++ Faas Runtime。不仅如此,我们还开发了 Go-Faas,我们为什么会做 Go-Faas 呢?这里也简单介绍一下背景,高德服务端 Go 部分的 qps 峰值已超百万。高德已补齐了阿里各中间件的 Go 客户端,和集团中间件部门共建。可观测性、自动化测试体系也基本完善,目前 Go 生态已基本完善。

补齐了 Go-Faas 之后,我们就既能用 Go 写 Baas 服务,又能用 Go 写 Faas 服务了,在不同的业务场景采用不同的服务实现方式,Go-Faas 主要应用于上文提到的 BFF 场景。

%title插图%num

How-技术方案介绍

1. 整体技术架构

上文讲了我们为什么要做这个事情,接下来讲我们具体是怎么做这个事情的,是如何实现的,具体的技术方案是什么样的。

本着在集团现有的基础设施、现有的中间件基础之上做加法的思想,我们和 CSE、阿里云 FC 函数计算团队合作共建,开发了 C++ Faas Runtime 和 Go Faas Runtime。整体和集团拉通的技术架构如下图所示,主要分为研发态、运行态、运维态三个部分。

%title插图%num

  • 运行态

先说运行态,业务流量从我们网关进来,调用到 FC API Server,转发到 C++/Go Faas Runtime,runtime 来完成用户函数里的功能。runtime 的架构本文下一章节会具体介绍。

和 runtime container 一起部署的有监控、日志、Dapr 各种 side car,side car 来完成各种日志采集上报功能,dapr side car 来完成调用集团中间件的功能。

另外目前 dapr 还在试点的阶段,调用中间件主要是通过 Broker 和各个中间件 proxy 来完成,中间件调用的有HSF、Tair、metaq、diamond 等中间件 proxy。

*后 Autoscaling 模块来管理函数实例的扩缩容,达到函数自动伸缩的目的。这里的调度就有各种策略了,有根据请求并发量的调度、函数实例的 CPU 使用率的调度。也能提前设置预留实例数,避免缩容到 0 之后的冷启动问题。

底层调用的是集团 ASI 的能力,ASI 可以简单理解为集团的 K8S+ sigma(集团的调度系统),*终的部署是 FC 调用 ASI 来完成函数实例部署,弹性伸缩的,部署的*小单位是上图中的 pod,一个 pod 里包含 runtime container 和 sidecar set container。

  • 研发态

再来看研发态,运行态决定函数是如何运行的,研发态关注函数的开发体验,如何方便地让开发者开发、调试、部署、测试一个函数。

C++ Faas 有个跨平台的难点问题,C++ Faas runtime 里有一些依赖库,这些依赖库没有 Java 依赖库管理那么方便。这样依赖库的安装比较麻烦,Faas 脚手架就是为了解决这个问题,调用脚手架,一键生成 C++ Faas 示例工程,安装好各种依赖包。为了本地能方便地 debug,开发了一个 C++ Faas Runtime Boot 模块,函数 runtime 启动入口在 boot 模块里,boot 模块里集成 runtime 和用户 Faas 函数,可以对 runtime 来做 debug 单步调试。

我们和集团 Aone 团队合作,函数的发布集成到 Aone 环境上了,可以很方便地在 Aone 上来发布 Go 或者 C++ Faas,Aone 上也集成了一键生成 example 代码库的功能。

C++ 和 Go Faas 的编译都依赖相应的编译环境,Aone 提供了自定义编译镜像的功能,我们上传了编译镜像到集团的公共镜像库,函数编译时,在函数的代码库里指定相应的编译镜像,编译镜像里安装了 Faas 的依赖库、SDK等。

  • 运维态

*后来看函数的运维监控,runtime 内部集成了鹰眼、sunfire 采集日志的功能,runtime 里面会写这些日志,通过 sidecar 里的 agent 采集到鹰眼、或者 sunfire 监控平台上去(FC 是通过 SLS 来采集的)之后,就能使用集团现有的监控平台来做 Faas 的监控了,也能接入集团的 GOC 报警平台。

2. C++/Go Faas Runtime 架构

上面讲的是和 Aone、FC/CSE、ASI 集成的一个整体架构,Runtime 是这个整体架构的一部分,下面具体讲讲 Runtime 的架构是怎样的,Runtime 是如何设计和实现的。

%title插图%num

*上面部分的用户 Faas 代码只需要依赖 Faas SDK 就可以了,用户只需要实现 Faas SDK 里的 Function 接口就能写自己的 Faas 了。然后如果需要调用外部系统,可以通过 SDK 里的 Http Client 来调用,如果要调用外部中间件,通过 SDK 里的 Diamond/Tair/HSF/metaq Client 来调用中间件就可以。SDK 里的这些接口屏蔽了底层实现的复杂性,用户不需要关心这些调用*后是如何实现,不需要关心 runtime 的具体实现。

SDK 层就是上面提到的 Function 定义和各种中间件调用的接口定义。SDK 代码是开发给 Faas 用户的。SDK 做的比较轻薄,主要是接口定义,不包含具体的实现。调用中间件的具体实现在 Runtime 里有两种实现方式。

往下是 Runtime 的一个整体架构。Starter 是 runtime 的启动模块,启动之后,runtime 自身是一个 Server,启动的时候根据 Function Config 模块的配置来启动 runtime,runtime 启动之后开启请求和管理监听模式。

再往下是 Service 层,实现 SDK 里定义的中间件调用的接口,包含 RSocket 和 dapr 两种实现方式,RSocket 是通过 RSocket broker 的模式来调用中间件的,runtime 里集成了 dapr(distributed application runtime),调用中间件也可以通过 dapr 来调用,在前期 dapr 试点阶段,如果通过 dapr 调用中间件失败了,会降级到 rsocket 的方式来调用中间件。

再往下就是 rsocket 的协议层,封装了调用 rsocket 的各种 metadata 协议。dapr 调用是通过 grpc 方式来调用的。

*下面一层就是集成了 rsocket 和 dapr 了。

rsocket 调用还涉及到 broker 选择的问题,upstream 模块来管理 broker cluster、broker 的注册反注册、keepalive 检查等等,LoadBalance 模块来实现 broker 的负载均衡选择以及事件管理、连接管理、重连等等。

*后 runtime 里的 metrics 模块负责鹰眼 trace 的接入,通过 filter 模式来拦截 Faas 链路的耗时,并输出鹰眼日志。打印 sunfire 日志,供 sidecar 去采集。下图是一个实际业务的 sunfire 监控界面:

%title插图%num

  • Dapr

dapr 架构如下图所示,具体可以参考官方文档:https://dapr.io/

%title插图%num

runtime 里以前调用中间件是通过 rsocket 方式来调用的,这里 rsocket broker 会有一个中心化问题,为了解决 outgoing 流量去中心化问题,和集团中间件团队合作引入了 dapr 架构。只是 runtime 层面集成了 dapr,对于用户 Faas 来说无感知,不需要关心具体调用中间件是通过 rsocket 调用的还是通过 dapr 调用的。后面 runtime 调用中间件切换到 dapr 之后,用户 Faas 也是不需要做任何修改的。

%title插图%num

How-业务如何接入 Serverless

如前文所述,接入统一在 Aone 上接入。提供了 C++ Faas/Go Faas 的接入文档。提供了函数的 example 代码库,代码库有各种场景的示例,包括调用集团各种中间件的代码示例。C++ Faas/Go Faas 的接入对整个集团开发,目前已经有一些高德以外的 BU,在自己的业务中落地了 C++ /Go Faas。Node.js Faas 使用淘宝提供的 runtime 和模板来接入,Java Faas 使用阿里云 FC 提供的 runtime 和模板来接入就可以了。

1. 接入规范-稳定性三板斧:可监控、可灰度、可回滚

针对落地新技术大家可能担心的稳定性问题,我们的应对法宝是阿里集团的稳定性三板斧:可监控、可灰度、可回滚。建立 Faas 链路保障群,拉通上下游各相关业务方、基础平台一起,按照集团的 1-5-10 要求,共同努力做到 1 分钟之内响应线上报警、快速排查;5 分钟之内处理;10 分钟之内恢复。

为了规范接入过程,避免犯错误引发线上故障,我们制定了 Faas 接入规范和 checkList,来帮助业务方快速使用 Faas。

可监控、可灰度、可回滚是硬性要求,除此之外,业务方如果能做到可降级就更好了。我们的 C++ 客户端上云业务,在开始试点阶段,就做好了可降级的准备,如果调用 Faas 端失败,本次调用将会自动降级到本地调用。基本对客户端功能无损,只是会增加一些响应延迟,另外客户端上该功能的版本,可能会比服务端稍微老一点,但是功能是向前兼容的,基本不影响客户端使用。

%title插图%num

Now-我们目前的情况

1. 基础平台建设情况

  • Go/C++ Faas Runtime 开发完成,对接 FC-Ginkgo/CSE、Aone 完成,已发布稳定的 1.0 版本。
  • 做了大量的稳定性建设、优雅下线、性能优化、C 编译器优化,使用了阿里云基础软件部编译器优化团队提供的编译方式来优化 C++ Faas 的编译,性能提升明显。
  • C++/Go Faas 接入鹰眼、sunfire 监控完成,函数具备了可观测性。
  • 池化功能完成,具备秒级弹性的能力。池化 runtime 镜像接入 CSE,扩一个新实例的时间由原来的分钟级变为秒级。

2. 高德的 Serverless 业务落地情况

C++ Faas 和 Go Faas 以及 Node.js Faas 在高德内部已经有大量的应用落地。举几个例子:

%title插图%num

上图中的前两个图是 C++ Faas 开发的业务:长途天气、沿途搜。后两个截图是 Go-Faas 开发的业务:导航 tips、足迹地图。

高德是阿里集团内 Serverless 应用落地规模*大 的BU,已落地的 Serverless 应用,日常峰值超过十万 qps 量级。

3. 主要收益

高德落地了集团内规模*大的 Serverless 应用之后,都有哪些收益呢?

首先*个*重要的收益是:开发提效。我们基于 Serverless 实现的端云一体组件,助力了客户端上云,解除了需要实时的客户端发版依赖问题,提升了客户端的开发迭代效率。基于 Serverless 开发的 BFF 层,提升了 BFF 类场景的开发迭代效率。

第二个收益是:运维提效。利用 Serverless 的自动弹性扩缩容技术,高德应对各种出行高峰就更从容了。例如每年的十一出行节、五一、清明、春节的出行高峰,不再需要运维或者业务开发同学在节前提前扩容,节后再缩容了。高德业务高峰的特点还不同于电商的秒杀场景。出行高峰的流量不是在 1 秒内突然涨起来的,我们目前利用池化技术实现的秒级弹性的能力,完全能满足高德的这个业务场景需求。

第三个收益是:降低成本。高德的业务特点,白天流量大、夜间流量低,高峰值和低谷值差异较大,时间段区分明显。利用 Serverless 在夜间流量低峰时自动缩容技术,*大地降低了服务器资源的成本。

%title插图%num

Next-后续计划

  • FC 弹内函数计算使用优化,和 FC 团队一起持续优化弹内函数计算的性能、稳定性、使用体验。用集团内丰富的大流量业务场景,来不断打磨好 C++/Go Faas Runtime,并*终输出到公有云,普惠数字化转型浪潮中的更多企业。
  • Dapr 落地,解决 outcoming 流量去中心化问题,逐步上线一些 C++/Go Faas,使用 Dapr 的方式调用集团中间件。
  • Faas 混沌工程,故障演练,逃生能力建设。Faas 在新财年也会参与我们 BU 的故障演练,逐一解决演练过程中发现的问题。
  • 接入边缘计算。端云一体的场景下,Faas + 边缘计算,能提供更低的延时,更好的用户体验。

 

新零售:从上云到云原生 Serverless

某零售商超行业的龙头企业,其主要业务涵盖购物中心、大卖场、综合超市、标准超市、精品超市、便利店及无人值守智慧商店等零售业态,涉及全渠道零售、仓储物流、餐饮、消费服务、数据服务、金融业务、跨境贸易等领域。为了持续支持业务高速且稳定地发展,其在快速上云后,将核心业务改造为全 Serverless 架构的中台模式,采用函数计算 + API 网关 + 表格存储 OTS 作为计算网络存储核心,弹性支撑日常和大促峰谷所需资源,轻松支撑 618/ 双11/ 双12 大促。

%title插图%num

传统企业为什么更需要关注 Serverless

为了降低技术研发成本、提升运维效率,越来越多的企业选择使用 Serverless 作为基础研发底座,大力发展业务。在 CNCF Serverless 研究报告中显示,大量的国内开发人员正在将传统架构往 Serverless 上做迁移。Serverless 的出现给传统企业数字化转型带了更多机遇。

传统企业为什么更要关注 Serverless ?

现如今,大量尖端技术人才更偏向在互联网公司就业,传统企业又面对着大量技术升级和重构技术架构的刚需,人才缺口和技术升级之间产生了对云原生技术的需求。Serverless 的出现抹平了研发人员在预算、运维经验上的不足。在帮助企业对抗业务洪峰的情况下,研发人员能轻易掌控处理,不仅*大地降低了研发技术门槛,而且大规模提升了研发效率。对于开发者而言,线上预警、流量观测等工具一应俱全,关键是免去了运维负担,切实为广大开发者提供了普惠技术红利。对传统企业而言,Serverless 缩短了互联网公司与传统企业之间技术竞争力的距离。

%title插图%num

从上云到云原生

2016 年以后,随着国内公共云的迅速发展,全面上云势不可挡。某知名大型商场在 2018~2019 年期间,把自建机房中的各个系统模块逐渐迁移到了公有云,整体架构没有太大改变,因此迁移工作比较顺利。

系统全面迁移上云后一些改进和不足:

1

改进

不再需要关心网络、操作系统的硬件细节

比如阿里云的 ECS 会提前做调度和预警,把用户数据转移并做多份数据的备灾,防止磁盘坏掉的情况发生。

升级快捷简单

比如用户使用的是 4 核的机器,当发现业务增长迅速需要做硬件升级时,就只需要做一个镜像。比如在夜间做一个磁盘快照,重新申请一台新机器,然后把快照恢复上去,就可以完成一键迁移。对用户来说这是非常快捷的方式,对开发者来说也是较好的体验。

机器扩容时间大幅缩短

上面提到的是单机扩容,比如 4 核升到 8 核、16G 升到 32G 的内存。除此之外还有横向的扩容,例如用户交易系统的 API 接口,随着业务的发展需要由原来的 2 台机器扩到 8 台机器,这种情况下用户只需去申请机器,然后将镜像扩展到不同的机器上即可。

2

不足

资源预算困难

无法预估业务遇到大促活动时所能达到的体量,因此无法准确计算出所需硬件的数量。

水平扩展

水平扩展对研发有较高的要求。比如数据是否要做到无状态,无状态的话水平扩展会比较容易,而如果是有状态,数据可能就需要做缓存,这就会涉及到数据库相关的问题,例如数据过期、一致性等。如果对这些了解不够透彻,做水平扩展就会比较困难。

水位监控

许多开发者在水位监控上处理得并不完善,如果将各个业务系统混在一台机器上,当遇到机器水位较高,想要快速排查问题并及时进行流控、拆分、临时修复等就显得尤为困难。

财务预算困难

与资源预算困难类似。

硬件升级成本高

要做到用户无感无损升级,可能会涉及到连接上的处理与数据库一致性的问题。如果多个模块需要同时升级,还要注意数据结构的兼容问题。

数据库单点故障

许多厂家将数据全部放在一个数据库中,如果处理不妥当可能会造成单点故障。这就要做数据拆分,粗拆的话,需要注意事务和锁相关的问题,效率会大打折扣;细拆的话,做查询和排序时就会比较困难,给业务实现造成一定麻烦。

%title插图%num

业务挑战

在一次年中大促时,由于线上业务用户访问不可控,数据量过大,MySQL 单机访问被打爆,导致了存储数据库出现问题,影响到了多个系统,造成了一定的损失。因此在后续服务化改造过程中,数据库选型由 MySQL 更改为表格存储 OTS,表格存储*大的优点是用户不需要关心访问量和机器数的比例关系。只要访问量扩大,后台会自动扩容增扩机器,满足高并发的数据读取;在数据并发请求降低处于低峰期时,后台就会将机器回收,用户不再需要关心机器的数量及如何调动。

%title插图%num

Severless改造

%title插图%num

针对用户流量不可控问题,客户引入了阿里云的产品“API 网关”,API 网关可以针对不同渠道商做管控发布及流量控制。比如发现微信渠道流量有异常,就可以借助 API 网关进行限流。

另外计算也是一个非常重要的问题,客户经过探索发现阿里云函数计算 FC非常契合其业务场景。比如定时抢购、优惠券投放等活动造成巨大的 burst 冲击,当发现计算资源不够的时候再去买机器肯定是来不及的,而函数计算及时扩容的功能就很好地解决了这个问题。另外其数据观测和异常报警功能,也吸引到了客户。

今年 3 月,权威咨询机构 Forrester 发布 2021 年*季度 FaaS 平台评估报告,阿里云函数计算凭借在产品能力、安全性、战略愿景和市场规模等方面的优势脱颖而出,产品能力位列全球*,这也是首次有中国云厂商进入 FaaS 领导者象限。

%title插图%num

在紧张的测试验证后,技术人员发现函数计算的优异表现很契合自身业务高度弹性的会员查询系统。从 2019 年 7 月开始,客户的技术团队在不到 3 个月的时间里,将原有的会员数据全部副本镜像迁移到表格存储,并将所有渠道商的 API 全面迁移到阿里云 API 网关做分发,会员查询业务的计算业务也全面迁移到阿里云函数计算。

2019 年的 双11,函数计算作为计算模块,表格存储作为存储模块,顺利地帮助客户渡过大促,扛住高峰流量的同时确保了应对业务的弹性。而未使用 Serverless 的业务因为预估不足,出现了一些异常。正是因为函数计算在 双11 中的表现让客户技术人很振奋。在顺利度过大促活动后,客户就在所有业务中全面使用函数计算及表格存储!

%title插图%num

新零售商超整体架构图

  • 全 Serverless 架构:函数计算 + API 网关 + 表格存储;
  • 弹性高可用:毫秒级弹性扩容、充足的资源池水位、跨可用区高可用;
  • 敏捷开发免运维:函数式*简编程可专注于业务创新,无采购和部署成本、提供监控报警等完备的可观测能力。

2019 年下半年,阿里云函数计算宣布推出 2.0,支持预留模式,全面解决冷启动延迟大的问题;推出单实例多请求问题,较少实例支持重 IO 高并发类型请求调用;支持自定义运行时,支持一键迁移传统 Web 架构服务器。2.0 的出现让函数计算在业务和规模上实现了巨大升级。

在经历了过去的线下场景考验后,客户将各渠道商的业务及旗下的移动 App,以及线上交易、定时抢优惠券、秒杀业务也全部从 ECS 迁移到了函数计算 2.0,在开启预留模式调整好单实例多并发的模式后,顺利地扛过了是平时数十倍的洪峰流量请求。

%title插图%num

%title插图%num

比较上述的“时间-流量图”及“时间-延迟”两图可以看到,急剧上升的突发流量对用户造成的延迟变化影响非常小,从实际用户反馈来看确实也证实了用户体验非常顺滑。

所有的数据和业务上云,减轻的不只是研发人员的心理压力,更为研发人员大量减负,从而让大家可以做更聚焦在业务逻辑上的事情。函数计算可以让研发人员不用管理服务器这些基础设施,只要编写代码上传,系统就会准备好计算资源,还提供日志查询、性能监控、报警等功能。如果是按照以前的模式,超市搞 双11 大促,相关的技术团队都睡不着觉,只靠扩展机器支撑大体量的流量和业务,谁心里都没谱。现在扩容的问题交给阿里云,水位远远高于客户原有的储备能力的*限。

今年,Serverless 迎来重大升级。函数计算重磅发布容器镜像加速技术,容器启动延时缩短 50%-80%,将原本属于开发者的镜像优化负担转由函数计算承担,进一步帮助开发者提高生产效率,专注业务创新。该技术源于阿里集团超大规模和场景高度复杂的容器环境,对镜像存储、加速技术有深厚的积累,并出色地承担了 3 年双十一、双十二、春节等大促秒杀场景的严苛的挑战。

同时,Serverless 应用引擎(SAE)重磅发布 Java 应用启动加速功能,首度将 Alibaba Dragonwell(阿里云开源的 Open JDK 长期支持版本)的冷启动加速技术、多线程运行加速技术和 SAE 自身的原地升级策略、镜像预热策略相结合,实现了 Java 应用的端到端启动速度提升 45%,*快仅需 15s,多线程性能提升 30%。

%title插图%num

由于业务场景、用户习惯迅速变化,许多行业数字化业务出现急速增长,加快数字化业务发展成为传统企业的必然选择。云原生是企业数字化*短路径,越来越多的传统企业正在拥抱云原生,借助更加快速、灵活的开发和交付模式,满足市场快速变化的需求,进而加速业务创新。传统零售企业借助 Serverless 保证了一次次大促的成功,正是这一趋势的*好证明。

妥了!微服务治理的困难,用 Serverless 来解决

 

%title插图%num

微服务治理面临的挑战

在业务初期,因人手有限,想要快速开发并上线产品,很多团队使用单体的架构来开发。但是随着公司的发展,会不断往系统里面添加新的业务功能,系统越来越庞大,需求不断增加,越来越多的人也会加入到开发团队,代码库也会增速的膨胀,慢慢的单体应用变得越来越臃肿,可维护性和灵活性逐渐降低,维护成本越来越高。

%title插图%num

这个时候很多团队会把单体应用架构改为微服务的架构,解决单体应用的问题。但随着微服务越来越多,运维投入会越来越大,需要保证几十甚至几百个服务正常运行与协作,这给运维带来了很大的挑战,下面从软件生命周期的角度来分析这些挑战:

  • 开发测试态
    • 如何实现开发、测试、线上环境隔离?
    • 如何快速调试本地变更?
    • 如何快速部署本地变更?
  • 发布态
    • 如何设计服务发布策略?
    • 如何无损下线旧版本服务?
    • 如何实现对新版本服务灰 度测试?
  • 运行态
    • 线上问题如何排查?有什么工具可以利用呢?
    • 对于服务质量差的节点如何处理?
    • 对于完全不工作的实例我们如何恢复?

面对以上问题,Serverless 应用引擎在这方面都做了哪些工作?

%title插图%num

Serverless 应用引擎

%title插图%num

如上图所示,Serverless 应用引擎(SAE)基于神龙 + ECI + VPC + SLB + NAS 等 IaaS 资源,构建了一个 Kubernetes 集群,在此之上提供了应用管理和微服务治理的一些能力。它可以针对不同应用类型进行托管,比如 Spring Cloud 应用、Dubbo 应用、HSF 应用、Web 应用和多语言应用。并且支持 Cloudtoolkit 插件、云效 RDC / Jenkins 等开发者工具。在 Serverless 应用引擎上,零代码改造就可以把 Java 微服务的应用迁移到 Serverless。

总的来说,Serverless 应用引擎能够提供成本更优、效率更高的一站式应用托管方案,零门槛、零改造、零容器基础,即可享受 Serverless + K8s + 微服务带来的技术红利。

%title插图%num

微服务治理实践

1. 开发态实践

1)多环境管理

%title插图%num

  • 多租户共有一个注册中心,通过不同的租户对流量进行隔离;更进一步可以通过网络 VPC 进行环境隔离;
  • 提供环境级别的运维操作,比如一键停止和拉起整个环境的功能;
  • 提供环境级别的配置管理;
  • 提供环境级别的网关路由流量管理。

2)云端联调

Serverless 应用引擎(SAE)基于 Alibaba CloudToolkit 插件+ 跳板机可以实现:

  • 本地服务订阅并注册到云端 SAE 内置的注册中心;
  • 本地服务可以和云端 SAE 服务互相调用。

%title插图%num

如上图所示,在实现的时候用户需要有一个 ECS 代理服务器,实际注册的是 ECS 代理服务器到 SAE 的注册中心,IDEA 在安装 Cloudtoolkit 插件以后,在启动进程时,会在本地拉起一个通道服务,这个通道服务会连上 ECS 代理服务器,本地所有的请求都会转到 ECS 代理服务器上,云端对服务的调用也会通过 ECS 代理转到本地,这样就可以以*新的代码在本地断点调试,这就是云端联调的实现。

3)构建快速开发体系

%title插图%num

代码在本地完成联调以后,要能快速地通过 Maven 插件和 IDEA-plugin,可以很快地一键部署到云端的开发环境。

2. 发布态实践

1)应用发布三板斧

%title插图%num

%title插图%num

  • 可灰度:应用在发布的过程中,运维平台一定要有发布策略,包括单批、分批、金丝雀等发布策略;同时还要支持流量的灰度;批次间也要允许自动/手动任选。
  • 可观测:发布过程可监控,白屏化实时查看发布的日志和结果,及时定位问题。
  • 可回滚:允许人工介入控制发布流程:异常中止、一键回滚。

通过这三点可以让应用发布做到可灰度、可观测、可回滚。

2)微服务无损下线

在版本更换的过程中,SAE 是如何保证旧版本的微服务流量可以无损地下线掉?

%title插图%num

上图是微服务注册和发行的整个流程,图中有服务消费者和服务提供者,服务提供者分别有 B1、B2 两台实例,服务消费者分别有 A1、A2 两台实例。

B1、B2 把自己注册到注册中心,消费者从注册中心刷新服务列表,发现服务提供者 B1、B2,正常情况下,消费者开始调用 B1 或者 B2,服务提供者 B 需要发布新版本,先对其中一个节点进行操作,如 B1,首先停止 Java 进程,服务停止过程又分为主动销毁和被动销毁,主动销毁是准实时的,被动销毁的时间由不同的注册中心决定,*差的情况可能需要一分钟。

如果应用是正常停止,Spring Cloud 和 Dubbo 框架的 ShutdownHook 能正常被执行,这一步的耗时基本上是可以忽略不计的。如果应用是非正常停止,比如说直接 Kill-9 的一个停止,或者是 Docker 镜像构建的时候,Java 进程不是一号进程,且没有把 Kill 信号传递给应用的话,那么服务提供者不会主动去注销节点,它会等待注册中心去发现、被动地去感知服务下线的过程。

当微服务注册中心感知到服务下线以后,会通知服务消费者其中一个服务节点已下线,这里有两种方式:注册中心的推送和消费者的轮巡。注册中心刷新服务列表,感知到提供者已经下线一个节点,这一步对于 Dubbo 框架来说不存在,但对于 Spring Cloud 来说,它*差的刷新时间是 30 秒。等消费者的服务列表更新以后,就不再调用下线节点 B。从第 2 步到第 6 步的过程中,注册中心如果是 Eureka,*差的情况需要消耗两分钟;如果是 Nacos,*差的情况需要消耗 50 秒。

在这个时间内请求都有可能出现问题,所以发布的时候会出现各种报错。

%title插图%num

经过上面的分析,在传统的发布流程中,客户端有一个服务端调用报错期,这是由于客户端没有及时感知到服务端下线的实例造成的,这种情况主要是因为服务提供者借助微服务,通知消费者来更新服务提供的列表造成的。

%title插图%num

那能否绕过注册中心,服务提供者直接通知服务消费者?答案是肯定的。SAE 做了两件事情,*,服务提供者在应用发布前,会主动向服务注册中心注销应用,并将应用标记为已下线状态,将原来停止进程阶段的注销变成了 preStop 阶段注销进程。

在接收到服务消费者的请求时,首先会正常处理本次请求,并且通知服务消费者此节点已经下线,在此之后消费者收到通知后,会立即刷新自己的服务列表,在此之后服务消费者就不会再把请求发到服务提供者 B1 的实例上。

通过上面这个方案,就使得下线感知时间大大缩短,从原来的分钟级别做到准实时的,确保你的应用在下线时能够做到业务无损。

3)基于标签的灰度发布

%title插图%num

发布策略分为分批发布和灰度发布,如何实现流量的灰度?从上面的架构图中可以看到,在应用发布之前,要配置一个灰度规则,比如按 uid 的取模余值 =20 来作为灰度流量的规则,当应用发布的时候,会对已发布的节点标识为一个灰度的版本,在这样的情况下,当有流量进来时,微服务网关和消费者都会通过配置中心拿到在治理中心配置的灰度规则。

消费者的 Agent 也会从注册中心拉取它所依赖的服务的一些信息,当一个流量进到消费者时,会按照灰度规则来做匹配,如果是灰度的流量,它会转化到灰度的机器上;如果是正常流量,它会转到正常的机器上,这是基于标签实现的灰度发布的具体逻辑。

3. 运行态实践

1)强大的应用监控 & 诊断能力

%title插图%num

运行态的实例,服务的运行过程中会出现这样或者那样的问题,怎么去排查和解决它?

排查和解决的前提是必须具有强大的应用监控能力和诊断能力,SAE 集成了云产品 ARMS,能够让跑在上面的 Java 微服务看到应用的调用关系拓扑图,可以定位到你的 MySQL 慢服务方法的调用堆栈,进而定位到代码级别的问题。

比如一个请求响应慢,业务出现问题,它可以定位到是哪个请求、哪个服务、服务的哪行代码出现了问题,这样就能为解决问题带来很多便利。总的来说,就是我们要先有监控报警的能力,才能帮助我们更好地诊断服务运营过程中的问题。

2)故障隔离和服务恢复

上面说到我们通过监控、报警来排查、解决遇到的问题,那我们的系统能否主动去做一些事情呢?SAE 作为一个 Serverless 平台,具备很多自运维的能力,下图中有两个场景:

%title插图%num

  • 场景1:某应用运营过程中,某几台机器由于磁盘满或者宿主机资源争抢,导致 load 很高或网络状态差,客户端出现调用超时或者报错。

面对这种情况,SAE 提供了服务治理能力,即离群摘除,它可以配置,当网络超时严重或者后端服务 5xx 报错达到一定比例时,可以选择把该节点从消费端服务列表中摘除,从而使得有问题的机器不再响应业务的请求,很好地保证业务的 SLA。

  • 场景2:某应用运行过程中,因突发流量导致内存耗尽,触发 OOM。

这种情况下,通过 SAE 这种 Serverless 应用引擎,节点在配置健康检查以后,节点里的容器是可以重新拉起的,可以做到快速对进程进行恢复。

3)精准容量+限流降级+*致弹性

%title插图%num

基于 Serverless Paas 平台 SAE 和其他产品的互动,来达到整个运维态的闭环。

用户在使用的时候,可以运用 PTS 压测工具构造场景,然后得出来一些阈值。比如可以对流量高峰所需要消耗的资源进行预估,这时就可以根据这些阈值设计弹性策略。当业务系统达到请求比例时,就可以按照所设置的弹性策略来扩缩容自己的机器。

扩缩容在时间上,有可能还跟不上处理大批量的请求,这时可以通过和 AHAS 的互动,配置限流降级的能力。当有突发大流量时,首先可以用 AHAS 的能力把一些流量挡在门外,然后同时触发 SAE 上应用的扩容策略去扩容实例,当这些实例扩容完成之后,整个机器的平均负载会下降,流量又重新放进来。从突发大流量到限流降级再到扩容,*后到流量达到正常状态,这就是“精准容量+限流降级+*致弹性”的*佳实践模型。

%title插图%num

总结

本文首先按照提出问题、解决问题的思路,阐述微服务在开发、发布和运行态是如何解决问题的;再介绍如何通过 Serverless 产品和其他产品的互动,从而实现精准流量、限流降级和*致弹性。

  • 开发测试态
    • 通过注册中心多租户和网络环境的隔离,并提供环境级别的能力;
    • 通过云端联调技术来快速调式本地变更;
    • 如果 IDE 插件快速部署本地变更。
  • 发布态
    • 运维平台针对应用发布需要具备可灰度、可观测、 可回滚;
    • 通过 MSE agent 能力实现服务无损下线;
    • 通过标签路由提供了线上流量灰度测试的能力。
  • 运行态
    • 建立强大应用监控和诊断能力;
    • 对服务质量差的节点具备离群摘除能力;
    • 对已经不工作的实例通过配置健康检查能够做到实例重启恢复业务;
    • 提供了精准容量+限流降级+*致弹性模型。

 

什么是Serverless?阿里云腾讯云都在发力「无服务器架构」

要说目前软件架构中热度十二分的话题,当属Serverless。

通常我们会将其翻译为“无服务器架构”。

尽管成天被称为“无服务器”,但该架构与传统架构不同,显然并不是真的不需要服务器。

而是选择将服务器等基础设施的管理“隐藏”起来,计算资源作为服务而不是作为服务器的概念出现。

兼具事件触发、短暂以及完全被第三方管理等多重属性,其中开发者只需关注业务逻辑即可。

那一年,也就是2012,TA首次出现在技术人的视野之中。

就在崭露头角之后的短短两年,号称云计算“3A巨头”之一的AWS,就于当年年底正式推出了Lambda 产品,标志着Serverless的商业化进程隆重被开启。

当时的Lambda曾被大家如此描述:这是一种计算服务,可以根据时间来运行用户的代码,无需关心底层的计算资源。

 

从2012年到2014年,Lambda着实不算早到。

但就像云计算PaaS初出茅庐时的说法一样:用户只管业务就好,底层IaaS就交给我们吧!

Serverless与PaaS带给人们的理念是如此惊人的相似。

随后的两年时间内,Google Cloud Function 和微软 Azure Function 在技术圈子的成功,也就顺理成章将 Serverless推进了热化阶段。

从架构变迁聚焦Serverless内涵

对于众多开发者而言,显然仅仅知道“Serverless被定义为无服务器架构”的概念完全不够,如何将Serverless的理解更具象化一些?

恐怕还是要从软件应用架构演进的角度说起。

或许你可能了解,在十几年前,单体应用作为*主流的应用架构形式被广泛认可。

依靠一台服务器外加一个数据库,就能让服务可用性达到峰值状态。

但随着服务器老化性能下降甚至自身损坏的情况,再加上企业业务量的逐渐扩大,单体架构再也不是“一招鲜吃遍天”。

哪怕在流量入口加入负载均衡器,让单体应用可以部署在多台服务器上来增加弹性,也不能完全解决由代码无物理边界所带来的大量冲突。

至此,单体应用架构*次有机会进化成微服务架构,而此时的架构师们也就不得不直面分布式带来的新挑战。

例如那些年的缓存服务 Redis、状态协调服务ZooKeeper、消息服务 Kafka等。

我们可以简单理解为,将一个大系统划分为多个业务模块,其中的业务模块又需要分别部署在不同的服务器上,各个业务模块之间通过接口进行数据交互,这件事儿似乎没那么简单。

当然除了分布式环境的特殊性以外,微服务架构也给运维带来了不小改变。

具体实践中,由于微服务可以部署在不同的服务器上,也可以部署在相同的服务器却不同的容器上,包括应用分发标准、生命周期标准以及自动化弹性等能力在内的重要性也就一一凸显出来。

转眼到了众所周知的云原生时代,业务直接上云不说,还能提供标准化的应用托管服务,包括版本管理、发布、上线后的观测、自愈等,价值红利得到进一步彰显。

而此时Serverless也正迎着这波技术红利闯入了大众的视线,得到关注。

可以看出,在架构的演进中,无论是研发还是运维人员都逐渐将着眼点从机器向平台系统转移,而不是单纯用人去管理,这或许是对于Serverless原理*朴素的阐释。

总结一下,Serverless的出现其实是将主机管理、操作系统管理、资源分配等,甚至是应用逻辑全部组件都集成为服务。

如果将其放在当下的云计算场景中,就不能单纯狭义理解为“不用关心服务器”那么简单,毕竟上云的资源除了服务器之外,还涉及基础计算、存储资源、网络资源等诸多,也包括数据库、缓存以及消息队列等更上层的范畴。

Serverless架构类同FaaS,又做何解?

提及 Serverless,很多人的*反应都是 FaaS+BaaS。

的确,这是 Serverless的一种实现形式,也是一种比较主流的理解。

所谓“FaaS+BaaS ”,其实就是函数即服务与后端即服务的结合体。

具体来说,BaaS(Backend as a Service)可以被解释为“后端即服务”。

一般是API调用后端或别人已经实现好的程序逻辑,通常用来管理数据。

例如,亚马逊RDS可以替代自己部署的MySQL,当然其中还有各种其它数据库、中间件的作用。

FaaS(Functions as a Service)则是函数即服务,作为无服务器计算的一种形式,当前使用*广泛的当属AWS的Lambada。

经过长期实践我们认为,Serverless架构可以提供一种更加“代码碎片化”的软件架构范式,而所谓的“函数”(Function),则是提供相比微服务更加细小的程序单元。

进一步来说,究竟该如何理解“函数即服务”的概念?

大致上是开发者先将函数定义封装在容器中,通过调用函数来实现调用后端存储等服务。

本质上,FaaS是一种事件驱动的由消息触发的服务。

与传统的服务器端软件的不同,经应用程序部署到拥有操作系统的虚拟机或者容器中,一般需要长时间驻留在操作系统中运行。

而FaaS则可以直接将程序部署上到平台上,当有事件到来时触发执行,执行完了就可以消灭。

更重要的一点,FaaS产品不需要对特定框架或库进行编码。

还是以AWS Lambda函数为例,函数可以在Javascript、Python、Go等,也就是任何JVM语言(Java,Clojure,Scala等)或.NET语言中实现;但与此同时,Lambda函数还可执行与其部署工件捆绑在一起的另一个进程。

在FaaS环境中,用户将函数功能代码上传到FaaS提供商,其中对的水平扩展是完全自动弹性的。

而“函数”还可以代表客户所要执行的每个操作,即每个函数完成一个相对简单的业务逻辑,一个完整的应用由若干个函数组成,主要包括创建、读取、更新以及删除等。

 

目前,函数即服务(Function as a Service,FaaS)是当下Serverless实现的技术基础。

因为FaaS和Serverless之间关系密切,所以FaaS的特点也可以被认为是Serverless平台的特点,但如果单纯认为Serverless就是FaaS,就比较狭义了。

BaaS 时代仅仅以 API 的方式提供应用依赖的后端服务;而在 FaaS 时期,用户与开发者不再关注底层,这么说Serverless繁荣也是合理有据的事儿。

使用Serverless,也是一把双刃剑

据实际观察,一直以来企业使用 Serverless 通常会涉及几方面因素,其中“减少运营成本”被认为是*直观有效的原因之一。

的确,应用Serverless后,企业就无需再为潜在的流量高峰买进大部分时间都可能空闲的服务器机架,而是根据流量进行自动伸缩,采用按请求量来付费的灵活方式。

此外“自动按需扩展”可以发挥到*致:随时扩展到当前的使用量,消除了意外或者季节性流量高峰的困扰。

更重要的是,Serverless 不需要关心内存泄露,还具备将云数据库、云消息队列等服务囊括在内的完善配套设施,*大减少工作量。

哪怕企业中大部分的开发人员都出身软件,对修复保护以及管理并不擅长,一样可以做到专注软件开发,Serverless*对没问题。

基于此,一直以来国内外都有很多企业致力于提供基于Serverless 框架的能力服务,接受程度更是水涨船高,简单盘点下,尤其是几家大型的公有云厂商。

例如里程碑式的AWS Lambda。

作为AWS针对Serverless架构推出的FaaS云服务,AWS Lambda自2014年上线以后就受到广泛关注,除了满足大家对Serverless的期望之外,更重要的是AWS平台的成功。

AWS Lambda的优势可以简单总结为:

成熟度高:*个在主流公有云平台上的Serverless FaaS平台,已经有数年的发展和沉淀用户基数大:AWS Lambda有较大的用户基数,参考案例很多活跃的社区:目前开源社区有很多围绕AWS Lambda展开的开源项目AWS的整合:AWS Lambda天然和AWS平台上的服务有良好集成紧随其后,Microsoft Azure也在2016年推出了事件驱动的函数式云计算服务Azure Functions。

其支持用户以多种语言进行函数开发,包括Java、Node.js、PHP、C#、F#、Bash及Microsoft Windows的PowerShell脚本等。

此外,Azure Functions除了提供公有云的版本之外,还提供私有化(On-premises)部署的版本Azure Functions Runtime。

产品功能也是可圈可点:

完整性:Azure Functions是一个功能比较完备的Serverless FaaS平台整合:Azure Functions天然与Azure云平台上各类服务有良好的集成平台:对于使用微软体系产品和工具构建IT能力的企业而言,Azure Functions是Serverless转型的首选平台私有化:提供带有商业支持的私有化部署版本,可满足不同层面的用户的需求同样是在2016年,Google Cloud Platform推出了Google Cloud Functions平台,也同时加入Serverless领域的竞争序列。

同为FaaS平台,Google Cloud Functions与AWS Lambda和Microsoft Azure在功能上*大的区别有啥?

细数以后,可能在于Google Cloud Functions目前仅支持JavaScript作为函数开发语言,运行环境为Node.js。

2018年7月,Google又顺势公布了开源项目Knative,定位为Kubernetes的Serverless插件,推出后得到了Pivotal、IBM以及Red Hat的大力支持。

国外争先恐后,国内也是蜂拥而至。阿里云作为国内*批推出Serverless平台的公有云厂商,其FaaS平台产品被称为阿里云函数计算。

如果从事件触发、支持语言以及用户体验等方面考量,该产品也有很多数据值得关注:

事件触发:阿里云函数计算可以被阿里云上的服务事件触发,例如阿里云对象存储(OSS)支持语言:阿里云函数计算目前支持的开发语言为Node.js,并计划后续将支持Java及Python整个函数代码的部署包大小不能超过50MB,部署包解压后的代码不能超过250MB用户体验:阿里云函数计算提供了基于Web的控制台和SDK;用户可以通过Web控制台管理函数应用,也可以通过交互式的命令行来操作服务规格:一个服务下*多包含50个函数和10个触发器。在运行时,函数*长的运行时间为300s,即5min,一个函数的*大并发数为100同为国内云计算竞争的翘楚,无服务器云函数(Serverless Cloud Function,SCF)是腾讯云推出的函数式计算平台,根据官方的资料,其发布时间是2017年4月26日。

 

总结下腾讯云Serverless平台的特点:

函数运行时:腾讯云SCF目前支持Python、Java及Node.js作为函数的开发语言用户可以以压缩包的形式从本地上传代码,也可以引用腾讯云对象存储中的代码文件事件触发:目前腾讯云SCF支持的事件触发源有腾讯云对象存储COS、定时器、腾讯云消息服务CMQ,以及用户手动通过API及控制台触发服务规格:每个函数将在一个基于CentOS Linux的环境中被执行。函数执行的内存范围为128MB至1536MB,单个区域支持的*大函数定义数量为20个,函数执行的*大时长为300秒,*大的并发数为5以上我们探讨的基本是大型公有云服务商针对Serverless的技术实践。

其实与公有云相比,在私有环境中构建Serverless平台,在技术上并没有什么太多障碍,自然也有不少*的技术尝试,对于此我们会专门成文详细探讨。

可以发现,哪怕是拥有世界范围影响力的公有云服务商针对Serverless的技术探究似乎也出现了缺乏统一认知以及相应标准,无法适应所有的云平台的情况,例如支持的开发语言不同,事件触发的机制有差异等。

毕竟Serverless从来都不是一款产品,也不是一个工具,而是一整套能力的合集。

甚至在实践中还会出现业务轻量化困难、难以在秒级甚至毫秒级别扩容出业务实例;基础设施响应能力不足导致服务发现和日志监控系统等问题。

进而带来大量其他web服务器托管提供商可能会倒闭,很多SaaS平台受到冲击以及运维和实施人员的生存空间进一步缩小等行业现象。

但不容规避的一点,Serverless 架构的兴起使“去服务器化”真正造福了开发者,让基础设施管理出现了新契机。

随着技术上对去中心化以及轻量虚拟化的需求越发强烈,这种“全云化”的模式似乎预示着真正的云时代正在到来,不是吗?
————————————————

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速