В Asana мы создали систему загрузки данных под названием LunaDb, которая служит основой нашего веб-приложения. Несмотря на название, это не база данных. Это, скорее, похожая на GraphQL система для декларативного извлечения данных — по сути, способ загрузки последней версии данных и всех будущих обновлений.
Первоначально мы запустили LunaDb в 2015 году в качестве радикально переработанной серверной инфраструктуры¹. Центральным компонентом этой новой системы был сервер синхронизации — монолит, который выполняет всё: от синхронизации клиентов до загрузки данных и управления доступом. Без существенных изменений эта первоначальная архитектура масштабировалась далеко за пределы первоначально ожидаемых уровней, вплоть до миллионов активных пользователей в неделю и миллиардов запросов в день.
Хотя производительность оставалась высокой, по мере роста трафика и сложности функций эксплуатация и улучшение LunaDb становились всё более сложными из-за ограничений сервера синхронизации.
обзор нашей инфраструктуры загрузки данных
Почему его было трудно эксплуатировать?
Перераспределение трафика обходится дорого
Сервер синхронизации напрямую управлял постоянными веб-сокетными соединениями с клиентами. Каждый веб-сокет поддерживался сессией клиента с отслеживанием состояния. При разрыве соединения всё это состояние терялось, и клиенту приходилось заново подписываться на все нужные ему данные. Когда это происходит с большим количеством сеансов, это быстро становится дорогостоящим. Поэтому нам приходилось проявлять осторожность при переключении этих соединений, учитывая большой всплеск работы, связанный с повторными подключениями.
сдвиг трафика
Развертывание серверов синхронизации означает перенос трафика
Конечно, вы не можете постоянно предотвращать перемещение трафика. Всякий раз, когда вы хотите внедрить новый код или увеличить/уменьшить масштаб, вам нужно будет вывести из эксплуатации серверы синхронизации, а это требует переноса всего трафика с завершаемых инстансов.
постоянные обновления
Серверы синхронизации неэффективны при запуске
В то же время серверы синхронизации начинают работать эффективно только после значительного прогрева процессов. Решение этих двух проблем требовало довольно хрупкого баланса и в прошлом предполагало большую инженерную работу.
Серверы синхронизации сложны и трудно поддаются мониторингу
Наконец, поскольку серверы синхронизации выполняли множество произвольных фрагментов кода продукта (через пользовательские функции на стороне сервера), они были очень уязвимы к регрессиям производительности, вызванным шумными соседями, которые было трудно идентифицировать².
проблема шумного соседа
Возникает резонный вопрос: «Почему серверы синхронизации сложны и неэффективны при запуске?»
Одна из частых причин этих проблем — наш серверный программный код. Основные части кода сервера синхронизации написаны на Scala. Несмотря на некоторые сложности, связанные с управлением состоянием сеанса и различными аспектами фреймворка Luna, этот код фреймворка/платформы в основном ведет себя так, как мы ожидаем (проблем с работой и производительностью относительно мало).
С другой стороны, эти вычисляемые сервером значения продукта (мы называем их SCV, но представляйте себе пользовательские резолверы) написаны на Typescript. Оба набора кода выполняются вместе в GraalVM — полиглотной виртуальной машине, которая позволяет использовать несколько языков с помощью своего фреймворка Truffle. Поскольку SCV написаны на Typescript, они по сути интерпретируются при запуске, что, как и ожидалось, приводит к неприемлемой производительности и использованию ЦП. GraalVM попытается выполнить JIT-компиляцию для вызванных SCV. Это хорошо! GraalVM/Truffle могут значительно оптимизировать их производительность, но это не бесплатно. Компиляция SCV может быть довольно затратной (с точки зрения использования процессора, кэша кода и т. д.).
настройка нашей полиглотной ВМ
Почему два языка?
Наш первый проект для SCV был полностью реализован на Scala. С другой стороны, наши системы мутаций и асинхронных задач написаны на Javascript/Typescript. Хотя SCV на основе Scala и работали, дублирование бизнес-логики между нашими системами мутаций и асинхронных заданий и LunaDb, а также незнание Scala инженерами по разработке продукта, стали серьёзным тормозом для скорости разработки продукта.
Почему GraalVM?
Мы кэшируем большой объём данных в процессе работы, чтобы ускорить вычисление (и повторное вычисление) результатов подписки. Использование GraalVM даёт нам простой способ совместного использования этих кэшей на разных языках без проблем с корректностью или производительностью, которые могут возникнуть в результате разделения частей Scala и TypeScript на отдельные контейнеры.
Почему было трудно внести улучшения?
Поскольку сервер выполнял так много функций и был относительно нестабильным в эксплуатации, мы старались избегать более существенных изменений. Не только из-за сложности кода, но и из-за высоких затрат на безопасное внедрение новых изменений.
Да, я люблю задавать вопросы
Учитывая трудности с эксплуатацией и улучшением сервера синхронизации, мы приняли непростое решение изменить нашу архитектуру. В основном мы решили заменить монолитный сервер синхронизации двумя более мелкими компонентами:
брокер сеансов, который управляет клиентскими соединениями и разрешением состояний
синхронизируемый загрузчик, отвечающий за загрузку данных, то есть выполнение запросов от брокеров сеансов
брокеры сеансов и загрузчики с возможностью синхронизации
Почему это помогает?
Эта новая архитектура сразу же отделяет перенос трафика WebSocket (т. е. развертывание брокеров сеансов) от подготовки новых процессов (т. е. развертывания синхронизируемых загрузчиков). В результате мы можем свести к минимуму сбои, развертывая загрузочные модули с возможностью синхронизации отдельно от брокеров сеансов.
Каждый из этих новых компонентов проще.
Брокеры сеансов намного легче, не требуют прогрева процессов и не выполняют код продукта. В результате нам не нужно развертывать их так часто, а когда мы это делаем, это довольно просто.
Загрузчики Syncable имеют более простой интерфейс (запросы на подписку без сохранения состояния), который легче адаптировать к стандартному горизонтальному автомасштабированию подов Kubernetes. Это также делает их прогрев более быстрым и простым — мы можем просто использовать зеркалирование трафика для окружающих запросов, проходящих между брокерами сеансов и загрузчиками Syncable.
Новая архитектура позволяет нам значительно упростить процесс разработки продукта. С самого начала независимые графики развертывания серверного кода продукта и вызова клиентского кода продукта тормозили скорость и были источником операционных трудозатрат (из-за несовместимости версий). Поскольку синхронизируемые загрузчики теперь являются единственным оставшимся процессом, который запускает код продукта, и их развертывание больше не вызывает сбоев, мы можем повторно развертывать их всякий раз, когда отправляем новый код продукта.
Эта новая архитектура позволяет нам лучше масштабироваться под новые функции, развертывая разные пулы загрузчиков syncable для разных типов рабочих нагрузок (например, для различных отдельных функций, таких как «Входящие», «��адачи», «Цели» и т. д.). Брокер сеансов функционирует как сервисный шлюз, который может напрямую контролировать, как запросы данных направляются в различные восходящие синхронизируемые загрузчики.
Отлично! Эта новая архитектура звучит намного лучше, но как мы воплотили её в жизнь? Сервер синхронизации по сути является монолитом, поскольку включает в себя несколько функций, а разбивать монолиты почти всегда сложно. В нашем случае нам пришлось преодолеть некоторые ключевые препятствия, связанные с архитектурой.
Разбивка PubSub
PubSub, наша система для реализации реактивности, была разработана на основе одного процесса (сервера синхронизации), отвечающего за загрузку новых данных и их отправку клиенту. Нам пришлось переработать PubSub таким образом, чтобы гарантировать корректность этих двух, теперь независимых, типов процессов (синхронизируемых загрузчиков и брокеров сеансов).
Давайте кратко рассмотрим, как это реализовано на серверах синхронизации. Примечание. Возможно, будет полезно прочитать нашу предыдущую публикацию о конвейере аннулирования, но мы предоставим обзор системы, не требующий предварительных знаний.
На сервере синхронизации мы отслеживаем подписки для каждого сеанса. Мы используем конвейер аннулирования, чтобы постоянно отслеживать обновления подписок. При каждом новом сообщении об аннулировании сервер синхронизации перезагружает все затронутые подписки.
Серверы синхронизации активно кэшируют результаты объектов/запросов БД, результаты пользовательских резолверов и результаты предыдущих подписок, чтобы оптимизировать перезагрузку подписок (т. е. используя шаблон сквозного чтения). Кэшированные артефакты пассивно признаются недействительными тем же конвейером признания недействительными, который используется для подписок. Всякий раз, когда мы пытаемся использовать кэшированные данные, мы проверяем их действительность и при необходимости возвращаемся к повторному вычислению.
Мы можем наблюдать чёткую зависимость между перезагрузкой подписок и аннулированием кэшированных данных. При получении сообщения об аннулировании, если мы перезагрузим подписку до того, как кэшированные данные будут аннулированы, мы потенциально вычислим устаревший результат. Когда загрузка данных и управление подписками происходят в рамках одного процесса, обеспечить эту зависимость тривиально — достаточно аннулировать кэши перед перезагрузкой.
Условие гонки может привести к устаревшим данным при обновлении
В предлагаемой нами новой архитектуре брокеры сеансов и синхронизируемые загрузчики являются независимыми потребителями конвейера аннулирования. Как же тогда мы можем обеспечить аннулирование кэшей до перезагрузки подписок?
Версии запросов и ответов
Мы могли бы решить эту проблему, заставив конвейер аннулирования доставлять сообщения синхронно. Или мы могли бы создать механизм, обеспечивающий гарантию упорядочения, чтобы никакие сообщения конвейера аннулирования не поступали в брокер сеансов раньше, чем в загрузчик с возможностью синхронизации. Однако оба этих решения имели неидеальные компромиссы — что особенно важно, они увеличивали связь между брокерами сеансов и загрузчиками с возможностью синхронизации.
Вместо этого мы решили эту проблему, дополнив наш протокол загрузки данных версиями запросов и ответов на основе их относительного прогресса в потоках аннулирования. Поскольку поток представляет собой полную упорядоченную последовательность обновлений баз данных, ход выполнения потока можно использовать в качестве глобального счётчика версий.
версии запроса и ответа
Перезагрузки при аннулировании
Серверы синхронизации загружают огромное количество данных — большую часть всех операций чтения для сайта. Наша новая архитектура требует, чтобы брокеры сеансов и загрузчики syncable обменивались большим объёмом данных по сети. Для новых подписок эти сетевые издержки относительно незначительны. Однако это особенно неэффективно для перезагрузок при аннулировании, поскольку нам часто не нужно возвращать полный ответ — только обновлённые данные³. В некоторых случаях ситуация особенно плохая. Представьте себе ситуацию, когда один пользователь выгрузил 10 000 задач в проекте, а другой пользователь постоянно меняет описания задач в этом проекте: все задачи нужно будет отправлять при каждом аннулировании! Очевидно, что в идеале нужно отправлять только обновлённые данные, но как это эффективно реализовать?
перезагрузки при аннулировании
Фингерпринтинг
Чтобы синхронизируемый загрузчик мог вычислить дельту обновлённых данных, он должен знать, какие данные уже есть у инициатора запроса. Но передача последних данных с запросом будет такой же затратной, как и возврат полного результата. Нам нужно представить данные более компактным способом.
Хэширование — отличный способ сэкономить место. Каждый бит детализированных данных, который нам нужен, называется синхронизируемым объектом. Мы можем вычислить 128-битный хеш Murmur для каждого сериализованного объекта syncable, чтобы использовать его в качестве цифрового отпечатка ⁴. В частности, этот отпечаток является идентификатором для этой версии syncable.
Везде, где мы отслеживаем данные syncable, мы можем вместо этого использовать их цифровые отпечатки. Теперь, когда мы хотим отследить полный ответ на подписку, мы можем просто использовать набор отпечатков, не передавая полные данные!
Примечание: что такое syncable и как он связан с подписками?
Syncable — это содержимое результата подписки. Когда мы загружаем подписку, результаты возвращаются в виде набора синхронизируемых объектов. Если точнее, синхронизируемым элементом может быть объект, запрос или результат SCV.
синхронизируется с сопоставлением подписок
Очевидно, что каждая подписка сопоставляется с несколькими объектами синхронизации. Однако синхронизируемые объекты могут быть общими для нескольких подписок (когда они загружают пересекающиеся данные). Поэтому между подписками и синхронизируемыми объектами фактически существует сопоставление «многие ко многим».
Мы передаём набор этих цифровых отпечатков с каждым запросом от брокера сеансов. В загрузчике объектов синхронизации мы вычисляем полный ответ, вычисляем его набор цифровых отпечатков, исключаем все данные, которые пересекаются с запросом, и возвращаем дельту.
Учитывая масштаб и критичность изменения, мы разделили внедрение примерно на 4 этапа.
Этап 1. Рефакторинг монолита
Разделение нашего тесно связанного кода управления сеансами и загрузки данных на независимые компоненты
Этап 2. Локальный загрузчик syncable
Использование нового компонента загрузки данных для создания локального сервера gRPC и переноса загрузки данных
Этап 3. Удалённый загрузчик syncable-loader
Создать новый двоичный файл syncable-loader и развертывание
Перенести всю загрузку данных sync-server в наше новое развертывание syncable-loader
Этап 4. Отдельный двоичный файл session-broker
Создание нового двоичного файла session-broker и его развертывание
Перенести весь трафик с sync-servers на session-brokers
Очень много. С чего начать?
Большие ответы
Одна из проблем, с которой мы быстро столкнулись, — это большой размер ответов. Поскольку загрузка данных на серверы синхронизации происходила в рамках одного процесса, до сих пор это не представляло большой проблемы⁵. Однако, как только мы начали загружать данные через локальную границу gRPC, мы столкнулись с множеством проблем.
Мы всё время подозревали, что некоторые ответы могут быть большими, но когда начали исследовать этот вопрос, то обнаружили действительно поразительные результаты. У нас были тысячи загрузок в день, регулярно превышающих 100 МБ! Мы физически не могли возвращать такие большие ответы через унарный метод gRPC (вы начинаете достигать максимального размера фрейма http2). Что делать?
Мы рассмотрели несколько способов систематического исправления этой проблемы, но в конечном итоге пришли к выводу, что нам нужно устранить основные причины. Мы могли бы реализовать потоковую передачу gRPC на стороне сервера, но связанные с этим высокие затраты на сериализацию и повышенная конкуренция за сокеты оказали бы довольно негативное влияние на задержку и пропускную способность. Мы могли бы просто отклонить эти ответы, но частота их появления была слишком высокой, чтобы это было приемлемо.
Мы остановились на трёхэтапном подходе, при котором мы отмечаем все большие ответы, анализируем и устраняем каждый проблемный случай, а затем устанавливаем строгую верхнюю границу размера ответа.
Мы пометили все загрузки размером более 1 МБ и зарегистрировали подробные события об источнике, использовании и разбивке данных. Было несколько разных ресурсоёмких вариантов использования, но самым печально известным, вероятно, были большие объекты BLOB с миниатюрами вложений. Оказывается, они кодировались как строки base64 и включались в сериализованные ответы. В небольшом количестве они были терпимы, но быстро становились огромными при массовой загрузке — например, при загрузке представления в виде сетки, в котором отображались миниатюры вложений для каждой задачи.
Мы смогли постепенно устранять подобные проблемы, ограничивая миниатюры для огромных ответов, используя миниатюры меньшего размера и, в конечном итоге, удаляя двоичные данные из ответа. После таких простых мер по снижению риска количество огромных ответов исчезло, и впоследствии мы смогли ввести ограничения на размер ответов на уровне фреймворка⁶.
Коллизии тем
Ещё одна странная проблема, с которой мы столкнулись, — это конфликты тем pubsub. Оказывается, у нас были некорректные варианты использования фреймворка, которые генерировали одну и ту же тему подписки независимо от домена. Когда pubsub происходил только в одном типе процесса, последствия были относительно безобидными. Как правило, одна тема pubsub соответствует данным из одного домена. Однако теперь, когда pubsub разделен между брокерами сеансов и загрузчиками синхронизации, два типа процессов могли не согласовываться по домену конкретной темы. Когда это происходило, мы наблюдали стабильно повышенную частоту перезагрузок из-за аннулирования в связи с этим «несоответствием доменов». К счастью, исправление было довольно простым, но интересно, как этот баг так долго существовал в нашем фреймворке, не будучи обнаруженным.
Повторная настройка загрузки
Рабочие нагрузки брокеров сеансов и загрузчиков syncable значительно отличаются от нагрузок исходных серверов синхронизации. Брокеры сеансов отвечают только за управление сеансами, а загрузочные модули, поддерживающие синхронизацию, — только за загрузку данных.
Мы не были точно уверены, как это повлияет на их требования к ресурсам, поэтому начали с одинаковых запросов ресурсов (cpu/mem) и настроек горизонтального автомасштабирования подов (HPA).
session-brokers
В ходе наблюдений стало ясно, что брокеры сеансов значительно легче. Они надежно работали при очень низкой нагрузке на процессор (скорее, они были гораздо более зависимы от памяти⁷). Нескольких реплик, казалось, было достаточно для обслуживания трафика всей ячейки инфраструктуры. Однако когда мы фактически уменьшили minReplicas на HPA, мы заметили, что перезагрузки данных и задержка перезагрузки резко возросли. Что происходило?
Если коротко, то мы не учли все наши общие настройки, касающиеся размера кэша и регуляторов. При наличии всего нескольких реплик каждый брокер сеансов обрабатывал гораздо больше сеансов на под (примерно в 3,5 раза), чем обычный сервер синхронизации. Поскольку каждый из них обрабатывал гораздо больше данных, они полностью заполняли свои кэши тем pubsub ⇔ подписок, и каждое удаление вызывало перезагрузку (из соображений безопасности). Соответствующее увеличение этого порога примерно в 6 раз устранило повышенную частоту вытеснения из кэша и перезагрузки. Кроме того, мы обнаружили, что наши иерархические регуляторы перезагрузки были неправильно настроены для новых скоростей входящего трафика. Корректировка настроек этих регуляторов также привела к резкому сокращению задержки перезагрузки и задержки сквозной реактивности (т. е. времени, которое требуется веб-приложению, чтобы увидеть свою собственную запись) примерно в 5–10 раз.
syncable-loaders
С другой стороны, синхронизируемые загрузчики были значительно тяжелее, чем ожидалось. Каждый сервер загружал больше подписок в секунду (примерно в 1,5 раза), чем сервер синхронизации с эквивалентными ресурсами. В отличие от брокеров сеансов, они были гораздо более зависимы от процессора ⁸.
Интересно, что незначительная часть нагрузки на процессор была связана с увеличением деоптимизаций Truffle, относящихся к нашему коду TS SCV. Скорее всего, это было вызвано тем, что каждый синхронизируемый загрузчик обращался к большему объему нашего кода SCV. В любом случае это потребовало небольшого увеличения размера нашего кэша кода⁹.
Разогрев процессов
Разогрев процессов для загрузки данных исторически был непростой задачей. К счастью, в нашей новой архитектуре всё немного проще. Основным интерфейсом наших syncable-loader являются запросы данных без сохранения состояния, поэтому мы можем прогреть их, просто воспроизведя или зеркально отобразив существующий трафик между session-broker'ами и syncable-loader'ами.
С другой стороны, мы по-прежнему сталкиваемся со многими из тех же проблем, что и при прогреве серверов синхронизации. В основном, прогрев процессов требует большой загрузки процессора, и это вызывает всевозможные проблемы с шумными соседями (для нас релевантной метрикой здесь является частичная остановка, поскольку мы не достигаем ограничений регулирования k8s) при запуске. Хорошим улучшением, которое мы сделали здесь, было использование изменения размера пода на месте, чтобы ограничить ресурсы syncable-loader при запуске, но позволить ему разгоняться после запуска.
Несмотря на это, прогрев каждого пода по-прежнему занимает порядка нескольких минут. Изучив профили JFR, мы пришли к выводу, что основным узким местом является недостаточная компиляция кода TS SCV во время прогрева, и считаем, что здесь есть дополнительный потенциал для улучшения. Мы активно изучаем возможность более точного прогрева соответствующих путей с помощью более целенаправленных методов и изменения наших интерфейсов TS¹⁰ для лучшей компиляции.
Наши брокеры сеансов не отвечают за загрузку данных и на практике никогда не требовали какого-либо прогрева.
Наша новая архитектура значительно снизила операционную сложность, ускорила развертывание и скорость кода, а также открыла будущие возможности для повышения скорости и масштабирования.
Наша новая архитектура просто автоматически масштабируется в соответствии с изменениями общего трафика чтения, не требуя сложного управления трафиком. Каждая операция по переключению трафика аккуратно обрабатывается простым перезапуском подов. Нужно загрузить все сеансы? Запустите цикл всех брокеров сеансов. Нужно очистить кэш? Запустите цикл всех загрузчиков, поддерживающих синхронизацию. Обе операции безопасны с точки зрения времени безотказной работы.
Раньше скорость разработки продукта в основном ограничивалась узкими местами, связанными с развертыванием серверов синхронизации. В нашем новом мире для развёртывания кода продукта нам нужно только развернуть загрузчики синхронизации. Мы сделали это, переместив загрузчики синхронизации в их собственную развёртываемую ячейку, которую мы стараемся развёртывать чаще (и в конечном итоге вместе с кодом продукта). Развёртывание загрузчиков syncable уже примерно на 40% быстрее (примерно на 20 минут быстрее), чем развёртывание серверов sync (мы можем безопасно увеличить этот показатель намного больше), и в будущем мы планируем добиться ещё большего увеличения.
Примечательно, что повышение производительности не было целью этой работы, но об этом стоит поговорить, учитывая, насколько сильно оно повлияло на проектирование и реализацию. Задержка вычисления результатов запросов в нашей новой системе фактически уменьшилась (вероятно, благодаря лучшей балансировке нагрузки/настройке/автомасштабированию). Соответственно, задержка первоначальной подписки заметно лучше. С другой стороны, задержка сквозного отражения мутаций (т. е. время, необходимое для распространения изменения на другие сеансы) примерно такая же. Трассировка показывает, что это, вероятно, связано с перекосом в потреблении потока PubSub между брокерами сеансов и загрузчиками syncable (загрузчики syncable не могут обслуживать запрос, пока не будут обновлены до версии запроса).
Наша нынешняя система значительно улучшена, но она по-прежнему ограничивает скорость продукта, требуя учитывать обратную/прямую совместимость с каждым выпуском. Однако благодаря всем нашим улучшениям в скорости развертывания теперь можно развертывать все изменения модели данных вместе, тем самым устраняя необходимость учитывать обратную/прямую совместимость. Мы активно рассматриваем возможность реализации этого в ближайшем будущем.
Одной из наших главных мотиваций для этой работы были регрессии производительности, причины которых трудно определить. Примечательно, что это одна из областей, в которой мы не добились значительных улучшений в результате этой работы. С другой стороны, теперь это одна из основных проблем, над решением которой мы будем работать в будущем. В отличие от большей части обсуждаемой здесь работы, это гораздо более межфункциональная проблема, которая затрагивает область продукта, модель данных, фреймворк и инфраструктуру.
синхронизируемые пулы рабочих процессов загрузчика по функциям
Мы с воодушевлением изучаем решения, связанные с инфраструктурой платформы (например, пулы рабочих процессов по функциям), фреймворками платформы и инструментами платформы (например, тестирование методом «чёрного ящика»/«белого ящика»).
Арвинд Виджаякумар — инженер в команде LunaDb, где он помогает создавать и масштабировать основную платформу загрузки данных Asana — критически важную инфраструктуру, которая гарантирует, что пользователи всегда видят точные, реактивные и молниеносные обновления в наших веб-приложениях и API.
Эта работа по масштабированию LunaDb — длительная командная работа, которая велась в течение последних нескольких лет и в которой участвовали многие нынешние и бывшие члены команды LunaDb: Брэндон Чжан, Алекс Матевиш, Шон Венцель, Эрик Уолтон, Спенсер Ю, София Яо, Джордж Онг, Тайлер Прете, Кушик Гош, Натан Дубицкий, Винод Чандра Мурти и многие другие
Примечательно, что мы создали её до того, как Facebook открыл исходный код GraphQL
Более подробная информация о прогреве процессов представлена в предыдущей публикации, но вкратце: мы полагаемся на произвольное увеличение мощности модулей синхронизации как способ быстрого масштабирования при локальных скачках трафика
Мы также выполняем обновления с детализацией на уровне объекта, а не на уровне поля (по историческим причинам). Это увеличивает объём данных, задействованных при перезагрузках для инвалидации.
Мы используем 128-битный хеш Murmur, чтобы избежать коллизий.
Примечательно, что мы очень давно используем сжатие WebSocket, что, вероятно, смягчает последствия, ощущаемые клиентом.
Примечательно, что у нас также изначально были некоторые проблемы с накладными расходами сетевого перехода. После некоторого исследования мы обнаружили, что большая часть накладных расходов на самом деле была связана с нашей сервисной сеткой (Istio/Envoy), и мы внесли некоторые целенаправленные изменения, чтобы улучшить производительность в этом отношении.
Важным фактором в отношении сохраняемой памяти стала переработка нашего синхронизируемого хранилища на стороне сервера таким образом, чтобы в нём сохранялись только хэши данных. Это была непростая проблема, и мы, возможно, опубликуем дополнительный пост на эту тему! Без этого сохранённые клиентские данные заставляли брокеры сеансов использовать значительно больше памяти.
До этого все серверы синхронизации работали на инстансах, оптимизированных для памяти. С другой стороны, загрузчики Syncable выигрывают от использования типов узлов с большим количеством процессоров, таких как смешанные инстансы.
Примечательной особенностью такого запуска TS на GraalVM является то, что он использует значительно больше кэша кода, чем это обычно бывает в более стандартном приложении Scala/Java JVM.
Насколько нам известно, высокий полиморфизм делает компиляцию TS более затратной. Мы рассматриваем возможность перехода на мономорфные интерфейсы и использования отчетных полиморфных специализаций