Lima siswa dan tiga toko nilai kunci yang didistribusikan

Atau bagaimana kami menulis perpustakaan klien C++ untuk ZooKeeper, dll dan Konsul KV

Dalam dunia sistem terdistribusi, terdapat sejumlah tugas umum: menyimpan informasi tentang komposisi cluster, mengelola konfigurasi node, mendeteksi node yang salah, memilih pemimpin. dan lainnya. Untuk mengatasi masalah ini, sistem terdistribusi khusus telah dibuat - layanan koordinasi. Sekarang kita akan tertarik pada tiga di antaranya: ZooKeeper, dll, dan Konsul. Dari semua fungsi Konsul yang kaya, kami akan fokus pada Konsul KV.

Lima siswa dan tiga toko nilai kunci yang didistribusikan

Intinya, semua sistem ini merupakan penyimpanan nilai kunci yang toleran terhadap kesalahan dan dapat dilinearisasi. Meskipun model datanya memiliki perbedaan yang signifikan, yang akan kita bahas nanti, model tersebut memecahkan masalah praktis yang sama. Jelasnya, setiap aplikasi yang menggunakan layanan koordinasi terikat pada salah satunya, yang mungkin memerlukan dukungan beberapa sistem dalam satu pusat data yang memecahkan masalah yang sama untuk aplikasi yang berbeda.

Ide untuk memecahkan masalah ini berasal dari sebuah lembaga konsultan Australia, dan diserahkan kepada kami, sebuah tim kecil yang terdiri dari mahasiswa, untuk menerapkannya, itulah yang akan saya bicarakan.

Kami berhasil membuat perpustakaan yang menyediakan antarmuka umum untuk bekerja dengan ZooKeeper, dll, dan Konsul KV. Pustaka ini ditulis dalam C++, tetapi ada rencana untuk memindahkannya ke bahasa lain.

Model Data

Untuk mengembangkan antarmuka umum untuk tiga sistem berbeda, Anda perlu memahami kesamaan apa yang dimilikinya dan perbedaannya. Mari kita cari tahu.

Penjaga kebun binatang

Lima siswa dan tiga toko nilai kunci yang didistribusikan

Kunci-kunci tersebut disusun menjadi sebuah pohon dan disebut node. Oleh karena itu, untuk sebuah node Anda bisa mendapatkan daftar anak-anaknya. Operasi pembuatan znode (buat) dan perubahan nilai (setData) dipisahkan: hanya kunci yang ada yang dapat dibaca dan diubah. Jam tangan dapat dilampirkan pada operasi pengecekan keberadaan node, membaca nilai, dan mendapatkan turunan. Watch adalah pemicu satu kali yang diaktifkan ketika versi data terkait di server berubah. Node sementara digunakan untuk mendeteksi kegagalan. Mereka terikat pada sesi klien yang membuatnya. Ketika klien menutup sesi atau berhenti memberi tahu ZooKeeper tentang keberadaannya, node ini secara otomatis dihapus. Transaksi sederhana didukung - serangkaian operasi yang semuanya berhasil atau gagal jika hal ini tidak memungkinkan untuk setidaknya salah satu dari operasi tersebut.

dll

Lima siswa dan tiga toko nilai kunci yang didistribusikan

Pengembang sistem ini jelas terinspirasi oleh ZooKeeper, dan karenanya melakukan segalanya secara berbeda. Tidak ada hierarki kunci, tetapi mereka membentuk kumpulan yang diurutkan secara leksikografis. Anda bisa mendapatkan atau menghapus semua kunci yang termasuk dalam rentang tertentu. Struktur ini mungkin tampak aneh, namun sebenarnya sangat ekspresif, dan pandangan hierarkis dapat dengan mudah ditiru melaluinya.

etcd tidak memiliki operasi perbandingan-dan-set standar, tetapi ia memiliki sesuatu yang lebih baik: transaksi. Tentu saja, mereka ada di ketiga sistem, tetapi transaksi dll sangat bagus. Mereka terdiri dari tiga blok: periksa, sukses, gagal. Blok pertama berisi sekumpulan kondisi, blok kedua dan ketiga berisi operasi. Transaksi dieksekusi secara atom. Jika semua kondisi benar, maka blok sukses akan dieksekusi, jika tidak maka blok kegagalan akan dieksekusi. Di API 3.3, blok sukses dan gagal dapat berisi transaksi bertingkat. Artinya, dimungkinkan untuk mengeksekusi konstruksi kondisional secara atom pada tingkat sarang yang hampir berubah-ubah. Anda dapat mempelajari lebih lanjut tentang pemeriksaan dan operasi apa yang ada dokumentasi.

Jam tangan juga ada di sini, meski sedikit lebih rumit dan dapat digunakan kembali. Artinya, setelah memasang jam tangan pada rentang utama, Anda akan menerima semua pembaruan dalam rentang ini hingga Anda membatalkan jam tangan tersebut, dan bukan hanya yang pertama. Di dll, analog dari sesi klien ZooKeeper adalah sewa.

Konsul K.V.

Juga tidak ada struktur hierarki yang ketat di sini, tetapi Konsul dapat menciptakan tampilan yang ada: Anda bisa mendapatkan dan menghapus semua kunci dengan awalan yang ditentukan, yaitu, bekerja dengan "subpohon" dari kunci tersebut. Kueri seperti ini disebut rekursif. Selain itu, Konsul hanya dapat memilih kunci yang tidak mengandung karakter tertentu setelah awalan, yang berhubungan dengan mendapatkan “anak” langsung. Namun perlu diingat bahwa inilah tepatnya tampilan struktur hierarki: sangat mungkin untuk membuat kunci jika induknya tidak ada atau menghapus kunci yang memiliki anak, sementara anak tersebut akan terus disimpan dalam sistem.

Lima siswa dan tiga toko nilai kunci yang didistribusikan
Alih-alih menonton, Konsul memblokir permintaan HTTP. Intinya, ini adalah panggilan biasa ke metode pembacaan data, yang, bersama dengan parameter lainnya, menunjukkan versi data terakhir yang diketahui. Jika versi saat ini dari data terkait di server lebih besar dari yang ditentukan, respons segera dikembalikan, jika tidak, ketika nilainya berubah. Ada juga sesi yang dapat dilampirkan ke kunci kapan saja. Perlu dicatat bahwa tidak seperti etcd dan ZooKeeper, di mana menghapus sesi menyebabkan penghapusan kunci terkait, ada mode di mana sesi tersebut diputuskan tautannya begitu saja. Tersedia transaksi, tanpa cabang, tetapi dengan segala macam pemeriksaan.

Menyatukan semuanya

ZooKeeper memiliki model data paling ketat. Kueri rentang ekspresif yang tersedia di etcd tidak dapat ditiru secara efektif di ZooKeeper atau Konsul. Mencoba menggabungkan yang terbaik dari semua layanan, kami mendapatkan antarmuka yang hampir setara dengan antarmuka ZooKeeper dengan pengecualian signifikan berikut:

  • urutan, wadah dan node TTL tidak didukung
  • ACL tidak didukung
  • metode set membuat kunci jika tidak ada (di ZK setData mengembalikan kesalahan dalam kasus ini)
  • metode set dan cas dipisahkan (dalam ZK keduanya pada dasarnya sama)
  • metode penghapusan menghapus sebuah node beserta subpohonnya (di ZK delete mengembalikan kesalahan jika node tersebut memiliki anak)
  • Untuk setiap kunci hanya ada satu versi - versi nilai (dalam ZK ada tiga)

Penolakan node berurutan disebabkan oleh fakta bahwa etcd dan Consul tidak memiliki dukungan bawaan untuk node tersebut, dan node tersebut dapat dengan mudah diimplementasikan oleh pengguna di atas antarmuka perpustakaan yang dihasilkan.

Menerapkan perilaku yang mirip dengan ZooKeeper saat menghapus sebuah titik akan memerlukan pemeliharaan penghitung anak terpisah untuk setiap kunci di etcd dan Konsul. Karena kami mencoba menghindari penyimpanan informasi meta, diputuskan untuk menghapus seluruh subpohon.

Seluk-beluk implementasi

Mari kita lihat lebih dekat beberapa aspek penerapan antarmuka perpustakaan di sistem yang berbeda.

Hierarki di dll

