Найдя достаточно хорошее решение проблем FastAPI и кооперативной многопоточности, часть меня все еще была недовольна результатами. Количество одновременных запросов значительно снизилось:
1643620388 309
1643620389 5
1643620390 3
1643620391 6
1643620392 5
1643620393 322
На практике или для любого реалистичного размера отклика все не так плохо. Приведенные выше цифры были получены при запуске всего процесса под strace
, что сделало его медленнее, но также показало, что потоки ожидают мьютекса (функции futex(...)
) и даже прерываются при попытке его получить. Другим частым действием было выделение памяти. Поэтому я начал следить за кодировщиком JSON модели ответа, так как это занимало больше всего времени.
Он начинается с кодирования корневой модели Pydantic в виде словаря, а затем вызова функции json.dumps
и ожидания результирующей строки JSON. Библиотека json
имеет смешанную реализацию на Python и C. Если возможно, она попытается использовать реализацию C для скорости, если только вы не запросите красивый вывод с отступами.
Таким образом, весь JSON кодируется одним вызовом функции C. Написав несколько модулей Python на C, я знаю, что код C вызывается с пресловутой глобальной блокировкой интерпретатора, и никакой код Python не может быть выполнен в это время. Ответственность за снятие этой блокировки, если это возможно, лежит на расширении C. И мы снова возвращаемся к теме совместной многопоточности, но на этот раз речь идет не о корутинах и потоках, а о потоках Python и нативных потоках.
CPython — это как однопроцессорный компьютер, который создает впечатление параллельной обработки, переключаясь между всеми потоками и позволяя каждому из них выполняться некоторое время. В любой момент времени выполняется только один поток кода Python. Интерпретатор переключается между потоками Python, когда поток выполнил определенное количество инструкций. Однако как только Python вызывает расширение C, интерпретатор больше не может считать инструкции и прерывать родной поток. Расширение C должно либо завершить, либо явно освободить GIL перед длительными системными вызовами и получить GIL после возвращения системного вызова. Все остальные потоки Python будут голодать, пока расширение C удерживает GIL.
Но если JSON кодируется расширением C, почему я все еще наблюдал несколько одновременных запросов вместо нуля? Просмотрев код еще раз, я понял, что библиотека json
знает, как кодировать только встроенные типы Python. Ответ, который я пытаюсь сериализовать, представляет собой дерево моделей Pydantic. Как это не приведет к ошибке «Account is not JSON serializable»?
Для нераспознанных типов json.dumps
использует необязательный параметр default
. Более подробную информацию о нем можно найти в документации. Библиотека Pydantic предоставляет реализацию этой функции, которая преобразует текущую модель в словарь. json.dumps
затем кодирует полученный словарь и может снова вызвать pydantic_encoder
для найденных им моделей Pydantic.
Таким образом, Python прыгает туда-сюда между модулем json
расширения C и кодировщиком Pydantic, написанным на Python. И пока выполняются инструкции Python, достигается порог для переключения на другой поток Python, выполняющий одновременные запросы. И вот здесь я застрял, думая о том, как улучшить параллелизм.
A) Я мог бы заранее преобразовать все модели Pydantic в словари, чтобы код на C выполнялся быстрее, не прыгая туда-сюда между расширением C и кодом Python. Но это означает, что никакой другой код не будет выполняться параллельно, и я вернусь к тому, с чего начал.
Б) Я мог бы использовать чисто Python-реализацию json.dumps
для лучшей многопоточности. Но это будет медленнее и приведет к увеличению времени отклика. Я думаю, что он оказывает большее давление на память, поскольку собирает все части строки JSON в список и строит из него одну строку, даже если используются генераторы.
Мне не понравился ни один из этих вариантов, поэтому на этом я закончил свои поиски лучшего параллелизма и не стал вносить никаких изменений в свой код.
Предупреждение о спойлерах! Следуя принципу бойскаута и оставляя мир лучше, чем я его нашел, я сделал пару патчей к CPython и улучшил производительность json.dumps
.