Bagaimana dan mengapa kami menulis layanan skalabel beban tinggi untuk 1C: Perusahaan: Java, PostgreSQL, Hazelcast

Pada artikel ini kita akan membahas tentang bagaimana dan mengapa kami berkembang Sistem Interaksi – mekanisme yang mentransfer informasi antara aplikasi klien dan server 1C:Enterprise - mulai dari menetapkan tugas hingga memikirkan arsitektur dan detail implementasi.

Sistem Interaksi (selanjutnya disebut SV) adalah sistem pesan yang terdistribusi dan toleran terhadap kesalahan dengan pengiriman yang terjamin. SV dirancang sebagai layanan beban tinggi dengan skalabilitas tinggi, tersedia baik sebagai layanan online (disediakan oleh 1C) dan sebagai produk produksi massal yang dapat diterapkan pada fasilitas server Anda sendiri.

SV menggunakan penyimpanan terdistribusi Siaran Hazel dan mesin pencari Elasticsearch. Kami juga akan berbicara tentang Java dan bagaimana kami menskalakan PostgreSQL secara horizontal.
Bagaimana dan mengapa kami menulis layanan skalabel beban tinggi untuk 1C: Perusahaan: Java, PostgreSQL, Hazelcast

Pernyataan masalah

Untuk memperjelas alasan kami membuat Sistem Interaksi, saya akan bercerita sedikit tentang cara kerja pengembangan aplikasi bisnis di 1C.

Pertama, sedikit tentang kami bagi yang belum tahu apa yang kami lakukan :) Kami membuat platform teknologi 1C:Enterprise. Platform ini mencakup alat pengembangan aplikasi bisnis, serta runtime yang memungkinkan aplikasi bisnis berjalan di lingkungan lintas platform.

Paradigma pengembangan klien-server

Aplikasi bisnis yang dibuat di 1C:Enterprise beroperasi dalam tiga tingkat server klien arsitektur “DBMS – server aplikasi – klien”. Kode aplikasi tertulis di bahasa 1C bawaan, dapat dijalankan di server aplikasi atau di klien. Semua pekerjaan dengan objek aplikasi (direktori, dokumen, dll.), serta membaca dan menulis database, hanya dilakukan di server. Fungsionalitas formulir dan antarmuka perintah juga diimplementasikan di server. Klien melakukan penerimaan, pembukaan dan tampilan formulir, “berkomunikasi” dengan pengguna (peringatan, pertanyaan...), perhitungan kecil dalam formulir yang memerlukan respons cepat (misalnya, mengalikan harga dengan kuantitas), bekerja dengan file lokal, bekerja dengan peralatan.

Dalam kode aplikasi, header prosedur dan fungsi harus secara eksplisit menunjukkan di mana kode akan dieksekusi - menggunakan arahan &AtClient / &AtServer (&AtClient / &AtServer dalam bahasa versi bahasa Inggris). Pengembang 1C sekarang akan mengoreksi saya dengan mengatakan bahwa sebenarnya ada arahan lebih baik, tapi bagi kami ini tidak penting sekarang.

Anda dapat memanggil kode server dari kode klien, namun Anda tidak dapat memanggil kode klien dari kode server. Ini adalah batasan mendasar yang kami buat karena sejumlah alasan. Khususnya, karena kode server harus ditulis sedemikian rupa sehingga dijalankan dengan cara yang sama di mana pun ia dipanggil - dari klien atau dari server. Dan jika kode server dipanggil dari kode server lain, tidak ada klien seperti itu. Dan karena selama eksekusi kode server, klien yang memanggilnya dapat menutup, keluar dari aplikasi, dan server tidak lagi memiliki siapa pun untuk dihubungi.

Bagaimana dan mengapa kami menulis layanan skalabel beban tinggi untuk 1C: Perusahaan: Java, PostgreSQL, Hazelcast
Kode yang menangani klik tombol: memanggil prosedur server dari klien akan berfungsi, memanggil prosedur klien dari server tidak akan berhasil

Artinya jika kita ingin mengirim pesan dari server ke aplikasi klien, misalnya pembuatan laporan yang “berjalan lama” telah selesai dan laporan dapat dilihat, kita tidak memiliki metode seperti itu. Anda harus menggunakan trik, misalnya melakukan polling server secara berkala dari kode klien. Namun pendekatan ini memuat sistem dengan panggilan yang tidak perlu, dan secara umum tidak terlihat elegan.

