Bagaimana Braze Memanfaatkan Ruby dalam Skala Besar

Diterbitkan: 2022-08-18

Jika Anda seorang insinyur yang membaca Berita Peretas, Pengembang Twitter, atau sumber informasi serupa lainnya di luar sana, Anda hampir pasti menemukan seribu artikel dengan judul seperti "Kecepatan Karat vs C", "Apa yang Membuat Node.js". js Faster Than Java?”, Atau “Mengapa Anda Harus Menggunakan Golang dan Bagaimana Memulainya.” Artikel-artikel ini biasanya menyatakan bahwa ada satu bahasa khusus yang merupakan pilihan yang jelas untuk skalabilitas atau kecepatan—dan satu-satunya hal yang harus Anda lakukan adalah menerimanya.

Ketika saya masih kuliah dan tahun pertama atau kedua saya sebagai seorang insinyur, saya akan membaca artikel-artikel ini dan segera membuat proyek hewan peliharaan untuk mempelajari bahasa atau kerangka kerja baru du jour. Bagaimanapun, itu dijamin untuk bekerja "dalam skala global" dan "lebih cepat dari apa pun yang pernah Anda lihat," dan siapa yang bisa menolaknya? Akhirnya saya menemukan bahwa saya sebenarnya tidak membutuhkan salah satu dari hal-hal yang sangat spesifik ini untuk sebagian besar proyek saya. Dan seiring kemajuan karir saya, saya menyadari bahwa tidak ada pilihan bahasa atau kerangka kerja yang benar- benar akan memberi saya hal-hal ini secara gratis.

Alih-alih, saya menemukan bahwa arsitekturlah yang sebenarnya merupakan pengungkit terbesar ketika Anda ingin menskalakan sistem, bukan bahasa atau kerangka kerja.

Di sini, di Braze, kami beroperasi pada skala global yang sangat besar. Dan ya, kami menggunakan Ruby dan Rails sebagai dua alat utama kami untuk melakukannya. Namun, tidak ada nilai konfigurasi “global_scale = true” yang memungkinkan semua itu—ini adalah hasil dari arsitektur yang dipikirkan dengan matang yang mencakup jauh di dalam aplikasi hingga topologi penerapan. Para insinyur di Braze terus-menerus memeriksa kemacetan penskalaan dan mencari cara untuk membuat sistem kami lebih cepat, dan jawabannya biasanya bukan "menjauh dari Ruby": Ini hampir pasti akan menjadi perubahan arsitektur.

Jadi mari kita lihat bagaimana Braze memanfaatkan arsitektur yang bijaksana untuk benar-benar memecahkan kecepatan dan skala global yang besar—dan di mana Ruby dan Rails cocok (dan tidak)!

Kekuatan Arsitektur Terbaik di Kelasnya

Permintaan Web Sederhana

Karena skala tempat kami beroperasi, kami tahu bahwa perangkat yang terkait dengan basis pengguna pelanggan kami akan membuat miliaran permintaan web setiap hari yang harus dilayani oleh beberapa server web Braze. Dan bahkan di situs web yang paling sederhana, Anda akan memiliki aliran yang relatif kompleks yang terkait dengan permintaan dari klien ke server dan sebaliknya:

  1. Ini dimulai dengan resolver DNS klien (biasanya ISP mereka) mencari tahu alamat IP yang akan dituju, berdasarkan domain di URL situs web Anda.

  2. Setelah klien memiliki alamat IP, mereka akan mengirim permintaan ke router gateway mereka, yang akan mengirimkannya ke router "hop berikutnya" (yang mungkin terjadi beberapa kali), hingga permintaan tersebut sampai ke alamat IP tujuan.

  3. Dari sana, sistem operasi di server yang menerima permintaan akan menangani detail jaringan dan memberi tahu proses menunggu server web bahwa permintaan masuk telah diterima di soket/port yang didengarkannya.

  4. Server web akan menulis respons (sumber daya yang diminta, mungkin index.html) ke soket itu, yang akan berjalan mundur melalui router kembali ke klien.

Hal-hal yang cukup rumit untuk situs web sederhana, bukan? Untungnya, banyak dari hal-hal ini diurus untuk kita (lebih lanjut tentang itu sebentar lagi). Tetapi sistem kami masih memiliki penyimpanan data, pekerjaan latar belakang, masalah konkurensi, dan banyak lagi yang harus ditangani! Mari selami seperti apa itu.

