
Halo semuanya! Nama saya Maxim Ryndin, saya adalah pemimpin tim dari dua tim di Gett - Billing dan Infrastruktur. Saya ingin berbicara tentang pengembangan web produk, yang kami di Gett terutama gunakan Go. Saya akan memberi tahu Anda bagaimana pada 2015-2017 kami beralih ke bahasa ini, mengapa kami memilihnya, masalah apa yang kami temui selama masa transisi dan solusi apa yang kami temukan. Dan saya akan menceritakan tentang situasi saat ini di artikel selanjutnya.
Bagi mereka yang tidak tahu: Gett adalah layanan taksi internasional yang didirikan di Israel pada tahun 2011. Gett sekarang diwakili di 4 negara: Israel, Inggris, Rusia, dan Amerika Serikat. Produk utama perusahaan kami adalah aplikasi mobile untuk klien dan driver, portal web untuk klien korporat tempat Anda dapat memesan mobil, dan sekelompok panel admin internal yang melaluinya karyawan kami menyiapkan paket tarif, menghubungkan driver baru, memantau kasus penipuan, dan banyak lagi. Pada akhir 2016, kantor R&D global dibuka di Moskow, yang berfungsi untuk kepentingan seluruh perusahaan.
Bagaimana kami sampai di Go
Pada tahun 2011, produk utama perusahaan adalah aplikasi monolitik pada Ruby on Rails, karena pada saat itu kerangka kerja ini sangat populer. Ada contoh bisnis yang sukses yang dengan cepat dikembangkan dan diluncurkan di Ruby on Rails, sehingga dikaitkan dengan keberhasilan dalam bisnis tersebut. Perusahaan berkembang, driver dan pengguna baru datang kepada kami, banyak yang tumbuh. Dan masalah pertama mulai muncul.
Agar aplikasi klien dapat menampilkan lokasi mobil, dan agar pergerakannya terlihat seperti kurva yang halus, pengemudi harus sering mengirim koordinatnya. Oleh karena itu, titik akhir yang bertanggung jawab untuk menerima koordinat dari driver hampir selalu yang paling banyak dimuat. Dan kerangka kerja server web di Ruby on Rails melakukan pekerjaan yang buruk ini. Itu mungkin untuk skala hanya secara luas, menambahkan server aplikasi baru, yang mahal dan tidak efisien. Sebagai hasilnya, kami mengeluarkan koleksi fungsional koordinat dalam layanan terpisah, yang awalnya ditulis dalam JS. Untuk sementara, ini menyelesaikan masalah. Namun, ketika beban bertambah, ketika kami mencapai 80 ribu RPM, layanan di Node.js berhenti menyelamatkan kami.
Lalu kami mendeklarasikan hackathon. Semua karyawan di perusahaan memiliki kesempatan dalam sehari untuk menulis prototipe, yaitu mengumpulkan koordinat pengemudi. Berikut adalah tolok ukur dari dua versi layanan itu: berjalan di prod dan ditulis ulang di Go.
Dalam hampir semua hal, layanan di Go menunjukkan hasil terbaik. Layanan di Node.js menggunakan sebuah cluster, itu adalah teknologi untuk menggunakan semua inti mesin. Artinya, eksperimen itu plus atau minus jujur. Meskipun Node.js memiliki kelemahan dari runtime single-threaded, itu tidak berpengaruh pada hasil.
Secara bertahap, permintaan produk kami tumbuh. Kami mengembangkan lebih banyak fungsionalitas, dan begitu kami menghadapi masalah seperti itu: ketika Anda menambahkan beberapa kode di satu tempat, sesuatu mungkin pecah di tempat lain di mana proyek terhubung dengan kuat. Kami memutuskan untuk mengatasi momok ini dengan beralih ke arsitektur berorientasi layanan. Tetapi kinerja menurun sebagai hasilnya: ketika permintaan jaringan ditemui oleh penerjemah Ruby on Rails ketika kode dieksekusi, itu diblokir dan pekerja menganggur. Dan operasi jaringan I / O menjadi semakin banyak.
Sebagai hasilnya, kami memutuskan untuk mengadopsi Go sebagai salah satu bahasa pengembangan utama.
Fitur pengembangan produk kami
Pertama, kami memiliki persyaratan produk yang sangat berbeda. Karena mobil kami berkendara di tiga negara dengan undang-undang yang sama sekali berbeda, maka perlu untuk mengimplementasikan serangkaian fungsi yang sangat berbeda. Misalnya, di Israel, secara hukum diperlukan bahwa biaya perjalanan dipertimbangkan oleh taksimeter - ini adalah perangkat yang lulus sertifikasi wajib setiap beberapa tahun. Ketika pengemudi memulai perjalanan, dia menekan tombol "go", dan ketika dia selesai, dia menekan tombol "stop", dan memasukkan harga yang ditunjukkan oleh taksimeter ke dalam aplikasi.
Tidak ada hukum yang ketat di Rusia. Di sini kita dapat mengkonfigurasi kebijakan penetapan harga sendiri. Misalnya, ikatkan dengan durasi perjalanan atau jarak. Terkadang, ketika kita ingin menerapkan fungsi yang sama, pertama-tama kita meluncurkannya di satu negara, dan kemudian beradaptasi dan meluncurkannya di negara lain.
Manajer produk kami menetapkan persyaratan dalam bentuk cerita produk, kami mencoba mematuhi pendekatan seperti itu saja. Ini secara otomatis meninggalkan jejak pada pengujian: kami menggunakan metodologi pengembangan yang didorong oleh perilaku sehingga persyaratan produk yang masuk dapat diproyeksikan ke situasi pengujian. Lebih mudah bagi orang yang jauh dari pemrograman untuk hanya membaca hasil tes dan memahami apa itu.
Kami juga ingin menyingkirkan duplikasi beberapa pekerjaan. Lagi pula, jika kita memiliki layanan yang mengimplementasikan beberapa jenis fungsi, dan kita perlu menulis layanan kedua, menyelesaikan kembali semua masalah yang kita selesaikan di pertama, mengintegrasikan kembali dengan alat pemantauan dan migrasi, maka ini tidak akan efektif.
Kami memecahkan masalah
Kerangka kerja
Ruby on Rails dibangun di atas arsitektur MVC. Pada saat transisi, kami benar-benar tidak ingin menyerah untuk membuat hidup lebih mudah bagi para pengembang yang hanya dapat memprogram pada kerangka kerja ini. Mengubah alat tidak menambah kenyamanan tanpa itu, dan jika Anda juga mengubah arsitektur aplikasi, itu sama dengan mendorong orang yang tidak tahu cara berenang dari perahu. Kami tidak ingin menyakiti pengembang dengan cara ini, jadi kami mengambil satu dari beberapa kerangka kerja MVC pada waktu itu yang disebut
Beego .
Kami mencoba menggunakan Beego, seperti pada Ruby on Rails, untuk melakukan rendering sisi server. Namun, halaman yang diberikan di server tidak benar-benar menyenangkan kami. Saya harus membuang satu komponen, dan hari ini Beego hanya memproduksi JSON dari backend, dan semua rendering dilakukan oleh React di bagian depan.
Beego memungkinkan Anda membangun proyek secara otomatis. Sangat sulit bagi beberapa pengembang untuk beralih dari bahasa scripting ke kebutuhan untuk dikompilasi. Ada cerita lucu ketika seseorang menerapkan beberapa fitur, dan hanya dengan review kode atau bahkan secara tidak sengaja menemukan bahwa, ternyata, Anda perlu melakukan Go-build. Dan tugasnya sudah ditutup.
Di Beego, router dihasilkan dari komentar di mana pengembang menulis path ke tindakan controller. Kami memiliki sikap ambigu terhadap ide ini, karena jika kesalahan ketik, misalnya, router diketik ulang, maka sulit bagi mereka yang tidak canggih dalam pendekatan ini untuk menemukan kesalahan. Orang-orang, kadang-kadang, tidak dapat menemukan alasannya bahkan setelah beberapa jam debug yang menarik.
Basis data
Kami menggunakan PostgreSQL sebagai databasenya. Ada praktik semacam itu - untuk mengontrol skema database dari kode aplikasi. Ini nyaman karena beberapa alasan: semua orang tahu tentang mereka; mereka mudah digunakan, database selalu disinkronkan dengan kode. Dan kami juga ingin menyimpan roti ini.
Ketika Anda memiliki beberapa proyek dan tim, terkadang untuk mengimplementasikan fungsionalitas Anda harus merangkak ke proyek orang lain. Dan sangat menggoda untuk menambahkan kolom ke tabel, di mana 10 juta catatan mungkin muncul. Dan orang yang tidak tenggelam dalam proyek ini mungkin tidak menyadari ukuran meja. Untuk mencegah hal ini, kami mengeluarkan peringatan tentang migrasi berbahaya yang dapat memblokir database untuk direkam, dan memberi pengembang cara untuk menghapus peringatan ini.
Migrasi
Kami memutuskan untuk
bermigrasi menggunakan
Swan , yang merupakan
angsa yang ditambal , di mana kami membuat beberapa perbaikan. Keduanya, seperti banyak alat migrasi, ingin melakukan semuanya dalam satu transaksi, sehingga jika terjadi masalah Anda dapat dengan mudah memutar kembali. Terkadang Anda perlu membuat indeks, dan tabel terkunci. PostgreSQL memiliki parameter
concurrently
yang menghindari ini. Masalahnya adalah jika di PostgreSQL Anda mulai membangun indeks pada
concurrently
ini
concurrently
, dan bahkan dalam suatu transaksi, kesalahan akan muncul. Awalnya kami ingin menambahkan bendera agar tidak membuka transaksi. Dan pada akhirnya, mereka melakukan ini:
COMMIT; CREATE INDEX CONCURRENTLY huge_index ON huge_table (column_one, column_two); BEGIN;
Sekarang, ketika seseorang menambahkan indeks dengan parameter
concurrently
, dia mendapatkan petunjuk ini. Perhatikan bahwa
commit
dan
begin
tidak bingung. Kode ini menutup transaksi yang dibuka alat migrasi, lalu menggulung indeks dengan parameter
concurrently
, lalu membuka transaksi lain sehingga alat menutup sesuatu.
Pengujian
Kami berusaha mematuhi perkembangan yang didorong oleh perilaku. Di Go, ini bisa dilakukan menggunakan alat
Ginkgo . Itu baik karena memiliki kata kunci yang biasa untuk BDD, "jelaskan", "kapan" dan lainnya, dan itu juga memungkinkan Anda untuk hanya memproyeksikan teks yang ditulis oleh manajer produk ke dalam situasi pengujian yang disimpan dalam kode sumber. Tapi kami dihadapkan dengan masalah: orang-orang yang datang dari dunia Ruby on Rails percaya bahwa dalam bahasa pemrograman apa pun ada sesuatu yang mirip dengan seorang gadis pabrik - sebuah pabrik untuk menciptakan kondisi awal. Namun, tidak ada yang seperti ini di Go. Sebagai hasilnya, kami memutuskan bahwa kami tidak akan menemukan kembali roda: tepat sebelum setiap tes, di kait sebelum dan setelah pengujian, kami mengisi database dengan data yang diperlukan, dan kemudian membersihkannya sehingga tidak ada efek samping.
Pemantauan
Jika Anda memiliki layanan produksi yang diakses orang, maka Anda perlu melacak pekerjaannya: apakah ada lima ratus kesalahan atau permintaan sedang diproses dengan cepat. Di dunia Ruby on Rails, NewRelic sangat sering digunakan untuk ini, dan banyak pengembang kami telah memilikinya dengan baik. Mereka mengerti bagaimana alat itu bekerja, ke mana harus mencari jika ada masalah. NewRelic memungkinkan Anda untuk menganalisis waktu pemrosesan permintaan melalui HTTP, mengidentifikasi panggilan eksternal yang lambat dan permintaan ke database, memantau aliran data, memberikan analisis kesalahan yang cerdas dan peringatan.
NewRelic memiliki fungsi agregat Apdex, yang tergantung pada histogram distribusi durasi jawaban dan beberapa nilai yang Anda anggap normal dan yang ditetapkan di awal. Fitur ini juga tergantung pada tingkat kesalahan dalam aplikasi. NewRelic menghitung Apdex dan mengeluarkan peringatan jika nilainya jatuh di bawah level tertentu.
NewRelic juga pandai memiliki agen Go resmi baru-baru ini. Seperti inilah gambaran umum pemantauan:

