從 Orleans 3.x 移轉至 7.0

Orleans 7.0 引入了數項實用的變更,包括裝載、自訂序列化、不變性和 grain 抽象化的改善。

遷移

因為 Orleans 變更了識別 grain 和資料流的方式,所以使用提醒、資料流或 grain 持續性的現有應用程式,無法輕易移轉至 Orleans 7.0。 我們計劃以累加方式提供這些應用程式的移轉路徑。

執行舊版 Orleans 的應用程式無法透過 Orleans 7.0 的輪流升級順暢升級。 因此,必須使用不同的升級策略,例如部署新的叢集並解除委任先前的叢集。 Orleans 7.0 會以不相容的方式變更網路通訊協定,這表示叢集不能同時包含 Orleans 7.0 主機和執行舊版 Orleans 的主機。

我們已避免這類中斷性變更數年,甚至跨主要版本,為何現在要變更? 有兩個主要原因:身分識別和序列化。 關於身分識別,Grain 和資料流識別現在都包含字串,讓 Grain 可正確地編碼泛型型別資訊,並可讓資料流程更輕鬆對應至應用程式定義域。 Grain 類型先前是使用無法代表泛型 grain 的複雜資料結構進行識別,導致邊角案例。 資料流是根據 string 命名空間和 Guid 索引鍵所識別,雖有效率,但開發人員難以對應到自己的應用程式定義域。 序列化現在容許版本不一致,這表示您可以透過特定的兼容方式修改類型、遵循一組規則,安心升級應用程式而不會發生序列化錯誤。 當應用程式類型保存在資料流或 grain 儲存體中時,特別容易發生問題。 下列各節將詳細說明主要變更,並更詳細地討論這些變更。

封裝變更

如要將專案升級至 Orleans 7.0,即必須執行下列動作:

提示

所有 Orleans 範例皆已升級至 Orleans 7.0,可當作變更內容參考。 如需詳細資訊,請參閱 Orleans 問題 #8035,以逐一列出每個範例的變更。

Orleansglobal using 指示詞

所有 Orleans 專案都會直接或間接參考 Microsoft.Orleans.Sdk NuGet 套件。 當 Orleans 專案已設定為「啟用」隱含使用時 (例如 <ImplicitUsings>enable</ImplicitUsings>),則會隱含使用 OrleansOrleans.Hosting 命名空間。 這表示應用程式程式碼不需要這些指示詞。

如需詳細資訊,請參閱 ImplicitUsingsdotnet/orleans/src/Orleans.Sdk/build/Microsoft.Orleans.Sdk.targets

裝載

ClientBuilder 類型已更換為 IHostBuilder 上的 UseOrleansClient 擴充方法。 IHostBuilder 類型來自 Microsoft.Extensions.Hosting NuGet 套件。 這表示您可以將 Orleans 用戶端新增至現有的主機,卻不必另外建立相依性插入容器。 用戶端在啟動期間會連線到叢集。 IHost.StartAsync 完成後,用戶端會自動連線。 新增至 IHostBuilder 的服務會依註冊順序啟動,因此,比方說先呼叫 UseOrleansClient 再呼叫 ConfigureWebHostDefaults,將可確保 Orleans 比 ASP.NET Core 先啟動,可讓您立即從 ASP.NET Core 應用程式存取用戶端。

如果您想要模擬之前的 ClientBuilder 行為,您可以另行建立一個 HostBuilder,並使用 Orleans 用戶端設定。 IHostBuilder 可設定 Orleans 用戶端或 Orleans silo。 所有 silo 都會註冊應用程式可以使用的 IGrainFactoryIClusterClient 執行個體,因此沒必要也不支援分別設定用戶端。

OnActivateAsyncOnDeactivateAsync 特徵標記變更

