Pada artikel sebelumnya , kita berbicara tentang mentransfer pengembangan ke repositori tunggal dengan pendekatan berbasis trunk untuk pengembangan, dengan sistem terpadu untuk perakitan, pengujian, penyebaran, dan pemantauan, tentang tugas apa yang harus diselesaikan sistem integrasi berkesinambungan untuk bekerja secara efektif dalam kondisi seperti itu.
Hari ini kami akan memberi tahu pembaca Habr tentang perangkat sistem integrasi berkelanjutan.

Sistem integrasi berkelanjutan harus bekerja dengan andal dan cepat. Sistem harus merespons dengan cepat peristiwa yang masuk dan tidak boleh menyebabkan penundaan tambahan dalam proses memberikan hasil uji coba kepada pengguna. Hasil perakitan dan pengujian harus dikirimkan kepada pengguna secara real time.
Sistem integrasi berkelanjutan adalah sistem pemrosesan data streaming dengan penundaan minimal.
Setelah mengirimkan semua hasil pada tahap tertentu (konfigurasi, build, style, tes kecil, tes menengah, dll.), Sistem build memberi sinyal ini ke sistem integrasi berkelanjutan ("menutup" panggung), dan pengguna melihat bahwa untuk pemeriksaan ini dan Pada tahap ini semua hasil diketahui. Setiap tahap ditutup secara independen. Pengguna menerima sinyal yang berguna lebih cepat. Setelah menutup semua tahapan, cek dianggap selesai.
Untuk mengimplementasikan sistem, kami memilih arsitektur Kappa . Sistem ini terdiri dari 2 subsistem:
- Acara dan pemrosesan data berlangsung dalam sirkuit waktu nyata. Setiap data input diperlakukan sebagai aliran data (stream). Pertama, peristiwa dicatat dalam aliran dan baru kemudian mereka diproses.
- Hasil pemrosesan data terus ditulis ke dalam basis data, di mana panggilan melalui API kemudian pergi. Dalam arsitektur Kappa, ini disebut layer penyajian.
Semua permintaan untuk modifikasi data harus melalui sirkuit realtime, karena di sana Anda selalu perlu memiliki kondisi sistem saat ini. Permintaan baca hanya pergi ke database.

Jika memungkinkan, kami mengikuti aturan append-only. Tidak ada modifikasi atau penghapusan objek, dengan pengecualian menghapus data lama yang tidak perlu.
Lebih dari 2 Tb data mentah melewati layanan per hari.
Keuntungan:
- Streaming berisi semua acara dan pesan. Kita selalu dapat memahami apa dan kapan terjadi. Stream dapat dianggap sebagai log besar.
- Efisiensi tinggi dan overhead minimal. Ternyata sistem yang sepenuhnya berorientasi pada acara, tanpa kehilangan polling. Tidak ada acara - kami tidak melakukan sesuatu yang ekstra.
- Kode aplikasi praktis tidak berurusan dengan primitif sinkronisasi ulir dan memori yang dibagi di antara utas. Ini membuat sistem lebih andal.
- Prosesor terisolasi dengan baik satu sama lain, karena jangan berinteraksi langsung, hanya melalui stream. Cakupan tes yang baik dapat disediakan.
Tetapi pemrosesan data streaming tidak begitu sederhana:
- Pemahaman yang baik tentang model komputasi diperlukan. Anda harus memikirkan kembali algoritma pemrosesan data yang ada. Tidak semua algoritma langsung jatuh ke model aliran dan Anda harus menghancurkan kepala Anda sedikit.
- Penting untuk memastikan bahwa urutan penerimaan dan pemrosesan acara dipertahankan.
- Anda harus dapat menangani acara yang saling terkait, yaitu memiliki akses cepat ke semua data yang diperlukan saat memproses pesan baru.
- Anda juga harus bisa menangani acara duplikat.
Pemrosesan aliran
Saat mengerjakan proyek, pustaka Stream Processor ditulis, yang membantu kami untuk dengan cepat mengimplementasikan dan meluncurkan algoritma pemrosesan data streaming dalam produksi.
Stream Processor adalah perpustakaan untuk membangun sistem pemrosesan data streaming. Aliran adalah urutan data (pesan) yang berpotensi tak berujung ke mana hanya menambahkan pesan baru dimungkinkan, pesan yang sudah direkam tidak diubah atau dihapus dari aliran. Konverter dari satu aliran ke yang lain (pemroses aliran) secara fungsional terdiri dari tiga bagian: penyedia pesan masuk, yang biasanya membaca pesan dari satu aliran atau lebih dan menempatkannya dalam antrian pemrosesan, pemroses pesan yang mengubah pesan yang masuk menjadi yang keluar dan menempatkannya dalam antrian ke catatan, dan penulis, di mana pesan keluar yang dikelompokkan dalam jendela waktu jatuh ke aliran output. Pesan data yang dihasilkan oleh satu prosesor aliran dapat digunakan oleh orang lain nanti. Dengan demikian, stream dan prosesor membentuk grafik berarah di mana loop dimungkinkan, khususnya, prosesor aliran bahkan dapat menghasilkan pesan dalam aliran yang sama dari tempat ia menerima data.
Dijamin bahwa setiap pesan dari aliran input akan diproses oleh setiap prosesor yang terkait dengannya setidaknya satu kali (semantik setidaknya satu kali). Juga dijamin bahwa semua pesan akan diproses sesuai urutan kedatangannya di aliran ini. Untuk melakukan ini, stream prosesor didistribusikan ke semua node layanan yang bekerja, sehingga pada satu waktu tidak lebih dari satu contoh dari setiap prosesor yang terdaftar berfungsi.
Memproses peristiwa yang saling terkait adalah salah satu masalah utama dalam membangun sistem untuk streaming pemrosesan data. Sebagai aturan, saat streaming pesan, stream processor secara bertahap membuat status tertentu yang valid pada saat pesan saat ini diproses. Objek keadaan seperti itu biasanya dikaitkan tidak dengan keseluruhan aliran secara keseluruhan, tetapi dengan bagian pesan tertentu, yang ditentukan oleh nilai kunci dalam aliran ini. Penyimpanan kekayaan yang efisien adalah kunci keberhasilan. Saat memproses pesan berikutnya, penting bagi prosesor untuk dapat dengan cepat mendapatkan status ini dan, berdasarkan itu dan pesan saat ini, menghasilkan pesan keluar. Objek keadaan ini dapat diakses oleh prosesor di L1 (tolong jangan bingung dengan cache CPU) LRU cache, yang terletak di memori. Dalam hal tidak ada keadaan dalam cache L1, itu dikembalikan dari cache L2 yang terletak di penyimpanan yang sama di mana stream disimpan dan di mana ia disimpan secara berkala selama operasi prosesor. Jika tidak ada keadaan dalam cache L2, maka itu dikembalikan dari pesan aliran asli, seolah-olah prosesor telah memproses semua pesan asli yang terkait dengan kunci pesan saat ini. Teknik caching juga memungkinkan Anda untuk menangani masalah latensi penyimpanan yang tinggi, karena pemrosesan sekuensial tidak bergantung pada kinerja server, tetapi pada keterlambatan permintaan dan respons saat berkomunikasi dengan gudang data.

Untuk secara efektif menyimpan data dalam cache L1 dan data pesan dalam memori, selain struktur hemat memori, kami menggunakan kumpulan objek yang memungkinkan Anda untuk hanya memiliki satu salinan objek (atau bahkan bagiannya) dalam memori. Teknik ini sudah digunakan di JDK untuk string string interning dan juga meluas ke jenis objek lainnya, yang harus tetap.
Untuk penyimpanan data yang ringkas dalam penyimpanan arus, beberapa data dinormalisasi sebelum menulis ke aliran, mis. berubah menjadi angka. Algoritma kompresi efektif kemudian dapat diterapkan pada angka (pengidentifikasi objek). Bilangan diurutkan, delta dihitung, kemudian enkode dengan ZigZag Enkode dan kemudian kompresi oleh pengarsipan. Normalisasi bukan teknik yang sangat standar untuk streaming sistem pemrosesan data. Tetapi teknik kompresi ini sangat efektif dan jumlah data dalam aliran paling banyak berkurang sekitar 1.000 kali.

Untuk setiap aliran dan prosesor, kami melacak siklus hidup pemrosesan pesan: tampilan pesan baru di aliran input, ukuran antrian pesan yang belum diproses, ukuran antrian untuk menulis ke aliran yang dihasilkan, waktu pemrosesan pesan, dan distribusi waktu menurut tahapan pemrosesan pesan:

Gudang data
Hasil pemrosesan data streaming harus tersedia untuk pengguna sesegera mungkin. Data yang diproses dari stream harus secara terus-menerus direkam dalam database, di mana Anda kemudian dapat pergi untuk data (misalnya, menunjukkan laporan dengan hasil tes, menunjukkan sejarah tes).
Karakteristik data dan kueri yang disimpan.
Sebagian besar data adalah uji coba. Lebih dari sebulan, lebih dari 1,5 miliar bangunan dan pengujian diluncurkan. Sejumlah besar informasi disimpan untuk setiap peluncuran: hasil dan jenis kesalahan, deskripsi singkat tentang kesalahan (snippet), beberapa tautan ke log, durasi pengujian, serangkaian nilai numerik, metrik, dalam format nama = nilai, dll. Beberapa data ini - misalnya, metrik dan durasi - sangat sulit untuk dikompres, karena sebenarnya ini adalah nilai acak. Bagian lain - misalnya, hasil, jenis kesalahan, log - dapat disimpan lebih efisien, karena hampir tidak berubah dalam pengujian yang sama dari menjalankan ke menjalankan.
Sebelumnya, kami menggunakan MySQL untuk menyimpan data yang diproses. Kami secara bertahap mulai bersandar pada kemampuan basis data:
- Jumlah data yang diproses berlipat ganda setiap enam bulan.
- Kami hanya dapat menyimpan data selama 2 bulan terakhir, tetapi kami ingin menyimpan data selama setidaknya satu tahun.
- Masalah dengan kecepatan eksekusi beberapa pertanyaan berat (hampir analitis).
- Skema basis data yang rumit. Banyak tabel (normalisasi), yang menyulitkan penulisan ke database. Skema dasar sangat berbeda dari skema objek yang digunakan dalam rangkaian waktu nyata.
- Tidak mengalami shutdown server. Kegagalan server atau shutdown pusat data yang terpisah dapat menyebabkan kegagalan sistem.
- Operasi yang cukup rumit.
Sebagai kandidat untuk gudang data baru, kami mempertimbangkan beberapa opsi: PostgreSQL, MongoDB dan beberapa solusi internal, termasuk ClickHouse .
Beberapa solusi tidak memungkinkan kami untuk menyimpan data kami lebih efisien daripada solusi berbasis MySQL yang lama. Yang lain tidak mengizinkan implementasi kueri yang cepat dan kompleks (hampir analitis). Misalnya, kami memiliki permintaan yang agak berat yang menunjukkan komit yang memengaruhi proyek tertentu (beberapa set tes). Dalam semua kasus di mana kita tidak dapat menjalankan query SQL cepat, kita harus memaksa pengguna untuk menunggu lama atau melakukan beberapa perhitungan di muka dengan kehilangan fleksibilitas. Jika Anda menghitung sesuatu terlebih dahulu, maka Anda perlu menulis lebih banyak kode dan pada saat yang sama kehilangan fleksibilitas - tidak ada cara untuk dengan cepat mengubah perilaku dan menceritakan apa pun. Jauh lebih mudah dan lebih cepat untuk menulis kueri SQL yang akan mengembalikan data yang dibutuhkan pengguna dan dapat dengan cepat memodifikasinya jika Anda ingin mengubah perilaku sistem.
Clickhouse
Kami memilih ClickHouse . ClickHouse adalah sistem manajemen basis data kolom (DBMS) untuk pemrosesan kueri analitik online (OLAP).
Beralih ke ClickHouse, kami sengaja meninggalkan beberapa peluang yang disediakan oleh DBMS lainnya, menerima lebih dari kompensasi yang layak untuk ini dalam bentuk pertanyaan analitis yang sangat cepat dan gudang data yang kompak.
Dalam DBMS relasional, nilai yang terkait dengan satu baris disimpan secara fisik berdampingan. Di ClickHouse, nilai-nilai dari berbagai kolom disimpan secara terpisah, dan data dari satu kolom disimpan bersama. Urutan penyimpanan data ini memungkinkan Anda untuk memberikan kompresi data tingkat tinggi dengan pilihan kunci utama yang tepat. Ini juga mempengaruhi skenario mana DBMS akan bekerja dengan baik. ClickHouse berfungsi lebih baik dengan kueri, di mana sejumlah kecil kolom dibaca dan kueri menggunakan satu tabel besar dan sisanya dari tabel kecil. Tetapi bahkan dalam permintaan non-analitik, ClickHouse dapat menunjukkan hasil yang baik.
Data dalam tabel diurutkan berdasarkan kunci utama. Penyortiran dilakukan di latar belakang. Ini memungkinkan Anda untuk membuat indeks jarang volume kecil, yang memungkinkan Anda untuk dengan cepat menemukan data. ClickHouse tidak memiliki indeks sekunder. Sebenarnya, ada satu indeks sekunder - kunci partisi (ClickHouse memotong data partisi di mana kunci partisi ditentukan dalam permintaan). Lebih detail .
Skema data dengan normalisasi tidak berfungsi, sebaliknya, lebih disukai untuk mendenormalisasi data tergantung pada permintaan untuk itu. Lebih disukai untuk membuat tabel "lebar" dengan sejumlah besar kolom. Item ini juga terkait dengan yang sebelumnya, karena tidak adanya indeks sekunder terkadang membuat salinan tabel menggunakan kunci primer yang berbeda.
ClickHouse tidak memiliki UPDATE dan DELETE dalam arti klasik, tetapi ada kemungkinan meniru mereka.
Data perlu dimasukkan dalam blok besar dan tidak terlalu sering (sekali setiap beberapa detik). Pemuatan data baris demi baris secara virtual tidak beroperasi pada volume data nyata.
ClickHouse tidak mendukung transaksi, sistem akhirnya menjadi konsisten .
Namun demikian, beberapa fitur ClickHouse, mirip dengan DBMS lainnya, membuatnya lebih mudah untuk mentransfer sistem yang ada padanya.
- ClickHouse menggunakan SQL, tetapi dengan sedikit perbedaan, berguna untuk kueri khas sistem OLAP. Ada sistem yang kuat dari fungsi agregat, ALL / ANY JOIN, ekspresi lambda dalam fungsi dan ekstensi SQL lainnya yang memungkinkan Anda untuk menulis hampir semua kueri analitik.
- ClickHouse mendukung replikasi, rekaman kuorum, membaca kuorum. Menulis kuorum diperlukan untuk penyimpanan data yang andal: INSERT hanya berhasil jika ClickHouse mampu menulis data ke sejumlah replika tanpa kesalahan.
Anda dapat membaca lebih lanjut tentang fitur ClickHouse dalam dokumentasi .
Fitur bekerja dengan ClickHouse
Pilihan kunci primer dan kunci partisi.
Bagaimana cara memilih kunci primer dan kunci partisi? Mungkin ini adalah pertanyaan pertama yang muncul saat membuat tabel baru. Pilihan kunci primer dan kunci partisi biasanya ditentukan oleh kueri yang akan dilakukan pada data. Pada saat yang sama, kueri yang menggunakan kedua kondisi berubah menjadi yang paling efektif: baik oleh kunci primer dan oleh kunci partisi.
Dalam kasus kami, tabel utama adalah matriks untuk menjalankan tes. Adalah logis untuk mengasumsikan bahwa dengan struktur data ini, kunci harus dipilih sehingga urutan memotong salah satu dari mereka berjalan dalam urutan meningkatkan nomor baris, dan urutan memotong yang lain - dalam urutan meningkatkan jumlah kolom.
Penting juga untuk diingat bahwa pilihan kunci primer dapat secara dramatis mempengaruhi kekompakan penyimpanan data, karena nilai-nilai yang identik dalam memotong kunci utama di kolom lain hampir tidak memakan ruang dalam tabel. Jadi, dalam kasus kami, misalnya, kondisi pengujian tidak banyak berubah dari komit ke komit. Fakta ini pada dasarnya telah menentukan pilihan kunci utama - sepasang pengidentifikasi tes dan nomor komit. Apalagi dalam urutan itu.

Kunci partisi memiliki dua tujuan. Di satu sisi, ini memungkinkan partisi menjadi "diarsipkan" sehingga mereka dapat dihapus secara permanen dari penyimpanan, karena data di dalamnya sudah usang. Di sisi lain, kunci partisi adalah indeks sekunder, yang berarti memungkinkan Anda untuk mempercepat kueri jika ada ekspresi untuknya.
Untuk matriks kami, memilih nomor komit sebagai kunci partisi tampaknya cukup alami. Tetapi jika Anda menetapkan nilai revisi dalam ekspresi untuk kunci partisi, maka akan ada banyak partisi yang tidak masuk akal dalam tabel seperti itu, yang akan menyebabkan penurunan kinerja permintaan untuk itu. Oleh karena itu, dalam ekspresi untuk kunci partisi, nilai revisi dapat dibagi menjadi sejumlah besar untuk mengurangi jumlah partisi, misalnya, PARTISI DENGAN intDiv (revisi, 2000). Jumlah ini harus cukup besar sehingga jumlah partisi tidak melebihi nilai yang disarankan, sementara itu harus cukup kecil sehingga tidak banyak data jatuh ke satu partisi dan database tidak harus membaca terlalu banyak data.
Bagaimana cara mengimplementasikan UPDATE dan DELETE?
Dalam arti biasa, UPDATE dan DELETE tidak didukung di ClickHouse. Namun, alih-alih UPDATE dan DELETE, Anda bisa menambahkan kolom dengan versi ke tabel dan menggunakan mesin ReplacingMergeTree khusus (menghapus catatan duplikat dengan nilai kunci utama yang sama). Dalam beberapa kasus, versi secara alami akan hadir dalam tabel sejak awal: misalnya, jika kita ingin membuat tabel untuk kondisi pengujian saat ini, versi dalam tabel ini akan menjadi nomor komit.
CREATE TABLE current_tests ( test_id UInt64, value Nullable(String), version UInt64 ) ENGINE = ReplacingMergeTree(version) ORDER BY test_id
Jika catatan diubah, kami menambahkan versi dengan nilai baru, dalam kasus penghapusan, dengan nilai NULL (atau beberapa nilai khusus lainnya yang tidak dapat ditemukan dalam data).
Apa yang Anda capai dengan penyimpanan baru?
Salah satu tujuan utama beralih ke ClickHouse adalah kemampuan untuk menyimpan riwayat pengujian selama periode waktu yang lama (beberapa tahun, atau setidaknya satu tahun dalam kasus terburuk). Sudah pada tahap prototipe, menjadi jelas bahwa kita dapat menjelajahi SSD yang ada di server kami untuk menyimpan setidaknya tiga tahun sejarah. Kueri analitik telah dipercepat secara signifikan, sekarang kami dapat mengekstraksi informasi yang jauh lebih berguna dari data kami. Margin RPS telah meningkat. Selain itu, nilai ini hampir secara linear diskalakan dengan penambahan server baru ke kluster ClickHouse. Membuat gudang data baru untuk database ClickHouse hanyalah langkah yang nyaris tidak terlihat bagi pengguna akhir menuju tujuan yang lebih penting - menambahkan fitur baru, mempercepat dan menyederhanakan pengembangan, berkat kemampuan untuk menyimpan dan memproses sejumlah besar data.
Datanglah ke kami
Departemen kami terus berkembang. Kunjungi kami jika Anda ingin mengerjakan tugas dan algoritma yang kompleks dan menarik. Jika Anda memiliki pertanyaan, Anda dapat bertanya langsung kepada saya di PM.
Tautan yang bermanfaat
Pemrosesan aliran
Arsitektur Kappa
ClickHouse: