Как боты положили мне Gitea и сожрали 92% RAM на VPS
В какой-то момент я обнаружил, что мой сервер, на котором запущена Gitea, стал потреблять много ресурсов. Захожу, смотрю free -h — а там:
Mem: 7.8Gi 7.2Gi used 368Mi free
Swap: 0B 0B 0B
368 мегабайт свободной памяти, свопа нет. Одна нагрузка — и OOM-killer начнёт убивать процессы. Полез разбираться.
Кто ест память?
docker stats --no-stream| Контейнер | RAM | CPU |
|---|---|---|
| nexus | 2.44 GiB | 0.2% |
| gitea | 2.34 GiB | 173% |
| всё остальное | ~2.4 GiB | — |
Nexus — JVM, 2.4 гига для него нормально, если не ограничивать heap. Но Gitea на 2.3 GiB и 173% CPU — это ненормально. Это Go-приложение, оно столько не должно есть.
Что происходит?
Смотрю логи Gitea:
GET /OpenSourceArk/mkdocs-material/compare/7.1.0...7.3.5 — 5148ms
GET /OpenSourceArk/mkdocs-material/compare/8.0.0b1...9.0.4 — 5897ms
GET /OpenSourceArk/mkdocs-material/commits/commit/... — 190ms
GET /OpenSourceArk/mkdocs-material/blame/commit/...
Десятки разных IP из Вьетнама, Бразилии, Нигерии. Каждую секунду — новый запрос на compare, blame, commits. Это самые тяжёлые операции в git — сравнение деревьев между коммитами. Каждый такой запрос — 3-6 секунд CPU.
Мои публичные репозитории краулят боты. Причём не просто главную страницу, а именно тяжёлые эндпоинты.
Внутри контейнера Gitea активно выполняла множество git-процессов: read-tree, rev-list, cat-file, check-attr, batch-check. Всё это — следствие обработки запросов к compare и blame.
Что пробовал сначала
Отключение фоновых задач
Первым делом я заподозрил фоновые задачи Gitea — у меня было много зеркал с GitHub. Проверить можно через API:
curl -X GET "http://localhost:3000/api/v1/admin/cron" \
-H "Authorization: token YOUR_ACCESS_TOKEN"Отключил все задачи кроме критичных. На первый взгляд помогло, но через час Gitea снова потребляла 41% CPU и 14% RAM — при том, что пользователей не было.
Самодиагностика
Провёл автоматическую диагностику Gitea — все проверки прошли успешно. Проблема была не в самой Gitea.
Решение: защита от ботов
1. robots.txt
Первое и самое простое — попросить честных ботов не ходить куда не надо:
User-agent: *
Disallow: /*/compare/
Disallow: /*/blame/
Disallow: /*/commits/
Disallow: /*/raw/
Disallow: /*/rss/
Отдаю через Nginx location до proxy_pass — запрос даже не попадает в Gitea:
location = /robots.txt {
alias /var/www/gitea/robots.txt;
access_log off;
}2. Rate limiting в Nginx
robots.txt — это просьба. Rate limiting — это закон.
limit_req_zone $binary_remote_addr zone=gitea:10m rate=5r/s;
# Тяжёлые эндпоинты — жёсткий лимит
location ~* /(compare|blame|commits|raw|rss)/ {
limit_req zone=gitea burst=3 nodelay;
limit_req_status 429;
proxy_pass http://gitea:3000;
}
# Остальное — мягче
location / {
limit_req zone=gitea burst=15 nodelay;
limit_req_status 429;
proxy_pass http://gitea:3000;
}5 запросов в секунду на IP. Для тяжёлых страниц — burst всего 3 (4 запроса залпом и жди). Для обычных — burst 15, чтобы легитимные пользователи не страдали.
3. Memory limits в Docker
Даже если боты пролезут — контейнеры не сожрут всю память:
gitea:
deploy:
resources:
limits:
memory: 2G
nexus:
deploy:
resources:
limits:
memory: 1536M
environment:
- INSTALL4J_ADD_VM_PARAMS=-Xms512m -Xmx1g -XX:MaxDirectMemorySize=256mДля Nexus ещё ограничил JVM heap до 1 гига — по дефолту JVM забирает столько памяти, сколько может.
4. Swap как страховка
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstabSwap на VPS — не для постоянного использования, а чтобы при пиковой нагрузке система не убивала процессы, а начинала свопить.
Результат
| Метрика | До | После |
|---|---|---|
| RAM used | 7.2 GiB | 3.5 GiB |
| RAM available | 578 MiB | 4.2 GiB |
| Gitea CPU | 173% | 26% |
| Gitea RAM | 2.34 GiB | 303 MiB |
| Nexus RAM | 2.44 GiB | 832 MiB |
С 578 мегабайт свободных до 4.2 гигабайта. Gitea с 173% CPU упал до 26%.
Выводы
-
Публичные git-репозитории краулят. Если у вас self-hosted Gitea/Gitlab/Gogs с публичными репами — проверьте логи. Compare и blame — это дорогие операции.
-
robots.txt не спасает один. Агрессивные боты его игнорируют. Rate limiting на уровне reverse proxy — обязательно.
-
Memory limits в Docker — не опция, а необходимость. Без них один контейнер может сожрать всю память на хосте.
-
JVM-приложения без -Xmx — классическая ловушка. Nexus, Jenkins, SonarQube — все они по дефолту заберут 25-50% RAM.
-
Swap на VPS. Многие хостеры не создают swap по умолчанию. 2 GiB swap file — 5 минут работы и страховка от OOM-kill.
Раунд 2: return 444 вместо rate limiting
Через год боты вернулись — на этот раз распределённый ботнет с тысячами уникальных IP. Каждый IP делает 1-2 запроса и уходит. Rate limiting и fail2ban оказались бесполезны: 431 match в fail2ban, 0 banned. Из 3.2 млн запросов только 13 получили 429.
Основная цель — src/commit/[hash], commits/commit/[hash], blame/commit/[hash], archive/[hash]. Нормальные пользователи ходят по веткам (/src/branch/main/), а не по хешам коммитов.
Решение — блокировка по паттерну URL с return 444 (Nginx закрывает соединение без ответа):
location ~* /(src|commits|blame|raw)/commit/[a-f0-9] {
return 444;
}
location ~* /archive/[a-f0-9] {
return 444;
}
location ~* /compare/ {
return 444;
}Также добавлена отдельная rate-limit зона для тяжёлых запросов:
limit_req_zone $binary_remote_addr zone=gitea_heavy:10m rate=1r/s;| Метрика | До | После |
|---|---|---|
| Gitea CPU | 136% | 12.7% |
| Gitea RAM | 2 GiB (лимит) | 306 MiB |
Не ломает: просмотр по веткам/тегам, git clone/push/pull, API, скачивание релизов.