3

在 Asp.net MVC 应用程序中,我们有一个页面可以下载动态生成的 Excel 报告。客户端应用程序调用生成 excel 文件并将文件名返回给客户端的 WCF 服务。WCF 服务使用 OpenXML Sax Approach 来生成 excel 文件。

服务器调用存储过程并使用数据读取器来获取数据。通常该文件包含 10000 条记录。我们在测试环境中没有遇到任何性能问题。在生产环境中,如果 10 个人访问该报告,服务器内存将达到最大值,CPU 利用率也为 98%。因此,它会给该服务器中的所有应用程序带来问题。服务器只有 4GB RAM。我运行 4 个应用程序。通常我的应用程序会占用更多内存。

这是代码:

public string GetMemberRosterHistoryFile(string VendorId, string versionId, DateTime FromDate, string ActionIndicator)
{
    string path = ConfigurationManager.AppSettings["FilePath"] + Guid.NewGuid() + ".xlsx";
    try
    {
        path = PathInfo.GetPath(path);
        log4net.ThreadContext.Properties["MethodName"] = "GetMemberRostersHistory";
        log.Info("Getting member rosters History");
        string sConn = ConfigurationManager.ConnectionStrings["VendorConnectContext"].ConnectionString;
        using (SqlConnection oConn = new SqlConnection(sConn))
        {
            oConn.Open();
            log.Debug("Connected");
            string sCmd = "pGetMemberRostersHistory";
            SqlCommand oCmd = new SqlCommand(sCmd, oConn);
            oCmd.CommandTimeout = Int32.MaxValue;
            oCmd.CommandType=CommandType.StoredProcedure;
            oCmd.Parameters.AddWithValue("@FromDate", FromDate.ToShortDateString());
            oCmd.Parameters.AddWithValue("@ActionIndicator", ActionIndicator);
            int index=1;
            StringBuilder programs = new StringBuilder();
            if (string.IsNullOrEmpty(versionId))
            {
                foreach (string value in GetPrograms(VendorId))
                {
                    if (index > 1)
                    {
                        programs.Append(",");
                    }
                    programs.Append(value);
                    index++;
                }
            }
            else
            {
                foreach (string value in GetPrograms(VendorId, versionId))
                {
                    if (index > 1)
                    {
                        programs.Append(",");
                    }
                    programs.Append(value);
                    index++;
                }
            }
            oCmd.Parameters.AddWithValue("@ProgramsList", programs.ToString());

            string[] FieldNames = new string[] 
                {
                        "ActionIndicator", 
                    "ChangeNotes",
                    "ActionEffectiveDate",
                    "MembershipTerminationDate",
                    "GPOId",
                    "GLN",
                    "HIN",
                    "Name1",
                    "Name2",
                    "AddressType",
                    "Address1",
                    "Address2",
                    "Address3",
                    "City",
                    "StateProvince",
                    "ZipPostalCode",
                    "Country",
                    "PhoneNbr",
                    "FaxNbr",
                    "RelationshipToGPO",
                    "RelationshipToDirectParent",
                    "DirectParentGPOId",
                    "DirectParentName1",
                    "TopParentGPOId",
                    "TopParentName1",
                    "MemberStatus",
                    "MembershipStartDate",
                    "OrganizationalStatus",
                    "ClassOfTradeName",
                    "DEA",
                    "DSHorHRSA",
                    "PHEffectiveDate",
                    "PHExpirationDate",
                    "BLPHEffectiveDate",
                    "BLPHExpirationDate",
                    "MMEffectiveDate",
                    "MMExpirationDate",
                    "BLMMEffectiveDate",
                    "BLMMExpirationDate",
                    "DIEffectiveDate",
                    "DIExpirationDate",
                    "LBEffectiveDate",
                    "LBExpirationDate",
                    "NMEffectiveDate",
                    "NMExpirationDate"
                    ,"BLMemberId"
                        ,"GPOCorporateGroup"
                        ,"GPOAffiliateGroup"
                        ,"GPO2AffiliateGroup"
                        ,"GPORelatedGroup"
                        ,"GPOIDNGroup"

                };
        string[] columnNames = new string[] 
                    {
                        "Action Indicator",
                        "Change Notes",
                        "Action Effective Date",
                        "Membership Termination Date",
                            "GPO ID",
                            "GLN",
                            "Health Industry Number (HIN)",
                            "Name 1",
                            "Name 2",
                            "Address Type",
                            "Address 1",
                            "Address 2",
                            "Address 3",
                            "City",
                            "State/Province",
                            "Postal Code",
                            "Country",
                            "Phone",
                            "Fax",
                            "Relationship to GPO",
                            "Relationship to Direct Parent",
                            "Direct Parent GPO ID",
                            "Direct Parent Name 1",
                            "Top Parent GPO ID",
                            "Top Parent Name 1",
                            "Member Status",
                            "Membership Start Date",
                            "Organizational Status",
                            "Class of Trade",
                            "DEA #",
                            "DSH and/or HRSA Number",
                            "Pharmacy Start Date",
                            "Pharmacy End Date",
                            "BL Pharmacy Start Date",
                            "BL Pharmacy End Date",
                            "Med Surg Start Date",
                            "Med Surg End Date",
                            "BL Med Surg Start Date",
                            "BL Med Surg End Date",
                            "Food Service Start Date",
                            "Food Service End Date",
                            "Laboratory Start Date",
                            "Laboratory End Date",
                            "NonMedical Start Date",
                            "NonMedical End Date"
                            ,"Broadlane ID"
                            ,"Corporate Group"
                            ,"Affiliate Group"
                            ,"2nd Affiliate Group"
                            ,"Related Group"
                        ,"IDN Group"
                    };
            //object result = oCmd.ExecuteScalar();
            //int count=(result!=null ? (int)result : 0);
            //oCmd.CommandText = "pGetMemberRostersHistory";
            //oCmd.CommandTimeout = Int32.MaxValue;
            using (SqlDataReader oReader = oCmd.ExecuteReader(CommandBehavior.CloseConnection))
            {
                SAXExcelExporter exporter = new SAXExcelExporter();
                exporter.Export(oReader, columnNames, FieldNames, path, "MemberRoster");

            }
        }
        return path;
    }
    catch (Exception ex)
    {
        log.Error("In exception", ex);
        return null;
    }
}

