C# - 浏览器正在发送重复请求,并且错误连接已为长 运行 进程重置

C# - Browser is sending duplicate request and error connection was reset for long running process

我有一个带有按钮的页面。单击时,我从 table 中获取 539,200 行并使用 OpenXML SDK 创建 excel 文件。它创建了超过 50MB 的文件,我通过 WebClient.UploadFile 方法在 FTP 上上传了这些文件。在整个过程中,IIS Worker 进程 CPU 消耗量达到 30%,内存利用率达到 1.2 GB。在我的服务器上完成整个过程大约需要 10 分钟。完成后浏览器 (Firefox) 需要额外 10 分钟并显示错误 "The connection was reset"。在上一次执行完成前 10 分钟后,我在日志中看到另一个执行开始了。我确定没有其他人在使用该服务器。

我的问题

  1. 为什么进程完成后内存利用率没有下降?我已经仔细处理了每一件物品。甚至称为垃圾收集器。我不得不重新启动 IIS 以释放内存。我可以用代码做什么?

  2. 根据我的日志,整个执行在 10 分钟内完成,但浏览器仍然没有响应,它一直显示 "Connecting....."。又过了大约 10 分钟,它给出了错误 "The connection was reset"。出了什么问题?

  3. 为什么我在上一个结束前看到另一个执行日志?浏览器是否发送另一个请求或 IIS/ASP.Net 疯了?

  4. 当使用 Ajax 请求执行时,我每隔大约 10 分钟就会看到重复的日志条目,直到我重新启动 IIS。发生什么事了?

重复的日志条目转化为重复执行相同的进程。我现在要疯了。

我正在使用 Windows Server 2012 和 IIS 8。

编辑

获取数据

    Function fun = new Function();
            List<SqlParameter> para = new List<SqlParameter>();
            para.Add(new SqlParameter() { ParameterName = "@IDs", SqlDbType = SqlDbType.NVarChar, Size = 4000, Value = "something" });
            para.Add(new SqlParameter() { ParameterName = "@Fromdate", SqlDbType = SqlDbType.Date, Value = "2017-06-01" });
            para.Add(new SqlParameter() { ParameterName = "@Todate", SqlDbType = SqlDbType.Date, Value = "2017-06-27" });
            dsExcel = fun.GetDataSet("sp_GetData", para);

