Пайпи, баш і конкурентність

💡 Усі статті, обговорення, новини про DevOps — в одному місці. Приєднуйтесь до DevOps спільноти!

Років десь 15 потому я був дурний і любив лишати на співбесідах одне-два питання по башу. Це дещо бісило усіляких sinior YAML engineer, але мені було цікаво наскільки люди розуміють (чи не розуміють) інструменти які використовують щодня.

Я знаю, знаю, баш як мова це стрьом і спроектовано дуже давно, людьми, які жили при фараонах і зараз вже спочивають у пірамідах.

Але вислухайте мене. Люди які писали баш були розумними людьми. І вони робили цікаві і практичні речі. Дуже багато цікавих і практичних речей.

І я вже давно не задаю питання по башу. Але досі дурний і отримую задоволення від гарно написаного скрипта, реалізація якого в іншій мові була б складнішою.

Задача

Правдами і неправдами в мене з’явилась задача виконати пайплайн операцій над списком. Для простоти і розуміння нехай це буде:

  1. Збілдити Docker-імеджі з переліку.
  2. Запушити їх в різні реджестрі.
  3. Після того як імедж запушився в усі реджестрі — смикнути вебхук.

Опис реальної задачі, виправдання і обмеження (треба клікнути):До речі, класно пушити у різні реджестрі можна за допомогою regctl, але не в моєму випадку, тому що мені було потрібно:
  1. Збілдити досить великі Docker-імеджі з нейронками.
  2. Експортнути їх в формат OCI (за допомогою skopeo).
  3. З OCI перевести їх в формат віддаленного реджестрі.
  4. Зробити rclone блобів в декілька віддаленних реджестрі по всьому світу.
  5. Перевести їх назад в OCI (щоб skopeo мало змогу перевикористати блоби).
  6. Запушити імеджи в реджестрі (щоб оновити віддалені маніфести під вже запушені блоби).

Обмеження

  • Білдити і експортити тільки один імедж одночасно.
  • Пушити тільки один імедж одночасно.
Але це багато контексту, тому я заникав це під спойлер для допитливих. Треба не забути зробити аналітику по кількості людей, які кликнули на спойлер. Знаючи вас, має бути 99.9%.

Усе просто, і в наївному варіанті це виглядає приблизно так:

#!/bin/bash -e

images=("image1" "image2" "image3")

for image in "${images[@]}"; do
  docker build -t "registry-1/$image" -t "registry-2/$image" .
  docker push "registry-1/$image"
  docker push "registry-2/$image"
  curl -X POST "https://example.com/webhook/$image"
done

Але це не оптимальна реалізація, тому що:

  • можна білдити image2, доки пушимо image1 (асинхронність)
  • можна пушити імеджі в обидва реджестрі паралельно

Обережно

Асинхронність ⊆ конкурентність і майже завжди — оптимізація. І нема нічого гірше в інфраструктурі ніж передчасна оптимізація. Натомість наївність майже завжди достатня сама по собі: чим простіша ідея, тим легше її читати і підтримувати.

Якщо мінуси наївного підходу не критичні, то завжди варто обирати наївний підхід із мінімумом оптимізацій.

Пам’ятайте: ваш код може колись прочитати джаваскріпт розробник і померти.

Конкурентність

Дебаг під час розробки — це важливоДля простоти дебагу і тестування ми не будемо білдити і пушити кожного разу, а зробимо mock функції:
function docker_build() {
    echo Start build for $1
    sleep 0.3
    echo End build for $1
}

function docker_push() {
    echo Start push for $1
    sleep $2
    echo End push for $1
}

function webhook() {
    echo Start webhook for $1
    sleep 0.1
    echo End webhook for $1
}

# і треба не забути їх ексопртувати
export -f docker_build
export -f docker_push
export -f webhook

У майбутньому ми просто замінимо тіло функції на щось валідне, а поки що матимемо змогу додати будь-який лог і навіть протестувати логіку. (Хоча тестування bash-скриптів — це тема для окремого поста.)

В баші досить просто запустити конкурентні задачі якщо додати символ & в кінець строки:

# Ця задача запускається у фоні  
some_task &  

# І ця також  
(  
    other_task  
    sleep 200  
    some_webhook  
)&  

