【问题标题】:Detecting/Diagnosing Thread Starvation检测/诊断线程饥饿
【发布时间】:2017-12-15 15:20:46
【问题描述】:

我正在对一个 IIS 应用程序进行一些性能/可扩展性测试,该应用程序有时似乎会在生产环境中慢下来。我能够使用 NUnit 始终如一地重现缓慢。

CPU 和内存在测试期间或生产中出现缓慢时不会出现峰值。我强烈怀疑应用程序正在遭受线程饥饿,因为它似乎不是导致瓶颈的 CPU、内存、I/O 或数据库访问。我确实看到了出现线程饥饿的迹象;例如,NLog 的异步日志文件写入往往有很长一段时间的静默,然后是带有较旧时间戳的活动突发(即较低优先级的线程正在等待线程释放以便写入)。

我可以采取哪些步骤来明确确定应用程序确实是线程匮乏的,并且(假设是这种情况)查明导致问题的系统的确切区域?

编辑

我没有提到几乎所有代码都是同步的(这是一个遗留系统)。

【问题讨论】:

  • 您是否一次启动许多长时间运行的任务而不指定TaskCreationOptions.LongRunning?如果你这样做了,那么任何新的本应短活的任务在启动时都会遇到 巨大 延迟,因为默认情况下,调度程序会等待大约 500 毫秒,然后再为其分配新线程。可以使用ThreadPool.SetMinThreads 进行修复,但最好使用长时间运行的选项(因为您可能会遇到切换性能下降的情况)。
  • 有趣。这也适用于同步代码吗?
  • @Sinatr 拥有长时间运行的任务是您确保不会出现线程饥饿的方法。使用LongRunning 创建大量任务是您创建线程饥饿的方式。
  • @Servy,我说的不是线程饥饿,只是关于“慢下来”的一些想法。
  • @Sinatr:你的 cmets 让我研究了 SetMinThreads,这让我想到了this 问题及其答案(其中一个是你的)。在启动时将 minThreads 设置为更高的数字会显着提高性能。

标签: c# .net iis threadpool starvation


【解决方案1】:

我可以采取哪些步骤来明确确定应用程序确实是线程匮乏的

在 Windows 操作系统中,进程和线程数仅受创建它们所需的资源的限制。 There is no fixed number that cannot be exceeded。线程饥饿可能会以这种方式发生,但这应该可以通过缺乏底层资源来检测,例如高 CPU 或低 RAM。

IIS 具有由registry keys 控制的固定限制。

如果超出这些限制,请求将排队。在线程饥饿的情况下,您会期望看到队列长度增加。这个post 显示了可以监控的队列性能计数器,看看是否是这种情况。

【讨论】:

    【解决方案2】:

    我是根据上面的来想出这个的

    using System;
    using System.Threading;
    using System.Timers;
    using log4net;
    
    using Timer = System.Timers.Timer;
    
    namespace somewhere
    {
        public class ThreadStatsLogger : IDisposable
        {
            private const int DEPLETION_WARN_LEVEL = 10;
            private const int HISTERESIS_LEVEL = 10;
    
        private const double SAMPLE_RATE_MILLISECONDS = 500;
        private bool _workerThreadWarned = false;
        private bool _ioThreadWarned = false;
        private bool _minWorkerThreadLevelWarned = false;
        private bool _minIoThreadLevelWarned = false;
    
        private readonly int _maxWorkerThreadLevel;
        private readonly int _maxIoThreadLevel;
        private readonly int _minWorkerThreadLevel;
        private readonly int _minWorkerThreadLevelRecovery;
        private readonly int _minIoThreadLevel;
        private readonly int _minIoThreadLevelRecovery;
        private Timer _timer;
    
        private static readonly ILog _logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
    
        public ThreadStatsLogger()
        {
    
            _timer = new Timer
            {
                AutoReset = true,
                Interval = SAMPLE_RATE_MILLISECONDS,
            };
    
            _timer.Elapsed += TimerElasped;
            _timer.Start();
            ThreadPool.GetMinThreads(out _minWorkerThreadLevel, out _minIoThreadLevel);
            ThreadPool.GetMaxThreads(out _maxWorkerThreadLevel, out _maxIoThreadLevel);
            ThreadPool.GetAvailableThreads(out int workerAvailable, out int ioAvailable);
    
            _logger.InfoFormat("Thread statistics at startup: minimum worker:{0} io:{1}", _minWorkerThreadLevel, _minIoThreadLevel );
            _logger.InfoFormat("Thread statistics at startup: maximum worker:{0} io:{1}", _maxWorkerThreadLevel, _maxIoThreadLevel);
            _logger.InfoFormat("Thread statistics at startup: available worker:{0} io:{1}", workerAvailable, ioAvailable);
    
            _minWorkerThreadLevelRecovery = (_minWorkerThreadLevel * 3) / 4;
            _minIoThreadLevelRecovery = (_minIoThreadLevel * 3) / 4;
            if (_minWorkerThreadLevelRecovery == _minWorkerThreadLevel) _minWorkerThreadLevelRecovery = _minWorkerThreadLevel - 1;
            if (_minIoThreadLevelRecovery == _minIoThreadLevel) _minIoThreadLevelRecovery = _minIoThreadLevel - 1;
        }
    
        private void TimerElasped(object sender, ElapsedEventArgs e)
        {
    
            ThreadPool.GetAvailableThreads(out int availableWorkerThreads, out int availableIoThreads);
    
            var activeWorkerThreads = _maxWorkerThreadLevel - availableWorkerThreads;
            var activeIoThreads = _maxIoThreadLevel - availableIoThreads;
    
            _logger.InfoFormat("Thread statistics: active worker:{0} io:{1}", activeWorkerThreads, activeIoThreads);
    
            if (activeWorkerThreads > _minWorkerThreadLevel && !_minWorkerThreadLevelWarned)
            {
                _logger.InfoFormat("Thread statistics WARN active worker threads above minimum {0}:{1}", activeWorkerThreads, _minWorkerThreadLevel);
                _minWorkerThreadLevelWarned = !_minWorkerThreadLevelWarned;
            }
            if (activeWorkerThreads < _minWorkerThreadLevelRecovery && _minWorkerThreadLevelWarned)
            {
                _logger.InfoFormat("Thread statistics RECOVERY active worker threads below minimum {0}:{1}", activeWorkerThreads, _minWorkerThreadLevel);
                _minWorkerThreadLevelWarned = !_minWorkerThreadLevelWarned;
            }
    
            if (activeIoThreads > _minIoThreadLevel && !_minIoThreadLevelWarned)
            {
                _logger.InfoFormat("Thread statistics WARN active io threads above minimum {0}:{1}", activeIoThreads, _minIoThreadLevel);
                _minIoThreadLevelWarned = !_minIoThreadLevelWarned;
            }
            if (activeIoThreads < _minIoThreadLevelRecovery && _minIoThreadLevelWarned)
            {
                _logger.InfoFormat("Thread statistics RECOVERY active io threads below minimum {0}:{1}", activeIoThreads, _minIoThreadLevel);
                _minIoThreadLevelWarned = !_minIoThreadLevelWarned;
            }
    
            if (availableWorkerThreads < DEPLETION_WARN_LEVEL && !_workerThreadWarned)
            {
                _logger.InfoFormat("Thread statistics WARN available worker threads below warning level {0}:{1}", availableWorkerThreads, DEPLETION_WARN_LEVEL);
                _workerThreadWarned = !_workerThreadWarned;
            }
    
            if (availableWorkerThreads > (DEPLETION_WARN_LEVEL + HISTERESIS_LEVEL) && _workerThreadWarned)
            {
                _logger.InfoFormat("Thread statistics RECOVERY available worker thread recovery {0}:{1}", availableWorkerThreads, DEPLETION_WARN_LEVEL);
                _workerThreadWarned = !_workerThreadWarned;
            }
    
            if (availableIoThreads < DEPLETION_WARN_LEVEL && !_ioThreadWarned)
            {
                _logger.InfoFormat("Thread statistics WARN available io threads below warning level {0}:{1}", availableIoThreads, DEPLETION_WARN_LEVEL);
                _ioThreadWarned = !_ioThreadWarned;
            }
    
            if (availableIoThreads > (DEPLETION_WARN_LEVEL + HISTERESIS_LEVEL) && _ioThreadWarned)
            {
                _logger.InfoFormat("Thread statistics RECOVERY available io thread recovery {0}:{1}", availableIoThreads, DEPLETION_WARN_LEVEL);
                _ioThreadWarned = !_ioThreadWarned;
            }
        }
    
        public void Dispose()
        {
            if (_timer == null) return;
            _timer.Close();
            _timer.Dispose();
            _timer = null;
        }
    }
    

    }

    【讨论】:

      【解决方案3】:

      根据 Sinatr 的评论,我阅读了 ThreadPool.SetMinThreads 和 TaskCreationOptions.LongRunning,包括对 When to use TaskCreationOptions.LongRunning? 的回答

      将 MinThreads 设置为更高的默认值对我来说有很大的不同。我创建了一个简单的后台进程,以查看 ThreadPool 中的可用线程在测试运行过程中是否发生了显着变化并超过了 MinThreads 值(确实如此)。

      这是我用来诊断的一些代码。这不适合生产使用,这里显示的线程使用报告只会在它们最初增加时才有意义。另请注意,Timer 在经过时需要一个线程,因此还需要等待可用的线程。

      静态变量:

          private static Timer _timer;
          private static int _lastActiveThreads;
          private static int _lastAvailableThreads;
          private static int _maxThreads;
          private static int _minThreads;
      

      在启动时运行:

          int completionPortThreads;
      
          ThreadPool.GetMaxThreads(out _maxThreads, out completionPortThreads);
          ThreadPool.GetMinThreads(out _minThreads, out completionPortThreads);
      
          _timer = new Timer
          {
              AutoReset = true,
              Interval = 500,
          };
      
          _timer.Elapsed += TimerElasped;
          _timer.Start();
      

      经过的方法:

          private static void TimerElasped(object sender, ElapsedEventArgs e)
          {
              int minWorkerThreads;
              int availWorkerThreads;
              int completionPortThreads;
      
              ThreadPool.GetMinThreads(out minWorkerThreads, out completionPortThreads);
              ThreadPool.GetAvailableThreads(out availWorkerThreads, out completionPortThreads);
      
              var activeThreads = _maxThreads - availWorkerThreads;
      
              if (availWorkerThreads != _lastAvailableThreads)
              {
                  _lastAvailableThreads = availWorkerThreads;
                  if (activeThreads > _lastActiveThreads)
                  {
                      _lastActiveThreads = activeThreads;
                      Logger.Log($"+++++ Active Threads is now: {activeThreads}");
      
                      if (activeThreads > _minThreads)
                      {
                          var diff = activeThreads - _minThreads;
                          Logger.Log($"+++++ Active threads is now {activeThreads}, which is {diff} more than minThread value of {_minThreads}.  This may be causing delays.");
                      }
                  }
              }
          }
      

      【讨论】:

        猜你喜欢
        • 2012-07-26
        • 1970-01-01
        • 2016-06-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-06-25
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多