Serverless 基础概念及原理解读

1,456 阅读12分钟

理念

小说《三体》中,描绘过未来宇宙飞船内部设施的场景,当科技高度发达之后,设施的复杂细节都被隐藏了起来,飞船内部看不到任何设施的影子,但当需要使用的时候,就会出现座椅、桌子等设施供人们使用。同样的,Serverless 也是一种理念,它把复杂的服务运维、物理设施的细节隐藏在了云平台内部,只给开发人员提供一些接口进行使用。通过这些接口,开发人员可以随时启动一个服务,运行完之后,所占的资源会被回收,等待下一次调用。站在普通用户的角度,所有的软件服务都应该是 Serverless 的,因为用户无需了解这些服务背后的运行原理。

针对我们遇到的服务,可以通过几个点来判断它是不是 Serverless 架构:

  1. 服务有多少台机器
  2. 机器部署在哪里
  3. 机器运行的是什么操作系统
  4. 机器上安装了哪些软件

如果我们无法明确回答这几个问题,那么我们所使用的服务就是 Serverless 架构的

Serverless 与前端

随着云计算技术的普及,Serverless 架构对于前端开发人员来说,已经慢慢变成了一个很重要的基础设施。而前端应用经过近几年的发展,也越来越复杂,更多地从只关注用户界面向承担更复杂的业务逻辑转变。伴随着 BFF 层的流行,不同的业务也都会有自己的 BFF 层,在提供了便利性的同时,也将面临新的问题:

  1. 更高的运维成本
  2. 服务端资源利用效率不高
  3. 大量基础逻辑可能需要独立实现多次

如何从前端开发人员的角度出发,去解决这些问题,是我们在工作中需要多加思考的。而当我们的 BFF 层切换为 Serverless 架构之后,就可以很好地解决上述几个问题,让我们更聚焦于解决业务问题。

image.png

前端与全栈

随着前端架构的不断演化,Serverless 取代单体应用架构已经是一种趋势,前端开发人员也可以通过使用 Serverless 补齐自己的短板,大大降低服务端的运维成本。

实际上,在很多公司里,前端工程师虽然不可或缺,但地位也很被动,自嘲式的叫法为“工具人”。有时候虽然对这种现状很不满意,但在大环境下,也很难做出有效的改变。造成这种局面的一个很重要的原因,就是前端工程师通常离业务都“太远”,而后端工程师呢,他们基本不需要知道前端该如何展示和交互,只需要知道业务逻辑,就已经有了足够的话语权。

伴随着前端架构的持续演进和前后端合作模式的变化,Serverless 将使得前端负责更多的上层业务逻辑,而不是只编写一些简单的页面。从这点来看,前端研发面临的挑战,是对业务流程的深入理解和对全局的把控。这将使我们变成一个真正的全栈工程师。

*aaS 的演化

Serverless 是一种抽象的理念,不代表其具体的实现方式只能是一种,这种理念是在实践中不断发展而来。那么,它和业界常提到的 PaaS(Platform as a Service)、BaaS(Backend as a Service) 有什么样的关系?

在早期的 IT 时代,如果想部署一个应用,可能得经过以下的步骤:

  1. 购买服务器
  2. 安装操作系统
  3. 安装依赖软件,如:MySQL、Nginx
  4. 部署应用。将代码部署到服务器上

这样部署一个应用,会花费很久的时间,投入的时间成本非常高昂。现在大量使用的云计算技术,则很好地解决了这个问题。

云计算中,提供了三种服务模式。即 IaaS、PaaS、SaaS。先看一个示意图:

image.png

IaaS(Infrastructure as a Service,基础设施即服务)提供基础的处理存储、网络连接、基础的运算资源服务,让用户能直接在其上部署操作系统。客户无需购买或租用物理服务器,就可以直接部署和运行自己的服务。

PaaS(Platform as a Service,平台即服务)在基础设施之上,进一步提供了运算平台与解决方案服务。例如:数据库服务、缓存服务、消息队列服务。

SaaS(Software as a Service,软件即服务)则更进一步,提供开箱即用的软件服务。这些软件无需安装,用户直接通过客户端就可以直接使用该软件提供的服务。SaaS 提供的服务都是能够直解决业务场景的,而对于 PaaS 服务,我们仍然需要在其基础之上实现业务逻辑。

以上三种 aaS 云计算服务,从不同层面向用户提供了服务,用户可以各取所需,选择适合自己的场景进行使用。云计算通过分级的模式,将计算资源进行了封装,让用户按需取用。

随着容器化技术的发展,在 IaaS 的基础之上出现了一种新的服务模式,这就是 CaaS(Container as a Service)。云计算服务商将计算资源从提供虚拟机的方式,变为提供容器的方式。通过容器的编排服务,让研发人员可以基于容器化的技术,通过描述文件的方式构建和部署应用程序。

如果 CaaS 是 IaaS 能力的演进,那么 BaaS 则是在 PaaS 之上的能力延伸。我们通常会使用一些第三方服务来替换应用程序中的一些技术功能。第三方服务一般以 API 的方式提供,这些 API 是自动伸缩的,对于研发人员来说,这些服务无需运维,它们是 Serverless 服务。从这些特点来看,PaaS 和 PaaS 没有太大区别,但 BaaS 面向的对象不同,BaaS 是直接面向终端的,如:移动 APP、Web 站点等。研发人员可以直接在终端使用这些 PaaS 能力。

以上主要介绍了云计算服务的演进,可以看到,如果拿开头中对 Serverless 的标准来判断,这些服务模式或多或少的都已经有了 Serverless 架构的特点。在 CNCF(云原生基金会) 的白皮书中,则对 Serverless 应该提供的能力下了明确的定义,Serverless 计算平台应该包含以下的一种或者两种能力:

  1. 函数即服务(Function as a Service)。提供基于事件驱动的计算服务。
  2. 后端即服务(Backend as a Service)。指的是可以用来替换应用程序中的一些核心能力,且直接通过 API 的方式提供第三方服务。

FaaS

这里着重介绍一下最新的 FaaS 技术,也是能和前端紧密结合的无服务技术。

它基于事件驱动的理念,提供了让开发者以函数为粒度运行的代码,且具有像 HTTP 或者其它时间一样被触发并被执行的能力,开发者只需编写业务代码,无须关注服务器资源。这样,从以往租虚拟机来实现按月付费,变成了按调度消耗计费,这种方式降低了服务器的运维成本和租赁费用,大大提升了硬件资源的使用效率。另一方面,代码的部署效率得到了提升,因为,在发布新功能时,只需上线一个函数就够了。

FaaS 的事件处理模型如下:

image.png

优点

  1. 更高的研发效率。在传统的研发过程中,我们通常需要完成两部分工作,业务实现和技术架构。我们的目标是实现业务,但在过程中,需要关注技术架构。FaaS 不仅给用户提供了函数的运行环境,也提供了函数的调度方式。让开发者更聚焦于业务,提升研发效率。
  2. 更低的部署成本。在 FaaS 的场景下,开发者完成函数编写之后,只需要通过 Web 控制台或者简单的命令行工具,就可以完成函数的部署。
  3. 更低的运维成本。得益于 Serverless 的弹性伸缩能力,我们无需关注服务器的负载情况。那些为了保证可用性的工作几乎都可以省掉。
  4. 更低的学习成本。就像驾驶员无需了解发动机原理、摄影师无需学习光学原理一样,我们直接通过 Faas 即可完成业务函数的部署。
  5. 更低的服务器费用。基于虚拟机技术、容器技术的服务,自申请完服务资源之后,就开始计费了,而且无论资源占用多还是少,都会产生费用。而 FaaS 则按函数调用量和函数执行时间进行计费,省下了不少成本。
  6. 更灵活的部署方案。由于每个函数都是独立发布和控制的,新的函数发布将启动一个新的实例而不是覆盖上一个版本的实例,因此不会影响原来函数的功能,这样,就可以很容易地实现多套部署环境以及灰度切分流量的能力。
  7. 更高的系统安全性。在 Serverless 中,因为以及没有了服务器的概念,所以研发和运维人员都不需要登录服务器。登录服务器的大门关闭了,那么要进行攻击就显得更加困难。

缺点

  1. 存在平台学习成本。由于 FaaS 是一种比较新的架构,因此,在文档、示例、最佳实践方面都比较欠缺。不同供应商在平台上的实现不同,也给研发人员提高了学习成本。
  2. 较高的调试成本。因为我无法直接在本地运行这个函数,所以,只能在本地配置相同的容器环境或者在远端进行调试,无论采取哪一种都比较麻烦,是一个需要解决的问题。
  3. 潜在的性能问题。因为服务在长时间没有调用后,会将函数的实例进行自动缩容为 0 ,如果这时有新的请求,则会立即启动容器并部署函数后再执行该函数。这个从 0 到 1 的过程被称为冷启动。不同语言,由于运行环境不同,因此冷启动的时间各不相同,从 10ms 到 5s 不等。
  4. 供应商锁定问题。由于 FaaS 属于新型云计算服务模式,在实现方式上,各个云计算厂商没有统一的标准可以参考,因此各厂商都有不同的实现。这就导致,我们无法轻易地从一个供应商迁移到另外一个供应商平台。

实现简易 FaaS

以上主要介绍了一些基础的概念,下面以 FaaS 为例,来看看怎样基于 nodejs 实现一个简易的 FaaS。

目前,在 FaaS 的实现中,应用得最普遍的是基于 Docker 技术实现的容器级别的隔离,同时还能对系统资源进行隔离和限制;另外一种是基于进程的隔离实现,相对来说,基于进程的隔离实现更轻便、灵活,但与容器级别的隔离实现相比,其隔离性还有一定差距。

本节以基于进程隔离的实现来举例。

  1. 沙箱环境。在操作系统中,不同的进程有独立的内存空间,不同的进程之间,无法访问彼此分配的内存,这样就可以预防进程 A 将数据信息写入进程 B 中的情况。在 nodejs 中,可以通过主进程来监听函数调用请求,当请求被触发时,再启动子进程执行函数,将执行完之后的结果返回到主进程中,最终返回给客户端。考虑到安全问题,直接使用 vm2 模块来进行代码执行,nodejs 自带的 vm 模块不是绝对安全的。
// 子进程代码
// 读入代码之后,在新的沙箱环境中执行,并返回执行的结果
const process = require('process');
const { VM } = require('vm2');

process.on('message', (data) => {
    const fnIIFE = `(${data.fn})()`;
    const result = new VM().run(fnIIFE);
    process.send({ result });
    process.exit();
});

// 主进程代码
// 从文件中读取函数代码,启动子进程,将函数交给子进程去执行
const fs = require('fs');
const child_process = require('child_process');
const child = child_process.fork('./child.js');

child.on('message', (data) => {
    console.log('function result', data.result);
});

const fn = fs.readFileSync('./func.js', { encoding: 'utf8'});
child.send({ action: 'run', fn});

// 函数代码
// 定义了一个立即执行函数
(event, context) => {
    return { message: 'function is running', status: 'ok' };
}
  1. 增加 HTTP 服务。在生产环境中,为了让函数能够对外提供服务,还需要提供一个 Web API 的能力。这个能力使得服务可以根据用户的不同请求路径,动态执行对应的函数代码,并将结果返给客户端。
// 子进程代码
const process = require('process');
const { VM } = require('vm2');

process.on('message', (data) => {
    const fnIIFE = `(${data.fn})()`;
    const result = new VM().run(fnIIFE);
    process.send({ result });
    process.exit();
});

// 主进程代码
// 使用 Koa 提供 http 服务,当请求到来之后,根据请求路径读取不同的函数代码来执行
const fs = require('fs');
const child_process = require('child_process');
const koa = require('koa');

const app = new koa();
app.use(async ctx => ctx.response.body = await run(ctx.request.path));
app.listen(3000);

async function run(path) {
    return new Promise((resolve, reject) => {
        const child = child_process.fork('./child.js');
        child.on('message', resolve);

        try {
            const fn = fs.readFileSync(`./${path}.js`, { encoding: 'utf8' });
            child.send({ action: 'run', fn });
        } catch (error) {
            if(error.code === 'ENOENT') {
                return resolve('not fond function');
            }
            reject(error.toString());
        }
    });
}

// 函数代码1
(event, context) => {
    return { message: 'function is running', status: 'ok' };
}

// 函数代码2
(event, context) => {
    return { name: 'func2' };
}

至此,基础的基于进程隔离的 FaaS 能力就完成了。在此基础之上,还可以进一步考虑提升性能、函数执行的超时时间、对函数资源进行限制(CGroup)等问题。

总结

Serverless 是一种理念,请牢记如何判断一种服务是否是 Serverless 的那几条规则。

任何解决方案,都是不断演化而来,如果想彻底了解一种技术到底能解决什么问题,就需要研究它产生的背景,并将整个线索串联起来。

前端开发工程师,在工作中应该多了解业务。掌握足够多的话语权,才会变成真正的全栈工程师。

抽象与对复杂事物的封装是各领域共同存在的模式,不是计算机界才有的。

我们不断地交出控制权,才能更加地聚焦于业务。因为交出控制权之后,会得到一定的保护,例如:交出服务的控制权之后,我们无须再对服务进行运维。由此我们和服务商之间达成了一种契约,使得事物更高效地运行。