导出到 Excel(使用 Pivot Table)

    private bool ExportDSToExcel(string destination)
    {
        LogUtil.LogInfo("Writing excel with rows: " + dsExcel.Tables[0].Rows.Count);
        try
        {
            using (var spreadsheet = SpreadsheetDocument.Open(destination, true))
            {
                foreach (DataTable table in dsExcel.Tables)
                {
                    WorkbookPart workbookPart = spreadsheet.WorkbookPart;
                    WorksheetPart worksheetPart = workbookPart.WorksheetParts.First();
                    string origninalSheetId = workbookPart.GetIdOfPart(worksheetPart);
                    WorksheetPart replacementPart =
                    workbookPart.AddNewPart<WorksheetPart>();
                    string replacementPartId = workbookPart.GetIdOfPart(replacementPart);
                    DocumentFormat.OpenXml.OpenXmlReader reader = DocumentFormat.OpenXml.OpenXmlReader.Create(worksheetPart);
                    DocumentFormat.OpenXml.OpenXmlWriter writer = DocumentFormat.OpenXml.OpenXmlWriter.Create(replacementPart);
                    while (reader.Read())
                    {
                        if (reader.ElementType == typeof(SheetData))
                        {
                            if (reader.IsEndElement)
                                continue;
                            writer.WriteStartElement(new SheetData());
                            DocumentFormat.OpenXml.Spreadsheet.Row headerRow = new DocumentFormat.OpenXml.Spreadsheet.Row();
                            writer.WriteStartElement(headerRow);

                            List<String> columns = new List<string>();
                            foreach (DataColumn column in table.Columns)
                            {
                                columns.Add(column.ColumnName);
                                Cell cell = new Cell();
                                cell.DataType = CellValues.String;
                                cell.CellValue = new CellValue(column.ColumnName);
                                writer.WriteElement(cell);
                            }
                            //End Row element writing
                            writer.WriteEndElement();

                            foreach (DataRow dsrow in table.Rows)
                            {
                                DocumentFormat.OpenXml.Spreadsheet.Row newRow = new DocumentFormat.OpenXml.Spreadsheet.Row();
                                writer.WriteStartElement(newRow);
                                foreach (String col in columns)
                                {
                                    Cell cell = new Cell();
                                    if ((dsrow[col].GetType().ToString().Contains(TypeCode.Int32.ToString()) || (dsrow[col].GetType().ToString().Contains(TypeCode.Decimal.ToString())) || (dsrow[col].GetType().ToString().Contains(TypeCode.Int64.ToString()))))
                                    {
                                        cell.DataType = CellValues.Number;
                                    }
                                    else
                                    {
                                        cell.DataType = CellValues.String;
                                    }
                                    cell.CellValue = new CellValue(dsrow[col].ToString()); //
                                    writer.WriteElement(cell);
                                }
                                writer.WriteEndElement();
                            }
                            //End SheetData writing
                            writer.WriteEndElement();
                        }
                        else
                        {
                            if (reader.IsStartElement)
                            {
                                writer.WriteStartElement(reader);
                            }
                            else if (reader.IsEndElement)
                            {
                                writer.WriteEndElement();
                            }
                        }
                    }
                    reader.Close();
                    reader.Dispose();
                    writer.Close();
                    writer.Dispose();
                    Sheet sheet = workbookPart.Workbook.Descendants<Sheet>()
                    .Where(s => s.Id.Value.Equals(origninalSheetId)).First();
                    sheet.Id.Value = replacementPartId;
                    workbookPart.DeletePart(worksheetPart);
                }
                PivotTableCacheDefinitionPart ptp = spreadsheet.WorkbookPart.PivotTableCacheDefinitionParts.First();
                ptp.PivotCacheDefinition.RefreshOnLoad = true;
                ptp.PivotCacheDefinition.RecordCount = Convert.ToUInt32(dsExcel.Tables[0].Rows.Count);                    ptp.PivotCacheDefinition.CacheSource.WorksheetSource.Reference = "A1:" + IntToLetters(dsExcel.Tables[0].Columns.Count) + (dsExcel.Tables[0].Rows.Count + 1);                    ptp.PivotTableCacheRecordsPart.PivotCacheRecords.RemoveAllChildren();
                ptp.PivotTableCacheRecordsPart.PivotCacheRecords.Count = 0;
                spreadsheet.Save();
                spreadsheet.Close();
                spreadsheet.Dispose();
                //GC.Collect();
                //GC.WaitForPendingFinalizers();
            }
            LogUtil.LogInfo("Wrote excel");
            return true;
        }
        catch (Exception ex)
        {
            return false;
        }
    }

上传到FTP

    public void UploadFileToFtp(string file)
    {
        FileInfo fileInfo = new FileInfo(file);
        using (WebClient client = new WebClient())
        {
            client.Credentials = ftpNetworkCredentials;
            client.UploadFile(ftpUri + fileInfo.Name, "STOR", file);
            client.Dispose();
        }
        LogUtil.LogInfo(file + " uploaded successfully");
    }

按钮点击事件代码

    protected void btnSubmit_Click(object sender, EventArgs e)
    {
        LogUtil.LogInfo("Getting data");
        FillReportTable();
        LogUtil.LogInfo("File upload is disabled");
        string IOPath = Server.MapPath("~/Report-" + DateTime.Now.ToString("MM-dd-yyyy-hh-mm-ss") + ".xlsx");
        if (System.IO.File.Exists(IOPath))
        {
            System.IO.File.Delete(IOPath);
        }
        System.IO.File.Copy(Server.MapPath("~/TempReport.xlsx"), IOPath);
        ExportDSToExcel(IOPath);
        if (Convert.ToBoolean(ConfigurationManager.AppSettings["ftpUpload"].ToString()))
        {
            UploadToFTP(IOPath);
        }
        else
        {
            LogUtil.LogInfo("File upload is disabled");
        }            
        lblMessage.Text = "File uploaded successfully";
    }

