first commit
This commit is contained in:
93
专栏/Serverless进阶实战课/00开篇词Serverless是降本增效浪潮下的必然选择.md
Normal file
93
专栏/Serverless进阶实战课/00开篇词Serverless是降本增效浪潮下的必然选择.md
Normal file
@ -0,0 +1,93 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
00 开篇词 Serverless是降本增效浪潮下的必然选择
|
||||
你好,我是静远。
|
||||
|
||||
先和你简单介绍一下自己。我现在是一家互联网公司的云原生资深技术专家,负责Serverless相关的业务和技术,主要为公有云和私有化的客户提供Serverless相关的落地解决方案,同时,我也是中国信通院在Serverless领域的标准专家。
|
||||
|
||||
伴随着云原生的日渐火热,Serverless作为云原生架构的核心组成部分,现在已然成为近年来技术圈内的当红炸子鸡。到底有多火呢?我们来看看下面的数据。
|
||||
|
||||
|
||||
Datadog的数据显示,在使用云服务的机构和组织中,有半数以上采用了Serverless技术。信通院2021年的最新报告也显示:在核心业务中使用Serverless的用户占到18.11%,已经开始和计划使用Serverless技术的用户超过了70%。
|
||||
|
||||
|
||||
|
||||
|
||||
目前,行业内云原生、Kubernetes、容器工程师供不应求。各一线大厂都在加急招聘云原生工程师,在这种情况下,让自己成为会Serverless、懂Serverless的工程师,能够助力我们弯道超车,向目前大热的云原生技术要红利。
|
||||
|
||||
让人“又爱又恨”的Serverless
|
||||
|
||||
Serverless的持续升温,得益于它在“快速的开发交付”“极高的运维效率”“极低的资源成本”这三个方面上的优势。
|
||||
|
||||
从容器、应用、函数等不同维度,Serverless可以让自己的业务更快上云,让我们享受云带来的便利性。把管理机器资源、网络资源这些繁琐的事情交给更专业的平台去处理,更加专注于业务和产品自身,进而更好地提升产品和业务的核心竞争力,让专业的人做专业的事。最后,也可以逃脱被产品挑战“这个功能这么简单,要这么长的时间才能开发完吗?”的质疑。
|
||||
|
||||
虽说Serverless有这么大的优势,经历过这几年的发展,关于Serverless的公众号、书籍和社区资料也不少,但真正落地使用的时候,不管是客户、团队的同学还是我,都还像是在摸着石头过河。
|
||||
|
||||
经历了N个令人头秃的夜晚后,我才发现,Serverless不断延伸发展的特性,极大地提升了我们学习和使用的难度。
|
||||
|
||||
所以接下来,我们就一块来看看,怎么从Serverless不断发展的“变”中去把握“不变”,在Serverless领域快速的成长进阶。
|
||||
|
||||
课程设计
|
||||
|
||||
我把整个课程设计为了“核心技术”“拓展能力”“实战进阶”三大模块,希望通过循序渐进的方式让你一步一步成为Serverless领域的专家。
|
||||
|
||||
导读
|
||||
|
||||
在课程开始,我会详细的介绍Serverless的前世今生,在学习之前把我和团队同学总结的成长路径分享给你。同时,我会结合中国信通院最新的标准,把Serverless的完整解决方案平铺在你面前。最后,我会讲解一下在Serverless道路上的学习方法,让你升级打怪的道路更加平坦,最终成为王者。
|
||||
|
||||
核心技术
|
||||
|
||||
这一模块,我会通过具象的FaaS产品和技术,将抽象的Serverless的核心要点梳理清楚,包括触发器、冷启动、扩缩容、流量转发、运行时等核心技术的实现机制。同时,我也会在原理中穿插案例和一线经验,让你能够即学即用,拨开云雾见青天。
|
||||
|
||||
有了核心技术的储备,就好像一棵大树有了“主干”。当你明白了Serverless的架构原理、核心特性以及技术细节之后,对设计一个同样Serverless形态的云计算产品就能做到心中有谱了。
|
||||
|
||||
拓展能力
|
||||
|
||||
这一模块我会讲解Serverless的延伸能力,让你了解Serverless的整体解决方案。它包括如何将WebIDE的能力在FaaS中具体实施起来、如何构建多任务的编排能力、如何构建多维度的Serverless可观测体系等6个小节。
|
||||
|
||||
这一模块的内容,相当于Serverless这棵大树的枝叶。如果之后让你主导一个新的Serverless形态的云原生产品的技术架构设计工作,你应该也能触类旁通,游刃有余了。我想,你离架构师的目标就进了一步。
|
||||
|
||||
实战进阶
|
||||
|
||||
通过前面几个模块的学习以及贯穿在课程中的实操,一般的场景实战和一线问题应该难不倒你了。
|
||||
|
||||
但纸上得来终觉浅,在实战进阶中,我会和你分享我作为业务方和平台方两种角色下的亲身经历,跟你聊聊一线的经验,也会通过几个典型的案例,帮你把前面学到的知识点用“活”起来。
|
||||
|
||||
另外,Serverless的发展离不开开源生态和社区的贡献。在这里,我还会跟你一起探讨开源引擎的那些事,并且结合我和客户、用户打交道总结下来的经验,分享一下落地思考的过程,并以如何基于核心引擎构建属于自己的Serverless平台作为这个模块的结束,也是整个专栏的升华。
|
||||
|
||||
通过这样“三位一体”的学习路径,我更希望你能领会到Serverless的“意”,而不是“形”,因为随着Serverless的发展越来越热,云上的产品也会越来越丰富。
|
||||
|
||||
如果你是Serverless平台的使用者,通过专栏的学习,一定会在应用上更加得心应手,知其然,也知其所以然。如果你是Serverless平台的开发者,通过专栏的学习,在云原生产品的开发和创新上,你的思路也一定会越来越开阔。
|
||||
|
||||
|
||||
|
||||
在你踏上Serverless的学习进阶之路之前,我再送你一张课程的知识地图,希望在接下来的学习中,它能作为你提前预习和课后复习的知识脉络,帮助你将以FaaS为起点的Serverless的技术融会贯通。
|
||||
|
||||
写在最后
|
||||
|
||||
这门课我最终选择了以FaaS形态的Serverless为切入点,来跟你剖析这里面的核心技术、拓展能力和实战经验。
|
||||
|
||||
为什么会这么选择呢?给你分享一下我的想法,当然,也欢迎你在留言区和我一块讨论。
|
||||
|
||||
Serverless现在还在快速成长的阶段,功能、产品、面向的领域也在延伸。想要在这个快速迭代、动态发展的技术领域里构建自己的知识体系实在是难上加难。况且,Serverless发展到今天,已经不是一两个明确的技术或者产品了,它包含着一整套的产品体系和技术理念。
|
||||
|
||||
我们可以从两个层面来看待。一个层面是理论学术层面,包括FaaS(Function as a Service)和BaaS(Backend as a Service),其中BaaS有Serverless化的消息中间件、对象存储、数据库等。另一个层面是产品技术层面,包括函数计算FaaS、弹性应用托管服务、弹性的容器服务等产品,以及一整套配套的工具链。
|
||||
|
||||
试想一下,难道我们要每拓展一个产品、功能或者领域,就要从0开始学习吗?
|
||||
|
||||
那么,我们是不是能找到一个较为成熟的切入点,在了解技术本身的同时,掌握学习它的方法呢?
|
||||
|
||||
FaaS作为Serverless意识形态的“元老”级别产品,从AWS Lambda发布,再到众多厂商开始紧跟潮流,发展到目前已经有8个多年头了。自定义运行时、自定义镜像、编排、应用中心等也都是基于FaaS打造和延伸的功能。因此,我觉得FaaS形态的Serverless是非常好的入手选择。
|
||||
|
||||
希望这样的聚焦方式,能够帮你养成举一反三的习惯和能力。这几年和Serverless打交道的一线经验、解决的问题,以及平台设计上抽象出来的方法让我获益良多,我希望这些经验也能够以案例和实战相结合的方式交到你手上,让我们一起推进Serverless向前迈进。
|
||||
|
||||
在伯克利大学发表的新论文中,预言了 Serverless 将主导云计算下一个十年的发展。我坚信,通过这样的方式,你一定可以成为Serverless的一员干将,在未来的十年里站得更稳。
|
||||
|
||||
接下来,让我们一起开启Serverless的学习之旅吧!
|
||||
|
||||
|
||||
|
||||
|
173
专栏/Serverless进阶实战课/00思维构建如何在新赛道下进阶Serverless能力?.md
Normal file
173
专栏/Serverless进阶实战课/00思维构建如何在新赛道下进阶Serverless能力?.md
Normal file
@ -0,0 +1,173 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
00 思维构建 如何在新赛道下进阶Serverless能力?
|
||||
你好,我是静远。
|
||||
|
||||
Serverless发展到现今,大部分的技术人已经对 Serverless 将引领云计算的下一个十年达成了共识。作为一名技术研发人员,“Serverless”对你来说大概率并不陌生,甚至,你已经在不知不觉中用上了,但是,它的快速发展也让我们很难对它形成一个较为全面的认知。
|
||||
|
||||
那么这节课,我们就一起了解Serverless的前世今生,重构Serverless思维,并选择一个具象化的Serverless形态作为切入点,开启后续的学习。最后,我也会和你探讨后续学习“升级打怪”的各个阶段,让我们的进阶目标更加清晰。
|
||||
|
||||
新赛道:Serverless的具象化表达
|
||||
|
||||
简单来说,Serverless是一种架构设计的理念,它不是一个具体的编程框架、类库或者工具。随着大家对于Serverless理解的深入,用词也越来越精准、具象。从之前“无服务器”的直译到目前“服务器无感知”的理解,是各界对Serverless这几年发展和使用的一个跨度的体现。
|
||||
|
||||
我们从头看一遍这个新赛道的发展历程。用Martin Fowler的观点来说,Serverless是FaaS和BaaS的组合,这个简单明了的定义,奠定了 Serverless 组成结构的基础。
|
||||
|
||||
CNCF在认定这个理念的同时,也在它的白皮书V1.0中对 Serverless 架构的定义有了进一步地完善描述,它指出:Serverless Computing 是构建和运行不需要服务器管理的应用程序。它描述了一种更细粒度的部署模型,将一个或多个功能的应用程序打包上传到Serverless平台,然后根据实际的需求,执行、扩展和计费。
|
||||
|
||||
我们发现,这个描述从执行路径和更细的角度进行了具象化的表述。
|
||||
|
||||
在2022年7月的云原生产业大会上,信通院发布的最新的云计算白皮书针对Serverless的描述中提到,“通过多种服务器无感知技术,将基础设施抽象成各种开箱即用的服务,以API接口的方式提供给用户按需调用,真正做到按需伸缩、按使用量收费”,又进一步从业务和基础设施服务的协作角度,凸显了Serverless的特性。
|
||||
|
||||
我沿用信通院白皮书的话来体现这种思想,也希望我们能够达成一个共识:“Serverless是能够实现业务和基础设施分离,提升用户业务创新能力的理念”。
|
||||
|
||||
发展历程:从单一到多元
|
||||
|
||||
除了理念上的落地,我们再说一下Serverless的技术和应用场景向多元化发展的历程。以史为镜可以知兴替,理解Serverless多元化的历程,才能更完整地构建你的Serverless思维。
|
||||
|
||||
雏形阶段
|
||||
|
||||
国外可以追溯到2006年,Fotango公司推出的PaaS平台Zimki,提出了按需使用、按量付费的概念,从那时Serverless的核心特征就已见雏形了。到2012年,基础设施服务提供商Iron.io的副总裁Ken在《Why The Future of Software and Apps is Serverless》中首次提到“Serverless”这个词,它正式走进历史的舞台。我们可以认为,这一阶段就是Serverless发展的“雏形阶段”。
|
||||
|
||||
发展阶段
|
||||
|
||||
AWS 于2014年发布第一款函数计算产品 —— Lambda预览版,并于2015年正式商用,使用量随后一路飙升,随后,IMB、Google、Micosoft也于2016年相继发布FaaS产品,开源项目也同时出现,这里包括比较知名的OpenWisk、Fission、Kubeless、OpenFaaS等。
|
||||
|
||||
到2017年,国内头部的四朵云(阿里、腾讯、华为、百度)也相继发布了FaaS产品。同年,Azure发布了首个Serverless容器ACI,AWS发布Serverless容器服务Fargate。
|
||||
|
||||
可以说,Serverless的领域内,大家追逐和创新的劲头彰显得非常明显。这一段时间出现了函数计算、容器实例、容器服务等产品。从下面的Google Trends也可以看出,人们对于无服务器和Lambda的关注度是不断提升的。我们可以认为,这是Serverless的发展阶段。
|
||||
|
||||
|
||||
|
||||
成熟阶段
|
||||
|
||||
2018年,华为云发布了Serverless容器服务CCI,阿里云发布面向应用的Serverless托管服务SAE。在开源方面,Knative发布,并于2022年3月成为CNCF孵化项目。
|
||||
|
||||
时隔一年,阿里云、Google、腾讯云先后发布了Serverless容器服务,截止到去年,各大公司又相继推出了Serverless应用托管服务,旨在进一步为用户提供全托管、免运维和高弹性的服务能力,一方面可以解决存量的微服务应用上云,另一方面可以高效地集成和持续交付等。这一期间直接把Serverless推到了一个相对成熟的阶段。
|
||||
|
||||
可以说,随着Serverelss技术和价值的体现,各大公司都在你追我赶地发展,国内厂商虽然是后来者,但追赶势头迅猛,目前的竞争力也能够和国外大公司相互比肩。
|
||||
|
||||
|
||||
|
||||
从发展历程中,我们也可以看出,以函数为起点,Serverless的技术和产品逐步趋于多元化。多种形态相互补充,为解决更复杂、更多样化的业务场景提供支撑,我们通常会把场景分为三类:
|
||||
|
||||
|
||||
函数计算(FaaS):面向函数,用户只需关注函数层级的代码,用于解决轻量型、无状态、有时效的任务;
|
||||
Serverless应用托管:面向应用,用户只需要更关注应用本身,与微服务结合,它融合应用治理、可观测,降低了新应用的构建成本、老应用的适配改造成本;
|
||||
Serverelss容器服务:面向容器,在不改变当前Kubernetes的前提下,由于不再需要关注节点,大大提升了前期资源的准备过程,降低了维护成本,使得应用的全生命周期管理更高效。
|
||||
|
||||
|
||||
随着Serverless的内涵持续外延,计算的Serverless化要求BaaS服务也具备与之配套的托管能力和极致弹性,全链路完整支撑真正Serverless化的应用构建。
|
||||
|
||||
那么,我们讨论的“Serverless”已经不再是原来狭义上的Serverless——函数即服务了。广义上,Serverless的特性应该同时体现在Serverless DB、Serverless消息队列、工作流、工具链等等方面。如今,在资源层的Serverless上,各大公司也相继研究发布了各种产品,如数据库层面的Amazon Aurora Serverless、PostgreSQL for Serverless、PolarDB Serverless等。
|
||||
|
||||
|
||||
|
||||
除了定义内涵在不断的丰富,Serverless的应用场景也在迅猛扩展。它不再局限在小程序、定时任务、简单事件的触发、批量场景的处理了,基于AI的集成、大规模数据的处理、IOT数据处理、微服务托管上云等领域都随处可见Serverless的身影。
|
||||
|
||||
我再附上一张信通院标准书中的Serverless解决方案图,帮助你更好地梳理Serverless的知识要点,在课程的开始,构建起Serverless的模型和思维。
|
||||
|
||||
|
||||
|
||||
切入点:升级打怪的进阶之路
|
||||
|
||||
了解了Serverless的发展、价值和架构模式,具备了Serverless的意识形态之后,相信你已经对Serverless这个新赛道脱离了之前的印象了。
|
||||
|
||||
Serverless的多而杂,一方面让我们更期待它未来的发展,但另一方面,也的确提升了我们学习的难度。那我们要怎么切入这个新赛道,紧跟云计算下一个十年的发展趋势呢?
|
||||
|
||||
正如开篇词中所说,我不希望你面面俱到地学习很多具象化的技术,那样只会“杂而不纯,博而不精”。我建议你从FaaS形态的Serverless入手,为什么要给出这个建议呢?
|
||||
|
||||
首先,Serverless如同太极,确实讲究“用意不用力”,但学习“太极”也讲究前期的武功招式,只是练就到最后,应该忘形存神。那么选择一个具象化的产品来学习新赛道下的Serverless是非常有必要的。
|
||||
|
||||
其次,FaaS作为Serverless意识形态的“元老”级别产品,从AWS Lambda的发布,到众多厂商的跟进,已经发展8个年头了。自定义运行时、自定义镜像、编排、应用中心等也都是基于FaaS打造和延伸的功能。因此,FaaS形态的Serverless是你入手的不二选择。
|
||||
|
||||
那么,我们如何学习Serverelss FaaS这一套“拳法”呢,我列了这一个简单示意图,下面我依次来跟你讲一下如何达到每一个层级。
|
||||
|
||||
|
||||
|
||||
|
||||
Hello World
|
||||
|
||||
|
||||
这个层面最简单,你只要具备任意一个云厂商的账号,创建一个函数运行起来,就算是迈出了Serverless学习的第一步。
|
||||
|
||||
在学习的过程中也不需要任何的成本,云厂商一般都有免费的额度,不用担心费用问题,直接上手练习、使用就好。
|
||||
|
||||
|
||||
能用
|
||||
|
||||
|
||||
要想达到这一层,你需要经常去云厂商的网站浏览,学习手册,再找一些公众号等博文资料了解。
|
||||
|
||||
完成这一次进阶的你,一定已经基本了解了一个Serverless的平台都有哪些功能,也能够简单的实操,可以做一般的售前解决方案了。不过,还并不具备很强的开发能力。
|
||||
|
||||
|
||||
会开发简单业务
|
||||
|
||||
|
||||
这一层开始,就是针对开发层面的学习了。
|
||||
|
||||
一方面,你需要掌握一门比较流行的编程语言,比如Python、Golang等,另一方面,你还需要持续关注业界公布的各种案例和云厂商的手册,跟着来改造你的代码。做到这两方面,这一层进阶也就完成了。
|
||||
|
||||
|
||||
会开发复杂业务
|
||||
|
||||
|
||||
达到这一层次的开发同学,必须将函数计算的各种技能融会贯通,知道什么情况用什么策略更好,当然,在社区和云厂商的答疑上也会有一些分享,可以持续关注。
|
||||
|
||||
|
||||
能沉淀公共能力
|
||||
|
||||
|
||||
这个层级属于业务开发的最高级。简单来说,你在开发自己业务代码的时候,能够沉淀出相关的类库、层供你和其他同学或者平台上的其他开发者使用的能力,这是抽象能力的体现。
|
||||
|
||||
在达成进阶之前,你应该就已经具备能撰写类库的能力,且学习过相关的抽象思维、设计相关的知识,并有这种意识去执行。
|
||||
|
||||
|
||||
计量计费
|
||||
|
||||
|
||||
作为一个平台,计量计费是少不了的。这个层级,也可以说是希望具备平台开发能力的同学需要达到的初级阶段。
|
||||
|
||||
你需要提前具备Kubernetes、Docker、架构设计、中间件的基础知识,在此基础上,你还需要系统性地吸收Serverless的一线实战案例和经验,才有可能避免踩坑。
|
||||
|
||||
|
||||
平台可迁移
|
||||
|
||||
|
||||
这个层级属于平台开发的王者阶段。你开发的平台不仅可以在你的云上使用,还可以帮助私有化企业部署,甚至可以组件化的方式输出。
|
||||
|
||||
想要完成这个层级的进阶,我的建议是加入一个云原生的Serverless团队,在里面打磨一万小时以上,扎扎实实地搞清楚每个点,假以时日,你才能在这个领域崭露头角。
|
||||
|
||||
写在最后
|
||||
|
||||
到这里,你应该已经对Serverless有了不一样的理解。
|
||||
|
||||
Serverless是能够实现业务和基础设施分离,提升用户业务创新能力的理念。我给你整理了一个图,突出几个核心特征组成了它的形式:
|
||||
|
||||
|
||||
|
||||
其中,从物理组成上,FaaS和BaaS组成了Serverless的基本构成,这就跟汽车由车身和车轮构成一个道理;而通过开箱即用、API形式调用等动作描述了Serverless的运作形态,这就跟汽车需要发动引擎、脚踩刹车的使用逻辑类似;最后,按需调用、按量收费的几个核心特征体现了Serverless的“最大卖点”,好比似汽车的自动驾驶、自动泊车功能一样。
|
||||
|
||||
这样了解下来,你是不是对Serverless的理解更亲切了呢?
|
||||
|
||||
Serverless发展到今天形成了以函数计算、弹性应用、容器服务为核心的产品形态,并逐步泛化和外延,使得其形态趋于多元化,形成互补之力,能够满足更多的场景。
|
||||
|
||||
但是,Serverless思维的建立一定不是一蹴而就的。在它未来发展的几年中,也一定会越来越泛化,它一定会继续融合更多的生态,更多的Serverless产品形态也将会不断涌现。我们要做的,就是“保持一颗拥抱变化的心”,扎根于Serverless的本质精髓,透过产品了解背后的技术原理和初衷。
|
||||
|
||||
在后面的课程中,我希望你不仅跟着学习文章中的理论知识,也能够随着课程中提到的实战去自己动手练习,积极地参与讨论和思考,通过“学习-实践-总结-表达”的过程去探索这门不断延伸的技术。
|
||||
|
||||
在这样的学习下,我相信你一定可以触类旁通,“随敌而变,随招而变”,达到真正的“用意不用力”。
|
||||
|
||||
思考题
|
||||
|
||||
你在步入Serverless领域的时候,有没有遇到什么困惑呢?欢迎分享出来,我们一起讨论解决。
|
||||
|
||||
感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起阅读和交流。我们下一讲见。
|
||||
|
||||
|
||||
|
||||
|
200
专栏/Serverless进阶实战课/01生命周期:函数计算的基本流程是如何执行的?.md
Normal file
200
专栏/Serverless进阶实战课/01生命周期:函数计算的基本流程是如何执行的?.md
Normal file
@ -0,0 +1,200 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
01 生命周期:函数计算的基本流程是如何执行的?
|
||||
你好,我是静远。
|
||||
|
||||
看过上一节思维储备的内容之后,相信你对Serverless的认知已经越来越清晰了。它是一种服务设计的理念,只需关注业务逻辑的开发,无需关注环境运维和机器的管理,同时具备弹性、按需付费等特点。
|
||||
|
||||
FaaS(函数即服务)作为我推荐给你的第一个步入Serverless世界的技术,今天,我想跟你介绍一下它的基本流程,让你从整体上对FaaS有一个初步的了解,心中能够勾勒出函数计算开发、调试、部署和运行的轮廓。
|
||||
|
||||
这样,在你后续碰到某一个困惑,例如“函数上传到哪里去了?”“函数运行为什么超时?”等流程问题时,就能快速知道自己需要深入了解哪一块的知识点。
|
||||
|
||||
这节课,我就以“Hello Serverless”为例,选取百度智能云函数计算CFC作为操作平台,分别从用户使用与平台服务两个视角,梳理一遍 FaaS 执行的流程和原理,这其中包括函数创建、存储、执行等一个生命周期的始终,同时,我也会带你体验一次开发、运行函数计算的流程。
|
||||
|
||||
初次体验推荐用云厂商平台
|
||||
|
||||
一般初次接触Serverless,我推荐先去公有云厂商的云平台上跟着他们的教程来体验,从使用角度上能够有一些认识,再看开源的框架或者找一些资料深入研究。
|
||||
|
||||
为什么这么推荐呢?
|
||||
|
||||
首先,云厂商的函数计算平台为用户提供了多种运行环境(Python、Java、PHP、Node.js、Golang 等),可以满足不同技术栈的开发需求。你可以选择任何你擅长的语言来体验,避免了语言的切换成本。如果选择的是解释性语言,可以直接在云平台上开发、调试、部署,非常方便。这样,通过低成本的学习,你可以快速地理解Serverless的产品形态。
|
||||
|
||||
其次,如果你自己要部署一套开源的框架,是要有机器资源的,而云厂商一般都会提供免费的额度。拿函数计算来说,像百度智能云、阿里云、华为云,每个月都有大概100万额度的调用次数,以及40万GB-秒的内存资源使用,基本上足够我们体验了。腾讯云做了一些调整,新人前3个月有一定的免费额度。当然,你在使用的时候,最好提前看一下云厂商的使用说明,避免多花冤枉钱。
|
||||
|
||||
最后,你也可以比较方便地通过云厂商提供的各类API/SDK、触发器集成、开发工具来了解整个Serverless的生态和解决方案。这也正是我们常说的“遇到新事物,先看看别人是怎么干的”,也是学习的一条捷径。
|
||||
|
||||
了解了大致的体验方式和流程之后,下面我们就要从用户角度来看函数计算是如何使用的了。
|
||||
|
||||
从用户视角看函数计算的生命周期
|
||||
|
||||
函数整个生命周期会经过“开发设置”“打包上传”“事件绑定与触发”“弹性执行”“实例销毁”五个过程。下面这张图片,就展示了用户视角下的函数计算的过程。我们一起来体验一下。
|
||||
|
||||
|
||||
|
||||
第一步,需要你编写相应的函数代码。我们以 Python3.6 为例,撰写一个“Hello Serverless”的Demo,函数运行时选择 Python3.6,执行内存选择 128MB,超时时间选择 3s,并发度设置为1。下图展示的就是我们创建好的函数信息概览。
|
||||
|
||||
|
||||
|
||||
然后,我们点击编辑函数,进入在线编辑模式,编写Demo案例代码。你也可以下载到本地进行代码开发。
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
def handler(event, context):
|
||||
return "Hello Serverless"
|
||||
|
||||
|
||||
编写完成之后,你还需要指定函数执行的入口,例如:填写 “index.handler”,就是指调用主程序文件 index.py 中定义的 handler 方法。当有事件触发函数执行时,就会先从handler方法开始执行。
|
||||
|
||||
第二步,需要将代码上传到函数计算平台上。你可以直接在界面提交并保存你的代码,也可以将代码打包成 zip 文件。上传的方式包括函数计算API/SDK、前端界面上传,还可以通过命令行工具 CLI 上传。
|
||||
|
||||
第三步,执行你刚刚上传的函数。通常可以利用 API/SDK 调用,或者在前端界面手动点击来执行函数,另外,你也可以通过各种触发器来触发函数的执行。
|
||||
|
||||
你可能会对触发器感到陌生,这里我简单说一下。
|
||||
|
||||
FaaS可以通过事件触发器打通众多的上下游服务,当触发源服务发出请求时,函数就会响应运行,作出处理和反馈。以 HTTP 触发器为例,当用户访问 HTTP 触发器的 URL 时,会向指定的云函数发出HTTP处理请求,随后平台会启动一个函数实例来对请求进行处理。
|
||||
|
||||
我们继续操作函数helloServerless,从平台中选择创建一个HTTP触发器,将URL路径设定为“/hello/serverless”,选择“GET”方式作为HTTP的请求方法。
|
||||
|
||||
|
||||
|
||||
创建完触发器后,函数计算平台会帮助你生成一个可访问的URL地址,你就可以通过这个URL地址进行触发函数的执行了。如果你是要上生产环境,最好能增加身份验证,以确保服务的安全可靠。
|
||||
|
||||
|
||||
|
||||
第四步,当函数执行完毕,函数计算平台会返回函数的执行结果。通常可以通过日志或者请求返回的信息来查看函数的执行结果。
|
||||
|
||||
curl https://$HTTP_TRIGGER_URL/hello/serverless
|
||||
|
||||
#输出结果
|
||||
Hello Serverless
|
||||
|
||||
|
||||
经过上面的步骤,你已经完成了一个“Hello Serverless”的小Demo。对于用户而言,只需要关注开发的代码本身,而不需要去关注环境的部署和维护。看到这里,你可能会有疑问,执行结果和传统代码执行没有区别,为什么要选择 FaaS 呢?
|
||||
|
||||
其实,FaaS最大的特点在于弹性扩缩容和缩容至0的能力,当你没有调用函数,FaaS是没有任何实例在计费的。也就是说,当你创建上传函数后,并没有产生计费,只有当你产生调用量才会开始计费。当流量达到一个阈值的时候,系统自动进行扩容。当流量变小时,系统会自动进行缩容。
|
||||
|
||||
另外,一般云厂商的 FaaS 都会有一定的免费额度,如果你的应用是基于事件触发或者流量的波峰波谷比较明显,那么 FaaS 绝对是你的明智之选。
|
||||
|
||||
从平台视角看函数计算的执行过程
|
||||
|
||||
前面我们已经从开发者的角度了解了函数计算的生命周期,但我相信你肯定不会只满足于表面的使用。 那么,函数计算内部具体是如何实现的呢?
|
||||
|
||||
简单来说,事件的请求,首先会到达路由服务,路由服务在缓存Cache中查看是否有准备就绪的实例。如果有就绪的实例,也就是热启动,直接使用该实例执行函数即可。如果没有就绪的实例,就会进入冷启动过程。函数计算引擎会启动容器的初始化流程,做一些准备工作:下载函数的代码包或者镜像、准备网络环境、加载运行时Runtime,进而执行函数,并将实例信息放入到Cache中,下次请求再过来的时候,就进入热启动流程。
|
||||
|
||||
执行完毕之后,实例会保留一定时间(通常是1-2分钟),随后被回收。
|
||||
|
||||
以上是正常的执行流量,当流量突增到一定阈值的时候,函数计算服务会快速扩容实例来满足提升的并发量。而空闲的实例太多的时候,也会缩容实例。
|
||||
|
||||
到这里你肯定会有疑问,很多新名词好像很陌生,冷启动、热启动、运行时等等。下面我将带你从开发态和运行态两个视角来梳理函数计算的生命周期,便于你理解它们的含义。
|
||||
|
||||
函数计算的开发态
|
||||
|
||||
当我们上传代码到FaaS平台后,后端服务会将代码包上传到对象存储中,并将函数相关信息,包括函数代码链接、Runtime信息、运行内存、超时时间等信息存储起来。
|
||||
|
||||
当我们再次修改函数相关信息,或者在线编写函数代码的时候,FaaS平台会将存储好的代码和附属信息读取出来,展示在界面上,供你修改。
|
||||
|
||||
这里需要注意的是,目前各大云厂商还只支持解释性语言的在线编译和调试,对于编译型语言,你还得下载到本地进行开发。所幸,目前有的云厂商(如阿里云)已经发布了端云联调能力的工具,从一定程度上来说,也便于你快速本地开发、调试和发布。后面的章节中,我也会跟你详细介绍这个技术的实现。
|
||||
|
||||
|
||||
|
||||
函数计算的运行态
|
||||
|
||||
那在你上传好代码之后,FaaS平台是如何执行函数代码的呢?我们还是以“Hello Serverless”为例来说明。
|
||||
|
||||
在开头,我们已经创建了一个函数、编写了相应的代码,并保存到了云厂商的平台上。
|
||||
|
||||
我们再次回到 HTTP 触发器的方式执行来讲解。当事件请求访问触发器的URL时,请求会被路由到相关函数实例,依据是否是第一次请求,会分为冷启动和热启动两种情况;根据流量的大小,会进行动态的扩缩容。
|
||||
|
||||
我将这个过程抽象成下面的函数计算架构示意图,通过这个示意图,我们来依次看一下“Hello Serverless”云函数是怎么执行的。
|
||||
|
||||
|
||||
|
||||
流量转发:热启动和冷启动
|
||||
|
||||
首先,当HTTP事件请求到来之时,流量转发服务负责接收转发请求,也就是图中的Route服务。当Route接收到请求后,首先就要在自己的缓存Cache里查看是否已经存在当前Hello Serverless函数和实例的对应信息。
|
||||
|
||||
如果有,那么根据存储的信息,直接在实例池(instance pool)中获取执行实例,这时请求就以热启动方式被执行。具体什么是热启动呢?就是当你的函数执行完成后,容器实例会保留1-2min的时间,如果此时触发执行函数,那么无需新增实例和执行函数 runtime 的挂载,直接复用。因此它的响应速度要快得多。
|
||||
|
||||
如果找不到相关的信息呢?那么就会通过一个类似激活器(如Activator)的组件,来创建并申请一个实例,执行本次请求,接着,将实例的相关信息存储到Route的缓存中,这就是冷启动的执行过程。
|
||||
|
||||
在冷启动过程中会有哪些操作呢?一般来说,包括实例调度和容器创建、下载并解压代码、准备函数执行环境、挂载用户代码、VPC网络准备、初始化运行时和用户代码,这一系列过程结束后,函数才开始执行。所以冷启动的消耗时间受到很多因素的影响,主要包括:
|
||||
|
||||
|
||||
不同语言的冷启动时间不同:一般 GoLang、Python的速度较快,而Java的速度相对较慢一些。
|
||||
代码包大小:下载解压代码的过程在冷启动过程中比较耗时,一般体积越大,耗时越多。
|
||||
容器创建速度和VPC网络的准备:这个过程的耗时往往取决于云服务商,不同平台的速度也是不一样的。
|
||||
|
||||
|
||||
当然,各云厂商对于冷启动也在不断地优化,推出了预留实例、通过缓存加快代码下载速度、VPC代理和IP隧道技术等方式来解决冷启动问题。你也可以通过一些方式来自行解决,例如:
|
||||
|
||||
|
||||
精简代码包,减少掉一些不必要的配置和依赖。
|
||||
用预热请求的方式来确保代码实例常驻在容器池中,比如用定时器触发代码实例进行一个非常快的空响应。
|
||||
选择冷启动时间较少语言,尽量避免使用 Java 等启动较慢的语言运行时。
|
||||
尽量选择较大的内存:函数的内存越大,冷启动速度越快。-
|
||||
后面的冷启动章节,我会详细地跟你聊一聊这一块的技术要点和一线实战经验。
|
||||
|
||||
|
||||
动态扩缩容
|
||||
|
||||
那么什么时候需要扩容和缩容呢?当我们首次通过HTTP触发器请求的时候,此时由于函数实例池里面没有已经加载好的Pod,需要进行一个从0到1的容器扩容过程。
|
||||
|
||||
此时,还需要将Hello Serverless的包从对象存储中加载到容器中运行起来。当执行结果返回后,一般FaaS平台会将函数实例保留一段时间再进行销毁。如果被保留的时间段内有请求再次进来,那么就可以直接复用,不需要扩容。但如果此时并发超过我们前面设置的1次请求,函数计算引擎监听到相关指标后,就会自动扩容。
|
||||
|
||||
当然,我这里举的情况比较极端了,通常的函数计算引擎会根据设定的监控阈值,提前就要扩容了。
|
||||
|
||||
扩缩容算法包含Node级别和Pod级别的扩缩容。Node和Pod一般会监控自定义的指标,如果指标有变化,会进行相应的扩缩容操作。
|
||||
|
||||
例如 ,Kubernetes 中的 HPA 扩缩容算法,通过安装一个 metrics-server 的监控组件,提供 HPA 和基础资源监控的能力。对CPU和Memory等指标进行监控,保证其维持在可控的范围内。这里先卖个小关子,函数计算的扩所容是否可以通过Kubernetes的HPA来直接进行,需不需要改动?可以先思考一下,我们在扩缩容一节中会详细跟你来探讨。
|
||||
|
||||
而Node级别的扩缩容,一般会根据Node的整体使用率,来判断Node数量是否需要扩容。一旦需要扩缩容,就会向Scheduler发送扩缩容请求,Scheduler调用相关接口执行扩容操作。
|
||||
|
||||
函数实例释放
|
||||
|
||||
最后,我还是要提一句,运行也是有始有终的。当函数执行完毕,并且在 1~2min 内没有再次执行时,FaaS 平台就会回收该实例。
|
||||
|
||||
各云厂商的回收时间各不相同,这里注意一下就行,以便我们将来基于云平台开发的时候,可以提前优化函数,确保请求在热请求的情况下执行。
|
||||
|
||||
Runtime
|
||||
|
||||
通过之前的步骤,你已经可以获得一个函数实例来执行你编写的函数Hello Serverless了。下面我们来重点看一下函数执行的关键底座:Runtime。Runtime 是为函数提供运行框架,并真正执行函数的进程。
|
||||
|
||||
云厂商一般将不同语言的执行环境打包为基础镜像。容器镜像包含多层镜像,第一层基础镜像为 ubuntu、alpine之类的文件系统;第二层镜像为代码的依赖库,如Python 代码需要使用 pip 库 , Node.js 代码需要使用 npm 库。也有的函数计算引擎,直接支持Docker镜像来运行。
|
||||
|
||||
我们以上述“Hello Serverless”的运行语言Python3为例来说明runtime的执行过程。Python3-runtime通常会开放一个handler的接口给开发者实现具体的业务逻辑。当一次请求到来之时,Python的运行时会通过动态加载的方式对你刚才定义的文件方法进行调用。
|
||||
|
||||
这里需要注意的是,对于编译型语言,需要引入FaaS平台提供的代码库,基于一套现成的框架来开发业务逻辑代码,不过思路一样,只是运行的方式不一样罢了。
|
||||
|
||||
具体的的实现过程,我会在运行时一节中跟你细聊。
|
||||
|
||||
小结
|
||||
|
||||
最后我们来小结一下。今天,我通过一个“Hello Serverless”的例子,带你了解了函数计算在不同视角下的运作过程。
|
||||
|
||||
从用户视角,通过四个步骤的实操,我们可以了解到作为一个业务开发人员,需要重点关心的部分是开发设置、打包上传、事件绑定与触发以及函数上线后的按需付费。对于函数如何执行、如何扩缩容、如何销毁等一系列函数计算引擎去做的事情,可以让平台运维人员更多的关注。
|
||||
|
||||
从平台视角,基于开发态,我们可以清晰地了解到函数计算控制面做的事情在于提供给业务人员一个好的操作平台;基于运行态,我们从整体上对运行期各环节的协作有了一定的了解。
|
||||
|
||||
一方面,事件初次请求时,函数计算平台有一个从0到1的扩容过程,随着流量的增加,平台会继续扩容以确保请求的正常执行。随着请求的减少,平台会通过释放实例来缩容。
|
||||
|
||||
另一方面,真正支持函数执行的运行时,也有着不同的实现方法,主要来源于语言本身特性的不一样。
|
||||
|
||||
|
||||
|
||||
通过今天的介绍,相信你对函数计算有了一个整体上的了解,已经摸到了这个领域的大门了。
|
||||
|
||||
接下来的课程,我也会详细地把本节提到的这些技术要点和你一一道来。
|
||||
|
||||
思考题
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个问题。
|
||||
|
||||
FaaS提高了开发人员的生产效率,使得产品能够快速地推向市场进行试错,你是否接触过FaaS,哪些业务已经在使用了,有遇到过什么问题么?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。感谢你的阅读,也欢迎你把这篇文章分享给更多的朋友一起交流进步。
|
||||
|
||||
|
||||
|
||||
|
257
专栏/Serverless进阶实战课/02触发器:如何构建事件源与函数计算的纽带?.md
Normal file
257
专栏/Serverless进阶实战课/02触发器:如何构建事件源与函数计算的纽带?.md
Normal file
@ -0,0 +1,257 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
02 触发器:如何构建事件源与函数计算的纽带?
|
||||
你好,我是静远。
|
||||
|
||||
在上一节课里,我通过一个函数的开发和请求过程,给你介绍了函数计算的生命周期,让你从用户和平台两个视角,了解了函数计算各阶段的执行流程和基础要点。
|
||||
|
||||
细心的你应该能发现,上传到函数计算平台的云函数,一般是需要通过一个触发器来执行的,如果之前你没有接触过事件驱动模型和Serverless相关的知识,可能对触发器这个概念有一些陌生。
|
||||
|
||||
相信学完今天这节课的你,不仅能够了解触发器的概念和原理,更能对触发器相关的知识点,例如事件、调用等等都有一定的理解。最后,我也会带你实现一个自定义的触发器。
|
||||
|
||||
初识触发器
|
||||
|
||||
上节课,我们知道了函数托管在平台后一般需要有一个事件来触发才能真正地运行起来。那么在了解触发器之前,我们先了解一下事件。
|
||||
|
||||
什么是事件?
|
||||
|
||||
说得直白一点,事件,就是系统运行期间发生的动作或者发生的事情。而函数计算,提供了一种事件驱动的计算模型。针对这个概念,我们需要了解三件事。
|
||||
|
||||
第一,CNCF关于事件的定义和初衷。CloudEvents 期望通过一种通用的格式描述事件数据的规范,以提供跨服务、平台和系统的互操作性。那么,我们自身在开发一个函数计算平台的时候,就可以更多的以这个规范为准绳来设计;在选型一个开源框架、函数计算平台开发业务代码的时候,这个互操作性也可以作为技术选型的参考因素之一。
|
||||
|
||||
第二,国内云厂商的事件规范程度。目前各大第三方云厂商都希望推广落地CloudEvents规范,但在事件规范定义上,多多少少都有各自平台的一些特有属性和参数包含在里面。
|
||||
|
||||
如果你在其中一家云产品上开发了云函数,一般来说,需要做一些简单的适配才能迁移。庆幸的是,部分云厂商目前具备支持脚本迁移和适配的能力,你可以通过工单的形式跟他们取得联系,大大减少迁移成本。
|
||||
|
||||
第三,CNCF在哪些Serverless相关产品定义了规范。在函数计算中,会接触到单函数的事件触发、多个简单函数通过异步调用方式形成的事件触发、复杂场景下通过工作流(WorkFlow)的形式来进行编排的事件交互。
|
||||
|
||||
CNCF Serverless工作组针对函数和工作流均定义了相应的格式规范或原语。现在很多的传统企业、云厂商也都在尽量按照这个标准去执行——尤其是复杂的多函数交互的场景下。你在选型的时候可以关注一下是否兼容这个标准,便于函数和编排规则在各大平台迁移。
|
||||
|
||||
什么是触发器?
|
||||
|
||||
了解“事件”这个触发条件之后,我们再看函数运行起来之前的“触发”动作。我们通常把这种由事件驱动连接上下游服务的关系组合称为触发器,它是触发函数执行的主要方式之一。
|
||||
|
||||
我们知道,函数计算由云函数和触发器组成。对函数计算来说,触发器描述了一组关系和规则,包括事件源、目标函数、触发条件三大核心要素。
|
||||
|
||||
|
||||
|
||||
其中,事件源是事件的生产者,目标函数是事件的处理者。当触发条件满足时,就会通知函数计算引擎,调度对应的目标函数来执行。
|
||||
|
||||
触发器的元信息可以由服务方持久存储,也可以由函数托管平台和服务方共同持有。根据不同的集成和调用方式,触发器也有着不同的分类。
|
||||
|
||||
触发器类型
|
||||
|
||||
按照触发器集成原则,通常分为单向集成触发器、双向集成触发器和代理集成触发器。它们的区别在于事件源和事件的规则存储在哪里,以及从哪里触发。
|
||||
|
||||
我们在设计触发器的时候,主要考虑的是事件源和函数计算的上下游关系以及操作的便捷度。
|
||||
|
||||
例如,我们在云厂商看到的对象触发器,在存储对象和函数计算两个平台上都可以来创建和配置触发器,这种就是双向集成触发器。而对于消息的监听和触发场景,由于大部分情况是对异步消息的计算处理,加上函数计算按需启动执行的特性,我们通常就会设计成单向集成触发器,你直接在函数计算平台创建和配置触发器即可。
|
||||
|
||||
按照调用方式,我们又可以将触发器分为同步触发器和异步触发器。
|
||||
|
||||
同步调用:触发事件被函数计算处理后直接返回结果。例如,公有云厂商提供的HTTP触发器、CDN触发器等,基本都是同步触发器。
|
||||
|
||||
异步调用:触发事件请求函数计算后,函数计算服务返回一个接收成功的状态码,由函数计算来确保事件被可靠处理。例如,定时触发器、消息队列触发器等,均属于异步触发器。
|
||||
|
||||
在使用的时候,你也可以优先考虑自己的需求,从不同的分类里找到最适合的触发器类型。
|
||||
|
||||
到这里,相信你对触发器的认知已经不仅仅是基于上节课的一个感性轮廓了。下面,我们来体验一个简单的触发器,为自己动手实现触发器打下基础。
|
||||
|
||||
动手体验
|
||||
|
||||
我们以百度智能云的函数计算平台为底座来体验一下,假设我们已经创建了一个名叫test_geekbang的函数,接下来需要创建一个触发器。
|
||||
|
||||
|
||||
|
||||
如图,我们创建了一个BOS触发器(BOS是百度智能云的对象存储服务),并设置了监听的目标对象是BOS下的一个Bucket(bos:/cfc-sns),事件类型是文件上传、追加上传等条件均可触发函数的执行。
|
||||
|
||||
创建好触发器后,我们就可以在BOS上传一个文件来测试了。由于我们没有设定额外的过滤规则,所以你上传任何一个文件,均可以触发云函数test_geekbang一次执行。
|
||||
|
||||
整体操作没有什么难度,你也可以参考百度智能云函数计算CFC的教程去体验其他细节设置。你会发现,触发器使得函数计算与外部服务的连接非常的快捷高效。
|
||||
|
||||
说到这里,估计你已经迫不及待地想知道这个连接的背后是如何实现的了。接下来,让我们一起来动手实现一个触发器,体会其中的原理。
|
||||
|
||||
动手实现
|
||||
|
||||
我们看不到云厂商的对象存储触发器中详细的实现,那么通过一个比较类似,且功能实现全面的开源存储服务来作为实现触发器的底座,也未尝不可。
|
||||
|
||||
这里,我选择了和百度智能云BOS类似的开源对象存储服务MinIO来演示。MinIO采用Golang实现,客户端有支持丰富的语言集成,如Golang、Java、Python、JavaScript等等,空闲的时候,你也可以用不同的语言尝试重现这次的实验。
|
||||
|
||||
假设我们需要满足这样一个功能场景:
|
||||
|
||||
|
||||
通过监听MinIO的事件动作(MinIO存储对象的创建、访问、删除)来触发函数计算的执行。例如:当一个文件存储到MInIO的Bucket中时,监听服务监测到该事件发生,通过查询判断是否存在一组关系和规则,如果存在,触发函数执行既定的业务逻辑。
|
||||
|
||||
|
||||
那么,针对这个功能诉求,你能拆分出一个实现流程吗?别着急回答,可以先暂停一会儿思考一下。
|
||||
|
||||
我将大致流程分为了5个步骤:
|
||||
|
||||
|
||||
用户在函数计算平台上对相关云函数绑定自定义触发器,设定好事件源标识;
|
||||
MinIO发生某个动作,假设是上传(Upload)文件的一个动作,该动作关联唯一的一个Bucket下的一条路径,构成了一次触发事件 (Event);
|
||||
事件监听器(Event Monitor Server)作为Deamon进程持续运行,会捕获到该事件;
|
||||
监听器通过“动作+Bucket+路径”构成的唯一标识,从数据库MySQL查询并获取相应的触发器;
|
||||
监听器对获取到的触发器进行逐个触发,对远端云函数进行触发调用。
|
||||
|
||||
|
||||
|
||||
|
||||
接下来,我们就根据这个场景和流程来定义这个触发器。整体需要通过事件类型确定、事件协议定义、元数据存储约定、事件绑定四个步骤来完成。
|
||||
|
||||
事件类型确定
|
||||
|
||||
由于MinIO已经实现了事件监听的API,我们可以直接从MinIO API目前的事件类型中选出我们需要的三种,s3:ObjectCreated:* 、s3:ObjectRemoved:* 、s3:ObjectAccessed:* 满足针对一个对象的CRUD要求,并进行监听。
|
||||
|
||||
|
||||
|
||||
其中,第一种类型满足了通过不同方式来创建一个对象的需求;第二种事件类型,支持从存储的bucket中删除指定的对象;第三种事件类型是用来监听访问对象时才触发的情况。
|
||||
|
||||
选定好事件类型后,我们选取GitHub上封装好的MinIO客户端,简化我们的开发实现,通过引入mino-go,可以快速实现一个事件类型的监听动作。
|
||||
|
||||
func (m * EventMonitor) work() error {
|
||||
miClient := m.miClient
|
||||
for miInfo := range miClient.ListenNotification(context.Background(), "", "", []string{
|
||||
"s3:ObjectCreated:*",
|
||||
"s3:ObjectAccessed:*",
|
||||
"s3:ObjectRemoved:*",
|
||||
}){
|
||||
//TODO:实现业务逻辑
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
事件协议定义
|
||||
|
||||
然后,我们就要定义事件协议,来传递MinIO和云函数之间的触发请求了。这里,我们采取HTTP+JSON的方式。你可以参考下面的事件格式定义方法:
|
||||
|
||||
{
|
||||
"eventId" : "事件id ",
|
||||
"source" : "事件源,此案例标识来源MiniIO",
|
||||
"specversion": "协议的版本",
|
||||
"type" : "事件类型 ",
|
||||
"timestamp" : "请求时间戳",
|
||||
"target": "函数唯一标识",
|
||||
"data" : {
|
||||
// 待处理数据
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
不过,定义了协议,能够传递请求还不够,函数计算平台通常为了能够识别可信的事件源,当有触发器创建之后,还需要进行授权认证,确保服务请求的安全性。-
|
||||
接下来,我们来看一下接口设计。在这个案例情境下,我们需要创建四个功能来满足触发器的控制台操作需要,分别用于触发器的创建、修改、删除、查询,你也可以通过REST来构建如下的接口协议。
|
||||
|
||||
func OperateMinioTriggers(request *restful.Request, response *restful.Response)
|
||||
|
||||
|
||||
其中,request中包括action动作用来标识CRUD操作即可。
|
||||
|
||||
这里你需要注意一点,如果一个函数被删除,那么它对应的附属特征、附属触发器等都应该剔除干净。这也是我们初次设计时候很容易遗漏的地方。
|
||||
|
||||
元数据存储约定
|
||||
|
||||
按照通常的操作习惯来说,在MinIO和函数计算服务侧均可实现触发器的集成功能,还可以在配置Bucket的时候创建事件绑定规则,在创建一个函数的时候也能够创建好监听对象的请求事件。
|
||||
|
||||
而这两处的绑定创建过程,除了UI界面和本身创建时的平台校验参数不一样外,都很雷同。为了降低第一次尝试的难度,我们就选择在MinIO服务侧集成。
|
||||
|
||||
由MinIO作为事件源服务方通知触发云函数的执行。那么,相应的关系信息只需要在MinIO服务侧存储,鉴权和规则在函数计算平台侧存储,其中:
|
||||
|
||||
|
||||
关系创建:确定具体的触发关系,如具体的Bucket、触发条件等;
|
||||
规则创建:确保触发源按照既定规则进行触发,如标识MiniIO的动作、触发形式等。
|
||||
|
||||
|
||||
你可以根据上述的描述进行设计,这里,我也给出了简单的待存储对象的数据结构,你可以自行转换成数据库表来存储,通常多个关系可以共用一个规则。你可以就此创建两张表,一张规则表tb_rule,用于存储规则,一般存储在函数计算的平台侧,一张关系表tb_relation,依据触发器的类型存储在事件触发源侧。
|
||||
|
||||
规则:
|
||||
|
||||
type Rule struct {
|
||||
ID uint
|
||||
Sid string // 规则唯一ID
|
||||
Resource string // 函数标识
|
||||
Source string // 触发源
|
||||
…… // 可以根据业务复杂度增加字段,如创建时间、用户ID等信息
|
||||
}
|
||||
|
||||
|
||||
关系:
|
||||
|
||||
type Relation struct{
|
||||
RelationId string // 关系ID
|
||||
RuleId string // 触发器关联的规则的id
|
||||
Source string // 触发源,可以具体到bucketName
|
||||
Target string // 函数的唯一标识
|
||||
Condition string // 触发条件
|
||||
…… // 可以根据业务复杂度增加字段,如触发器自定义参数
|
||||
}
|
||||
|
||||
|
||||
事件绑定
|
||||
|
||||
根据上述的约定设计方案,我们来完成事件的绑定规程,从MinIO服务侧来绑定,需要3个步骤。
|
||||
|
||||
步骤1:请求上述定义好的MinIO接口,创建一个自定义的触发器配置,例如监听一个对象存储到bucket的动作。
|
||||
|
||||
步骤2:MinIO侧的服务(例如API-Server)在接收到请求后,将关系存储在数据库中,并同步通知函数计算平台,创建对应的规则。
|
||||
|
||||
步骤3:函数计算平台接收到MinIO侧服务的请求后,检查当前库里面是否已经存在同样的规则,如果存在直接返回响应成功;如果不存在,记录规则,用于触发时校验,最后返回通知MinIO侧绑定成功。
|
||||
|
||||
到这里,我们已经完成了MinIO作为事件源,函数计算平台作为处理方的事件关系的绑定过程。
|
||||
|
||||
触发器的调用过程
|
||||
|
||||
定义好触发器并绑定关系后,我们可以以上传一个文件为例,整体看一下MinIO的事件触发过程,也就是触发器的调用过程:
|
||||
|
||||
|
||||
用户操作MinIO,上传了一个文件;
|
||||
MinIO侧事件监听服务(EventMonitorServer)对触发的事件进行捕获;
|
||||
EventMonitorServer从数据库关系表(tb_relation)中获取匹配该事件的触发关系;
|
||||
根据关系发起调用函数计算服务的请求;
|
||||
函数计算服务平台侧接收到请求后,进行鉴权和规则的验证,并执行相应的函数进行处理,最后根据异步或者同步约定方式,进行结果返回。
|
||||
|
||||
|
||||
|
||||
|
||||
到这里,我们已经定义好一个基于MInIO的对象存储触发器,并通过时序关系描述了调用时刻的交互过程。
|
||||
|
||||
如前所述,由于MinIO已经提供了相关的事件监听API,在本案例中,你只需要开发一个简单的服务,如事件监听服务EventMonitorServer,来监听这些API接口的变动即可。你可以按照此方法进一步实战演练。
|
||||
|
||||
如果需要上生产环境,你还需要根据性能、稳定性等相关的要求,增加缓存、重试机制等能力,来确保生产环境的SLA。
|
||||
|
||||
小结
|
||||
|
||||
今天,我给你介绍了事件源与函数计算的纽带——触发器,以及事件和触发器的相关知识,同时,也动手体验了云厂商的对象存储触发器。最后,我们也通过自己构建一个基于MinIO的对象存储触发器,深入了解了触发器的执行原理。
|
||||
|
||||
这其中,最重要的就是以MinIO为核心底座,来构建一个自定义的触发器,我希望你能够记住关于动手实现触发器的三点关键信息。
|
||||
|
||||
首先,我们选择MinIO来作为本次动手实现的初衷,是为了更好地了解云厂商关于对象存储的相关实现原理,在你之后遇到类似存储事件触发函数执行的时候,也可以用本节讲到的方法来去应对。
|
||||
|
||||
其次,触发器实现的来龙去脉,大致包含四个核心流程:事件类型确定、事件协议定义、元数据存储约定、事件绑定。
|
||||
|
||||
最后,今天我们引入的MinIO客户端,监听MinIO的三大事件类型,以及给出的代码示例,都可以即学即用,你也可以尝试快速上手实战,实现一个触发器。
|
||||
|
||||
希望通过以上的介绍,你能对触发器以及相关的事件、调用原理有一个比较深入的理解,在之后使用或者自定义触发器的时候,就可以“拿捏”得比较准了。
|
||||
|
||||
思考题
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个思考题。
|
||||
|
||||
在你的工作场景中,还发现哪些场景可以构造成触发器?改造过程中,我们通常需要注意哪些技术卡点?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起交流进步。
|
||||
|
||||
延伸阅读
|
||||
|
||||
|
||||
CloudEvents规范 你可以通过这个链接来了解CNCF事件的定义和详解
|
||||
Minio-go 这是我案例中使用的访问minio的一个封装库,供你参考
|
||||
Minio支持的事件类型介绍 这是MinIO官方的事件支持书册,感兴趣的话,可以了解一下
|
||||
Serverless Workflow Specification 这个是关于工作流规范的描述,在构建函数和应用编排的时候,可以提前参考一下
|
||||
|
||||
|
||||
|
||||
|
||||
|
177
专栏/Serverless进阶实战课/03高级属性:应对生产级别的应用,你需要掌握哪些技能?.md
Normal file
177
专栏/Serverless进阶实战课/03高级属性:应对生产级别的应用,你需要掌握哪些技能?.md
Normal file
@ -0,0 +1,177 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
03 高级属性:应对生产级别的应用,你需要掌握哪些技能?
|
||||
你好,我是静远。
|
||||
|
||||
通过前面的学习,我们已经掌握了函数计算的基本特性和操作流程,可以通过配置触发器的形式来完成一些较为简单的Serverless业务场景的开发和运维了。
|
||||
|
||||
但是,针对复杂的场景或者是要上生产环境的应用服务,这样的操作肯定是不够的。就拿我们比较熟悉的微服务来说,一个服务模块从开发到上线运行,需要注意公共代码的复用、开发框架的灵活性、上线时候的灰度发布、线上运维的容灾,针对上下游还需要考虑到如何应对流量的波峰波谷等因素。
|
||||
|
||||
那么,在Serverless的函数计算中就不要注意这些了么?Serverless虽然是无服务器化的,但如果要用好函数计算平台,或者是自己要开发一个FaaS化的Serverless平台,都应该对这些因素有一个比较清晰的认识,才能更好地驾驭Serverless。
|
||||
|
||||
今天这节课,我们就按照一个应用“开发-发布-运维”的顺序,以我们熟知的微服务流程为参照,看看函数计算中,你需要了解哪些高阶的技能。我们先从应用的开发阶段说起。
|
||||
|
||||
如何提取公共的服务能力?
|
||||
|
||||
开发微服务的时候,如果是一个复杂一点的功能,则需要多人协作或者复用一些别人开发好的类库。那么,函数计算中是否也有类似的特性呢?
|
||||
|
||||
层的提出就是为了解决这个问题。你可以将函数依赖的公共库提炼到层,以减少部署、更新时的代码包体积。
|
||||
|
||||
目前,大部分的云厂商都支持各种语言的层,如Java、Python、Node.js、PHP等。对于支持层功能的运行时,函数计算会将特定的目录(如/opt)添加到运行时语言的依赖包搜索路径中。你可以自定义层或者使用云厂商开发好的公共层,这些公共层包含了一些常用的依赖包。
|
||||
|
||||
对于自定义的层,你通常需要将所有内容打包到一个ZIP文件包中,并上传到函数计算平台。函数计算运行时会将层的内容解压并部署在特定的目录,如/opt目录下。
|
||||
|
||||
使用了层的功能之后,有这样3个好处:
|
||||
|
||||
|
||||
函数程序包更小,只关注核心代码,使开发变得更加轻松方便;
|
||||
使用层也可以避免在制作函数zip包和依赖项过程中出现未知的错误;
|
||||
由于可以在多个函数中引入使用,可以减少不必要的存储资源浪费。
|
||||
|
||||
|
||||
当然,不同云厂商构建层的方式也不同,我们以百度智能云为例,一起尝试构建一个Node.js语言运行时的层。
|
||||
|
||||
首先,我们新建一个目录,如 mkdir my_nodejs_layer,并在该目录下新建名为“nodejs”的目录,目录结果如下所示:
|
||||
|
||||
➜ my_nodejs_layer tree
|
||||
.
|
||||
└── nodejs
|
||||
1 directory, 0 files
|
||||
|
||||
|
||||
下面,假设我们的函数代码中,需要用到JavaScript实用工具库Lodash,我们将Lodash打包到自定义的层中。进入nodejs目录,执行 npm install lodash ,我们再来查看一下目录结构:
|
||||
|
||||
➜ my_nodejs_layer tree -d
|
||||
.
|
||||
└── nodejs
|
||||
└── node_modules
|
||||
└── lodash
|
||||
└── fp
|
||||
|
||||
4 directories
|
||||
|
||||
|
||||
最后,在my_nodejs_layer目录中将nodejs打包:
|
||||
|
||||
zip -r nodejs_layer_lodash.zip nodejs
|
||||
|
||||
|
||||
这样,我们自定义层的依赖就打好了。我们来看一下层上传之后的运作逻辑:
|
||||
|
||||
|
||||
|
||||
我们上传层之后,函数计算会将层的ZIP包上传到对象存储。当调用函数执行时,会从对象存储中下载层的ZIP包并解压到/opt目录。你只需要在代码中访问/opt目录,即可读取层的依赖和公共代码。
|
||||
|
||||
当我们上传多个层的时候,一定要注意次序问题,后序的层会覆盖相同目录下的文件。
|
||||
|
||||
到这里,我们已经构建并上传了自己的层,之后,就可以在编写函数的时候,通过约定的路径使用了。是不是非常方便?它其实有点类似我们微服务的依赖包,通常来说,也会通过上传自定义的类库到仓库中,让后续开发的同学更加方便地使用。
|
||||
|
||||
如何快速地开发和交付?
|
||||
|
||||
层确实可以解决我们公共代码逻辑和工具库的使用问题。不过,你可能还很好奇,“在微服务的开发中,可以比较灵活地使用某种自己擅长的语言和框架来开发代码,只要遵循服务之间的接口协议规范,就可以开发出一个模块,来提供服务能力。那么,Serverless的函数计算是否也有类似的能力,让我可以快速灵活地搞定一个服务并且执行呢?”
|
||||
|
||||
答案是肯定的。为了让更多开发者能够快速灵活地开发自己的服务,除了你熟知的基于函数平台提供的标准运行时来开发函数体之外,函数计算平台通常还会支持用户以自定义镜像的方式上传应用来运行。
|
||||
|
||||
我们以阿里云自定义镜像为例来说明其工作原理。函数计算系统初始化执行环境实例前,会扮演该函数的服务角色,获得临时用户名和密码并拉取镜像。拉取成功后,会根据指定的启动命令、参数以及端口,启动你定义的HTTP Server。然后这个HTTP Server就会接管函数计算系统所有请求的调用。
|
||||
|
||||
在开发函数具体的逻辑之前,我们一般还需要确认开发的是事件函数还是HTTP函数,通常来说,平台服务后端对这两种调用方式实现的逻辑不一样。
|
||||
|
||||
下面我们来具体看一下自定义镜像的构建和使用过程,包括五个步骤。
|
||||
|
||||
|
||||
|
||||
第一,在创建函数之前,我们需要基于自身业务来实现一个HTTP Server,并且将其打包成镜像,上传到镜像仓库,供函数计算使用。
|
||||
|
||||
各个云厂商的实现方式不同,主要体现在请求和响应接口上的协议适配以及服务运行方式上。针对HTTP Server的实现,这里需要强调4个点:
|
||||
|
||||
|
||||
建议基于HTTP和事件的触发方式,实现相应的接口功能;
|
||||
需要根据接口协议规范,实现请求Header和Body中值的解析和处理;
|
||||
上传镜像时,需要指定容器的启动命令Command、参数Args及监听端口Port;
|
||||
推荐实现一个Healthz函数,用于健康监测。
|
||||
|
||||
|
||||
第二,我们来创建一个函数,此时,就只需要设置函数运行相关的一些基本属性,如运行超时时间、内存大小、并发度等,再关联第一步中构建的镜像即可。
|
||||
|
||||
第三,根据我们自身的业务场景,选择合适的触发方式来请求函数计算平台,比如,我们可以通过创建和设置一个HTTP触发器来链接事件源。
|
||||
|
||||
第四,通过HTTP触发器进行请求。由于自定义镜像是HTTP Server的形式存在,我们通常会设定监听的IP、端口(如0.0.0.0:9000)、超时时间、HTTP参数(如Keep-alive)等。这里需要说明一下,如果是首次启动,函数计算平台会从镜像仓库拉取自定义的镜像,并启动容器。
|
||||
|
||||
最后,函数计算服务控制器会将接收到的请求,调度到对应的HTTP Server,由HTTP Server接管后续的服务处理。
|
||||
|
||||
这样,我们就通过自定义镜像的功能,达到了快速开发和交付的能力,对于老的服务改造迁移,成本也非常低了。
|
||||
|
||||
如何优雅地切换流量?
|
||||
|
||||
我经常会被客户问到,我们的函数功能开发完成后,如何灰度发布,怎么保证我的流量无损切换,如何让我小流量实验后再全面转正呢?这可能是刚接触FaaS的通用问题了。接下来,我们可以看看函数计算的流量是如何做到优雅切换的。
|
||||
|
||||
这里引出两个概念:版本和别名,函数计算支持为发布的函数版本创建别名。别名可以理解为指向特定版本的一个指针。你可以利用别名来轻松实现流量切换、版本回滚以及灰度发布等功能。下面这个示意图,可以很好地说明这个运行机制。
|
||||
|
||||
|
||||
|
||||
以HTTP触发器为例,如果没有别名,每次新版本上线,你都需要手动修改HTTP触发器关联的版本号。如上图所示。
|
||||
|
||||
在实际业务开发中,一般会基于最新版本$LATEST的代码来开发。针对开发好的函数,你可以发布一个版本出来,比如Public Version1,函数计算平台会记录发布的版本信息,通过配置一个别名指向该版本,就可以用来处理对外的请求了。
|
||||
|
||||
而且,你还可以在$LATEST版本上继续开发。同样,开发完成之后,只需要再次发布新的版本Public Version2即可。
|
||||
|
||||
如果此时,我们需要将流量切换到新发布的版本上,只需要切换别名的指向即可,而用户在这个过程是无感知的,也不会有任何的流量损失。
|
||||
|
||||
如果我们对新的功能版本不是那么放心,还可以设置主版本和新版本的流量比例。比如我们设置95%的流量指向主版本,5%的指向最新版本。观察没问题后,再全流量切换到新版本。如果出现问题,需要进行回滚,只需将指针指向原来的版本或者关闭新版本的测试流量即可。
|
||||
|
||||
这样就可以优雅地完成版本的迭代升级了。别名无法脱离服务或版本单独存在,使用别名访问服务或函数时,函数计算会将别名解析为其指向的版本,调用方无需了解别名指向的具体版本。有了版本控制和别名,我们就可以在线上服务不受影响的基础上,完成优雅的流量变更。
|
||||
|
||||
如何削峰和容灾?
|
||||
|
||||
上线的平滑性问题解决了,那么,函数在运行的时候呢?是否也像微服务那样,考虑到了服务的治理呢?
|
||||
|
||||
这里我们聚焦一下,看看微服场景下老生常谈的流量突增和服务处理异常的情况。这种问题,在Serverless领域也同样需要关注。
|
||||
|
||||
|
||||
|
||||
微服务中,主要是通过异步消息队列的方式来应对流量峰值,当然,这只是针对延时不敏感的服务场景来说,如日志的ETL、批量处理、事件处理等。针对这种情况,在函数计算的Serverless平台中,也往往会通过引入消息队列的方式,先将突增的流量引入进来,然后再通过后端的调度系统调度给云函数处理。
|
||||
|
||||
这样的做法,既可以避免突增流量使得后端资源池来不及扩容,导致出现失败的情况,又让函数计算平台可以快速地响应请求方,避免等待,由函数计算平台来保证请求可靠地执行。
|
||||
|
||||
既然谈到万一资源池扩容没跟上请求速度,或者像公有云这种资源池出现调度问题的情况了,那么我们可以想一想,函数计算平台是否有容灾的能力呢?
|
||||
|
||||
从平台的角度来说,云厂商都是做了同步重试、异步延时重试的逻辑的。针对异步的信息,甚至引入了递增重试机制和异步策略到用户自己的容灾队列、容灾处理函数中去处理,来进一步保障消息的可靠传输和处理。
|
||||
|
||||
那么,平台是怎么做的呢?一般来说,云厂商会选择在函数计算引擎之上增加流量转发和调度层。如果收到函数实例返回错误的信息,通过在调度层用户配置的策略来进行针对性的分发即可。
|
||||
|
||||
|
||||
|
||||
从使用者的角度来看,针对延时不敏感的场景,建议选择异步方式来处理函数的计算,同时配置容灾队列或者云函数的链式调用方法来解决处理异常的情况。
|
||||
|
||||
到这里,我们今天要讲的内容也快进入尾声了。不过,要做到微服务成熟的治理模式,对于Serverless 的函数计算平台和业务来说,都还有比较长的路要走。
|
||||
|
||||
FaaS作为云原生时代的“连接器”“胶水语言”,在处理复杂场景逻辑的时候,必不可少地要跟其他VPC内的服务进行交互,在开发和运维过程中,也需要完善的开发工具链和可观测能力的支撑。后续我也会逐一来展开,跟你介绍其中的实现机制和实战策略。
|
||||
|
||||
小结
|
||||
|
||||
最后,我来小结一下今天的内容。虽然通过前面的小节,你已经可以在一些简单的场景使用函数计算了,但复杂的场景还需要进一步掌握它的一些高级属性,这些属性包括层、自定义镜像、别名和版本、异步策略的使用等。
|
||||
|
||||
函数计算平台,通过提供层的能力,让你能够轻松地封装起公共的代码库和工具依赖库,解决了公共代码复用和依赖的问题;考虑到传统开发者切换的需求,采用自定义镜像的方式,不仅可以提高开发和交付的效率,还能够从一定程度上复用原来的CICD系统和老的服务模块,进一步提升人效。
|
||||
|
||||
考虑到我们的服务是需要上生产环境的,别名和版本这一对孪生兄弟,也就正好有了用武之地。其实,在其他的云产品中也有这种用法,比如Elasticsearch中的别名也有类似的作用。
|
||||
|
||||
服务的线上运行,少不了容灾降级和应对峰值压力的考量,虽然函数计算具备弹性伸缩的能力,但考虑到资源池的调度速度和未知的错误容灾,作为一名“FaaSer”,你也应该学会将微服务的成熟的经验引入进来,并在函数计算、Serverless领域发挥起来。
|
||||
|
||||
如果你的工作内容仅限于使用函数计算平台来开发业务代码,那么通过本节课程,你应该对平台的使用和注意事项已经非常熟悉了,对开发一个高可用、高可扩展的函数服务也一定有了较为深入的理解。
|
||||
|
||||
如果你的工作角色是一个云平台开发者,那么,本节课对你步入函数计算领域或者进一步梳理、补充FaaS形态的Serverless平台的能力,都是一个很好的参考支点。
|
||||
|
||||
思考题
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个思考题。
|
||||
|
||||
函数计算形态下的Serverless,是如何打造高级的能力来应对长进程的处理场景的?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起交流进步。
|
||||
|
||||
|
||||
|
||||
|
254
专栏/Serverless进阶实战课/04冷启动:如何加快函数的第一次调用过程?.md
Normal file
254
专栏/Serverless进阶实战课/04冷启动:如何加快函数的第一次调用过程?.md
Normal file
@ -0,0 +1,254 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
04 冷启动:如何加快函数的第一次调用过程?
|
||||
你好,我是静远。
|
||||
|
||||
前面几讲,我们基本上都是在谈Serverless的“好处”或者如何“用好”Serverless。那么Serverless有没有不完美的地方呢?
|
||||
|
||||
带着这个问题,我今天就来跟你聊聊它的一个瑕疵——“冷启动(Code Start)”。这个问题比较重要,为什么呢?因为不论是面对私有化企业还是公有云上的客户,大家首先问到的,通常都是和冷启动相关的函数性能问题。冷启动关乎到服务的时延等稳定性问题,进而会影响到用户使用服务的体验。在“即时时代”的今天,这是一个不可忽视的问题。
|
||||
|
||||
这节课,我将通过分析冷启动存在的原因、冷启动的过程,从平台和开发者的角度,带你了解如何加速函数的启动,掌握这里面的优化手段。
|
||||
|
||||
冷启动是什么?
|
||||
|
||||
在前面的课程中,我们知道,当请求被调度到函数实例上时,如果这个函数实例在上一次执行完代码后没有被回收,那么接收到请求后只需要复用这个实例进行代码的执行即可,这个过程被称为热启动过程。
|
||||
|
||||
如果服务是首次请求,或者是容器实例在服务请求后被回收了,就会触发冷启动。那么,冷启动具体是怎么工作的呢?
|
||||
|
||||
冷启动都有哪些步骤?
|
||||
|
||||
业界中,云厂商或者开源项目因为优化手段的不同,导致冷启动耗时或多或少会有些不一样,但基本的原理是差不多的。我们看一下冷启动都有哪些步骤。
|
||||
|
||||
|
||||
|
||||
第一步,容器创建。这一步通常会体现在扩缩容过程,当所有容器实例都在处理请求时,就需要向集群申请创建新的容器。
|
||||
|
||||
这里需要提一下,函数计算平台通常会支持多种语言的运行时,这些运行时一般来说会打包一个镜像,以DeamonSet的方式运行在Kubernetes集群中。我们在冷启动的时候,会根据不同的参数请求,动态挂载所需的运行时到对应的运行路径下。这个运行的过程,我没有体现在图上,而是把它算作资源调度层面了。
|
||||
|
||||
第二步,代码、层依赖下载。这一步应该是整个冷启动耗时比较长的过程。函数计算本身并不具备持久化的能力,代码包和层依赖通常都是从其他存储服务端拉取。因此,这一步的耗时会受到用户上传代码包大小、网速等因素的影响。当然,代码包通常是压缩包的形式,下载到本地后,还需要解压。
|
||||
|
||||
第三步,环境变量、参数文件准备。主流的函数计算平台往往提供了环境变量注入的能力,这个过程就发生在冷启动阶段。除此之外,运行时以及容器本身可能还需要准备一些参数配置文件等,这个过程耗时相对较短,我们可以暂时不关注这块的优化。
|
||||
|
||||
第四步,用户VPC以及相关资源准备。如果用户还为函数接入了私有网络,那么还需要为容器进行一些VPC网络打通的初始化工作。同时,如果用户使用了类似分布式文件系统这类功能,还需要进行挂载,也会产生一部分额外耗时。
|
||||
|
||||
第五步,运行时初始化。通常指的是云厂商标准的Runtime环境的启动过程。这一步受编程语言类型的影响比较大,比如Java类型的代码由于要启动JVM,启动相对其他语言就比较慢。
|
||||
|
||||
第六步,用户代码初始化。这一步有的时候和运行时初始化容易混淆,为什么这么说呢?比如编译型的语言,打包后就是一个完整的可执行程序,在运行时启动的时候,就已经有一部分工作加载起来了。那么,如果是解释型语言,我们就可以把它理解为用户代码包的加载过程。
|
||||
|
||||
因此,针对这一步,我们可以理解为用户的自定义业务逻辑代码的加载,某些业务逻辑init的过程,比如写在Handler之外的数据库连接的创建、Handler方法内的缓存初始化等。
|
||||
|
||||
看到这里你可能会有一个疑问,如果使用的是公有云的函数计算平台,一旦代码包比较大,就会增加整个调用链的耗时,那么每次冷启动执行的过程,会不会都额外产生费用呢?
|
||||
|
||||
实际上是不会的,你可以看到,上图中我专门区分了平台侧和用户侧两部分耗时,函数的计费都是从程序开始执行开始计算,也就是从第六步开始收费。即使是Java这种启动较慢的运行时,也只是你感觉慢了,但费用并没有多收。
|
||||
|
||||
影响冷启动时长的因素有哪些?
|
||||
|
||||
六个步骤这么说下来,冷启动(Cold Start)的耗时大部分和以下4个方面有关。
|
||||
|
||||
|
||||
资源调度和容器创建过程: 一般在秒级别时间完成;
|
||||
代码和依赖层的下载过程:取决于代码大小以及是否有加速,一般从毫秒到秒级别不等;
|
||||
VPC网络的打通过程:主要是弹性网卡和路由下发耗时,通常在秒级别;
|
||||
运行时与用户代码初始化过程:和用户比较贴近,依据不同的语言,启动时间会有所影响,大概在毫秒到秒之间不等。
|
||||
|
||||
|
||||
从上述的分析,我们基本可以看出,优化冷启动的手段不是一方的事情,需要平台和用户双方努力去完成,遵循的逻辑就是“哪里慢就让哪里快”。下面,我们一起让“慢”的地方“快”起来。
|
||||
|
||||
平台侧的优化手段
|
||||
|
||||
细心的你应该能看到,在这张图里,我将不同的阶段用平台侧和用户侧两个阶段进行了标识。那么首先,我们按照函数实例加载运行的时间顺序来看看平台侧的优化。
|
||||
|
||||
|
||||
|
||||
资源调度和容器创建
|
||||
|
||||
我们知道,容器启动时一般会先检查本地有无相关镜像,如果没有的话,需要从远端仓库拉取,而函数所处的容器环境通常都是一个Alpine或者Ubuntu之类的基础镜像,应用层的资源对象则会基于基础镜像之上进行打包,如果出现远端拉取的情况,还是比较耗时的。
|
||||
|
||||
传统容器运行需要将全量的镜像数据下载后再解压,一方面导致容器启动耗时较长,另一方面也可能因为集群规模过大,下载解压环节造成了较大的网络、磁盘读写压力,进而导致大规模容器启动不符合部署预期。但实际上在启动容器时,可能又仅使用容器镜像内的部分数据。
|
||||
|
||||
镜像加速要解决的就是这种传统容器需要提前下载镜像再启动导致耗时较长的问题。一般业界会从两个路径入手。
|
||||
|
||||
|
||||
按需加载:使用加速镜像版本,配置加速规则和标签,通过按需加载的模式,免于全量下载镜像,避免带宽浪费和分发效率的影响;
|
||||
P2P加速:利用计算节点的内网带宽资源,在节点之间分发镜像,降低镜像仓库的压力。
|
||||
|
||||
|
||||
另外,头部的云厂商,还会通过启用多镜像仓库,进一步缓解单仓库的存储和服务压力,来进一步提升资源调度和下载速度。
|
||||
|
||||
代码和层依赖下载
|
||||
|
||||
在准备好基础资源和容器之后,我们来看代码和层依赖相关的加速方法。它们或多或少和用户也有一些联系,这里,我来给你分享三个常用的方法。
|
||||
|
||||
|
||||
方法一:压缩包暂存
|
||||
|
||||
|
||||
虽然平台不能直接动手操作用户的代码,但我们可以通过实现本地缓存来提高代码以及层依赖的获取速度。
|
||||
|
||||
在函数第一次冷启动处理某个请求时,会将代码下载到Node节点上,并通过特殊编码进行标记。那么,当Node上有其他函数实例需要使用这份代码时,可以根据标记码查看本地缓存是否存在,如果有的话,就直接将其挂载到函数实例对应的路径下,省去代码包再次下载的过程。
|
||||
|
||||
|
||||
方法二:常用依赖的内置化
|
||||
|
||||
|
||||
平台也可以根据普遍用户的使用场景,将一些常用的依赖提前内置化。比如Python做数据处理的NumPy、Panda,处理PDF的底层依赖Xpdf等。
|
||||
|
||||
这些常用的依赖一般都会在构造容器基础镜像时一起构建。这样,就能保证这些公共依赖在任何容器实例下,都不需要额外下载就能够直接使用。
|
||||
|
||||
|
||||
方法三:极致压缩
|
||||
|
||||
|
||||
我们还可以在用户创建函数时将代码包进行二次压缩,来让代码包变得更小。常见的方法是使用squashfs这种高度压缩的文件系统进行处理,它甚至可以将几个G的文件压缩成几百M。
|
||||
|
||||
跨VPC网络加速
|
||||
|
||||
一般从平台的角度来看,函数运行在一个专有的VPC集群内,如果我们的云函数需要和用户VPC集群内的资源(如Elasticsearch、Redis等)打交道,该怎么办呢?
|
||||
|
||||
原来业内比较传统的方式,是通过动态创建弹性网卡ENI来实现跨VPC的访问。这样做有3个比较明显的缺点:
|
||||
|
||||
|
||||
弹性网卡的创新耗时比较久,导致冷启动耗时急剧增加;
|
||||
弹性网卡占用用户VPC的IP资源,如果没有空闲的资源,会导致创建失败;
|
||||
造成弹性网卡较大的浪费。
|
||||
|
||||
|
||||
|
||||
|
||||
那么我们可以怎么改进呢?目前,我比较推荐在集群VPC内部创建代理的方式。这个方式的核心在于IP隧道技术的使用,使得平台可以在保持函数原始目标IP地址的情况下,将流量通过Proxy代理节点的方式触达用户端VPC。
|
||||
|
||||
当然,函数平台会为关联了VPC的函数创建轻量的虚机,将弹性网卡绑定在代理虚机上,所有关联该VPC网络的流量最终都会通过代理进行发送。
|
||||
|
||||
为了确保服务的稳定,Proxy代理机通常采用主备的方式进行,正常情况下,Node的出口都是通过Master Proxy来代理请求。但Mater Proxy挂掉之后,平台会切换到Slave Proxy来转发。
|
||||
|
||||
|
||||
|
||||
这种方法,可以通过监听用户在Console关联VPC的操作时,提前进行创建。这样,在函数第一次请求处理的时候,由于只需要转发操作,就可以直接带来一个数量级的性能提升。
|
||||
|
||||
需要注意的是,这种方式虽好,但也有一定的瑕疵,我们需要额外消耗Proxy的Node资源。现在,你可以先想想它的具体实现方式是怎么样的,之后我也会在小试牛刀中跟你仔细来演练和讲解。
|
||||
|
||||
用户实例提前预加载
|
||||
|
||||
涉及到预加载的过程,一般都跟算法和策略有关系了。我在这里基于一线的经验给你提供一些思路,当然,在应用前,你还是需要根据自己平台的实际调用量和场景进行训练调优,才可以达到不错的效果。下面,我们可以看看一些常用的预加载用户代码实例方法。
|
||||
|
||||
第一种,基于函数互相调用的场景进行提前预测。这里,你可以参考腾讯函数计算公开的函数链式调用预热的方法。如下图所示,函数A调用B和C,我们可以根据它们之间的调用拓扑关系,提前进行预启动函数B和C。
|
||||
|
||||
|
||||
|
||||
第二种,基于函数的版本进行预测,如果流量版本1的流量比例是不足100%的,那么我们可以推测另一个占据流量的版本X,也应该加载起来。这种方式,在腾讯函数计算中也公布过。
|
||||
|
||||
|
||||
|
||||
第三种,我们还可以根据提前加载镜像的方式,部署到Node中,尤其是对于一些大镜像,如10G的镜像,提前分发,然后通过单机侧的Agent上报标记。流量到来时,直接分发到预加载过的Node的容器上,以快速应对函数的执行处理。
|
||||
|
||||
类似的方法还有不少,你还可以在机器学习和预测预取的思路上进一步思考,也欢迎你随时和我讨论。
|
||||
|
||||
到这里,我们平台侧的优化手段就讨论得差不多了。Serverless的技术在不停地进步,如果你是一个平台的开发人员,可以看看还有没有哪些方法可以更好地优化你的函数计算平台。当然,我们优化的时候,也不要忘了ROI。经营成本也是我们每个平台开发者需要关注的重点。
|
||||
|
||||
用户侧的优化手段
|
||||
|
||||
如果你是一个使用函数计算开发业务代码的人员,由于无法接触到底层基础设施,那还有没有你可以做的事情呢?选择一个功能完善、性能齐全、价格便宜的云厂商平台。是的,这绝对是你第一个要考虑的因素。因为平台会帮你做很多包括上述优化手段在内的事情,免去了你一大部分的烦恼。
|
||||
|
||||
那么,除此之外,我们自身还有没有可以优化的方向呢?我这里先给你列一下思考的5个方向:
|
||||
|
||||
|
||||
合理控制代码包的大小;
|
||||
选择性能较高的运行时;
|
||||
成本可控范围内合理使用预留实例;
|
||||
定时任务激活延时敏感较高的函数实例;
|
||||
本地缓存的的合理利用。
|
||||
|
||||
|
||||
你可以先暂停下来想一想,思考之后,我们开始逐一探讨。
|
||||
|
||||
合理控制代码包的大小
|
||||
|
||||
首先,代码包的拉取是整个冷启动过程最耗时的部分,即使是从内部网络进行下载,如果代码包特别臃肿,也会严重增加整个端到端的响应时长。
|
||||
|
||||
因此在函数部署之前,你可以手动删除执行过程中不必要的一些文件和依赖,让整个代码包尽可能的轻盈。
|
||||
|
||||
选择性能较高的运行时
|
||||
|
||||
在冷启动的耗时流程分析中,我们知道编程语言的类型会影响程序的启动速度。通常来说,Python、Node.js这类解释型语言的速度都会比较快,Golang这种编译形成的可执行二进制文件,加载速度也是非常快的。但像Java这一类需要在JVM中运行的程序,速度相对来说比较慢。
|
||||
|
||||
这里,我以阿里云函数计算平台为例,通过一个Hello World的程序来跟你直观地看一下语言运行时选取带来的差别。我选取了Golang和Java来做一个对比,通过Trace链路跟踪,结果如下图所示:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
你可以很明显地看到,Java的冷启动耗时282.342ms,Golang耗时67.771ms,Java的冷启动耗时是Golang的4.166倍。另外,我们从调用链路还可以发现,在运行Invocation过程中,Golang的优势也是比较明显的。
|
||||
|
||||
成本可控范围内合理使用预留实例
|
||||
|
||||
目前主流的公有云函数计算平台(阿里云FC、腾讯云SCF、百度智能云CFC等)都提供了预留实例功能。
|
||||
|
||||
用户可以为某个函数根据实际需要申请固定数量的预留实例,每个实例都已经提前准备好函数代码,并且直到用户主动释放,都会始终处于常驻状态。这样可以随时以热启动的形式应对请求的到来。
|
||||
|
||||
这种机制下,函数平台会根据用户的申请为函数实例进行预留。当函数收到请求后,会优先被调度到预留实例上并且以热启动的方式进行处理。直到该函数的所有预留实例都处于工作状态,才会将请求调度到一个非预留的实例上以冷启动的方式执行。
|
||||
|
||||
不过,在使用预留实例来解决冷启动问题时,还需要考虑到闲置成本。因为平台需要额外为用户保留一部分资源,需要用户承担一定的费用,因此,使用者对预期的请求量要有一个合理的估计范围。
|
||||
|
||||
定时激活
|
||||
|
||||
如果觉得预留实例的费用不是很划算,我们还有一种方法。你可以通过一个定时触发器来提前预热你的服务,比如每分钟预热调用一次你的服务,由于函数平台通常会有一定的时间来保留“热”的容器,所以这种方法在一定程度上可以加快你的函数的响应。
|
||||
|
||||
有一点需要注意的,也是经常很多业务客户问我的一个事:我的函数涉及到业务处理,不能乱调用。
|
||||
|
||||
这时我们要怎么处理呢?其实,一个判断逻辑就可以解决。
|
||||
|
||||
def handler(event,context):
|
||||
if event["exec"]==true:
|
||||
doSomething()
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
这个示例中,我通过一个“exec”的标志位,区分了预热和非预热的请求。当然,通过预热的方式,有的云厂商也做了定时触发策略的配置能力,进一步给你节省了成本。你也可以在实际场景中把这个方法和上面说的“预留实例”做个对比看看。
|
||||
|
||||
本地缓存的的合理利用
|
||||
|
||||
最后,在一些AI场景中,往往会涉及到数据训练任务。通常,我们的处理思路是将数据从远端拉倒本地再执行训练任务。但是如果我们在程序中加上缓存逻辑,每次拉取前都提前从本地临时磁盘空间判断数据是不是已经存在,就可以避免每次数据拉取造成的延迟了。
|
||||
|
||||
小结
|
||||
|
||||
到这里,我们针对冷启动的优化手段也聊得差不多了。最后,我来小结一下今天的内容。
|
||||
|
||||
今天我从冷启动存在的原因、工作原理入手,总结了冷启动存在的一些核心因素。从这些因素着手,以平台侧和用户侧的视角,我也提出了相关的优化手段。相信我在一线工作中积累的经验一定对你有所帮助。
|
||||
|
||||
冷启动的过程,包含容器创建、代码和层依赖的下载、环境变量与参数的准备、VPC网络及相关资源准备、运行时和用户代码初始化,一共6个部分。
|
||||
|
||||
如果你是一个平台的开发者,可以通过4种方法来优化冷启动的耗时时长。
|
||||
|
||||
|
||||
镜像加速手段优化资源调度和容器创建:如果你使用的是云平台厂商的Kubernetes调度服务,还可以通过配置预加载脚本的方式来进一步并行处理你的容器镜像;
|
||||
包的缓存、极致的压缩和内置化通用依赖:减少用户在运行时刻的依赖;
|
||||
利用代理模式的方法进行VPC网络加速:VPC的网络加速通常只会在跨VPC的业务场景用到,通过IP隧道技术 + 代理模式的方法,让前几年一直比较诟病的网络延时问题降低到毫秒级别,这是Serverless函数计算领域的一个比较大的突破;
|
||||
通过函数实例的预测算法,也能从一定程度上帮助用户解决冷启动问题。
|
||||
|
||||
|
||||
如果你是一个业务开发者,我也在本节课给了你5个方向的指引。
|
||||
|
||||
|
||||
合理控制代码包大小:这里需要我们尽量改掉原来在传统编码部分不好的习惯,尽可能精简代码;
|
||||
选择性能较高的运行时:相信通过Golang和Java的对比,你也能直观地感受到语言选择带来的性能差异;
|
||||
预留实例和定时预热:这两个方向,也是用户和平台相互协作的过程,更需要你灵活使用;
|
||||
合理利用本地缓存:对于一些大的数据拉取场景,可以适当的使用缓存的方式,避免频繁拉取数据,造成的网络带宽和执行耗时,这不仅影响你的服务处理性能,还会额外增加费用成本。
|
||||
|
||||
|
||||
Serverless的技术一直在不断的更新迭代,希望我今天的抛砖引玉,能带动你的思维,举一反三,想出更多的优化方法。
|
||||
|
||||
思考题
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个思考题。
|
||||
|
||||
你在实际工作中,还用到了哪些优化手段?或者,对哪些场景卡住了,需要一起来讨论交流的?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起学习。
|
||||
|
||||
|
||||
|
||||
|
207
专栏/Serverless进阶实战课/05扩缩容:如何应对流量的波峰波谷?.md
Normal file
207
专栏/Serverless进阶实战课/05扩缩容:如何应对流量的波峰波谷?.md
Normal file
@ -0,0 +1,207 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
05 扩缩容:如何应对流量的波峰波谷?
|
||||
你好,我是静远。
|
||||
|
||||
今天我要和你分享的主题是Serverless函数计算的核心特性:动态扩缩容。
|
||||
|
||||
在介绍扩缩容的实现原理之前,我们首先来看这样2个真实的场景。
|
||||
|
||||
场景1:汽车日志的准实时流推荐服务,白天车流量比较大的时候,访问量较大,夜晚随着车流量的降低,需要处理的量就比较小了。
|
||||
|
||||
场景2:针对线上服务的监控程序,以10秒请求一次重要接口的方式进行轮训服务,确保服务的可用性。
|
||||
|
||||
这两个场景的区别,就在于量级是否平稳。试想一下,如果用PaaS、K8s服务的思路实现场景1的功能,就需要按白天最大请求量的规格来申请资源,确保服务能实时处理完。那么,夜间大部分的资源就是空闲的,利用率很低。对于场景2来说,访问量比较平稳,PaaS服务的确是比较合理的应用,资源的使用也相对合理。
|
||||
|
||||
Serverless由于具备弹性扩缩容的能力,可以完美地解决场景1的问题,而针对这种比较轻量的场景,函数计算就是不二的选择了。因为它可以将实例缩容为0,并根据请求量级自动扩缩容,从而有效提升资源的利用率。
|
||||
|
||||
可以说,极致的动态扩缩容机制是Serverless函数计算的那顶皇冠,戴上它,才能彰显和PaaS平台的不同之处。
|
||||
|
||||
那么,这个“不同之处”背后的实现机制是什么样的呢?
|
||||
|
||||
今天,我会带你深入了解不同形态下的自动扩缩容架构原理,从K8s的HPA开始分析,再去聊聊目前开源Serverless明星框架Knative的KPA,看看它的实现和HPA有什么不同。最后,我们再提取出自动扩缩容的核心思想,让你能够理解自己要设计一个系统的时候该怎么做。
|
||||
|
||||
基本框架
|
||||
|
||||
开源的Serverless函数计算引擎核心,基本上是通过K8s的HPA来做,而云厂商一般有封装好的各种底座服务,通常可以基于底座的产品来封装,例如容器服务、容器镜像服务、云服务器等。但我们也不排除云厂商基于开源框架做二次开发的可能性。
|
||||
|
||||
云厂商容器调度服务的发展历史中,通常有两种调度形态,一种是基于节点Node调度的方式,一种是基于容器实例的方式(我们通常把这种称为Serverless的,即不感知Node维护的方式),你可以简单地认为,前者是拿了一整套房子过来,自己维护整套房子和隔间的运行,而后者只关心隔间的运行情况。
|
||||
|
||||
由于云厂商的函数计算通常是基于容器服务的底座来进行的,接下来,我就从Node和Pod两个形态,分别来跟你讲一讲它们的调度方式。
|
||||
|
||||
Node维度
|
||||
|
||||
首先,我们来看Node调度形态下,函数计算框架是如何进行动态扩缩容的。
|
||||
|
||||
|
||||
|
||||
如上图所示,这是一张基于容器PaaS服务和云服务Node的扩缩容调度示意图,我来解释一下它的各个部分都有着什么样的作用。
|
||||
|
||||
|
||||
Scheduler:调度模块负责将请求打到指定的函数实例上(Pod),同时负责为集群中的Node标记状态,记录在etcd中;
|
||||
Local-controller:Node上的本地控制器,负责管理Node上所有函数实例的生命周期,以DeamonSet的形式存在;
|
||||
AutoScaler:自动扩缩容模块,会定期检测集群中Node和Pod的使用情况,同时根据自定义策略进行扩缩容。扩容时,往往会向底层的PaaS资源进行申请,例如百度智能云的CCE、阿里云的ACK/ASK等;
|
||||
Pod :也就是上图标识的Cold和Warm两种状态的容器,Cold表示该Pod未被使用,Warm表示正在被使用或者处于等待回收的状态;
|
||||
Node:包含闲置、占用两种状态,如果一个Node上所有Pod都是“Cold”,那么该Node为闲置状态,否则为占用。
|
||||
|
||||
|
||||
当系统运转时,AutoScaler会定期检查集群中所有Node,如果检测到Node处于一个需要扩容的状态,则根据策略进行扩容。当然,这个策略完全可以由你自己定义,通常可以按照“占用除以总数的比例是否处于一个合理区间”来进行判定。
|
||||
|
||||
在这种形态下, Pod通常会作为函数实例的通用形态。代码以及不同运行时往往也是以挂载的形式注入到容器内。Autoscaler也会在轮询时根据Warm Pod的闲置时间将其重置为Cold Pod,从而让整个集群资源达到一个较高复用能力的水平。
|
||||
|
||||
在缩容时,理想情况下AutoScaler可以将Node缩容到0,但通常为了应对突发的流量请求,也会预留一部分Buffer存在。
|
||||
|
||||
你会发现,Node的调度方式对于函数计算调度引擎来说更加的灵活。但“权限越大责任越大”,Node维度的方案,除了需要管理Node的调度之外,还需要处理Node中Pod的安全隔离和使用。这种形态下,我们可以通过“空Pod”的方式来提前占用,预加载一部分Pod出来,加快“真实Pod”的创建速度。
|
||||
|
||||
Pod维度
|
||||
|
||||
可能你也猜到了,以Pod为扩缩容单元的好处就在于它能够更加细粒度地控制函数实例的数量。下面,我们就从扩缩容的“鼻祖”HPA(HorizontalPodAutoscaler)开始,了解Pod维度的扩缩容动作流程。
|
||||
|
||||
开始之前,你可以先思考这样一个问题,HPA既然就是用来负责自动扩缩容的,那么它是否可以直接用于Serverless呢?先不要急着回答,我们先看HPA的扩缩容机制。
|
||||
|
||||
常规扩缩容
|
||||
|
||||
在K8s官网有这样一段关于自身扩缩容控制器HPA的定义。
|
||||
|
||||
|
||||
The HorizontalPodAutoscaler is implemented as a Kubernetes API resource and a controller. The resource determines the behavior of the controller. The horizontal pod autoscaling controller, running within the Kubernetes control plane, periodically adjusts the desired scale of its target (for example, a Deployment) to match observed metrics such as average CPU utilization, average memory utilization, or any other custom metric you specify.
|
||||
|
||||
|
||||
不用非要看懂,这里说的,大体可以理解为这个扩缩容控制器做了两件事:
|
||||
|
||||
第一件事,定期从k8s控制面获取资源的各项指标数据(CPU利用率、内存使用率等);
|
||||
|
||||
第二件事,根据这些指标数据将资源数量控制在一个目标范围内。
|
||||
|
||||
|
||||
|
||||
从K8s的官方文档中,你基本可以看到HPA的雏形:HPA通过控制Deployment或者RC来实现对Pod实际数量的控制。官方给出的HPA架构图可能还不太好理解,我们把指标(metric)数据的收集过程加上再看看:
|
||||
|
||||
|
||||
|
||||
具体的工作流程是什么样呢?你可以对照着示意图来理解我下面说的流程。
|
||||
|
||||
在K8s中,不同的Metric会由对应的Metrics Server持续采集(Heapster 或自定义 Metrics Server),HPA会定期通过Metrics Server的API或者聚合的API Server 获取到这些Metric指标数据(CPU 和 内存使用情况),从而根据你自己定义的扩缩容规则计算出Pod的期望个数,最后,根据Pod当前的实际数量对RC/Deployment做出调整,使Pod达到期望的数量。
|
||||
|
||||
讲到这里,你应该能很快地回答出来,HPA形态的扩缩容还不能直接用于Serverless。
|
||||
|
||||
因为Serverless语义下的动态扩缩容是可以让服务缩容到0的,但HPA不能。HPA的机制是通过监测Pod的Metrics指标来完成Deployment的扩缩容,如果Deployment的副本数缩容到0,流量没有了,指标为0,那HPA也无能为力了。
|
||||
|
||||
极致扩缩容
|
||||
|
||||
正因为如此,从0到1的扩缩容过程,是需要额外的机制来加持的。由于目前社区和各大云厂商都非常重视Knative,部分的传统企业在转型过程中也选择Knative作为私有化部署方案的底座首选。所以,我这里同样选择以Knative的KPA来做说明。
|
||||
|
||||
Knative的扩缩容主要包括三个方面:流量指标的收集、实例数量的调整、从0到1的过程。
|
||||
|
||||
|
||||
|
||||
|
||||
流量指标的收集
|
||||
|
||||
|
||||
扩缩容最重要的一步,就是收集我们所需要的指标。我们可以对照着流程图来理解一下。
|
||||
|
||||
在Knative中,Revision代表一个不变的、某一时刻的代码和Configuration的快照。每个 Revision会引用一个特定的容器镜像和运行它所需要的任何特定对象(例如环境变量和卷),再通过Deployment控制函数实例的副本数,而我所说的实例,就是图中的User Pod了。
|
||||
|
||||
和我们之前介绍的Node维度扩缩容机制中的函数实例不同,这里,你可以看到每个函数实例(User Pod)中都有两个容器:Queue Proxy和User Container。其中,User Container,我相信你一定猜到了,是部署和启动我们自身业务代码的容器,那么Queue Proxy呢?
|
||||
|
||||
实际上,在每个函数实例被创建时,都会被以Sidecar的方式将Queue Proxy注入,也就是在原来的业务逻辑上再新加一个抽象层。Queue Proxy作为每一个User Pod的流量入口,负责限流和流量统计的工作。每隔一定时间,AutoScaler都会收集Queue Proxy统计的流量数量,作为后续扩缩容的重要依据。
|
||||
|
||||
通俗地来讲,其实就是在自身业务代码的容器旁边,又加了一个双胞胎Queue Proxy,双胞胎加入的方法,就是Sidecar。后续不管在这个房子的(User Pod)兄弟干了什么,都会让这个双胞胎去记录。在最外层的AutoScaler,则会去找这位双胞胎收集所有的信息,为后续的扩缩容做准备。
|
||||
|
||||
|
||||
实例数量的调整
|
||||
|
||||
|
||||
当收集到流量的指标后,AutoScaler就需要根据指标调整实例的数量了。Autoscaler会通过改变实例的Deployment来决定实例最终的个数,以便确定扩容出来多少个Pod。
|
||||
|
||||
简单的算法,就是按照将当前总的并发数平均划分到期望数量的实例上,使其符合设定的并发值。
|
||||
|
||||
这里简单举个例子,比如当前总并发数为100,设定的并发值为10,那么最终调整出的实例数量就是:100/10=10个。当然,扩缩容的实例数量还会考虑到系统的当前的负载和调度周期等因素。
|
||||
|
||||
|
||||
从0到1
|
||||
|
||||
|
||||
看到这,可能你会有个疑问:我们能够确定的,是当Revision存在实例时流量接收的情况,那么如果Revision实例缩容到了0,流量又是怎么被接收的呢?是直接丢弃,还是暂存在某个地方呢?
|
||||
|
||||
为了解决这一问题,Knative专门引入了一个叫做Activator的组件来做流量暂存和代理负载。
|
||||
|
||||
|
||||
|
||||
我们还是来看这张图。事实上,当AutoScaler将函数实例缩容为0时,会控制Activator作为实例为0时的流量接收入口,也就是图中红线的部分。Activator在收到流量后,会将请求和信息暂时缓存,并主动告知AutoScaler进行扩容,直至成功扩容出函数实例,Activator才会将暂存的流量转发到新生成的函数实例上。
|
||||
|
||||
这里带来一个问题:流量什么时候会切换到Activator或者Knative的Pod上呢?我们会在流量转发一节中仔细讲此类问题。当然,你也可以提前关注ServerlessServices(SKS)和 TBC (target-burst-capacity)等关键组件和参数的相关设置。
|
||||
|
||||
扩缩容模型设计思路
|
||||
|
||||
到这里,我们已经从Node和Pod 两个不同的维度学习了Serverless比较典型的扩缩容机制方案了。那么,思考一下,如果让你来设计一个扩缩容系统,该怎么去实现呢?
|
||||
|
||||
Node和Pod的方案一定给了你不小的启发。下面,我将从指标、决策、数量三个核心点,来给你讲一总体的设计思路。
|
||||
|
||||
|
||||
|
||||
|
||||
指标
|
||||
|
||||
|
||||
无论是Node纬度还是Pod纬度,我们会发现最终系统都是根据一些监控的指标来作为扩缩容的依据,同时,这些指标的采集任务往往都是通过定时轮询的方式来完成。
|
||||
|
||||
比如,Node扩缩容案例中用的是Node 处于空闲或占用的数量,HPA中采用CPU利用率、内存使用率来作为Pod扩缩容的依据,而Knative是通过Collector统计Pod的并发情况来进行扩缩容判断。
|
||||
|
||||
所以,指标的选取是扩缩容的首要因素,你需要按照你的平台特性,来确定需要收集哪些指标,并且通过某种手段将它们收集上来为你所用。
|
||||
|
||||
|
||||
决策
|
||||
|
||||
|
||||
拿到指标之后,最后都会通过一个专门负责扩缩容的模块(如AutoScaler)来根据这些指标进行决策。决策的过程,就是以这些指标作为输入,最终输出是否扩缩容、扩容多少或者缩容多少。
|
||||
|
||||
决策过程完全可以由你自己定,比如按照比例、固定数量,按照操作后的指标平均值等等。
|
||||
|
||||
你也可以根据系统调度情况,设置不同的调度方式,比如针对流量突然上涨采取及时扩容,针对流量的平滑升降采取延时扩容等等。像Knative就有两种模式:Stable /稳定模式和Panic /恐慌模式。
|
||||
|
||||
|
||||
数量
|
||||
|
||||
|
||||
决策结果出来之后就简单了。你可以根据结果来让资源数量处于一个预期值,这一步完全不需要你操心,不论是Node或者Pod维度,K8s的HPA都会来做好。
|
||||
|
||||
为服务引入预测能力
|
||||
|
||||
最后,还记得我们最开始说的场景1的日志处理情况吗?日志的流量出现了明显的波浪式浮动,比如晚上23点之后流量减少,上午6点之后流量增加。那么,为了应对流量的波峰到来比较迅猛的情况,我们还可以提前预测预扩,让系统的扩容更平滑。
|
||||
|
||||
|
||||
|
||||
这里还是以Node级别的扩缩容为例。我们可以在扩缩容模块的基础上,以旁路的方式额外增加一个预测模块。定期统计Node状态和数量的信息,同时根据历史信息对外进行周期预测。当然,历史的信息时间窗口和预测周期都需要经过大量的数据训练以及人工干预因子,才可以在生产环境用得比较顺手。
|
||||
|
||||
但我们也要知道,它不是“万金油”,并不是什么情况都能预测准确,只能说在一定阈值范围内起到一定的辅助作用。
|
||||
|
||||
同时,这是一个性能和成本博弈的过程,尤其对于云厂商来说,极致的优化成本是我们每个研发人员追求的目标。
|
||||
|
||||
小结
|
||||
|
||||
最后,我来小结一下我们今天的内容。
|
||||
|
||||
这节课,我从常见的两个场景案例入手,介绍了Serverless核心特性扩缩容相对于普通PaaS服务的优势。这个优势不仅体现在资源的利用率上,进一步来说,更反应在成本上。
|
||||
|
||||
接着,我从实际市场运用和历史演进的角度出发,介绍了Node和Pod两个维度中市面上常见的扩缩容机制。介绍的重点放在了Kubernetes的HPA以及目前比较火热的明星Serverless引擎Knative的KPA机制上,也从流量指标的收集、实例数据的调整和区别最大的的0到1的过程来讲解了整个扩缩容的原理。
|
||||
|
||||
而上述的讲解,都是为了我们能从共性中抽象出一套设计扩缩容系统的思路,这里面,最主要的还是要抓住三个核心要点:指标、决策、数量。
|
||||
|
||||
最后,我们如果是要以平台的视角提供服务,还可以做得更智能,通过预测系统让扩容变得更加平滑,性能更优。
|
||||
|
||||
思考题
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个思考题。
|
||||
|
||||
想一想,还有没有什么更好的方法能够避免系统频繁扩缩容,从而保证稳定的资源供给呢?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起进步。
|
||||
|
||||
|
||||
|
||||
|
256
专栏/Serverless进阶实战课/06流量转发:函数在不同情形下是如何执行的?.md
Normal file
256
专栏/Serverless进阶实战课/06流量转发:函数在不同情形下是如何执行的?.md
Normal file
@ -0,0 +1,256 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
06 流量转发:函数在不同情形下是如何执行的?
|
||||
你好,我是静远。
|
||||
|
||||
在前面的课程中,我跟你分享了函数实例的冷启动和扩缩容,这两个点是Serverless的核心特征。可以说,提到Serverless必然会提到冷启动和扩缩容。但你是否想过,是什么促使Serverless的函数实例从0到1,从1扩容到N,又从N缩容到0的呢?
|
||||
|
||||
这正是我本节课程要跟你分享的主题——流量机制。确切地说,是流量在这些情形下的转发机制。希望通过这节课,你能够了解Serverless在冷热启动、常规流量升降、异步请求、多并发等不同情形下流量的转发过程,并在脑海中构筑出一幅Serverless的全链路流量转发拓扑图。
|
||||
|
||||
这节课,我选择了Knative作为开源的Serverless引擎框架,来介绍冷启动和分流机制的流量转发。至于详细的开源引擎的分析、以及开源引擎私有化整体解决方案,我会在第三模块实战进阶中跟你详细探讨。
|
||||
|
||||
知识储备
|
||||
|
||||
在讲流量转发之前,我们先来回顾一下Knative,它主要包括Eventing、Serving,Build三大组件,其中Build现在已经被tektoncd/pipeline替代。这三大组件中,跟我们主题相关的主要是分管流量的Serving组件。
|
||||
|
||||
Knative Serving定义了一组特定的对象,使用Kubernetes CRD的方式来实现这些对象。我们看一张Knative官方简单示意图:
|
||||
|
||||
|
||||
|
||||
当用户创建一个Knative Service的时候,它的Controller会自动创建对应的Route和Configuration资源,其中Configuration就对应了我们关于业务代码的镜像配置,每次Configuration修改后,就会创建出对应的Revision版本。
|
||||
|
||||
Revision则代表某一时刻的Configuration的快照,每个Revision会拥有一个自己的Deployment,如果流量策略定义了转发给不同的Revision,那么实际上,就是转发给这些Revision的Deployment。
|
||||
|
||||
图中的Route继承了Service中的流量策略的相关配置,也会决定请求的流量转发走向。如果Service中没有定义相关流量策略的话,那么就是默认所有流量转发给最新的Revision。
|
||||
|
||||
流量转发
|
||||
|
||||
有了Knative Serving基本概念的知识储备,我们就可以开始从流量的入口说起了。
|
||||
|
||||
入口流量
|
||||
|
||||
入口的网关层面,Knative从设计之初就考虑到了扩展性的问题,通过抽象出Knative Ingress资源来对接不同的网络扩展。一般来说,我推荐使用Kourier、Istio、Contour这三种来做对接。如果我们的 Knative使用了Istio作为网络组件,那么外部流量就会传给 istio-ingressgateway,它会根据 Knative Serving 创建的 VirtualService,并结合Gateway来决定把流量转发给哪个用户 Service。
|
||||
|
||||
那么Gateway和VirtualService具体的角色和协作方式是什么样子的呢?我们一起看一下。
|
||||
|
||||
|
||||
Gateway:用于暴露对外服务的端口和域名,决定了哪些流量是可以经过的。比如下面这个例子定义的是访问域名jy.geekbang.com且指定80端口的流量可以经过这个Gateway。
|
||||
|
||||
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: jy-gateway
|
||||
spec:
|
||||
selector:
|
||||
app: istio-ingressgateway
|
||||
servers:
|
||||
- port:
|
||||
number: 80
|
||||
name: http
|
||||
protocol: HTTP
|
||||
hosts:
|
||||
- jy.geekbang.com
|
||||
|
||||
|
||||
|
||||
VirtualService:用于配置流量的路由,类似 Nginx 的 Server 配置,通过和Gateway关联,来决定流量的最终去处。这里我也给你一个示例,结合上面的Gateway,这里表示的是从上面网关过来的流量最终会发往jy-demo-v1.default.svc.cluster.local。
|
||||
|
||||
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: VirtualService
|
||||
metadata:
|
||||
name: jingyuan-vs
|
||||
spec:
|
||||
hosts:
|
||||
- jy.geekbang.com
|
||||
gateways:
|
||||
- jy-gateway
|
||||
http:
|
||||
- route:
|
||||
- destination:
|
||||
host: jy-demo-v1.default.svc.cluster.local
|
||||
|
||||
|
||||
冷启动的流量转发
|
||||
|
||||
下面我们来看一下Knative的冷启动情况下,流量是如何被处理的。
|
||||
|
||||
|
||||
|
||||
这里需要说明一下,为了方便你专注于冷启动的转发过程,我将一些不涉及到流量部分的交互暂时去掉了,比如ServerlessService的模式转变,AcutoScaler指标上报等。
|
||||
|
||||
另外,图中我针对不同对象资源做了颜色和线条的区分。其中,所有的实线矩形都表示Pod对象,也就是具体处理请求的进程,而虚线矩形则代表非Pod类型的其他资源对象。
|
||||
|
||||
这里,我们着重介绍图里的Public Service和Private Service,它们是决定流量走向Pod IP还是Activator的关键。我们来分别看一下。
|
||||
|
||||
|
||||
Public Service:由Knative管控,其EndPoints是可变的。如果当前的Revision存在User Pod,那么Public Service的EndPoints会与Private Service的保持一致,指向这些实例;而如果不存在User Pod,那么冷启动流程时,其EndPoints将指向Activator的IP地址。
|
||||
Private Service:通过Label Selector 来筛选图中对应的Deployment产生的User Pod,其他组件可以通过监听 Private Service的EndPoints情况,来了解当前的Revision是否有User Pod存在。因为其本身不做流量的转发,所以我在图中用灰色体现。
|
||||
|
||||
|
||||
|
||||
|
||||
了解了冷启动流程图中每一个区块的含义,我们再来对照着图片了解一下冷启动的实际请求流向。
|
||||
|
||||
步骤0:这里要完成的是准备工作。当你打包好函数镜像,创建好Knative Service之后,Knative会根据定义来创建Route、Configuration(cfg)资源,然后cfg会创建对应的Revision版本。Revision又会创建Deployment提供服务。
|
||||
|
||||
步骤1:当请求到达Istio Ingress Gateway后,Istio Ingress Gateway会根据Gateway和virtualService配置信息,将请求转发给Public Service。
|
||||
|
||||
步骤2:由于此时集群中还没有User Pod(图中的User Pod需要等到AutoScaler扩容后才有),而Public Service的EndPoints配置的是Activator,所以请求接下来会交给Activator处理。
|
||||
|
||||
步骤3:Activator会将收到的请求暂存,同时统计请求对应Revision的实际流量并上报给AutoScaler。另外,Activator还会不断监听Private Service,检查是否已经存在User Pod 了。
|
||||
|
||||
步骤4:AutoScaler根据流量情况控制对应Revision的Deployment来实现User Pod的扩容。在冷启动过程中,会至少保证创建一个Pod出来。
|
||||
|
||||
步骤5:Deployment创建新的User Pod,同时会更新Private Service的EndPoints,将Private Service的EndPoints指向新生成的User Pod。
|
||||
|
||||
步骤6:Activator检测到Private Service关联的EndPoints后,会将请求直接转发到新的User Pod。User Pod收到请求后,就会执行业务逻辑。
|
||||
|
||||
其实在从0到1的过程中,还有一点需要说明的,那就是在User Pod创建成功后,AutoScaler会触发SKS的模式为serve,同时将Public Service的EndPoints同步为Private Service的Endpoints,也就是切换回了常规流量的处理模式。而新的流量到来后,就会通过Public Service直接到达User Pod,不再走Activator代理。
|
||||
|
||||
但Knative本身引入了一个TBC(Target Burst Capacity)来控制什么情况下Activator会从流量路径中摘除,这样做的好处是为了防止流量突然的增大,导致单个Pod失衡,进而影响到请求的丢失。
|
||||
|
||||
这就是Knative在冷启动下的流程机制,我们可以看出,Knative针对流量从0到1的启动过程,考虑到了流量的突增的因素,在流量入口,考虑到了便捷的扩展能力,能够更好地复用网络组件。
|
||||
|
||||
那么,他又是怎么基于已有的网络组件快速做到流量的治理,做到小流量上线的呢?
|
||||
|
||||
流量分流机制
|
||||
|
||||
我们在知识储备中了解到,Service负责创建Route、Configuration 以及 Revision 资源,通过 Service 可以将流量路由到指定的 Revision中。而在指定的过程中,分流的核心是Route,它负责把网络端点映射到一个或多个 Revision,可以通过多种方式管理流量,包括灰度流量和重命名路由。
|
||||
|
||||
首先来看一个普通的 Knative Service 定义,该 Knative Service 下有两个版本,分别是 Revision traffic-demo-v1、traffic-demo-v2,流量策略按80:20的比例路由到不同 Revision,tag 分别是 tg1 和 tg2。其中流量策略定义如下所示:
|
||||
|
||||
traffic:
|
||||
- tag: tg1
|
||||
revisionName: traffic-demo-v1
|
||||
percent: 80
|
||||
- tag: tg2
|
||||
revisionName: traffic-demo-v2
|
||||
percent: 20
|
||||
|
||||
|
||||
在 Knative Service 部署到 default namespace 后,Knative 就会创建 Route 对象,因为我们的 Knative 用了 Istio 作为网络组件,所以还会继续创建 Istio VirtualService(traffic-demo-ingress)。
|
||||
|
||||
在图中,你可以看到第一个 VirtualService 关联了 Knative 创建的两个 Gateway,第二个关联的 Gateway 是 “mesh”,这是 Istio 保留的一个关键字,意思是将 VirtualService 的规则应用到集群里所有的 sidecar,这里我们可以先忽略它。
|
||||
|
||||
|
||||
|
||||
我们看一下traffic-demo-ingress的路由生成,由于配置信息量比较大,我将route相关的部分列了出来,方便你更清晰地查阅:
|
||||
|
||||
route:
|
||||
- destination:
|
||||
host: traffic-demo-v1.default.svc.cluster.local
|
||||
port:
|
||||
number: 80
|
||||
headers:
|
||||
request:
|
||||
set:
|
||||
Knative-Serving-Namespace: default
|
||||
Knative-Serving-Revision: traffic-demo-v1
|
||||
weight: 80
|
||||
- destination:
|
||||
host: traffic-demo-v2.default.svc.cluster.local
|
||||
port:
|
||||
number: 80
|
||||
headers:
|
||||
request:
|
||||
set:
|
||||
Knative-Serving-Namespace: default
|
||||
Knative-Serving-Revision: traffic-demo-v2
|
||||
weight: 20
|
||||
|
||||
|
||||
你可能会想,destination 的host格式为什么要命名为:traffic-demo-v1.default.svc.cluster.local。
|
||||
|
||||
这里我简单提一下,这种命名本身就是Kubernetes Service的DNS的名字,是Kubernetes内部使用的名称。名称的组成规则是 ..svc. ,zone 名字一般是 cluster.local。比如 namespace default 下部署了一个traffic-demo-v1 service,那么它的 DNS 名称就是 traffic-demo-v1.default.svc.cluster.local。
|
||||
|
||||
在Knative中,我们通常可以通过修改config-domain的配置来自定义一个域名后缀,使得外部请求的时候可以访问。再由istio ingress gateway根据virtualService 中配置的这些策略实现分流,最终实现下图的效果。
|
||||
|
||||
|
||||
|
||||
在小流量/灰度的发布上,开源的引擎框架Knative就是这样做的了。但云厂商可能由于平台的完备性,具备专门的自研小流量实验基建的服务能力,一般也会基于流量的抽样落点来进行。
|
||||
|
||||
常用功能点
|
||||
|
||||
在上面的内容中,我们从流量入口着手,了解了Serverless引擎在冷启动和小流量下的流量转发机制。那么,云厂商在流量转发上还有哪些加持呢?我们可以聊聊平时常用的两个功能点:异步调用和单实例多并发下的流量转发机制。
|
||||
|
||||
异步下的流量调度
|
||||
|
||||
我们从前面已经了解到,同步的调用请求可以通过路由功能进行分流,那么异步和它有什么样的不同呢?
|
||||
|
||||
一些离线任务往往适合从同步流程中剥离出来,进行异步处理。比如像日志清洗、定时触发、音视频处理等等。目前主流的函数计算平台都提供了函数的异步处理能力。
|
||||
|
||||
用户通过异步接口的API主动发起,在异步调用函数时,你不需要等待函数代码的响应,只要把这次请求交给平台侧处理就可以了。当然,更多的场景通常是结合各类异步触发器来使用。比如我们之前学到的对象存储触发器,通过设置好存储桶Bucket和存储对象的匹配规则,就可以简单快捷地实现音视频转码以及ETL等业务逻辑的处理。
|
||||
|
||||
|
||||
|
||||
这里,我们可以简单说一下异步下的流量调度过程。
|
||||
|
||||
请求首先会到达异步处理模块,并按照顺序排队,这也是异步处理模块通常会结合消息队列实现的原因。当轮到你的请求被处理时,异步处理服务会再将请求发到对应的函数实例进行执行,并将执行结果反馈到你配的监控日志上。除此之外,像AWS、阿里等主流的函数计算平台还会支持异步的执行策略,你可以根据每次异步执行的最终结果来配置“执行成功”和“执行失败”的下游服务。
|
||||
|
||||
总的来说,异步调用主要通过事件/消息队列的方式来缓冲一步,然后通过异步处理模块,按照同步请求的请求机制来处理。流量的转发除了在入口处不一样之外,后续的同步请求依然跟上面类似。
|
||||
|
||||
多并发下的流量转发
|
||||
|
||||
下面我们再来看请求在单实例多并发下的流量调度过程。
|
||||
|
||||
先看两个概念:
|
||||
|
||||
|
||||
QPS,函数实例Pod每秒可以响应的请求数;
|
||||
并发数,同一时刻函数实例Pod接收的请求数。
|
||||
|
||||
|
||||
从这两个概念中我们能够发现,并发数的增加并不一定会导致QPS增加。达成了这个共识之后,我们再来看一下函数计算的并发实现需要注意哪些要点。
|
||||
|
||||
首先,你需要是HttpServer的服务类型。
|
||||
|
||||
如果你是基于云厂商的标准运行时,就需要看一下他是否支持。我会在运行时这节课里给你分析关于GoLang Runtime的代码片段,当请求到来时,除了以HttpServer的形式运行之外,还会通过go func的形式进行处理,这种方式可以从一定程度上进一步提升函数的并发处理能力。
|
||||
|
||||
其次,你的服务需要能在框架中暴露端口,供流量转发进来。
|
||||
|
||||
第三,需要有一个组件来控制并发。由于云厂商没有公布实现,我们还是回到Knative中来看,它提供了queue-proxy和AutoScaler的搭配能力,完美解决了流量的并发问题。
|
||||
|
||||
queue-proxy 是 一个伴随着用户容器运行的 Sidecar 容器,跟用户容器运行在同一个 Pod 中。每个请求到达业务容器User Container之前,都会经过 queue-proxy 容器。
|
||||
|
||||
它的主要作用是统计和限制到达User Container的请求并发量。当你对一个Revision设置了并发之后,比如设置了10,那么queue-proxy就会确保不会有超过10个的请求达到User Container。
|
||||
|
||||
那么,如果有超过10个请求到来,又该怎么办呢?它会先暂存在自己的队列queue里面,同时通过9090端口,统计当前User Pod的并发情况,提供给AutoScaler采集和扩缩容参考。
|
||||
|
||||
|
||||
|
||||
小结
|
||||
|
||||
最后我们来小结一下。今天我给你介绍了Serverless形态下的流量转发过程,包括冷启动、小流量、异步调用、多并发下几种场景的流量调度机制。
|
||||
|
||||
首先,我们选取了社区活跃度较高和经验客户使用较多的Knative作为切入点来展开,为了便于你理解流量的转发过程,我们先解释了Knative的关键资源对象及其作用。通过流量入口的介绍,我们可以发现Knative网关层面的扩展性是非常值得我们平时在设计云原生架构时参考的。
|
||||
|
||||
接着,我详细分析了容器实例在流量到来时,从0到1时请求的执行过程。这里面涉及到核心的Activator用来承载从0到1和从N到0的桥接,它能根据请求量来对请求进行缓存和负载。与它一起协作的资源有Serverless Service(SKS)和AutoScaler。
|
||||
|
||||
同时,我们可以通过在Knative Service中定义Revision的流量比例,通过创建Istio VirtualService来配置不同版本的分流策略。
|
||||
|
||||
在异步场景下,我们还可以通过队列的方式来做到削峰填谷,进一步完善Serverless平台的能力。
|
||||
|
||||
最后,我提到了实现并发响应的几个要素,HttpServer服务和旁路的流量代理组件是一个不错的组合。通过流量的转发能力,使得你可以在Serverless下依然享受高并发的处理能力。
|
||||
|
||||
相信在学习过今天的课程之后,你已经能通过开源的引擎框架去学习,并且在查阅云厂商的介绍时更加的得心应手了。在进阶实战的板块里,我还会跟你一起来探讨基于开源引擎构建一个Serverless平台的能力,可以期待一下。
|
||||
|
||||
思考题
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个思考题。
|
||||
|
||||
在遇到突发流量到来的时候,Knative是如何应对的?有哪些参数控制?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起交流学习。
|
||||
|
||||
延伸阅读
|
||||
|
||||
关于Knative Serving路由机制可以继续通过官方手册深入阅读一下。
|
||||
|
||||
|
||||
|
||||
|
272
专栏/Serverless进阶实战课/07运行时(上):不同语言形态下的函数在容器中是如何执行的?.md
Normal file
272
专栏/Serverless进阶实战课/07运行时(上):不同语言形态下的函数在容器中是如何执行的?.md
Normal file
@ -0,0 +1,272 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
07 运行时(上):不同语言形态下的函数在容器中是如何执行的?
|
||||
你好,我是静远。
|
||||
|
||||
在生命周期这节课里,我和你提到了支撑函数运行起来的核心之一是运行时,而且我们还可以在函数计算的平台上选择不同语言的运行时来开发我们的函数,这其实也是函数的一大优势:多语言的运行时,可以极大降低开发者的语言门槛。
|
||||
|
||||
那么函数计算层面的运行时是什么呢?不同语言的运行时,它们的工作机制是一样的么?如果让你来自定义一个运行时,你打算怎么做呢?
|
||||
|
||||
带着这些问题,今天我们来聊聊这背后的实现。我会从源码的角度,以编译型语言Golang、解释型语言Python为代表,来跟你层层分析函数计算运行时的机制,带你抽象出通用的思路,并体验如何构建一个自定义运行时。
|
||||
|
||||
希望通过这两节课,你能够对运行时的原理和特性有一定的理解,搞清楚函数计算平台究竟是如何打破编程语言技术栈的限制,为开发人员提供多种开发环境的。同时,相信这节课也会帮助你在后续的函数计算使用和开发中更加得心应手。
|
||||
|
||||
今天这节课,我将重点介绍运行时的基本特性和实现原理,并以编译型语言Golang为切入点来讲解它的运行流程,让你先有一个从0到1的认知过程。
|
||||
|
||||
运行时究竟是什么?
|
||||
|
||||
我们对“运行时”这个名词并不陌生,任何编程语言都有它自己的运行时。比如Java的运行时叫Java Runtime,它能够让机器读懂这些Java的代码并且运行起来,换个说法,就是它让代码可以和机器“打交道”,进而实现你的业务逻辑。
|
||||
|
||||
那么,迁移过来理解,函数计算运行时(Runtime)就是能够让函数在机器或容器中执行起来,实现业务逻辑功能的执行环境,它通常由特定语言构建的框架构成。函数计算的运行时依赖语言的运行时存在。不过由于更贴近上层应用,分析它的工作原理要相对简单一些。
|
||||
|
||||
|
||||
|
||||
上面的示意图,展示的是运行时、函数的组合与初始化进程的关系。在函数实例初始化时,函数运行时一般会由一个初始化进程给加载起来,然后Runtime就可以进行服务请求的内部通信,正常接收、处理请求。当请求到达后,你的代码就会在对应的语言运行时中被加载起来的代码处理。
|
||||
|
||||
因此,我们可以简单地将Runtime理解为一个特定语言环境下的服务框架环境,这个服务将以一个进程的形态运行在用户容器中,并与用户代码相关联。当服务启动后,会一直等待请求的到来。一旦请求到达,运行时就会关联你的代码去执行,执行完成后,又会继续处理下一个请求。
|
||||
|
||||
这里我要强调的是,这个执行不一定是串行的,有的架构为了提升并发,也会采用协程或者线程的方式去执行。
|
||||
|
||||
这个流程看起来比较容易理解对不对?那么,我们再深入流程看一眼,想一想,不同的语言运行时的实现,是一样的吗?
|
||||
|
||||
实现原理
|
||||
|
||||
运行时,归根到底还是一个编程语言编写出来的具体程序,所以对于上面这个问题,我们先来看看编程语言本身有哪些区别。
|
||||
|
||||
语言类型
|
||||
|
||||
我们知道,计算机只能执行二进制指令,而我们根据不同编程语言转换为二进制指令的时机,将不同的编程语言分为了解释型语言和编译型语言。
|
||||
|
||||
编译型语言,比如C、C++、GoLang,在编译时需要将所有用到的静态依赖、源码一起打包,编译完成后就可以直接执行。而像Python、Node.js这种解释型语言则只需要通过解释器执行,因此,完全可以做到业务代码与依赖分离的形式。
|
||||
|
||||
这里需要注意一点,我们常用的Java,虽然需要经过编译,但是编译产生的机器码需要由JVM再次转换为二进制指令,因此是具有解释型和编译型两种特性的。我个人更偏向将其定位为编译型语言,因为在你使用Java开发函数的时候也会发现,我们通常是将所有依赖包打包成一个Jar包或者War包的方式上传,比较符合编译语言的风格。
|
||||
|
||||
另外,如果你使用过不同云厂商的函数计算平台,你会发现,像GoLang、Java这类编译型语言,通常在开发过程中,都需要强依赖一个平台提供的包,而Python、Node.js则不需要,这是为什么呢?
|
||||
|
||||
在上面提到的语言类型的区别中我们提到,因为编译型语言需要将所有关联的静态代码依赖一起打包,所以在函数实例上具体的体现,就是你的业务代码和运行时会生成一个完整二进制文件、Jar包或者War包。
|
||||
|
||||
了解完语言的不同,你应该也做好这两种函数计算运行时的实现也会不同的心理准备了。接下来,我会从从编译型和解释型两个角度来分别跟你聊一聊运行时的实现。
|
||||
|
||||
Golang运行时
|
||||
|
||||
上面我提到,对于编译型语言,用户代码通常需要和运行时一起编译,所以一般的云厂商都会将编译型语言的运行时开源。
|
||||
|
||||
这里我们以阿里云函数计算FC的GoLang Runtime运行时为例,一起看看它的实现原理。
|
||||
|
||||
|
||||
|
||||
GoLang Runtime运行时主要做的事情有三点。
|
||||
|
||||
|
||||
获取请求
|
||||
|
||||
|
||||
在GoLang Runtime中,平台会提前将RUNTIME API写到环境变量里,Runtime会通过初始化客户端对象runtimeAPIClient,从这个接口中获取到请求。
|
||||
|
||||
|
||||
关联用户的入口函数
|
||||
|
||||
|
||||
用户入口函数,也就是图中的UserHandler。在Golang 的运行时中,其实是通过反射机制获取的,并将UserHandler封装到一个统一格式的结构体Handler中,再将Handler作为Function结构体类型的一个属性进行赋值,这样做的好处是用户完全可以按照自己的编程习惯去定义,而不用对UserHandler的结构做出任何限制。
|
||||
|
||||
这里解释一下,在源代码中,作者沿用的是handler,图中的UserHandler是我为了区分主进程中的handler而替换的一个名字,下文在用户侧定义的handler,我们都统一用UserHandler来做区分。
|
||||
|
||||
|
||||
调用UserHandler对请求进行处理
|
||||
|
||||
|
||||
在获取请求与UserHandler(如:HandleRequest(ctx context.Context, event string))后,就可以用第二步创建的Function对象去执行请求了。
|
||||
|
||||
接下来,我将从GoLang用户侧代码的入口函数(main函数)开始 ,详细地梳理一遍上面的处理过程。
|
||||
|
||||
入口
|
||||
|
||||
当整个二进制被加载起来后,程序首先会进入main函数并将用户定义的函数入口方法userHandler作为参数传入Start方法,并对Start方法进行调用。
|
||||
|
||||
Start的入参为interface{},这样的传参方式可以让你的userHandler被定义为任何类型。
|
||||
|
||||
/**
|
||||
* base function type
|
||||
* eventFunction functionType = 101
|
||||
* httpFunction functionType = 102
|
||||
**/
|
||||
func Start(userhandler interface{}) {
|
||||
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
||||
log.Println("start")
|
||||
StartWithContext(context.Background(), userhandler, eventFunction)
|
||||
}
|
||||
|
||||
func StartWithContext(ctx context.Context, userhandler interface{}, funcType functionType) {
|
||||
StartHandlerWithContext(ctx, userhandler, funcType)
|
||||
}
|
||||
|
||||
|
||||
进入Start()方法后,再深入下去,最终会调用StartHandlerWithContext这个方法。接着,会将全局变量runtimeAPIStartFunction赋值给startFunction这个局部变量。
|
||||
|
||||
关于runtimeAPIStartFunction,我也列出了下方的代码。你可以发现,它包含了一个环境变量名env以及一个之后循环处理请求的方法startRuntimeAPILoop。
|
||||
|
||||
func StartHandlerWithContext(ctx context.Context,
|
||||
userhandler interface{}, funcType functionType) {
|
||||
startFunction := runtimeAPIStartFunction
|
||||
// 获取RUNTIMEAPI
|
||||
config := os.Getenv(startFunction.env)
|
||||
...
|
||||
err := startFunction.f(ctx, config, handlerWrapper{userhandler, funcType}, lifeCycleHandlers)
|
||||
...
|
||||
}
|
||||
|
||||
// runtimeAPIStartFunction是提前定义好的全局变量
|
||||
runtimeAPIStartFunction = &startFunction{
|
||||
env: "FC_RUNTIME_API",
|
||||
f: startRuntimeAPILoop,
|
||||
}
|
||||
|
||||
|
||||
最后,在获取到startFunction.env的环境变量真实值后,你会发现前面我们传入的userHandler、上下文Context以及刚刚获取的环境变量,都传递给了startRuntimeAPILoop,并对它进行了调用,这些其实就是请求处理流程之前所需要的关键信息。
|
||||
|
||||
准备工作
|
||||
|
||||
拿到了函数请求需要的参数后,就需要拉取请求并对其进行处理了,你可以先通过代码来感知一下startRuntimeAPILoop是如何工作的:
|
||||
|
||||
func startRuntimeAPILoop(ctx context.Context, api string, baseHandler handlerWrapper, lifeCycleHandlers []handlerWrapper) (e error) {
|
||||
...
|
||||
// 创建连接RuntimeAPI接口的客户端,之后客户端会从这个接口获取请求信息
|
||||
client := newRuntimeAPIClient(api)
|
||||
// 根据传入的UserHandler转化为运行时中标准的Function结构体
|
||||
function := NewFunction(baseHandler.handler, baseHandler.funcType).withContext(ctx)
|
||||
...
|
||||
for {
|
||||
// 获取请求信息
|
||||
req, err := client.next()
|
||||
...
|
||||
// 启动新的协程让function对请求进行处理
|
||||
go func(req *invoke, f *Function) {
|
||||
err = handleInvoke(req, function)
|
||||
...
|
||||
}(req, function)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
首先,程序会根据前面获取到的RUNTIMEAPI创建一个客户端,这样就保证了请求的获取途径。接着,会根据之前传入的userHandler以及userHandler类型创建出一个Function类型的对象。
|
||||
|
||||
这个Function对象的创建,会根据start入口传递下来的function type的值来确定是创建event类型的handler还是http类型的handler,分别对应处理事件的请求和Http的请求。
|
||||
|
||||
这里我提供的代码中传输的是eventFuntion,后文就通过这一条方法调用流来进行追踪。
|
||||
|
||||
func NewFunction(handler interface{}, funcType functionType) *Function {
|
||||
f := &Function{
|
||||
funcType: funcType,
|
||||
}
|
||||
// 这里根据传入funcType来决定构造哪种类型的handler
|
||||
if f.funcType == eventFunction {
|
||||
f.handler = NewHandler(handler)
|
||||
} else {
|
||||
f.httpHandler = NewHttpHandler(handler)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
|
||||
event类型的handler会通过NewHandler获得,该函数的返回值要求返回一个Handler类型的接口,这个接口需要实现一个标准的Invoke方法。
|
||||
|
||||
type Handler interface {
|
||||
Invoke(ctx context.Context, payload []byte) ([]byte, error)
|
||||
}
|
||||
func NewHandler(userhandler interface{}) Handler {
|
||||
...
|
||||
// 获取userhandler的动态值
|
||||
handler := reflect.ValueOf(userhandler)
|
||||
// 获取userhandler的类型信息
|
||||
handlerType := reflect.TypeOf(userhandler)
|
||||
...
|
||||
return fcHandler(func(ctx context.Context, payload []byte) (interface{}, error) {
|
||||
...
|
||||
// 通过动态值对userhandler进行调用
|
||||
response := handler.Call(args)
|
||||
...
|
||||
return val, err
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
在NewHandler中,也会利用GoLang中的反射机制,获取到userhandler的类型和动态值,并通过反射信息构造出一个有标准传参以及返回值的fcHandler类型的方法。在fcHandler中,由于代码中的handler本身为Value类型,因此可以通过Call方法调用其本身所代表的函数。如果你对反射的细节感兴趣,也可以看看关于Go语言反射的官方手册介绍。
|
||||
|
||||
fcHandler是很特殊的,它本身是一个函数类型,并且已经实现了Invoke方法,因此也是一个Handler类型,这就解释了上文为什么以fcHandler作为返回值。而fcHandler中的Invoke最后调用了自己本身对应函数对请求进行了处理。
|
||||
|
||||
当准备工作就绪后,程序就开始对请求进行处理了,通过上述代码分析不难得出,主协程在这个函数中主要做了三件事:
|
||||
|
||||
|
||||
准备获取用户请求的客户端(newRuntimeAPIClient)以及Funtion(对Handler封装);
|
||||
不断通过客户端获取新的请求,也就是代码中的client.next()方法;
|
||||
分配一个新的协程,并让Function在新协程中处理获取到的请求。
|
||||
|
||||
|
||||
执行流程
|
||||
|
||||
那么,进入新的协程以后,请求才会真正地被最初传入的userHandler所执行,我们深入到协程中的handleInvoke方法,会发现存在这样的调用关系:
|
||||
|
||||
->handleInvoke 1
|
||||
->function.Invoke 2
|
||||
->function.invokeEventFunc || function.invokeHttpFunc) 3
|
||||
->function.handler.Invoke 4
|
||||
|
||||
|
||||
在代码标号的第三步里,如果我们在入口start处传递的是httpFunction类型,这里就会调用function.invokeHttpFunc。当然,我们还是继续沿着上面提到的event事件请求来追踪,继续调用function.invokeEventFunc,在这个函数里面,会调用fn.handler.Invoke。
|
||||
|
||||
结合上面的函数调用关系来看,当执行到f.handler.Invoke时,实际上Invoke会对fcHandler进行一次调用,最后fcHandler通过handler.Call完成了对userHandler的调用。
|
||||
|
||||
type fcHandler func(context.Context, []byte) (interface{}, error)
|
||||
|
||||
func (handler fcHandler) Invoke(ctx context.Context, payload []byte) ([]byte, error) {
|
||||
|
||||
response, err := handler(ctx, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseBytes, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return responseBytes, nil
|
||||
}
|
||||
|
||||
|
||||
我将上面的流程梳理成了如下的示意图,你可以对照着再回溯一遍GoLang运行时的主流程:
|
||||
|
||||
|
||||
|
||||
这就是GoLang Runtime运行时在event调用的主流程,那么针对更细的流程和定义,你可以从Github上将代码下载下来,按照这个思路,逐一理解就可以了。
|
||||
|
||||
通过GoLang Runtime运行时的学习,相信你已经清楚了解了运行时需要完成的工作以及它整个的处理流程。
|
||||
|
||||
今天我们就先讲到这里,你可以先消化一下。解释型语言的运行时如何运行?在云厂商不开源情况下,我们又要如何剖析它?这些问题,我们下一节课再来继续讨论。
|
||||
|
||||
小结
|
||||
|
||||
最后,我来小结一下我们今天的内容。这节课,我给你介绍了以Golang为代表的编译型语言运行时在Serverless函数计算形态下的实现原理,函数计算运行时(Runtime),本质上就是一个让函数在容器中执行起来的代码框架。
|
||||
|
||||
运行时通常会由一个初始化进程加载起来,然后进行内部服务的通信,接收和处理该函数收到的请求。
|
||||
|
||||
根据编程语言类型的不同,运行时的实现上也会略微有所不同。编译型语言的运行时需要和用户代码一起打包成二进制文件或者其他特定语言类型的包(如Jar包、War包),而解释型语言的运行时则可以与用户代码分离存在。所以,厂商一般都会将编译型运行时的代码进行开源,以SDK的形式提供给开发者使用。
|
||||
|
||||
从Golang Runtime的代码框架中,我们可以看出,运行时主要就是获取请求、关联用户的函数入口Handler、执行用户的实现。
|
||||
|
||||
希望你通过今天的课程,能够对函数计算形态下的语言运行时有一定的了解,不仅会用,更知道它如何实现的,在后续遇到问题或者开发更复杂的功能时,能够做到心中有数。
|
||||
|
||||
思考题
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个思考题。
|
||||
|
||||
我们上一节课中讲了Knative,那么Knative涉不涉及运行时一说呢?运行时只存在在云厂商的平台上么?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起交流学习。
|
||||
|
||||
|
||||
|
||||
|
295
专栏/Serverless进阶实战课/08运行时(下):不同语言形态下的函数在容器中是如何执行的?.md
Normal file
295
专栏/Serverless进阶实战课/08运行时(下):不同语言形态下的函数在容器中是如何执行的?.md
Normal file
@ -0,0 +1,295 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
08 运行时(下):不同语言形态下的函数在容器中是如何执行的?
|
||||
你好,我是静远。这节课,我们继续来看运行时的原理和实践。
|
||||
|
||||
在上一节课里,我跟你分享了运行时的基本理念以及不同语言下运行时的区别,并且通过GoLang的例子让你对运行时有了一个具象化的理解。
|
||||
|
||||
今天我将重点介绍后半部分:解释型语言的实现和异同。通过今天的课程,相信你会对运行时有一个从1到N的认知,并从中抽象出一套构建思路,用于自己的实战中。
|
||||
|
||||
Python运行时
|
||||
|
||||
通过GoLang Runtime运行时的学习,相信你已经了解运行时需要完成的工作以及整个处理流程了。
|
||||
|
||||
在前面我也提到了,编译型语言的业务逻辑与依赖通常都存在着强绑定的关系,需要将运行时和用户编写的函数代码统一编译成一个二进制文件或者Jar包、War包后再被执行,但解释型语言不需要。这也从一定程度上说明了很多函数计算平台只对编译型语言运行时进行开源的原因。
|
||||
|
||||
接下来,我以Python语言为例来介绍一下解释型语言的运行机制,带你进一步加深对函数计算运行时的理解。
|
||||
|
||||
如果你使用Python或者Node.js开发过函数计算的代码,你会发现,其实上传的文件完全不需要任何依赖,甚至两行代码就可以定义好你的函数入口,比如下面这个Python的案例:
|
||||
|
||||
def handler(event, context):
|
||||
return 'hello world'
|
||||
|
||||
|
||||
在这种情况下,其实是不利于我们对运行时做分析的,但有的云厂商提供了登录函数实例的能力。我们就继续以阿里云函数计算FC来说明,它提供的实例登录功能,可以让我们直接到实例中查看运行时的执行过程。
|
||||
|
||||
|
||||
|
||||
路径
|
||||
|
||||
登录函数实例后,我们首先要找到运行时被挂载在哪个路径下。想一想,我们有可能在哪里找到它呢?
|
||||
|
||||
运行时作为一个容器的进程,不断等待请求的到来,它的生命周期和函数容器实例是保持一致的,因此,我们可以直接在当前路径下使用“ps” 命令,查看当前实例内进程的运行状态。
|
||||
|
||||
root@s-62c8e50b-80f87072536d411b8914:/code# ps -ef
|
||||
UID PID PPID C STIME TTY TIME CMD
|
||||
user100+ 1 0 0 02:16 pts/0 00:00:00 /var/fc/runtime/rloader
|
||||
user100+ 4 1 0 02:31 pts/0 00:00:00 /var/fc/lang/python3/bin/python3 -W ignore /var/fc/runtime/python3/bootstrap.py
|
||||
root 6 0 0 02:31 ? 00:00:00 /bin/sh -c cd /code > /dev/null 2>&1 && COLUMNS=167; export COLUMNS; LINES=27; export LINES; TERM=xterm-256color; expor
|
||||
root 7 6 0 02:31 ? 00:00:00 /bin/sh -c cd /code > /dev/null 2>&1 && COLUMNS=167; export COLUMNS; LINES=27; export LINES; TERM=xterm-256color; expor
|
||||
root 8 7 0 02:31 ? 00:00:00 /usr/bin/script -q -c /bin/bash /dev/null
|
||||
root 9 8 0 02:31 pts/0 00:00:00 sh -c /bin/bash
|
||||
root 10 9 0 02:31 pts/0 00:00:00 /bin/bash
|
||||
|
||||
|
||||
1号进程为实例启动的初始进程,4号进程是通过1号进程fork出来的,它其实就是Python运行时了,而bootstrap.py是整个运行时的入口,看进程的启动参数就可以知道Python运行时的路径其实就在/var/fc/runtime/python3/ 下。
|
||||
|
||||
核心流程
|
||||
|
||||
针对Python运行时这一部分,我将只截取main函数的部分代码来跟你介绍它的主要流程,你也可以顺便体会一下Python和GoLang运行时在处理请求中有哪些异同。完整的代码,你可以进入到/var/fc/runtime/python3/中看到。
|
||||
|
||||
def main():
|
||||
......
|
||||
try:
|
||||
_init_log() # 日志初始化
|
||||
...
|
||||
_set_global_concurrent_params() # 设置全局并发参数
|
||||
fc_api_addr = os.environ['FC_RUNTIME_API']
|
||||
fc_runtime_client = FcRuntimeClient(fc_api_addr) # 创建runtime client
|
||||
_sanitize_envs()
|
||||
except Exception as e:
|
||||
...
|
||||
return
|
||||
# 等待请求
|
||||
....
|
||||
while True:
|
||||
try:
|
||||
if global_concurrent_limit == 1:
|
||||
request = fc_runtime_client.wait_for_invocation()
|
||||
else:
|
||||
request = fc_runtime_client.wait_for_invocation_unblock()
|
||||
...
|
||||
handler = FCHandler(request, fc_runtime_client) # 获取函数入口函数
|
||||
if global_concurrent_limit == 1:
|
||||
handler.handle_request() # 执行业务逻辑
|
||||
else:
|
||||
# 如果是多并发执行,则通过线程启动执行任务
|
||||
from threading import Thread
|
||||
Thread(target=handler.handle_request).start()
|
||||
except Exception as e:
|
||||
...
|
||||
|
||||
|
||||
从代码结构看,整个运行时的核心处理流程和GoLang运行时中的逻辑基本类似,包括了以下三个方面。
|
||||
|
||||
|
||||
服务初始化: 包括日志初始化、设置并发模式下的全局参数。同时,还是通过FC_RUNTIME_API这个环境变量所提供的服务地址,构造之后获取请求的客户端;
|
||||
获取请求:使得客户端不断获取请求信息;
|
||||
根据请求信息执行用户代码:这里和Golang唯一的区别就是,在多并发情况下,Python指派的是线程来完成任务的处理,而不是协程。
|
||||
|
||||
|
||||
这里我再简单提一下关联用户handler的方法,以及调用用户handler方法处理请求的过程,也就是FCHandler的handle_request方法:
|
||||
|
||||
def handle_request(self):
|
||||
...
|
||||
event, ctx, handler = self.get_handler()
|
||||
# load user handler
|
||||
valid_handler, request_handler = _concurrent_get_handler(handler)
|
||||
# execute handler
|
||||
if self.function_type == Constant.HANDLE_FUNCTION and self.request.http_params:
|
||||
# http function
|
||||
succeed, resp = wsgi_wrapper(self.request_id, request_handler, event, ctx, self.request.http_params)
|
||||
if succeed:
|
||||
self.print_end_log(True)
|
||||
self.fc_runtime_client.return_http_result(self.request_id, resp[0], resp[1], Constant.CONTENT_TYPE)
|
||||
return
|
||||
else:
|
||||
succeed, resp = execute_request_handler(self.function_type, request_handler, event, ctx)
|
||||
if succeed:
|
||||
self.print_end_log(True)
|
||||
self.fc_runtime_client.return_invocation_result(self.request_id, resp, Constant.CONTENT_TYPE)
|
||||
return
|
||||
...
|
||||
|
||||
|
||||
程序首先会调用get_handler,获取请求中的userHandler信息。这个信息其实就是你在云函数元信息中配置的类似index.handler这种形式的入口函数。后续的过程和GoLang的处理思路基本上是一样的,也是通过反射机制获取到具体的userHandler,然后对其进行调用。
|
||||
|
||||
我们可以用一个示意图来梳理一下上面的主流程,便于你之后回溯执行过程:
|
||||
|
||||
|
||||
|
||||
分析运行时的小技巧
|
||||
|
||||
像GoLang、Java这一类运行时,一般都有开源的代码可以查看,而像Python这一类解释型语言的运行时,代码通常都不能直接获取到,并且很多函数计算平台也不会像阿里云函数计算FC一样,为用户提供函数实例的登录入口。这个时候,如果你想要分析运行时,其实是不太方便的。
|
||||
|
||||
这里教给你一个小技巧。一般来说,用户都是有函数容器实例内文件的可读权限的,你可以通过在代码中注入命令的方式查看。
|
||||
|
||||
我继续以Python运行时为例,简单演示一下。通过在函数代码内执行一些shell命令,就可以将想看的运行时代码打印出来,如通过ps 、ls、cat等常用命令,就可以完成我们需要的任务:
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
def handler(event, context):
|
||||
print(os.system("ps -ef"))
|
||||
print(os.system("ls -l /var/fc/runtime"))
|
||||
print(os.system('cat /var/fc/runtime/python3/bootstrap.py'))
|
||||
return 'hello world'
|
||||
|
||||
|
||||
这里需要说明一点,不同的云厂商可能实现方式不一样,有的厂商对运行时代码路径做了屏蔽处理,你只能看到一层入口而已。
|
||||
|
||||
运行时构建思路
|
||||
|
||||
学习了两种不同类型运行时的实现原理之后,相信你对运行时的整个工作流程已经有了一个比较清晰的认知。接下来,我们就可以利用上面学到的知识,构建一个自己的运行时了。
|
||||
|
||||
来回想一下,请求从到达函数实例再到被执行经过了哪些关键步骤?
|
||||
|
||||
|
||||
需要有一个初始化进程,将运行时加载到内存中运行起来;
|
||||
运行时需要获取到请求的详细信息。在GoLang、Python中,都是通过客户端请求约定的接口获取的,如RUTIME_API。这里也可以通过其他方式来获取,比如管道;
|
||||
调用用户代码执行请求,并在处理后,请求返回结果。
|
||||
|
||||
|
||||
我们在自己构建函数平台的时候,就可以参考这种方式,但如果你是一个函数业务开发人员,也完全可以基于云厂商的工具和平台来定义一个自己的运行时。
|
||||
|
||||
实战体验
|
||||
|
||||
接下来我会带你利用阿里云函数计算FC提供的开发工具Serverless Devs手动搭建一个自定义运行时,帮助你加深对整个运行时概念的理解。
|
||||
|
||||
我们首先选择python37作为自定义运行时,创建一个初始项目。
|
||||
|
||||
s init fc-custom-python37-event
|
||||
|
||||
|
||||
在初始化项目时候,会提示你进行一些配置,按照提示一一输入即可,这里我给新的项目叫做GeekBang。
|
||||
|
||||
|
||||
|
||||
进入到项目路径下,我们可以看到都创建了哪些东西:
|
||||
|
||||
$ tree .
|
||||
.
|
||||
├── code
|
||||
│ ├── gunicorn_conf.py
|
||||
│ ├── requirements.txt
|
||||
│ └── server.py
|
||||
├── readme.md
|
||||
├── s.yaml
|
||||
└── s_en.yaml
|
||||
|
||||
|
||||
可以看到,它包括了一些yaml格式的配置文件,以及一个code 文件夹。这两个yaml文件其实一样,只不过s_en.yaml里是英文注释,我们点开s.yaml进行查看,就会发现里面除了配置超时时间、内存限制等常见参数外,还有一个自定义运行时的启动参数。
|
||||
|
||||
customRuntimeConfig:
|
||||
command:
|
||||
- gunicorn
|
||||
args:
|
||||
- '-c'
|
||||
- 'gunicorn_conf.py'
|
||||
- 'server:app'
|
||||
|
||||
|
||||
这其中,gunicorn 是一个Python的http服务器,在函数实例第一次被初始化时会执行如下的命令,并且这条命令最终会将代码路径下的server.py文件启动起来。
|
||||
|
||||
gunicorn -c gunicorn_conf.py server:app
|
||||
|
||||
|
||||
我们再看server.py的代码,会发现其实他本质上就是一个http服务。
|
||||
|
||||
@app.route('/invoke', methods=['POST'])
|
||||
def event_invoke():
|
||||
rid = request.headers.get(REQUEST_ID_HEADER)
|
||||
print("FC Invoke Start RequestId: " + rid)
|
||||
|
||||
data = request.stream.read()
|
||||
print(data)
|
||||
|
||||
try:
|
||||
# do your things, for example:
|
||||
evt = json.loads(data)
|
||||
print(evt)
|
||||
except Exception as e:
|
||||
exc_info = sys.exc_info()
|
||||
trace = traceback.format_tb(exc_info[2])
|
||||
errRet = {
|
||||
"message": str(e),
|
||||
"stack": trace
|
||||
}
|
||||
print(errRet)
|
||||
print("FC Invoke End RequestId: " + rid +
|
||||
", Error: Unhandled function error")
|
||||
return errRet, 404, [("x-fc-status", "404")]
|
||||
|
||||
print("FC Invoke End RequestId: " + rid)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
当你在控制台或者开发工具执行调用时,最终就会调用/invoke这个接口,来实现你的业务逻辑。在try结构下,你可以直接调用一个定义好的函数,我这里只是简单地将event事件进行了打印而已。
|
||||
|
||||
然后,我们依次按照通过build、deploy等代码部署到云端,最后,可以通过invoke命令调用。下面,是我们体验的结果:
|
||||
|
||||
$ s invoke -e '{"key":"value"}'
|
||||
|
||||
========= FC invoke Logs begin =========
|
||||
FC Invoke Start RequestId: 5c44f71b-eadb-4bbf-8969-396ecf4e25a4
|
||||
b'{"key":"value"}'
|
||||
{'key': 'value'}
|
||||
FC Invoke End RequestId: 5c44f71b-eadb-4bbf-8969-396ecf4e25a4
|
||||
21.0.0.1 - - [27/Jun/2022:07:17:14 +0000] "POST /invoke HTTP/1.1" 200 15 "-" "Go-http-client/1.1"
|
||||
|
||||
Duration: 2.60 ms, Billed Duration: 3 ms, Memory Size: 1536 MB, Max Memory Used: 80.95 MB
|
||||
========= FC invoke Logs end =========
|
||||
|
||||
|
||||
你会发现,调用结果和常规的运行时执行基本没有区别。
|
||||
|
||||
当然,我们还是登录云端的控制台来看一下进程状态,可以发现最终是由gunicorn启动的server在提供http 服务。
|
||||
|
||||
root@c-62ca6e0c-3721605419294647aac9:/code# ps -ef
|
||||
UID PID PPID C STIME TTY TIME CMD
|
||||
root 1 0 4 06:13 pts/0 00:00:00 /usr/local/bin/python /code/.s/python/bin/gunicorn -c gunicorn_conf.py server:app
|
||||
root 4 1 3 06:13 pts/0 00:00:00 /usr/local/bin/python /code/.s/python/bin/gunicorn -c gunicorn_conf.py server:app
|
||||
root 5 1 3 06:13 pts/0 00:00:00 /usr/local/bin/python /code/.s/python/bin/gunicorn -c gunicorn_conf.py server:app
|
||||
root 11 0 0 06:13 ? 00:00:00 /bin/sh -c cd /code > /dev/null 2>&1 && COLUMNS=167; export COLUMNS; LINES=27; export LINES; TERM=xterm-256color; expor
|
||||
root 12 11 0 06:13 ? 00:00:00 /bin/sh -c cd /code > /dev/null 2>&1 && COLUMNS=167; export COLUMNS; LINES=27; export LINES; TERM=xterm-256color; expor
|
||||
root 13 12 0 06:13 ? 00:00:00 /usr/bin/script -q -c /bin/bash /dev/null
|
||||
root 14 13 0 06:13 pts/0 00:00:00 sh -c /bin/bash
|
||||
root 15 14 0 06:13 pts/0 00:00:00 /bin/bash
|
||||
root 17 15 0 06:13 pts/0 00:00:00 ps -ef
|
||||
|
||||
|
||||
通过这种方式,你应该已经明白,运行时的实现机制和执行流程其实是一样的,只是基于不同的语言类型和本身的特性,编码的方式不一样而已。
|
||||
|
||||
小结
|
||||
|
||||
今天我以不同的方式带你了解了Python运行时的运行流程,在这期间,也跟你分享了一个查看函数计算实例中运行情况的方法——在代码中注入命令。
|
||||
|
||||
通过这两节课程的介绍,我们可以总结出来,运行时主要做的是这三件事:
|
||||
|
||||
|
||||
获取请求信息;
|
||||
关联用户代码;
|
||||
调用用户代码处理请求。
|
||||
|
||||
|
||||
这里我们要注意的是,在获取请求信息时,通常都是从单机侧的约定接口或管道获取,在获取请求后,可以通过反射机制获取到用户代码的入口信息,最后再调用。
|
||||
|
||||
如果只考虑最简单的运行流程,运行时中甚至不需要做一些复杂的类型转换工作(如反射机制等),只需要像自定义运行时中的例子一样,定义好函数实例启动的执行命令,并提供一个简单的http服务即可。
|
||||
|
||||
希望你通过这节课程,对函数计算形态下的语言运行时有一定的了解,不仅会用,更知道它是如何实现的。在后续遇到问题和开发更复杂的功能时,能够做到心中有数。
|
||||
|
||||
思考题
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个思考题。
|
||||
|
||||
你的场景用自定义运行时的时候多么?有遇到过哪些问题吗?又是如何解决的呢?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起交流学习。
|
||||
|
||||
|
||||
|
||||
|
170
专栏/Serverless进阶实战课/09小试牛刀(一):如何利用函数之间的调用解决业务问题?.md
Normal file
170
专栏/Serverless进阶实战课/09小试牛刀(一):如何利用函数之间的调用解决业务问题?.md
Normal file
@ -0,0 +1,170 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
09 小试牛刀(一):如何利用函数之间的调用解决业务问题?
|
||||
你好,我是静远。
|
||||
|
||||
在前面的课程中,我跟你分享了基于FaaS形态的Serverless核心技术,从用户和平台不同视角,学习了函数的初步使用方法,理解了其中的原理机制和设计思想。
|
||||
|
||||
虽然是基于FaaS的介绍,但不论是FaaS还是托管服务形态,在扩缩容、计量计费、流量调度等方面,Serverless的基础特征都是如出一辙的。FaaS只是基于这些基础特征之上一个具象化的产品体现而已。我相信,现在的你再去研究其他具体的Serverless的产品,也已经能够驾轻就熟了。
|
||||
|
||||
接下来,我们先不要急着深入学习它的拓展能力,先通过两节贴近一线实战的课程,分别从函数的调用和跨VPC的实现让你能一边用起来一边继续深入学习。函数的调用只要用到云上的资源,大部分都会涉及到VPC的调用,这两者放在一起来讲,一定能够加深你对这两者紧密关系的理解。
|
||||
|
||||
今天,我就和你先聊聊函数的调用。希望你能通过这节课,了解函数与函数之间是如何组装,函数与其他云资源是如何协作的,更能够判断在哪些场景适合用什么样的调用方式,以及这些调用需要注意些什么。
|
||||
|
||||
进行函数拆分调用的必要性
|
||||
|
||||
首先,我们来了解一下函数拆分调用的必要性。可以说,只要你有将函数用于实际业务的需求,单个函数就很难满足你的诉求。比如业务函数对公共函数的调用,函数对云资源的调用等。
|
||||
|
||||
另外,函数拆分管理也是有好处的,我们一个一个来说。
|
||||
|
||||
|
||||
成本:云函数的收费由调用次数、公网流量、占用资源时间三部分组成,其中,最贵的就是占用资源时间。比如有的客户调用次数比较少,但费用也不低的原因,就在于他们的函数每次运行的耗时比较长。那么,你可以通过缩短超时时间、优化第三方的服务调用时效(如查询语句、对象池)等方式来降低一定的成本。
|
||||
|
||||
复用性:如果将所有功能写在一个函数体或者函数文件中,它沉淀为模板或者被再次复用的可能性就比较小,这也不符合我们组件化的思想。
|
||||
|
||||
性能:对于非串行的功能,拆分成多个函数可以提供并发性。因为每个函数都有自己的并发限制,另外,一旦某个并发的函数出现错误,拆分调用也不会像串行调用一样受到超时影响。
|
||||
|
||||
|
||||
这三个好处,其实也是我们函数拆分调用的目的和方向,有了这个意识后,我们再来看在什么样的场景下,应该使用什么样的方式来实现。
|
||||
|
||||
实现方式
|
||||
|
||||
函数的调用从直观的实现上来看,分为同步、异步、编排。编排其实可以划到异步的大范围内,我这里把它单独拎出来,主要是因为它属于一个产品级的术语,具有调度和管理的意义。
|
||||
|
||||
|
||||
|
||||
这个思维导图,其实就是函数调用的思路,接下来,我们分别看下它们的具体实现。
|
||||
|
||||
同步
|
||||
|
||||
在同步模式下,主要使用的有直接调用、网关调用、借助触发器3种实现方法。这也是我们在轻量级应用场景下通常会使用的方式,比如BFF层的开发、对话式交互、活动运营页等。我们分别看下它们的特性。
|
||||
|
||||
|
||||
直接调用
|
||||
|
||||
|
||||
通常我们使用云厂商提供的SDK,通过调用指定的函数,就可以实现直接调用。我们以阿里云的fc-python-sdk为例,通过invoke_function调用,主要的代码如下所示:
|
||||
|
||||
import fc2
|
||||
client = fc2.Client(
|
||||
endpoint='<Your Endpoint>',
|
||||
accessKeyID='<Your AccessKeyID>',
|
||||
accessKeySecret='<Your AccessKeySecret>')
|
||||
|
||||
// 同步调用
|
||||
client.invoke_function('service_name', 'function_name')
|
||||
|
||||
|
||||
这种方法通常用于调用时间较少、需要及时返回处理结果、业务处理较为简单的场景。它和我们平时开发的微服务有几分相似,比如对调用结果的缓存、超时重试等,我们都可以将微服务上的经验迁移到函数计算中来使用。
|
||||
|
||||
但要注意的是,函数计算本身是有超时时间的,你还需要考虑超时带来的费用成本,按次付费是函数区别于微服务的一个重要特性。
|
||||
|
||||
|
||||
网关调用
|
||||
|
||||
|
||||
这个方法,就是通过网关的方式调用函数了。它同样与微服务类似,主要是通过API网关的方式来达到限流、验证、参数映射、域名映射等作用。如果是两个功能模块之间的调用,就可以选择网关调用了。不过,如果是自己业务内部的函数互调,那么采用上面所说的直接调用的方法即可。
|
||||
|
||||
|
||||
借助触发器
|
||||
|
||||
|
||||
我在做业务的时候,经常看到有的客户会在函数计算的平台上构建好触发器和函数之后,在自己的平台上写一个服务来通过HTTP触发器的方式使用函数计算。这个方法,一般用来做服务部分的Serverless化、函数检测等场景。
|
||||
|
||||
最后,在同步调用模式下,你还要注意调用延时和超时带来的级联反应,这会加重你额外的费用成本。
|
||||
|
||||
异步
|
||||
|
||||
在异步模式下,我们通常有直接的异步调用、借助平台异步策略配置、借助介质触发的方式。具体怎么理解呢?我来跟你一一梳理下。
|
||||
|
||||
|
||||
直接异步调用
|
||||
|
||||
|
||||
这种方式是比较好理解的,我们还是拿阿里云的函数计算调用方式来说明。它跟直接同步调用的区别,在于在多传一个参数值“x-fc-invocation-type”为“Async”:
|
||||
|
||||
// 异步调用函数。
|
||||
client.invoke_function('service_name', 'function_name', headers = {'x-fc-invocation-type': 'Async'})
|
||||
|
||||
|
||||
这种方式下,调用invoke_function之后,函数会立马返回,并不会关心被调函数的执行情况,而是由平台保证可靠地执行。
|
||||
|
||||
它同样和微服务的异步调用有些类似,除了可以增大并发之外,还可以尽可能地节省函数调用的成本。不过,它的局限性在于我们无法第一时间知道执行的结果,因此,我们最好在一些延时不敏感、无需等待返回给客户端结果的场景下使用。
|
||||
|
||||
|
||||
异步策略配置
|
||||
|
||||
|
||||
这种方式下,你无需进行额外的代码开发,这是平台厂商通常为了使得产品更具竞争力,让你快速实现对函数的处理结果、异常等方面的再次处理而提供的能力。所以我在这里把它称为“策略配置”。
|
||||
|
||||
你可以通过设置诸如“最大重试次数”、“消息存活时间”这些条件来保证请求的可靠处理。同时,也可以通过“成功”或“失败”调用其他云函数或者已经集成的云函数、消息队列等,进一步丰富处理能力。通过云厂商的这种配置,我们也可以快速地搭建一个类似微服务异步处理架构的能力。
|
||||
|
||||
|
||||
借助介质触发
|
||||
|
||||
|
||||
这种方式其实就是巧妙地运用函数与云服务的集成来处理,同时,也将函数计算由原来的被动触发模式,转变成了“生产者模式”,可以说真正体现出了函数计算胶水语言的特性。
|
||||
|
||||
如下面的示意图所示。我们通常可以看云厂商集成了多少的云服务,也就是说它集成了多少触发器。之后,你就可以通过函数的方式来调用它(如HTTP触发器的异步触发方式),也可以通过云服务开放的API接口存储下来,再由其配套的事件触发方式触发目标函数就行。我们以对象存储举例,你可以将文件存储到一个bucket下,然后进行触发操作。
|
||||
|
||||
|
||||
|
||||
这样的方式,可以极大地降低你的运维成本,让你更专注于业务逻辑的处理,这一点也正是Serverless的精髓所在,也是不同于微服务的一大特点。
|
||||
|
||||
我们都知道,微服务的治理其实已经很不简单了。随着函数粒度越来越低,虽然灵活性提升了,但如果是复杂的场景,即使有这些同步和异步的方式,一旦处理逻辑比较多,链路比较长,等待时间不确定,亦或者需要回滚操作等,那么上述这两大操作模式(同步、异步)也就显得有点力不从心了。
|
||||
|
||||
下面,我就给你在介绍一个Serverless的杀手锏工具——工作流。我们可以把上面的方式看成是“函数形态下的P2P操作”,而工作流,就可以看作是“函数下的Kubernetes”。
|
||||
|
||||
编排
|
||||
|
||||
如下图所示,我们可以发现,函数(或者服务)之间,不再以主动或者被动的触发方式来交互,而是通过预制好的步骤来进行,这些步骤通常通过JSON或者YAML的格式定义好函数之间的协作关系存在。
|
||||
|
||||
|
||||
|
||||
我们通常把它称为Serverless工作流,它可以让你通过顺序、分支、并行的方式来协调一个或多个分布式任务,这些任务不仅包括函数,还可以是服务和应用的形式,并且通过平台提供的状态跟踪、日志记录和异常重试逻辑,将你从繁琐的工作中解脱出来,享受全托管的服务能力。
|
||||
|
||||
那么它能具体适用于什么样的场景呢?我来跟你分别来介绍一下。
|
||||
|
||||
|
||||
长流程的场景:假如你的业务流程耗时很长,为了确保流程的执行结束和状态追踪,就可以使用工作流编排的方式;
|
||||
事务型业务流程:比如我们常见的电商订单流程,涉及到预留库存、下单结算、配送、退款等有状态的流程,通过Serverless工作流,就可以提供这种长流程分布式的事务保证;
|
||||
并发型业务流程:这里一般指的是执行时间长、并发量高的大规模计算场景,比如机器训练,需要先拆解小文件计算,再聚合处理;
|
||||
需要状态全链路监测的场景:由于云厂商开发的工作流配备了可观测、执行记录等可视化功能,那么对链路长,需要监测的业务,通过Serverless工作流就可以便捷地查看状态、执行记录、定位故障。
|
||||
|
||||
|
||||
总的来说,Serverless工作流适合用于解决复杂的、执行流程长、有状态的、多步骤的、并发聚合的业务流程。其实,当你用同步、异步的常规方法不太好处理的时候,就可以想想,你的业务是否可以通过编排的方式来解决。关于编排的核心技术实现,我也会在第二模块中跟你细聊。
|
||||
|
||||
注意要点
|
||||
|
||||
了解完常用的函数之间的调用方法和使用场景之后,相信你已经可以通过这些思路,驾轻就熟地去云厂商的平台上体验一番了。如果你是一个Serverless平台的新人,那么相信你对你的平台已经具备或者应该具备的能力也心中有谱了。下面,我再提供给你几点使用过程中的注意事项,避免踩坑。
|
||||
|
||||
首先,安全性问题。云厂商通过在代码级别加密的方式确保代码安全,运行时的隔离机制确保执行安全,访问时的鉴权验证确保资源请求的安全。那么,你需要做的,就是在使用的时候确保你的访问密钥是安全的。除此之外,在设置诸如HTTP触发器之类的服务可选项时,一定记得在生产环境使用身份鉴权验证机制。
|
||||
|
||||
其次,监控报警问题。你可以通过我在热启动部分中讲到的预热方法来访问你的重要函数,确保函数能被正常访问,如果不能正常访问,你可以沿用你原来微服务的治理机制方法来报警,或者可以直接使用云厂商的报警策略机制,此时你可能需要支付一定的额外费用,这个依据不同的平台而定。
|
||||
|
||||
最后,容错问题。如果你的下游服务出现问题(如超时、访问不通、超限等),不仅会影响你本身的函数使用,还会增加额外的耗时费用。所以,你可以针对性地增加一些缓存方式来避免无状态请求的调用。其余的方法,其实跟微服务就差不多了,比如熔断机制、降级机制等,并结合上面的报警来处理。其实,在合适的场景选择异步的方式来处理,本身就是一种高容错、低耦合的方式。
|
||||
|
||||
小结
|
||||
|
||||
最后,我来小结一下今天的内容。这节课,我分别从函数拆分调用的必要性和实现方式上,跟你介绍了如何利用函数之间的调用来解决日常的业务需要。要注意的是,函数计算的使用也是需要从成本、复用性和性能方面来权衡的,以便于获得最佳的ROI。
|
||||
|
||||
在实现方式上面,我把函数的调用分为同步、异步以及编排的处理方式。其中,同步适合及时响应、执行较短等场景。在大部分时候,我还是推荐你使用异步的处理方式来确保业务的稳定可靠和服务的解耦。
|
||||
|
||||
而针对复杂的业务场景、需要支持长时间、有状态、多步骤的执行流程,这里,我推荐你使用Serverless工作流。
|
||||
|
||||
当然,我更希望你能够灵活地运用今天学习过的调用方法,在前面学习单函数的基础上,能够通过“积木的方式”实现更复杂的业务逻辑。
|
||||
|
||||
课后练习
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个小练习。
|
||||
|
||||
想一想,通过函数调用的方式实现音视频的处理,针对不同的场景,都有哪些方法?你可以选择一家云厂商先练习练习,体验一下函数计算、Serverless的便捷之处。
|
||||
|
||||
我会在第二模块的动手体验中跟你讲解实现它的多重途径和详细方法。感谢你的阅读,也欢迎你把这篇文章分享给更多的朋友一起交流学习。
|
||||
|
||||
|
||||
|
||||
|
200
专栏/Serverless进阶实战课/10小试牛刀(二):如何突破VPC网络的速度限制?.md
Normal file
200
专栏/Serverless进阶实战课/10小试牛刀(二):如何突破VPC网络的速度限制?.md
Normal file
@ -0,0 +1,200 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
10 小试牛刀(二):如何突破VPC网络的速度限制?
|
||||
你好,我是静远。
|
||||
|
||||
在冷启动这节课里,我跟你聊到了影响函数第一次执行速度的因素有很多,其中一个关键因素就是:VPC网络的建联时间。
|
||||
|
||||
我们知道,VPC是云厂商提供给用户的一个安全隔离的网络环境,用户可以在VPC空间中部署自己的服务,如Redis、数据库、MQ等。而我们的函数通常是创建在函数专属的VPC中的。一个生产级别的函数服务应用,少不了需要和其他的资源和中间件打交道。那么,跨VPC之间的通信效率就显得尤为重要了。
|
||||
|
||||
今天这节课,我就针对VPC之间的通信效率,通过案例实操的方式,为你剖析函数在跨VPC上的技术要点。
|
||||
|
||||
希望通过这节课,能够让你得到一些在Serverless的实战和后续优化工作的启发,能举一反三地找到更多优化方法,进一步突破函数访问速度的上限。
|
||||
|
||||
跨VPC问题的演进
|
||||
|
||||
针对云上网络连接的方式,相信你已经了解到不少,如专线(高速通道)、VPN网关、对等连接等方式。这些方式,有的用来解决IDC和VPC的连接,有的用来解决VPC与VPC之间的互通。
|
||||
|
||||
这其中,对等连接的确可以解决函数跨VPC的问题,但我们试想一下,如果每个用户都通过这种方式来解决这种问题,那么平台每次都需要搭建两端的连接,并且还可能会遇到IP网段冲突的问题。这样的做法显然不具备普适性,成本也过高。
|
||||
|
||||
为此,针对Serverless的函数计算场景,我们通常会通过弹性网卡(ENI)的方式来打通。弹性网卡是绑定在云主机或者容器实例上的,也就是说,我们是通过弹性网卡ENI来关联用户的VPC和用户的设备实例,使得设备和VPC互通的。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
但实际上,我们暂且抛开这两者在网络速率上的差别,不论是在容器实例还是在主机上,他们都会面临同样的问题。
|
||||
|
||||
|
||||
冷启动耗时较长:弹性网卡这种虚拟化的资源,它的创建时间是避免不了的;
|
||||
资源受限:正因为资源虚拟化,也就意味着要依赖于硬件的基本参数,受限于内核、CPU等因素,为此,几乎所有的云厂商都会限制VPC内弹性网卡的数量,也就从一定程度上导致了此方法的局限性;
|
||||
资源浪费:由于容器或者主机上要绑定弹性网卡,也就意味着多个实例就得占用多个网卡资源,造成资源浪费。
|
||||
|
||||
|
||||
我们可以看出,它们虽然解决了跨VPC的问题,但还是会带来性能和资源成本上的压力。那么,我们是否可以不创建这么多弹性网卡,探索一种共用的方式呢?通常我们立马能想到的方式,就是出口代理了。
|
||||
|
||||
什么是代理?
|
||||
|
||||
如何代理呢?我们可以了解一下它的整体流程。
|
||||
|
||||
简单来说,就是我们在函数计算的集群与用户的VPC之间添加Proxy Node,在Proxy Node上构建网卡(如eth1),网卡会提前在用户VPC所在的子网Subnet中创建,但租户的权限属于函数计算VPC。
|
||||
|
||||
有了弹性网卡,接下来就需要解决函数到代理节点的打通问题了,也就是在函数的主机和代理主机之间构建一条链路,将数据包传递过去,且保证源IP和目的IP保持不变。
|
||||
|
||||
我们通常采用的是隧道技术。隧道打通后,我们还得注意两件事情。其一就是在Proxy Node中多张网卡的转发处理;其二,由于函数通常以容器的方式运行,所以需要有一个容器网络转发的过程。
|
||||
|
||||
接下来,我将一步一步介绍它们是如何起作用的。
|
||||
|
||||
动手实操
|
||||
|
||||
这节课,我会通过一个打通跨VPC网络的实验案例,带你实现函数通过Proxy机器代理的方式访问用户VPC的资源的全过程。在这个案例中,我选取了百度智能云服务器BCC来进行实验。
|
||||
|
||||
在实验之前,我们需要准备两个VPC网段,模拟创建函数集群和用户集群,并在云上购买三台云服务器BCC,其中2台BCC在VPC1,另外1台在VPC2。下面这个图可以清晰的描述部署的逻辑:
|
||||
|
||||
|
||||
|
||||
隧道技术
|
||||
|
||||
准备工作完成之后,我们来看隧道技术。
|
||||
|
||||
什么是隧道?在网络中,隧道是一种使用网络不支持的协议,构建网络之间高效和安全的链接,实现在网络中传输数据的方法。
|
||||
|
||||
内核中的tunnel可以通过ip tunnel help看到,当前有五个:IPIP、GRE、SIT、ISATAP和VTI。
|
||||
|
||||
|
||||
|
||||
本节课,我们选择使用GRE(Generic Routing Encapsulation),它是IPIP(IP in IP)协议的更进一步版本,从官方的描述上来看,可以取代IPIP。隧道技术本身的概念和区别不是本节的重点。不过,如果感兴趣的话,你也可以通过我文末给的延伸阅读深入了解。
|
||||
|
||||
接下来我们来创建隧道,使得func bcc1和proxy bcc能通过隧道打通连接。我们先在func bcc1中执行如下命令来创建GRE隧道:
|
||||
|
||||
# 加载GRE模块
|
||||
modprobe ip_gre
|
||||
lsmod | grep gre
|
||||
# 创建gre模式的通道tunnel,关联上IP并赋予虚拟本端IP为10.0.0.3
|
||||
ip tunnel add gre0 mode gre local 192.168.80.8 remote 192.168.4.4 dev eth0
|
||||
ip addr add 10.0.0.3 peer 10.0.0.4 dev mgre0
|
||||
ip link set dev mgre0 up
|
||||
|
||||
|
||||
在proxy bcc上执行如下命令来打通GRE隧道:
|
||||
|
||||
modprobe ip_gre
|
||||
lsmod | grep gre
|
||||
ip tunnel add mgre0 mode gre local 192.168.4.4 dev eth0
|
||||
ip link set dev mgre0 up
|
||||
ip address add 10.0.0.4/24 dev mgre0
|
||||
ip neigh replace 10.0.0.3 lladdr 192.168.80.8 dev mgre0
|
||||
|
||||
|
||||
最后,我们还可以通过ping命令来验证proxy bcc与func bcc1的GRE隧道是否打通。
|
||||
|
||||
|
||||
|
||||
到这里,我们隧道的打通任务已经完成一半了,下面,我们要看proxy bcc如何与user bcc打通。
|
||||
|
||||
转发技术
|
||||
|
||||
我们注意到,proxy bcc和user bcc是不在一个VPC集群内的。那我们要怎么解决这个问题呢?
|
||||
|
||||
构建的逻辑就是在user vpc下构建一个虚拟网卡eth1,而eth1,又隶属于proxy bcc下。这个方法,和我们在上面“跨VPC的演进”中讲到的“动态创建网卡”的方法是一致的。
|
||||
|
||||
这里需要说明一点,如果你是一个业务函数的开发者,可能云厂商没有开放相应的特权账号,没有创建弹性网卡的能力,实操不了,那么,这里你只要了解过程即可。如果你是云平台维护工作者,可以体验一下这其中的操作方式。
|
||||
|
||||
弹性网卡创建并绑定好后,我们会发现,有2个网卡存在于proxy bcc中。
|
||||
|
||||
|
||||
|
||||
我们通过ping命令,可以验证是否能够和user bcc打通。这里要注意,proxy bcc与user bcc可以互通是因为弹性网卡是基于user bcc所在的VPC的子网Subnet创建的,但租户还是属于proxy bcc。
|
||||
|
||||
|
||||
|
||||
打通之后,我们就需要用到转发技术了,也就是将func bcc1的数据包通过proxy bcc的请求到达user bcc。
|
||||
|
||||
首先,我们需要通过将ip_forward设置为1,开启proxy bcc的转发能力。
|
||||
|
||||
echo 1 > /proc/sys/net/ipv4/ip_forward
|
||||
|
||||
|
||||
这里需要说明一下,出于安全考虑,Linux系统默认的是禁止数据包转发的。所谓转发,就是当主机拥有多于一块网卡时,其中一块收到数据包,根据数据包的目的IP地址将包发往本机的另一网卡,该网卡即会根据路由表继续发送数据包。
|
||||
|
||||
我们这里因为是eth0接收云主机的数据,通过eth1来发送出去,所以需要开启转发能力。当然,为了能让func bcc1的路由触达user bcc,我们还需要继续完善路由规则。
|
||||
|
||||
我们在func bcc1中通过ip route add命令添加一条到user bcc子网172.16.101.0/24的路由:
|
||||
|
||||
ip route add 172.16.101.0/24 dev mgre0
|
||||
|
||||
|
||||
此时 func bcc1 访问 user bcc 172.16.101.4 仍然访问不通,在 proxy bcc 侧监听 eth1 流量可以发现,eth1 发往用户 VPC 的包的源 IP 仍是 func1 bcc gre0 设备的 IP 10.0.0.3,这个 IP实际只存在于我们 bcc 内部,因此用户 bcc 收到请求后,它的返回流量无法路由到这个 IP。
|
||||
|
||||
因此,我们这里需要在proxy bcc添加一条 iptables 规则,在 POSTROUTING 链里把从 eth1 出口的流量的源 IP SNAT 设置为 eth1 的IP 地址,即 172.16.101.4。
|
||||
|
||||
iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
|
||||
|
||||
|
||||
到这里,我们就可以在func bcc1中ping通user bcc了。
|
||||
|
||||
|
||||
|
||||
容器网络
|
||||
|
||||
在虚机上,我们已经操作体验一遍了,但通常我们的函数是在容器实例里面运行的 的,容器在访问用户vpc服务的时候,是存在一点差别的。我们也可以验证一下,会发现,pod 发起的请求正常路由到了 proxy bcc 下的 tunnel 设备,但是没有继续转发到 eth1 中。
|
||||
|
||||
为什么会出现这样的情况呢?通常是因为Pod的IP和我们在proxy bcc上构建的路由规则是不一致的。至于怎么解答,我先不做回答,你可以先思考一下。我们先来看一个概念:ip-masq-agent。
|
||||
|
||||
ip-masq-agent 是一个用来管理 IP 伪装的扩展,即管理Kubernetes集群节点中 IP 网段的 SNAT 规则。它以 DaemonSet 形式部署在Kubernetes的集群中,在每个节点上启动一个代理程序。代理程序通过配置 iptables 规则,再将容器流量发送到集群节点的 IP 和集群 IP 范围之外的目标时,就可以选择以容器自己的 IP 地址或伪装成节点的 IP 地址作为出向流量的源地址。
|
||||
|
||||
看完ip-masq-agent的解说,相信你已经有答案了。解决办法,就是把 Pod IP SNAT 设置成 gre0 设备的 IP,再转给 proxy bcc,再对 proxy bcc 隐藏 Pod的IP即可。
|
||||
|
||||
ip-masq-agent 的配置来自于 configMap,可以通过 kubectl edit cm 命令来查看或修改配置。
|
||||
|
||||
到这里,我们在Pod中也能访问user bcc的服务了。
|
||||
|
||||
高可用
|
||||
|
||||
如果我们要上生产环境,你还需要考虑服务的高可用。在我们这节课反复提到的案例图中,我只画了一个Proxy Node,用来实验还可以,但生产环境肯定是不够的。为此,我们需要为每个VPC/Subnet建立一对主备Proxy。主备Proxy要使用HaVip进行容灾切换。
|
||||
|
||||
HaVip是一种可以独立创建和释放的内网IP,可以与高可用软件(例如,KeepAlived)配合使用,搭建高可用主备服务,提高业务的可用性。
|
||||
|
||||
另外,从Serverless的角度看,你还需要依据函数访问的流量对Proxy Node进行自动扩缩容。当流量增大时,需要自动对Proxy Node进行扩容;当流量降低时,需要自动对Proxy Node进行缩容。
|
||||
|
||||
我们继续在上面的架构图上进一步完善一下,增加主备Proxy Node和容器Pod之后的架构示意图如下所示。
|
||||
|
||||
|
||||
|
||||
小结
|
||||
|
||||
最后,我来小结一下我们今天的内容。
|
||||
|
||||
这节课主要围绕着优化函数在跨VPC场景的访问速度来做讲解。我们学习了跨VPC打通的演进过程以及代理的四大要素,最后,也结合一个案例的实操过程,了解了函数代理技术的实现过程。
|
||||
|
||||
在演进过程的回顾里,我们会发现,对等连接由于其在函数计算上的构建不具备普适性,只有在极少数大客户的场景下会使用。而弹性的网卡实时创建,虽然具备一定的通用能力,但由于其高延迟和对资源的浪费,我们也不太推荐使用。
|
||||
|
||||
这两个技术,给我们来带来的思考是,如果我们能提前准备好访问的通路且可复用,是不是就可以解决这样的问题了?是的。为此,我们提出了函数代理技术。
|
||||
|
||||
其实,代理技术需要解决的技术卡点还不少,主要包括我们提到的隧道通路、网卡转发、容器网络转发技术,以及最后在生产环境下,还需要考虑到的高可用的问题。我们可以通过高可用的虚拟IP和主备Proxy集群解决。同时,从弹性的角度,我也建议你引入扩缩容的能力,这个能力,你也可以贯穿在整个Serverless的学习生涯中,可以说,它就是Serverless的精髓所在。
|
||||
|
||||
最后,函数计算由于其胶水语言的特性,跨介质跨网络的访问是不可忽视的一件事情。虽然我们已经在不断的演进解决,但函数代理技术也有其缺点的,比如资源一定程度上的增加。因此,希望通过这节课的内容和思考的过程,能让你打开眼界,提出更多的优化手段,让Serverless的技术积淀越来越深厚。
|
||||
|
||||
思考题
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个思考题。
|
||||
|
||||
如果我们在隧道打通那里,采用IPIP隧道协议,会有什么问题?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起交流学习。
|
||||
|
||||
延伸阅读
|
||||
|
||||
|
||||
你可以通过VPC的对等链接,继续深入了解一下对等链接的概念。
|
||||
这一篇隧道技术,可以帮助你进一步了解隧道的知识要点。
|
||||
腾讯云的这一篇VPC网络优化的文章,也是值得我们一看的。
|
||||
文中提到的ip-masq-agent,感兴趣的话,你可以继续看这一篇ip-mast-agent指南,写得也是非常详细的。
|
||||
|
||||
|
||||
|
||||
|
||||
|
193
专栏/Serverless进阶实战课/11WebIDE:如何让函数远离繁琐的本地开发模式?.md
Normal file
193
专栏/Serverless进阶实战课/11WebIDE:如何让函数远离繁琐的本地开发模式?.md
Normal file
@ -0,0 +1,193 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
11 WebIDE:如何让函数远离繁琐的本地开发模式?
|
||||
你好,我是静远。
|
||||
|
||||
在开始今天的主题之前,我想跟你先聊一下我的客户在WebIDE这块的诉求。我在金融领域客户的提问中挑选了两个比较典型的问题:
|
||||
|
||||
|
||||
银行要求生产环境不带自己的电脑,请问我如何操作我们的代码?
|
||||
编辑框有点小,我用的是Python,是否能提供在线调试的能力?
|
||||
|
||||
|
||||
你会发现,不管是工作硬性的要求还是自身对开发效率的追求,他们最终的目的,都是希望有一个能够随时随地、打开即用的代码编辑器——WebIDE。
|
||||
|
||||
我想,WebIDE这个概念你一定不陌生,但如何让WebIDE更好地用于函数计算的开发呢?我们又需要注意哪些细节呢?这里面有哪些不一样的技术点呢?
|
||||
|
||||
今天,我就来跟你聊一聊函数计算中WebIDE是如何工作的。希望通过这节课程,让你在了解VS Code Server原理的基础上,在插件机制、环境的依赖、弹性伸缩等几个方面都对函数计算形态的WebIDE有一个比较清晰的认识。
|
||||
|
||||
WebIDE的优势是什么?
|
||||
|
||||
首先,我们来体验一下云厂商的WebIDE,直观感受一下它的优势。
|
||||
|
||||
我们可以以阿里云函数计算FC为例,创建一个Python3语言运行时的函数。点击控制台页面的“函数代码”,就可以加载出来在线编辑的工作空间。
|
||||
|
||||
|
||||
|
||||
我们可以看到,这和我们本地的VS Code IDE几乎没有太大差别,左上角是我们熟悉的“文件”、“资源管理器”、“查找”等菜单按钮。而且,在线编写代码的体验和本地IDE也几乎是一样的,包括代码提示功能等都有。当你完成代码编写之后,还可以点击测试按钮,直接测试编写的函数。确认无误后,点击部署函数,测试好的代码也可以直接部署在云端。
|
||||
|
||||
这种方式的好处是,当我们想要细微调整函数的时候,无需下载代码到本地,可以直接在线完成编辑、测试、部署这一系列的动作。
|
||||
|
||||
现在,我想你应该对WebIDE的优势有一个直观的感受了:可快速地编辑、调试、运行、部署云端代码。它的另一个优势就是在金融银行领域,出于安全的考虑,有的环境往往是不能带自己的电脑和本地IDE的,只能通过WebIDE的方式来操作。
|
||||
|
||||
针对函数计算的WebIDE,各大云厂商都有不同程度的实现,虽然不管是在公有云还是在私有云上,实现方面可能有一定的差异,但背后的实现思想和机制是通用的。接下来,就让我们深入了解它的整体架构。
|
||||
|
||||
整体架构
|
||||
|
||||
为了让你更直观地理解在线的函数计算IDE,我在架构图中用不同的颜色做了标识。我分别来做一下说明。
|
||||
|
||||
|
||||
|
||||
|
||||
蓝色部分:是WebIDE客户端的核心,我们选取了知名的VS Code Server来作为核心底座,它在很大程度上解决了 Client 端的问题,其自身基于 VS Code 二次开发,让VS Code在任何地方都可以通过浏览器访问和操作;
|
||||
绿色部分:是将WebIDE与函数计算结合起来的核心,主要包括函数计算业务形态下的功能插件、工具、SDK以及运行时环境能力;
|
||||
橘色部分:是Serverless形态下的必备支撑服务,承担代码的存储、安全管控和针对用户操作请求时的资源调度和伸缩事项,主要包括对象存储、权限认证、资源伸缩、健康监测能力。
|
||||
|
||||
|
||||
在图中,我也用序号标注了WebIDE在函数计算平台的简要执行过程,接下来,我以一个函数的Deploy请求为例,帮助你从整体上快速了解每次请求涉及的全貌。
|
||||
|
||||
第1-2步,用户会在VS Code的前端页面向后端发出函数在线编辑的请求,假设为“XXX/ deployFunction”,那么服务端,也就是函数计算的控制层Controller在接收到请求后,就会验证权限,再转给VS Code Server容器实例。
|
||||
|
||||
第3-4步,VS Code Server容器就会获取用户代码,再加载函数计算的资源调度系统,也会根据目前容器池Container Pool的资源现状,动态扩缩容WebIDE Pod资源。
|
||||
|
||||
第5-7步,Server端根据用户的请求,会调用插件后端功能Serverless Extension BE,基于此时语言的环境,执行Deploy操作,并将执行结果返回给Client端。
|
||||
|
||||
这里需要额外说明三点:
|
||||
|
||||
第一,我们可以将Serverless Extension插件提前集成在VS Code Server的镜像里面;
|
||||
|
||||
第二,Runtime依据原来函数计算执行的架构来部署即可,你可以将各语言运行时打包成一个镜像,然后以动态挂载的方式加载每个语言,也可以集成在WebIDE的大镜像里面,不过这样会比较笨重;
|
||||
|
||||
第三,健康监测主要用于监听VS Code Server的状态。虽然网页版的服务都在服务端运行,但界面Client和后台Server是有心跳关联的,这个健康监测的作用,就是心跳监测。它会按照一定的逻辑来判断页面是否不再使用,以便通知资源调度服务,释放后端的容器实例。
|
||||
|
||||
基于基础架构和流程的展示,我们应该已经能够理解,函数计算的WebIDE的主要功能就在于,基于一个可视化的在线编辑软件的基础,增加了Serverless函数计算的插件功能,通过集成运行时环境依赖和弹性伸缩的能力,提供用户在线开发、调试、部署和运行函数的能力。
|
||||
|
||||
接下来,我们就沿着这个思路,对可视化的在线编辑软件VS Code Server、Serverless Extension插件功能、环境依赖和弹性伸缩的能力,依次展开说明,看看它们有什么需要注意的技术要点。
|
||||
|
||||
VS Code Server
|
||||
|
||||
我在开头提到过,本次讨论基于VS Code Server来展开,主要是因为VS Code的背景公司是微软,使用人群基数庞大。微软将VS Code进行了开源,而Code Server又对VS Code进行了二次封装,使其可以在任何机器上部署并且通过浏览器来访问。
|
||||
|
||||
我们还是来体验一下,这样更直观一点。
|
||||
|
||||
首先,你需要提前准备一个2核4G的机器(区别于code-server在github上的最低要求2核1G),确保执行所需的空间资源,并且确保有docker环境。
|
||||
|
||||
接下来,你可以下载code-server镜像到本地,并启动为容器。
|
||||
|
||||
# 拉取vscode server镜像到本地
|
||||
$ docker pull jmcdice/vscode-server
|
||||
# 启动一个docker
|
||||
# -d是在后台运行容器
|
||||
# -p将本地的端口(9000)映射到容器的端口(8080)
|
||||
# --restart=always是在重启容器时,能重新启动参数
|
||||
# --name指定启动容器的名字
|
||||
$ docker run -d -p 9000:8080 --restart=always --name=myVscodeServer jmcdice/vscode-server
|
||||
|
||||
|
||||
启动动完成之后,我们要使用docker ps命令,查看容器是否已经启动。最后,我们在浏览器中输入127.0.0.1:9000,就可以使用在线VS Code了,这个流程操作下来是非常方便、快捷的。
|
||||
|
||||
需要注意的是,我们这里只是以本地docker的方式启动了VS Code,如果我们想对外使用,只需要申请部署云服务器,并对外暴露URL就可以了。
|
||||
|
||||
|
||||
|
||||
有了本地的体验,我们放到函数计算平台,无非就是新增一个镜像并启动为容器的过程。我们的重点,是集成函数服务的能力,这就是我们下面要讨论的Serverless Extension插件功能了。
|
||||
|
||||
Serverless Extension
|
||||
|
||||
各大云厂商基于WebIDE,通过开发插件Extension的方式集成了函数平台的常用功能。
|
||||
|
||||
如百度智能云函数计算的Baidu Serverless VS Code 插件、腾讯云函数的Tencent Serverless Toolkit for VS Code插件、阿里云函数计算的Aliyun Serverless VSCode Extension插件等等。从产品形态上来看,大体比较类似,你选择其中一个来体验就行。
|
||||
|
||||
但要注意的是,各云厂商的VS Code插件底层实现都是集成了自家的命令行工具和SDK的,如百度智能云集成了BSAM CLI、腾讯云集成了Serverless Framework 语法、阿里云的插件结合了函数计算命令行工具Funcraft和函数计算SDK的功能。
|
||||
|
||||
那么,他们都是怎么实现的呢?虽然各家集成的工具和SDK多多少少带有自家的特性,但设计的通用原则是不变的。接下来,我就以这种通用的设计法则和思路,来阐述如何构建一个Serverless VS Code Extension。
|
||||
|
||||
首先,我们可以发现,各家的插件都是放在了微软Visual Studio Marketplace之上,你可以从这里任意点击一个插件进去,可以看到相应的源代码、手册等维护信息。
|
||||
|
||||
|
||||
|
||||
其次,该插件只是一个端的体现,还是得由函数计算平台的SDK和CLI命令工具来实现。例如函数的上传、下载功能底层,都是调用SDK中函数的接口实现,而函数的部署、依赖安装、调试、执行,则一般是在CLI中实现的。
|
||||
|
||||
总结起来就是两部分,一部分是端的UI层逻辑的体现,集成在VS Code里面,一部分是工具CLI和SDK的集成,用于真正执行相关的命令,在服务端容器中运行。如下图所示:
|
||||
|
||||
|
||||
|
||||
我们可以将CLI、SDK、Serverless Extension与VS Code Server打包在一个大镜像里面,在请求的时候,通过下载镜像,在启动后为容器提供服务。
|
||||
|
||||
环境依赖
|
||||
|
||||
接下来,我们需要了解比较关键的一步——如何让函数“跑”起来。这就涉及到环境依赖了。
|
||||
|
||||
根据编程语言的不同特性,通常函数计算平台需要针对各个语言做不同的Runtime,为了区别于正式生产环境下的Runtime,我这边采用dev-runtime命名。
|
||||
|
||||
|
||||
|
||||
它们主要的不同就在于,dev-runtime需要根据不同的指令,来进行不同的操作,比如根据UI层发送的部署指令,进行deploy操作。而我们之前第一模块中提到的Runtime,是运行态直接挂载的。通常,函数计算平台也是会对Runtime和dev-runtime进行区分,分开管理的。
|
||||
|
||||
这里,我们具体看下dev-runtime的执行流程。我以Python3的调试为例,简单跟你聊一下这个过程。
|
||||
|
||||
就Python3而言,如果需要支持调试,我们需要提前在Docker镜像中指定安装debugpy。有的操作系统可能还需要安装GCC。
|
||||
|
||||
我们通常可以通过一个脚本来接收各种指令,并调起各个依赖工具。那么,针对Python3语言,如果接收到WebIDE安装扩展依赖的指令,就可以通过pip3来install了。
|
||||
|
||||
# -r 安装需求文件里的依赖包
|
||||
# -t 安装目标位置
|
||||
# -i 指定镜像源,本案例指定清华源,具体你可以自定义
|
||||
# -I 如果已经安装依赖,忽略
|
||||
/XXX/bin/pip3 install -I -r /XXX/requirements.txt -t /XXX/site-packages -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
|
||||
类似的,如果需要支持调试和执行,通过Python3和debugpy相应的命令来执行即可。而其他语言的调试实现,依赖于各语言的调试方式,比如Node.js会使用node-inspect调试工具。
|
||||
|
||||
需要补充说明的是,虽然我在文章开头指出大部分云厂商在其公有云上不支持Java的在线WebIDE的能力。但其实,Java也是可以支持网页版能力的,我们只需要在Java调试的启动脚本中,添加相应的命令启动调试即可。
|
||||
|
||||
弹性伸缩
|
||||
|
||||
到这里,针对函数计算的WebIDE已经讲解得差不多了。但如果只是这样,它还不能被称为“Serverless”的,我们需要再给它加上一层“可弹”的特性。
|
||||
|
||||
这其实和我们前面涉及到的扩缩容有些类似,我先给你一个思路,之后你也可以自己完整走一遍Serverless下的WebIDE流程。
|
||||
|
||||
关于这一层“可弹”的特性,你可以通过KEDA、Prometheus、HPA、metrics来支撑WebIDE在Kubernetes环境下的动态扩缩容。为什么需要引入KEDA?如果结合扩缩容 那一节课,你应该可以很快回答出来,因为KEDA可以支持0-1之间的切换。
|
||||
|
||||
最后,还剩一个比较重要的问题,就是我在基础架构图中画的“健康监测”。为什么要有这个功能呢?
|
||||
|
||||
我们可以这么想,前端页面和提供服务的VS Code Server在第一次建立链接后,是需要有页面的触发,才会有下一步的执行动作的。那么,如果用户没有触发,该怎么办?是释放掉容器实例还是持续存在?存在多久合适呢?
|
||||
|
||||
比较合理的设计是在VS Code Server中存在两个进程,Server进程负责和前端页面的Edit请求交互,而Status管理进程负责状态管理上报。其中,VS Code Server和前端通信的协议一般使用WebSocket协议。
|
||||
|
||||
依据这样的设计思路,当WebIDE前端和Server超过一定时间没有响应时,可能就是网络原因了。此时不会立马回收VS Code Server容器实例,会重试连接一定次数。当连接成功时,就会继续使用该容器实例。如果重试仍然连接失败,就说明该容器实例已经不再被使用了,会将该实例标记为不可用状态,定期执行清除任务,将不可用状态的容器实例回收。
|
||||
|
||||
通过这样的状态监测管理,使得我们函数计算平台的WebIDE也是“可弹”的。
|
||||
|
||||
小结
|
||||
|
||||
最后,我来小结一下我们今天的内容。这节课,我从函数计算WebIDE的整体架构出发,分别介绍了它的核心四要点:VS Code Server、Serverless Extension、环境依赖、弹性伸缩。
|
||||
|
||||
在课程的开头,我们通过云厂商已经发布的WebIDE体验了一次,清晰直观地感受到了我们今天内容的价值:使用Web IDE,不仅能对用户的代码进行快速的编辑和修改,还能够高效地加载代码所需的外部依赖,从而大大降低开发人员重新部署的次数以及时间。
|
||||
|
||||
再来,通过整体架构的介绍,相信你已经从概貌上了解了设计一个Serverless形态的WebIDE需要考虑的四大要点。我们一个一个来说。
|
||||
|
||||
首先,VS Code Server的讲解,主要想向你传递两件事情:第一,code-server是对VS Code的二次封装,让我们平时在本机使用的VS Code可以无处不在的访问;第二,函数计算平台也是站在巨人的肩膀上,进一步构建属于Serverless下的WebIDE的。这对于我们平时在设计架构和工作中也有一定的启发:没必要什么事情都自己重复去造轮子。
|
||||
|
||||
其次,Serverless Extension和环境依赖是本节课的“灵魂”,没有了这两个功能,就不能称之为函数计算形态下的WebIDE。它们主要是基于函数计算的工具CLI和SDK,以及调试需要下不同运行时的封装。
|
||||
|
||||
最后,为了符合Serverless的“可弹”特性,我也从扩缩容的角度,介绍了VS Code Server在容器中的两种服务的进程,希望你能清晰地了解网页版“端和服务”的联动关系。
|
||||
|
||||
思考题
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个思考题。
|
||||
|
||||
如果让你来在现有云厂商平台上新增支持Java的在线调试功能,你打算如何实现?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。感谢你阅读,也欢迎你把这节课分享给更多的朋友一起交流学习。
|
||||
|
||||
延伸阅读
|
||||
|
||||
你可以通过VS Code插件官网搜索到对应的插件库,更详细地了解extension是如何实现的。
|
||||
|
||||
|
||||
|
||||
|
283
专栏/Serverless进阶实战课/12编排:如何协调多任务的运行?.md
Normal file
283
专栏/Serverless进阶实战课/12编排:如何协调多任务的运行?.md
Normal file
@ -0,0 +1,283 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
12 编排:如何协调多任务的运行?
|
||||
你好,我是静远。
|
||||
|
||||
今天我要和你分享的主题跟“编排”这个词相关,引入这个技术的关键目的,就是处理较为复杂的场景。
|
||||
|
||||
我们在前面的课程提到了函数之间的同步和异步策略机制、有状态的业务处理,这些场景虽然和单个函数比起来已经复杂了一些,但函数本身的策略依然可以解决这些问题的。
|
||||
|
||||
但是试想一下,如果你遇到每个业务都有几十个函数,需要多个业务配合调用的情况,函数之间的调用是否还可以轻易驾驭呢?
|
||||
|
||||
|
||||
电商中订单的创建和管理;
|
||||
文件、视频分片处理后,再整合形成新的报表或视频媒体;
|
||||
广告物料的审核(包括涉政、涉黄、二跳、作弊等);
|
||||
……
|
||||
|
||||
|
||||
这些是不是想想都复杂?相信你在微服务架构中治理起来都比较困难,更何况现在是更细粒度的函数维度呢?我们知道,服务拆得越细,服务粒度就越小,虽然组装性会更好,但与之相对的,服务治理的难度也就会越大。
|
||||
|
||||
那么,是否有一种能力,可以在Serverless的领域中,通过某种方式来协调各个服务和函数的执行,使得我们在享受高弹性、低成本的同时,也降低业务处理上的复杂度呢?这种能力的确存在,业界普遍称之为“工作流(Serverless WorkFlow)”,它就可以应用在上面提到的场景中。
|
||||
|
||||
工作流是什么?
|
||||
|
||||
什么是工作流呢?CNCF 在它的标准协议说明中给出了一些解释。总的来说,可以这样概括:通过DSL来定义一组结构和模型,它以JSON或YAML的形式存在,服务引擎会根据定义的蓝图来进行调度和执行任务。
|
||||
|
||||
云厂商实现的工作流,可以让你能够通过顺序、分支、并行的方式来协调一个或多个分布式任务,这些任务不仅包括函数、还可以是服务和应用的形式,并且通过平台提供的状态跟踪、日志记录和异常重试逻辑,使得你可以从繁琐的工作中解脱出来,享受全托管的服务能力。
|
||||
|
||||
有了这个抽象的认知,那么它具体长什么样子,它是如何构成的,它的运行机制是怎么样的?下面,我就带你来一层一层揭开它的面纱。
|
||||
|
||||
基本构成
|
||||
|
||||
首先,我们通过一个简单示意图看一下工作流的形态。
|
||||
|
||||
|
||||
|
||||
可以看到,工作流的状态扭转跟我们接触到的状态机的转换是很像的,除状态的转换外,它还有这样几个特性。
|
||||
|
||||
|
||||
每个State状态是一个工作节点
|
||||
|
||||
|
||||
它定义了一种特殊的逻辑,决定了工作流当前流程应该执行什么样的控制逻辑,通常包括:操作、传递、选择、并行、等待和循环等等。这些状态通过不同的组合形式,让业务模型的构建变得像平时编程一样简单,极大地丰富了工作流的实际应用场景。
|
||||
|
||||
|
||||
工作节点可以关联服务
|
||||
|
||||
|
||||
工作节点可以通过API完成对其他服务的调用。比如最常见的就是结合函数计算,每一个原子业务都会采用云函数实现,而业务之间的关联关系会通过工作流实现。一些厂商为了丰富Serverless工作流的应用场景,通常会和同一云生态下的其他云服务关联,比如阿里云的Serverless Workflow、腾讯云的ASW,就支持云服务API的集成。
|
||||
|
||||
|
||||
每个节点有明确的输入和输出
|
||||
|
||||
|
||||
每个节点都可以设定输入和输出来作为数据传递的手段,但在工作流中有两个比较特殊的规则:
|
||||
|
||||
第一,当前节点为初始节点时,该节点的输入为工作流的初始输入;
|
||||
|
||||
第二,当前节点不为初始节点时,该节点的输入为前一父节点的输出。
|
||||
|
||||
|
||||
|
||||
因此,你可以将工作流视为一组状态节点的集合以及这些状态节点的转换,每个状态节点都可以关联事件或功能,并且有明确的输入和输出。从解决方案的完整性来看,我们也需要额外地增加日志记录和审计来监视工作流的执行,也需要有安全的校验机制和错误异常处理能力。
|
||||
|
||||
DSL是什么?
|
||||
|
||||
了解了工作流的基本构成之后,我们先不要着急深入架构设计。还记得我在提到“工作流是什么?”的时候,在定义里提到的“通过DSL来定义一组结构和模型”吗?这个DSL又是什么,它在工作流架构的设计当中,又充当着什么样的角色呢?
|
||||
|
||||
DSL(Domain Specific Language),是用来描述和定义业务逻辑的。工作流执行时,会根据你定义的这套逻辑执行。在基本构成的示意图中可以看到,一次执行包含了多个状态,这些状态可以是简单的一次操作,在DSL中,就会表示为如下的状态类型。
|
||||
|
||||
|
||||
Event:会在收到指定触发类型的事件后(比如之前课程提到的S3的对象存储事件),去执行调用动作(调用动作在DSL中被称为action),比如调用某个云函数。这里需要注意的是,触发事件需要严格满足Cloud Events格式协议;
|
||||
|
||||
Operation:也会执行调用动作,但是和Event状态不同的是,Operation不需要收到事件触发,而是可以主动执行调用动作;
|
||||
|
||||
Sleep:可以将当前执行挂起,并设置等待时间,类似于线程中的Sleep操作。等待结束后,则恢复执行过程;
|
||||
|
||||
|
||||
Inject:可以向工作流的输入数据中注入一些静态数据,然后传递给下一个状态。-
|
||||
|
||||
这些状态也可以用来控制复杂流程的执行逻辑,在DSL中表示为如下状态类型。
|
||||
|
||||
Switch:类似编程语言中的switch关键字,会通过eventConditions关键字来定义多个选项分支,并根据一些筛选条件决定下一状态。在所有条件都不满足时,defaultCondition还可以作为默认下一状态;
|
||||
|
||||
Parallel:包含一系列分支,通过branch关键字定义。但和Switch不同的是,这些分支的子状态是并行执行的;
|
||||
|
||||
ForEach:类似于编程语言中的迭代器,一般来说,该状态的输入都是数组或者集合的形式,然后循环执行迭代逻辑。
|
||||
|
||||
|
||||
这里我们要注意的是,不同厂商在实现上,虽然大体逻辑大致是遵循CNCF的标准协议的,但在语言的定义和约束上,还是稍微有些不同,且各家的名字也有不同,比如阿里云FDL、腾讯云TCSL、亚马逊ASL等。你可以参考下面我列出的对比表格看一下:
|
||||
|
||||
|
||||
|
||||
你会发现,部分厂商根据自身情况还增加了一些状态,如成功Succeed和失败Fail,其他的状态除了名字上的差异外,功能上基本相同。我们以阿里云Serverless Workflow举一个例子来加深一下理解:
|
||||
|
||||
version: v1
|
||||
type: flow
|
||||
steps: #对应DSL中的状态,FDL中称为步骤
|
||||
- type: parallel #type必须为parallel,表示为并行任务
|
||||
name: parallelDemo #name为当前步骤的名称
|
||||
branches: # 表示多个分支,可以对应多个子步骤。
|
||||
- steps:
|
||||
- type: task #子步骤的类型为Task
|
||||
name: function1 #子步骤的名称为function1
|
||||
# resourceArn表示调用的函数标识
|
||||
resourceArn: acs:fc:{region}:{accountID}:services/{serviceName}/functions/{functionName}
|
||||
- steps:
|
||||
- type: task
|
||||
name: function2
|
||||
resourceArn: acs:fc:{region}:{accountID}:services/{serviceName}/functions/{functionName}
|
||||
|
||||
|
||||
你也可以利用resourceArn和pattern的组合,用于集成多个云服务的能力。其中,resourceArn来定义集成的目标服务,使用pattern定义集成模式。这里每一个状态的具体使用字段我就不详细展开了,如果你需要开发和上线,可以根据具体选择的云厂商通读一遍他们的手册。
|
||||
|
||||
架构设计
|
||||
|
||||
了解完工作流的基本构成和模型的定义语言之后,如果仅仅是使用,那么其实就差不多了,我们只要能熟练地使用语言定义出相应的JSON或者YAML格式的文件就好。
|
||||
|
||||
那如果,我们想自己开发一个这样的功能,又要怎么实现呢?我们来看一下它的整体架构示意图:
|
||||
|
||||
|
||||
|
||||
如图所示,Serverless工作流通常需要具备元信息管理、调度和执行三个核心服务的模块。
|
||||
|
||||
|
||||
APIServer:负责元信息生命周期的管理,包括状态管理、模板、执行记录等信息;
|
||||
调度服务:根据数据流请求,转发调度到对应的执行引擎,一般来说,它还需要具备负载均衡、限流、故障迁移等能力;
|
||||
执行引擎:负责解析流程语言、执行流程,上报执行历史和调用其他云服务等。
|
||||
|
||||
|
||||
在使用上,用户通过APIServer控制工作流的基本信息,然后通过请求调度模块执行工作流,具体步骤和图片的示意相同。这里,我们要重点关注一下数据面的请求流向:
|
||||
|
||||
|
||||
请求到达调度模块后,调度模块首先会从APIServer获取当前工作流的定义内容和执行任务的元信息;
|
||||
调度模块根据请求内容将元信息和请求内容分发到指定的执行引擎;
|
||||
执行引擎会为本次执行生成一些元信息并记录到数据库,因为除了工作流本身的定义外,每一次执行都是无状态的,所以需要设置单独的任务ID ,方便后续做一些请求重入的操作;
|
||||
执行引擎根据流程定义语言语法,解析传入的工作流定义;
|
||||
执行引擎根据解析出的状态有序执行,并根据状态语义调用其他服务进行处理。
|
||||
|
||||
|
||||
最后,当所有State执行结束后,工作流执行引擎会根据最后的输出定义,返回结果。
|
||||
|
||||
当然,你也可以拆得更细一些,比如可以将与前端打交道的部分拆出来一个Console服务,将模型配置和模板示例拆分成一个模板服务出来,服务的粒度拆分没有硬性规定。
|
||||
|
||||
生产级考虑因素
|
||||
|
||||
按照上面的架构,你应该可以实现一个Demo级的工作流了,但是要完成一个生产级别的工作流,还需要考虑高可用、高性能和安全等相关的因素。
|
||||
|
||||
|
||||
|
||||
首先,我们的三大服务就不能以单点方式呈现了,需要以分布式的方式来部署。每个服务上层通过负载均衡层转发,提供一个对外暴露的HaVIP或者域名。
|
||||
|
||||
其次,对于执行引擎来说,因为工作流的核心内容是语义的解析与工作流程的执行,并且每次工作流任务都会启动一个独立的协程或线程工作,因此该引擎的负载较高,需要保证其具备横向扩缩容的能力。通常的做法是部署一个注册中心,然后将执行引擎进行服务注册,调度服务则会通过服务发现了解当前执行引擎的实例情况。
|
||||
|
||||
第三,随着应用和调用次数的增加,在生产环境中也需要格外注意消息中间件、数据库中间件、存储中间件的选取,比如执行记录的数据,就不能像DEMO期间那样用MySQL应付,你需要选择诸如Elasticsearch、Apache Doris等相关的存储系统。
|
||||
|
||||
第四,考虑到系统的性能以及面向流量洪峰的情况,还可以在调度服务和执行引擎中间采取消息中间件、缓存中间件来处理。比如频繁数据的读取这种情况,就可以将元数据和模板等信息预加载到诸如Redis这样的缓存系统中。
|
||||
|
||||
另外,还有两个注意事项没有在我们的示意图中体现。
|
||||
|
||||
|
||||
安全相关:你需要将业务服务接入用户认证和权限系统,确保服务的安全性;
|
||||
可观测:工作流面向的是复杂的业务编排场景,发生问题后排查起来难度较高,因此,系统的全链路监测就更加重要了。你需要提供链路状态的转换记录、执行结果和执行耗时等指标,并根据这些指标设定相应的预警机制。
|
||||
|
||||
|
||||
请求可重入
|
||||
|
||||
我们再来看请求的可重入。前面我们提到,调度服务要具备请求路由、服务发现、故障转移、实现限流、负载均衡等能力,这些都是我们在实现一个微服务功能需要考虑的治理手段。
|
||||
|
||||
除此之外,你还需要关注一下请求重入的能力,保证同一次请求的执行可以落在同一个执行引擎的实例。这样,就避免了同一个请求重复执行的问题发生。
|
||||
|
||||
为了保证请求可重入,首先,我们要尽量让同一工作流的任务落在相同的执行引擎上,这里可以通过hash映射的方式,将每个用户ID和执行引擎实例相关联,并写入redis中。
|
||||
|
||||
然后,为了区分重复的执行任务,还需要为每个执行任务打上唯一标识。因为不同工作流的执行任务可能会有相同的name,为了保证唯一性和可读性,我这里给你提供一种方式来生成任务ID:
|
||||
|
||||
TaskID = UserName|workFlowName|TaskName
|
||||
|
||||
|
||||
将TaskID与执行引擎实例的关联关系记录在Redis中,每次执行前,就可以判断当前的执行请求是否已经被执行了,这样,就可以避免重复的执行任务。
|
||||
|
||||
|
||||
|
||||
需要注意的是,当某个执行引擎出现故障时,调度模块会将Redis中故障执行引擎的所有执行任务全部调度到其他实例上,以此实现任务的迁移,保证整个系统的稳定性。当然,你也可以选择其他的K-V中间件来实现这个思路。
|
||||
|
||||
语法解析流程
|
||||
|
||||
对基本的架构了解清楚之后,我们来看一下执行引擎的语法解析是如何实现的。
|
||||
|
||||
前面我们提到,工作流的定义格式以JSON和YAML的形式存在,我们需要先解析定义的文件信息,然后再按照顺序去执行每个定义好的状态State。工作流内,*状态之间的关系是具备数据结构中*“*n叉树*”特征的,所以,可以采用深度优先的遍历思想来执行工作流中的每一个状态。
|
||||
|
||||
我这里根据DSL的状态分类给出了一段核心处理过程的代码,为了便于你理解,我采取switch…case的方式进行,你可以选择更好的设计模式来实施:
|
||||
|
||||
func (flow *Flow) ProcessState(input *Input, flowDefinition *FlowDefinition) *Output {
|
||||
|
||||
...
|
||||
|
||||
var output *Output
|
||||
state := flow.getStartState(flowDefinition)
|
||||
|
||||
for {
|
||||
switch state.GetType() {
|
||||
case api.Event:
|
||||
output = flow.processEvent(state, input)
|
||||
case api.Operation:
|
||||
output = flow.processOperation(state, input)
|
||||
case api.Inject:
|
||||
output = flow.processSleep(state, input)
|
||||
case api.Switch:
|
||||
output = flow.processSwitch(state, input)
|
||||
case api.Parallel:
|
||||
output = flow.processParallel(state, input)
|
||||
case api.Foreach:
|
||||
output = flow.processForeach(state, input)
|
||||
default:
|
||||
flow.logger.Errorf("invaid state type: %v", state)
|
||||
...
|
||||
break
|
||||
}
|
||||
|
||||
if output.Error != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// 遍历到最后一个state退出
|
||||
state = state.NextState(flowDefinition)
|
||||
if state == nil {
|
||||
break
|
||||
}
|
||||
|
||||
// 当前state的输出为下一state的输入
|
||||
input.Input = output.Output
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
|
||||
接下来,我们就根据示意代码理解一遍语法解析的流程。
|
||||
|
||||
按照深度优先的遍历原则,我们首先需要根据定义内容获取开始状态,并将开始状态作为当前状态,同时将调用请求中的输入数据作为开始状态的输入。
|
||||
|
||||
接着,根据当前状态的状态类型选择指定的处理流程,比如Event类型,你的processEvent方法实现就可以根据State中的Action去调用相关服务的API接口,来执行该次任务。如果是Parallel状态,就可以为每个状态子任务启动一个协程或者线程去执行,然后同步等待所有子状态执行完,再将结果返回。
|
||||
|
||||
当前状态处理完成后,再获取下一状态作为当前状态,并将当前状态的输出作为下一状态执行时的输入。如上述代码中的state.NextState(flowDefinition)。
|
||||
|
||||
最后,一直到当前状态不存在下一状态时,就完成了整个工作流的执行,并且将最后一个状态的输出作为整个工作流的输出来返回。
|
||||
|
||||
到这里,关于工作流的几个核心要点就讲解得差不多了。其实除了上面提到的这些,要做一个功能比较完善的工作流解决方案,像计量计费、支持回调、如何设定通用的调用协议等,也都需要留意。
|
||||
|
||||
是不是还意犹未尽呢?提前预告一下,我在第16节课也会跟你一起动手,体验基于模板的核心理念以及快速构建服务的上线方法,其中,就包含了单函数模板、多函数编排的具体实操以及构建模板的方法,让你不仅知道原理,还会动手实战。
|
||||
|
||||
小结
|
||||
|
||||
这节课,我向你介绍了协调多任务执行的编排技术——工作流。
|
||||
|
||||
首先,我们要知道,工作流可以用于多个函数和子流程的编排和协调执行,像订单等事务型业务,多媒体大文件处理,数据流水线作业自动化运维等场景都会用到工作流。
|
||||
|
||||
再来,几个厂商描述和定义业务逻辑的实现虽然不太相同,但基本都遵循CNCF的规范。你可以通过顺序、分支、并行等方式来协调一个或多个分布式任务,这些任务不仅包括函数,还可以是服务和应用的形式。
|
||||
|
||||
工作流的核心构成,包括流程的定义、流程的解析和各节点的输入输出。我们可以通过DSL语言来编排各个节点状态,这些节点包括操作、并行、选择、延迟等8个常用的状态。
|
||||
|
||||
在架构设计上,我们需要关注它的三大核心服务:APIServer、调度服务和执行引擎。但如果你是一个平台设计者,还需要考虑安全、可观测、计量计费、日志存储等多个辅助功能项,才能完整地设计出一个解决方案。
|
||||
|
||||
希望通过这节课,你能发现Serverless另一个强大的功能,除了函数计算的形态,工作流在并行调用、批处理、事务型场景都发挥着巨大的作用,Server Less is More,并不是一句空话。
|
||||
|
||||
思考题
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个思考题。
|
||||
|
||||
工作流执行引擎的执行记录是否需要一直保存?如果不需要,你打算如何设计一个清理程序?需要考虑哪些要点呢?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起交流讨论。
|
||||
|
||||
延伸阅读
|
||||
|
||||
CNCF Serverless WG workflow 小组制定了一个完整的协议Serverless Workflow Specification,上面不仅给出了规范,还给出了相关使用案例。
|
||||
|
||||
|
||||
|
||||
|
216
专栏/Serverless进阶实战课/13可观测(上):如何构建多维度视角下的Serverless监测体系?.md
Normal file
216
专栏/Serverless进阶实战课/13可观测(上):如何构建多维度视角下的Serverless监测体系?.md
Normal file
@ -0,0 +1,216 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
13 可观测(上): 如何构建多维度视角下的Serverless监测体系?
|
||||
你好,我是静远。
|
||||
|
||||
今天我要跟你分享的主题是函数计算平台必备的另一个高级能力——可观测。
|
||||
|
||||
我们都知道,线上故障的快速定位几乎是所有开发人员都会面临的问题。在函数计算中,一个生产环境下的应用可以包括几十甚至上百个云函数,加上函数本身具有无状态、底层资源透明化等特点,这些都会增加用户排查问题的难度。
|
||||
|
||||
因此,可观测性对更加偏向业务的云服务平台来说,无疑是平台的另一双眼睛,能够帮助运维人员和开发者更快地排查故障,定位问题。
|
||||
|
||||
也许,你在进行函数开发的过程中,也遇到过这样的问题:
|
||||
|
||||
|
||||
为什么我的函数响应时间突然变得那么长?
|
||||
为什么我的函数突然访问不通了?
|
||||
为什么我的函数一直返回4xx、5xx错误?
|
||||
为什么我的函数一直超时?
|
||||
……
|
||||
|
||||
|
||||
带着这些问题,我将通过可观测的三大支柱,指标、日志和链路,从不同维度带你学习Serverless函数计算平台下可观测体系的构建。
|
||||
|
||||
希望通过这两节课,让你对函数计算在用户开发和平台运维过程中可能遇到的一系列问题和相应的解决方案有一定的了解,构建多维度的Serverless检测体系,助力业务更快、更稳地运行,而我们最开始的疑问,也都会一一得到解答。
|
||||
|
||||
Serverless下可观测的重要性
|
||||
|
||||
在Serverless的函数计算范畴内,除了函数开发,函数计算平台本身的运维工作也会暴露出各种问题。我把这些问题分为函数执行和平台运维两大类。
|
||||
|
||||
|
||||
|
||||
针对这两大类问题,我们往往不能根据函数返回的结果或者问题的表象立刻找到问题的症结,而是需要随着调用链路层层深挖。
|
||||
|
||||
可以说,资源的不可见让函数计算平台在用户视角下显得更像是一个“黑盒”,用户向“黑盒”发出请求后得到执行结果,但并不知道请求是如何调度到函数实例的,也不清楚代码究竟是如何运行的。图中像代码异常、执行超时、并发超限等等这一系列问题,最终只能通过“黑盒”的输出得到。
|
||||
|
||||
另一方面,函数计算平台涉及到调度、扩缩容、元信息管理等多个中控服务之间的交互,函数实例又离散分布在一个庞大的Kubernetes集群中,单节点故障时有发生,并且所依赖的下游服务也有不稳定的可能性,这样复杂的架构让线上问题的排查变得更加困难。
|
||||
|
||||
种种问题都表明了可观测性对于函数计算平台的重要性。那么,一个多维度的函数计算可观测体系应该如何构建呢?
|
||||
|
||||
我们可以参照云原生的可观测思路,提供监控度量、日志以及链路三种数据,再根据用户开发与平台运维的实际需求,构造一个多维度的可观测体系。
|
||||
|
||||
需要注意的是,构建可观测体系时,也要结合函数计算平台的特点,比如在用户侧,开发者更关心的是业务层面的处理流程,并不会像使用其他PaaS平台一样,关心请求在服务内部卡在了哪个环节。因此,用户侧的可观测数据应该尽量与调度层面的处理细节解耦。而在平台侧,我们又要考虑到复杂的集群环境以及多服务之间的关联。
|
||||
|
||||
整体解决方案
|
||||
|
||||
下面的思维导图总结了Serverless函数计算可观测体系建设的核心要点,结合函数计算在用户与平台下的两个视角,列出了我们在构建监测体系时需要考虑的关键点。我们通常需要通过可观测的三大数据支柱:指标(Metrics)、日志(Logs)、链路(Traces)来思考函数计算的不同场景。
|
||||
|
||||
|
||||
|
||||
接下来,我会根据整个大纲带你体验构建一个函数计算可观测体系的过程。
|
||||
|
||||
指标
|
||||
|
||||
在服务出现问题的时候,不管是用户还是运维最先关心的总是请求出错的整体情况,而了解整体概况最快的方法就是监控指标。
|
||||
|
||||
确定指标
|
||||
|
||||
要想通过监控指标观察服务的整体运行状态,我们面对的第一个问题,就是指标类型的设定。对于使用函数计算的用户而言,他们更关心的是业务逻辑,因此,常用的用户侧函数监控指标会包括以下几种:
|
||||
|
||||
|
||||
函数的总执行次数;
|
||||
函数的平均执行耗时;
|
||||
关键错误类型的发生次数(超时、并发超限、5xx系统故障等);
|
||||
函数执行的资源使用情况;
|
||||
……
|
||||
|
||||
|
||||
而对于平台侧来说,我们往往更加关注的是函数计算的服务体系是否在正常工作以及涉及到的底层集群资源是否健康。因此,平台侧的系统监控通常会关注以下几个方面:
|
||||
|
||||
|
||||
服务处理的总请求数量;
|
||||
服务处理的失败请求数量;
|
||||
单次扩缩容成功/失败的实例数量;
|
||||
集群节点总数的整体变化趋势;
|
||||
集群节点的资源使用情况;
|
||||
服务节点的资源使用情况;
|
||||
……
|
||||
|
||||
|
||||
指标的收集与上报
|
||||
|
||||
在指标类型确定后,我们就可以收集、上报指标了。对于这部分流程,业内已经有很多不错的解决方案了。接下来,我将以Prometheus这款较为成熟的开源监控产品作为示例来讲解。关于Prometheus的安装过程,你可以参考官方手册进行搭建。
|
||||
|
||||
如果你对Prometheus有一定的了解,那应该也知道它的基本工作原理是不断地向Exporter(指标上报器)获取数据,而像CPU、Disk、Memory这类资源指标类型,可以利用Promethues官方提供的node-exporter进行采集。另外,对于集群和单节点的部署,也有了比较成熟的高可用解决方案。
|
||||
|
||||
我们这里主要还是聚焦于业务指标。以函数调用次数为例,我们利用Promethues提供的SDK模拟一个简单的上报指标,看看自定义的指标数据最终是如何展现到监控大盘上的。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 构造一个名为function_x_invocation_times的自定义指标
|
||||
// 模拟函数的调用次数
|
||||
invocationTimes := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "function_x_invocation_times",
|
||||
Help: "count the function x of invocation times",
|
||||
},
|
||||
[]string{`functionName`},
|
||||
)
|
||||
// 将指标写入注册表中
|
||||
registry := prometheus.NewRegistry()
|
||||
registry.MustRegister(invocationTimes)
|
||||
|
||||
// 函数调用次数模拟器
|
||||
mockFunction := func(functionName string) {
|
||||
for {
|
||||
invocationTimes.WithLabelValues(functionName).Set(float64(100 + rand.Intn(10)))
|
||||
time.Sleep(time.Second * 2)
|
||||
}
|
||||
}
|
||||
// 模拟两个函数的调用
|
||||
go mockFunction("functionA")
|
||||
go mockFunction("functionB")
|
||||
|
||||
//启动自定义的Exporter
|
||||
http.Handle("/metrics", promhttp.HandlerFor(
|
||||
registry,
|
||||
promhttp.HandlerOpts{Registry: registry},
|
||||
))
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
||||
|
||||
|
||||
|
||||
如上述的代码所示,我们构造一个名为function_x_invocation_times的自定义指标来模拟函数的调用次数,并将它写入Promethues的注册表中。然后,我们需要模拟一下函数的请求,通过启动两个协程模拟两次函数的调用,并启动一个自定义的Exproter供收集器采集。
|
||||
|
||||
在程序启动后,访问本地的http://localhost:8080/metrics 路径,就会看到上报的两个函数的调用次数了。
|
||||
|
||||
|
||||
|
||||
接下来,我们需要从metics接口获取数据,发送到Promethues,不过在启动收集器之前,还需要进行一些简单的配置。我们可以在prometheus.yml增加函数指标的Exporter。
|
||||
|
||||
scrape_configs:
|
||||
- job_name: "serverless"
|
||||
static_configs:
|
||||
- targets: ["localhost:8080"]
|
||||
|
||||
|
||||
完成配置之后,我们再执行二进制文件启动Prometheus,通过默认的http://localhost:9090 地址即可登录。进入主界面后,我们就可以查询到刚才上传的函数调用信息了。
|
||||
|
||||
|
||||
|
||||
监控数据被收集完成后,需要对其进行存储以便下游服务使用。这类具有时序关系的指标数据通常采用时序数据库TSDB进行存储。这类存储专门用来解决像监控指标这类产生频率快、种类繁多且强依赖于时间顺序的数据持久化的问题。
|
||||
|
||||
Prometheus其实已经内置了一个时序数据库,在一些数据量不大的场景下,是完全够用的,但如果你需要打造一个生产级别的可观测体系,我还是强烈建议你外接一个专门的时序数据库。
|
||||
|
||||
另外,在函数计算场景下,由于单个函数的实例可能遍布在集群的任意节点,这时如果仅靠DaemonSet方式部署的Exporter进行信息上报,其实效率并不高。因为Prometheus除了收集外,还需要汇聚来自不同节点上同一函数的指标,一旦请求激增,汇聚服务的压力就变得非常大。
|
||||
|
||||
考虑到需要让Prometheus的收集工作更加稳定,我们也可以在每次请求处理完后,将指标存入消息队列,并通过消费的方式提前汇总一轮指标。汇总完成后,再将指标上报给Prometheus进行二次汇聚。加上上面提到的持久化方案,一个轻量级的、函数计算下的监控体系架构就搭建好了,示意图如下。
|
||||
|
||||
|
||||
|
||||
你会发现,其中的Scheduler就是扩缩容课程中提到的流量调度模块。在请求的返回路径上,它会将函数指标上报到kafka,之后通过Function Exporter进行数据消费,并对不同节点产生的数据进行一次汇聚,完成之后,再交由Prometheus二次汇聚,最终再通过TSDB完成数据落盘。
|
||||
|
||||
可以看到,图中Prometheus不光需要从FunctionExporter获取指标,还可能从每个节点的Node-Exporter获取资源信息指标,这就是我前面提到的“指标数据一旦增多就会加大汇聚服务的压力”的原因。
|
||||
|
||||
最后,我还是要强调一下,在集群规模不大时,这的确是一个比较轻便的解决方案,但在集群节点数量比较多的情况下,我更建议你针对业务指标和资源指标分别配置Promethues。
|
||||
|
||||
另外,如果你是基于前面章节中提到的Knative来作为Serverless引擎,由于Knative内部采用的OpenCensus协议会暴露trace和metrics数据,采集也会更方便。你可以配置istio-ingressgateway、Knative Activator和queue-proxy来上传,对于用户容器实例,如果是java代码,还可以无侵入地通过Opentelemetry的Java客户端otel-javaagent来上报。
|
||||
|
||||
监控大盘的展示
|
||||
|
||||
完成了对函数指标的收集,我们又要如何对这些指标做可视化展示,清晰地看到数据的变化呢?接下来,我就利用Grafana采集的指标构造一个函数调用次数的监控大盘。
|
||||
|
||||
完成Grafana的安装后,我们首先需要配置Grafana的数据源。
|
||||
|
||||
在左侧菜单栏依次点击“Configuration->Data sources->Add data source”,接着,选择Prometheus作为配置的数据源,点击最下方的“Save & test”,当出现“Data source is working”的提示时,就表示已经能够正常连接到Prometheus了。
|
||||
|
||||
|
||||
|
||||
接着,我们再构造一个监控面板,在左侧菜单栏依次选择“Create->Dashboard->add panel->add a new panel”,进入新面板后,我们通过建立一个Query并输入“function_x_invocation_times{functionName=“functionA”}”,就可以将刚才上报的关于functionA函数的调用次数信息展示出来了。
|
||||
|
||||
|
||||
|
||||
按照刚才的方式,我们同样还可以看到functionB的调用次数。
|
||||
|
||||
|
||||
|
||||
到这里,我们就完成了一个简单的函数调用次数的指标监控。
|
||||
|
||||
相信你对这些函数计算业务指标从上报到收集再到展示的过程已经有了一个清晰的思路。像前面提到的执行时长、QPS、扩缩容数量等指标,在监控的实现上大同小异,你都可以尝试自己实现,也欢迎你在留言区和我互动讨论。
|
||||
|
||||
最后,我们再补充一个细节。如果出现线上问题,比如开发者的函数返回结果不符合预期,或者运维人员发现某个时间段的扩缩容数量始终为0时,我们也可以利用到可视化工具的时间段选择功能。选择时,最好先将时间粒度调大一些,比如我们先观察一天内或者一周内的整体情况,看看异常的大致时间拐点发生在哪一天,再根据时间拐点附近的时间段做细粒度的排查,这样可以有效提高我们的排障效率。
|
||||
|
||||
小结
|
||||
|
||||
通过本节内容的梳理,我们对FaaS形态Serverless可观测的重要性以及方案都有了一定的了解。
|
||||
|
||||
在Serverless的架构下,由于黑盒的调度和云组件集成的复杂性,可观测的构建要比传统微服务的架构难度更大。我们不仅要具备可观测的基础搭建思路,对Serverless产品本身也要非常熟悉。
|
||||
|
||||
我们还梳理了三大数据支柱之一,“指标”。首先,我们要确定指标的类型,熟悉常用的用户侧、平台侧函数监控指标有哪些。收集、汇聚指标的时候,也要注意汇聚的量级、存储形式、收集工具等要素;最后,收集到的指标也要利用起来,我们可以将采集到的指标做成监控大盘,最大化利用数据,为后续有可能出现的优化动作给出数据支撑。
|
||||
|
||||
好了,这节课我们就先到这里,下一节课,我再来跟你细聊三大数据支柱中日志和链路这两部分的内容。
|
||||
|
||||
思考题
|
||||
|
||||
想一想,日志和链路应该如何处理呢?是否可以归到一套架构上来实现信息的收集?
|
||||
|
||||
如果有时间,你可以先按照今天的示例自己操作演练一下指标的收集。
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起交流学习。
|
||||
|
||||
|
||||
|
||||
|
138
专栏/Serverless进阶实战课/14可观测(下):如何构建多维度视角下的Serverless监测体系?.md
Normal file
138
专栏/Serverless进阶实战课/14可观测(下):如何构建多维度视角下的Serverless监测体系?.md
Normal file
@ -0,0 +1,138 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
14 可观测(下): 如何构建多维度视角下的Serverless监测体系?
|
||||
你好,我是静远。
|
||||
|
||||
上一节课,我们一起梳理了Serverless下可观测的重要性和构建可观测监测体系的要点,也结合案例,学习了指标的收集方法,了解了FaaS形态下指标上报的架构设计和注意事项。
|
||||
|
||||
今天这节课,我们继续来看可观测的另外两个数据支柱:日志和链路追踪。
|
||||
|
||||
日志
|
||||
|
||||
我们知道,通常在运维一个系统的时候,从监控大盘了解问题的大致轮廓后,经常会根据日志去查看具体的错误细节。
|
||||
|
||||
日志的作用是记录离散事件,并通过分析这些记录了解程序的整体行为,比如出现过哪些关键数据,调用过哪些方法。也就是说,它能够帮我们定位问题的根源。
|
||||
|
||||
在函数计算场景下,我们需要考虑到用户日志与系统日志两种类型。其中,用户日志记录的主要是用户函数代码中业务流程发生的过程。这部分日志信息是在函数维度上独立收集的,并且用户可以通过前端控制台查看相关日志信息。而系统日志,则是整个平台侧发生事件的信息记录,最终汇总在一起,供平台侧的运维或开发人员排查问题。
|
||||
|
||||
日志数据源
|
||||
|
||||
那么日志应该什么时候打印又应该怎么打印呢?
|
||||
|
||||
首先,我们需要明确日志的级别。以系统日志为例,常见的包括Error、Info和Warn,分别表示错误日志、信息日志以及警告日志,在开发调试过程中可能还会用到Debug类型。我们需要根据实际的执行逻辑来设定不同的等级。
|
||||
|
||||
其次,在添加日志时,我们要尽可能地收敛错误信息,尽量避免重复信息的打印,因为频繁的IO不仅会加大日志采集的工作量,更会影响服务性能。
|
||||
|
||||
再次,日志应该尽量打在每个服务模块的入口和出口,服务内方法之间的调用和错误信息,也应该尽量通过传递的方式上报,而不是每个方法内都打印一条日志。
|
||||
|
||||
比如函数计算中请求的调度过程。调度模块中可能涉及到获取元信息、鉴权、并发度限制、获取函数实例信息等等一系列串行过程,每一个过程都可能包含多个方法之间的调用,其中任意一步出现问题都会导致调度失败。那么,我们在开发时就要尽可能给每个方法都添加一个error类型的返回值,在出现错误后,只需按照递归调用栈逐级返回,最终在入口处打印一条即可。这样可以有效减少重复信息的打印次数。
|
||||
|
||||
另外,对于函数开发者用到的用户日志,我也有两条使用上的建议。
|
||||
|
||||
一方面,减少print的使用,控制整体的信息大小。为了方便在函数这个黑盒中快速定位问题,一些开发者习惯性地将print当成Debug工具来使用,但因为平台侧都会对单次的函数执行日志有一定限制,所以在函数开发过程中应该尽量减少无用信息的打印。
|
||||
|
||||
另一方面,关注Event。Event作为函数入口的基本传参,携带了请求源的关键信息,关注Event,也可以方便后续的溯源。
|
||||
|
||||
日志的采集与清洗
|
||||
|
||||
有了日志数据后,我们就可以进行收集了。
|
||||
|
||||
系统日志的数据都会写入到固定路径的文件中,而用户日志,通常都是采用DaemonSet日志组件进行收集。所以,一般函数实例内的日志文件都会存放在节点的挂载路径下,或者是集群内的持久卷中。
|
||||
|
||||
前面我们也提到用户日志是以函数为粒度的,而为了缓解不断增长的日志数据造成的节点磁盘的压力,通常会一次请求对应一个日志文件,请求结束则上报并删除文件内容。而系统日志则可以通过设置“定时删除任务”来处理。
|
||||
|
||||
在开源日志收集器的选型上,常用的有Logstash,Fluentd,Fluent-Bit以及Vector等比较不错的采集工具,他们之间各有不同的优势。
|
||||
|
||||
具体的对比,你可以参考Stela Udovicic 2021年12月在ERA Software’s blog的文章,她指出,我们很难找到一个完美的日志收集器,选择正确的日志收集器主要取决于你自己的特定需求。
|
||||
|
||||
比如,如果你需要资源占用较少的日志收集器,那么使用Vector或者Fluent-Bit就是一个不错的选择,而不是占用资源较高的Logstash。如果你需要寻找不具供应商色彩的收集器,那么Fluentd和Fluent-Bit是不错的选择。
|
||||
|
||||
在函数计算平台的构建中,通常我们会结合这几种工具的能力共同部署,你也可以根据具体的业务情况自己选择。这里,为了便于你理解他们各自的优势,我画了一个示意图,来看一下数据采集的具体流程。
|
||||
|
||||
|
||||
|
||||
在采集数据时,由于Fluent-Bit在Kubernetes集群等容器化环境中的运行比较出色,所以通常我们会使用轻量的Fluent-Bit对日志数据进行整体的上报,如果是集群的日志信息,则会以DaemonSet的形式部署。
|
||||
|
||||
因为Logstash过滤功能强大,但资源耗费多,所以并不能像Fluent-Bit那样以DaemonSet的形式部署在整个集群,只需部署少量虚机实例,并利用Logstash进行整体的数据清洗即可。
|
||||
|
||||
如果考虑到峰值问题,比如前面提到的某一时刻存在请求高峰导致日志量显著增大,也可以利用kafka缓冲一轮。最后,再由Logstash将过滤后的数据交由相应的存储服务。
|
||||
|
||||
日志的存储与检索
|
||||
|
||||
最后,我们再说日志的存储和检索。
|
||||
|
||||
|
||||
|
||||
Logstash支持丰富的插件,除了可以支持像kafka、本地文件等多种数据输入源,输出的部分同样也支持与Elasticsearch、对象存储等多种数据存储服务的对接。
|
||||
|
||||
其中,Elasticsearch是一个分布式、高扩展、高实时的搜索与数据分析引擎,配合Kibana这种数据可视化工具,能快速搜索和分析我们上传的日志。
|
||||
|
||||
为了方便利用Kibana进行快速筛查,我们在日志打印阶段就应该以键值对的形式标记出关键信息,这样就可以在Kibana根据key进行筛查,比如:
|
||||
|
||||
{
|
||||
"level":"info", //日志等级
|
||||
"ts":1657957846.7413378, // 时间戳
|
||||
"caller":"apiserver/handler.go:154", // 调用的代码行数
|
||||
"msg":"service start", // 关键信息
|
||||
"request_id":"41bae18f-a083-493f-af43-7c3aed7ec53c",
|
||||
"service":"apiserver" // 服务名称
|
||||
}
|
||||
|
||||
|
||||
|
||||
另外,如果需要对日志文件进行溯源,或者需要考虑为函数计算平台拓展一些长期的数据报表功能,也可以让Logstash对接一个对象存储服务。
|
||||
|
||||
链路
|
||||
|
||||
了解了指标和日志两个可观测数据之后,我们再去看第三个数据,链路。
|
||||
|
||||
除了代码出错,在一些延迟敏感的场景下,性能分析也是必不可少的,尤其是函数计算这种架构复杂,模块交互较多的服务。这个时候,链路追踪功能就派上了用场。
|
||||
|
||||
在函数计算场景下,它不仅可以提高函数计算系统的可观察性,帮助系统管理员检测、诊断系统的性能问题以保证预期的服务水平,还可以帮助开发者追踪函数的执行过程,快速分析、诊断函数计算架构下的调用关系及性能瓶颈,提高开发和诊断效率。
|
||||
|
||||
链路信息
|
||||
|
||||
我们先说链路信息的获取。对用户而言,更关心的是端到端的整体耗时,而除了代码本身的执行,其余耗时主要发生在冷启动的准备阶段。因此,平台可以默认提供给用户函数总耗时以及冷启动过程的耗时,其中也可以包括准备代码、运行时初始化等步骤的耗时。
|
||||
|
||||
而在复杂的业务场景中,往往会涉及到函数与函数或者函数与其他云服务之间的调用。这时,我们可以为开发者提供自定义的链路支持,将链路信息记录在相应的结构体中(如Header),就可以完整地串联起整个外部的调用链路。而内部的调用链路,也可以通过上下文的形式用SDK去处理。
|
||||
|
||||
通过这种与内置链路结合的方式,平台可以有效地帮助用户定位出超时、性能瓶颈以及涉及多个云服务关联等类似的故障问题。
|
||||
|
||||
在平台层面,则可以根据模块之间的关系以及系统架构的实际情况来构建链路,整体思路和用户侧的链路构建差不多。不过需要注意的是,并不是链路信息越详细越好,因为链路追踪本身也需要耗费一定资源,所以最好根据实际的运维需求来构建。
|
||||
|
||||
链路拓扑的可视化
|
||||
|
||||
那么,我们如何追踪链路信息并友好地展示出来呢?常用的解决方案,一般是基于标准的OpenTelemetry协议,利用其提供的SDK和Otel Agent完成对链路Span的生成、传播和上报,最终通过分布式追踪系统(如Jaeger)进行收集,形成链路拓扑的可视化。
|
||||
|
||||
结合函数计算的特点,这里我也给出了一个基本的链路追踪功能架构图供你参考。从图中可以看出,OpenTelemetry可以通过三种方式来上报链路信息,包括直接使用SDK、Agent Sidecar和Agent DaemonSet,你可以根据自己的业务情况选择一种,或者多种组合方式。
|
||||
|
||||
|
||||
|
||||
以Jaeger为例,节点服务可以使用Opentelemetry 的SDK或者Agent上报链路信息,通过Jaeger Collector统一收集,并由ElasticSearch进行存储,最终由Jaeger-Query负责展示数据查找。
|
||||
|
||||
到这里,我们来解答一下上一节课的课后思考题,Metrics指标数据是否也可以用此方式来收集呢?完全可以,基于Otel Agent,我们可以将数据发往Kafka、Promethues等数据存储后端的Backends。
|
||||
|
||||
小结
|
||||
|
||||
最后,我来小结一下这两节课的内容。这两节课,我们一直在讨论函数计算平台下可观测体系的解决方案。我们可以按照可观测中指标、日志和链路这三要素的架构去构建解决方案。
|
||||
|
||||
首先,监控指标是指对系统中某一类信息的统计聚合。在函数计算平台上的监控,不仅需要考虑常见资源指标,比如CPU、Memory利用率等,还需要考虑用户实际关心的业务指标,比如函数调用次数、错误次数、执行时间等。
|
||||
|
||||
再说日志的构建。日志起到的作用,更像是在“保留现场”。通过日志,我们可以分析出程序的行为,比如曾经调用过什么方法,操作过哪些数据等。在打印日志时,我们也需要关注函数在关键节点上的输出。
|
||||
|
||||
最后,链路的追踪是通过对请求打标、透传、串联还原完整的请求过程。追踪主要是为了排障和优化,比如分析调用链的哪一部分、哪个方法出现了错误或阻塞,输入输出是否符合预期等。服务之间的链路信息耶可以通过Header进行传递,而整个数据过程其实和日志采集类似。但从形态上来看,日志更像是离散的事件,而链路追踪更像是连续的事件。
|
||||
|
||||
思考题
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个思考题。
|
||||
|
||||
随着OpenTelemetry的盛行,原则上使用一套Library或SDK,就可以自动地收集三种数据,然后由统一的Collector处理,但实际应用中,还会受限于新老系统、Otel的成熟度,你是怎么处理的?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起交流学习。
|
||||
|
||||
|
||||
|
||||
|
245
专栏/Serverless进阶实战课/15选型:不同阶段的数据应如何存储?.md
Normal file
245
专栏/Serverless进阶实战课/15选型:不同阶段的数据应如何存储?.md
Normal file
@ -0,0 +1,245 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
15 选型:不同阶段的数据应如何存储?
|
||||
你好,我是静远。
|
||||
|
||||
今天我要与你分享的主题和Serverless平台“存储”设计相关。通常我们在做一个服务或者平台的设计时,存储都是必不可少的一环,比如一些用户数据、日志信息、中间状态等。
|
||||
|
||||
在你构建一个生产级FaaS形态的Serverless平台时,也许会遇到这些问题:
|
||||
|
||||
|
||||
函数定义该存在哪里?
|
||||
代码包这么大该怎么存?
|
||||
怎么才能保证扩缩容资源的状态被实时感知?
|
||||
……
|
||||
|
||||
|
||||
这节课,我就来和你聊聊Serverless下的数据存储是如何设计的。我将结合上面这些问题,从控制面和数据面两个维度展开,带你了解一个函数计算平台的存储应该考虑哪些方面。
|
||||
|
||||
希望通过这节课,你能够对函数计算存储的数据类型,以及针对不同特点数据的存储方案有更深的理解,从而顺利地设计出Serverless平台的存储方案。
|
||||
|
||||
整体思路
|
||||
|
||||
那么,针对函数计算的形态特征,我们该如何来设计和选型呢?
|
||||
|
||||
我通过下面这个思维导图总结了设计一个函数计算平台的核心要点和思路。
|
||||
|
||||
|
||||
|
||||
其中,控制面由元数据和代码包两个维度组成;从请求的执行过程出发,数据面主要需要考虑元数据在获取时的缓存处理、 函数Pod资源的调度处理、服务之间的协同操作以及日志相关的收集等维度。其中,日志和服务的协同也可以认为是贯穿在整个系统始终的,比如服务协同包括服务的注册与发现、消息的通知与协调、选主等。
|
||||
|
||||
控制面的存储
|
||||
|
||||
我们先从控制面来看,在创建一个函数时需要准备哪些数据。
|
||||
|
||||
元数据
|
||||
|
||||
在创建函数之前,首先需要有一个命名空间。虽然大部分函数计算产品都会提供一个默认的命名空间来省略这一步,但在实际的业务上云过程中,都会使用命名空间来管理不同的函数,区分业务,这也属于函数计算中的一部分元信息。
|
||||
|
||||
接着,我们再选择创建一个函数。云函数的基本属性就比较多了,除了要考虑控制台上你看到的函数名、函数入口、超时时间、内存大小、运行时类型等常见的函数属性,在实际存储时,我们还需要记录函数的创建时间、更改时间以及函数的唯一标识符。除此之外,当使用灰度发布时,我们还需要考虑函数的版本信息。如果用户绑定了触发器的话,还需要记录触发器的一些基本信息,比如事件源服务、触发规则等。
|
||||
|
||||
除了函数本身的附属属性外,平台通常还需要考虑用户相关的信息存储,例如用户ID、权限和并发上限等信息。
|
||||
|
||||
我们可以看到,与函数相关的元信息是比较多的,涉及函数、触发器、用户、版本、别名、命名空间等多个实体,其中每个实体又包含众多的属性。
|
||||
|
||||
它们之间具有复杂的关联关系,相信你也能猜到,这时,我们需要使用关系型数据库进行存储。它在支持复杂操作和字段更新方面会更灵活,更重要的是,你肯定对关系型数据库并不陌生,几乎每个同学在学校就已经开始接触了。
|
||||
|
||||
但估计在数据库的选型上,大部分同学基本上都是靠直觉选择一个自己最熟悉的来做。这里,我跟你分享几个常见数据库的选择。
|
||||
|
||||
这是db-engines给出的最新数据库排名,其中前四名都是关系型数据库。
|
||||
|
||||
|
||||
|
||||
排名第一的Oracle作为数据库的顶级霸主已经霸榜多年,但是价格昂贵且闭源。而我们熟悉的云厂商的云数据库基本上也是围绕MySQL、SQL Server、PostgreSQL来提供云服务能力。所以可以特别关注一下这几个选项。
|
||||
|
||||
其中,SQL Server虽然随着新版本的发布和云原生技术的发展,也能在诸如Linux的系统上安装运行,但从互联网发展历程和各大公司系统采用的数据库来看,我更推荐你使用MySQL、PostgreSQL。
|
||||
|
||||
MySQL发展至今已经非常成熟,并且社区内也很活跃,其可用性、性能都非常不错。而PostgreSQL是号称世界上最先进的开源关系型数据库,几乎具备MySQL的全部特性,在国外已经非常流行,但在国内的普及率并不如MySQL。
|
||||
|
||||
关于它们的比较和使用推荐,你可以参考这篇博文《PostgreSQL vs. MySQL: What you need to know》,文中指出:
|
||||
|
||||
|
||||
Consider PostgreSQL for any application that might grow to enterprise scope, with complex queries and frequent write operations. If you’re new to the world of databases and don’t expect your application to scale up, or you’re looking for a quick tool for prototyping, then consider MySQL.
|
||||
|
||||
|
||||
你不用非要看懂,简单来说,其实就是如果你是刚迈入数据库领域的新手,或者应用程序规模不大,那么MySQL比较合适,否则的话推荐使用PostgreSQL。另外要补充的是,如果你用的是云厂商的RDS数据库,在你选择存储引擎的时候,一定要注意规格和价格,因为适合的才是最好的。
|
||||
|
||||
最后,在元数据的存储上,有的Serverless函数计算平台,考虑到以函数为轴心的存储,也会选用类似Mongodb的文档数据库来存储。不过,从CRUD和实体管理的角度来看,我比较推荐你使用关系型数据库来管控元数据。
|
||||
|
||||
代码包
|
||||
|
||||
接下来,我们来看代码包的存储设计。代码包一般有两种形态,一种是基于云厂商代码框架编写的函数打包成ZIP、WAR、JAR包等形式存在,一种以自定义镜像的形式存在。
|
||||
|
||||
|
||||
传统压缩包
|
||||
|
||||
|
||||
针对第一种格式的代码包,相信经过这一段时间对云产品的了解,在你的脑海中立马就能显示出“对象存储”的选择。
|
||||
|
||||
通常我们提到对数据的存储,一般会想到文件、块、对象三种不同的方式。它们之间的差别我也在这里列出来了:
|
||||
|
||||
|
||||
|
||||
考虑到函数计算的特点,资源实例因为是无状态的,所以如果采用分布式文件系统,每次都需要进行挂载操作,非常麻烦。块存储也有同样的问题,并且需要自己实现对块的操作。
|
||||
|
||||
而对象存储是支持HTTP访问的,并不需要进行挂载操作,并且KV的特性也会让代码包路径具有很高的可读性,虽然在表格上也提到了对象存储是无法直接对对象内容进行修改的,但是对于函数代码包而言,通常都是读多写少,因此,对象存储非常适合用来保存用户的代码。
|
||||
|
||||
另外,因为对象存储是通过键值对的方式保存每一个对象的,所以,为每一个函数代码包设计一个Key的生成规则就显得尤为重要了。
|
||||
|
||||
比如,可以将用户信息与代码信息关联:
|
||||
|
||||
userId/hash(codeFile)
|
||||
|
||||
|
||||
这样,在上传代码包时,你只需要将函数代码包的key作为函数的一个属性存入数据库,然后再单独将代码包根据key存入对象存储服务中就可以了。而在代码发生变更时,因为代码包和哈希编码也会改变,因此需要做到和数据库的元信息同步。
|
||||
|
||||
在启动函数实例时,再通过对象存储的API根据key值将代码包下载下来,这样就完成了代码包的获取。
|
||||
|
||||
|
||||
自定义镜像
|
||||
|
||||
|
||||
讲到“镜像”,我们很容易对应上“镜像仓库”的概念。是的,我们设计一个平台或者一个系统的时候,尽可能地依托一些现有的基础服务,是进一步加快和加固我们服务构建的关键因素。
|
||||
|
||||
其实不止是函数代码的自定义镜像文件,函数计算平台本身的服务也会以镜像的形式存在,这些都可以存储在镜像仓库中。
|
||||
|
||||
那么,选择一个合适的容器镜像服务就是非常有必要的。并且在你创建镜像仓库的时候也要记得选择私有的限制以确保安全性。目前云厂商提供的容器镜像服务不光支持镜像仓库的托管,通常还会支持像镜像安全扫描以及多地域镜像加速等高级能力。
|
||||
|
||||
国内主流的公有云厂商都有相关的容器镜像服务,像阿里云的ACR、腾讯云的TCR、华为云SWR以及百度云的CCR目前都做得比较成熟了,你可以综合价格、使用地域等多方面来进行选择。
|
||||
|
||||
数据面的存储
|
||||
|
||||
接下来,我们来看看数据面的存储设计和访问。在第六节课中,我跟你讲到了流量进来后的调度过程,这里,我们可以提取出来几个和数据获取相关的要点。
|
||||
|
||||
|
||||
元信息的获取:生产级别的函数计算平台通常会将控制面的数据管控能力和调度能力进行架构拆分,那么,流量的调度服务就需要通过一个微服务(如APIServer)来获取函数信息。这里为了提高并发和性能,我们可以考虑通过缓存的方式来保存不经常变更的元数据;
|
||||
资源的占用:我们知道,冷启动实际上是占用一个Cold Pod的调度过程,由于生产级别的资源控制模块通常采用分布式方式部署,如果两个资源控制模块分别根据不同的请求去占用同一个Cold Pod,那么到底是谁获取这个Pod呢?又如何保证不会被重复获取呢?这就涉及到锁定的问题。
|
||||
并发排序:函数的实例Pod是可以支持多个并发操作的,这种情况下,我们如何均衡调度呢?比如如何知道哪个Pod调度的并发多,哪个目前处理处于空闲?这就涉及到排序和调度的问题。
|
||||
|
||||
|
||||
除此之外,在异步场景下,还要看重复、丢失等问题。我们会发现,这些问题其实都可以通过分布式缓存中间件来解决,比如Redis。它可以解决掉上述的性能与并发问题,资源加锁的问题,以及排序、去重等问题。
|
||||
|
||||
我们看一个跟函数场景比较相关的Pod排序问题。你可以通过Redis的ZSET数据结构,用key记录函数唯一标识,用member记录关联该函数的warm pod, 用score记录当前warm pod 的并发情况。
|
||||
|
||||
key:函数唯一标识
|
||||
member:pod的ip
|
||||
score:并发度
|
||||
|
||||
|
||||
这样我们就可以将函数、资源与请求三者关联起来。当出现多个资源控制模块占用同一资源时,抢到Cold Pod的会先将这个Pod根据key写入对应的ZSET中,这样,其他资源控制模块就无法占用了。而score 因为记录了Pod的当前并发,我们也可以根据score的数值来判断Pod并发的程度以及是否空闲。
|
||||
|
||||
这些都可以通过Redis这样的缓存中间件来完成,缓存中间件中的数据也可以预加载,我们在Console操作的时候,就可以提前加载到Redis中。
|
||||
|
||||
接下来,我们再来看贯穿系统运行始终的数据:日志和服务状态。
|
||||
|
||||
日志
|
||||
|
||||
日志对你来说应该已经非常熟悉了。在函数计算中,比较重要的是执行阶段产生的运行日志如何设计和存储。这关系到整个系统问题的排查,链路追踪的可观测能力,以及报表统计等能力。
|
||||
|
||||
关于可观测的部分,我们在上一节已经详细介绍了。这里,我们来说一下如何利用日志生成平台的报表数据,比如平台一天的调用量、某个函数一天消耗的资源量等等。
|
||||
|
||||
报表虽然是源于日志的采集和汇聚,但和日志本身的存储还是有明显区别的:
|
||||
|
||||
|
||||
报表数据只需要一些关键指标,比如函数的调用次数、资源的使用以及调用时间等,而像一些辅助节点的日志信息,其实我们并不关心;
|
||||
报表数据的存储时间比较长,执行日志有可能只需要保存1-2周或者1个月,一般都会定期清理日志,而报表和营收挂钩,通常保存时间都是以年为单位。
|
||||
|
||||
|
||||
针对报表的存储,这里我推荐使用Apache Doris,它最早是解决百度凤巢统计报表的专用系统,经过多轮迭代构建成基于MPP 架构的云数据仓库,并开源贡献给了Apache基金。所以,从技术沉淀、业务打磨上来说Doris绝对是你的首选。
|
||||
|
||||
Doris不仅兼容 MySQL 协议,并且使用标准 SQL,只是这个特点,就足以让你快速介入了。另外,它的导入和周边生态也非常丰富,比如我们日志数据清洗时用到的Logstash,就可以作为Doris的输入,这样我们的报表就可以通过Logstash筛选出关键的信息,保存到Doris中。
|
||||
|
||||
当然,你也可以根据所在团队的历史或者业务的基础设施情况,选择ClickHouse、GreenPlum、或者自研引擎的OLAP数据库。由于底层存储系统的复杂性和运维难度,选择一个运维团队合适的存储底座或者云厂商的一个数据库产品,也是一种不错的方案。
|
||||
|
||||
服务状态
|
||||
|
||||
另外,在函数执行过程中涉及到的服务还需要考虑到高可用的部署,这时候,服务状态也需要记录下来。
|
||||
|
||||
比如像离线的扩缩容模块,往往会采用主备的方式进行部署,这就涉及到了选主问题。另外像一些服务可能还需要横向扩容,则需要用到服务注册与发现的能力。
|
||||
|
||||
这一类问题最常见的解决方案就是使用一个可以同步服务状态的注册中心,来处理不同服务模块的选主、通知、协调以及消息发布等问题。
|
||||
|
||||
在云原生时代的今天,我推荐你使用Etcd。它天生具有分布式协同源语(如事件Watch、租约、选举和分布式共享锁等),对自己的定位也是云计算的基础设施。很多上层系统,如Kubernetes、CloudFoundry、Mesos都使用了Etcd。同时,Google、阿里巴巴等多个大型互联网公司也都在广泛使用,可以称得上是大中型集群管理系统的重要解决方案。
|
||||
|
||||
关于Etcd的技术和特性比较,官网也给出了具体与ZooKeeper、Consul、NewSQL的对比,重点从并发原语、线性读、多并发版本控制等10个维度来对比。
|
||||
|
||||
|
||||
|
||||
简单来说,Etcd和Zookeeper解决了相同的问题,也就是分布式系统的协同和元数据的存储,至于它们两者的差异,是设计理念的不同造成的。Etcd相对ZooKeeper“更年轻更轻量”,它针对ZooKeeper进行了一些如租约、MVCC等方面的改进。出于稳定性、新特性上的考虑,如果是新的应用,我更推荐你用Etcd。
|
||||
|
||||
Consul的优势在于服务发现,它提供内置的健康检查、故障检测和DNS服务。它和Etcd解决的问题侧重点不一样,如果是为了解决分布式系统一致性key-value存储的话,我那么推荐使用Etcd,但如果是端到端的服务发现,那么Consul要更好一些。
|
||||
|
||||
NewSQL更适合于存储GB级别的数据或者需要完整SQL查询能力的场景。
|
||||
|
||||
小结
|
||||
|
||||
最后,我来小结一下我们今天的内容。这节课我从控制面和数据面两个维度给你介绍了FaaS形态Serverless平台的存储设计思路和资源选型。
|
||||
|
||||
我们从中了解了不同的数据实体和运行阶段选型的一些方法和思考。我们需要从数据的特性、企业的基础设施、开发人员的熟悉成本、业务的量级等角度来做一个合理的选择,而不是单单从存储资源的技术指标上来衡量。
|
||||
|
||||
正如我们在日志的存储环节中跟你提到的那样,如果我们自己运维Doris的成本比较大,那“借力打力”,选择原本团队就在使用的数据库系统,是不是也可以呢?做平台也好,做业务也好,ROI也是我们要考虑的因素。
|
||||
|
||||
今天我们谈到的控制面的关系型数据库,如果从另一个角度出发,以函数为Key存储是不是也可以?是的,选择类似一个KV的存储也可以,就看架构师怎么衡量系统实现的复杂度和便利性,这种衡量依赖于业务和系统发展的阶段不同而变化。
|
||||
|
||||
所以,今天提到的MySQL、PostgreSQL、Redis、Doris、Etcd等,都是我们思考的一个具象化的表达而已。我们更关心的,应该是构建一个系统的思维。
|
||||
|
||||
课后作业
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个小作业。
|
||||
|
||||
根据本节课的了解,你可以想一想如果让你设计一个Serverless平台,还有哪些数据在本文中没提到,你打算如何存储?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起交流学习。
|
||||
|
||||
延伸阅读
|
||||
|
||||
(Etcd,Concurrency Primitives):链接1,链接2,链接3,链接4,链接5
|
||||
|
||||
(ZooKeeper,Concurrency Primitives):链接6
|
||||
|
||||
(Consul,Concurrency Primitives):链接7
|
||||
|
||||
(NewSQL,Concurrency Primitives):链接8
|
||||
|
||||
(Etcd,Linearizable Reads):链接9
|
||||
|
||||
(Consul,Linearizable Reads):链接10
|
||||
|
||||
(Etcd,Multi-version Concurrency Control):链接11
|
||||
|
||||
(Etcd,Transactions):链接12
|
||||
|
||||
(ZooKeeper,Transactions):链接13
|
||||
|
||||
(Consul,Transactions):链接14
|
||||
|
||||
(Etcd,Change Notification):链接15
|
||||
|
||||
(ZooKeeper,Change Notification):链接16
|
||||
|
||||
(Consul,Change Notification):链接17
|
||||
|
||||
(Etcd,User permissions):链接18
|
||||
|
||||
(ZooKeeper,User permissions):链接19
|
||||
|
||||
(Consul,User permissions):链接20
|
||||
|
||||
(NewSQL,User permissions):链接21、链接22
|
||||
|
||||
(Etcd,HTTP/JSON API):链接23
|
||||
|
||||
(Consul,HTTP/JSON API):链接24
|
||||
|
||||
(Etcd,Membership Reconfiguration):链接25
|
||||
|
||||
(ZooKeeper,Membership Reconfiguration):链接26
|
||||
|
||||
(Consul,Membership Reconfiguration):链接27
|
||||
|
||||
|
||||
|
||||
|
308
专栏/Serverless进阶实战课/16动手体验(一):如何实现业务高效率地开发上线?.md
Normal file
308
专栏/Serverless进阶实战课/16动手体验(一):如何实现业务高效率地开发上线?.md
Normal file
@ -0,0 +1,308 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
16 动手体验(一):如何实现业务高效率地开发上线?
|
||||
你好,我是静远。
|
||||
|
||||
在前面的课程中,我不止一次提到,在Serverless的产品形态中,你只需要关注业务的逻辑,无需管理复杂的基础设施,可以快速地实现应用产品的交付和试错。
|
||||
|
||||
但如果你刚接触Serverless,使用起来应该会时常磕磕碰碰,主要体现在熟悉开发形式的转变、掌握对框架语法、理解对集成服务等多个方面,相信你在前面的学习和实践中也有过这样的感受。
|
||||
|
||||
其实,在Serverless的应用领域,已经有了不少常用场景的模板,来提升你使用的便捷度了。不知道你有没有用起来过呢?
|
||||
|
||||
今天,我将带你了解基于“模板”的理念,实现业务快速上线的方式。
|
||||
|
||||
希望通过这节课程,让你从实战层面体会到Serverless形态技术在“提质增效、快速交付”上面的优势。
|
||||
|
||||
Serverless的模板是什么?
|
||||
|
||||
当你在控制台或者开发工具选择使用模板后,通常会在WebIDE或者你的本地编辑器生成一段模板代码,然后你再根据实际的需要去更改模板中代码,最后再按照代码包的处理方式将这段更改后的模板打包上传,就完成了一个云函数的部署。
|
||||
|
||||
有的比较复杂一点的场景,会包含多个函数模板,组成一个应用级别的模板。而我们又可以通过编排多个函数进行,来满足我们在12课中提到的工作流场景,我们在课程中知道,工作流的核心之一是“编排结构”,这个JSON或者YAML结构可以沉淀为这一业务场景下的通用模板。
|
||||
|
||||
也就是说,Serverless下的模板,从单个函数模板到多个函数组成的应用模板,函数和应用编排下的工作流模板都存在,这些模板会给你带来快捷编码、连接上下游配置的能力。
|
||||
|
||||
下面我们一起来看看怎么把模版用起来吧。
|
||||
|
||||
基于函数模板的转码实战
|
||||
|
||||
在我们常见的业务中,会为了适配各种终端和网络条件,将原来的视频转码成各种格式。那么接下来,我就带着你利用函数模板,一起实现一个音视频转码的功能,加深对模板核心能力的体会。
|
||||
|
||||
搭建
|
||||
|
||||
我以阿里云函数计算FC作为本次练习的平台。
|
||||
|
||||
首先我们进入“应用中心”,点击“创建应用”,会看到很多的应用模板展示,这些模板是平台根据用户的使用经验来提供的,像函数计算常见的Web应用、文件处理、音视频转码等场景,你都可以在这里面找到。今天,我们选择“音视频处理”用例中的“音视频转码job”。
|
||||
|
||||
|
||||
|
||||
接着,我们点击“直接创建”,进入“创建应用”页面,填写设置“部署类型”“应用名称”“RAM角色”等,点击创建,就完成了一个音视频转码模板应用的构建。
|
||||
|
||||
|
||||
|
||||
接下来,应用会进行一系列资源的检查,如果你是第一次创建,还需要创建和绑定RAM角色,云厂商基本上都会有这一步的操作。
|
||||
|
||||
创建完成后,我们点击进入具体的应用详情页,你会发现音视频的应用包含了多个函数的组成,包括dest-fail、dest-succ、transcode三个函数。
|
||||
|
||||
|
||||
|
||||
相信你通过他们的函数名也能猜出具体功能,dest-fail和dest-succ分别对应了转码失败或成功的操作逻辑。比如转码失败的情况下,你可能需要添加一些通知操作,也可以根据实际的需求通过编码实现。
|
||||
|
||||
这个案例中的核心函数是音视频转码的模板函数transcode。它具体做了什么呢?我们来看下入口函数handler的实现:
|
||||
|
||||
def handler(event, context):
|
||||
LOGGER.info(event)
|
||||
evt = json.loads(event)
|
||||
oss_bucket_name = evt["bucket"]
|
||||
object_key = evt["object"]
|
||||
output_dir = evt["output_dir"]
|
||||
dst_format = evt['dst_format']
|
||||
shortname, _ = get_fileNameExt(object_key)
|
||||
creds = context.credentials
|
||||
auth = oss2.StsAuth(creds.accessKeyId,
|
||||
creds.accessKeySecret, creds.securityToken)
|
||||
oss_client = oss2.Bucket(auth, 'oss-%s-internal.aliyuncs.com' %
|
||||
context.region, oss_bucket_name)
|
||||
|
||||
|
||||
# simplifiedmeta = oss_client.get_object_meta(object_key)
|
||||
# size = float(simplifiedmeta.headers['Content-Length'])
|
||||
# M_size = round(size / 1024.0 / 1024.0, 2)
|
||||
|
||||
|
||||
input_path = oss_client.sign_url('GET', object_key, 6 * 3600)
|
||||
# m3u8 特殊处理
|
||||
rid = context.request_id
|
||||
if dst_format == "m3u8":
|
||||
return handle_m3u8(rid, oss_client, input_path, shortname, output_dir)
|
||||
else:
|
||||
return handle_common(rid, oss_client, input_path, shortname, output_dir, dst_format)
|
||||
|
||||
|
||||
handler首先会从event中获取bucket、object、output_dir以及dst_format等几个字段,它们分别表示什么呢?
|
||||
|
||||
|
||||
bucket:视频所在对象存储的bucket名称;
|
||||
object:视频在对象存储中的objectName;
|
||||
output_dir:转码后的视频需要存放在对象存储中的路径;
|
||||
dst_format:需要转成的视频格式。
|
||||
|
||||
|
||||
handler会根据认证信息创建OSS客户端对象、生成带签名的OSS文件的URL。
|
||||
|
||||
接着,根据转换格式对视频进行转换,如果目标格式是m3u8,就会进行特殊处理,否则函数会按照通用格式进行转码。
|
||||
|
||||
我们选择其中一个转码功能的代码分支handle_common,来看一下它的具体实现:
|
||||
|
||||
def handle_common(request_id, oss_client, input_path, shortname, output_dir, dst_format):
|
||||
transcoded_filepath = os.path.join('/tmp', shortname + '.' + dst_format)
|
||||
if os.path.exists(transcoded_filepath):
|
||||
os.remove(transcoded_filepath)
|
||||
cmd = ["ffmpeg", "-y", "-i", , transcoded_filepath]
|
||||
try:
|
||||
subprocess.run(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
|
||||
|
||||
|
||||
oss_client.put_object_from_file(
|
||||
os.path.join(output_dir, shortname + '.' + dst_format), transcoded_filepath)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
# if transcode fail,trigger invoke dest-fail function
|
||||
raise Exception(request_id +
|
||||
" transcode failure, detail: " + str(exc))
|
||||
finally:
|
||||
if os.path.exists(transcoded_filepath):
|
||||
os.remove(transcoded_filepath)
|
||||
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
你会发现,其实在handle_common中,主要封装了ffmpeg,程序通过调用ffmpeg的执行转码命令转码音视频文件,最后将转码后的文件通过OSS的客户端写回到对象存储中。
|
||||
|
||||
到这里,我们就基于应用函数模板创建了一个音视频转码的“模子”,也理解了其中的实现机制。下面,我来带你配置特定的业务参数,让它能够执行起来,处理具体的业务。
|
||||
|
||||
执行
|
||||
|
||||
我们首先在函数计算的同地域,通过对象存储OSS建立一个bucket,然后上传一个视频到该bucket中。这个过程不难理解,你可以参考OSS的使用手册快速实现。
|
||||
|
||||
比如,我创建的bucket是“geekbangbucket”,上传的视频为“demo.mp4”。
|
||||
|
||||
|
||||
|
||||
然后,我们需要通过函数的测试事件来验证音视频的转码是否可以运行。在点击函数测试之前,我们还需要配置一些参数,以便transcode知道从哪里读取,转成什么格式的文件,输出到哪里:
|
||||
|
||||
{
|
||||
"bucket": "geekbangbucket", // 你在OSS上创建的bucket名称
|
||||
"object": "demo.mp4", // 你上传的音视频名称
|
||||
"output_dir": "result", // 你希望将转码后的视频存储的目录
|
||||
"dst_format" : "mov" // 你希望转码后的视频格式
|
||||
}
|
||||
|
||||
|
||||
这些参数,就是我们在搭建阶段分析的transcode的入参,它们会通过event对象传入。
|
||||
|
||||
最后,我们点击“测试函数”,在函数执行成功后,我们就可以去OSS上查看result目录下是否生成demo.mov了。
|
||||
|
||||
|
||||
|
||||
这样,我们就完成了一个具有视频转码功能的云函数的开发。你在实际的业务工作中,也可以通过设置OSS触发器的方式自动触发执行音视频转码函数。这样一遍操作下来,是不是比你平时的开发要高效多了呢?
|
||||
|
||||
音视频转码进阶
|
||||
|
||||
现在,我们已经能够满足一般的业务了,但有的时候,我们也需要同时实现多个音视频的转码。那么,如何基于上述函数模板的能力,快速实现并行的转码处理服务呢?
|
||||
|
||||
在开始之前,你可以先回想一下,在编排这节课,我们提到的工作流是不是正好可以解决这个场景?
|
||||
|
||||
整体设计
|
||||
|
||||
在编排小节中,我给你介绍过工作流的并行步骤,它包含了多个分支,在运行时,这些分支会并行执行。你可以将每一个格式的转码任务对应一个并行分支,然后通过工作流提供的输入映射,将目标格式传入到转换任务中。
|
||||
|
||||
考虑到视频转码任务在实际的业务处理中会批量地运行,你还可以使用循环的能力来处理。
|
||||
|
||||
结合这个思路,我给你提供了一张工作流的执行示意图,让你理解得更加清晰一些:
|
||||
|
||||
|
||||
|
||||
这里,我为了使模拟的转码场景更贴近真实业务,假设的是需要转码成avi、mov、flv三种格式,并在转码任务之后添加了一个子任务,通过它通知对应的平台或者播放终端。因此,你看到的子任务中包含了两个task,一个是转码任务,另一个是转码后的收尾任务。
|
||||
|
||||
具体实现
|
||||
|
||||
虽然每个分支对应了一种格式的转换任务,但实际上在你前面实现的函数中,也可以通过传参的方式控制不同格式的转换。所以,这些分支中的task 都可以使用你前面构建的云函数来完成。
|
||||
|
||||
我以avi为例做个演示。
|
||||
|
||||
正如整体设计中所说的,我们首先需要定义一个parallel步骤,来控制多个并行任务的处理,在我的yaml中,对应的是parallel_for_videos。
|
||||
|
||||
steps:
|
||||
- type: parallel
|
||||
name: parallel_for_videos
|
||||
branches:
|
||||
|
||||
|
||||
接下来,我们可以在branches下配置并行的分支任务。由于我们这边设置的是循环foreach步骤,所以在yaml中创建的是循环foreach_to_avi,如下所示:
|
||||
|
||||
- steps:
|
||||
- type: foreach
|
||||
name: foreach_to_avi
|
||||
iterationMapping:
|
||||
collection: $.videos
|
||||
item: video
|
||||
inputMappings:
|
||||
- target: videos
|
||||
source: $input.videos
|
||||
|
||||
|
||||
需要注意的是,foreach步骤中需要你定义好迭代的变量,即yaml中的iterationMapping,后续的子步骤也会用item定义字段作为输入变量(也就是video)。我这里的输入是将待转码信息定义在videos数组中,你也可以按照自己喜欢的方式定义。
|
||||
|
||||
接下来,我们再来定义子步骤。子步骤包含转码和通知两个task,我们先看转码任务。转码任务需要用到你前面创建的音视频转码函数,也就是resourceArn字段(如果你不记得它的使用,可以用工作流定义语言任务步骤)。
|
||||
|
||||
另外在工作流中,因为是通过inputMappings来定义函数的传参,因此,输入就需要和函数中定义的事件字段保持一致了。可以看到,我这里仍然需要传dst_format、output_dir、bucket、object这几个参数。下面是转码任务的task,我定义为avi_transcode_task。
|
||||
|
||||
steps:
|
||||
- type: task
|
||||
name: avi_transcode_task
|
||||
resourceArn: acs:fc:*******:services/VideoTranscoder-t6su/functions/transcode
|
||||
inputMappings:
|
||||
- target: dst_format
|
||||
source: avi
|
||||
- target: output_dir
|
||||
source: avi_videos
|
||||
- target: bucket
|
||||
source: $input.video.bucket
|
||||
- target: object
|
||||
source: $input.video.object
|
||||
|
||||
|
||||
完成转码task的定义后,我们继续定义另一个task任务,我这里定义为avi_inform_task。
|
||||
|
||||
- type: task
|
||||
name: avi_inform_task
|
||||
resourceArn: acs:fc:*******:services/VideoTranscoder-54jr.LATEST/functions/dest-succ
|
||||
inputMappings:
|
||||
- target: platform
|
||||
source: avi_platform
|
||||
|
||||
|
||||
这样我们就完成了一个分支的定义。这里,我也列出了完整的yaml分支给你参考,相信接下来,你也可以举一反三地实现另外两个分支的yaml定义了。
|
||||
|
||||
- steps:
|
||||
- type: foreach
|
||||
name: foreach_to_avi
|
||||
iterationMapping:
|
||||
collection: $.videos
|
||||
item: video
|
||||
inputMappings:
|
||||
- target: videos
|
||||
source: $input.videos
|
||||
steps:
|
||||
- type: task
|
||||
name: avi_transcode_task
|
||||
resourceArn: acs:fc:cn-hangzhou:*******:services/VideoTranscoder-54jr.LATEST/functions/transcode
|
||||
inputMappings:
|
||||
- target: dst_format
|
||||
source: avi
|
||||
- target: output_dir
|
||||
source: avi_videos
|
||||
- target: bucket
|
||||
source: $input.video.bucket
|
||||
- target: object
|
||||
source: $input.video.object
|
||||
- type: task
|
||||
name: avi_inform_task
|
||||
resourceArn: acs:fc:cn-hangzhou:*******:services/VideoTranscoder-54jr.LATEST/functions/dest-succ
|
||||
inputMappings:
|
||||
- target: platform
|
||||
source: avi_platform
|
||||
|
||||
|
||||
定义完工作流的yaml后,我们来验证一下,这时还需要配置一下工作流的测试参数,才能点击执行。
|
||||
|
||||
我的输入还是使用的前面用到的视频demo,因为我们在工作流中的每个分支定义好了output_dir和dst_format,因此这两个参数并不需要传入。而前面我也说过foreach的输入需要定义在videos这个数组中,所以,我的输入如下:
|
||||
|
||||
{
|
||||
"videos": [
|
||||
{"bucket": "geekbangbucket","object": "demo.mp4"}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
然后我们点击执行,可以在状态栏下看到执行成功的反馈了。
|
||||
|
||||
|
||||
|
||||
最后,我们还可以到OSS中检查一下看看是否转码成功。如果转码成功,你一定可以在OSS中看到分别生成的三个不同的目标目录。
|
||||
|
||||
|
||||
|
||||
你可以结合实际业务场景,选择通过API或者定时调度的方式来启动工作流。这样,一个具备多种格式并行转码功能的工作流就完成了。
|
||||
|
||||
此时,你可以将YAML文件保存下来,共享给你的其他同学,他只需要改动几个参数,比如resourceARN、自己的入口Bucket等变量,就可以实现跟你一样的函数编排功能了。
|
||||
|
||||
这种保存下来的YAML文件同样可以作为模板存在。而我们有的云厂商为了提升开发者的效率,也在函数编排的产品化中实现了这样的模板能力。
|
||||
|
||||
小结
|
||||
|
||||
最后,我来小结一下我们今天的内容。今天我跟你从如何实现业务高效率地开发上线的视角,以Serverless下的模板为基石,和你一起体验了一把Serverless“提质增效、快速交付”的优势。
|
||||
|
||||
对于一个刚步入Serverless领域的新手来说,使用模板来构建自身的业务应用,无疑是一个捷径。我们可以根据业务的复杂程度来选择使用单函数的模板,还是结合多个函数应用形态的模板。
|
||||
|
||||
面对更复杂的场景,我们还可以发挥工作流编排的优势,将函数与其他的云服务(如文件存储、对象存储、日志服务)结合起来,迅速地完成一个复杂应用系统的搭建。
|
||||
|
||||
更进一步的,我们还可以将这样的编排YAML文件沉淀下来,供身边的同学使用,让模版的能力得到真正的体现。
|
||||
|
||||
课后作业
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个延伸作业。
|
||||
|
||||
这节课,我们一直是通过主动调用的方式进行实验,那通过触发器的方式能不能实现呢?快动动你的小手试试吧。
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。
|
||||
|
||||
感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起交流学习。
|
||||
|
||||
|
||||
|
||||
|
245
专栏/Serverless进阶实战课/17动手体验(二):如何在云函数场景下实现一个有状态的服务?.md
Normal file
245
专栏/Serverless进阶实战课/17动手体验(二):如何在云函数场景下实现一个有状态的服务?.md
Normal file
@ -0,0 +1,245 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
17 动手体验(二):如何在云函数场景下实现一个有状态的服务?
|
||||
你好,我是静远。
|
||||
|
||||
今天我们一起动手体验有状态存储的实现方法。
|
||||
|
||||
FaaS作为一种典型的无状态服务,虽然为应用的开发、运行和管理提供了重要的Serverless基础,但服务并不是孤立存在的,一个完整的应用系统往往需要依赖不少第三方的服务,比如认证、存储、消息队列等。
|
||||
|
||||
另一方面,随着微服务理念和架构的盛行,业务功能和人员分工越来越精细化,我们在工作中,不可能在一个单体应用下实现很多的功能,那么协作就显得尤为重要了。而协作的关键,就在于这些第三方介质能够存储事物状态的特性。
|
||||
|
||||
我们从第一节课开始,就在以FaaS视角探究Serverless领域的技术和实践。今天,我会带你一起了解云函数和有状态背后的故事,并带你体验Serverless = FaaS + BaaS 这一理念的运用,实现一个广告物料巡检的有状态服务的设计体验。
|
||||
|
||||
FaaS的局限性
|
||||
|
||||
FaaS一个比较明显的特点是:应用程序的颗粒度不再是集众多业务功能于一身的集合体,而是一个个细粒度的函数(Function),每个函数完成一个具象化的业务逻辑。函数由事件驱动执行,由业务处理结束。
|
||||
|
||||
这就带来了一个明显的问题:如果需要处理比较复杂的功能,如微服务中多线程的数据共享、状态驱动的组合能力处理、会话保持等,那么FaaS就会显得力不从心。
|
||||
|
||||
但是,这并不代表FaaS下的Serverless架构就不能处理。你可以先想一想,我们有多少方法可以来解决这一问题,带着这个问题,我们再开始下面的内容。
|
||||
|
||||
函数的无限可能
|
||||
|
||||
刚才我们讲到函数计算的基本组成单位是“函数(Function)”,虽然FaaS本身有其局限性,但“函数”本身是强大的。为什么这么说呢?
|
||||
|
||||
相信你一定玩过积木。其实,函数就像乐高中的一块块积木一样,它可以作为FaaS的产品存在其中,也可以作为微服务模块中的一个函数嵌套功能而存在,只是颗粒度的大小不同。
|
||||
|
||||
函数本身就是随着服务架构的发展出现的产物,从单体应用到微服务的发展,就是从功能上进行了横向和纵向的切分;从微服务到函数的实现方式,也是整个形态和效率意识上的升华。
|
||||
|
||||
|
||||
|
||||
为此,函数同样作为Serverless的基石而存在。你可以在Serverless的框架应用上,采用函数框架的形态来构建,比如Google和百度的图谱搜索卡片,完全可以用函数来构建那一小块的卡片功能,那么,搜索的功能就可以基于Serverless存在,随着流量的大和小而弹升弹降。
|
||||
|
||||
这也就说明了一个重要的点:函数是实现业务的一个具体因子,你可以将这个因子用于FaaS的构建,也可以用于其他Serverless产品的构建。有了这个认知,我们来看基于函数形态下,有状态的两个方向的实现。
|
||||
|
||||
有状态函数服务的两个方向
|
||||
|
||||
所谓的“有状态”和“无状态”,其实是针对服务架构来说的。维基百科将计算机中有状态的系统定义为:需要记住之前的事件或用户交互的系统。而这些状态,通常以循环使用的数据、共享的地址等形式存在。比如机器学习中的数据过程、批处理中消息传递的阶段、交互访问中会话的保持,都属于这个范畴。
|
||||
|
||||
回到函数的计算中来看,针对有状态的场景,函数的处理目前一般有两种方法。
|
||||
|
||||
一种是有状态的函数编程模型。以华为云函数计算为代表,主要通过context字段记录函数的状态。函数执行的时候,会加载context字段保存的内容。同时也会通过settimeout来设置老化时间,如果在老化超时时间内状态没有更新,就会把该状态删掉。
|
||||
|
||||
这种有状态服务可以不依赖外部模块,因此在响应速度上,会有很大的优势。这种有状态的函数模型主要用于四类场景。
|
||||
|
||||
|
||||
大规模分布式机器学习中的迭代计算场景:这种模式的参数可能达到十亿至万亿个。使用有状态函数服务,可以节省参数的存储空间和网络通信的开销;
|
||||
大数据计算场景:使用有状态函数服务,可以避免计算过程中与外部介质的I/O,大大降低延时;
|
||||
实时交互型场景:例如多人对战游戏,对延时要求很高,通过对游戏状态的内存化和本地化操作,就可以极大的降低时延;
|
||||
多人协作场景:如在线编辑文档,这种可以依靠用户的Session分发到同一个实例上来处理。
|
||||
|
||||
|
||||
总的来说,有状态函数服务的选型,需要考虑到计算与数据的优先关系以及选取哪些数据作为过程状态。
|
||||
|
||||
不过,它存储的状态信息有限,如果涉及到上下游服务的依赖和持久化存储,例如高并发的日志消息、视频/图片审核等,仅依靠函数状态context是无法实现的,也就不太适用了。
|
||||
|
||||
FaaS是需要和消息队列和对象存储这些服务上下游交互的。因此,FaaS经常需要结合BaaS服务共同实现Serverless化的能力,也就是说,用户的请求还需要依赖于其他持久化模块。
|
||||
|
||||
这就是我要提的另一种方式:典型的FaaS + BaaS的组合,FaaS提供业务逻辑层的处理,BaaS提供基础服务层的能力,让有状态的数据、过程消息等存储在BaaS化的服务中。
|
||||
|
||||
根据有状态函数的需求不同,我们往往会选择不同的BaaS服务,常用的有Serverless数据库、Serverless消息队列等。
|
||||
|
||||
先说Serverless数据库。业界内知名的Serverless数据库有阿里云的PolarDB Serverless、腾讯云的PostgreSQL for Serverless、亚马逊的Amazon Aurora Serverless。Serverless数据库会将计算能力和存储资源解耦,根据业务需求合理分配CPU和Memory资源。
|
||||
|
||||
对于内存数据库而言,需要大量的Memory资源,但CPU资源使用量很少;对于事务操作很多的数据库而言,资源需求恰好相反。Serverless数据库可以大幅降低成本,适合多样化的业务场景。
|
||||
|
||||
再说Serverless消息队列。它支撑着函数计算的异步执行,消息缓冲、消息解耦能力,业界内常用的有Apache Pulsar等。
|
||||
|
||||
如思维构建所述,正是由FaaS和BaaS的结合,才真正完整的实现了架构的Serverless化。BaaS弥补了FaaS的无状态性,也就是说,Serverless=FaaS+BaaS。
|
||||
|
||||
一个典型的案例,就是我们在上传图片或视频到云端对象存储时,经常需要对内容和格式执行一些操作:将涉黄的图片/视频进行标记或者删除、修改图片格式、添加/去掉水印等。
|
||||
|
||||
这时,我们就可以将处理的逻辑封装成函数并部署在FaaS平台,随后创建对象存储触发器。当云端对象处检测到用户上传图片/视频的事件时,就会触发函数计算来执行处理图片/视频的函数,处理完成之后会保存在对象存储中,这里的对象存储就是BaaS,上层的业务逻辑处理就是FaaS。
|
||||
|
||||
通过这样的实现方式,也就实现了计算和存储分离,计算层面是无状态的,可以随着流量进行扩缩容函数实例,同时由于BaaS化的存储,确保流量应对的同时,也保证了数据的安全和故障恢复能力。
|
||||
|
||||
|
||||
|
||||
看完这个案例,你会感觉在云函数场景下,实现一个有状态的服务还是比较容易的。这也是函数计算这几年越来越普及的原因之一。
|
||||
|
||||
动手体验
|
||||
|
||||
接下来,我们就通过一个案例来动手实操,探究如何在云函数场景下实现一个有状态的服务。
|
||||
|
||||
在我们日常搜索的网页中,经常会看到各种各样的广告物料,然而,这些物料可能会混入非法的广告文案,也可能会存在URL二跳等问题,因此,我们要巡检广告物料,以确保其合法性。
|
||||
|
||||
针对这样一个事情,我们通常的做法就是启动一定量的巡检程序去一遍一遍地扫描广告库的物料,但是,如果遇到节假日,广告物料会激增,或者隔三差五就有一批很大的物料量,那么,我们怎么能够保证快速的扫描完成呢?
|
||||
|
||||
如何抉择?
|
||||
|
||||
这是一个非常典型的场景,类似的有文章内容的审核、交易对账等。我们来分析一下这类场景的特点:
|
||||
|
||||
第一,不是每时每刻的流量都很均匀,时大时小,存在明显的峰值和峰谷;
|
||||
|
||||
第二,对延时不是很敏感,不需要毫秒级响应;
|
||||
|
||||
第三,有状态,需要对处理过的内容打上标记;
|
||||
|
||||
第四,处理起来不复杂,有明确的规则。
|
||||
|
||||
这正符合用FaaS形态的Serverless的技术来解决,既能应对流量的峰值,也能降低成本。至于需要支持状态的标记处理,我们可以选择用BaaS化云存储来解决。
|
||||
|
||||
如何设计?
|
||||
|
||||
在代码开发之前,我们先来确认整体的设计框架。扫描任务的触发,我们可以选择函数计算的crontab触发器或者一个常驻的任务来监听广告库的物料情况,亦或者通过kafka触发器,就看你希望怎么实现前面的物料获取部分了。我们的重点是在FaaS形态下开展有状态的存储和业务实现。
|
||||
|
||||
广告物料数据的存储,我们选择云数据库。函数计算负责对读取的物料数据进行校验。示意图如下:
|
||||
|
||||
|
||||
|
||||
最后,我们还可以流程图来梳理一遍物料巡检的流程。我们使用FaaS的定时触发器来触发校验任务执行,从云数据库中读取需要校验的数据:包括物料信息URL、页面文字以及非法关键词。再将广告文档和非法关键词比对,以及校验URL链接是否失效。校验完成之后,会对不合法的数据和URL进行标记,写入到云数据库中。
|
||||
|
||||
|
||||
|
||||
如何实施?
|
||||
|
||||
了解完设计思想后,我们接下来看看如何实施。今天的实践,我选择了阿里云函数计算和云数据库RDS进行。
|
||||
|
||||
我们需要提前在阿里云FC新建一个Golang的函数,并下载到本地。同时开通RDS MySQL数据库,注意,它是支持以Serverless的模式开通的。
|
||||
|
||||
BaaS存储
|
||||
|
||||
正如前面所说,物料数据和非法文字是要存储到云数据中的。我们来看看如何设计这两个数据结构。
|
||||
|
||||
首先是物料数据,可以按照如下的样例设计实施:
|
||||
|
||||
// 物料存储
|
||||
type AdvertData struct {
|
||||
//用户的ID
|
||||
UserID string
|
||||
//用户的名称
|
||||
UserName string
|
||||
//用户的身份证号码
|
||||
UserIDNumber string
|
||||
//广告的url链接
|
||||
AdUrl string
|
||||
//广告文案内容
|
||||
AdContent string
|
||||
//页面文案是否合法
|
||||
LegalStatus bool
|
||||
//url链接是有效
|
||||
UrlValidStatus bool
|
||||
}
|
||||
|
||||
|
||||
这个核心结构体,记录的是投放用户的信息以及投放的广告文案和url数据。同时使用“UrlValidStatus”和“LegalStatus”字段来表示该用户的url链接是否有效,页面展示的文案信息是否合法。
|
||||
|
||||
接下来是非法关键词,同样可以按照下面的代码示例来实施。但是,如果是要用到生产环境,字段就要按照系统的复杂程度来设计了:
|
||||
|
||||
// 非法关键词存储
|
||||
type IllegalKeywords struct {
|
||||
//关键词名称
|
||||
Keyword string
|
||||
//关键词语种:Chinese/English
|
||||
languages string
|
||||
// 非法等级:高/中/低
|
||||
IllegalLevel string
|
||||
}
|
||||
|
||||
|
||||
有了存储结构,我们再来看看函数计算是如何和云数据库连接的。
|
||||
|
||||
首先,我们要在下载的Golang代码中的s.yaml文件里配置RDS数据库环境变量。这里需要注意,如果创建的RDS实例不在函数计算支持的可用区,则需要额外增加VPC配置。
|
||||
|
||||
随后在数据库的初始化代码ConnectDb()中,我们可以从环境变量里直接读取数据库的配置信息,连接到RDS数据库。你也可以将ConnectDb()放到函数入口处理handler的外边,这样数据库连接实例就可以在销毁之前继续复用,避免重复创建。至于关闭RDS数据库的连接,我们可以利用CloseDb()函数实现。
|
||||
|
||||
核心流程
|
||||
|
||||
接下来我们来看看函数计算处理的核心逻辑,获取所有非法关键词、获取物料、检验物料合法性。为了方便理解,我们以伪代码的形式简述实现思想:
|
||||
|
||||
func CheckAllAdvertData() {
|
||||
// 获取所有的非法关键词
|
||||
keywords, _ := illegalKeyword.GetAllIllegalKeywords()
|
||||
i := 0
|
||||
for {
|
||||
// 获取物料,真实生产环境你得考虑一下索引问题
|
||||
advertDataList, _ := ad.GetAdvertData("desc", PAGE_SIZE, i)
|
||||
if len(advertDataList) > 0 {
|
||||
// 校验物料的合法性
|
||||
CheckUrlStatus(advertDataList, keywords)
|
||||
}
|
||||
i++
|
||||
}
|
||||
CloseDb()
|
||||
}
|
||||
|
||||
|
||||
这里,我们可以重点关注一下物料合法性校验函数CheckUrlStatus实现过程。
|
||||
|
||||
func CheckUrlStatus(advertDataList []*AdvertData, keyWords []*IllegalKeywords) error {
|
||||
for _, advertData := range advertDataList {
|
||||
// Get the data
|
||||
resp, _ := http.Get(advertData.AdUrl)
|
||||
//url 链接已经失效
|
||||
if resp.StatusCode != 200 {
|
||||
newAdvertData := &AdvertData{UrlValidStatus: false}
|
||||
err = advertising.UpdateAdvertData(advertData, newAdvertData)
|
||||
}
|
||||
.....
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
bodyString := string(body)
|
||||
|
||||
//校验url的数据中是否有非法的字符
|
||||
checkResult, _ := checkUrlData(bodyString, keyWords)
|
||||
// 如果校验失败,那么写入数据库中,更改字段
|
||||
if checkResult {
|
||||
newAdvertData := &AdvertData{LegalStatus: false}
|
||||
err = advertising.UpdateAdvertData(advertData, newAdvertData)
|
||||
}
|
||||
}
|
||||
fmt.Println("校验成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
校验函数会先请求该URL,判断返回是否正常。如果URL链接已经失效,那么会更改云数据库中UrlValidStatus字段,标记为URL失效状态。如果URL正常,则会分析返回的数据中,是否还有非法字符,如果有非法字符,会更改LegalStatus字段,将该物料信息标记为非法。
|
||||
|
||||
到这里,一个广告物料巡检的有状态服务的主要实践要点就已经讲解完了。实际上,关于广告物料和Landing Page的扫描、校验过程远比我们今天的实操要复杂得多,不过,只要我们掌握了最基础的流程设计以及最关键的要点设计,那么更多生产环境的复杂情况,相信你也可以触类旁通地胜任。
|
||||
|
||||
小结
|
||||
|
||||
最后,我来小结一下今天的内容。
|
||||
|
||||
首先,我从函数计算FaaS的局限性,介绍了FaaS在处理有状态诉求的场景下的力不从心。但我们可以看到,函数作为Serverless的重要因子,可以说是无处不在的,甚至,我们可以基于函数的形式,让更多微服务的业务功能开发更高效。
|
||||
|
||||
由此,我们可以看到在有状态场景下两种不同方式的实现:一种是基于函数计算本身有状态服务的实现,另一种是FaaS+BaaS的结合来实现。
|
||||
|
||||
有状态函数服务在机器学习的迭代计算场景、大数据的计算场景、实时交互和多人协作场景上比较适用;而FaaS + BaaS,其实就是完整版的Serverless的实现,我们考量一个应用系统的Serverless化的能力,也是要从这两方面来看的。目前各大云厂商也都陆续推出了不同的BaaS化服务,如数据库、消息队列、缓存等,旨在将计算和存储分离的更彻底,让弹性做的更好。
|
||||
|
||||
最后,我们通过一个广告物料巡检的案例,也帮助你加深了对于“基于存储介质实现更复杂的云函数功能”的理解。希望你在后续的工作中,能在降本增效方面有更多的参考选择。
|
||||
|
||||
思考题
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个思考题。
|
||||
|
||||
你的工作中现在或者未来可能会有哪些FaaS+BaaS结合的场景,你目前遇到哪些BaaS化的服务,使用上感觉如何呢?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起交流学习。
|
||||
|
||||
|
||||
|
||||
|
226
专栏/Serverless进阶实战课/18实战指南:Serverless沙场老兵的一线使用经验.md
Normal file
226
专栏/Serverless进阶实战课/18实战指南:Serverless沙场老兵的一线使用经验.md
Normal file
@ -0,0 +1,226 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
18 实战指南:Serverless沙场老兵的一线使用经验
|
||||
你好,我是静远。
|
||||
|
||||
恭喜你完成了Serverless核心技术和扩展能力的学习,从这节课开始,我们就要运用之前所学的知识点,开始实战演练了。
|
||||
|
||||
前面我们讲到的触发器、冷启动、扩缩容、运行时、可观测等知识点,相信你都已经掌握得差不多了,随便拿出一个相关的问题,你也可以快速应对。但是,这些零碎的“武功招式”怎么才能在实战中运用起来呢?Serverless架构模式的开发和传统架构应用的开发如此不同,我们需要格外注意哪些“坑”呢?
|
||||
|
||||
今天,我就把平常“取经”得到的经验以及自己总结出来的“客户常见难题”,分成方案设计、资源评估、调用速度、开发技巧、线上运维等几个方面,跟你聊聊如何用好“Serverless”。
|
||||
|
||||
希望这节课,让你能够在选择合适的方案和开发手段这些方面得到启发,真正享受Serverless带来的红利。
|
||||
|
||||
方案选型
|
||||
|
||||
方案选型的时候,选错场景和技术大方向是比较忌讳的,不仅可能导致服务不稳定,还会导致修补工作量上升。那么,什么情况下适合用Serverless技术呢?
|
||||
|
||||
比较适合Serverless技术的场景,通常由数据处理计算、应用后端服务、事件触发处理、有状态服务和传统服务升级 5个维度组成。
|
||||
|
||||
|
||||
|
||||
接下来,我们可以对每一个业务的特性做一个抽象总结,看看5个维度不同的侧重点,让你在日新月异的Serverless场景迭代下,也能够自己动手,丰富这张技术选型图谱。
|
||||
|
||||
|
||||
数据处理计算的场景
|
||||
|
||||
|
||||
如果是大流量数据,且没有峰谷显著特性的话,你完全可以使用Spark、Flink这样的技术来处理,只有在峰谷显著的时候,Serverless的“极致弹性”才有发挥的余地。也就是,数据场景,流量变化较大、需要算力动态调配的时候,选用Serverless。
|
||||
|
||||
|
||||
事件触发类场景
|
||||
|
||||
|
||||
这个场景是从FaaS诞生就常用的领域了。它的特性在于“流量的不稳定”,有就触发,没有就缩容至0。如果你的业务场景比较轻量,且流量不稳定,那么“精益成本”(按需使用,按量付费)的Serverless就是你的最佳选择。
|
||||
|
||||
|
||||
应用后端服务
|
||||
|
||||
|
||||
区别于传统的微服务应用,应用后端服务更多的是对于微服务的一种补充,多用于相对独立、架构简单的业务应用。比如小程序、H5活动页、BFF的场景,它印证了Serverless“快速交付”这一特点。
|
||||
|
||||
这三个场景,我用蓝色做了区分,不管是通过函数编写还是自定义镜像,FaaS形态的技术都足以支撑了。而黄色的部分代表Serverless内涵延伸后,尤其是近几年技术底座和产品发展后,能够支撑更多的场景。我们继续往下说。
|
||||
|
||||
|
||||
有状态的场景
|
||||
|
||||
|
||||
有状态的场景打破了之前狭义上Serverless无状态的特性,让长进程、多任务、有状态需求的交互也能在Serverless下完成。
|
||||
|
||||
|
||||
传统服务升级
|
||||
|
||||
|
||||
当我们遇到历史包袱比较重,比如服务架构集成了SpringBoot、SpringCloud、Dubbo等框架的情况,就可以考虑通过Serverless托管服务的模式转型了。另外如果希望省去K8s的运维和容量的规划成本,考虑Serverless的容器转型,也可以划分在这类场景中。
|
||||
|
||||
除去这张图上的5个维度,反过来想一想,哪些情况不太建议使用Serverless技术呢?
|
||||
|
||||
我们之前选入的场景,都具有“运维门槛较高需要免运费和全托管”“流量变化较大需要弹性伸缩”“轻量应用需要快速交付”“流量不确定需要能缩容至0,按需使用按量付费”的特性,那么反过来,比如延时非常敏感的、SLA级别极高的场景,比如类似网页搜索、银行交易,就不太建议使用Serverless技术了。
|
||||
|
||||
在这里,我也给你分享一个技巧:你在初次接触Serverless的时候,可以多参考云厂商官网的案例,提工单和他们的售前工程师交流,可以帮助你更好的来选型和用好他们的产品。虽然我前面的课程中,已经给出了很多一线案例和经验,但Serverless的发展是日新月异的,紧贴官网是最好的选择之一。
|
||||
|
||||
资源评估
|
||||
|
||||
方案确定之后,我们就要做资源评估了。虽然Serverless具备按需使用、按量付费的特点,但这并不能说明Serverless的架构所产生的费用就比传统服务低。
|
||||
|
||||
你可以尝试按照这两个步骤评估:
|
||||
|
||||
第一,你的服务一天跑下来,是否存在明显的空闲期或者具备明显的“峰谷”现象?
|
||||
|
||||
第二,具备第一点后,是否选择了合适的资源规格去计算、对比各个方案费用?
|
||||
|
||||
这两点适用于所有选用Serverless技术的产品。我们拿函数计算来说,它的收费是按照调用次数、资源使用耗时和出网流量来计算的。从各个云厂商的计价表可以看出,主要的费用来自于这三点中的资源使用耗时,而“资源耗时=内存大小*运行时间”。
|
||||
|
||||
从公式可以看出,资源的耗时成本是和内存大小、运行时间成正比关系的。在资源评估阶段,我们先说内存大小的问题。
|
||||
|
||||
我之前遇到一个客户,他的调用次数每天大概是50W次,每次执行在3秒左右,但费用还不低。
|
||||
|
||||
通过平台监控,我们发现他为了处理少部分数据量计算比较大的文件,准备的是1G的内存,这个数量只有不到1000次的调用量。
|
||||
|
||||
听到这里,你有啥感想呢?不急,我们先来看一下这两种内存下的收费情况,我以阿里云函数计算FC活跃实例资源价格计算如下:
|
||||
|
||||
|
||||
|
||||
我们可以发现,在不同的内存资源下,费用相差4倍。所以,当时我们给客户的建议,是在第一个函数 Func1 中判断一下文件大小,如果超过256MB承受的范围,就通过异步策略发往下一个函数 Func2 处理,这个函数设置为1GB,就可以在比较低的成本下完成他的业务处理。
|
||||
|
||||
我们来看一下费用对比:
|
||||
|
||||
|
||||
|
||||
所以,在资源评估时,依据情况合理地进行函数拆分,也是至关重要的。
|
||||
|
||||
调用速度
|
||||
|
||||
刚才公式里面的第二个关键因素是“运行时间”,这就和调用速度相关了。那影响调用速度的因素有哪些?或者说,调用速度卡在哪里了呢?
|
||||
|
||||
从我跟客户打交道的经验来看,常见的有这么几个点,也是代码编程中容易忽视Serverless这种开发模式的地方。
|
||||
|
||||
|
||||
代码包太大,引入了不必要的依赖包:这种情况尤其在Java的开发者中特别常见,要善于用exclude去掉一些不相关的依赖。
|
||||
运行时的影响:之前我的一个客户,由于使用了Java,导致出现偶发的超时错误,发现就是冷启动的时候耗时较长,后来干脆替换成了Python就没有这种问题了。尽管在阿里云等厂商对Java的运行时做了优化,但还记得我之前在运行时小节中跟你对比的么,Java的耗时除了在冷启动,在Invocation阶段也比Golang要长。
|
||||
调用下游服务返回慢:要知道,函数是按照使用次数和时长来计费的,抛开这个不说,函数的超时时间设定的限制,如果太慢的下游服务返回,会出现超时错误。不仅浪费成本,还使得服务体验不好。
|
||||
代码习惯问题:比如引入不必要的框架,它确实可以加快开发的速度,但也会影响服务的速度;另外,就是不必要的循环和Sleep关键字在代码中的使用,这些都是在设计函数代码架构和逻辑中格外需要注意的。
|
||||
|
||||
|
||||
总结下来,攻克这些问题需要做的就是两件事,一个是函数特性的优化,另一个是函数之间的协作优化。那么,作为一名Serverless平台的用户,具体可以做些什么呢?
|
||||
|
||||
在函数特性优化方面,你可以做5件事:
|
||||
|
||||
|
||||
尽可能减少不必要的代码依赖,优化代码体积,提升代码的下载和加载速度;
|
||||
选择性能较高的运行时,提升函数的启动速度,比如Golang语言;
|
||||
合理利用本地缓存,对于比较大的数据,可以适当缓存在挂载路径中;
|
||||
预留实例。我们知道Serverless的实例在没有流量进来的时候,是会缩容掉的,那么下次再有流量进来,就会进入冷启动的流程,你可以在成本允许的范围内对部分延时敏感性较高的函数设置一定的实例进行预留和分策略加载;
|
||||
请求预热。你可以通过配置一个定时触发器的方式,来主动调用函数来请求预热,设置的时间间隔你可以根据云厂商的实例回收时间来灵活配置。
|
||||
开发模式。尽量让自己的代码在一层的处理逻辑中,以“平铺”的方式解决,避免层层嵌套、循环、停顿这种在微服务或者脚本上经常使用的习惯发生。
|
||||
|
||||
|
||||
针对函数之间的协作,你同样可以做如下优化,来提升调用的速度:
|
||||
|
||||
|
||||
尽可能使用异步调用,尤其是级联的函数调用避免同步调用下游服务产生的耗时,因此我们在选型的时候得要注意评估;
|
||||
一个函数只做一件事情,也就是说要根据业务进行合理的函数粒度拆分。在成本、复用性、性能上都存在其必要性。
|
||||
|
||||
|
||||
这样的优化能带来什么好处呢?相信学过冷启动的你能立马回答出来:提升服务的体验。如果你是一个HTTP网站,服务响应越慢,用户的流失也就越严重,间接上你的页面访问和流量也会降低,最后影响的是营收。
|
||||
|
||||
开发技巧
|
||||
|
||||
接下来,我们就到了开发阶段的讨论了。我会从开发工具选择以及编码技巧两个方面切入,这也是我们工作中最重要的两个抓手。
|
||||
|
||||
工具的选择
|
||||
|
||||
先说工具的选择。一般云厂商都会提供各种方便的工具来支撑项目的开发、调试、打包、发布和运维,实现项目全生命周期的管理能力。比如阿里云的Serverless Devs,腾讯云的Serverless Framework CLI,百度智能云的BSAM等等。
|
||||
|
||||
|
||||
|
||||
开发工具一般分为三种,本地开发工具CLI、WebIDE和IDE插件集成。其中,WebIDE的能力更偏向支持轻量化的在线开发管理,IDE插件的能力更偏向照顾那些使用成熟IDE的技术人员,如VSCode的开发人员。
|
||||
|
||||
那面对这些工具我们该怎么选呢?
|
||||
|
||||
|
||||
如果你的业务比较简单,采用的又是解释型语言,那么首选WebIDE,关于它的优势和好处,可以在回到第11节课回顾一下加深一下记忆;
|
||||
如果你的业务比较复杂或者是编译型语言,那么我推荐你使用本地CLI工具或者插件集成。
|
||||
|
||||
|
||||
这几类工具的区别在于,如果你希望交互体验好一点,并且对类似VSCode的IDE工具开发的比较顺手了,那么将插件绑定在编辑器上是一个不错的选择。而类似Serverless Devs的这种工具,如果你是富有极客精神的命令行狂热爱好者,你可以用起来,丰富的命令足可以支撑你完成项目的管理工作了。
|
||||
|
||||
编码技巧
|
||||
|
||||
有了开发工具,那编码有啥需要注意的呢?一方面,是资源的初始化复用思想;另一方面,是函数业务的调用和设置细节。我们通过一个例子来看一下。
|
||||
|
||||
我在运行时小节中跟你提到过,基于标准运行时构建的函数其实是基于一个代码框架运行的。平台提供给你的是一个handler的入口函数,你可以直接在这个函数里编写你的业务代码逻辑。但如果函数需要和资源打交道,那么就需要在执行存储操作之前先建立连接了。
|
||||
|
||||
如果我们在handler函数中写了如下一段连接数据库的代码,想一想,这会有啥问题?
|
||||
|
||||
是不是每次请求过来都要重新连接做一遍这个操作?如果这是一个偶尔触发一次的函数,可能看不出问题。但如果一旦并发量比较高的服务场景,就会极大的影响性能,还可能导致数据库的连接句柄超限。
|
||||
|
||||
import pymysql
|
||||
def handler(event, context):
|
||||
db =pymysql.connect(
|
||||
host=os.Getenv("MYSQL_HOST"),
|
||||
port=os.Getenv("MYSQL_PORT"),
|
||||
user=os.Getenv("MYSQL_USER"),
|
||||
passwd=os.Getenv("MYSQL_PASSWORD"),
|
||||
charset='utf8',
|
||||
db=os.Getenv("MYSQL_DBNAME")
|
||||
)
|
||||
cursor = db.cursor()
|
||||
|
||||
|
||||
正确的做法应该把它单独拎出来,放到一个函数中,然后单独调用一次,这样在函数实例被回收之前,如果有新的请求过来,就可以直接复用了。
|
||||
|
||||
这个解决方案不仅利用了FaaS平台的实例复用技术,也在实例复用的基础上复用了资源对象。如果你还想进一步优化,也可以在给数据库对象db赋值和获取的时候判断是否为Null值,使得代码更鲁棒。类似地,像kafkaClient、redisClient等中间件资源的初始化都可以这样进行。
|
||||
|
||||
资源的初始化这个关键问题非常关键,一旦处理不好,可能还会导致资源服务的连接超限等问题;除此以外,根据客户常见的业务逻辑问题,我综合选取了所有技巧中常用、高效方面的TOP5,希望你能灵活用起来。
|
||||
|
||||
首先,print日志的时候尽可能少而精。如果你使用的是云平台,直接print“event”这种整个结构体很可能会撑爆日志的限制。所以,尽量打印出你需要的业务字段。
|
||||
|
||||
其次,尽量使用异步策略代替微服务形态下的异步框架。假如你在原生的微服务中使用Python的Tornado框架,你会发现FaaS形态的Serverless是很难发挥其异步的价值的。这个时候,你就可以直接选用我在第3讲高级应用中跟你提到的FaaS平台的异步处理框架。
|
||||
|
||||
再次,调用下游服务的接口,设置的超时时间一定要小于函数的超时时间。否则会因为短板在下游,而造成一次请求超时。浪费成本不说,也会让服务不稳定。
|
||||
|
||||
另外,要将公共代码放到层上。这也是我在高级属性这一节中跟你提到的必备高级技能之一,它类似于我们平时的公共类库,可以让具体的函数代码包更轻量。
|
||||
|
||||
最后,提前设置并发数的上限。一般云厂商都会设置默认的实例并发数,比如阿里云FC限定300 * InstanceConcurrency,百度智能云CFC限定单实例100等,这对于一个并发量稍微高一点的生产应用来说是不够的,你需要提前设置,申请扩大并发度。
|
||||
|
||||
线上运维
|
||||
|
||||
开发完代码,发布到线上后,有没有想当然的认为Serverless就是“免运维的”?虽然我们多次强调过Serverless免运维的特性,但上线后的服务稳定性、访问指标以及可观测等问题,也是需要关注的。我们着重说一下稳定性和可观测。
|
||||
|
||||
稳定性方面,云厂商设置的异步调用策略,可以确保失败后的请求重试,预留实例可以确保流量突增的响应及时性。
|
||||
|
||||
除此之外,我之前遇到的一位客户简单的方法也很值得借鉴。这位客户是一个在线的网站,对重要接口采用轮训模拟请求的方式进行探活检测,当出现连续N次出现问题的时候,就会报警和切换到备用集群、备用区域。
|
||||
|
||||
你会发现,在Serverless云厂商还不是很完善的情况下,作为一个开发者,可以基于“免运费”之上做一层“运维保险”。这个方式不一定最优,但确实很实用,我们交流之后也发现,对保障服务的稳定和客户的体验都有明显的效果。
|
||||
|
||||
可观测方面,除了我在可观测小节中跟你细聊了通过指标、日志、链路三大支柱实现一个监测系统的机制,从使用的经验上,我也建议你“活”用云厂商关联的支撑服务。他们的监控仪表盘足以支撑你常用的指标,你还可以通过关联的报警服务、链路追踪服务来更全面地“问诊”服务。
|
||||
|
||||
小结
|
||||
|
||||
最后我们来小结一下。今天我给你介绍了Serverless实战中的一些使用经验,包括方案选型、资源评估、调用速度、开发技巧、线上运维这一个完整生命周期里的心得。
|
||||
|
||||
你会发现,这节课我多次提到了在第1、2模块中的知识点。比如在调用速度里,你可以关注冷启动、函数之间调用这两节课;在开发技巧方面,你可以着重关注WebIDE、高级应用这两节课,在线上运维方面,可以更多地关注可观测的内容。
|
||||
|
||||
这节课不一定能够解决你在实战过程中遇到的所有的问题,但一定能够作为你的知识地图,帮助你定位每一个困惑背后需要不断精进的知识点。希望能够让你在开始接下来的实战进阶课之前,串联起讲解过的技术细节,形成自己的知识网络。
|
||||
|
||||
随着Serverless的不断演进,未来的众多产品也一定会朝着Serverless的形态转变。你在后续的工作中,还会遇到更多的业务诉求转型、难点挑战,希望你也能一个一个找到更优的解决方案,并沉淀成方法分享给更多的Serverless前行者。
|
||||
|
||||
后续的实战课中,我们将一起针对性地练习Serverless技术中的5大核心场景:连接云生态、跨平台开发、传统服务的迁移、私有化部署和引擎底座选取以及基于引擎构建平台。准备好了吗?
|
||||
|
||||
思考题
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个思考题。
|
||||
|
||||
你在使用Serverless的过程中还有哪些“踩坑”经验,或者你在技术选型的时候还有哪些疑惑的地方?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。
|
||||
|
||||
感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
|
||||
|
273
专栏/Serverless进阶实战课/19实战进阶(一):Serverless“连接器”的能力到底有多大?.md
Normal file
273
专栏/Serverless进阶实战课/19实战进阶(一):Serverless“连接器”的能力到底有多大?.md
Normal file
@ -0,0 +1,273 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
19 实战进阶(一):Serverless “连接器” 的能力到底有多大?
|
||||
你好,我是静远。
|
||||
|
||||
在思维构建这节课,我提到了Serverless的基本构成是“FaaS + BaaS”。FaaS由于其轻便、灵活、开放的输入和输出,以及集成了众多云生态能力,同时也被云生态所集成的特性,被称为Serverless“连接器”的存在,也是当之无愧的。
|
||||
|
||||
那么,它的能力到底有多大?它又是如何与云产品连接,提供强有力的Serverless能力的呢?
|
||||
|
||||
今天这节课,我将为你讲解连接的方式和组合,并通过ETL的场景,依次递进,带你重识“连接器”的不同使用方式。
|
||||
|
||||
重识“连接器”
|
||||
|
||||
我们知道,任何单一云产品所提供的业务解决方案都是有限的,而云服务生态下“互相打通、互相连接”的能力,在很大程度上是业务上云的关键。
|
||||
|
||||
我们通常会根据实现方式,把函数计算的连接能力分为三类:触发器连接能力、访问VPC资源的能力、访问公网服务的能力。函数计算通过这些连接能力可以与整个云上的产品服务关联,从而形成一个完整的Serverless生态。
|
||||
|
||||
|
||||
|
||||
触发器的连接能力
|
||||
|
||||
说到触发器,我们并不陌生。触发器提供的是一种事件驱动的计算模型,充当的往往是一个桥梁的角色,将事件源的服务与函数连接起来。在第2节课,我们还详细梳理过一遍触发器的执行过程和实现原理。那么,你知道触发器的连接能力能够应用在哪些场景吗?
|
||||
|
||||
不同于触发器小节中的集成方式来分类,从场景应用层面来看,按照触发器的联动程度,我们可以把它划分为三大类,分别是云服务集成触发器、原生触发器、自定义触发器。
|
||||
|
||||
首先,我们来看云服务集成触发器。顾名思义,它是需要联动其他云服务才可以产生“连接”能力的一种场景。比如,我们熟悉的对象存储触发器可以用来进行一些音视频转码处理,当音视频文件上传后,可以通过触发器对函数进行调用,从而对其进行处理;消息队列触发器可以用来做一些异步消息的通知,当检测到消息队列内的数据发生变化时,也可以通过函数进行消费。
|
||||
|
||||
这种方式,要么是函数计算集成云服务的打通能力,要么是云服务集成函数计算的打通能力。通常来说,可以通过SDK/API提前对接好,按照服务之前约定的协议(如事件驱动、回调等方式)来互相联动。
|
||||
|
||||
那么,原生触发器呢?在这里,它指的是不需要借助其他云服务,自身即可实现的一类触发器。比如定时触发器仅需配置一个时间点,就可以让云函数实现一些定时邮件通知、定时天气预报之类的任务处理。
|
||||
|
||||
从联动性和场景使用程度看,原生触发器也可以包括HTTP触发器,但我更倾向于将把它作为一种延伸的访问云函数的能力,你可以通过HTTP触发器生成的链接作为的Web服务暴露的访问地址。
|
||||
|
||||
最后,大部分云厂商为了给用户提供更多的可能空间,通常还会提供自定义触发器的能力,允许用户接入任意云服务的事件,比如通过自定义触发器的方式,对接自研的资源存储或者服务等等。这也极大地丰富了函数计算的使用场景。
|
||||
|
||||
跨VPC的连接能力
|
||||
|
||||
函数计算另一个连接能力的体现,就是跨VPC的访问。通过VPC建联,能够让函数更加安全地访问用户的私有网络资源。
|
||||
|
||||
比如通过跨VPC,使得函数具备访问云数据库、云消息队列的能力,从而实现一个较为复杂的后台处理逻辑;另外,通过VPC的提前打通,可以将分布式文件系统直接挂载到函数实例上,使得云函数具备大文件处理的能力。
|
||||
|
||||
至于为什么提出VPC、跨VPC的意义以及如何优化函数在跨VPC场景的访问速度,你可以再回到第10课复习一下,加深记忆。
|
||||
|
||||
公网访问能力
|
||||
|
||||
部分云服务本身就暴露了公网访问接口,这类服务甚至不需要为云函数做任何配置,就可以访问。但云厂商为了安全考虑,同样地,需要设置鉴权和身份认证等保障机制。
|
||||
|
||||
我们在前面课程中提得最多的对象存储触发器,就可以无需配置VPC,直接访问;还有一些人工智能场景,比如自然语言处理、图像识别等,也完全可以通过直接访问公网接口来调用。
|
||||
|
||||
这里需要注意的是,通过VPC访问与通过公网访问的服务场景是有重叠的,这取决于服务本身支持哪种访问方式,比如阿里云的云消息队列Kafka就支持公网与VPC两种访问方式。
|
||||
|
||||
连接器的组合
|
||||
|
||||
无论是事件驱动还是主动请求,上面这几种连接能力并不是完全割裂的。在实际应用中,往往会通过不同的组合方式来实现一个完整的解决方案。
|
||||
|
||||
|
||||
|
||||
我们举2个例子。
|
||||
|
||||
还记得我们在16课动手体验中讲到的“通过工作流进行音视频转码”吗?这个问题,就可以通过触发器+公网访问的连接能力将工作流、对象存储和云函数关联起来。当一个或者多个视频上传到对象存储,通过触发器触发后,函数可以直接调用工作流进行批处理,然后按照分类转码的处理逻辑给视频转码,最后同步调用对象存储的接口,将转码文件进行写回。
|
||||
|
||||
|
||||
|
||||
还有,我们日常开发的Web应用或者小程序后台,也可以部署在云函数上。通过VPC让函数具备访问云数据库的能力,再配上HTTP触发器或者API网关,用户就能够非常方便的通过暴露的链接进行访问。
|
||||
|
||||
相信现在,你已经能够理解连接器的强大所在了。接下来,我就通过一个ETL场景的实战,带你切实感受一下连接器的强大。
|
||||
|
||||
ETL实战
|
||||
|
||||
在很多边缘计算的场景中,日志信息往往并不能做到实时上报,所以通常会采用定时上传日志文件的方式来进行后续的日志处理和分析。
|
||||
|
||||
比如我们最常见的车载日志,大量的车载终端源源不断地产生着行驶日志。实时分析这些行驶信息非常困难,一般都会是在收集到一定数量之后,以文件的形式整体上报。这样不仅可以提高日志传输效率,还能够对整体的日志文件进行暂存,方便后续的溯源。
|
||||
|
||||
连接器常规组合
|
||||
|
||||
对于这个处理过程,我的客户经常会采取对象存储触发器和消息队列,分别实现函数计算的输入和输出。下面是我给出的一个完整的流程示意图。接下来,我会借助阿里云相关云产品服务展开实验操作。
|
||||
|
||||
|
||||
|
||||
首先,为了接收上传的日志文件,我们需要提前在对象存储OSS上创建一个Bucket。同时,我们还需要在消息队列Kafka服务上创建一个实例,具体可以参考这篇快速入门文档。
|
||||
|
||||
接着,我在函数计算FC下,创建一个处理日志信息的云函数。还记得之前用到的函数模板吗?这里我们也可以选择Python3.6运行环境,选用“向消息Kafka投递消息”的模板来创建函数。至于和OSS交互的部分,可以参考OSS操作模板。
|
||||
|
||||
|
||||
|
||||
接着,我们需要为其配置Kafka访问的地址以及Topic,因为模板已经将其声明在环境变量当中了,我们可以直接在函数的环境变量中配置。
|
||||
|
||||
|
||||
|
||||
Kafka服务提供公网与VPC两种访问方式,如果通过公网访问的话,走SSL协议需要下载证书,会比较麻烦,这里我推荐你使用VPC进行访问,便于你快速实验。
|
||||
|
||||
|
||||
|
||||
需要注意的是,VPC访问需要在函数所在服务下额外配置VPC,让函数服务与Kafka实例处于同一私有网络中。具体配置,你可以参考下面的图片:
|
||||
|
||||
|
||||
|
||||
创建好函数以后,你还需要绑定一个OSS触发器,关联上刚才创建的bucket,前后缀可以根据你自己的业务需要设定。触发器的配置之前也带你体验过,这里我就不再重复了。
|
||||
|
||||
最后,结合我们的设计思路,来看看主要的代码处理逻辑吧:
|
||||
|
||||
from confluent_kafka import Producer
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
import oss2, json
|
||||
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
||||
def initialize(context):
|
||||
global p, TOPIC_NAME
|
||||
|
||||
|
||||
""" Get the environment variables """
|
||||
BOOTSTRAP_SERVERS = os.getenv("BOOTSTRAP_SERVERS")
|
||||
TOPIC_NAME = os.getenv("TOPIC_NAME")
|
||||
|
||||
|
||||
p = Producer({'bootstrap.servers': BOOTSTRAP_SERVERS})
|
||||
|
||||
|
||||
def deliveryReport(err, msg):
|
||||
""" Called once for each message produced to indicate delivery result.
|
||||
Triggered by poll() or flush(). """
|
||||
if err is not None:
|
||||
logger.info('Message delivery failed: {}'.format(err))
|
||||
raise Exception('Message delivery failed: {}'.format(err))
|
||||
else:
|
||||
logger.info('Message delivered to {} [{}]'.format(msg.topic(), msg.partition()))
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
|
||||
|
||||
creds = context.credentials
|
||||
|
||||
|
||||
# 设置权鉴,供 OSS sdk 使用
|
||||
# Setup auth, required by OSS sdk
|
||||
auth=oss2.StsAuth(
|
||||
creds.access_key_id,
|
||||
creds.access_key_secret,
|
||||
creds.security_token)
|
||||
|
||||
|
||||
# Load event content
|
||||
oss_raw_data = json.loads(event)
|
||||
# Get oss event related parameters passed by oss trigger
|
||||
oss_info_map = oss_raw_data['events'][0]['oss']
|
||||
# Get oss bucket name
|
||||
bucket_name = oss_info_map['bucket']['name']
|
||||
# Set oss service endpoint
|
||||
endpoint = 'oss-' + oss_raw_data['events'][0]['region'] + '-internal.aliyuncs.com'
|
||||
# Initiate oss client
|
||||
bucket = oss2.Bucket(auth, endpoint, bucket_name)
|
||||
object_name = oss_info_map['object']['key']
|
||||
|
||||
# 将日志文件下载到本地处理
|
||||
bucket.get_object_to_file(object_name, "/tmp/tmp.log")
|
||||
|
||||
# 逐行读取
|
||||
f = open( "/tmp/tmp.log", 'rb')
|
||||
line = f.readline()
|
||||
while line:
|
||||
content=json.loads(line)
|
||||
info = {}
|
||||
|
||||
# 提取关键信息
|
||||
info['speed']=content['speed']
|
||||
info['carID']=content['carID']
|
||||
|
||||
|
||||
info_str = json.dumps(info)
|
||||
|
||||
|
||||
line = f.readline()
|
||||
|
||||
# 发往kafka
|
||||
p.produce(TOPIC_NAME, str(info_str).encode('utf-8'), callback=deliveryReport)
|
||||
p.poll(0)
|
||||
|
||||
|
||||
""" Flush the internel queue, wait for message deliveries before return """
|
||||
p.flush()
|
||||
f.close()
|
||||
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
这段代码一共有3个要点,分别是资源初始化、输入数据的获取、数据结果输出。
|
||||
|
||||
首先,我们来看资源的初始化部分,在initialize 函数中会先完成全局的初始化操作,主要是Kafka的相关信息,这也是我建议你使用模板的原因,因为模板里面已经包含访问Kafka的依赖库了。
|
||||
|
||||
其次,是如何获取数据,处理并输出。我们来看handler的逻辑。因为在发生日志上传时,需要将文件下载到函数实例内处理,因此,这一步主要是初始化一个OSS的客户端。
|
||||
|
||||
最后,利用刚才创建的client以及event内的对象信息,我们可以调用client的接口将日志文件下载到本地。拿到日志文件之后,处理过程就简单了。你只需要遍历文件内容,提取关键信息,再推送到Kafka就行了。比如我这里提取的就是distance和carID,然后调用p.produce进行推送,供下游服务进行处理。
|
||||
|
||||
我们注意到,在p.produce函数存在一个callback的参数,因为deliveryReport是一个向Kafka发送消息后的回调函数,会根据发送结果输出一个提示信息。
|
||||
|
||||
之后,我们就可以测试一下上述的配置和代码是否能够正常运行了。我这里提前模拟了一个日志文件,里面的内容包含了一些基本的车辆行驶信息,我将其命名为driving.log并上传到了OSS。
|
||||
|
||||
{"speed":12,"carID":1234,"manufacturer":"BenZ","zone":"China","distance":102,"endAt":"Beijing","startAt":"Shanghai"}
|
||||
{"speed":24,"carID":3512,"manufacturer":"BenZ","zone":"China","distance":102,"endAt":"Beijing","startAt":"Shanghai"}
|
||||
|
||||
|
||||
完成上传后,我们观察一下函数的执行情况,可以看到确实已经触发执行了。
|
||||
|
||||
|
||||
|
||||
我们再看一下Kafka的监控,可以发现也有一定的写入量。
|
||||
|
||||
|
||||
|
||||
按照上面的步骤,你已经可以实现一个日志处理业务的基本功能了。如果你觉得不够完美,也可以更进一步优化一下代码的分层和鲁棒性,来用于你的生产环境中。
|
||||
|
||||
连接器延伸能力
|
||||
|
||||
那么,鲁棒性的提升除了我们自己的代码优化之外,函数计算作为连接器,是否本身也具备一些延伸能力来使得系统跟稳定和健壮呢?
|
||||
|
||||
我们知道,日志文件的触发是属于异步调用方式的,而异步调用还可以通过异步策略的配置来支持在异常情况下进行重试、分流处理,以确保数据的高可靠性。
|
||||
|
||||
也就是说,我们可以为刚才的函数设置超时时间。进入异步策略界面,我们可以根据异步执行结果配置策略,比如我配置的策略就是执行失败后调用下游的通知函数,上报任务失败的情况。你也可以按照你的实际需求进行配置。
|
||||
|
||||
|
||||
|
||||
进阶ETL
|
||||
|
||||
通过上面的场景,我们了解了函数计算在解决日志数据分析问题时,连接其他云服务的方法和过程。在常规版的ETL实现中,日志文件一旦上传就可以通过OSS触发器让云函数进行处理,并将抽取转化处理的日志关键信息发往云消息队列服务,同时异步策略也为我们提供了额外的重试保障。
|
||||
|
||||
但有的时候,我们只是为了数据的暂存而选择了对象存储,那么,是否有一种方式,可以直接上传日志,而不需要额外占用对象存储的资源呢?答案是有的,拿阿里云的“连接器”函数计算FC来说,我们还可以采用日志服务SLS触发器。
|
||||
|
||||
|
||||
|
||||
SLS触发器能够定时获取到日志的增量信息,而不用像对象存储触发器那样,每次将日志文件上传的时候才会触发ETL任务。
|
||||
|
||||
|
||||
|
||||
配置了触发间隔后,SLS触发器就会按时调用函数,并传入日志数据变化的游标。这样就可以直接获取变化的增量数据。
|
||||
|
||||
相比于OSS触发器,SLS触发器具有更加丰富的接入方案,你可以直接采集阿里云产品的日志,也可以通过Logstash、FluentD等开源软件采集日志,通过HTTP/HTTPS、Promethues等标准协议接入数据。
|
||||
|
||||
如果你的处理场景实效性不敏感,从性价比来说,采用小文件上传到OSS触发也是不错的选择。你可以根据实际的业务需要来进行综合考虑,最终选用什么样的方案更合适。
|
||||
|
||||
小结
|
||||
|
||||
最后我们来小结一下,今天这节课我跟你介绍了函数计算FaaS是如何扮演好Serverless“连接器”这个角色的。
|
||||
|
||||
它以具备的三大连接能力为基础,通过SDK/API、事件驱动等不同的方式,形成了“FaaS + 云服务”的Serverless新生态组合。正如你在ETL实战中看到的那样,通过对象存储触发器和消息队列的组合,5分钟不到的时间,轻松的玩转了日志分析处理开发和上线等一系列事情的全流程,而平时可能需要好几个小时才能完成。
|
||||
|
||||
触发器的连接能力,更像是“引流”的利器,通过被云服务集成或者集成云服务的方式,网罗了云上的各种“门客”,例如我们在上面示意图中展示的消息服务、文件存储、API网关、对象存储等。这些“门客”带来的数据等资源,在计算之后,也可以通过跨VPC和公网访问两大能力输送出去。
|
||||
|
||||
这样“输入”+ “计算” + “输出”高效快速的实现,不就是我们想要的“降本增效”吗?这样的粘合剂的功效,让云服务通过物理组合的方式发挥着巨大的化学反应,“连接器”的函数计算再一次通过它“胶水语言”的特性,证明了它自身的价值。
|
||||
|
||||
课后作业
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个课后作业。
|
||||
|
||||
这节课我带你通过一个ETL的场景体验了Serverless“连接器”的精髓,那么如何实现一个根据日志的关键指标信息发送报警邮件的功能呢?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。
|
||||
|
||||
感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
|
||||
|
249
专栏/Serverless进阶实战课/20实战进阶(二):如何基于智能音箱开发一个BOT技能?.md
Normal file
249
专栏/Serverless进阶实战课/20实战进阶(二):如何基于智能音箱开发一个BOT技能?.md
Normal file
@ -0,0 +1,249 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
20 实战进阶(二):如何基于智能音箱开发一个BOT技能?
|
||||
你好,我是静远。
|
||||
|
||||
上节课,我们一起感受了FaaS 作为Serverless “连接器”的强大,但也仅限于云厂商单个云平台上的体验。
|
||||
|
||||
今天,我将带你体验跨平台的开发,通过智能音箱、IOT 开发平台、云函数计算平台三者之间的联动,一起完成一个不同年龄段宝宝食谱推荐BOT技能的开发。
|
||||
|
||||
你也可以通过这样的方式,完成智能客服、智能问答等对话式应用的开发。通过这个实战,相信你能进一步感受到Serverless的无限可能。
|
||||
|
||||
如何实现
|
||||
|
||||
我们本次的目标是通过语音对话的方式来完成宝宝食谱的推荐。这个功能符合Serverless的特性:事件触发、轻量应用、按需调用、流量不固定。同时,该功能需要有语音转文字的处理能力以及一个物理载体。
|
||||
|
||||
因此,我们以百度智能云函数计算平台、百度DuerOS开放平台来进行本次的案例开发,并选择一个小度X8作为载体。当然,你也可以直接用DuerOS平台上的模拟测试工具达到一样的效果。
|
||||
|
||||
正所谓磨刀不误砍柴工,在开始实战之前,我们先了解一下今天涉及到的各类概念以及整体的实现思路。
|
||||
|
||||
什么是技能(BOT)?
|
||||
|
||||
我们通常将在DuerOS平台上开发的对话式服务称为“技能”,它还有一个英文名,“BOT”。一般来说,平台有内置技能和开发者提交发布的两种技能。前者可以直接拿来用,后者分为免费和收费两种,收费的技能和你在小度音箱上看到的一样,是需要支付购买才能使用的。
|
||||
|
||||
一个技能通常由4个重要部分组成,我们的动手实战中都会涉及到。
|
||||
|
||||
|
||||
意图:指技能要满足的用户请求或目的。比如这次的意图就是给不同年龄段的宝宝推荐食谱,由“常用表达语”“槽位信息”等组成;
|
||||
词典:是某领域词汇的集合,也是用户与技能交互过程中的重要信息。比如我们本次实验中需要识别的关键词“年龄”就是一个词典中的词条,对应英文识别为number。为了方面开发者,DuerOS在词典中内置了不少词条,比如这里的number就是sys.number;
|
||||
用户表达:是用户表达意图时具体的样例,它是组成意图的关键。用户表达样例越多,意图识别能力越强。槽位信息就是从用户表达语句中提取出的关键信息,并能够将这些关键信息和词典一一匹配。比如“3岁宝宝吃什么”就是一句用户表达,提取的槽位标识是number,数值是3,对应的词典是sys.number;
|
||||
配置服务:技能创建成功后,需要部署到云服务上,我们配置函数计算的入口函数标识BRN就可以了。BRN可以在函数计算平台上的函数基本信息展示里获取到。如“brn:bce:cfc:bj:*******:function:babaycook:$LATEST”。
|
||||
|
||||
|
||||
实现思路
|
||||
|
||||
接下来,我们看联动示意图,感知一下“宝宝食谱”技能通过设备和平台实现交互的过程。
|
||||
|
||||
|
||||
|
||||
如图所示,小度音箱在收到语音输入后,如“小度小度,宝宝能吃什么?”,会通过既定协议请求DuerOS平台,通过DuerOS触发器触发函数计算CFC的执行。之后,你编写好的处理逻辑就会按照指令输出返回给DuerOS平台,并通过语音的方式在小度音箱播放。
|
||||
|
||||
这里使用的DuerOS触发器是百度CFC平台特有的一种触发器类型,用于和DuerOS联动,当然,你也可以自定义任何的触发器。
|
||||
|
||||
如果涉及到存储,你可以选择云上的存储资源来使用,如Redis、RDS等等,这些也都在之前的课程中讲到过,相信你已经驾轻就熟了。
|
||||
|
||||
接下来,我带你来梳理一下开发的实现思路,我把整个过程划分为3大步骤。
|
||||
|
||||
步骤一,云函数创建与自定义:你需要在CFC管理端选择一个合适的模板,快速创建一个DuerOS的初始函数,完成函数的开发和配置,最后设置触发器和配置日志存储就可以了。
|
||||
|
||||
步骤二,技能创建与绑定:在DuerOS平台选择一个技能模板,并设置相应的元信息,根据你的诉求,设置意图和词典、话术场景配置等等。最后绑定CFC函数的时候,将函数入口的BRN拷贝到DuerOS平台即可。
|
||||
|
||||
步骤三,运行时请求路由:其实这一步,不需要你做什么,直接体验就行。但如果你想发布到智能音箱上,还需要在控制台发布。发布之后,你就可以通过音箱来体验了。
|
||||
|
||||
整个流程你也可以对着下面的架构示意图梳理一遍。
|
||||
|
||||
|
||||
|
||||
动手实战
|
||||
|
||||
下面,我们就可以根据上面的思路开始动手实现不同年龄段宝宝食谱推荐BOT技能的开发了。
|
||||
|
||||
云函数创建和定义
|
||||
|
||||
首先,我们在函数计算平台CFC上创建一个函数,就本案例来说,为了便于你熟悉DuerOS的处理方式,可以选择一个模板来生成,比如我这里选择“dueros-bot-tax”来做案例演示。
|
||||
|
||||
|
||||
|
||||
然后,输入相关的函数元信息,这里函数名为“baby_recipe”。
|
||||
|
||||
|
||||
|
||||
我们先不用关心模板代码是什么,先继续创建触发器。这里需要选择DuerOS触发器来对接小度音箱的请求。
|
||||
|
||||
|
||||
|
||||
好了,到这里,一个小度处理的函数就搞定了。接下来,我们就基于模板编写自己的业务逻辑代码。
|
||||
|
||||
这里需要注意一下,生成的模板代码中,在文件的开头,有一段PUBLIC KEY需要复制到DuerOS平台。先不要急,我们继续关注代码。
|
||||
|
||||
*/
|
||||
/**
|
||||
* @file index.js 此文件是函数入口文件,用于接收函数请求调用
|
||||
* @author dueros
|
||||
*/
|
||||
const Bot = require('bot-sdk');
|
||||
const privateKey = require('./rsaKeys.js').privateKey;
|
||||
|
||||
class InquiryBot extends Bot {
|
||||
constructor(postData) {
|
||||
super(postData);
|
||||
|
||||
this.addLaunchHandler(() => {
|
||||
this.waitAnswer();
|
||||
return {
|
||||
outputSpeech: '欢迎使用小宝宝食谱推荐!'
|
||||
};
|
||||
});
|
||||
|
||||
this.addSessionEndedHandler(() => {
|
||||
this.endSession();
|
||||
return {
|
||||
outputSpeech: '多谢使用小宝宝食谱推荐!'
|
||||
};
|
||||
});
|
||||
|
||||
this.addIntentHandler('babyCookBooks', () => {
|
||||
let ageVar = this.getSlot('number');
|
||||
if (!ageVar) {
|
||||
this.nlu.ask('number');
|
||||
let card = new Bot.Card.TextCard('宝宝几岁了呢');
|
||||
// 可以返回异步 Promise
|
||||
return Promise.resolve({
|
||||
card: card,
|
||||
outputSpeech: '宝宝几岁了呢'
|
||||
});
|
||||
}
|
||||
|
||||
if (this.request.isDialogStateCompleted()) {
|
||||
|
||||
let cook_1 = '母乳为主,可以混合牛奶,建议辅以虾仁碎沫或者菜沫等';
|
||||
let cook_3 = '可以吃富含蛋白质的食物如:鸡蛋,瘦肉。也可以吃富含维生素的食物:如芹菜、菠菜。';
|
||||
|
||||
var speechOutput = cook_1;
|
||||
|
||||
if(ageVar > 1 ) {
|
||||
speechOutput = cook_3;
|
||||
}
|
||||
|
||||
let card = new Bot.Card.TextCard(speechOutput);
|
||||
|
||||
return {
|
||||
card: card,
|
||||
outputSpeech: speechOutput
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
exports.handler = function (event, context, callback) {
|
||||
try {
|
||||
let b = new InquiryBot(event);
|
||||
// 0: debug 1: online
|
||||
b.botMonitor.setEnvironmentInfo(privateKey, 0);
|
||||
b.botMonitor.setMonitorEnabled(true);
|
||||
b.run().then(function (result) {
|
||||
callback(null, result);
|
||||
}).catch(callback);
|
||||
}
|
||||
catch (e) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
代码中,我们主要关注的是意图的处理入口this.addIntentHandler(‘babyCookBooks’),其中“babyCookBooks”是在DuerOS技能平台创建技能的时候,需要填写的意图名称,你也可以按照后续自己定义的技能名称来修改。
|
||||
|
||||
接下来的代码部分,为了理解方便,我引入了一个宝宝菜谱推荐的简单判断,你可以在这之上添加更丰富的功能。将复杂的功能抽离出来,然后引入调用,就能实现比较完整的技能了。
|
||||
|
||||
技能创建与绑定
|
||||
|
||||
接下来就是创建技能了。进入DuerOS技能开放平台,点击授权,点击创建新技能。
|
||||
|
||||
|
||||
|
||||
选择“自定义技能”,“从头开始”。
|
||||
|
||||
|
||||
|
||||
下拉页面,在“技能名称”和“调用名称”部分输入“宝宝食谱推荐”,应用场景选择有屏和无屏场景,点击确定:
|
||||
|
||||
|
||||
|
||||
接下来,继续创建意图:
|
||||
|
||||
|
||||
|
||||
这里,我们需要注意两点。
|
||||
|
||||
第一,意图识别名的填写,需要跟你在函数计算CFC中addIntentHandler设置的名称保持一致。
|
||||
|
||||
第二,表达式和槽位比较关键。不同用户的表达形式不同,我们需要确保常用表达覆盖尽可能多的用户口语表达,这样意图识别就会更加准确。DuerOS平台可以通过表达式提取出槽位信息,你也可以手动核准和纠正。
|
||||
|
||||
槽位是意图的参数信息,是非常关键的一环,我们在代码里面获取到的参数就是槽位标识。更多关于技能的使用方法,如果你感兴趣的话,也可以在DuerOS开发平台深入了解。
|
||||
|
||||
|
||||
|
||||
接着,我们来关联技能与后端的服务。我们选用百度CFC作为服务部署,依次拷贝函数计算代码中的PUBLIC KEY以及函数的唯一标识BRN到“配置服务”页面的“Public Key”和“BRN”框中。
|
||||
|
||||
|
||||
|
||||
测试与发布
|
||||
|
||||
到这里,我们就完成了云函数的创建和定义,技能创建与绑定两个关键的步骤。最后,我们来测试一下程序是否能够正常运行。
|
||||
|
||||
我们可以选择模拟测试或者真机测试两种方法。假如你身边暂时没有小度音箱,可以选择模拟测试来验证一下。我们打开“模拟测试”,选择“小度在家”,输入“打开宝宝食谱推荐”这个技能。
|
||||
|
||||
|
||||
|
||||
你会看到,小度的屏幕上出现了我们刚才代码中的欢迎语“欢迎使用小宝宝食谱推荐”。
|
||||
|
||||
|
||||
|
||||
接着,我们输入“1岁宝宝吃什么”,会发现技能服务给我们返回的正是代码中“1岁宝宝的食谱推荐”用语:“母乳为主,可以混合牛奶,建议辅以虾仁碎沫或者菜沫等”。
|
||||
|
||||
|
||||
|
||||
假如你的身边有小度音箱,也可以选择“真机测试”,打开“技能调试模式”。这里要注意的是,你登录的DuerOS平台的账号一定要和登录小度智能音箱的账号一致。
|
||||
|
||||
|
||||
|
||||
我们来看一段视频,感受一下通过函数计算实现的技能:
|
||||
|
||||
最后,经过精修和加工后的程序,也可以正式发布出来分享给其他用户。
|
||||
|
||||
小结
|
||||
|
||||
最后我们来小结一下,今天这节课我们通过联动智能音箱、DuerOS技能开放平台、云函数计算CFC一起完成了一个宝宝食谱BOT技能的开发体验。
|
||||
|
||||
通过这次的实验,我们可以发现,一个基于Serverless和语音交互的对话式应用开发,主要包括如下三个步骤:
|
||||
|
||||
步骤一,构建一个符合业务场景的Serverless应用,你可以使用函数打包或者镜像的方式完成开发和上线,如本案例中的宝宝食谱函数服务;
|
||||
|
||||
步骤二,构建一个交互技能的意图,设置好语音交互规则,如本案例中的宝宝食谱意图;
|
||||
|
||||
步骤三,构建技能和函数的触发绑定。如本案例中创建DuerOS触发器的过程。
|
||||
|
||||
基于这三个流程和延伸,由于DuerOS的开源开放,可以广泛支持手机、电视、音箱、汽车、机器人等多种硬件设备,我们还可以用来开发智能客服、居家电器控制、智能助理等多种对话式应用,通过DuerOS与Serverless函数计算的结合,可以进一步降低健康、金融、酒旅、电信等各行各业使用人工智能对话系统的门槛。
|
||||
|
||||
其实,各大云厂商在基于Serverless的生态集成上都有非常丰富的经验和成熟的产品,如果你的业务已经在云上,那么可以挖掘一下哪些场景还可以用起来,进一步减少运维和处理带来的工作量;如果你的业务暂时还没有上云,这种开放集成和Serverless本身所体现出的这种思想,希望你也能在现有的工作中灵活运用起来。
|
||||
|
||||
最后,这节课虽然是实验课,但我最想跟你交流的,是这次实验背后的两个感悟。一个是发散思维很重要,我们可以有意识地将身边的需求和技术联系起来,活学活用Serverless;另一方面,在工作生活压力比较大的现在,作为码农的我们,也可以运用自己的技术,给孩子带来成长的快乐,还可能会带来一份收入,在这么“卷”的当下,是不是也能感受到不一样的幸福呢?
|
||||
|
||||
思考题
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个思考题。
|
||||
|
||||
少儿编程的后端服务是否可以采用Serverless的技术?哪些地方可以用到函数计算,哪些可以用弹性应用托管服务更合适?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。
|
||||
|
||||
感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
|
||||
|
295
专栏/Serverless进阶实战课/21实战进阶(三):传统的服务如何迁移到Serverless平台?.md
Normal file
295
专栏/Serverless进阶实战课/21实战进阶(三):传统的服务如何迁移到Serverless平台?.md
Normal file
@ -0,0 +1,295 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
21 实战进阶(三):传统的服务如何迁移到Serverless平台?
|
||||
你好,我是静远。
|
||||
|
||||
今天我们来聊一聊系统架构面临新技术到来的时候该如何思考,以及如何引入新技术。
|
||||
|
||||
构建一个新的系统架构,因为没有历史包袱,无论是在技术选型还是在开发部署上都比较方便。但是,“老”的应用服务由于已有的运行业务和原有技术架构的包袱,考虑的事情就比较多了。
|
||||
|
||||
Serverless也是如此,它在开发、测试、部署、运维等方面跟传统服务的形态不一样,近些年来,Serverless形态的PaaS产品越来越多,选择合适的业务、通过合适的途径来迭代我们的服务,也成为了一件复杂的工程。
|
||||
|
||||
今天这节课,我将结合与客户打交道的沉淀,跟你一起来聊聊传统服务迁移到Serverless技术架构的经验,最后完成一次迁移的实操。希望通过这样的方式,让你在做技术决策的时候,对“用与不用”“自建还是上云”“用一个或多个功能”等等这些细节都有一个相对全面的了解。
|
||||
|
||||
你真的需要迁移吗?
|
||||
|
||||
通常来说,但凡要做技术改造或者迁移,无非出于以下3个因素。
|
||||
|
||||
|
||||
架构:现有的技术架构不足以支撑业务的流量增长,“系统扛不住”了;
|
||||
人员:人手不够,可能是人的数量不够、人的知识储备不够,“人扛不住”了;
|
||||
成本:归根到底,还是“钱扛不住”,尤其是初创公司或是大公司里面的创新团队,业务还没起来,预算就那么多,该怎么办?
|
||||
|
||||
|
||||
那么,只要是遇到这三个问题,就要毫不犹豫地迁移吗?在迁移之前,我建议你先回顾一下实战指南中的选型建议,再沉下心来想清楚两个问题。
|
||||
|
||||
第一,这是否是一个盲从的选项?
|
||||
|
||||
对业务方同学来说,不管是用云厂商还是企业内部的平台,你的技术栈都是跟着平台走的,极度依赖平台的服务配套和稳定性。也许我们能够快速上线,但却失去了对服务器的管控权。
|
||||
|
||||
另一方面,代码也是交出去的,那么,代码和数据安全性的保障问题有后备方案了吗?云厂商也不是没有发生过数据安全故障的问题,这也是如金融银行这类企业都需要私有化部署Serverless平台的核心原因之一。
|
||||
|
||||
第二,你的迁移会对技术、业务和团队造成什么影响?
|
||||
|
||||
|
||||
技术方面,是整体改造迁移?还是部分改造迁移?还是先在新架构使用,老业务再逐步过渡?
|
||||
业务方面,要从可持续发展的角度衡量,改造一个系统架构,要么使得现有业务在资源耗费上的成本更小(也就是维系现状的同时降低成本),要么以更优的方式支撑流量上涨的业务,否则,改造都是徒劳的。
|
||||
团队方面,人才和知识储备是否足够?遇到问题了可以Hold住吗?
|
||||
|
||||
|
||||
多问几个为什么,也许你就可以在改造Serverless的道路上少走不少坑。想清楚了这些问题,我们就可以开始讨论具体的方案了。
|
||||
|
||||
迁往何处?
|
||||
|
||||
我们先来说个迁移的例子。现在有一个中小公司的管理网站系统,希望进一步节省网站费用,网站原来部署在云服务托管应用上,访问流量时有时没有。
|
||||
|
||||
想一想,面对Serverless形态的容器服务、应用托管服务、函数计算等具象化的产品技术,我们该如何抉择呢?
|
||||
|
||||
是否迁移到容器服务?
|
||||
|
||||
Serverless的容器服务,跟普通的K8s容器服务的主要区别在于是否按量付费、是否节点可见、是否需要自己来做容量规划。
|
||||
|
||||
|
||||
|
||||
如果你的服务本来就部署在容器服务上且有弹性诉求,那么迁移到Serverless容器服务也不失为一个明智之举。如果团队K8s的运维经验不是很丰富,那么,就要着重考虑函数计算和弹性应用了。
|
||||
|
||||
是否迁移到函数计算服务?
|
||||
|
||||
还记得我们在运行时课程中讲到的,函数计算平台支持标准的运行时和自定义运行时吗?下面,我们就针对迁移的初衷,看看如何把运行时“用”起来。
|
||||
|
||||
|
||||
标准运行时
|
||||
|
||||
|
||||
云厂商内置的运行时其实已经能够满足大多数业务场景的代码运行了,尤其是不会涉及到复杂交互流程的一些事件驱动场景的迁移。比如离线的机器学习任务,有时仅需要将原本训练逻辑的启动方法放在函数计算入口,即可完成迁移。
|
||||
|
||||
下面的一段伪代码可以帮助你理解得更清晰一些。
|
||||
|
||||
def handler(event,context):
|
||||
result = training()
|
||||
return result
|
||||
|
||||
def training:
|
||||
...
|
||||
|
||||
|
||||
|
||||
自定义运行时和自定义镜像
|
||||
|
||||
|
||||
自定义运行时和自定义镜像是函数计算内置运行时的一种延伸能力,能够为用户提供更加灵活的服务接入模式。
|
||||
|
||||
自定义运行时允许用户不必按照严格的事件驱动模型进行编程。也就是说,用户不需要使用handler(event,context)作为函数计算入口定义,可以打造更加个性化的编程环境,比如一些常见的Web 框架,都可以直接在自定义运行时中启动。
|
||||
|
||||
相较于自定义运行时,自定义镜像更加自由。它支持将整个环境打包成一个容器镜像的方式部署。
|
||||
|
||||
总的来说,这二者都更加适合于一些迁移到常规运行时比较麻烦的场景。比如一些Web 框架有固定的HTTP处理接口,无法再按照标准的事件传参处理。这个时候使用自定义运行时或者自定义镜像,就可以使用一些定制化的服务启动方式来部署了。
|
||||
|
||||
除此之外,函数计算的衍生能力也能帮助你处理一些更加复杂的场景,让函数计算应对的场景更加丰富。比如通过函数的连接器能力和异步策略来完成一些音视频转码任务的迁移,通过函数工作流迁移一些状态转换的任务场景等等。一些函数计算服务考虑到业务之间功能的拆分,还提供了应用中心的管理概念。
|
||||
|
||||
是否迁移到应用托管服务?
|
||||
|
||||
如果你了解微服务在云PaaS上发展的历程,应该不难理解,面向应用的Serverless PaaS平台,就是在原来的微服务PaaS平台基础之上的演进版本,在原有的支持多语言、多框架、微服务治理能力的基础上,提供了全托管、免运维、按需使用、按量计费的特性。
|
||||
|
||||
如果你的服务本来就是一个基于类似Springboot、Dubbo、HSF的微服务框架,那么使用云厂商的这种Serverless的产品也是不错的选择。
|
||||
|
||||
最后,我来回答一下管理网站系统案例的问题,这位客户对性能要求不高,主要的诉求还是在实例常驻的成本上,而目前Serverless托管服务最小实例为1,不能缩容到0。所以,FaaS对于这样的客户来说比较合适。你自己在做服务改造和迁移的时候,也要把“成本”作为一个不可忽视的因素好好规划一下。
|
||||
|
||||
如何迁移?
|
||||
|
||||
说完了迁移的考虑和产品技术选型,下面,我们就以Web服务为例,在阿里云函数计算FC上实际感受一下传统服务迁移的过程。我们分别来看简单HTTP的服务迁移和高级Web框架迁移。
|
||||
|
||||
HTTP服务迁移
|
||||
|
||||
对于一般的简单HTTP服务,通过函数计算提供的HTTP触发器,基于标准的运行时就可以运行起来了。
|
||||
|
||||
|
||||
HTTP请求是如何到达业务代码的?
|
||||
|
||||
|
||||
Web请求的入参通常都是一个标准的HTTP Request 信息,包括HTTP请求头、传参等,而我们一直所用的云函数在事件触发场景下都是处理一个JSON 类型的事件字符串。因此,Web服务并不能直接套用事件驱动的这套编程模型。
|
||||
|
||||
我们在创建函数时就会发现,FC提供了两种请求处理程序的类型,处理事件请求和处理HTTP请求。
|
||||
|
||||
|
||||
|
||||
其中“处理事件请求”就是我们平时使用最多的事件驱动类型,“处理HTTP请求”则需要在入口方法提供标准的HTTP处理接口。不知道你还记不记得我们在分析GoLang Runtime运行时的时候,提到过一个funcType的分支判断,这里就用得上了。GoLang的运行时会根据函数的类型来选择具体的执行方法,也就是下面这部分运行时代码:
|
||||
|
||||
// 运行时中分类处理的逻辑代码
|
||||
if fn.funcType == eventFunction {
|
||||
return fn.invokeEventFunc(invokeContext, req.Payload, response)
|
||||
}
|
||||
return fn.invokeHttpFunc(invokeContext, req.HttpParams, req.Payload, response)
|
||||
|
||||
|
||||
在处理HTTP请求类型的函数中,需要将函数入口按照定义好的接口实现,这个接口包括了请求信息、响应信息以及上下文信息,比如FC官方SDK中给的HTTP函数Demo:
|
||||
|
||||
func HandleHttpRequest(ctx context.Context, w http.ResponseWriter, req *http.Request) error {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
w.Write([]byte("hello, world!\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
在invokeHttpFunc中,最终会通过透传的方式,将HTTP请求内容直接交到你刚才定义的这个方法中,即HandleHttpRequest,对应下面代码中的httpHandler:
|
||||
|
||||
// 运行时中执行http handler的逻辑
|
||||
func (fn *Function) invokeHttpFunc(invokeContext context.Context, httpParams *string,
|
||||
reqPayload []byte, response *messages.InvokeResponse) error {
|
||||
...
|
||||
err = fn.httpHandler.Invoke(invokeContext, resp, req)
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
这样,我们就完成了一次完整的HTTP请求传递过程。
|
||||
|
||||
|
||||
动手体验
|
||||
|
||||
|
||||
那么接下来,我们就要开始简单HTTP服务的迁移了。我们以Golang Runtime运行时为基础来创建函数,使用官方的示例代码作为参考。选定请求处理程序类型为“处理HTTP请求”。平台会自动为你创建一个HTTP触发器,我们可以点击触发器管理查看:
|
||||
|
||||
|
||||
|
||||
需要注意一下,如果你需要公网访问,还需要点击编辑配置域名。
|
||||
|
||||
最后我们更新一下代码。我这里是将请求中的geekbang query 参数打印到了响应信息中,在具体的迁移时,你也可以在获取到请求信息后根据实际需求处理业务,如下面代码中注释的函数doSomething()一样:
|
||||
|
||||
package main
|
||||
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
|
||||
"github.com/aliyun/fc-runtime-go-sdk/fc"
|
||||
)
|
||||
|
||||
|
||||
func main() {
|
||||
fc.StartHttp(HandleHttpRequest)
|
||||
}
|
||||
|
||||
|
||||
// func doSomething(){}
|
||||
|
||||
|
||||
func HandleHttpRequest(ctx context.Context, w http.ResponseWriter, req *http.Request) error {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
w.Write([]byte(req.URL.Query().Get("geekbang")))
|
||||
|
||||
|
||||
//doSomething()
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
然后编译、打包上传。最后的效果如下:
|
||||
|
||||
|
||||
|
||||
高级Web框架迁移
|
||||
|
||||
前面我们提到,如果你的老应用服务是基于高级的服务框架进行的,那么可以通过自定义运行时、自定义镜像、弹性应用这些更“自由”的服务模式来迁移。
|
||||
|
||||
以阿里函数计算FC为例,官方就推荐采用自定义运行时的方式来进行高级的框架迁移:
|
||||
|
||||
|
||||
|
||||
我们以Golang中常用的Gin框架为例,选择自定义运行时创建,运行环境选择Go,同时选择示例代码。
|
||||
|
||||
|
||||
|
||||
完成创建后,我们开始用Gin编写一段代码。你可以参考我的示例,在Gin中,通过自定义的方法来对请求进行处理,我这里定义了一个geekHandler处理访问/geekbang路径的请求:
|
||||
|
||||
package main
|
||||
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
router.LoadHTMLGlob("templates/*")
|
||||
router.GET("/geekbang", geekHandler)
|
||||
router.Run(":9000")
|
||||
}
|
||||
|
||||
|
||||
func geekHandler(c *gin.Context) {
|
||||
c.String(http.StatusOK, "do something")
|
||||
}
|
||||
|
||||
|
||||
你会发现,这和你原本服务的代码形式几乎没有什么区别,在实际迁移时,如果你本身的服务采用的是Gin框架,甚至只需要在控制台配置一下启动命令,就可以几乎原封不动地将二进制部署上去,是不是很方便呢?
|
||||
|
||||
本地编写完成后,就需要编译然后将二进制打包上传了。这里要注意的是,在前面创建函数的部分,我们采用的是默认的启动命令./main,那么你的二进制产出也需要和它对应。当然,你也可以指定一个产出名,如果你是用非Linux 环境编写的话,还需要在本地采用交叉编译的方式进行编译:
|
||||
|
||||
GOOS=linux GOARCH=amd64 go build -o main main.go
|
||||
|
||||
|
||||
如上面的代码所示,GOOS和GOARCH会指定Linux操作系统与amd64架构,而-o 可以用来指定产出,最后的参数是待编译的文件。
|
||||
|
||||
最后部署完成以后,我们来看一下实际的运行效果:
|
||||
|
||||
|
||||
|
||||
回想一下管理网站系统案例的场景,如果还涉及到自定义的域名,还可以通过FC的控制台,通过“域名管理”的能力,进一步添加自定义域名。又或者,你还可以将该自定义域名作为源站域名,为其添加加速域名,然后为加速域名配置CNAME,也就是域名设置CDN加速功能。
|
||||
|
||||
效果评估
|
||||
|
||||
管理网站系统其实是一个真实的案例。通过从原来常驻的资源应用迁移到Serverless函数计算,客户的费用降低了10倍。相信现在的你已经能够想清楚原因了:他的服务属于中长尾应用,按需付费极大地降低了客户的成本。
|
||||
|
||||
那么,我们是否有一个通用的准则来评估迁移的效果呢?回到开头,但凡要做技术改造或者迁移,无非出于架构、人员、成本三个主要因素。除去这三个方面,迁移后的架构,还可以通过测试系统的弹性和抗洪峰等能力来评估其是否真正得到了增强。
|
||||
|
||||
我们的做法通常是在上生产环境、对外放开流量之前,通过全链路压测的方式尽可能仿真线上的流量升降情况,来看资源池的调度、请求并发和响应等是否符合预期。这里需要注意一下,有的云厂商默认的函数和用户维度的并发度有限,如果流量超过默认限制,还需要提前发工单提升配额。
|
||||
|
||||
具体来说,基于Serverless平台的特性,你的压测可以从三个点着手。
|
||||
|
||||
|
||||
全链路:针对链式调用,你不能只关注某一个函数的压测结果,想一想,整条链路里有没有瓶颈存在?
|
||||
模拟真实:尽可能贴近业务,测试平台的资源调度是否跟得上你的诉求。不同的厂商优化的参数可能不同,你选择的云厂商平台是否可以胜任呢?
|
||||
高覆盖:尽可能多地覆盖流程分支,在上线之前发现潜在的问题。
|
||||
|
||||
|
||||
除此之外,我们还可以关注一下团队的人员是否更高效,最终的体现,一定是原本的预算足够应对流量的上涨了。
|
||||
|
||||
小结
|
||||
|
||||
好了,以上就是传统服务迁移的全部内容。相信你现在对迁移涉及到的四件事情也非常清楚了:想清楚要不要迁移,迁到哪类Serverless的平台,改造方法和成本以及迁移效果评估。那我们该如何将这四个要点落实到日常的业务处理中呢?
|
||||
|
||||
在是否迁移方面,我们今天提到的案例是出于财务压力,考虑到客户的业务比较轻量,几乎没有改造成本,就非常适合迁移到函数平台。而你可以从是否要将控制权交给云厂商、业务是否符合Serverless特性、架构改造成本、团队技术储备等多个方面考量。
|
||||
|
||||
在迁移到哪类平台方面,就需要你深入了解云厂商Serverless形态的产品了。今天比较主流的Serverless容器、函数、应用托管服务,明天可能又会新增更多的Serverless产品出来,而我们能做的,就是找到业务、架构与产品形态三者的最佳匹配状态。比如,如果你使用了Springboot、Dubbo框架,需要服务治理的能力,那么Serverless的应用托管服务,如SAE这类产品就是你应该考虑的首选。
|
||||
|
||||
在改造成本和方法上,云厂商都提供了不错的迁移适配能力,你可以通过开发工具实现Java、Node.js、Python等各种语言框架的迁移。这里要注意的是,虽然你可以通过改造代码以及HTTP触发器来实现低成本的迁移,但是,我们大部分情况下都是基于某种框架来开发的Web服务,所以我更建议你使用自定义运行时的方式迁移。如果你想完全控制容器运行的环境,那么镜像的迁移方式也是不错的。
|
||||
|
||||
最后,别忘了你迁移的初衷,记得做效果的评估。你可以通过架构、人力、成本这迁移三要素来评估,从Serverles的角度,一切都可以从“降本”“增效”这两个关键词出发,考量迁移是否达到了你的预期效果。
|
||||
|
||||
思考题
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个思考题。
|
||||
|
||||
你有没有准备要迁移的服务呢?你做了哪些技术调研?有遇到什么卡点么?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。
|
||||
|
||||
感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起交流学习。
|
||||
|
||||
|
||||
|
||||
|
187
专栏/Serverless进阶实战课/22私有云:赛马时代的Serverless核心引擎谁能胜出?.md
Normal file
187
专栏/Serverless进阶实战课/22私有云:赛马时代的Serverless核心引擎谁能胜出?.md
Normal file
@ -0,0 +1,187 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
22 私有云:赛马时代的Serverless核心引擎谁能胜出?
|
||||
你好,我是静远。
|
||||
|
||||
Serverless平台的实现分为两个流派,分别为公有云和私有化。前面的几节实战课,我们主要基于公有云的Serverless平台来展开,从这节课开始,我将带你一起了解私有化部署领域的知识。
|
||||
|
||||
为什么要做私有化部署呢?相信你回答这个问题的时候一定是有些犹豫的。
|
||||
|
||||
我们知道,Serverless由云厂商运维并提供服务,带来高效率开发的同时,也大大降低了成本。但就如我上一节课给你讲到的那样,我们享受这种“好处”的同时,也失去了对平台的掌控力,面临着数据的安全问题。
|
||||
|
||||
我的很多私有化客户也是基于这两个核心因素,找到我们来共建企业内的Serverless平台。这对于企业的业务团队来说,可以同样的享受Serverless带来的价值,只是这一层的维护工作由原来的云厂商交到了企业内部的基础平台运维团队。
|
||||
|
||||
那么,私有化的部署通常是怎么实现的,部署的方式都有哪些呢?希望这节课,能够让你对私有化部署的方法,开源引擎的发展现状以及选取一个开源引擎作为Serverless平台底座的方法有一个通盘的了解。
|
||||
|
||||
私有化部署的方法有哪些?
|
||||
|
||||
当前绝大多数的Serverless平台底座都是基于容器技术来实现的,而容器技术在近几年的日趋成熟和盛行,也使得云厂商自研的Serverless平台、开源的Serverless引擎在私有化企业的数据中心部署变得相对容易了。
|
||||
|
||||
不同的企业,在私有化这里一般有两种做法。
|
||||
|
||||
|
||||
自研:一些基础设施团队能力很强的企业,一般会选择自己研发。有的企业会基于开源的Serverless引擎做二次开发,有的直接基于已有的IAAS和PAAS资源自己研发,并配备专业的团队进行升级和维护。
|
||||
购买:还有一些企业,会选择ToB的云厂商合作共建,基于云厂商售卖的Serverless标品来适配和升级。比如金融银行领域,你可以通过一些公布的文稿发现,类似中国工商银行、光大银行等公布的招标网站上,采用了百度智能云的Serverless FaaS产品来进行私有化部署升级。
|
||||
|
||||
|
||||
目前来看,选择“购买”的企业是占大多数的。传统企业和ToB云厂商合作共建,传统企业有上云Serverless化的诉求,ToB云厂商有丰富的实战经验和技术,两者形成合力。
|
||||
|
||||
但这并不代表购买方可以一劳永逸,不需要掌握Serverless的技术。
|
||||
|
||||
一方面,你需要提前了解云厂商是基于什么样的Serverless引擎开发的,这个是核心。因为云厂商除了自研平台外,基本上都是基于开源的Serverless引擎(Self-hosted Frameworks)来进一步打造和适配的。另一方面,你还需要了解整套解决方案是如何围绕这个核心构建起来的。这节课,我们先说Serverless引擎的部分。
|
||||
|
||||
|
||||
|
||||
你可以从CNCF Serverless LandScape了解到引擎和平台的发展情况。左侧的“Hosted Platform(托管平台)”包括我们提及的AWS、阿里云FC、百度智能云CFC、华为FunctionStage、Azure Functions、腾讯云SCF等企业的Serverless托管平台。而右侧“Installable Platform(可安装平台)”包括我们平时关注度比较高的Knative、OpenWisk、Fission、OpenFaaS等开源框架。
|
||||
|
||||
那么它们具体有什么特点,我们又该如何选择使用呢?
|
||||
|
||||
开源引擎现状与选择
|
||||
|
||||
我们先来看2020年CNCF的报告,感受一下开源Serverless的使用情况:
|
||||
|
||||
|
||||
|
||||
我们可以发现35%的调查对象有使用“installable software”,在使用“installable software”中有27%使用Knative、10%使用OpenFaaS、5%使用Kubeless。随着时间的推移,现在已经有一些开源平台没能经受得住时间的考验,比如Fn、Iron Functions已经逐步退出历史舞台了,图中的Kubeless在今年年初也宣布停止维护了。
|
||||
|
||||
这里,我参考CNCF Serverless LandScape和CNCF调研报告,并基于受欢迎程度、稳定贡献、工具和易用性等方面,选取了Knative、OpenFaaS、OpenWhisk、Fission这四款开源的平台来跟你介绍。需要说明的是,Nuclio也是不错的开源框架,但它是专注于数据、I/O和计算密集型的业务处理场景的,且自身不考虑缩容至零的问题。所以暂时不去探讨。
|
||||
|
||||
|
||||
OpenWhisk也算元老级的开源Serverless框架了,目前代码库也有一定的commit提交,但发展速度并不如其他几款,在调研图中显示,仅有3%的Serverless 用户选择了该款产品,远低于Knative和OpenFaaS,而且自2020年1月以来到现在,它还没有发布过一个新版本。
|
||||
Knative虽然是2018年发布的开源Serverless产品,但发展迅速,依托谷歌和Kubernetes的紧密结合,短短几年间Github 上Star的总量已经接近1万,图中显示已经有27%的Serverless 用户选择了它。目前Knative也成为了CNCF的孵化项目。
|
||||
OpenFaaS可以说是发展最稳定的一款了,到目前仍然有稳定的commit提交,并且Star数是所有开源Serverless平台中最高的,总量接近3万。调研图显示有10%的用户支持。
|
||||
Fission在调研报告中虽然仅有2%的用户支持,但它目前仍有稳定的贡献者和版本发布,依然很活跃。
|
||||
|
||||
|
||||
这四个Serverless开源平台,我从实现语言、支持的编程语言、贡献人数等12个方面做了概览对比,方便你进一步查阅。
|
||||
|
||||
|
||||
|
||||
开源引擎的不同
|
||||
|
||||
从CNCF报告、各个开源Serverless平台的官方总结这些对比项之后,你可能很快就能抉择一个开源平台作为自己研究和构建的对象了。但这些数据终究是纸上谈兵,我们还可以再深入一点,我会从Serverless的核心特征“扩缩容机制”和相关使用限制上给你几点参考建议。
|
||||
|
||||
|
||||
Fission的扩缩容机制及限制
|
||||
|
||||
|
||||
Fission有Pool-based和New Deploy两种执行模式,能够支持资源池以及从0到1两种扩容模式,官方更是宣称冷启动速度控制在了百毫秒以内,这对于一些延迟敏感型场景显然是不错的选择。而它的限制就在于只支持通过CPU指标来控制实例数量,这就导致很多Web场景下并不能很好地感知实际的资源用量。
|
||||
|
||||
|
||||
OpenWhisk的扩缩容机制及限制
|
||||
|
||||
|
||||
不同于Fission,OpenWhisk的扩缩容更加简单。每次请求到来后会由invoker创建一个业务容器,然后进行代码注入,执行,执行完毕后退出并销毁容器。整个流程并不会产生冗余的资源,但如果应对一些高并发场景效果可能并不会那么理想。
|
||||
|
||||
正如上面的表格所示,它的局限性在于依赖的组件较多,不管你是开发者还是使用者都需要有这些工具的储备知识,如果你希望做二次开发,那要先想一想是不是熟悉Scala?否则的话,可以考虑其他三个以Go语言为主的开源框架。
|
||||
|
||||
|
||||
Knative的扩缩容机制以及限制
|
||||
|
||||
|
||||
我们之前在扩缩容这节课介绍过Knative KPA。Knative本身的KPA扩缩容逻辑比较复杂,除了AutoScaler和Activator这两个Serving组件外,还需要向用户Pod注入一个queue-proxy容器,以此来进行实时的请求量统计。基于这些组件,除了能够实现从0到1、1到N、N到0的扩缩容,还能支持在Stable稳定模式和Panic恐慌模式两种模式下的扩缩容算法。
|
||||
|
||||
那么它有什么不足呢?Knative完全依赖Kubernetes,由于发版很快,高一点的版本就需要1.22以上的Kubernetes版本,但有的企业升级没那么快,需要配套支持。除此之外,可扩展的网络组件,也是一把双刃剑,带来便利的同时也需要运维人员提前具备这一方面的知识,比如Istio。可以发现,它对于企业、技术人员的要求都不低。
|
||||
|
||||
|
||||
OpenFaaS的扩缩容机制以及限制
|
||||
|
||||
|
||||
OpenFaaS Pro完全将扩缩容的发起交给了Prometheus。OpenFaaS通过Prometheus获取到监控的指标,再由AlertManager根据配置参数进行扩缩容请求,最终让资源达到预期的值。目前OpenFaaS支持rps、capacity和cpu三种扩缩容模式,并且可以通过com.openfaas.scale.zero来控制是否可以缩容到0。
|
||||
|
||||
然而,OpenFaaS依赖Promethues和配置参数控制扩缩容到0的这两个特性,也可以算是它的限制。这两个特性导致它完全依赖Promethues的稳定性和负载,也默认关闭了缩容至0的功能,当然,你也可以打开,不过会带来一定的冷启动时间。
|
||||
|
||||
总得来说,目前Knative、OpenWhisk以及OpenFaaS都能够基于请求数量进行动态伸缩,而Fission目前只支持通过CPU指标来控制实例数量。相比于Fission和OpenWhisk,OpenFaaS与Knative的扩缩容模式就丰富了许多。
|
||||
|
||||
如果你的场景比较简单,可以快速体验一下OpenWisk,如果对性能要求比较高,Fission和Knaitve都是不错的选择。它们之间的界限并不是不可逾越的,我们在选择的时候可以更灵活机动一些。
|
||||
|
||||
你的选择
|
||||
|
||||
有了上面这些分析,相信你选择的大方向已经有了,下面我再给你几点技术以外的经验参考:
|
||||
|
||||
|
||||
考虑你公司或者团队的技术栈,如语言、现有的服务治理体系、中间件等;
|
||||
考虑你的业务性质,是专门做数据计算、还是Web服务,还是都要兼顾等等;
|
||||
考虑未来趋势,结合目前开源框架的成熟度和产品力来衡量,看产品的潜力。比如Knative,背靠Google和CNCF,和Kubernetes紧密结合,随着时间的推移,未来可期。
|
||||
|
||||
|
||||
购买Serverless产品需要注意什么?
|
||||
|
||||
最后,我们还可以说说在“购买”这个方式中,作为“甲方”或者“乙方”的你,应该分别具备哪些知识储备。
|
||||
|
||||
如果你是甲方,一定要有基础技术方面的储备,比如一定的云原生技术基础储备,包括Docker、Kubernetes、Service Mesh、可观测等,具备云原生的思维;在Serverless方面,了解竞标厂商的产品功能、成功案例和落地规模,知道在POC的环节需要验收哪些要点;最后,也要具备架构思维,明确哪些功能点在一期进行,哪些在二期进行,在合理的架构范围内有节奏地迭代。
|
||||
|
||||
具备上面的必要储备后,和产品方交流沟通起来就不会磕磕碰碰了。当然,积极上进的你如果能掌握招标、合同、损益评估和入场等注意事项,就能真正称得上这方面的“老手”了。
|
||||
|
||||
如果你是乙方,还记得我在思维构建这节课跟你提到的“升级打怪”的路径么?你必须达到“王者”阶段,达到产品和组件输出的能力,并且能够在POC环节胜出。我列了几个要点,方便你对照着来看。
|
||||
|
||||
|
||||
技术知识:如我之前所说,“加入一个云原生Serverless团队,在里面打磨一万小时以上,扎扎实实搞清楚每个点”,只有这样,你才有可能解决掉在产品交付过程中遇到的问题。假如你有部署私有化产品的经历,你一定理解这句话的含义,甲方的环境有时确实不会那么“如你所愿”。
|
||||
产品思维:对于基础服务类产品,闭门造车肯定是不行的。你必须要基于一个“标品”来打造一个Serverless产品方案,然后基于和甲方的交流,找到可适配的架构产品方案。比如,在IAM的适配上,完全可以独立一个模块单独构建认证鉴权的服务,通过配置化的方式来适配不同的企业。通过“独立标品”+“插拔适配”的方式构建产品,才更适合私有化部署。
|
||||
软技能:甲方通常在尝试新技术落地的时候,自己也没有太多的经验,你需要有足够的耐心来帮助和引导他们交付和落地场景,这样才有后续的“二期”“三期”“周边产品”等再次合作。
|
||||
|
||||
|
||||
其实,不管是自研还是二次开发,不少企业也借鉴了这些Serverless开源引擎的思想,所以,研究一款开源产品,就相当于研究一个云厂商的服务,不同的是云厂商还融入了一些自己专业的技术特性和底座服务。
|
||||
|
||||
希望这些我和私有化打交道的心得,能够帮助作为甲方或者乙方的你,也欢迎你在留言区进一步与我探讨。
|
||||
|
||||
小结
|
||||
|
||||
最后,我来小结下今天的内容。私有化部署的选择,主要是出于数据安全和平台掌控力两方面的考量。考虑到私有化部署的Serverless平台本身的门槛,一般会有“自研”和“购买”两种部署方式。一些基础设施团队能力很强的企业会选择前者,一些企业会选择较为方便的后者。
|
||||
|
||||
目前来看,选择“购买”的企业占比较大。那么,假如你是甲方,就需要储备适当的云原生知识,掌握Serverless的核心要点和架构思维,这会让你在沟通和选择合作厂商的时候更加游刃有余。假如你是乙方,就要在技术、产品和软技能上面都要过关,才有可能获得甲方的认可,拿到这一笔单子。
|
||||
|
||||
而我们了解私有化部署最关键的途径,就是去学习开源的Serverless平台,并基于此来构建和实操。因此,我们综合了CNCF的调研、热度、贡献度稳定性等多个指标,选取了四个开源框架OpenWhisk、OpenFaaS、Fission、Knative来做深入对比。Knative作为后起之秀,依托谷歌和CNCF,和Kubernetes的紧密结合,有“跨平台的函数计算平台”的响亮称号,可以说是未来可期。
|
||||
|
||||
一直以来,在了解一款产品特性的方面,我的建议都是多多动手实操,再回头看看官网最新的特性。如果再结合课程中的一些思路,找到一条自己学习新框架和新技能的道路,就说明你已经迈入一个新的台阶了。
|
||||
|
||||
下节课,我们就进入到动手实操的环节,以Knative为例,看看如何从0到1部署Serverless核心引擎。
|
||||
|
||||
思考题
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个思考题。
|
||||
|
||||
针对上述提到的Serverless引擎框架,你在研究过程中最关心哪个点呢?你有什么新的发现吗?
|
||||
|
||||
感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起阅读。
|
||||
|
||||
补充阅读
|
||||
|
||||
链接1,OpenWhisk
|
||||
|
||||
链接2,knative
|
||||
|
||||
链接3,Fission
|
||||
|
||||
链接4,OpenFaaS
|
||||
|
||||
链接5,5.8k stars
|
||||
|
||||
链接6,4.6k stars
|
||||
|
||||
链接7,7.2k stars
|
||||
|
||||
链接8,22.1k stars
|
||||
|
||||
链接9,Serverless Framework
|
||||
|
||||
链接10,JavaScript、Python、PHP、.NET、Go、Java、Ruby、Swift、二进制
|
||||
|
||||
链接11,NodeJs、Python3、Ruby、PHP7、Java、Go、.NET、Perl、二进制
|
||||
|
||||
链接12,支持Go、NodeJs、PHP、Python、Ruby、Java、C#等语言的模板,并打包成Docker镜像
|
||||
|
||||
链接13,https://openwhisk.apache.org/
|
||||
|
||||
链接14,https://knative.dev/docs/
|
||||
|
||||
链接15,https://fission.io/
|
||||
|
||||
链接16,https://www.openfaas.com/
|
||||
|
||||
|
||||
|
||||
|
366
专栏/Serverless进阶实战课/23实战进阶(四):如何从0到1进阶一个开源引擎?.md
Normal file
366
专栏/Serverless进阶实战课/23实战进阶(四):如何从0到1进阶一个开源引擎?.md
Normal file
@ -0,0 +1,366 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
23 实战进阶(四):如何从0到1进阶一个开源引擎?
|
||||
你好,我是静远。
|
||||
|
||||
从这节课开始,我们就要完整体验一个Serverless平台的构建,学习其中的注意事项了。
|
||||
|
||||
今天的前半节课,我们会从环境准备、组件安装以及组件验证几个角度先重点演练一遍Knative核心组件的部署和验证,后半节课,我们再去探讨一下,在面临一个开源Serverless框架的时候应该怎么去学习。
|
||||
|
||||
在实战开始之前,你可以先复习一下扩缩容和流量转发这两节课中Knative的知识点,再来开始动手实操。
|
||||
|
||||
话不多说,我们现在就开始实操准备吧。
|
||||
|
||||
环境准备
|
||||
|
||||
由于Knative本身完全依赖于Kubernetes,因此我们需要提前准备一个Kubernetes集群,对于集群的规格,官网也有建议,你可以参考最新版本来进行。
|
||||
|
||||
原型环境方面,如果只是搭建一个试用的原型Knative函数环境,那么至少需要一个单节点K8s集群,并且这个节点的规格至少应该是2核4GB。
|
||||
|
||||
如果想达到生产级别的Knative函数环境,单节点集群的规格至少应该是6核6G以及30G的磁盘空间,多节点环境的规格应该保证每个节点都是2核4G以及20GB的磁盘空间。另外,还有几个要点需要注意:
|
||||
|
||||
|
||||
必须保证K8s集群版本应该至少是1.22以上;
|
||||
必须安装K8s的命令行访问工具kubectl;
|
||||
必须保证K8s能够正常访问到镜像仓库。
|
||||
|
||||
|
||||
这节课,我会按照生产级别环境搭建,其中K8s集群节点数量为2,每个节点的规格为4核8G,同时配备50GB磁盘空间。我选择了百度智能云CCE来构建K8s集群。
|
||||
|
||||
组件部署与验证
|
||||
|
||||
准备好K8s集群环境以后,我们就可以开始在集群上部署Knative Serving组件了。不要着急,我们一步步来。
|
||||
|
||||
部署Serving组件
|
||||
|
||||
首先,我们来部署Serving组件,按照官网命令即可:
|
||||
|
||||
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.4.0/serving-crds.yaml
|
||||
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.4.0/serving-core.yaml
|
||||
|
||||
|
||||
部署之后,在knative-serving这个namespace下,所有的Pod都应该是Running状态:
|
||||
|
||||
$ kubectl get pod -n knative-serving
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
activator-847d5df4bc-psd7k 1/1 Running 0 47h
|
||||
autoscaler-65898c7fcf-wkkmc 1/1 Running 0 46h
|
||||
controller-775b774844-fwm2k 1/1 Running 0 46h
|
||||
domain-mapping-555ff9b9c8-8dtcw 1/1 Running 0 46h
|
||||
domainmapping-webhook-57456fcb8c-x6frp 1/1 Running 0 46h
|
||||
net-istio-controller-59b6b7765-z8w2g 1/1 Running 0 46h
|
||||
net-istio-webhook-6b98fd5bd4-4pcdf 1/1 Running 0 46h
|
||||
webhook-7f987868-5f8bg 1/1 Running 0 46h
|
||||
|
||||
|
||||
|
||||
如果出现异常,那么可以通过kubectl describe 查看Pod部署卡在哪一步了,你可以参考如下的命令查看:
|
||||
|
||||
kubectl describe pod controller-744577dddc-7jf5r -nknative-serving
|
||||
|
||||
|
||||
注意,如果是网络问题,可能会出现下载镜像卡住的情况,这个时候通过Google Cloud虚机下载镜像,上传到可访问的镜像仓库上就可以,或者直接找到可供访问的镜像仓库,手工替换掉gcr.io的镜像包。
|
||||
|
||||
部署网络组件
|
||||
|
||||
完成Serving 组件的部署,我们就需要部署Knative所依赖的网络组件了。官网推荐了三种,我们以Isito来部署。
|
||||
|
||||
kubectl apply -l knative.dev/crd-install=true -f https://github.com/knative/net-istio/releases/download/knative-v1.4.0/istio.yaml
|
||||
kubectl apply -f https://github.com/knative/net-istio/releases/download/knative-v1.4.0/istio.yaml
|
||||
kubectl apply -f https://github.com/knative/net-istio/releases/download/knative-v1.4.0/net-istio.yaml
|
||||
|
||||
|
||||
完成之后,我们需要检查一下istio-system的namespace下pod是否正常:
|
||||
|
||||
$ k get pod -nistio-system
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
istio-ingressgateway-666588bf64-jpwgl 1/1 Running 0 46h
|
||||
istiod-56967d8fcc-d6vmr 1/1 Running 0 46h
|
||||
istiod-56967d8fcc-f46kr 1/1 Running 0 2d2h
|
||||
istiod-56967d8fcc-mmfkd 1/1 Running 0 46h
|
||||
|
||||
|
||||
同时还需要检查一下是否能够获得gateway的external-ip,用于后续的函数访问:
|
||||
|
||||
$ kubectl --namespace istio-system get service istio-ingressgateway
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
istio-ingressgateway LoadBalancer 10.0.142.236 182.61.144.199,192.168.0.2 15021:31462/TCP,80:30977/TCP,443:32684/TCP 2d2h
|
||||
|
||||
|
||||
这里需要说明一点,百度智能云CCE里,如果用户创建类型是LoadBalancer的Service,默认情况下,CCE会联动的创建BLB,并为此BLB绑定EIP。所以你可以看到表格中显示的EIP地址是182.61.144.199。
|
||||
|
||||
部署Eventing组件
|
||||
|
||||
接着我们再来安装一下Eventing的相关组件。Eventing的安装和Serving类似,包括两个yaml文件:
|
||||
|
||||
kubectl apply -f https://github.com/knative/eventing/releases/download/knative-v1.4.1/eventing-crds.yaml
|
||||
kubectl apply -f https://github.com/knative/eventing/releases/download/knative-v1.4.1/eventing-core.yaml
|
||||
|
||||
|
||||
执行完成后,我们来检查一下knative-eventing这个命名空间下的pod是否正常运行:
|
||||
|
||||
$ kubectl get pod -n knative-eventing
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
eventing-controller-c7bdccd6d-ktnmc 1/1 Running 0 13m
|
||||
eventing-webhook-7c49c58fcb-ng576 1/1 Running 0 13m
|
||||
|
||||
|
||||
如果部署成功的话,应该可以看到这个命名空间下有两个running状态的pod,如果出现Error,那么还是按照我们在小试牛刀这节课中讨论的方法,通过kubectl的describe命令查看错误原因,对症下药即可。
|
||||
|
||||
部署完Serving和Eventing,你会发现,是不是还缺少Tekton组件?我把它作为课后作业留给你来进一步演练,你可以按照同样的方式进行部署。需要注意的是,Tekton是从Knative的Building组件孵化出来的,目前功能已经相当丰富,涉及的组件较多,因此最好根据官网的安装手册一步一步进行部署,避免遗漏。
|
||||
|
||||
到这里,你的Knative组件已经可以正常运行了,接下来我们尝试执行一个简单的Demo验证一下。
|
||||
|
||||
验证函数调用
|
||||
|
||||
我们先验证一下Knative最基本的函数调用功能,确保Serving组件能够正常运行。这里我使用的是官网给的helloworld-go的例子进行演示,我将handler中的响应内容改成了如下的打印语句“Hello Serverless!”:
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Print("helloworld: received a request")
|
||||
|
||||
|
||||
fmt.Fprintf(w, "Hello Serverless!\n")
|
||||
}
|
||||
|
||||
|
||||
改完代码之后,我们接着进行镜像打包,这里需要注意下,如果你电脑是mac 的m1,需要加上 –platform linux/amd64 这个参数,如下所示:
|
||||
|
||||
$ docker --platform linux/amd64 build -t jingyuan/hello:v1 .
|
||||
|
||||
|
||||
然后,就可以将镜像推送到一个集群可以访问到的仓库中,这里我使用的是CCR,具体使用可以参照他的说明文档,接着,我们替换一下knative demo中service.yaml的image,然后执行kubectl apply就可以了。
|
||||
|
||||
apiVersion: serving.knative.dev/v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: helloworld-go
|
||||
namespace: default
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- image: jingyuan/hello:v1
|
||||
env:
|
||||
- name: TARGET
|
||||
value: "Go Sample v1"
|
||||
|
||||
|
||||
|
||||
我这里只使用了最简单的一些字段,你还可以在Knative Service中设置不同的revision以及流量比例。-
|
||||
最后会是什么样的效果呢?我们来一起看下:
|
||||
|
||||
curl -H "Host: helloworld-go.default.example.com" http://182.61.144.199
|
||||
|
||||
返回:
|
||||
Hello Serverless!
|
||||
|
||||
|
||||
这一步,如果你观察仔细的话,会发现在default 命名空间下,在调用之前是不存在helloworld-go函数的相关pod的,而在调用之后产生了,这其实就是第一次调用产生的冷启动现象了。
|
||||
|
||||
验证事件触发
|
||||
|
||||
最后,我们再来验证一下Knative的事件触发功能,确保Eventing的组件确实能够正常工作。这里我们采用官网给出的PingSource模型进行验证,我们首先创建一个用于接收事件的服务,名为 event-display。这里采用的是官网的Demo,它的deployment和service定义如下:
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: event-display
|
||||
namespace: default
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels: &labels
|
||||
app: event-display
|
||||
template:
|
||||
metadata:
|
||||
labels: *labels
|
||||
spec:
|
||||
containers:
|
||||
- name: event-display
|
||||
image: gcr.io/knative-releases/knative.dev/eventing/cmd/event_display
|
||||
|
||||
---
|
||||
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: event-display
|
||||
namespace: default
|
||||
spec:
|
||||
selector:
|
||||
app: event-display
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
|
||||
|
||||
接着创建一个 PingSource 对应:
|
||||
|
||||
apiVersion: sources.knative.dev/v1
|
||||
kind: PingSource
|
||||
metadata:
|
||||
name: test-ping
|
||||
namespace: default
|
||||
spec:
|
||||
schedule: "*/1 * * * *"
|
||||
contentType: "application/json"
|
||||
data: '{"message": "Hello geek!"}'
|
||||
sink:
|
||||
ref:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
name: event-display
|
||||
|
||||
|
||||
它的含义是根据Schedule定义的规则每分钟发送一次data指定的数据,发送的目的端是Sink.name指定的服务 event-display ,即我们前一步定义的Service。
|
||||
|
||||
接着,我们可以查看一下knative-eventing命名空间下的deployment,会发现多了一个pingsource的deployment,这就是事件的生产者,会根据 PingSource 对象的定义,产生事件并发送给对应的服务。
|
||||
|
||||
$ kubectl get deploy -nknative-eventing
|
||||
NAME READY UP-TO-DATE AVAILABLE AGE
|
||||
eventing-controller 1/1 1 1 3h36m
|
||||
eventing-webhook 1/1 1 1 3h36m
|
||||
pingsource-mt-adapter 1/1 1 1 3h36m
|
||||
|
||||
|
||||
然后我们查看刚才部署在default 命名空间下的pod ,并查看他的日志,可以看到每分钟打印的消息:
|
||||
|
||||
$ kubectl logs event-display-685f8f46f4-k5dbw
|
||||
2022/10/07 01:06:32 Failed to read tracing config, using the no-op default: empty json tracing config
|
||||
☁️ cloudevents.Event
|
||||
Context Attributes,
|
||||
specversion: 1.0
|
||||
type: dev.knative.sources.ping
|
||||
source: /apis/v1/namespaces/default/pingsources/test-ping
|
||||
id: caa8d0f9-f3bb-4c4d-a777-749241a9cb32
|
||||
time: 2022-10-07T06:52:00.301997865Z
|
||||
datacontenttype: application/json
|
||||
Data,
|
||||
{
|
||||
"message": "Hello geek!"
|
||||
}
|
||||
☁️ cloudevents.Event
|
||||
Context Attributes,
|
||||
specversion: 1.0
|
||||
type: dev.knative.sources.ping
|
||||
source: /apis/v1/namespaces/default/pingsources/test-ping
|
||||
id: a029df47-fd4b-4eb4-986d-75d6dd9814d0
|
||||
time: 2022-10-07T06:53:00.001908595Z
|
||||
datacontenttype: application/json
|
||||
Data,
|
||||
{
|
||||
"message": "Hello geek!"
|
||||
}
|
||||
☁️ cloudevents.Event
|
||||
Context Attributes,
|
||||
specversion: 1.0
|
||||
type: dev.knative.sources.ping
|
||||
source: /apis/v1/namespaces/default/pingsources/test-ping
|
||||
id: b4331a71-031f-4b3a-93cb-8764a0c5830e
|
||||
time: 2022-10-07T06:54:00.309563083Z
|
||||
datacontenttype: application/json
|
||||
Data,
|
||||
{
|
||||
"message": "Hello geek!"
|
||||
}
|
||||
|
||||
|
||||
到这里,我们就完成了Knative最基本的函数调用以及事件触发功能的验证,我们来总结一下。Knative的安装大致分为环境准备、组件部署以及功能验证三个部分,这也是安装任意一个Serverless框架的基本思路。
|
||||
|
||||
|
||||
环境准备:你需要在安装之前就在官网的安装手册上了解清楚相关内容,尤其要注意对资源、操作系统、依赖版本的限制,一旦没有注意到,可能往往问题到安装的最后一步才会暴露出来,费时费力。比如我们这个版本的Knative就明确要求K8s的版本要达到1.22以上,并且多节点集群下,单节点的规格也至少是2核4G。
|
||||
组件部署:这里我建议直接参照官方的安装手册进行,因为网上的很多技术博客写的并不全面,并且版本可能和你计划安装的也有差别。同时也要注意一些镜像的访问问题,受限于网络问题,需要你自己去寻找国内的镜像源进行替换才能成功。
|
||||
功能验证:完成组件安装后,最重要的还是进行功能验证,这一步我建议不要一上来就选用复杂的case测试,而是先用最简单的demo去验证,看看基本的功能有没有问题,保证基本的工作流程以后,再验证扩展功能,这样不仅可以让你在最短的时间内验证完组件的部署正确性,还能够降低问题的排查难度。
|
||||
|
||||
|
||||
如何学习一个开源serverless引擎
|
||||
|
||||
结合之前的理论学习和本节课的实操,相信你对Knative已经有了一个比较全面的认识了,但我相信,在你之后搭建Serverless引擎的过程中,还会遇到Knative更多的特性。课程中,我们不可能面面俱到,不过像是Knative中 Triggers、Sinks、CLI等知识点,你完全可以通过我们总结的方法举一反三的学习起来。
|
||||
|
||||
什么意思呢?
|
||||
|
||||
拿到一个开源框架,我的攻坚三步曲是“上手体验”“研究架构”“深入源码”,我们逐一探讨下。
|
||||
|
||||
上手体验
|
||||
|
||||
在接触到任何一个开源Serverless引擎时,我并不建议一开始就通过网上的资料去学习各种概念、架构原理以及源码分析,这对于刚接触到引擎框架的你来说几乎没有任何帮助,这些繁杂的概念反而会让你一头雾水。
|
||||
|
||||
相反,上手体验永远应该放在你学习一个框架的第一步。使用带来的直观感受能够帮助你更快地理解这个框架最基本的产品形态以及工作过程。
|
||||
|
||||
这节课我并没有上来就给你长篇介绍Knative的各种概念,而是直接手把手带你体验了从搭建Knative到跑起来两个demo的整个过程,动手之后,你基本就清楚整个Knative运作需要哪些组件,同时也能观察到一些现象(比如冷启动、事件触发过程)了。这样的操作过程,会让你后续的学习更加高效。
|
||||
|
||||
另外,有一个小技巧。很多成熟的框架,在官网除了常规的搭建步骤,还提供了非生产线部署的Quick Start 部署方式(比如我们这节课要学习的knative quick start),可以帮你更快地体验。
|
||||
|
||||
研究架构
|
||||
|
||||
在体验完简单的功能之后,我们再回过头来去学习基本的概念,就很容易结合前面的操作去理解了。在这个阶段,我建议你采用由全貌到细节的一个学习思路,先学习基本概念以及整体的架构,接着再深入到某一个具体的处理流程或者组件中去进行探究。
|
||||
|
||||
在体验过Knative 之后,我们再去学习Serving中一些组件的交互过程,你就很容易理解了。就像这一块核心逻辑我之前已经提前带你学习过了,但在这节课实操之后再回过头看一看,是不是就更好理解了呢?
|
||||
|
||||
比如冷启动过程,我们通过动手操作发现了第一次请求函数时,实例会从无到有,并且隔一段时间不请求后,实例又会被释放,这就很容易让你明白Activator的作用,以及AutoScaler的扩缩容流程。
|
||||
|
||||
再来,Knative中的事件驱动机制。我们部署了一个用于打印事件的服务以及一个定时产生事件的PingSource对象,接着,我们可以观察到定时事件的任务处理。这个时候我们再去从宏观的角度学习Eventing,就可以明白为什么他是一套松耦合的事件驱动体系了。
|
||||
|
||||
在Eventing的事件驱动的体系中,你可以通过API进行事件的创建,而事件将由事件的生产者发起,由事件的消费者接收。系统学习过后,你也可以知道我们的Demo是多种事件驱动模式中最简单的一种——Source&Sink模式。
|
||||
|
||||
|
||||
|
||||
其中,Source声明的是事件源与事件接受者的一个关联关系,而Sink表示一个可以接收事件的对象。结合前面的实践过程,你一定可以理解,我们部署的PingSource对象就是一个Source类型,event-display就是一个事件接收的服务。再去细究PingSource的yaml,可以看到Shedule定义的是定时触发的cron表达式,而sink.name表示事件将交由event-display处理。
|
||||
|
||||
apiVersion: sources.knative.dev/v1
|
||||
kind: PingSource
|
||||
metadata:
|
||||
name: test-ping
|
||||
namespace: default
|
||||
spec:
|
||||
schedule: "*/1 * * * *"
|
||||
contentType: "application/json"
|
||||
data: '{"message": "Hello geek!"}'
|
||||
sink:
|
||||
ref:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
name: event-display
|
||||
|
||||
|
||||
这样下来,理解起来是不是就很迅速了呢?学习了这种最基础的事件驱动模型,你还可以再进一步去学习Channel&Subscription 以及 Broker&Trigger 这两个较为复杂的模型,通过这样的动手实践,由浅入深,不仅可以让你学得更快,还能记得更牢。
|
||||
|
||||
深入源码
|
||||
|
||||
对整体的架构以及关健处理流程有了一个清晰的认知以后,就可以深入到源码当中去了,源码能够帮助你更好地理解引擎框架设计者的思路。这其实是一个更高的要求,需要你对框架开发语言以及代码编写风格都有一定的掌握,才能够更好的理解。
|
||||
|
||||
源码的阅读途径也比较丰富,除了官方文档以外,开源的框架代码基本都会放在Github,对于一些较难理解的地方,也可以通过阅读博客以及社区留言等方式去解决。
|
||||
|
||||
以Knative为例,Github包含了它全套的代码,除了Eventing和Serving这两个核心组件代码库,还包含客户端、一些使用文档等,对于一些像扩缩容一类的关键流程,有些Contributor甚至还会专门给出详细的流程说明,可以说是非常全面了。遇到问题的时候,Issue也能给你一定程度的参考。除此之外,Knative的官方也有一些不错的技术博客,这些都是能帮助你深入到源码的有效手段。
|
||||
|
||||
最后,在学习Serverless框架时,提前的知识储备也很重要,比如学习Knative之前你至少需要对K8s的基本概念有一些了解,而涉及到源码部分,还需要掌握Golang的基本语法等等。
|
||||
|
||||
这些学习经验其实并不局限于本节课Knative的学习,希望你更能够举一反三,灵活运用到之后遇到的每一个新框架的学习过程中去。
|
||||
|
||||
小结
|
||||
|
||||
今天这节课,你应该对Knative的核心组件有了更直观的了解:Serving提供了管理整个Serverless工作负载的能力,Knative Serving 通过自定义的一组特定对象,使用 Kubernetes CRD(自定义资源)的方式,配合上Activator以及AutoScaler等关键组件,从而实现了整个流量控制和自动扩缩容能力。
|
||||
|
||||
Knative Eventing实际上是一组API,能够帮助你使用事件驱动的方式对函数进行调用。Source与Sink模型是Knative中最简单的事件驱动模型,除此之外,还有Channel&Subscriptions和Broker&Trigger。前者提供了一种事件传输机制,能把事件分发给多个目的地或Sink。后者跟前者类似,但不同的是后者更方便对事件进行过滤,适用于一个 namespace 下的多个事件源,然后消费者根据事件的类型、属性挑选感兴趣的事件进行消费。
|
||||
|
||||
Tekton组件主要负责从代码仓库获取源码并编译成镜像,推送到镜像仓库,所有这些操作都是在Kubernetes Pod中进行的。
|
||||
|
||||
整体而言,Tekton补充的是Knative快速开发与集成的能力,提升了开发与部署的效率;Eventing则通过松耦合的构造以及对CloudEvents的支持,极大地丰富了Knative 的应用场景;Serving组件贯穿到了整个Serverless容器的生命周期。
|
||||
|
||||
最后我想跟你说的是,Serverless的技术是在不停更迭的,我们需要灵活地研究一个新发布的Serverless引擎、框架、平台,你可以通过直接上手体验,再去研究架构原理,最后深入源码细节升华的方式进行学习。
|
||||
|
||||
课后作业
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个课后作业。
|
||||
|
||||
你可以继续尝试搭建一下Knative Tekton,并且看看能否通过Trigger搭建一个完整的CI/CD系统?如果有问题,欢迎你在留言区和我交流讨论。
|
||||
|
||||
感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,437 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
24 实战进阶(五):如何从Serverless引擎蜕变成一个Serverless平台?
|
||||
你好,我是静远。
|
||||
|
||||
我在前两节课详细介绍了私有化Serverless核心引擎的特性和选型方法,并基于当前盛行的Knative引擎进行了部署。相信现在的你,已经可以玩转Serverless底座了。
|
||||
|
||||
但仅仅研究核心引擎是远远不够的,和构建Serverless平台还有一定的距离。今天这节课,我将在引擎底座的基础上,继续围绕“私有化”这个主题,来跟你详细讲解如何基于一个Serverless 引擎底座构建完整的平台。
|
||||
|
||||
希望通过这节课的学习,你能够具备构建一个平台的能力,也让你在选择“最合适”的ToB厂商共建Serverless平台的时候更加得心应手。
|
||||
|
||||
定位:引擎和平台有什么不同?
|
||||
|
||||
我们先来明确一下Serverless下“引擎”与“平台”的侧重点。
|
||||
|
||||
一个引擎或者框架的目标更多的是提供一个具有特定功能的系统或者架构,比如Knative、OpenFaaS、Fission等开源的Serverless引擎,都是提供一个FaaS形态产品运行的系统。
|
||||
|
||||
对于一个引擎的研究和部署,你甚至不需要任何业务和架构的经验,通过官网的介绍通读一遍,然后按照使用文档就可以很快地搭建一个Serverless系统来做测试、实验了。但并不能直接在企业的生产线上使用,它还缺少权限管控、元信息管理、BaaS集成等能力。
|
||||
|
||||
而平台则需要考虑到生产级别中遇到的实际问题。从开始设计到完全投入生产使用,需要经过整体的规划设计、迭代升级,从内测到公测,再到完全开放的过程,服务过程中还需要考虑给用户承诺的SLA等等,这就需要设计者具备较为丰富的业务领域经验、架构设计能力以及中长期的规划迭代思维。
|
||||
|
||||
公有云和私有云的平台,虽然在展现形态上不太一样,比如公有云需要适配云厂商的容器服务、IAM、云产品联动、多租户等,私有云需要适配企业自身的容器底座、中间件、各类BaaS服务等,但他们的思想是一样的:私有云的Serverless平台,主要享受到了对平台的控制权和安全把控,只是将云厂商维护的工作转嫁到了企业的运维团队而已。唯一不同的是,私有云可能不太关心业务用户的计费问题。
|
||||
|
||||
那么,在构建一个Serverless平台的时候,我们需要考虑哪些方面呢?
|
||||
|
||||
如何构建Serverless平台?
|
||||
|
||||
基于用户、平台、函数的关系,我从安全性、易用性、可运维、可扩展性的角度,整理出了7个核心要点来介绍。
|
||||
|
||||
|
||||
|
||||
整体平台以容器为底座,构建于Kubernetes集群之上,而我们的核心引擎可以选择上节课介绍的Serverless引擎框架,或者自研引擎也是可以的。我们分别说一下每个功能的注意要点。
|
||||
|
||||
函数管理
|
||||
|
||||
一般开源引擎都会自带部分的函数管理能力,比如通过CLI的方式来对函数进行CRUD,也有一些引擎框架也集成了部分的Console操作能力。但不管是可视化界面还是CLI的操作性,都达不到一个生产级别的要求。
|
||||
|
||||
从函数自身管理角度看,需要集成函数的创建、配置、部署发布、测试、删除、上下线功能,并且支持底层资源规格的选取。
|
||||
|
||||
从函数代码开发角度看,需要提供多种函数语言的开发(如Java、Python、Node.js、Golang等),区别于函数计算引擎自带的模板和函数语言支持,平台更需要易用性,比如代码包的上传部署、持久化存储以及常用触发器的适配集成等等。
|
||||
|
||||
除此之外,还有最容易忽视的一点,那就是由于企业操作系统的不同,我们在制作函数计算基础镜像的时候,也得考虑不同操作系统版本不一致的问题。
|
||||
|
||||
平台管控
|
||||
|
||||
这部分的功能包括了用户的隔离、鉴权验证、用户角色、权限管理。这几大功能都是跟安全分不开的。我们一个一个来说。
|
||||
|
||||
|
||||
用户隔离:不仅包括了函数实体管控上的命名空间、并发度设置和持久化存储的隔离,还包括运行容器的隔离;而容器方面,你需要考虑追加一层安全容器或者基于Node节点来分配,这两种模式,我在扩缩容这节课对比过,可以复习一下,加深记忆。
|
||||
鉴权验证:通常和企业的公共IAM系统对接,但如果从高可用角度出发,你可以设计两套系统,一套企业IAM系统正常访问模式,一套是本地系统的IAM应急模式。
|
||||
用户角色和权限管理:目前主流的权限管理系统的设计大都基于RBAC模型或者是RBAC的变形,可以复用你之前设计微服务系统的经验。
|
||||
|
||||
|
||||
资源控制
|
||||
|
||||
这个功能也可以放到平台管控里面,单独讨论是为了强调平台的另一个重要特性:计量计费。针对公有云和私有云,不同的特点在于公有云面向的是外部的众多客户,云厂商以营收为主,不仅计量还需要计费,那作为Serverless平台就需要对接企业的计费系统。而对于私有云来说,更多的是企业内部使用,计量的重要性更多一些,计费并不是一个非常急迫的功能。
|
||||
|
||||
第二个特性在于对资源的使用限制。比如对实例的内存、CPU、GPU的使用,在单个地域的最大使用量,都是平台需要考虑的点。
|
||||
|
||||
开发工具链
|
||||
|
||||
一套好的开发工具,不仅可以使得开发调试部署更加方便,而且还更有利于老的服务迁移到新的技术平台中来,这才是设计新平台的初衷。
|
||||
|
||||
那么除了Serverless引擎自带的CLI 命令之外,我们还应该开发一套满足自身企业的CLI命令工具应对通用的开发,WebIDE工具应对在线快速开发,IDE集成插件应对习惯于类似VSCode的开发人员。
|
||||
|
||||
高可用
|
||||
|
||||
企业一般会在服务器宕机、流量异常、同步/异步失败、冷启动、扩缩容等方面提出要求。我们发现,除了冷启动和扩缩容属于Serverless的特有特征之外,其他的方面和我们在微服务治理中的知识点是一样的。
|
||||
|
||||
总的来说,我们可以通过多AZ的部署、异地备份等方式来解决。通过引入服务治理中间件(如断路器、注册中心等)来解决流量的异常调度、同步请求失败等问题,但异步调用失败需要结合中间件来处理。回想一下,我们之前提到的异步调用,平台就需要延迟加载重试等策略方式来保证用户的请求可靠。
|
||||
|
||||
至于冷启动和扩缩容,可以再回过头复习一下,相信你就有答案了。
|
||||
|
||||
到这里,一个Serverless平台的主干功能其实就差不多了。但如果要用的“爽”,你还得具备可观测、编排、异步调用、层、模板、单实例多并发、预留实例的能力,这个核心能力的实现和使用,是不是都可以从前面的课程中找到答案呢?
|
||||
|
||||
|
||||
|
||||
接下来,我将在上节课的基础上,结合可观测的知识点,跟你详细梳理一遍设计一个Serverless平台的步骤。
|
||||
|
||||
可观测功能的构建
|
||||
|
||||
可能你会想:为什么是可观测的角度?首先,可观测几乎引入的是开源组件,跟某一个企业或者业务耦合程度很低,便于你理解和实操。其次,可以把之前可观测学习到的理论知识点在这里通过平台视角演练一下。
|
||||
|
||||
在可观测课程里,我跟你提到了可观测中的三大要素:指标、链路、日志,但只掌握这三大要素还远远不够,你还可以从以下5个方面进一步考虑,完善可观测的构建,查漏补缺。
|
||||
|
||||
|
||||
明确收集的数据:无论是指标、链路还是日志,都应该明确我们需要什么样的数据,而不是毫无目的地将所有数据收集上来,每一种类型都应该从平台运维和用户关心两个维度去考虑。比如指标应该包含用户关心的调用次数、执行时长,还要包括各个实例的资源使用情况等。
|
||||
组件的选型:组件的引入最好是从一些成熟的产品中选择,我个人建议使用一些CNCF推荐且较为成熟的产品,比如Prometheus、fluent-bit等。除此之外,你也需要考虑到组件引入时的学习成本和后期的运维工作,一些可观测组件尽管功能丰富,但熟悉和部署的过程就需要花费上大量的时间。
|
||||
资源成本:可观测的组件也需要占用一部分资源,像Logstash这种强大的日志清洗组件,可能单个实例就需要消耗几个G的内存,而可观测的数据落盘还需要用到较大的存储空间。这些都需要结合具体的资源预算进行评估。如果预算足够,你完全可以使用公有云上的服务,免去后期的组件维护工作。
|
||||
适配问题:像金融、电信等企业,为了提高系统的安全性,操作系统往往是Kylin这一类国产操作系统,并且有自己的一套PaaS底座,因此为了适配PaaS底座,可能还需要对这些可观测组件进行二次开发,这也是在选型阶段需要考虑到的问题。
|
||||
后期的扩展能力:可观测组件的扩展能力也需要在设计之初就考虑进来,这需要你对整个平台后期的使用量有一个长期的判断。比如Prometheus就可以考虑采用联邦集群的部署方式,毕竟如果后期有明显的增长,单点部署就会遇到瓶颈。
|
||||
|
||||
|
||||
在开始实战之前,你可以先看一下整体的架构示意图,涵盖了基于Knaitve围绕三大支柱构建可观测的解决方案。接下来,我们来分别看一下具体的实现路径。
|
||||
|
||||
|
||||
|
||||
指标
|
||||
|
||||
首先明确收集的指标数据。在12讲中,我从平台与用户两个视角提出了调用次数、执行时长、资源使用情况等具体的细项,而Knative引擎已经提供了部分指标收集的支持,包括业务指标和模块指标。
|
||||
|
||||
其中业务指标会由queue-proxy提供,包括revision_request_count、revision_request_latencies、revision_app_request_count等5种与业务紧密相关的指标数据。
|
||||
|
||||
在模块指标方面,我们可以收集Activator、Autoscaler、Controller等上报的指标数据,这其中包括与整体冷启动并发相关的Activator级别的并发量统计,以及和当前资源使用情况相关的函数pod数量等。
|
||||
|
||||
那我们要用什么组件收集呢?这里我推荐采用Prometheus。原因有两点:
|
||||
|
||||
|
||||
第一,Knative官方推荐Prometheus作为指标的收集工具,并且前面提到的模块都有各自的exporter用于上报,使用非常方便,
|
||||
第二,Prometheus属于CNCF可观测系列中比较成熟的一款指标采集工具,社区也非常活跃,对于后期的维护工作非常有帮助。
|
||||
|
||||
|
||||
指标的收集工具确定为Prometheus后,可视化工具推荐配套的Grafana,这二者几乎是默认关联在一起使用的,也是Knative官网的推荐。
|
||||
|
||||
链路
|
||||
|
||||
链路信息提供的是关键过程的耗时情况。对于Serverless 平台来说,最关键的两个阶段就是冷启动与业务代码整体的执行时长。
|
||||
|
||||
回到Knative,冷启动耗时包括从Activator接受到请求开始,直到用户容器和环境准备完成的过程,而业务代码的执行时长则为函数运行的整个时长。在采集的过程中,我们往往通过span的duration相减的方式来计算链路耗时。比如冷启动耗时通过Activator Service的Duration减去User Container执行完成的Duration就可以得到。
|
||||
|
||||
另外,除了经过Activator和User Pod 这两个节点,还需要考虑到对用户自定义链路的支持,即允许用户通过SDK的方式在函数内的关键路径上进行链路标记并上报。
|
||||
|
||||
链路的相关工具,我还是采用了Knative推荐的Opentelemetry和Jaeger 。其中Opentelemetry组件用于链路信息的收集与上报,Jaeger 用于链路信息的可视化。
|
||||
|
||||
可以看到上图中,我采用了DaemonSet部署Otel-agent(这里你也可以通过Sidecar或者SDK注入的方式进行部署),用于上报函数的tracing信息,然后通过Otel-collector进行采集,最后通过Jaeger-Query访问collector中的数据就可以查看了。这样就形成了一个完整的链路信息闭环。
|
||||
|
||||
日志
|
||||
|
||||
关于日志部分,Knative 官方推荐使用fluent-bit,主要是因为fluent-bit超快速、轻量级、高度可扩展,是容器化日志收集和处理的首选工具,同时可以对接ElasticSearch、Kafka等多种日志存储下游。
|
||||
|
||||
图上的整体日志处理流程采用的是fluent-bit,由ElasticSearch 负责存储,再由kibana进行可视化。
|
||||
|
||||
好,接下来我们就利用上节课搭建好的Knative,结合上面的设计方案进行实操。
|
||||
|
||||
可观测功能实战
|
||||
|
||||
我会通过搭建构成中的几个关键步骤带你一步步实操,其中主要讲解指标和链路这两个更为复杂的部分,学习完之后,关于日志的部分你也一定能举一反三,自己搭建起来。
|
||||
|
||||
指标
|
||||
|
||||
首先我们来搭建采集指标的相关组件。指标的可观测构建包括数据上报、Prometheus数据收集以及Grafana数据可视化三个部分。
|
||||
|
||||
其中前面提到的Knative引擎本身会暴露很多指标,像Serving中的Autoscaler、Activator等主要模块以及User Pod中的queue-proxy都实现了数据上报,并暴露了专门的端口供Prometheus采集。比如Activator的request_concurrency和request_latencies就是对请求并发情况和延迟的统计上报,这相当于Activator本身会作为一个exporter。
|
||||
|
||||
我以Activator为例演示一下关于Prometheus和Grafana的配置。首先,我们先根据Knative官网提供的config yaml 配置一个Prometheus的采集规则,其中,Activator 的采集规则如下:
|
||||
|
||||
# Activator pods
|
||||
- job_name: activator
|
||||
scrape_interval: 3s
|
||||
scrape_timeout: 3s
|
||||
kubernetes_sd_configs:
|
||||
- role: pod
|
||||
relabel_configs:
|
||||
# Scrape only the the targets matching the following metadata
|
||||
- source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_pod_label_app, __meta_kubernetes_pod_container_port_name]
|
||||
action: keep
|
||||
regex: knative-serving;activator;metrics-port
|
||||
# Rename metadata labels to be reader friendly
|
||||
- source_labels: [__meta_kubernetes_namespace]
|
||||
action: replace
|
||||
regex: (.*)
|
||||
target_label: namespace
|
||||
replacement: $1
|
||||
- source_labels: [__meta_kubernetes_pod_name]
|
||||
action: replace
|
||||
regex: (.*)
|
||||
target_label: pod
|
||||
replacement: $1
|
||||
- source_labels: [__meta_kubernetes_service_name]
|
||||
action: replace
|
||||
regex: (.*)
|
||||
target_label: service
|
||||
replacement: $1
|
||||
|
||||
|
||||
其中scrape_interval 和 scrape_timeout 分别表示抓取间隔和抓取数据的超时时间,而最重要的是第9行到第11行,定义了Prometheus是如何抓取Activator数据的。
|
||||
|
||||
source_labels表示数据的原始标签,regex表示与source_labels标签匹配的值,他们是一一对应的,最后,我使用action:keep表示保留下regex字段指定的标签。比如__meta_kubernetes_pod_label_app这个标签的值需要为Activator才能被抓取。
|
||||
|
||||
在这里,我们需要将configmap创建在knative-monitoring 这个命名空间下,如果没有的话,需要你提前创建:
|
||||
|
||||
kubectl create ns knative-monitoring
|
||||
kubectl apply -f 100-scrape-config.yaml
|
||||
|
||||
|
||||
接着,再通过200-prometheus.yaml创建Service、Deployment、Role等K8s资源:
|
||||
|
||||
kubectl apply -f 200-prometheus.yaml
|
||||
|
||||
|
||||
执行成功之后,就可以看到在knative-monitoring下,有一个Prometheus的pod正在运行:
|
||||
|
||||
$ kubectl get pod -n knative-monitoring
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
prometheus-system-0 1/1 Running 0 4d23h
|
||||
|
||||
|
||||
然后,我们通过Prometheus 提供UI进行查看,如果你的K8s不是部署在本地的话,可以通过port-forward的方式进行本地端口映射,我们看一下Prometheus Service 暴露的端口:
|
||||
|
||||
$ k get svc -nknative-monitoring
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
prometheus-system-np NodePort 10.0.133.199 <none> 8080:30609/TCP 4d23h
|
||||
|
||||
|
||||
可以看到暴露的端口是8080,然后我们通过port-forward 命令进行本地访问:
|
||||
|
||||
k -nknative-monitoring port-forward svc/prometheus-system-np 8080
|
||||
|
||||
|
||||
打开localhost:8080后,可以进入Prometheus的主页。我们通过curl 命令访问上一节课部署的函数,并在Prometheus主页搜索activator_request_count这个指标。之后可以看到,Activator上报的指标已经被Prometheus采集上来了,这个指标的标签还会包含访问函数的revision name:
|
||||
|
||||
|
||||
|
||||
然后我们再按照相同的方式部署Grafana,你同样可以参考官方提供的grafana yaml:
|
||||
|
||||
kubectl apply -f 100-grafana-dash-knative-efficiency.yaml
|
||||
kubectl apply -f 100-grafana-dash-knative.yaml
|
||||
kubectl apply -f 100-grafana.yaml
|
||||
|
||||
|
||||
需要注意的是,在100-grafana.yaml这个文件中,需要关联很多的configmap作为Grafana的视图配置,我这里只使用了grafana-dashboard-definition-knative-efficiency和grafana-dashboard-definition-knative 这两个与Knative 相关的,剔除了其他的configmap 配置。
|
||||
|
||||
配置完成后,还是进入knative-monitoring,可以看到已经有两个正在运行的pod 了:
|
||||
|
||||
$ kubectl get pod -n knative-monitoring
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
grafana-cbb657d6-twc2l 1/1 Running 0 4d7h
|
||||
prometheus-system-0 1/1 Running 0 4d23h
|
||||
|
||||
|
||||
然后还是按照前面的方式查看service并使用port-forward 进行本地访问:
|
||||
|
||||
$ kubectl get svc -nknative-monitoring
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
grafana NodePort 10.0.63.40 <none> 30802:30870/TCP 4d8h
|
||||
prometheus-system-np NodePort 10.0.133.199 <none> 8080:30609/TCP 4d23h
|
||||
|
||||
$ kubectl -nknative-monitoring port-forward svc/grafana 30802
|
||||
|
||||
|
||||
通过默认用户名、密码和admin 登录后,创建一个数据面板,就可以看刚才采集的指标了:
|
||||
|
||||
|
||||
|
||||
链路
|
||||
|
||||
搭建完采集指标的相关组件,我们接下来再来动手搭建链路信息采集的相关组件,主要包括两个部分:Opentelemetry和Jaeger。整个搭建的详细过程可以参考官方给出的这篇文章,接下来,我们开始动手搭建。
|
||||
|
||||
第一步,我们需要在部署Opentelemetry组件之前先安装证书管理器:
|
||||
|
||||
# 创建命名空间
|
||||
kubectl create ns cert-manager
|
||||
# 部署证书管理器
|
||||
kubectl apply -f https://github.com/jetstack/cert-manager/releases/latest/download/cert-manager.yaml
|
||||
|
||||
|
||||
在完成后,可以在命名空间下看到三个运行的pod:
|
||||
|
||||
kubectl get pod -n cert-manager
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
cert-manager-bfcbf79f6-jvwnr 1/1 Running 0 16s
|
||||
cert-manager-cainjector-65c9c8779f-wsk8v 1/1 Running 0 16s
|
||||
cert-manager-webhook-848ff6dc66-cmq8t 1/1 Running 0 16s
|
||||
|
||||
|
||||
第二步,完成证书安装后,我们接着部署Opentelemetry 的operator。其中operator负责管理opentelemetry collector,也就是指标部分截图里的Otel-collcetor:
|
||||
|
||||
# 创建命名空间
|
||||
kubectl create ns opentelemetry-operator-system
|
||||
# 部署operator
|
||||
kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/download/v0.40.0/opentelemetry-operator.yaml
|
||||
|
||||
|
||||
成功部署后,可以看到对应运行的pod:
|
||||
|
||||
$ kubectl get pod -n opentelemetry-operator-system
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
opentelemetry-operator-controller-manager-596866cc59-7p6cr 2/2 Running 0 19s
|
||||
|
||||
|
||||
第三步,部署Jaeger Operator,用于控制Jaeger实例。
|
||||
|
||||
kubectl create namespace observability &&
|
||||
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/v1.28.0/deploy/crds/jaegertracing.io_jaegers_crd.yaml &&
|
||||
kubectl create -n observability \
|
||||
-f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/v1.28.0/deploy/service_account.yaml \
|
||||
-f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/v1.28.0/deploy/role.yaml \
|
||||
-f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/v1.28.0/deploy/role_binding.yaml \
|
||||
-f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/v1.28.0/deploy/operator.yaml
|
||||
|
||||
|
||||
部署完成后,查看pod状态:
|
||||
|
||||
$ kubectl get pod -n observability
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
jaeger-operator-7576fbc794-7gr8n 1/1 Running 0 16s
|
||||
|
||||
|
||||
第四步,创建Jaeger 实例。
|
||||
|
||||
kubectl apply -n observability -f - <<EOF
|
||||
apiVersion: jaegertracing.io/v1
|
||||
kind: Jaeger
|
||||
metadata:
|
||||
name: simplest
|
||||
EOF
|
||||
|
||||
|
||||
我们检查一下pod以及service的状态,可以看到上述的Jaeger类型会创建出simplest-agent、simplest-collector、simplest-query 等多个service。
|
||||
|
||||
$ kubectl get pod -n observability
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
jaeger-operator-7576fbc794-7gr8n 1/1 Running 0 11m
|
||||
simplest-746f54765d-zdb7d 1/1 Running 0 9s
|
||||
|
||||
$ k get svc -nobservability | grep simplest
|
||||
simplest-agent ClusterIP None <none> 5775/UDP,5778/TCP,6831/UDP,6832/UDP 9d
|
||||
simplest-collector ClusterIP 10.0.94.222 <none> 9411/TCP,14250/TCP,14267/TCP,14268/TCP 9d
|
||||
simplest-collector-headless ClusterIP None <none> 9411/TCP,14250/TCP,14267/TCP,14268/TCP 9d
|
||||
simplest-query ClusterIP 10.0.239.242 <none> 16686/TCP,16685/TCP 9d
|
||||
|
||||
|
||||
然后还是利用port-forward 访问simplest-query服务,打开localhost:16686后,可以看到如下界面:
|
||||
|
||||
kubectl -n observability port-forward service/simplest-query 16686
|
||||
|
||||
|
||||
|
||||
|
||||
第五步,部署Opentelemetry Collector和Opentelemetry Agent。二者本质上都是tracing数据的收集器,只是数据来源和流向不同。其中Opentelemetry Agent负责接收实例trace数据,并转发给Opentelemetry Collector,而Opentelemetry Collector负责接收由Otel Agent或应用实例本身发送的trace数据,并将其转发给Jaeger。
|
||||
|
||||
根据前面的设计,我们可以按照Knative官网给出的Opentelemetry Collector 案例进行配置,注意exporters中的jaeger.endpoint这个字段,需要配置成下游的endpoint,即我们前面创建的jaeger-collcetor。另外,需要在最后一行指定好exporter的类型为jaeger。
|
||||
|
||||
apiVersion: opentelemetry.io/v1alpha1
|
||||
kind: OpenTelemetryCollector
|
||||
metadata:
|
||||
name: otel
|
||||
namespace: observability
|
||||
spec:
|
||||
config: |
|
||||
receivers:
|
||||
zipkin:
|
||||
exporters:
|
||||
logging:
|
||||
jaeger:
|
||||
endpoint: "simplest-collector.observability.svc.cluster.local:14250"
|
||||
tls:
|
||||
insecure: true
|
||||
service:
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [zipkin]
|
||||
processors: []
|
||||
exporters: [logging, jaeger]
|
||||
|
||||
|
||||
同理,Opentelemetry Agent的下游因为是Opentelemetry Collector,所以在exporters中需要指定otlp.endpoint为刚刚Opentelemetry Collector的endpoint,而最后一行也需要指定exporter的类型为otlp(Opentelemetry Protocol)。
|
||||
|
||||
.....
|
||||
exporters:
|
||||
otlp:
|
||||
endpoint: "otel-collector.observability.svc.cluster.local:4317"
|
||||
tls:
|
||||
insecure: true
|
||||
.....
|
||||
service:
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [zipkin,otlp]
|
||||
processors: []
|
||||
exporters: [otlp]
|
||||
|
||||
|
||||
配置完成后,执行apply命令即可:
|
||||
|
||||
kubectl apply -f opentelemetry-collector.yaml
|
||||
kubectl apply -f opentelemetry-agent.yaml
|
||||
|
||||
|
||||
部署成功后,在observability namespace下会有5个running pods,分别为jaeger operator、jaeger instance、otel collector和两个otel agent。
|
||||
|
||||
☁ opentelemetry kubectl get pod -n observability
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
jaeger-operator-7576fbc794-7gr8n 1/1 Running 0 28m
|
||||
otel-agent-collector-4j9sc 1/1 Running 0 9s
|
||||
otel-agent-collector-xd52d 1/1 Running 0 9s
|
||||
otel-collector-6f49dfc856-ntwpp 1/1 Running 0 48s
|
||||
simplest-746f54765d-zdb7d 1/1 Running 0 16m
|
||||
|
||||
|
||||
第六步,配置serving的链路上报信息。serving关于tracing的配置放在了knative-serving命名空间下的config-tracing 这个configmap中,这里需要将zipkin-endpoint配置为我们前一步创建的Opentelemetry Agent的endpoint,之后所有serving组件上报的tracing信息都会上报给opentelemetry agent:
|
||||
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: config-tracing
|
||||
namespace: knative-serving
|
||||
data:
|
||||
backend: "zipkin"
|
||||
zipkin-endpoint: "http://otel-agent-collector.observability.svc.cluster.local:9411/api/v2/spans"
|
||||
debug: "true"
|
||||
|
||||
|
||||
最后,我们来验证一下,在本地浏览器打开localhost:16886 ,访问我们之前配置的函数,接着会在界面看到activator-service这个关键字:
|
||||
|
||||
|
||||
|
||||
点击其中一条trace信息后,就可以看到一次完整的链路过程:
|
||||
|
||||
|
||||
|
||||
小结
|
||||
|
||||
最后我们来小结下今天的内容。通过这节课,相信你已经知道了,掌握一个Serverless的引擎和构建一个Serverless平台还有一定的距离,需要我们基于底层技术,进一步结合丰富的业务领域经验、架构设计能力以及中长期的规划思维来迭代构建。
|
||||
|
||||
今天,我通过7个核心功能要点:函数管理、平台管控、资源控制、开发工具链、高可用、可观测、延伸能力,分别跟你介绍了基于一个引擎构建一个平台的思想。相当于我们在一幅轮廓画上增加了单色的素描,有了明显的形象和空间。
|
||||
|
||||
这样就够了呢?在实际的运行过程中,我们还需要考虑:新老业务如何基于这个平台来产生价值?如何方便的构建应用?如何满足不断延伸的场景需求?
|
||||
|
||||
那么,我们还需要再素描的基础上,增加色彩,使这幅画更具美感。这些“色彩”包括便捷的迁移工具和框架集成、专业的培训和实战手册、屏蔽底层异构资源的调度、可自定义运行时的能力等。
|
||||
|
||||
你还可以想象一下,现有的Serverless平台是否还有提升的空间,它的未来之路在哪里?终极形态是什么样子的? 比如业界是否会有一个统一的跨平台工具链,Serverless化内涵的延伸不再局限于一个产品平台的表现,而是一种设计范式件。这些问题,我会在结束语跟你一起探讨。
|
||||
|
||||
课后作业
|
||||
|
||||
好了,这节课到这里也就结束了,最后我给你留了一个课后作业。
|
||||
|
||||
基于上述的解决方案,尝试将日志采集的部分补充完整,想一想,要用到哪些开源组件?要如何联动和展示?
|
||||
|
||||
感谢你的阅读,也欢迎你把这节课分享给更多的朋友一起交流探讨。
|
||||
|
||||
|
||||
|
||||
|
73
专栏/Serverless进阶实战课/结束语在实战中把握事物本质,不断革新.md
Normal file
73
专栏/Serverless进阶实战课/结束语在实战中把握事物本质,不断革新.md
Normal file
@ -0,0 +1,73 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
结束语 在实战中把握事物本质,不断革新
|
||||
你好,我是静远。
|
||||
|
||||
时间过得真快,我们的课程已经接近尾声了。从筹备、上线到今天专栏的完结,历时半年左右的时间。
|
||||
|
||||
说实在的,我没想过战线会拉得如此之长。做专栏是一个知识沉淀的过程,需要耗费的心力和精力远超我的预期,课程和制作公众号、PR稿不同,它一定是“治学严谨”“授业解惑”的,我希望对每个知识点都需要高度负责,在构思如何输出之外,还要引入最新的实战技术和经验,并通过实操案例展示出来。
|
||||
|
||||
也因此,我特别感谢我的小伙伴“哲哥”非常用心、耐心地和我一起深度Review每一篇文章,有时我们还会找用户和其他专家一起来探讨,扮演不同的角色来提出问题,为了一个课程的打磨,我们经常还会来来回回沟通到后半夜。
|
||||
|
||||
但这都是值得的。在课程打磨、留言区解答疑问的同时,我也在不断地成长和完善Serverless这一领域的知识体系。
|
||||
|
||||
其实,Serverless的理念,也是在认同和挑战的螺旋迭代中不断向前迈进的。我本人、团队内部、客户和用户以及行业专家讨论时,都会提出诸如“Serverless的路子到底怎样走?”“函数计算目前的形态在N年后是不是非常原始?”“Serverless的治理如何做到更原生?”等等这些问题。
|
||||
|
||||
而面对这些不确定性极高的未来,我们能做的只有两点:第一,吃透一个具体的产品技术,不断在实战中理解Serverless的本质;第二,从终局出发,不断地创造新的产品和方案。这两点也是我在Serverless领域践行的法则。
|
||||
|
||||
在实战中把握事物本质
|
||||
|
||||
这个标题有两个关键词,实战和本质。
|
||||
|
||||
先说实战。在实战之前,我们需要从快速迭代技术中选取一片确定性极高的核心区域,例如我们的课程,就是从FaaS形态的Serverless切入学习的。在吃透了这部分的技术原理之后,我们才能更高效地吸收实战经验,快速提升自己的即时战力。而每一次实战,都会反过来促使我们更加贴近技术的本质。正如我们在开篇词中提到的那样,这个闭环让我们最终达到忘形存神、用意不用力的境界。
|
||||
|
||||
那Serverless的本质是什么呢?提出Serverless的初心到底是为了什么?
|
||||
|
||||
每个人站的角度不一样,出发点不一样,得到的结论也就不一样。
|
||||
|
||||
在我看来,用户能够更专注于自己的业务,从而产出更大的社会价值,正是因为Serverless“降本增效”的初心。而在平台方面,它一直在向着更弹性、更易用、更可运维的方向迭代。而我们的一切实战也都是在朝着这个本质迈进。
|
||||
|
||||
所以,在你不知道往哪个方向提升自己的时候,不妨回头想想这个技术本身要解决的问题。有了本质的思考后,我们后续做的事情也就不至于跑偏了。
|
||||
|
||||
不断革新
|
||||
|
||||
不跑偏就是终点了吗?
|
||||
|
||||
我们在课程里不止一次提到过“举一反三”。Serverless的内涵一直在不断地外延,我们不能只学习“招式”,如果只是根据知识点来一步一步操作,而不懂得变通,那就说明还没有真正的理解。我们需要在本质和初心之上,发挥更多的想象力,不断革新。
|
||||
|
||||
那么,我们具体要怎么做呢?
|
||||
|
||||
首先,具备交流意识的学习。在课程里,我也有特意留有思考的地方。有的读者的思考非常好,比如在第3讲中对中控调度服务的理解,在10讲对VPC创建的发问等等。
|
||||
|
||||
其次,带有深度思考的工作。比如查看系统的问题时,眼中看到的如果仅仅是日志和报警,最多觉得它是一个BUG,看不出产品的“症结”和客户的内心诉求,这样是不行的。
|
||||
|
||||
因此,我们可以这样在工作中多想想症结背后的问题,比如:
|
||||
|
||||
|
||||
客户提出这个业务场景和这个问题,它其实背后真正想要什么?-
|
||||
那我们下一步该怎么做呢?-
|
||||
我为什么会这么想呢?还可以怎么做呢?
|
||||
|
||||
|
||||
你可能会觉得第一个问题和自己无关,更多的是产品经理的工作范畴。但这种方式的自问自答,可以促进你对工作进一步的思考。
|
||||
|
||||
如果不这样追问,不这样要求自己,那么我们工作的想象力和热情会逐渐消退。这种思考能力的弱化,会妨碍我们创造更有价值的产品。
|
||||
|
||||
最后,不受束缚,勇于尝试。我给你分享一个我自身的例子。由于团队人手不够,同时需要兼顾函数计算的公有云、私有化和其他产品的研发,以至于要同时维护多套引擎底座,非常麻烦,团队的同学经常需要“打一枪换一炮”。后来,我们仔细分析研究了几个产品的共性,大胆地改造通用认证和引擎调度系统。结果这样的建设,在后来的弹性应用和容器调度上也同样值得借鉴和复用,一组L型赋能,大大降低了维护成本。
|
||||
|
||||
所以,抛弃所谓的常识和教条吧,尤其是站在技术浪潮的前沿,我们更应该倾听好的想法,吸收新的知识,勇于尝试。
|
||||
|
||||
希望你在这个专栏中不单单可以系统学习到Serverless的相关知识,还可以利用学到的知识和方法给自己的业务“降本增效”,让自己的加薪升职增加一个不小的筹码。更重要的是,你不会再被既定的一些规则束缚,开始培养新的思考问题的方式,主动进化自己的思维,透过产品抓住技术本质,勇于尝试。
|
||||
|
||||
我坚信,在你的加持下,还会有更多Serverless形态的技术和产品发布。
|
||||
|
||||
最后,我准备了一份毕业问卷,希望你可以花两分钟的时间填写一下。我会认真倾听你对这个专栏的意见或建议,期待你的反馈。我也会同时保持课程的最新技术更新,再次感谢你选择了我,我们江湖再见。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user