MongoDB
 sql >> Baza danych >  >> NoSQL >> MongoDB

System.TimeoutException:przekroczono limit czasu po wybraniu serwera przez 30000 ms przy użyciu CompositeServerSelector

Jest to bardzo trudny problem związany z Biblioteką Zadań. Krótko mówiąc, jest zbyt wiele utworzonych i zaplanowanych zadań, więc jedno z zadań, na które czeka sterownik MongoDB, nie będzie mogło zostać ukończone. Zajęło mi bardzo dużo czasu, aby zdać sobie sprawę, że to nie jest impas, chociaż na to wygląda.

Oto krok do odtworzenia:

  1. Pobierz kod źródłowy sterownika CSharp MongoDB .
  2. Otwórz to rozwiązanie i utwórz projekt konsoli wewnątrz i odwołując się do projektu sterownika.
  3. W funkcji Main utwórz System.Threading.Timer, który wywoła TestTask na czas. Ustaw minutnik, aby uruchomił się natychmiast raz. Na koniec dodaj Console.Read().
  4. W TestTask użyj pętli for, aby utworzyć 300 zadań, wywołując Task.Factory.StartNew(DoOneThing). Dodaj wszystkie te zadania do listy i użyj Task.WaitAll, aby poczekać, aż wszystkie zostaną zakończone.
  5. W funkcji DoOneThing utwórz MongoClient i wykonaj proste zapytanie.
  6. Teraz uruchom go.

To nie powiedzie się w tym samym miejscu, o którym wspomniałeś:MongoDB.Driver.Core.Clusters.Cluster.WaitForDescriptionChangedHelper.HandleCompletedTask(Task completedTask)

Jeśli umieścisz kilka punktów przerwania, będziesz wiedział, że WaitForDescriptionChangedHelper utworzył zadanie limitu czasu. Następnie czeka na zakończenie dowolnego zadania DescriptionUpdate lub zadania limitu czasu. Jednak aktualizacja DescriptionUpdate nigdy się nie dzieje, ale dlaczego?

Wracając do mojego przykładu, jest jedna interesująca część:uruchomiłem minutnik. Jeśli wywołasz TestTask bezpośrednio, będzie działać bez problemu. Porównując je z oknem zadań programu Visual Studio, zauważysz, że wersja z zegarem utworzy znacznie więcej zadań niż wersja bez zegara. Pozwólcie, że wyjaśnię tę część nieco później. Jest jeszcze jedna ważna różnica. Musisz dodać wiersze debugowania w 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]);
        }
    }

Dodając te wiersze, dowiesz się również, że wersja bez zegara zaktualizuje się dwukrotnie, ale wersja z zegarem zaktualizuje się tylko raz. A drugi pochodzi z "MonitorServerAsync" w ServerMonitor.cs. Okazało się, że w wersji z zegarem, MontiorServerAsync został wykonany, ale po przejściu przez ServerMonitor.HeartbeatAsync, BinaryConnection.OpenAsync, BinaryConnection.OpenHelperAsync i TcpStreamFactory.CreateStreamAsync w końcu dotarł do TcpStreamFactory.Resync.Resolve.EndPoints Tutaj dzieje się coś złego:Dns.GetHostAddressesAsync . Ten nigdy nie zostanie stracony. Jeśli nieznacznie zmodyfikujesz kod i zmienisz go w:

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

Będziesz mógł znaleźć identyfikator zadania. Patrząc w okno zadań programu Visual Studio, jest całkiem oczywiste, że przed nim jest około 300 zadań. Tylko kilka z nich jest wykonywanych, ale zablokowanych. Jeśli dodasz Console.Writeline w funkcji DoOneThing, zobaczysz, że harmonogram zadań uruchamia kilka z nich prawie w tym samym czasie, ale potem zwalnia do około jednego na sekundę. Oznacza to, że musisz poczekać około 300 sekund, zanim rozpocznie się zadanie rozwiązywania dns. Dlatego przekracza 30-sekundowy limit czasu.

Oto szybkie rozwiązanie, jeśli nie robisz szalonych rzeczy:

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

Zmusi to ThreadPoolScheduler do natychmiastowego uruchomienia wątku zamiast czekać jedną sekundę przed utworzeniem nowego.

Jednak to nie zadziała, jeśli robisz naprawdę szalone rzeczy, takie jak ja. Zmieńmy pętlę for z 300 na 30000, nawet to rozwiązanie może się nie powieść. Powodem jest to, że tworzy zbyt wiele wątków. Jest to czasochłonne i zasobożerne. I może zacząć proces GC. Podsumowując, może nie być w stanie zakończyć tworzenia wszystkich tych wątków przed upływem czasu.

Idealnym sposobem jest zaprzestanie tworzenia wielu zadań i użycie domyślnego harmonogramu do ich zaplanowania. Możesz spróbować utworzyć element pracy i umieścić go w ConcurrentQueue, a następnie utworzyć kilka wątków jako pracowników, aby wykorzystać elementy.

Jeśli jednak nie chcesz zbytnio zmieniać oryginalnej struktury, możesz spróbować w następujący sposób:

Utwórz ThrottledTaskScheduler pochodzący z TaskScheduler.

  1. Ten ThrottledTaskScheduler akceptuje TaskScheduler jako podstawowy, który uruchomi rzeczywiste zadanie.
  2. Zrzuć zadania do bazowego harmonogramu, ale jeśli przekroczy limit, zamiast tego umieść je w kolejce.
  3. Jeśli którekolwiek z zadań zostało zakończone, sprawdź kolejkę i spróbuj zrzucić je do podstawowego harmonogramu w ramach limitu.
  4. Użyj poniższego kodu, aby rozpocząć wszystkie te szalone nowe zadania:

·

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

Jako odniesienie można przyjąć System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ConcurrentExclusiveTaskScheduler. To trochę bardziej skomplikowane niż to, którego potrzebujemy. To w innym celu. Tak więc nie martw się o te części, które poruszają się tam iz powrotem z funkcją wewnątrz klasy ConcurrentExclusiveSchedulerPair. Nie możesz jednak użyć go bezpośrednio, ponieważ nie przekazuje TaskCreationOptions.LongRunning podczas tworzenia zadania zawijania.

Mi to pasuje. Powodzenia!

P.S.:Powodem posiadania wielu zadań w wersji z zegarem jest prawdopodobnie wnętrze TaskScheduler.TryExecuteTaskInline. Jeśli znajduje się w głównym wątku, w którym tworzona jest pula wątków, będzie mógł wykonać niektóre zadania bez umieszczania ich w kolejce.




  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. Jak zaktualizować te konkretne dane w tej kolekcji użytkowników w mongodb?

  2. Użyj AND - operatora w metodzie find

  3. Zachowanie wymagania w node.js

  4. Strasznie obniżona wydajność z innymi warunkami łączenia w $ lookup (przy użyciu potoku)

  5. Problemy z użyciem MongoDB jako backendu dla projektu Django (Django 1.7)