当应用程序需要向 Dataverse 发送大量请求时,可以通过使用多个线程并行发送请求来实现更高的总吞吐量。 Dataverse 旨在支持多个并发用户,因此并行发送请求会利用这种优势。
注释
不支持在插件内发送并行请求。 有关详细信息,请参阅 不要在插件和工作流活动中使用并行执行。
最佳并行度 (DOP)
Dataverse 管理环境的资源分配。 许多许可用户使用的生产环境具有更多已分配的资源。 分配的服务器的数量和功能可能会随时间而变化,因此,最佳并行度没有固定数字。 请改用从响应标头返回的 x-ms-dop-hint 整数值。 此值为环境提供建议的并行度。
在 .NET 中使用并行编程时,默认并行度取决于运行代码的客户端上的 CPU 核心数。 如果 CPU 内核数超过环境的最佳匹配,则请求可能过多。 设置 ParallelOptions.MaxDegreeOfParallelism 属性 以定义最大并发任务数。
服务保护限制
监视服务保护限制的三个方面之一是并发请求数。 默认情况下,此值为 52,但它可能更高。 如果超出限制,则返回错误。 如果依赖于 x-ms-dop-hint 响应标头值来限制并行度,则很少达到此限制。 如果遇到此错误,请减少并发线程数。
达到此限制时,将返回特定的错误:
| 错误代码 | 十六进制代码 | 消息 |
|---|---|---|
-2147015898 |
0x80072326 |
Number of concurrent requests exceeded the limit of 52. |
还可以通过禁用服务器相关性将请求发送到支持环境的所有服务器来减少此错误的可能性。
服务器相关性
连接到 Azure 上的服务时,该服务会返回包含响应的 Cookie。 除非容量管理强制请求转到另一个服务器,否则所有后续请求都会尝试转到同一服务器。 交互式客户端应用程序(尤其是浏览器客户端)受益于此 Cookie,因为它允许应用程序重用服务器上缓存的数据。 Web 浏览器始终启用服务器相关性,并且无法禁用它。
从客户端应用程序并行发送请求时,可以通过禁用此 Cookie 来获得性能优势。 每个请求都会被路由到任何符合条件的服务器。 此更改不仅会增加总吞吐量,而且还有助于降低服务保护限制的影响,因为每个限制都适用于每台服务器。
以下示例演示如何使用 .NET 禁用服务器相关性。
如果使用 ServiceClient 或 CrmServiceClient 类,请将以下代码添加到 AppSettings 文件中的 App.config 节点。
<add key="PreferConnectionAffinity" value="false" />
还可以使用
还可以通过使用 ServiceClient(ConnectionOptions、Boolean、ConfigurationOptions) 构造函数和 ConfigurationOptions.EnableAffinityCookie 属性来设置此属性。
优化连接
使用 .NET 并并行发送请求时,请更改默认设置,以便请求不受它们的限制。 进行以下更改:
// Bump up the min threads reserved for this app to ramp connections faster - minWorkerThreads defaults to 4, minIOCP defaults to 4
ThreadPool.SetMinThreads(100, 100);
// Change max connections from .NET to a remote service default: 2
System.Net.ServicePointManager.DefaultConnectionLimit = 65000;
// Turn off the Expect 100 to continue message - 'true' will cause the caller to wait until it round-trip confirms a connection to the server
System.Net.ServicePointManager.Expect100Continue = false;
// Can decrease overall transmission overhead but can cause delay in data packet arrival
System.Net.ServicePointManager.UseNagleAlgorithm = false;
ThreadPool.SetMinThreads
此设置控制线程池随新请求传入时按需创建的最小线程数。 达到此数字后,线程池将切换到管理线程创建和销毁的算法。
默认情况下,最小线程数设置为处理器的数量。 用于 SetMinThreads 增加最小线程数。 例如,可以临时增加数量以解决某些排队工作项或任务阻塞线程池中的线程的问题。 这些阻塞有时会导致所有工作线程或 I/O 完成线程发生阻塞(资源饥饿)的情况。 但是,以其他方式增加最小线程数可能会降低性能。
你使用的数字可能因硬件而异。 对于基于消耗的 Azure 函数,使用的数字比在具有高端硬件的专用主机上运行的代码要低。
详细信息:System.Threading.ThreadPool.SetMinThreads
System.Net.ServicePointManager 设置
在 .NET Framework 中, ServicePointManager 是用于创建、维护和删除 ServicePoint 类实例的静态类。 将这些设置与 ServiceClient 或 CrmServiceClient 类配合使用。 在 .NET Framework 中将 HttpClient 与 Web API 配合使用时,还应应用这些设置。 但是,对于 .NET Core,Microsoft 建议改为在 HttpClient 中进行设置。
DefaultConnectionLimit
硬件最终会限制此值。 如果设置太高,其他机制会对它进行节流。 将其提升到默认值之上,并将其至少设置为要发送的并发请求数。
在使用 .NET Core 与 HttpClient 时,HttpClientHandler.MaxConnectionsPerServer 属性控制此设置。 其默认值为 int。MaxValue。
有关详细信息,请参见:
- System.Net.ServicePointManager.DefaultConnectionLimit
- .NET Framework 连接池限制和适用于 .NET 的新 Azure SDK
- 为 WebJobs 配置 ServicePointManager
- HttpClientHandler.MaxConnectionsPerServer
Expect100Continue
当您将此属性设置为 true 时,客户端会等待与服务器连接的往返确认。 对于 HttpClient, HttpRequestHeaders.ExpectContinue 的默认值为 false。
有关详细信息,请参见:
UseNagleAlgorithm
Nagle 算法通过缓冲小数据包并将其作为单个数据包传输来减少网络流量。 此过程也称为“nagling”,由于它减少了数据包的传输数量,并降低了每个数据包的开销,因此被广泛使用。 将此值设置为 false 可能会降低总体传输开销,但可能会导致数据包到达延迟。
有关详细信息,请参阅 System.Net.ServicePointManager.UseNagleAlgorithm。
示例
以下 .NET 示例演示如何将 任务并行库 (TPL) 与 Dataverse 配合使用。
可以通过在ServiceClient或CrmServiceClient中的RecommendedDegreesOfParallelism属性访问x-ms-dop-hint响应值。 使用 Parallel.ForEach 时设置 ParallelOptions.MaxDegreeOfParallelism 时,请使用此值。
这些示例还显示将 EnableAffinityCookie 属性设置为 false。
在以下示例中,将响应的 ID 值添加到 GUID 的 ConcurrentBag 。
ConcurrentBag 在排序无关紧要时,提供线程安全的无序对象集合。 不能期望此方法返回的 GUID 的顺序与参数中 entityList 发送的项的顺序匹配。
将 ServiceClient 与 .NET 6 或更高版本配合使用
通过使用 .NET 6 或更高版本,可以将 Parallel.ForEachAsync 方法与 ServiceClient 附带的异步方法(如 CreateAsync)配合使用。
/// <summary>
/// Creates records in parallel
/// </summary>
/// <param name="serviceClient">The authenticated ServiceClient instance.</param>
/// <param name="entityList">The list of entities to create.</param>
/// <returns>The id values of the created records.</returns>
static async Task<Guid[]> CreateRecordsInParallel(
ServiceClient serviceClient,
List<Entity> entityList)
{
ConcurrentBag<Guid> ids = new();
// Disable affinity cookie
serviceClient.EnableAffinityCookie = false;
var parallelOptions = new ParallelOptions()
{ MaxDegreeOfParallelism =
serviceClient.RecommendedDegreesOfParallelism };
await Parallel.ForEachAsync(
source: entityList,
parallelOptions: parallelOptions,
async (entity, token) =>
{
ids.Add(await serviceClient.CreateAsync(entity, token));
});
return ids.ToArray();
}
将 CrmServiceClient 与 .NET Framework 配合使用
使用 .NET Framework 时,CrmServiceClient 中提供的 Clone 方法会复制与 Dataverse 的现有连接,以便可以使用 Parallel.ForEach 方法。
/// <summary>
/// Creates records in parallel
/// </summary>
/// <param name="crmServiceClient">The authenticated CrmServiceClient instance.</param>
/// <param name="entityList">The list of entities to create.</param>
/// <returns>The id values of the created records.</returns>
static Guid[] CreateRecordsInParallel(
CrmServiceClient crmServiceClient,
List<Entity> entityList)
{
ConcurrentBag<Guid> ids = new ConcurrentBag<Guid>();
// Disable affinity cookie
crmServiceClient.EnableAffinityCookie = false;
Parallel.ForEach(entityList,
new ParallelOptions()
{
MaxDegreeOfParallelism = crmServiceClient.RecommendedDegreesOfParallelism
},
() =>
{
//Clone the CrmServiceClient for each thread
return crmServiceClient.Clone();
},
(entity, loopState, index, threadLocalSvc) =>
{
ids.Add(threadLocalSvc.Create(entity));
return threadLocalSvc;
},
(threadLocalSvc) =>
{
//Dispose the cloned crmServiceClient instance
threadLocalSvc?.Dispose();
}
);
return ids.ToArray();
}
另见
服务保护 API 限制
Web API WebApiService 并行作示例 (C#)
使用 TPL 数据流组件的 Web API 并行操作示例 (C#)
示例:使用 CrmServiceClient 的任务并行库 (Task Parallel Library)