Sistem Pertama Yang Mendukung Skala

DNS dan server nama biasanya tidak memerlukan banyak perhatian dalam banyak kasus. Server nama Domain Tingkat Atas Anda mungkin akan memiliki beberapa entri untuk memetakan "situs web Anda.com" ke server nama untuk domain Anda, dan jika Anda menggunakan layanan seperti Amazon Route 53 atau Azure DNS, mereka akan menangani nama tersebut server untuk domain Anda (mis. mengelola A, CNAME, atau jenis data lainnya). Anda biasanya tidak perlu memikirkan penskalaan bagian ini, karena itu akan ditangani secara otomatis oleh sistem yang Anda gunakan.

Namun, bagian perutean dari aliran bisa menjadi menarik. Ada beberapa algoritma perutean yang berbeda, seperti Open Shortest Path First atau Routing Information Protocol, semuanya dirancang untuk menemukan rute tercepat/terpendek dari klien ke server. Karena internet secara efektif adalah grafik terhubung raksasa (atau, secara bergantian, jaringan aliran), mungkin ada beberapa jalur yang dapat dimanfaatkan, masing-masing dengan biaya yang lebih tinggi atau lebih rendah. Akan menjadi penghalang untuk melakukan pekerjaan untuk menemukan rute tercepat mutlak, sehingga sebagian besar algoritme menggunakan heuristik yang masuk akal untuk mendapatkan rute yang dapat diterima. Komputer dan jaringan tidak selalu dapat diandalkan, jadi kami mengandalkan Fastly untuk meningkatkan kemampuan klien kami untuk merutekan ke server kami lebih cepat.

Cepat bekerja dengan menyediakan point-of-presence (POP) di seluruh dunia dengan koneksi yang sangat cepat dan andal di antara mereka. Anggap saja mereka sebagai jalan raya antarnegara bagian Internet. Catatan A dan CNAME domain kami mengarah ke Fastly, yang menyebabkan permintaan klien kami langsung menuju jalan raya. Dari sana, Fastly dapat mengarahkan mereka ke tempat yang tepat.

Pintu Depan untuk Braze

Oke, jadi permintaan klien kami telah melewati jalan raya Fastly dan tepat di pintu depan platform Braze—apa yang terjadi selanjutnya?

Dalam kasus sederhana, pintu depan itu akan menjadi server tunggal yang menerima permintaan. Seperti yang dapat Anda bayangkan, itu tidak akan menskala dengan baik, jadi kami sebenarnya mengarahkan Fastly ke satu set penyeimbang beban. Ada semua jenis strategi yang dapat digunakan oleh penyeimbang beban, tetapi bayangkan bahwa, dalam skenario ini, permintaan round-robin Fastly ke kumpulan penyeimbang beban secara merata. Penyeimbang beban ini akan mengantri permintaan, kemudian mendistribusikan permintaan tersebut ke server web, yang juga dapat kita bayangkan sedang menangani permintaan klien secara round-robin. (Dalam praktiknya mungkin ada keuntungan untuk jenis afinitas tertentu, tapi itu topik untuk lain waktu.)

Ini memungkinkan kami untuk meningkatkan jumlah penyeimbang beban dan jumlah server web tergantung pada throughput permintaan yang kami dapatkan dan throughput permintaan yang dapat kami tangani. Sejauh ini, kami telah membangun arsitektur yang dapat menangani banyak permintaan tanpa berkeringat! Ia bahkan dapat menangani pola lalu lintas yang padat melalui elastisitas antrian permintaan penyeimbang beban—yang luar biasa!

Server Web

Akhirnya, kita sampai ke bagian (Ruby) yang menarik: Server web. Kami menggunakan Ruby on Rails, tapi itu hanya kerangka kerja web—server web sebenarnya adalah Unicorn. Unicorn bekerja dengan memulai sejumlah proses pekerja pada mesin, di mana setiap proses pekerja mendengarkan pada soket OS untuk bekerja. Ini menangani manajemen proses untuk kami, dan menunda penyeimbangan beban permintaan ke OS itu sendiri. Kami hanya membutuhkan kode Ruby kami untuk memproses permintaan secepat mungkin; segala sesuatu yang lain dioptimalkan secara efektif di luar Ruby untuk kami.