似乎内存利用率没有下降,因为服务器在完成上一个请求之前重复执行,或者因为浏览器每 10 分钟后不断发送请求。我检查了 fiddler,但它用轮询调用淹没了日志。无法彻底检查。 相同的代码需要相同的时间,并且在同一台服务器上访问该页面时工作正常。在互联网上做同样的事情时会产生问题。 我在工作时有 10mbps 的连接,在 Azure 云上有 VM。

您可能会遇到这种行为的原因有很多,但一般来说,您需要克服重重困难,让浏览器等待 10 分钟的响应。相反,一般来说,您应该在此之前 return 对浏览器作出响应,并定期轮询您的应用程序以查看任务是否已完成。当然还有其他方法,比如使用websockets,发起进程等待响应。

  1. 如果您的应用程序仍然有对该对象的引用,它将保留在内存中,即使您进行了垃圾回收。在没有看到您的代码的情况下,很难说出该引用的位置。

  2. 如前所述,在 10 分钟过去之前,浏览器将停止等待响应并关闭底层连接。

  3. 浏览器完全有可能在关闭无响应连接后自动尝试请求。

  4. 不看代码很难判断。

运行 长 运行ning 任务的一种简单方法是使用 Ajax 触发它,正如您所说的那样 运行 使用 System.Threading.Tasks.Task。您可以根据需要存储对任务的引用。 Ajax 然后可用于轮询任务状态以检查是否已完成。

听说在服务器端使用通用处理程序的样板实现 运行 需要 1 分钟才能完成的任务和使用 JQuery 启动的 HTML 页面任务使用 Ajax 并监控进度。

LongRunningTask.ashx

<%@ WebHandler Language="C#" Class="LongRunningTask" %>

using System;
using System.Web;
using System.Web.SessionState;
using System.Web.Script.Serialization;
using System.Threading.Tasks;
public class LongRunningTask : IHttpHandler, IRequiresSessionState
{
    private const string INVALID = "Invalid value for m";
    private const string SESSIONKEY = "LongRunningTask";
    private const string STARTED = "Task Started";
    private const string RUNNING = "Task Running";
    private const string COMPLETED = "Task Completed";

    public void ProcessRequest(HttpContext context)
    {
        HttpRequest request = context.Request;
        string m = request.QueryString["m"];
        switch (m)
        {
            case "start":
                TaskRunner runner = new TaskRunner();
                context.Session[SESSIONKEY] = runner.DoWork();
                ShowResponse(context, STARTED);
                break;
            case "progress":
                Task<int> t = (Task<int>)context.Session[SESSIONKEY];
                ShowResponse(context, t.IsCompleted ? COMPLETED : RUNNING);
                return;
            default:
                ShowResponse(context, INVALID);
                break;
        }
    }

    private void ShowResponse(HttpContext context, string message)
    {
        JavaScriptSerializer ser = new JavaScriptSerializer();
        string json = ser.Serialize(message);
        context.Response.ContentType = "text/javascript";
        context.Response.Write(json);
    }


    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
    private class TaskRunner
    {
        public bool Finished { get; set; }
        private Task<int> longTask;
        public TaskRunner()
        {

        }
        public Task<int> DoWork()
        {
            var tcs = new TaskCompletionSource<int>();
            Task.Run(async () =>
            {
                // instead of the following line, launch you method here.
                await Task.Delay(1000 * 60 * 1);
                tcs.SetResult(1);
            });
            longTask = tcs.Task;
            return longTask;
        }
    }

}

RunLongTask.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Run Long Task</title>
    <script src="//code.jquery.com/jquery-2.2.4.min.js" type="text/javascript"></script>
    <script type="text/javascript">
        $(document).ready(function () {
            $('#runLongTask').click(function () { runLongTask(); })
        });
        function runLongTask() {
            $.ajax
                ({
                    type: "GET",
                    url: "LongRunningTask.ashx?m=start",
                    dataType: 'json',
                    success: function (data) {
                        $('#status').html(data);
                        window.setTimeout(checkStatus, 1000);
                    }
                });
        }
        function checkStatus() {
            $.ajax
                ({
                    type: "GET",
                    url: "LongRunningTask.ashx?m=progress",
                    dataType: 'json',
                    success: function (data) {
                        $('#status').html(new Date() + ' ' + data);
                        if (data !== "Task Completed") {
                            window.setTimeout(checkStatus, 1000);
                        }
                    }
                });
        }
    </script>
</head>
<body>
    <div>
        <input id="runLongTask" type="button" value="Run Long Task" title="Run Long Task" />
    </div>
    <div id="status"></div>
</body>
</html>

编辑

看到你新添加的代码,你可以集成相同的方法。

将通用处理程序添加到您的项目中。您可以删除任务 运行ner 和 "start" 的开关。

将任务中的代码 btnSubmit_Click 修改为 运行:

protected void btnSubmit_Click(object sender, EventArgs e)
{
        //prevent running of duplicate tasks.
        if(context.Session[SESSIONKEY]!=null && ((Task<int>)context.Session[SESSIONKEY]).IsCompleted==false) return;
        var tcs = new TaskCompletionSource<int>();
        Task.Run(async () =>
        {


            LogUtil.LogInfo("Getting data");
            FillReportTable();
            LogUtil.LogInfo("File upload is disabled");
            string IOPath = Server.MapPath("~/Report-" + DateTime.Now.ToString("MM-dd-yyyy-hh-mm-ss") + ".xlsx");
            if (System.IO.File.Exists(IOPath))
            {
                System.IO.File.Delete(IOPath);
            }
            System.IO.File.Copy(Server.MapPath("~/TempReport.xlsx"), IOPath);
            ExportDSToExcel(IOPath);
            if (Convert.ToBoolean(ConfigurationManager.AppSettings["ftpUpload"].ToString()))
            {
                UploadToFTP(IOPath);
            }
            else
            {
                LogUtil.LogInfo("File upload is disabled");
            }
            tcs.SetResult(1);
        });
        context.Session[SESSIONKEY] = tcs.Task;
        lblMessage.Text = "File uploaded started";
}

然后在您的网络表单的 HTML 中添加使用 ajax:

监控进度的方法
<script>
    $(document).ready(function() {
        $('#btnSubmit").click(function() {checkStatus();});
    }
    function checkStatus() {
        $.ajax
            ({
                type: "GET",
                url: "LongRunningTask.ashx?m=progress",
                dataType: 'json',
                success: function (data) {
                    $('#lblMessage').html(new Date() + ' ' + data);
                    if (data !== "Task Completed") {
                        window.setTimeout(checkStatus, 1000);
                    }
                }
            });
    }
</script>

编辑 2

您新添加的代码也解释了为什么数据保留在内存中。您正在使用的数据存储为局部变量。在执行提交按钮单击方法中的代码时,对页面及其变量的引用保留在应用程序的内存中。即使浏览器正在断开连接,代码仍在服务器端继续执行。它不会从内存中释放,直到它完成执行并且页面生命周期完成。垃圾收集不会删除它,因为它仍在被引用。

您首先使用这么多内存的全部原因是您要将数据导出到 excel。根据我的经验,我看到 excel 内存量是原始数据集的数倍。事实上,对于像您这样大的数据集,您几乎会遇到内存不足的异常。如果您可以使用其他选项,例如 CSV,您的代码将 运行 快一个数量级,以秒而不是分钟为单位。

就是说,一旦您的页面超出范围,它的内存将不再被使用,并且内存将在垃圾回收时被释放。您可以通过对所有操作(包括数据检索、转换和上传)使用完全不同的 class 来强制更快地发生这种情况。如果您在单独的线程中实例化此任务,一旦任务完成 class 并且其所有变量都将超出范围并且内存将被释放。