什么是快速测试多个盒子的最佳方法

时间:2013-06-29 09:34:43

标签: c# monitoring ping

我有一个应用程序,我监视和控制一堆计算机(可能是3到35左右,可能是本地的)。

我监控的一件事是正常运行时间/ ping状态。应用程序的目的之一是重新启动框,有时它们会因其他原因重新启动。

我希望能够快速获取pingable / non-pingable更改。

我在一个线程上有一个旋转循环。

在我看来,阻止ping会阻止它更新一点,即使你并行运行它(防止一个盒子的ping阻塞另一个盒子)

(并行实现示例,请注意以下内容仅仅是我的头脑并没有实现,可能包含错误)

var startTime = DateTime.Now;
var period = TimeSpan.FromSeconds();
Parallel.ForEach(boxes, (box) => 
{
    var now = DateTime.Now;
    var remainingTime = (now - startTime) - period;
    while(remainingTime > TimeSpan.Zero)
    {
        box.CanPing.TryUpdate();
    }
});

其中TryUpdate就像

using(ping = new Ping())
{
    var reply = ping.Send (IP);
    bool upStatus = (reply.Status == IPStatus.Success);
    this.Value = upStatus;
}

或者我尝试使用多个SendAsync(一次多个异步ping)来尽快发现正常运行时间,并在SendAsync的回调中使用双重检查锁定

if(upStatus != this.Value)
{
    lock(_lock)//is it safe to have a non static readonly lock object, all the examples seem to use a static object but that wouldn't scale to  locking in multiple instances of the containing class object
    {
        if(upStatus != this.Value)
        {
            ...
        }
    }
}

这是一个可怕的内存泄漏,但这可能是因为我正在进行过多的异步ping调用(每个都带有一个线程)太快,而且没有处理ping。如果我一次将自己限制在每台计算机3个,或者在中间放置一个更长的停顿,并且Dispose()ping你认为这是个好主意吗?

什么是更好的策略?还有其他想法吗?

4 个答案:

答案 0 :(得分:8)

这是多线程的一个特例,你不需要更快地使程序更快,你需要使它更具响应性。您的操作几乎没有计算能力。因此,我不会害怕为每台受监视的计算机创建一个单独的线程。无论如何,他们大部分时间都会做sleep()。它们应该被创建一次,因为创建线程实际上是最昂贵的东西。

我会像这样创建对象层次结构:

  • GUIProxy - 会处理所有gui操作,例如更改coputer名称旁边的通知颜色
  • HostManager - 会注册新计算机,删除旧计算机,对Monitors执行计时检查
  • HostMonitor - 会定期发送ping以检查计算机。稍后会有更多关于它的行为

检查算法

在局域网中,大部分时间ping在发送后1-2毫秒内返回。在互联网上,时间可能会有所不同。 我会为每个Monitor分别设置两个ping时阈值,具体取决于计算机的位置。当LAN ping大于5ms或Internet ping>时,一个是“警告”阈值(GUI中的黄灯或sth)。 200毫秒。第二个是“错误”阈值,LAN> 1s,互联网> 2s或sth。 每个Monitor都会发送ping,等待答案,并在收到答案后发送另一个ping。它应该存储lastPingSendTimelastPingReceiveTimecurrentPingSendTime。前者用于确定延迟,后者用于检查HostManager中的延迟。当然Monitor应该正确处理超时和其他系统/网络事件。

HostManager中,也在单个线程上运行,我会检查每个监视器上的currentPingSendTime并根据监视器的阈值进行检查。如果超过阈值,将通知GUIProxy以显示GUI中的情况。

优点

  • 你自己控制线程
  • 您可以使用synchronous(simpler) ping method
  • Manager不会挂起,因为它会异步访问监视器
  • 您可以实现一个抽象的Monitor界面,您可以使用它来监视其他内容,而不仅仅是计算机

缺点

  • 纠正Monitor线程实现可能不简单

答案 1 :(得分:2)

根据您是否需要扩展解决方案,您可以像Dariusz所说的那样实施状态检查(这是绝对合法的方法)。

此方法只有一个缺点,可能与您的场景相关或不相关: 扩展到hundrets甚至数千个受监控的盒子或服务将导致大量的线程。关于64位模式的.net应用程序的事实支持数千个并发线程我不会建议产生那么多工作者。如果你给他的工作安排如此大量的工人,资源调度程序将不再是你最好的朋友。

为了获得具有横向扩展能力的解决方案,这有点困难。 让我们回到最初的问题:你想快速监视一堆盒子,流水线处理效果不佳。 请记住,您可能在将来监视其他服务(tcp)并等待超时会完全杀死此方法。

解决方案:自定义线程池或线程重用

