diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 81d41ca..1fdfcc0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -45,6 +45,6 @@ deploy: --wait --create-namespace --atomic - --timeout 2m + --timeout 3m --namespace "$NAMESPACE" -f "$HELM_CONFIG" diff --git a/content/ru/docker-envs.md b/content/ru/docker-envs.md index 79873a0..3740a59 100644 --- a/content/ru/docker-envs.md +++ b/content/ru/docker-envs.md @@ -37,7 +37,7 @@ deploy Запуск таких конфигураций выглядит следующим образом: ```bash -docker-compose \ +$ docker-compose \ -f "deploy/docker-compose.yml" \ -f "deploy/docker-compose.db.yml" \ -f "deploy/docker-compose.dev.yml" \ @@ -101,10 +101,10 @@ services: Теперь запустим всё это чудо. ```bash -d-test docker-compose \ - -f "./deploy/docker-compose.yml" \ - --project-directory "." \ - run --rm script +$ docker-compose \ + -f "./deploy/docker-compose.yml" \ + --project-directory "." \ + run --rm script ``` Вот что будет выведено на экран. @@ -129,11 +129,11 @@ services: Теперь добавим ещё один файл в нашу команду запуска. ```bash -d-test docker-compose \ - -f "deploy/docker-compose.yml" \ - -f "deploy/docker-compose.dev.yml" \ - --project-directory "." \ - run --rm script +$ docker-compose \ + -f "deploy/docker-compose.yml" \ + -f "deploy/docker-compose.dev.yml" \ + --project-directory "." \ + run --rm script ``` Вот что будет выведено на экран: diff --git a/content/ru/makefiles.md b/content/ru/makefiles.md index cfc5482..0806700 100644 --- a/content/ru/makefiles.md +++ b/content/ru/makefiles.md @@ -133,3 +133,7 @@ Hi! А что если я вот не хочу чтобы команда создавала и проверяла файлы? Для этого пишут `.PHONY: ${target}`. Например у нас так объявлен таргет `run` и, даже если файл с названием `run` будет присутствовать в директории цель не будет выполняться. + +
+ +До новых встреч. diff --git a/content/ru/project-start.md b/content/ru/project-start.md index 767bc3f..43b3482 100644 --- a/content/ru/project-start.md +++ b/content/ru/project-start.md @@ -232,15 +232,14 @@ $ poetry publish -u "user" -p "password" ```console $ poetry add \ -$ flake8 \ -$ black \ -$ isort \ -$ mypy \ -$ pre-commit \ -$ yesqa \ -$ autoflake \ -$ wemake-python-styleguide --dev ----> 100% + flake8 \ + black \ + isort \ + mypy \ + pre-commit \ + yesqa \ + autoflake \ + wemake-python-styleguide --dev ``` Теперь добавим конфигурационных файлов в корень проекта. @@ -581,3 +580,7 @@ $ ab_solver 1 2 ``` Если запаблишить проект, то у пользователя тоже установится ваша cli-программа. + +А на этом всё. + +До новых встреч. diff --git a/content/ru/python-speedup-with-rust.md b/content/ru/python-speedup-with-rust.md new file mode 100644 index 0000000..7c33532 --- /dev/null +++ b/content/ru/python-speedup-with-rust.md @@ -0,0 +1,479 @@ +--- +title: Ускоряем Python используя Rust. +description: Как встроить Rust в проект на Python. +position: 2 +category: 'Python' +--- + +# Описание проблемы + +Каким бы Python удобным не был, скоростью он похвастаться никак не может. +И для некоторых задач это достаточно критично. + +Например, не так давно у меня была задача достать много информации +из файлов логов. Проблема в том, что один лог-файл занимает от 3-4 ГБ. А файлов +таких много и информацию достать требуется быстро. Изначальный вариант на Python +был написан за минут 15-20, но скорость его работы занимала 30-40 минут на один файл. После переписывания одной функции на Rust, скрипт отработал за 1 минуту. + +Давайте напишем свой проект со встроенной функцией на Rust. + +
+ +Задача проекта будет следующей: + +Дан файл лога запросов на некоторый сервер. Требуется +найти количество запросов к определённому файлу и сумму переданных байт. + +Для демонстрации мы напишем 2 функции, одна будет использовать наивное решение задачи на питоне, +вторая будет выполнять парсинг на расте. + +Сам файл лог имеет следующую структуру: +``` +"$line_num" "-" "$method $url" "$current_time" "$bytes_sent" +``` + +где, +* $line_num - номер строчки; +* $method - HTTP метод; +* $url - URL до файла, содержащий один из сгенерированных идентификаторов; +* $current_time - текущее время (время выполнения запроса); +* $bytes_sent - сколько байт было отправлено. + +Данный формат логов отчасти подражает формату [G-Core labs](https://gcorelabs.com/support/articles/115000511685/). + +# Проект на Python + +Сначала мы напишем весь функционал на Python. +Для этого создадим проект `python-rust` по [гайду из блога](/project-start). + +Для создания CLI я буду использовать [typer](https://pypi.org/project/typer/). + +Весь код проекта доступен в [репозитории](https://github.com/s3rius/blog_examples/tree/master/python-rust). + +```bash +$ poetry add typer +``` + +## Генератор логов + +Для тестов напишем генератор лог-файла. + +Данный генератор должен работать следующим образом. +Я указываю сколько строк лог-файла я хочу увидеть, +сколько уникальных id файлов использовать и название файла, куда писать. + +Функция же генерирует лог в заданном формате. + +```python{}[python_rust/main.py] +import secrets +import time +import uuid +from pathlib import Path +from typing import Any + +import typer + +tpr = typer.Typer() + + +def quote(somthing: Any) -> str: + """ + Quote string. + + :param somthing: any string. + :return: quoted string. + """ + return f'"{somthing}"' + + +@tpr.command() +def generator( # noqa: WPS210 + output: Path, + lines: int = 2_000_000, # noqa: WPS303 + ids: int = 1000, +) -> None: + """ + Test log generator. + + :param ids: how many file id's to generate. + :param output: output file path. + :param lines: how many lines to write, defaults to 2_000_000 + """ + ids_pool = [uuid.uuid4().hex for _ in range(ids)] + + with open(output, "w") as out_file: + for line_num in range(lines): + item_id = secrets.choice(ids_pool) + prefix = secrets.token_hex(60) + url = f"GET /{prefix}/{item_id}.jpg" + current_time = int(time.time()) + bytes_sent = secrets.randbelow(800) # noqa: WPS432 + line = [ + quote(line_num), + quote("-"), + quote(url), + quote(current_time), + quote(bytes_sent), + ] + out_file.write(" ".join(line)) + out_file.write("\n") + typer.secho("Log successfully generated.", fg=typer.colors.GREEN) + + +@tpr.command() +def parser(input_file: Path, rust: bool = False) -> None: + """ + Parse given log file. + + :param input_file: path of input file. + :param rust: use rust parser implementation. + """ + typer.secho("Not implemented", err=True, fg=typer.colors.RED) + + +def main() -> None: + """Main program entrypoint.""" + tpr() + +``` + +Для удобства работы добавим в pyproject.toml информацию о командах. + +```toml +[tool.poetry.scripts] +pyrust = "python_rust.main:main" +``` + +Теперь мы можем вызывать нашу программу. + +``` +$ pyrust --help +Usage: pyrust [OPTIONS] COMMAND [ARGS]... + +Options: + --install-completion [bash|zsh|fish|powershell|pwsh] + Install completion for the specified shell. + --show-completion [bash|zsh|fish|powershell|pwsh] + Show completion for the specified shell, to + copy it or customize the installation. + --help Show this message and exit. + +Commands: + generator Test log generator. + parser Parse given log file. +``` + +Работает отлично. + +С помощью данного генератора сгенерируем файл на 8 милионов строк. + +```bash +$ pyrust generator test.log --lines 8000000 +Log successfully generated. +``` + +У нас получился файл на 1.5G. Для начального теста этого будет достаточно. + +## Реализация парсера на Python + +Представленный формат разделяет ифнормацию кавычками, поэтому +для сплита строк мы будем использовать встроенный в python модуль [shlex](https://docs.python.org/3/library/shlex.html). + +Функция парсинга логов будет выглядить следующим образом: + + +```python{}[python_rust/py_parser.py] +import shlex +from pathlib import Path +from typing import Dict, Tuple + + +def parse_python( # noqa: WPS210 + filename: Path, +) -> Dict[str, Tuple[int, int]]: + """ + Parse log file with python. + + :param filename: log file. + :return: parsed data. + """ + parsed_data = {} + with open(filename, "r") as input_file: + for line in input_file: + spl = shlex.split(line) + # Splitting method and actual url. + url = spl[2].split()[1] + # Splitting url by / + # This split will turn this "/test/aaa.png" + # into "aaa". + file_id = url.split("/")[-1].split(".")[0] + file_info = parsed_data.get(file_id) + # If information about file isn't found. + if file_info is None: + downloads, bytes_sent = 0, 0 + else: + downloads, bytes_sent = file_info + # Incrementing counters. + downloads += 1 + bytes_sent += int(spl[4]) + # Saving back. + parsed_data[file_id] = (downloads, bytes_sent) + return parsed_data + +``` + + +Давайте импортируем её, модифицируем команду парсинга и замерим скорость работы. + + +```python{}[python_rust/main.py] +@tpr.command() +def parser(input_file: Path, rust: bool = False) -> None: + """ + Parse given log file. + + :param input_file: path of input file. + :param rust: use rust parser implementation. + """ + if rust: + typer.secho("Not implemented", input_file, color=typer.colors.RED) + return + else: + parsed_data = parse_python(input_file) + + typer.secho( + f"Found {len(parsed_data)} files", # noqa: WPS237 + color=typer.colors.CYAN, + ) + +``` + +Как можно видеть из кода, мы просто подсчитываем итоговое количество найденных файлов. + +Посмотрим сколько времени займёт парсинг нашего сгенерированного файла. + +```bash +$ time pyrust parser "test.log" +Found 1000 files +pyrust parser test.log 2443.42s user 2.10s system 99% cpu 40:59.30 total +``` + +Обработка данного файла заняла 40 минут. Это довольно печальный результат. +Самое время попробовать использовать Rust. + +# Интеграция Rust + +Для интеграции Rust в python код я буду использовать [PyO3](https://github.com/PyO3/pyo3) для биндингов и [maturin](https://github.com/PyO3/maturin) для сборки проекта. + +Начнём с инициализации проекта и установки сборщика. В корневой папке проекта +создайте папку проекта с растом. + + + +В теории можно использовать maturin как основной инструмент сборки проекта, +но это лишает вас всех прелестей poetry. Поэтому удобнее использовать Rust в +подпроекте и указать его как зависимость в списке зависимостей своего проекта. + + + +```bash +$ cargo new --lib rusty_log_parser +``` + +Теперь создадим `pyproject.toml` и напишем туда описание нашего python пакета. + +```toml{}[pyproject.toml] +[tool.poetry] +name = "rusty_log_parser" +version = "0.1.0" +description = "Log file parser with Rust core" +authors = ["Pavel Kirilin "] + +[build-system] +requires = ["maturin>=0.12,<0.13"] +build-backend = "maturin" + +``` + +Также поправим `Cargo.toml`. + +```toml{}[Cargo.toml] +[lib] +# Название модуля +name = "rusty_log_parser" +# Обязательный тип крейта, чтобы можно было использовать его +# в питоне. +crate-type = ["cdylib"] + +[dependencies] +# Библиотека биндингов для питона. +pyo3 = { version = "0.15.1", features = ["extension-module"] } +# Библиотека для сплита. Повторяет функционал shlex. +shell-words = "1.0.0" +``` +После этих нехитрых изменений попробуем собрать проект. + + +```shell +$ cd rusty_log_parser +$ cargo build +``` + +Теперь напишем сам парсер логов в Rust. + +```rust{}[rusty_log_parser/src/lib.rs] +use std::collections::HashMap; +use std::fs::File; +use std::io; +use std::io::BufRead; + +use pyo3::prelude::*; + +/// Parse log file in rust. +/// This function parses log file and returns HashMap with Strings as keys and +/// Tuple of two u128 as values. +#[pyfunction] +fn parse_rust(filename: &str) -> PyResult> { + let mut result_map = HashMap::new(); + // Iterating over file. + for log in io::BufReader::new(File::open(filename)?).lines().flatten() { + // Splitting log string. + if let Ok(mut spl) = shell_words::split(&log) { + // Getting file id. + let file_id_opt = spl.get_mut(2).and_then(|http_req| { + // Splitting method and URL. + http_req.split(' ').into_iter().nth(1).and_then(|url| { + // Splitting by / and getting the last split. + url.split('/') + .into_iter() + .last() + // Split file id and extension. + .and_then(|item_url| item_url.split('.').into_iter().next()) + // Turning &str into String. + .map(String::from) + }) + }); + // Getting number of bytes sent. + let bytes_sent_opt = + spl.get_mut(4) + // Parsing string to u128 + .and_then(|bytes_str| match bytes_str.parse::() { + Ok(bytes_sent) => Some(bytes_sent), + Err(_) => None, + }); + if file_id_opt.is_none() || bytes_sent_opt.is_none() { + continue; + } + let file_id = file_id_opt.unwrap(); + let bytes_sent = bytes_sent_opt.unwrap(); + + match result_map.get(&file_id) { + Some(&(downloads, total_bytes_sent)) => { + result_map.insert(file_id, (downloads + 1, total_bytes_sent + bytes_sent)); + } + None => { + result_map.insert(file_id, (1, bytes_sent)); + } + } + } + } + Ok(result_map) +} + +/// A Python module implemented in Rust. The name of this function must match +/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to +/// import the module. +#[pymodule] +fn rusty_log_parser(_py: Python, m: &PyModule) -> PyResult<()> { + // Adding function to the module. + m.add_function(wrap_pyfunction!(parse_rust, m)?)?; + Ok(()) +} +``` + +Это и будет наша функция, которую мы будем вызывать. +Теперь добавим `python` обёртку для нашего модуля. + +Для этого в проекте `rusty_log_parser` надо создать +папку с тем же названием что и проект. В данном случае +это будет `rusty_log_parser`. + +Внутри этой папки мы создадим `__init__.py`, в котором +опишем и задокументируем доступные функции модуля. + +```python{}[rusty_log_parser/rusty_log_parser/__init__.py] +from pathlib import Path +from typing import Dict, Tuple + +from .rusty_log_parser import parse_rust as _parse_rust + + +def parse_rust(input_file: Path) -> Dict[str, Tuple[int, int]]: + """ + Parse log file using Rust as a backend. + + :param input_file: log file to parse. + :return: Parsed + """ + return _parse_rust(str(input_file.expanduser())) + +``` + +Осталось только добавить свеженаписанный пакет как зависимости нашего проекта. + +Для этого в корневой `pyproject.toml` надо добавить следующую зависимость. + + +```toml{}[pyproject.toml] +[tool.poetry.dependencies] +... +rusty_log_parser = { path = "./rusty_log_parser" } +``` + +Теперь мы можем использовать нашу функцию. + +Перепишем нашу функцию парсера таким образом, +чтобы она использовала rust, когда был передан +соответствующий параметр. + +```python{}[python_rust/main.py] +from rusty_log_parser import parse_rust + +... + +@tpr.command() +def parser(input_file: Path, rust: bool = False) -> None: + """ + Parse given log file. + + :param input_file: path of input file. + :param rust: use rust parser implementation. + """ + if rust: + parsed_data = parse_rust(input_file) + else: + parsed_data = parse_python(input_file) + + typer.secho( + f"Found {len(parsed_data)} files", # noqa: WPS237 + color=typer.colors.CYAN, + ) + +``` + +Теперь проведём итоговый замер. + +```bash +$ time pyrust parser test.log --rust +Found 1000 files +pyrust parser test.log --rust 20.44s user 0.35s system 99% cpu 20.867 total +``` + +Итого виден небольшой выйигрыш во времени в 2423 секунды, из чего следует, +что реализация на Rust быстрее всего в 122 раза. + +Данной статьёй я не подталкиваю всех переписывать всё на Rust, +а просто ознакамливаю с возможностями. Но если всё же хочется, +то можно и попробовать. + +До новых встреч. diff --git a/content/ru/start-with-k8s.md b/content/ru/start-with-k8s.md index 94892e3..95f9add 100644 --- a/content/ru/start-with-k8s.md +++ b/content/ru/start-with-k8s.md @@ -358,6 +358,8 @@ sudo systemctl start k3s.service # Ваше первое приложение в кластере. +Вы можете следовать статье, а можете подсмотреть весь код в [репозитории](https://github.com/s3rius/blog_examples/tree/master/req_counter). + ### Сервер Давайте создадим своё первое приложение. diff --git a/content/ru/traefik.md b/content/ru/traefik.md index f6d69ca..04c0d7d 100644 --- a/content/ru/traefik.md +++ b/content/ru/traefik.md @@ -468,4 +468,4 @@ services: [![GIF](/images/traefik_imgs/traefik_web.png)](/images/traefik_imgs/traefik_web.png) -Разве это не круто? +До новых встреч. diff --git a/deploy/helm/templates/deployment.yaml b/deploy/helm/templates/deployment.yaml index 797eef7..64a686a 100644 --- a/deploy/helm/templates/deployment.yaml +++ b/deploy/helm/templates/deployment.yaml @@ -38,11 +38,14 @@ spec: httpGet: path: / port: http + initialDelaySeconds: 40 + periodSeconds: 20 readinessProbe: httpGet: path: / port: http - initialDelaySeconds: 15 + initialDelaySeconds: 30 + periodSeconds: 15 resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.nodeSelector }}