Этот журнал разработки посвящен путешествию 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, и он не защищен от исключений).

👨‍💻