出口代码:

public void Export(SqlDataReader export, string[] columnNames, string[] fieldNames, string filename, string sheetName)
{
    Assembly _assembly = Assembly.GetExecutingAssembly();
    Stream stream = _assembly.GetManifestResourceStream("MA.VMS.Server.Template.xlsx");
    FileStream newfile = new FileStream(filename, FileMode.Create, FileAccess.ReadWrite);
    stream.CopyTo(newfile);
    stream.Close();
    newfile.Close();
    using (SpreadsheetDocument myDoc = SpreadsheetDocument.Open(filename, true))
    {
        WorkbookPart workbookPart = myDoc.WorkbookPart;
        WorksheetPart worksheetPart = workbookPart.WorksheetParts.Last();
        string origninalSheetId = workbookPart.GetIdOfPart(worksheetPart);

        WorksheetPart replacementPart = workbookPart.AddNewPart<WorksheetPart>();
        string replacementPartId = workbookPart.GetIdOfPart(replacementPart);

        OpenXmlReader reader = OpenXmlReader.Create(worksheetPart);
        OpenXmlWriter writer = OpenXmlWriter.Create(replacementPart);

        while (reader.Read())
        {
            if (reader.ElementType == typeof(SheetData))
            {
                if (reader.IsEndElement)
                    continue;
                writer.WriteStartElement(new SheetData());
    Row hr = new Row();
                writer.WriteStartElement(hr);

                for (int col = 0; col < columnNames.Length; col++)
                {
                    Cell c = new Cell();
                    c.DataType = CellValues.InlineString;
                    InlineString iss = new InlineString();
                    iss.AppendChild(new Text() { Text = columnNames[col] });
                    c.AppendChild(iss);
                    writer.WriteElement(c);
                }
    writer.WriteEndElement();

                //for (int row = -1; row < count; row++)
                while (export.Read())
                {

                    Row r = new Row();
                    writer.WriteStartElement(r);
                    //if (row == -1)
                    //{
                    //    for (int col = 0; col < columnNames.Length; col++)
                    //    {
                    //        Cell c = new Cell();
                    //        c.DataType = CellValues.InlineString;
                    //        InlineString iss = new InlineString();
                    //        iss.AppendChild(new Text() { Text = columnNames[col] });
                    //        c.AppendChild(iss);
                    //        writer.WriteElement(c);
                    //    }
                    //}
                    //else
                    //{
                        //export.Read();
                        for (int col = 0; col < fieldNames.Length; col++)
                        {
                            Cell c = new Cell();
                            c.DataType = CellValues.InlineString;
                            InlineString iss = new InlineString();
                            iss.AppendChild(new Text() { Text = GetValue(export[fieldNames[col]]) });

                            c.AppendChild(iss);
                            writer.WriteElement(c);
                        }
                    //}
                    writer.WriteEndElement();

                }

                writer.WriteEndElement();
            }
            else
            {
                if (reader.IsStartElement)
                {
                    writer.WriteStartElement(reader);
                }
                else if (reader.IsEndElement)
                {
                    writer.WriteEndElement();
                }
            }
        }

        reader.Close();
        writer.Close();

        Sheet sheet = workbookPart.Workbook.Descendants<Sheet>().Where(s => s.Id.Value.Equals(origninalSheetId)).First();
        sheet.Id.Value = replacementPartId;
        workbookPart.DeletePart(worksheetPart);

    }
}

