性能计数器 - System.InvalidOperationException:类别不存在

时间:2011-11-17 17:49:54

标签: c# asp.net iis performancecounter

我有以下类返回IIS当前每秒请求数。我每分钟调用RefreshCounters以保持每秒请求数值的刷新(因为它是平均值,如果我保持太长时间,旧值会影响结果太多)......当我需要显示当前的RequestsPerSecond时,我会调用该属性。

public class Counters
{
    private static PerformanceCounter pcReqsPerSec;
    private const string counterKey = "Requests_Sec";
    public static object RequestsPerSecond
    {
        get
        {
            lock (counterKey)
            {
                if (pcReqsPerSec != null)
                    return pcReqsPerSec.NextValue().ToString("N2"); // EXCEPTION
                else
                    return "0";
            }
        }
    }

    internal static string RefreshCounters()
    {
        lock (counterKey)
        {
            try
            {
                if (pcReqsPerSec != null)
                {
                    pcReqsPerSec.Dispose();
                    pcReqsPerSec = null;
                }

                pcReqsPerSec = new PerformanceCounter("W3SVC_W3WP", "Requests / Sec", "_Total", true);
                pcReqsPerSec.NextValue();

                PerformanceCounter.CloseSharedResources();

                return null;
            }
            catch (Exception ex)
            {
                return ex.ToString();
            }
        }
    }
}

问题是,以下异常是有时抛出:

System.InvalidOperationException: Category does not exist.

at System.Diagnostics.PerformanceCounterLib.GetCategorySample(String machine,\ String category)
at System.Diagnostics.PerformanceCounter.NextSample()
at System.Diagnostics.PerformanceCounter.NextValue()
at BidBop.Admin.PerfCounter.Counters.get_RequestsPerSecond() in [[[pcReqsPerSec.NextValue().ToString("N2");]]]

我是否未正确关闭PerformanceCounter的先前实例?我做错了什么,以便我最终得到这个例外?

修改 只是为了记录,我在IIS网站上托管这个类(当然,托管在具有管理权限的App Pool中)并从ASMX服务调用方法。使用Counter值(显示它们)的站点每1分钟调用一次RefreshCounters,每5秒调用一次RequestsPerSecond; RequestPerSecond在调用之间缓存。

我每隔1分钟调用一次RefreshCounters,因为值会变得“过时” - 受旧值的影响(例如1分钟前的实际值)。

5 个答案:

答案 0 :(得分:16)

Antenka带领你走向了一个好方向。您不应该在每次更新/请求值时处置和重新创建性能计数器。实例化性能计数器是有成本的,并且第一次读取可能不准确,如下面的引用中所示。此外,您的lock() { ... }陈述非常广泛(它们涵盖了很多陈述)并且速度很慢。最好让你的锁尽可能小。我正在为Antenka投票提供质量参考和好建议!

但是,我想我可以为您提供更好的答案。我在监控服务器性能和准确理解您的需求方面拥有相当丰富的经验。您的代码没有考虑的一个问题是,无论代码显示您的性能计数器(.aspx,.asmx,控制台应用程序,winform应用程序等)都可以无论如何请求此统计信息;它可以每10秒请求一次,也许每秒5次,你不知道也不应该关心。因此,您需要将PerformanceCounter集合代码与实际报告当前请求/秒值的代码进行监视。出于性能原因,我还将向您展示如何在第一次请求时设置性能计数器,然后保持运行直到没有人提出任何请求5秒,然后正确关闭/处置PerformanceCounter。

public class RequestsPerSecondCollector
{
    #region General Declaration
    //Static Stuff for the polling timer
    private static System.Threading.Timer pollingTimer;
    private static int stateCounter = 0;
    private static int lockTimerCounter = 0;

    //Instance Stuff for our performance counter
    private static System.Diagnostics.PerformanceCounter pcReqsPerSec;
    private readonly static object threadLock = new object();
    private static decimal CurrentRequestsPerSecondValue;
    private static int LastRequestTicks;
    #endregion

    #region Singleton Implementation
    /// <summary>
    /// Static members are 'eagerly initialized', that is, 
    /// immediately when class is loaded for the first time.
    /// .NET guarantees thread safety for static initialization.
    /// </summary>
    private static readonly RequestsPerSecondCollector _instance = new RequestsPerSecondCollector();
    #endregion

    #region Constructor/Finalizer
    /// <summary>
    /// Private constructor for static singleton instance construction, you won't be able to instantiate this class outside of itself.
    /// </summary>
    private RequestsPerSecondCollector()
    {
        LastRequestTicks = System.Environment.TickCount;

        // Start things up by making the first request.
        GetRequestsPerSecond();
    }
    #endregion

    #region Getter for current requests per second measure
    public static decimal GetRequestsPerSecond()
    {
        if (pollingTimer == null)
        {
            Console.WriteLine("Starting Poll Timer");

            // Let's check the performance counter every 1 second, and don't do the first time until after 1 second.
            pollingTimer = new System.Threading.Timer(OnTimerCallback, null, 1000, 1000);

            // The first read from a performance counter is notoriously inaccurate, so 
            OnTimerCallback(null);
        }

        LastRequestTicks = System.Environment.TickCount;
        lock (threadLock)
        {
            return CurrentRequestsPerSecondValue;
        }
    }
    #endregion

    #region Polling Timer
    static void OnTimerCallback(object state)
    {
        if (System.Threading.Interlocked.CompareExchange(ref lockTimerCounter, 1, 0) == 0)
        {
            if (pcReqsPerSec == null)
                pcReqsPerSec = new System.Diagnostics.PerformanceCounter("W3SVC_W3WP", "Requests / Sec", "_Total", true);

            if (pcReqsPerSec != null)
            {
                try
                {
                    lock (threadLock)
                    {
                        CurrentRequestsPerSecondValue = Convert.ToDecimal(pcReqsPerSec.NextValue().ToString("N2"));
                    }
                }
                catch (Exception) {
                    // We had problem, just get rid of the performance counter and we'll rebuild it next revision
                    if (pcReqsPerSec != null)
                    {
                        pcReqsPerSec.Close();
                        pcReqsPerSec.Dispose();
                        pcReqsPerSec = null;
                    }
                }
            }

            stateCounter++;

            //Check every 5 seconds or so if anybody is still monitoring the server PerformanceCounter, if not shut down our PerformanceCounter
            if (stateCounter % 5 == 0)
            {
                if (System.Environment.TickCount - LastRequestTicks > 5000)
                {
                    Console.WriteLine("Stopping Poll Timer");

                    pollingTimer.Dispose();
                    pollingTimer = null;

                    if (pcReqsPerSec != null)
                    {
                        pcReqsPerSec.Close();
                        pcReqsPerSec.Dispose();
                        pcReqsPerSec = null;
                    }
                }                                                      
            }

            System.Threading.Interlocked.Add(ref lockTimerCounter, -1);
        }
    }
    #endregion
}

现在好了解一下。

  1. 首先你会注意到这个类被设计成一个静态单例。 你不能加载它的多个副本,它有一个私有的构造函数 并且急切地初始化了自身的内部实例。这使得 确保您不会意外地创建相同的多个副本 PerformanceCounter
  2. 接下来你会注意到私有构造函数(这只会运行 一旦第一次访问该类时,我们创建了两个 PerformanceCounter和一个用于轮询的计时器 PerformanceCounter
  3. Timer的回调方法将创建PerformanceCounter if 需要并获得其下一个值。也是每5次迭代 我们要看看自你上次请求以来已经有多长时间了 PerformanceCounter的价值。如果它超过5秒,我们会 暂时关闭轮询计时器。我们可以 如果我们再次需要,请务必稍后重新开始。
  4. 现在我们有一个名为GetRequestsPerSecond()的静态方法 call将返回RequestsPerSecond的当前值 PerformanceCounter
  5. 此实现的好处是,您只需创建一次性能计数器,然后继续使用,直到完成它为止。它易于使用,因为您可以从任何需要的地方(.aspx,.asmx,控制台应用程序,winforms应用程序等)简单地调用RequestsPerSecondCollector.GetRequestsPerSecond()。总是只有一个PerformanceCounter,无论您拨打RequestsPerSecondCollector.GetRequestsPerSecond()的速度有多快,它总是每秒轮询1次。如果您在超过5秒内未请求其值,它还将自动关闭并处置PerformanceCounter。当然,您可以调整计时器间隔和超时毫秒以满足您的需要。你可以更快地轮询并在60秒而不是5秒内超时。我选择5秒,因为它证明它在visual studio中调试时工作得非常快。一旦你测试并知道它有效,你可能需要更长的超时。

    希望这不仅可以帮助您更好地使用PerformanceCounters,而且还可以安全地重用此类,这个类与您想要显示统计信息的内容是分开的。可重用代码始终是一个加号!

    编辑:作为一个后续问题,如果您希望在此性能计数器运行时每60秒执行一次清理或保姆任务,该怎么办?好吧,我们已经有每1秒运行一次的计时器和一个跟踪我们的循环迭代的变量,称为stateCounter,它在每个计时器回调时递增。所以你可以添加一些像这样的代码:

    // Every 60 seconds I want to close/dispose my PerformanceCounter
    if (stateCounter % 60 == 0)
    {
        if (pcReqsPerSec != null)
        {
            pcReqsPerSec.Close();
            pcReqsPerSec.Dispose();
            pcReqsPerSec = null;
        }
    }
    

    我应该指出,示例中的这个性能计数器不应该“过时”。我认为'Request / Sec'应该是平均值,而不是移动平均值统计信息。但此示例只是说明了 的方式> 定期对PerformanceCounter进行任何类型的清理或“保姆”。在这种情况下,我们将关闭并处理性能计数器,这将导致在下一次计时器回调时重新创建它。你可以根据你的用例和你使用的具体PerformanceCounter修改它。大多数读这个问题/答案的人不应该这样做。检查你想要的PerformanceCounter的文档,看看它是连续计数,平均,移动平均线等......并适当调整您的实施。

答案 1 :(得分:3)

我不知道,如果这通过了你......我读过文章PerformanceCounter.NextValue Method

有评论:

// If the category does not exist, create the category and exit.
// Performance counters should not be created and immediately used.
// There is a latency time to enable the counters, they should be created
// prior to executing the application that uses the counters.
// Execute this sample a second time to use the category.

所以,我有一个问题,可以导致回答:是不是过早地调用了RequestsPerSecond方法? 此外,我建议您尝试检查类别是否不存在并在某处记录信息,以便我们分析它并确定我们拥有的条件以及发生的频率。

答案 2 :(得分:3)

我刚用:

解决了这种类型的错误或异常

使用,

new PerformanceCounter("Processor Information", "% Processor Time", "_Total");

而不是,

new PerformanceCounter("Processor", "% Processor Time", "_Total");

答案 3 :(得分:1)

我在IIS上使用类似于以下代码检索每秒请求时遇到问题

var pc = new PerformanceCounter();
pc.CategoryName = @"W3SVC_W3WP";
pc.InstanceName = @"_Total";
pc.CounterName = @"Requests / Sec";
Console.WriteLine(pc.NextValue());

这有时会抛出InvalidOperationException,我可以通过重新启动IIS来重现异常。如果我使用非预热的IIS运行,例如在笔记本电脑重启或IIS重启后,我得到了这个例外。首先点击网站,事先发出任何http请求,然后等待一两秒,我就不会得到例外。这有点像性能计数器被缓存,当空闲时它们被转储,并需要一段时间来重新缓存? (或类似的)。

Update1 :最初,当我手动浏览网站并进行热身时,它解决了问题。我已尝试以new WebClient().DownloadString(); Thread.Sleep()编程方式为服务器预热最多3000毫秒,但这不起作用?因此,我手动预热服务器的结果可能在某种程度上是误报。我在这里留下我的答案,因为它可能是原因,(即手动升温),也许其他人可以进一步阐述?

Update2 :好的,这是一些单元测试,总结了我昨天进行的进一步实验的一些经验教训。 (关于这个主题的谷歌没有太多关于btw。)

据我所知,以下陈述可能属实; (我提交下面的单元测试作为证据。)我可能误解了结果,所以请仔细检查;-D

  1. 创建一个性能计数器并在类别存在之前调用getValue,例如查询IIS计数器,而IIS很冷并且没有进程正在运行,将抛出InvalidOperation异常“类别不存在”。 (我认为这适用于所有计数器,而不仅仅是IIS。)

  2. 在Visual Studio单元测试中,一旦您的计数器抛出异常,如果您随后在第一个异常后预热服务器,并再次创建一个新的PerformanceCounter并进行查询,它仍会抛出异常! (这个是一个惊喜,我认为这是因为一些单身人士行动。我很抱歉我没有足够的时间来反编译来源以便在发布此回复之前进一步调查。)

  3. 在上面的2中,如果用[STAThread]标记单元测试,那么我可以在一个失败之后创建一个新的PerformanceCounter。 (这可能与Performance计数器可能是单例有关吗?需要进一步测试。)

  4. 在创建计数器并使用它之前,我没有需要暂停,尽管在MSDN中有一些警告相同的代码文档,除了在调用NextValue()之前创建性能计数器本身所需的时间。在我的情况下,为了使计数器热身并使“类别”存在,我是在IIS的弓上射了一枪,即发出一个GET请求,而中提琴,不再得到“InvalidOperationException”,这似乎是一个对我来说可靠的解决方案,现在。至少在查询IIS性能计数器时。

  5. <强> CreatingPerformanceCounterBeforeWarmingUpServerThrowsException

    [Test, Ignore("Run manually AFTER restarting IIS with 'iisreset' at cmd prompt.")]
    public void CreatingPerformanceCounterBeforeWarmingUpServerThrowsException()
    {
        Console.WriteLine("Given a webserver that is cold");
        Console.WriteLine("When I create a performance counter and read next value");
        using (var pc1 = new PerformanceCounter())
        {
            pc1.CategoryName = @"W3SVC_W3WP";
            pc1.InstanceName = @"_Total";
            pc1.CounterName = @"Requests / Sec";
            Action action1 = () => pc1.NextValue();
            Console.WriteLine("Then InvalidOperationException will be thrown");
            action1.ShouldThrow<InvalidOperationException>();                
        }
    }
    
    
    [Test, Ignore("Run manually AFTER restarting IIS with 'iisreset' at cmd prompt.")]
    public void CreatingPerformanceCounterAfterWarmingUpServerDoesNotThrowException()
    {
        Console.WriteLine("Given a webserver that has been Warmed up");
        using (var client = new WebClient())
        {
            client.DownloadString("http://localhost:8082/small1.json");
        }
        Console.WriteLine("When I create a performance counter and read next value");
        using (var pc2 = new PerformanceCounter())
        {
            pc2.CategoryName = @"W3SVC_W3WP";
            pc2.InstanceName = @"_Total";
            pc2.CounterName = @"Requests / Sec";
            float? result = null;
            Action action2 = () => result = pc2.NextValue();
            Console.WriteLine("Then InvalidOperationException will not be thrown");
            action2.ShouldNotThrow();
            Console.WriteLine("And the counter value will be returned");
            result.HasValue.Should().BeTrue();
        }
    }
    

答案 4 :(得分:0)

出于好奇,您在Visual Studio中为属性设置了什么?在VS中,转到项目属性,构建,平台目标并将其更改为AnyCPU。我之前看到过,当性能计数器设置为x86时,并不总是检索它,并将其更改为AnyCPU可能会修复它。