Osvědčené postupy z hlediska výkonu s využitím gRPC
Poznámka:
Toto není nejnovější verze tohoto článku. Aktuální verzi najdete v tomto článku ve verzi .NET 9.
Upozorňující
Tato verze ASP.NET Core se už nepodporuje. Další informace najdete v zásadách podpory .NET a .NET Core. Aktuální verzi najdete v tomto článku ve verzi .NET 9.
Důležité
Tyto informace se týkají předběžného vydání produktu, který může být podstatně změněn před komerčním vydáním. Microsoft neposkytuje žádné záruky, výslovné ani předpokládané, týkající se zde uváděných informací.
Aktuální verzi najdete v tomto článku ve verzi .NET 9.
Autor: James Newton-King
GRPC je navržená pro vysoce výkonné služby. Tento dokument vysvětluje, jak dosáhnout nejlepšího možného výkonu z gRPC.
Opakované použití kanálů gRPC
Při volání gRPC by se měl znovu použít kanál gRPC. Opětovné použití kanálu umožňuje, aby volání byla multiplexována prostřednictvím existujícího připojení HTTP/2.
Pokud se pro každé volání gRPC vytvoří nový kanál, může se výrazně zvýšit doba potřebnou k dokončení. Každé volání bude vyžadovat více síťových odezv mezi klientem a serverem, aby se vytvořilo nové připojení HTTP/2:
- Otevření soketu
- Navazování připojení TCP
- Vyjednávání protokolu TLS
- Spuštění připojení HTTP/2
- Volání gRPC
Kanály jsou bezpečné pro sdílení a opakované použití mezi voláními gRPC:
- Klienti gRPC se vytvářejí s kanály. Klienti gRPC jsou jednoduché objekty a není nutné je ukládat do mezipaměti ani opakovaně používat.
- Z kanálu lze vytvořit více klientů gRPC, včetně různých typů klientů.
- Kanál a klienti vytvořené z kanálu můžou bezpečně používat více vláken.
- Klienti vytvořená z kanálu můžou provádět více souběžných volání.
Klientská továrna gRPC nabízí centralizovaný způsob konfigurace kanálů. Automaticky znovu používá podkladové kanály. Další informace najdete v tématu integrace klientské továrny gRPC v .NET.
Souběžnost připojení
Připojení HTTP/2 obvykle mají omezení počtu maximálních souběžných datových proudů (aktivních požadavků HTTP) na připojení najednou. Ve výchozím nastavení nastaví většina serverů tento limit na 100 souběžných datových proudů.
Kanál gRPC používá jedno připojení HTTP/2 a souběžná volání jsou v daném připojení multiplexovaná. Když počet aktivních volání dosáhne limitu datového proudu připojení, další volání se zařadí do fronty v klientovi. Volání ve frontě čekají na dokončení aktivních volání před jejich odesláním. Aplikace s vysokým zatížením nebo dlouhotrvajícími voláními gRPC streamování můžou kvůli tomuto limitu zobrazit problémy s výkonem způsobené frontami volání.
.NET 5 zavádí SocketsHttpHandler.EnableMultipleHttp2Connections vlastnost. Pokud je nastavená hodnota true
, při dosažení limitu souběžného datového proudu se v kanálu vytvoří další připojení HTTP/2.
GrpcChannel
Když se vytvoří jeho interníSocketsHttpHandler
, automaticky se nakonfiguruje pro vytvoření dalších připojení HTTP/2. Pokud aplikace nakonfiguruje vlastní obslužnou rutinu, zvažte nastaveníEnableMultipleHttp2Connections
:true
var channel = GrpcChannel.ForAddress("https://localhost", new GrpcChannelOptions
{
HttpHandler = new SocketsHttpHandler
{
EnableMultipleHttp2Connections = true,
// ...configure other handler settings
}
});
Aplikace rozhraní .NET Framework, které konfigurovaly volání gRPC, musí být nakonfigurovány tak, aby používaly WinHttpHandler
. Aplikace .NET Framework můžou vlastnost nastavit WinHttpHandler.EnableMultipleHttp2Connections tak, aby true
vytvářely další připojení.
Existuje několik alternativních řešení pro aplikace .NET Core 3.1:
- Vytvořte samostatné kanály gRPC pro oblasti aplikace s vysokým zatížením. Například
Logger
služba gRPC může mít vysoké zatížení. K vytvořeníLoggerClient
v aplikaci použijte samostatný kanál. - Použijte fond kanálů gRPC, například vytvořte seznam kanálů gRPC.
Random
slouží k výběru kanálu ze seznamu pokaždé, když je potřeba kanál gRPC. PoužitíRandom
náhodně distribuuje volání přes více připojení.
Důležité
Dalším způsobem, jak tento problém vyřešit, je zvýšení maximálního limitu souběžného datového proudu na serveru. V Kestrel tomto nastavení je nakonfigurovaný MaxStreamsPerConnection.
Zvýšení maximálního limitu počtu souběžných datových proudů se nedoporučuje. Příliš mnoho datových proudů na jednom připojení HTTP/2 přináší nové problémy s výkonem:
- Kolize vláken mezi datovými proudy, které se pokouší zapsat do připojení.
- Ztráta paketů připojení způsobí zablokování všech volání ve vrstvě TCP.
ServerGarbageCollection
v klientských aplikacích
Uvolňování paměti .NET má dva režimy: uvolňování paměti pracovní stanice (GC) a uvolňování paměti serveru. Každý z nich je vyladěný pro různé úlohy. ASP.NET aplikace Core ve výchozím nastavení používají serverový GC.
Vysoce souběžné aplikace obecně fungují lépe se serverovým GC. Pokud klientská aplikace gRPC odesílá a přijímá vysoký počet volání gRPC současně, může být při aktualizaci aplikace na používání serverového GC přínosem výkon.
Pokud chcete povolit GC serveru, nastavte <ServerGarbageCollection>
v souboru projektu aplikace:
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
Další informace o uvolňování paměti naleznete v tématu Pracovní stanice a server uvolňování paměti.
Poznámka:
ASP.NET aplikace Core ve výchozím nastavení používají serverový GC. Povolení <ServerGarbageCollection>
je užitečné jenom v klientských aplikacích, které nejsou serverové, například v klientské konzole gRPC.
Vyrovnávání zatížení
Některé nástroje pro vyrovnávání zatížení nefungují efektivně s gRPC. Nástroje pro vyrovnávání zatížení L4 (transportní) fungují na úrovni připojení tím, že distribuují připojení TCP mezi koncové body. Tento přístup funguje dobře pro načítání volání rozhraní API pro vyrovnávání zatížení provedených pomocí protokolu HTTP/1.1. Souběžná volání vytvořená pomocí protokolu HTTP/1.1 se odesílají na různá připojení, což umožňuje vyrovnávání zatížení napříč koncovými body.
Vzhledem k tomu, že nástroje pro vyrovnávání zatížení L4 fungují na úrovni připojení, nefungují s gRPC dobře. gRPC používá protokol HTTP/2, který multiplexuje více volání jednoho připojení TCP. Všechna volání gRPC přes toto připojení přejdou do jednoho koncového bodu.
Existují dvě možnosti efektivního vyrovnávání zatížení gRPC:
- Vyrovnávání zatížení na straně klienta
- Vyrovnávání zatížení proxy serveru L7 (aplikace)
Poznámka:
Mezi koncovými body je možné vyrovnát zatížení pouze volání gRPC. Jakmile se vytvoří volání gRPC streamování, všechny zprávy odeslané přes stream se přejdou do jednoho koncového bodu.
Vyrovnávání zatížení na straně klienta
Při vyrovnávání zatížení na straně klienta klient ví o koncových bodech. Pro každé volání gRPC vybere jiný koncový bod pro odeslání volání. Vyrovnávání zatížení na straně klienta je dobrou volbou, pokud je latence důležitá. Mezi klientem a službou není žádný proxy server, takže se volání odešle přímo do služby. Nevýhodou vyrovnávání zatížení na straně klienta je, že každý klient musí sledovat dostupné koncové body, které by měl používat.
Vyrovnávání zatížení klienta lookaside je technika, kdy je stav vyrovnávání zatížení uložený v centrálním umístění. Klienti se pravidelně dotazuje na centrální umístění, kde se mají informace použít při rozhodování o vyrovnávání zatížení.
Další informace najdete v tématu vyrovnávání zatížení na straně klienta gRPC.
Vyrovnávání zatížení proxy serveru
Proxy server L7 (aplikace) funguje na vyšší úrovni než proxy server L4 (transport). Proxy servery L7 rozumí http/2. Proxy server přijímá volání gRPC multiplexed na jednom připojení HTTP/2 a distribuuje je napříč několika back-endovými koncovými body. Použití proxy serveru je jednodušší než vyrovnávání zatížení na straně klienta, ale zvyšuje latenci volání gRPC.
K dispozici je mnoho proxy serverů L7. Tady jsou některé možnosti:
- Envoy – oblíbený open source proxy server.
- Linkerd – Síť služeb pro Kubernetes
- YARP: Další reverzní proxy server – open source proxy napsaný v .NET.
Komunikace mezi procesy
Volání gRPC mezi klientem a službou se obvykle odesílají přes sokety TCP. Protokol TCP je skvělý pro komunikaci přes síť, ale komunikace mezi procesy (IPC) je efektivnější, když je klient a služba na stejném počítači.
Zvažte použití přenosu, jako jsou sokety domény unixu nebo pojmenované kanály pro volání gRPC mezi procesy na stejném počítači. Další informace naleznete v tématu Komunikace mezi procesy s gRPC.
Udržovat naživu příkazy ping
Příkazy ping udržovat naživu je možné použít k udržování připojení HTTP/2 během období nečinnosti. Když bude aktivita obnovení aplikace připravená na připojení HTTP/2, umožní se rychle rychle provést počáteční volání gRPC, a to bez zpoždění způsobené opětovným publikováním připojení.
Příkazy ping naživu jsou nakonfigurované na 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
});
Předchozí kód konfiguruje kanál, který každých 60 sekund odesílá na server příkaz ping naživo během období nečinnosti. Příkaz ping zajistí, že server a všechny používané proxy servery připojení kvůli nečinnosti nezavřou.
Poznámka:
Udržovat naživu příkazy ping pomáhají udržet připojení naživu. Dlouho běžící volání gRPC připojení může server nebo proxy server ukončit kvůli nečinnosti.
Řízení toku
Řízení toku HTTP/2 je funkce, která brání zahlcení aplikací daty. Při použití řízení toku:
- Každé připojení HTTP/2 a požadavek mají k dispozici okno vyrovnávací paměti. Okno vyrovnávací paměti určuje, kolik dat může aplikace přijímat najednou.
- Řízení toku se aktivuje, pokud je vyplněno okno vyrovnávací paměti. Při aktivaci aplikace pro odesílání pozastaví odesílání dalších dat.
- Jakmile přijímající aplikace zpracuje data, bude k dispozici mezera v okně vyrovnávací paměti. Odesílající aplikace obnoví odesílání dat.
Řízení toku může mít negativní dopad na výkon při příjmu velkých zpráv. Pokud je okno vyrovnávací paměti menší než datové části příchozích zpráv nebo latence mezi klientem a serverem, můžou se data odesílat v nárůstech spuštění/zastavení.
Problémy s výkonem řízení toku je možné opravit zvýšením velikosti okna vyrovnávací paměti. V Kestreltomto nastavení je nakonfigurované InitialConnectionWindowSize spuštění aplikace a InitialStreamWindowSize při spuštění aplikace:
builder.WebHost.ConfigureKestrel(options =>
{
var http2 = options.Limits.Http2;
http2.InitialConnectionWindowSize = 1024 * 1024 * 2; // 2 MB
http2.InitialStreamWindowSize = 1024 * 1024; // 1 MB
});
Doporučení:
- Pokud služba gRPC často přijímá zprávy větší než 768 kB, Kestrelvýchozí velikost okna datového proudu, zvažte zvýšení velikosti připojení a okna datového proudu.
- Velikost okna připojení by měla být vždy rovna nebo větší než velikost okna datového proudu. Stream je součástí připojení a odesílatel je omezen oběma.
Další informace o tom, jak řízení toku funguje, najdete v tématu HTTP/2 Flow Control (blogový příspěvek).
Důležité
Zvětšení Kestrelvelikosti okna umožňuje Kestrel ukládat více dat jménem aplikace do vyrovnávací paměti, což může zvýšit využití paměti. Vyhněte se konfiguraci zbytečně velké velikosti okna.
Řádné dokončení streamovacích volání
Zkuste bezúhotně dokončit streamovaná volání. Řádné dokončování volání zabraňuje zbytečným chybám a umožňuje serverům opakovaně používat interní datové struktury mezi požadavky.
Volání je dokončeno elegantně, když klient a server dokončili odesílání zpráv a partnerský vztah přečetl všechny zprávy.
Stream požadavků klienta:
- Klient dokončil zápis zpráv do datového proudu požadavku a dokončí stream s
call.RequestStream.CompleteAsync()
. - Server přečetl všechny zprávy ze streamu požadavku. V závislosti na tom, jak čtete zprávy,
requestStream.MoveNext()
vrátítefalse
neborequestStream.ReadAllAsync()
dokončíte.
Stream odpovědí serveru:
- Server dokončil zápis zpráv do streamu odpovědí a metoda serveru se ukončila.
- Klient přečetl všechny zprávy ze streamu odpovědí. V závislosti na tom, jak čtete zprávy,
call.ResponseStream.MoveNext()
vrátítefalse
nebocall.ResponseStream.ReadAllAsync()
dokončíte.
Příklad elegantního dokončení obousměrného streamovacího volání najdete v tématu Obousměrné volání streamování.
Volání streamování serveru nemají stream požadavků. To znamená, že jediným způsobem, jak klient může komunikovat se serverem, že by se datový proud měl zastavit, je zrušením. Pokud režie zrušených volání ovlivní aplikaci, zvažte změnu volání streamování serveru na obousměrné volání streamování. V obousměrné streamovací volání klienta, který dokončí datový proud požadavku, může být signálem pro server, který hovor ukončí.
Vyřaďte streamovaná volání
Jakmile už je nepotřebujete, vždy vyřaďte volání streamování. Typ vrácený při spouštění volání streamování implementuje IDisposable
. Uvolnění volání, jakmile už není potřeba, zajistí, že se zastaví a všechny prostředky se vyčistí.
V následujícím příkladu deklarace using ve AccumulateCount()
volání zajišťuje, že je vždy uvolněna, pokud dojde k neočekávané chybě.
var client = new Counter.CounterClient(channel);
using var call = client.AccumulateCount();
for (var i = 0; i < 3; i++)
{
await call.RequestStream.WriteAsync(new CounterRequest { Count = 1 });
}
await call.RequestStream.CompleteAsync();
var response = await call;
Console.WriteLine($"Count: {response.Count}");
// Count: 3
V ideálním případě by se volání streamování měla dokončit elegantně. Zrušení volání zajistí, že se požadavek HTTP mezi klientem a serverem zruší, pokud dojde k neočekávané chybě. Volání streamování, která jsou náhodně spuštěná, nevracejí paměť a prostředky na klientovi, ale jsou spuštěná i na serveru. Mnoho nevracených volání streamování může mít vliv na stabilitu aplikace.
Zrušení streamovaného volání, které už bylo úspěšně dokončeno, nemá žádný negativní dopad.
Nahrazení unárních volání streamováním
Obousměrné streamování gRPC je možné použít k nahrazení neárních volání gRPC ve scénářích s vysokým výkonem. Po spuštění obousměrného datového proudu je streamování zpráv zpět a zpět rychlejší než odesílání zpráv s několika voláními gRPC unárních dat. Streamované zprávy se odesílají jako data pro stávající požadavek HTTP/2 a eliminují režii při vytváření nového požadavku HTTP/2 pro každé unární volání.
Příklad služby:
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);
}
}
Příklad klienta:
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}");
}
Nahrazení unárních volání obousměrným streamováním z důvodů výkonu je pokročilá technika a v mnoha situacích není vhodná.
Použití volání streamování je dobrou volbou v následujících případech:
- Vyžaduje se vysoká propustnost nebo nízká latence.
- GRPC a HTTP/2 jsou identifikovány jako kritické body výkonu.
- Pracovník v klientovi odesílá nebo přijímá pravidelné zprávy se službou gRPC.
Mějte na paměti další složitost a omezení používání volání streamování místo unárních volání:
- Stream může být přerušen službou nebo chybou připojení. Logika se vyžaduje k restartování streamu, pokud dojde k chybě.
-
RequestStream.WriteAsync
není bezpečné pro více vláken. Do datového proudu lze najednou zapsat jenom jednu zprávu. Odesílání zpráv z více vláken přes jeden datový proud vyžaduje frontu producenta nebo příjemce, jako je Channel<T> marshall zprávy. - Metoda streamování gRPC je omezena na příjem jednoho typu zprávy a odesílání jednoho typu zprávy. Například
rpc StreamingCall(stream RequestMessage) returns (stream ResponseMessage)
přijímáRequestMessage
a odesíláResponseMessage
. Protobuf podporuje neznámé nebo podmíněné zprávy používajícíAny
aoneof
může toto omezení obejít.
Binární datové části
Binární datové části jsou podporovány v Protobuf s typem bytes
skalární hodnoty. Vygenerovaná vlastnost v jazyce C# se používá ByteString
jako typ vlastnosti.
syntax = "proto3";
message PayloadResponse {
bytes data = 1;
}
Protobuf je binární formát, který efektivně serializuje velké binární datové části s minimální režií. Textové formáty, jako je JSON, vyžadují kódování bajtů do base64 a k velikosti zprávy přidají 33 %.
Při práci s velkými ByteString
datovými částmi existuje několik osvědčených postupů, které zabrání zbytečným kopiím a přidělením, které jsou popsány níže.
Odesílání binárních datových částí
ByteString
instance se obvykle vytvářejí pomocí ByteString.CopyFrom(byte[] data)
. Tato metoda přidělí novou ByteString
a novou byte[]
. Data se zkopírují do nového pole bajtů.
Dalším přidělením a kopiím se můžete vyhnout použitím UnsafeByteOperations.UnsafeWrap(ReadOnlyMemory<byte> bytes)
k vytváření ByteString
instancí.
var data = await File.ReadAllBytesAsync(path);
var payload = new PayloadResponse();
payload.Data = UnsafeByteOperations.UnsafeWrap(data);
Bajty se nekopírují, UnsafeByteOperations.UnsafeWrap
takže je nesmíte upravovat, pokud ByteString
se používá.
UnsafeByteOperations.UnsafeWrap
vyžaduje Google.Protobuf verze 3.15.0 nebo novější.
Čtení binárních datových částí
Data je možné efektivně číst z ByteString
instancí pomocí ByteString.Memory
a ByteString.Span
vlastností.
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]);
}
Tyto vlastnosti umožňují kódu číst data přímo z ByteString
přidělení nebo kopií.
Většina rozhraní API .NET má ReadOnlyMemory<byte>
a byte[]
přetíží, proto ByteString.Memory
se doporučuje používat podkladová data. Existují však okolnosti, kdy může aplikace potřebovat získat data jako pole bajtů. Pokud je požadováno pole bajtů, MemoryMarshal.TryGetArray lze metodu použít k získání pole z ByteString
pole bez přidělení nové kopie dat.
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;
Předchozí kód:
- Pokusí se získat pole pomocí
ByteString.Memory
MemoryMarshal.TryGetArrayfunkce . -
ArraySegment<byte>
Použije, pokud byl úspěšně načten. Segment má odkaz na matici, posun a počet. - V opačném případě se vrátí zpět k přidělení nového pole s
ByteString.ToByteArray()
.
Služby gRPC a velké binární datové části
gRPC a Protobuf můžou odesílat a přijímat velké binární datové části. I když je binární Protobuf efektivnější než text json při serializaci binárních datových částí, při práci s velkými binárními datovými částmi je stále důležité charakteristiky výkonu.
gRPC je architektura RPC založená na zprávách, což znamená:
- Celá zpráva se načte do paměti, než ji gRPC může odeslat.
- Při přijetí zprávy se celá zpráva deserializuje do paměti.
Binární datové části se přidělují jako bajtové pole. Například binární datová část o velikosti 10 MB přiděluje pole 10 MB bajtů. Zprávy s velkými binárními datovými částmi mohou přidělovat pole bajtů v haldě velkého objektu. Velké alokace mají vliv na výkon a škálovatelnost serveru.
Rady pro vytváření vysoce výkonných aplikací s velkými binárními datovými částmi:
- Vyhněte se velkým binárním datovým částem ve zprávách gRPC. Bajtové pole větší než 85 000 bajtů je považováno za velký objekt. Pokud je tato velikost nižší, vyhněte se přidělování haldy velkého objektu.
-
Zvažte rozdělení velkých binárních datových částí pomocí streamování gRPC. Binární data se blokují a streamují přes více zpráv. Další informace o tom, jak streamovat soubory, najdete v příkladech v úložišti grpc-dotnet:
- Ke stažení streamovacího souboru gRPC.
- Nahrání streamovaného souboru gRPC
- Zvažte, že pro velká binární data nepoužíváte gRPC. V ASP.NET Core je možné webová rozhraní API používat společně se službami gRPC. Koncový bod HTTP má přímý přístup k textu streamu požadavku a odpovědi: