QCon 全球软件开发大会(北京站)门票 9 折倒计时 4 天,点击立减 ¥880 了解详情
写点什么

传统框架部署到 Serverless 架构的利与弊

2020 年 6 月 05 日

传统框架部署到Serverless架构的利与弊

Serverless 是一个比较新的概念、架构,让开发者放弃之前的开发习惯、放弃现有的 Express、Koa、Flask、Django 等框架,无缝转向 Serverless 架构,显然是不可能的,必须得有一段过渡和适应的时间。在这段时间内,开发者需要思考是否可以将现有的框架部署到 Serverless 架构上?如果要部署,如何才能顺利上云呢?


Web 框架在 Serverless 上的表现

首先,我们以 Flask 框架进行一个简单的测试:


测试四种接口:


  • Get 请求(可能涉及到通过路径传递参数)

  • Post 请求(通过 Formdata 传递参数)

  • Get 请求(通过 url 参数进行参数传递)

  • Get 请求(带有 jieba 等计算功能)


测试两种情况:


  • 本地表现

  • 通过 Flask-Component 部署表现


测试两种性能:


  • 传统云服务器上的性能表现

  • 云函数性能表现


测试代码如下:


from flask import Flask, redirect, url_for, requestimport jiebaimport jieba.analyse
app = Flask(__name__)

@app.route('/hello/<name>')def success(name): return 'hello %s' % name

@app.route('/welcome/post', methods=['POST'])def welcome_post(): user = request.form['name'] return 'POST %s' % user

@app.route('/welcome/get', methods=['GET'])def welcome_get(): user = request.args.get('name') return 'GET %s' % user

@app.route('/jieba/', methods=['GET'])def jieba_test(): str = "Serverless Framework 是业界非常受欢迎的无服务器应用框架,开发者无需关心底层资源即可部署完整可用的 Serverless 应用架构。Serverless Framework 具有资源编排、自动伸缩、事件驱动等能力,覆盖编码、调试、测试、部署等全生命周期,帮助开发者通过联动云资源,迅速构建 Serverless 应用。" print(", ".join(jieba.cut(str))) print(jieba.analyse.extract_tags(str, topK=20, withWeight=False, allowPOS=())) print(jieba.analyse.textrank(str, topK=20, withWeight=False, allowPOS=('ns', 'n', 'vn', 'v'))) return 'success'

if __name__ == '__main__': app.run(debug=True)
复制代码


这段测试代码是比较有趣的,它包括了最常用的请求方法、传参方法,同时还囊括了简单的接口和稍微复杂的接口。


本地表现

本地运行之后,通过 Postman 进行三个接口简单测试:


  • Get 请求:



  • Post 参数传递:



  • Get 参数传递:



通过 Flask-Component 部署表现

接下来,我们将这个代码部署到云函数中:


Flask-Component 的部署操作,可以参考 Tencent 给予的文档,Github 地址https://github.com/serverless-components/tencent-flask


Yaml 文档内容:


FlaskComponent:  component: '@gosls/tencent-flask'  inputs:    region: ap-beijing    functionName: Flask_Component    code: ./flask_component    functionConf:      timeout: 10      memorySize: 128      environment:        variables:          TEST: vale      vpcConfig:        subnetId: ''        vpcId: ''    apigatewayConf:      protocols:        - http      environment: release
复制代码


部署完成



接下来测试三个目标接口


  • Get 通过路径传参:



  • Post 参数传递:



  • Get 参数传递:



从上面的测试,我们可以看出,通过 Flask-Component 部署的云函数同样可以具备常用的几种请求形式和传参形式。一般情况下,用户的 Flask 项目可以直接通过腾讯云提供的 Flask-component 快速部署到 Serverless 架构上,并获得比较良好的运行。


简单的性能测试

接下来我们对性能进行一些简单的测试,首先购买一个云服务器,将这个部分代码部署到云服务器上。


首先,我们购买了 1 核 2G 的云服务器



配置环境,使得服务可以正常运行:




通过 Post 设置简单的 Tests:



对接口进行测试:



完成接口测试:



通过接口测试结果进行部分可视化:



统计数据:



通过上面的图表,我们可以看到服务器的整体响应时间都快于云函数的响应时间,同时函数是存在冷启动的,一旦出现冷启动,其响应时间会增长 20 余倍。


接下来,我们再测试一下稍微复杂的接口


在由于上述测试,仅仅是非常简单的接口,接下来我们来测试一下稍微复杂的接口,使用了 jieba 分词的接口。


测试结果:



可视化结果:




根据对 Jieba 接口的测试,我们发现虽然服务器也会有因分词组件进行初始化而产生比较慢的响应时间,但是整体而言,速度依旧是远远低于云函数。


那么问题来了,这是函数本身的性能有问题,还是增加了 Flask 框架+APIGW 响应集成之后才有问题?


接下来,我们做一组新的接口测试,在函数中,直接返回内容,不进行额外处理,看看函数+API 网关性能和正常情况下的服务器性能对比




从上图我们可以看出最小和平均耗时的区别不是很大,最大耗时基本上是持平,框架的加载会导致函数冷启动时间长度变得异常可怕。


通过 Python 代码,我们对 Flask 框架进行并发测试:


对函数进行 3 次压测,每次并发 300:


===========task end===========total:301,succ:301,fail:0,except:0response maxtime: 1.2727971077response mintime 0.573610067368
===========task end===========total:301,succ:301,fail:0,except:0response maxtime: 1.1745698452response mintime 0.172255039215
===========task end===========total:301,succ:301,fail:0,except:0response maxtime: 1.2857568264response mintime 0.157210826874
复制代码


对服务器进行 3 次压测,同样是每次并发 300:


===========task end===========total:301,succ:301,fail:0,except:0response maxtime: 3.41151213646response mintime 0.255661010742
===========task end===========total:301,succ:301,fail:0,except:0response maxtime: 3.37784004211response mintime 0.212490081787
===========task end===========total:301,succ:301,fail:0,except:0response maxtime: 3.39548277855response mintime 0.439364910126
复制代码


这一波压测,我们可以看到了一个奇怪现象:在函数和服务器预热完成之后,连续三次并发 301 个请求,函数的整体表现反而比服务器的要好。这说明在 Serverless 架构下,弹性伸缩发挥了重要作用。传统服务器,如果出现了高并发现象,很容易会导致整体服务受到严重影响,例如响应时间变长,无响应,甚至是服务器直接挂掉,但是在 Serverless 架构下,具备弹性伸缩能力,因此当并发量达到一定的时候,优势就会凸显出来。


传统 Web 框架上云方法(以 Python Web 框架为例)

分析已有 Component(Flask 为例)

首先,我们要知道其他的框架是怎么运行的,例如 Flask 等。我们先按照腾讯云的 Flask-Component 的说明部署一下:



部署上线之后,在函数的控制台把部署好的下载下来:



下载解压之后,我们可以看到这样一个目录结构:



蓝色框中的是依赖包,黄色的 app.py 是我们自己写的代码,而红色框中的是什么?


api_server.py 文件内容:


import app  # Replace with your actual applicationimport severless_wsgi
# If you need to send additional content types as text, add then directly# to the whitelist:## serverless_wsgi.TEXT_MIME_TYPES.append("application/custom+json")
def handler(event, context): return severless_wsgi.handle_request(app.app, event, context)
复制代码


可以看到,这是将我们创建的 app.py 文件引入,并且拿到了 app 对象,将 event 和 context 同时传递给 severless_wsgi.py 中的 handle_reques 方法中,那么问题来了,这个方法是什么?



再观察一下这段代码:



实际上,这一段代码就是将我们拿到的参数(event 和 context)进行转换,转换之后统一到 environ 中,通过 werkzeug 依赖将其变成 request 对象,并与 app 对象一起调用 from_app 方法。



按照 API 网关的响应集成的格式,将结果返回。


相信执行到这里,大家可能就有所感悟了,我们再看一下 Flask/Django 这些框架的实现原理:



根据简版原理图,相信大家都明白正常用的时候要通过 web_server,进入到下一个环节,而云函数更多是一个函数,不需要启动 web server,所以可以直接调用 wsgi_app 方法,其中 environ 就是对 event/context 等进行处理后的对象,start_response 可以认为是一种特殊的数据结构,例如 response 结构形态等。


因此,如果我们想要自己动手实现这个过程,可以这样做:


import sys
try: from urllib import urlencodeexcept ImportError: from urllib.parse import urlencode
from flask import Flask
try: from cStringIO import StringIOexcept ImportError: try: from StringIO import StringIO except ImportError: from io import StringIO
from werkzeug.wrappers import BaseRequest
__version__ = '0.0.4'

def make_environ(event): environ = {} for hdr_name, hdr_value in event['headers'].items(): hdr_name = hdr_name.replace('-', '_').upper() if hdr_name in ['CONTENT_TYPE', 'CONTENT_LENGTH']: environ[hdr_name] = hdr_value continue
http_hdr_name = 'HTTP_%s' % hdr_name environ[http_hdr_name] = hdr_value
apigateway_qs = event['queryStringParameters'] request_qs = event['queryString'] qs = apigateway_qs.copy() qs.update(request_qs)
body = '' if 'body' in event: body = event['body']
environ['REQUEST_METHOD'] = event['httpMethod'] environ['PATH_INFO'] = event['path'] environ['QUERY_STRING'] = urlencode(qs) if qs else '' environ['REMOTE_ADDR'] = 80 environ['HOST'] = event['headers']['host'] environ['SCRIPT_NAME'] = '' environ['SERVER_PORT'] = 80 environ['SERVER_PROTOCOL'] = 'HTTP/1.1' environ['CONTENT_LENGTH'] = str(len(body)) environ['wsgi.url_scheme'] = '' environ['wsgi.input'] = StringIO(body) environ['wsgi.version'] = (1, 0) environ['wsgi.errors'] = sys.stderr environ['wsgi.multithread'] = False environ['wsgi.run_once'] = True environ['wsgi.multiprocess'] = False
BaseRequest(environ)
return environ

class LambdaResponse(object): def __init__(self): self.status = None self.response_headers = None
def start_response(self, status, response_headers, exc_info=None): self.status = int(status[:3]) self.response_headers = dict(response_headers)

class FlaskLambda(Flask): def __call__(self, event, context): if 'httpMethod' not in event: print('httpMethod not in event') return super(FlaskLambda, self).__call__(event, context)
response = LambdaResponse()
body = next(self.wsgi_app( make_environ(event), response.start_response ))
return { 'statusCode': response.status, 'headers': response.response_headers, 'body': body }

复制代码


整个实现过程可以认为是对 web server 部分进行了一种“截断”或者是“替换”:



以上就是对 Flask-Component 的基本分析思路,按照这个思路我们是否可以将 Django 框架也部署在 Serverless 架构上呢?Flask 和 Django 有什么区别呢?(特指运行启动过程)


拓展思路:实现 Django-component

我们是否可以直接使用 Flask 的转换逻辑,将 flask 的 app 替换成 django 的 app?


把:


from flask import Flaskapp = Flask(__name__)
复制代码


替换成:


import osfrom django.core.wsgi import get_wsgi_applicationos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mydjango.settings')application = get_wsgi_application()
复制代码


替换完成,我们来测试一下:



建立好 Django 项目,直接增加 index.py:


# -*- coding: utf-8 -*-
import osimport sysimport base64from werkzeug.datastructures import Headers, MultiDictfrom werkzeug.wrappers import Responsefrom werkzeug.urls import url_encode, url_unquotefrom werkzeug.http import HTTP_STATUS_CODESfrom werkzeug._compat import BytesIO, string_types, to_bytes, wsgi_encoding_danceimport mydjango.wsgi
TEXT_MIME_TYPES = [ "application/json", "application/javascript", "application/xml", "application/vnd.api+json", "image/svg+xml",]

def all_casings(input_string): if not input_string: yield "" else: first = input_string[:1] if first.lower() == first.upper(): for sub_casing in all_casings(input_string[1:]): yield first + sub_casing else: for sub_casing in all_casings(input_string[1:]): yield first.lower() + sub_casing yield first.upper() + sub_casing

def split_headers(headers): """ If there are multiple occurrences of headers, create case-mutated variations in order to pass them through APIGW. This is a hack that's currently needed. See: https://github.com/logandk/serverless-wsgi/issues/11 Source: https://github.com/Miserlou/Zappa/blob/master/zappa/middleware.py """ new_headers = {}
for key in headers.keys(): values = headers.get_all(key) if len(values) > 1: for value, casing in zip(values, all_casings(key)): new_headers[casing] = value elif len(values) == 1: new_headers[key] = values[0]
return new_headers

def group_headers(headers): new_headers = {}
for key in headers.keys(): new_headers[key] = headers.get_all(key)
return new_headers

def encode_query_string(event): multi = event.get(u"multiValueQueryStringParameters") if multi: return url_encode(MultiDict((i, j) for i in multi for j in multi[i])) else: return url_encode(event.get(u"queryString") or {})

def handle_request(application, event, context):
if u"multiValueHeaders" in event: headers = Headers(event["multiValueHeaders"]) else: headers = Headers(event["headers"])
strip_stage_path = os.environ.get("STRIP_STAGE_PATH", "").lower().strip() in [ "yes", "y", "true", "t", "1", ] if u"apigw.tencentcs.com" in headers.get(u"Host", u"") and not strip_stage_path: script_name = "/{}".format(event["requestContext"].get(u"stage", "")) else: script_name = ""
path_info = event["path"] base_path = os.environ.get("API_GATEWAY_BASE_PATH") if base_path: script_name = "/" + base_path
if path_info.startswith(script_name): path_info = path_info[len(script_name) :] or "/"
if u"body" in event: body = event[u"body"] or "" else: body = ""
if event.get("isBase64Encoded", False): body = base64.b64decode(body) if isinstance(body, string_types): body = to_bytes(body, charset="utf-8")
environ = { "CONTENT_LENGTH": str(len(body)), "CONTENT_TYPE": headers.get(u"Content-Type", ""), "PATH_INFO": url_unquote(path_info), "QUERY_STRING": encode_query_string(event), "REMOTE_ADDR": event["requestContext"] .get(u"identity", {}) .get(u"sourceIp", ""), "REMOTE_USER": event["requestContext"] .get(u"authorizer", {}) .get(u"principalId", ""), "REQUEST_METHOD": event["httpMethod"], "SCRIPT_NAME": script_name, "SERVER_NAME": headers.get(u"Host", "lambda"), "SERVER_PORT": headers.get(u"X-Forwarded-Port", "80"), "SERVER_PROTOCOL": "HTTP/1.1", "wsgi.errors": sys.stderr, "wsgi.input": BytesIO(body), "wsgi.multiprocess": False, "wsgi.multithread": False, "wsgi.run_once": False, "wsgi.url_scheme": headers.get(u"X-Forwarded-Proto", "http"), "wsgi.version": (1, 0), "serverless.authorizer": event["requestContext"].get(u"authorizer"), "serverless.event": event, "serverless.context": context, # TODO: Deprecate the following entries, as they do not comply with the WSGI # spec. For custom variables, the spec says: # # Finally, the environ dictionary may also contain server-defined variables. # These variables should be named using only lower-case letters, numbers, dots, # and underscores, and should be prefixed with a name that is unique to the # defining server or gateway. "API_GATEWAY_AUTHORIZER": event["requestContext"].get(u"authorizer"), "event": event, "context": context, }
for key, value in environ.items(): if isinstance(value, string_types): environ[key] = wsgi_encoding_dance(value)
for key, value in headers.items(): key = "HTTP_" + key.upper().replace("-", "_") if key not in ("HTTP_CONTENT_TYPE", "HTTP_CONTENT_LENGTH"): environ[key] = value
response = Response.from_app(application, environ)
returndict = {u"statusCode": response.status_code}
if u"multiValueHeaders" in event: returndict["multiValueHeaders"] = group_headers(response.headers) else: returndict["headers"] = split_headers(response.headers)
if event.get("requestContext").get("elb"): # If the request comes from ALB we need to add a status description returndict["statusDescription"] = u"%d %s" % ( response.status_code, HTTP_STATUS_CODES[response.status_code], )
if response.data: mimetype = response.mimetype or "text/plain" if ( mimetype.startswith("text/") or mimetype in TEXT_MIME_TYPES ) and not response.headers.get("Content-Encoding", ""): returndict["body"] = response.get_data(as_text=True) returndict["isBase64Encoded"] = False else: returndict["body"] = base64.b64encode(response.data).decode("utf-8") returndict["isBase64Encoded"] = True
return returndict


def main_handler(event, context): return handle_request(mydjango.wsgi.application, event, context)
复制代码


将其部署到函数上,看一下效果:


函数信息:


from django.shortcuts import renderfrom django.http import HttpResponsefrom django.views.decorators.csrf import csrf_exempt
# Create your views here.@csrf_exemptdef hello(request): if request.method == "POST": return HttpResponse("Hello world ! " + request.POST.get("name")) if request.method == "GET": return HttpResponse("Hello world ! " + request.GET.get("name"))

复制代码


部署完成,绑定 apigw 触发器,并在 postman 中进行测试:


get:



post:



通过对运行原理的基本剖析和对 django 的改造,我们已经通过增加一个文件和相关依赖的方法,成功将 Django 搬上了 Serverless。


接下来,我们看一下,如何将代码写成一个 Component:


首先 Clone 下来 Flask-Component 的代码:



按照 Django 的部分模式进行修改:



第一部分,是我们可能会依赖的一个依赖包,以及刚才放入的 index.py 文件。在用户调用这个 Component 的时候,我们会把这两个文件放入用户的代码中,一并上传。


第二部分是 Serverless.js,下面是一个基本格式:


const { Component } = require('@serverless/core')class TencentDjango extends Component {  async default(inputs = {}) {  }  async remove(inputs = {}) {  }}module.exports = TencentDjango
复制代码


用户在执行 sls 时会默认调用 default 方法,在执行 sls remove 时会调用 remove 方法,所以可以认为 default 的内容是部署,而 remove 的内容是移除。


主要流程的部署也很简单,首先将文件进行复制和处理,然后直接调用云函数的组件,通过函数中的 include 参数将这些文件额外加入,再通过调用 apigw 的组件来进网关的管理。用户写的 yaml 中 inpust 的内容会在 inputs 中获取,我们要做的就是对应的传给不同的组件:



除了将这两部分对应部署,region 等一些信息也要进行对应处理。调用底层组件方法也很简单:


const tencentCloudFunction = await this.load('@serverless/tencent-scf'const tencentCloudFunctionOutputs = await tencentCloudFunction(inputs)
复制代码


处理好之后,只需要修改一下 package.json 和 readme 就可以了。


至此,我们完成了一个 Django Component 的开发,发布到 NPM 后,在使用时只需要引入这个 Component 即可:


DjangoTest:  component: '@serverless/tencent-django'  inputs:    region: ap-guangzhou    functionName: DjangoFunctionTest    djangoProjectName: mydjango    code: ./    functionConf:      timeout: 10      memorySize: 256      environment:        variables:          TEST: vale      vpcConfig:        subnetId: ''        vpcId: ''    apigatewayConf:      protocols:        - http      environment: release

复制代码


总结:

Flask 可以通过很简单的方法部署在 Serverless 架构上,用户基本可以按照原生 Flask 开发习惯来开发 Flask 项目,尤其是使用 Flask 开发接口服务的项目。


整体框架迁移上 Serverless 架构有几个需要额外注意的点:


  1. 如果接口比较多,需要按照资源消耗比较大的那个接口来设置内存大小。如果是按照文章中的例子,非 jieba 接口使用时的最小内存(64M),而 jieba 接口却需要 256M 的内存。因为项目是一体的,只能设置一个内存,所以为了保证项目可用性,应该整体设置为 256M 的内存,这样一来在另外三个接口访问比较多的前提下,资源消耗可能会相对增加比较大,有条件的话,可考虑将资源消耗比较大的接口额外提取出来;

  2. 云函数+API 网关的组合对静态资源以及文件上传等的支持并不是十分友好,尤其是云函数+API 网关的双重收费,所以建议将 Flask 中的静态资源统一放在对象存储中,同时将文件上传逻辑修改成优先上传到对象存储中。


框架越大、框架内的资源越多,函数冷启动的时间就会越大。在上文的测试过程中,非框架下,最高耗时是平均耗时的 3 倍,而在加载 Flask 框架和 Jieba 的前提下,最高耗时是平均的 10+倍!如果能保证函数都是热启动还好,一旦出现冷启动,就会有一定的影响。


由于用户发起请求是客户端到 API 网关再到函数,然后从函数回到 API 网关,再回到客户端。相对于直接访问服务器获得结果,这个过程明显链路更长,所以在实际测试过程中,用户量较少的时候,表现不是很好,几次测试基本上都是 1 核 2G 的服务器优于函数。但当并发量上来的之后,函数的表现实现了大超车。因此,我们可以得出一个有趣的结论:对于极小规模请求,函数是按量付费,在性能上有一定的劣势,但在价格上有一定的优势;当流量逐渐变大之后,函数在性能上的优势也逐渐凸显。


除了对传统 Web 框架部署到 Serverless 架构的利弊分析之外,通过对 Flask 框架进行分析,我们可以总结出 Web 框架搬上 Serverless 架构的原理思路,虽然说 Serverless 团队很活跃,各个云厂商也很活跃,但在实际生产中,必然会用到很多定制化组件,Serverless Component 本身就是组件化的产品,如果深度使用就可以定制化开发 Component 满足自身需求,这时候就要知道一些原理和技巧。


我相信,Serverless 架构虽然目前还存在一些问题,但一定会随着时间发展得越发成熟!


2020 年 6 月 05 日 08:463394
用户头像
田晓旭 InfoQ 编辑

发布了 480 篇内容, 共 220.2 次阅读, 收获喜欢 1515 次。

关注

评论

发布
暂无评论
发现更多内容

GO 训练营第 3 周总结

Glowry

区块链技术生态持续优化,五大趋势不容忽视

CECBC区块链专委会

区块链 场景应用

Spock单元测试框架实战指南五 - void方法测试

Java老k

Java 单元测试 spock

Alibaba Java面试题大揭秘,把这些知识点吃透去面试成功率高达100%

Java架构之路

Java 程序员 架构 面试 编程语言

4项探索+4项实践,带你了解华为云视觉预训练研发技术

华为云开发者社区

AI 华为云 modelarts

Flutter技术在会展云中大显身手

京东科技开发者

flutter 跨平台 移动开发

话题讨论 | 对于懂得编程的人来说,编程对你来说有什么乐趣?编程大概是什么感觉?

xcbeyond

话题讨论

架构作业--大数据

Nick~毓

LeetCode题解:45. 跳跃游戏 II,贪心从后向前,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

C++typename的由来和用法

良知犹存

c++

摄像机不智能,基本等于不讲武德

脑极体

Java并发编程:进程、线程、并行与并发

码农架构

Java并发

某Javva程序员金秋9月靠这份文档涨薪10K,你把这份Java进阶文档吃透涨薪超简单!

Java架构之路

Java 程序员 架构 面试 编程语言

卧槽,牛皮了!某程序员苦刷这两份算法PDF47天,四面字节斩获心仪大厂offer!

Java架构之路

Java 程序员 架构 面试 编程语言

LeetCode题解:102. 二叉树的层序遍历,BFS,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

生产环境全链路压测建设历程第四篇 技术体系的发力

数列科技杨德华

中国SaaS的病与痛?

ToB行业头条

华为工程师:扔掉你手里的其他Netty资料吧,有这份足以

小Q

Java 学习 面试 Netty 网络

为什么删除数据后,Redis内存占用依然很高?

Java架构师迁哥

只谈链不谈币,区块链会发展成什么样的方向?

CECBC区块链专委会

区块链

Mock | 拦截ajax的两种实现方式

梁龙先森

Java 前端 前端进阶

如何使用 JuiceFS 在云上优化 Kylin 4.0 的存储性能?

苏锐

大数据 kylin 性能优化 JuiceFS

Norns.Urd 中的一些设计

八苦-瞿昙

C# 随笔 随笔杂谈 aop

密码学系列之:明文攻击和Bletchley Park

程序那些事

加密解密 密码学 程序那些事 明文攻击

学习笔记3

Qx

最值得Deepin的思维模型“组合创新” | 技术人应知的创新思维模型 (3)

Alan

创新 思维模型 28天写作

区块链打破数字医疗桎梏,赢数据未来新生

CECBC区块链专委会

区块链 医疗

小熊派开发实践丨小熊派+合宙Cat.1接入云服务器

华为云开发者社区

IoT 小熊派 实践

测试右移之日志收集与监控

BY林子

敏捷 软件测试

话题讨论 | 聊聊那些年你重构过的代码?

xcbeyond

话题讨论

四面阿里终于如愿拿到P7级offer【Java岗】,分享面经与面试资料

Crud的程序员

Java 程序员 java面试

边缘计算隔离技术的挑战与实践

边缘计算隔离技术的挑战与实践

传统框架部署到Serverless架构的利与弊-InfoQ