使用像OpenWhisk这样的无服务器平台几乎就像是在练习黑魔法。你插入代码,它就按你想要的频率执行代码。你永远不会按部署键,永远不用担心缩放。你也不需要关注它的底层设施。这么魔幻吗?不完全是!


本文将解释在OpenWhisk中调用操作时会发生什么。它将解释OpenWhisk部署的所有组件,并概述每个组件在系统中所扮演的角色。今天我们将揭开地下的秘密,古老的咒语。其实也不是什么秘密。因为OpenWhisk开源!


作为一个开源项目,OpenWhisk站在巨人的肩膀上。那些巨人可真高!Nginx、Kafka、Consul、Docker、CouchDB……它们将一起形成一个“无服务器的基于事件的编程服务”。

为了更详细地解释所有组件,我将在操作调用发生时跟踪整个系统。OpenWhisk中的调用是无服务器引擎所做的核心事情:执行用户输入到系统中的代码,并返回执行的结果。

0. 创建action

为了解释这个问题,让我们先在系统中创建一个动作。稍后在跟踪系统时,我将使用该操作来解释这些概念。下面的命令在OpenWhisk CLI已经正确设置的情况下执行。


首先,我们将创建一个包含以下代码的文件action.js,该代码将打印“Hello World”到标准输出,并返回一个JSON对象,该对象包含键“Hello”下的“World”。


function main() {
console.log('Hello World');
return { hello: 'world' };
}

我们使用action命令创建

wsk action create myAction action.js

完成了。现在我们想要调用这个动作。为此,我们输入。

wsk action invoke myAction

现在,OpenWhisk的幕后实际发生了什么?

揭开serverless框架OpenWhisk的工作原理_kafka

架构图

1. 请求进入系统:nginx

首先:OpenWhisk面向用户的API完全基于HTTP,并遵循RESTful设计。因此,通过wsk-CLI发送的命令本质上是针对OpenWhisk系统的HTTP请求。上面的特定命令大致可以翻译为

POST /api/v1/namespaces/$userNamespace/actions/myAction
Host: $openwhiskEndpoint

注意这里的$userNamespace变量。一个用户至少可以访问一个命名空间。为简单起见,我们假设用户拥有放置myAction的名称空间。


进入系统的第一个入口是通过nginx,“一个HTTP和反向代理服务器”。它主要用于SSL终止和将适当的HTTP调用转发到下一个组件。


2. 进入Controller,此时真正进入系统

nginx没有对我们的HTTP请求做太多的处理,只是将它转发给Controller。它是一个基于scala的REST API的实现(基于Akka和Spray),所有用户操作都经过这里,包括在OpenWhisk中对实体的CRUD请求和动作的调用。


Controller首先消除用户试图做的非法请求。非法请求的判断基于HTTP请求中使用的HTTP方法来判断。按照上面的转换,用户向现有操作发出POST请求,控制器将其转换为操作的调用。


下面的步骤都将在一定程度上涉及到它。

3. 身份验证和授权:CouchDB

现在,Controller将验证您是谁(身份验证),以及您是否有权对该实体执行您想要执行的操作(授权)。请求中包含的凭据将根据CouchDB实例中的所谓subject数据库进行验证。


在本例中,检查用户是否存在于OpenWhisk的数据库中和具有调用操作myAction的权限。


假设本例拥有空间下的权限操作,因此当一切正常时,下一个处理阶段的大门就打开了。

4.重新访问CouchDB获取Action


由于Controller现在确定用户允许进入并且有权限调用他的action,它实际上从CouchDB的whisks数据库中加载这个action(在本例中是myAction)。


action的记录主要包含要执行的代码(如上所示)和希望传递给动作的默认参数,这些参数与实际调用请求中包含的参数合并在一起。它还包含在执行过程中对其施加的资源限制,例如允许使用的内存。


在这个特殊的例子中,我们的动作不接受任何参数(函数的参数定义是一个空列表),因此我们假设我们没有设置任何默认参数,也没有向动作发送任何特定参数,从这个角度来看,这是最简单的情况。

5. Consul发起执行action行动

Controller现在已经准备好了一切,可以让你的代码真正运行。但它需要知道谁可以这么做。Consul是一种服务发现,用于通过不断检查执行程序的健康状态来跟踪系统中可用的执行程序。这些执行程序被称为Invokers。


Controller现在知道哪些调用器可用,选择其中一个调用所请求的操作。


在这种情况下,让我们假设系统有3个可用的Invokers, Invoker 0到2(当然我们算上0,毕竟我们是程序员),并且控制器选择Invoker 2来调用当前的操作。

6. Kafka

从现在开始,你发送的调用请求可能出现下面二种场景:



  1. 系统可能会崩溃,失去您的调用。
  2. 系统可能会承受如此沉重的负载,以至于调用需要等待其他调用先完成。

这两个问题的答案就是Kafka,“一个高吞吐量、分布式、发布-订阅消息系统”。控制器和调用器只能通过Kafka缓存和持久化的消息进行通信。这减轻了在内存中缓冲的负担,同时也避免了控制器和调用者发生OutOfMemoryException异常的风险,同时还确保了在系统崩溃时消息不会丢失。


为了让这个动作被调用,Controller发布了一个消息给Kafka,这个消息包含了要调用的动作和传递给这个动作的参数(在这个例子中是none)。此消息发送给上面控制器从Consul获得的列表中选择的调用者。


一旦Kafka确认它收到了消息,对用户的HTTP请求被响应一个ActivationId。用户稍后将使用它来访问这个特定调用的结果。请注意,这是一个异步调用模型,在该模型中,一旦系统接受了调用操作的请求,HTTP请求就会终止。


7. Invoker真正的代码执行者


这是OpenWhisk最有趣的部分。调用程序。调用者的职责是…调用一个action。它也是用Scala实现的。为了以一种隔离和安全的方式执行操作,它使用了另一个厉害的工具:Docker。


Docker用来为我们以快速、隔离和受控的方式调用的每个action建立一个新的自封装环境(称为容器)。简而言之,每次action调用都会生成一个Docker容器,action代码会被注入,它会使用传递给它的参数执行,结果会被获取,容器会被销毁。​这也是需要进行大量性能优化以减少开销和降低响应时间的地方。


在我们的具体例子中,当我们手上有一个基于Node.js的操作时,调用器将启动一个Node.js容器,从myAction注入代码,不带参数运行它,提取结果,保存日志并再次销毁Node.js容器。


8. 存储结果:是的……还是CouchDB


由于结果是由调用程序获得的,因此它作为上文提到的ActivationId下的激活存储到whisks数据库中。whisks数据库在哪里?你猜对了,CouchDB。


在我们的具体例子中,调用程序从动作中获取结果JSON对象,获取Docker编写的日志,将它们全部放入激活记录中,并将其存储到数据库中。它大概是这样的:


{
"activationId": "31809ddca6f64cfc9de2937ebd44fbb9",
"response": {
"statusCode": 0,
"result": {
"hello": "world"
}
},
"end": 1474459415621,
"logs": [
"2016-09-21T12:03:35.619234386Z stdout: Hello World"
],
"start": 1474459415595,
}

注意记录如何同时包含返回的结果和写入的日志。它还包含操作调用的开始和结束时间。激活记录中有更多的字段,这是一个简化的版本。


现在您可以再次使用REST API(我猜还是从第1步开始)来实际获得激活,从而获得操作的结果。你可以用


wsk activation get 31809ddca6f64cfc9de2937ebd44fbb9


总结


这就是它的调用原理。我们已经看到了一个简单的wsk操作调用myAction是如何通过OpenWhisk系统的不同阶段的。系统本身主要由两个自定义组件组成,Controller和Invoker。其他的组件都已经采用开源软件进行了集成。