把这二周做的一个 .NET 应用性能优化的实践经验分享出来,记录的同时也请大家踊跃发言,分享更多。由于业务特点、整体架构设计和外围系统等因素,这个应用的性能瓶颈主要是由于 XML 相关处理造成的,其中包括大 XML 数据(50M 以上)的解析和查询、从外围系统下载 XML 数据、B/S 结构中的并发处理快速响应要求等。通过本次实践,对 .NET Framework 提供的 XML 处理有了更加深入的研究和理解,并做了一些验证并实施。最终结果还是很理想的,主要降低服务器 CPU 利用率的目标也达到,从之前的 50% 以上 (双核机器,实际上单个核心已经饱和)降低到 10% 左右,有些业务可降到 5% 左右。
首先列下我所了解的 XML 处理方式有哪些。 在 .NET Framework 3.5 中,如果不使用其它的解析器(如 vtd-xml)的话,主要会有以下 4 种:
基于 XML Dom 的 Document 树处理方式
主要使用的方法或属性有 Load、LoadXml、OuterXMl、InnerXml、SelectNodes (XPath 查询)等。
- XmlDocument doc = new XmlDocument();
- doc.Load("data.xml");
- XmlNodeList children = doc DocumentElement.SelectNodes("*");
- XmlNodeList allauthors = doc.SelectNodes("//authors/author");
- foreach (XmlNode node in allauthors)
- {
- // handle node.
- }
基于 XML Dom 的准 SAX 处理方式
主要使用的方法或属性有 LoadXml、CreateNavigator、Select (XPath 查询)、MoveNext 等。这里之所以说是“准 SAX 方式”,是因为这里虽然具备了单向向前和只读以及标签触发的性质,但这毕竟是在使用的 Xml Dom 的前提下,也就是必须要有构造 Document 树(LoadXml 方法调用)的过程。
- XmlDocument dom = new XmlDocument();
- dom.Load("data.xml");
- XPathNavigator nav = dom.CreateNavigator();
- nav.MoveToRoot();
- string xpath = @"//Rows[OneColumn = 'SomeValue']";
- XPathNodeIterator ite = nav.Select(xpath, null);
- while (ite.MoveNext())
- {
- // handle ite.Current.Value.
- }
基于 XmlTextReader 的 SAX 处理方式
主要使用的方法或属性有 Read、LocalName、ReadString 等。这种方法与 SAX 处理模型一致,单向向前、标签触发、无需 Dom 树支持。
- xmlReader = new XmlTextReader("data.xml");
- while (xmlReader.Read())
- {
- switch (xmlReader.LocalName)
- {
- case "OneColumn":
- {
- // handle xmlReader.ReadString().
- break;
- }
- case "OtherColumn":
- {
- // handle "OtherColumn" element.
- break;
- }
- default:
- {
- break;
- }
- }
- }
需要注意的是,在使用这种 XmlTextReader 拉模型的处理方式时,很少会使用以 XML 文件作为数据源来构造 XmlTextReader 对象的,更多方式会使用流来构造,因此也可以想到,很容易使用这种流加上单向向前推进的方式来实现 XML 数据的加载、解析加处理的并行处理,可以提高整体性能,这在后面还会提到。
基于 Linq to XML 的处理方式
这是 .NET Framework 3.5 提供的新特性,与上面 3 种方式截然不同,它使用 XDocument 作为底层数据结构支持,使用 Linq to Entity 对集合的扩展框架直接查询 XDocument 内容,可以附加条件并排顺序、分组等,并通过延迟加载、处理的方式来减少不必要的操作,提高性能。主要会使用这些方法或属性 Load、Descendants、Element().Value、Select、Where、Order 等。
- private void InitData()
- {
- XDocument xdom = XDocument.Load("data.xml");
- // Query rows by "OonColumn" value equal "SomeValue"
- var data = from d in xdom.Descendants("Rows")
- where d.Element("OneColumn").Value == "SomeValue"
- orderby d.Element("OneColumn").Value
- select d;
- BuildColumnDropDownList(data, "OneColumn", this.DropDownList1);
- BuildColumnDropDownList(data, "TwoColumn", this.DropDownList2);
- var subData = data.Skip(this.pageIndex * 20).Take(20); // Paging.
- this.Repeater1.DataSource = subData;
- this.Repeater1.DataBind();
- }
- private void BuildColumnDropDownList(IOrderedEnumerable<XElement> data, string columnName, DropDownList dropDownList)
- {
- if (data == null)
- throw new ArgumentNullException("data");
- if (string.IsNullOrEmpty(columnName))
- throw new ArgumentNullException("columnName");
- if (dropDownList == null)
- throw new ArgumentNullException("dropDownList");
- var colData = from irx in data.Descendants(columnName)
- group irx by irx.Value into g
- orderby g.Key
- select g.Key;
- dropDownList.DataSource = colData.ToArray();
- dropDownList.DataBind();
- }
上面的 4 种处理方法各有利弊,选择出相对较高性能的处理方法不是绝对的,这需要考虑具体的 XML 处理特点和设计。下面列出我了解的一些会影响 XML 处理性能的关键点,欢迎讨论、补充。
- 要处理的 XML 数据量大小。可以以 1M 大小为界线。
- 如果采用构建 XML Dom 树的方式,那么 XML 源是否每次都变化。也就是说是否每次处理 XML 数据都需要通过 Load 来重新构建 Dom 树,即 Dom 树是否可以缓存。典型的如查询类业务,所查询的源是不变的,但每次都要重查,这样就可以一次加载 Dom 树并缓存。
- 程序内部存储 XML 数据的方式。是采用 Dom 树(XmlDocument 类型)还是采用 XML 字符串(String 类型),实际上采用哪种方式都可以,但关键是要统一,不能某些模块使用 XmlDocument,某些模块使用 String,原因也很简单,InnerXML 或 OuterXML、LoadXml 都不是免费的午餐,每次调用(成对)都无谓的浪费些资源。
结合上面列出的关键点,把已有的 4 种方法来一一对比。
首先是 XML 数据量的事,根据测试和验证发现,数据量大小直接影响 Dom 树的解析、构造性能(主要是 CPU、Memory 利用率和响应时间),即 Load、LoadXML 方法,可见除了第三种“基于 XmlTextReader 的 SAX 处理方式”之外,其它的 3 种处理方式都有明确的 Load 动作。验证结果表明 1M 大小的 XML 数据量基本可以定做一个分界线,如果要处理的数据量大于 1M,则应该选择基于 XMLTextReader 的 SAX 处理方式来处理数据。
但是,通过验证也发现,虽然 XmlTextReader 省略了 Dom 树解析过程,但也正因为它没有树型关系,因而造成了用 XmlTextReader 遍历 XML 的性能不如用 XmlDocument 的性能好。 因此,如果业务属于可以一次构造 Dom 树并缓存,留作后面多次使用的话(如查询场景),还是推荐使用生成 Dom 树的处理方式,第二种“基于 XML Dom 的准 SAX 处理方式”,虽然首次加载会吃力。
对于 5M 以下数据量的 XML 处理,这里验证发现最好的选择就是 Linq to XML 了 ,呵呵,延迟加载的设计让 Linq 构造很快,而且在真正解析、查询、过滤时性能表现也满意。
关于大 XML 数据的并行处理。 实际上,对于基于 XMLTextReader 的 SAX 处理方式来讲,如果输入的 XML 数据源是流形式的,则 XML 加载可以与 SAX 解析并行,而且往往也的确是可以设计成这样的,因为很难想象一个 30、40M 的 XML 数据先全都加载进内存,然后再开始 XmlTextReader 的 while(reader.MoveNext) 过程,上面也说了。在我所涉及的这个应用中,这个很大的 XML 数据是通过 HTTP GET 请求从外围系统中下载过来的,这样的场景下,我们就可以使用并行的方式来处理,即边下载边解析。
- using(WebClient client = new WebClient())
- {
- // string content = client.DownloadString(@"http://server.foo/get_xml?id=123");
- // StringReader sr = new StringReader(content);
- // XmlTextReader reader = new XmlTextReader(new MemoryStream(ASCIIEncoding.Default.GetBytes(content)));
- WebRequest request = HttpWebRequest.Create(@"http://server.foo/get_xml?id=123");
- XmlTextReader reader = new XmlTextReader(request.GetResponse().GetResponseStream());
- while (xmlReader.Read())
- {
- // handle read.
- }
- }
当然了,这种并行可行性和设计还要看具体的业务场景,具体分析。如果能并行必然会很好。
最后想说的是,SQL Server 数据库也提供了 XQuary 的方式来处理 XML 数据,允许直接从关系型数据库中存储的 XML 数据直接返回 DataSource。不过以前试验过,性能不理想,SQL Server 内存涨幅较大,而且响应时间也不靠谱。这点还想请教哪位高人指点~~
忘记了说,尽量始终不要使用“基于 XML Dom 的 Document 树处理方式”来处理 XML,尽量选择其它 3 种方式。尤其是在 XML 数据比较大时。