Di sebelah kiri adalah diagram pemrosesan kueri, yang masing-masing dibagi menjadi beberapa segmen. Segmen termasuk antrian permintaan, pemrosesan middleware, lama tinggal di interpreter Ruby on Rails, dan akses ke repositori.
Grafik Apdex ditampilkan di kanan atas. Kanan bawah - frekuensi permintaan pemrosesan.
Intriknya adalah bahwa di Ruby on Rails untuk menghubungkan NewRelic Anda perlu menambahkan satu baris kode dan menambahkan kredensial Anda ke konfigurasi. Dan semuanya bekerja secara ajaib. Hal ini dimungkinkan karena pada kenyataan bahwa di Ruby on Rails ada patching monyet, yang tidak di Go, jadi ada banyak yang harus dilakukan secara manual.
Pertama-tama, kami ingin mengukur durasi pemrosesan permintaan. Ini dilakukan menggunakan kait yang disediakan oleh Beego.
beego.InsertFilter("*", beego.BeforeRouter, StartTransaction, false) beego.InsertFilter("*", beego.AfterExec, NameTransaction, false) beego.InsertFilter("*", beego.FinishRouter, EndTransaction, false)
Satu-satunya poin non-sepele adalah bahwa kami berbagi pembukaan transaksi dan penamaannya. Kenapa kita melakukan ini? Saya ingin mengukur durasi pemrosesan permintaan dengan mempertimbangkan waktu yang dihabiskan untuk routing. Pada saat yang sama, kami membutuhkan laporan yang dikumpulkan oleh titik akhir di mana permintaan datang. Tetapi pada saat membuka transaksi, kami belum menetapkan pola URL yang dengannya kecocokan akan terjadi. Oleh karena itu, ketika permintaan datang, kami membuka transaksi, lalu di hook, setelah mengeksekusi controller, beri nama, dan setelah diproses, tutuplah. Karenanya, hari ini laporan kami terlihat seperti ini:

Kami menggunakan ORM yang disebut GORM karena kami ingin mempertahankan abstraksi dan tidak memaksa pengembang untuk menulis SQL murni. Pendekatan ini memiliki kelebihan dan kekurangan. Di dunia Ruby on Rails, ada ORM Active Record yang benar-benar memanjakan orang. Pengembang lupa bahwa Anda dapat menulis SQL murni, dan beroperasi hanya dengan panggilan ORM.
db.Callback().Create().Before("gorm:begin_transaction"). Register("newrelicStart", startSegment) db.Callback().Create().After("gorm:commit_or_rollback_transaction"). Register("newrelicStop", endSegment)
Untuk mengukur durasi eksekusi kueri dalam database saat menggunakan GORM, Anda harus mengambil objek
db
. Callback mengatakan bahwa kami ingin mendaftarkan panggilan balik. Itu harus dipanggil saat membuat entitas baru - panggilan untuk
Create
. Kemudian kami menunjukkan kapan tepatnya meluncurkan Callback.
Before
bertanggung jawab untuk ini dengan argumen
begin_transaction
:
begin_transaction
adalah beberapa poin pada saat transaksi dibuka. Selanjutnya, dengan nama
newrelicStart
mendaftarkan fungsi
startSegment
, yang cukup memanggil agen Go dan membuka segmen baru untuk mengakses database.
ORM akan memanggil fungsi ini sebelum kita membuka transaksi, dan dengan demikian membuka segmen. Kita harus melakukan hal yang sama untuk menutup segmen: cukup tutup Callback.
Selain PostgreSQL, kami menggunakan Redis, yang juga tidak mulus. Untuk pemantauan ini, kami menulis pembungkus atas klien standar, dan melakukan hal yang sama untuk memanggil layanan eksternal. Inilah yang terjadi:

