部署到 IIS8 时 EF 6 与 EF 5 的相对性能问题

Posted

技术标签:

【中文标题】部署到 IIS8 时 EF 6 与 EF 5 的相对性能问题【英文标题】:EF 6 vs EF 5 relative performance issue when deploying to IIS8 【发布时间】:2014-03-17 18:31:02 【问题描述】:

我有一个带有 EF 6 的 MVC 4 应用程序。从 EF 5 升级到 EF 6 后,我注意到我的一个 linq-entities 查询存在性能问题。起初我很兴奋,因为在我的开发箱中,我注意到从 EF 5 到 EF 6 的改进 50%。这个查询返回了大约 73,000 条记录。在生产服务器上运行的 SQL 被 Activity Monitor,Recent Expensive Queries 拦截,这个时间也包含在下表中。以下数字是 DB 预热后的数据:

开发:64 位操作系统,SS 2012,2 核,6 GB RAM,IIS Express。

EF 5 ~30 sec
EF 6 ~15 sec
SQL ~26 sec

生产:64 位操作系统,SS 2012,32 核,32 GB RAM,IIS8。

EF 5 ~8 sec
EF 6 ~4 minutes
SQL ~6 sec.

我包含规格只是为了让您了解相对性能应该是什么。因此,当我在我的开发环境中使用 EF 6 时,我似乎获得了性能提升,当我发布到我的生产服务器时,一个巨大的性能问题。如果不完全相同,数据库是相似的。所有索引都已重建,SQL 查询似乎也表明没有理由怀疑数据库有问题。应用程序池是生产中的 .Net 4.0。开发和生产服务器都安装了 .Net 4.5。我不知道接下来要检查什么或如何调试此问题,对如何做或如何进一步调试有任何想法?

更新: 使用 SQL Server Profiler 发现 EF5 和 EF6 生成的 TSQL 略有不同。 TSQL的区别如下:

EF5: LEFT OUTER JOIN [dbo].[Pins] AS [Extent9] ON [Extent1].[PinId] = [Extent9].[PinID]
EF6: INNER JOIN [dbo].[Pins] AS [Extent9] ON [Extent1].[PinId] = [Extent9].[PinID]

EF6 中的相同 TSQL 的执行方式也不同,具体取决于执行 TSQL 的服务器/数据库。在检查了 EF6 和慢速数据库(生产服务器 SS 构建 11.0.3000 企业版)的查询计划后,该计划执行所有扫描,与具有少量搜索的相同实例(测试服务器 SS 构建 11.0.3128 开发人员版)相比,不进行搜索这有所作为。挂钟时间> 4 分钟用于生产,12 秒用于小型测试服务器。 EF 将这些查询放入 sp_executesql proc 中,截获的 sp_executesql proc 用于上述时间。在开发服务器上执行 EF5 或 EF6 生成的代码时,我不会得到缓慢的时间(错误的查询计划)。同样奇怪的是,如果我从 sp_executesql 中删除 TSQL 并在生产服务器上运行它,查询会快速执行(6 秒)。总而言之,慢执行计划需要发生三件事:

1. Execute on production server build 11.0.3000
2. Use Inner Join with Pins table (EF6 generated code).
3. Execute TSQL inside of sp_executesql.

测试环境是用我的生产数据的备份创建的,两台服务器上的数据是相同的。创建备份和恢复数据库是否可以解决数据的一些问题?我没有尝试在生产服务器上删除实例并恢复,因为我想在删除和恢复实例之前确定问题是什么,以防万一它确实解决了问题。我确实尝试使用以下 TSQL 刷新缓存

select DB_ID() 
DBCC Flushprocindb(database_Id)
and 
DBCC FREEPROCCACHE(plan_handle)

使用上述刷新不会影响查询计划。有什么建议接下来要尝试什么?