当您处理特殊类型的线程时,该线程受到从默认线程池生成线程的时间的影响,需要一个解决方案来摆脱产生问题。考虑到能够扩展我会建议这样:

使用自定义或默认线程池生成多个处于挂起状态的线程。 现在你是系统想要测量几个盒子。因此:找到预热的线程并获取第一个暂停/免费的线程,并为监控工作保留它。 在获得使用的线程后,您可以为他的实际工作方法(将由线程异步调用)提供一些句柄。 监视迭代完成后(可能需要一些时间)线程返回结果(好的方法是回调)并将自己设置为挂起模式。

所以这只是一个带有预热线程的自定义调度程序。如果您使用ManualResetEvents构建暂停/恢复,则几乎可以立即获得线程。

还想要更高的效果吗?

如果你仍然获得更多的性能,并希望能够以更细粒度的方式调整你的结果系统,我会推荐专门的线程池(比如zabbix用于监控)。 因此,您不仅要分配一堆线程,这些线程可以调用自定义方法来检查是否可以通过ping或tcp访问某个框,您可以为每个Monitoring类型分配一个单独的池。 因此,在icmp(ping)和tcp Monitoring的情况下,您将创建至少两个线程池,其中线程已包含关于“如何检查”的基本知识。 在ping监视器的情况下,线程将准备好并等待初始化的ping实例,该实例只是等待目标检查。 当您从挂起状态获取线程时,它会立即检查主机并返回结果。然后它准备睡眠(在这种情况下,已经为下一次运行初始化环境)。 如果您以一种好的方式实现这一点,您甚至可以重用套接字等资源。

总而言之,这种方法可以让您监控3,35甚至是箱子的背景而不会陷入麻烦。当然,监控仍然有限,你不应该分叉成千上万的预热线程。这不是背后的想法:这个想法是你已经定义了可以使用的最大线程数,只是等待到目的地检查。在为多个主机启动监控时,您不必处理分叉问题 - 如果监控的次数超过定义的并发允许,则只需处理排队(这可能比默认情况下的Parallel.ForEach高得多) spawns每个核心最多一个线程!检查方法的重载以增加此数量。)

绝对优化

如果您仍然愿意改进系统,那么让您的调度程序和资源规划器不仅仅是预先计划的线程。给他限制,如最小4,最多42个线程。 Scheduler会考虑启动和停止这些边框内的其他线程。如果您的系统在夜间降低监控率并且您不希望挂起的线程挂起,则此功能非常有用。

这将是A +实现,因为您不仅能够立即为至少一些主机立即从冷状态启动监控,而且很快就会为许多主机启动监控 - 您还可以长时间回馈您真正不需要的资源。

答案 2 :(得分:0)

由于这似乎是应用程序的一项专用任务,我同意自己管理用于特定任务的线程数可能是有意义的。

此外,您的流程似乎有多个阶段:

  1. 提供下一个要检查的地址
  2. 确定检查时使用的超时时间。使用超时可能取决于几个因素,包括地址是否被确定为在之前的检查中没有响应,响应时间通常是什么,以及Dariusz提到的,如果它在LAN,外联网,互联网上,...
  3. 执行ping
  4. 处理和解释ping回复vs.s.先前的回复状态和累积状态(例如,更新地址的统计信息,甚至可能存储此信息)。
  5. 对(重复)无反应发出“警报”
  6. 发出重启命令。
  7. 因此,如果您可以使用前一阶段生成的输出清楚地执行可以独立执行的阶段,您可以选择SEDA(分阶段事件驱动架构)类型的解决方案,您可以在其中分配多个专用线程每个阶段。并且阶段可以使用提供者/生产者/消费者角色相互连接,用于流经阶段的特定信息项,其中有ProducerConsumerQueues来吸收临时不匹配(偷看负载)和自动限制(例如,太多未决的ping请求将会阻止ping请求的生成者,直到执行ping的消费者已经充分赶上)。

    对于“Ping流程”的基本结构,您可能会有以下几个阶段:

    1. “PingRequest”生产者阶段,由IP地址提供者提供,并使用工厂创建请求(因此工厂可以根据历史记录和IPAddress的上次已知状态确定请求的超时) 。它将请求传递给“PingRequests”的连接使用者。
    2. “Pinger”阶段,从其使用者队列中检索PingRequests,执行Ping并将结果传递给“PingResults”的连接使用者
    3. “ResultProcessor”阶段,从其使用者队列中检索PingResults,更新IPAddress的状态,并将结果传递给连接的“PingStatus”使用者。
    4. 在第3阶段之后,您可能希望以相同的方式生成其他阶段,以生成警报,重新启动请求等。

      可以为这些阶段中的每一个分配专用的线程数,并且可以非常灵活地更改流程。

      一些代码示例来说明:

      /// <summary>
      /// Coordinates and wires up the processing pipeline.
      /// </summary>
      public class PingModule : IConsumer<PingStatus>
      {
          private readonly ConcurrentDictionary<IPAddress, PingStatus> _status = new ConcurrentDictionary<IPAddress,PingStatus>();
          private readonly CancellationTokenSource _cancelTokenSource;
          private readonly PingRequestProducerWorkStage _requestProducer;
          private readonly PingWorkStage _pinger;
          private readonly PingReplyProcessingWorkStage _replyProcessor;
      
          public PingModule(IProvider<IPAddress> addressProvider)
          {
              _cancelTokenSource = new CancellationTokenSource();
      
              _requestProducer = new PingRequestProducerWorkStage(1, addressProvider, NextRequestFor, _cancelTokenSource.Token);
              _pinger = new PingWorkStage(4, 10 * 2, _cancelTokenSource.Token);
              _replyProcessor = new PingReplyProcessingWorkStage(2, 10 * 2, _cancelTokenSource.Token);
      
              // connect the pipeline.
              _requestProducer.ConnectTo(_pinger);
              _pinger.ConnectTo(_replyProcessor);
              _replyProcessor.ConnectTo(this);
          }
      
          private PingRequest NextRequestFor(IPAddress address)
          {
              PingStatus curStatus;
              if (!_status.TryGetValue(address, out curStatus))
                  return new PingRequest(address, IPStatus.Success, TimeSpan.FromMilliseconds(120));
              if (curStatus.LastResult.TimedOut)
              {
                  var newTimeOut = TimeSpan.FromTicks(curStatus.LastResult.TimedOutAfter.Ticks * 2);
                  return new PingRequest(address, IPStatus.TimedOut, newTimeOut);
              }
              else
              {
                  var newTimeOut = TimeSpan.FromTicks(curStatus.AverageRoundtripTime + 4 * curStatus.RoundTripStandardDeviation);
                  return new PingRequest(address, IPStatus.Success, newTimeOut);
              }
          }
          // ...
      }
      

      现在可以轻松修改此管道。例如,您可能决定要有2个或3个并行“Pinger”阶段流,其中一个服务于之前断开连接的地址,一个服务于“慢响应者”,另一个服务于其余部分。这可以通过将阶段1连接到执行此路由的使用者,并将PingRequest传递给正确的“Pinger”来实现。

      public class RequestRouter : IConsumer<PingRequest>
      {
          private readonly Func<PingRequest, IConsumer<PingRequest>> _selector;
      
          public RequestRouter(Func<PingRequest, IConsumer<PingRequest>> selector)
          {
              this._selector = selector;
          }
          public void Consume(PingRequest work)
          {
              _selector(work).Consume(work);
          }
          public void Consume(PingRequest work, CancellationToken cancelToken)
          {
              _selector(work).Consume(work, cancelToken);
          }
      }
      
      public class PingModule : IConsumer<PingStatus>
      {
          // ...
          public PingModule(IProvider<IPAddress> addressProvider)
          {
              _cancelTokenSource = new CancellationTokenSource();
      
              _requestProducer = new PingRequestProducerWorkStage(1, addressProvider, NextRequestFor, _cancelTokenSource.Token);
              _disconnectedPinger = new PingWorkStage(2, 10 * 2, _cancelTokenSource.Token);
              _slowAddressesPinger = new PingWorkStage(2, 10 * 2, _cancelTokenSource.Token);
              _normalPinger = new PingWorkStage(3, 10 * 2, _cancelTokenSource.Token);
              _requestRouter = new RequestRouter(RoutePingRequest);
              _replyProcessor = new PingReplyProcessingWorkStage(2, 10 * 2, _cancelTokenSource.Token);
      
              // connect the pipeline
              _requestProducer.ConnectTo(_requestRouter);
              _disconnectedPinger.ConnectTo(_replyProcessor);
              _slowAddressesPinger.ConnectTo(_replyProcessor);
              _normalPinger.ConnectTo(_replyProcessor);
              _replyProcessor.ConnectTo(this);
          }
          private IConsumer<PingRequest> RoutePingRequest(PingRequest request)
          {
              if (request.LastKnownStatus != IPStatus.Success)
                  return _disconnectedPinger;
              if (request.PingTimeOut > TimeSpan.FromMilliseconds(500))
                  return _slowAddressesPinger;
              return _normalPinger;
          }
          // ...
      } 
      

答案 3 :(得分:0)

我知道这是编码问题的终点,但您是否考虑过使用NagiOS或冒烟或其他开源监控解决方案?这些可以快速检测到连接中的丢失,并且可能还有许多其他功能,您可能不想自己挖掘它们。