lingxiao-Zhu / blog

总结积累,读书笔记
3 stars 0 forks source link

《Serverless入门课》 #39

Open lingxiao-Zhu opened 3 years ago

lingxiao-Zhu commented 3 years ago

极客时间课程链接:http://gk.link/a/10icc

lingxiao-Zhu commented 3 years ago

什么是Serverless?

阶段 资源 研发 运维 特点
Serverfull 功能编写 应用部署日志收集回滚 分工明确运维做了很多繁琐的事情
DevOps 运维控制台 功能编写部署上线日志抓取 优化架构节省资源手动扩缩容资源 部分人力的工作工具化了,更加高效
Serverless 性能监控 + 流量估算解决自动扩缩容自动化发布的流水线:代码扫描 - 测试 - 灰度验证 - 上线 功能编写 转型去做更底层的服务,做基础架构的建设,提供更加智能、更加节省资源、更加周到的服务 服务端运维工作全部自动化,研发只需要关心自己的应用业务。运维的工作都由自动化工具代替。

最终目标就是服务端免运维,要解决的就是将运维的工作彻底透明化;研发同学只关心业务逻辑,不用关心部署运维和线上的各种问题。

第一种:狭义 Serverless(最常见)= Serverless computing 架构 = FaaS 架构 = Trigger(事件驱动)+ FaaS(函数即服务)+ BaaS(后端即服务,持久化或第三方服务)= FaaS + BaaS

第二种:广义 Serverless = 服务端免运维 = 具备 Serverless 特性的云服务,其实就是指服务端免运维,也是未来的主要趋势。

image

FaaS:Function as a Service

FaaS,函数即服务,它还有个名字叫作 Serverless Computing,它可以让我们随时随地创建、使用、销毁一个函数。

FaaS 和普通函数一样的,函数需要实例化,然后被调用,存在执行上下文 Runtime。

FaaS 的 Runtime 是预先设置好的,Runtime 里面加载的函数和资源都是云服务商提供的,我们可以使用却无法控制。你可以理解为 FaaS 的 Runtime 是临时的,函数调用完后,这个临时 Runtime 和函数一起销毁。

FaaS 的函数调用完后,云服务商会销毁实例,回收资源,所以 FaaS 推荐无状态的函数。如果你是一位前端工程师的话,可能很好理解,就是函数不可改变 Immutable。简单解释一下,就是说一个函数只要参数固定,返回的结果也必须是固定的。

此刻或许你会有点疑惑,Runtime 不可控,FaaS 函数无状态,函数的实例又不停地扩容缩容,那我需要持久化存储一些数据怎么办?答案是 BaaS。

BaaS:Backend as a Service

BaaS 其实是一个集合,是指具备高可用性和弹性,而且免运维的后端服务。说简单点,就是专门支撑 FaaS 的服务。

MVC 架构中的 Model 层,就需要我们用 BaaS 来解决。Model 层我们以 MySQL 为例,后端服务最好是将 FaaS 操作的数据库的命令,封装成 HTTP 的 OpenAPI,提供给 FaaS 调用,自己控制这个 API 的请求频率以及限流降级。这个后端服务本身则可以通过连接池、MySQL 集群等方式去优化。

