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:
[](/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 }}