Offheap майнкрафт что это

Обновлено: 02.05.2024

Около года назад я задался целью получить оффер от FAANG. Как следствие, постоянной частью моей жизни стали тематические форумы, площадки и вся сопутствующая атрибутика. Спустя какое-то время я попробовал себя на собеседованиях в околоFAANGoвые компании: Lyft, Spotify, Booking и т. д, где-то успешно, где-то не очень. В это же время мне порекомендовали попробовать пройти собеседование в Тинькофф банк, который внедрил схожий процесс. После стандартного общения с HR менеджером была получена ссылка на описание процесса собеседования. “Хм, почти что FAANG + тех. интервью по Primary Skill”, - подумал я и сказал, что готов приступать. В тот же час было назначено 2 интервью: техническое и coding, а вот 3 этап, system design, нужно было заслужить успешным прохождением первых двух. Почему именно эти 2 части являлись основополагающими, осталось неясным.

Акт первый, технический

В назначенный час я встретился со своим интервьюером. Собеседование выглядело “добротным” и стандартным в заданной проф. области, оттого местами скучным. Было много задач на ревью кода и обсуждения специфики языка, в частности:

Ревью кода. Разнообразные задачи от Spring до concurrency.

Spring до concurrency. Вопросы по Spring оказались весьма тривиальными, входящими в подборку “ТОП-25 вопросов по Spring” (скоупы бинов, виды прокси и т. д.). Можно ли такими вопросами действительно проверить понимание и опыт использования Spring? Сомнительно.

Транзакции. ACID. Уровни изоляций. Как устроена работа в Spring? Несомненно, данный топик поражает своей новизной. Почему бы не отойти от заученной статьи с Википедии (которая читается 5 минут) и стандартных вопросов как будто бы про транзакцию (а на самом деле про прокси в Спринге), не мучить Senior+ кандидата выученными статьями, а поговорить о необходимости транзакций, архитектурных подходах и сопутствующих темах? Тем более вы рекомендуете к прочтению книгу “Высоконагруженные приложения. Программирование, масштабирование, поддержка / Мартин Клеппман”, где тема БД, транзакций и прочего раскрыта очень хорошо.

Concurrency. И снова стандартные вопросы про “deadlock, synchronized, reentrantLock”, принцип “happens before” и “volatile” (хотя это JMM, знатоки, не кидайтесь тапками), как-то по верхам цепляем java.util.concurrent. Зачем-то пересказываем друг другу то, что миллион раз слышали, а на практике почти никогда не применяли. В голове крутится “зачем, почему, ведь мы можем обсудить столько всего”. “Столько всего” - это:

Базовая теория потоков (какими потоками оперирует Java, почему, есть ли альтернативы в Java или других JVM-based языках, их плюсы и минусы).

Методы поиска deadlock и livelock с различными UI тулами или без них (только терминал, только хардкор). Для кандидатов уровня ниже можно просто дать на ревью thread dump.

Если уж дошли до java.util.concurrent, то вместо вопроса, что за зверь ConcurrentHashMap, давайте посмотрим на изменение подхода к ее реализации в разных версиях JVM и поразмышляем о причинах. Также можно обсудить “диковинные” коллекции в виде зоопарка очередей и, к примеру, skip lists (заодно проверив теорию алгоритмов).

Думаю, для “бесплатного” списка тем достаточно)

