waqur: (Default)
[personal profile] waqur
Недавно я рассказывал о новом языке программирования от Google — Go, в контексте применимости к программированию настольных приложений для конечного пользователя.

Сегодня мы поговорим о другой возможной области применения языка Go — программирование высоконагруженных серверов (например, web-серверов, серверов приложения, СУБД и так далее).


Сначала о том, что находится за пределами нашей дискуссии. Достаточно понятно, что для модели "web-сервер по каждому входящему запросу запускает внешний процесс" подойдёт практически любой язык программирования, который вы можете себе вообразить, в том числе и Go, ведь вопросы общей робастности и культуры управления памятью в этом контексте неактуальны: обработав очередной запрос, процесс просто умирает, вместе со своей кучей, где возможно содержатся неудалённые объекты или возможно она сильно фрагментирована — всё это не имеет абсолютно никакого значения, т.к. процесс короткоживущий.

Однако такой подход недостаточно хорош с точки зрения производительности: создавая по каждому микрозапросу новый процесс, вы достаточно сильно нагружаете ядро операционной системы, и если для 300 одновременных подключений это работает, то для 30000 одновременных подключений это не работает. Планировщик задач ядра (kernel scheduler) имеет слишком много работы с вашими микро-задачами, кроме того, каждой задаче отводится своё виртуальное адресное пространство, своя таблица страниц, свой стек, свой контекст обработки сигналов и другие ресурсы ядра, положенные "тяжеловесному" процессу.

Замена процессов потоками не слишком помогает: с точки зрения планировщика, единица планирования — это поток, а не процесс; большое количество объектов синхронизации, которые связаны с этими потоками, по-прежнему тормозят ядро в функциях ожидания и пробуждения; хотя вы экономите на таблицах страниц, но у вас остаются накладные расходы на стек, TLS и контекст обработки сигналов для каждого потока. К тому же, появляются новые накладные расходы: изменение таблицы страниц процесса требует входа в блокировку. Теперь, представьте: у вас 30000 потоков и примерно 900 из них прямо сейчас хотят распределить память в куче. Пусть даже и в разных кучах, это предполагает редактирование таблицы страниц процесса, а значит блокировку — сначала в первом потоке, затем во втором, ... и наконец в девятисотом. Не очень быстро.

Здесь нужны легковесные потоки — перенос работы планировщика задач ядра в пользовательский режим (user space scheduler). Скажем, ядро планирует работу 100 процессов, и в каждом из них крутится по 300 легковесных задач. Планировщик задач пользовательского режима планирует их выполнение поверх, скажем, 8 тяжеловесных потоков. Индивидуального стека у легковесных потоков нет, они вынуждены хранить свои стековые кадры в общей куче (в виде дерева, это называется cactus stack). Ещё, они написаны (автоматически сгенерированы компилятором) так, чтобы периодически обращаться к планировщику задач пользовательского режима и быть готовыми прервать свою работу в любой момент (и продолжить позже с этой точки без потери контекста).

Go поддерживает легковесные потоки, легковесные средства синхронизации между ними (типизированные каналы), и функции ожидания на этих каналах, которые не задействуют объекты ядра. Также Go содержит планировщик задач пользовательского режима.

Таким образом, он решает вышеописанный комплекс проблем, т.е. снимает излишнюю нагрузку с ядра ОС.

Ещё Go хорош тем, что это компилируемый, строго типизированный язык — это значит, что он хорошо масштабируется, т.е. на нём можно писать не только hello world'ы, но и большие проекты с огромным числом модулей, и все ошибки их стыковки будут выявлены на стадии компиляции проекта.

Значит, Go пригоден для программирования высоконагруженных серверов?

Вообще-то, нет. Не совсем.

Проблема заключается в плохом способе управления памятью, а точнее в сборке мусора. Go задействует сборку мусора по алгоритму mark-and-sweep, это значит, что все потоки, кроме сборщика мусора, должны быть предварительно остановлены. Этим занимается функция runtimeВ·stoptheworld() из src/pkg/runtime/proc.c , можете полюбопытствовать. Этот подход — "остановите Землю, я сойду" — на практике означает периодическую внезапную деградацию качества услуг, которые предоставляет сервер, фактически исчезновение сервера из сети. Кроме того, этот подход совершенно не масштабируется — ни по объёму памяти, ни по числу вычислительных ядер. С ростом этих аппаратных ресурсов простои будут становиться менее частыми, но всё более длительными.

Я ещё раз подчеркну тезис, который был в начале этого постинга. Если действует принцип "один новый запрос — один новый процесс", тогда такая практика вполне допустима, ведь локально-изолированная сборка мусора в одном обработчике запроса никому не мешает — сервер-то продолжает работу и продолжает обслуживать другие запросы, запуская новые процессы.

Более того, в среде "один новый запрос — один новый процесс" сборку мусора можно вообще отключить через переменную окружения "GOGC=off", т.к. обработчик запроса — это короткоживущий процесс и все его страницы будут в любом случае освобождены ядром по завершению.

Однако для долгоживущих серверных процессов этот подход не катит. Долгоживущие серверные процессы должны иметь робастную, нефрагментируемую кучу, в идеале состоящую из объектов фиксированного размера — например, 128 байт — иначе она будет фрагментироваться и постепенно "стареть", и в итоге может прийти к ситуации, когда свободной памяти много, а объект распределить нельзя. Сборки мусора, а в особенности никаких фокусов в стиле stop-the-world не должно быть в принципе; подсчёт ссылок — это единственный путь.

Что касается циклических ссылок, то вряд-ли это такая уж проблема. Для начала можно задекларировать, что такая память не освобождается, this is by design. Вполне сишный подход, типа, "а там посмотрим, сколько человек на этом расшибут себе лоб, если это станет проблемой номер один, что-нибудь придумаем в следующем языке". Вообще, мне кажется, что проблема надуманная и в реальной жизни она на сотом, если не на пятисотом месте в списке вещей, о которых следует помнить программисту. Теоретикам-пуристам это может мозолить глаза, но с практической точки зрения это песчинка на пляже. В версии 2.0 языка можно сделать фоновый поток, который сканирует память, выявляет такие вещи, и если находит хоть один цикл — вызывает panic() с подробным описанием проблемы и со ссылками на исходный код. В условиях дефицита ресурсов этот "контролёр" может вообще прекращать работу.

Однако, авторы языка Go пошли другим путём, и уже вряд-ли вернутся с подсчёту ссылок и робастной куче из объектов фиксированного размера. К тому же, подсчёт ссылок почти наверное потребует синтаксической сепарации владеющих и невладеющих ссылок (например как ^T и *T), что уже поздно делать для такого языка, как Go. Поезд тю-тю.

Что ж, it was a nice try. Легковесные потоки и средства синхронизации убедительно показали дорогу в будущее; перенос коллекций из STL в ядро языка был уместен; интеграция с Си и ассемблером была сделана на высочайшем уровне — в целом, как для прототипа, очень неплохо; а если бы не принципиальный, архитектурный дефект с управлением памятью, тогда этот язык был бы годен для написания web-серверов и СУБД следующего поколения.

Эволюция экспериментальных языков в этом секторе прошла длинный путь: C, CSP -> Newsqueak -> Alef -> Limbo -> Go. Но ещё не закончилась. Не хватает одного финального штриха.

P.S. если ваш сервер делает неинтерактивную, фоновую работу и выпадение из сети на минуту-другую для него — это нормально и допустимо, тогда Go всё ещё подходит вам.

Date: 2011-05-24 10:26 am (UTC)
From: [identity profile] cd-riper.livejournal.com
автор Go столь сурового ненавидит C++, что даже реализацию языка решили делать на си :)

треды на уровня языка штука совсем не новая, очередная вариация на тему green threads, в случае веба это отличные компромисс между многопоточным и однопоточным (epoll) подходом.

что касается памяти и GC, страхи тут надуманы.

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

во-вторых, GC как решает проблему фрагментации кучи, в отличии от...

в-третьих, есть куча веб сервисов написанных на Java и работающих в неком режиме "реального времени". с этим никаких проблем нет.

Date: 2011-05-24 10:34 am (UTC)
From: [identity profile] waqur.livejournal.com
жаба всё ещё юзает принцип stop-the-world?

Date: 2011-05-24 10:39 am (UTC)
From: [identity profile] cd-riper.livejournal.com
хз, там 150 стратегий этого дела.
фишка в том, что алгоритмы GC всегда можно заимпрувить, кто мешает это сделать и для Go?

Date: 2011-05-24 11:33 am (UTC)
From: [identity profile] waqur.livejournal.com
можно, однако текущая реализация на основе тулчейна из plan9 сосёт, что я изначально и хотел сказать

Date: 2011-05-24 02:32 pm (UTC)
From: [identity profile] japanspy.livejournal.com
да, насколько знаю, непредсказуемые паузы - это принципиальная проблема при большом heap (8, 16, 24 Gb) - в отдельных случаях
Даже есть сторонние решения для выделения памяти вне JVM, ну и какая-то работа по realtime Java, новым сборщикам мусора и т.п.

March 2024

S M T W T F S
     12
3456789
10111213141516
17181920212223
24252627282930
31      

На этой странице

Автор стиля

Развернуть

No cut tags
Page generated 2026-03-02 05:35 am
Powered by Dreamwidth Studios