快速测试多个ping框的最佳方法是什么

本文关键字:最佳 方法 是什么 ping 测试 | 更新日期: 2023-09-27 18:20:26

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

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

我希望能够快速获取可Ping/不可Ping的更改。

我在一根线上绕了一圈。

在我看来,阻止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个,或者在中间暂停更长的时间,并处理()ping,你认为这是个好主意吗?

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

快速测试多个ping框的最佳方法是什么

这是多线程的一个特殊情况,在这种情况下,您不需要线程来使程序更快,您需要使它更具响应性。您的操作几乎不需要计算能力。因此,我不会害怕为每台被监控的计算机创建一个线程。无论如何,他们大部分时间都在做sleep()。它们应该创建一次,因为线程创建实际上是这里最昂贵的事情。

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

  • GUIProxy-将处理所有的gui操作,比如更改coputer名称旁边的通知颜色
  • HostManager-将注册新机器,删除旧机器,对Monitors执行定时检查
  • HostMonitor-将周期性地、顺序地向检查计算机发送ping。稍后会详细了解它的行为

检查算法

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

在同样在单个线程上运行的HostManager中,我会检查每个监视器上的currentPingSendTime,并将其与该监视器的阈值进行比较。如果超过阈值,则会通知GUIProxy在GUI中显示情况。

优点

  • 你自己控制线程
  • 可以使用同步(更简单)的ping方法
  • Manager不会挂起,因为它异步访问监视器
  • 你可以实现一个抽象的监视器接口,可以用来监视其他东西,而不仅仅是计算机

的缺点

  • 正确的Monitor线程实现可能并不简单

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

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

为了得到一个可扩展的解决方案,这有点困难。让我们简单地回到最初的问题:您想要快速监控一堆盒子,而流水线处理执行不好。考虑到您将来可能会监视其他服务(tcp)也在等待超时,这将完全扼杀这种方法。

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

当你处理一种特殊类型的线程时,它会受到从默认线程池派生线程的时间的影响,需要一个解决方案来解决派生问题。考虑到能够扩大规模,我建议这样做:

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

因此,这只是一个带有预热线程的自定义调度器。如果您使用ManualResetEvents构建挂起/恢复,线程几乎立即可用。

还想要更多性能吗

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

总之,这种方法使您能够监控3、35甚至数百个盒子,而不会遇到麻烦。当然,监控仍然是有限的,你不应该分叉数千个预热线程。这并不是背后的想法:这个想法是,您已经定义了可供使用的线程的最大数量,这些线程只是等待检查目的地。在为多个主机启动监控时,您不必处理分叉问题-如果您的监控超出了您定义的并发允许范围,您只需要处理排队问题(这可能远高于Parallel.ForEach,默认情况下每个内核最多产生一个线程!检查方法的过载以增加这个数量。)

绝对优化

如果你仍然愿意进一步改进系统,请获得你的调度器和资源规划器,而不仅仅是预热线程的计数。给他限制,比如最少4个,最多42个线程。调度器会考虑在这些边界内启动和停止其他线程。如果您的系统在夜间降低了监控速率,并且您不希望挂起的线程挂起,这将非常有用。

这将是A+实施,因为您不仅能够立即从冷状态开始监视至少某些主机,而且能够快速地从冷状态启动监视许多主机,您还可以返还长期不需要的资源。

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

此外,在您的流程中似乎有许多阶段:

  1. 提供下一个要检查的地址
  2. 正在确定检查时要使用的超时。使用超时可能取决于几个因素,包括地址在上次检查中是否被确定为无响应,它的响应时间通常是多少,以及,正如Dariusz所提到的,如果它在LAN、extranet、internet上
  3. 执行ping
  4. 处理和解释ping回复与之前的回复状态和累积状态(例如,更新地址的统计信息,甚至可能存储这些信息)
  5. 对(重复)无响应发出"警报"
  6. 正在发出重新启动命令

因此,如果您使用前一阶段产生的输出清除了可以独立执行的阶段,您可以选择SEDA(阶段事件驱动架构)类型的解决方案,在该解决方案中,您可以为每个阶段分配多个专用线程。并且对于流经各阶段的特定信息项目,可以使用提供者/生产者/消费者角色来将各阶段彼此连接,其中存在ProducerConsumerQueues来吸收临时不匹配(peek负载)和自动节流(例如,对ping的太多未决请求将阻塞ping请求的生产者,直到执行ping的消费者已经充分赶上为止)。

对于"Ping流"的基本结构,您可能有以下阶段:

  1. "PingRequest"生产者阶段,由IPAddresses的提供者提供,并使用工厂创建请求(因此工厂可以根据IPAddress的历史记录和上次已知状态来确定请求的超时)。它将请求传递给"PingRequests"的连接使用者
  2. "Pinger"阶段从其使用者队列中检索PingRequest,执行Ping并将结果传递给"PingResults"的连接使用者
  3. "ResultProcessor"阶段从其使用者队列中检索PingResults,更新IPAddress的状态,并将结果传递给"PingStatus"的连接使用者

在阶段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传递到正确的"Piner"来实现。

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;
    }
    // ...
} 

我知道这是编码问题的结束,但你是否考虑过使用NagiOS或smokping或其他开源监控解决方案?这些功能可以快速检测连接的下降,而且可能还有许多其他你可能不想自己使用的功能。