
Всем привет, сегодня я хотел бы поговорить о некоторых сложностях и заблуждениях, которые встречаются у многих соискателей. Наша компания активно растет, и я часто провожу или участвую в проведении собеседований. В итоге я выделил несколько вопросов, которые многих кандидатов ставят в сложное положение. Давайте вместе рассмотрим их. Я опишу специфические вопросы для Python, но в целом статья подойдет для любого собеседования. Для опытных разработчиков никаких истин тут открыто не будет, но тем, кто только начинает свой путь, будет легче определиться с темами на ближайшие несколько дней.
Отличие процессов от потоков в Linux
Ну вы знаете, такой типичный и, в целом, несложный вопрос, чисто на понимание, без копания в деталях и тонкостях. Конечно, большинство соискателей расскажет, что потоки более легковесны, между ними быстрее переключается контекст, и вообще они живут внутри процесса. И всё это правильно и замечательно, когда мы говорим не о Linux. В ядре Linux потоки реализованы так же, как и обычные процессы. Поток— это просто процесс, который использует некоторые ресурсы совместно с другими процессами.
Для создания процессов в Linux можно использовать два системных вызова:
clone(). Это основная функция для создания дочерних процессов. С помощью флагов разработчик указывает, какие структуры родительского процесса должны быть общими с дочерним. Базово используется для создания потоков (имеют общее адресное пространство, файловые дескрипторы, обработчики сигналов).fork(). Эта функция используется для создания процессов (которые имеют собственное адресное пространство), но под капотом вызываетclone()с определенным набором флагов.
Я бы обратил внимание на следующее: когда вы сделаете
fork() процесса, вы не сразу получите копию памяти родительского процесса. Ваши процессы будут работать с единым экземпляром в памяти. Поэтому, если суммарно у вас должно было случиться переполнение памяти, то всё продолжит работать. Ядро пометит дескрипторы страниц памяти родительского процесса как «только для чтения», а при попытке записи в них (дочерним или родительским процессом) будет вызвано и обработано исключение, которое вызовет создание полной копии. Этот механизм называется Copy-on-Write. Отличной книгой об устройстве Линукса я считаю «Linux. Системное программирование» за авторством Роберта Лава.
Проблемы с Event Loop
В нашей компании повсеместно распространены асинхронные сервисы и воркеры на Python или Go. Поэтому мы считаем важным общее понимание асинхронности и работы Event Loop. Многие кандидаты уже довольно неплохо отвечают на вопросы о плюсах асинхронного подхода и правильно представляют Event Loop как некий бесконечный цикл, который позволяет понять, не пришло ли определенное событие от операционной системы (например, запись данных в сокет). Но не хватает связующего элемента: как программа получает эту информацию от операционной системы?
Конечно, самое простое, что можно вспомнить — это
Select. С его помощью формируется список файловых дескрипторов, за которыми планируется наблюдать. В клиентском коде придется проверять все переданные дескрипторы на наличие событий (и их количество ограничено 1024), что делает его медленным и неудобным.Ответа про
Select более чем достаточно, но если вы вспомните про Poll или Epoll, и расскажете о проблемах, которые они решают, то это будет большим плюсом к вашему ответу. Чтобы не вызывать лишних волнений: код на C и детальную спецификацию у нас не спрашивают, мы говорим лишь о базовом понимании происходящего. Прочитать про различия Select, Poll и Epoll можно в этой статье.Еще советую посмотреть на тему асинхронности в Python Девида Бизли.
GIL защищает, но не вас
Еще одно распространенное заблуждение заключается в том, что GIL придумали, чтобы защитить разработчиков от проблем с конкурентным доступом к данным. Но это не так. GIL, конечно, не даст вам распараллелить программу с помощью потоков (но не процессов). Проще говоря, GIL — это блокировка, которая должна быть взята перед любым обращением к Python (не так важно. исполняется Python-код или вызовы Python C API). Поэтому GIL защитит внутренние структуры от неконсистентных состояний, но вам, как и в любом другом языке, придется использовать примитивы синхронизации.
Также говорят, что GIL нужен только для корректной работы GC. Для неё он, конечно, нужен, но этим дела не ограничиваются.
С точки зрения исполнения даже самая простая функция будет разбита на несколько шагов:
import dis
def sum_2(a, b):
return a + b
dis.dis(sum_2)
4 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 RETURN_VALUE
С точки зрения процессора каждая из этих операций не является атомарной. Python выполнит очень много процессорных инструкций на каждую строчку байт-кода. При этом нельзя давать другим потокам изменять состояние стека или производить любую другую модификацию памяти, это приведет к Segmentation Fault или некорректному поведению. Поэтому интерпретатор запрашивает глобальную блокировку на выполнение каждой инструкции байт-кода. Однако между отдельными инструкциями контекст может быть изменен, и тут GIL нас никак не спасает. Подробнее про байт-код и как с этим работать можно почитать в документации.
На тему защиты GIL посмотрите простой пример:
import threading
a = 0
def x():
global a
for i in range(100000):
a += 1
threads = []
for j in range(10):
thread = threading.Thread(target=x)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
assert a == 1000000
На моей машине ошибка вылетает стабильно. Если вдруг у вас оно не отработает, то запустите несколько раз или добавьте тредов. При небольшом количестве тредов вы получите плавающую проблему (ошибка то появляется, то не появляется). То есть помимо некорректности данных у таких ситуаций есть еще проблема в виде ее плавающего характера. Также это подводит нас к следующей проблеме: примитивам синхронизации.
И опять не могу не сослаться на Девида Бизли.
Примитивы синхронизации
В целом, примитивы синхронизации — не самый лучший вопрос для Python, но они показывают общее понимание проблемы и то, насколько глубоко вы копали в эту сторону. Тема многопоточности, по крайней мере у нас, спрашивается как бонусная, и будет только плюсом (если вы ответите). Но ничего страшного, если вы с ней еще не сталкивались. Можно сказать, что этот вопрос не привязан к конкретному языку.
Многие начинающие питонисты, как я уже писал выше, надеются на чудотворную силу GIL, поэтому в тему примитивов синхронизации не заглядывают. А зря, это может пригодится при выполнении фоновых операций и задач. Тема примитивов синхронизации большая и хорошо разобранная, в частности, рекомендую почитать об этом в книге «Core Python Applications Programming» автора Wesley J. Chun.
И раз мы уже посмотрели пример, где нам нам не помог GIL в работе с потоками, то рассмотрим самый простой пример, как защититься от подобной проблемы.
import threading
lock = threading.Lock()
a = 0
def x():
global a
lock.acquire()
try:
for i in range(100000):
a += 1
finally:
lock.release()
threads = []
for j in range(10):
thread = threading.Thread(target=x)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
assert a == 1000000
Retry всему голова
Никогда нельзя полагаться на то, что инфраструктура будет всегда стабильно работать. На собеседованиях мы часто просим спроектировать простой микросервис, взаимодействующий с другими (например, по HTTP). Вопрос стабильности сервиса иногда сбивает кандидатов с толку. Я бы хотел обратить внимание на несколько проблем, которые кандидаты не учитывают, когда предлагают делать retry по HTTP.
Первая проблема: сервис может просто не работать продолжительное время. Повторные запросы в реальном времени будут бессмысленны.
Retry, сделанные неаккуратно, могут добить сервис, который начал замедляться под нагрузкой. Меньшее, что ему нужно, это увеличение нагрузки, которая за счет повторных запросов может вырасти в разы. Нам всегда интересно обсудить методы сохранения состояния и осуществления досылки после того, как сервис начнет работать штатно.
Как вариант, можно попытаться сменить протокол с HTTP на что-то с гарантированной доставкой (AMQP и т. д.).
Еще задачу retry может взять на себя service mesh. Подробнее можно почитать в этой статье.
В целом, как я и говорил, никаких сюрпризов тут нет, но эта статья может помочь вам понять, какие темы следует подтянуть. Не только для прохождения собеседований, но и для более глубокого понимания сути происходящих процессов.
oleg_gavrilov
А как, по вашему, гарантированная доставка в AMPQ работает, если не через retry? Звучит так, как будто вы предлагаете спрятать проблему ретраев на нагруженный сервис в черный ящик, и представить что таким образом всё само заработает.
aranax Автор
Есть типовые решения для определенных сценариев. Пользоваться или нет это выбор каждого в каждом конткретном случаи. Будет ли это черным ящиком зависит от того насколько вы захотите разобраться в том как это работает.
InteractiveTechnology
В данном примере должен быть circuit breaker