优化 Azure Functions 的性能和可靠性

本文为提高无服务器函数应用的性能和可靠性提供了指南。

常规最佳做法

下面是有关如何使用 Azure Functions 生成和构建无服务器解决方案的最佳做法。

避免使用长时间运行的函数

长时间运行的大型函数可能会引起意外超时问题。 函数规模可能因含有许多 Node.js 依赖项而变大。 导入依赖项也会导致加载时间增加,引起意外的超时问题。 显式和隐式加载依赖项。 由代码加载的单个模块可能会加载自己的附加模块。

尽可能将大型函数重构为可协同工作且快速返回响应的较小函数集。 例如,webhook 或 HTTP 触发器函数可能需要在特定时间限制内确认响应;webhook 需要快速响应,这很常见。 可将 HTTP 触发器有效负载传递到由队列触发器函数处理的队列。 此方法允许延迟实际工作并返回即时响应。

跨函数通信

Durable FunctionsAzure 逻辑应用用于管理状态转换以及多个函数之间的通信。

如果不使用 Durable Functions 或逻辑应用来集成多个函数,将存储队列用于跨函数通信通常是最佳做法。 主要原因是因为存储队列成本更低、更易预配。

存储队列中各消息的大小限制为 64 KB。 如果需要在函数之间传递更大的消息,可使用 Azure 服务总线队列,以在标准层中支持最大为 256 KB 的消息大小,在高级层中最大为 1 MB 的消息大小。

如果在处理前需要筛选消息,则服务总线主题十分有用。

对于支持大容量通信,事件中心十分有用。

将函数编写为无状态

如有可能,函数应为无状态和幂等。 将任何所需的状态信息与用户的数据相关联。 例如,正在处理的排序可能具有关联的 state 成员。 函数本身保持无状态时,该函数可根据该状态处理排序。

对于计时器触发器,特别建议采用幂等函数。 例如,如果有必须每天运行一次的内容,则编写它,使它可在一天内的任何时间运行,并生成相同的结果。 某天没有任何工作时,可退出该函数。 此外,如果未能完成以前的运行,则下次运行应从中断的位置继续运行。

编写防御函数

假定任何时候函数都可能会遇到异常。 设计函数,使其具有在下次执行期间从上一失败点继续执行的能力。 请考虑需执行以下操作的方案:

  1. 在 DB 中进行 10,000 行的查询。
  2. 为每行创建队列消息,从而处理下一行。

根据系统复杂程度,可能有:行为有误的相关下游服务,网络故障或已达配额限制等等。所有这些可在任何时间影响用户的函数。 需设计函数,使其做好该准备。

如果将 5,000 个那些项插入到队列中进行处理,然后发生故障,代码将如何响应? 跟踪已完成的一组中的项。 否则,下次可能再次插入它们。 这会严重影响工作流。

如果已处理队列项,则允许函数不执行任何操作。

利用已为 Azure Functions 平台中使用的组件提供的防御措施。 有关示例,请参阅 Azure 存储队列触发器和绑定文档中的处理有害队列消息

可伸缩性最佳做法

有许多因素会影响函数应用实例的缩放方式。 有关函数缩放的文档中提供了详细信息。 下面是确保以最佳方式缩放函数应用的最佳做法。

共享和管理连接

只要可能,请重用与外部资源的连接。 请参阅如何管理 Azure Functions 中的连接

请勿在同一函数应用中混合测试和生产代码

Function App 中的各函数共享资源。 例如,共享内存。 如果生产中使用的是 Function App,则请勿向其添加与测试相关的函数和资源。 生产代码执行期间,这可能会导致意外的开销。

请注意在生产 Function App 中加载的内容。 将内存平均分配给应用中的每个函数。

如果在多个 .NET 函数中引用共享程序集,请将其放在常用的共享文件夹中。 如果使用 C# 脚本 (.csx),请使用类似于以下示例的语句引用程序集:

#r "..\Shared\MyAssembly.dll". 

否则,很容易意外部署在函数之间表现不同的同一二进制的多个测试版本。

请勿在生产代码中使用详细日志记录。 其对性能有负面影响。

使用异步代码,但避免阻止调用

异步编程是推荐的最佳做法。 但是,请始终避免引用 Result 属性或在 Task 实例上调用 Wait 方法。 这种方法会导致线程耗尽。

Tip

如果计划使用 HTTP 或 WebHook 绑定,请制定计划来避免因实例化 HttpClient 不当导致的端口耗尽现象。 有关详细信息,请参阅如何在 Azure Functions 中管理连接

尽量批量接收消息

某些触发器(例如事件中心)允许通过单次调用接收一批消息。 批处理消息可大幅提升性能。 可以根据 host.json 参考文档中的详述,在 host.json 文件中配置最大批大小

对于 C# 函数,可将类型更改为强类型化数组。 例如,方法签名可以是 EventData[] sensorEvent,而不是 EventData sensorEvent。 对于其他语言,需要根据此文所述,在 function.json 中将基数属性显式设置为 many,以启用批处理。

配置主机行为以更好地处理并发性

使用函数应用中的 host.json 文件可以配置主机运行时和触发器行为。 除了批处理行为以外,还可以管理大量触发器的并发性。 调整这些选项中的值往往有助于每个实例根据被调用函数的需求适当缩放。

主机文件中的设置应用于应用中的所有函数,以及函数的单个实例。 例如,如果有包含 2 个 HTTP 函数的函数应用,并且并发请求设置为 25,则针对任一 HTTP 触发器发出的请求将计入 25 个共享的并发请求。 如果该函数应用扩展到 10 个实例,则 2 个函数将有效地允许 250 个并发请求(10 个实例 * 每个实例 25 个并发请求)。

HTTP 并发性主机选项

{
    "http": {
        "routePrefix": "api",
        "maxOutstandingRequests": 200,
        "maxConcurrentRequests": 100,
        "dynamicThrottlesEnabled": true
    }
}
属性 默认 说明
routePrefix api 应用到所有路由的路由前缀。 使用空字符串可删除默认前缀。
maxOutstandingRequests 200* 在任意给定时间搁置的未完成请求数上限。 此限制包括已排队但尚未开始执行的请求,以及正在执行的所有请求。 超出此限制的任何传入请求将被拒绝,并返回 429“太忙”响应。 允许调用方使用基于时间的重试策略,还可帮助控制最大请求延迟。 此设置仅控制脚本宿主执行路径中发生的排队。 其他队列(例如 ASP.NET 请求队列)仍有效,不受此设置的影响。 *版本 1.x 的默认值是无限制的。 专用计划中版本 2.x 的默认值是无限制的。
maxConcurrentRequests 100* 要并行执行的 http 函数数目上限。 这样,可以控制并发性,从而帮助管理资源利用率。 例如,某个 http 函数可能使用了大量系统资源(内存/CPU/插槽),从而在并发性过高时导致问题。 或者,某个函数向第三方服务发出出站请求,则可能需要限制这些调用的速率。 在这种情况下,应用限制可能有帮助。 *版本 1.x 的默认值是无限制的。 专用计划中版本 2.x 的默认值是无限制的。
dynamicThrottlesEnabled true* 启用时,将为此设置将导致请求处理管道,以定期检查系统性能计数器类似连接/线程/进程/内存/CPU 等,并通过内置的高阈值 (80%),如果有任何这些计数器请求拒绝与 429“太忙”响应,直至恢复到正常水平的计数器。 *版本 1.x 的默认值是 false。 专用计划中版本 2.x 的默认值为 false。

在主机配置文档中找到其他主机配置选项。

后续步骤

有关详细信息,请参阅以下资源: