Pull to refresh

Comments 46

А как же накладные расходы на доступ из кучи потоков к общим ресурсам?
Хорошее замечание.
В тесте я хотел показать, что накладные расходы на переключение между контектами потоков — не проблема при нашем профиле нагрузки.
Проблема синхронизации на общих ресурсах — отдельная проблема.
Но у нас запросы достаточно независимы. Они делят не так много общих ресурсов. Клиент для похода по бэкендам, например. Однако большую часть времени потоки проводят в блокировке ожидания ответа от бэкенда, так что рассчитываем, что проблему синхронизации на общих ресурсах мы не заметим.
"Как мы деградировали и вернулись к потокопроблемам."
Интересно другое — как в Tornado можно «умудриться» устроить callback hell?
Люди не знают или не хотят знать про Futures, Deferred и т.д., а еще игнорируют наличие async/await и asyncio. О причинах в статье ни слова, кроме "мы решили, что так будет проще" — и вместе с простотой получили обратно весь ворох проблем, для ухода от которых асинхронное программирование и было придумано.
Чем им блокирующий код слаще и приятнее, чем псевдоблокирующий код с await-ами (или yield-ами) — не знаю.
Уход от асинхронного кода не был основной мотивацией. Когда мы выбирали, как писать logic на java, мы рассматривали как асинхронные, так и синхронные варианты. Нам понравился синхронный варинат, потому это просто, понятно и не создает существенных проблем при нашем профиле нагрузки. О каком ворохе проблем мы забыли?
Единственная проблема, для ухода от которой и было придумано асинхронное программирование — это расход памяти под стек, пока поток вычислений (thread) ждет получения данных извне. Но раз они посчитали, что для этого им надо 600 мб и они у них есть, то и логично было для них не заморачиваться с ворохом проблем асинхронного программирования, для решения которых и было придумано многопоточное программирование.
У вас же вроде раньше Frontik/Tortik бегали по бекэндам, в статье про их замену или дополнительный слой-сборщик?
От Tortik мы оказались в пользу Frontik.
Frontikи у нас выступают в качестве фронтовых серверов, которые занимаются шаблонизацией.
Им разрешено ходить по бэкендам, но только параллельно.
Как только при походах на бэкенды появляется последовательная логика, например "после похода на бэкенд 1 проверь ответ и реши, стоит ли ходить на бэкенд 2", тогда эта логика выносится в отдельный слой, который мы называем logic.
Мы выделили этот слой, чтобы переиспользовать логику для основной версии сайта, мобильной версии сайта и api.
Изначально python logic был написан на Frontik. Теперь мы переписываем его на java.
Расскажите поподробней хотя бы про часть этой новой архитектуры, потому что синтетические тесты с кучей потоков выполняющих складывание рандомных чисел ничего не показывает в реальном приложении.
мы решили еще раз оценить, хотим ли продолжать писать вывернутый коллбэчный код
могли взять gevent/eventlet и не писать коллбэчный код.
Основная мотивация — переписать на java, так как основная часть бэкендов у нас на java.
А зачем вы писали колбечную лапшу в питоне? @coroutine в торнадо работает и во второй ветке. А в третьей есть asyncio.
Ок, надо переписать всё на java. Но там тоже можно не писать лапшу. А единственный аргумент в пользу такого решения звучит как-то так: мы хотим писать в синхронном стиле.
В итоге: у вас таймауты, куча памяти расходуется на треды в которых в основном I/O, вы плохо масштабируетесь, но зато нет callback hell, который вы же сами и устроили.
Спасибо за интересную статью.
Сам в последнее время часто думаю о дизайне вроде того что вы описали. Поправьте меня, если я неправ, но верно ли я понимаю, что описанный подход не сработает, если
а) вы хотите слать пользователю пуши, скажем по вебсокету или если у вас кастомный tcp протокол? В этом случае "по треду на запрос" превращается в "по треду на коннект". Или есть идеи как это обойти?
б) если в бэкенде вы ходите в другие бэкенды или СУБД которые выполняют долгие (скажем, больше 1 сек) операции. Опять же, ваши трэды будут висеть по много секунд в ожидании ответа и быстро кончатся.
Все верно? Если да, не боитесь в будущем столкнуться с этими проблемами при вашем текущем дизайне?
Я не автор но отвечу,
а) автор написал «никто не заставляет нас писать все в синхронном стиле. Например, какие-то контроллеры мы вполне можем написать в асинхронном стиле», веб-сокеты как раз про это.
б) если какой-то сервис тормозит и не укладывается в рамки то нужно его фиксить, если это нормальное поведение, тогда можно переложить на асинхронный вариант.
Это понятно. На самом деле я всего лишь спрашиваю, не известно ли автору каких-то способов решения этих проблем, о которых я мог не подумать, без перекладывания на асинхронный вариант. Вот чуть ниже он пишет что возможно в случае а) такой способ есть, я правда пока не понял, в чем именно он заключается.
Если взять такой цикл запроса: вычитывание запроса из сокета -> походы по бэкендам для формирования результата -> записть результата в сокет, то только этап походов по бэкендам у нас блокирующийся. Вычитываение запроса и запись результата в сокет по-прежнему происходят без блокировки с помощью селекторов. В селекторе можно зарегистрировать тысячи сокетов, а выделять тред только тогда, когда в сокете появятся данные. Поэтому с пушами необязательно выделять тред под коннект.

По поводу проблем нехватки тредов в будущем.
При расчетах я заложил увеличение нагрузки в 2 раза и все равно получил, что 600 тредов на сервер нам вполне хватит.
Не хватит — сделаем 1000, 2000, 4000 тредов.
Но, на самом деле, мы раньше упремся в CPU из-за десериализации ответов от бэкендов, чем в нехватку тредов.
Мм… не могли бы вы пояснить переход к «необязательно выделять тред под коннект». Вот у меня крутится какой-то background процесс который в какой-то момент, не суть важно какой, приходит к пониманию, что пользователю с коннектом 12345 нужно послать в вебсокет очередной кусок данных. Что происходит дальше?
Например, этот background процесс может записать данные в промежуточный буфер, найти socket, в который нужно записать эти данные, и создать задание по асинхронной записи данных из промежуточного буфера в этот socket. Heinz Kabutz неплохо рассказывает как это происходит в java на низком уровне. Но обычно на таком низком уровне никто не работает, а используют более высокоуровневые библиотеки, например Netty. Это как раз асинхронный неблокирующий ввод-вывод, и в этой задаче он оправдан.
Вот ещё 5 копеек за "блокирующий" подход:
Так что и в смысле накладных расходов от переключения контекстов 600 потоков для нас не является проблемой.
А в асинхронном коде на переключение контекстов, конкретно в питоне, на это будет тратится много* (гораздо больше) CPU, в итоге асинхронный код все больше проигрывает чем быстрее асинхронные вызовы.
За таймаутами надо следить, это может быть проблемой. Стоит ли она того, чтобы писать асинхронный код? Вопрос открытый.
В асинхронном коде тоже за этим нужно следить, т.к. проблемные конекты тоже потребляют ресурсы.
и мы не можем быстро отмасштабироваться в 100 раз
Это не зависит от подхода, асинхронный или блокирующий.

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

Вообщем асинхронный код нужно применять по месту, а не везде подряд.
Так же ещё существуют горутины/корутины/файберы/микро-треды и т.д. которые берут плюсы от обоих подходов.
4 сервера только для поиска вакансий? Сколько ж там запросов в секунду на пиковых нагрузках?
4 сервера на весь слой rpc. Он обслуживает не только поиск вакансий.
Мы тут в своей песочнице тоже пришли примерно к такому же выводу. Асинхронищина навязывается Scala Play + Slick фреймворками, однако писать\читать такой код достаточно сложно (по началу, потом наичнаешь привыкать и учишься его «выпрямлять», но это все равно требует доп. усилий), плюс к этому, «оказывается», что не все библиотеки имеют async API — приходится все равно городить thread pool'ы и тюнить их. А вот бонусов от этой асинхронщины нам было вообще никакой — нагрузки у нас нет, проблем запустить рядом еще один инстанс сервиса тоже нет, если появляются проблемы то упираемся в базу, а не в сервис.
До тех пор, пока вы 2+2 складываете, бойлерплейт с Future действительно кажется лишним. А если я хочу N параллельных запросов в базу отослать? Это мне N потоков руками запускать и их контролировать, синхронизировать результат выполнения и вот это всё. Хоп, и уже всё не так радужно с блокирующим подходом.
У нас бывают случаи, когда нужно сделать несколько параллельных запросов к разным бэкендам. В этом случае мы отправляем запросы асинхронно, получаем CompletableFuture, комбинируем их в одну, блочимся, а дальше опять работаем в синхронном стиле.
Но мы стараемся не отправлять N параллельных запросов, то есть стараемся не делать запросов в цикле, иначе можно одним запросом положить несолько бэкендов :-)
Асинхронный подход хорош для ограниченного круга задач, типа на один запрос пользователя сгенерировать множество запросов в другие системы. Для реализации бизнес логики чуть сложнее бложика сложность разработки возрастает непропорционально, не смотря на все Futures, Deferred, Promises и т.д. Да что там бизнес-логика — задача по чтению файла строка за строкой с последующим выводом счетчика строк, из задачи для школьников превращается в задачу, не каждому девелоперу по плечу, если решать ее через асинхронный подход. Как потом искать девелоперов, которые смогут поддерживать такой код, и сколько это будет стоить?
Поэтому со статьей согласен полностью.
Вот странно: ну пусть в торнадо с python2 сопрограммы на генераторах требовали некоторого бойлерплейта, в 3м питоне появился async/await, под JVM есть Scala, где Future комбинируются так же, как Option, и любые другие местные монадические типы; во всех перечисленных случаях вложенность кода с вовлечением большего числа отложенных эффектов не возрастает. О каких проблемах идет речь?
Да, по моему опыту, монадические типы действительно упрощают асинхронный код. Но ведь без них еще проще.
Проблема только в том, как выглядит асинхронный код?
В c# это вовсе не проблема.
Вопроса “блокироваться или не блокироваться при походе на бэкенды” даже не стояло: из-за GIL в python нет настоящего параллельного исполнения потоков, поэтому хочешь — не хочешь, а запросы надо обрабатывать в одном потоке и не блокироваться при походах в другие сервисы.

