Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bump the dotnet group in /docs/orleans/grains/snippets/timers with 3 updates #3735

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

dependabot[bot]
Copy link

@dependabot dependabot bot commented on behalf of github Jul 17, 2024

Bumps the dotnet group in /docs/orleans/grains/snippets/timers with 3 updates: Microsoft.Orleans.Core.Abstractions, Microsoft.Orleans.Reminders and Microsoft.Orleans.Sdk.

Updates Microsoft.Orleans.Core.Abstractions from 8.1.0 to 8.2.0

Release notes

Sourced from Microsoft.Orleans.Core.Abstractions's releases.

v8.2.0

New features

Activation repartitioning

ActivationRepartitioning.mp4

Above: a demonstration showing Activation Repartitioning in action. The red lines represent cross-silo communication. As the red lines are eliminated by the partitioning algorithm, throughput improves to over 2x the initial throughput.

Ledjon Behluli and @​ReubenBond implemented activation repartitioning in #8877. When enabled, activation repartitioning collocates grains based on observed communication patterns to improve performance while keeping load balanced across your cluster. In initial benchmarks, we observe throughput improvements in the range of 30% to 110%. The following paragraphs provide more background and implementation details for those who are interested. The feature is currently experimental and to enable it you need to opt-in on every silo in your cluster using the ISiloBuilder.AddActivationRepartitioner() extension method, suppressing the experimental feature warning:

#pragma warning disable ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
siloBuilder.AddActivationRepartitioner();
#pragma warning restore ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

The fastest and cheapest grains calls are ones which don't cross process boundaries. These grain calls do not need to be serialized and do not need to incur network transmission costs. For that reason, collocating related grains within the same host can significantly improve the performance of your application. On the other hand, if all grains were placed in a single host, that host may become overloaded and crash, and you would not be able to scale your application across multiple hosts. How can we maximize collocation of related grains while keeping load across your hosts balanced? Before describing our solution, we need to provide some background.

Grain placement in Orleans is flexible: Orleans executes a user-defined function when deciding where in a cluster to place each grain, providing your function with a list of the compatible silos in your cluster, that is, the silos which support the grain type and interface version which triggered placement. Grains calls are location-transparent, so callers do not need to know where a grain is located, allowing grains to be placed anywhere across your cluster of hosts. Each grain's current location is stored in a distributed directory and lookups to the directory are cached for performance.

Resource-optimized placement was implemented by @​ledjon-behluli in #8815. Resource-optimized placement uses runtime statistics such as total and available memory, CPU usage, and grain count, collected from all hosts in the cluster, smooths them, and combines them to calculate a load score. It selects the least-loaded silo from a subset of hosts to balance load evenly across the cluster[^4]. If the load score of the local silo is within some configured range of the best candidate's load score, the local silo is chosen preferentially. This improves grain locality by leveraging the knowledge that the local silo initiated a call to the grain and therefore has some relation to that grain. Ledjon wrote more about Resource-optimized placement in this blog post.

Originally, there was no straightforward way to move an active grain from one host to another without needing to fully deactivate the grain, unregister it from the grain directory, contend with concurrent callers on where to place the new activation, and reload its state from the database when the new activation is created. [Live grain migration was introduced in #8452](dotnet/orleans#8452), allowing grains to transparently migrate from one silo to another on-demand without needing to reload state from the database, and without affecting pending requests. Live grain migration introduced two new lifecycle stages: dehydration and rehydration. The grain's in-memory state (application state, enqueued messages, metadata) is dehydrated into a migration packet which is sent to the destination silo where it's rehydrated. Live grain migration provided the mechanism for grains to migrate across hosts, but did not provide any out-of-the-box policies to automate migration. Users trigger grain migration by calling this.MigrateOnIdle() from within a grain, optionally providing a placement hint which the grain's configured placement director can use to select a destination host for the grain activation.

Finally, we have the pieces in place for activation repartitioning: grain activations are load-balanced across the cluster, and they are able to migrate from host to host quickly. While live grain migration gives developers a mechanism to migrate grain activations from one host to another, it does not provide any automated policy to do so. Remember, we want grains to be balanced across the cluster and collocated with related grains to reduce networking and serialization cost. This is a difficult challenge since:

  • An application can have millions of in-memory grains spread across tens or hundreds of silos.
  • Each grain can message any other grain.
  • The set of grains which each grain communicates with can change from minute to minute. For example, in an online game, player grains may join one match and communicate with each other for some time and then join a different match with an entirely different set of players afterwards.
  • Computing the minimum edge-cut for an arbitrary graph is NP-hard.
  • No single host has full knowledge of which grains are hosted on which other host and which grains they communicate with: the graph is distributed across the cluster and changes dynamically.
  • Storing the entire communication graph in memory could be prohibitively expensive.

Folks at Microsoft Research studied this problem and proposed a solution in a paper titled Optimizing Distributed Actor Systems for Dynamic Interactive Services. The paper, dubbed ActOp, proposes a decentralized approximate solution which achieves good results in their benchmarks. Their implementation was never merged into Orleans and we were unable to find the original implementation on Microsoft's internal network. So, after first implementing resource-optimized placement, community contributor @​ledjon-behluli set out to implement activation repartitioning from scratch based on the ActOp paper. The following paragraphs describe the algorithm and the enhancements we made along the way.

The activation repartitioning algorithm involves pair-wise exchange of grains between two hosts at a time. Silos compute a candidate set of grains to send to a peer, then the peer does similarly, and uses a greedy algorithm to determine a final exchange set which minimizes cost while keeping silos balanced.

To compute the candidate sets, silos track which grains communicate with which other grains and how frequently. The whole graph would be unwieldy, so we only maintain the top-K communication edges using a variant of the Space-Saving[^1] algorithm. Messages are sampled via a multi-producer, single consumer ring buffer which drops messages if the partition is full. They are then processed by a single thread, which yields frequently to give other threads CPU time. When the distribution has low skew and the K parameter is fairly small, Space-Saving can require a lot of costly shuffling at the bottom of its max-heap (we use the heap variant to reduce memory). To address this, we use Filtered Space-Saving[^2] instead of Space-Saving. Filtered Space-Saving involves putting a 'sketch' data structure at the bottom of the max heap for the lower end of the distribution, which can greatly reduce churn at the bottom and improve performance by up to ~2x in our tests.

If the top-K communication edges are all internal (eg, because the algorithm has already optimized partitioning somewhat), silos won't find many good transfer candidates. We need to track internal edges to work out which grains should/shouldn't be transferred (cost vs benefit). To address this, we introduced a bloom filter to track grains where the cost of movement is greater than the benefit, removing them from the top-K data structure. From our experiments, this works very well with even a 10x smaller K. This performance improvement will come with a reduced ability to handle dynamic graphs, so in the future we may need to implement a decay strategy to address this as the bloom filter becomes saturated. To improve lookup performance, @​ledjon-behluli implemented a blocked bloom filter[^3], which is used instead of a classic bloom filter.

[^1]: Efficient Computation of Frequent and Top-k Elements in Data Streams by Metwally, Agrawal, and Abbadi [^2]: Finding top-k elements in data streams by Nuno Homem & Joao Paulo Carvalho [^3]: Cache-, Hash- and Space-Efficient Bloom Filters by Felix Putze, Peter Sanders and Johannes Single [^4]: The Power of Two Choices in Randomized Load Balancing by Michael David Mitzenmacher

Enhancements to grain timers

... (truncated)

Commits
  • 92e0bf3 Allow GrainTimers to dispose themselves from their own callback (#9065)
  • 77a187e StatelessWorker: pump work item queue consistently (#9064)
  • 2af2970 ActivationData: get IGrainActivator from shared components consistently (#9063)
  • e19a176 Promptly terminate AdaptiveDirectoryCacheMaintainer (#9062)
  • a264ce0 Coordinate shutdown of AdaptiveDirectoryCacheMaintainer with LocalGrainDirect...
  • 8035ed9 Replace custom GetHashCode implementations with HashCode.Combine (#9059)
  • e033fbd MessagePack codec (#8546)
  • a85e337 Activation repartitioner: use null for no-op message observer to avoid inte...
  • a44d4a3 Implement incoming grain call filters for observers (#9054)
  • 6ff7edc Fix flaky `Should_ConvertAllRemoteCalls_ToLocalCalls_WhileRespectingTolerance...
  • Additional commits viewable in compare view

Updates Microsoft.Orleans.Reminders from 8.1.0 to 8.2.0

Release notes

Sourced from Microsoft.Orleans.Reminders's releases.

v8.2.0

New features

Activation repartitioning

ActivationRepartitioning.mp4

Above: a demonstration showing Activation Repartitioning in action. The red lines represent cross-silo communication. As the red lines are eliminated by the partitioning algorithm, throughput improves to over 2x the initial throughput.

Ledjon Behluli and @​ReubenBond implemented activation repartitioning in #8877. When enabled, activation repartitioning collocates grains based on observed communication patterns to improve performance while keeping load balanced across your cluster. In initial benchmarks, we observe throughput improvements in the range of 30% to 110%. The following paragraphs provide more background and implementation details for those who are interested. The feature is currently experimental and to enable it you need to opt-in on every silo in your cluster using the ISiloBuilder.AddActivationRepartitioner() extension method, suppressing the experimental feature warning:

#pragma warning disable ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
siloBuilder.AddActivationRepartitioner();
#pragma warning restore ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

The fastest and cheapest grains calls are ones which don't cross process boundaries. These grain calls do not need to be serialized and do not need to incur network transmission costs. For that reason, collocating related grains within the same host can significantly improve the performance of your application. On the other hand, if all grains were placed in a single host, that host may become overloaded and crash, and you would not be able to scale your application across multiple hosts. How can we maximize collocation of related grains while keeping load across your hosts balanced? Before describing our solution, we need to provide some background.

Grain placement in Orleans is flexible: Orleans executes a user-defined function when deciding where in a cluster to place each grain, providing your function with a list of the compatible silos in your cluster, that is, the silos which support the grain type and interface version which triggered placement. Grains calls are location-transparent, so callers do not need to know where a grain is located, allowing grains to be placed anywhere across your cluster of hosts. Each grain's current location is stored in a distributed directory and lookups to the directory are cached for performance.

Resource-optimized placement was implemented by @​ledjon-behluli in #8815. Resource-optimized placement uses runtime statistics such as total and available memory, CPU usage, and grain count, collected from all hosts in the cluster, smooths them, and combines them to calculate a load score. It selects the least-loaded silo from a subset of hosts to balance load evenly across the cluster[^4]. If the load score of the local silo is within some configured range of the best candidate's load score, the local silo is chosen preferentially. This improves grain locality by leveraging the knowledge that the local silo initiated a call to the grain and therefore has some relation to that grain. Ledjon wrote more about Resource-optimized placement in this blog post.

Originally, there was no straightforward way to move an active grain from one host to another without needing to fully deactivate the grain, unregister it from the grain directory, contend with concurrent callers on where to place the new activation, and reload its state from the database when the new activation is created. [Live grain migration was introduced in #8452](dotnet/orleans#8452), allowing grains to transparently migrate from one silo to another on-demand without needing to reload state from the database, and without affecting pending requests. Live grain migration introduced two new lifecycle stages: dehydration and rehydration. The grain's in-memory state (application state, enqueued messages, metadata) is dehydrated into a migration packet which is sent to the destination silo where it's rehydrated. Live grain migration provided the mechanism for grains to migrate across hosts, but did not provide any out-of-the-box policies to automate migration. Users trigger grain migration by calling this.MigrateOnIdle() from within a grain, optionally providing a placement hint which the grain's configured placement director can use to select a destination host for the grain activation.

Finally, we have the pieces in place for activation repartitioning: grain activations are load-balanced across the cluster, and they are able to migrate from host to host quickly. While live grain migration gives developers a mechanism to migrate grain activations from one host to another, it does not provide any automated policy to do so. Remember, we want grains to be balanced across the cluster and collocated with related grains to reduce networking and serialization cost. This is a difficult challenge since:

  • An application can have millions of in-memory grains spread across tens or hundreds of silos.
  • Each grain can message any other grain.
  • The set of grains which each grain communicates with can change from minute to minute. For example, in an online game, player grains may join one match and communicate with each other for some time and then join a different match with an entirely different set of players afterwards.
  • Computing the minimum edge-cut for an arbitrary graph is NP-hard.
  • No single host has full knowledge of which grains are hosted on which other host and which grains they communicate with: the graph is distributed across the cluster and changes dynamically.
  • Storing the entire communication graph in memory could be prohibitively expensive.

Folks at Microsoft Research studied this problem and proposed a solution in a paper titled Optimizing Distributed Actor Systems for Dynamic Interactive Services. The paper, dubbed ActOp, proposes a decentralized approximate solution which achieves good results in their benchmarks. Their implementation was never merged into Orleans and we were unable to find the original implementation on Microsoft's internal network. So, after first implementing resource-optimized placement, community contributor @​ledjon-behluli set out to implement activation repartitioning from scratch based on the ActOp paper. The following paragraphs describe the algorithm and the enhancements we made along the way.

The activation repartitioning algorithm involves pair-wise exchange of grains between two hosts at a time. Silos compute a candidate set of grains to send to a peer, then the peer does similarly, and uses a greedy algorithm to determine a final exchange set which minimizes cost while keeping silos balanced.

To compute the candidate sets, silos track which grains communicate with which other grains and how frequently. The whole graph would be unwieldy, so we only maintain the top-K communication edges using a variant of the Space-Saving[^1] algorithm. Messages are sampled via a multi-producer, single consumer ring buffer which drops messages if the partition is full. They are then processed by a single thread, which yields frequently to give other threads CPU time. When the distribution has low skew and the K parameter is fairly small, Space-Saving can require a lot of costly shuffling at the bottom of its max-heap (we use the heap variant to reduce memory). To address this, we use Filtered Space-Saving[^2] instead of Space-Saving. Filtered Space-Saving involves putting a 'sketch' data structure at the bottom of the max heap for the lower end of the distribution, which can greatly reduce churn at the bottom and improve performance by up to ~2x in our tests.

If the top-K communication edges are all internal (eg, because the algorithm has already optimized partitioning somewhat), silos won't find many good transfer candidates. We need to track internal edges to work out which grains should/shouldn't be transferred (cost vs benefit). To address this, we introduced a bloom filter to track grains where the cost of movement is greater than the benefit, removing them from the top-K data structure. From our experiments, this works very well with even a 10x smaller K. This performance improvement will come with a reduced ability to handle dynamic graphs, so in the future we may need to implement a decay strategy to address this as the bloom filter becomes saturated. To improve lookup performance, @​ledjon-behluli implemented a blocked bloom filter[^3], which is used instead of a classic bloom filter.

[^1]: Efficient Computation of Frequent and Top-k Elements in Data Streams by Metwally, Agrawal, and Abbadi [^2]: Finding top-k elements in data streams by Nuno Homem & Joao Paulo Carvalho [^3]: Cache-, Hash- and Space-Efficient Bloom Filters by Felix Putze, Peter Sanders and Johannes Single [^4]: The Power of Two Choices in Randomized Load Balancing by Michael David Mitzenmacher

Enhancements to grain timers

... (truncated)

Commits
  • 92e0bf3 Allow GrainTimers to dispose themselves from their own callback (#9065)
  • 77a187e StatelessWorker: pump work item queue consistently (#9064)
  • 2af2970 ActivationData: get IGrainActivator from shared components consistently (#9063)
  • e19a176 Promptly terminate AdaptiveDirectoryCacheMaintainer (#9062)
  • a264ce0 Coordinate shutdown of AdaptiveDirectoryCacheMaintainer with LocalGrainDirect...
  • 8035ed9 Replace custom GetHashCode implementations with HashCode.Combine (#9059)
  • e033fbd MessagePack codec (#8546)
  • a85e337 Activation repartitioner: use null for no-op message observer to avoid inte...
  • a44d4a3 Implement incoming grain call filters for observers (#9054)
  • 6ff7edc Fix flaky `Should_ConvertAllRemoteCalls_ToLocalCalls_WhileRespectingTolerance...
  • Additional commits viewable in compare view

Updates Microsoft.Orleans.Core.Abstractions from 8.1.0 to 8.2.0

Release notes

Sourced from Microsoft.Orleans.Core.Abstractions's releases.

v8.2.0

New features

Activation repartitioning

ActivationRepartitioning.mp4

Above: a demonstration showing Activation Repartitioning in action. The red lines represent cross-silo communication. As the red lines are eliminated by the partitioning algorithm, throughput improves to over 2x the initial throughput.

Ledjon Behluli and @​ReubenBond implemented activation repartitioning in #8877. When enabled, activation repartitioning collocates grains based on observed communication patterns to improve performance while keeping load balanced across your cluster. In initial benchmarks, we observe throughput improvements in the range of 30% to 110%. The following paragraphs provide more background and implementation details for those who are interested. The feature is currently experimental and to enable it you need to opt-in on every silo in your cluster using the ISiloBuilder.AddActivationRepartitioner() extension method, suppressing the experimental feature warning:

#pragma warning disable ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
siloBuilder.AddActivationRepartitioner();
#pragma warning restore ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

The fastest and cheapest grains calls are ones which don't cross process boundaries. These grain calls do not need to be serialized and do not need to incur network transmission costs. For that reason, collocating related grains within the same host can significantly improve the performance of your application. On the other hand, if all grains were placed in a single host, that host may become overloaded and crash, and you would not be able to scale your application across multiple hosts. How can we maximize collocation of related grains while keeping load across your hosts balanced? Before describing our solution, we need to provide some background.

Grain placement in Orleans is flexible: Orleans executes a user-defined function when deciding where in a cluster to place each grain, providing your function with a list of the compatible silos in your cluster, that is, the silos which support the grain type and interface version which triggered placement. Grains calls are location-transparent, so callers do not need to know where a grain is located, allowing grains to be placed anywhere across your cluster of hosts. Each grain's current location is stored in a distributed directory and lookups to the directory are cached for performance.

Resource-optimized placement was implemented by @​ledjon-behluli in #8815. Resource-optimized placement uses runtime statistics such as total and available memory, CPU usage, and grain count, collected from all hosts in the cluster, smooths them, and combines them to calculate a load score. It selects the least-loaded silo from a subset of hosts to balance load evenly across the cluster[^4]. If the load score of the local silo is within some configured range of the best candidate's load score, the local silo is chosen preferentially. This improves grain locality by leveraging the knowledge that the local silo initiated a call to the grain and therefore has some relation to that grain. Ledjon wrote more about Resource-optimized placement in this blog post.

Originally, there was no straightforward way to move an active grain from one host to another without needing to fully deactivate the grain, unregister it from the grain directory, contend with concurrent callers on where to place the new activation, and reload its state from the database when the new activation is created. [Live grain migration was introduced in #8452](dotnet/orleans#8452), allowing grains to transparently migrate from one silo to another on-demand without needing to reload state from the database, and without affecting pending requests. Live grain migration introduced two new lifecycle stages: dehydration and rehydration. The grain's in-memory state (application state, enqueued messages, metadata) is dehydrated into a migration packet which is sent to the destination silo where it's rehydrated. Live grain migration provided the mechanism for grains to migrate across hosts, but did not provide any out-of-the-box policies to automate migration. Users trigger grain migration by calling this.MigrateOnIdle() from within a grain, optionally providing a placement hint which the grain's configured placement director can use to select a destination host for the grain activation.

Finally, we have the pieces in place for activation repartitioning: grain activations are load-balanced across the cluster, and they are able to migrate from host to host quickly. While live grain migration gives developers a mechanism to migrate grain activations from one host to another, it does not provide any automated policy to do so. Remember, we want grains to be balanced across the cluster and collocated with related grains to reduce networking and serialization cost. This is a difficult challenge since:

  • An application can have millions of in-memory grains spread across tens or hundreds of silos.
  • Each grain can message any other grain.
  • The set of grains which each grain communicates with can change from minute to minute. For example, in an online game, player grains may join one match and communicate with each other for some time and then join a different match with an entirely different set of players afterwards.
  • Computing the minimum edge-cut for an arbitrary graph is NP-hard.
  • No single host has full knowledge of which grains are hosted on which other host and which grains they communicate with: the graph is distributed across the cluster and changes dynamically.
  • Storing the entire communication graph in memory could be prohibitively expensive.

Folks at Microsoft Research studied this problem and proposed a solution in a paper titled Optimizing Distributed Actor Systems for Dynamic Interactive Services. The paper, dubbed ActOp, proposes a decentralized approximate solution which achieves good results in their benchmarks. Their implementation was never merged into Orleans and we were unable to find the original implementation on Microsoft's internal network. So, after first implementing resource-optimized placement, community contributor @​ledjon-behluli set out to implement activation repartitioning from scratch based on the ActOp paper. The following paragraphs describe the algorithm and the enhancements we made along the way.

The activation repartitioning algorithm involves pair-wise exchange of grains between two hosts at a time. Silos compute a candidate set of grains to send to a peer, then the peer does similarly, and uses a greedy algorithm to determine a final exchange set which minimizes cost while keeping silos balanced.

To compute the candidate sets, silos track which grains communicate with which other grains and how frequently. The whole graph would be unwieldy, so we only maintain the top-K communication edges using a variant of the Space-Saving[^1] algorithm. Messages are sampled via a multi-producer, single consumer ring buffer which drops messages if the partition is full. They are then processed by a single thread, which yields frequently to give other threads CPU time. When the distribution has low skew and the K parameter is fairly small, Space-Saving can require a lot of costly shuffling at the bottom of its max-heap (we use the heap variant to reduce memory). To address this, we use Filtered Space-Saving[^2] instead of Space-Saving. Filtered Space-Saving involves putting a 'sketch' data structure at the bottom of the max heap for the lower end of the distribution, which can greatly reduce churn at the bottom and improve performance by up to ~2x in our tests.

If the top-K communication edges are all internal (eg, because the algorithm has already optimized partitioning somewhat), silos won't find many good transfer candidates. We need to track internal edges to work out which grains should/shouldn't be transferred (cost vs benefit). To address this, we introduced a bloom filter to track grains where the cost of movement is greater than the benefit, removing them from the top-K data structure. From our experiments, this works very well with even a 10x smaller K. This performance improvement will come with a reduced ability to handle dynamic graphs, so in the future we may need to implement a decay strategy to address this as the bloom filter becomes saturated. To improve lookup performance, @​ledjon-behluli implemented a blocked bloom filter[^3], which is used instead of a classic bloom filter.

[^1]: Efficient Computation of Frequent and Top-k Elements in Data Streams by Metwally, Agrawal, and Abbadi [^2]: Finding top-k elements in data streams by Nuno Homem & Joao Paulo Carvalho [^3]: Cache-, Hash- and Space-Efficient Bloom Filters by Felix Putze, Peter Sanders and Johannes Single [^4]: The Power of Two Choices in Randomized Load Balancing by Michael David Mitzenmacher

Enhancements to grain timers

... (truncated)

Commits
  • 92e0bf3 Allow GrainTimers to dispose themselves from their own callback (#9065)
  • 77a187e StatelessWorker: pump work item queue consistently (#9064)
  • 2af2970 ActivationData: get IGrainActivator from shared components consistently (#9063)
  • e19a176 Promptly terminate AdaptiveDirectoryCacheMaintainer (#9062)
  • a264ce0 Coordinate shutdown of AdaptiveDirectoryCacheMaintainer with LocalGrainDirect...
  • 8035ed9 Replace custom GetHashCode implementations with HashCode.Combine (#9059)
  • e033fbd MessagePack codec (#8546)
  • a85e337 Activation repartitioner: use null for no-op message observer to avoid inte...
  • a44d4a3 Implement incoming grain call filters for observers (#9054)
  • 6ff7edc Fix flaky `Should_ConvertAllRemoteCalls_ToLocalCalls_WhileRespectingTolerance...
  • Additional commits viewable in compare view

Updates Microsoft.Orleans.Sdk from 8.1.0 to 8.2.0

Release notes

Sourced from Microsoft.Orleans.Sdk's releases.

v8.2.0

New features

Activation repartitioning

ActivationRepartitioning.mp4

Above: a demonstration showing Activation Repartitioning in action. The red lines represent cross-silo communication. As the red lines are eliminated by the partitioning algorithm, throughput improves to over 2x the initial throughput.

Ledjon Behluli and @​ReubenBond implemented activation repartitioning in #8877. When enabled, activation repartitioning collocates grains based on observed communication patterns to improve performance while keeping load balanced across your cluster. In initial benchmarks, we observe throughput improvements in the range of 30% to 110%. The following paragraphs provide more background and implementation details for those who are interested. The feature is currently experimental and to enable it you need to opt-in on every silo in your cluster using the ISiloBuilder.AddActivationRepartitioner() extension method, suppressing the experimental feature warning:

#pragma warning disable ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
siloBuilder.AddActivationRepartitioner();
#pragma warning restore ORLEANSEXP001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

The fastest and cheapest grains calls are ones which don't cross process boundaries. These grain calls do not need to be serialized and do not need to incur network transmission costs. For that reason, collocating related grains within the same host can significantly improve the performance of your application. On the other hand, if all grains were placed in a single host, that host may become overloaded and crash, and you would not be able to scale your application across multiple hosts. How can we maximize collocation of related grains while keeping load across your hosts balanced? Before describing our solution, we need to provide some background.

Grain placement in Orleans is flexible: Orleans executes a user-defined function when deciding where in a cluster to place each grain, providing your function with a list of the compatible silos in your cluster, that is, the silos which support the grain type and interface version which triggered placement. Grains calls are location-transparent, so callers do not need to know where a grain is located, allowing grains to be placed anywhere across your cluster of hosts. Each grain's current location is stored in a distributed directory and lookups to the directory are cached for performance.

Resource-optimized placement was implemented by @​ledjon-behluli in #8815. Resource-optimized placement uses runtime statistics such as total and available memory, CPU usage, and grain count, collected from all hosts in the cluster, smooths them, and combines them to calculate a load score. It selects the least-loaded silo from a subset of hosts to balance load evenly across the cluster[^4]. If the load score of the local silo is within some configured range of the best candidate's load score, the local silo is chosen preferentially. This improves grain locality by leveraging the knowledge that the local silo initiated a call to the grain and therefore has some relation to that grain. Ledjon wrote more about Resource-optimized placement in this blog post.

Originally, there was no straightforward way to move an active grain from one host to another without needing to fully deactivate the grain, unregister it from the grain directory, contend with concurrent callers on where to place the new activation, and reload its state from the database when the new activation is created. [Live grain migration was introduced in #8452](dotnet/orleans#8452), allowing grains to transparently migrate from one silo to another on-demand without needing to reload state from the database, and without affecting pending requests. Live grain migration introduced two new lifecycle stages: dehydration and rehydration. The grain's in-memory state (application state, enqueued messages, metadata) is dehydrated into a migration packet which is sent to the destination silo where it's rehydrated. Live grain migration provided the mechanism for grains to migrate across hosts, but did not provide any out-of-the-box policies to automate migration. Users trigger grain migration by calling this.MigrateOnIdle() from within a grain, optionally providing a placement hint which the grain's configured placement director can use to select a destination host for the grain activation.

Finally, we have the pieces in place for activation repartitioning: grain activations are load-balanced across the cluster, and they are able to migrate from host to host quickly. While live grain migration gives developers a mechanism to migrate grain activations from one host to another, it does not provide any automated policy to do so. Remember, we want grains to be balanced across the cluster and collocated with related grains to reduce networking and serialization cost. This is a difficult challenge since:

  • An application can have millions of in-memory grains spread across tens or hundreds of silos.
  • Each grain can message any other grain.
  • The set of grains which each grain communicates with can change from minute to minute. For example, in an online game, player grains may join one match and communicate with each other for some time and then join a different match with an entirely different set of players afterwards.
  • Computing the minimum edge-cut for an arbitrary graph is NP-hard.
  • No single host has full knowledge of which grains are hosted on which other host and which grains they communicate with: the graph is distributed across the cluster and changes dynamically.
  • Storing the entire communication graph in memory could be prohibitively expensive.

Folks at Microsoft Research studied this problem and proposed a solution in a paper titled Optimizing Distributed Actor Systems for Dynamic Interactive Services. The paper, dubbed ActOp, proposes a decentralized approximate solution which achieves good results in their benchmarks. Their implementation was never merged into Orleans and we were unable to find the original implementation on Microsoft's internal network. So, after first implementing resource-optimized placement, community contributor @​ledjon-behluli set out to implement activation repartitioning from scratch based on the ActOp paper. The following paragraphs describe the algorithm and the enhancements we made along the way.

The activation repartitioning algorithm involves pair-wise exchange of grains between two hosts at a time. Silos compute a candidate set of grains to send to a peer, then the peer does similarly, and uses a greedy algorithm to determine a final exchange set which minimizes cost while keeping silos balanced.

To compute the candidate sets, silos track which grains communicate with which other grains and how frequently. The whole graph would be unwieldy, so we only maintain the top-K communication edges using a variant of the Space-Saving[^1] algorithm. Messages are sampled via a multi-producer, single consumer ring buffer which drops messages if the partition is full. They are then processed by a single thread, which yields frequently to give other threads CPU time. When the distribution has low skew and the K parameter is fairly small, Space-Saving can require a lot of costly shuffling at the bottom of its max-heap (we use the heap variant to reduce memory). To address this, we use Filtered Space-Saving[^2] instead of Space-Saving. Filtered Space-Saving involves putting a 'sketch' data structure at the bottom of the max heap for the lower end of the distribution, which can greatly reduce churn at the bottom and improve performance by up to ~2x in our tests.

If the top-K communication edges are all internal (eg, because the algorithm has already optimized partitioning somewhat), silos won't find many good transfer candidates. We need to track internal edges to work out which grains should/shouldn't be transferred (cost vs benefit). To address this, we introduced a bloom filter to track grains where the cost of movement is greater than the benefit, removing them from the top-K data structure. From our experiments, this works very well with even a 10x smaller K. This performance improvement will come with a reduced ability to handle dynamic graphs, so in the future we may need to implement a decay strategy to address this as the bloom filter becomes saturated. To improve lookup performance, @​ledjon-behluli implemented a blocked bloom filter[^3], which is used instead of a classic bloom filter.

[^1]: Efficient Computation of Frequent and Top-k Elements in Data Streams by Metwally, Agrawal, and Abbadi [^2]: Finding top-k elements in data streams by Nuno Homem & Joao Paulo Carvalho [^3]: Cache-, Hash- and Space-Efficient Bloom Filters by Felix Putze, Peter Sanders and Johannes Single [^4]: The Power of Two Choices in Randomized Load Balancing by Michael David Mitzenmacher

Enhancements to grain timers

... (truncated)

Commits
  • 92e0bf3 Allow GrainTimers to dispose themselves from their own callback (#9065)
  • 77a187e StatelessWorker: pump work item queue consistently (#9064)
  • 2af2970 ActivationData: get IGrainActivator from shared components consistently (#9063)
  • e19a176 Promptly terminate AdaptiveDirectoryCacheMaintainer (#9062)
  • a264ce0 Coordinate shutdown of AdaptiveDirectoryCacheMaintainer with LocalGrainDirect...
  • 8035ed9 Replace custom GetHashCode implementations with HashCode.Combine (#9059)
  • e033fbd MessagePack codec (#8546)
  • a85e337 Activation repartitioner: use null for no-op message observer to avoid inte...
  • a44d4a3 Implement incoming grain call filters for observers (#9054)
  • 6ff7edc Fix flaky `Should_ConvertAllRemoteCalls_ToLocalCalls_WhileRespectingTolerance...
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot merge will merge this PR after your CI passes on it
  • @dependabot squash and merge will squash and merge this PR after your CI passes on it
  • @dependabot cancel merge will cancel a previously requested merge and block automerging
  • @dependabot reopen will reopen this PR if it is closed
  • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore <dependency name> major version will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
  • @dependabot ignore <dependency name> minor version will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
  • @dependabot ignore <dependency name> will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
  • @dependabot unignore <dependency name> will remove all of the ignore conditions of the specified dependency
  • @dependabot unignore <dependency name> <ignore condition> will remove the ignore condition of the specified dependency and ignore conditions

@dependabot dependabot bot added .NET Pull requests that update .net code dependencies Pull requests that update a dependency file labels Jul 17, 2024
@dependabot dependabot bot force-pushed the dependabot/nuget/docs/orleans/grains/snippets/timers/dotnet-065a960c22 branch from 41e05d3 to 7e36bdd Compare July 24, 2024 05:14
Bumps the dotnet group in /docs/orleans/grains/snippets/timers with 3 updates: [Microsoft.Orleans.Core.Abstractions](https://github.com/dotnet/orleans), [Microsoft.Orleans.Reminders](https://github.com/dotnet/orleans) and [Microsoft.Orleans.Sdk](https://github.com/dotnet/orleans).


Updates `Microsoft.Orleans.Core.Abstractions` from 8.1.0 to 8.2.0
- [Release notes](https://github.com/dotnet/orleans/releases)
- [Commits](dotnet/orleans@v8.1.0...v8.2.0)

Updates `Microsoft.Orleans.Reminders` from 8.1.0 to 8.2.0
- [Release notes](https://github.com/dotnet/orleans/releases)
- [Commits](dotnet/orleans@v8.1.0...v8.2.0)

Updates `Microsoft.Orleans.Core.Abstractions` from 8.1.0 to 8.2.0
- [Release notes](https://github.com/dotnet/orleans/releases)
- [Commits](dotnet/orleans@v8.1.0...v8.2.0)

Updates `Microsoft.Orleans.Sdk` from 8.1.0 to 8.2.0
- [Release notes](https://github.com/dotnet/orleans/releases)
- [Commits](dotnet/orleans@v8.1.0...v8.2.0)

---
updated-dependencies:
- dependency-name: Microsoft.Orleans.Core.Abstractions
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dotnet
- dependency-name: Microsoft.Orleans.Reminders
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dotnet
- dependency-name: Microsoft.Orleans.Core.Abstractions
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dotnet
- dependency-name: Microsoft.Orleans.Sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dotnet
...

Signed-off-by: dependabot[bot] <[email protected]>
@dependabot dependabot bot force-pushed the dependabot/nuget/docs/orleans/grains/snippets/timers/dotnet-065a960c22 branch from 7e36bdd to 59db71b Compare July 24, 2024 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dependencies Pull requests that update a dependency file .NET Pull requests that update .net code
Projects
None yet
Development

Successfully merging this pull request may close these issues.

0 participants