Seperti inilah pemantauan untuk aplikasi yang ditulis dalam Go. Di sebelah kiri adalah laporan tentang durasi pemrosesan kueri, yang terdiri dari segmen: eksekusi kode itu sendiri di Go, akses ke database PostgreSQL dan Replica. Panggilan ke layanan eksternal tidak ditampilkan pada grafik ini, karena jumlahnya sangat sedikit dan tidak terlihat saat dirata-rata. Kami juga memiliki informasi tentang Apdex dan frekuensi pemrosesan permintaan. Secara umum, pemantauan itu ternyata cukup informatif dan berguna untuk digunakan.
Adapun aliran data, berkat pembungkus kami di atas klien HTTP, kami dapat melacak permintaan ke layanan eksternal. Skema permintaan layanan promosi ditunjukkan di sini: mengacu pada empat layanan kami yang lain dan dua repositori.

Kesimpulan
Hari ini kami memiliki lebih dari 75% layanan produksi yang ditulis dalam Go, kami tidak melakukan pengembangan aktif di Ruby, tetapi hanya mendukungnya. Dan dalam hal ini, saya ingin mencatat:
- Kekhawatiran bahwa kecepatan pengembangan akan menurun belum dikonfirmasi. Pemrogram mencurahkan ke dalam teknologi baru masing-masing dalam mode sendiri, tetapi, rata-rata, setelah beberapa minggu bekerja aktif, pengembangan di Go menjadi dapat diprediksi dan secepat di Ruby on Rails.
- Kinerja aplikasi Go yang sedang dimuat sangat mengejutkan dibandingkan dengan pengalaman sebelumnya. Kami secara signifikan menghemat penggunaan infrastruktur di AWS, secara signifikan mengurangi jumlah instance yang digunakan.
- Perubahan teknologi telah secara signifikan mendorong programmer, dan ini adalah bagian penting dari proyek yang sukses.
- Hari ini kita telah meninggalkan Beego dan Gorm, lebih lanjut tentang ini di artikel selanjutnya.
Meringkas, saya ingin mengatakan bahwa jika Anda menulis tidak di Go, Anda menderita masalah beban kerja yang tinggi dan bosan dengan lalu lintas, buka bahasa ini. Hanya saja, jangan lupa bernegosiasi dengan bisnis.