В первом посте я рассказывал, как Raspberry Pi 5 стала домашним сервером для ботов, медиатеки и этого блога. С тех пор на ней поселились ещё несколько проектов, сейчас это 7 compose-стеков и 22 контейнера. Чтобы узнать, как она себя чувствует, приходилось заходить по SSH и запускать htop. Захотелось открыть страницу и сразу увидеть всё: температуру, вентилятор, нагрузку, память, сеть. И чтобы выглядело как терминал из фильма про хакеров.
Так появился pi-monitor. Он живёт на pi.ava-code.com, его можно открыть прямо сейчас в соседней вкладке.

Что там видно
Почти всё, до чего можно дотянуться без root:
- CPU: загрузка и частота каждого ядра, load average, переключения контекста и прерывания в секунду
- температуры: SoC, чип ввода-вывода RP1 и контроллер питания, плюс флаги троттлинга и недонапряжения
- вентилятор: обороты, PWM и текущая ступень
- питание: 14 шин PMIC с напряжением, током и мощностью, и всё, что прошивка узнала о блоке питания
- частоты и напряжения блоков чипа, а ещё сколько времени процессор провёл на каждой частоте
- GPU: загрузка V3D по очередям
- память: приложения, кэш, zram, page faults, swap
- диски, сеть, Wi-Fi и TCP: скорости, ошибки, ретрансмиты
- туннели Cloudflare: запросы и коды ответов для каждого сайта на малинке
- процессы и контейнеры, разложенные по стекам, с временем работы
Всё обновляется раз в секунду, на графиках последние пять минут.

Пока разглядывал цифры, узнал про свою малинку несколько вещей:
- Процессор почти не бывает на промежуточных частотах. С загрузки он провёл 56% времени на максимальных 2,4 ГГц и 41% на минимальных 1,5 ГГц, а на всё, что между ними, пришлось меньше 4%. Губернатор
ondemandработает по принципу «либо отдыхаем, либо сразу на полную». - zram сжимает выгруженную память почти в 7 раз: 216 МБ занимают в RAM 31 МБ.
- По данным PMIC, сама плата со всеми 22 контейнерами потребляет 2–5 Вт. Вентилятор при этом крутится на 5300 оборотах, это половина его мощности.
- Блок питания определяется как официальный на 27 Вт и предлагает четыре профиля USB-PD: 5 В/5 А, 9 В/3 А, 12 В/2,25 А и 15 В/1,8 А. Малинка, понятно, берёт пять вольт.
Как это устроено
Python без единой зависимости, только стандартная библиотека. Один поток раз в секунду читает /proc, /sys и cgroup v2, а раз в три секунды ещё и спрашивает vcgencmd, iw и метрики cloudflared. Готовый снимок уходит во все открытые вкладки через Server-Sent Events, поэтому нагрузка не зависит от числа зрителей: сто человек смотрят на один и тот же замер. Сам монитор занимает 3–5% одного ядра и около 30 МБ памяти.
Контейнер запускается в пространствах имён процессов, сети и cgroup хоста (pid: host, network_mode: host, cgroup: host), поэтому /proc и /sys/fs/cgroup внутри него описывают всю машину. При этом он работает от обычного пользователя, без capabilities и с файловой системой только для чтения. Менять ему ничего не нужно, только читать.
Откуда что берётся:
| Что | Откуда |
|---|---|
| загрузка CPU, процессы | /proc/stat, /proc/<pid>/stat |
| температуры, вентилятор | /sys/class/thermal, /sys/class/hwmon |
| шины питания, частоты, троттлинг | vcgencmd pmic_read_adc, measure_clock, get_throttled |
| блок питания | device tree, узел /chosen/power |
| GPU | gpu_stats драйвера V3D |
| контейнеры | cgroup system.slice/docker-*.scope |
| Wi-Fi | /proc/net/wireless и iw |
| туннели | Prometheus-метрики cloudflared |
Больше всего мне нравится, как сделаны контейнеры. Доступа к docker.sock у монитора нет: это фактически root на хосте, и отдавать его публичной страничке не хотелось. Поэтому CPU и I/O контейнера берутся из его cgroup, а стек, к которому он относится, монитор угадывает по точкам монтирования. Тома compose называются <проект>_<том>, а bind-маунты ведут в ~/Projects/<проект>/. Если нет ни того ни другого, контейнер сравнивается по сетевым маршрутам с соседями по стеку.
Что я сознательно не собираю
Страница публичная, поэтому на ней нет ничего, что помогло бы найти малинку в сети или навредить ей: ни IP-адресов, ни SSID и MAC, ни серийных номеров платы и SD-карты. У процессов показывается только имя программы, без аргументов командной строки: в аргументах иногда лежат токены и пароли.
Грабли
Docker прячет /sys/firmware. Внутри контейнера модель платы оказалась пустой строкой. /proc/device-tree — это ссылка в /sys/firmware/devicetree, а Docker этот каталог маскирует. Пришлось пробросить нужные файлы device tree по отдельности: модель платы и сведения о блоке питания.
Лимит памяти, которого нет. Я поставил контейнеру mem_limit: 128m, а Docker вежливо ответил: Your kernel does not support memory limit capabilities or the cgroup is not mounted. Limitation discarded. На Raspberry Pi контроллер памяти cgroup выключен прямо в командной строке ядра, там стоит cgroup_disable=memory. Монитору хватает 30 МБ, так что жить можно, но про любой контейнер на малинке это стоит помнить. Лимит CPU при этом работает.
Скобки в имени процесса. Строка /proc/<pid>/stat выглядит так: 42 (tmux: (server)) S .... Имя процесса стоит в скобках, и внутри него тоже бывают скобки. Сначала имя бралось между последней открывающей и последней закрывающей скобкой, и получалось server). Правильно брать от первой ( до последней ). Баг поймал тест ещё до того, как он попал на страницу.
Кэш Cloudflare. Cloudflare кэширует JS и CSS, но не HTML. После обновления страница могла прийти новая, а скрипт к ней из кэша, старый. Теперь в ссылках на скрипт и стили стоит хэш их содержимого, и сервер подставляет его в HTML при отдаче.
Деплой. Писал всё это, как и остальное здесь, вместе с Claude Code. С кодом, тестами и даже скриншотами он справился сам, а на запуске упёрся в собственные ограничения. Автоматический фильтр прав посчитал docker compose up деплоем в прод, а создание DNS-записи изменением домена, и не пропустил ни то ни другое. Тогда агент собрал всё в скрипт, чтобы я запустил его сам. Скрипт добавлял сайт в общий туннель блога, но конфиг того туннеля лежит в /etc/cloudflared и правится только через sudo, а при запуске через ! прямо в Claude Code sudo не может спросить пароль: там нет терминала. В итоге я явно разрешил агенту всё, и он сделал так же, как у двух других моих сайтов: отдельный именованный туннель и cloudflared контейнером прямо в стеке монитора. sudo не понадобился вообще, а стек получился самодостаточным: поднял compose, поднялся и сайт.
Боты

Самой неожиданной оказалась панель туннелей. Из ~100 тысяч запросов, которые туннель блога принял с августа, когда его последний раз перезапускали, 84 тысячи закончились ответом 404. Это не читатели ошиблись адресом, это сканеры. Я заглянул в логи nginx, вот самые частые адреса:
335 /.env
303 /wp-admin/install.php
241 /.env.local
192 /@fs/.env
189 /.env.production
157 /.env.development
115 /.git/config
Ищут забытые файлы с секретами, недоустановленный WordPress, открытый .git и dev-сервер Vite, который отдаёт файлы по /@fs/. У меня Django, .env и .git наружу не отдаются, так что пусть ищут. Но цифра отрезвляет: пять из шести запросов к блогу уходят в 404.
Что можно улучшить
Каждая открытая вкладка получает около 8,6 КБ в секунду, это примерно 30 МБ в час. Для компьютера ерунда, для телефона в роуминге уже нет. Большая часть снимка от секунды к секунде не меняется, так что ужать есть что.
Как и всё здесь, это вайбкод: я ставил задачи и проверял результат, писал Claude Code. Открывайте pi.ava-code.com и смотрите, чем малинка занята прямо сейчас. А если на графике туннеля появился всплеск, возможно, это вы.
ava-code
Comments
No comments yet. Be the first.
Sign in or register to leave a comment.