💀⚰️🔥 Файловые дескрипторы: часть 2 — как всё идёт по пизде



Добро пожаловать во вторую часть нашего образовательного пиздеца! Теперь, когда мы знаем, что такое файловые дескрипторы, пора разобрать, как именно совет мудрецов умудряется превратить стабильную систему в утекающую развалину.

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


🤡🎪🎭 "Гениальное" решение консилиума мудрецов: "поднимем лимиты нахуй!"

Симпозиум высшей мысли решил проблему утечек увеличением ulimit! Охренительная идея! Вместо жалких 1024 теперь 65536. Блядь, теперь система сдохнет в 64 раза медленнее — какой научный прорыв!

Дворняга срёт в углу, подальше от миски. А здесь срут прямо в продакшн и называют это "инновационным решением".


# "Решение" от светочей мысли
echo "* soft nofile 65536" >> /etc/security/limits.conf
echo "* hard nofile 65536" >> /etc/security/limits.conf

# Что происходит на самом деле:
# Было: система падает через час с 1024 утечками
# Стало: система падает через 64 часа с 65536 утечками
# "Прогресс", блядь!

Поднять лимиты без фикса утечек — это как дать алкашу ящик водки и ждать, что он завяжет. Хуевая логика от академии наук.


💩🚽 Архитектурные шедевры: музей утечек и гениальных решений

Node.js: когда "асинхронность" встречается с "забыл закрыть, сука"

// Шедевр современной архитектуры
async function processFilesWithGenius(fileList) {
    for (const file of fileList) {
        const fd = fs.openSync(file, 'r');           // FD +1
        const req = http.request('http://api.com');  // FD +1  
        const dbSocket = net.connect(5432);          // FD +1
        
        req.write('data');
        req.end();
        
        // fs.closeSync(fd);        // Закрытие портит красоту!
        // req.destroy();           // Уничтожение ресурсов — не наш уровень!
        // dbSocket.destroy();      // База сама разберётся, да?
    }
}

// Результат: каждый файл = **+3 FD навечно** = система в жопе

PHP: когда FPM превращается в крематорий ресурсов

<?php
class MasterfulFileProcessor 
{
    private array $handles = [];    // Коллекция утечек
    
    public function processFiles(array $files): array 
    {
        foreach ($files as $file) {
            $handle = fopen($file, 'r');                     // FD +1
            $pdo = new PDO('mysql:host=localhost', 'u', 'p'); // FD +1
            $curl = curl_init('http://api.com');             // FD +1
            
            $this->handles[] = [$handle, $pdo, $curl];
            
            $data = fread($handle, 1024);
            $pdo->exec("INSERT INTO logs VALUES ('$data')");
            curl_exec($curl);
            
            // fclose($handle);    // Закрытие портит архитектуру!
            // $pdo = null;        // Освобождение нарушает flow!
            // curl_close($curl);  // cURL сам разберётся!
        }
        
        return []; // Возвращаем пустоту и тонну утечек
    }
    
    // Деструктор? Это не наш калибр!
}

// Результат: FPM воркеры дохнут от EMFILE через час

Python: asyncio и "оптимизация высшего уровня"

class AsyncArchitecturalMasterpiece:
    def __init__(self):
        self.sessions = []      # HTTP сессии (сокеты)
        self.connections = []   # Соединения с базой
        self.files = []         # Открытые файлы
        
    async def processUrls(self, urls):
        for url in urls:
            session = aiohttp.ClientSession(timeout=None)     # FD +1
            conn = psycopg2.connect("host=localhost")         # FD +1  
            f = await aiofiles.open(f'/tmp/{url}.txt', 'w')   # FD +1
            
            self.sessions.append(session)
            self.connections.append(conn)
            self.files.append(f)
            
            async with session.get(url) as resp:
                data = await resp.text()
                await f.write(data)
                conn.execute(f"INSERT INTO logs VALUES ('{data}')")
            
            # await session.close()    # Закрытие нарушает архитектуру!
            # conn.close()             # Освобождение портит стиль!
            # await f.close()          # Файлы сами закроются!

# Результат: каждый URL = **3 FD утекли навсегда**, event loop в агонии

💀⚰️🔥 Диагностика пиздеца: как понять, что всё просрано

Мониторинг утечек в реальном времени:

# Следим за ростом дескрипторов у процесса
watch -n 1 "lsof -p $PID | wc -l"

# Детальная разбивка по типам утечек
lsof -p $PID | awk '{print $5}' | sort | uniq -c | sort -nr
# Типичный вывод катастрофы:
#    1500 IPv4    # TCP сокеты не закрыты
#     800 REG     # Обычные файлы висят
#     300 IPv6    # И IPv6 тоже течёт
#      50 FIFO    # Даже pipes забыли закрыть

# Топ процессов-пожирателей дескрипторов
for pid in $(ps -eo pid --no-headers); do 
  count=$(lsof -p $pid 2>/dev/null | wc -l)
  if [ $count -gt 100 ]; then
    echo "$pid $count $(ps -p $pid -o comm=)"
  fi
done | sort -k2 -nr

# Системная статистика утечек
cat /proc/sys/fs/file-nr
# Формат: allocated_fds free_fds max_fds
# Пример: 50000 1000 2097152
# Означает: 50К открыто, 1К свободно, лимит 2М

Prometheus метрики для тех, кто не хочет сдохнуть ночью:

# node_exporter метрики
node_filefd_allocated_ratio     # Процент использования системных FD
node_filefd_maximum            # Системный лимит

# process_exporter метрики  
process_open_fds{job="myapp"}  # Открытые дескрипторы процесса
process_max_fds{job="myapp"}   # Лимит процесса

# Алерты для выживания
groups:
- name: fd_leaks
  rules:
  - alert: ProcessFDUsageHigh
    expr: process_open_fds / process_max_fds > 0.8
    for: 5m
    labels:
      severity: "критический_пиздец"
      
  - alert: ProcessFDLeak  
    expr: increase(process_open_fds[10m]) > 100
    for: 2m
    labels:
      severity: "утечка_обнаружена"
      
  - alert: SystemFDExhaustion
    expr: node_filefd_allocated_ratio > 0.9
    for: 1m
    labels:
      severity: "система_сдохнет"

🤡🎪🎭 Таблица переводов с корпоративного пиздежа на техническую правду

Что пиздят мудрецы в стендапахЧто происходит в /proc/PID/fd/Техническая сутьКоличество утекших FD"Небольшой фикс"Переписать половину IO-подсистемыРефакторинг connection pooling+500-1000 FD"Как у Netflix"1 senior и 2 джунаНет экспертизы по производительности+2000-5000 FD"Временное решение"Костыль проживёт 5 летТехнический долг накапливается+100 FD в день"Добавим кеширование"Ещё один источник утечекНовые соединения без контроля+1000-3000 FD"Микросервисы"50 сервисов, каждый течётDistributed monolith с утечками+10000+ FD"Оптимизируем позже"Никогда не оптимизируемZero capacity planningБесконечность

💩🚽 Классические паттерны утечек

1. Database Connection Massacre

def get_user_data(user_id):
    conn = psycopg2.connect(DATABASE_URL)  # FD +1
    result = conn.execute("SELECT * FROM users WHERE id = %s", (user_id,))
    # conn.close()  # Забыли закрыть, естественно
    return result

# **1000 RPS** = 1000 соединений/сек = `EMFILE` через минуту

2. HTTP Client Apocalypse

async function callAPI(data) {
    const client = axios.create({ timeout: 0 });  // FD +1
    const response = await client.post('/api', data);
    // client.destroy(); // Уничтожение? Не слышали!
    return response.data;
}

// **100 вызовов/сек** = 100 FD/сек = система в жопе

3. File Handle Holocaust

public List<String> processFiles(List<String> paths) {
    for (String path : paths) {
        FileInputStream fis = new FileInputStream(path);  // FD +1
        String line = new BufferedReader(new InputStreamReader(fis)).readLine();
        // fis.close();  // try-with-resources? Не слышали!
    }
    return results;
}

4. Socket Leak Extravaganza

func processRequests(urls []string) {
    for _, url := range urls {
        go func(u string) {
            conn, _ := net.Dial("tcp", u)  // FD +1
            fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
            // conn.Close() // Закрытие? Это не Go-way!
        }(url)
    }
}

// 1000 URL = 1000 горутин = 1000 утекших соединений

💀⚰️🔥 Docker и Kubernetes: как усугубить пиздец в облаке

Docker-контейнеры дохнут как мухи

# docker-compose.yml от великих архитекторов
version: '3.8'
services:
  masterpiece-app:
    image: mybrilliantapp:latest
    deploy:
      resources:
        limits:
          memory: 512M        # Память ограничили
          cpus: '0.5'         # CPU ограничили
        # А про файловые дескрипторы забыли!
    ulimits:
      # nofile: 1024        # Эта строчка закомментирована
      nproc: 65535          # Процессы можно создавать сколько угодно
    environment:
      - NODE_ENV=production
      # Переменных для лимитов нет, конечно

