Переход с Orleans версии 3.x на 7.0

Orleans 7.0 представляет несколько полезных изменений, в том числе улучшения размещения, пользовательской сериализации, неизменяемости и абстракции зерна.

Миграция

Существующие приложения, использующие напоминания, потоки или сохраняемость зерна, нельзя легко перенести на Orleans 7.0 из-за изменений в том, как Orleans идентифицирует зерна и потоки. Мы планируем постепенно предложить путь миграции для этих приложений.

Приложения, работающие в предыдущих версиях, не могут быть плавно обновлены с помощью последовательного Orleans обновления до Orleans 7.0. Поэтому необходимо использовать другую стратегию обновления, например развертывание нового кластера и вывод из эксплуатации предыдущего кластера. Orleans 7.0 изменяет протокол провода несовместимым образом, что означает, что кластеры не могут содержать сочетание Orleans узлов 7.0 и узлов с предыдущими версиями Orleans.

Мы избегали таких критических изменений в течение многих лет, даже в крупных выпусках, так почему сейчас? Существует две основные причины: удостоверения и сериализация. В отношении удостоверений, удостоверений зерна и потоков теперь состоят из строк, что позволяет зернам правильно кодировать сведения о универсальном типе и позволяет потокам проще сопоставить домен приложения. Типы зерна были ранее определены с помощью сложной структуры данных, которая не могла представлять универсальные зерна, что приводит к угловой ситуации. Потоки были определены пространством имен и ключом stringGuid, что было трудно для разработчиков сопоставить с доменом приложения, однако эффективным. Сериализация теперь является терпимой к версии, что означает, что вы можете изменять типы определенными совместимыми способами, следуя набору правил, и быть уверенным, что вы можете обновить приложение без ошибок сериализации. Это особенно проблематично, когда типы приложений сохраняются в потоках или хранилище зерна. В следующих разделах подробно описаны основные изменения и подробно их обсуждение.

Изменения упаковки

Если вы обновляете проект до Orleans версии 7.0, вам потребуется выполнить следующие действия:

  • Все клиенты должны ссылаться на Microsoft.Orleans. Клиент.
  • Все силосы (серверы) должны ссылаться на Microsoft.Orleans. Сервер.
  • Все остальные пакеты должны ссылаться на Microsoft.Orleans. Пакет SDK.
  • Удалите все ссылки на Microsoft.Orleans.CodeGenerator.MSBuild и Microsoft.Orleans.OrleansCodeGenerator.Build.
    • Замените использование KnownAssemblyGenerateCodeForDeclaringAssemblyAttributeна .
    • Пакет Microsoft.Orleans.Sdk ссылается на пакет генератора источников C# (Microsoft.Orleans.CodeGenerator).
  • Удаление всех ссылок на Microsoft.Orleans.OrleansRuntime.
  • Удаление вызовов ConfigureApplicationParts. Части приложения удалены. Генератор Orleans источников C# добавляется ко всем пакетам (включая клиент и сервер) и автоматически создает эквивалент частей приложения.
  • Замените ссылки на Microsoft.Orleans.OrleansServiceBusMicrosoft.Orleans. Streaming.EventHubs
  • Если вы используете напоминания, добавьте ссылку на Microsoft.Orleans. Напоминания
  • Если вы используете потоки, добавьте ссылку на Microsoft.Orleans. Потокового

Совет

Orleans Все примеры обновлены до Orleans версии 7.0 и могут использоваться в качестве ссылки на внесенные изменения. Дополнительные сведения см. в статье Orleans #8035 , которая содержит изменения, внесенные в каждый пример.

Директивы Orleansglobal using

Все Orleans проекты напрямую или косвенно ссылались на Microsoft.Orleans.Sdk пакет NuGet. Orleans Если проект настроен для включениянеявных использования (например<ImplicitUsings>enable</ImplicitUsings>), Orleans пространства имен и Orleans.Hosting пространства имен используются неявно. Это означает, что код приложения не нуждается в этих директивах.

