带自定义Docker容器的Azure Durable Functions应用部署策略咨询
问题场景与部署策略咨询
我正在开发一个C# .NET Azure Functions应用,采用Http启动器、Durable编排器、活动函数的经典模式。其中活动函数需要访问Simba Spark ODBC驱动,从Azure Databricks SQL Warehouse获取数据,因此必须部署在预配置该驱动的自定义Docker容器中。现在针对应用打包与部署有以下两个问题:
- 仅活动函数需要自定义容器,是否应该将其单独部署为一个Functions应用,其余两个函数部署在另一个独立应用中?如果采用这个方案,Durable编排器该如何调用跨应用的活动函数?
- 如果把三个函数打包到同一个自定义Docker容器并部署为单个应用,扩缩容时会复制全部三个函数而非仅活动函数,这种情况下该如何实现水平扩缩容?
简言之,这个场景下最适配的打包与部署策略是什么?
最优部署策略推荐
优先选择将三个函数打包到同一个自定义Docker容器,部署为单个Azure Functions应用,原因及优化方案如下:
为什么不拆分部署?
拆分部署(活动函数单独一个应用)会引入额外复杂度:
- Durable Functions原生不支持跨应用直接调用活动函数,你需要手动实现跨应用通信(比如让编排器通过HTTP调用独立的活动函数,或者用Azure Service Bus等消息队列中转),这会打破Durable的原生重试、跟踪、状态管理机制,增加开发和维护成本。
- 拆分后需要维护两个应用的配置、权限、监控,运维复杂度翻倍。
单容器应用的扩缩容优化方案
虽然单容器会包含所有函数,但可以通过以下方式实现精准扩缩容:
- 函数级弹性缩放:在Azure Functions应用中,为活动函数单独设置缩放规则,基于活动函数的队列长度、CPU使用率等指标触发扩缩容,编排器和启动器函数由于资源消耗低,无需频繁扩缩,不会浪费资源。
- 资源隔离配置:在应用宿主配置中,限制非活动函数的资源使用率(如CPU/内存配额),确保活动函数在高负载时能获取足够资源。
- 选择合适的托管计划:使用Premium计划或Dedicated(App Service)计划,这两种计划支持自定义容器,且提供更灵活的扩缩容控制,比Consumption计划更适配自定义容器的资源需求。
备选拆分方案(仅当单容器资源消耗过高时考虑)
如果活动函数的资源需求极高,单容器的资源占用影响到编排器和启动器的性能,可以考虑拆分,但需要做以下改造:
- 将活动函数改为普通HTTP触发函数,部署到自定义容器应用中。
- 在编排器中使用
CallHttpAsync方法调用这个独立的活动函数,同时自行实现重试、超时、状态跟踪逻辑。 - 为活动函数应用配置专用的缩放规则,确保其能独立扩缩。
相关代码实现
OrchestratorStarter.cs
using Microsoft.DurableTask.Client; using Microsoft.Extensions.Logging; namespace DurableOrchestratorDotnet8Linux { public static class OrchestratorStarter { [Function("Orchestrator_HttpStart")] public static async Task<HttpResponseData> HttpStart( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, [DurableClient] DurableTaskClient client, FunctionContext executionContext) { ILogger logger = executionContext.GetLogger("Orchestrator_HttpStart"); // 函数输入来自请求内容 string instanceId = await client.ScheduleNewOrchestrationInstanceAsync( nameof(Orchestrator)); logger.LogInformation("已启动编排实例,ID = '{instanceId}'.", instanceId); // 返回包含实例管理信息的HTTP 202响应 return await client.CreateCheckStatusResponseAsync(req, instanceId); } } }
Orchestrator.cs
using Microsoft.Azure.Functions.Worker; using Microsoft.DurableTask; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Threading.Tasks; namespace DurableOrchestratorDotnet8Linux { public static class Orchestrator { [Function(nameof(Orchestrator))] public static async Task<List<string>> RunOrchestrator( [OrchestrationTrigger] TaskOrchestrationContext context) { ILogger logger = context.CreateReplaySafeLogger(nameof(Orchestrator)); logger.LogInformation("正在调用活动函数."); var outputs = new List<string>(); outputs.Add(await context.CallActivityAsync<string>(nameof(ActivityFunction), "Databricks")); return outputs; } } }
Activity.cs(需自定义容器支持ODBC驱动)
using Microsoft.Azure.Functions.Worker; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Data.Odbc; namespace DurableOrchestratorDotnet8Linux { public static class ActivityFunction { [Function(nameof(ActivityFunction))] public static string RetrieveDatabricksData([ActivityTrigger] string name, FunctionContext executionContext) { ILogger logger = executionContext.GetLogger("SayHello"); logger.LogInformation("DB Warehouse 正在向 {name} 打招呼.", name); var connectionString = "DSN=DatabricksWarehouse"; var query = "SELECT c.customer_id, c.customer_name, c.customer_email, o.order_id, o.product, o.amount" + " FROM Customers c" + " JOIN Orders o ON c.customer_id = o.customer_id"; List<string> records = new List<string>(); try { using (OdbcConnection connection = new OdbcConnection(connectionString)) { try { connection.Open(); using (OdbcCommand command = new OdbcCommand(query, connection)) { using (OdbcDataReader reader = command.ExecuteReader()) { while (reader.Read()) { string record = reader.GetString(0) + " " + reader.GetString(1) + " " + reader.GetString(2) + " " + reader.GetString(3) + " " + reader.GetString(4) + " " + reader.GetString(5); records.Add(record); } } } } catch (System.Exception ex) { logger.LogError($"发生错误: {ex.Message}"); name = ex.Message; } } } catch (System.Exception ex) { logger.LogError($"发生错误: {ex.Message}"); name = ex.Message; } return $"Hello {name}!"; } } }
Dockerfile(预安装ODBC驱动)
# 使用官方Microsoft Azure Functions基础镜像 FROM mcr.microsoft.com/azure-functions/dotnet:4.0-appservice # 安装依赖 RUN apt-get update && \ apt-get install -y curl apt-transport-https unixodbc-dev libsasl2-modules-gssapi-mit && \ curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && \ curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list > /etc/apt/sources.list.d/mssql-release.list && \ apt-get update && \ ACCEPT_EULA=Y apt-get install -y msodbcsql17 && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* # 将本地Simba Spark ODBC驱动的.deb文件复制到镜像中 COPY simbaspark_2.8.0.1002-2_amd64.deb /tmp/simbaspark_2.8.0.1002-2_amd64.deb COPY ./ini/odbc/* /usr/local/odbc/ COPY ./ini/simba.sparkodbc.ini /etc/ # 安装Simba Spark ODBC驱动所需的依赖 RUN apt-get update && \ apt-get install -y libsasl2-modules-gssapi-mit # 安装Simba Spark ODBC驱动 RUN dpkg -i /tmp/simbaspark_2.8.0.1002-2_amd64.deb && \ apt-get install -f -y && \ rm /tmp/simbaspark_2.8.0.1002-2_amd64.deb # 设置ODBC相关环境变量 ENV ODBCINI=/usr/local/odbc/odbc.ini ENV ODBCSYSINI=/usr/local/odbc/ ENV SPARKINI=/etc/simba.sparkodbc.ini ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib # 将函数应用文件复制到镜像中 COPY . /home/site/wwwroot # 暴露Azure Functions运行时监听的端口 EXPOSE 80
内容的提问来源于stack exchange,提问作者Eugene Goldberg
相关产品推荐
相关产品推荐

