Kinerja FactoryFinder / caching buruk

9

Saya punya aplikasi java ee agak besar dengan classpath besar melakukan banyak pemrosesan xml. Saat ini saya mencoba untuk mempercepat beberapa fungsi saya dan menemukan jalur kode lambat melalui sampling profiler.

Satu hal yang saya perhatikan adalah bahwa terutama bagian dari kode kita di mana kita memiliki panggilan seperti TransformerFactory.newInstance(...)sangat lambat Saya melacak ini ke FactoryFindermetode yang findServiceProviderselalu membuat ServiceLoadercontoh baru . Di ServiceLoader javadoc saya menemukan catatan berikut tentang caching:

Penyedia ditempatkan dan dipakai secara malas, yaitu, sesuai permintaan. Pemuat layanan menyimpan cache dari penyedia yang telah dimuat sejauh ini. Setiap permohonan metode iterator mengembalikan iterator yang pertama-tama menghasilkan semua elemen cache, dalam urutan instantiation, dan kemudian dengan malas menemukan dan membuat instantiate penyedia yang tersisa, menambahkan masing-masing ke cache pada gilirannya. Cache dapat dihapus melalui metode reload.

Sejauh ini bagus. Ini adalah bagian dari FactoryFinder#findServiceProvidermetode 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) {
            ...
        }
    }

Setiap panggilan ke findServiceProviderpanggilan ServiceLoader.load. Ini menciptakan yang baru ServiceLoader setiap kali. Dengan cara ini tampaknya tidak ada penggunaan mekanisme caching ServiceLoaders sama sekali. Setiap panggilan memindai classpath untuk ServiceProvider yang diminta.

Apa yang sudah saya coba:

  1. Saya tahu Anda dapat mengatur properti sistem ingin javax.xml.transform.TransformerFactorymenentukan implementasi tertentu. Dengan cara ini FactoryFinder tidak menggunakan proses ServiceLoader dan sangat cepat. Sayangnya ini adalah properti jvm lebar dan mempengaruhi proses java lainnya yang berjalan di jvm saya. Misalnya aplikasi saya dikirim dengan Saxon dan harus menggunakan com.saxonica.config.EnterpriseTransformerFactorysaya punya aplikasi lain yang tidak dikirimkan dengan Saxon. Segera setelah saya mengatur properti sistem, aplikasi saya yang lain gagal untuk memulai, karena tidak ada com.saxonica.config.EnterpriseTransformerFactorydi classpath-nya. Jadi ini sepertinya tidak menjadi pilihan bagi saya.
  2. Saya sudah refactored setiap tempat di mana a TransformerFactory.newInstancedipanggil dan menyimpan cache TransformerFactory. Tetapi ada berbagai tempat di dependensi saya di mana saya tidak bisa memperbaiki kode.

Pertanyaan saya adalah: Mengapa FactoryFinder tidak menggunakan kembali ServiceLoader? Apakah ada cara untuk mempercepat seluruh proses ServiceLoader ini selain menggunakan properti sistem? Tidak bisakah ini diubah di JDK sehingga FactoryFinder menggunakan instance ServiceLoader? Juga ini tidak spesifik untuk FactoryFinder tunggal. Bahaviour ini sama untuk semua kelas FactoryFinder dalam javax.xmlpaket yang telah saya lihat sejauh ini.

Saya menggunakan OpenJDK 8/11. Aplikasi saya digunakan dalam instance Tomcat 9.

Sunting: Memberikan rincian lebih lanjut

Berikut adalah tumpukan panggilan untuk panggilan XMLInputFactory.newInstance tunggal: masukkan deskripsi gambar di sini

Di mana sebagian besar sumber daya digunakan ServiceLoaders$LazyIterator.hasNextService. Metode ini memanggil getResourcesClassLoader untuk membaca META-INF/services/javax.xml.stream.XMLInputFactoryfile. Panggilan itu sendiri membutuhkan sekitar 35 ms setiap kali.

Apakah ada cara untuk menginstruksikan Tomcat untuk melakukan cache yang lebih baik pada file-file ini sehingga dilayani lebih cepat?