Дополнительные сведения см. в разделе "НеявныеUsings" и dotnet/orleans/src/Orleans. Пакет SDK/build/Microsoft.Orleans. Sdk.targets.

Размещение на компьютере

Тип ClientBuilder был заменен методом UseOrleansClientIHostBuilderрасширения. Тип IHostBuilder поставляется из пакета NuGet Microsoft.Extensions.Hosting . Это означает, что клиент можно добавить к Orleans существующему узлу, не создавая отдельный контейнер внедрения зависимостей. Клиент подключается к кластеру во время запуска. После IHost.StartAsync завершения клиент будет автоматически подключен. Службы, добавленные в IHostBuilder порядок регистрации, поэтому вызов перед вызовом UseOrleansClientConfigureWebHostDefaults будет выполнен Orleans до запуска ASP.NET Core, например, позволяя получить доступ к клиенту из приложения ASP.NET Core немедленно.

Если вы хотите эмулировать предыдущее ClientBuilder поведение, можно создать отдельный HostBuilderOrleans объект и настроить его с помощью клиента. IHostBuilder может быть Orleans настроен клиент или Orleans silo. Все силосы регистрируют экземпляр IGrainFactory и IClusterClient который может использовать приложение, поэтому настройка клиента отдельно не требуется и не поддерживается.

OnActivateAsync изменение подписи и OnDeactivateAsync изменение подписи

Orleans позволяет зернам выполнять код во время активации и деактивации. Это можно использовать для выполнения таких задач, как чтение состояния из сообщений жизненного цикла хранилища или журнала. В Orleans версии 7.0 сигнатура этих методов жизненного цикла изменилась:

  • OnActivateAsync() теперь принимает CancellationToken параметр. CancellationToken При отмене процесса активации следует отказаться.
  • OnDeactivateAsync() теперь принимает DeactivationReason параметр и CancellationToken параметр. Указывает DeactivationReason , почему активация деактивируется. Ожидается, что разработчики будут использовать эту информацию для ведения журнала и диагностика целей. CancellationToken После отмены процесс деактивации должен быть выполнен быстро. Обратите внимание, что так как любой узел может завершиться ошибкой в любое время, не рекомендуется полагаться OnDeactivateAsync на выполнение важных действий, таких как сохранение критического состояния.

Рассмотрим следующий пример переопределения этих новых методов:

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 и IGrainBase

Зерна больше Orleans не должны наследовать от базового класса или любого другого Grain класса. Эта функция называется зернами POCO . Для доступа к методам расширения, таким как любой из следующих способов:

Ваше зерно должно реализовывать IGrainBase или наследовать от Grain. Ниже приведен пример реализации IGrainBase для класса grain:

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

    public IGrainContext GrainContext { get; }

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

IGrainBase Также определяется OnActivateAsync и OnDeactivateAsync с реализацией по умолчанию, что позволяет вашему зерне участвовать в его жизненном цикле при необходимости:

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:

Удостоверений зерна

Каждый из них имеет уникальное удостоверение, состоящее из типа зерна и его ключа. Предыдущие версии используемого составного Orleans типа для S для GrainIdподдержки ключей зерна:

Это связано с некоторой сложностью, когда речь идет о работе с ключами зерна. Удостоверения зерна состоят из двух компонентов: типа и ключа. Компонент типа ранее состоял из числового кода типа, категории и 3 байта сведений универсального типа.

Теперь удостоверения зерна имеют форму type/key , в которой оба type и key являются строками. Наиболее часто используемый интерфейс ключа зерна — это IGrainWithStringKeyинтерфейс. Это значительно упрощает работу удостоверений зерна и улучшает поддержку универсальных типов зерна.

Интерфейсы зерна также представлены с помощью имени, доступного для чтения человеком, а не сочетания хэш-кода и строкового представления любых параметров универсального типа.

Новая система является более настраиваемой, и эти настройки могут быть основаны на атрибутах.

  • GrainTypeAttribute(String) в зерне class указывает часть типа идентификатора его зерна.
  • DefaultGrainTypeAttribute(String)в зерне указывает тип зернаinterface, который IGrainFactory должен разрешаться по умолчанию при получении ссылки на зерно. Например, при вызове IGrainFactory.GetGrain<IMyGrain>("my-key")фабрика зерна вернет ссылку на "my-type/my-key" зерно, если IMyGrain указан указанный выше атрибут упоминание.
  • GrainInterfaceTypeAttribute(String) позволяет переопределить имя интерфейса. Указание имени явным образом с помощью этого механизма позволяет переименовать тип интерфейса без критической совместимости с существующими ссылками на зерна. Обратите внимание, что интерфейс также должен иметь AliasAttribute в этом случае, так как его удостоверение может быть сериализовано. Дополнительные сведения об указании псевдонима типа см. в разделе о сериализации.

Как упоминание выше, переопределение классов и интерфейсов по умолчанию для типов позволяет переименовать базовые типы без критической совместимости с существующими развертываниями.

Поток удостоверений

Когда Orleans потоки были выпущены в первый раз, потоки можно определить только с помощью .Guid Это было эффективно с точки зрения выделения памяти, но пользователям было трудно создать значимые удостоверения потока, часто требуя некоторого кодирования или косвенного определения соответствующего удостоверения потока для определенной цели.

В Orleans версии 7.0 потоки теперь определяются с помощью строк. Содержит Orleans.Runtime.StreamIdstruct три свойства: a, a StreamId.NamespaceStreamId.Keyи aStreamId.FullKey. Эти значения свойств кодируются строками UTF-8. Например, StreamId.Create(String, String).

Замена SimpleMessage Потоки на broadcastChannel

SimpleMessageStreams (также называемый SMS) был удален в версии 7.0. SMS имел тот же интерфейс, что Orleans.Providers.Streams.PersistentStreamsи , но его поведение было очень разными, так как он опирался на прямые вызовы к зерне. Чтобы избежать путаницы, sms было удалено, и был введен новый вызов Orleans.BroadcastChannel замены.

BroadcastChannel Поддерживает только неявные подписки и может быть прямой заменой в этом случае. Если вам нужны явные подписки или необходимо использовать PersistentStream интерфейс (например, вы использовали SMS в тестах во время использования EventHub в рабочей среде), то MemoryStream это лучший кандидат для вас.

BroadcastChannel будет иметь то же поведение, что и SMS, в то время MemoryStream как поведение других поставщиков потоков. Рассмотрим следующий пример использования канала широковещательного канала:

// 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, и предыдущая система была удалена в пользу стандартных API .NET, таких как Метрики .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.Streamingи Orleans.RemindersOrleans.Transactions были отделены от ядра. Это означает, что эти пакеты полностью оплачиваются за то, что вы используете , и код в основном Orleans не предназначен для этих функций. Это сокращает размер основной поверхности API и размера сборки, упрощает ядро и повышает производительность. В отношении производительности транзакции в Orleans ранее требовали некоторый код, который был выполнен для каждого метода для координации потенциальных транзакций. Это с тех пор было перемещено в метод.

Это критическое изменение компиляции. Возможно, у вас есть код, взаимодействующий с напоминаниями или потоками, вызывая методы, которые ранее были определены в базовом Grain классе, но теперь являются методами расширения. Такие вызовы, которые не указывают this (например), необходимо обновить для включения this (GetRemindersнапримерthis.GetReminders()), так как методы расширения должны быть квалифицированы. Если вы не обновляете эти вызовы, возникает ошибка компиляции, и изменение требуемого кода может не быть очевидным, если вы не знаете, что изменилось.

Клиент транзакции

Orleans 7.0 представляет новую абстракцию для координации транзакций, Orleans.ITransactionClient. Ранее транзакции могут быть координированы только зернами. С ITransactionClientпомощью , которая доступна через внедрение зависимостей, клиенты также могут координировать транзакции без необходимости промежуточного зерна. Следующий пример выводит кредиты из одной учетной записи и помещает их в другую в рамках одной транзакции. Этот код может вызываться из зерна или из внешнего клиента, который извлек из ITransactionClient контейнера внедрения зависимостей.

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

Для транзакций, координируемых клиентом, клиент должен добавить необходимые службы во время настройки:

clientBuilder.UseTransactions();

Пример BankAccount демонстрирует использование ITransactionClient. Дополнительные сведения см. в разделе Orleans "Транзакции".

Повторное получение цепочки вызовов

Зерна — это однопотоковые запросы, по одному от начала до завершения по умолчанию. Иными словами, зерна по умолчанию не повторно используются. Добавление в ReentrantAttribute класс зерна позволяет одновременно обрабатывать несколько запросов в чередующемся режиме, сохраняя однопотоковые. Это может быть полезно для зерен, которые не содержат внутреннего состояния или выполняют много асинхронных операций, таких как выдача http-вызовов или запись в базу данных. Необходимо учитывать, когда запросы могут взаимодействовать: возможно, что состояние зерна наблюдается до того, как await инструкция изменилась с момента завершения асинхронной операции, и метод возобновляет выполнение.

Например, следующее зерно представляет счетчик. Она помечена Reentrant, что позволяет выполнять несколько вызовов для взаимодействия. Метод Increment() должен увеличивать внутренний счетчик и возвращать наблюдаемое значение. Однако, так как Increment() тело метода наблюдает состояние зерна до await точки и обновляет его после этого, возможно, что несколько переключений могут привести к _value меньшему количеству Increment() полученных вызовов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;
    }
}

Чтобы предотвратить такие ошибки, по умолчанию зерна не будут повторно выполняться. Недостатком этого является снижение пропускной способности для зерна, выполняющих асинхронные операции в их реализации, так как другие запросы не могут обрабатываться, пока зерно ожидает завершения асинхронной операции. Чтобы устранить эту проблему, Orleans предлагает несколько вариантов, чтобы разрешить повторную отправку в некоторых случаях:

  • Для всего класса: размещение ReentrantAttribute на зерне позволяет любому запросу пересеять с любым другим запросом.
  • Для подмножества методов: размещение AlwaysInterleaveAttribute метода в интерфейсе зерна позволяет запросам к этому методу взаимодействовать с любым другим запросом и запрашивать запросы к этому методу, которые будут пересекаться любым другим запросом.
  • Для подмножества методов: размещение ReadOnlyAttribute метода в интерфейсе зерна позволяет запросам к этому методу взаимодействовать с любым другим запросом и запрашивать запросы к этому методу, которые будут пересекаться любым другим ReadOnlyReadOnly запросом. В этом смысле это более ограниченная форма AlwaysInterleave.
  • Для любого запроса в цепочке вызовов: RequestContext.AllowCallChainReentrancy() и <xref:Orleans. Runtime.RequestContext.SuppressCallChainReentrancy?displayProperty=nameWithType позволяет отключать и отключать подчиненные запросы для повторного входа в зерно. Оба вызова возвращают значение, которое должно быть удалено при выходе из запроса. Поэтому правильное использование выглядит следующим образом:
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;

Повторное размещение в цепочке вызовов должно быть настроено для каждого зерна, для каждой цепочки вызовов. Например, рассмотрим два зерна, зерна A и зерна B. Если зерно A включает повторное выполнение цепочки вызовов перед вызовом зерна B, зерно B может вернуться в зерно A в этом вызове. Тем не менее, зерно A не может вернуться в зерно B, если зерна B также не включили повторную цепочку вызовов. Это по зерню, по цепочке вызовов.

Зерна также могут подавлять сведения о повторном входе в цепочку вызовов из-за того, что они передаются по цепочке вызовов.using var _ = RequestContext.SuppressCallChainReentrancy() Это предотвращает повторные вызовы.

скрипты миграции ADO.NET

Чтобы обеспечить прямую совместимость с кластеризация, сохраняемостью и напоминаниями, которые зависят от Orleans ADO.NET, вам потребуется соответствующий скрипт миграции SQL:

Выберите файлы для используемой базы данных и примените их в порядке.