JMM и т. д. Вот тут прозвучал вопрос, на котором я, признаюсь завис. “Что знаешь о JMM?” Максимально высокоуровневый вопрос, на который мозг начинает генерировать: “Что от меня хотят услышать? Стандартное определение правил из спецификации типа happens before, reordering? А может, типы мониторов, их схему переходов, цикл парковки потоков? А может, еще глубже о работе с ОС?” В общем, не смог я вспомнить, с чего начинаются статьи “JMM, топ 10 вопросов”. “Кое-что знаю, но гораздо меньше, чем Шипилев” - отшучиваюсь и решаю брать инициативу в свои руки. Быстро выдаю всё, что помню про hb, и рассказываю о том, какие проблемы решал за последние несколько лет, тем самым решив перескочить на тему Java Memory. Сразу оговариваюсь, что про heap говорить не будем, так как здесь всё уже сказано и описано, а обсудим лучше NonHeap. Рассказываю о том, почему Spring Boot application не сможет долго жить на 256 Mб оперативы и при чем здесь non-heap (на самом деле, очень советую добавить вопросы про это в интервью, так как приложений, запускающихся не в контейнерах, почти не осталось). Плавно перехожу на коварную Cassandra (как пример сервиса на Java) и ее работу с offheap, заканчиваю тем, что в великолепном докладе Андрея Паньгина всё есть и он мне очень помог. Складывается впечатление, что такого мой собеседующий не ожидал. Возникает неловкая пауза, которую я решаю прервать вопросом “Обсудим gc? Недавно подбирал оптимальные настройки для нашей конфигурации Кассандры, могу рассказать”. То ли из-за ограничений по времени, то ли не выдержав пытку Кассандрой, собеседующий завершает данный блок.

Вопрос со звездочкой. Так как перед собеседованием я прочитал подборки “Топ-50 вопросов по Java, Spring и т. д”, получаю вопрос повышенной сложности со следующими входными данными: Есть сервис, работающий с Third-party сервисом. Также данный сервис работает с реляционной базой, в частности Postgres. В какой-то момент сервис начинает тормозить. Каков алгоритм моих действий? Последовательность моих рассуждений:

Полагаю, что в моем распоряжение есть все необходимые метрики системы и окружения. Поэтому первым делом предлагаю проверить наличие пиков в графиках latency: запрос в БД, запросы к third-party сервисам, сетевые задержки. Если проблема в БД - то смотрим на количество локов и потребление CPU (потребление memory нам ничего не даст, так как в БД встроен механизм кэширования). Есть проблемы - тюним БД или оптимизируем запросы.

Интервьюер отвечает, что с латенси все в порядке, поэтому я предлагаю проверить метрики JVM, в том числе GC (могли увеличиться паузы на stop-the-world).

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

Письмо ответственным за систему с подробным описанием проблемы

Использование сircuit-breaker на нашей стороне, дабы ограничить кол-во запросов к данной системе.

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

Акт второй, coding

При подготовке к собеседованию я внимательно изучил описание процесса и выделил два важных для себя момента:

Не используйте для созвона телефон: Вы будете много кодить в онлайн-IDE, рисовать схемы и общаться с интервьюером.

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

Из этого я сделал вывод, что решать задачи потребуется в онлайн-IDE вроде Coderpad, а по сложности они будут ближе к easy (возможно, middle) на leetcode. Но, как говорится, мои ожидания - это мои ожидания.

В начале собеседования интервьюер сообщил, что меня ждут 2 задачи, и время я должен рассчитывать сам.

После этого мне было предложено пошарить мой монитор, чтобы решать задачу в удобной для себя IDE. К такому повороту я готов не был, поэтому, сославшись на их регламент, попросил предоставить ссылку на online-IDE. К такому повороту, как оказалось, готов не был уже интервьюер, и ему пришлось достаточно долго настраивать редактор.

Первой оказалась задача на поиск одинаковых элементов в 3 отсортированных массивах с использованием O(1) памяти. Решения я не знал, но рассуждал вслух, предлагая оптимальные по времени, но не по памяти, решения. В процессе раздумий появилось решение на основе 3 указателей, оптимальное с точки зрения времени и памяти. Решение было реализовано, код запустился (как оказалось, выбранный редактор очень медленно запускает код) и в целом задача была решена.

Вторая задача оказалась уровнем выше. Я сразу определил, что решаться она должна методом DP, и тем самым словил диссонанс с ожиданиями: “облегчённые задачи по кодированию, нацеленные на умение использовать простые структуры данных”. Не считаю, что DP задачи, требующие двумерного массива (в простом для понимания решении), можно назвать “облегчёнными”. Объяснив, как в принципе решаются такие задачи, я взял несколько минут на раздумье, но не смог составить рекуррентную формулу, честно об этом сказав. Затем мы вывели формулу вместе с интервьюером - на этом этапе я откровенно не блистал. После этого интервьюер сообщил мне, что данный этап собеседования завершен, и мы попрощались. (Сделанные выводы: стоит подтянуть задачи на DP.)

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

Акт третий, мой любимый. System design

Приглашение на System design секцию стало для меня сюрпризом, поэтому я допустил две ошибки: выбрал не совсем удачное для себя время интервью и не уточнил, какой редактор для проектирования будет использоваться (казалось, что на первой встрече был упомянут Google Draw).

На интервью было предложено спроектировать Messenger (стандартный дизайн для FAANG собеседований). Решать задачу я собирался по удобной для себя схеме:

Расчет объема данных и ожидаемой нагрузки

HLD (High Level Design)

Возможно, обсуждение API, схемы БД

Со своей стороны могу сказать, что прохождение данного этапа было далеко от идеала, ввиду путаницы по подключениям и генерации messageId, себя я бы оценил на 6 из 10.В конце собеседования я получил фидбэк, что мой System Design плох по perfomance и latency, так как содержит очень много компонентов и медленных сервисов вроде Kafka. Занавес. “Надо было рисовать 3 квадратика”, - подумал я, - “один клиент, другой - чат-сервис монолит, третий - vendor-specific db, вроде Oracle”.

Уже после этого интервью мне понадобилось зайти в чат приложения Тинькофф, и он сразу напомнил мне о заключительном этапе. Почему? Потому что чат постоянно терял связь с сервером по причине его недоступности. Здесь я понял, что был прав и от меня действительно требовался монолит в 3-х квадратах…

Заключение

Так почему же получилось “как всегда”? Конкретно на этапах, характерных для FAANGa, я выделил для себя несколько причин:

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

Ни на одном этапе не был соблюден тайминг. Все этапы длились на 10-15 минут больше запланированного. (хотя на встрече с hr, было предложено ставить этапы друг за другом, но для себя я просил получасовой перерыв)

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

Не было знакомства с инструментами, которые использовались во время интервью

Хочется верить, что описание моих впечатлений окажется полезным и, возможно, поможет что-то улучшить. Мои предложения:

Заранее определиться с редактором для кодинга (если это будет online редактор, то высылать кандидату ссылку заранее, чтобы он мог поработать с редактором и настроить его под себя).

Не давать субъективных оценок уровню алгоритмических задач - слово “облегченные” может восприниматься разными кандидатами очень по-разному.

Предупреждать о том, какой именно редактор будет использоваться для System design, чтобы кандидат мог потренироваться. Все редакторы очень разные, и на своем опыте могу сказать, что привыкнув к популярным googleDraw и excilaryDraw, мгновенно переключиться на Sketch Tool было весьма тяжело.

Если вы стремитесь к распределенной архитектуре (судя по рекомендации книги “Высоконагруженные приложения. Программирование, масштабирование, поддержка / Мартин Клеппман”, это именно так), то стоит придерживаться её принципов и на System Design секции. Кроме того, в рекомендации можно добавить ссылку на книгу System Design Interview – An insider's guide (часто идёт в паре с рекомендованной вами).

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

Если процесс интервью и вопросы являются тайной, то ввести NDA для собеседующихся.

Также отмечу, что вышеизложенный текст предназначался в первую очередь для самой компании Тинькофф, и после всех этапов я обратился к уже упомянутой инструкции и отправил данный текст на указанную там почту. На момент публикации статьи прошло примерно 2 недели - ответа пока нет.

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

Использование разделяемой памяти в Java и off-heap кеширование

Вполне естественно, что высокие нагрузки требовали нестандартных решений. В цикле статей о разработке высоконагруженного сервера на Java я расскажу о проблемах, с которыми нам пришлось столкнуться, и о том, как мы их преодолели. Сегодня речь пойдет о кешировании изображений вне Java Heap и об использовании Shared Memory в Java.

Кеширование

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

  • 64-bit keys, byte array values: идентификатор изображения — целое число типа long, а данные — картинка в формате PNG, GIF или JPG со средним размером 4 KB;
  • In-process, in-memory: для максимальной скорости доступа все данные — в памяти процесса;
  • RAM utilization: под кеш выделяется вся доступная оперативная память;
  • Off-heap: 50 GB данных разместить в Java Heap было бы проблематично;
  • LRU или FIFO: устаревшие ключи могут вытесняться более новыми;
  • Concurrency: одновременное использование кеша в сотне потоков;
  • Persistence: приложение может быть перезапущено с сохранением уже закешированных данных.
