Този devlog е за пътуването на grpcxx — опит за изграждане на по-добър gRPC сървър API с помощта на модерен C++ (C++20).

Въпреки че имах своя справедлив дял от разочарованието от официалните gRPC C++ APIs, той е бърз. Моите показатели показват, че „официалните примери за helloworld“ могат да обслужват до 160 000 заявки в секунда.

Нека сравним с примера 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%

Пропускателната способност е около 56k req/s. Не е твърде бавен, но все пак е около 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%

Сега това е 141k req/s, което е по-бързо от официалните API за синхронизиране и Async, но все още е с 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%

Това е почти 70k req/s, което е с 15k req/s повече от първите резултати от бенчмарка (преди всякакви опити за подобряване на пропускателната способност).

А използването на множество едновременни потоци увеличава пропускателната способност до 168k req/s 🚀 (което е дори повече от официалната пропускателна способност на API за обратно извикване от 160k req/s).

❯ 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 в кода и не е безопасно за изключения).

👨‍💻