我很担心。当我查看 proc 在 26 秒内返回数据时,excel 下载需要 3 分钟以上。

我应该怎么做这种情况?以下是我正在考虑的解决方案:

  1. 将excel下载异步移动并发送下载链接
  2. 在不同的服务器上部署 2 个应用程序。
  3. 在服务器上做内存分析器
4

3 回答 3

1

问题可能是混合使用 Open XML SDK 类和 SAX 方法类(尤其是 OpenXmlWriter)。SDK 中有很多 DOM 包袱,这就是它们速度较慢的原因。

对于这种特殊情况,它是 Cell 类。整个 Worksheet、SheetData 和 Row SDK 类都是用 OpenXmlWriter 编写的,但 Cell 类仍然使用 SDK 版本的填充数据。这是瓶颈。尝试这个:

List<OpenXmlAttribute> oxa;
for (int col = 0; col < fieldNames.Length; col++)
{
    oxa = new List<OpenXmlAttribute>();
    // this is the data type ("t"), with CellValues.String ("str")
    oxa.Add(new OpenXmlAttribute("t", null, "str"));

    // it's suggested you also have the cell reference, but
    // you'll have to calculate the correct cell reference yourself.
    // Here's an example:
    //oxa.Add(new OpenXmlAttribute("r", null, "A1"));

    writer.WriteStartElement(new Cell(), oxa);

    writer.WriteElement(new CellValue(GetValue(export[fieldNames[col]])));

    // this is for Cell
    writer.WriteEndElement();
}

此外, CellValues.InlineString 用于内联富文本。如果您只有纯文本,则 CellValues.String 枚举值更简单。

前段时间我也写了一篇关于它的文章。您可以在此处了解更多信息

于 2013-07-22T03:13:39.457 回答
0

您对 WCF 使用什么序列化程序,默认情况下您可能在 XML 中序列化。我领导了一个类似的项目,我们使用 Microsoft Sync Services 向客户端发送大量数据,其中包含大量 Xml 数据,导致服务器和客户端的序列化和反序列化速度非常慢。这是因为包含在返回的实体中的任何 xml 都必须在服务器上转义,然后在客户端上取消转义,这对于大数据块来说真的很慢。

为了解决这个问题,我们使用了二进制序列化程序,但这不仅仅是服务器端修复,您还需要更改客户端。这假定连接的唯一客户端是您的 .Net 客户端。

如果要返回字符串,WCF 响应可能如下所示:

<GetMemberRosterHistoryFile>
    <MethodParameters>
        <String>
            &lt;WordDocXml&gt;
                &lt;SomeXmlElement someAttribute=&quot;foo&quot; /&gt;
            &lt;/WordDoxXml&gt;
        </String>
    </MethodParameters>
</GetMemberRosterHistoryFile>

我在我的示例中使用了 html 编码,这是我认为 WCF 所做的,但无论如何它说明了这一点。想象一下,如果 .Net 必须为大型文档执行此操作。

WCF Xml 与二进制序列化的优缺点

以二进制方式序列化 WCF 消息,而不是作为 SOAP 消息

于 2013-07-22T09:08:41.857 回答
0

这些都是好主意。但是,我会更多地研究性能调整。

你说你有4GB的内存。假设所有开销都损失了 1GB,而服务器上的其他应用程序有点空闲。这样就剩下 3GB 的空间来处理 10 份报告。每个 300MB。你的报告在那个大小吗?假设您的输出为 100MB,那么您将轻松达到 300MB,其中包含您应该删除的各种内部表示。但是,如果您只生成 10MB,那么您需要解决一些严重的性能问题。

你说这已经在生产中了。所以一个快速的创可贴解决方案是让这个应用程序拥有自己的服务器,并拥有大量的 RAM。这应该会为您争取一点时间,以便您找出它占用如此多内存的原因。

对于更长期的解决方案,我会像您建议的那样,将其设为异步过程。我认为超过几秒钟的时间应该是异步的。客户端将请求报告,您的前端会将这个请求放入队列中。一些后台工作人员将从队列中提取此请求并进行处理。完成后,客户端可以回来下载它。

这将使您更好地控制有多少并发请求。您将需要限制此操作,以免内存不足或数据库负担过重。当然,您的客户会不耐烦,但这总比您自己的系统在您的应用程序为内存而挣扎时陷入停顿要好。

于 2013-07-20T18:37:48.460 回答