# Ми можемо очікувати завершення задачі, знаючи її PID  
last_task_pid=$!  
wait $last_task_pid  

# Або можемо чекати, поки виконається все, що запущено у фоні  
wait

Тобто, якщо б в нас були фунції docker_build, docker_push та webhook ми могли б зробити щось на кшталт цього

docker_build &
docker_push reg1/$image &
docker_push reg2/$image &
webhook $image &
wait

Але як передати інформацію від процеса білдера до процесів які пушать і процесів вебхуків?

Пайпи

Ми знаємо, що якщо написати ls | grep, то всі дані, які ls надрукує в stdout, підуть до команди grep.

Це відбувається тому, що кожен процес у Linux типово має щонайменше три відкритих файлових дескриптори (якщо потім самостійно їх не закриє):

  • stdin (0)
  • stdout (1)
  • stderr (2)

Пайпи — це просто перенаправлення даних з одного дескриптора в інший.

Ми могли б замість пайпів використати і файли, але пайпи зручні тим, що це ще й інструмент синхронізації: коли пайп закривається, процес, який з нього читає, розуміє, що більше інформації немає, і може спокійно «повзти в помиральну яму».

Щоб краще зрозуміти пайпи, рекомендую почитати код в ядрі і подивитися, як можна імплементувати bash-пайпи в простій програмі на C.


Приклад імпліментації на C
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    int pipefd[2];
    pid_t pid1, pid2;

    // Створюємо пайп
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    // Створюємо child process
    pid1 = fork();
    if (pid1 == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid1 == 0) { // Child 1 (Writer)
        // Close the read end of the pipe
        close(pipefd[0]);

        // Redirect standard output to the write end of the pipe
        dup2(pipefd[1], STDOUT_FILENO);

        // Close the original write end of the pipe (important!)
        close(pipefd[1]);

        // Execute a command or perform some action that writes to stdout
        // Example:
        execlp("ls", "ls", "-l", NULL); // List files in long format
        perror("execlp"); // Only reached if execlp fails
        exit(EXIT_FAILURE);
    }

    // Create the second child process
    pid2 = fork();
    if (pid2 == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid2 == 0) { // Child 2 (Reader)
        // Close the write end of the pipe
        close(pipefd[1]);

        // Redirect standard input to the read end of the pipe
        dup2(pipefd[0], STDIN_FILENO);

        // Close the original read end of the pipe (important!)
        close(pipefd[0]);

        // Execute a command or perform some action that reads from stdin
        // Example:
        execlp("wc", "wc", "-l", NULL); // Count lines from input
        perror("execlp"); // Only reached if execlp fails
        exit(EXIT_FAILURE);
    }

    // Parent process (Closes both ends of the pipe)
    close(pipefd[0]);
    close(pipefd[1]);

    // Wait for both child processes to finish
    waitpid(pid1, NULL, 0);
    waitpid(pid2, NULL, 0);

    printf("Parent process finished.\n");

    return 0;
}

Збілдити можна так:

gcc filename.c -o filename
./filename

В нашому прикладі було потрібно розпочати docker push після того як пройшов docker build, тобто написати в два процесси з першого.

Це можна зробити так:

(echo "to_stdout"; echo "to_stderr" >&2) \
  2> >(xargs -I{} echo "from_stderr:{}") \
  1> >(xargs -I{} echo "from_stdout:{}")

Чи використовуючи tee(1):

echo "image" | tee >(xargs -I{} echo "from_tee:{}") | xargs -I{} echo "from_tee2:{}"

Але простіше для читання і розуміння буде якщо ми використаємо іменовані пайпи .


Зв’язування пайпів з файловими дескрипторами

У прикладі далі ви побачите exec 3>/tmp/reg1.fifo — це перенаправлення третього (нового) файлового дескриптора в іменований пайп. І це зроблено не для того, щоб заплутати читача, а для того, щоб відкрити пайп лише один раз і тримати його відкритим.
# Створюємо іменовані пайпи для пушерів
mkfifo /tmp/reg1.fifo
mkfifo /tmp/reg2.fifo

