diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 81d41caba0f698c2c2b353e4776ad42eb5cdfbfc..1fdfcc0815a7c20f961ac68bb5bc19340a303c19 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 79873a04eb944992466f9e4366dc622f8670a682..3740a599d5d72bde3592e73f1c13a4c370158b39 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 cfc5482111612ecd7577e0a21c609d01e9bb81ca..0806700e85f4a71c8110d8414c612708fe70eb5d 100644 --- a/content/ru/makefiles.md +++ b/content/ru/makefiles.md @@ -133,3 +133,7 @@ Hi! Рчто еÑли Ñ Ð²Ð¾Ñ‚ не хочу чтобы команда Ñоздавала и проверÑла файлы? Ð”Ð»Ñ Ñтого пишут `.PHONY: ${target}`. Ðапример у Ð½Ð°Ñ Ñ‚Ð°Ðº объÑвлен таргет `run` и, даже еÑли файл Ñ Ð½Ð°Ð·Ð²Ð°Ð½Ð¸ÐµÐ¼ `run` будет приÑутÑтвовать в директории цель не будет выполнÑтьÑÑ. + +<br> + +До новых вÑтреч. diff --git a/content/ru/project-start.md b/content/ru/project-start.md index 767bc3fedafbb7c5bf5845f0ba4a4c4c7f5180b7..43b34823ec33ab34901b196a3107d6fc735e2540 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 0000000000000000000000000000000000000000..7c3353247116e64ddf347a3b0dfc68d9b100b0e7 --- /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. + +<br> + +Задача проекта будет Ñледующей: + +Дан файл лога запроÑов на некоторый Ñервер. ТребуетÑÑ +найти количеÑтво запроÑов к определённому файлу и Ñумму переданных байт. + +Ð”Ð»Ñ Ð´ÐµÐ¼Ð¾Ð½Ñтрации мы напишем 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) Ð´Ð»Ñ Ñборки проекта. + +Ðачнём Ñ Ð¸Ð½Ð¸Ñ†Ð¸Ð°Ð»Ð¸Ð·Ð°Ñ†Ð¸Ð¸ проекта и уÑтановки Ñборщика. Ð’ корневой папке проекта +Ñоздайте папку проекта Ñ Ñ€Ð°Ñтом. + +<b-message type="is-info" has-icon> + +Ð’ теории можно иÑпользовать maturin как оÑновной инÑтрумент Ñборки проекта, +но Ñто лишает Ð²Ð°Ñ Ð²Ñех прелеÑтей poetry. ПоÑтому удобнее иÑпользовать Rust в +подпроекте и указать его как завиÑимоÑть в ÑпиÑке завиÑимоÑтей Ñвоего проекта. + +</b-message> + +```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 <win10@list.ru>"] + +[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<HashMap<String, (u128, u128)>> { + 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::<u128>() { + 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 94892e385069ef763b5b0b3a3401ad5321074ab7..95f9add2940864c2a0351ff133c56c6250659ad1 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 f6d69cad70bf4b2927d17b8ab688bbc138d4763c..04c0d7dab53b6a552d1775504335bddf0df1f501 100644 --- a/content/ru/traefik.md +++ b/content/ru/traefik.md @@ -468,4 +468,4 @@ services: [](/images/traefik_imgs/traefik_web.png) -Разве Ñто не круто? +До новых вÑтреч. diff --git a/deploy/helm/templates/deployment.yaml b/deploy/helm/templates/deployment.yaml index 797eef7699fde4dfd01bcbbc2bec41633e4882df..64a686a170ba4702f21b7d72160494362711be38 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 }}