Shared Memory

В Linux объекты Shared Memory реализованы посредством специальной файловой системы, монтируемой к /dev/shm . Так, например, POSIX функция shm_open("name", . ) эквивалентна системному вызову open("/dev/shm/name", . ) . Таким образом, в Java мы можем работать с разделяемой памятью Linux как с обычными файлами. Следующий фрагмент кода откроет объект разделяемой памяти с именем image-cache размером 1 GB. Если объекта с таким именем не существует, будет создан новый. Важно, что после завершения приложения объект останется в памяти и будет доступен при следующем запуске.


Теперь созданный объект-файл надо отобразить в адресное пространство процесса и получить адрес этого участка памяти.

Способ 1. Легальный, но неполноценный

Воспользуемся Java NIO API:


Самый главный недостаток этого метода заключается в том, что нельзя отображать файлы размером более 2 GB, что и описано в Javadoc к методу map: The size of the region to be mapped; must be non-negative and no greater than Integer.MAX_VALUE.

Работать с полученным участком памяти можно либо стандартными методами ByteBuffer'а, либо напрямую через Unsafe, вытащив адрес памяти с помощью Reflection:


Публично доступного метода unmap у такого MappedByteBuffer'а нет, однако есть полу-легальный способ освободить память без вызова GC:

Способ 2. Полностью на Java, но с использованием «тайных знаний»


Такой механизм будет работать как в Linux, так и под Windows. Единственный его недостаток — отсутствие возможности выбора конкретного адреса, куда будет «замаплен» файл. Необходимость в этом может возникнуть, если в кеше присутствуют абсолютные ссылки на адреса памяти внутри этого же кеша: такие ссылки станут невалидными, если отобразить файл по другому адресу. Выхода два: либо хранить относительные ссылки в виде смещения относительно начала файла, либо прибегнуть к вызову нативного кода через JNI или JNA. Системные вызовы mmap в Linux и MapViewOfFileEx в Windows позволяют задать предпочитаемый адрес, куда «замапить» файл.

Алгоритм кеширования

Ключевым для производительности кеша, да и download-сервера в целом, является алгоритм поиска в кеше, т.е. метод get . Метод put в нашем сценарии вызывается значительно реже, но тоже не должен быть слишком медленным. Хочу представить наше решение для быстрого потокобезопасного FIFO кеша в непрерывной области памяти фиксированного размера.

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



Сегментов может быть много — несколько тысяч. Каждому из них сопоставляется ReadWriteLock . Одновременно с сегментом может работать либо неограниченное количество читателей, либо только один писатель.

Интересная деталь: использование стандартных ReentrantReadWriteLock'ов привело к потере 2 GB в Java Heap. Как оказалось, в JDK 6 существует ошибка, приводящая к чрезмерному потреблению памяти таблицами ThreadLocal в реализации ReentrantReadWriteLock . Хотя в JDK 7 ошибка уже исправлена, в нашем случае мы заменили прожорливый Lock на Semaphore . Кстати, вот вам и маленькое упражнение:
как реализовать ReadWriteLock при помощи Semaphore?

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


Блоки данных, то есть, собственно сами изображения, выравнены по восьмибайтовой границе для оптимального копирования. Сегмент также хранит количество ключей в нем и адрес следующего блока данных для метода put . Новые блоки записываются друг за другом по принципу циклического буфера. Как только место в сегменте кончается, происходит запись с начала сегмента поверх более ранних данных.

  1. по хешу ключа вычисляется сегмент, в котором будет производиться поиск;
  2. в области индекса бинарным поиском ищется ключ;
  3. если ключ найден, из массива ссылок достается смещение, по которому располагаются данные.
  1. по хешу ключа вычисляется сегмент;
  2. считывается адрес очередного блока данных и вычисляется адрес следующего блока путём прибавления размера записываемого объекта с учетом выравнивания;
  3. если сегмент заполнен, линейным поиском по массиву ссылок находятся и удаляются из индекса ключи, чьи данные будут перезаписаны очередным блоком;
  4. значение, представленное байтовым массивом, копируется в область данных;
  5. бинарным поиском находится место в индексе, куда вставляется новый ключ.