# Запускаємо воркера який білдить імеджі
(
	# Перенаправляємо дескриптори в пайпи
	exec 3>/tmp/reg1.fifo
	exec 4>/tmp/reg2.fifo
	
	# Білдимо імеджі
	for i in $LIST; do
	  docker_build ${i}
	  
	  # Пишемо про успіх в пайпи
	  echo ${i} >&3
	  echo ${i} >&4
	done
	
	# Коли всі імеджі збілджені — закриваємо пайпи 
	exec 3>&-
	exec 4>&-
	# І видаляємо їх
	rm /tmp/reg1.fifo /tmp/reg2.fifo
)& # `&` значить що ми запускаємо в фоні

# Використовуємо xargs щоб не блокувати docker_build воркера
# Без xargs буде блокування
xargs -n1 -I{} bash -c "docker_push reg1/{}" < /tmp/reg1.fifo &
xargs -n1 -I{} bash -c "docker_push reg2/{}" < /tmp/reg2.fifo &

# Чекаємо доки завершаться усі процесси
wait

Таким чином ми у фоні запустили процес, який незалежно білдить імеджі, і крізь пайпи сповіщає інші два процеси, які по одному, незалежно один від одного, пушать імеджі до реєстрів.

Усе, що залишилося, — це сповістити про успіх і зробити вебхук.

Синхронізація незалежних процесів

Якщо те, що було вище, здалося вам оверінженерингом, то заплющуйте очі.

Адекватна реалізаціяСинхронізація не те щоб потрібна в цьому прикладі, тому ми могли б просто смикнути всі вебхуки після команди wait, тобто коли ми вже точно впевнені, що все запушено. Але можуть бути інші задачі, коли треба щось запустити після кожної ітерації, а не після всіх.

Як є багато способів освіжувати кота, так і синхронізувати потоки можна декількома способами: за допомогою сигналів і команди trap, за допомогою лок-файлів, команди waitабо пайпів.

Як на мене, пайпи — це найпростіша і найшвидша реалізація, але якщо вам цікаві інші способи (або ви знаєте ще щось), залишайте коментарі.

Синхронізація пайпами

Що ми знаємо про пайпи? Ну, як мінімум, що якщо ми намагаємося прочитати з пайпа, у який ніхто не пише, то будемо чекати вічність.

#!/usr/bin/env bash

mkfifo /tmp/pipe
(
  echo "I'm waiting for pipe"
  < /tmp/pipe
  echo "I'm done waiting for pipe"
)&

sleep 1
echo "I'm going to write to pipe"
echo > /tmp/pipe
sleep 1
echo "I'm done writing to 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

mkfifo /tmp/pipe1
mkfifo /tmp/pipe2
(
  < /tmp/pipe1
  < /tmp/pipe2
)&

echo > /tmp/pipe2
echo > /tmp/pipe1

Вихід з цієї ситуації — запустити очікування пайпів в окремих процесах і очікувати на завершення цих процесів:

mkfifo /tmp/pipe1
mkfifo /tmp/pipe2
(
  cat < /tmp/pipe1 &
  pid1=$!
  cat < /tmp/pipe2 &
  pid2=$!
  wait $pid1 $pid2
  echo "I'm done with pipes!"
)&

echo > /tmp/pipe2
echo > /tmp/pipe1

Додаємо очікувач на вебхук

Тепер кожен раз коли ми білдимо імедж ми відразу створюємо пайпи для очікування і запускаємо очікувач, який буде тупити доки ніхто не напише в обидва пайпи.

for i in $LIST; do
  docker_build ${i}
  
  # Створюємо пайпи очікування під кожен імедж
  mkfifo /tmp/reg1-${i}.fifo
  mkfifo /tmp/reg2-${i}.fifo
  
  # Після кожного білда запускаємо воркер вебхуку
  # який очікує коли імеджі запушать
  (
    # І запускаємо очікувачі в фоні 
    cat /tmp/reg1-${i}.fifo &> /dev/null &
    # Записуємо PID очікувача
    wait_for_reg1=$!
    
    cat /tmp/reg2-${i}.fifo &> /dev/null &
    wait_for_reg2=$!
    
    # Очікуємо на обидва очікувача
    wait $wait_for_reg1 $wait_for_reg2
    
    # виконуємо вебхук
    webhook ${i}
    # І чистимо їх після себе
    rm /tmp/reg1-${i}.fifo /tmp/reg2-${i}.fifo
  )&
  
  echo ${i} >&3
  echo ${i} >&4
done
exec 3>&-
exec 4>&-
rm /tmp/reg1.fifo /tmp/reg2.fifo
)&

# Оновлюємо команду — додаємо нотифікацію для вебхука
# після того як запушили імедж
xargs -n1 -I{} bash -c "docker_push reg1/{}; echo > /tmp/reg1-{}.fifo" < /tmp/reg1.fifo &
xargs -n1 -I{} bash -c "docker_push reg2/{}; echo > /tmp/reg2-{}.fifo" < /tmp/reg2.fifo &
wait

Таким чином процеси які пушать — незалежно один від одного сповіщують процес вебхука про своє завершення і не блокуються.

Результат

Ось щось таке мало в нас вийти.

#!/usr/bin/env bash

LIST="image1 image2 image3"

function docker_build() {
    echo Start build for $1
    sleep 1.5
    echo End build for $1
}

function docker_push() {
    echo Start push for $1
    sleep $2
    echo End push for $1
}

function webhook() {
    echo Start webhook for $1
    sleep 0.1
    echo End webhook for $1
}

export -f docker_build
export -f docker_push
export -f webhook

mkfifo /tmp/reg1.fifo
mkfifo /tmp/reg2.fifo
(
exec 3>/tmp/reg1.fifo
exec 4>/tmp/reg2.fifo
for i in $LIST; do
  mkfifo /tmp/reg1-${i}.fifo
  mkfifo /tmp/reg2-${i}.fifo
  docker_build ${i}
  (
    cat /tmp/reg1-${i}.fifo &> /dev/null &
    wait_for_reg1=$!
    cat /tmp/reg2-${i}.fifo &> /dev/null &
    wait_for_reg2=$!
    wait $wait_for_reg1 $wait_for_reg2
    webhook ${i}
    rm /tmp/reg1-${i}.fifo /tmp/reg2-${i}.fifo
  )&
  echo ${i} >&3
  echo ${i} >&4
done
exec 3>&-
exec 4>&-
rm /tmp/reg1.fifo /tmp/reg2.fifo
)&

xargs -n1 -I{} bash -c "docker_push reg1/{} 5; echo > /tmp/reg1-{}.fifo" < /tmp/reg1.fifo &
xargs -n1 -I{} bash -c "docker_push reg2/{} 1; echo > /tmp/reg2-{}.fifo" < /tmp/reg2.fifo &
wait

Такий паттерн можливо (не завжди треба, але можливо) також використовувати в мовах де є канали

Код на Go. Створений для ілюстрації
package main

import (
	"log"
	"math/rand"
	"sync"
	"time"
)

type wait struct {
	image string
	done  chan<- struct{}
}

func main() {
	group := sync.WaitGroup{}
	ch1 := make(chan wait, 100)
	ch2 := make(chan wait, 100)
	images := []string{"image1", "image2", "image3", "image4", "image5"}

	group.Add(1)
	go func() {
		defer group.Done()
		for _, i := range images {
			// random sleep
			wait1 := make(chan struct{})
			wait2 := make(chan struct{})
			log.Printf("Start docker build %s", i)
			time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
			log.Printf("Sending %s to channels", i)
			group.Add(1)
			go func() {
				defer group.Done()
				webhook_wg := sync.WaitGroup{}
				webhook_wg.Add(2)
				go func() {
					defer webhook_wg.Done()
					<-wait1
				}()
				go func() {
					defer webhook_wg.Done()
					<-wait2
				}()
				webhook_wg.Wait()
				log.Printf("Start webhook %s", i)
			}()
			ch1 <- wait{i, wait1}
			ch2 <- wait{i, wait2}
		}
		close(ch1)
		close(ch2)
	}()
	group.Add(2)
	go func() {
		defer group.Done()
		for i := range ch1 {
			log.Printf("Processing first repo %s", i.image)
			time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)
			log.Printf("Processed %s in first repo", i.image)
			close(i.done)
		}
	}()
	go func() {
		defer group.Done()
		for i := range ch2 {
			log.Printf("Processing second repo %s", i.image)
			time.Sleep(time.Duration(rand.Intn(3200)) * time.Millisecond)
			log.Printf("Processed %s in second repo", i.image)
			close(i.done)
		}
	}()
	group.Wait()
}

