Kurir: Migrasi Dropbox ke gRPC



Catatan Penerjemah


Sebagian besar produk perangkat lunak modern tidak monolitik, tetapi terdiri dari banyak bagian yang saling berinteraksi. Dalam situasi ini, penting bahwa komunikasi bagian-bagian yang berinteraksi dari sistem berlangsung dalam satu bahasa (terlepas dari kenyataan bahwa bagian-bagian ini sendiri dapat ditulis dalam bahasa pemrograman yang berbeda dan dijalankan pada mesin yang berbeda). Untuk menyederhanakan solusi untuk masalah ini, membantu gRPC - open-source-framework dari Google, dirilis pada 2015. Dia segera memecahkan sejumlah masalah, memungkinkan:

  • menggunakan bahasa Protokol Buffer untuk menggambarkan interaksi layanan;
  • menghasilkan kode program berdasarkan protokol yang dijelaskan untuk 11 bahasa yang berbeda untuk bagian klien dan bagian server;
  • menerapkan otorisasi antara komponen yang berinteraksi;
  • gunakan interaksi sinkron dan asinkron.

Bagi saya, gRPC merupakan kerangka kerja yang cukup menarik, dan saya tertarik untuk belajar tentang pengalaman nyata Dropbox dalam membangun sistem yang didasarkan padanya. Artikel ini memiliki banyak detail terkait dengan penggunaan enkripsi, membangun sistem yang andal, dapat diamati, dan produktif, proses migrasi dari solusi RPC lama ke yang baru.

Penafian
Artikel asli tidak mengandung deskripsi gRPC, dan beberapa poin mungkin tidak jelas bagi Anda. Jika Anda tidak terbiasa dengan gRPC atau kerangka kerja serupa lainnya (misalnya, Apache Thrift), saya sarankan Anda membiasakan diri dengan ide-ide utama (cukup membaca dua artikel kecil dari situs web resmi: "Apa itu gRPC?" Dan "Konsep gRPC" ).

Terima kasih kepada Aleksey Ivanov alias SaveTheRbtz untuk menulis artikel asli dan membantu menerjemahkan tempat-tempat sulit.

Dropbox mengelola banyak layanan yang ditulis dalam berbagai bahasa dan melayani jutaan permintaan per detik. Di pusat arsitektur berorientasi layanan kami adalah Courier, kerangka kerja RPC berbasis gPC. Dalam proses pengembangannya, kami belajar banyak tentang ekstensibilitas gRPC, optimalisasi kinerja, dan transisi dari sistem RPC sebelumnya.

Catatan: pos berisi cuplikan kode untuk Python dan Go. Kami juga menggunakan Rust dan Java.

Jalan menuju gRPC


Courier bukan kerangka kerja Dropbox RPC pertama. Bahkan sebelum kami mulai membagi sistem Python monolitik menjadi layanan terpisah, kami membutuhkan dasar yang dapat diandalkan untuk bertukar data antara layanan - terutama karena memilih kerangka kerja akan memiliki konsekuensi jangka panjang.

Sebelum itu, Dropbox bereksperimen dengan kerangka kerja RPC yang berbeda. Pertama, kami memiliki protokol individual untuk serialisasi manual dan deserialisasi. Beberapa layanan, seperti logging berbasis Scribe , menggunakan Apache Thrift . Pada saat yang sama, kerangka kerja RPC utama kami adalah protokol HTTP / 1.1 dengan pesan-pesan berseri menggunakan Protobuf.

Membuat kerangka kerja, kami memilih dari beberapa opsi. Kita bisa memperkenalkan Swagger (sekarang dikenal sebagai OpenAPI ) ke dalam kerangka kerja RPC lama, memperkenalkan standar baru, atau membangun kerangka kerja berdasarkan hemat atau gRPC. Argumen utama yang mendukung gRPC adalah kemungkinan menggunakan protobuf yang sudah ada sebelumnya. Juga, multiplex HTTP / 2 dan transfer data dua arah berguna untuk tugas kami.

Catatan: jika fbthrift ada pada saat itu, kami mungkin akan melihat lebih dekat solusi Thrift.

Apa yang dibawa oleh Courier ke gRPC


Kurir bukan protokol RPC; ini adalah cara mengintegrasikan gRPC ke dalam infrastruktur yang ada. Kerangka kerja ini seharusnya kompatibel dengan perangkat otentikasi, otorisasi, dan penemuan layanan kami, serta pengumpulan statistik, pencatatan, dan pelacakan. Jadi kami menciptakan Kurir.

Meskipun dalam beberapa kasus kami menggunakan Bandaid sebagai proksi gRPC, sebagian besar layanan kami berkomunikasi langsung satu sama lain untuk meminimalkan dampak RPC pada latensi.

Penting bagi kami untuk mengurangi jumlah kode rutin yang perlu ditulis. Karena Courier berfungsi sebagai kerangka kerja umum untuk mengembangkan layanan, Courier berisi fitur yang dibutuhkan semua orang. Sebagian besar dari mereka diaktifkan secara default dan dapat dikontrol oleh argumen baris perintah, dan beberapa dicentang dengan kotak centang.

Keamanan: Identitas Layanan dan TLS Mutual Authentication


Courier mengimplementasikan mekanisme identifikasi layanan standar kami. Setiap server dan klien diberi sertifikat TLS individu yang dikeluarkan oleh otoritas sertifikasi kami sendiri. Sertifikat pengidentifikasi pribadi yang disandikan, yang digunakan untuk otentikasi bersama - server memverifikasi klien, klien memverifikasi server.

Di TLS, tempat kami mengontrol kedua sisi koneksi, kami telah menerapkan batasan ketat. Semua RPC internal memerlukan enkripsi PFS . Versi TLS yang diperlukan adalah 1.2 dan lebih tinggi. Kami juga membatasi jumlah algoritma simetris dan asimetris, lebih memilih ECDHE-ECDSA-AES128-GCM-SHA256 .

Setelah melewati identifikasi dan dekripsi permintaan, server memeriksa apakah klien memiliki izin yang diperlukan. Daftar kontrol akses (ACL) dan batas kecepatan dapat dikonfigurasi baik untuk layanan secara umum maupun untuk metode individual. Parameter mereka juga dapat diubah melalui sistem file terdistribusi kami (AFS). Berkat ini, pemilik layanan dapat menjatuhkan beban dalam hitungan detik, bahkan tanpa memulai kembali prosesnya. Courier akan mengurus berlangganan pemberitahuan dan memperbarui konfigurasi.

Layanan Identity adalah pengidentifikasi global untuk ACL, batas kecepatan, statistik, dll. Selain itu, layanan ini aman secara kriptografis.

Berikut adalah contoh konfigurasi ACL dan batas kecepatan yang digunakan dalam layanan pengenalan pola optik kami:

limits:  dropbox_engine_ocr:    # All RPC methods.    default:      max_concurrency: 32      queue_timeout_ms: 1000      rate_acls:        # OCR clients are unlimited.        ocr: -1        # Nobody else gets to talk to us.        authenticated: 0        unauthenticated: 0 



Kami sedang mempertimbangkan kemungkinan beralih ke format SVID ( SPIFFE dokumen yang diverifikasi secara kriptografis), yang akan membantu menggabungkan kerangka kerja kami dengan banyak proyek sumber terbuka.

Observabilitas: statistik dan pelacakan


Dengan hanya satu pengidentifikasi, Anda dapat dengan mudah menemukan log, statistik, melacak file, dan data lainnya tentang Courier.



Selama pembuatan kode, pengumpulan statistik ditambahkan untuk setiap layanan dan setiap metode baik di sisi klien maupun di sisi server. Statistik sisi server dibagi dengan ID klien. Dalam konfigurasi standar, Anda akan menerima data terperinci tentang beban, kesalahan dan waktu tunda untuk setiap layanan menggunakan Kurir.



Statistik kurir mencakup data tentang ketersediaan dan latensi di sisi klien, serta jumlah permintaan dan ukuran antrian di sisi server. Ada grafik lain yang bermanfaat, khususnya histogram waktu respons untuk setiap metode dan waktu jabat tangan TLS untuk setiap klien.

Salah satu keuntungan dari pembuatan kode kami adalah kemungkinan inisialisasi statis struktur data, seperti histogram dan grafik jejak. Ini meminimalkan dampak kinerja.



Sistem RPC lama hanya mendistribusikan request_id melalui API. Ini memungkinkan untuk menggabungkan data dari log layanan yang berbeda. Di Courier, kami memperkenalkan API berdasarkan subset dari spesifikasi OpenTracing . Kami menulis perpustakaan kami sendiri di sisi klien, dan di sisi server kami menerapkan solusi berdasarkan Cassandra dan Jaeger .



Tracing memungkinkan kami untuk menghasilkan diagram dependensi dari suatu layanan saat runtime. Ini membantu insinyur melihat semua dependensi transitif dari layanan tertentu. Selain itu, fungsi ini berguna untuk melacak dependensi yang tidak diinginkan setelah penyebaran.

Keandalan: tenggat waktu dan pemutusan


Courier menyediakan tempat sentral untuk mengimplementasikan fungsi-fungsi klien umum (misalnya, batas waktu) dalam berbagai bahasa. Kami secara bertahap menambahkan berbagai fitur, seringkali berdasarkan hasil analisis "anumerta" dari masalah yang muncul.

Tenggat waktu


Setiap permintaan gRPC memiliki tenggat waktu yang menunjukkan batas waktu klien. Karena bertopik Courier secara otomatis mendistribusikan metadata yang diketahui, batas waktu permintaan bahkan ditransfer di luar API. Dalam proses tersebut, tenggat waktu menerima tampilan asli. Misalnya, di Go, mereka diwakili oleh hasil konteks . Konteks dari metode WithDeadline .

Bahkan, kami dapat memperbaiki seluruh kelas masalah keandalan dengan memaksa insinyur untuk menetapkan tenggat waktu dalam menentukan layanan yang sesuai.

Pendekatan ini bahkan melampaui RPC. Sebagai contoh, MySQL ORM kami membuat serial konteks RPC bersama dengan tenggat waktu dalam komentar permintaan SQL. Proxy SQL kami dapat menguraikan komentar dan "membunuh" kueri saat tenggat waktu terjadi. Dan sebagai bonus ketika melakukan debug panggilan basis data, kami memiliki kueri SQL yang mengikat ke kueri RPC tertentu.

Putuskan sambungan


Masalah umum lain yang dihadapi klien dari sistem RPC sebelumnya adalah penerapan algoritma dari keterlambatan eksponensial individu dan fluktuasi atas permintaan berulang.

Kami mencoba menemukan solusi cerdas untuk masalah pemutusan di Courier, dimulai dengan implementasi buffer LIFO (terakhir masuk, keluar pertama) antara layanan dan kumpulan tugas.



Jika terjadi kelebihan, LIFO akan otomatis terputus. Antrian, yang penting, dibatasi tidak hanya berdasarkan ukuran, tetapi juga berdasarkan waktu (permintaan dapat menghabiskan dalam antrian hanya pada waktu tertentu).

Minus LIFO - mengubah urutan permintaan pemrosesan. Jika Anda ingin mempertahankan pesanan awal, gunakan CoDel . Di sana juga, ada kemungkinan pemutusan, dan urutan permintaan pemrosesan akan tetap sama.



Introspeksi: titik akhir debugging


Meskipun debugging titik akhir tidak secara langsung menjadi bagian dari Courier, mereka banyak digunakan di seluruh Dropbox dan terlalu berguna untuk tidak disebutkan.

Untuk alasan keamanan, Anda dapat membukanya di port terpisah atau pada soket Unix (untuk mengontrol akses menggunakan izin file). Anda juga harus mempertimbangkan otentikasi timbal balik TLS, yang pengembang harus memberikan sertifikat mereka untuk akses ke titik akhir (terutama tidak hanya baca-saja).

Eksekusi


Kemampuan untuk menganalisis status suatu layanan selama operasinya sangat berguna untuk debugging. Misalnya, memori dinamis dan profil CPU dapat diakses melalui titik akhir HTTP atau gRPC .

Kami berencana untuk menggunakan kesempatan ini dalam prosedur verifikasi kenari - untuk mengotomatiskan pencarian perbedaan antara versi kode lama dan baru.

Endpoint memungkinkan untuk mengubah keadaan layanan saat runtime. Secara khusus, layanan berbasis Golang dapat secara dinamis mengkonfigurasi GCPercent .

Perpustakaan


Ekspor otomatis data khusus perpustakaan sebagai titik akhir RPC mungkin berguna untuk pengembang perpustakaan. Misalnya, perpustakaan malloc dapat membuang statistik internal ke dump . Contoh lain: titik akhir debugging dapat mengubah tingkat layanan log on the fly.

Rpc


Tentu saja, pemecahan masalah dalam protokol terenkripsi dan disandikan tidak mudah. Oleh karena itu, memperkenalkan alat sebanyak mungkin di tingkat RPC adalah ide yang bagus. Salah satu contoh dari API introspektif tersebut adalah solusi Channelz .

Tingkat aplikasi


Mampu mempelajari opsi tingkat aplikasi juga dapat bermanfaat. Contoh yang baik adalah titik akhir dengan informasi umum tentang aplikasi (dengan hash file sumber atau rakitan, baris perintah, dll.). Ini dapat digunakan oleh sistem orkestrasi untuk memverifikasi integritas ketika menggunakan layanan.

Optimalisasi kinerja


Dengan memperluas kerangka kerja gRPC kami ke skala yang diperlukan, kami menemukan beberapa kemacetan khusus untuk Dropbox.

Konsumsi Sumber Daya Jabat Tangan TLS


Dalam layanan yang melayani banyak hubungan, sebagai hasil dari jabat tangan TLS, beban CPU gabungan bisa sangat serius (terutama ketika me-reboot layanan populer).

Untuk meningkatkan kinerja saat menandatangani, kami mengganti pasangan kunci RSA-2048 dengan ECDSA P-256. Berikut adalah contoh kinerja mereka (catatan: dengan RSA, verifikasi tanda tangan lebih cepat).

RSA:

 ~/c0d3/boringssl bazel run -- //:bssl speed -filter 'RSA 2048' Did ... RSA 2048 signing operations in ..............  (1527.9 ops/sec) Did ... RSA 2048 verify (same key) operations in .... (37066.4 ops/sec) Did ... RSA 2048 verify (fresh key) operations in ... (25887.6 ops/sec) 

ECDSA:

 ~/c0d3/boringssl bazel run -- //:bssl speed -filter 'ECDSA P-256' Did ... ECDSA P-256 signing operations in ... (40410.9 ops/sec) Did ... ECDSA P-256 verify operations in .... (17037.5 ops/sec) 

Karena verifikasi dengan RSA-2048 sekitar tiga kali lebih cepat daripada dengan ECDSA P-256, Anda dapat memilih RSA untuk sertifikat root dan akhir untuk meningkatkan kecepatan operasi. Tetapi dari sudut pandang keamanan, tidak semuanya begitu sederhana: Anda akan membangun rantai berbagai kriptografi primitif, dan oleh karena itu, tingkat parameter keamanan yang dihasilkan akan menjadi yang terendah. Dan jika Anda ingin meningkatkan kinerja, kami tidak menyarankan menggunakan sertifikat versi RSA-4096 (dan lebih tinggi) sebagai sertifikat root dan akhir.

Kami juga menemukan bahwa memilih perpustakaan TLS (dan bendera kompilasi) memiliki dampak signifikan pada kinerja dan keamanan. Bandingkan, misalnya, LibreSSL dibangun di atas macOS X Mojave dengan OpenSSL yang ditulis sendiri pada perangkat keras yang sama.

LibreSSL 2.6.4:

 ~ openssl speed rsa2048 LibreSSL 2.6.4 ...                 sign verify sign/s verify/s rsa 2048 bits 0.032491s 0.001505s     30.8 664.3 

OpenSSL 1.1.1a:

  ~ openssl speed rsa2048 OpenSSL 1.1.1a  20 Nov 2018 ...                 sign verify sign/s verify/s rsa 2048 bits 0.000992s 0.000029s   1208.0 34454.8 

Namun, cara tercepat untuk membuat jabat tangan TLS adalah dengan tidak membuatnya sama sekali! Kami telah menyertakan dukungan untuk dimulainya kembali sesi di gRPC-core dan gRPC-python, sehingga mengurangi beban pada CPU selama penyebaran.

Enkripsi tidak mahal


Banyak yang secara keliru percaya bahwa enkripsi itu mahal. Bahkan, bahkan komputer modern paling sederhana pun melakukan enkripsi simetris hampir secara instan. Prosesor standar dapat mengenkripsi dan mengotentikasi data pada kecepatan 40 Gb / s per inti:

 ~/c0d3/boringssl bazel run -- //:bssl speed -filter 'AES' Did ... AES-128-GCM (8192 bytes) seal operations in ... 4534.4 MB/s 

Namun demikian, kami masih harus mengonfigurasi gRPC untuk blok memori kami, yang beroperasi pada kecepatan 50 Gb / s. Kami menemukan bahwa jika kecepatan enkripsi kira-kira sama dengan kecepatan salin, maka penting untuk meminimalkan jumlah operasi memcpy. Selain itu, kami membuat beberapa perubahan pada gRPC itu sendiri.

Protokol yang diautentikasi dan terenkripsi menghindari banyak masalah yang tidak menyenangkan (misalnya, kerusakan data oleh prosesor, DMA atau pada jaringan). Bahkan jika Anda tidak menggunakan gRPC, kami menyarankan untuk menggunakan TLS untuk kontak internal.

Saluran Data Latensi Tinggi (BDP)


Catatan Penerjemah: subtitle asli menggunakan istilah produk bandwidth-delay , yang tidak memiliki terjemahan mapan ke dalam bahasa Rusia.

Jaringan backbone Dropbox mencakup banyak pusat data . Kadang-kadang node yang berada di berbagai daerah harus berkomunikasi melalui RPC, misalnya, untuk replikasi. Saat menggunakan TCP, kernel sistem bertanggung jawab untuk membatasi jumlah data yang dikirimkan dalam koneksi tertentu (dalam / proc / sys / net / ipv4 / tcp_ {r, w} mem ), meskipun gRPC berdasarkan HTTP / 2 memiliki alat sendiri kontrol aliran. Batas atas BDP di grpc-go terbatas pada 16 MB , yang dapat memicu kemacetan.

net.Server Golang atau grpc.Server


Awalnya, dalam kode Go kami, kami mendukung HTTP / 1.1 dan gRPC dengan satu net.Server . Solusinya masuk akal dalam hal mempertahankan kode program, tetapi tidak berhasil sama sekali. Mendistribusikan HTTP / 1.1 dan gRPC di seluruh server dan memigrasikan gRPC ke grpc.Server secara signifikan meningkatkan bandwidth Courier dan penggunaan memori.

golang / protobuf atau gogo / protobuf


Beralih ke gRPC dapat meningkatkan biaya marshaling dan unmarshaling. Untuk kode Go, kami dapat secara signifikan mengurangi beban CPU pada server Kurir dengan beralih ke gogo / protobuf .

Seperti biasa, transisi ke gogo / protobuf disertai oleh beberapa masalah , tetapi jika Anda cukup membatasi fungsionalitasnya, seharusnya tidak ada masalah.

Detail Implementasi


Pada bagian ini, kami akan menembus lebih dalam ke perangkat Courier, mempertimbangkan skema protobuf dan contoh stub dari berbagai bahasa. Semua contoh diambil dari layanan Uji, yang kami gunakan selama pengujian integrasi Kurir.

Deskripsi Layanan


Lihatlah kutipan dari definisi layanan Uji:

 service Test {   option (rpc_core.service_default_deadline_ms) = 1000;   rpc UnaryUnary(TestRequest) returns (TestResponse) {       option (rpc_core.method_default_deadline_ms) = 5000;   }   rpc UnaryStream(TestRequest) returns (stream TestResponse) {       option (rpc_core.method_no_deadline) = true;   }   ... } 

Sebagaimana disebutkan di atas, batas waktu diperlukan untuk semua metode Kurir. Dengan menggunakan opsi berikut, Anda dapat mengatur batas waktu untuk seluruh layanan:

 option (rpc_core.service_default_deadline_ms) = 1000; 

Pada saat yang sama, setiap metode dapat diatur ke batas waktunya sendiri, membatalkan batas waktu seluruh layanan (jika ada):

 option (rpc_core.method_default_deadline_ms) = 5000; 

Dalam kasus yang jarang terjadi ketika tenggat waktu tidak masuk akal (misalnya, saat melacak sumber daya), pengembang dapat menonaktifkannya:

 option (rpc_core.method_no_deadline) = true; 

Selain itu, deskripsi layanan harus berisi dokumentasi API terperinci, mungkin dengan contoh penggunaan.

Generasi rintisan


Untuk memberikan fleksibilitas yang lebih besar, Courier menghasilkan bertopik sendiri tanpa bergantung pada fungsi pencegat yang disediakan oleh gRPC (dengan pengecualian Java, di mana API pencegat memiliki daya yang cukup). Mari kita bandingkan bertopik kita dengan bertopik standar Golang.

Ini adalah apa yang tampak seperti rintisan server gRPC standar:

 func _Test_UnaryUnary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {       in := new(TestRequest)       if err := dec(in); err != nil {               return nil, err       }       if interceptor == nil {               return srv.(TestServer).UnaryUnary(ctx, in)       }       info := &grpc.UnaryServerInfo{               Server: srv,               FullMethod: "/test.Test/UnaryUnary",       }       handler := func(ctx context.Context, req interface{}) (interface{}, error) {               return srv.(TestServer).UnaryUnary(ctx, req.(*TestRequest))       }       return interceptor(ctx, in, info, handler) } 

Semua pemrosesan terjadi di dalam: decoding protobuf, meluncurkan pencegat (lihat variabel interceptor dalam kode), meluncurkan penangan UnaryUnary.

Sekarang lihat bertopik Courier:

 func _Test_UnaryUnary_dbxHandler(       srv interface{},       ctx context.Context,       dec func(interface{}) error,       interceptor grpc.UnaryServerInterceptor) (       interface{},       error) {       defer processor.PanicHandler()       impl := srv.(*dbxTestServerImpl)       metadata := impl.testUnaryUnaryMetadata       ctx = metadata.SetupContext(ctx)       clientId = client_info.ClientId(ctx)       stats := metadata.StatsMap.GetOrCreatePerClientStats(clientId)       stats.TotalCount.Inc()       req := &processor.UnaryUnaryRequest{               Srv: srv,               Ctx: ctx,               Dec: dec,               Interceptor: interceptor,               RpcStats: stats,               Metadata: metadata,               FullMethodPath: "/test.Test/UnaryUnary",               Req: &test.TestRequest{},               Handler: impl._UnaryUnary_internalHandler,               ClientId: clientId,               EnqueueTime: time.Now(),       }       metadata.WorkPool.Process(req).Wait()       return req.Resp, req.Err } 

Ada sedikit kode di sini, jadi mari kita uraikan.

Pertama, kami menunda panggilan ke panic handler, yang bertanggung jawab untuk mengumpulkan kesalahan secara otomatis. Ini akan memungkinkan kami untuk mengumpulkan semua pengecualian yang tidak tertangkap dalam repositori pusat untuk pengumpulan dan pelaporan selanjutnya:

 defer processor.PanicHandler() 

Alasan lain kami menjalankan panic handler kami sendiri adalah untuk memastikan bahwa aplikasi mogok jika terjadi kesalahan. Golang / net HTTP handler standar dalam kasus ini akan mengabaikan masalah dan terus melayani permintaan baru (bahkan rusak dan tidak konsisten).

Lalu kami meneruskan konteks, mendefinisikan ulang nilai berdasarkan metadata dari permintaan yang masuk:

 ctx = metadata.SetupContext(ctx) clientId = client_info.ClientId(ctx) 

Kami juga membuat (dan cache untuk efisiensi yang lebih besar) statistik klien sisi server untuk agregasi yang lebih rinci:

 stats := metadata.StatsMap.GetOrCreatePerClientStats(clientId) 

Baris ini membuat statistik untuk setiap klien (yaitu, pengidentifikasi TLS) selama eksekusi. Kami juga memiliki statistik tentang semua metode untuk setiap layanan. Karena stub generator memiliki akses ke semua metode selama pembuatan kode, kita dapat membuatnya secara statis sebelumnya, sehingga tidak memperlambat program.

Setelah itu, kami membuat struktur permintaan, mentransfernya ke kumpulan tugas dan menunggu eksekusi:

 req := &processor.UnaryUnaryRequest{       Srv:        srv,       Ctx:        ctx,       Dec:        dec,       Interceptor:    interceptor,       RpcStats:       stats,       Metadata:       metadata,       ... } metadata.WorkPool.Process(req).Wait() 

Harap dicatat bahwa pada titik ini kami tidak memecahkan kode protobuf, atau meluncurkan pencegat. Sebelum ini, kumpulan akses, penentuan prioritas dan batasan jumlah permintaan yang dieksekusi harus melalui kumpulan tugas.

Perhatikan bahwa pustaka gRPC mendukung antarmuka TAP, yang memungkinkan Anda mencegat permintaan dengan kecepatan luar biasa. Antarmuka menyediakan infrastruktur untuk membangun pembatas kecepatan yang efektif dengan konsumsi sumber daya minimal.

Kode kesalahan khusus untuk aplikasi yang berbeda


Generator rintisan kami juga memungkinkan pengembang untuk menetapkan kode kesalahan khusus aplikasi menggunakan opsi khusus:

 enum ErrorCode { option (rpc_core.rpc_error) = true; UNKNOWN = 0; NOT_FOUND = 1 [(rpc_core.grpc_code)="NOT_FOUND"]; ALREADY_EXISTS = 2 [(rpc_core.grpc_code)="ALREADY_EXISTS"]; ... STALE_READ = 7 [(rpc_core.grpc_code)="UNAVAILABLE"]; SHUTTING_DOWN = 8 [(rpc_core.grpc_code)="CANCELLED"]; } 

Baik gRPC dan kesalahan aplikasi menyebar dalam layanan, dan di perbatasan API, semua kesalahan diganti oleh UNKNOWN. Berkat ini, kami dapat menghindari mentransfer masalah ke layanan lain, yang dapat mengakibatkan perubahan dalam semantik mereka.

Perubahan Python


Rintisan python menambahkan parameter konteks eksplisit ke semua penangan Courier:

 from dropbox.context import Context from dropbox.proto.test.service_pb2 import (       TestRequest,       TestResponse, ) from typing_extensions import Protocol class TestCourierClient(Protocol):   def UnaryUnary(           self,           ctx, # type: Context           request, # type: TestRequest           ):       # type: (...) -> TestResponse       ... 

Pada awalnya itu terlihat aneh, tetapi seiring waktu, para pengembang terbiasa dengan ctx eksplisit seperti dulu.

Harap dicatat bahwa stub kami sepenuhnya diketik untuk mypy , yang diimbangi selama refactoring utama. Selain itu, integrasi dengan beberapa IDE (mis. PyCharm) disederhanakan.

Terus mengikuti tren pengetikan statis, kami menambahkan anotasi mypy ke protokol itu sendiri:

 class TestMessage(Message):   field: int   def __init__(self,       field : Optional[int] = ...,       ) -> None: ...   @staticmethod   def FromString(s: bytes) -> TestMessage: ... 

Anotasi ini akan menghindari banyak bug umum, misalnya menetapkan nilai Tidak Ada pada bidang string jenis, misalnya .

Kode ini tersedia di sini .

Proses migrasi


Membuat tumpukan-RPC baru bukanlah tugas yang mudah, tetapi bahkan tidak berada di samping proses transisi penuh ke sana, jika Anda melihat dari sudut pandang kerumitan operasi. Oleh karena itu, kami mencoba membuatnya semudah mungkin bagi pengembang untuk beralih dari RPC lama ke Courier. Karena migrasi sering disertai dengan kesalahan, kami memutuskan untuk mengimplementasikannya secara bertahap.

Langkah 0: bekukan RPC lama


Pertama-tama, kami membekukan RPC lama agar tidak menembak target yang bergerak. Itu juga mendorong orang untuk beralih ke Courier, karena semua fitur baru seperti tracing hanya tersedia dalam layanan di Courier.

Langkah 1: antarmuka umum untuk RPC dan Kurir lama


Kami mulai dengan mendefinisikan antarmuka umum untuk RPC dan Kurir yang lama. Pembuatan kode kami seharusnya memastikan bahwa kedua versi bertopik sesuai dengan antarmuka ini:

 type TestServer interface {  UnaryUnary(     ctx context.Context,     req *test.TestRequest) (     *test.TestResponse,     error)  ... } 

Langkah 2: bermigrasi ke antarmuka baru


Setelah itu, kami mulai mengalihkan setiap layanan ke antarmuka baru, sambil terus menggunakan RPC yang lama. Seringkali, perubahan kode sangat berbeda, yang memengaruhi semua metode layanan dan kliennya. Karena tahap ini adalah yang paling bermasalah, kami ingin sepenuhnya menghilangkan risiko dengan mengubah hanya satu hal pada satu waktu.

Layanan sederhana dengan sejumlah kecil metode dan hak untuk melakukan kesalahan dapat dimigrasikan secara bersamaan, tanpa memperhatikan peringatan kami.

Langkah 3: memigrasikan pelanggan ke Kurir RPC


Selama proses migrasi, kami mulai meluncurkan server lama dan baru secara bersamaan di berbagai port dari mesin yang sama. Mengalihkan implementasi RPC sisi klien dilakukan dengan mengubah satu baris:

 class MyClient(object): def __init__(self): -   self.client = LegacyRPCClient('myservice') +   self.client = CourierRPCClient('myservice') 

Harap dicatat bahwa dengan model ini, Anda dapat mentransfer satu klien pada suatu waktu, dimulai dengan yang memiliki tingkat SLA yang lebih rendah.

Langkah 4: membersihkan


, , RPC ( ). — .

Kesimpulan


, Courier — RPC-, , Dropbox.

, Courier:

  1. — . .
  2. — , .
  3. , . Codegen.
  4. . , , . , : .
  5. RPC- — , . . .


Courier, gRPC , , , .

gRPC Python , C++ Python Rust . ALTS TLS- (, ).

Source: https://habr.com/ru/post/id438474/


All Articles