Я поддерживаю это приложение под названием:
которое, по сути, транслирует в прямом эфире песню, которую я слушаю на Spotify, и позволяет пользователям присоединиться и синхронизироваться со мной.
Оно не передает буферы аудио, только название песни.
Архитектура довольно проста:
- опрашивать spotify api в фоновом режиме
- поддерживать состояние в памяти
- передавать изменения состояния клиенту через вебсокеты.
И это работает довольно хорошо, приемлемая задержка, никаких ограничений скорости (трудно, когда опрашивается только мой пользователь).
Но каждый месяц я получаю счет на 25$ от Digital Ocean.
Это происходит потому, что я использую их кластер k8s и балансировщик нагрузки. Что, как я знаю, является излишеством для чего-то подобного.
Вот почему я решил не только перейти на что-то более простое и дешевое, но и изучить, можем ли мы платить только за ту работу, которую делаем?
Пересмотр архитектуры
Во-первых, нам нужно перейти от режима «всегда включен» к режиму «по требованию».
Падает ли дерево в лесу только тогда, когда мы его наблюдаем? В нашем случае — да. Потому что тогда нам не нужно ничего рендерить/вычислять без необходимости.
Поэтому мы убираем слой сокетов и позволяем клиенту опрашивать нашу систему.
Теперь у нас есть два опроса:
- Клиент опрашивает нашу систему
- Система опрашивает Spotify
Ничего, если мы воспользуемся коротким путем и объединим эти два опроса? То есть, вызывать API Spotify только тогда, когда клиент вызывает нашу систему.
Теоретически это может сработать, но в этом случае количество запросов API к Spotify будет зависеть от количества клиентов. Один клиент — это нормально, может быть даже 10, но 100? 100,000?
Вот тогда у нас возникнут проблемы. Такие проблемы, как ограничение скорости и исчерпание квоты.
Кроме того, допустим, я слушаю песню в течение нескольких минут, имеет ли смысл вызывать API spotify 100 000k раз, чтобы проверить текущий трек?
Таким образом, мы видим, что объединение этих двух систем — не самая лучшая идея. Что же нам делать дальше?
Нам нужно опрашивать spotify, но не без необходимости, только когда у нас есть намерение, но мы не хотим тесно связываться с входящими запросами.
Слабое сопряжение?
Допустим, у нас есть таймер для опроса spotify, скажем, 10 секунд. Наша система будет опрашивать spotify каждую секунду в течение 10 секунд, а затем остановится. Пока не поступит запрос, таймер сбрасывается на 10 с, и цикл начинается снова.
Если в систему поступает 100 000 запросов, максимум, что может сделать запрос — это сбросить таймер, это не влияет на скорость опроса Spotify, просто удлиняет процесс.
Визуально это лучше всего описать здесь:

Защелка тайм-аута / iostreamer / Observable
Это негерметичное ведро, которое вытекает с постоянной скоростью, пока не будет сброшено каким-либо действием. В данном примере, нажатие кнопки «Проверить проект» здесь

Защелка тайм-аута
Это легко смоделировать с помощью простых таймеров. Например, мы запускаем setTimeout
и всякий раз, когда мы хотим сбросить, мы очищаем этот таймаут и начинаем снова.
Но мне не хотелось накладных расходов на создание и удаление таймеров только для сброса часов.
Поэтому я создал пользовательский планировщик, который тикает каждые 1 мс. С другой стороны, у нас есть latch
, по сути, просто объект со счетчиком.
Задача планировщика — разряжать защелки, уменьшая счетчики.
В этой вселенной планировщик создает время с помощью тиков, а защелки отражают это время с помощью связанных счетчиков.
Это довольно просто и понятно, но, как я уже упоминал, я хотел бы исследовать, можем ли мы платить только за фактическую работу, которую мы делаем?
Запуск планировщика на неопределенное время все равно означает, что мы находимся в режиме «всегда включен». Какой смысл создавать время, если нет сущностей, которые могли бы его наблюдать?
И это последнее, что нам нужно сделать, — остановить планировщик, если все защелки выполнены или отменены.
Если добавляется что-то новое или сбрасывается старая защелка, то снова запустить планировщик.
Код поддерживается здесь и также опубликован на npm.
iostreamer-X / timeout-latch
Простая защелка тайм-аута. Как обратное протекающее ведро.
timeout-latch
Простая защелка тайм-аута. Как обратное «дырявое ведро».
Это простой механизм на основе обратного вызова для получения уведомления о возникновении тайм-аута и сброса этого тайм-аута при необходимостиПосмотрите наглядную демонстрацию здесь.
Почему не обычный таймаут?
Обычные таймауты могут работать не так просто. Например, можно запустить тайм-аут и управлять состоянием вокруг него.
function run() { setTimeout( () => { // your callback }, 3000 ) } function reset() { // when you want to reset the timer run(); }
Но мы понимаем, что сброс работает только тогда, когда тайм-аут завершил свою работу. Если сброс произойдет до этого, то у нас просто будет два таймаута!
Для борьбы с этим мы можем очистить предыдущий таймер и начать заново. Это работает, но при этом приходится отменять и создавать новые таймеры. Кроме того, это таймер…
Почему это лучше?
Итак, мы уже выяснили, что в данном случае «по требованию» лучше, чем «всегда включен».
И в режиме «по требованию» тоже, мы хотели быть действительно «по требованию», то есть расходовать ресурсы только при необходимости.
С текущей настройкой, особенно с timeout-latch, мы находимся в состоянии, когда ничего не выполняется без необходимости, и останавливается, если намерение выполнить больше не возникает.
Это свойство чрезвычайно полезно, если мы посмотрим на бессерверные или граничные функции.
Это модель, в которой вы платите за все, что выполняете.
Даже за простые приложения на Digital Ocean придется заплатить минимум 5 долларов.
Но для платформы, которая забирает у вас «сервер», можно по-настоящему использовать взрывной характер работы.
И это служит примером того, как мы трансформировали опрос (очень непрерывный) в то, что является взрывным и «по требованию».
И на этом я завершаю процесс сокращения расходов, то, что вы должны делать в этой экономике 😁.