Я обожаю читать на хабре статьи про то, как устроены системы больших интернет-компаний. Кластеры SQL-серверов, монг и редисов. Тут у нас кластер ELK собирает трейсинг, там – сборка логов, здесь балансер выдает входящим запросам traceID и можно отслеживать, как запрос ходит по всем нашим микросервисам. Класс. Но, допустим, у вас совсем маленький проект и вы можете себе позволить лишь VPS минимальной конфигурации. Реально ли на ней сделать мониторинг не хуже, чем у больших проектов? Я решил – надо попробовать.
Создаем VPS
Сразу оговорюсь, что я ни разу не devops и не особо глубоко разбираюсь в Linux, поэтому, если что-то сделал неправильно, или у вас есть идеи, как можно было сделать то, что я делаю в этой статье проще и лучше – пишите в комментариях, буду рад любым вашим советам и замечаниям!
Для экспериментов я создал на Маклауде VPS следующей конфигурации: 1 CPU, 1 Гб RAM и 20 Гб диск.
Для удобства я загрузил свой SSH ключ, и мог заходить в консоль сразу после запуска сервера. Также по умолчанию включено резервное копирование, я его отключил, так как в целях эксперимента мне оно было не нужно. Далее требовалось выбрать ОС. Для этого хотелось понять, какие ресурсы будут доступны на VPS сразу после создания. Меня интересовала свободная память и место на диске. Для этого, в панели управления можно инициировать переустановку ОС. Я поочередно установил доступные ОС и для каждой посмотрел, какие параметры она дает на старте:
CentOS 8:
[root@v54405 ~]# df
Filesystem 1K-blocks Used Available Use% Mounted on
devtmpfs 406744 0 406744 0% /dev
tmpfs 420480 0 420480 0% /dev/shm
tmpfs 420480 5636 414844 2% /run
tmpfs 420480 0 420480 0% /sys/fs/cgroup
/dev/vda1 20582864 1395760 18300472 8% /
tmpfs 84096 0 84096 0% /run/user/0
[root@v54405 ~]# free
total used free shared buff/cache available
Mem: 840960 106420 525884 5632 208656 600868
Swap: 0 0 0
Debian 10
root@v54405:~# df
Filesystem 1K-blocks Used Available Use% Mounted on
udev 490584 0 490584 0% /dev
tmpfs 101092 1608 99484 2% /run
/dev/vda1 20608592 1001560 18736224 6% /
tmpfs 505448 0 505448 0% /dev/shm
tmpfs 5120 0 5120 0% /run/lock
tmpfs 505448 0 505448 0% /sys/fs/cgroup
tmpfs 101088 0 101088 0% /run/user/0
root@v54405:~# free
total used free shared buff/cache available
Mem: 1010900 43992 903260 1608 63648 862952
Swap: 0 0 0
Ubuntu 20.04
root@v54405:~# df
Filesystem 1K-blocks Used Available Use% Mounted on
udev 473920 0 473920 0% /dev
tmpfs 100480 592 99888 1% /run
/dev/vda1 20575824 1931420 17757864 10% /
tmpfs 502396 0 502396 0% /dev/shm
tmpfs 5120 0 5120 0% /run/lock
tmpfs 502396 0 502396 0% /sys/fs/cgroup
tmpfs 100476 0 100476 0% /run/user/0
root@v54405:~# free
total used free shared buff/cache available
Mem: 1004796 65800 606824 592 332172 799692
Swap: 142288 0 142288
Итак, в CentOS не доложили оперативной памяти (кстати почему – хороший вопрос сервису), а Убунту занял на гигабайт больше места на диске. Так что я остановил свой выбор на Debian 10.
Для начала обновим систему:
apt-get update
apt-get upgrade
Также установим sudo
apt-get install sudo
Для того, чтобы реализовать мою задумку первым делом я установил докер по инструкции с официального сайта.
Проверяем, что докер установлен
# docker -v
Docker version 20.10.6, build 370c289
Также понадобится docker-compose. Процесс установки можно посмотреть тут.
Проверим, что докер установился:
# docker-compose -v
docker-compose version 1.29.1, build c34c88b2
Итак, все приготовления выполнены, посмотрим, сколько места осталось на диске:
Filesystem 1K-blocks Used Available Use% Mounted on
udev 490584 0 490584 0% /dev
tmpfs 101092 2892 98200 3% /run
/dev/vda1 20608592 1781756 17956028 10% /
tmpfs 505448 0 505448 0% /dev/shm
tmpfs 5120 0 5120 0% /run/lock
tmpfs 505448 0 505448 0% /sys/fs/cgroup
tmpfs 101088 0 101088 0% /run/user/0
Запускаем проект
Для эксперимента я написал на NestJS небольшой веб-сервис, который работает с изображениями. Он позволяет загружать изображения на сервер, извлекает из них метаданные, записывает их в MongoDB, а информация о сохраненных изображениях пишется в Postgres. Для каждого загруженного изображения можно получить метаданные и скачать само изображение. Изображения, к которым не обращались более 10 минут удаляются с сервера при помощи функции очистки, которая запускается раз в минуту.
Исходный код проекта на github.
Я клонировал его на сервер при помощи команды:
git clone https://github.com/debagger/observable-backend.git
Чтобы было удобно разворачивать сервис на сервере я написал файл
docker-compose.nomon.yml
следующего содержания:version: "3.9"
volumes:
imagesdata:
grafanadata:
postgresdata:
mongodata:
tempodata:
services:
backend:
image: node:lts
volumes:
- ./backend:/home/backend
- imagesdata:/images
working_dir: /home/backend
environment:
OT_TRACING_ENABLED: "false"
PROM_METRICS_ENABLE: "false"
ports:
- 3000:3000
entrypoint: ["/bin/sh"]
command: ["prod.sh"]
restart: always
db:
image: postgres
restart: always
expose:
- "5432"
volumes:
- postgresdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: password
POSTGRES_USER: images
adminer:
image: adminer
restart: always
ports:
- 8080:8080
mongo:
image: mongo
restart: always
volumes:
- mongodata:/data/db
mongo-express:
image: mongo-express
restart: always
ports:
- 8081:8081
Для запуска проекта переходим в его директорию
cd observable-backend
И запускаем:
docker-compose -f docker-compose.nomon.yml up -d
Сервису понадобится некоторое время чтобы стартовать, я настроил его таким образом, чтобы он при запуске автоматически загружал зависимости и собирался.
После запуска можно проверить что он работает в браузере по ссылке
http://<ip сервера>:3000/
Должна вывестись строка Hello World!
Для того, чтобы испытывать производительность сервиса при помощи библиотеки autocannon я написал нагрузочный тест. Он находится в том же репозитории, в директории autocannon. Его надо запускать на машине с установленным node.js предварительно установив адрес сервера, где запущен проект в .env файле.
После запуска двухминутного теста я получил следующий результат:
В процессе теста я мог наблюдать за поведением системы при помощи стандартной команды linux —
top
, а также docker stats
. Помимо этого можно смотреть логи, при помощи команды docker logs
. Но этого недостаточно, хочется лучше понимать, что происходит с моим сервисом под нагрузкой. Поэтому следующим шагом я решил добавить к проекту сбор метрик.Настраиваем метрики
После недолгого гугления решений для сбора метрик я остановил свой выбор на связке Prometheus + Grafana.
Для использования этой связки я добавил в конфигурацию docker-compose следующее:
prometheus:
image: prom/prometheus
ports:
- 9090:9090
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
mongo-exporter:
image: bitnami/mongodb-exporter
ports:
- 9091:9091
command: [«--mongodb.uri=mongodb://mongo», «--web.listen-address=0.0.0.0:9091»]
pg-exporter:
image: bitnami/postgres-exporter
ports:
- 9092:9092
environment:
DATA_SOURCE_NAME: sslmode=disable user=images password=password host=db
PG_EXPORTER_WEB_LISTEN_ADDRESS: 0.0.0.0:9092
grafana:
image: grafana/grafana
ports:
- 3001:3000
volumes:
- grafanadata:/var/lib/grafana
Здесь минимальная конфигурация для запуска Prometheus и Grafana, а также экспортеры для метрик из Postgres и Mongo. Для Prometheus я написал конфиг prometheus.yml со следующим содержимым.
global:
scrape_interval: 10s
scrape_configs:
- job_name: 'nodejs'
honor_labels: true
static_configs:
- targets: ['backend:3000']
- job_name: «mongodb»
honor_labels: true
static_configs:
- targets: ['mongo-exporter:9091']
- job_name: «postgres»
scrape_timeout: 9s
honor_labels: true
static_configs:
- targets: ['pg-exporter:9092']
Чтобы собирать метрики из своего приложения я использовал библиотеку
express-prom-bundle
, которая позволяет собирать стандартные метрики и создавать свои собственные. Также я добавил в свой сервис переменную окружения PROM_METRICS_ENABLE
для того, чтобы можно было включать и отключать метрики из конфигурации контейнера. Если активировать данную функцию, метрики, собираемые приложением, будут доступны по адресу http://<ip сервера>:3000/metrics
. Я включил сбор дефолтных системных метрик, метрики по запросам, обрабатываемым сервером Express, а также несколько своих, которые позволяют контролировать скорость и количество загружаемых и скачиваемых изображений, количество хранимых изображений и занимаемое ими место на диске.
Получившуюся конфигурацию я сохранил под именем
docker-compose.metrics.yml
.Запустить эту конфигурацию можно командой
docker-compose -f docker-compose.metrics.yml up -d
После запуска можно зайти в интерфейс Grafana по адресу
http://<ip сервера>:3001/
Логин/пароль по умолчанию admin/admin.
Здесь в настройках я добавил источник данных Prometheus
После этого нам доступны все метрики, которые собирает Prometheus.
Для примера выведем графики загрузки процессора по всем сервисам:
Для своих целей я настроил такую панель:
Теперь мне стало гораздо проще разобраться, что происходит с сервисом.
ELK – неудача
Итак, я настроил метрики, и теперь мне хотелось заняться сбором логов. Я решил попробовать поднять для этих целей связку Elasticsearch + Logstash. Это просто первое, что пришло в голову, ибо читал много хорошего про эти инструменты. Особенно интересовало, удастся ли сделать сбор логов прямо с контейнеров, потому что у докера для этой целей есть встроенный плагин, позволяющий экспортировать вывод консоли сервисов в формате gelf, который поддерживает Logstash. Я добавил в docker-compose следующее
elasticsearch:
image: elasticsearch:7.12.1
environment:
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms250m -Xmx250m
ports:
- 9200:9200
- 9300:9300
logstash:
image: logstash:7.12.1
links:
- elasticsearch
volumes:
- ./logstash.conf:/etc/logstash/logstash.conf
command: logstash -f /etc/logstash/logstash.conf
ports:
- 12201:12201/udp
depends_on:
- elasticsearch
Также для начала настроил экспорт логов из Mongo. Для этого описание сервиса mongo в файле docker-compose я дополнил следующим образом:
mongo:
image: mongo
restart: always
logging:
driver: gelf
options:
gelf-address: "udp://localhost:12201"
Когда я запустил новую конфигурацию – я понял, что это конец. Ничего не работало. Сервер стал жутко тормозить, а kswapd0 периодически выходил на первое место по загрузке процессора, а свободная память была почти на нуле. Памяти для такой конфигурации явно не хватало.
Забегая вперед, когда я активировал файл подкачки, мне удалось запустить проект. Но все равно всё работало очень медленно, причем дольше всего запускался Logstash. Инструмент, задача которого всего лишь на всего грузить логи – стартовал минут 20. Хотя, когда он наконец запустился, работал как предполагалось, и я даже смог посмотреть в Grafana кусочек лога Mongo, так что, в принципе решение работало, просто для системы с таким объемом оперативной памяти оно не подходило, что не удивительно, ведь если погуглить, каковы минимальные требования для Elasticsearch, то ответ будет таким:
Я действительно этого не знал, поэтому немного приуныл, поскольку я хотел позже использовать Elasticsearch в качестве хранилища данных для jaeger, чтобы реализовать сбор трейсов приложения и поставить Kibana чтобы добить ELK стек. Но, как говорится, на нет и суда нет, поэтому я стал искать альтернативу.
Loki
И альтернатива нашлась! Искать, к слову, долго не пришлось, потому что в списке поддерживаемых источников данных Grafana обнаружился зверь под названием Loki. Это сборщик логов из той же эко-системы, что Prometheus и Grafana. Напомню, что моя идея была в том, чтобы писать логи из стандартного потока контейнеров. И для этого сценария тоже быстро нашлось решение. Оказалось, для докера есть плагин, который позволяет делать именно то, что мне надо – отправлять потоки стандартного вывода в Loki. Поставить его можно следующей командой:
# docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
Я добавил в конфигурацию docker-compose сервис loki:
loki:
image: grafana/loki:2.0.0
ports:
- «3100:3100»
command: -config.file=/etc/loki/local-config.yaml
Кроме этого, я добавил ко всем сервисам, логи с которых хотел собрать, следующую секцию:
logging:
driver: loki
options:
loki-url: «http://localhost:3100/loki/api/v1/push»
А к своему приложению добавил еще
loki-pipeline-stages: |
- json:
expressions:
output: msg
level: level
timestamp: time
pid: pid
hostname: hostname
context: context
traceID: traceID
Чтобы из лога, который у меня в формате json парсились важные поля.
Получившийся конфиг я сохранил под именем
docker-compose.metrics_logs.yml
.Теперь результат можно запустить при помощи команды
docker-compose -f docker-compose.metrics_logs.yml up -d
После запуска я понял, что что-то идет не так, потому что команда вылетела с сообщением Killed. Я попробовал еще раз – сервисы запустились частично. На третий раз все заработало, но когда я заглянул в top то увидел, что там периодически проскакивает
kswapd0
, а это значило, что системе жестко не хватало памяти.Простой выход – добавить в конфигурацию хотя бы гигабайт оперативной памяти, но по условиям эксперимента я хотел запустить все на VPS минимальной конфигурации. Поэтому я решил активировать swap-файл, который в системе по умолчанию отключен. Сколько было эпичных баттлов в комментариях про то, нужен ли файл подкачки в Linux, но у меня выбора особо не было. Заодно я решил проверить, как будет влиять наличие файла подкачки на производительность системы.
Включаем swap:
# sudo fallocate -l 1G /swapfile
# sudo chmod 600 /swapfile
# sudo mkswap /swapfile
# sudo swapon /swapfile
Проверяем про помощи команды
free
: total used free shared buff/cache available
Mem: 1010900 501760 202344 26500 306796 353952
Swap: 4194300 0 4194300
В системе появился файл подкачки размером 4Гб. Должно хватить!
Снова пытаемся запустить нашу систему:
# docker-compose -f docker-compose.metrics_logs.yml up -d
Все работает! Теперь в Grafana добавляем в качестве источника логов Loki
Идем в Explore и видим, что логи начали подгружаться.
Проверим, что стало с производительностью.
Раз все работает, осталось закрепить файл подкачки в системе. Для этого надо в файле /etc/fstab добавить строку
/swapfile swap swap defaults 0 0
После этого файл подкачки останется в системе даже после перезагрузки.
Добавляем сбор трейсов при помощи Tempo
Для полного счастья мне нужна была система сбора трейсов. Чтобы не мудрить, раз уж так вышло, что я использую стек Grafana, можно добавить в качестве сборщика данных еще один инструмент от Grafana Lab – сервер для сбора трейсов Tempo. Он из коробки поддерживается Grafana, поэтому попробуем добавить его в систему.
Для того, чтобы приложение стало генерировать трейсы, его надо специальным образом инструментировать. Для этого есть замечательный проект под названием OpenTelemetry, который развивает систему спецификаций и библиотек для реализации трейсинга под различные платформы и системы. В нем есть готовые библиотеки для автоматической инструментации Node.js и сервера express.js, который работает под капотом у nest.js. Их я и добавил в свой проект.
Tempo может принимать трейсы про всем распространённым протоколам. Я выбрал протокол Jagger Trift binary – простой двоичный формат, передаваемый по UDP. Также, как и в случае с метриками, я в своем приложение я добавил переменную окружения
OT_TRACING_ENABLED
, которая, если ее установить в true
включает в приложении телеметрию.Для запуска Tempo я добавил в файл конфигурации docker-compose следующее:
tempo:
image: grafana/tempo:latest
command: [«-config.file=/etc/tempo.yaml»]
volumes:
- ./tempo-local.yaml:/etc/tempo.yaml
- tempodata:/tmp/tempo
ports:
- «6832/udp» # Jaeger - Thrift Binary
и сохранил его под названием
docker-compose.metrics_logs_tempo.yml
Для настройки Tempo я создал файл конфигурации tempo-local.yaml (на самом деле просто скопировал из репозитория Tempo подходящий и немного поправил). Запустим его командой
docker-compose -f docker-compose.metrics_logs_tempo.yml up -d
Теперь осталось в Grafana настроить источник данных:
Чтобы было удобно переходить к просмотру трейсов из логов надо настроить источник данных Loki:
После такой настройки рядом с полем traceID появится ссылка:
По этой ссылке будет открываться окно с данным трейсом:
Испытываем производительность нашего сервиса.
Здесь уже видно заметное падение производительности сервиса, но надо понимать, что эта плата за детальную телеметрию.
Дополнение: уже когда я прогнал нагрузочные тесты, результаты которых приведены ниже и дописывал статью, изучая документацию Jaeger я выяснил, что он может использовать для хранения данных локальное хранилище на основе key-value базы данных Badger, и, таким образом, может работать без Elasticsearch. Я добавил в репозиторий файл конфигурации для docker-compose где вместо tempo используется jaeger (
docker-compose.metrics_logs_jaeger.yml
), но не проводил всего набора тестов. Я запустил тест производительности только на базовой конфигурации, и в этом режиме получилось 19,92 запроса в секунду, что несколько больше по сравнению с вариантом, где используется tempo — 18,84. В отличии от tempo, который позволяет искать трейсы только по traceID, jeaeger дает возможность поиска по различным параметрам и у него есть собственный, достаточно удобный интерфейс для просмотра трейсов.
Результаты тестов
Итак, мне удалость запустить все необходимые компоненты мониторинга и телеметрии. Осталось понять, насколько использование различных компонентов влияет на производительность системы.
Для каждого из перечисленных выше вариантов я запускал нагрузочный тест продолжительностью 20 минут. Для того, чтобы задействовать все компоненты системы, включая сетевой интерфейс я запускал тест autocannon со своей VPS размещённой у другого провайдера, предварительно проверив скорость соединения при помощи iperf – она составила 90 Мбит/сек. Так же между запусками тестов я дожидался, пока отработает функция удаления старых изображений, полностью удалив загруженные в предыдущем тесте файлы с диска и информацию из баз данных.
Результаты я свел в таблицу. Для оценки производительности я решил ориентироваться на число запросов в секунду, которое может обрабатывать система. Конечно, есть куча других метрик, но именно эта наиболее наглядно, на мой взгляд, показывает общее влияние различных факторов на производительность системы. Вот что получилось.
Запросов в секунду | Снижение производительности | |
Без мониторинга | 28,07 | 100% |
Prometheus | 27,19 | 97% |
Prometheus+Loki | 25,47 | 91% |
Prometheus+Loki+Tempo | 18,84 | 67% |
Выходит, что только использование трейсинга приводит к значительным потерям производительности, а в случае со сбором метрик и логов потери составляют менее 10%.
Добавляем ядра и память
Также я решил посмотреть, как будет влиять на производительность сервиса увеличение объема памяти и количества ядер процессора. Macloud.ru позволяет менять параметры тарифа и я решил посмотреть как работает эта функция. Первым делом я добавил еще 1 Гб оперативной памяти.
После нажатия кнопки «Сменить тариф» сервер перезагрузился и вот что получилось:
total used free shared buff/cache available
Mem: 2043092 309876 1190416 14284 542800 1576804
Swap: 4194300 0 4194300
Все правильно. Теперь можно отключить файл подкачки.
swapoff /swapfile
Посмотрим, что покажут тесты:
Запросов в секунду | Снижение производительности | |
Без мониторинга | 27,52 | 100% |
Prometheus | 24,78 | 90% |
Prometheus+Loki | 21,58 | 78% |
Prometheus+Loki+Tempo | 21,44 | 78% |
Почему-то производительность в целом стала немного ниже, но не так сильно зависит от включения функций мониторинга. Я предположил, что может быть дело все-таки в файле подкачки и включил его обратно. Вот что получилось:
Запросов в секунду | Снижение производительности | |
Без мониторинга | 29,64 | 100% |
Prometheus | 26,97 | 91% |
Prometheus+Loki | 25,7 | 87% |
Prometheus+Loki+Tempo | 22,95 | 77% |
Выводы такие – добавление памяти улучшает производительность системы, наличие файла подкачки также влияет положительно. Почему так происходит, требует более детального изучения, могу лишь предположить, что менеджер памяти при наличии файла подкачки имеет возможность вытеснить на диск малоиспользуемые данные чтобы дать больше места активным приложениям. Я оставил файл подкачки включенным несмотря на то, что памяти в принципе хватало для работы системы и без него.
После этого мне стало интересно – а как повлияет на производительность добавление второго ядра CPU? Сказано-сделано:
После добавления второго ядра я погнал всё те же тесты и вот какой результат получился.
Запросов в секунду | Снижение производительности | |
Без мониторинга | 49,05 | 100% |
Prometheus | 44,52 | 91% |
Prometheus+Loki | 45,64 | 93% |
Prometheus+Loki+Tempo | 40,34 | 82% |
Производительность увеличилась в 1,75 раза если сравнивать с базовым вариантом. Это хорошо, ведь если нагрузка на сервер будет расти, я могу просто докупить второе ядро, когда в этом появится необходимость.
Дальше я экспериментировать не стал, мне понятно, что, если продолжить добавлять память и процессорные ядра, производительность системы не будет расти так заметно. Чтобы обеспечить прирост производительности в этом случае нужно будет организовать запуск приложения в режиме кластера и внести еще ряд архитектурных изменений.
Выводы
Начиная этот эксперимент, я задался целью проверить, возможно ли на VPS c очень ограниченными ресурсами (я знаю, что можно найти предложения с еще более скромными параметрами, но 1 CPU + 1 Гб RAM – это доступный минимум у большей части провайдеров) запустить полноценную систему мониторинга для приложения. Как видите, это оказалось вполне возможно. Конечно, не все инструменты, которые используют крупные компании, применимы, но вполне можно найти такой набор, который позволит организовать мониторинг вашей системы не сильно влияя на ее производительность.
Также в ходе эксперимента я смог ответить для себя на ряд вопросов:
Стоит ли заморачиваться с настройкой мониторинга для совсем небольшого проекта?
Я считаю, что да, стоит. Мониторинг помогает понять, как ведет себя ваш проект в рабочей среде и дает информацию для дальнейшего улучшения качества кода. Я в процессе данного эксперимента, благодаря изучению данных мониторинга я смог заметить ряд недостатков в своем приложении, которые мне бы вряд ли удалось выявить другим способом.
Нужно ли делать нагрузочное тестирование?
Однозначно да. Во-первых, это позволяет найти узкие места в вашем приложении. Во-вторых, позволяет оценить, с какой нагрузкой сможет справиться система. Ну и в-третьих, имея настроенный мониторинг очень интересно наблюдать как система ведет себя под нагрузкой, это дает очень много пищи для размышлений и позволяет улучшить понимание того, как взаимодействуют компоненты системы.
Сложно ли настроить мониторинг
Ох, хотел бы я сказать, что это просто, но нет. Если делаешь это впервые, скорее всего придется вдоволь походить по граблям. Тут нет единого рецепта, как построить работающую систему, которая будет делать то, что вам нужно. Часто приходится собирать необходимую информацию по крупицам, и действовать интуитивно, потому что какие-то важные для тебя моменты не озвучиваются в документации. Если вы решите пойти этим путем, надеюсь, что данная статья и прилагающийся репозиторий поможет вам пройти его быстрее, чем мне.
А почему не облачные решения?
Мне просто спокойней платить фиксированную сумму и иметь в своем распоряжении все ресурсы, которые дает VPS. Истории, как разработчик что-то сделал неправильно и получил многотысячный счет – пугают. Хотя возможно, что в облаке все, что я описываю в этой статье настраивается за пару кликов мышкой, и тогда это, конечно, огромный плюс.
В заключении хотелось бы сказать – даже если у вас небольшой проект, и еще нет мониторинга, имеет смысл задуматься о его внедрении. Если вы опасаетесь, что это сложно сделать, или что у вашей системы недостаточно ресурсов для его реализации – можете использовать в качестве отправной точки материалы данной статьи и пример реализации, который я разместил в репозитории.
Репозиторий можно посмотреть по этому адресу: github.com/debagger/observable-backend
Облачные серверы от Маклауд быстрые и безопасные.
Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!