EF Core 5 升级 - 查询超时

EF Core 5 Upgrade - Query Timeouts

我们将一个成功的 .net Core 2.1 MVC 应用程序升级到 .net Core 5,一切进展顺利,除了一些曾经完美运行的查询中的一些令人困惑的 Microsoft.Data.SqlClient.SqlException 'Execution Timeout Expired' 异常在 EF Core 2.1 中很好。

这是我们遇到问题的查询之一的示例

 var products = await _context.ShopProducts
                    .Include(p => p.Category)
                    .Include(p => p.Brand)
                    .Include(p => p.CreatedBy)
                    .Include(p => p.LastUpdatedBy)
                    .Include(p => p.Variants).ThenInclude(pv => pv.ProductVariantAttributes)
                    .Include(p => p.Variants).ThenInclude(pv => pv.CountryOfOrigin)
                    .Include(p => p.Page)
                    .Include(p => p.CountryOfOrigin)
                    .OrderBy(p => p.Name)
                    .Where(p =>
                        (string.IsNullOrEmpty(searchText)
                            || (
                                    p.Name.Contains(searchText)
                                    || p.Description.Contains(searchText)
                                    || p.Variants.Any(v => v.SKU.Contains(searchText))
                                    || p.Variants.Any(v => v.GTIN.Contains(searchText))
                                    || p.Brand.BrandName.Contains(searchText)
                                    || p.CountryOfOriginCode == searchText
                                    || p.Category.Breadcrumb.Contains(searchText)
                                )
                         )
                    ).ToPagedListAsync(page, pageSize);
            

我们得到的异常。

