随着更多的开发人员使用 SharePoint 对象模型编写自定义代码,他们会遇到一些可能影响应用程序性能的普遍性问题。本文尝试解决其中的一些问题,并推荐找出和纠正这些问题的方法。
开发人员使用 SharePoint 对象模型编写自定义代码时遇到的主要问题存在于下列领域:
处理 SharePoint 对象
缓存数据和对象
编写具有可伸缩性的代码
开发人员使用 SharePoint 对象模型编写自定义代码时遇到的一个最大问题是不能正确释放 SharePoint 对象。整个 SharePoint 对象处理问题的详细情况不是本文的范畴;但是我们可提供一种方法来确定问题,并提供有关如何纠正问题的一般信息。有关处理 SharePoint 对象最佳实践的详细信息,请参阅最佳实践:使用可处理的 Windows SharePoint Services 对象。
在 SharePoint 对象模型内,将托管代码中的 Microsoft.SharePoint.SPSite 和 Microsoft.SharePoint.SPWeb 对象创建为小包装(大小约为 2 KB)。此包装随后创建非托管对象,其平均大小约为 1–2 MB。如果代码类似于下列代码示例,并且如果假定 SPWeb.Webs 集合有 10 个子站点,则总共创建 10 个项目,每个的平均内存为 2 MB(总计 20 MB)。
C#
public void GetNavigationInfo() { SPWeb oSPWeb = SPContext.Web; // .. Get information oSPWeb for navigation .. foreach(SPWeb oSubWeb in oSPWeb.GetSubWebsForCurrentUser()) { // .. Add subweb information for navigation .. } }
在用户负荷较小时,上述情况可能不会出现问题,但是随着用户负荷增加,可能开始出现性能降低、用户超时、意外错误,在某些情况下,还会出现 SharePoint 应用程序或应用程序池故障。使用先前的每次用户点击页面时创建 10 个对象的示例,您会看到内存耗费上升非常快。
例如,下表显示了用户在相对短的时间内点击系统时内存是如何分配的。
表 1. 用户数增加时内存使用率的最好和最坏情况
用户 |
最好情况 |
最坏情况 |
10 |
100 MB |
200 MB |
50 |
500 MB |
1000 MB |
100 |
1000 MB |
2000 MB |
250 |
2500 MB |
5000 MB |
随着点击系统的用户数增加,此情况会变得更糟糕。随着内存使用率的增加,系统开始出现异常行为,包括性能降低,并可能出现故障,直至应用程序池再生或发出 iisreset 命令。
通过询问以下问题,可以轻松确定此难题:
应用程序池频繁回收,尤其是在繁重负荷情况下更是这样吗?
假定应用程序池已设定为在达到内存阈值时回收。内存阈值应在 800 MB 和 1.5 GB 之间(假定具有 2 GB 或更多的 RAM)。将应用程序的回收设置为接近 1 GB 时出现会得到最佳结果,但是应进行试验,看看哪种设置最符合您的情况。如果再生设置太低,则会出现由于频繁进行应用程序回收而导致性能问题。如果设置过高,则系统将出现由于页面交换、内存碎片和其他问题而导致的性能问题。
系统性能降低,尤其是在繁重负荷情况下更是这样吗?
内存使用率开始增加时,系统必须加以补偿,例如通过分页内存和处理内存碎片。
系统出现崩溃或用户遇到意外错误,如超时或页面不可用错误,尤其是在繁重负荷情况下更是这样吗?
此外,内存利用率变高或出现碎片时,某些功能将由于无法为其他操作分配内存而出现故障。在很多情况下,代码不能正确处理“内存不足”异常,从而导致故障或令人误解的错误。
系统使用自定义的 Web Parts 或使用任何第三方 Web Parts 吗?
大多数 Web Part 开发人员没有意识到自己必须处理 SharePoint 对象以及为什么要处理。他们假定垃圾回收自动执行此功能,但并不是所有情况下这都是正确的。
如果您对第 4 个问题和一个或多个其他问题的回答为“是”,则大约有 90% 的机会认定您的自定义代码没有正确处理项目。从表 1 可以看出,只需一个没有正确处理项目的使用强度高的页面就可导致某些问题。以下是如何修复先前 GetNavigationInfo 功能的一个示例。
C#
public void GetNavigationInfo() { SPWeb oSPWeb = SPContext.Web; foreach(SPWeb oSubWeb in oSPWeb.GetSubWebsForCurrentUser())) { // .. Add subweb information for navigation .. oSubWeb.Dispose(); } }
在 foreach 循环中,新的 SPWeb 对象在每次从集合中检索时创建。大多数开发人员假定对象在超出范围时会被清理,但是这在使用 SharePoint 对象模型时不会发生。
您还必须意识到可导致错误的其他问题。例如,在站点上调用 RootWeb 属性时,必须通过调用 RootWeb.Dispose() 方法处理其创建的 SPWeb 对象。有关如何正确处理项目的详细信息,请参阅最佳实践:使用可处理的 Windows SharePoint Services 对象。
注意:
不要处理直接从 Microsoft.SharePoint.SPContext.Site 或 Microsoft.SharePoint.SPContext.Web 属性返回的任何项目。否则会导致 SharePoint 系统不稳定,并可导致应用程序出现故障。
许多开发人员开始使用 Microsoft .NET Framework 缓存对象(例如,System.Web.Caching.Cache),以帮助更好地使用内存并提高系统整体性能。但是,许多对象不是“线程安全”并且缓存这些对象会导致应用程序崩溃或异常用户错误。
开发人员尝试通过调用从查询返回的 SPListItemCollection 对象提高性能和内存使用率。通常,这是一个很好的操作,但 SPListItemCollection 对象包含一个不是线程安全的嵌入 SPWeb 对象,不应缓存。例如,假定 SPListItemCollection 对象在线程 A 中缓存。随后在其他线程尝试读取它时,应用程序会出现故障或出现异常行为,因为对象不是线程安全。
有关详细信息,请参阅 Microsoft.SharePoint.SPWeb 类。
有些开发人员没有意识到他们是在多线程环境中运行(默认情况下,Internet Information Services 是多线程的),或没意识到该如何管理该环境。以下代码示例显示了一些开发人员如何缓存 Microsoft.SharePoint.SPListItemCollection 对象。
C#
public void CacheData() { SPListItemCollection oListItems; oListItems = (SPListItemCollection)Cache["ListItemCacheName"]; if(oListItems == null) { oListItems = DoQueryToReturnItems(); Cache.Add("ListItemCacheName", oListItems, ..); } }
在前一代码示例中,问题在于如果检索数据的查询要用 10 秒钟,则可能有许多用户同时点击该页面,都运行同一查询并尝试在同一时间更新同一缓存对象。这会导致性能出现问题,因为可能要运行 10 次、50 次或 100 次同一查询,也会因多个线程尝试在同一时间更新同一对象导致系统崩溃,尤其是在多进程、超线程的计算机上更是如此。要修复此问题,必须将代码更改为如下内容。
C#
public void CacheData() { SPListItemCollection oListItems; lock(this) { oListItems = (SPListItemCollection)Cache["ListItemCacheName"]; if(oListItems == null) { oListItems = DoQueryToReturnItems(); Cache.Add("ListItemCacheName", oListItems, ..); } } }
注意:
将 lock 放在 if(oListItems == null) 代码块内,可以稍微提高一些性能。这样做时,不必在检查数据是否已经缓存时挂起所有线程。多个用户仍可能同时运行查询,具体要视查询返回数据所用的时间而定。如果在多处理器计算机上运行,情况更是如此。切记,运行的处理器越多以及查询所需的时间越长,将 lock 放在 if() 代码块中会出现问题的可能性越高。尽管上一示例可能导致轻微的性能影响,但是这是确保不会同时运行多个查询的唯一方式。
此代码挂起在 Internet Information Services 中临界区内运行的所有其他线程,并阻止其他线程在缓存对象完全建立前对其进行访问。
上一示例解决了线程同步问题,但是方法仍不适当,因为其缓存的是线程不安全的对象。要解决线程安全问题,应缓存从 SPListItemCollection 对象创建的 DataTable 对象。例如,应将上一示例修改为以下内容。
C#
public void CacheData() { DataTable oDataTable; SPListItemCollection oListItems; lock(this) { oDataTable = (DataTable)Cache["ListItemCacheName"]; if(oDataTable == null) { oListItems = DoQueryToReturnItems(); oDataTable = oListItems.GetDataTable(); Cache.Add("ListItemCacheName", oDataTable, ..); } } }
代码随后从 DataTable 对象获取数据。有关使用 DataTable 对象以及其他开发 SharePoint 应用程序的好方法的详细信息和示例,请参阅 Windows SharePoint Services 开发技巧。
有些开发人员没有意识到需要编写具有可伸缩性的代码,以便同时处理多个用户。一个很好的示例就是在每个页面上为所有站点和子站点创建自定义导航信息,或将其作为母版页的一部分。例如,如果在企业 Intranet 上有一个 SharePoint 站点,并且每个部门都有自己的具有许多子站点的站点,则代码可以与如下代码类似。
C#
public void GetNavigationInfoForAllSitesAndWebs() { foreach(SPSite oSPSite in SPContext.Current.Site.WebApplication.Sites) { using(SPWeb oSPWeb = oSPSite.RootWeb) { AddAllWebs(oSPWeb ); } } }C#
public void AddAllWebs(SPWeb oSPWeb) { foreach(SPWeb oSubWeb in oSPWeb.Webs) { //.. Code to add items .. AddAllWebs(oSubWeb); oSubWeb.Dispose(); } }
尽管上一代码正确处理了对象,但是仍会出现问题,因为代码会反复通过同一列表。例如,如果有 10 个站点集合并且每个站点集合平均有 20 个站点或子站点,则会迭代通过相同代码 200 次。如用户数量少,这不会导致性能变差。但是,在向系统添加更多用户时,问题会变得更糟糕。表 2 显示了这种情况。
表 2. 迭代随用户数的增加而增加
用户 | 迭代 |
---|---|
10 |
2000 |
50 |
10000 |
100 |
200000 |
250 |
500000 |
为每个点击系统的用户执行代码,但是数据对于每个人都保持一样。它的影响会随代码执行的操作而变化。在某些情况下,反复重复代码可能不会导致性能出现问题,但是在上一示例中,系统必须创建一个 COM 对象(SPSite 或 SPWeb 对象在从其集合中检索时创建),从中检索数据,然后针对集合中的每个项目处理代码。这产生了许多性能开销。
如何才能使此代码对多用户环境更具可伸缩性或可以精细调整?这个问题很难回答,具体情况视应用程序的设计目标而定。在询问如何使代码更具可伸缩性时,有许多事情需要加以考虑:
数据是静态(很少变化)、一定程度静态(偶尔变化)还是动态(不断变化)的?
数据对所有用户都是相同的还是有所变化?例如,数据是根据登录的用户、要访问的站点区域或是每年的时间(季节信息)而变化吗?
数据可轻松地访问还是需要较长时间才能返回数据?例如,数据是从长时间运行的 SQL 查询返回的,还是从数据传输中有一些网络延迟的远程数据库返回的?
数据是公共的还是要求更高级别的安全?
数据多大?
SharePoint 站点在单个服务器上还是在服务器场上?
根据对上述问题的不同回答,可以使用几种不同的方法使自己的代码更具可伸缩性并处理多个用户。本文的目的不是为所有问题或案例提供答案,而是提供几个可应用于特定要求的意见。以下列出了几项要考虑的内容。
可使用 System.Web.Caching.Cache 对象缓存自己的数据。此对象要求进行一次数据查询并将其存储在缓存中供其他用户访问。
如果数据是静态的,则可将缓存设置为一次加载数据并且在应用程序重新启动前不过期,或设置为一天加载一次以确保数据为最新状态。可以在应用程序启动时、首次启动用户会话时或用户首次尝试访问数据时创建缓存项目。
如果数据在一定程度上是静态的,则可将缓存项设置为在其创建后某秒、某分或某小时到期。这样可以在用户可接受的时间范围内刷新数据。即使数据只缓存 30 秒,在繁重的负荷下仍可看到性能有所提高,因为对于每个点击系统的用户,每隔 30 秒运行一次代码,而不是每秒运行多次。
确保考虑了先前在缓存数据和对象中概述的问题。
考虑如何使用缓存的数据。如果此数据用于作出运行时决定,将其放入 DataSet 或 DataTable 对象中应该是存储它的最好方法。然后可以查询这些对象的数据以便作出运行时决定。如果数据要用于向用户显示一个列表、表格或格式页面,应考虑建立一个显示对象并将该对象存储在缓存中。在运行时,只需从缓存检索对象并调用其渲染函数以显示其内容。也可存储渲染的输出,但是会引起安全问题并且缓存的项目会非常巨大,产生许多页面交换或内存碎片。
根据 SharePoint 站点的设置方式,可能要有区别地解决一些缓存问题。如果数据需要在所有服务器上保持不变,则必须确保在每个服务器上都缓存相同的数据。确保这点的一种方式是创建缓存的数据并将其存储在公共服务器或 SQL 数据库中。此外,还必须考虑访问数据所花费的时间以及存储在公共服务器上的数据的所有安全问题。
也可创建在公共服务器上缓存数据的业务层对象,然后通过网络对象或 API 中可用的不同进程间通信访问该数据。
要确保 SharePoint 系统性能最佳,您需要能对以下与所编写代码相关的问题给出答案:
代码正确处理了 SharePoint 对象吗?
代码正确缓存了对象吗?
代码缓存了正确的对象类型吗?
代码在需要时使用了线程同步吗?
对于 1000 个用户和 10 个用户,代码的工作效率一样吗?
如果在编写代码时考虑了这些问题,您将会发现 SharePoint 系统能更有效地运行,用户会具有更好的体验。还可帮助防止在系统中出现意外故障和错误。
在本文的技术审查中,非常感谢下列人员给予的巨大帮助:James Waymire(Microsoft 公司)、Duray Akar(Microsoft 公司)、Rashid Aga(Microsoft 公司)、Roger Lamb(Microsoft 公司)以及 Jeff Lin(Microsoft 公司)。