Orleans 允許 grain 在啟用和停用期間執行程式碼。 這可用來執行讀取儲存體狀態或記錄生命週期訊息等工作。 Orleans 7.0 中變更了下列生命週期方法的簽章:

  • OnActivateAsync() 現在接受 CancellationToken 參數。 取消 CancellationToken 後,應該放棄啟用流程。
  • OnDeactivateAsync() 現在接受 DeactivationReason 參數和 CancellationToken 參數。 DeactivationReason 會指出啟用的停用原因。 開發人員應將這項資訊用於記錄與診斷。 取消 CancellationToken 後,應該會提示完成停用流程。 請注意,因為所有主機隨時都可能會故障,因此不建議依賴 OnDeactivateAsync 執行重要動作,例如保存重大狀態。

請考慮以下 grain 覆寫這些新方法的範例:

public sealed class PingGrain : Grain, IPingGrain
{
    private readonly ILogger<PingGrain> _logger;

    public PingGrain(ILogger<PingGrain> logger) =>
        _logger = logger;

    public override Task OnActivateAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("OnActivateAsync()");
        return Task.CompletedTask;
    }

    public override Task OnDeactivateAsync(DeactivationReason reason, CancellationToken token)
    {
        _logger.LogInformation("OnDeactivateAsync({Reason})", reason);
        return Task.CompletedTask;
    }

    public ValueTask Ping() => ValueTask.CompletedTask;
}

POCO Grain 和 IGrainBase

Orleans 中的 grain 不再需要自 Grain 基底類別或其他類別繼承。 此功能即是 POCO grain。 存取擴充方法,例如下列任一項:

您的 grain 必須實作 IGrainBase 或繼承自 Grain。 以下是在 grain 類別上實作 IGrainBase 的範例:

public sealed class PingGrain : IGrainBase, IPingGrain
{
    public PingGrain(IGrainContext context) => GrainContext = context;

    public IGrainContext GrainContext { get; }

    public ValueTask Ping() => ValueTask.CompletedTask;
}

IGrainBase 也會使用預設實作定義 OnActivateAsyncOnDeactivateAsync,視需要讓您的 grain 參與其生命週期:

public sealed class PingGrain : IGrainBase, IPingGrain
{
    private readonly ILogger<PingGrain> _logger;

    public PingGrain(IGrainContext context, ILogger<PingGrain> logger)
    {
        _logger = logger;
        GrainContext = context;
    }

    public IGrainContext GrainContext { get; }

    public Task OnActivateAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("OnActivateAsync()");
        return Task.CompletedTask;
    }

    public Task OnDeactivateAsync(DeactivationReason reason, CancellationToken token)
    {
        _logger.LogInformation("OnDeactivateAsync({Reason})", reason);
        return Task.CompletedTask;
    }

    public ValueTask Ping() => ValueTask.CompletedTask;
}

序列化

Orleans 7.0 最重大的變更是引入版本兼容序列化程式。 進行這項變更是因為應用程式往往需要持續改進,由於舊版序列化程式無法容許將屬性新增至現有的型別,這為開發人員帶給莫大的陷阱。 換言之,序列化程式具備彈性,可讓開發人員在不修改的情況下呈現大部分的 .NET 類型,其中包括泛型、多型和參考追蹤等功能。 舊版序列化程式早已需要替代,但使用者仍然需要其型別的高度精確表示法。 因此,Orleans 7.0 引入替代的序列化程式,支援 .NET 型別的高度精確表示法,同時可允許型別持續演進。 新的序列化程式比舊版序列化程式更有效率,提高的端對端輸送量最高可達 170%。

如需詳細資訊,請參閱下列 Orleans 7.0 相關文章:

Grain 識別

每個 grain 都有唯一的身分識別,由 grain 的類型及索引鍵所組成。 舊版的 Orleans 使用 GrainId 的複合類型支援下列兩個 grain 索引鍵的其中之一:

處理 grain 索引鍵時存在一些複雜性。 Grain 識別包含兩個元件:型別和索引鍵。 型別元件過去是由數字型別代碼、類別和 3 個位元組的泛型型別資訊所組成。

Grain 識別現在採用 type/key 格式,其中 typekey 都是字串。 最常使用的 grain 索引鍵介面是 IGrainWithStringKey。 這可大幅簡化 grain 識別的運作方式,並改善對泛型 grain 型別的支援。

Grain 介面現在也使用人類可閱讀的名稱來表示,而不是雜湊碼與任何泛型型別參數字串表示的組合。

