Kami di perusahaan selalu berusaha untuk meningkatkan kelestarian kode kami, menggunakan praktik yang diterima secara umum, termasuk dalam hal multithreading. Ini tidak menyelesaikan semua kesulitan yang dibawa oleh beban yang terus meningkat, tetapi menyederhanakan dukungan - ia juga memenangkan keterbacaan kode dan kecepatan mengembangkan fitur baru.
Kami sekarang memiliki 47.000 pengguna setiap hari, sekitar 30 server dalam produksi, 2.000 permintaan API per detik, dan rilis harian. Layanan Miro telah berkembang sejak 2011, dan dalam implementasi saat ini, permintaan pengguna diproses secara paralel oleh sekelompok server heterogen.

Subsistem Kontrol Akses yang Kompetitif
Nilai utama dari produk kami adalah papan pengguna yang kolaboratif, jadi beban utama ada pada mereka. Subsistem utama yang mengendalikan sebagian besar akses kompetitif adalah sistem stateful sesi pengguna di papan tulis.
Untuk setiap papan yang dapat dibuka di salah satu server, statusnya naik. Ini menyimpan data runtime aplikasi yang diperlukan untuk memastikan kolaborasi dan tampilan konten, serta data sistem, seperti mengikat ke pemrosesan utas. Informasi tentang server tempat negara disimpan ditulis ke struktur terdistribusi dan tersedia untuk cluster selama server berjalan, dan setidaknya satu pengguna ada di papan tulis. Kami menggunakan Hazelcast untuk menyediakan bagian dari subsistem ini. Semua koneksi baru ke board dikirim ke server dengan status ini.
Saat menyambung ke server, pengguna memasuki aliran penerima, yang tugas utamanya adalah mengikat koneksi ke keadaan papan yang sesuai, di mana semua pekerjaan selanjutnya akan terjadi.
Dua aliran dikaitkan dengan papan: jaringan, pemrosesan koneksi, dan "bisnis", yang bertanggung jawab untuk logika bisnis. Ini memungkinkan Anda untuk mengubah pelaksanaan tugas-tugas heterogen dari pemrosesan paket jaringan dan menjalankan perintah bisnis dari serial ke paralel. Perintah jaringan yang diproses dari pengguna membentuk tugas bisnis terapan dan mengarahkannya ke aliran bisnis, tempat mereka diproses secara berurutan. Ini menghindari sinkronisasi yang tidak perlu ketika mengembangkan kode aplikasi.
Pembagian kode ke bisnis / aplikasi dan sistem adalah konvensi internal kami. Ini memungkinkan Anda untuk membedakan antara kode yang bertanggung jawab untuk fitur dan kemampuan bagi pengguna, dari detail komunikasi tingkat rendah, penjadwalan, dan penyimpanan, yang merupakan alat layanan.
Jika aliran penerima mendeteksi bahwa tidak ada keadaan untuk papan, tugas inisialisasi yang sesuai diatur. Inisialisasi negara ditangani oleh jenis utas terpisah.
Jenis-jenis tugas dan arahnya dapat direpresentasikan sebagai berikut:

Implementasi semacam itu memungkinkan kami untuk menyelesaikan masalah-masalah berikut:
- Tidak ada logika bisnis dalam aliran penerimaan yang dapat memperlambat koneksi baru. Jenis aliran di server ada dalam satu salinan, jadi penundaan di dalamnya akan segera mempengaruhi waktu pembukaan papan, dan jika ada kesalahan dalam kode bisnis, itu dapat dengan mudah digantung.
- Inisialisasi negara tidak dilakukan dalam aliran bisnis board dan tidak memengaruhi waktu pemrosesan perintah bisnis dari pengguna. Mungkin perlu waktu, dan aliran bisnis memproses beberapa papan sekaligus, sehingga pembukaan papan baru tidak secara langsung memengaruhi papan yang sudah ada.
- Parsing perintah jaringan seringkali lebih cepat daripada mengeksekusi mereka secara langsung, sehingga konfigurasi kumpulan utas jaringan mungkin berbeda dari konfigurasi kumpulan utas bisnis untuk menggunakan sumber daya sistem secara efisien.
Pewarnaan aliran
Subsistem yang dijelaskan di atas dalam implementasi cukup tidak trivial. Pengembang harus mengingat skema sistem dan mempertimbangkan proses kebalikan dari papan penutup. Saat menutup, perlu untuk menghapus semua langganan, menghapus entri dari pendaftar dan melakukan ini dalam aliran yang sama di mana mereka diinisialisasi.
Kami memperhatikan bahwa bug dan kesulitan memodifikasi kode yang muncul dalam subsistem ini sering dikaitkan dengan kurangnya pemahaman tentang konteks eksekusi. Menyulap utas dan tugas membuatnya sulit untuk menjawab pertanyaan di mana utas tertentu dijalankan oleh sepotong kode tertentu.
Untuk mengatasi masalah ini, kami menggunakan metode pewarnaan benang - ini adalah kebijakan yang bertujuan mengatur penggunaan benang dalam sistem. Warna ditugaskan untuk utas, dan metode menentukan ruang untuk mengeksekusi dalam utas. Warna di sini adalah abstraksi, bisa berupa entitas apa saja, misalnya enumerasi. Di Jawa, anotasi dapat berfungsi sebagai bahasa penanda warna:
@Color @IncompatibleColors @AnyColor @Grant @Revoke
Anotasi ditambahkan ke metode, dengan menggunakannya Anda dapat mengatur validitas metode. Misalnya, jika penjelasan metode memungkinkan kuning dan merah, maka utas pertama dapat memanggil metode, dan untuk yang kedua, panggilan semacam itu akan salah.

