UPD. Тестирование REST API на Golang. 120 000 [#/sec] не предел?

    На глаза попалась не особо позитивное сравнение Java vs GO. Тестирование большим числом пользователей.


    Решил проверить, действительно ли так все не радужно с Go.
    Забегая вперед скажу, что при кэшировании в памяти и формировании JSON "на лету" удалось получить до 120 000 [#/sec] на 8 физических ядра.


    Базовый сценарий GET запроса:


    • Если данные найдены в in memory кэше и они валидные, то формируем JSON из структуры
    • Если данных в кэше нет, то ищем их в Bolt DB, если находим, то считываем готовый JSON
    • Если данных нет в Bolt DB, то запрашиваем их из БД, сохраняем их в in memory кэше
    • Данные в in memory кэше накапливаются в буферном канале, после накопления около 10000 элементов они сбрасываются единым save в Bolt DB
    • Если данные в БД менялись (update / insert) то через pg_notify передается уведомление и данные в кэше помечаются как невалидные, при следующем обращении они считываются заново из БД

    Под катом результаты тестирования, и код тестового стенда GitHub


    Update 06.05.2020


    Повилась возможность протестировать в облаке Oracle.
    get_db_memory_json1


    • стенд собран на 3 серверах — 8 Core Intel (16 virtual core), 120 Memory (GB), Oracle Linux 7.7
    • локальные NVMe диски — 6.4 TB NVMe SSD Storage min 250k IOPS (4k block)
    • локальная сеть между серверами — 8.2 Network Bandwidth (Gbps)
    • в режиме прямого чтения из PostgreSQL — до 16 000 [get/sec], сoncurrency 1024, медиана 60 [ms]. Кажды Get запрашивает данные из двух таблиц общим размером 360 000 000 строк. Размер JSON 1800 байт.
    • в режиме кэширования — до 100 000 — 120 000 [get/sec], сoncurrency 1024, медиана 2 [ms].
    • на вставку в PostgreSQL — около 10 000 [insert/sec].
    • при масштабировании с 2 до 4 и 8 Core, рост производительности практически линейный.

    Краткие выводы


    • использование кэширования JSON в Bolt DB дает прибавку в скорости примерно в 2 раза как на HDD, так и на SSD дисках
    • перенос БД PostgreSQL с HDD на SDD дает прибавку минимум в 100 раз
    • перенос Bolt DB на SDD с HDD так же дает прибавку минимум в 100 раз
    • кэширование структуры в памяти и формирование JSON "на лету" дает прибавку до 10 раз по сравнению с БД PostgreSQL на SDD
    • стабильность отличная — ни каких Failed requests вплоть до Concurrency 16 384

    Тестирования на локальной машине


    Результаты тестирования, доступны по ссылке
    Кому интересен код тестового стенда — добро пожаловать на GitHub. Кода много около 4000 строк, но ни чего супер интересного использовано не было.


    Единственно, разве что кастомный JSON encoder/decode — это ускорило формирование JSON более чем в 2 раза.


    HTTP Get handler со стандартным пакетом JSON


    BenchmarkHandler_DeptGetHandler-2         1000000          41277 ns/op     4845.26 MB/s        10941 B/op          259 allocs/op

    HTTP Get handler с пакетом francoispqt/gojay


    BenchmarkHandler_DeptGetHandler-2         1000000          13300 ns/op     15036.97 MB/s         3265 B/op           11 allocs/op

    Параметры стенда для тестирования:


    • БД PostgreSQL 11 под Windows 10, размер БД — до 30 ГБайт.
    • Два таблицы — мастер до 10 000 000 строк, подчиненная таблица 100 000 000 строк.
    • Размер JSON сообщения 1500 байт, формируется из 1 мастер строки и 10 подчиненных строк.
    • компьютер — core i7-3770 — 4 core (8 thread), 16 Гбайт оперативка, HDD (WD 2.0TB WD2000fyyz), SSD (Intel 530 Series).
    • Тестирование велось через ApacheBench, concurrency level от 1 до 4096, 1 000 000 запросов случайными образом.
    • Что бы минимизировать кэширование Windows, перед каждым тестом машина перегружалась.
    • Ни каких RAID не использовалось. Интересно было замерить максимальные показатели на одном диске.

    В ходе тестирования проверялись следующие граничные случаи:



    Падение показателей при росте Concurrency Level связано с тем, что ApacheBench запускался на том же компьютере и активно "отъедал процессорное время".


    GET с прямым доступом к PostgreSQL — HDD (при каждом запросе идет чтение БД)


    Concurrency Level Requests per second Time per req. 99% percentile
    1 36.84 [#/sec] (mean) 27.145 [ms] (mean) 116 [ms]
    2 38.27 [#/sec] (mean) 52.262 [ms] (mean) 165 [ms]
    4 41.34 [#/sec] (mean) 96.755 [ms] (mean) 268 [ms]
    8 45.02 [#/sec] (mean) 177.687 [ms] (mean) 420 [ms]
    16 47.56 [#/sec] (mean) 336.428 [ms] (mean) 813 [ms]
    128 49.19 [#/sec] (mean) 2602.228 [ms] (mean) 13055 [ms]
    512 50.5 [#/sec] (mean) 10122.343 [ms] (mean) 19394 [ms]
    2048 51.91 [#/sec] (mean) 39453.681 [ms] (mean) 57018 [ms]

    GET с прямым доступом к PostgreSQL — SSD (при каждом запросе идет чтение БД)


    Concurrency Level Requests per second Time per req. 99% percentile
    1 713.82 [#/sec] (mean) 1.401 [ms] (mean) 2 [ms]
    2 1914.61 [#/sec] (mean) 1.045 [ms] (mean) 2 [ms]
    4 3326.52 [#/sec] (mean) 1.202 [ms] (mean) 2 [ms]
    8 4599.95 [#/sec] (mean) 1.739 [ms] (mean) 4 [ms]
    16 4599.80 [#/sec] (mean) 3.478 [ms] (mean) 9 [ms]
    128 5243.76 [#/sec] (mean) 24.410 [ms] (mean) 102 [ms]
    512 5354.35 [#/sec] (mean) 95.623 [ms] (mean) 506 [ms]
    2048 5285.83 [#/sec] (mean) 387.451 [ms] (mean) 2871 [ms]

    GET с кэширование в BOLT DB — HDD диск (JSON предварительно сохранен в BOLT DB)


    Concurrency Level Requests per second Time per req. 99% percentile
    1 81.55 [#/sec] (mean) 12.262 [ms] (mean) 38 [ms]
    2 67.04 [#/sec] (mean) 29.832 [ms] (mean) 97 [ms]
    4 72.51 [#/sec] (mean) 55.167 [ms] (mean) 183 [ms]
    8 92.48 [#/sec] (mean) 86.502 [ms] (mean) 291 [ms]
    16 89.42 [#/sec] (mean) 178.923 [ms] (mean) 550 [ms]
    128 86.76 [#/sec] (mean) 1475.378 [ms] (mean) 11280 [ms]
    512 92.38 [#/sec] (mean) 2771.145 [ms] (mean) 10238 [ms]
    2048 100.56 [#/sec] (mean) 20366.847 [ms] (mean) 18632 [ms]
    4096 106.98 [#/sec] (mean) 38289.063 [ms] (mean) 37074[ms]

    GET с кэширование в BOLT DB — SSD диск (JSON предварительно сохранен в BOLT DB)


    Concurrency Level Requests per second Time per req. 99% percentile
    1 3411.07 [#/sec] (mean) 0.293 [ms] (mean) 1 [ms]
    2 7468.21 [#/sec] (mean) 0.268 [ms] (mean) 1 [ms]
    4 9501.15 [#/sec] (mean) 0.421 [ms] (mean) 2 [ms]
    8 10481.68 [#/sec] (mean) 0.763 [ms] (mean) 3 [ms]
    16 10052.14 [#/sec] (mean) 1.592 [ms] (mean) 5 [ms]
    128 10754.02 [#/sec] (mean) 11.903 [ms] (mean) 20 [ms]
    512 11030.61 [#/sec] (mean) 46.416 [ms] (mean) 66 [ms]
    2048 10634.72 [#/sec] (mean) 192.577 [ms] (mean) 362 [ms]
    4096 10659.04 [#/sec] (mean) 384.275 [ms] (mean) 720 [ms]

    GET с кэшированием структуры в памяти


    Concurrency Level Requests per second Time per req. 99% percentile
    1 9178.22 [#/sec] (mean) 0.109 [ms] (mean) 1 [ms]
    2 22580.40 [#/sec] (mean) 0.089 [ms] (mean) 1 [ms]
    4 36163.33 [#/sec] (mean) 0.111 [ms] (mean) 1 [ms]
    8 56109.17 [#/sec] (mean) 0.143 [ms] (mean) 1 [ms]
    16 43942.75 [#/sec] (mean) 0.364 [ms] (mean) 2 [ms]
    128 55005.53 [#/sec] (mean) 2.327 [ms] (mean) 6 [ms]
    512 35338.01 [#/sec] (mean) 14.489 [ms] (mean) 25 [ms]
    2048 38090.35 [#/sec] (mean) 53.767 [ms] (mean) 228 [ms]
    4096 30196.47 [#/sec] (mean) 135.645 [ms] (mean) 609 [ms]

    Тестирование в облаке Oracle


    Тестовый стенд


    Тестовый стенд был собран в облаке Oracle на 3-х идентичных серверах:


    • конфигурации VM.DenseIO2.8 — 8 Core Intel (16 virtual core), 120 Memory (GB), Oracle Linux 7.7
    • использовались локальные NVMe диски — 6.4 TB NVMe SSD Storage (1 drives) min 250k IOPS (4k block)
    • локальная сеть между серверами — 8.2 Network Bandwidth (Gbps)
    • БД — PostgreSQL 12. Оптимизация настроек БД не производилась.
    • тестирование велось через ApacheBench:
      • concurrency level от 4 до 16384
      • от 10 000 до 1 000 000 запросов случайными образом.
      • в фоне запускалось от 2 до 8 экземпляров ApacheBench (один процесс ab не может нагрузить более 1 виртуального процессора)

    тестовый стенд


    Результаты тестирования GET с разными видами кэширования


    • Размер JSON — 1800 байт. Один мастер объект Dept и 10 вложенных объектов Emp.
    • Размер таблицы Dept — 33 000 000, таблицы Emp — 330 000 000 строк.

    Тестировались следующие сценарии:


    • GET без кэширования с прямым доступом к PostgreSQL
    • GET с кэшированием JSON из BBolt (размер 74 Гбайт)
    • GET с кэшированием в памяти (стандартный map)

    Исходные результаты тестирования доступны в репозитории.
    Таблица с агрегированными данными доступна по ссылке


    График зависимости Requests per second от Concurrency.


    get_db_memory_json1


    График зависимости Time per req. [ms] от Concurrency (логарифмическая шкала).


    get_db_memory_json1


    График зависимости 99% percentile [ms] от Concurrency.


    get_db_memory_json1


    График зависимости 99% percentile [ms] от Concurrency (логарифмическая шкала).


    get_db_memory_json1


    Так выглядит загрузка backend сервера при 100 000 [#/sec] и кэшировании данных в BBolt (сoncurrency 128).
    Из 1600% процессорного времени, программа использует 1464%.
    Средняя задержка ввода / вывода на NVMe диск 0,02 [ms] (500 000 IOPS).


    get_db_memory_json1


    Профилирование


    По данным Benchmark, HTTP Get handler отрабатывает за 13300 ns


    BenchmarkHandler_DeptGetHandler-2         1000000          13300 ns/op     15036.97 MB/s         3265 B/op           11 allocs/op

    По данным ApacheBench, лучшее mean выполнения запроса — 0.088 ms.
    Стало интересно где разница 0.088 — 0.013 = 0.055 ms.
    Включил профилирование — результаты


    На верхнем уровне:


    • net/http.(*conn).serve — (61.85%)
    • net/http.(*connReader).backgroundRead — (7.04%)
    • runtime.gcBgMarkWorker — (18.30%)

    Чем занимался HTTP server из (61.85%):


    • Считывание входящих запросов — net/http.(*conn).readRequest — (9.29%). Входящие запросы были без тела.
    • Запись исходящих ответов — net/http.(*response).finishRequest — (21.74%). Запись тела ответа.
    • Собственно обработчик запроса — net/http.(*ServeMux).ServeHTTP — (23.33%).

    img


    Из суммарных затрат на readRequest, finishRequest и backgroundRead (39.66%), на обработку системных вызовов IO Windows — internalpoll.(ioSrv).ExecIO — пришлось (24.78%).


    Посмотрим чем занимался наш основной обработчик net/http.(*ServeMux).ServeHTTP:


    • Парсинг URL и обработка параметров — github.com/gorilla/mux.(*Route).Match — (2.29%)
    • Формирование JSON из структуры в памяти — github.com/francoispqt/gojay.marshal — (9.92%)
    • Работка с кэшем в памяти — (4.53%)

    img

    Средняя зарплата в IT

    120 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 3 391 анкеты, за 1-ое пол. 2021 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 9

      +2
      Выбор ос по мне странен.
        0
        поддержу
        кроме того, для разработки и доведения до ума можно было и локально гонять, но с финальными тестами неплохо было бы заморочиться и прогнать в облаке, разделив бд, сервис и тест по трем инстансам.
          0
          У потенциального заказчика используется платформа Windows.
          +1

          нужно больше тестов, хороших и разных, чтобы можно было строить всяческие аппроксимации

            +2

            В тесте очень много проблем, которые не составляет большого труда починить, и вот основные:


            1. Описание стенда недостаточно: слова «Core i7 4/8 cores/threads» покрывают кучу процессоров (включая ноутбучные), выпускавшихся в течение последних 10 лет. То же про диск HDD/SSD: это один гибридный накопитель или несколько разных? Если разные, то на каком располагалась БД? Значительно лучше идентифицировать компоненты стенда полностью, чтобы читатель не гадал (например, диски бывают 5400 RPM и 15000 RPM, и разница для БД значительная, хоть они и оба HDD);


            2. Неясно, каков размер БД в PostgreSQL. Лично видел людей, которые тестировали влияние жестких дисков на модельной БД, целиком влезающей в память дважды, и не чистили память между тестами. Модельная БД «весила» 2 гигабайта, а проектная боевая — 200 гигабайт. В таких условиях в результаты теста можно даже не смотреть;


            3. Расположение нагружающего элемента на той же железке ведёт к разнообразным спецэффектам, которые, вообще говоря, не могут быть строго объяснены: в том же случае с ab неясно, это нагружатель не смог нагрузить и вынудил приложение работать с «длинными» подключениями, или же приложение не смогло качественно отвечать, потому что нагружатель занял CPU (кстати, чем?). Поэтому нагружатель нужно выносить на другую железку, и ещё неплохо бы использовать несколько разных время от времени, поскольку у нагружателей тоже бывают проблемы.



            В целом бирки клеить нужно. Но аккуратно.

              0
              Добавил в статью описание характеристики процессора, HDD и SSD, размер БД, а так же результаты тестов на HDD.
              Ab не стал выносить на отдельную машину — чтобы не учитывать сетевые задержки.
                0
                Нуу, сколько там задержек, половина миллисекунды, если второй компьютер в том же здании и в не шибко занятой сети? Плюс 55 тысяч пакетов по полтора килобайта, это 3/4 гигабитного линка по пропускной способности, ±. И в итоге всё равно непонятно, сколько стенд выдаст сам, а не с висящим на нём же нагружатором (держать базу и кэш на той же железке, впрочем, законно: не у всех эти 55 тысяч запросов вообще случатся когда-нибудь, чтобы их разделять). Тестировать в облаках отличный план, потому что дёшево (поднял стенд, прогнал тест, удалил стенд, заплатил 50 центов), и для повторения Ваших результатов кому-то ещё совершенно не нужно искать в точности такой же процессор.
                  0
                  Добавил профилирование
              –1

              nginx на одном ядре без особой настройки с включённым кэшированием способен обрабатывать 50.000+ запросов в секунду. В чём смысл городить огород на бэкенде?

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое