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