І під кінець опитування — було б вам цікаво побачити більше постів про адвансед баш? Лишайте відповіді у коментарях.

Оригінал статті доступний за посиланням. Увесь текст статті поширюється відповідно до умов вказаної ліцензії.

👍ПодобаєтьсяСподобалось41
До обраногоВ обраному27
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

у данных скриптов нет таймаутов и graceful shutdown. На го или чем-то другом это можно прикрутить, а как на баше хз, а так выходит зависший скрипт помрет только после kill −9 с мертвыми неподчищенными артифактами

як цінитель закопатись в баш і вийти з потоку вранці маючи саме те що хо з відчуттям що я завалив антилопу — лю )

Але в проект я таке не засовую, а засовувачам руки відриваю, і розвішую на вході в офіс

Будьте уважні, в наведеному прикладі коду Go є race condition: виклик group.Add(1) відбувається всередині горутини.

Детально можна почитати в тікеті github.com/golang/go/issues/18022 та в коментах до тікету. Є також чек staticcheck для знаходження цієї помилки staticcheck.dev/docs/checks#SA2000. Але в коді зі статті staticcheck не задетектить race condition, тому що тут трохи нестандартна ситуація.

Правильно:

var group sync.WaitGroup
defer group.Wait()

group.Add(1)
go func() {
	defer wg.Done()
}()

Неправильно:

var group sync.WaitGroup
defer group.Wait()

go func() {
	group.Add(1)
	defer group.Done()
}()

доречний коментар, дякую

У прикладі не буде рейскондішену відразу тому що все-таки робота (sleep random) виконається і group.add(1) відбувається з затримкою

Але і корректний приклад який ви привели також не є корректним, тому що правильно було б не додавати в існуючу группу `group`, а створити нову для елементів циклу і потім очікувати на неї.

Написано явно не так, як пишуть на баш. Як на мене, то дуже ускладнено. Я б простіше розбив на функції отаким чином

Ваш оригінал:

images=("image1" "image2" "image3")

for image in "${images[@]}"; do
  docker build -t "registry-1/$image" -t "registry-2/$image" .
  docker push "registry-1/$image"
  docker push "registry-2/$image"
  curl -X POST "https://example.com/webhook/$image"
done


#   Мій варіант:

process_image() {
  echo "building image $1"
  docker build -t "registry-1/$1" -t "registry-2/$1" .
  echo "pushing image $1 to registry-1"
  docker push "registry-1/$1"
  echo "pushing image $1 to registry-2"
  docker push "registry-2/$1"
  curl -X POST "https://example.com/webhook/$1"
  echo "processing image $1 completed"
}

images=("image1" "image2" "image3")

for image in "${images[@]}"; do
  process_image &
done
wait
echo "Everything completed'

таким чином в мене кожен image йде окремо, при цьому блок цілком працює в одному stdout/stderr можно легко перенаправляти кожен в окремий файл, чи загальний. Можно в принципі робити перенаправлення всього блоку    process_image >fil1.log &
і простіше скрипт менеджити

На додаток, я зовсім не розумію оцей ваш приклад:

docker_build &
docker_push reg1/$image &
docker_push reg2/$image &
webhook $image &
wait

Як воно може пушити, якщо воно ще в процесі build?

На що ви ставите webhook, якщо попередні процеси просто запустилися, і ви від них нічого не отримали?

Як воно може пушити, якщо воно ще в процесі build?

так ви статтю дочитайте спочатку 😅 там же прямо під кодом написано

Але як передати інформацію від процеса білдера до процесів які пушать і процесів вебхуків?

Якщо ви не хочете читати статтю, то просто читайте останній блок коду, бо попередні це приклади, історія і роздуми

я прочитав, і я розумію що таке docker build
і я не розумію як це можно запушити докер image, коли він ще не готовий.
Веб хук — це просто виклик curl після якогось процесу, для цього не треба build, push та curl кидати паралельно, вони повинні бути послідовними. А ось обробку різних image можно паралельно, що я і роблю

