Этот журнал разработки посвящен путешествию grpcxx — попытке создать лучший API-интерфейс сервера gRPC с использованием современного C++ (C++20).
Хотя у меня была изрядная доля разочарований по поводу официальных API-интерфейсов gRPC C++, они работают быстро. Мои тесты показывают, что официальные примеры helloworld могут обслуживать до 160 тысяч запросов в секунду.
Давайте проверим пример grpcxx Hello World (devlog #5), чтобы получить некоторые показатели пропускной способности.
Это слишком медленно? 🐌
Я не ожидаю, что grpcxx будет быстрым, по крайней мере, пока. Он неэффективно использует память, и вся обработка выполняется в одном потоке, чтобы назвать пару узких мест в производительности. Но насколько он медленный по сравнению с официальной реализацией C++?
После удаления журналов отладки я использовал h2load, чтобы получить некоторые начальные тесты.
❯ h2load --clients=100 --requests=100000 --header='Content-Type: application/grpc' --data=examples/helloworld/testdata/hello.grpc-lpm.data http://localhost:7000/helloworld.v1.Greeter/Hello starting benchmark... spawning thread #0: 100 total client(s). 100000 total requests Application protocol: h2c progress: 10% done progress: 20% done progress: 30% done progress: 40% done progress: 50% done progress: 60% done progress: 70% done progress: 80% done progress: 90% done progress: 100% done finished in 1.79s, 55935.33 req/s, 3.26MB/s requests: 100000 total, 100000 started, 100000 done, 100000 succeeded, 0 failed, 0 errored, 0 timeout status codes: 100000 2xx, 0 3xx, 0 4xx, 0 5xx traffic: 5.82MB (6104700) total, 196.48KB (201200) headers (space savings 94.71%), 2.96MB (3100000) data min max mean sd +/- sd time for request: 30us 9.78ms 1.62ms 568us 85.30% time for connect: 2.24ms 3.25ms 2.86ms 268us 59.00% time to 1st byte: 9.45ms 9.80ms 9.61ms 81us 69.00% req/s : 559.54 604.29 583.43 11.53 64.00%
Пропускная способность составляет около 56 тыс. запросов в секунду. Это не слишком медленно, но все же примерно на 38% медленнее, чем официальные реализации C++ (на 65% медленнее, если сравнивать пропускную способность с несколькими одновременными потоками HTTP/2).
Обратите внимание, что использование нескольких одновременных потоков HTTP/2 не имеет большого значения, поскольку h2::conn{} ограничивает его только одним одновременным потоком.
❯ h2load --clients=100 --requests=100000 --max-concurrent-streams=10 --header='Content-Type: application/grpc' --data=examples/helloworld/testdata/hello.grpc-lpm.data http://localhost:7000/helloworld.v1.Greeter/Hello starting benchmark... spawning thread #0: 100 total client(s). 100000 total requests Application protocol: h2c progress: 10% done progress: 20% done progress: 30% done progress: 40% done progress: 50% done progress: 60% done progress: 70% done progress: 80% done progress: 90% done progress: 100% done finished in 1.72s, 57771.96 req/s, 3.37MB/s requests: 100000 total, 100000 started, 100000 done, 99100 succeeded, 900 failed, 900 errored, 0 timeout status codes: 99100 2xx, 0 3xx, 0 4xx, 0 5xx traffic: 5.78MB (6061500) total, 194.73KB (199400) headers (space savings 94.70%), 2.93MB (3072100) data min max mean sd +/- sd time for request: 31us 12.45ms 1.61ms 586us 91.18% time for connect: 2.03ms 3.13ms 2.67ms 245us 70.00% time to 1st byte: 5.63ms 9.42ms 6.92ms 1.29ms 72.00% req/s : 578.39 618.56 606.51 11.11 79.00%
Могу ли я улучшить пропускную способность? Надеюсь, да 🤞.
В идеале я должен профилировать код и понять, где теряется производительность. Но сейчас мне немного лень это делать, и я считаю, что использование нескольких потоков должно повысить пропускную способность. Поэтому я сделал быстрое изменение, чтобы использовать рабочие потоки для отправки запросов RPC.
Это не улучшило пропускную способность, а наоборот, использование потоков сделало ее медленнее.
❯ h2load --clients=100 --requests=100000 --header='Content-Type: application/grpc' --data=examples/helloworld/testdata/hello.grpc-lpm.data http://localhost:7000/helloworld.v1.Greeter/Hello starting benchmark... spawning thread #0: 100 total client(s). 100000 total requests Application protocol: h2c progress: 10% done progress: 20% done progress: 30% done progress: 40% done progress: 50% done progress: 60% done progress: 70% done progress: 80% done progress: 90% done progress: 100% done finished in 2.05s, 48881.33 req/s, 2.85MB/s requests: 100000 total, 100000 started, 100000 done, 100000 succeeded, 0 failed, 0 errored, 0 timeout status codes: 100000 2xx, 0 3xx, 0 4xx, 0 5xx traffic: 5.82MB (6104700) total, 196.48KB (201200) headers (space savings 94.71%), 2.96MB (3100000) data min max mean sd +/- sd time for request: 424us 26.14ms 1.88ms 647us 97.69% time for connect: 2.15ms 2.96ms 2.64ms 154us 74.00% time to 1st byte: 7.12ms 8.15ms 7.49ms 332us 61.00% req/s : 489.16 505.06 500.33 3.61 79.00% ❯ h2load --clients=100 --requests=100000 --max-concurrent-streams=10 --header='Content-Type: application/grpc' --data=examples/helloworld/testdata/hello.grpc-lpm.data http://localhost:7000/helloworld.v1.Greeter/Hello starting benchmark... spawning thread #0: 100 total client(s). 100000 total requests Application protocol: h2c progress: 10% done progress: 20% done progress: 30% done progress: 40% done progress: 50% done progress: 60% done progress: 70% done progress: 80% done progress: 90% done progress: 100% done finished in 2.03s, 48868.91 req/s, 2.85MB/s requests: 100000 total, 100000 started, 100000 done, 99100 succeeded, 900 failed, 900 errored, 0 timeout status codes: 99100 2xx, 0 3xx, 0 4xx, 0 5xx traffic: 5.78MB (6061500) total, 194.73KB (199400) headers (space savings 94.70%), 2.93MB (3072100) data min max mean sd +/- sd time for request: 404us 15.96ms 1.88ms 661us 98.46% time for connect: 2.23ms 3.34ms 2.89ms 257us 71.00% time to 1st byte: 5.48ms 10.51ms 7.40ms 1.83ms 73.00% req/s : 488.86 504.86 497.52 4.21 65.00%
🤔 Я не ожидал, что будет медленнее. Каждый клиентский запрос по-прежнему управляется основным потоком, поэтому, возможно, использование потоков для простого запуска кода приложения для запросов RPC на самом деле не увеличивает пропускную способность, но увеличивает накладные расходы.
Что, если h2:conn{} разрешает несколько одновременных потоков? Хотя при использовании одного потока для каждого соединения особой разницы нет, при использовании нескольких одновременных потоков наблюдается значительное улучшение.
❯ h2load --clients=100 --requests=100000 --max-concurrent-streams=10 --header='Content-Type: application/grpc' --data=examples/helloworld/testdata/hello.grpc-lpm.data http://localhost:7000/helloworld.v1.Greeter/Hello starting benchmark... spawning thread #0: 100 total client(s). 100000 total requests Application protocol: h2c progress: 10% done progress: 20% done progress: 30% done progress: 40% done progress: 50% done progress: 60% done progress: 70% done progress: 80% done progress: 90% done progress: 100% done finished in 706.69ms, 141221.55 req/s, 8.23MB/s requests: 100000 total, 100000 started, 100000 done, 99800 succeeded, 200 failed, 200 errored, 0 timeout status codes: 99800 2xx, 0 3xx, 0 4xx, 0 5xx traffic: 5.81MB (6095100) total, 196.09KB (200800) headers (space savings 94.71%), 2.95MB (3093800) data min max mean sd +/- sd time for request: 110us 141.26ms 5.24ms 5.54ms 99.41% time for connect: 1.87ms 3.12ms 2.67ms 291us 76.00% time to 1st byte: 4.90ms 144.26ms 51.05ms 42.84ms 60.00% req/s : 1414.72 1653.13 1513.65 85.05 56.00%
Теперь это 141 тыс. запросов в секунду, что быстрее, чем официальные API синхронизации и асинхронизации, но все же на 12 % медленнее, чем API обратного вызова.
На данный момент я вполне доволен пропускной способностью, но давайте попробуем еще кое-что. Для каждой попытки чтения из TCP libuv запрашивает выделение некоторого объема памяти. Я выделяю предложенный размер — 65 536. Это кажется немного расточительным, что, если я уменьшу его до 1024?
❯ h2load --clients=100 --requests=100000 --header='Content-Type: application/grpc' --data=examples/helloworld/testdata/hello.grpc-lpm.data http://localhost:7000/helloworld.v1.Greeter/Hello starting benchmark... spawning thread #0: 100 total client(s). 100000 total requests Application protocol: h2c progress: 10% done progress: 20% done progress: 30% done progress: 40% done progress: 50% done progress: 60% done progress: 70% done progress: 80% done progress: 90% done progress: 100% done finished in 1.44s, 69465.77 req/s, 4.04MB/s requests: 100000 total, 100000 started, 100000 done, 100000 succeeded, 0 failed, 0 errored, 0 timeout status codes: 100000 2xx, 0 3xx, 0 4xx, 0 5xx traffic: 5.82MB (6104700) total, 196.48KB (201200) headers (space savings 94.71%), 2.96MB (3100000) data min max mean sd +/- sd time for request: 65us 4.89ms 1.13ms 300us 74.28% time for connect: 2.22ms 2.91ms 2.58ms 150us 69.00% time to 1st byte: 7.14ms 7.42ms 7.28ms 66us 70.00% req/s : 694.68 697.93 695.77 0.61 69.00%
Это почти 70 тысяч запросов в секунду, что на 15 тысяч запросов в секунду больше, чем результаты первых тестов (до каких-либо попыток улучшить пропускную способность).
А использование нескольких одновременных потоков увеличивает пропускную способность до 168 тыс. запросов/с 🚀 (что даже больше, чем официальная пропускная способность Callback API, составляющая 160 тыс. запросов/с).
❯ h2load --clients=100 --requests=100000 --max-concurrent-streams=10 --header='Content-Type: application/grpc' --data=examples/helloworld/testdata/hello.grpc-lpm.data http://localhost:7000/helloworld.v1.Greeter/Hello starting benchmark... spawning thread #0: 100 total client(s). 100000 total requests Application protocol: h2c progress: 10% done progress: 20% done progress: 30% done progress: 40% done progress: 50% done progress: 60% done progress: 70% done progress: 80% done progress: 90% done progress: 100% done finished in 594.08ms, 168326.93 req/s, 9.80MB/s requests: 100000 total, 100000 started, 100000 done, 100000 succeeded, 0 failed, 0 errored, 0 timeout status codes: 100000 2xx, 0 3xx, 0 4xx, 0 5xx traffic: 5.82MB (6104700) total, 196.48KB (201200) headers (space savings 94.71%), 2.96MB (3100000) data min max mean sd +/- sd time for request: 111us 143.55ms 5.42ms 6.26ms 99.32% time for connect: 3.18ms 4.54ms 4.10ms 274us 70.00% time to 1st byte: 7.59ms 147.57ms 52.59ms 43.60ms 53.00% req/s : 1683.27 1965.56 1819.08 107.83 48.00%
Результаты тестов показывают, что grpcxx работает быстрее, чем официальная реализация C++ для простого примера Hello World, с потенциалом еще большего увеличения пропускной способности (например, за счет исключения лишних копий данных при отправке кадров ответа HTTP/2). .
grpcxx не только быстрее, но и обеспечивает большую гибкость кода приложения и генерирует на 95 % меньше кода (devlog #5) по сравнению с официальной реализацией.
Что дальше?
Еще предстоит проделать большую работу, чтобы подготовить рабочую версию grpcxx (например, в коде есть несколько FIXME, и он не защищен от исключений).
👨💻