- 后端
- 微服务
【免费下载链接】orleans
Cloud Native application framework for .NET
RequestContext是 Orleans 运行时提供的一块随请求流动的元数据载体:调用方在发起 grain 调用前写入条目,运行时自动将其复制进请求消息,接收方 grain 与下游调用都能读取同一份上下文。本文基于官方文档与仓库源码,完整讲解其 API、传播机制、安全边界、与放置(placement)及调用链可重入性的交互,帮助你用它承载关联 ID(correlation ID)、租户 ID 与授权上下文等应用元数据。
RequestContext 是什么
在 Orleans.Runtime.RequestContext 的源码注释中,它被定义为"当前正在处理请求的相关信息集合",且明确设计为可供应用代码直接使用。它是一个属性包(property bag):某些值由运行时默认提供(例如内部用于调度控制的重入标识),其余值由应用代码写入,随请求自动传播。
典型用途包括:
- 关联 ID(correlation ID):把一次用户操作在客户端、多个 grain、多次调用间的日志串联起来;
- 租户 ID(tenant ID):多租户应用在 grain 内快速识别当前租户;
- 授权上下文(authorization context):由可信应用代码建立的调用方身份信息。
源码中还保留了两个内部保留键,应用代码不应直接操作:
| 常量 | 值 | 用途 |
|---|---|---|
CALL_CHAIN_REENTRANCY_HEADER | #CCR | 携带当前调用链的重入标识(Guid),见"调用链可重入性"一节 |
PING_APPLICATION_HEADER | Ping | 用于运行时内部的 Ping 请求标记 |
设置与读取:最小可用示例
完整的示例代码位于 RequestsAndVersioningSnippets.cs(Documentation.Grains.RequestContextExamples命名空间)。
调用方写入上下文,再发起调用:
RequestContext.Set("trace-id", Guid.NewGuid().ToString("N")); IOrderGrain order = grainFactory.GetGrain<IOrderGrain>("order-42"); await order.Submit();接收方 grain 读取该值:
public sealed class OrderGrain( ILogger<OrderGrain> logger) : Grain, IOrderGrain { public Task Submit() { string? traceId = RequestContext.Get("trace-id") as string; logger.LogInformation( "Submitting order with trace ID {TraceId}", traceId); return Task.CompletedTask; } }两个使用要点(文档明确强调):
- 值必须可被 Orleans 序列化。因为请求上下文会被放进请求消息跨 silo 传输,任何不可序列化的对象都会在调用链路上失败;
- 值应尽量小。上下文随每个请求消息携带,条目越多、值越大,网络开销越高。
Get返回object?,因此示例中读取后需要as string进行类型转换;从源码 RequestContext.Get 看,它只是从当前AsyncLocal中的字典取出值,找不到键时返回null。
静态 API 全览:Get / Set / Remove / Clear
RequestContext是一个静态类,全部通过静态方法操作,管理示例同样来自文档片段:
object? value = RequestContext.Get("tenant-id"); RequestContext.Set("tenant-id", "tenant-17"); bool removed = RequestContext.Remove("tenant-id"); RequestContext.Clear();完整 API 及语义如下(对应源码 RequestContext.cs):
| API | 签名 | 说明 |
|---|---|---|
Get(string key) | object? | 读取指定键的值,不存在时返回null |
Set(string key, object value) | void | 写入或更新指定键 |
Remove(string key) | bool | 移除指定键,若该键原本存在则返回true |
Clear() | void | 清空当前请求上下文 |
Keys | IEnumerable<string> | 当前上下文中所有键 |
Entries | IEnumerable<KeyValuePair<string, object>> | 当前上下文中所有条目 |
AllowCallChainReentrancy() | ReentrancySection | 打开调用链可重入作用域(见后文) |
SuppressCallChainReentrancy() | ReentrancySection | 抑制调用链可重入作用域 |
ReentrancyId | Guid | 获取或设置当前调用链重入标识 |
底层实现:AsyncLocal 与写时复制
从源码看,RequestContext的存储核心是一行:
internal static readonly AsyncLocal<ContextProperties> CallContextData = new();ContextProperties内部持有一个Dictionary<string, object>?。值得注意的实现细节是 Set 采用写时复制(copy-on-write):每次写入都会基于旧字典复制出一份新字典再替换回去。源码注释解释了原因——AsyncLocal的"复制"语义是共享同一个字典对象引用,如果不复制字典,一个执行流中的修改就会泄漏影响其他线程。Remove也遵循同样的复制策略。
由此可以推断两个行为特性:
- 每个异步执行流对上下文的修改是隔离的,不会相互污染;
- 频繁调用
Set/Remove会带来字典复制的分配成本,因此应当"在需要它的操作附近尽早设置,用完即清理",而不是在长生命周期流程中反复增删。
仓库中的 RequestContextTestsNonSiloRequired.cs 用 1000 个并行Task.Run任务验证了这种隔离性:每个子任务对同名键的修改互不影响,主执行流的值始终保持最初设置的结果(RequestContext_CrossThread测试)。
传播机制:从 async-local 到请求消息再到下游调用
文档对传播机制的定义是:
请求上下文使用异步局部存储(async-local storage)。当代码发送 grain 调用时,Orleans 会把当前条目复制进发出的请求;接收方 grain 能看到这些条目,而它发起的调用会把自身当前上下文继续向下游传播。
这条链路的源码级证据清晰可循:
- 导出(Export):RequestContextExtensions.Export 使用
DeepCopier对当前字典做深拷贝,返回一份独立副本; - 装入消息:MessageFactory.cs 在创建请求消息时调用
RequestContextExtensions.Export(_deepCopier)并把结果赋给Message.RequestContextData; - 序列化传输:MessageSerializer.cs 在消息头带
HasRequestContextData标志时,将请求上下文作为消息的最后一个字段写入并读取; - 接收端导入:接收方运行时调用
RequestContextExtensions.Import(RequestContextExtensions.cs),清空当前上下文并载入消息中携带的条目; - 下游继续传播:grain 内再发起的调用会重复上述导出→装入→传输→导入过程,因此上下文沿整条调用链逐跳向下游流动。
两条重要语义
- 调用方对上下文的修改不会回流:文档明确"被调用方做的修改不会随响应返回",请求上下文是下游元数据(downstream metadata),不要把它当作返回通道。想回传数据请使用方法返回值或
Response; - 上下文是调用链视角的:接收方 grain 看到的是上游传入的快照;如果接收方在同一异步流里先处理了一个上游请求的上下文,又处理另一个无关操作,就需要主动
Clear()或恢复,避免把上一个请求的上下文泄漏给无关的下游调用。
测试 RequestContext_ActivityId_ExportImport 直接验证了"导出到消息 → 清空当前上下文 → 从消息导入 → 重新读到相同值"的完整往返过程,可作为理解该机制的行为基准。
安全边界:请求上下文是调用方提供的数据
文档用专门一节强调安全问题:
请求上下文是调用方提供的数据。不要仅仅因为某个角色、用户 ID 或租户 ID 出现在
RequestContext中就信任它。应当在可信边界建立认证,并使用调用过滤器(call filters)或应用授权逻辑来校验访问。
这意味着:
- 认证(authentication)必须发生在可信边界,例如客户端连接层、网关或专门的认证服务,而不是信任上游 grain 塞进上下文的字符串;
- 授权(authorization)应当在每次需要时重新校验,可以用 Orleans 的调用过滤器(
IIncomingGrainCallFilter/IOutgoingGrainCallFilter)统一检查,也可以在每个 grain 方法内做应用级校验; - 上下文里的身份信息只应作为"便捷的请求属性",而非安全凭证。
关于身份传播与授权的完整指引,见仓库文档 authentication-authorization.md。
Placement 与迁移:激活创建前的特殊读取路径
放置(placement)发生在新激活(activation)被创建之前,此时还没有任何 grain 激活在运行,因此静态RequestContext在放置判定器(placement directors)和过滤器内部是空的。文档给出的正确读取方式是:
读取
Orleans.Runtime.Placement.PlacementTarget.RequestContextData。
源码印证了这一设计:PlacementTarget 在构造时接收Dictionary<string, object> requestContextData并暴露为RequestContextData属性;而 PlacementService.cs 正是用触发放置的第一条消息(firstMessage)携带的RequestContextData来构造PlacementTarget:
var target = new PlacementTarget( firstMessage.TargetGrain, firstMessage.RequestContextData!, firstMessage.InterfaceType, firstMessage.InterfaceVersion);所以自定义放置逻辑若要感知请求元数据(例如根据租户 ID 选择 silo),应当从PlacementTarget.RequestContextData读取,而不是RequestContext.Get。
MigrateOnIdle 与放置提示
当 grain 通过MigrateOnIdle()请求迁移时,Orleans 会捕获当前请求上下文并使其对放置逻辑可用(见文档"Placement and migration"一节)。这允许应用借助请求上下文提供放置提示(placement hints),例如"这个 grain 应迁移到租户所在的区域"。文档同时提醒:自定义放置逻辑属于高级运行时扩展,需要谨慎使用。
调用链可重入性与保留键
调用链可重入(call-chain reentrancy)用于解决一类经典死锁:grain A 调用 grain B,B 又回调 A,而 A 默认不可重入,于是 B 的调用一直排队直到 A 完成,但 A 又在等 B——形成循环死锁。RequestContext.AllowCallChainReentrancy()允许某个明确的调用路径回调到发起该链的激活中:
public async Task JoinRoom(string roomName) { using var scope = RequestContext.AllowCallChainReentrancy(); IChatRoomGrain room = GrainFactory.GetGrain<IChatRoomGrain>(roomName); await room.Join(this.AsReference<IUserGrain>()); }从源码看,其实现正是借助请求上下文内部的#CCR键(CALL_CHAIN_REENTRANCY_HEADER):
AllowCallChainReentrancy()(RequestContext.cs):若当前没有重入标识则生成一个新的Guid,构造一个ReentrancySection作用域;SuppressCallChainReentrancy()(RequestContext.cs):把重入标识临时置为Guid.Empty;ReentrancySection.Dispose():作用域结束时恢复进入前的原始ReentrancyId,并通知激活离开重入区段。
文档对使用方式给出两条硬性约束:
- 必须使用
using作用域消费返回的ReentrancySection,确保在异步流程的合适位置自动恢复状态; - 不要直接设置 Orleans 保留的上下文键(即
#CCR等),内部机制依赖这些键的语义,直接写入会破坏运行时调度。
该机制的完整调度语义(回合制执行、交错与可重入)见 request-scheduling.md 的 "Call-chain reentrancy" 一节;文档还提示它比给整个 grain 打[Reentrant]更窄、更可控——只放行属于该调用链的回调,而不是所有请求。
最佳实践小结
综合官方文档与源码实现,将请求上下文用好可以总结为几条实践准则:
- 尽早设置、用后即清:在需要上下文的那次操作附近写入,并在同一异步流执行无关操作前
Clear()或恢复原值,防止上下文泄漏; - 保持小而可序列化:只放必要元数据(关联 ID、租户 ID、身份标识等),避免放入大对象或不可序列化类型,因为它会随每个请求消息传输;
- 只作下游传播,不作回传通道:被调用方的修改不会回流,跨 grain 回传数据请走返回值;
- 不信任上下文中的身份:认证在可信边界完成,授权用调用过滤器或应用逻辑校验;
- 放置逻辑读
PlacementTarget.RequestContextData:激活尚未创建时静态RequestContext不可用; - 可重入用作用域 API:用
using var scope = RequestContext.AllowCallChainReentrancy();解决明确的循环回调死锁,不要直接写内部保留键。
掌握这些约定后,你可以在 Orleans 应用中用一套统一的机制串联日志、隔离租户、传递授权属性,并让自定义放置逻辑感知请求元数据——而这一切都建立在运行时对 async-local 存储、消息导出/导入与序列化的完整支持之上。
- 后端
- 微服务
【免费下载链接】orleans
Cloud Native application framework for .NET
相关推荐
Orleans 客户端与 Grain 调用安全:身份认证、授权边界与纵深防御实践
Orleans 客户端与 Grain 调用安全:身份认证、授权边界与纵深防御实践 本文聚焦 .NET 云原生框架 Orleans( Orleans.slnx h
后端微服务gorush中的上下文传递:在协程间传递请求ID与元数据
gorush中的上下文传递:在协程间传递请求ID与元数据 在分布式系统中,追踪请求流转路径是排查问题的关键。当一个推送请求从API网关进入gorush(一个用G
后端Permify 多租户架构实战:用 tenant_id 隔离 Schema 与授权数据的完整指南
Permify 多租户架构实战:用 tenant_id 隔离 Schema 与授权数据的完整指南 本文以 Permify 官方文档 docs/use cases
认证鉴权后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考