![image](https://user-images.githubusercontent.com/22609330/119759536-03744980-bedb-11eb-8d61-6d0826668df6.png) > 基于 Serverless 架构,我们完全可以把传统的 MVC 架构转换为 BaaS+View+FaaS 的组合,重构或实现。 ## FaaS运行原理 Serverless 是对服务端运维体系的极端抽象,用户 HTTP 数据请求的全链路,并没有质的改变,Serverless 只是将全链路的模型简化了。 先来看传统流程:: - 购买虚拟机服务 - 初始化虚拟机运行环境 - 安装我们需要的应用运行环境 - 购买域名,用虚拟机 IP 注册域名 - 配置 Nginx,启动 Nginx - 上传应用代码,启动应用 ![image](https://user-images.githubusercontent.com/22609330/119759691-41716d80-bedb-11eb-9faf-452b94aa69e7.png)

而 FaaS 只需要三步,而且目前这样操作下来,没有产生任何费用

传统 web 应用 | FaaS 应用 -- | -- 服务端构建代码的运行环境 | 函数服务 负载均衡和反向代理 | HTTP 函数触发器 上传代码和启动应用 | 函数代码 ![image](https://user-images.githubusercontent.com/22609330/119759701-47674e80-bedb-11eb-9d33-79df594cc40e.png)

FaaS 运行流程:

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

FaaS 与应用托管 PaaS 平台对比,最大的区别在于资源利用率,这也是 FaaS 最大的创新点。FaaS 的应用实例可以缩容到 0,而应用托管 PaaS 平台则至少要维持 1 台服务器或容器。

函数在第一次调用之前,实际的服务器占用为 0。因为直到用户第一次 HTTP 数据请求过来时,函数服务才被 HTTP 事件触发,启动函数实例。也就是说没有用户请求时,函数服务没有任何的函数实例,也就不占用任何的服务器资源。而应用托管 PaaS 平台,创建应用实例的过程通常需要几十秒,为了保证你的服务可用性,必须一直维持着至少一台服务器运行你的应用实例。

FaaS这样优势背后的关键点是可以极速启动

FaaS 为什么可以极速启动?

FaaS 中的冷启动是指从调用函数开始到函数实例准备完成的整个过程。

现在的云服务商,基于不同的语言特性,冷启动平均耗时基本在 100~700 毫秒之间。得益于 Google 的 JavaScript 引擎 Just In Time 特性,Node.js 在冷启动方面速度是最快的。

这也是 FaaS 敢缩容到 0 的主要原因。

![image](https://user-images.githubusercontent.com/22609330/119760138-0c194f80-bedc-11eb-9aaa-b6094b776bb3.png)

上图中,蓝色部分是云服务商负责的,红色部分由你负责,而函数代码初始化,一人一半。也就是说蓝色部分在冷启动时候的耗时你不用关心,而红色部分就是你的函数耗时。

云服务商还会不停地优化自己负责的部分:例如冷启动过程中耗时比较长的是下载函数代码;所以一旦你更新代码后,云服务商就会偷偷的下载你的代码并且构建函数实例的镜像,请求第一次访问时,云服务商就可以利用构建好的缓存镜像,直接跳过冷启动的下载函数代码步骤,从镜像启动容器,这个也叫预热冷启动。所以如果我们有些业务场景对响应时间比较敏感,我们就可以通过预热冷启动或预留实例策略,加速或绕过冷启动时间。

为什么 PaaS 不能极速启动?

首先应用托管平台 PaaS 为了适应用户的多样性,必须支持多语言兼容,还要提供传统后台服务,例如 MySQL、Redis。

这也意味着,应用托管平台 PaaS 在初始化环境时,有大量依赖和多语言版本需要兼容,而且兼容多种用户的应用代码往往也会增加应用构建过程的时间。所以通常应用托管平台 PaaS 无法抽象出轻量的可复用的层级,只能选择服务器或容器方案,从操作系统层开始构建应用实例。

FaaS 设计之初就牺牲了用户的可控性和应用场景,来简化代码模型,并且通过分层结构进一步提升资源的利用率。学到这里,我们得来看看隐藏在 FaaS 冷启动中最重要的革新技术:分层结构。

FaaS 是怎么分层的?

![image](https://user-images.githubusercontent.com/22609330/119760165-19363e80-bedc-11eb-8b69-d5d338b51427.png)

你的 FaaS 实例执行时,就如上图所示,至少是 3 层结构:

容器

代码要运行,总需要和硬件打交道,容器就是模拟出内核和硬件信息,让你的代码和 Runtime 可以在里面运行。容器的信息包括内存大小、OS 版本、CPU 信息、环境变量等等。

目前的 FaaS 实现方案中,容器方案可能是 Docker 容器、VM 虚拟机,甚至 Sandbox 沙盒环境。

运行时 Runtime

就是你的函数执行时的上下文 context。Runtime 的信息包括代码运行的语言和版本,例如 Node.js v10,Python3.6;可调用对象,例如 aliyun SDK;系统信息,例如环境变量等等。

具体函数代码

你编写的代码。

关于 FaaS 的 3 层结构,你可以这么想象:容器层就像是 Windows 操作系统;Runtime 就像是 Windows 里面的播放器暴风影音;你的代码就像是放在 U 盘里的电影。

这样分层有什么好处呢?容器层适用性更广,云服务商可以预热大量的容器实例,将物理服务器的计算资源碎片化。Runtime 的实例适用性较低,可以少量预热;容器和 Runtime 固定后,下载你的代码就可以执行了。

通过分层,我们可以做到资源统筹优化,这样就能让你的代码快速低成本地被执行


FaaS 的两种进程模型

Q:函数执行完之后实例能否不结束,让它继续等待下一次函数被调用呢?这样省去了每次都要冷启动的时间,响应时间不就可以更快了吗?

A:是的,本身 FaaS 也考虑到了这种情况,所以从运行函数实例的进程角度来看,就有两种模型。

常驻进程型(常见的MVC)

函数实例准备好后,执行完函数不结束,而是返回继续等待下一次函数被调用。这里需要注意,即使 FaaS 是常驻进程型,如果一段时间没有事件触发,函数实例还是会被云服务商销毁

一般来讲,常驻型应用在初始化时做的事情比较多,比如需要连接数据库,连接日志服务器,初始化状态,而该初始化过程是会算在冷启动的耗时中的,所以增加了冷启动时间如果仅仅只是常驻,不做初始化,耗时跟用完即毁型没有太大区别的,而导致第一次请求长延迟甚至失败。所以我们要尽量避免冷启动,避免冷启动通常又需要做一些额外的工作,比如定时触发一下实例或者购买预留实例,这地方就会增加额外的费用了。

![image](https://user-images.githubusercontent.com/22609330/119760191-23f0d380-bedc-11eb-971f-6e8dbe83713c.png)

用完即毁型

函数实例准备好后,执行完函数就直接结束。这是 FaaS 最纯正的用法。

用完即毁型使用场景

数据编排

Node.js 的 BFF 层 (Backend For Frontend),将后端数据和后端接口编排,适配成前端需要的数据结构,提供给前端使用。

![image](https://user-images.githubusercontent.com/22609330/119760220-31a65900-bedc-11eb-9d85-e75ef63f3892.png)

如上图所示,BFF 层充当了中间胶水层的角色,粘合前后端。未经加工的数据,我们称为元数据 Raw Data,对于普通用户来说元数据几乎不可读。所以我们需要将有用的数据组合起来,并且加工数据,让数据具备价值。对于数据的组合和加工,我们称之为数据编排。

但是传统的服务端运维 Node.js 应用还是比较重的,需要我们购买虚拟机,或者使用应用托管 PaaS 平台。

因为 BFF 层只是做无状态的数据编排,所以我们完全可以用 FaaS 用完即毁型模型替换掉 BFF 层的 Node.js 应用,也就是最近圈子里老说的那个新名词 SFF(Serverless For Frontend)。

![image](https://user-images.githubusercontent.com/22609330/119760269-44b92900-bedc-11eb-9cc1-b256940a819c.png)

服务编排

服务编排和数据编排很像,主要区别是对云服务商提供的各种服务进行组合和加工。

比如需要调用阿里云的一个服务,但是这个服务只有Java版本的SDK,而我们使用的 Node.js 开发。但是由于这个功能很简单,我们完全可以用Java 创建一个 FaaS 服务。

这个也是 FaaS 一个亮点:语言无关性。它意味着你的团队不再局限于单一的开发语言了,你们可以利用 Java、PHP、Python、Node.js 各自的语言优势,混合开发出复杂的应用。


FaaS应用如何才能快速扩缩容?

比如有一个 Node 应用,我们让 200 个用户同时并发访问,首先客户端与 PC 建立了 200 个 TCP/IP 的连接,这时 PC 还可以勉强承受得住。然后 200 个客户端同时发起 HTTP 请求"/ GET",我们 Web 服务的主进程,会创建“CPU 核数 -1”个子进程并发,来处理这些请求。注意,这里 CPU 核数之所以要减一,是因为有一个要留给主进程。

例如 4 核 CPU 就会创建 3 条子进程,并发处理 3 个客户端请求,剩下的客户端请求排队等待;子进程开始处理"/ GET",命中路由规则,进入对应的 Control 函数,返回 index.html 给客户端;子进程发送完 index.html 文件后,被主进程回收,主进程又创建一个新的子进程去处理下一个客户端请求,直到所有的客户端请求都处理完。具体如下图所示。

![image](https://user-images.githubusercontent.com/22609330/119760081-f015ae00-bedb-11eb-8b54-8905a02bbfe6.png)

纵向扩缩容与横向扩缩容

增加或减少单机性能就是纵向扩缩容,纵向扩缩容随着性能提升成本曲线会陡增,通常我们采用时要慎重考虑。而增加或减少机器数量就是横向扩缩容,横向扩缩容成本更加可控,也是我们最常用的默认扩缩容方式。

但是,无论是横向还是纵向扩容,都需要重启服务器,那么存在服务器内存中的数据怎么办?它每次重启都会被还原到最开始的时候,那我们要如何在扩缩容的时候保存我们的数据呢?哪些节点可以扩缩容,哪些节点不容易扩缩容呢?

首先,根据网络拓扑图,我们可以根据是否保存状态分为 Stateful 和 Stateless:

Stateful VS Stateless

Stateful 就是有状态的节点,Stateful 节点用来保存状态,也就是存储数据,因此 Stateful 节点我们需要额外关注,需要保障稳定性,不能轻易改动

Stateless 就是无状态的节点,Stateless 不存储任何状态,或者只能短暂存储不可靠的部分数据。Stateless 节点没有任何状态,因此在并发量高的时候,我们可以对 Stateless 节点横向扩容,而没有流量时我们可以缩容到 0(是不是有些熟悉了?)。

Stateful 节点则不行,如果面对流量峰值峰谷的流量差比较大时,我们要按峰值去设计 Stateful 节点来抗住高流量,没有流量时我们也要维持开销

回到我们的进程模型,用完即毁型是天然的 Stateless,因为它执行完就销毁,你无法单纯用它持久化存储任何值;常驻进程型则是天然的 Stateful,因为它的主进程不退出,主进程可以存储部分值。

但是扩容出来的节点与节点之间,它们各自内存中的数据是无法共享,所以我们要让常驻进程型也变成 Stateless,我们就要避免在主进程中保存值,或者只保存临时变量,而将持久化保存的值,移出去交给 Stateful 的节点,例如数据库

![image](https://user-images.githubusercontent.com/22609330/119760358-6e725000-bedc-11eb-8f92-5798e5c27425.png) ![image](https://user-images.githubusercontent.com/22609330/119760365-70d4aa00-bedc-11eb-8c95-fc06f4a50e02.png) 但这样做的弊端其实也很明显,主进程启动时需要连接数据库,通过子进程访问数据库数据,它会直接增加冷启动时间。那有没有更好的解决方案呢?

换一种数据持久化的思路,我们为什么非要自己连接数据库呢?我们对数据的增删改查,无非就是子进程复用主进程建立好的 TCP 链接,发送数据库语句,获取数据。咱们大胆想象下,如果向数据库发送指令,变成 HTTP 访问数据接口 POST、DELETE、PUT、GET,那是不是就可以利用上一课的数据编排和服务编排了?那就是 BaaS 化。

再进一步考虑,既然 FaaS 不适合用作 Stateful 的节点,那我们是不是可以将 Stateful 的操作全部变成数据接口,外移?这样我们的 FaaS 就可以用我们上一课讲的数据编排,自由扩缩容了。

BaaS 化的核心思想就是将后端应用转换成 NoOps 的数据接口,这样 FaaS 在 SFF 层就可以放开手脚,而不用再考虑冷启动时间了。


后端 BaaS 化

后端应用 BaaS 化,就是 NoOps 的微服务。在我看来后端应用 BaaS 化,跟微服务高度重合,微服务几乎涵盖了我们 BaaS 化要做的所有内容。

微服务的 10 要素:API、服务调用、服务发现;日志、链路追踪;容灾性、监控、扩缩容;发布管道;鉴权。

BaaS 虽然解决了 FaaS 的扩缩容问题,但是 BaaS 本身的扩容怎么办?

答案是通过引入消息队列,解耦数据库。

消息队列:它是一个稳定的绝对值得信赖的 Stateful 节点,而且对你的应用来说消息队列是全局唯一的。解耦数据库的思路就是,通过消息队列解决数据库和副本之间的同步问题,有了消息队列,剩下的就简单了。

BaaS 节点每次写数据库的时候,都将数据写入消息队列中,并监听消息队列的更新在本地执行。

![image](https://user-images.githubusercontent.com/22609330/119760409-86e26a80-bedc-11eb-86c2-0259405d3ed6.png)

Container Serverless

当我们后端应用 BaaS 化后,想采用 FaaS 方案部署的话则会碰到 Runtime 这个拦路虎。如果我们依赖特殊的 Runtime 而云服务商没有提供,我们就可以考虑下降一层,使用 FaaS 的底层支撑技术 Docker 容器了

它可以将应用以及应用依赖的 Runtime 打包成镜像。

![image](https://user-images.githubusercontent.com/22609330/119760432-92ce2c80-bedc-11eb-9f49-81a14b2e5942.png)

上图是 Docker 模型,对照着 FaaS 来看,最上层就是函数, Bins/Libs 就是 Runtime,容器引擎及以下就是 操作系统

你也应该猜到了,其实很多云服务商 FaaS 和 PaaS 的底层技术就是容器即服务 CaaS

云服务商的冷启动加速,就是利用 Docker 镜像缓存加速。

FaaS 与 Docker 的扩缩容

FaaS 很简单,就是告诉函数服务我们的单个函数实例可以承载多少的并发量,如果事件触发并发量超出了这个并发度,则会自动扩容。

使用 Docker 时,要考虑的就是:监控指标 metrics 以及扩容水位

监控指标 metrics ,是一系列需要我们关心的单个容器核心指标,包括 CPU 利用率、内存使用率、硬盘使用率、网络连接数等等。所以我们要跟这些指标设定水位告警,一旦超过,就要给容器扩容。


K8s和云原生CNCF

K8s,用于自动部署、扩展和管理容器化应用程序的开源系统,是 Docker 集群的管理工具,它具备跨环境统一部署的能力。

![image](https://user-images.githubusercontent.com/22609330/119760455-9e215800-bedc-11eb-9cfa-716752cc857a.png)

Master 节点是集群的中心,也是一个 Stateful 节点,它只负责维持整个 K8s 集群的状态,为了保证职责单一,Master 节点不会运行我们的容器实例

Work 节点,也就是 K8s Node 节点, 这里才是我们部署容器真正运行的地方,但是在 K8s 中,运行容器最小的单位是 Pod。一个 Pod 具备一个集群 IP 且端口共享,一个 Pod 中可以运行一个或多个容器,但最佳的做法还是一个 Pod 只运行一个容器。这是因为一个 Pod 里面运行多个容器,容器会竞争 Pod 的资源,也会影响 Pod 的启动速度;而一个 Pod 里只运行一个容器,可以方便我们快速定位问题,监控指标也比较明确。

在 K8s 集群中,它会构建自己的私有网络,每个容器都有自己的集群 IP,容器在集群内部可以互相访问,集群外却无法直接访问。

K8s 运行原理

![image](https://user-images.githubusercontent.com/22609330/119760491-b09b9180-bedc-11eb-8e11-d2c76d03a22c.png)

K8s 其实就是一套 Docker 容器实例的运行保障机制。我们自己 Run 一个 Docker 镜像,会有许多因素要考虑,例如安全性、网络隔离、日志、性能监控等等。这些 K8s 都帮我们考虑到了,它提供了一个 Docker 运行的最佳环境架构,而且还是开源的。

K8s 如何实现扩缩容?

K8s 既然能管理容器集群,控制容器运行实例的个数,那应该也能实时监测容器,帮我们解决扩缩容的问题。

Service Mesh

Service Mesh 简单来说就是让微服务应用无感知的微服务网络通讯方案。

![image](https://user-images.githubusercontent.com/22609330/119760509-babd9000-bedc-11eb-97d4-5ffd815b41c8.png)

而SDK 版本的微服务框架,其实很重的一块都在于微服务之间网络通讯的实现。例如服务请求失败重试,调用多个服务实例的负载均衡,服务请求量过大时的限流降级等等。这些逻辑往往还需要微服务的开发者关心,而且在每种 SDK 语言中都需要重复实现一遍。那有没有可能将微服务的网络通信逻辑从 SDK 中抽离出来呢?让我们的微服务变得更加轻量,甚至不用去关心网络如何通讯呢?这就需要 Service Mesh。

它将微服务中的网络通信逻辑抽离了出来,通过无侵入的方式接管我们的网络流量,让我们不用再去关心那么重的微服务 SDK 了。下面我们看看 Service Mesh 是怎么解决这个问题的。

![image](https://user-images.githubusercontent.com/22609330/119760535-c446f800-bedc-11eb-9817-9c94adf82629.png)

Service Mesh 可以分为数据面板和控制面板,数据面板负责接管我们的网络通讯;控制面板则控制和反馈网络通讯的状态。Service Mesh 将我们网络的通讯通过注入一个边车 Sidecar 全部承接了过去。

数据面板比较简单,就是实现了流量劫持,控制面板则复杂一些,它也是 Service Mesh 的运作核心。pilot 是整个 Control Plane 的驾驶员,它负责服务发现、流量管理和扩缩容;citadel 则是控制面板的守护堡垒,它负责安全证书和鉴权;Mixer 则是通讯官,将我们控制面板的策略下发,并且收集每个服务的运行状况。

Istio

作为 Service Mesh 在 K8s 上的实现,

Knative

Knative 是通过整合:工作负载管理(和动态扩缩)以及事件模型来实现的 Serverless 标准,也叫容器化 Serverless。

Knative 在 Istio 的基础上,加上了流量管控和灰度发布能力、路由 Route 控制、版本 Revision 快照和自动扩缩容,就组成了 Server 集合;它还将触发器、发布管道 Pipeline 结合起来组成了 Event 集合。 ![image](https://user-images.githubusercontent.com/22609330/119759828-7da4ce00-bedb-11eb-9699-dde84c16be7d.png) ![image](https://user-images.githubusercontent.com/22609330/119759841-839aaf00-bedb-11eb-9315-71a0aa7c0dcc.png)