以下是 linq 查询:

    result =
    (
    from p1 in context.CookSales

    join l2 in context.CookSaleStatus on new  ID = p1.PinId, YEAR = year1  equals new  ID = l2.PinId, YEAR = l2.StatusYear  into list2
    from p3 in list2.DefaultIfEmpty()
    join l3 in context.CookSaleStatus on new  ID = p1.PinId, YEAR = year2  equals new  ID = l3.PinId, YEAR = l3.StatusYear  into list3
    from p4 in list3.DefaultIfEmpty()
    join l4 in context.CookSaleStatus on new  ID = p1.PinId, YEAR = year3  equals new  ID = l4.PinId, YEAR = l4.StatusYear  into list4
    from p5 in list4.DefaultIfEmpty()
    join l10 in context.CookSaleStatus on new  ID = p1.PinId, YEAR = year4  equals new  ID = l10.PinId, YEAR = l10.StatusYear  into list10
    from p11 in list10.DefaultIfEmpty()

    join l5 in context.ILCookAssessors on p1.PinId equals l5.PinID into list5
    from p6 in list5.DefaultIfEmpty()
    join l7 in context.ILCookPropertyTaxes on new  ID = p1.PinId  equals new  ID = l7.PinID  into list7
    from p8 in list7.DefaultIfEmpty()

    join l13 in context.WatchLists on p1.PinId equals l13.PinId into list13
    from p14 in list13.DefaultIfEmpty()

    join l14 in context.Pins on p1.PinId equals l14.PinID into list14
    from p15 in list14.DefaultIfEmpty()
    orderby p1.Volume, p1.PIN
    where p1.SaleYear == userSettings.SaleYear 
    where ((p1.PinId == pinId) || (pinId == null))
    select new SaleView
    
        id = p1.id,
        PinId = p1.PinId,
        Paid = p1.Paid == "P" ? "Paid" : p1.Paid,
        Volume = p1.Volume,
        PinText = p15.PinText,
        PinTextF = p15.PinTextF,
        ImageFile = p15.FnaImage.TaxBodyImageFile,
        SaleYear = p1.SaleYear,
        YearForSale = p1.YearForSale,
        Unpaid = p1.DelinquentAmount,
        Taxes = p1.TotalTaxAmount,
        TroubleTicket = p1.TroubleTicket,
        Tag1 = p1.Tag1,
        Tag2 = p1.Tag2,
        HasBuildingPermit = p1.Pin1.BuildingPermitGeos.Any(p => p.PinId == p1.PinId),
        BidRate = p1.BidRate,
        WinningBid = p1.WinningBid,
        WinningBidderNumber = p1.BidderNumber,
        WinningBidderName = p1.BidderName,
        TaxpayerName = p1.TaxpayerName,
        PropertyAddress = SqlFunctions.StringConvert((double?)p1.TaxpayerPropertyHouse) + " " + p1.TaxpayerPropertyDirection + " "
                        + p1.TaxpayerPropertyStreet
                        + " " + p1.TaxpayerPropertySuffix +
                        System.Environment.NewLine + (p1.TaxpayerPropertyCity ?? "") + ", " + (p1.TaxpayerPropertyState ?? "") +
                        " " + (p1.TaxpayerPropertyZip ?? ""),
        MailingAddress = (p1.TaxpayerName ?? "") + System.Environment.NewLine + (p1.TaxpayerMailingAddress ?? "") +
                        System.Environment.NewLine + (p1.TaxpayerMailingCity ?? "") + ", " + (p1.TaxpayerMailingState ?? "") +
                        " " + (p1.TaxpayerMailingZip ?? ""),
        Status1 = p3.Status.Equals("Clear") ? null : p3.Status,
        Status2 = p4.Status.Equals("Clear") ? null : p4.Status,
        Status3 = p5.Status.Equals("Clear") ? null : p5.Status,
        Status4 = p11.Status.Equals("Clear") ? null : p11.Status,
        Township = p6.Township,
        AssessorLastUpdate = p6.LastUpdate,
        Age = p6.Age,
        LandSquareFootage = p6.LandSquareFootage,
        BuildingSquareFootage = p6.BuildingSquareFootage,
        CurrLand = p6.CurrLand,
        CurrBldg = p6.CurrBldg,
        CurrTotal = p6.CurrTotal,
        PriorLand = p6.PriorLand,
        PriorBldg = p6.PriorBldg,
        PriorTotal = p6.PriorTotal,
        ClassDescription = p6.ClassDescription,
        Class = p1.Classification == null ? p6.Class.Trim() : p1.Classification,
        TaxCode = p6.TaxCode,
        Usage = p6.Usage,

        Status0 = (p8.CurrentTaxYear != null && p8.CurrentTaxYearPaidAmount == 0) ? "Paid" : null, 
        LastTaxYearPaidAmount = p8.LastTaxYearPaidAmount,
        NoteStatus = p15.PinNotes.Any(p => p.PinId == p15.PinID),
        EntryComment = p1.EntryComment,
        IsInScavenger = p14.IsInScavenger ?? false,
        IsInTbs = p14.IsInTbs ?? false,
        RedeemVts = (p3.Redeemed == "VTS" || p4.Redeemed == "VTS" || p5.Redeemed == "VTS" || p11.Redeemed == "VTS") ? true : false,
        FivePercenter = (p3.FivePercenter || p4.FivePercenter || p5.FivePercenter || p11.FivePercenter) ? true : false,
    
    ).ToList();