Kubernetes: orchestrated disaster

apiVersion: v1
kind: Pod
metadata:
  name: leaky-masterpiece
  labels:
    app: brilliant-solution
spec:
  containers:
  - name: app-container
    image: mybrilliantapp:latest
    resources:
      limits:
        memory: "1Gi"
        cpu: "500m"
        # ephemeral-storage: "2Gi"  # Диск не ограничили
      requests:
        memory: "512Mi"
        cpu: "250m"
    # securityContext нет — значит дефолтные лимиты
    # А дефолтные лимиты = 1024 FD максимум
    env:
    - name: NODE_ENV
      value: "production"
    # Переменных для настройки лимитов нет

Helm charts от консультантов

# values.yaml — конфиг от экспертов
replicaCount: 10  # 10 реплик приложения-дыры

image:
  repository: mybrilliantapp
  tag: "latest"   # Тегов не используем, живём опасно

resources:
  limits:
    cpu: 500m
    memory: 1Gi
    # Лимиты файловых дескрипторов? Не слышали!
  requests:
    cpu: 250m
    memory: 512Mi

# securityContext не настроен
# ulimits не заданы
# initContainers нет (могли бы настроить лимиты)

service:
  type: ClusterIP
  port: 80

# monitoring отсутствует
# alerting не настроен  
# healthcheck примитивный

🔥 Логи как крик души умирающей системы

Что орёт система перед смертью:

# Kernel messages
[ERROR] kernel: pid 1234 (myapp): file descriptor table full
[ERROR] kernel: VFS: file-max limit 2097152 reached
[CRIT]  kernel: Too many open files in system

# Application logs  
[ERROR] myapp: connect EMFILE (Too many open files)
[ERROR] myapp: Error: ENFILE: file table overflow, open '/tmp/data.txt'
[FATAL] myapp: Cannot allocate memory for file descriptor
[ERROR] myapp: accept: too many open files
[WARN]  myapp: connection pool exhausted after 30s
[ERROR] myapp: dial tcp: socket: too many open files

# Database logs
[ERROR] postgres: FATAL: sorry, too many clients already
[ERROR] mysql: ERROR 1040 (08004): Too many connections  
[ERROR] redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk

# Web server logs
[ERROR] nginx: accept() failed (24: Too many open files)
[ERROR] apache: server reached MaxRequestWorkers setting
[CRIT]  haproxy: backend has no available servers!

# Container logs
[ERROR] dockerd: containerd: container failed to start: fork/exec: resource temporarily unavailable
[ERROR] kubelet: Failed to create pod sandbox: rpc error: code = Unknown
[WARN]  systemd: myapp.service: Failed with result 'exit-code'

📊 Математика пиздеца для технарей

Формула утечки дескрипторов:

Скорость_утечки = (Открытые_FD_за_сек - Закрытые_FD_за_сек) × Время_работы
Время_до_краха = (Лимит_FD - Текущие_FD) / Скорость_утечки
Стоимость_простоя = Утечка_FD² × Количество_пользователей × Стоимость_минуты_простоя

Экспоненциальный рост катастрофы:

# День 1: 10 утечек в час
echo "Скорость утечки: 10 FD/час"
echo "Время до краха: $((1024 / 10)) часов = 4 дня"

# День 7: 70 утечек в час (нагрузка выросла)
echo "Скорость утечки: 70 FD/час" 
echo "Время до краха: $((1024 / 70)) часов = 14 часов"

# День 30: 300 утечек в час (микросервисы добавили)
echo "Скорость утечки: 300 FD/час"
echo "Время до краха: $((1024 / 300)) часов = 3 часа"

# День 90: система мертва
echo "FD исчерпаны. Game over."

Расчёт стоимости говна:

def calculate_cost_of_stupidity():
    # Исходные данные
    avg_requests_per_sec = 1000
    fd_leak_per_request = 0.1  # 10% запросов течёт
    fd_limit = 1024
    cost_per_minute_downtime = 10000  # $10k в минуту
    
    # Расчёт
    leaks_per_second = avg_requests_per_sec * fd_leak_per_request
    time_to_crash_seconds = fd_limit / leaks_per_second
    time_to_crash_minutes = time_to_crash_seconds / 60
    
    # Частота крашей (если не перезапускаем)
    crashes_per_day = (24 * 60) / time_to_crash_minutes
    cost_per_day = crashes_per_day * cost_per_minute_downtime
    
    print(f"Утечки в секунду: {leaks_per_second}")
    print(f"Время до краха: {time_to_crash_minutes:.1f} минут")
    print(f"Крашей в день: {crashes_per_day:.1f}")
    print(f"Стоимость простоев в день: ${cost_per_day:,.0f}")