Mempertahankan tampilan hierarki di dlld ternyata menjadi salah satu tugas yang paling menarik. Kueri rentang memudahkan untuk mengambil daftar kunci dengan awalan tertentu. Misalnya, jika Anda membutuhkan segala sesuatu yang dimulai dengan "/foo", Anda menanyakan kisarannya ["/foo", "/fop"). Tapi ini akan mengembalikan seluruh subpohon kunci, yang mungkin tidak dapat diterima jika subpohonnya besar. Awalnya kami berencana menggunakan mekanisme penerjemahan kunci, diimplementasikan di zetcd. Ini melibatkan penambahan satu byte di awal kunci, sama dengan kedalaman node di pohon. Izinkan saya memberi Anda sebuah contoh.

"/foo" -> "u01/foo"
"/foo/bar" -> "u02/foo/bar"

Kemudian dapatkan semua turunan kunci tersebut "/foo" mungkin dengan meminta kisaran ["u02/foo/", "u02/foo0"). Ya, di ASCII "0" berdiri tepat setelahnya "/".

Tetapi bagaimana cara menerapkan penghapusan simpul dalam kasus ini? Ternyata Anda perlu menghapus semua rentang tipe tersebut ["uXX/foo/", "uXX/foo0") untuk XX dari 01 hingga FF. Dan kemudian kami bertemu batas nomor operasi dalam satu transaksi.

Hasilnya, sistem konversi kunci sederhana ditemukan, yang memungkinkan penerapan penghapusan kunci dan perolehan daftar turunan secara efektif. Cukup menambahkan karakter khusus sebelum token terakhir. Misalnya:

"/very" -> "/u00very"
"/very/long" -> "/very/u00long"
"/very/long/path" -> "/very/long/u00path"

Kemudian menghapus kuncinya "/very" berubah menjadi penghapusan "/u00very" dan jangkauan ["/very/", "/very0"), dan mendapatkan semua anak - dalam permintaan kunci dari jangkauan ["/very/u00", "/very/u01").

Menghapus kunci di ZooKeeper

Seperti yang telah saya sebutkan, di ZooKeeper Anda tidak dapat menghapus sebuah node jika node tersebut memiliki anak. Kami ingin menghapus kunci bersama dengan subpohonnya. Apa yang harus saya lakukan? Kami melakukan ini dengan optimisme. Pertama, kita menelusuri subpohon secara rekursif, mendapatkan anak dari setiap simpul dengan kueri terpisah. Kemudian kita membuat transaksi yang mencoba menghapus semua node subpohon dalam urutan yang benar. Tentu saja, perubahan dapat terjadi antara membaca subpohon dan menghapusnya. Dalam hal ini, transaksi akan gagal. Selain itu, subpohon dapat berubah selama proses membaca. Permintaan untuk anak-anak dari node berikutnya mungkin menghasilkan kesalahan jika, misalnya, node ini telah dihapus. Dalam kedua kasus tersebut, kami mengulangi seluruh proses lagi.

Pendekatan ini membuat penghapusan kunci menjadi sangat tidak efektif jika kunci tersebut memiliki anak, dan terlebih lagi jika aplikasi terus bekerja dengan subpohon, menghapus dan membuat kunci. Namun, hal ini memungkinkan kami menghindari kerumitan penerapan metode lain di dlld dan Konsul.

diatur di ZooKeeper

Di ZooKeeper ada metode terpisah yang bekerja dengan struktur pohon (buat, hapus, getChildren) dan yang bekerja dengan data dalam node (setData, getData). Selain itu, semua metode memiliki prasyarat yang ketat: buat akan mengembalikan kesalahan jika node sudah telah dibuat, hapus atau setData – jika belum ada. Kami membutuhkan satu set metode yang dapat dipanggil tanpa memikirkan keberadaan kunci.

Salah satu pilihannya adalah mengambil pendekatan optimis, seperti penghapusan. Periksa apakah ada node. Jika ada, panggil setData, jika tidak, buat. Jika metode terakhir menghasilkan kesalahan, ulangi lagi. Hal pertama yang perlu diperhatikan adalah tes keberadaan tidak ada gunanya. Anda dapat langsung menelepon buat. Penyelesaian yang berhasil berarti node tersebut tidak ada dan telah dibuat. Jika tidak, buat akan mengembalikan kesalahan yang sesuai, setelah itu Anda perlu memanggil setData. Tentu saja, di antara panggilan, sebuah simpul dapat dihapus oleh panggilan yang bersaing, dan setData juga akan mengembalikan kesalahan. Dalam hal ini, Anda dapat mengulanginya lagi, tetapi apakah itu sepadan?

Jika kedua metode mengembalikan kesalahan, maka kita tahu pasti bahwa penghapusan yang bersaing telah terjadi. Bayangkan penghapusan ini terjadi setelah pemanggilan set. Maka makna apa pun yang ingin kita bangun sudah terhapus. Ini berarti kita dapat berasumsi bahwa set telah berhasil dijalankan, meskipun sebenarnya tidak ada yang ditulis.

Detail teknis lebih lanjut

Pada bagian ini kita akan istirahat dari sistem terdistribusi dan berbicara tentang pengkodean.
Salah satu persyaratan utama pelanggan adalah lintas platform: setidaknya salah satu layanan harus didukung di Linux, MacOS, dan Windows. Awalnya, kami hanya mengembangkannya untuk Linux, dan kemudian mulai mengujinya pada sistem lain. Hal ini menyebabkan banyak masalah, yang selama beberapa waktu tidak jelas bagaimana pendekatannya. Hasilnya, ketiga layanan koordinasi kini didukung di Linux dan MacOS, sementara hanya Konsul KV yang didukung di Windows.

Sejak awal, kami mencoba menggunakan perpustakaan yang sudah jadi untuk mengakses layanan. Dalam kasus ZooKeeper, pilihannya jatuh pada Penjaga Kebun Binatang C++, yang akhirnya gagal dikompilasi di Windows. Namun, hal ini tidak mengherankan: perpustakaan ini diposisikan sebagai khusus linux. Bagi Konsul, satu-satunya pilihan adalah konsultan pp. Dukungan harus ditambahkan ke dalamnya sesi и transaksi. Untuk dlld, perpustakaan lengkap yang mendukung protokol versi terbaru tidak ditemukan, jadi kami sederhana saja klien grpc yang dihasilkan.

Terinspirasi oleh antarmuka asinkron dari pustaka ZooKeeper C++, kami memutuskan untuk juga mengimplementasikan antarmuka asinkron. ZooKeeper C++ menggunakan primitif masa depan/janji untuk ini. Sayangnya, di STL, penerapannya sangat sederhana. Misalnya, tidak lalu metode, yang menerapkan fungsi yang diteruskan ke hasil masa depan ketika sudah tersedia. Dalam kasus kami, metode seperti itu diperlukan untuk mengubah hasilnya ke dalam format perpustakaan kami. Untuk mengatasi masalah ini, kami harus menerapkan kumpulan thread sederhana kami sendiri, karena atas permintaan pelanggan kami tidak dapat menggunakan perpustakaan pihak ketiga yang berat seperti Boost.

Implementasi kami kemudian bekerja seperti ini. Saat dipanggil, pasangan janji/masa depan tambahan dibuat. Masa depan baru dikembalikan, dan masa depan yang dilewati ditempatkan bersama dengan fungsi terkait dan janji tambahan dalam antrian. Sebuah thread dari pool memilih beberapa futures dari antrian dan melakukan polling menggunakan wait_for. Ketika hasilnya tersedia, fungsi terkait dipanggil dan nilai kembaliannya diteruskan ke janji.

Kami menggunakan kumpulan utas yang sama untuk menjalankan kueri ke dll dan Konsul. Ini berarti perpustakaan yang mendasarinya dapat diakses oleh beberapa thread berbeda. ppconsul tidak aman untuk thread, jadi panggilan ke sana dilindungi oleh kunci.
Anda dapat bekerja dengan grpc dari beberapa thread, tetapi ada beberapa kehalusan. Di jam tangan dlld diimplementasikan melalui aliran grpc. Ini adalah saluran dua arah untuk pesan jenis tertentu. Perpustakaan membuat satu thread untuk semua jam tangan dan satu thread yang memproses pesan masuk. Jadi grpc melarang penulisan paralel ke streaming. Artinya saat menginisialisasi atau menghapus jam tangan, Anda harus menunggu hingga permintaan sebelumnya selesai dikirim sebelum mengirim permintaan berikutnya. Kami menggunakannya untuk sinkronisasi variabel bersyarat.

Total

Lihat sendiri: liboffkv.

Tim kita: Raed Romanov, Ivan Glushenkov, Dmitry Kamaldinov, Victor Krapvensky, Vitaly Ivanin.

Sumber: www.habr.com

Tambah komentar