В питоне потоки непригодны для параллельных вычислений, а вот для параллельного ввода-вывода они вполне пригодны — GIL-то в сисколлах не участвует.
Да, я неточно написал. У нас на самом деле смешанная нагрузка: есть как ввод-вывод при походах на бэкенды, так и вычислительная нагрузка при сериализации, десериализации и бизнес-логике.
Интересно, что Google применил аналогичный подход thread-per-request при разработке Google Percolator, системы инкрементного обновления индекса (вместо Map-Reduce-based).
Описано здесь. В качестве плюсов они приводят:
— код проще
— хорошая утилизация CPU на многоядерных машинах
— легче читать stack trace'ы
— гонок в коде оказалось «меньше, чем опасались»

Самое интересное, что для решения проблем с масштабируемостью и большим числом потоков, они специально пропатчили Linux ядра на своих серверах. И видимо удалось как-то сгладить проблемы. Жаль подробности не приводятся.

Такое получилось вынесение сложности из Application кода в kernel.

Полная цитата из документа:
Early in the implementation of Percolator, we decided to make all API calls blocking and rely on running thousands of threads per machine to provide enough parallelism to maintain good CPU utilization. We chose this thread-per-request model mainly to make application code easier to write, compared to the event-driven model. Forcing users to bundle up their state each of the (many) times they fetched a data item from the table would have made application development much more difficult. Our experience with thread-per-request was, on the whole, positive: application code is simple, we achieve good utilization on many-core machines, and crash debugging is simplified by meaningful and complete stack traces. We encountered fewer race conditions in application code than we feared. The biggest drawbacks of the approach were scalability issues in the Linux kernel and Google infrastructure related to high thread counts.

Our in-house kernel development team was able to deploy fixes to address the kernel issues.
2016 год, а все еще кто-то пишет про проблему GIL на Python. Ну серьезно, есть же multiprocessing, есть интерпретаторы в которых вообще нет GIL. GIL это не проблема, это фича. Не нравиться — не используй.
Мультипроцессинг это не замена потокам. Какие альтернативные интерпретаторы питона (кроме pypy-stm, который ещё не стабилен), лишённые ограничений GIL, вы знаете?
«В питоне нет проблем с GIL, возможно вы просто выбрали не правильный инструмент» © Guido van Rossum
Википедия подсказывает:
Реализации интерпретаторов на JVM (Jython, JRuby) и на .NET (IronPython, IronRuby) не используют GIL

И чем, простите, мультипроцессинг не замена потокам? В большинстве веб проектов на питоне uwsgi --processes=10 и вполне себе.
Jython по скорости проигрывает даже CPython-у.
С IronPython то же самое, и это если зависимость от .NET/Mono не считать проблемой.

Насчёт мультипроцессинга нужно сойтись в терминологии.

Во всех случаях разные процессы — не альтернатива тредам, так как они не разделяют данные в общем адресном пространстве.

Если вы говорите о модуле multiprocessing в питоне, который пытается обойти проблему GIL, то он имеет ряд ограничений и не является заменой потокам: между исполняющимися «тредами» возможен обмен сообщениями и передача данных, но не разделение общих данных. А все ограничения сводятся к необходимости сериализовать данные при передаче, что ещё и оверхед привносит, и блокировки при ожидании очередной порции данных.

