Bagaimana dan mengapa kami menulis perkhidmatan berskala beban tinggi untuk 1C: Enterprise: Java, PostgreSQL, Hazelcast

Dalam artikel ini kita akan bercakap tentang bagaimana dan mengapa kita berkembang Sistem Interaksi – mekanisme yang memindahkan maklumat antara aplikasi klien dan pelayan 1C:Enterprise - daripada menetapkan tugas kepada memikirkan butiran seni bina dan pelaksanaan.

Sistem Interaksi (selepas ini dirujuk sebagai SV) ialah sistem pemesejan yang diedarkan, tahan terhadap kesalahan dengan penghantaran yang terjamin. SV direka bentuk sebagai perkhidmatan muatan tinggi dengan skalabiliti tinggi, tersedia sebagai perkhidmatan dalam talian (disediakan oleh 1C) dan sebagai produk yang dihasilkan secara besar-besaran yang boleh digunakan pada kemudahan pelayan anda sendiri.

SV menggunakan storan teragih hazelcast dan enjin carian Elasticsearch. Kami juga akan bercakap tentang Java dan cara kami menskalakan PostgreSQL secara mendatar.
Bagaimana dan mengapa kami menulis perkhidmatan berskala beban tinggi untuk 1C: Enterprise: Java, PostgreSQL, Hazelcast

Pernyataan masalah

Untuk menjelaskan sebab kami mencipta Sistem Interaksi, saya akan memberitahu anda sedikit tentang cara pembangunan aplikasi perniagaan dalam 1C berfungsi.

Sebagai permulaan, sedikit tentang kami untuk mereka yang belum tahu apa yang kami lakukan :) Kami sedang membuat platform teknologi 1C:Enterprise. Platform ini termasuk alat pembangunan aplikasi perniagaan, serta masa jalan yang membolehkan aplikasi perniagaan dijalankan dalam persekitaran merentas platform.

Paradigma pembangunan pelanggan-pelayan

Aplikasi perniagaan yang dibuat pada 1C:Enterprise beroperasi dalam tiga peringkat pelayan-pelanggan seni bina "DBMS - pelayan aplikasi - klien". Kod permohonan ditulis dalam bahasa 1C terbina dalam, boleh dilaksanakan pada pelayan aplikasi atau pada klien. Semua kerja dengan objek aplikasi (direktori, dokumen, dll.), serta membaca dan menulis pangkalan data, dilakukan hanya pada pelayan. Kefungsian borang dan antara muka arahan juga dilaksanakan pada pelayan. Pelanggan melakukan penerimaan, membuka dan memaparkan borang, "berkomunikasi" dengan pengguna (amaran, soalan...), pengiraan kecil dalam bentuk yang memerlukan respons pantas (contohnya, mendarabkan harga mengikut kuantiti), bekerja dengan fail tempatan, bekerja dengan peralatan.

Dalam kod aplikasi, pengepala prosedur dan fungsi mesti menunjukkan dengan jelas tempat kod akan dilaksanakan - menggunakan arahan &AtClient / &AtServer (&AtClient / &AtServer dalam versi bahasa Inggeris). Pembangun 1C kini akan membetulkan saya dengan mengatakan bahawa arahan sebenarnya lebih, tetapi bagi kami ini tidak penting sekarang.

Anda boleh memanggil kod pelayan daripada kod klien, tetapi anda tidak boleh memanggil kod klien daripada kod pelayan. Ini adalah had asas yang kami buat atas beberapa sebab. Khususnya, kerana kod pelayan mesti ditulis sedemikian rupa sehingga ia melaksanakan dengan cara yang sama tidak kira di mana ia dipanggil - dari klien atau dari pelayan. Dan dalam kes memanggil kod pelayan daripada kod pelayan lain, tiada pelanggan seperti itu. Dan kerana semasa pelaksanaan kod pelayan, pelanggan yang memanggilnya boleh menutup, keluar dari aplikasi dan pelayan tidak lagi mempunyai sesiapa untuk dihubungi.

Bagaimana dan mengapa kami menulis perkhidmatan berskala beban tinggi untuk 1C: Enterprise: Java, PostgreSQL, Hazelcast
Kod yang mengendalikan klik butang: memanggil prosedur pelayan daripada klien akan berfungsi, memanggil prosedur klien daripada pelayan tidak akan

Ini bermakna jika kami ingin menghantar beberapa mesej daripada pelayan kepada aplikasi klien, sebagai contoh, bahawa penjanaan laporan "berjalan lama" telah selesai dan laporan itu boleh dilihat, kami tidak mempunyai kaedah sedemikian. Anda perlu menggunakan helah, sebagai contoh, secara berkala mengundi pelayan dari kod klien. Tetapi pendekatan ini memuatkan sistem dengan panggilan yang tidak perlu, dan secara amnya tidak kelihatan sangat elegan.

Dan terdapat juga keperluan, sebagai contoh, apabila panggilan telefon tiba SIP- semasa membuat panggilan, maklumkan aplikasi pelanggan tentang perkara ini supaya ia boleh menggunakan nombor pemanggil untuk mencarinya dalam pangkalan data rakan niaga dan tunjukkan maklumat pengguna tentang rakan niaga yang memanggil. Atau, sebagai contoh, apabila pesanan tiba di gudang, maklumkan aplikasi pelanggan pelanggan tentang perkara ini. Secara umum, terdapat banyak kes di mana mekanisme sedemikian akan berguna.

Pengeluaran itu sendiri

Buat mekanisme pemesejan. Pantas, boleh dipercayai, dengan penghantaran yang terjamin, dengan keupayaan untuk mencari mesej secara fleksibel. Berdasarkan mekanisme, laksanakan messenger (mesej, panggilan video) yang dijalankan di dalam aplikasi 1C.

Reka bentuk sistem untuk berskala mendatar. Beban yang semakin meningkat mesti dilindungi dengan menambah bilangan nod.

Реализация

Kami memutuskan untuk tidak menyepadukan bahagian pelayan SV terus ke dalam platform 1C:Enterprise, tetapi untuk melaksanakannya sebagai produk berasingan, yang APInya boleh dipanggil daripada kod penyelesaian aplikasi 1C. Ini dilakukan atas beberapa sebab, yang utamanya ialah saya ingin membolehkan pertukaran mesej antara aplikasi 1C yang berbeza (contohnya, antara Pengurusan Perdagangan dan Perakaunan). Aplikasi 1C yang berbeza boleh dijalankan pada versi berbeza platform 1C:Enterprise, terletak pada pelayan yang berbeza, dsb. Dalam keadaan sedemikian, pelaksanaan SV sebagai produk berasingan yang terletak "di sisi" pemasangan 1C adalah penyelesaian yang optimum.

Jadi, kami memutuskan untuk menjadikan SV sebagai produk yang berasingan. Kami mengesyorkan agar syarikat kecil menggunakan pelayan CB yang kami pasang dalam awan kami (wss://1cdialog.com) untuk mengelakkan kos overhed yang berkaitan dengan pemasangan dan konfigurasi pelayan setempat. Pelanggan besar mungkin merasa dinasihatkan untuk memasang pelayan CB mereka sendiri di kemudahan mereka. Kami menggunakan pendekatan yang sama dalam produk SaaS awan kami 1cSegar – ia dihasilkan sebagai produk yang dihasilkan secara besar-besaran untuk pemasangan di tapak pelanggan, dan juga digunakan dalam awan kami https://1cfresh.com/.

Permohonan

Untuk mengagihkan beban dan toleransi kesalahan, kami tidak akan menggunakan satu aplikasi Java, tetapi beberapa, dengan pengimbang beban di hadapannya. Jika anda perlu memindahkan mesej dari nod ke nod, gunakan terbitkan/langgan dalam Hazelcast.

Komunikasi antara klien dan pelayan adalah melalui websocket. Ia sangat sesuai untuk sistem masa nyata.

Cache yang diedarkan

Kami memilih antara Redis, Hazelcast dan Ehcache. Dah 2015. Redis baru sahaja mengeluarkan kluster baharu (terlalu baharu, menakutkan), terdapat Sentinel dengan banyak sekatan. Ehcache tidak tahu cara memasang ke dalam kelompok (fungsi ini muncul kemudian). Kami memutuskan untuk mencubanya dengan Hazelcast 3.4.
Hazelcast dipasang ke dalam kelompok di luar kotak. Dalam mod nod tunggal, ia tidak begitu berguna dan hanya boleh digunakan sebagai cache - ia tidak tahu cara membuang data ke cakera, jika anda kehilangan satu-satunya nod, anda kehilangan data. Kami menggunakan beberapa Hazelcasts, antaranya kami membuat sandaran data kritikal. Kami tidak menyandarkan cache – kami tidak kisah.

Bagi kami, Hazelcast ialah:

  • Penyimpanan sesi pengguna. Ia mengambil masa yang lama untuk pergi ke pangkalan data untuk satu sesi setiap kali, jadi kami meletakkan semua sesi dalam Hazelcast.
  • Cache. Jika anda sedang mencari profil pengguna, semak cache. Menulis mesej baharu - letakkan dalam cache.
  • Topik untuk komunikasi antara contoh aplikasi. Nod menjana acara dan meletakkannya dalam topik Hazelcast. Nod aplikasi lain yang melanggan topik ini menerima dan memproses acara tersebut.
  • Kunci kluster. Sebagai contoh, kami membuat perbincangan menggunakan kunci unik (perbincangan tunggal dalam pangkalan data 1C):

conversationKeyChecker.check("БЕНЗОКОЛОНКА");

      doInClusterLock("БЕНЗОКОЛОНКА", () -> {

          conversationKeyChecker.check("БЕНЗОКОЛОНКА");

          createChannel("БЕНЗОКОЛОНКА");
      });

Kami menyemak bahawa tiada saluran. Kami mengambil kunci itu, menyemaknya semula dan menciptanya. Jika anda tidak menyemak kunci selepas mengambil kunci, maka terdapat kemungkinan bahawa urutan lain turut diperiksa pada masa itu dan kini akan cuba mencipta perbincangan yang sama - tetapi ia sudah wujud. Anda tidak boleh mengunci menggunakan java Lock yang disegerakkan atau biasa. Melalui pangkalan data - ia perlahan, dan sayang untuk pangkalan data; melalui Hazelcast - itulah yang anda perlukan.

Memilih DBMS

Kami mempunyai pengalaman yang luas dan berjaya bekerja dengan PostgreSQL dan bekerjasama dengan pembangun DBMS ini.

Ia tidak mudah dengan kluster PostgreSQL - ada XL, XC, Citus, tetapi secara amnya ini bukan NoSQL yang keluar dari kotak. Kami tidak menganggap NoSQL sebagai storan utama; cukuplah kami mengambil Hazelcast, yang tidak pernah kami gunakan sebelum ini.

Jika anda perlu menskalakan pangkalan data hubungan, itu bermakna serpihan. Seperti yang anda ketahui, dengan sharding kami membahagikan pangkalan data kepada bahagian yang berasingan supaya setiap satu daripadanya boleh diletakkan pada pelayan yang berasingan.

Versi pertama sharding kami menganggap keupayaan untuk mengedarkan setiap jadual aplikasi kami merentas pelayan yang berbeza dalam perkadaran yang berbeza. Terdapat banyak mesej pada pelayan A - sila, mari kita alihkan sebahagian daripada jadual ini ke pelayan B. Keputusan ini hanya menjerit tentang pengoptimuman pramatang, jadi kami memutuskan untuk menghadkan diri kepada pendekatan berbilang penyewa.

Anda boleh membaca tentang penyewa berbilang, sebagai contoh, di tapak web Data Citus.

SV mempunyai konsep aplikasi dan pelanggan. Aplikasi ialah pemasangan khusus aplikasi perniagaan, seperti ERP atau Perakaunan, dengan pengguna dan data perniagaannya. Pelanggan ialah organisasi atau individu yang bagi pihaknya permohonan itu didaftarkan dalam pelayan SV. Seorang pelanggan boleh mempunyai beberapa aplikasi yang didaftarkan, dan aplikasi ini boleh bertukar-tukar mesej antara satu sama lain. Pelanggan menjadi penyewa dalam sistem kami. Mesej daripada beberapa pelanggan boleh didapati dalam satu pangkalan data fizikal; jika kami melihat bahawa pelanggan telah mula menjana banyak trafik, kami memindahkannya ke pangkalan data fizikal yang berasingan (atau malah pelayan pangkalan data yang berasingan).

Kami mempunyai pangkalan data utama di mana jadual penghalaan disimpan dengan maklumat tentang lokasi semua pangkalan data pelanggan.

Bagaimana dan mengapa kami menulis perkhidmatan berskala beban tinggi untuk 1C: Enterprise: Java, PostgreSQL, Hazelcast

Untuk mengelakkan pangkalan data utama daripada menjadi hambatan, kami menyimpan jadual penghalaan (dan data lain yang sering diperlukan) dalam cache.

Jika pangkalan data pelanggan mula perlahan, kami akan memotongnya menjadi sekatan di dalamnya. Pada projek lain yang kami gunakan pg_pathman.

Memandangkan kehilangan mesej pengguna adalah buruk, kami mengekalkan pangkalan data kami dengan replika. Gabungan replika segerak dan tak segerak membolehkan anda menginsuranskan diri anda sekiranya kehilangan pangkalan data utama. Kehilangan mesej hanya akan berlaku jika pangkalan data utama dan replika segeraknya gagal serentak.

Jika replika segerak hilang, replika tak segerak menjadi segerak.
Jika pangkalan data utama hilang, replika segerak menjadi pangkalan data utama, dan replika asynchronous menjadi replika segerak.

Elasticsearch untuk carian

Oleh kerana, antara lain, SV juga merupakan utusan, ia memerlukan carian yang cepat, mudah dan fleksibel, dengan mengambil kira morfologi, menggunakan padanan yang tidak tepat. Kami memutuskan untuk tidak mencipta semula roda dan menggunakan enjin carian percuma Elasticsearch, yang dibuat berdasarkan perpustakaan Lucene. Kami juga menggunakan Elasticsearch dalam kelompok (master – data – data) untuk menghapuskan masalah sekiranya berlaku kegagalan nod aplikasi.

Di github kami dapati Pemalam morfologi Rusia untuk Elasticsearch dan gunakannya. Dalam indeks Elasticsearch kami menyimpan akar perkataan (yang ditentukan oleh pemalam) dan N-gram. Apabila pengguna memasukkan teks untuk mencari, kami mencari teks yang ditaip di antara N-gram. Apabila disimpan ke indeks, perkataan "teks" akan dibahagikan kepada N-gram berikut:

[yang, tek, tex, text, texts, ek, ex, ext, texts, ks, kst, ksty, st, sty, you],

Dan akar perkataan "teks" juga akan dipelihara. Pendekatan ini membolehkan anda mencari di awal, di tengah, dan di akhir perkataan.

Gambaran Keseluruhan

Bagaimana dan mengapa kami menulis perkhidmatan berskala beban tinggi untuk 1C: Enterprise: Java, PostgreSQL, Hazelcast
Ulangi gambar dari awal artikel, tetapi dengan penjelasan:

  • Pengimbang terdedah di Internet; kami mempunyai nginx, ia boleh menjadi apa-apa.
  • Contoh aplikasi Java berkomunikasi antara satu sama lain melalui Hazelcast.
  • Untuk bekerja dengan soket web yang kami gunakan Netty.
  • Aplikasi Java ditulis dalam Java 8 dan terdiri daripada berkas OSGi. Pelan itu termasuk pemindahan ke Java 10 dan peralihan kepada modul.

Pembangunan dan ujian

Dalam proses membangunkan dan menguji SV, kami menemui beberapa ciri menarik bagi produk yang kami gunakan.

Ujian beban dan kebocoran memori

Keluaran setiap keluaran SV melibatkan ujian beban. Ia berjaya apabila:

  • Ujian berfungsi selama beberapa hari dan tiada kegagalan perkhidmatan
  • Masa tindak balas untuk operasi utama tidak melebihi ambang yang selesa
  • Kemerosotan prestasi berbanding versi sebelumnya tidak melebihi 10%

Kami mengisi pangkalan data ujian dengan data - untuk melakukan ini, kami menerima maklumat tentang pelanggan paling aktif dari pelayan pengeluaran, darabkan nombornya dengan 5 (bilangan mesej, perbincangan, pengguna) dan mengujinya dengan cara itu.

Kami menjalankan ujian beban sistem interaksi dalam tiga konfigurasi:

  1. ujian tekanan
  2. Sambungan sahaja
  3. Pendaftaran pelanggan

Semasa ujian tekanan, kami melancarkan beberapa ratus utas, dan mereka memuatkan sistem tanpa henti: menulis mesej, membuat perbincangan, menerima senarai mesej. Kami mensimulasikan tindakan pengguna biasa (dapatkan senarai mesej saya yang belum dibaca, tulis kepada seseorang) dan penyelesaian perisian (hantar pakej konfigurasi berbeza, proses makluman).

Sebagai contoh, ini adalah bahagian ujian tekanan:

  • Pengguna log masuk
    • Meminta perbincangan anda yang belum dibaca
    • 50% berkemungkinan membaca mesej
    • 50% berkemungkinan menghantar teks
    • Pengguna seterusnya:
      • Mempunyai peluang 20% ​​untuk mencipta perbincangan baharu
      • Memilih mana-mana perbincangannya secara rawak
      • Masuk ke dalam
      • Meminta mesej, profil pengguna
      • Mencipta lima mesej yang ditujukan kepada pengguna rawak daripada perbincangan ini
      • Meninggalkan perbincangan
      • Ulang 20 kali
      • Log keluar, kembali ke permulaan skrip

    • Chatbot memasuki sistem (meniru pemesejan daripada kod aplikasi)
      • Mempunyai peluang 50% untuk mencipta saluran baharu untuk pertukaran data (perbincangan khas)
      • 50% berkemungkinan akan menulis mesej kepada mana-mana saluran sedia ada

Senario "Sambungan Sahaja" muncul atas sebab tertentu. Terdapat situasi: pengguna telah menyambungkan sistem, tetapi masih belum terlibat. Setiap pengguna menghidupkan komputer pada pukul 09:00 pagi, membuat sambungan ke pelayan dan kekal senyap. Lelaki ini berbahaya, terdapat ramai daripada mereka - satu-satunya pakej yang mereka ada ialah PING/PONG, tetapi mereka mengekalkan sambungan ke pelayan (mereka tidak dapat meneruskannya - bagaimana jika ada mesej baharu). Ujian itu menghasilkan semula keadaan di mana sebilangan besar pengguna sedemikian cuba log masuk ke sistem dalam masa setengah jam. Ia serupa dengan ujian tekanan, tetapi tumpuannya tepat pada input pertama ini - supaya tidak ada kegagalan (seseorang tidak menggunakan sistem, dan ia sudah jatuh - sukar untuk memikirkan sesuatu yang lebih buruk).

Skrip pendaftaran pelanggan bermula dari pelancaran pertama. Kami menjalankan ujian tekanan dan yakin bahawa sistem tidak menjadi perlahan semasa surat-menyurat. Tetapi pengguna datang dan pendaftaran mula gagal kerana tamat masa. Semasa mendaftar kami gunakan / dev / rawak, yang berkaitan dengan entropi sistem. Pelayan tidak mempunyai masa untuk mengumpul entropi yang mencukupi dan apabila SecureRandom baharu diminta, ia membeku selama berpuluh-puluh saat. Terdapat banyak cara untuk keluar dari situasi ini, contohnya: beralih kepada /dev/urandom yang kurang selamat, pasang papan khas yang menjana entropi, jana nombor rawak terlebih dahulu dan simpannya dalam kolam. Kami menutup sementara masalah dengan kumpulan, tetapi sejak itu kami telah menjalankan ujian berasingan untuk mendaftarkan pelanggan baharu.

Kami gunakan sebagai penjana beban JMeter. Ia tidak tahu cara bekerja dengan websocket; ia memerlukan pemalam. Yang pertama dalam hasil carian untuk pertanyaan "soket web jmeter" ialah: artikel daripada BlazeMeter, yang mengesyorkan pemalam oleh Maciej Zaleski.

Di situlah kami memutuskan untuk bermula.

Hampir serta-merta selepas memulakan ujian yang serius, kami mendapati bahawa JMeter mula membocorkan ingatan.

Pemalam adalah cerita besar yang berasingan; dengan 176 bintang, ia mempunyai 132 garpu pada github. Penulis sendiri tidak komited kepadanya sejak 2015 (kami mengambilnya pada 2015, kemudian ia tidak menimbulkan syak wasangka), beberapa isu github mengenai kebocoran memori, 7 permintaan tarik yang tidak ditutup.
Jika anda memutuskan untuk melakukan ujian beban menggunakan pemalam ini, sila beri perhatian kepada perbincangan berikut:

  1. Dalam persekitaran berbilang benang, LinkedList biasa telah digunakan, dan hasilnya ialah NPE dalam masa larian. Ini boleh diselesaikan sama ada dengan menukar kepada ConcurrentLinkedDeque atau dengan blok yang disegerakkan. Kami memilih pilihan pertama untuk diri kami sendiri (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Kebocoran memori; apabila memutuskan sambungan, maklumat sambungan tidak dipadamkan (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. Dalam mod penstriman (apabila soket web tidak ditutup pada penghujung sampel, tetapi digunakan kemudian dalam pelan), Corak respons tidak berfungsi (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Ini adalah salah satu daripada yang ada di github. Apa yang kami lakukan:

  1. Telah mengambil garpu Elyran Kogan (@elyrank) – ia menyelesaikan masalah 1 dan 3
  2. Selesai masalah 2
  3. Kemas kini jeti dari 9.2.14 hingga 9.3.12
  4. Dibungkus SimpleDateFormat dalam ThreadLocal; SimpleDateFormat tidak selamat untuk benang, yang membawa kepada NPE pada masa jalan
  5. Membetulkan satu lagi kebocoran memori (sambungan ditutup dengan tidak betul apabila diputuskan)

Namun ia mengalir!

Ingatan mula habis bukan dalam sehari, tetapi dalam dua. Tiada masa lagi, jadi kami memutuskan untuk melancarkan lebih sedikit rangkaian, tetapi pada empat ejen. Ini sepatutnya cukup untuk sekurang-kurangnya seminggu.

Dua hari telah berlalu...

Kini Hazelcast kehabisan ingatan. Log menunjukkan bahawa selepas beberapa hari ujian, Hazelcast mula mengadu tentang kekurangan ingatan, dan selepas beberapa lama kelompok itu runtuh, dan nod terus mati satu demi satu. Kami menyambungkan JVisualVM kepada hazelcast dan melihat "gergaji naik" - ia kerap memanggil GC, tetapi tidak dapat mengosongkan ingatan.

Bagaimana dan mengapa kami menulis perkhidmatan berskala beban tinggi untuk 1C: Enterprise: Java, PostgreSQL, Hazelcast

Ternyata dalam hazelcast 3.4, apabila memadam peta / multiMap (map.destroy()), memori tidak dibebaskan sepenuhnya:

github.com/hazelcast/hazelcast/issues/6317
github.com/hazelcast/hazelcast/issues/4888

Pepijat kini telah diperbaiki dalam 3.5, tetapi ia adalah masalah ketika itu. Kami mencipta multiMaps baharu dengan nama dinamik dan memadamkannya mengikut logik kami. Kod itu kelihatan seperti ini:

public void join(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.put(auth.getUserId(), auth);
}

public void leave(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.remove(auth.getUserId(), auth);

    if (sessions.size() == 0) {
        sessions.destroy();
    }
}

Hubungi:

service.join(auth1, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");
service.join(auth2, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");

multiMap telah dicipta untuk setiap langganan dan dipadamkan apabila ia tidak diperlukan. Kami memutuskan bahawa kami akan memulakan Peta , kuncinya ialah nama langganan, dan nilainya akan menjadi pengecam sesi (dari mana anda boleh mendapatkan pengecam pengguna, jika perlu).

public void join(Authentication auth, String sub) {
    addValueToMap(sub, auth.getSessionId());
}

public void leave(Authentication auth, String sub) { 
    removeValueFromMap(sub, auth.getSessionId());
}

Carta telah bertambah baik.

Bagaimana dan mengapa kami menulis perkhidmatan berskala beban tinggi untuk 1C: Enterprise: Java, PostgreSQL, Hazelcast

Apa lagi yang telah kita pelajari tentang ujian beban?

  1. JSR223 perlu ditulis dalam groovy dan menyertakan cache kompilasi - ia lebih pantas. Link.
  2. Graf Jmeter-Plugins lebih mudah difahami daripada yang standard. Link.

Mengenai pengalaman kami dengan Hazelcast

Hazelcast ialah produk baharu untuk kami, kami mula bekerja dengannya dari versi 3.4.1, kini pelayan pengeluaran kami menjalankan versi 3.9.2 (pada masa penulisan, versi terkini Hazelcast ialah 3.10).

Penjanaan ID

Kami bermula dengan pengecam integer. Mari bayangkan bahawa kita memerlukan satu lagi Long untuk entiti baharu. Urutan dalam pangkalan data tidak sesuai, jadual terlibat dalam sharding - ternyata terdapat ID mesej=1 dalam DB1 dan ID mesej=1 dalam DB2, anda tidak boleh meletakkan ID ini dalam Elasticsearch, mahupun dalam Hazelcast , tetapi perkara yang paling teruk ialah jika anda ingin menggabungkan data daripada dua pangkalan data menjadi satu (contohnya, memutuskan bahawa satu pangkalan data cukup untuk pelanggan ini). Anda boleh menambah beberapa AtomicLongs pada Hazelcast dan menyimpan kaunter di sana, maka prestasi mendapatkan ID baharu ialah incrementAndGet ditambah masa untuk permintaan kepada Hazelcast. Tetapi Hazelcast mempunyai sesuatu yang lebih optimum - FlakeIdGenerator. Apabila menghubungi setiap pelanggan, mereka diberikan julat ID, contohnya, yang pertama - dari 1 hingga 10, yang kedua - dari 000 hingga 10, dan seterusnya. Kini pelanggan boleh mengeluarkan pengecam baharu sendiri sehingga julat yang dikeluarkan kepadanya tamat. Ia berfungsi dengan cepat, tetapi apabila anda memulakan semula aplikasi (dan klien Hazelcast), urutan baharu bermula - oleh itu langkauan, dsb. Di samping itu, pembangun tidak benar-benar memahami mengapa ID adalah integer, tetapi sangat tidak konsisten. Kami menimbang segala-galanya dan beralih kepada UUID.

Ngomong-ngomong, bagi mereka yang ingin menjadi seperti Twitter, terdapat perpustakaan Snowcast - ini adalah pelaksanaan Snowflake di atas Hazelcast. Anda boleh melihatnya di sini:

github.com/noctarius/snowcast
github.com/twitter/snowflake

Tetapi kami tidak melakukannya lagi.

TransactionalMap.replace

Satu lagi kejutan: TransactionalMap.replace tidak berfungsi. Berikut adalah ujian:

@Test
public void replaceInMap_putsAndGetsInsideTransaction() {

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            context.getMap("map").put("key", "oldValue");
            context.getMap("map").replace("key", "oldValue", "newValue");
            
            String value = (String) context.getMap("map").get("key");
            assertEquals("newValue", value);

            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }        
    });
}

Expected : newValue
Actual : oldValue

Saya terpaksa menulis pengganti saya sendiri menggunakan getForUpdate:

protected <K,V> boolean replaceInMap(String mapName, K key, V oldValue, V newValue) {
    TransactionalTaskContext context = HazelcastTransactionContextHolder.getContext();
    if (context != null) {
        log.trace("[CACHE] Replacing value in a transactional map");
        TransactionalMap<K, V> map = context.getMap(mapName);
        V value = map.getForUpdate(key);
        if (oldValue.equals(value)) {
            map.put(key, newValue);
            return true;
        }

        return false;
    }
    log.trace("[CACHE] Replacing value in a not transactional map");
    IMap<K, V> map = hazelcastInstance.getMap(mapName);
    return map.replace(key, oldValue, newValue);
}

Uji bukan sahaja struktur data biasa, tetapi juga versi transaksinya. Ia berlaku bahawa IMap berfungsi, tetapi TransactionalMap tidak lagi wujud.

Masukkan JAR baharu tanpa masa henti

Mula-mula, kami memutuskan untuk merakam objek kelas kami dalam Hazelcast. Sebagai contoh, kami mempunyai kelas Aplikasi, kami ingin menyimpan dan membacanya. Simpan:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
map.set(id, application);

Kami baca:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
return map.get(id);

Semuanya berfungsi. Kemudian kami memutuskan untuk membina indeks dalam Hazelcast untuk mencari melalui:

map.addIndex("subscriberId", false);

Dan apabila menulis entiti baharu, mereka mula menerima ClassNotFoundException. Hazelcast cuba menambah indeks, tetapi tidak tahu apa-apa tentang kelas kami dan mahu JAR dengan kelas ini dibekalkan kepadanya. Kami melakukan perkara itu, semuanya berfungsi, tetapi masalah baru muncul: bagaimana untuk mengemas kini JAR tanpa menghentikan kluster sepenuhnya? Hazelcast tidak mengambil JAR baharu semasa kemas kini nod demi nod. Pada ketika ini kami memutuskan bahawa kami boleh hidup tanpa carian indeks. Lagipun, jika anda menggunakan Hazelcast sebagai kedai nilai kunci, maka semuanya akan berfungsi? Tidak begitu. Di sini sekali lagi tingkah laku IMap dan TransactionalMap adalah berbeza. Di mana IMap tidak peduli, TransactionalMap melemparkan ralat.

IMap. Kami menulis 5000 objek, membacanya. Semuanya dijangka.

@Test
void get5000() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application");
    UUID subscriberId = UUID.randomUUID();

    for (int i = 0; i < 5000; i++) {
        UUID id = UUID.randomUUID();
        String title = RandomStringUtils.random(5);
        Application application = new Application(id, title, subscriberId);
        
        map.set(id, application);
        Application retrieved = map.get(id);
        assertEquals(id, retrieved.getId());
    }
}

Tetapi ia tidak berfungsi dalam transaksi, kami mendapat ClassNotFoundException:

@Test
void get_transaction() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application_t");
    UUID subscriberId = UUID.randomUUID();
    UUID id = UUID.randomUUID();

    Application application = new Application(id, "qwer", subscriberId);
    map.set(id, application);
    
    Application retrievedOutside = map.get(id);
    assertEquals(id, retrievedOutside.getId());

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            TransactionalMap<UUID, Application> transactionalMap = context.getMap("application_t");
            Application retrievedInside = transactionalMap.get(id);

            assertEquals(id, retrievedInside.getId());
            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }
    });
}

Dalam 3.8, mekanisme Penerapan Kelas Pengguna muncul. Anda boleh menetapkan satu nod induk dan mengemas kini fail JAR padanya.

Kini kami telah mengubah sepenuhnya pendekatan kami: kami mensirikannya sendiri ke dalam JSON dan menyimpannya dalam Hazelcast. Hazelcast tidak perlu mengetahui struktur kelas kami dan kami boleh mengemas kini tanpa masa henti. Versi objek domain dikawal oleh aplikasi. Versi aplikasi yang berbeza boleh dijalankan pada masa yang sama, dan situasi mungkin berlaku apabila aplikasi baharu menulis objek dengan medan baharu, tetapi yang lama belum mengetahui tentang medan ini. Dan pada masa yang sama, aplikasi baru membaca objek yang ditulis oleh aplikasi lama yang tidak mempunyai medan baru. Kami mengendalikan situasi sedemikian dalam aplikasi, tetapi untuk memudahkan kami tidak menukar atau memadam medan, kami hanya mengembangkan kelas dengan menambah medan baharu.

Bagaimana kami memastikan prestasi tinggi

Empat perjalanan ke Hazelcast - baik, dua ke pangkalan data - buruk

Pergi ke cache untuk data sentiasa lebih baik daripada pergi ke pangkalan data, tetapi anda juga tidak mahu menyimpan rekod yang tidak digunakan. Kami meninggalkan keputusan tentang perkara yang hendak dicache sehingga peringkat terakhir pembangunan. Apabila fungsi baharu dikodkan, kami menghidupkan pengelogan semua pertanyaan dalam PostgreSQL (log_min_duration_statement to 0) dan menjalankan ujian beban selama 20 minit. Menggunakan log yang dikumpul, utiliti seperti pgFouine dan pgBadger boleh membina laporan analisis. Dalam laporan, kami terutamanya mencari pertanyaan yang perlahan dan kerap. Untuk pertanyaan perlahan, kami membina pelan pelaksanaan (EXPLAIN) dan menilai sama ada pertanyaan sedemikian boleh dipercepatkan. Permintaan yang kerap untuk data input yang sama sesuai dengan baik ke dalam cache. Kami cuba mengekalkan pertanyaan "rata", satu jadual bagi setiap pertanyaan.

eksploitasi

SV sebagai perkhidmatan dalam talian telah mula beroperasi pada musim bunga 2017, dan sebagai produk berasingan, SV telah dikeluarkan pada November 2017 (pada masa itu dalam status versi beta).

Dalam lebih setahun beroperasi, tiada masalah serius dalam pengendalian perkhidmatan dalam talian CB. Kami memantau perkhidmatan dalam talian melalui Zabbix, kumpulkan dan gunakan daripada Buluh.

Pengedaran pelayan SV dibekalkan dalam bentuk pakej asli: RPM, DEB, MSI. Tambahan untuk Windows kami menyediakan pemasang tunggal dalam bentuk EXE tunggal yang memasang pelayan, Hazelcast dan Elasticsearch pada satu mesin. Kami pada mulanya merujuk kepada versi pemasangan ini sebagai versi "demo", tetapi kini telah menjadi jelas bahawa ini adalah pilihan penggunaan yang paling popular.

Sumber: www.habr.com

Tambah komen