Wagner Michael
sumber
Saya setuju dengan penilaian Anda tentang FactoryFinder.java. Sepertinya itu harus caching ServiceLoader. Sudahkah Anda mencoba mengunduh sumber openjdk dan membangunnya. Saya tahu itu terdengar seperti tugas besar tetapi mungkin tidak. Juga, mungkin layak untuk menulis masalah terhadap FactoryFinder.java dan melihat apakah seseorang mengambil masalah dan menawarkan solusi.
djhallx
Sudahkah Anda mencoba menyetel properti menggunakan -Dbendera ke Tomcatproses Anda ? Misalnya: -Djavax.xml.transform.TransformerFactory=<factory class>.Seharusnya tidak menimpa properti untuk aplikasi lain. Posting Anda dijelaskan dengan baik dan mungkin Anda telah mencobanya tetapi saya ingin mengonfirmasi. Lihat Cara mengatur properti sistem Javax.xml.transform.TransformerFactory , Cara mengatur Argumen HeapMemory atau JVM di Tomcat
Michał Ziober

Jawaban:

1

35 ms terdengar seperti ada waktu akses disk yang terlibat, dan itu menunjukkan masalah dengan cache OS.

Jika ada direktori / entri non-jar di classpath yang dapat memperlambat segalanya. Juga jika sumber daya tidak ada di lokasi pertama yang diperiksa.

ClassLoader.getResourcedapat ditimpa jika Anda dapat mengatur loader kelas konteks thread, baik melalui konfigurasi (saya belum menyentuh kucing hutan selama bertahun-tahun) atau hanya Thread.setContextClassLoader.

Tom Hawtin - tackline
sumber
Kedengarannya seperti ini mungkin berhasil. Saya akan melihat ini cepat atau lambat. Terima kasih!
Wagner Michael
1

Saya bisa mendapatkan 30 menit untuk men-debug ini dan melihat bagaimana Tomcat melakukan Caching Sumber Daya.

Khususnya CachedResource.validateResources(yang dapat ditemukan di flamegraph di atas) menarik bagi saya. Itu kembali truejika CachedResourcemasih valid:

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

Sepertinya CachedResource sebenarnya memiliki waktu untuk hidup (ttl). Sebenarnya ada cara di Tomcat untuk mengkonfigurasi cacheTtl tetapi Anda hanya dapat meningkatkan nilai ini. Konfigurasi caching sumber daya tidak terlalu fleksibel sepertinya.

Jadi Tomcat saya memiliki nilai default 5.000 ms yang dikonfigurasi. Ini menipu saya saat melakukan pengujian kinerja karena saya memiliki sedikit lebih dari 5 detik antara permintaan saya (melihat grafik dan lainnya). Itu sebabnya semua permintaan saya pada dasarnya berjalan tanpa cache dan memicu ini berat ZipFile.opensetiap kali.

Jadi karena saya tidak terlalu berpengalaman dengan konfigurasi Tomcat, saya belum yakin apa solusi yang tepat di sini. Meningkatkan cacheTTL membuat cache lebih lama tetapi tidak memperbaiki masalah dalam jangka panjang.

Ringkasan

Saya pikir sebenarnya ada dua penyebab di sini.

  1. Kelas FactoryFinder tidak menggunakan kembali ServiceLoader. Mungkin ada alasan yang sah mengapa mereka tidak menggunakan kembali - saya tidak bisa memikirkannya.

  2. Tomcat mengusir cache setelah waktu yang tetap untuk sumber daya aplikasi web (file di classpath - seperti ServiceLoaderkonfigurasi)

Gabungkan ini dengan tidak menetapkan Properti Sistem untuk kelas ServiceLoader dan Anda mendapatkan panggilan FactoryFinder lambat setiap cacheTtldetik.

Untuk saat ini saya dapat hidup dengan meningkatkan cacheTtl ke waktu yang lebih lama. Saya juga mungkin melihat saran Tom Hawtins untuk mengesampingkan Classloader.getResourcesmeskipun saya agak berpikir ini adalah cara yang keras untuk menghilangkan hambatan kinerja ini. Mungkin layak untuk dilihat.

Wagner Michael
sumber