新的系統更容易自訂,而且這些自訂項目可由屬性驅動。

  • Grain class 上的 GrainTypeAttribute(String) 會指定其 grain 識別碼的「型別」部分。
  • Grain interface 上的 DefaultGrainTypeAttribute(String) 會指定 grain 的「型別」,在取得 grain 參考時,依預設應解析 IGrainFactory。 例如,呼叫 IGrainFactory.GetGrain<IMyGrain>("my-key") 時,如果 IMyGrain 指定了上述屬性,則 grain 處理站會傳回 grain "my-type/my-key" 的參考。
  • GrainInterfaceTypeAttribute(String) 允許覆寫介面名稱。 使用此機制明確指定名稱,可重新命名介面類型,且不會中斷與現有 grain 參考的相容性。 請注意,您的介面應該也有此案例中的 AliasAttribute,因為其識別可能已序列化。 如需指定類型別名的詳細資訊,請參閱序列化內容章節。

如上所述,覆寫類型的預設 grain 類別和介面名稱,可讓您重新命名基礎類型,卻不會中斷與現有部署的相容性。

資料流識別

第一次釋出 Orleans 資料流時,只能使用 Guid 識別資料流。 這在配置記憶體方面非常有效率,但使用者很難建立有意義的資料流識別,通常需要利用編碼或靠間接方法以決定指定用途的適當資料流識別。

Orleans 7.0 現在使用字串識別資料流。 Orleans.Runtime.StreamIdstruct 包含三個屬性:StreamId.NamespaceStreamId.KeyStreamId.FullKey。 這些屬性值會編碼為 UTF-8 字串。 例如: StreamId.Create(String, String)

以 BroadcastChannel 取代 SimpleMessageStreams

7.0 已移除 SimpleMessageStreams (也稱為 SMS)。 SMS 的介面與 Orleans.Providers.Streams.PersistentStreams 相同,但行為截然不同,因為 SMS 依賴於直接的 grain 對 grain 呼叫。 為免混淆,已移除 SMS,並引入名為 Orleans.BroadcastChannel 的新替代方案。

BroadcastChannel 僅支援隱含訂閱,在此情況下可當成直接替代方案。 如果您需要明確訂閱或需要使用 PersistentStream 介面 (例如,在測試中使用 SMS,同時在生產環境中使用 EventHub),則 MemoryStream 是您的最佳選擇。

BroadcastChannel 的行為會與 SMS 相同,而 MemoryStream 的行為則與其他串流提供者相同。 請考慮下列 Broadcast Channel 使用範例:

// Configuration
builder.AddBroadcastChannel(
    "my-provider",
    options => options.FireAndForgetDelivery = false);

// Publishing
var grainKey = Guid.NewGuid().ToString("N");
var channelId = ChannelId.Create("some-namespace", grainKey);
var stream = provider.GetChannelWriter<int>(channelId);

await stream.Publish(1);
await stream.Publish(2);
await stream.Publish(3);

// Simple implicit subscriber example
[ImplicitChannelSubscription]
public sealed class SimpleSubscriberGrain : Grain, ISubscriberGrain, IOnBroadcastChannelSubscribed
{
    // Called when a subscription is added to the grain
    public Task OnSubscribed(IBroadcastChannelSubscription streamSubscription)
    {
        streamSubscription.Attach<int>(
          item => OnPublished(streamSubscription.ChannelId, item),
          ex => OnError(streamSubscription.ChannelId, ex));

        return Task.CompletedTask;

        // Called when an item is published to the channel
        static Task OnPublished(ChannelId id, int item)
        {
            // Do something
            return Task.CompletedTask;
        }

        // Called when an error occurs
        static Task OnError(ChannelId id, Exception ex)
        {
            // Do something
            return Task.CompletedTask;
        }
    }
}

移轉至 MemoryStream 會比較容易,因為只需要變更組態即可。 請考慮以下 MemoryStream 組態:

builder.AddMemoryStreams<DefaultMemoryMessageBodySerializer>(
    "in-mem-provider",
    _ =>
    {
        // Number of pulling agent to start.
        // DO NOT CHANGE this value once deployed, if you do rolling deployment
        _.ConfigurePartitioning(partitionCount: 8);
    });