使用此查询生成的 SQL 似乎是合理的。 (我没有包含它,因为当我粘贴它时它没有格式化且难以阅读。)

【问题讨论】:

您使用的是EF6.0.2吗?它修复了纯 EF6.0.0 中的几个性能问题。 根据NuGet它的版本是6.0.2,但是参考EntityFramework的属性表明版本是6.0.0.0。我怀疑我有最新版本,因为我上周刚从 NuGet 安装了这个。 你有没有直接在数据库上运行EF生成的TSQL?如果有,结果如何?可能值得检查您的生产服务器上没有安装 EF,并且部署的 EF 二进制文件是正确的版本。程序集版本为6.0.0.0,但如果您检查二进制文件的属性,它应该有一个File Version,如6.0.21211.0 @Xenolightning,我在 dev 和 prod 上都运行了 TSQL,时间包含在我上面的 SQL 行中的表中,分别为 26 秒和 6 秒。我检查了 IIS prod bin 和本地 dev bin 中的二进制文件和文件版本是 6.0.21211.0,并且我已经复制了本地检查。 由于一个错误的查询计划卡在缓存中乙>。解决方法是查找并刷新相关计划。您还可以使用诸如MiniProfiler 之类的分析器来确定时间是花在 EF 执行的 SQL 上还是花在代码的其他部分上。 【参考方案1】:

在研究这个问题时,我发现了一些我不知道的关于 SQL Server 的事情。这对某些人来说可能是常识,但对我来说不是。以下是我的总体亮点。

    EF 对所有查询使用动态 sql,特别是 sp_exectutesql()。 sp_executesql() 执行动态 SQL,如果您删除此 SQL 并在 SSMS 中作为临时查询执行,则不要期望获得相同的性能结果。这在here 和参考this 文档中有很好的记录,如果您遇到这些类型的问题,我强烈建议您阅读。 在某些条件下,EF5 生成的动态 SQL 与 EF6 不同。 很难优化 linq 到实体,因为您可能会根据硬件获得不同的结果,参考资料中对此进行了说明。我最初的目标是在升级到 EF6 时优化 linq 查询。我注意到使用导航属性提高了我的开发和测试服务器的性能,但在生产中却扼杀了它。 在所有环境中具有可接受性能的最终结果是连接和导航属性的组合。最后,如果我使用了所有导航属性,它会从一开始就更好地工作。使用的连接键来自错误的表,当您编写临时 SQL 时,这并不重要,但它必须用于动态 SQL。如果我使用导航,就不会有任何按键出错。但是,最好的性能是使用一个连接和剩余的导航属性。对于所有场景,生成的动态 SQL 都非常相似,但 SQL Server 查询计划优化器在使用导航属性时会获得更好的线索(这是猜测)。

linq 改变的关键部分是这样的:

                from p1 in context.CookSales
                join p15 in context.Pins on p1.PinId equals p15.PinID
                where p1.SaleYear == userSettings.SaleYear
                where ((p1.PinId == pinId) || (pinId == null))
                orderby p1.Volume, p1.PIN
                select new SaleView bla bla

Pins 表包含 PinId 的主键,而所有其他表都将 PinId 作为外键。将 Pin 图保留为连接而不是导航属性可提高性能。

【讨论】:

以上是关于部署到 IIS8 时 EF 6 与 EF 5 的相对性能问题的主要内容,如果未能解决你的问题,请参考以下文章

将 asp.net 5 MVC 6 与 Identity 和 EF 6 一起使用的示例

使用EF Database-First Application部署ASP.NET Web API

升级到 .NET 4.5 和 EF5。不再能够部署 WCF 服务。

EF 配置实现建表与迁移

C# EF 代码首先部署到 Godaddy

WCF / EF / SQL Server / VS2017 / C#:多层应用程序在部署到本地 IIS 10.0 作为托管服务器时出错