Warna yang tidak valid dapat ditentukan:

Anda dapat menambah dan menghapus hak utas dalam dinamika:

Tidak adanya anotasi atau anotasi seperti pada contoh di bawah ini mengatakan bahwa metode ini dapat dieksekusi di utas apa pun:

Pengembang Android mungkin akrab dengan pendekatan ini untuk anotasi MainThread, UiThread, WorkerThread, dll.
Pewarnaan benang menggunakan prinsip kode dokumentasi diri, dan metode itu sendiri cocok untuk analisis statis. Menggunakan analisis statis, Anda dapat mengatakan sebelum kode dijalankan bahwa itu ditulis dengan benar atau tidak. Jika kami mengecualikan Grant dan Mencabut anotasi dan berasumsi bahwa aliran pada inisialisasi sudah memiliki seperangkat hak istimewa yang tidak dapat diubah, maka ini akan menjadi analisis yang tidak peka terhadap aliran - versi sederhana analisis statis yang tidak memperhitungkan urutan panggilan.
Pada saat penerapan metode pewarnaan aliran, tidak ada solusi siap pakai untuk analisis statis di infrastruktur devops kami, jadi kami menggunakan cara yang lebih sederhana dan lebih murah - kami memperkenalkan anotasi kami, yang secara unik terkait dengan setiap jenis aliran. Kami mulai memeriksa kebenarannya dengan bantuan aspek dalam runtime.
@Aspect public class ThreadAnnotationAspect { @Pointcut("if()") public static boolean isActive() { …
Untuk aspek, kami menggunakan pustaka Aspj dan plugin pakar, yang menyediakan tenun saat mengkompilasi proyek. Tenun awalnya dikonfigurasi untuk memuat waktu ketika memuat kelas dengan ClassLoader. Namun, kami dihadapkan dengan fakta bahwa weaver terkadang berperilaku tidak benar ketika memuat kelas yang sama secara kompetitif, sehingga kode sumber kelas tetap tidak berubah. Akibatnya, ini menghasilkan perilaku produksi yang sangat tidak terduga dan sulit direproduksi. Mungkin di versi perpustakaan saat ini tidak ada masalah seperti itu.
Solusi pada aspek memungkinkan kami untuk dengan cepat menemukan sebagian besar masalah dalam kode.
Penting untuk tidak lupa untuk selalu memperbarui anotasi: mereka dapat dihapus, menambah kemalasan, aspek menenun dapat dimatikan sama sekali - dalam hal ini pewarnaan akan dengan cepat kehilangan relevansi dan nilainya.
Dijaga oleh
Salah satu varietas pewarnaan adalah penjelasan GuardedBy dari java.util.concurrent. Ini membatasi akses ke bidang dan metode, menunjukkan kunci mana yang diperlukan untuk akses yang benar.
public class PrivateLock { private final Object lock = Object(); @GuardedBy (“lock”) Widget widget; void method() { synchronized (lock) {
IDE modern bahkan mendukung analisis anotasi ini. Misalnya, IDEA menampilkan pesan ini jika ada yang salah dengan kode:
Metode pewarnaan benang bukanlah hal baru, tetapi tampaknya dalam bahasa seperti Java, di mana akses multi-ulir sering digunakan untuk objek yang dapat berubah, penggunaannya tidak hanya sebagai bagian dari dokumentasi, tetapi juga pada tahap kompilasi, perakitan dapat sangat menyederhanakan pengembangan kode multi-utas.
Kami masih menggunakan implementasi pada aspek. Jika Anda terbiasa dengan solusi atau alat analisis yang lebih elegan yang memungkinkan Anda meningkatkan stabilitas pendekatan ini terhadap perubahan sistem, silakan bagikan dalam komentar.