Скорость работы
  • put: запись 1 млн. значений размером от 0 до 8 KB каждое;
  • get: поиск по ключу 1 млн. значений;
  • 90% get + 10% put: комбинирование get/put в отношении, приближенном к практическому сценарию использования кеша.


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

MineCraft and Off Heap Memory

Join the DZone community and get the full member experience.

MineCraft is a really good example of when off heap memory can really help.

  • The bulk of the retained data is a simple data structure (in minecraft's case its lots of byte[])
  • The usage of off heap memory can be hidden in abstraction.

The test

I used the following test for starting minecraft server from a seed from scratch which is a particularly expensive operation for the server.

  • Preset the level-seed=114 in server.properties
  • Delete the world* directories
  • Start the server with these options to see what the GC is doing -Xloggc:gc.log
    -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps
    -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -Xmx6g
  • Connect with one client
  • Perform /worldgen village
  • Perform /save-all
  • Exit.

Standard MineCraft

There is two particularly expensive things it does

  • It caches block stage in many byte[]s
  • It attempts to cache int[] used for processing without limit.


The high pause times are partly due to having to manage the large objects.

Off heap MineCraft

  • Use off heap ByteBuffer for long term caching. Unsafe would be more efficient but not as portable.
  • Put a cap on the number of int[] cached.


There is still some premature promotion i.e. further improvements can be made but you can see that the application is spending.

Conclusion

Using off heap memory can help you tame you GC pause times, especially if the bulk of your data is in simple data structures which can be easily abstracted. Doing so can also help reveal other simple optimisations you can do to improve the consistency of your performance.

Как не нужно писать большие сервера

Те, кто мог видеть мою прошлую статью (а она довольно related к данной теме), знают, что вот уже больше полутора лет я разрабатываю собственную реализацию сервера Minecraft, рассчитанную, в первую очередь, на высокие нагрузки. Тем не менее, в своей работе мы используем так же и стандартный сервер (Bukkit) для нескольких мини-серверов, просто чтобы было разнообразие. И вот, столкнувшись с очередной версией сервера, которая стала раз в 5 хуже предыдущих, я уже не выдержала, и решила написать эту статью.

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

Вам не нужно знать ничего о майнкрафте и особенно о его сервере, в данной статье я хочу просто рассказать, как работает оригинальный сервер Minecraft, а так же его «обвязка» — Bukkit, рассказать, почему такая система не работает и не должна. Я не претендую на идеальные знания о разработке серверов и не утверждаю, что мой сервер написан правильно и лучше всех. Я просто делюсь своим опытом, основанным на двух годах работы с сервером от всем известной Mojang и на полутора годах разработки своего сервера. Вся представленная здесь информация является моим личным мнением, а статья предназначена для расширения кругозора или даже обучения и может быть интересна как новичкам, так и продвинутым профессионалам.

  • Чанки — для тех, кто не знает, весь мир Minecraft разделён на куски площадью 16х16 кубов и высотой в зависимости от настроек. Все чанки в радиусе видимости игроков загружены в память сервера и находятся в HashMap-е, каждый «тик» сервера каждый чанк обрабатывается. В это время выполняется следующее: все активные чанки (те, которые находятся в определенном радиусе от игроков) перебираются по очереди. Для каждого чанка выполняется обработка погоды (насыпать снег, ударить молнией), а так же случайная обработка блоков — из всего чанка выбирается несколько десятков случайных блоков, проверяется, нужно ли эти блоки обновлять (по типу блока) и вызывается специальная функция на выбранном блоке.
  • Тайлы — это специальные блоки, которые обрабатываются каждый тик, а не случайно. К данным блокам относятся печи (обновление статуса пережигания материала, оставшегося топлива, это должно делаться равномерно, а не случайно, как тик остальных блоков), так же там находятся спавнеры мобов (блоки, которые спавнят мобов вокруг себя), котлы для зельеварения и подобные вещи. Они все хранятся в списке (List), который заполняется при загрузке чанка или при установке нового тайла во время работы, и перебираются каждый цикл по очереди.
  • Срочные блоки — они, конечно, так не называются, но тем не менее, это блоки, которые нужно обработать «срочно», то есть на следующем цикле или с небольшой задержкой (тоже в циклах, тут все считается в циклах, даже время, даже аллах), а не случайно, т.к. случайные блоки обрабатываются раз в несколько минут в среднем. Обрабатываются примерно как тайлы, только у них может быть указана задержка, через сколько циклов их нужно обработать. Задачи на обработку обычно генерируются во время работы сервера из-за действий игрока или других блоков. В частности, так обрабатывается редстоун, который должен очень быстро реагировать на внешние изменения, блоки огня, текущая вода и подобные.
  • Обновление света — Minecraft использует статическое освещение, разделенное на блоки. Каждый блок имеет свой уровень освещённости от 15 до 0. При изменении блоков их освещённость должна быть пересчитана, алгоритм не очень сложный, но рекурсивный, а так же есть два типа освещения — от блоков (факелов, огня и тп) и от неба, они должны рассчитываться независимо, то есть два раза на каждое изменение (если в мире есть небо, в Nether его нет).
  • Entity — это практически все объекты. Мобы, игроки, предметики, валяющиеся на полу, тележки, лодки, картины, молния, стрелы и прочее. Все они хранятся в одном большом списке и по очереди на них вызывается функция tick(), перед этим проверяется, не умерли ли они, если умерли, то они удаляются из списка и из памяти сервера, соответственно.
  • Спавн мобов — тоже отдельное действие. Мобы спавнятся в определенном радиусе от игрока, при этом выбирается случайная точка в чанке и на основе нескольких сдвигов в разные стороны выбирается, можно ли поставить туда моба, и он создаётся
  • Обработка игроков — все пакеты, которые прислали игроки необходимо обработать, очевидно.
  • Загрузка и генерация чанков — если происходит попытка доступа к блоку чанка, которого нет в памяти, чанк должен быть загружен с диска, если его нет на диске, он должен быть сгенерирован. Не надо объяснять, что жесткий диск почти всегда очень узкое место. Генерация чанка ещё сложнее, чем его загрузка.
  • Сохранение чанка — во время общего сохранения сервера или просто, когда чанк долго не использовался и может быть выгружен, чанки необходимо сохранить на диск — преобразовать в поток и записать в файл.

Вы спросите, в чем же тут проблема? Так многие делают: основная логика приложения в одном потоке, это очень удобно программировать, не нужно заботиться о синхронизации и прочих проблемах параллельных приложений. Проблема тут в том, что если на сервере больше 40 человек, вместо стандартных 20 циклов он делает уже 15, если 70 человек, то 10, если 100 — то проседает до невероятных значений. Это при том, что у меня вообще-то мощный 6-ядерный Core i7 и 64Gb оперативной памяти! И куда мне теперь деть эти ресурсы, если из 12 потоков заняты от силы два?

Не буду пустословить, приведу пример:
На сервере 223 игрока, при этом радиус видимости выбран достаточно маленький, в памяти находится 46577 чанков, 524 «срочных» блока, 87 блоков редстоуна и поршней, 11240 Entity предметов, 4274 Entity животных, 19 тележек и лодок, 717 других Entity, игроки, которые тоже являются Entity и требуют соответствующей обработки.
Количество тайлов и обновлений света мой сервер не выводит в информации (мне это не нужно), но можете поверить, их много.

Одна лишь обработка животных ужасно тяжелый процесс — они регулярно выполняют поиск пути, поиск других Entity вокруг, у них есть AI (в последних версиях довольно продвинутый), поэтому обработать 4 тысячи животных — уже большая работа.

Обойти 3 миллиона блоков (примерно столько обрабатывается случайных блоков при таком количестве чанков) — тоже не тривиальная задача.

И всё это нужно успеть сделать за 50 миллисекунд, иначе все начнет тормозить, ведь скорость рассчитывается в циклах. Если сервер делает меньше циклов в секунду, чем должен, то, например, мобы начинают ходить медленно и рывками. Плюс расчётов в циклах очевиден — если сервер подвиснет или произойдёт огромная сборка мусора (сервер ведь на Java), то не получится так, что тележка, едущая на полной скорости на следующем цикле превратится в быстро движущийся маленький объект, и придётся рассчитывать её движение по более сложным алгоритмам.

При этом есть ещё и Bukkit!
Bukkit — это такой «враппер» для ванильного сервера. Он добавляет API для создания плагинов, он супер-удобен для разработчиков плагинов и сделан действительно качественно. Но, грубо говоря, всё становится только хуже. Если игрок присылает пакет, что он немного сдвинулся или повернул голову… создаётся событие и посылается по всем плагинам, которые его обрабатывают. А при этом функция обработки движения и так довольно сложная. При ломании блока или установке происходит то же самое, а так же при около сотни других действий, которые создаёт игрок или сам сервер, включая махание рукой, смена состояния редстоуна, перетекание воды, спавн моба, AI, тысячи их… То есть, как бы система хорошая, но создаёт кучу дополнительных вызовов при обработке всего.

К счастью, некоторые разработчики плагинов научились вытаскивать тяжелую логику своих плагинов в отдельный поток. Яркими и хорошими примерами служат плагины OreObfuscator и Dynmap. Первый «чистит» посылаемые игроку блоки от лишних данных, чтобы игрок не мог читами смотреть сквозь стены. Он делает это в отдельном потоке, складывая пакеты в очередь и обрабатывая их отдельно от логики сервера. Второй генерирует динамическую карту для браузера, тоже очень качественно сделан. В общем, хвала им, что не нагружают основной поток ещё сильнее.

Так же есть плагин, который уменьшает количество вещей, которые обрабатывает сервер за цикл. Объединяет лежащие рядом предметы, выгружает мобов, ограничивает обработку чанков. Это очень круто, ни один сервер не обходится без этого плагина — NoLagg.

Как делать правильно (по-моему)
Мы долго мучились со всем этим, когда полтора года назад наш онлайн вырос до 100 человек, а скорость работы просела до 0.5-1 цикла в секунду. Мы пытались делать оптимизации сервера, правили код, пытались убрать как можно больше лишнего, изменили в некоторых местах работу не по циклам а по секундам (например, в печи. В Bukkit это потом тоже добавили… через несколько месяцев). В конце концов мы достигли ужасной нестабильности сервера и решили плюнуть на всё это.

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

Разделить уже существующий сервер на несколько потоков не представляется возможным. Многопоточное программирование — штука тонкая, сложная, требующая большого знания кода, с которым работаешь, и в существующее приложение практически не встраиваемая. Код надо писать с нуля.

Так родился сервер, в основе которого было заложено как можно больше потоков: мир делится на куски по 64х64 чанка и каждый такой кусок обрабатывает чанки в одном потоке, один поток для обработки срочных блоков, один поток для редстоуна и поршней, один поток для мобов, один поток для предметов, один поток для тележек, один поток обрабатывает другие Entity и прочую информацию о мире, один поток пересчитывает свет, четыре потока по разным частям света сохраняют мир на диск, один поток рендерит карту, один поток занимается обслуживанием сервера и команд консоли, обновляет статистику. Для игроков используется система, которая позволяет обработку пакетов ставить либо на отдельный поток для каждого игрока, либо на пул потоков, либо на отдельные потоки для каждого игрока. При этом всё можно разделить на ещё несколько потоков: обрабатывать один и тот же тип объектов хоть в 20 разных потоках. А так же Netty (NIO) в качестве сетевого движка, в отличии от стандартного I/O.

Разработка стабильной версии такого сервера, не обладающего всем функционалом, стоило примерно 8 месяцев работы одной меня без обладания опытом. Весь код рассчитан на асинхронный доступ ко всем данным. Но оно того стоило — совсем недавно мы поставили рекорд в 559 человек, которые не просто стояли в одном месте, лагали и снимались на фрапс, а проходили очень большой ивент с редстоуном, и при этом чувствовали себя комфортно.

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

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

Поток мыслей может содержать орфографические ошибки, т.к. писалось на одном дыхании.

Читайте также: