在 Amazon Neptune 中使用 Amazon Lambda 函数 - Amazon Neptune
Amazon Web Services 文档中描述的 Amazon Web Services 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅 中国的 Amazon Web Services 服务入门 (PDF)

本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。

在 Amazon Neptune 中使用 Amazon Lambda 函数

Amazon Lambda 函数在 Amazon Neptune 应用程序中有许多用途。在这里,我们提供了将 Lambda 函数与任何流行的 Gremlin 驱动程序和语言变体一起使用的一般指南,以及用 Java、JavaScript 和 Python 编写的 Lambda 函数的具体示例。

注意

在 Neptune 中使用 Lambda 函数的最佳方式随着最近发布的引擎而发生了变化。过去,Neptune 在回收 Lambda 执行上下文很久之后就会让空闲连接保持打开状态,这可能会导致服务器上的资源泄漏。为了缓解这种情况,我们过去建议在每次 Lambda 调用时打开和关闭连接。但是,从引擎版本 1.0.3.0 开始,空闲连接超时已缩短,以便在回收非活动的 Lambda 执行上下文后,连接不再泄漏,因此我们现在建议在执行上下文期间使用单个连接。这应该包括一些错误处理以及回退并重试样板代码,以处理意外关闭的连接。

在 Amazon Lambda 函数中管理 Gremlin WebSocket 连接

如果您使用 Gremlin 语言变体来查询 Neptune,则驱动程序将使用 WebSocket 连接来连接到数据库。WebSocket 旨在支持长寿命的客户端/服务器连接方案。另一方面,Amazon Lambda 旨在支持相对短寿命和无状态的执行。在使用 Lambda 查询 Neptune 时,这种设计理念的不匹配可能会导致一些意想不到的问题。

Amazon Lambda 函数在执行环境中运行,该环境将该函数与其它函数隔离开来。执行上下文是在第一次调用该函数时创建的,并且可以重用于后续调用同一函数。

但是,任何一个执行上下文都不会用于处理函数的多个并发调用。如果您的函数由多个客户端同时调用,Lambda 会为该函数的每个实例启动一个额外的执行上下文。反过来,所有这些新的执行上下文可重用于该函数的后续调用。

在某个时候,Lambda 会回收执行上下文,尤其是在它们已经处于非活动状态一段时间的情况下。Amazon Lambda 通过 Lambda 扩展公开执行上下文生命周期,包括 InitInvokeShutdown 阶段。使用这些扩展,您可以编写在回收执行上下文时清理外部资源(例如数据库连接)的代码。

常见的最佳实践是在 Lambda 处理程序函数之外打开数据库连接,以便每次调用处理程序时都可重用该连接。如果数据库连接在某个时候断开,则可以从处理程序内部重新连接。但是,这种方法存在连接泄露的危险。如果空闲连接在执行上下文被破坏后很长一段时间仍处于打开状态,则间歇性或突发性 Lambda 调用场景可能会逐渐泄漏连接并耗尽数据库资源。

随着更高的引擎版本推出,Neptune 连接限制和连接超时已发生变化。以前,每个实例最多支持 60000 个 WebSocket 连接。现在,每个 Neptune 实例的最大并发 WebSocket 连接数因实例类型而异

此外,从引擎版本 1.0.3.0 开始,Neptune 将连接的空闲超时从一小时缩短到大约 20 分钟。如果客户端未关闭连接,则在空闲超时 20 到 25 分钟后,连接会自动关闭。Amazon Lambda 不记录执行上下文生命周期,但实验表明,新的 Neptune 连接超时与非活动的 Lambda 执行上下文超时非常吻合。当不活跃的执行上下文被回收时,Neptune 很有可能已经关闭了它的连接,或者很快就会关闭。

将 Amazon Lambda 与 Amazon Neptune Gremlin 结合使用的建议

现在,我们建议在 Lambda 执行上下文的整个生命周期内使用单个连接和图形遍历源,而不是为每个函数调用使用一个连接和图形遍历源(每个函数调用仅处理一个客户端请求)。由于并发客户端请求是由在不同执行上下文中运行的不同函数实例处理的,因此无需维护连接池来处理函数实例内的并发请求。如果您使用的 Gremlin 驱动程序有连接池,请将其配置为仅使用一个连接。

要处理连接失败,请围绕每个查询使用重试逻辑。尽管目标是在执行上下文的生命周期内保持单个连接,但意外的网络事件可能会导致该连接突然终止。此类连接失败表现为不同的错误,具体取决于您使用的驱动程序。您应该对 Lambda 函数进行编码,以处理这些连接问题,并在必要时尝试重新连接。

