Rust для Big Data: Обработка миллионов строк быстрее Python

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему Rust — новый король обработки больших данных?

Вы когда-нибудь ждали 20 минут, пока скрипт на Python обработает 10-гигабайтный CSV-файл? А потом ещё 10 минут, потому что память закончилась и система ушла в своп? Знакомо?

Мир больших данных долгое время принадлежал Python и его экосистеме: Pandas, NumPy, Dask. Но есть одна проблема — Python медленный. Очень медленный. И когда данные измеряются терабайтами, каждый лишний миллисекунда превращается в часы простоя.

Здесь на сцену выходит Rust. Системный язык, который даёт производительность C++ и безопасность памяти, но с синтаксисом, который (спойлер) вполне можно полюбить. И да, он идеально подходит для обработки больших данных.

В этой статье мы разберём, как Rust позволяет обрабатывать огромные объёмы данных быстрее, безопаснее и с меньшим потреблением памяти. Без магии — только код и замеры.

Rust vs Python для Big Data: что внутри?

Давайте сразу к цифрам. Вот простой тест: прочитать CSV-файл на 500 МБ, отфильтровать строки по условию и посчитать сумму.

ПараметрPython (Pandas)Rust (Polars)
Время выполнения12.4 секунды0.8 секунды
Пиковое потребление RAM2.1 ГБ380 МБ
Количество строк5 млн5 млн

В 15 раз быстрее и в 5 раз экономичнее по памяти. И это не предел. Rust даёт контроль над каждым байтом, над каждым аллоцированием. Он не прощает ошибок, но даёт скорость, которую Python никогда не догонит.

Инструментарий Rust для Big Data

Прежде чем писать код, давайте посмотрим, что есть в арсенале. Rust не стоит на месте — экосистема для работы с данными активно растёт.

Основные библиотеки

  • Polars — аналог Pandas, но на Rust. Поддерживает lazy-вычисления, параллельную обработку и работает с данными, которые не влезают в память.
  • Apache Arrow — колоночный формат данных, который позволяет обмениваться данными между разными языками без копирования.
  • DataFusion — SQL-движок на Rust, который умеет выполнять запросы прямо над Arrow-данными.
  • rayon — библиотека для параллельных итераций. Позволяет легко распараллелить любую обработку.
  • csv — быстрый парсер CSV-файлов с минимальными аллокациями.
  • serde — сериализация/десериализация. Обязательна для работы с JSON, YAML, TOML.

Мы будем использовать Polars и rayon, так как это де-факто стандарт для обработки больших данных на Rust.

Практический пример: обработка логов веб-сервера

Представьте, что у вас есть 100 миллионов записей логов Nginx. Нужно:

  1. Отфильтровать только ошибки 5xx
  2. Сгруппировать по IP-адресу
  3. Посчитать количество ошибок на каждый IP
  4. Вывести топ-10 злоумышленников

На Python с Pandas это займёт около 2-3 минут и сожрёт всю оперативку. На Rust — 15-20 секунд.

Структура данных

Допустим, каждый лог выглядит так:

192.168.1.1 - - [10/Oct/2023:13:55:36 +0000] "GET /api/data HTTP/1.1" 500 1234 "-" "Mozilla/5.0"

Нам нужно парсить строку, извлекать IP и код ответа. В Rust это делается через собственный структурный тип:

use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct LogEntry {
    ip: String,
    status_code: u16,
    method: String,
    path: String,
    user_agent: String,
}

Парсинг CSV с помощью Polars

Polars — это то, ради чего стоит попробовать Rust для аналитики данных. Он умеет читать CSV напрямую в DataFrame с минимальным потреблением памяти.

use polars::prelude::*;

fn main() -> Result<()> {
    // Lazy-чтение: данные не загружаются в память полностью
    let df = LazyCsvReader::new("logs.csv")
        .has_header(false)
        .with_separator(b' ')
        .finish()?
        .with_column(
            // Разбиваем строку на колонки по пробелам
            col("column_0").alias("ip"),
        )
        .filter(col("column_8").cast(&DataType::UInt16).gt(lit(499)))
        .group_by([col("ip")])
        .agg([col("ip").count().alias("error_count")])
        .sort(["error_count"], SortOptions::default())
        .limit(10)
        .collect()?;

    println!("{:?}", df);
    Ok(())
}

Обратите внимание на lazy-режим. Это ключевая фишка: Polars не загружает все данные в память, а строит план выполнения. Оптимизатор сам решает, какие колонки читать, какие фильтры применить раньше. Для обработки больших данных на Rust это must-have.

Параллельная обработка с rayon

Но что, если вам нужно сделать что-то, что не влезает в DataFrame? Например, сложная трансформация каждой строки. Здесь в игру вступает rayon.

use rayon::prelude::*;

fn process_logs(lines: Vec<String>) -> Vec<LogEntry> {
    lines
        .par_iter() // параллельный итератор
        .filter_map(|line| parse_line(line))
        .collect()
}

fn parse_line(line: &str) -> Option<LogEntry> {
    let parts: Vec<&str> = line.split(' ').collect();
    if parts.len() < 9 { return None; }
    
    let status: u16 = parts[8].parse().ok()?;
    if status < 500 { return None; }
    
    Some(LogEntry {
        ip: parts[0].to_string(),
        status_code: status,
        method: parts[5].trim_matches('"').to_string(),
        path: parts[6].to_string(),
        user_agent: parts[9..].join(" ").trim_matches('"').to_string(),
    })
}

par_iter() автоматически распределяет работу по всем ядрам процессора. На 8-ядерной машине вы получите ускорение в 6-7x почти без усилий. Попробуйте сделать то же самое на Python с multiprocessing — и вы поймёте разницу.

Блоги

Рекомендаций по книгам