Praktik terbaik performa dengan gRPC
Oleh James Newton-King
gRPC dirancang untuk layanan berkinerja tinggi. Dokumen ini menjelaskan cara mendapatkan performa terbaik dari gRPC.
Menggunakan kembali saluran gRPC
Saluran gRPC harus digunakan kembali saat melakukan panggilan gRPC. Menggunakan kembali saluran memungkinkan panggilan untuk di-multipleks melalui koneksi HTTP/2 yang ada.
Jika saluran baru dibuat untuk setiap panggilan gRPC, jumlah waktu yang diperlukan untuk menyelesaikannya dapat meningkat secara signifikan. Setiap panggilan akan memerlukan beberapa komunikasi dua arah jaringan antara klien dan server untuk membuat koneksi HTTP/2 baru:
- Membuka soket
- Membuat koneksi TCP
- Menegosiasikan TLS
- Memulai koneksi HTTP/2
- Melakukan panggilan gRPC
Saluran aman untuk dibagikan dan digunakan kembali antara panggilan gRPC:
- Klien gRPC dibuat dengan saluran. Klien gRPC adalah objek ringan dan tidak perlu di-cache atau digunakan kembali.
- Beberapa klien gRPC dapat dibuat dari saluran, termasuk berbagai jenis klien.
- Saluran dan klien yang dibuat dari saluran dapat digunakan dengan aman oleh beberapa utas.
- Klien yang dibuat dari saluran dapat melakukan beberapa panggilan simultan.
Pabrik klien gRPC menawarkan cara terpusat untuk mengonfigurasi saluran. Ini secara otomatis menggunakan kembali saluran yang mendasar. Untuk informasi selengkapnya, lihat integrasi pabrik klien gRPC di .NET.
Konkurensi koneksi
Koneksi HTTP/2 biasanya memiliki batasan jumlah aliran bersamaan maksimum (permintaan HTTP aktif) pada koneksi pada satu waktu. Secara default, sebagian besar server menetapkan batas ini ke 100 aliran bersamaan.
Saluran gRPC menggunakan satu koneksi HTTP/2, dan panggilan bersamaan di-multipleks pada koneksi tersebut. Ketika jumlah panggilan aktif mencapai batas aliran koneksi, panggilan tambahan diantrekan di klien. Panggilan antrean menunggu panggilan aktif selesai sebelum dikirim. Aplikasi dengan beban tinggi, atau panggilan gRPC streaming yang berjalan lama, dapat melihat masalah performa yang disebabkan oleh antrean panggilan karena batas ini.
.NET 5 memperkenalkan SocketsHttpHandler.EnableMultipleHttp2Connections properti . Ketika diatur ke true, koneksi HTTP/2 tambahan dibuat oleh saluran ketika batas aliran bersamaan tercapai. GrpcChannel Ketika dibuat, internalnya SocketsHttpHandler secara otomatis dikonfigurasi untuk membuat koneksi HTTP/2 tambahan. Jika aplikasi mengonfigurasi handler-nya sendiri, pertimbangkan untuk mengatur EnableMultipleHttp2Connections ke true:
var channel = GrpcChannel.ForAddress("https://localhost", new GrpcChannelOptions
{
HttpHandler = new SocketsHttpHandler
{
EnableMultipleHttp2Connections = true,
// ...configure other handler settings
}
});
Ada beberapa solusi untuk aplikasi .NET Core 3.1:
- Buat saluran gRPC terpisah untuk area aplikasi dengan beban tinggi. Misalnya,
Loggerlayanan gRPC mungkin memiliki beban tinggi. Gunakan saluran terpisah untuk membuatLoggerClientdi aplikasi. - Gunakan kumpulan saluran gRPC, misalnya, buat daftar saluran gRPC.
Randomdigunakan untuk memilih saluran dari daftar setiap kali saluran gRPC diperlukan. MenggunakanRandommendistribusikan panggilan secara acak melalui beberapa koneksi.
Penting
Meningkatkan batas aliran bersamaan maksimum pada server adalah cara lain untuk menyelesaikan masalah ini. Dalam hal ini dikonfigurasi Kestrel dengan MaxStreamsPerConnection.
Meningkatkan batas aliran bersamaan maksimum tidak disarankan. Terlalu banyak aliran pada satu koneksi HTTP/2 memperkenalkan masalah performa baru:
- Pertikaian utas antara aliran yang mencoba menulis ke koneksi.
- Kehilangan paket koneksi menyebabkan semua panggilan diblokir di lapisan TCP.
ServerGarbageCollection di aplikasi klien
Pengumpul sampah .NET memiliki dua mode: pengumpulan sampah stasiun kerja (GC) dan pengumpulan sampah server. Masing-masing disetel untuk beban kerja yang berbeda. ASP.NET Core menggunakan GC server secara default.
Aplikasi yang sangat bersamaan umumnya berkinerja lebih baik dengan GC server. Jika aplikasi klien gRPC mengirim dan menerima sejumlah besar panggilan gRPC secara bersamaan, maka mungkin ada manfaat performa dalam memperbarui aplikasi untuk menggunakan GC server.
Untuk mengaktifkan GC server, atur <ServerGarbageCollection> dalam file proyek aplikasi:
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
Untuk informasi selengkapnya tentang pengumpulan sampah, lihat Stasiun kerja dan pengumpulan sampah server.
Catatan
ASP.NET Core menggunakan GC server secara default. Mengaktifkan <ServerGarbageCollection> hanya berguna di aplikasi klien gRPC non-server, misalnya di aplikasi konsol klien gRPC.
Penyeimbangan beban
Beberapa load balancer tidak bekerja secara efektif dengan gRPC. Penyeimbang beban L4 (transportasi) beroperasi pada tingkat koneksi, dengan mendistribusikan koneksi TCP di seluruh titik akhir. Pendekatan ini berfungsi dengan baik untuk memuat panggilan API penyeimbangan yang dilakukan dengan HTTP/1.1. Panggilan bersamaan yang dilakukan dengan HTTP/1.1 dikirim pada koneksi yang berbeda, memungkinkan panggilan dimuat seimbang di seluruh titik akhir.
Karena load balancer L4 beroperasi pada tingkat koneksi, penyeimbang beban tidak berfungsi dengan baik dengan gRPC. gRPC menggunakan HTTP/2, yang multipleks beberapa panggilan pada satu koneksi TCP. Semua panggilan gRPC melalui koneksi tersebut masuk ke satu titik akhir.
Ada dua opsi untuk memuat keseimbangan gRPC secara efektif:
- Penyeimbangan beban sisi klien
- Penyeimbangan beban proksi L7 (aplikasi)
Catatan
Hanya panggilan gRPC yang dapat diseimbangkan bebannya di antara titik akhir. Setelah panggilan gRPC streaming dibuat, semua pesan yang dikirim melalui aliran masuk ke satu titik akhir.
Penyeimbangan beban sisi klien
Dengan penyeimbangan beban sisi klien, klien tahu tentang titik akhir. Untuk setiap panggilan gRPC, ia memilih titik akhir yang berbeda untuk mengirim panggilan. Penyeimbangan beban sisi klien adalah pilihan yang baik ketika latensi penting. Tidak ada proksi antara klien dan layanan, sehingga panggilan dikirim ke layanan secara langsung. Kelemahan dari penyeimbangan beban sisi klien adalah bahwa setiap klien harus melacak titik akhir yang tersedia yang harus digunakan.
Penyeimbangan beban klien Lookaside adalah teknik di mana status penyeimbangan beban disimpan di lokasi pusat. Klien secara berkala mengkueri lokasi pusat untuk informasi yang akan digunakan saat membuat keputusan penyeimbangan beban.
Untuk informasi selengkapnya, lihat penyeimbangan beban sisi klien gRPC.
Penyeimbangan beban proksi
Proksi L7 (aplikasi) bekerja pada tingkat yang lebih tinggi daripada proksi L4 (transportasi). Proksi L7 memahami HTTP/2, dan dapat mendistribusikan panggilan gRPC multipleks ke proksi pada satu koneksi HTTP/2 di beberapa titik akhir. Menggunakan proksi lebih sederhana daripada penyeimbangan beban sisi klien, tetapi dapat menambahkan latensi ekstra ke panggilan gRPC.
Ada banyak proksi L7 yang tersedia. Beberapa opsinya adalah:
- Envoy - Proksi sumber terbuka populer.
- Linkerd - Jala layanan untuk Kubernetes.
- YARP: Namun Proksi Terbalik Lainnya - Proksi sumber terbuka yang ditulis dalam .NET.
Komunikasi antar-proses
Panggilan gRPC antara klien dan layanan biasanya dikirim melalui soket TCP. TCP sangat bagus untuk berkomunikasi di seluruh jaringan, tetapi komunikasi antar-proses (IPC) lebih efisien ketika klien dan layanan berada di komputer yang sama.
Pertimbangkan untuk menggunakan transportasi seperti soket domain Unix atau pipa bernama untuk panggilan gRPC antar proses pada komputer yang sama. Untuk informasi selengkapnya, lihat Komunikasi antarproses dengan gRPC.
Ping tetap hidup
Ping tetap hidup dapat digunakan untuk menjaga koneksi HTTP/2 tetap hidup selama periode tidak aktif. Memiliki koneksi HTTP/2 yang ada siap ketika aplikasi melanjutkan aktivitas memungkinkan panggilan gRPC awal dilakukan dengan cepat, tanpa penundaan yang disebabkan oleh koneksi yang dipublikasikan kembali.
Ping tetap hidup dikonfigurasi pada SocketsHttpHandler:
var handler = new SocketsHttpHandler
{
PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan,
KeepAlivePingDelay = TimeSpan.FromSeconds(60),
KeepAlivePingTimeout = TimeSpan.FromSeconds(30),
EnableMultipleHttp2Connections = true
};
var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
{
HttpHandler = handler
});
Kode sebelumnya mengonfigurasi saluran yang mengirim ping tetap hidup ke server setiap 60 detik selama periode tidak aktif. Ping memastikan server dan proksi apa pun yang digunakan tidak akan menutup koneksi karena tidak aktif.
Kontrol aliran
Kontrol alur HTTP/2 adalah fitur yang mencegah aplikasi kewalahan dengan data. Saat menggunakan kontrol alur:
- Setiap koneksi dan permintaan HTTP/2 memiliki jendela buffer yang tersedia. Jendela buffer adalah berapa banyak data yang dapat diterima aplikasi sekaligus.
- Kontrol alur diaktifkan jika jendela buffer terisi. Saat diaktifkan, aplikasi pengirim menjeda pengiriman lebih banyak data.
- Setelah aplikasi penerima memproses data, maka ruang di jendela buffer tersedia. Aplikasi pengirim melanjutkan pengiriman data.
Kontrol alur dapat berdampak negatif pada performa saat menerima pesan besar. Jika jendela buffer lebih kecil dari payload pesan masuk atau ada latensi antara klien dan server, maka data dapat dikirim dalam semburan mulai/berhenti.
Masalah performa kontrol alur dapat diperbaiki dengan meningkatkan ukuran jendela buffer. Dalam Kestrel, ini dikonfigurasi dengan InitialConnectionWindowSize dan InitialStreamWindowSize saat startup aplikasi:
builder.WebHost.ConfigureKestrel(options =>
{
var http2 = options.Limits.Http2;
http2.InitialConnectionWindowSize = 2 * 1024 * 1024 * 2; // 2 MB
http2.InitialStreamWindowSize = 1024 * 1024; // 1 MB
});
Rekomendasi:
- Jika layanan gRPC sering menerima pesan yang lebih besar dari 96 KB, Kestrelukuran jendela streaming default, pertimbangkan untuk meningkatkan koneksi dan ukuran jendela streaming.
- Ukuran jendela koneksi harus selalu sama dengan atau lebih besar dari ukuran jendela aliran. Aliran adalah bagian dari koneksi, dan pengirim dibatasi oleh keduanya.
Untuk informasi selengkapnya tentang cara kerja kontrol alur, lihat Kontrol Alur HTTP/2 (posting blog).
Penting
Meningkatkan Kestrelukuran jendela memungkinkan Kestrel buffer lebih banyak data atas nama aplikasi, yang mungkin meningkatkan penggunaan memori. Hindari mengonfigurasi ukuran jendela yang tidak perlu besar.
Streaming
Streaming dua arah gRPC dapat digunakan untuk menggantikan panggilan gRPC unary dalam skenario performa tinggi. Setelah aliran dua arah dimulai, streaming pesan bolak-balik lebih cepat daripada mengirim pesan dengan beberapa panggilan gRPC unary. Pesan yang di-streaming dikirim sebagai data pada permintaan HTTP/2 yang ada dan menghilangkan overhead pembuatan permintaan HTTP/2 baru untuk setiap panggilan tidak sah.
Contoh layanan:
public override async Task SayHello(IAsyncStreamReader<HelloRequest> requestStream,
IServerStreamWriter<HelloReply> responseStream, ServerCallContext context)
{
await foreach (var request in requestStream.ReadAllAsync())
{
var helloReply = new HelloReply { Message = "Hello " + request.Name };
await responseStream.WriteAsync(helloReply);
}
}
Contoh klien:
var client = new Greet.GreeterClient(channel);
using var call = client.SayHello();
Console.WriteLine("Type a name then press enter.");
while (true)
{
var text = Console.ReadLine();
// Send and receive messages over the stream
await call.RequestStream.WriteAsync(new HelloRequest { Name = text });
await call.ResponseStream.MoveNext();
Console.WriteLine($"Greeting: {call.ResponseStream.Current.Message}");
}
Mengganti panggilan tidak biasa dengan streaming dua arah karena alasan performa adalah teknik lanjutan dan tidak sesuai dalam banyak situasi.
Menggunakan panggilan streaming adalah pilihan yang baik ketika:
- Throughput tinggi atau latensi rendah diperlukan.
- gRPC dan HTTP/2 diidentifikasi sebagai penyempitan performa.
- Pekerja di klien mengirim atau menerima pesan reguler dengan layanan gRPC.
Waspadai kompleksitas dan batasan tambahan dalam menggunakan panggilan streaming alih-alih unary:
- Aliran dapat terganggu oleh kesalahan layanan atau koneksi. Logika diperlukan untuk memulai ulang aliran jika ada kesalahan.
RequestStream.WriteAsynctidak aman untuk multi-utas. Hanya satu pesan yang dapat ditulis ke aliran pada satu waktu. Mengirim pesan dari beberapa utas melalui satu aliran memerlukan antrean produsen/konsumen seperti Channel<T> untuk melakukan marshall pesan.- Metode streaming gRPC terbatas untuk menerima satu jenis pesan dan mengirim satu jenis pesan. Misalnya,
rpc StreamingCall(stream RequestMessage) returns (stream ResponseMessage)menerimaRequestMessagedan mengirimResponseMessage. Dukungan Protobuf untuk pesan yang tidak diketahui atau bersyukur menggunakanAnydanoneofdapat mengatasi batasan ini.
Payload biner
Payload biner didukung di Protobuf dengan bytes jenis nilai skalar. Properti yang dihasilkan dalam C# menggunakan ByteString sebagai jenis properti.
syntax = "proto3";
message PayloadResponse {
bytes data = 1;
}
Protobuf adalah format biner yang secara efisien menserialisasikan payload biner besar dengan overhead minimal. Format berbasis teks seperti JSAKTIF memerlukan pengodean byte ke base64 dan menambahkan 33% ke ukuran pesan.
Saat bekerja dengan payload besar ByteString , ada beberapa praktik terbaik untuk menghindari salinan dan alokasi yang tidak perlu yang dibahas di bawah ini.
Mengirim payload biner
ByteString instans biasanya dibuat menggunakan ByteString.CopyFrom(byte[] data). Metode ini mengalokasikan baru ByteString dan baru byte[]. Data disalin ke dalam array byte baru.
Alokasi dan salinan tambahan dapat dihindari dengan menggunakan UnsafeByteOperations.UnsafeWrap(ReadOnlyMemory<byte> bytes) untuk membuat ByteString instans.
var data = await File.ReadAllBytesAsync(path);
var payload = new PayloadResponse();
payload.Data = UnsafeByteOperations.UnsafeWrap(data);
Byte tidak disalin dengan UnsafeByteOperations.UnsafeWrap sehingga tidak boleh dimodifikasi saat ByteString sedang digunakan.
UnsafeByteOperations.UnsafeWrap memerlukan Google.Protobuf versi 3.15.0 atau yang lebih baru.
Membaca payload biner
Data dapat dibaca secara efisien dari ByteString instans dengan menggunakan ByteString.Memory properti dan ByteString.Span .
var byteString = UnsafeByteOperations.UnsafeWrap(new byte[] { 0, 1, 2 });
var data = byteString.Span;
for (var i = 0; i < data.Length; i++)
{
Console.WriteLine(data[i]);
}
Properti ini memungkinkan kode untuk membaca data langsung dari alokasi ByteString atau salinan tanpa alokasi.
Sebagian besar API .NET memiliki ReadOnlyMemory<byte> dan byte[] kelebihan beban, begitu juga ByteString.Memory cara yang disarankan untuk menggunakan data yang mendasar. Namun, ada keadaan di mana aplikasi mungkin perlu mendapatkan data sebagai array byte. Jika array byte diperlukan maka MemoryMarshal.TryGetArray metode dapat digunakan untuk mendapatkan array dari ByteString tanpa mengalokasikan salinan data baru.
var byteString = GetByteString();
ByteArrayContent content;
if (MemoryMarshal.TryGetArray(byteString.Memory, out var segment))
{
// Success. Use the ByteString's underlying array.
content = new ByteArrayContent(segment.Array, segment.Offset, segment.Count);
}
else
{
// TryGetArray didn't succeed. Fall back to creating a copy of the data with ToByteArray.
content = new ByteArrayContent(byteString.ToByteArray());
}
var httpRequest = new HttpRequestMessage();
httpRequest.Content = content;
Kode sebelumnya:
- Mencoba untuk mendapatkan array dari
ByteString.Memorydengan MemoryMarshal.TryGetArray. ArraySegment<byte>Menggunakan jika berhasil diambil. Segmen memiliki referensi ke array, offset, dan hitungan.- Jika tidak, kembali ke alokasi array baru dengan
ByteString.ToByteArray().
Layanan gRPC dan payload biner besar
gRPC dan Protobuf dapat mengirim dan menerima payload biner besar. Meskipun Protobuf biner lebih efisien daripada BERBASIS teks JSpada serialisasi payload biner, masih ada karakteristik performa penting yang perlu diingat saat bekerja dengan payload biner besar.
gRPC adalah kerangka kerja RPC berbasis pesan, yang berarti:
- Seluruh pesan dimuat ke dalam memori sebelum gRPC dapat mengirimkannya.
- Ketika pesan diterima, seluruh pesan dideserialisasi ke dalam memori.
Payload biner dialokasikan sebagai array byte. Misalnya, payload biner 10 MB mengalokasikan array byte 10 MB. Pesan dengan payload biner besar dapat mengalokasikan array byte pada timbunan objek besar. Alokasi besar memengaruhi performa dan skalabilitas server.
Saran untuk membuat aplikasi berkinerja tinggi dengan payload biner besar:
- Hindari payload biner besar dalam pesan gRPC. Array byte yang lebih besar dari 85.000 byte dianggap sebagai objek besar. Menjaga di bawah ukuran itu menghindari alokasi pada tumpukan objek besar.
- Pertimbangkan untuk memisahkan payload biner besar menggunakan streaming gRPC. Data biner dipotong dan dialirkan melalui beberapa pesan. Untuk informasi selengkapnya tentang cara melakukan streaming file, lihat contoh di repositori grpc-dotnet:
- Pertimbangkan untuk tidak menggunakan gRPC untuk data biner besar. Di ASP.NET Core, API Web dapat digunakan bersama layanan gRPC. Titik akhir HTTP dapat mengakses isi aliran permintaan dan respons secara langsung: