Pull to refresh

Comments 55

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

18 рпс

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

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

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

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

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

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

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

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.5к. Видимо это для моей машины предел

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 отлично справляется со своей работой


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

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

А мы тут попробовали шарповый 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 ядра, там одно на сеть, одно на логику, но это просто шарповый сервис на коленке на потестить.

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Sign up to leave a comment.

Articles