sql >> Database >  >> NoSQL >> MongoDB

System.TimeoutException:er is een time-out opgetreden na 30000 ms bij het selecteren van een server met CompositeServerSelector

Dit is een heel lastig probleem in verband met de taakbibliotheek. Kortom, er zijn te veel taken gemaakt en gepland, zodat een van de taken waar MongoDB's stuurprogramma op wacht, niet kan worden voltooid. Ik heb er lang over gedaan om te beseffen dat het geen impasse is, hoewel het er wel op lijkt.

Hier is de stap om te reproduceren:

  1. Download de broncode van de MongoDB's CSharp-stuurprogramma .
  2. Open die oplossing en maak een consoleproject binnenin en verwijs naar het driverproject.
  3. Maak in de hoofdfunctie een System.Threading.Timer die TestTask op tijd aanroept. Stel de timer in om één keer direct te starten. Voeg aan het einde een Console.Read() toe.
  4. Gebruik in de TestTask een for-lus om 300 taken te maken door Task.Factory.StartNew(DoOneThing) aan te roepen. Voeg al die taken toe aan een lijst en gebruik Task.WaitAll om te wachten tot ze allemaal zijn voltooid.
  5. Maak in de DoOneThing-functie een MongoClient en voer een eenvoudige query uit.
  6. Voer het nu uit.

Dit zal mislukken op dezelfde plaats die u noemde:MongoDB.Driver.Core.Clusters.Cluster.WaitForDescriptionChangedHelper.HandleCompletedTask(Task completedTask)

Als u enkele breekpunten plaatst, weet u dat de WaitForDescriptionChangedHelper een time-outtaak heeft gemaakt. Vervolgens wacht het totdat een van de DescriptionUpdate-taken of de time-outtaak is voltooid. De BeschrijvingUpdate gebeurt echter nooit, maar waarom?

Nu, terug naar mijn voorbeeld, er is één interessant onderdeel:ik heb een timer gestart. Als u de TestTask rechtstreeks aanroept, werkt deze probleemloos. Door ze te vergelijken met het venster Taken van Visual Studio, zult u merken dat de timerversie veel meer taken zal creëren dan de niet-timerversie. Ik zal dit onderdeel wat later uitleggen. Er is nog een belangrijk verschil. U moet foutopsporingsregels toevoegen in de Cluster.cs :

    protected void UpdateClusterDescription(ClusterDescription newClusterDescription)
    {
        ClusterDescription oldClusterDescription = null;
        TaskCompletionSource<bool> oldDescriptionChangedTaskCompletionSource = null;

        Console.WriteLine($"Before UpdateClusterDescription {_descriptionChangedTaskCompletionSource?.Task.Id}, {_descriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");
        lock (_descriptionLock)
        {
            oldClusterDescription = _description;
            _description = newClusterDescription;

            oldDescriptionChangedTaskCompletionSource = _descriptionChangedTaskCompletionSource;
            _descriptionChangedTaskCompletionSource = new TaskCompletionSource<bool>();
        }

        OnDescriptionChanged(oldClusterDescription, newClusterDescription);
        Console.WriteLine($"Setting UpdateClusterDescription {oldDescriptionChangedTaskCompletionSource?.Task.Id}, {oldDescriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");
        oldDescriptionChangedTaskCompletionSource.TrySetResult(true);
        Console.WriteLine($"Set UpdateClusterDescription {oldDescriptionChangedTaskCompletionSource?.Task.Id}, {oldDescriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");
    }

    private void WaitForDescriptionChanged(IServerSelector selector, ClusterDescription description, Task descriptionChangedTask, TimeSpan timeout, CancellationToken cancellationToken)
    {
        using (var helper = new WaitForDescriptionChangedHelper(this, selector, description, descriptionChangedTask, timeout, cancellationToken))
        {
            Console.WriteLine($"Waiting {descriptionChangedTask?.Id}, {descriptionChangedTask?.GetHashCode().ToString("F8")}");
            var index = Task.WaitAny(helper.Tasks);
            helper.HandleCompletedTask(helper.Tasks[index]);
        }
    }

Door deze regels toe te voegen, kom je er ook achter dat de niet-timerversie twee keer wordt bijgewerkt, maar de timerversie slechts één keer. En de tweede komt van de "MonitorServerAsync" in ServerMonitor.cs. Het bleek dat in de timerversie de MontiorServerAsync werd uitgevoerd maar nadat deze helemaal door ServerMonitor.HeartbeatAsync, BinaryConnection.OpenAsync, BinaryConnection.OpenHelperAsync en TcpStreamFactory.CreateStreamAsync kwam, bereikte het uiteindelijk TcpResolveamFactory. Het slechte gebeurt hier:Dns.GetHostAddressesAsync . Deze wordt nooit uitgevoerd. Als je de code iets aanpast en dat verandert in:

    var task = Dns.GetHostAddressesAsync(dnsInitial.Host).ConfigureAwait(false);

    return (await task)
        .Select(x => new IPEndPoint(x, dnsInitial.Port))
        .OrderBy(x => x, new PreferredAddressFamilyComparer(preferred))
        .ToArray();

U kunt de taak-ID vinden. Door in het venster Taken van Visual Studio te kijken, is het vrij duidelijk dat er ongeveer 300 taken voor staan. Slechts enkele van hen worden uitgevoerd maar geblokkeerd. Als je een Console.Writeline toevoegt aan de DoOneThing-functie, zul je zien dat de taakplanner er meerdere bijna tegelijkertijd start, maar dan vertraagt ​​het tot ongeveer één per seconde. Dit betekent dus dat u ongeveer 300 seconden moet wachten voordat de taak om de dns op te lossen begint te lopen. Daarom overschrijdt het de time-out van 30 seconden.

Nu, hier komt een snelle oplossing als je geen gekke dingen doet:

Task.Factory.StartNew(DoOneThing, TaskCreationOptions.LongRunning);

Dit dwingt de ThreadPoolScheduler om onmiddellijk een thread te starten in plaats van een seconde te wachten voordat een nieuwe wordt gemaakt.

Dit zal echter niet werken als je echt gekke dingen doet zoals ik. Laten we de for-lus veranderen van 300 naar 30000, zelfs deze oplossing kan ook mislukken. De reden is dat het te veel threads creëert. Dit kost middelen en tijd. En het zou kunnen beginnen met het GC-proces. Alles bij elkaar is het misschien niet mogelijk om al die threads te maken voordat de tijd om is.

De perfecte manier is om te stoppen met het maken van veel taken en de standaardplanner te gebruiken om ze te plannen. U kunt proberen een werkitem te maken en het in een ConcurrentQueue te plaatsen en vervolgens verschillende threads maken als werkers om de items te consumeren.

Als u de oorspronkelijke structuur echter niet te veel wilt veranderen, kunt u het op de volgende manier proberen:

Maak een ThrottledTaskScheduler afgeleid van TaskScheduler.

  1. Deze ThrottledTaskScheduler accepteert een TaskScheduler als de onderliggende die de eigenlijke taak zal uitvoeren.
  2. Dump de taken naar de onderliggende planner, maar als deze de limiet overschrijdt, plaats deze dan in een wachtrij.
  3. Als een van de taken is voltooid, controleer dan de wachtrij en probeer ze binnen de limiet in de onderliggende planner te dumpen.
  4. Gebruik de volgende code om al die gekke nieuwe taken te starten:

·

var taskScheduler = new ThrottledTaskScheduler(
    TaskScheduler.Default,
    128,
    TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler,
    logger
    );
var taskFactory = new TaskFactory(taskScheduler);
for (var i = 0; i < 30000; i++)
{
    tasks.Add(taskFactory.StartNew(DoOneThing))
}
Task.WaitAll(tasks.ToArray());

U kunt System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ConcurrentExclusiveTaskScheduler als referentie gebruiken. Het is een beetje ingewikkelder dan degene die we nodig hebben. Het is voor een ander doel. Maak je dus geen zorgen over die onderdelen die heen en weer gaan met de functie binnen de klasse ConcurrentExclusiveSchedulerPair. U kunt het echter niet rechtstreeks gebruiken omdat het de TaskCreationOptions.LongRunning niet doorstaat wanneer het de inpaktaak maakt.

Het werkt voor mij. Veel succes!

P.S.:De reden voor het hebben van veel taken in de timerversie ligt waarschijnlijk in de TaskScheduler.TryExecuteTaskInline. Als het zich in de hoofdthread bevindt waar de ThreadPool is gemaakt, kan het sommige taken uitvoeren zonder ze in de wachtrij te plaatsen.




  1. mongo createIndex achtergrond blokkeert de shell

  2. Ophalen uit meerdere, afzonderlijke collecties met Express en MongoDB

  3. MongoDB en Mongoid in productie

  4. Beknopte referentiegids voor verschillende NoSQL-databases