Если вы говорите о простом запуске множества отдельных процессов — под управлением какого-то сервера или самостоятельных — так это тем более не альтернатива тредам. В ситуации, когда вебприложение на питоне в рамках одного запроса должно параллельно обращаться к различным хранилищам и обрабатывать их ответ, параллелизм возможен в лучшем случае для ввода-вывода. И то, с использованием тех же тредов или гринлетов.
Чем вам GIL не уходил? У меня вот с ним проблем нет.
Да как сказать — не то что бы я против него, но в задачах, отличных от «сходить в базу и шаблонизировать страничку», он сильно мешает и приходится как-то выкручиваться.
Конечно проигрывает по скорости. Капитан Очевидность подсказывает, что именно потому что там нет GIL. Оно как бы существует вовсе не от того, что не нашлось человека с прямыми руками разрулить вопросы с памятью, а именно потому что это дает сильный прирост производительности, достаточный, что бы чем то пожертвовать. По этому говоря «Нам не подходит питон из-за GIL» вы говорите «Нам не подходит питон, потому что он быстр», что более чем не логично.
И мультипроцессинг не пытается обойти проблему GIL, он устраняет ее ограничения. О тредах в питоне не нужно думать как о стандартных тредах в других языках, это просто реализация асинхронности средствами системы. Чистые треды в питоне это именно мультипроцессинг.
Про блокировки при передачи данных от одного треда к другому? Ну так без этого особо никак, синхронизация тредов она в принципе всегда решается каким либо синхронизационным примитивом, на чем и как вы бы это не реализовывали.

Если вы хотите на питоне паралельно обращатся к нескольким хранилищам, то на это есть coroutines или asyncio. Если вы хотите параллельно обрабатывать большие объемы ответов при быстром времени отклика, активно обмениваясь между этой паралельностью большими объемами данных, то вы либо неправильно построили архитектуру(ибо задача звучит так себе), либо выбрали не тот язык, потому что питон для этого просто не предназначен. И дело тут не в GIL, а в том что это достаточно узкоспециализированная вещь которая не для питона.
да, кстати на питоне такие задачи тоже реализуются. Там для этого есть C++ )
Конечно проигрывает по скорости. Капитан Очевидность подсказывает, что именно потому что там нет GIL.

У Капитана есть какие-либо подтверждения этого факта? На тех же самых платформах, а именно JVM и CLR, работают Java и C#, которые не имеют проблем ни со скоростью, ни с многопоточностью.
О тредах в питоне не нужно думать как о стандартных тредах в других языках, это просто реализация асинхронности средствами системы. Чистые треды в питоне это именно мультипроцессинг.
Треды остаются тредами безотносительно языка, не нужно подменять понятия. Другое дело, что в скриптовых языках этим не воспользоваться.
Если вы хотите параллельно обрабатывать большие объемы ответов при быстром времени отклика, активно обмениваясь между этой паралельностью большими объемами данных, то вы либо неправильно построили архитектуру(ибо задача звучит так себе), либо выбрали не тот язык, потому что питон для этого просто не предназначен.
А это свойство не питона, а большинства интерпретируемых языков. Про задачу — задача как у топикстартера, он подтверждает, что дело одним вводом-выводом не кончается.
У Капитана есть какие-либо подтверждения этого факта? На тех же самых платформах, а именно JVM и CLR, работают Java и C#, которые не имеют проблем ни со скоростью, ни с многопоточностью.

Капитан Очевидность готов процитировать вам википедию, которая цитирует Гвидо ван Россума:
В сети не раз появлялись петиции и открытые письма с просьбой убрать GIL из Python[6]. Однако создатель и «великодушный пожизненный диктатор» проекта, Гвидо ван Россум, заявляет, что GIL не так уж и плох и он будет в CPython до тех пор, пока кто-то другой не представит реализацию Python без GIL, с которой бы однопоточные скрипты работали так же быстро[7][8].

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

Вот это новость. Thread. как опять же подсказывает википедия, это поток исполнения, и тред системы это только одна из его реализации. В питоне (как и многих других языках) помимо тредов системы, есть еще green threads, которые как Капитану Очевидность подсказывает Капитан Википедия, тоже являются потоками исполнения. По этому сложно понять, о какой безотносительности идет речь, если даже внутри одного языка это могут быть разные вещи.
А это свойство не питона, а большинства интерпретируемых языков. Про задачу — задача как у топикстартера, он подтверждает, что дело одним вводом-выводом не кончается.

При чем тут интерпретирумость? Отказ от синхронизации потоков в пользу однопоточной реализации сильно ускоряет дело не зависимо от языка или его интерпретируемости (я уж молу про то что питон помимо интерпретируемости еще и вполне себе компилируется). Тот же Redis написан вовсе не на интерпретируемом языке, и совсем не на питоне, однако он невероятно быстр исключительно потому что работает в один поток. И вводом выводом дело не ограничивается — это вполне себе NoSQL база данных. Довольно простая, но невероятно быстрая. И ограничения те же — недостаточно одного потока, поднимай два процесса и синхронизируй их с практически теми же самыми сложностями.
Sign up to leave a comment.