Atau ketika kami menulis perpustakaan klien C ++ untuk ZooKeeper, etcd dan Consul KV
Dalam dunia sistem terdistribusi, ada sejumlah tugas khas: menyimpan informasi tentang komposisi cluster, mengelola konfigurasi node, mendeteksi node gagal, memilih pemimpin,
dan lain-lain . Untuk mengatasi masalah ini, sistem terdistribusi khusus telah dibuat - layanan koordinasi. Sekarang kita akan tertarik pada tiga di antaranya: ZooKeeper, etcd dan Consul. Dari semua fungsi yang kaya dari Konsul, kami akan fokus pada Konsul KV.

Faktanya, semua sistem ini adalah toko kunci-nilai linier yang toleran terhadap kesalahan. Meskipun model data mereka memiliki perbedaan yang signifikan, yang akan kita bahas nanti, mereka memungkinkan kita untuk memecahkan masalah praktis yang sama. Jelas, setiap aplikasi yang menggunakan layanan koordinasi terkait dengan salah satunya, yang dapat mengarah pada kebutuhan untuk mendukung beberapa sistem yang menyelesaikan tugas yang sama dalam satu pusat data untuk aplikasi yang berbeda.
Sebuah ide yang dirancang untuk menyelesaikan masalah ini berasal dari agen konsultasi Australia, dan kami, sebuah tim kecil siswa, harus mengimplementasikannya, yang akan saya ceritakan kepada Anda.
Kami dapat membuat perpustakaan yang menyediakan antarmuka umum untuk bekerja dengan ZooKeeper, etcd dan Consul KV. Perpustakaan ditulis dalam C ++, tetapi ada rencana untuk porting ke bahasa lain.
Model data
Untuk mengembangkan antarmuka umum untuk tiga sistem yang berbeda, Anda perlu memahami apa yang mereka miliki bersama dan bagaimana mereka berbeda. Mari kita perbaiki.
Penjaga kebun binatang
Kunci disusun menjadi pohon dan disebut node (znodes). Dengan demikian, untuk situs Anda bisa mendapatkan daftar anak-anaknya. Operasi pembuatan znode (buat) dan mengubah nilai (setData) terpisah: hanya kunci yang ada yang dapat membaca dan mengubah nilai. Jam tangan dapat dilampirkan pada operasi memeriksa keberadaan simpul, membaca nilai, dan mendapatkan anak. Watch adalah pemicu satu kali yang menyala ketika versi data terkait di server berubah. Simpul Ephemeral digunakan untuk mendeteksi kegagalan. Mereka melekat pada sesi klien yang menciptakannya. 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 setidaknya salah satu dari mereka tidak mungkin.
dll
Pengembang sistem ini jelas terinspirasi oleh ZooKeeper, dan karena itu melakukan segalanya dengan berbeda. Hirarki kunci tidak ada di sini, tetapi mereka membentuk set yang diatur secara leksikografis. Anda bisa mendapatkan atau menghapus semua kunci yang termasuk dalam rentang tertentu. Struktur seperti itu mungkin tampak aneh, tetapi sebenarnya sangat ekspresif, dan pandangan hierarkis melalui itu mudah ditiru.
Tidak ada operasi perbandingan dan set standar di etcd, tetapi ada sesuatu yang lebih baik - transaksi. Tentu saja, mereka ada di ketiga sistem, tetapi dalam transaksi dll sangat bagus. Mereka terdiri dari tiga blok: cek, keberhasilan, kegagalan. Blok pertama berisi serangkaian kondisi, operasi kedua dan ketiga. Transaksi dilakukan secara atom. Jika semua kondisi benar, maka blok sukses dijalankan, jika tidak - kegagalan. Di API versi 3.3, blok kesuksesan dan kegagalan dapat berisi transaksi bersarang. Artinya, adalah mungkin untuk mengeksekusi konstruksi kondisional dari tingkat persarangan yang hampir sewenang-wenang. Anda dapat mempelajari lebih lanjut tentang pemeriksaan dan operasi apa yang ada dari
dokumentasi .
Jam tangan juga ada di sini, meskipun sedikit lebih rumit dan dapat digunakan kembali. Artinya, setelah memasang arloji di berbagai tombol, Anda akan menerima semua pembaruan dalam rentang ini hingga Anda membatalkan arloji, dan bukan hanya yang pertama. Di etcd, sewa sama dengan sesi klien ZooKeeper.
Konsul KVJuga tidak ada struktur hierarki yang ketat, tetapi Konsul dapat membuat tampilan yang ada: Anda dapat menerima dan menghapus semua kunci dengan awalan yang ditentukan, yaitu, bekerja dengan "subtree" dari kunci. Pertanyaan seperti itu disebut rekursif. Selain itu, Konsul hanya dapat memilih kunci yang tidak mengandung karakter yang ditentukan setelah awalan, yang sesuai dengan penerimaan langsung "anak-anak". Tetapi perlu diingat bahwa ini adalah penampilan struktur hierarkis: sangat mungkin untuk membuat kunci jika orang tuanya tidak ada atau menghapus kunci yang memiliki anak, sedangkan anak-anak akan terus disimpan dalam sistem.

Alih-alih jam tangan, ada yang memblokir permintaan HTTP di Konsul. Pada dasarnya, ini adalah panggilan biasa ke metode pembacaan data, di mana, bersama dengan parameter lain, versi terakhir dari data diindikasikan. Jika versi saat ini dari data yang sesuai di server lebih besar dari yang ditentukan, respons akan segera dikembalikan, jika tidak, ketika nilainya berubah. Ada juga sesi di sini yang dapat dilampirkan ke tombol kapan saja. Perlu dicatat bahwa, tidak seperti etcd dan ZooKeeper, di mana menghapus sesi mengarah pada penghapusan kunci terkait, ada mode di mana sesi hanya terlepas dari mereka.
Transaksi tersedia, tanpa bercabang, tetapi dengan semua jenis cek.
Satukan semuanya
Model data yang paling ketat adalah ZooKeeper. Permintaan rentang ekspresif yang tersedia di etcd tidak dapat ditiru secara efisien di ZooKeeper atau Konsul. Mencoba mengambil yang terbaik dari semua layanan, kami mendapat 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 (dalam ZK setData mengembalikan kesalahan dalam kasus ini)
- set dan metode cas terpisah (dalam ZK, mereka pada dasarnya adalah hal yang sama)
- metode erase menghapus simpul bersama dengan subtree (dalam penghapusan ZK mengembalikan kesalahan jika simpul memiliki anak)
- untuk setiap kunci hanya ada satu versi - versi nilainya (di ZK ada tiga dari mereka )
Penolakan node berurutan disebabkan oleh fakta bahwa di etcd dan Consul tidak ada dukungan bawaan untuk mereka, dan di atas antarmuka pustaka yang dihasilkan, mereka dapat dengan mudah diimplementasikan oleh pengguna.
Menerapkan perilaku yang sama ketika menghapus ZooKeeper atas akan memerlukan mempertahankan meja anak-anak yang terpisah di etcd dan Konsul untuk setiap kunci. Karena kami berusaha menghindari penyimpanan meta-informasi, diputuskan untuk menghapus seluruh subtree.
Kehalusan implementasi
Mari kita pertimbangkan secara lebih rinci beberapa aspek implementasi antarmuka perpustakaan dalam sistem yang berbeda.
Hierarki di etcdMempertahankan tampilan hierarkis di etcd adalah salah satu tugas yang paling menarik. Permintaan rentang memudahkan untuk mendapatkan daftar kunci dengan awalan yang ditentukan. Misalnya, jika Anda menginginkan semua yang dimulai dengan
"/foo"
, Anda meminta rentang
["/foo", "/fop")
. Tetapi ini akan mengembalikan seluruh subtree kunci, yang mungkin tidak dapat diterima jika subtree besar. Pada awalnya, kami berencana untuk menggunakan mekanisme konversi utama yang
diterapkan dalam zetcd . Ini melibatkan penambahan satu byte di awal kunci, sama dengan kedalaman node di pohon. Saya akan memberi contoh.
"/foo" -> "\u01/foo" "/foo/bar" -> "\u02/foo/bar"
Maka Anda bisa mendapatkan semua anak langsung dari kunci
"/foo"
dengan meminta rentang
["\u02/foo/", "\u02/foo0")
. Ya, di ASCII,
"0"
langsung mengikuti
"/"
.
Tetapi bagaimana cara menghapus titik? Ternyata Anda perlu menghapus semua rentang bentuk
["\uXX/foo/", "\uXX/foo0")
untuk XX dari 01 hingga FF. Dan kemudian kami mencapai
batas pada jumlah operasi dalam satu transaksi.
Akibatnya, sistem konversi kunci sederhana ditemukan, yang memungkinkan kami untuk secara efektif mengimplementasikan penghapusan kunci dan penerimaan daftar anak-anak. Cukup menambahkan simbol khusus sebelum token terakhir. Sebagai contoh:
"/very" -> "/\u00very" "/very/long" -> "/very/\u00long" "/very/long/path" -> "/very/long/\u00path"
Kemudian menghapus kunci
"/very"
berubah menjadi menghapus
"/\u00very"
dan kisaran
["/very/", "/very0")
, dan memasukkan semua anak ke dalam permintaan kunci dari rentang
["/very/\u00", "/very/\u01")
.
Menghapus kunci di ZooKeeperSeperti yang telah saya sebutkan, di ZooKeeper Anda tidak dapat menghapus simpul jika memiliki anak. Kami ingin menghapus kunci bersama subtree. Bagaimana menjadi Kami melakukannya dengan optimis. Pertama, kami secara rekursif melintasi subtree, membuat anak-anak dari setiap simpul dalam kueri yang terpisah. Kemudian kami membuat transaksi yang mencoba menghapus semua simpul subtree dalam urutan yang benar. Tentu saja, perubahan dapat terjadi antara membaca subtree dan menghapusnya. Dalam hal ini, transaksi akan gagal. Selain itu, subtree dapat berubah selama proses membaca. Kueri untuk anak-anak dari simpul berikutnya dapat mengembalikan kesalahan jika, misalnya, simpul ini sudah dihapus. Dalam kedua kasus, kami mengulangi seluruh proses lagi.
Pendekatan ini membuat menghapus kunci sangat tidak efektif jika memiliki anak, dan terlebih lagi jika aplikasi terus bekerja dengan subtree, menghapus dan membuat kunci. Namun, ini memungkinkan kami untuk tidak mempersulit pelaksanaan metode lain di etcd dan Consul.
diatur di ZooKeeperDi 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 ketat: buat akan mengembalikan kesalahan jika simpul sudah dibuat, dihapus atau setData - jika belum ada. Kami membutuhkan metode yang ditetapkan, yang dapat dipanggil tanpa memikirkan kuncinya.
Salah satu opsi adalah menerapkan pendekatan optimis, seperti saat menghapus. Periksa apakah ada simpul. Jika ada, panggil setData; jika tidak, buat. Jika metode terakhir menghasilkan kesalahan, ulangi sekali lagi. Hal pertama yang perlu diperhatikan adalah tidak ada gunanya memeriksa keberadaan. Anda dapat langsung memanggil buat. Penyelesaian yang berhasil akan berarti bahwa node tidak ada dan telah dibuat. Jika tidak, create akan mengembalikan kesalahan yang sesuai, setelah setData harus dipanggil. Tentu saja, di antara panggilan, titik dapat dihapus oleh panggilan yang bersaing, dan setData juga akan mengembalikan kesalahan. Dalam hal ini, Anda bisa mengulang semuanya lagi, tetapi apakah itu layak?
Jika kedua metode mengembalikan kesalahan, maka kami tahu pasti bahwa ada penghapusan yang bersaing. Bayangkan bahwa penghapusan ini terjadi setelah memanggil set. Maka tidak peduli nilai apa yang kami coba tegakkan, itu sudah terhapus. Jadi, Anda dapat mengasumsikan bahwa set berhasil, bahkan jika sebenarnya tidak ada yang ditulis.
Lebih detail teknis
Pada bagian ini, kami menyimpang dari sistem terdistribusi dan berbicara tentang pengkodean.
Salah satu persyaratan utama pelanggan adalah cross-platform: di Linux, MacOS dan Windows, setidaknya salah satu layanan harus didukung. Awalnya, kami melakukan pengembangan hanya di Linux, dan di sistem lain kami mulai menguji nanti. Ini menyebabkan banyak masalah, yang selama beberapa waktu tidak jelas cara pendekatannya. Akibatnya, ketiga layanan koordinasi sekarang didukung di Linux dan MacOS, dan hanya Konsul KV di Windows.
Sejak awal, kami mencoba menggunakan perpustakaan yang sudah jadi untuk mengakses layanan. Dalam kasus ZooKeeper, pilihan jatuh pada
ZooKeeper C ++ , yang pada akhirnya tidak dapat dikompilasi pada Windows. Namun, ini tidak mengejutkan: perpustakaan diposisikan sebagai linux-only. Untuk Konsul,
ppconsul adalah satu-satunya pilihan. Saya harus menambahkan dukungan untuk
sesi dan
transaksi . Untuk etcd, pustaka lengkap yang mendukung versi terbaru dari protokol tidak pernah ditemukan, jadi kami baru saja
menghasilkan klien grpc .
Terinspirasi oleh antarmuka asinkron dari pustaka ZooKeeper C ++, kami memutuskan untuk mengimplementasikan antarmuka asinkron juga. Dalam ZooKeeper C ++, primitif masa depan / janji digunakan untuk ini. Di STL, sayangnya, mereka diimplementasikan dengan sangat sederhana. Misalnya, tidak ada
metode yang menerapkan fungsi yang diteruskan ke hasil di masa mendatang ketika tersedia. Dalam kasus kami, metode seperti itu diperlukan untuk mengonversi hasil ke format perpustakaan kami. Untuk mengatasi masalah ini, kami harus menerapkan kumpulan utas sederhana kami, karena atas permintaan pelanggan kami tidak dapat menggunakan perpustakaan pihak ketiga yang berat, seperti Boost.
Implementasi kami kemudian berfungsi sebagai berikut. Saat dipanggil, pasangan janji / masa depan tambahan dibuat. Masa depan baru dikembalikan, dan yang ditransfer ditempatkan bersama dengan fungsi yang sesuai dan janji tambahan dalam antrian. Utas dari kumpulan memilih beberapa berjangka dari antrian dan memungutnya menggunakan wait_for. Ketika hasilnya tersedia, fungsi yang sesuai dipanggil, dan nilai kembalinya diteruskan ke janji.
Kami menggunakan kumpulan utas yang sama untuk mengeksekusi permintaan ke etcd dan Consul. Ini berarti bahwa beberapa utas berbeda dapat bekerja dengan pustaka yang mendasarinya. ppconsul bukan utas aman, jadi panggilan ke sana dilindungi oleh kunci.
Anda dapat bekerja dengan grpc dari beberapa utas, tetapi ada kehalusannya. Etcd jam tangan diimplementasikan melalui aliran grpc. Ini adalah saluran dua arah untuk jenis pesan tertentu. Perpustakaan membuat aliran tunggal untuk semua jam tangan dan aliran tunggal yang memproses pesan masuk. Jadi GRPC melarang rekaman paralel untuk streaming. Ini berarti bahwa ketika menginisialisasi atau menghapus arloji, Anda harus menunggu sampai pengiriman permintaan sebelumnya selesai sebelum mengirim yang berikutnya. Kami menggunakan
variabel bersyarat untuk sinkronisasi.
Ringkasan
Lihat sendiri:
liboffkv .
Tim kami:
Raed Romanov ,
Ivan Glushenkov ,
Dmitry Kamaldinov ,
Victor Krapivensky ,
Vitaly Ivanin .