你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn。
本文将向您展示如何在现有的 Durable Functions 应用中采用类型安全(也称为类型感知)的载荷序列化,该应用采用 Python 编程模型。 类型安全的序列化会根据预期类型验证反序列化后的负载,并允许你选择启用更严格的 严格模式,从而消除对不可信负载进行反序列化的风险。
采用类型安全序列化是所有使用 Python 的 Durable Functions 应用(包括不安全敏感的应用)的推荐最佳实践。 它帮助你及早发现类型不匹配的错误,因为SDK会根据代码预期的类型验证每个负载,而不是默默重建存储数据命名的类型。 严格模式还能增强应用对不可信负载反序列化的防御,从而提升代码的安全。
azure-functions SDK将严格模式宣传为最佳实践,本文将一步步引导你逐步采用,从向后兼容的步骤开始。
该功能通过两个协同工作的软件包提供:
-
azure-functions为集中式串行器df_dumps/df_loads()提供可选的类型验证和严格类型支持。 -
azure-functions-durable将 Durable Functions 的所有负载序列化都交由这些序列化器处理,并为编排和实体 API 添加了expected_type参数以及自动类型发现功能。
有关哪些数据 Durable Functions 会持久化以及自定义类型如何被序列化,请参见 Durable Functions 中的 Data persistence and serialization。
哪些更改
在此功能推出之前,Durable Functions 会通过读取存储的 JSON 中嵌入的 __module__ 和 __class__ 字段,并调用 importlib.import_module() 来定位该类,从而反序列化自定义对象有效负载。 没有检查载荷中的类是否符合你的代码预期类型。
类型安全序列化新增:
- 关于编排和实体 API 的可选
expected_type参数,用于反序列化负载。 -
自动类型发现会读取使用 v2 装饰器修饰的活动和子编排器函数的返回类型注解,并将其直接用作
expected_type,无需更改任何代码。 - 一种可通过 环境变量启用的
AZURE_FUNCTIONS_DURABLE_STRICT_TYPING,会将类型不匹配直接报错,并在不调用importlib.import_module()的情况下反序列化自定义对象。
序列化格式保持不变。 内置类型仍然序列化为纯JSON,自定义对象也仍使用该 {"__class__", "__module__", "__data__"} 惯例。 这意味着松散模式完全向下兼容:现有历史和飞行中编排继续像以前一样去序列化。
先决条件
一个现有的 Durable Functions 应用,使用 Python 编程模型(v1 或 v2)。
以下是包含集中式
df_dumps/df_loads序列化器的最低软件包版本:Python 版本 最低 azure-functions版本3.13及以后版本 2.2.0 3.10 – 3.12 1.26.0 azure-functions-durable1.6.0或更高版本。
注释
如果已安装的 azure-functions 包不提供 df_dumps / df_loads,Durable Functions 会回退到旧版序列化管道。 持久化的JSON格式保持不变,但 expected_type 参数和严格模式没有影响。 升级到前表中的版本以启用类型验证序列化。
松散模式与严格模式的比较
类型安全序列化有两种模式。
| Behavior | 松散模式(默认) | 严格模式 |
|---|---|---|
| 选择加入 | 始终打开 | 设 AZURE_FUNCTIONS_DURABLE_STRICT_TYPING 为 1, true或 yes |
| 类型不匹配 | 先记录一条警告日志,然后回退到旧版解码器 | 加薪 TypeError |
| 自定义对象解码 | 使用 importlib.import_module()(遗留路径) |
直接调用 expected_type.from_json();从不调用 import_module |
to_json
/
from_json 合同 |
不变 | 必须对称且能原生生成可JSON序列化的数据(参见 更新to_json和from_json) |
| 向后兼容 | 是的 | No. 需要代码更改 |
松散模式可以立即采用,因为它不会改变正确类型负载的行为。 严格模式是一种有意为之、加强安全防护的变更,需要后续的迁移步骤。
逐步迁移
分阶段采用类型安全序列化。 步骤1和2是向下兼容的,可以单独发货。 只有在准备好启用严格模式时,才完成第3和第4步。
步骤1:升级套餐
将应用的要求更新为 先决条件中的最低版本。 例如,在 requirements.txt中:
azure-functions>=2.2.0
azure-functions-durable>=1.6.0
升级后,应用仍会以松散模式运行,行为没有变化。 你不需要做其他改动来保持现有应用正常运行。
步骤2:采用松散模式类型验证
在宽松模式下,提供预期类型,以便 SDK 能够验证反序列化后的载荷,并在出现任何不匹配时记录警告。 你可以用三种方式供应这种类型,并根据需要混合。
在活动和子编排器中添加返回类注释。 在 Python v2 编程模型中,SDK 会自动发现返回注释并用它来验证结果。 无需更换通话地点。
@myApp.activity_trigger(input_name="city")
def get_weather(city: str) -> WeatherReport:
return WeatherReport(city=city, temperature_c=21)
@myApp.orchestration_trigger(context_name="context")
def orchestrator(context: df.DurableOrchestrationContext):
# The WeatherReport return annotation on get_weather is discovered
# automatically and used to validate the result.
report = yield context.call_activity("get_weather", "Seattle")
return report.temperature_c
显式地传递 expected_type。 显式注释 expected_type 优先于发现的注释。 当返回类型不是具体类时使用。 例如,诸如 list[Order] 或 Optional[Order] 之类的通用别名无法被自动发现。
orders = yield context.call_activity("get_orders", customer_id, expected_type=list)
该 expected_type 论证可在以下编排API中找到:
-
call_activity和call_activity_with_retry -
call_sub_orchestrator和call_sub_orchestrator_with_retry call_entitywait_for_external_eventget_input
对于这些实体 API,可通过 DurableEntityContext:
get_stateget_input
在触发器上声明编排输入类型。 在 input_type 上使用 orchestration_trigger 参数,以便让 context.get_input() 验证输入。 位于 expected_type 上的调用点 get_input() 具有更高优先级。
@myApp.orchestration_trigger(context_name="context", input_type=OrderRequest)
def orchestrator(context: df.DurableOrchestrationContext):
request = context.get_input() # validated against OrderRequest
...
完成这一步后,运行你的应用,观察日志下 azure.functions.DurableFunctions 是否有类型不匹配的警告。 在进入严格模式之前,先解决任何警告。 因为这一步只会增加警告,所以单独部署是安全的。
Tip
自动类型发现只解析具体 type 对象。 通用别名(例如 list[Order]、dict[str, Order] 和 Optional[Order])会解析为“无类型信息”,解码时则会回退为仅模块解析。 当你需要验证这些形状时,要明确提供 expected_type 。
步骤3:更新to_json和from_json以适应严格模式
严格模式会改变自定义类型的合同。 在严格模式下, to_json() 必须返回一个可以本地序列化的值 json.dumps ,如字典、列表、字符串、数字、布尔值或 None。 你必须显式序列化嵌套的自定义对象,而不是以实例形式返回,并且 from_json() 必须对称地重建它们。
该要求在每个嵌套层移除 __module__ 存储载荷的字符串,因此反序列化不再需要从载荷数据中解析类型名称。
class Order:
def __init__(self, item, hat):
self.item = item
self.hat = hat
@staticmethod
def to_json(obj):
return {
"item": obj.item,
"hat": Hat.to_json(obj.hat), # explicit, not obj.hat
}
@staticmethod
def from_json(data):
return Order(
item=data["item"],
hat=Hat.from_json(data["hat"]), # symmetric
)
在部署过程中处理飞行中的遗留有效载荷。 如果你的应用在升级后仍可能读取升级前以宽松模式写入的有效负载,请让 from_json 兼容这两种形式。 松散编码的嵌套值会以已重建实例的形式传入(此时会触发旧版的 object_hook),而严格编码的值则会以普通字典的形式传入。
@staticmethod
def from_json(data):
hat_data = data["hat"]
if isinstance(hat_data, Hat):
hat = hat_data # loose-encoded: object already built
else:
hat = Hat.from_json(hat_data) # strict-encoded: plain dict
return Order(item=data["item"], hat=hat)
步骤4:启用严格模式
将应用设置设置为 AZURE_FUNCTIONS_DURABLE_STRICT_TYPING1、 或 trueyes (不区分大小写)。
在你的本地 local.settings.json 中:
{
"Values": {
"AZURE_FUNCTIONS_DURABLE_STRICT_TYPING": "true"
}
}
或者,将其设为你的函数应用上的一项应用设置:
az functionapp config appsettings set --name <APP_NAME> --resource-group <RESOURCE_GROUP> --settings AZURE_FUNCTIONS_DURABLE_STRICT_TYPING=true
严格模式下:
- 类型不匹配会引发
TypeError,而不是记录警告日志。 - 自定义对象通过直接调用
expected_type.from_json()来反序列化,因此import_module从未被使用。 - 任何未使用
expected_type反序列化自定义对象的调用点都会引发TypeError。 在启用严格模式之前,确保每个此类呼叫网站都通过 步骤2 中的某个机制提供类型。 - 活动函数 的输入 不能是自定义对象。 请参阅以下注释。
Important
在严格模式下,活动函数的 输入 不能是自定义对象。 当主机调用活动时,azure-functions 活动触发器转换器会在没有 expected_type 的情况下反序列化输入,因为 Functions 工作进程不会将该活动的参数类型注解转发给转换器。 因此,自定义对象输入在遇到 ValueError 时会失败。 将活动输入传递为原生可序列化的 JSON 值,如字典、列表、字符串、数字、布尔值或 None。 如果你需要发送自定义对象,先用它的 to_json() 方法在调用前转换它,然后在活动中重建它 from_json() 。 这一限制仅适用于活动输入。 活动返回值、编排和实体输入、实体状态以及外部事件载荷在你提供类型时都支持严格模式下的自定义类型。
Important
只有在所有应用实例升级完毕,且携带松散编码历史的飞行中编排被耗尽,或者你的 from_json 方法容忍两种格式(步骤3)后,才启用严格模式。 这段在升级前开始的编排,重现了其原始、松散编码的历史。 如果你的代码在严格模式下无法解码这些历史,重放就会失败。
版本管理对现有编排的影响
更新为类型安全序列化后,如果有效载荷类型与遗留实现不同,运行中的编排就会中断。 每当一个编曲继续时,它都会重播其存储的历史。 如果某个解码位置现在期望的类型与旧负载中存储的类型不匹配,严格模式就会抛出一个TypeError,而这个错误在首次写入历史记录时并不存在,并会导致编排失败。 两种常见的迁移变化导致了这种不匹配:
- 这条路以前存在不止一种类型。 如果单一反序列化路径(例如活动结果)此前可能返回不同的对象类型,而现在你使用单个
expected_type对其进行注解,那么先前使用其他类型的已存储负载将不再匹配,并会解码失败。 - 用作活动输入的自定义类型 由于 严格模式下活动输入不能是自定义对象,采用严格模式下需要将这些输入改为可序列化的 JSON 值,这会改变运行实例所保留的有效载荷形状。
更一般地,任何使有效载荷存储类型与解码站现在预期的类型不同的更改都会导致同样的错误。 例如,在实例被持久化后重命名或移动自定义类,也会导致同样的不匹配。
为了安全迁移,请采用以下方法之一:
- 建议:将推送过程与编排版本分开。 使用编排版本控制和
Strict版本匹配策略,以便新的严格模式工作者仅处理在新版本上启动的编排。 这种最佳实践让两个版本在 滚动升级 时可以共存,避免了重放失败。 - 另一种选择:先排水。 让所有飞行中的编排结束,然后启用严格模式。
在生产环境中启用严格模式之前,请先确认每个自定义对象解码位置都提供类型信息,并且你的自定义类保持与运行中的实例持久化其数据载荷时相同的类名和模块名。
有关如何安全部署会影响正在运行的业务流程编排的更改的更多指导,请参阅 Durable Functions 中的版本控制。
安全性强化
严格模式增强了自定义对象负载的反序列化方式。 严格模式不会信任存储的或传入载荷中嵌入的模块名和类名来定位类型,而是使用你的代码提供的 expected_type 重建自定义对象;并且,严格模式下的 to_json() 输出不会在任何嵌套层级保留模块名。 这一变更消除了在反序列化期间从负载数据中解析任意类型名称的需求;相较于依赖负载中携带的类型信息,这是一项纵深防御方面的改进。
如果你的有效载荷可能包含敏感数据,另请参阅处理敏感数据。