Пайпи, баш і конкурентність
May 12, 2024 • Updated: October 18, 2025 · Edit this page on GitHub
Років десь 15 потому я був дурний і любив лишати на співбесідах одне-два питання по башу. Це дещо бісило усіляких sinior YAML engineer, але мені було цікаво наскільки люди розуміють (чи не розуміють) інструменти які використовують щодня.
Я знаю, знаю, баш як мова це стрьом і спроектовано дуже давно, людьми, які жили при фараонах 1 і зараз вже спочивають у пірамідах.
Це пояснює деякі античні знахідки:

Але вислухайте мене. Люди які писали баш були розумними людьми. І вони робили цікаві і практичні речі. Дуже багато цікавих і практичних речей.
І я вже давно не задаю питання по башу. Але досі дурний і отримую задоволення від гарно написаного скрипта, реалізація якого в іншій мові була б складнішою.
Задача
Правдами і неправдами в мене з’явилась задача виконати пайплайн операцій над списком. Для простоти і розуміння 2 нехай це буде:
- Збілдити Docker-імеджі з переліку.
- Запушити їх в різні реджестрі.
- Після того як імедж запушився в усі реджестрі — смикнути вебхук.
---
config:
theme: neo
look: neo
layout: dagre
---
flowchart LR
list["List of Images"] --> build["docker build"]
build --> worker1["push worker 1"] & worker2["push worker 2"]
worker1 --> webhook["webhook"]
worker2 --> webhook
list@{ shape: procs}
style list fill:#f9f,stroke:#FFCDD2
style build fill:#C8E6C9
style webhook stroke:#BBDEFB,stroke-width:1px,stroke-dasharray: 1,fill:#BBDEFB
До речі, класно пушити у різні реджестрі можна за допомогою regctl, але не в моєму випадку, тому що мені було потрібно:
- Збілдити досить великі Docker-імеджі з нейронками.
- Експортнути їх в формат OCI (за допомогою
skopeo) - З OCI перевести їх в формат віддаленного реджестрі.
- Зробити rclone блобів в декілька віддаленних реджестрі по всьому світу .
- Перевести їх назад в OCI (щоб
skopeoмало змогу перевикористати блоби) - Запушити імеджи в реджестрі (щоб оновити віддалені маніфести під вже запушені блоби)
Обмеження
- Білдити і експортити тільки один імедж одночасно.
- Пушити тільки один імедж одночасно.
Але чим детальніший приклад, тим менша вірогідність що хтось витратить час на його розуміння.
Усе просто, і в наївному варіанті це виглядає приблизно так:
#!/bin/bash -e
images=("image1" "image2" "image3")
for; do
done
Але це не оптимальна реалізація, тому що:
- можна білдити image2, доки пушимо image1 (асинхронність)
- можна пушити імеджі в обидва реджестрі паралельно
Асинхронність ⊆ конкурентність і майже завжди — оптимізація. І нема нічого гірше в інфраструктурі ніж передчасна оптимізація. Натомість наївність майже завжди достатня сама по собі: чим простіша ідея, тим легше її читати і підтримувати.
Якщо мінуси наївного підходу не критичні, то завжди варто обирати наївний підхід із мінімумом оптимізацій.
Пам’ятайте: ваш код може колись прочитати джаваскріпт розробник і померти.
Конкурентність
В баші досить просто запустити конкурентні задачі якщо додати символ & в кінець строки:
# Ця задача запускається у фоні
&
# І ця також
(
)&
# Ми можемо очікувати завершення задачі, знаючи її PID
last_task_pid=
# Або можемо чекати, поки виконається все, що запущено у фоні
Тобто, якщо б в нас були фунції docker_build, docker_push та webhook 3 ми могли б зробити щось на кшталт цього
Для простоти дебагу і тестування ми не будемо білдити і пушити кожного разу, а зробимо mock функції:
# і треба не забути їх ексопртувати
У майбутньому ми просто замінимо тіло функції на щось валідне, а поки що матимемо змогу додати будь-який лог і навіть протестувати логіку. (Хоча тестування bash-скриптів — це тема для окремого поста.)
&
&
&
&
Але як передати інформацію від процеса білдера до процесів які пушать і процесів вебхуків?
Пайпи
Ми знаємо, що якщо написати ls | grep, то всі дані, які ls надрукує в stdout, підуть до команди grep.
Це відбувається тому, що кожен процес у Linux типово має щонайменше три відкритих файлових дескриптори (якщо потім самостійно їх не закриє):
stdin(0),stdout(1),stderr(2).
Пайпи — це просто перенаправлення даних з одного дескриптора в інший.
Ми могли б замість пайпів використати і файли, але пайпи зручні тим, що це ще й інструмент синхронізації: коли пайп закривається, процес, який з нього читає, розуміє, що більше інформації немає, і може спокійно "повзти в помиральну яму".
Щоб краще зрозуміти пайпи, рекомендую почитати код в ядрі і подивитися, як можна імплементувати bash-пайпи в простій програмі на C.
int
Збілдити можна так:
В нашому прикладі було потрібно розпочати docker push після того як пройшов docker build , тобто написати в два процесси з першого.
Це можна зробити так:
(; )
Чи використовуючи tee(1)
| |
Але простіше для читання і розуміння буде якщо ми використаємо іменовані пайпи 4.
У прикладі далі ви побачите exec 3>/tmp/reg1.fifo — це перенаправлення третього (нового) файлового дескриптора в іменований пайп.
І це зроблено не для того, щоб заплутати читача, а для того, щоб відкрити пайп лише один раз і тримати його відкритим.
# Створюємо іменовані пайпи для пушерів
# Запускаємо воркера який білдить імеджі
(
# Перенаправляємо дескриптори в пайпи
# Білдимо імеджі
for; do
# Пишемо про успіх в пайпи
done
# Коли всі імеджі збілджені — закриваємо пайпи
# І видаляємо їх
)& # `&` значить що ми запускаємо в фоні
# Використовуємо xargs щоб не блокувати docker_build воркера
# Без xargs буде блокування
&
&
# Чекаємо доки завершаться усі процесси
Таким чином ми у фоні запустили процес, який незалежно білдить імеджі, і крізь пайпи сповіщає інші два процеси, які по одному, незалежно один від одного, пушать імеджі до реєстрів.
Усе, що залишилося, — це сповістити про успіх і зробити вебхук.
Синхронізація незалежних процесів
Якщо те, що було вище, здалося вам оверінженерингом, то заплющуйте очі.
Як є багато способів освіжувати кота, так і синхронізувати 5 потоки можна декількома способами: за допомогою сигналів і команди trap, за допомогою лок-файлів, команди wait або пайпів.
Синхронізація не те щоб потрібна в цьому прикладі, тому ми могли б просто смикнути всі вебхуки після команди wait, тобто коли ми вже точно впевнені, що все запушено. Але можуть бути інші задачі, коли треба щось запустити після кожної ітерації, а не після всіх.
Як на мене, пайпи — це найпростіша і найшвидша реалізація, але якщо вам цікаві інші способи (або ви знаєте ще щось), залишайте коментарі.
Синхронізація пайпами
Що ми знаємо про пайпи? Ну, як мінімум, що якщо ми намагаємося прочитати з пайпа, у який ніхто не пише, то будемо чекати вічність.
#!/usr/bin/env bash
(
< /tmp/pipe
)&
# Result:
# I'm waiting for pipe
# I'm going to write to pipe
# I'm done waiting for pipe
# I'm done writing to pipe
Це працює з одним процессом, а як зробити з двома? Тут вже не вийде просто очікувати пайп, бо можна залочити самих себе. Ось приклад як перший процесс очікує поки хтось напише в pipe1, в той час як другий процес очікує доки хтось прочитає pipe2
#!/usr/bin/env bash
(
< /tmp/pipe1
< /tmp/pipe2
)&
Вихід з цієї ситуації — запустити очікування пайпів в окремих процесах і очікувати на завершення цих процесів:
(
&
pid1=
&
pid2=
)&
Додаємо очікувач на вебхук
Тепер кожен раз коли ми білдимо імедж ми відразу створюємо пайпи для очікування і запускаємо очікувач, який буде тупити доки ніхто не напише в обидва пайпи.
for; do
# Створюємо пайпи очікування під кожен імедж
# Після кожного білда запускаємо воркер вебхуку
# який очікує коли імеджі запушать
(
# І запускаємо очікувачі в фоні
&
# Записуємо PID очікувача
wait_for_reg1=
&
wait_for_reg2=
# Очікуємо на обидва очікувача
# виконуємо вебхук
# І чистимо їх після себе
)&
done
)&
# Оновлюємо команду — додаємо нотифікацію для вебхука
# після того як запушили імедж
&
&
Таким чином процеси які пушать — незалежно один від одного сповіщують процес вебхука про своє завершення і не блокуються.
Результат
Ось щось таке мало в нас вийти.
#!/usr/bin/env bash
LIST="image1 image2 image3"
(
for; do
(
&
wait_for_reg1=
&
wait_for_reg2=
)&
done
)&
&
&
Такий паттерн можливо (не завжди треба, але можливо) також використовувати в мовах де є канали
package main
import (
"log"
"math/rand"
"sync"
"time"
)
type wait struct
func main()
І під кінець опитування - було б вам цікаво побачити більше постів про адвансед баш? Лишайте відповіді у коментарях.