це ж ілюстрація неіснуючих функцій

Як на мене, то дуже ускладнено. Я б простіше розбив на функції отаким чином

це ви щодо «наївного» варіанту сперечаєтесь? Я взагалі думав просто навіть без циклу написати щоб ілюструвати наївний підхід. Але ідея наївного підходу це як раз зробити з мінімумом логіки (там далі у статті окремий блок навіть про це є)

(
  < /tmp/pipe1
  < /tmp/pipe2
)&

це не буде очікування двох пайпів паралельно, це буде очікуватись в бекграунді спершу перший, потім другий.
Тобто я не розумію навіщо треба ускладнювати та писати очікувач, якщо можно просто зробити функцію яка послідовно робить build/push1/push2/call webook и викликати ії для різних докер образів паралельно.
Це можно і через цикл, і через xargs, наприклад умовний неповний код:

function process_image() {
  docker_build
  docker_push1
  docker_push2
}

images=("image1" "image2" "image3")

echo ${images[@]} | xargs -n1 -p2
Таким чином, xargs сам буде слідкувати що зараз паралельно оброблюється 2 докер образи
це не буде очікування двох пайпів паралельно, це буде очікуватись в бекграунді спершу перший, потім другий.

так, не буде, тому що над кодом який ви процитували написано

Тут вже не вийде просто очікувати пайп, бо можна залочити самих себе.

ви взагалі як саме статтю читаєте? Типу кожен блок це бестпрактіс на вашу думку?

а щодо xargs — ну це один з варіантів, я взагалі можу штук десять інших варіантів написати і що? А так xargs -p з функцією не задовільняє умові «пушити в реджестрі паралельно» і «можна білдити лише один імедж одночасно»

а якщо так

function process_image() {
  docker_build
  docker_push1 &
  docker_push2 &
  wait
}

echo $myimages | xargs -n1 -p2

xargs -p2 буде запускати обидва білда одночасно (конкретно докер білд не буде одночасним на рівні докера, але усе інше що входить в етап білда — наприклад експорт може один одному мішати) і може відбуватися кілька пушів у тей самий реджестрі одночасно (що досить непогано ложить cloudflare r2 registry)
також якщо після wait буде вебхук (а там має бути вебхук), то можливі випадки коли обидва білди пройшли, в швидкі реджестрі пуши пройшли і усі чекають і нічого не роблять доки в повільний реджестрі не запушить

так і планується, два білда одночасно. Білд зазвичай іде довше, ніж пуш

Дуже цікаво! Дякую!

обожаю зубодробительные шелловые скрипты баша и мейкфайлов, но на практике когда проект большой, то под сборку выделяется несколько нод (подов) и билдить параллельно нет смысла, а пуш в локальный реестр он тупо секунды выполняется, если вы тамм гигабайтные образы не делаете, ну и в случае багов, крашей, утечек важно чтоб процесс билдера помер со всеми временными файлами которые насоздавал. Когда в этой шелловой магии будет баг или залипнет, то что что-то не предусмотрено, никто это дебажить не будет, а тупо дропнут

паралеллизм в баше очень полезная штука, но явно не в таком виде, как я вижу в статье.

Когда в этой шелловой магии будет баг или залипнет, то что что-то не предусмотрено, никто это дебажить не будет, а тупо дропнут

В баш же можно проверять статус завершения процессов. И даже можно делать обычный try/catch на линукс сигналы, чтобы перехватывать ексепшены

шось ви якось не туди пішли
сам що досвідом прийшов до того що баш всьому голова і 99% автоматизацій та СI/CD пишу тільки на ньому

але ви привели приклади задач які краше було б написати вже на чомусь більш гнучному.
я не кажу за python — це не піпдтривувана та тяжко маштабуема ***ня.
З вашою задачею на тому-ж Go написати і все. Да потрібно буде компілювати по місцю (якшо всі робочі машини однакові то вистачить готового файлу) але вся складна асинхронна логіка буде написана «просто».

взагалі для докеру е безліч готових інструментів.
стаття гарна, але приклади якісь кубічно-ваккумні. І якщо це реально бойова реалізація то лізти в це та розбиратись буде адово, якшо потрібно іншій людині шось змінити

Есть конкретная сложная задача.
Мы её решаем при помощи инструментов, которые для этого никогда не были предназначены.
Вместо того, что бы взять подходящий инструментарий.

Суть то в чём? В том что «потому что гладиолус можем»?

Чудова стаття, дякую!

було б вам цікаво побачити більше постів про адвансед баш

Так!

Можна взяти GNU parallel, і сильно спростити життя з синхронізацією паралельних задач.

Цікаво, що автор хоче в складний баш, але не хоче спочатку спроектувати як у кінці повинна працювати система згідно з реальним сценарієм з роботи.

норм я таке полюбляю
я декілька раз заменяв потенційні проекти на хз скілки тижднів девелопменту від программістів на якусь дичину на bash говні та pipes
дуже виручало

нуууднооо. але вкрай доречно

Я у телеграмі потім ще невеличкий додаток зробив, думаю може бути цікавим і читачам доу

Якщо нам треба послідовно прочитати з двох процессів, то можна виконати щось на кшталт

Дебаг функція (простіше читати ніж strace)

log_with_sleep() {
    local label=$1
    echo «$(date +„%H:%M:%S.%3N“) starting${label}»
    sleep 1
    echo «$(date +„%H:%M:%S.%3N“) end${label}»
}

Простий приклад послідовного виконання і читання (увага на дату):


(log_with_sleep "-1"; log_with_sleep "-2") | cat
# 10:08:06.474 starting-1
# 10:08:07.482 end-1
# 10:08:07.488 starting-2
# 10:08:08.498 end-2

Але якщо ми хочемо послідовно прочитати, але виконувати функції паралельно, то можна зробити так:


cat <(log_with_sleep 1) <(log_with_sleep 2)
# 10:10:05.529 starting1
# 10:10:06.538 end1
# 10:10:05.529 starting2
# 10:10:06.537 end2

А якщо треба паралельно читати і паралельно виконувати то можна зробити дуже просто:


(log_with_sleep "-1" & log_with_sleep "-2"&  wait) | cat
# 10:11:43.079 starting-1
# 10:11:43.080 starting-2
# 10:11:44.094 end-1
# 10:11:44.094 end-2

Усе це можна комибінувати


(log_with_sleep "1"; cat <(log_with_sleep "2") <(log_with_sleep "3"); (log_with_sleep "3" & log_with_sleep "4" & wait); log_with_sleep "5") | cat
# 10:30:25.737 starting1
# 10:30:26.748 end1
# 10:30:26.754 starting2
# 10:30:27.763 end2
# 10:30:26.754 starting3
# 10:30:27.765 end3
# 10:30:27.773 starting3
# 10:30:27.775 starting4
# 10:30:28.786 end4
# 10:30:28.789 end3
# 10:30:28.796 starting5
# 10:30:29.805 end5

Bash це чудовий інструмент для написання CI/CD пайплайнів і покриває 100% потреб, які у мене були.

Bash це чудовий інструмент для написання CI/CD пайплайнів і покриває 100% потреб, які у мене були.

Ну... відносно, коли у тебе CI/CD це невеличкий проект на Python (20-30 файликів по 1000 рядків), то мені страшно думати, що буде на bash).

Дякую за крутий матеріал. Чекатиму на нові статті!

Опис російською та посилання на неробочий сайт:
"""
Консультирую компании по DevOps, инфраструктуре, архитекутре. Основал и веду крупнейше в наших палестинах сообщество DevOps — ukrops.club

Выступаю на конференциях и митапах.
"""

Та людина нагадала мені (не дуже ввічливо, але нагадала) що я не обновляв опис профілю останніх років з шість

Класний матеріал, дякую

був дурний
досі дурний

Оскільки не помиляється лише той хто нічого не робить, то мабуть інколи бути від трішки до середньо дурним — це нормально)

І під кінець опитування — було б вам цікаво побачити більше постів про адвансед баш? Лишайте відповіді у коментарях.

Не то, чтобы лично мне это было бы нужно, но сам факт появления такой статьи на ДОУ радует чрезвычайно, продолжай.
Отдельный плюсик за иллюстрацию на Go.

Підписатись на коментарі