Yandex.Taxi menganut arsitektur microservice. Dengan peningkatan jumlah layanan-mikro, kami memperhatikan bahwa pengembang menghabiskan banyak waktu pada pelat-pelat dan masalah-masalah tipikal, sementara solusi tidak selalu berhasil dengan optimal.
Kami memutuskan untuk membuat kerangka kerja kami sendiri, dengan C ++ 17 dan coroutine. Beginilah tampilan kode microservice biasa:
Response View::Handle(Request&& request, const Dependencies& dependencies) { auto cluster = dependencies.pg->GetCluster(); auto trx = cluster->Begin(storages::postgres::ClusterHostType::kMaster); const char* statement = "SELECT ok, baz FROM some WHERE id = $1 LIMIT 1"; auto row = psql::Execute(trx, statement, request.id)[0]; if (!row["ok"].As<bool>()) { LOG_DEBUG() << request.id << " is not OK of " << GetSomeInfoFromDb(); return Response400(); } psql::Execute(trx, queries::kUpdateRules, request.foo, request.bar); trx.Commit(); return Response200{row["baz"].As<std::string>()}; }
Dan inilah mengapa ini sangat efektif dan cepat - kami akan menjelaskannya.
Userver - Asynchronous
Tim kami tidak hanya terdiri dari pengembang C ++ yang berpengalaman: ada trainee, pengembang junior, dan bahkan orang-orang yang tidak terlalu terbiasa menulis dalam C ++. Oleh karena itu, desain pengguna didasarkan pada kemudahan penggunaan. Namun, dengan volume dan muatan data kami, kami juga tidak mampu memboroskan sumber daya besi secara tidak efisien.
Layanan-layanan microser dicirikan oleh ekspektasi input / output: seringkali respon dari sebuah layanan-mikro dibentuk dari beberapa tanggapan dari layanan-layanan dan basis data mikro lainnya. Masalah menunggu I / O yang efisien diselesaikan melalui metode asinkron dan callback: dengan operasi asinkron, tidak perlu untuk membuat utas eksekusi, dan karenanya, tidak ada overhead yang besar untuk beralih aliran ... hanya kode yang cukup sulit untuk ditulis dan dipelihara:
void View::Handle(Request&& request, const Dependencies& dependencies, Response response) { auto cluster = dependencies.pg->GetCluster(); cluster->Begin(storages::postgres::ClusterHostType::kMaster, [request = std::move(request), response](auto& trx) { const char* statement = "SELECT ok, baz FROM some WHERE id = $1 LIMIT 1"; psql::Execute(trx, statement, request.id, [request = std::move(request), response, trx = std::move(trx)](auto& res) { auto row = res[0]; if (!row["ok"].As<bool>()) { if (LogDebug()) { GetSomeInfoFromDb([id = request.id](auto info) { LOG_DEBUG() << id << " is not OK of " << info; }); } *response = Response400{}; } psql::Execute(trx, queries::kUpdateRules, request.foo, request.bar, [row = std::move(row), trx = std::move(trx), response]() { trx.Commit([row = std::move(row), response]() { *response = Response200{row["baz"].As<std::string>()}; }); }); }); }); }
Dan di sini stackfull-coroutine datang untuk menyelamatkan. Pengguna kerangka berpikir bahwa ia menulis kode sinkron biasa:
auto row = psql::Execute(trx, queries::kGetRules, request.id)[0];
Namun, kira-kira hal berikut terjadi di bawah tenda:
- Paket TCP dihasilkan dan dikirim dengan permintaan ke database;
- eksekusi coroutine, di mana fungsi View :: Handle sedang berjalan, ditangguhkan;
- kami mengatakan kepada kernel OS: "" Masukkan coroutine yang ditangguhkan dalam antrian tugas yang siap untuk dieksekusi segera setelah cukup paket TCP dari database ";
- tanpa menunggu langkah sebelumnya, kami mengambil dan meluncurkan coroutine lain yang siap dieksekusi dari antrian.
Dengan kata lain, fungsi dari contoh pertama bekerja secara sinkron dan dekat dengan kode tersebut menggunakan C ++ 20 Coroutines:
Response View::Handle(Request&& request, const Dependencies& dependencies) { auto cluster = dependencies.pg->GetCluster(); auto trx = co_await cluster->Begin(storages::postgres::ClusterHostType::kMaster); const char* statement = "SELECT ok, baz FROM some WHERE id = $1 LIMIT 1"; auto row = co_await psql::Execute(trx, statement, request.id)[0]; if (!row["ok"].As<bool>()) { LOG_DEBUG() << request.id << " is not OK of " << co_await GetSomeInfoFromDb(); co_return Response400{"NOT_OK", "Please provide different ID"}; } co_await psql::Execute(trx, queries::kUpdateRules, request.foo, request.bar); co_await trx.Commit(); co_return Response200{row["baz"].As<std::string>()}; }
Itu hanya pengguna tidak perlu berpikir tentang co_await dan co_return, semuanya berfungsi "sendiri".
Dalam kerangka kami, beralih di antara coroutine lebih cepat daripada memanggil std :: this_thread :: yield (). Seluruh layanan microsecer biayanya sangat sedikit.
Saat ini, userver berisi driver asinkron:
* untuk soket OS;
* http dan https (klien dan server);
* PostgreSQL;
* MongoDB;
* Redis;
* bekerja dengan file;
* pengatur waktu;
* primitif untuk menyinkronkan dan meluncurkan coroutine baru.
Pendekatan asinkron di atas untuk menyelesaikan tugas-tugas yang terikat I / O harus akrab bagi pengembang Go. Tetapi, tidak seperti Go, kami tidak mendapatkan overhead untuk memori dan CPU dari pengumpul sampah. Pengembang dapat menggunakan bahasa yang lebih kaya, dengan berbagai wadah dan perpustakaan berkinerja tinggi, tanpa menderita kurangnya konsistensi, RAII atau templat.
Pengguna - komponen
Tentu saja, kerangka kerja yang lengkap bukan hanya coroutine. Tugas pengembang di Taxi sangat beragam, dan masing-masing membutuhkan seperangkat alat sendiri untuk dipecahkan. Karena itu, userver memiliki semua yang Anda butuhkan:
* untuk logging;
* caching;
* bekerja dengan berbagai format data;
* bekerja dengan konfigurasi dan memperbarui konfigurasi tanpa memulai kembali layanan;
* kunci terdistribusi;
* pengujian;
* otorisasi dan otentikasi;
* membuat dan mengirim metrik;
* menulis penangan REST;
+ pembuatan kode dan dukungan dependensi (dibuat di bagian terpisah dari kerangka kerja).
Pengguna - pembuatan kode
Mari kita kembali ke baris pertama dari contoh kita dan melihat apa yang tersembunyi di balik Respons dan Permintaan:
Response Handle(Request&& request, const Dependencies& dependencies);
Dengan userver, Anda dapat menulis layanan microser apa pun, tetapi ada persyaratan untuk layanan microser kami bahwa API mereka harus didokumentasikan (dijelaskan melalui skema swagger).
Misalnya, untuk Pegangan dari contoh, diagram kesombongan mungkin terlihat seperti ini:
paths: /some/sample/{bar}: post: description: | Habr. summary: | , - . parameters: - in: query name: id type: string required: true - in: header name: foo type: string enum: - foo1 - foo2 required: true - in: path name: bar type: string required: true responses: '200': description: OK schema: type: object additionalProperties: false required: - baz properties: baz: type: string '400': $ref: '#/responses/ResponseCommonError'
Nah, karena pengembang sudah memiliki skema dengan deskripsi permintaan dan tanggapan, lalu mengapa tidak menghasilkan permintaan dan jawaban ini berdasarkan itu? Pada saat yang sama, tautan ke file protobuf / flatbuffer / ... juga dapat ditunjukkan dalam skema - pembuatan kode dari permintaan itu sendiri akan mendapatkan segalanya, memvalidasi data input sesuai dengan skema dan menguraikannya ke dalam bidang struktur Respons. Pengguna hanya perlu menulis fungsionalitas dalam metode Handle, tanpa terganggu oleh boilerplate dengan parsing permintaan dan serialisasi respons.
Pada saat yang sama, pembuatan kode berfungsi untuk pelanggan layanan. Anda dapat menunjukkan bahwa layanan Anda membutuhkan klien yang bekerja sesuai dengan skema tersebut, dan menyiapkan kelas yang siap digunakan untuk membuat permintaan asinkron:
Request req; req.id = id; req.foo = foo; req.bar = bar; dependencies.sample_client.SomeSampleBarPost(req);
Pendekatan ini memiliki kelebihan lainnya: selalu merupakan dokumentasi terbaru. Jika pengembang tiba-tiba mencoba menggunakan parameter yang tidak ada dalam dokumentasi, ia akan mendapatkan kesalahan kompilasi.
Pengguna - logging
Kami suka menulis log. Jika Anda hanya mencatat informasi yang paling penting, maka beberapa terabyte log per jam akan berjalan. Oleh karena itu, tidak mengherankan bahwa penebangan kami memiliki triknya sendiri:
* tidak sinkron (tentu saja :-));
* kita dapat mencatat bypassing std :: locale dan std :: ostream;
* kita dapat mengganti level logging dengan cepat (tanpa memulai kembali layanan);
* kami tidak mengeksekusi kode pengguna jika hanya diperlukan untuk logging.
Misalnya, selama operasi normal dari layanan microser, level logging akan diatur ke INFO, dan seluruh ekspresi
LOG_DEBUG() << request.id << " is not OK of " << GetSomeInfoFromDb();
tidak akan dihitung. Termasuk panggilan ke fungsi intensif sumber daya GetSomeInfoFromDb () tidak akan terjadi.
Jika tiba-tiba layanan mulai βbodohβ, pengembang selalu dapat memberi tahu layanan yang berfungsi: βMasuk dalam mode DEBUGβ. Dan dalam hal ini entri "tidak apa-apa" akan mulai muncul di log, fungsi GetSomeInfoFromDb () akan dieksekusi.
Alih-alih total
Dalam satu artikel tidak mungkin untuk mengetahui sekaligus tentang semua fitur dan trik. Karena itu, kami mulai dengan pengantar singkat. Tulis di komentar tentang hal-hal apa dari pengguna yang Anda ingin pelajari dan baca.
Sekarang kami sedang mempertimbangkan apakah akan memposting kerangka kerja dalam sumber terbuka. Jika kita memutuskan ya, menyiapkan kerangka kerja untuk membuka sumber akan membutuhkan banyak upaya.