В прошлой статье я разбирался с тем как работает GIL, а сегодня меня захватила идея узнать насколько дорого обходится этот GIL для кода, который исполняется у нас на бэкенде. Для этого я решил пропатчить MRI и добавить пару переменных, в которых буду засекать сколько времени поток реально выполнял код, а сколько ничего не делал и ждал пока ему удастся завладеть локом.

Для теста была использована кастомная сборка Ruby 2.7.4 (флаги компиляции не изменены, только добавлен новый код), Rails 6.1.4.1 с puma 5.5 в качестве апп сервера с 1 инстансом и 5ю тредами (по умолчанию). Все это дело запускалось на моем ноутбуке HP с 4 ядрами и 8GB RAM, а также на google cloud в регионе us-central2 с инстансом e2-medium (2 vCPU, 4 GB memory). Результаты между локальной машиной и облаком в процентном соотношении примерно равны. Для измерения только рабочих воркеров пумы я еще и ее запатчил, чтобы достучаться до ее тред пула.

В качестве приложения я создал 2 эндпоинта: один для статистики, а другой рабочий

class TestingController < ApplicationController
  def stats
    threads = $puma_thread_poll.workers.map do
      { 
        running: _1.running_time,
        wait_gvl: _1.wait_gvl_time,
        gvl_vs_running: (_1.wait_gvl_time / _1.running_time * 100).round(2),
      }.tap(&:clear_timings)
    end

    avg_gvl_vs_running = threads.sum { _1[:gvl_vs_running] } / threads.size

    render json: { avg_gvl_vs_running: avg_gvl_vs_running, threads: threads }
  end

  def do_work
    # имитирую бурную деятельность с походами в базу и сериализацией
    # жсона
    50.times { User.last }
    10.times { make_user }

    render json: User.last(25).map(&:as_json)
  end

  def make_user
    User.create!(email: "#{SecureRandom.hex}@mail.ru", username: SecureRandom.hex, country_code: "RU", logins_count: rand(10..150))
  end
end

Методика измерения внутри руби такая: любой поток, перед тем как сможет выполняться, должен взять GIL, а после того как исчерпает свое время, он должен его вернуть. Таким образом я буду замерять 2 промежутка: непосредственно время захвата лока, а так же время между захватом и освобождением.

Код не особо интересен, но вдруг кому-то надо...

Да, да, код картинками, можете начинать хейтить, но так проще

Когда все было приготовлено я 10 раз дернул рабочий эндпоинт руками, чтобы пума подняла все свои воркеры. Время ответа вполне себе приемлимое (выбрал средний результат)

Completed 200 OK in 91ms (Views: 1.3ms | ActiveRecord: 18.6ms | Allocations: 36166)

Затем пошел глянул стату

{
   "avg_gvl_vs_running":"16.09%",
   "threads":[
      {
         "running":423187434.0,
         "wait_gvl":14709976.0,
         "gvl_vs_running":"3.48%"
      },
      {
         "running":494142788.0,
         "wait_gvl":1916580.0,
         "gvl_vs_running":"0.39%"
      },
      {
         "running":1263953814.0,
         "wait_gvl":35301242.0,
         "gvl_vs_running":"2.79%"
      },
      {
         "running":19586474.0,
         "wait_gvl":10783870.0,
         "gvl_vs_running":"55.06%"
      },
      {
         "running":17937350.0,
         "wait_gvl":3359780.0,
         "gvl_vs_running":"18.73%"
      }
   ]
}

Ух ты! Всего лишь 16 процентов времени, что работал воркер, было потрачено впустую! Как вам такое, хейтеры руби? Кажется, гил не такая уж и большая проблема. Для верности нужно провести небольшое нагрузочное тестирование.

ab -n 500 -c 4 http://192.168.100.9:3000/testing/do_work
...
Concurrency Level:      4
Time taken for tests:   31.135 seconds
Complete requests:      500
Failed requests:        0
   (Connect: 0, Receive: 0, Length: 0, Exceptions: 0)
Total transferred:      1645986 bytes
HTML transferred:       1142366 bytes
Requests per second:    16.06 [#/sec] (mean)
Time per request:       249.082 [ms] (mean)
Time per request:       62.270 [ms] (mean, across all concurrent requests)
Transfer rate:          51.63 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        4   10   5.6      9      45
Processing:   106  238  87.7    209     633
Waiting:      105  236  87.9    207     632
Total:        115  248  88.4    219     644

Percentage of the requests served within a certain time (ms)
  50%    219
  66%    233
  75%    254
  80%    290
  90%    362
  95%    491
  98%    519
  99%    546
 100%    644 (longest request)

Ага, тут уже среднее время ответа более 200мс, что довольно таки много, интересно что же там в статистике...

{
   "avg_gvl_vs_running":"94.936%",
   "threads":[
      {
         "running":810864938.0,
         "wait_gvl":714257002.0,
         "gvl_vs_running":"88.09%"
      },
      {
         "running":1022361816.0,
         "wait_gvl":881835262.0,
         "gvl_vs_running":"86.25%"
      },
      {
         "running":687098840.0,
         "wait_gvl":728491848.0,
         "gvl_vs_running":"106.02%"
      },
      {
         "running":706853620.0,
         "wait_gvl":696489552.0,
         "gvl_vs_running":"98.53%"
      },
      {
         "running":601701804.0,
         "wait_gvl":576366940.0,
         "gvl_vs_running":"95.79%"
      }
   ]
}

Вот это поворот! 94%!!! А это еще не максимум, там и болше 100 было. Таким образом при нагрузке чуть более чем 18 рпс из-за гила ответ от сервера увеличивается чуть более чем в 2 раза!!! и если убрать гил, то для обработки того же количества клиентов нужно будет в 2 раза меньше воркеров. Что ж, выводы делать не буду, просто оставлю это здесь.

PS: Я не гуру, возможно моя методика подсчета не верна, но мне она кажется логичной. С исправлениями прошу в комментарии.

UPD: Выражаю благодарность Aquahawk за то, что дал мне понять, что 18 рпс это супермало, в виду определенных причин для свего домашнего компа и мелкого гугл клауда я думал что 18 рпс это норм. А ошибка была банальна: запускал сервер в девелопмент режиме, он пишет кучу логов в консоль, после запуска в продакшн режиме я получил результаты получше

ab -n 500 -c 4 http://192.168.100.9:3000/testing/do_work
...
Concurrency Level:      4
Time taken for tests:   7.435 seconds
Complete requests:      500
Failed requests:        0
   (Connect: 0, Receive: 0, Length: 0, Exceptions: 0)
Total transferred:      904000 bytes
HTML transferred:       817500 bytes
Requests per second:    67.25 [#/sec] (mean)
Time per request:       59.480 [ms] (mean)
Time per request:       14.870 [ms] (mean, across all concurrent requests)
Transfer rate:          118.74 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        4   22 101.5     11    1305
Processing:    15   37  15.4     35     194
Waiting:       15   35  14.7     33     191
Total:         21   59 102.7     47    1329

Percentage of the requests served within a certain time (ms)
  50%     47
  66%     50
  75%     53
  80%     55
  90%     61
  95%     69
  98%    178
  99%    247
 100%   1329 (longest request)

С локалхоста удалось выжать 327 рпс. Так же стоит отметить что в продакшн моде расход на гил упал до 60% (но иногда там проскакивает больше ста)

{
   "avg_gvl_vs_running":57.06,
   "threads":[
      {
         "running":421967344.0,
         "wait_gvl":210011148.0,
         "gvl_vs_running":49.77
      },
      {
         "running":357689510.0,
         "wait_gvl":184854136.0,
         "gvl_vs_running":51.68
      },
      {
         "running":191738150.0,
         "wait_gvl":148564240.0,
         "gvl_vs_running":77.48
      },
      {
         "running":348492592.0,
         "wait_gvl":186578158.0,
         "gvl_vs_running":53.54
      },
      {
         "running":357769938.0,
         "wait_gvl":188994538.0,
         "gvl_vs_running":52.83
      }
   ]
}

Комментарии (55)


  1. amarao
    26.09.2021 21:23
    +2

    Проблема языков старого поколения, что у них нет внутриязыковой семантики, позволяющей добавлять многопоточность и не боятся этого. Например, если бы у значения был владелец, и только он мог бы менять его, или передавать в пользование другим (теряя право менять). Вот если бы была модель владения, то RAII вместе borrow checker позволила бы спокойно работать без gil'а. И без garbage collector'а.

    ... но это уже был бы не руби, а язык следующего поколения на R...


    1. shybovycha
      27.09.2021 09:12

      если я правильно понимаю, std::unique_ptr и std::move есть в С++... а самое близкое на "R" - VC++ Redistributable что ли?..


      1. amarao
        27.09.2021 11:16
        +1

        Нет, это Rust. Который похож на современный C++, но без легаси, отголосков С, с разумными дефолтами по move/copy семантике и явном запрете тайпалиасинга за пределами unsafe-кода.


        1. motoroller95 Автор
          27.09.2021 11:31

          Я кстати тоже поглядываю в сторону раста, но там щас с вакансиями и проектами беда, используют только как второй язык на каком-нибудь вторичном недопроекте


          1. amarao
            27.09.2021 11:53

            Я, вообще, админ, и Rust для меня - отдушина разумного инженерного дизайна, где не срезаны углы, всё правильно, имеет под собой разумную причину и не вызывает facepalm'ов. Но пару мелочей я уже на Rust'е делал (вместо C), и по ощущениям, в режиме говнокода (написать и забыть) оно не сильно медленее питона в разработке. 99% типов автоматически выводятся, пока пишешь в рамках main() про борьбу с borrowck можно забыть, короче, как нашкрябал, так и работает. С добавленным бонусом, что большинство ошибок отлавливает компилятор, а не `TypeError: unsupported operand type(s) for +: 'NoneType' and 'str'`.


        1. shybovycha
          27.09.2021 11:39

          это была поптка тонкого троллинга, ну да ладно...

          а как у раста с существующим кодом? те же существующие либы на сях и плюсах как-то туго втягиваются, если я правильно помню. не сказать чтобы они на самих сях и плюсах хорошо втягиваются, но все же?


          1. amarao
            27.09.2021 11:50

            Совместимость с so'шками есть, и для этого есть достаточно интерфейса. Оно не "туго" втягивается, просто притаскивающий so'шку должен каким-то образом компилятору объяснить, что происходит с точки зрения безопасности памяти, владения значений и т.д. В обратную сторону проще - на rust'е можно написать so'шку, и любой С-совместимый код будет видеть С-подобный ABI.

            https://docs.rust-embedded.org/book/interoperability/rust-with-c.html


  1. Mox
    26.09.2021 21:24

    Не совсем понял идею:

    • сам подход не оч понятный - пока один "легкий тред" залочен, это означает что вычисления происходит в другом треде. Просто в одном интерпретаторе вы ограничены одним СPU, а в "параллельной" реализации без GIL росла бы загрузка процессора одним воркером до нескольких сот процентов от 1 CPU

    • Из этого вывод - при большой нагрузке нужно просто увеличить количество воркеров

    • Но больше чем позволяет вычислительная мощность сервера вы все равно не загрузите

    По моему passenger мог сам при нагрузке увеличивать количество воркеров и снижать их количество при бездействии.


    1. motoroller95 Автор
      26.09.2021 21:38

      Строго говоря методика расчета это величина обратная КПД, т.е. 94% времени что работал системный (не рубишный) поток ушло на ожидание гила, коэффициент бесполезного действия.
      Мне кажется когда встает вопрос о том сколько воркеров поднять и сколько в них тредов держать четкого ответа нету. С одной стороны Вы правы, больше воркеров, больше конектов обработаем, однако это еще больше памяти и еще больше машин нужно. Юникорн потому и умер, что он слишком обжорный, что только процессами скейлится. Просто при выборе соотношения нужно еще и про гил помнить (как оказалось).

      пока один "легкий тред" залочен, это означает что вычисления происходит в другом треде

      Что Вы имеете в виду под "легкий тред"? буду считать что это тред который не грузит проц, таковым может являться тот который пошел в базу. Ну так вот когда он в базу пошел он вернул гил, когда ответ с базы пришел ему гил нужно взять заново. И получается что он уже готов работать, но не может

      По моему passenger мог сам при нагрузке увеличивать количество воркеров и снижать их количество при бездействии.

      Пума так тоже может, но с тредами. когда нагрузка уменьшается она убьет лишние треды


      1. Mox
        26.09.2021 22:15

        Похоже что RoR все таки масштабируется про процессам а не по тредам. Поэтому вопрос стоит ставить в контексте "перепелаты за память".

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

        >Ну так вот когда он в базу пошел он вернул гил, когда ответ с базы пришел ему гил >нужно взять заново. И получается что он уже готов работать, но не может.

        Верно, потому что работает другой тред, поэтому и имеет смысл масштабировать про процессам, чтобы было поменьше залоченных тредов. Я еще не знаю как сейчас, давно была сборка ruby ee от passenger, которая форкала процессы ruby со стратегией работы с памятью "copy on write", то есть потребление памяти росло не очень быстро.


      1. Mox
        27.09.2021 00:26

        И еще - в rails очень удобное кэширование фрагментов view, что есть не во всех фреймворках-конкурентах.


  1. Aquahawk
    26.09.2021 21:50
    +2

    18 рпс

    Я всё правильно понял? Реквестов в секунду? Не тысяч а штук?


    1. motoroller95 Автор
      26.09.2021 21:54

      18к рпс это прям мощно, мб у каких-нибудь яндексов и есть такая нагрузка. У меня на работе в пике 80рпс, все это ложится на 2 машины с 10ю процессами пумы. А так да, Вы правы, именно 18 штук


      1. Aquahawk
        26.09.2021 22:01

        Тоже сходил посмотреть на одну тачку, там порядок 10к rps и примерно втрое больше конкарренси. Это правда чистый nginx но там и близко тачка не уложена. Ох как мне странно слышать такие цифры. Два сервера на 80 rps это что-то невероятное. А если к вам в 10 раз больше потребителей придёт и станет 800, оно ляжет чтоли всё?


        1. motoroller95 Автор
          26.09.2021 22:10
          -1

          а можете скинуть проект где такая нагрузка в 10к? я мб рили что-то не понимаю, но это же супердохера


          1. Aquahawk
            26.09.2021 22:13

            Одна из подсистем игровой компании у которой под 200 миллионов инсталлов игр. Больше деталей наверно в публичном поле не могу озвучить.


      1. Aquahawk
        26.09.2021 22:06

        https://habr.com/ru/post/414541/

        Вот ребята обсуждают цифры порядка 10к рпс с ядра(!) На Node.js это не статику nginx'ом раздавать, да логи писать.


        1. motoroller95 Автор
          26.09.2021 22:32

          require 'socket'
          include Socket::Constants
          
          $socket = Socket.new(AF_INET, SOCK_STREAM, 0)
          sockaddr = Socket.sockaddr_in(2000, '0.0.0.0')
          $socket.bind(sockaddr)
          $socket.listen(1024)
          
          connections = Queue.new
          
          5.times do
            fork do
              5.times do
                Thread.new do
                  loop do
                    client = connections.pop
                    client.recv(1024)
                    client.send("hello world\n", 0)
                    client.close
                  end
                end
              end
          
              loop do
                client = $socket.accept.first
                connections << client
              end
            end
          end
          
          sleep

          вот этот код на моей машине выдает 1.5к рпс, что оч мало по Вашим рассказам. Ну мб и норм что пума с рельсой в обвязке, на 5 тредах, с записью кучей логов и внутринними синхронизациями дает мне 18)


          1. motoroller95 Автор
            26.09.2021 22:36
            +1

            вот такой же только на плюсах, выдает опять таки 1.5к. Видимо это для моей машины предел


            1. Aquahawk
              26.09.2021 22:43
              +1

              Попрбуйте nginx и нагрузмте его ab


          1. Aquahawk
            26.09.2021 22:40

            Просто на сокетах, без http и всего прочего, в 5 потоков это невероятно мало. Блин у нас плюсовый http недавно отпубликовался, там еще доки нет, но есть перловая дока https://metacpan.org/pod/UniEvent::HTTP а вот плюсовая репа https://github.com/CrazyPandaLimited/UniEvent-HTTP без док.Надо попросить ребят цифры бенчмарков написать.


            1. motoroller95 Автор
              26.09.2021 22:49
              +2

              19к рпс получил на нгинксе через ab -n 10000 -c 4 http://localhost/ Кажется нужно разбираться, спасибо за идею


              1. Aquahawk
                26.09.2021 22:50
                +1

                А вот это уже что-то достойное. Рад что открыл вам глаза на то, что вообще бывает


              1. Aquahawk
                26.09.2021 22:58

                Отдельно обратите внимание что по дефолту у nginx один воркер. Дав ему 5 вы и что-то близкое к 100к рпс на своей тачке можете увидеть. Линейно оно конечно не будет скейлиться, и вопрос еще как сам ab масштабируется, этого я не копал.


                1. motoroller95 Автор
                  26.09.2021 23:25

                  у меня 5 воркеров нгинкса было 19к рпс. Еще я нашел затык почему всего 18 рпс: я запускал в девелопмент (лол кек) режиме, а он пишет логи. Запустил в продакшн и получил 327 рпс, а так же процент гила упал до 60. Не обратил на это внимание сразу т.к. привык что прилага сыплет дебаг логами. При разборе продакшн инцидентов они очень помогают.


                  1. Aquahawk
                    26.09.2021 23:29

                    Я цифрами не обладаю, спрошу у ребят наших за рпс на игровых серверах, но там сильно больше цифры, и логов оно пишет дай боже, гигабайтами с каждого процесса. А тот сервис что держит около 10к рпс, пишет около 50 гигабайт логов в час.


                    1. motoroller95 Автор
                      26.09.2021 23:36

                      нгинкс асинхронный, рельса нет


                      1. Aquahawk
                        26.09.2021 23:40

                        Наш http умеет и синхронный и асинхронный режимы, попрошу ребят забенчить в обоих, если удастся в обозримые сроки, напишу тут. А в режиме долбёжки ab сервис который никуда не ходит(ни в базу ни к другим сервисам) не должен видеть разницу синхронный он или нет


                      1. Aquahawk
                        26.09.2021 23:42
                        +1

                        Перляка у нас тоже синхронная, и каталист синхронный, оно десятки тысяч рпс конечно не тащит, но и не такие цифры.


                      1. Aquahawk
                        27.09.2021 00:08

                        Вот мне подсказывают что перловый каталист, синхронный и считающийся очень медленным, с ядра, отдаёт примерно 1-2к рпс


                  1. Aquahawk
                    27.09.2021 08:48

                    Вот посмотрите как выглядит по настоящему высокая нагрузка и какие rps можно выжимать из операционной системы https://www.techempower.com/benchmarks/


                    1. motoroller95 Автор
                      27.09.2021 09:56

                      Ну рельсы там кстати в самом дне)


      1. saboteur_kiev
        27.09.2021 01:36

        Даже обычный форум, где онлайн 10-20к, может дать гораздо больше чем 18 рпс.


      1. RarogCmex
        25.10.2021 22:30

        Я на Хаскель-Хелло-Ворлд проекте спокойно выжимал четыре (!) тысячи RPS. Подозреваю, что если подкрутить пару вещей, и самого начала подойти к оптимизации параноидально, то можно получить две тысячи RPS на боевом проекте.


    1. niko1aev
      27.09.2021 00:20
      +1

      18 запросов в секунду - это действительно очень мало.  Не знаю, как так надо готовить Rails, чтобы получить такие низкие цифры. Но в целом 50 SELECT и 10 INSERT на один запрос к серверу - это как-то странно. Если Вы создаете 180 пользователей в секунду, то за день у Вас будет 46 млн users.

      Вообще Rails из коробки выдает где-то 200-300 RPS

      При хорошей настройке мы делали
      - 20 000 к API в минуту ( 300 RPS, но очень тяжелых)
      - 1-4 млн insert в базу в сутки
      - и все это крутится на одном сервере

      Плюс на входе вешается Cloudflare + Nginx Cache и Ruby on Rails в виде монолита легко на одном сервере вытягивает несколько миллионов посетителей в месяц. Причем не пользователей, который получили статики или публичную страничку, а пользователей с поисками, авторизацией, генерацией кучи данных в БД плюс работу парочки сотен человек в коллцентре и отделе по работе с клиентами.

      Вообщем хороший Rails проект с сотнями тысяч визитов/посетителей в месяц отлично крутится на $100 сервере и отличненько выдерживает нагрузку c Latency 100-150-200ms.

      Когда проект подрастает то стоимость серваков вырастает до сотен долларов, и по опыту база обычно кушает больше чем app сервер.

      Ну и напомню, что Github (360 млн визитов в месяц по данным Similarweb) работает на Rails и отлично себя чувствует.

      Давайте кстати посчитаем. По данным Similarweb у Github (входит в топ 100 сайтов в мире по посещаемости) 7,6 page per visit.

      360 млн * 7,6 = 2,5 млрд page request per month
      это 84 млн page request per day
      это 3,5 млн page request per hour
      это 972 page request per second

      Если у Github 100 серверов, то одному серверу нужно обрабатывать только 9,7 page request per second. Пусть даже один page request породит 10 запросов, помимо статики - и мы получим 97 request per second.

      Запрос на страницу pull request порождает 10 не статичных запросов к серверу, 5 из которых вернули 304 Not Modified.

      Так что если Github хватит сотни серверов, а типичному проекту с сотнями тысяч посетителей в месяц хватит одного сервера за пару сотен долларов - то значит Rails отлично справляется со своей работой



      1. Aquahawk
        27.09.2021 00:28
        +1

        Чет я только сейчас понял к какой нагрузке я привык. Если пересчитать на месяц то некоторые наши апи обрабатывают десятки миллиардов https обращений в месяц на одной тачке и имеют запас в разы. Естественно на такой нагрузке традиционной базы там нет вообще.


        1. motoroller95 Автор
          27.09.2021 10:17

          @niko1aev @Aquahawk После очередного разбирательства выяснил что под нагрузкой я упираюсь в базу. Пока дергаю руками 1-2 запроса все летает, под бенчмарком суммарно база выходит в 200мс, при том что весь запрос занимает около 270. Копну чуть глубже, потом дам апдейт из-за чего это дело. Так что не торопимся хоронить руи)


          1. Aquahawk
            27.09.2021 10:43

            А мы тут попробовали шарповый http сервачок, в один логический поток

            ab -n 10000 -c 4 http://test-server.dev.crazypanda.ru/echo
            Requests per second:    3822.06 [#/sec] (mean)
            
            ab -n 100000 -c 50 http://test-server.dev.crazypanda.ru/echo
            Requests per second:    6836.86 [#/sec] (mean)

            По факту он жрёт конечно 2 ядра, там одно на сеть, одно на логику, но это просто шарповый сервис на коленке на потестить.


          1. Source
            28.09.2021 01:27

            Саму СУБД надо настроить для эффективной работы на SSD, плюс connection pool на стороне Rails. Настройки по умолчанию далеки от целей нагрузочного тестирования.


            1. motoroller95 Автор
              28.09.2021 11:58

              конекшн пул на стороне рельсы равен кол-ву воркеров пумы. базу поднастроить мб надо, согласен. Но суть статьи в другом была, а все свелось к мерянью рпсами(


        1. niko1aev
          27.09.2021 11:56

          У каждого инструмента своя зона применимости. В том месте где сотни миллионов https обращений в день оставлять на входе сервер, написанный на интерпретируемом ЯП - ну это глупо. Rails в этом контексте даже рассматривать нет смысла.

          Обычный web-server на Go или Rust уже выдерживает в 50-100 раз больше чем на Ruby on Rails.



          1. motoroller95 Автор
            27.09.2021 12:29

            Ну гитхаб же выдерживает. Шопифай тоже выдерживает. Писать все на руби при такой нагрузке мб не очень затея, а написать веб морду, которая только и будет что кнопки отображать норм затея, оно в принципе горизонтально масштабируется


          1. Aquahawk
            27.09.2021 14:18

            У каждого инструмента своя зона применимости.

            Не спорю. В данном случае топикстартер думал что такая производительность обусловлена IO сети, я показал что нет. Сеть в современных операционах позволяет протаскивать ооочень много rps. Это не значит что на медленных языках нужно выжимать столько, это значит нужно понимать в чём проблема, и выбирать инструмент понимая ограничения которые он накладывает и цену которую придётся заплатить.


  1. Areso
    26.09.2021 21:53

    Спасибо, всегда было интересно, какие накладные расходы идут в тех или иных вариантах многопоточности приложений, но поскольку сам в Ruby не умею, то не выдавалось возможности проверить самому.


  1. synacker
    26.09.2021 22:09
    +2

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

    1. Работа сервера состоит по сути из двух больших блоков - обработка ввода вывода и бизнес логика

    2. Блок ввода вывода линейно зависит от количества данных - у вас одна сетевая карта и один интерфейс, в котором данные располагаются последовательно и последовательно извлекаются. Добавьте к этому ещё что обращение идёт через едро ос на каждый новый запрос, что тоже не мало

    3. А вот бизнес логика, поскольку тест синтетический, имеет константную или максимум логарифмическую сложность.

    4. Поэтому вы и видите, что ваше приложение висит в блокировке, потому что большую часть времени принимает и посылает данные


    1. motoroller95 Автор
      26.09.2021 22:12

      Спасибо, интересное замечание


      1. motoroller95 Автор
        26.09.2021 22:17

        Кстати вот за ИО тоже: если в моем синтетическом тесте все ходят в ИО то большую часть времени гил свободен и треды не должны ждать друг друга. Возможно тут действительно упирается все в то, что машина занята и приемом соединений, и базой и еще чем-то. Теоретически я могу провести тест на стейдже рабочем)


        1. Aquahawk
          26.09.2021 22:21

          Какой приём соединений, ну поднимите рядом ноду, nginx или еще что нибудь. Десятки тысяч рпс вот что вы увидите.

          Edited: сорри, это вы же, я думал это другой человек обсуждает. Если кратко то нет, никакая сеть и диск не может так ограничивать производительность.


          1. Source
            28.09.2021 01:30

            Зато в CPU легко упереться. Всё-таки запускать ab на той же машине, что и испытуемый сервер - это вообще не комильфо.


            1. Aquahawk
              28.09.2021 05:57

              На масштабах топикстартера никакой проблемы нет. А для кейсов что я привожу вообще плохо подходит, т.к. захлёбыыается на 25к rps в силу однопоточности и неэффективности самого ab. Я тесты гонял на тачке с 64 ядра и она вообще ничего не заметила


  1. ivankudryavtsev
    27.09.2021 08:10

    Я так понимаю, что GIL ограничивает тогда, когда есть общие данные и многопоточность. В случае web-сервисов почти всегда можно увеличивать параллелизм за счет масштабирования количества развернутых приложений, при этом в рамках самого приложения concurrency уменьшать. Что, собственно, делается в Tornado aiohttp, etc. И без тестов понятно, что GIL эффективно работает только для длительного IO, чем больше молотит сервер тем больше мешает GIL.


    1. motoroller95 Автор
      27.09.2021 09:57

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


  1. Aquahawk
    27.09.2021 13:03

    Подозрительно похожие цифры в статье 6 лет назад https://habr.com/ru/company/1cloud/blog/272471/ и также народ недоумевает, как вообще так жить можно


    1. motoroller95 Автор
      27.09.2021 15:55

      кароче вы не поверите, но я забенчмаркал наш прод, и там 22 рпс. о чем мы тут тогда вообще говорим) там хоить и вертятся другие запросы помимо моих, но число мелкое. в общем да, рубя масштабируется наращиванием серваков. Но тут уже стоит смотреть что дешевле: купить пару машин, либо найти разрабов на жсе


  1. shybovycha
    27.09.2021 17:31
    +1

    вообще, руби (особенно с рельсами) - это не про выжимание процессора по максимуму и не про простой потоков, а про скорость разработки (благодаря удобненькому синтаксису, метапрограммированию и магии ActiveWhatever).

    для перформанса на уровне потоков и памяти придется поднапрячься. тем не более, самое близкое к руби - crystal и zig, но они очень молоды. ну или какой-нибудь play framework или grails (с перформансом все же не уверен).

    но любые такие оптимизации будут стоить времени разработки, отладки и поддержки (а мы говорим о многопоточных и оптимизированных по времени-памяти решениях). а это не просто "х*як-х*як - и в продакшен!"