Wydajność FactoryFinder / złe buforowanie

9

Mam dość dużą aplikację Java ee z ogromną ścieżką klasy, która wykonuje wiele przetwarzania XML. Obecnie próbuję przyspieszyć niektóre z moich funkcji i zlokalizować wolne ścieżki kodu za pomocą profilerów próbkujących.

Zauważyłem, że szczególnie części naszego kodu, w których mamy wywołania, TransformerFactory.newInstance(...)są desperacko wolne. Śledziłem to FactoryFindermetodą, która findServiceProviderzawsze tworzy nową ServiceLoaderinstancję. W ServiceLoader javadoc znalazłem następującą notatkę o buforowaniu:

Dostawcy są zlokalizowani i utworzeni leniwie, czyli na żądanie. Program ładujący utrzymuje pamięć podręczną dostawców, którzy zostali załadowani do tej pory. Każde wywołanie metody iteratora zwraca iterator, który najpierw zwraca wszystkie elementy pamięci podręcznej w kolejności tworzenia instancji, a następnie leniwie lokalizuje i tworzy instancję pozostałych dostawców, dodając kolejno każdego z nich do pamięci podręcznej. Pamięć podręczną można wyczyścić za pomocą metody przeładowania.

Na razie w porządku. Jest to część FactoryFinder#findServiceProvidermetody OpenJDK :

private static <T> T findServiceProvider(final Class<T> type)
        throws TransformerFactoryConfigurationError
    {
      try {
            return AccessController.doPrivileged(new PrivilegedAction<T>() {
                public T run() {
                    final ServiceLoader<T> serviceLoader = ServiceLoader.load(type);
                    final Iterator<T> iterator = serviceLoader.iterator();
                    if (iterator.hasNext()) {
                        return iterator.next();
                    } else {
                        return null;
                    }
                 }
            });
        } catch(ServiceConfigurationError e) {
            ...
        }
    }

Każde połączenie do findServiceProviderpołączeń ServiceLoader.load. Tworzy to za każdym razem nowy ServiceLoader. W ten sposób wydaje się, że mechanizm buforowania ServiceLoaders w ogóle nie jest używany. Każde połączenie skanuje ścieżkę klasy w poszukiwaniu żądanego dostawcy usługi.

Co już próbowałem:

  1. Wiem, że możesz ustawić właściwość systemową, np. javax.xml.transform.TransformerFactoryOkreślić konkretną implementację. W ten sposób FactoryFinder nie korzysta z procesu ServiceLoader i jego superszybkiego działania. Niestety jest to właściwość dla całego środowiska JVM i wpływa na inne procesy Java uruchomione w moim środowisku JVM. Na przykład moja aplikacja jest dostarczana z Saxonem i powinienem użyć com.saxonica.config.EnterpriseTransformerFactoryMam inną aplikację, która nie jest dostarczana z Saxonem. Gdy tylko ustawię właściwość systemową, moja inna aplikacja nie uruchamia się, ponieważ nie ma com.saxonica.config.EnterpriseTransformerFactoryjej w ścieżce klasy. Więc nie wydaje mi się to rozwiązaniem.
  2. Już dokonałem refaktoryzacji każdego miejsca, w którym TransformerFactory.newInstancenazywa się a, i buforuję TransformerFactory. Ale w moich zależnościach są różne miejsca, w których nie mogę refaktoryzować kodu.

Moje pytania brzmią: dlaczego FactoryFinder nie używa ponownie programu ServiceLoader? Czy istnieje sposób na przyspieszenie całego procesu ServiceLoader, inny niż użycie właściwości systemowych? Czy nie można tego zmienić w JDK, aby FactoryFinder ponownie używał instancji ServiceLoader? Nie dotyczy to również pojedynczego FactoryFinder. To zachowanie jest takie samo dla wszystkich klas FactoryFinder w javax.xmlpakiecie, na który patrzyłem do tej pory.

Używam OpenJDK 8/11. Moje aplikacje są wdrażane w instancji Tomcat 9.

Edycja: podając więcej szczegółów

Oto stos wywołań dla pojedynczego wywołania XMLInputFactory.newInstance: wprowadź opis zdjęcia tutaj

Tam, gdzie wykorzystuje się większość zasobów, znajduje się ServiceLoaders$LazyIterator.hasNextService. Ta metoda wywołuje getResourcesClassLoader w celu odczytania META-INF/services/javax.xml.stream.XMLInputFactorypliku. Samo połączenie za każdym razem zajmuje około 35 ms.

Czy istnieje sposób, aby poinstruować Tomcat, aby lepiej buforował te pliki, aby były szybciej dostarczane?

Wagner Michael
źródło
Zgadzam się z twoją oceną FactoryFinder.java. Wygląda na to, że powinien buforować ServiceLoader. Czy próbowałeś pobrać źródło openjdk i go zbudować. Wiem, że to brzmi jak duże zadanie, ale może nie być. Warto też napisać problem przeciwko FactoryFinder.java i sprawdzić, czy ktoś podejmie problem i zaoferuje rozwiązanie.
djhallx
Czy próbowałeś ustawić właściwość za pomocą -Dflagi do swojego Tomcatprocesu? Na przykład: -Djavax.xml.transform.TransformerFactory=<factory class>.nie powinno zastępować właściwości innych aplikacji. Twój post jest dobrze opisany i prawdopodobnie próbowałeś, ale chciałbym to potwierdzić. Zobacz Jak ustawić właściwość systemową Javax.xml.transform.TransformerFactory , Jak ustawić argumenty HeapMemory lub JVM w Tomcat
Michał Ziober

Odpowiedzi:

1

35 ms wydaje się, że są zaangażowane czasy dostępu do dysku, co wskazuje na problem z buforowaniem systemu operacyjnego.

Jeśli w ścieżce klasy znajdują się wpisy katalogu / nie-jar, które mogą spowolnić działanie. Również jeśli zasób nie jest obecny w pierwszej sprawdzanej lokalizacji.

ClassLoader.getResourcemożna zastąpić, jeśli można ustawić moduł ładujący klasy kontekstu wątku, albo przez konfigurację (nie dotykałem tomcat od lat), albo po prostu Thread.setContextClassLoader.

Tom Hawtin - tackline
źródło
Wygląda na to, że to może działać. Wcześniej czy później przyjrzę się temu. Dziękuję Ci!
Wagner Michael
1

Mogłem uzyskać kolejne 30 minut na debugowanie tego i przyjrzałem się, jak Tomcat wykonuje buforowanie zasobów.

W szczególności CachedResource.validateResources(które można znaleźć w powyższym flamegrafie) było dla mnie interesujące. Zwraca, truejeśli CachedResourcenadal jest ważny:

protected boolean validateResources(boolean useClassLoaderResources) {
        long now = System.currentTimeMillis();
        if (this.webResources == null) {
            ...
        }

        // TTL check here!!
        if (now < this.nextCheck) {
            return true;
        } else if (this.root.isPackedWarFile()) {
            this.nextCheck = this.ttl + now;
            return true;
        } else {
            return false;
        }
    }

Wygląda na to, że CachedResource ma naprawdę czas na życie (ttl). W Tomcat jest sposób na skonfigurowanie cacheTtl, ale można tylko zwiększyć tę wartość. Wygląda na to, że konfiguracja buforowania zasobów nie jest tak łatwo elastyczna.

Więc mój Tomcat ma skonfigurowaną domyślną wartość 5000 ms. To mnie oszukało podczas testowania wydajności, ponieważ miałem trochę ponad 5 sekund między moimi żądaniami (patrząc na wykresy i inne rzeczy). Właśnie dlatego wszystkie moje żądania w zasadzie działały bez pamięci podręcznej i za ZipFile.openkażdym razem powodowały to obciążenie .

Ponieważ nie mam zbyt dużego doświadczenia z konfiguracją Tomcat, nie jestem jeszcze pewien, jakie jest właściwe rozwiązanie. Zwiększenie cacheTTL utrzymuje pamięć podręczną dłużej, ale nie rozwiązuje problemu na dłuższą metę.

Podsumowanie

Myślę, że w rzeczywistości są tutaj dwaj winowajcy.

  1. Klasy FactoryFinder nie wykorzystują ponownie programu ServiceLoader. Może istnieć ważny powód, dla którego nie używają ich ponownie - nie mogę jednak wymyślić żadnego z nich.

  2. Tomcat eksmitujący pamięć podręczną po ustalonym czasie dla zasobu aplikacji internetowej (pliki w ścieżce klas - jak ServiceLoaderkonfiguracja)

Po połączeniu tego z brakiem zdefiniowania właściwości systemowej dla klasy ServiceLoader, otrzymujemy powolne wywołanie FactoryFinder co cacheTtlsekundę.

Na razie mogę żyć z powiększaniem cacheTtl do dłuższego czasu. Mogę również rzucić okiem na sugestię Toma Hawtinsa, by zastąpić ją, Classloader.getResourcesnawet jeśli wydaje mi się, że jest to trudny sposób na pozbycie się tego wąskiego gardła. Warto jednak na to spojrzeć.

Wagner Michael
źródło