某些 Gremlin 驱动程序会自动处理重新连接。例如,Java 驱动程序会自动尝试代表您的客户端代码重新建立与 Neptune 的连接。使用此驱动程序,您的函数代码只需要退出并重试查询即可。相比之下,JavaScript 和 Python 驱动程序不实现任何自动重新连接逻辑,因此使用这些驱动程序,您的函数代码必须在回退后尝试重新连接,并且只有在重新建立连接后才会重试查询。

这里的代码示例确实包括重新连接逻辑,而不是假设客户端正在处理它。

在 Lambda 中使用 Gremlin 写入请求的建议

如果您的 Lambda 函数修改了图形数据,请考虑采用回退并重试策略来处理以下异常:

  • ConcurrentModificationException – Neptune 事务语义意味着写入请求有时会失败并引发 ConcurrentModificationException。在这些情况下,请尝试使用基于指数回退的重试机制。

  • ReadOnlyViolationException – 由于计划内或计划外的事件会导致集群拓扑随时可能发生变化,因此,写入职责可能会从集群中的一个实例迁移到另一个实例。如果您的函数代码尝试向不再是主(写入器)实例的实例发送写入请求,则请求将失败并引发 ReadOnlyViolationException。发生这种情况时,请关闭现有连接,重新连接到集群端点,然后重试请求。

另外,如果您使用回退并重试策略来处理写入请求问题,请考虑为创建和更新请求实现幂等性查询(例如,使用 fold().coalesce().unfold())。

在 Lambda 中使用 Gremlin 读取请求的建议

如果您的集群中有一个或多个只读副本,则最好在这些副本之间平衡读取请求。一种选择是使用读取器端点。即使在您添加或删除副本或将副本提升为新的主实例时,集群拓扑发生了变化,读取器端点也会平衡副本间的连接。

但是,在某些情况下,使用读取器端点可能会导致集群资源的使用不均衡。读取器端点的工作方式是定期更改 DNS 条目指向的主机。如果客户端在 DNS 条目更改之前打开了大量连接,则所有连接请求都将发送到单个 Neptune 实例。高吞吐量 Lambda 场景可能就是这种情况,在这种场景中,对 Lambda 函数的大量并发请求会导致创建多个执行上下文,每个上下文都有自己的连接。如果这些连接几乎同时创建,则这些连接很可能都指向集群中的同一个副本,并且一直指向该副本,直到执行上下文被回收为止。

跨实例分配请求的一种方法是将 Lambda 函数配置为连接到从副本实例端点列表中随机选择的实例端点,而不是读取器端点。这种方法的缺点是,它要求 Lambda 代码通过监控集群并在集群成员资格发生变化时更新端点列表,来处理集群拓扑的变化。

如果您正在编写需要在集群中的实例之间平衡读取请求的 Java Lambda 函数,则可以使用适用于 Amazon Neptune 的 Gremlin 客户端,这是一款 Java Gremlin 客户端,它知道您的集群拓扑,可以公平地在 Neptune 集群中的一组实例间分配连接和请求。这篇博客文章包括一个使用适用于 Amazon Neptune 的 Gremlin 客户端的 Java Lambda 函数示例。

可能减慢 Neptune Gremlin Lambda 函数冷启动速度的因素

第一次调用 Amazon Lambda 函数称为冷启动。有几个因素会增加冷启动的延迟:

  • 请务必为您的 Lambda 函数分配足够的内存。  – Lambda 函数在冷启动期间的编译速度可能比在 EC2 上慢得多,因为 Amazon Lambda 按您分配给该函数的内存成比例线性分配 CPU 周期。在内存为 1,792MB 时,函数收到相当于 1 个完整 vCPU(每秒一个 vCPU 秒的积分)的处理能力。对于用 Java 编写的大型 Lambda 函数,未分配足够的内存来接收足够的 CPU 周期的影响尤其明显。

  • 请注意,启用 IAM 数据库身份验证可能会减慢冷启动速度 – Amazon Identity and Access Management (IAM) 数据库身份验证还会减慢冷启动速度,尤其是在 Lambda 函数必须生成新的签名密钥的情况下。这种延迟仅影响冷启动,而不影响后续请求,因为一旦 IAM 数据库身份验证建立了连接凭证,Neptune 只会定期验证这些凭证是否仍然有效。