sql >> Database teknologi >  >> NoSQL >> MongoDB

System.TimeoutException:Der opstod en timeout efter 30.000 ms valg af en server ved hjælp af CompositeServerSelector

Dette er et meget vanskeligt problem relateret til opgavebiblioteket. Kort sagt, der er for mange opgaver oprettet og planlagt, så en af ​​de opgaver, MongoDB's driver venter på, ikke vil være i stand til at blive færdig. Jeg tog meget lang tid at indse, at det ikke er et dødvande, selvom det ser ud som det er det.

Her er trinnet til at reproducere:

  1. Download kildekoden til MongoDB's CSharp-driver .
  2. Åbn den løsning, og opret et konsolprojekt indeni og referer til driverprojektet.
  3. I hovedfunktionen skal du oprette en System.Threading.Timer, som kalder TestTask til tiden. Indstil timeren til at starte med det samme én gang. Til sidst tilføjer du en Console.Read().
  4. I TestTask skal du bruge en for-løkke til at oprette 300 opgaver ved at kalde Task.Factory.StartNew(DoOneThing). Tilføj alle disse opgaver til en liste, og brug Task.WaitAll til at vente på, at de alle er færdige.
  5. I DoOneThing-funktionen skal du oprette en MongoClient og lave en simpel forespørgsel.
  6. Kør det nu.

Dette vil mislykkes på samme sted, som du nævnte:MongoDB.Driver.Core.Clusters.Cluster.WaitForDescriptionChangedHelper.HandleCompletedTask(Task completedTask)

Hvis du sætter nogle pausepunkter, vil du vide, at WaitForDescriptionChangedHelper oprettede en timeout-opgave. Derefter venter den på, at en af ​​opgaverne BeskrivelseOpdatering eller timeout er fuldført. BeskrivelseOpdateringen sker dog aldrig, men hvorfor?

Nu, tilbage til mit eksempel, er der en interessant del:Jeg startede en timer. Hvis du ringer til TestTask direkte, vil den køre uden problemer. Ved at sammenligne dem med Visual Studios Opgavevindue, vil du bemærke, at timerversionen vil skabe mange flere opgaver end ikke-timerversionen. Lad mig forklare denne del lidt senere. Der er en anden vigtig forskel. Du skal tilføje debug-linjer i 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]);
        }
    }

Ved at tilføje disse linjer vil du også finde ud af, at ikke-timerversionen vil opdatere to gange, men timerversionen vil kun opdatere én gang. Og den anden kommer fra "MonitorServerAsync" i ServerMonitor.cs. Det viste sig, at i timer-versionen blev MontiorServerAsync udført, men efter at den kom hele vejen gennem ServerMonitor.HeartbeatAsync, BinaryConnection.OpenAsync, BinaryConnection.OpenHelperAsync og TcpStreamFactory.CreateStreamAsyncpStream nåede den endelig frem til TcReamEnFactory. Den dårlige ting sker her:Dns.GetHostAddressesAsync . Denne bliver aldrig henrettet. Hvis du ændrer koden lidt og gør den til:

    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();

Du vil kunne finde opgave-id'et. Ved at kigge ind i Visual Studios Opgavevindue er det helt indlysende, at der er omkring 300 opgaver foran det. Kun flere af dem udføres, men blokeres. Hvis du tilføjer en Console.Writeline i DoOneThing-funktionen, vil du se, at opgaveplanlæggeren starter flere af dem næsten på samme tid, men så bremses den ned til omkring én pr. sekund. Så det betyder, at du skal vente i omkring 300 sekunder, før opgaven med at løse dns begynder at køre. Derfor overskrider den timeout på 30 sekunder.

Nu kommer her en hurtig løsning, hvis du ikke laver skøre ting:

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

Dette vil tvinge ThreadPoolScheduler til at starte en tråd med det samme i stedet for at vente et sekund, før du opretter en ny.

Dette vil dog ikke virke, hvis du laver en rigtig skør ting som mig. Lad os ændre for-løkken fra 300 til 30000, selv denne løsning kan også mislykkes. Årsagen er, at det skaber for mange tråde. Dette er ressource- og tidskrævende. Og det kan begynde at sætte gang i GC-processen. Alt i alt vil det muligvis ikke være i stand til at oprette alle disse tråde, før tiden er ved at løbe ud.

Den perfekte måde er at stoppe med at oprette masser af opgaver og bruge standardplanlægningsprogrammet til at planlægge dem. Du kan prøve at oprette et arbejdsemne og sætte det i en ConcurrentQueue og derefter oprette flere tråde som arbejdere for at forbruge elementerne.

Men hvis du ikke vil ændre den oprindelige struktur for meget, kan du prøve på følgende måde:

Opret en ThrottledTaskScheduler afledt af TaskScheduler.

  1. Denne ThrottledTaskScheduler accepterer en TaskScheduler som den underliggende, der kører den faktiske opgave.
  2. Dump opgaverne til den underliggende planlægger, men hvis den overskrider grænsen, skal du placere den i en kø i stedet for.
  3. Hvis en af ​​opgaverne er afsluttet, skal du kontrollere køen og prøve at dumpe dem ind i den underliggende planlægger inden for grænsen.
  4. Brug følgende kode til at starte alle de skøre nye opgaver:

·

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());

Du kan tage System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ConcurrentExclusiveTaskScheduler som reference. Det er lidt mere kompliceret end det, vi har brug for. Det er til et andet formål. Så du skal ikke bekymre dig om de dele, der går frem og tilbage med funktionen inde i ConcurrentExclusiveSchedulerPair-klassen. Du kan dog ikke bruge den direkte, da den ikke består TaskCreationOptions.LongRunning, når den opretter indpakningsopgaven.

Det virker for mig. Held og lykke!

P.S.:Grunden til at have mange opgaver i timerversionen ligger sandsynligvis inde i TaskScheduler.TryExecuteTaskInline. Hvis det er i hovedtråden, hvor ThreadPool er oprettet, vil den være i stand til at udføre nogle af opgaverne uden at sætte dem i køen.




  1. MongoDB og Google Cloud Funktioner VPC Peering?

  2. Parse server - Filen blev ikke fundet

  3. Kør Mahout RowSimilarity-anbefaling på MongoDB-data

  4. Forstå MongoDB BSON Dokumentstørrelsesgrænse