Microsoft.Data.SqlClient.SqlException (0x80131904): Execution Timeout Expired.  The timeout period elapsed prior to completion of the operation or the server is not responding.
Execution Timeout Expired.  The timeout period elapsed prior to completion of the operation or the server is not responding.
 ---> System.ComponentModel.Win32Exception (258): The wait operation timed out.
   at Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
   at Microsoft.Data.SqlClient.TdsParserStateObject.ThrowExceptionAndWarning(Boolean callerHasConnectionLock, Boolean asyncClose)
   at Microsoft.Data.SqlClient.TdsParserStateObject.ReadSniError(TdsParserStateObject stateObj, UInt32 error)
   at Microsoft.Data.SqlClient.TdsParserStateObject.ReadSniSyncOverAsync()
   at Microsoft.Data.SqlClient.TdsParserStateObject.TryReadNetworkPacket()
   at Microsoft.Data.SqlClient.TdsParserStateObject.TryPrepareBuffer()
   at Microsoft.Data.SqlClient.TdsParserStateObject.TryReadByteArray(Span`1 buff, Int32 len, Int32& totalRead)
   at Microsoft.Data.SqlClient.TdsParserStateObject.TryReadChar(Char& value)
   at Microsoft.Data.SqlClient.TdsParser.TryReadPlpUnicodeCharsChunk(Char[] buff, Int32 offst, Int32 len, TdsParserStateObject stateObj, Int32& charsRead)
   at Microsoft.Data.SqlClient.TdsParser.TryReadPlpUnicodeChars(Char[]& buff, Int32 offst, Int32 len, TdsParserStateObject stateObj, Int32& totalCharsRead)
   at Microsoft.Data.SqlClient.TdsParser.TryReadSqlStringValue(SqlBuffer value, Byte type, Int32 length, Encoding encoding, Boolean isPlp, TdsParserStateObject stateObj)
   at Microsoft.Data.SqlClient.TdsParser.TryReadSqlValue(SqlBuffer value, SqlMetaDataPriv md, Int32 length, TdsParserStateObject stateObj, SqlCommandColumnEncryptionSetting columnEncryptionOverride, String columnName, SqlCommand command)
   at Microsoft.Data.SqlClient.SqlDataReader.TryReadColumnInternal(Int32 i, Boolean readHeaderOnly)
   at Microsoft.Data.SqlClient.SqlDataReader.ReadColumnHeader(Int32 i)
   at Microsoft.Data.SqlClient.SqlDataReader.IsDBNull(Int32 i)
   at lambda_method1671(Closure , QueryContext , DbDataReader )
   at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.PopulateIncludeCollection[TIncludingEntity,TIncludedEntity](Int32 collectionId, QueryContext queryContext, DbDataReader dbDataReader, SingleQueryResultCoordinator resultCoordinator, Func`3 parentIdentifier, Func`3 outerIdentifier, Func`3 selfIdentifier, IReadOnlyList`1 parentIdentifierValueComparers, IReadOnlyList`1 outerIdentifierValueComparers, IReadOnlyList`1 selfIdentifierValueComparers, Func`5 innerShaper, INavigationBase inverseNavigation, Action`2 fixup, Boolean trackingQuery)
   at lambda_method1679(Closure , QueryContext , DbDataReader , ResultContext , SingleQueryResultCoordinator )
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()

如果未指定 searchText 参数,此查询将完美运行,所以我认为它一定是与索引/数据相关的内容,但是 运行ning 生成的 SQL查询,有和没有 searchText 参数,当 运行 直接在数据库上立即执行,所以它似乎排除了数据库的问题。

EF Core 5 能否努力将所有数据 assemble 放入对象实例?我意识到我们正在从此查询中返回一个大对象树,总共 152 列,但由于 pageSize 变量只有 10 行。

因为当没有指定 searchText 时它不会超时,而且 EF Core 2.1 能够毫无问题地将它们放在一起,这似乎也不太有意义。

对于调整查询的方法的任何建议,或任何人从他们自己的 EF Core 2.1 到 3.1 / 5 升级的见解,或从异常堆栈跟踪中跳出的任何内容,我们将不胜感激。

更新

很抱歉在 SO 上进行实时调试,但我发现似乎是查询中的 p.Description.Contains(searchText) 子句导致了超时。如果我注释掉它,查询 运行s 成功。

Product.Description 数据是一个 HTML 字符串,最多 1028 个字符,平均长度为 350 个字符,同样,直接在 SQL 中查询也没有问题全部。它会不会以其他方式导致 EF 出现问题?

[DataType(DataType.Html)]
public string Description { get; set; }

考虑使用 Split Queries 来提高包含大量 Include 的查询的性能。

空搜索文本将起作用,因为组合 SQL 如果为空,将忽略任何其他条件。更好的实现方式是将条件逻辑保留在代码中而不是表达式中:

var query = _context.ShopProducts
    .Include(p => p.Category)
    .Include(p => p.Brand)
    .Include(p => p.CreatedBy)
    .Include(p => p.LastUpdatedBy)
    .Include(p => p.Variants)
        .ThenInclude(pv => pv.ProductVariantAttributes)
    .Include(p => p.Variants)
        .ThenInclude(pv => pv.CountryOfOrigin)
    .Include(p => p.Page)
    .Include(p => p.CountryOfOrigin)
    .OrderBy(p => p.Name);

 if (!string.IsNullOrEmpty(searchText))
    query = query.Where(p => p.Name.Contains(searchText)
        || p.Description.Contains(searchText)
        || p.Variants.Any(v => v.SKU.Contains(searchText))
        || p.Variants.Any(v => v.GTIN.Contains(searchText))
        || p.Brand.BrandName.Contains(searchText)
        || p.CountryOfOriginCode == searchText
        || p.Category.Breadcrumb.Contains(searchText));

var products = await query.ToPagedListAsync(page, pageSize);

在代码中保留 if 语句可确保这些条件仅在需要时才达到 SQL。这是明智的,尤其是在您可能有多个单独的搜索词并有条件地应用每个搜索词的情况下。

您的问题的症结很可能是您正在启动一个肯定是效率极低的查询,它不能利用任何形式的索引,是一个多 LIKE %{term}% 查询。如果有一种方法可以让用户对您的服务器触发有效的 DDOS,那就是这个。它会很慢,用户会怀疑它是否启动并反复启动它。 (生成新标签等)

在系统中通常有文本搜索功能,可以在多个不同的可能值之间进行搜索。将这些压缩成一个超级搜索不会给您自己或您的用户带来任何好处。问题是,如果 95% 的时间搜索可以由用户定向到最常见的搜索类型或字段,它可能会快得多,但要迎合 5%,所有 搜索必须是最小公分母。

需要考虑的一些选项:

  1. 让用户指定他们要搜索的内容。这可以是下拉式多 select 或面包屑自动 select 最常见的搜索目标(名称和 SKU 等)
  2. 默认为“开头为”类型搜索,并为用户提供选择较慢的“包含”类型搜索的选项。
  3. 应用一些逻辑来预先检查搜索文本以了解它可能匹配的字段。例如,如果其中一些值是数字或遵循特定模式(正则表达式可以匹配),则将搜索定向到这些值,或者如果搜索文本不合适则忽略这些值。
  4. 始终对搜索文本强制执行最小长度检查,或者至少检查该搜索文本是否应用于特定值。

这样的更改可以帮助保持 95% 以上的搜索尽可能快速和高效。我还会考虑为可能昂贵的搜索采用排队机制,其中将标准和分页数据弹出到队列中,并将 searchQueueID 传递回调用者,然后调用者使用该 ID 轮询结果,或者可以取消他们的搜索。该队列由一小部分工作线程提供服务,这些工作线程按顺序处理搜索请求,并根据 searchQueueID 和完成状态填充结果存储,以供轮询循环获取。这有助于确保在任何给定时间只执行这些可能代价高昂的搜索中的这么多。 (即全部由不同的用户启动。)