# calculate_cost_of_stupidity()
# Утечки в секунду: 100.0
# Время до краха: 0.2 минуты  
# Крашей в день: 7200.0
# Стоимость простоев в день: $72,000,000

🖕 Конкретные советы для тех, кто хочет выжить в этом пиздеце

Для разработчиков (которых ещё не съело выгорание):

1. Connection Pooling — не опция, а способ не сдохнуть

// НЕПРАВИЛЬНО (путь к EMFILE)
async function getUserData(userId) {
    const client = new Pool({ max: 1000 });  // FD +1000!
    const result = await client.query('SELECT * FROM users WHERE id = $1', [userId]);
    // await client.end(); // Забыли закрыть пул
    return result.rows[0];
}

// ПРАВИЛЬНО (один пул на приложение)
const globalPool = new Pool({
    max: 10,          // Разумный лимит
    idleTimeoutMillis: 30000,
});

async function getUserData(userId) {
    const client = await globalPool.connect();
    try {
        const result = await client.query('SELECT * FROM users WHERE id = $1', [userId]);
        return result.rows[0];
    } finally {
        client.release(); // Вернули в пул
    }
}

2. HTTP клиенты — переиспользуй или сдохни

# НЕПРАВИЛЬНО
async def call_api_wrong(url, data):
    async with httpx.AsyncClient() as client:  # FD +1 каждый раз
        response = await client.post(url, json=data)
        return response.json()

# ПРАВИЛЬНО
class APIClient:
    def __init__(self):
        self.client = httpx.AsyncClient(
            timeout=httpx.Timeout(30.0),
            limits=httpx.Limits(max_connections=100)
        )
    
    async def call_api(self, url, data):
        response = await self.client.post(url, json=data)
        return response.json()
    
    async def close(self):
        await self.client.aclose()

api_client = APIClient()  # Один клиент на приложение

3. Файлы — всегда используй контекстные менеджеры

// НЕПРАВИЛЬНО
func processFileBad(filename string) error {
    file, err := os.Open(filename)  // FD +1
    if err != nil {
        return err
    }
    
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    
    // file.Close() // ЗАБЫЛИ ЗАКРЫТЬ!
    return scanner.Err()
}

// ПРАВИЛЬНО (defer)
func processFileGood(filename string) error {
    file, err := os.Open(filename)  // FD +1
    if err != nil {
        return err
    }
    defer file.Close()  // Закроется автоматически
    
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    
    return scanner.Err()
}

4. Таймауты везде — никаких висячих соединений

// НЕПРАВИЛЬНО
async fn fetch_data_bad(url: &str) -> Result<String, Box<dyn std::error::Error>> {
    let client = Client::new();
    let response = client.get(url).send().await?;  // Без таймаута!
    let text = response.text().await?;
    Ok(text)
}

// ПРАВИЛЬНО  
async fn fetch_data_good(url: &str) -> Result<String, Box<dyn std::error::Error>> {
    let client = Client::builder()
        .timeout(Duration::from_secs(30))
        .build()?;
        
    let response = timeout(Duration::from_secs(30), client.get(url).send()).await??;
    let text = timeout(Duration::from_secs(10), response.text()).await??;
    
    Ok(text)
}

Для DevOps (которые заебались тушить пожары):

5. Мониторинг файловых дескрипторов — критически важно

# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'node'
    static_configs:
      - targets: ['localhost:9100']
    
  - job_name: 'process'
    static_configs:
      - targets: ['localhost:9256']
    scrape_interval: 5s  # Чаще для процессов

# alertmanager rules
groups:
- name: file_descriptors
  rules:
  - alert: ProcessFDUsageCritical
    expr: process_open_fds / process_max_fds > 0.9
    for: 1m
    labels:
      severity: critical
    annotations:
      summary: "Process {{ $labels.instance }} FD usage critical"
      description: "{{ $labels.instance }} using {{ $value | humanizePercentage }} of available file descriptors"
      
  - alert: ProcessFDLeak
    expr: increase(process_open_fds[5m]) > 50
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: "Possible FD leak in {{ $labels.instance }}"
      description: "{{ $labels.instance }} opened {{ $value }} FDs in last 5 minutes"
      
  - alert: SystemFDExhaustion  
    expr: node_filefd_allocated / node_filefd_maximum > 0.8
    for: 5m
    labels:
      severity: critical
    annotations:
      summary: "System FD exhaustion on {{ $labels.instance }}"
      description: "System using {{ $value | humanizePercentage }} of available FDs"

6. Правильные лимиты в контейнерах

# docker-compose.yml (ПРАВИЛЬНЫЙ)
version: '3.8'
services:
  app:
    image: myapp:latest
    ulimits:
      nofile:
        soft: 4096
        hard: 8192
      nproc:
        soft: 2048
        hard: 4096
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: '1.0'
        reservations:
          memory: 512M
          cpus: '0.5'
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
# kubernetes deployment (ПРАВИЛЬНЫЙ)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: app
        image: myapp:latest
        resources:
          limits:
            memory: "1Gi"
            cpu: "1000m"
            ephemeral-storage: "2Gi"
          requests:
            memory: "512Mi"
            cpu: "500m"
            ephemeral-storage: "1Gi"
        securityContext:
          runAsNonRoot: true
          runAsUser: 1000
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop:
            - ALL
        # Лимиты через initContainer
        lifecycle:
          postStart:
            exec:
              command:
              - /bin/sh
              - -c
              - |
                ulimit -n 4096
                ulimit -u 2048
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 3

7. Системные настройки для выживания

# /etc/systemd/system/myapp.service
[Service]
LimitNOFILE=8192
LimitNPROC=4096
MemoryMax=2G
CPUQuota=150%
# /etc/security/limits.conf
myapp    soft    nofile    8192
myapp    hard    nofile    16384

# /etc/sysctl.conf
fs.file-max = 2097152
net.core.somaxconn = 1024

💀⚰️🔥 Три ебучие истины от животных (потому что людям не доходит)

  1. Кот закапывает своё дерьмо — программисты оставляют открытые дескрипторы висеть навечно
  2. Собака приносит палку обратно хозяину — разработчики открывают ресурсы и забывают их возвращать системе
  3. Хомяк точно знает размер своих защёчных мешков — архитекторы понятия не имеют о лимитах своих систем

И самое главное: даже дворняга не срёт в своей конуре, а "senior" разработчики срут прямо в продакшн и называют это "техническим решением".


🚀 Финальный чек-лист выживания

Обязательно к выполнению:

  •  Аудит всех open() без соответствующих close()
  •  Connection pools с жёсткими лимитами и таймаутами
  •  Graceful shutdown для всех сервисов и ресурсов
  •  Мониторинг FD на всех уровнях: процесс, контейнер, система
  •  Алерты по утечкам и приближению к лимитам
  •  Нагрузочное тестирование с ограниченными ресурсами
  •  Code review с обязательным фокусом на управление ресурсами
  •  Документация по всем внешним соединениям и их лимитам
  •  Chaos engineering: регулярно душить лимиты и смотреть, где рвётся

Метрики для отслеживания:

# Процессы
process_open_fds / process_max_fds          # Загрузка FD по процессам
increase(process_open_fds[5m])              # Скорость роста FD
process_resident_memory_bytes               # Корреляция с памятью

# Система  
node_filefd_allocated / node_filefd_maximum # Системная загрузка FD
node_network_receive_packets_total          # Сетевая активность
node_disk_io_time_seconds_total             # Дисковая активность

# Приложение
http_requests_total                         # Корреляция с нагрузкой
database_connections_active                 # Активные соединения с БД
redis_connected_clients                     # Клиенты Redis

💀 Последние слова выгоревшего техлида

Для совета мудрецов: Заткнитесь и не лезьте в архитектурные решения. Нанимайте людей, которые знают разницу между soft и hard лимитами.

Для разработчиков: Валите, пока не поздно — жизнь одна, а legacy-код вечен. Или изучайте системное программирование и становитесь теми, кто фиксит пиздец, а не создаёт его.

Для DevOps: Автоматизируйте всё. Мониторьте всё. Алертьте на всё. И помните: если что-то может утечь — оно утечёт.


P.S. Если думаешь "это точно не про нас" — поздравляю, это именно про вас, блядь.

P.P.S. Пёс лает на воров — его хвалят. Инженер предупреждает об утечках — его увольняют за "негативное мышление".

P.P.P.S. Помните: каждый незакрытый дескриптор — это гвоздь в гроб вашей системы. И забивают эти гвозди обычно самые "опытные" разработчики.


#ЗакрывайДескрипторыИлиСдохни #МониторингВсегоИВся #ТехДолгЭтоДиагноз #EMFILE_Means_GameOver #SystemProgrammingMatters