Swift的无服务器计算

为什么选择无服务器Swift?

与无服务器计算是否值得追求的问题不同,为什么要在Swift中实现无服务器系统?

有三个主要原因使Swift成为Lambda实现语言的理想选择。 首先,Swift是一种功能强大,健壮且富有表现力的语言,旨在在包括服务器端计算在内的各种环境中发挥作用。 就其本身而言,区别不大,因为可以用这种方式描述其他几种语言(Rust,Go)。 但是,Swift提供了两种附加的可能性-利用现有开发人员资源的机会,以及在系统的多个层(尤其是后端和移动客户端)之间共享代码的机会。

让我们考虑一个示例场景。

一个例子

我决定组建一家初创公司-这是我可以做的酵母菌,一家专门提供美味面包的在线面包店。 由于没有健壮的云基础架构,没有任何自重的面包店会被杀死,因此我的首要任务是构建微服务来处理向客户发送收据的工作。

具体来说,我想要一种服务,其输入是要订购的项目列表。 物品是面包的种类和数量。 例如三个羊角面包。 输入以JSON编码。 输出是收据的字符串表示形式。 它列出了每个订购的项目,其小计以及整个订单的总计。 现在,我不必担心收据太漂亮。

首先,我忽略了网络连接并编写了可用作命令行工具的代码。 首先,我将创建一个新目录,并使用Swift Package Manager(SPM)创建一个Swift应用程序。 请注意,该应用程序将命名为bru

 姆迪尔布鲁 
光盘布鲁
快速包初始化—type = executable

现在,我指定数据类型。 考虑到可重用性,我在与主应用程序不同的模块中定义它们。 我创建一个目录Sources / bruModels,并在该目录中使用以下代码创建Item.swiftOrder.swiftReceipt.swift文件(完整列表可在https://gist.github.com/profburke/2e951f48542a9a1ff470515725727751c中找到):

  // Item.swift 
 枚举样式:字符串,可编码{ 
羊角面包
凯恩
裸露镍
黑麦
}

 结构项目:可编码{ 
出租数量:整数
let style:样式
}
  // Order.swift 
  struct顺序:可编码{ 
公共私有(设置)各种商品:[商品]
  } 
  // Receipt.swift 
  struct Receipt:可编码,CustomStringConvertible { 
  } 

我依靠Swift 4中新的Codable协议来神奇地处理数据与JSON的相互转换。 正确组合Swift,Codable和JSON有时会很棘手。 但我将在另一篇博客文章中讨论可能的困难。 对于此示例,序列化很简单,我可以使用自动生成的序列化代码。

使用指定的数据类型,我将转向订单处理。 这是main.swift ,它位于Sources / bru中

进口基金会 
导入bruModels

让inData = FileHandle.standardInput.readDataToEndOfFile()
让解码器= JSONDecoder()
  func格式(_响应:字符串,有效载荷:字符串)->字符串{ 
  let result =“ {\” response \“:\” \(response)\“,\” payload \“:\” \(payload)\“}” 
 返回结果 
}

 做{ 
让order =试试decoder.decode(Order.self,来自:inputData)
让收据= order.receipt()
打印(格式(“成功”,有效载荷:receiving.description))
} {
打印(格式(“错误”,有效负载:“在实际应用中,这将具有有用的信息。”))
}

此代码执行从输入到输出的简单转换。 它接收以JSON格式格式化的项目数组,并使用Swift的JSONDecoder反序列化数据。 然后,它在Order类上调用一个实例方法来创建收据,并将其发送到流程的标准输出。

在生成程序之前,我需要对Package.swift进行两个小更改,即运行包初始化命令时自动生成的包清单文件。 我需要为bruModels添加一个目标,并将此新目标指定为主要目标的依赖项。 有关详细信息,请参见完整列表(https://gist.github.com/profburke/2e951f48542a9a1ff47051572d77584c)。

现在,我将编译如下:

 快速构建 

接下来,我可以在命令行中通过输入一些JSON并查看返回的内容来对其进行测试。 创建具有以下内容的文件order.json

  {“项目”:[ 
{“ style”:“ naan”,“ amount”:2},
{“样式”:“黑麦”,“数量”:3},
{“ style”:“牛角包”,“ amount”:6}]
}

现在输入命令:

 猫order.json |  .build /调试/应用 

果然,测试返回:

  {“响应”:“成功”,“有效负载”: 
“于2018-01-09 21:07:19 +0000收到订单
— — — — —
2 NAAN @ 0.87 = 1.74
3拉伊@ 0.62 = 1.86
6牛角面包@ 1.23 = 7.38
— — — — —
总计:10.98”
}

正如我上面提到的,这不是最漂亮的输出,但现在就足够了。

在一次简单的测试成功之后,我宣布胜利并继续下一步。

Swift和AWS Lambda

在撰写本文时(2018年1月),Lambda支持JavaScript,Python,Java,C#和Go。 显然,Swift不在该列表中。 但是不要害怕! AWS Lambda支持Node的子进程模块,我可以使用它来启动Lambda函数并与任意可执行文件进行交互。

我将编写一个简短的JavaScript函数,通常称为shim ,它将作为Lambda函数调用。 填充程序启动Swift可执行文件,捕获其输出,并将其作为Lambda调用的结果返回。

在上一节中,我决定忽略网络,并将Swift程序编写为命令行工具。 该决定是有回报的,因为这正是垫片所需的!

这是代码:

  const spawnSync = require('child_process')。spawnSync; 
  exports.handler =(事件,上下文,回调)=> { 
const command ='libraries / ld-linux-x86–64.so.2';
const childObject = spawnSync(command,
[“ —library-path”,“ libraries”,“ ./ bru”],
{input:JSON.stringify(event)});
  var stdout = childObject.stdout.toString('utf8'); 
callback(null,stdout);
};

该代码的作用是什么? 将导入child_process库,并导出函数handler处理程序是Lambda函数。 它启动一个新流程,并向其提供事件作为新流程的标准输入。 然后, Handler捕获流程的标准输出,该输出作为Lambda函数的结果返回。

现在您可能已经发现spawnSync是启动新进程的Node函数。 但是您可能希望它的命令参数是Swift可执行文件。 不幸的是,它并不是那么简单。

相反,子进程运行Linux动态链接器(通常名为ld.so ,尽管在这种情况下,我们使用符号链接的目标ld-linux-x86–64.so.2 )。 什么是动态链接器? 我将引用(Linux)手册页的内容:链接器“查找并加载程序所需的共享库(共享库),准备要运行的程序,然后运行它。”

但是,为什么要采用这种鲁伯·戈德堡的方法呢?

并发症

Lambda函数在特定的AMI下运行,并且该AMI没有Swift编译器。 因此,我需要使用确实支持Swift编译器的Linux版本来构建可执行文件,然后我必须安排在Lambda的AMI上运行时运行该可执行文件。

为此,我使用Swift可执行文件,JavaScript填充程序以及运行Swift代码所需的所有必要动态库创建一个zip文件。 压缩文件已上传到AWS Lambda,一切顺利。

除此以外,如何收集所有正确的动态库?

这就是Docker进来的地方。 我可以找到一个运行适当操作系统的备用盒,然后在其中编译我的Swift代码。 但是使用Docker可以使我在Mac笔记本电脑上进行所有开发。

通过选择适当的Docker映像,我们可以轻松地在Linux环境中编译Swift应用程序并收集必要的动态库。 Dockerhub有几个支持Swift编译器的Docker镜像可供选择。 我选择了一个名为Doctorimpossible / swift4ubuntu 。 以下命令指导我们完成必要的步骤:

  docker run -it -v“ $(PWD):/ bru” doctorimpossible / swift4ubuntu bash 
光盘布鲁
swift build -c release —构建路径.build / native
mkdir -p .build /部署/库

这些命令做什么?

1.启动Docker映像,使bru目录在我们的Docker容器内可用,并通过外壳连接到我们的容器。

2.转到bru目录。

3.使用指定的构建位置编译应用程序的发行版。

4.创建一个目录,在其中放置所有必需的动态库。

最后,我需要确定运行Swift可执行文件所涉及的所有动态库。 因此,最后一个稍微复杂的咒语将达到目的:

  ldd .build / native / release / bru 
| grep所以
| sed -e'/ ^ [^ \ t] / d'
| sed -e's / \ t //'
| sed -e's /(0。*)//'
| xargs -i%cp%.build /部署/库

首先,运行ldd 。 该实用程序列出了其命令行上列出的可执行文件的所有动态依赖项( ldd 是列表动态依赖项的首字母缩写 )。 在Linux上,动态库的文件扩展名为.so 。 因此,为了安全起见并防止来自ldd的潜在干扰线路,请将其输出传递给grep 。 然后,在几个简单的阶段中,我使用流编辑器sed删除剩余的每一行中的所有多余字符,以便为我提供一个简单的文件路径。 最后,将路径列表传递到xargs中xargs使用cp将库复制到特定目录中。

理论上,应该可以静态地编译Swift程序,从而避免了查找和捆绑所有动态库的需要。但是,我尚未能够使它正常工作。)

收集了所有必要的动态库后,所需要做的就是使用库,应用程序本身和JavaScript填充程序创建一个zip文件。 因此,退出Docker(在命令提示符下键入`exit`),然后:

  cd .build /部署 
cp ../native/release/bru。
cat> index.js //粘贴上面列出的JavaScript代码,然后按Control-D
zip -r lambda.zip *

现在,我将lambda.zip上传到AWS Lambda,并且准备测试我的Lambda函数。 如果您安装了AWS命令行界面(CLI),则按以下步骤创建Lambda函数:

  aws lambda创建功能—功能名称bru 
—运行时nodejs6.10
—角色
—处理程序index.handler
— zip文件fileb://lambda.zip

将替换为具有执行Lambda函数的适当权限的IAM角色。

使用CLI轻松测试新功能。 请注意示例部分中上述提到的order.json文件的文件路径,然后输入以下命令:

  aws lambda invoke —函数名bru 
—调用类型的RequestResponse
—log型尾巴
-有效载荷文件://
outputfile.txt

如果一切成功,您将在终端上看到一个JSON代码段,其中包含StatusCode (应该为200)和LogResult字段 。 日志结果并不重要,但是如果您好奇,则需要从base64编码中对其进行解码。 您的Swift程序创建的收据将位于outputfile.txt中

如果愿意,您可以使用AWS Web控制台创建Lambda函数,上传zip文件并测试该函数。 由于该zip文件相当大(25M),因此您可能会收到一条警告,提示您通过Amazon S3上传该文件,但该文件可以使用。

下一步

安装并测试了Lambda函数后,下一步就是将其集成到基础架构的其余部分中。 该功能在Swift中实现的事实与系统的其余部分几乎无关。 您可以将该功能放在Amazon API Gateway后面,或使用任何标准AWS触发器(例如SNS Topics,DynamoDB事件等)对其进行配置。

更重要的是,在了解了创建Swift Lambda函数背后的过程并着眼于减少繁琐的事情之后,您应该考虑使该过程自动化的方法。 为此,现有的项目是Hexaville。 尽管我没有使用过它,但我已经阅读了该代码,它看起来很可靠。 它具有额外的优势,即提供Swift库以与DynamoDB进行交互并利用OAuth。 另一个值得研究的项目是Apex。 尽管Apex当前不支持Swift,但它使您可以用Clojure,Rust和Go编写Lambda函数,并且可能为改进Swift Lambda流程提供有用的灵感来源。

如果您在无服务器上出售,但正在寻找其他选择,请留意第二部分,在第二部分中,我将讨论将Swift与Apache OpenWhisk和IBM的Blue Mix一起使用。