Заметки на полях

Черновики, шпаргалки и заметки, которые обычно теряются в телефоне. Пишу в основном для себя — вдруг пригодится ещё кому.

свежие сверху

systemd-таймеры: cron больше не нужен

#linux

Как переехать с cron на таймеры systemd и получить логи, зависимости и докрутку пропущенных запусков.

Раньше на каждой машине первым делом настраивал cron. Теперь просто пишу два юнита: таймеры пишут логи в journalctl, умеют зависимости через After= и главное — с Persistent=true догоняют пропущенный запуск, если машина была выключена.

Минимальный набор из двух файлов:

# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh

# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup nightly

[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target

Проверить расписание и заглянуть в логи:

systemctl list-timers
journalctl -u backup.service -n 50

Грабли: после правки юнитов не забывайте systemctl daemon-reload, а RandomizedDelaySec полезен, когда таймеров много и все они хотят стартовать ровно в 03:00.

Читаем EXPLAIN ANALYZE без страха

#postgres

Если планировщик ожидал 10 строк, а получил 100 000 — статистика устала. Куда смотреть в выводе.

EXPLAIN без ANALYZE показывает только прогноз планировщика. Добавляйте ANALYZE — получите фактическое время и число строк, а с BUFFERS ещё и статистику по страницам памяти.

Главное, куда смотрю: расхождение между ожидаемым rows и фактическими actual ... rows. Ожидали десять, получили сто тысяч — статистика устала, бегом ANALYZE таблицы;.

EXPLAIN (ANALYZE, BUFFERS)
SELECT u.email, count(o.id)
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.created_at > now() - interval '30 days'
GROUP BY u.email;

И не пугайтесь Seq Scan на маленькой таблице — прочитать её целиком иногда дешевле, чем ходить по индексу.

Multi-stage в Docker: с 1.2 ГБ до 90 МБ

#docker

Builder тащит тулчейн, финальный образ — только артефакт. Плюс пара слов про кэш слоёв.

Классическая ошибка — собирать образ одним этапом: вместе с приложением тащат компилятор, кэш сборщика и половину интернета. Лечится двумя stage: в первом всё собираем, во второй забираем только готовый артефакт.

FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app .

FROM alpine:3.20
COPY --from=build /out/app /usr/local/bin/app
ENTRYPOINT ["app"]

Отдельная история — кэш слоёв: COPY go.mod (или package.json) должен идти до COPY . ., иначе любой чих в коде пересобирает все зависимости с нуля.

rebase или merge: перестаём бояться git

#git

Rebase — для локальных веток, merge — для публичных. Правило одной строкой и грабли вокруг него.

Правило одной строкой: rebase — для локальных веток, которые ещё никто не видел; merge — для всего, что уже запушено. Переписывание публичной истории — верный способ разозлить всю команду.

Из полезного: git pull --rebase избавляет от коммитов вида «Merge branch 'main' of ...», а git rebase -i HEAD~5 — от «fix», «fix2» и «really fix» в истории.

Если ребейз пошёл не туда — git reflog помнит всё, паниковать рано.

slots=True: бесплатная память для датаклассов

#python

Один параметр — и сто тысяч инстансов весят примерно вдвое меньше. С замером и оговорками.

Один параметр — и обычная датаклассовая строка превращается в класс с __slots__: без __dict__ на каждый инстанс. На ста тысячах объектов разница по памяти получается примерно двукратной.

from dataclasses import dataclass

@dataclass(slots=True, frozen=True)
class Point:
    x: float
    y: float

frozen=True бонусом делает объект hashable — удобно для value-объектов и ключей словарей. Минус один: динамические атрибуты после slots не добавить, и некоторых это печалит ровно до первого профиля памяти.

Makefile для тех, кто его боится

#tools

Табы, .PHONY и автоматические переменные — медленно и по-взрослому. Спойлер: бояться там нечего.

Главный источник страха — табы: рецепты в Makefile отделяются строго табами, и пробел ломает всё молча. Второй источник — .PHONY: без него цель clean сломается, как только в каталоге появится файл с таким именем.

NAME = app
CC = cc

.PHONY: all clean

all: $(NAME)

$(NAME): main.c
	$(CC) -O2 -o $@ $<

clean:
	rm -f $(NAME)

$@ — цель, $< — первая зависимость. Большей половины Make в обычном проекте не нужно, если честно.

Минимальный .vimrc для серверов

#tools

Пятнадцать строк в ~/.vimrc, после которых vim на чужом сервере перестаёт быть лотереей.

Vim на сервере — почти всегда девственно чистый. Пятнадцать строк, после которых он перестаёт быть лотереей:

set number
set hlsearch incsearch
set ignorecase smartcase
set scrolloff=5
set noswapfile
syntax on

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

Regex, который я гуглю каждый раз

#tools

Именованные группы, lookahead и ленивые квантификаторы — три приёма вместо гугления каждой запятой.

Регулярки я не запоминаю — я их гуглю. Но три приёма стоит держать в голове: именованные группы, lookahead и ленивые квантификаторы.

(?P<year>\d{4})-(?P<month>\d{2})    # именованные группы
\d+(?= руб.)         # lookahead: число, за которым «руб.»
\d+(?! руб.)         # негативный lookahead: все остальные
<.+?>                # ленивый квантификатор — до первого «>»

И универсальное правило: если регулярка не влезает в одну строку осмысленно — это уже не регулярка, а повод написать десять строк кода.

О сайте

Это личный блокнот: сюда падают шпаргалки и черновики, которые обычно теряются в заметках на телефоне. Темы плавают вокруг linux, docker, postgres, python и всего, что сломалось на прошлой неделе.

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