Karena sebagian besar permintaan yang dibuat oleh SDK kami di dalam aplikasi pelanggan kami atau melalui REST API kami bersifat asinkron (yaitu kami tidak perlu menunggu operasi selesai untuk mengembalikan respons tertentu kepada klien), sebagian besar permintaan kami Server API sangat sederhana—mereka memvalidasi struktur permintaan, batasan kunci API apa pun, lalu melemparkan permintaan ke antrean Redis dan mengembalikan 200 respons ke klien jika semuanya beres.

Siklus permintaan/tanggapan ini membutuhkan waktu sekitar 10 milidetik untuk diproses kode Ruby—dan sebagian darinya dihabiskan untuk menunggu Memcached dan Redis. Bahkan jika kita menulis ulang semua ini dalam bahasa lain, tidak mungkin untuk memeras lebih banyak kinerja dari ini. Dan, pada akhirnya, arsitektur dari semua yang telah Anda baca sejauh ini yang memungkinkan kami untuk menskalakan proses penyerapan data ini untuk memenuhi kebutuhan pelanggan kami yang terus berkembang.

Antrian Pekerjaan

Ini adalah topik yang telah kami jelajahi di masa lalu, jadi saya tidak akan membahas aspek ini terlalu dalam—untuk mempelajari lebih lanjut tentang sistem antrian pekerjaan kami, lihat posting saya di Mencapai Ketahanan Dengan Antrian. Pada tingkat tinggi, apa yang kami lakukan adalah memanfaatkan banyak instans Redis yang bertindak sebagai antrean pekerjaan, buffering lebih lanjut untuk pekerjaan yang perlu dilakukan. Serupa dengan server web kami, instans ini dibagi di seluruh zona ketersediaan—untuk memberikan ketersediaan yang lebih tinggi jika terjadi masalah di zona ketersediaan tertentu—dan instans ini hadir dalam pasangan primer/sekunder menggunakan Redis Sentinel untuk redundansi. Kami juga dapat menskalakan ini secara horizontal dan vertikal untuk mengoptimalkan kapasitas dan throughput.

Para pekerja

Ini tentu saja bagian yang paling menarik—bagaimana kita membuat skala pekerja?

Pertama dan terpenting, pekerja dan antrian kami tersegmentasi oleh sejumlah dimensi: Pelanggan, jenis pekerjaan, penyimpanan data yang dibutuhkan, dll. Ini memungkinkan kami untuk memiliki ketersediaan tinggi; misalnya, jika penyimpanan data tertentu mengalami kesulitan, fungsi lain akan terus bekerja dengan baik. Ini juga memungkinkan kita untuk melakukan penskalaan otomatis jenis pekerja secara mandiri, bergantung pada salah satu dimensi tersebut. Kami akhirnya dapat mengelola kapasitas pekerja dengan cara yang dapat diskalakan secara horizontal—yaitu, jika kami memiliki lebih banyak jenis pekerjaan tertentu, kami dapat meningkatkan lebih banyak pekerja.

Inilah tempat di mana Anda mungkin mulai melihat masalah pilihan bahasa atau kerangka kerja. Pada akhirnya, pekerja yang lebih efisien akan dapat melakukan lebih banyak pekerjaan, lebih cepat. Bahasa yang dikompilasi seperti C atau Rust cenderung jauh lebih cepat dalam tugas komputasi daripada bahasa yang ditafsirkan seperti Ruby, dan itu dapat menghasilkan pekerja yang lebih efisien untuk beberapa beban kerja. Namun, saya menghabiskan banyak waktu untuk melihat jejak, dan pemrosesan CPU mentah adalah jumlah yang sangat kecil dalam gambaran besar di Braze. Sebagian besar waktu pemrosesan kami dihabiskan untuk menunggu tanggapan dari penyimpanan data atau dari permintaan eksternal, bukan menghitung angka; kita tidak perlu kode C yang sangat dioptimalkan untuk itu.

Toko Data

Sejauh ini, semua yang telah kita bahas cukup terukur. Jadi, mari luangkan waktu sebentar dan bicarakan di mana pekerja kita menghabiskan sebagian besar waktunya—penyimpanan data.