Dan ada juga kebutuhannya, misalnya saat ada panggilan telepon SIP- saat melakukan panggilan, beri tahu aplikasi klien tentang hal ini sehingga dapat menggunakan nomor penelepon untuk menemukannya di database rekanan dan menampilkan informasi pengguna tentang rekanan yang menelepon. Atau, misalnya, ketika pesanan tiba di gudang, beri tahu aplikasi klien pelanggan tentang hal ini. Secara umum, ada banyak kasus di mana mekanisme seperti itu bermanfaat.

Produksi itu sendiri

Buat mekanisme pengiriman pesan. Cepat, andal, dengan jaminan pengiriman, dengan kemampuan mencari pesan secara fleksibel. Berdasarkan mekanismenya, mengimplementasikan messenger (pesan, panggilan video) yang berjalan di dalam aplikasi 1C.

Rancang sistem agar dapat diskalakan secara horizontal. Peningkatan beban tersebut harus ditutupi dengan penambahan jumlah node.

Implementasi

Kami memutuskan untuk tidak mengintegrasikan bagian server SV langsung ke platform 1C:Enterprise, tetapi mengimplementasikannya sebagai produk terpisah, API yang dapat dipanggil dari kode solusi aplikasi 1C. Hal ini dilakukan karena beberapa alasan, yang utama adalah saya ingin memungkinkan pertukaran pesan antara aplikasi 1C yang berbeda (misalnya, antara Manajemen Perdagangan dan Akuntansi). Aplikasi 1C yang berbeda dapat berjalan di versi platform 1C:Enterprise yang berbeda, ditempatkan di server yang berbeda, dll. Dalam kondisi seperti itu, penerapan SV sebagai produk terpisah yang terletak “di samping” instalasi 1C merupakan solusi optimal.

Jadi, kami memutuskan untuk menjadikan SV sebagai produk tersendiri. Kami menyarankan perusahaan kecil menggunakan server CB yang kami pasang di cloud kami (wss://1cdialog.com) untuk menghindari biaya overhead yang terkait dengan instalasi lokal dan konfigurasi server. Klien besar mungkin disarankan untuk memasang server CB mereka sendiri di fasilitas mereka. Kami menggunakan pendekatan serupa di produk cloud SaaS kami 1cSegar – diproduksi sebagai produk yang diproduksi secara massal untuk dipasang di lokasi klien, dan juga diterapkan di cloud kami https://1cfresh.com/.

Web

Untuk mendistribusikan beban dan toleransi kesalahan, kami akan menerapkan tidak hanya satu aplikasi Java, namun beberapa, dengan penyeimbang beban di depannya. Jika Anda perlu mentransfer pesan dari satu node ke node lain, gunakan terbitkan/berlangganan di Hazelcast.

Komunikasi antara klien dan server dilakukan melalui websocket. Ini sangat cocok untuk sistem real-time.

Cache terdistribusi

Kami memilih antara Redis, Hazelcast, dan Ehcache. Ini tahun 2015. Redis baru saja merilis cluster baru (terlalu baru, menakutkan), ada Sentinel dengan banyak batasan. Ehcache tidak tahu cara berkumpul menjadi sebuah cluster (fungsi ini muncul kemudian). Kami memutuskan untuk mencobanya dengan Hazelcast 3.4.
Hazelcast dirakit menjadi sebuah cluster di luar kotak. Dalam mode node tunggal, ini tidak terlalu berguna dan hanya dapat digunakan sebagai cache - ia tidak tahu cara membuang data ke disk, jika Anda kehilangan satu-satunya node, Anda kehilangan data. Kami menerapkan beberapa Hazelcast, yang di antaranya kami mencadangkan data penting. Kami tidak mencadangkan cache – kami tidak mempermasalahkannya.

Bagi kami, Hazelcast adalah:

  • Penyimpanan sesi pengguna. Dibutuhkan waktu lama untuk masuk ke database untuk satu sesi setiap saat, jadi kami menempatkan semua sesi di Hazelcast.
  • Cache. Jika Anda mencari profil pengguna, periksa cache. Tulis pesan baru - simpan di cache.
  • Topik untuk komunikasi antar instance aplikasi. Node tersebut menghasilkan sebuah peristiwa dan menempatkannya di topik Hazelcast. Node aplikasi lain yang berlangganan topik ini menerima dan memproses acara tersebut.
  • Kunci cluster. Misalnya, kami membuat diskusi menggunakan kunci unik (diskusi tunggal dalam database 1C):

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

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

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

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

Kami memeriksa bahwa tidak ada saluran. Kami mengambil kuncinya, memeriksanya lagi, dan membuatnya. Jika Anda tidak memeriksa kunci setelah mengambil kunci, maka ada kemungkinan thread lain juga memeriksa pada saat itu dan sekarang akan mencoba membuat diskusi yang sama - tetapi diskusi tersebut sudah ada. Anda tidak dapat mengunci menggunakan Java Lock yang disinkronkan atau biasa. Melalui database - lambat, dan sayang sekali untuk database; melalui Hazelcast - itulah yang Anda butuhkan.

Memilih DBMS

Kami memiliki pengalaman luas dan sukses bekerja dengan PostgreSQL dan berkolaborasi dengan pengembang DBMS ini.

Tidak mudah dengan cluster PostgreSQL - memang ada XL, XC, jeruk, namun secara umum ini bukanlah NoSQL yang dapat diperluas skalanya. Kami tidak menganggap NoSQL sebagai penyimpanan utama; kami cukup menggunakan Hazelcast, yang belum pernah kami gunakan sebelumnya.

Jika Anda perlu menskalakan database relasional, itu artinya pecahan. Seperti yang Anda ketahui, dengan sharding kami membagi database menjadi beberapa bagian terpisah sehingga masing-masing dapat ditempatkan di server terpisah.

Versi pertama sharding kami mengasumsikan kemampuan untuk mendistribusikan setiap tabel aplikasi kami ke server yang berbeda dalam proporsi yang berbeda. Ada banyak pesan di server A - tolong, mari kita pindahkan sebagian tabel ini ke server B. Keputusan ini hanya menunjukkan optimasi prematur, jadi kami memutuskan untuk membatasi diri pada pendekatan multi-penyewa.

Anda dapat membaca tentang multi-tenant, misalnya di website Data Citus.

SV memiliki konsep aplikasi dan pelanggan. Aplikasi adalah instalasi spesifik aplikasi bisnis, seperti ERP atau Akuntansi, beserta pengguna dan data bisnisnya. Pelanggan adalah organisasi atau individu yang atas namanya aplikasi terdaftar di server SV. Seorang pelanggan dapat mendaftarkan beberapa aplikasi, dan aplikasi tersebut dapat saling bertukar pesan. Pelanggan menjadi penyewa di sistem kami. Pesan dari beberapa pelanggan dapat ditempatkan dalam satu database fisik; jika kami melihat pelanggan mulai menghasilkan banyak lalu lintas, kami memindahkannya ke database fisik terpisah (atau bahkan server database terpisah).

Kami memiliki database utama tempat tabel routing disimpan dengan informasi tentang lokasi semua database pelanggan.

Bagaimana dan mengapa kami menulis layanan skalabel beban tinggi untuk 1C: Perusahaan: Java, PostgreSQL, Hazelcast

Untuk mencegah database utama mengalami kemacetan, kami menyimpan tabel perutean (dan data lain yang sering dibutuhkan) dalam cache.

Jika database pelanggan mulai melambat, kami akan memotongnya menjadi beberapa partisi di dalamnya. Pada proyek lain yang kami gunakan pg_pathman.

Karena kehilangan pesan pengguna itu buruk, kami memelihara database kami dengan replika. Kombinasi replika sinkron dan asinkron memungkinkan Anda mengasuransikan diri jika terjadi kehilangan database utama. Kehilangan pesan hanya akan terjadi jika database utama dan replika sinkronnya gagal secara bersamaan.

Jika replika sinkron hilang, replika asinkron menjadi sinkron.
Jika database utama hilang, replika sinkron menjadi database utama, dan replika asinkron menjadi replika sinkron.

Pencarian elastis untuk pencarian

Karena SV juga merupakan seorang messenger, maka diperlukan pencarian yang cepat, nyaman dan fleksibel, dengan mempertimbangkan morfologi, menggunakan pencocokan yang tidak tepat. Kami memutuskan untuk tidak menemukan kembali roda dan menggunakan mesin pencari gratis Elasticsearch, yang dibuat berdasarkan perpustakaan Lusen. Kami juga menerapkan Elasticsearch dalam sebuah cluster (master – data – data) untuk menghilangkan masalah jika terjadi kegagalan node aplikasi.

Di github kami menemukan Plugin morfologi Rusia untuk Elasticsearch dan menggunakannya. Dalam indeks Elasticsearch kami menyimpan akar kata (yang ditentukan oleh plugin) dan N-gram. Saat pengguna memasukkan teks untuk dicari, kami mencari teks yang diketik di antara N-gram. Saat disimpan ke indeks, kata “teks” akan dibagi menjadi N-gram berikut:

[itu, tek, tex, teks, teks, ek, ex, ext, teks, ks, kst, ksty, st, sty, kamu],

Dan akar kata “teks” juga akan dipertahankan. Pendekatan ini memungkinkan Anda mencari di awal, di tengah, dan di akhir kata.

Gambar besar

Bagaimana dan mengapa kami menulis layanan skalabel beban tinggi untuk 1C: Perusahaan: Java, PostgreSQL, Hazelcast
Ulangi gambar dari awal artikel, namun dengan penjelasan:

  • Penyeimbang terekspos di Internet; kami punya nginx, bisa apa saja.
  • Contoh aplikasi Java berkomunikasi satu sama lain melalui Hazelcast.
  • Untuk bekerja dengan soket web yang kami gunakan Netty.
  • Aplikasi Java ditulis dalam Java 8 dan terdiri dari bundel OSGI. Rencananya mencakup migrasi ke Java 10 dan transisi ke modul.

Pengembangan dan pengujian

Dalam proses pengembangan dan pengujian SV, kami menemukan sejumlah fitur menarik dari produk yang kami gunakan.

Pengujian beban dan kebocoran memori

Rilis setiap rilis SV melibatkan pengujian beban. Ini berhasil ketika:

  • Pengujian berhasil selama beberapa hari dan tidak ada kegagalan layanan
  • Waktu respons untuk operasi utama tidak melebihi ambang batas yang nyaman
  • Penurunan performa dibandingkan versi sebelumnya tidak lebih dari 10%

Kami mengisi database pengujian dengan data - untuk melakukan ini, kami menerima informasi tentang pelanggan paling aktif dari server produksi, mengalikan angkanya dengan 5 (jumlah pesan, diskusi, pengguna) dan mengujinya seperti itu.

Kami melakukan pengujian beban sistem interaksi dalam tiga konfigurasi:

  1. tes stres
  2. Koneksi saja
  3. Pendaftaran pelanggan

Selama stress test, kami meluncurkan beberapa ratus thread, dan thread tersebut memuat sistem tanpa henti: menulis pesan, membuat diskusi, menerima daftar pesan. Kami mensimulasikan tindakan pengguna biasa (mendapatkan daftar pesan saya yang belum dibaca, menulis kepada seseorang) dan solusi perangkat lunak (mentransmisikan paket dengan konfigurasi berbeda, memproses peringatan).

Misalnya, seperti inilah bagian dari stress test:

  • Pengguna masuk
    • Meminta diskusi Anda yang belum dibaca
    • 50% kemungkinan membaca pesan
    • 50% kemungkinan akan mengirim pesan teks
    • Pengguna berikutnya:
      • Memiliki peluang 20% ​​untuk membuat diskusi baru
      • Memilih secara acak salah satu diskusinya
      • Masuk ke dalam
      • Meminta pesan, profil pengguna
      • Membuat lima pesan yang ditujukan kepada pengguna acak dari diskusi ini
      • Meninggalkan diskusi
      • Ulangi sebanyak 20 kali
      • Logout, kembali ke awal skrip

    • Chatbot memasuki sistem (mengemulasi pesan dari kode aplikasi)
      • Memiliki peluang 50% untuk membuat saluran baru untuk pertukaran data (diskusi khusus)
      • 50% kemungkinan akan menulis pesan ke salah satu saluran yang ada

Skenario “Hanya Koneksi” muncul karena suatu alasan. Ada situasi: pengguna telah menghubungkan sistem, namun belum terlibat. Setiap pengguna menyalakan komputer pada pukul 09:00 pagi, membuat koneksi ke server dan tetap diam. Orang-orang ini berbahaya, ada banyak dari mereka - satu-satunya paket yang mereka miliki adalah PING/PONG, tetapi mereka tetap menjaga koneksi ke server (mereka tidak dapat mempertahankannya - bagaimana jika ada pesan baru). Pengujian ini mereproduksi situasi di mana sejumlah besar pengguna mencoba masuk ke sistem dalam waktu setengah jam. Ini mirip dengan stress test, tetapi fokusnya justru pada masukan pertama ini - sehingga tidak ada kegagalan (seseorang tidak menggunakan sistem, dan sistem sudah jatuh - sulit untuk memikirkan sesuatu yang lebih buruk).

Skrip pendaftaran pelanggan dimulai dari peluncuran pertama. Kami melakukan stress test dan yakin bahwa sistem tidak melambat selama korespondensi. Namun pengguna datang dan pendaftaran mulai gagal karena batas waktu. Saat mendaftar kami menggunakan / dev / random, yang terkait dengan entropi sistem. Server tidak punya waktu untuk mengumpulkan entropi yang cukup dan ketika SecureRandom baru diminta, server terhenti selama puluhan detik. Ada banyak jalan keluar dari situasi ini, misalnya: beralih ke /dev/urandom yang kurang aman, memasang papan khusus yang menghasilkan entropi, menghasilkan angka acak terlebih dahulu dan menyimpannya dalam kumpulan. Kami menutup sementara masalah dengan pool, namun sejak itu kami telah menjalankan tes terpisah untuk mendaftarkan pelanggan baru.

Kami menggunakannya sebagai generator beban JMeter. Ia tidak tahu cara bekerja dengan websocket; ia memerlukan plugin. Yang pertama dalam hasil pencarian untuk kueri “jmeter websocket” adalah: artikel dari BlazeMeter, yang merekomendasikan plugin oleh Maciej Zaleski.

Di situlah kami memutuskan untuk memulai.

Hampir segera setelah memulai pengujian serius, kami menemukan bahwa JMeter mulai membocorkan memori.

Plugin ini merupakan cerita besar yang terpisah; dengan 176 bintang, ia memiliki 132 fork di github. Penulis sendiri belum berkomitmen sejak tahun 2015 (kami ambil tahun 2015, lalu tidak menimbulkan kecurigaan), beberapa masalah github terkait kebocoran memori, 7 pull request yang belum ditutup.
Jika Anda memutuskan untuk melakukan load test menggunakan plugin ini, harap memperhatikan pembahasan berikut:

  1. Dalam lingkungan multi-utas, LinkedList biasa digunakan, dan hasilnya adalah NPE dalam waktu proses. Hal ini dapat diatasi dengan beralih ke ConcurrentLinkedDeque atau dengan blok yang disinkronkan. Kami memilih opsi pertama untuk diri kami sendiri (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Kebocoran memori; saat memutuskan sambungan, informasi koneksi tidak terhapus (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. Dalam mode streaming (ketika soket web tidak ditutup pada akhir sampel, namun digunakan nanti dalam rencana), pola respons tidak berfungsi (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

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

  1. Telah diambil garpu Elyran Kogan (@elyrank) – ini memperbaiki masalah 1 dan 3
  2. Masalah terpecahkan 2
  3. Dermaga yang diperbarui dari 9.2.14 menjadi 9.3.12
  4. Membungkus SimpleDateFormat dalam ThreadLocal; SimpleDateFormat tidak aman untuk thread, yang menyebabkan NPE saat runtime
  5. Memperbaiki kebocoran memori lainnya (koneksi ditutup secara tidak benar saat terputus)

Namun itu mengalir!

Memori mulai habis bukan dalam sehari, tapi dalam dua hari. Sama sekali tidak ada waktu tersisa, jadi kami memutuskan untuk meluncurkan lebih sedikit thread, tetapi pada empat agen. Ini seharusnya cukup untuk setidaknya satu minggu.

Dua hari telah berlalu...

Sekarang Hazelcast kehabisan memori. Log menunjukkan bahwa setelah beberapa hari pengujian, Hazelcast mulai mengeluh tentang kurangnya memori, dan setelah beberapa waktu cluster tersebut berantakan, dan node terus mati satu per satu. Kami menghubungkan JVisualVM ke hazelcast dan melihat "gergaji yang meningkat" - yang biasa disebut GC, tetapi tidak dapat menghapus memori.

Bagaimana dan mengapa kami menulis layanan skalabel beban tinggi untuk 1C: Perusahaan: Java, PostgreSQL, Hazelcast

Ternyata di hazelcast 3.4, saat menghapus peta/multiMap (map.destroy()), memori tidak sepenuhnya terbebas:

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

Bug ini sekarang telah diperbaiki di versi 3.5, tetapi saat itu merupakan masalah. Kami membuat multiMaps baru dengan nama dinamis dan menghapusnya sesuai logika kami. Kodenya terlihat 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();
    }
}

Jawaban:

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

multiMap dibuat untuk setiap langganan dan dihapus jika tidak diperlukan. Kami memutuskan bahwa kami akan memulai Map , kuncinya adalah nama langganan, dan nilainya adalah pengidentifikasi sesi (yang kemudian Anda dapat memperoleh pengidentifikasi 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());
}

Grafiknya telah membaik.

Bagaimana dan mengapa kami menulis layanan skalabel beban tinggi untuk 1C: Perusahaan: Java, PostgreSQL, Hazelcast

Apa lagi yang telah kita pelajari tentang pengujian beban?

  1. JSR223 perlu ditulis dengan cara yang asyik dan menyertakan cache kompilasi - ini jauh lebih cepat. Link.
  2. Grafik Jmeter-Plugins lebih mudah dipahami daripada grafik standar. Link.

Tentang pengalaman kami dengan Hazelcast

Hazelcast adalah produk baru bagi kami, kami mulai mengerjakannya dari versi 3.4.1, sekarang server produksi kami menjalankan versi 3.9.2 (pada saat penulisan, versi terbaru Hazelcast adalah 3.10).

pembuatan ID

Kami mulai dengan pengidentifikasi bilangan bulat. Mari kita bayangkan bahwa kita memerlukan Long yang lain untuk entitas baru. Urutan di database tidak sesuai, tabel terlibat dalam sharding - ternyata ada pesan ID=1 di DB1 dan pesan ID=1 di DB2, Anda tidak dapat memasukkan ID ini di Elasticsearch, atau di Hazelcast , tetapi yang terburuk adalah jika Anda ingin menggabungkan data dari dua database menjadi satu (misalnya, memutuskan bahwa satu database cukup untuk pelanggan tersebut). Anda dapat menambahkan beberapa AtomicLongs ke Hazelcast dan menyimpan penghitung di sana, maka kinerja untuk mendapatkan ID baru adalah incrementAndGet ditambah waktu untuk permintaan ke Hazelcast. Namun Hazelcast memiliki sesuatu yang lebih optimal - FlakeIdGenerator. Saat menghubungi setiap klien, mereka diberikan rentang ID, misalnya yang pertama – dari 1 hingga 10, yang kedua – dari 000 hingga 10, dan seterusnya. Sekarang klien dapat mengeluarkan pengidentifikasi baru sendiri hingga rentang yang dikeluarkan untuknya berakhir. Ini bekerja dengan cepat, tetapi ketika Anda me-restart aplikasi (dan klien Hazelcast), urutan baru dimulai - karenanya ada lompatan, dll. Selain itu, pengembang tidak begitu memahami mengapa ID-nya bilangan bulat, tetapi sangat tidak konsisten. Kami menimbang semuanya dan beralih ke UUID.

Omong-omong, bagi mereka yang ingin seperti Twitter, ada perpustakaan Snowcast - ini adalah implementasi Snowflake di atas Hazelcast. Anda dapat melihatnya di sini:

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

Tapi kami belum sempat melakukannya lagi.

Peta Transaksional.ganti

Kejutan lainnya: TransactionalMap.replace tidak berfungsi. Ini tesnya:

@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 harus 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 tidak hanya struktur data biasa, tetapi juga versi transaksionalnya. Kebetulan IMap berfungsi, tetapi TransactionalMap sudah tidak ada lagi.

Masukkan JAR baru tanpa downtime

Pertama, kami memutuskan untuk merekam objek kelas kami di Hazelcast. Misalnya kita mempunyai kelas Aplikasi, kita ingin menyimpan dan membacanya. Menyimpan:

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

Kami membaca:

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

Semuanya berfungsi. Kemudian kami memutuskan untuk membuat indeks di Hazelcast untuk mencari berdasarkan:

map.addIndex("subscriberId", false);

Dan saat menulis entitas baru, mereka mulai menerima ClassNotFoundException. Hazelcast mencoba menambahkan ke indeks, tetapi tidak tahu apa pun tentang kelas kami dan ingin JAR dengan kelas ini disertakan ke dalamnya. Kami melakukan hal itu, semuanya berfungsi, tetapi masalah baru muncul: bagaimana cara memperbarui JAR tanpa menghentikan cluster sepenuhnya? Hazelcast tidak mengambil JAR baru selama pembaruan node demi node. Pada titik ini kami memutuskan bahwa kami dapat hidup tanpa pencarian indeks. Lagi pula, jika Anda menggunakan Hazelcast sebagai penyimpan nilai kunci, semuanya akan berfungsi? Tidak terlalu. Di sini sekali lagi perilaku IMap dan TransactionalMap berbeda. Jika IMap tidak peduli, TransactionalMap menimbulkan kesalahan.

Peta IM. Kami menulis 5000 objek, membacanya. Semuanya diharapkan.

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

Tapi itu tidak berfungsi dalam transaksi, kami mendapatkan 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();
        }
    });
}

Di 3.8, mekanisme Penerapan Kelas Pengguna muncul. Anda dapat menunjuk satu node master dan memperbarui file JAR di dalamnya.

Sekarang kami telah sepenuhnya mengubah pendekatan kami: kami membuat serial sendiri ke dalam JSON dan menyimpannya di Hazelcast. Hazelcast tidak perlu mengetahui struktur kelas kami, dan kami dapat memperbarui tanpa downtime. Pembuatan versi objek domain dikontrol oleh aplikasi. Versi aplikasi yang berbeda dapat berjalan pada waktu yang sama, dan mungkin ada situasi ketika aplikasi baru menulis objek dengan bidang baru, tetapi aplikasi lama belum mengetahui tentang bidang ini. Dan pada saat yang sama, aplikasi baru membaca objek yang ditulis oleh aplikasi lama yang tidak memiliki field baru. Kami menangani situasi seperti itu dalam aplikasi, tetapi untuk kesederhanaan kami tidak mengubah atau menghapus bidang, kami hanya memperluas kelas dengan menambahkan bidang baru.

Bagaimana kami memastikan kinerja tinggi

Empat perjalanan ke Hazelcast - bagus, dua ke database - buruk

Pergi ke cache untuk data selalu lebih baik daripada pergi ke database, tapi Anda juga tidak ingin menyimpan catatan yang tidak terpakai. Kami menyerahkan keputusan tentang apa yang akan di-cache hingga tahap terakhir pengembangan. Saat fungsionalitas baru diberi kode, kami mengaktifkan pencatatan semua kueri di PostgreSQL (log_min_duration_statement hingga 0) dan menjalankan pengujian beban selama 20 menit.Dengan menggunakan log yang dikumpulkan, utilitas seperti pgFouine dan pgBadger dapat membuat laporan analitis. Dalam laporan, kami terutama mencari kueri yang lambat dan sering. Untuk kueri yang lambat, kami membuat rencana eksekusi (JELASKAN) dan mengevaluasi apakah kueri tersebut dapat dipercepat. Permintaan yang sering untuk data masukan yang sama cocok dengan cache. Kami mencoba menjaga kueri tetap “datar”, satu tabel per kueri.

Eksploitasi

SV sebagai layanan online mulai dioperasikan pada musim semi tahun 2017, dan sebagai produk tersendiri, SV dirilis pada bulan November 2017 (saat itu dalam status versi beta).

Selama lebih dari setahun beroperasi, tidak ada kendala serius dalam pengoperasian layanan CB online. Kami memantau layanan online melalui Zabbix, kumpulkan dan sebarkan dari Bambu.

Distribusi server SV disediakan dalam bentuk paket asli: RPM, DEB, MSI. Ditambah lagi untuk Windows kami menyediakan single installer berupa single EXE yang menginstall server, Hazelcast dan Elasticsearch dalam satu mesin. Awalnya kami menyebut versi penginstalan ini sebagai versi “demo”, namun kini menjadi jelas bahwa ini adalah opsi penerapan yang paling populer.

Sumber: www.habr.com

Tambah komentar