OpenTelemetry

Orleans 7.0 已更新遙測系統並移除舊系統,採用標準化的 .NET API,例如用於計量的 .NET 計量和用於追蹤的 ActivitySource

與此同時,也已移除現有的 Microsoft.Orleans.TelemetryConsumers.* 套件。 我們正考慮引進一組新的套件,以簡化將 Orleans 發出的計量整合到所選監視解決方案的流程。 我們一秉初衷,歡迎意見反應和投稿文章。

dotnet-counters 工具的效能監視功能,適用於臨機操作狀況監控和第一級的效能調查。 Orleans 計數器可使用 dotnet-counters 工具監視:

dotnet counters monitor -n MyApp --counters Microsoft.Orleans

同樣地,OpenTelemetry 計量可以新增 Microsoft.Orleans 計量器,如下列程式碼所示:

builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
        .AddPrometheusExporter()
        .AddMeter("Microsoft.Orleans"));

若要啟用分散式追蹤,您可以依下列程式碼設定 OpenTelemetry:

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing.SetResourceBuilder(ResourceBuilder.CreateDefault()
            .AddService(serviceName: "ExampleService", serviceVersion: "1.0"));

        tracing.AddAspNetCoreInstrumentation();
        tracing.AddSource("Microsoft.Orleans.Runtime");
        tracing.AddSource("Microsoft.Orleans.Application");

        tracing.AddZipkinExporter(options =>
        {
            options.Endpoint = new Uri("http://localhost:9411/api/v2/spans");
        });
    });

上述程式碼已設定 OpenTelemetry 監視:

  • Microsoft.Orleans.Runtime
  • Microsoft.Orleans.Application

若要傳播活動,請呼叫 AddActivityPropagation

builder.Host.UseOrleans((_, clientBuilder) =>
{
    clientBuilder.AddActivityPropagation();
});

將核心套件的功能重構為不同的套件

在 Orleans 7.0 中,我們努力將延伸模組分解成不依賴 Orleans.Core 的個別套件。 也就是說,Orleans.StreamingOrleans.RemindersOrleans.Transactions 已與核心分開。 這表示這些套件是完全是「以量計價」,而且 Orleans 核心中沒有專供這些功能使用的程式碼。 這會縮減核心 API 介面和組件大小、簡化核心並改善效能。 至於效能方面,過去 Orleans 中的交易需要有針對每個方法執行的一些程式碼,才能協調潛在的交易。 這已改為依方法而定。

這是編譯的中斷性變更。 您可以呼叫方法藉以讓現有的程式碼與提醒或資料流互動,這些方法過去是在 Grain 基底類別中定義,但現在是擴充方法。 不指定 this 的這類呼叫 (例如 GetReminders) 必須更新以包含 this (例如 this.GetReminders()),因為必須限定擴充方法。 如果不更新這些呼叫,可能會發生編譯錯誤,而您如果不知道變更的內容,則可能看不太出來所需的程式碼變更。

交易用戶端

Orleans 7.0 引入了新的協調交易抽象概念:Orleans.ITransactionClient。 過去,交易只能由 grain 協調。 透過相依性插入取得的 ITransactionClient,用戶端不需要中繼 grain 也可以協調交易。 下列範例是僅用一筆交易,將一個帳戶的現金提出並存入到另一個帳戶。 您可以從 grain 內呼叫此程式碼,或從已自相依性插入容器擷取 ITransactionClient 的外部用戶端呼叫此程式碼。

await transactionClient.RunTransaction(
  TransactionOption.Create,
  () => Task.WhenAll(from.Withdraw(100), to.Deposit(100)));

針對用戶端協調的交易,用戶端必須在組態期間新增必要的服務:

clientBuilder.UseTransactions();

BankAccount 範例會示範 ITransactionClient 的使用方式。 如需詳細資訊,請參閱 Orleans 交易

呼叫鏈可重新進入

Grain 是單一執行緒,預設從開始到完成逐一處理的要求。 換言之,grain 預設不可重新進入。 將 ReentrantAttribute 新增至 grain 類別可允許以交錯方式同時處理多個要求,同時仍保持為單一執行緒。 這對於沒有內部狀態或執行許多非同步作業的 grain 很有用,例如發出 HTTP 呼叫或寫入資料庫。 當要求可以交錯時必須格外小心:當非同步作業完成且方法繼續執行時,可能會在 await 陳述式變更前先觀察到 grain 狀態。

例如,下列 grain 代表計數器, 已標示為 Reentrant,允許多個呼叫交錯。 Increment() 方法應該遞增內部計數器,並傳回觀察到的值。 不過,由於 Increment() 方法主體在 await 點之前便觀察到 grain 的狀態並隨之更新,因此 Increment() 的多次交錯執行可能會導致 _value 少於收到的 Increment() 呼叫總數。 這是不當使用重新進入所造成的錯誤。

移除 ReentrantAttribute 便能修正問題。

[Reentrant]
public sealed class CounterGrain : Grain, ICounterGrain
{
    int _value;
    
    /// <summary>
    /// Increments the grain's value and returns the previous value.
    /// </summary>
    public Task<int> Increment()
    {
        // Do not copy this code, it contains an error.
        var currentVal = _value;
        await Task.Delay(TimeSpan.FromMilliseconds(1_000));
        _value = currentVal + 1;
        return currentValue;
    }
}

為避免這類錯誤,grain 預設不可重新進入。 這個設定的缺點是,在實作中減少執行非同步作業的 grain 會降低輸送量,因為當 grain 等候非同步作業完成時,無法處理其他要求。 為緩和此種情況,Orleans 提供幾個選項讓您在某些情況下可重新進入:

  • 針對整個類別:將 ReentrantAttribute 放在 grain 上,可讓 grain 的任何要求與任何其他要求交錯。
  • 針對方法子集:將 AlwaysInterleaveAttribute 放在 grain 的 interface 方法上,可讓該方法的要求與任何其他要求交錯,使該方法的要求被任何其他要求交錯。
  • 針對方法子集:將 ReadOnlyAttribute 放在grain 的 interface 方法上,可讓該方法的要求與任何其他 ReadOnly 要求交錯,使該方法的要求被任何其他 ReadOnly 要求交錯。 就此而言,這是受限更多的 AlwaysInterleave
  • 對於呼叫鏈內的任何要求:RequestContext.AllowCallChainReentrancy() 和 <xref:Orleans.Runtime.RequestContext.SuppressCallChainReentrancy?displayProperty=nameWithType 可設定選擇是否允許下游要求重新進入 grain。 這兩個呼叫都會傳回結束要求時「必須」處置的值。 因此,適當的使用方式如下:
public Task<int> OuterCall(IMyGrain other)
{
    // Allow call-chain reentrancy for this grain, for the duration of the method.
    using var _ = RequestContext.AllowCallChainReentrancy();
    await other.CallMeBack(this.AsReference<IMyGrain>());
}

public Task CallMeBack(IMyGrain grain)
{
    // Because OuterCall allowed reentrancy back into that grain, this method 
    // will be able to call grain.InnerCall() without deadlocking.
    await grain.InnerCall();
}

public Task InnerCall() => Task.CompletedTask;

呼叫鏈結可重新進入必須針對每個 grain、每個呼叫鏈選擇加入。 例如,假設有兩個 grain:grain A 與 grain B。如果 grain A 在呼叫 grain B 之前啟用呼叫鏈重新進入,則 grain B 可以在該呼叫中回呼 grain A。 不過,如果 grain B 沒有「同時」啟用呼叫鏈重新進入,則 grain A 無法回呼 grain B。 這是每個 grain、每個呼叫鏈獨立的設定。

Grain 也可以使用 using var _ = RequestContext.SuppressCallChainReentrancy(),在呼叫鏈向下流動時隱藏呼叫鏈重新進入資訊。 這可阻止後續呼叫重新進入。

ADO.NET 移轉指令碼

為確保 Orleans 叢集的正向相容性、持續性以及依賴 ADO.NET 的提醒,您將需要適當的 SQL 移轉指令碼:

選取所用的資料庫檔案,並依序套用。