Siapa pun yang pernah meningkatkan server web atau pekerja asinkron yang menggunakan database SQL mungkin mengalami masalah skala tertentu: Transaksi. Anda mungkin memiliki titik akhir yang menangani penyelesaian Pesanan, yang membuat dua FulfillmentRequests dan PaymentReceipt. Jika ini tidak semua terjadi dalam suatu transaksi, Anda dapat berakhir dengan data yang tidak konsisten. Menjalankan banyak transaksi pada satu database secara bersamaan dapat mengakibatkan banyak waktu yang dihabiskan untuk mengunci, atau bahkan kebuntuan. Di Braze, kami menangani masalah penskalaan itu secara langsung dengan model data itu sendiri, melalui independensi objek dan konsistensi akhirnya. Dengan prinsip-prinsip ini, kami dapat memeras banyak kinerja dari penyimpanan data kami.

Objek Data Independen

Kami sangat memanfaatkan MongoDB di Braze, untuk alasan yang sangat bagus: Yaitu, ini memungkinkan kami untuk menskalakan shard MongoDB secara substansial secara horizontal dan mendapatkan peningkatan penyimpanan dan kinerja yang mendekati linier. Ini bekerja sangat baik untuk profil pengguna kami karena independensinya satu sama lain—tidak ada pernyataan GABUNG atau hubungan batasan yang harus dipertahankan di antara profil pengguna. Saat setiap pelanggan kami tumbuh atau saat kami menambahkan pelanggan baru (atau keduanya), kami cukup menambahkan basis data baru dan pecahan baru ke basis data yang ada untuk meningkatkan kapasitas kami. Kami secara eksplisit menghindari fitur seperti transaksi multi-dokumen untuk mempertahankan tingkat skalabilitas ini.

Selain MongoDB, kami sering menggunakan Redis sebagai penyimpanan data sementara untuk hal-hal seperti buffering informasi analitik. Karena sumber kebenaran untuk banyak analitik tersebut ada di MongoDB sebagai dokumen independen untuk jangka waktu tertentu, kami mempertahankan kumpulan instans Redis yang dapat diskalakan secara horizontal untuk bertindak sebagai buffer; di bawah pendekatan ini, ID dokumen hash digunakan dalam skema sharding berbasis kunci, menyebarkan beban secara merata karena independensi. Pekerjaan berkala menyiram buffer tersebut dari satu penyimpanan data berskala horizontal ke penyimpanan data berskala horizontal lainnya. Skala tercapai!

Selanjutnya, kami menggunakan Redis Sentinel untuk contoh ini seperti yang kami lakukan untuk antrian pekerjaan yang disebutkan di atas. Kami juga menyebarkan banyak "jenis" klaster Redis ini untuk tujuan yang berbeda, memberikan kami aliran kegagalan yang terkendali (yaitu jika satu jenis klaster Redis tertentu memiliki masalah, kami tidak melihat fitur yang tidak terkait mulai gagal secara bersamaan).

Konsistensi Akhirnya

Braze juga memanfaatkan konsistensi akhirnya sebagai prinsip untuk sebagian besar operasi baca. Ini memungkinkan kami untuk memanfaatkan pembacaan dari anggota utama dan sekunder set replika MongoDB dalam banyak kasus, membuat arsitektur kami lebih efisien. Prinsip ini dalam model data kami memungkinkan kami untuk banyak menggunakan caching di seluruh tumpukan kami.

Kami menggunakan pendekatan multi-layer menggunakan Memcached—pada dasarnya, saat meminta dokumen dari database, pertama-tama kami akan memeriksa proses Memcached mesin-lokal dengan waktu hidup yang sangat rendah (TTL), kemudian memeriksa instance Memcached jarak jauh (dengan TTL yang lebih tinggi), sebelum menanyakan database secara langsung. Ini membantu kami mengurangi secara dramatis pembacaan database untuk dokumen umum, seperti pengaturan pelanggan atau detail kampanye. "Akhirnya" mungkin terdengar menakutkan, tetapi, pada kenyataannya, itu hanya beberapa detik, dan mengambil pendekatan ini memotong sejumlah besar lalu lintas dari sumber kebenaran. Jika Anda pernah mengambil kelas arsitektur komputer, Anda mungkin mengenali betapa miripnya pendekatan ini dengan cara kerja sistem cache CPU L1, L2, dan L3!

Dengan trik ini, kita dapat memeras banyak kinerja dari bagian paling lambat dari arsitektur kita, dan kemudian menskalakannya secara horizontal sesuai kebutuhan saat throughput atau kapasitas kita meningkat.

Di mana Ruby dan Rails Cocok

Begini masalahnya: Ternyata, ketika Anda menghabiskan banyak upaya membangun arsitektur holistik di mana setiap lapisan skala horizontal dengan baik, kecepatan bahasa atau runtime jauh lebih penting daripada yang Anda kira. Itu berarti pilihan bahasa, kerangka kerja, dan waktu proses dibuat dengan serangkaian persyaratan dan batasan yang sama sekali berbeda.

Ruby dan Rails memiliki rekam jejak yang terbukti membantu tim melakukan iterasi dengan cepat saat Braze dimulai pada tahun 2011—dan mereka masih digunakan oleh GitHub, Shopify, dan merek terkemuka lainnya karena terus memungkinkan hal itu. Mereka terus dikembangkan secara aktif oleh komunitas Ruby dan Rails, masing-masing, dan keduanya masih memiliki kumpulan pustaka sumber terbuka yang tersedia untuk berbagai kebutuhan. Pasangan ini adalah pilihan tepat untuk iterasi cepat, karena mereka memiliki fleksibilitas yang sangat besar, dan mempertahankan kesederhanaan yang signifikan untuk kasus penggunaan umum. Kami menemukan bahwa menjadi sangat benar setiap hari kami menggunakannya.

Sekarang, bukan berarti Ruby on Rails adalah solusi sempurna yang akan bekerja dengan baik untuk semua orang. Namun di Braze, kami menemukan bahwa ini berfungsi sangat baik untuk memberi daya pada sebagian besar jalur penyerapan data, jalur pengiriman pesan, dan dasbor yang menghadap pelanggan kami, yang semuanya memerlukan iterasi cepat dan merupakan inti dari keberhasilan Braze platform secara keseluruhan.

Saat Kami Tidak Menggunakan Ruby

Tapi tunggu! Tidak semua yang kami lakukan di Braze ada di Ruby. Ada beberapa tempat selama bertahun-tahun di mana kami telah membuat panggilan untuk mengarahkan berbagai hal ke bahasa dan teknologi lain karena berbagai alasan. Mari kita lihat tiga di antaranya, hanya untuk memberikan beberapa wawasan tambahan tentang kapan kita bersandar dan tidak bersandar pada Ruby.

1. Layanan Pengirim

Ternyata, Ruby tidak hebat dalam menangani tingkat permintaan jaringan bersamaan yang sangat tinggi dalam satu proses. Itu masalah karena ketika Braze mengirim pesan atas nama pelanggan kami, beberapa penyedia layanan akhir mungkin memerlukan satu permintaan per pengguna. Ketika kami memiliki setumpuk 100 pesan yang siap dikirim, kami tidak ingin menunggu satu per satu selesai sebelum melanjutkan ke yang berikutnya. Kami lebih suka melakukan semua pekerjaan itu secara paralel.

Masukkan "Layanan Pengirim" kami—yaitu, layanan mikro tanpa kewarganegaraan yang ditulis dalam Golang. Kode Ruby kami dalam contoh di atas dapat mengirim 100 pesan ke salah satu layanan ini, yang akan mengeksekusi semua permintaan secara paralel, menunggu hingga selesai, lalu mengembalikan respons massal ke Ruby. Layanan ini secara substansial lebih efisien daripada yang dapat kami lakukan dengan Ruby dalam hal jaringan konkuren.

2. Konektor Arus

Fitur ekspor data volume tinggi Braze Currents kami memungkinkan pelanggan Braze untuk terus mengalirkan data ke satu atau lebih dari banyak mitra data kami. Platform ini didukung oleh Apache Kafka, dan streaming dilakukan melalui Konektor Kafka. Anda secara teknis dapat menulis ini di Ruby, tetapi cara yang didukung secara resmi adalah dengan Java. Dan karena tingkat dukungan Java yang tinggi, menulis konektor ini jauh lebih mudah dilakukan di Java daripada di Ruby.

3. Pembelajaran Mesin

Jika Anda pernah melakukan pekerjaan apa pun dalam pembelajaran mesin, Anda tahu bahwa bahasa pilihannya adalah Python. Banyaknya paket dan alat untuk beban kerja pembelajaran mesin di Python melampaui dukungan Ruby yang setara—hal-hal seperti notebook TensorFlow dan Jupyter berperan penting bagi tim kami, dan jenis alat tersebut tidak ada atau tidak mapan di dunia Ruby. Oleh karena itu, kami condong ke Python dalam hal membangun elemen produk kami yang memanfaatkan pembelajaran mesin.

Ketika Bahasa Itu Penting

Jelas, kami memiliki beberapa contoh bagus di atas di mana Ruby bukanlah pilihan yang ideal. Ada banyak alasan mengapa Anda mungkin memilih bahasa yang berbeda—berikut adalah beberapa yang menurut kami sangat berguna untuk dipertimbangkan.

Membangun Hal Baru tanpa Mengganti Biaya

Jika Anda akan membangun sistem yang sama sekali baru, dengan model domain baru dan tidak ada integrasi yang erat dengan fungsionalitas yang ada, Anda mungkin memiliki kesempatan untuk menggunakan bahasa yang berbeda jika Anda mau. Terutama dalam kasus di mana organisasi Anda mengevaluasi peluang yang berbeda, proyek lapangan hijau yang lebih kecil dan terisolasi bisa menjadi eksperimen dunia nyata yang hebat dalam mencoba bahasa atau kerangka kerja baru.

Ekosistem dan Ergonomi Bahasa Khusus Tugas

Beberapa tugas jauh lebih mudah dengan bahasa atau kerangka kerja tertentu—kami terutama menyukai Rails dan Grape untuk pengembangan fungsionalitas dasbor, tetapi kode pembelajaran mesin akan menjadi mimpi buruk mutlak untuk ditulis di Ruby, karena perkakas open-source tidak ada. Anda mungkin ingin menggunakan kerangka kerja atau pustaka tertentu untuk mengimplementasikan beberapa jenis fungsi atau integrasi, dan terkadang pilihan bahasa Anda akan dipengaruhi oleh itu, karena hampir pasti akan menghasilkan pengalaman pengembangan yang lebih mudah atau lebih cepat.

Kecepatan Eksekusi

Terkadang, Anda perlu mengoptimalkan kecepatan eksekusi mentah, dan bahasa yang digunakan akan sangat memengaruhi hal itu. Ada alasan bagus mengapa banyak platform perdagangan frekuensi tinggi dan sistem penggerak otonom ditulis dalam C++; kode yang dikompilasi secara asli bisa sangat cepat! Layanan Pengirim kami mengeksploitasi paralelisme/konkurensi primitif Golang yang tidak tersedia di Ruby karena alasan itu.

Keakraban Pengembang

Di sisi lain, Anda mungkin sedang membangun sesuatu yang terisolasi, atau memiliki perpustakaan dalam pikiran yang ingin Anda gunakan, tetapi pilihan bahasa Anda benar-benar asing bagi seluruh tim Anda. Memperkenalkan proyek baru di Scala dengan kecenderungan berat ke pemrograman fungsional mungkin memperkenalkan penghalang keakraban dengan pengembang lain di tim Anda, yang pada akhirnya akan menghasilkan isolasi pengetahuan atau penurunan kecepatan bersih. Kami menemukan ini menjadi sangat penting di Braze, karena kami sangat menekankan pada iterasi cepat, jadi kami cenderung mendorong penggunaan alat, pustaka, kerangka kerja, dan bahasa yang sudah digunakan secara luas di organisasi.

Pikiran Akhir

Jika saya dapat kembali ke masa lalu dan mengatakan pada diri sendiri satu hal tentang rekayasa perangkat lunak dalam sistem raksasa, itu adalah ini: Untuk sebagian besar beban kerja, keseluruhan pilihan arsitektur Anda akan menentukan batas dan kecepatan penskalaan Anda lebih dari pilihan bahasa yang pernah ada. Wawasan itu terbukti setiap hari di Braze.

Ruby dan Rails adalah alat luar biasa yang, ketika menjadi bagian dari sistem yang dirancang dengan benar, dapat diskalakan dengan sangat baik. Rails juga merupakan kerangka kerja yang sangat matang, dan mendukung budaya kami di Braze untuk mengulangi dan menghasilkan nilai pelanggan nyata dengan cepat. Ini menjadikan Ruby dan Rails alat yang ideal bagi kami, alat yang kami rencanakan untuk terus digunakan selama bertahun-tahun yang akan datang.

Tertarik bekerja di Braze? Kami sedang merekrut untuk berbagai peran di seluruh tim Teknik, Manajemen Produk, dan Pengalaman Pengguna kami. Lihat halaman karir kami untuk mempelajari lebih lanjut tentang peran terbuka dan budaya kami.