সরাসরি পর্বে যান
পর্ব ০৯ · ৮১-এর মধ্যে · মডিউল ২

Ops-এর shell — pipe, grep, awk, sed আর log বিশ্লেষণ

The Ops Shell — Pipes, grep, awk, sed and Log Analysis

Taught in Bangla, this lesson teaches the shell as the first tool in an incident: streams, redirection and exit status, pipes, grep with regular expressions, and cut, sort, uniq and awk for answering real questions from an access log, including p95. It follows one incident from report to root cause, then covers sed, find, xargs, quoting traps and when to leave a pipeline behind.

এই পর্বে যা শিখবেন

  • Stream, redirection আর exit status দিয়ে যেকোনো command-এর ইনপুট, আউটপুট আর সাফল্য নিয়ন্ত্রণ করা
  • grep, cut, sort, uniq, awk জুড়ে access log থেকে সংখ্যা বের করা
  • “500 error আসছে” রিপোর্ট থেকে ধাপে ধাপে মূল কারণে পৌঁছানো
  • sed আর awk দিয়ে config নিরাপদে বদলানো, find আর xargs দিয়ে অনেক ফাইলে কাজ করা
  • Quoting-এর ফাঁদ এড়ানো, আর AI-এর দেওয়া shell command চালানোর আগে যাচাই করা

১ · রাত দুইটার ফোন — shell কেন এখনো প্রথম হাতিয়ার

ধরুন আপনি একটি কাল্পনিক অনলাইন বাজার অ্যাপের on-call দায়িত্বে আছেন। ফোন এলো: “checkout-এ error আসছে।” আপনার হাতে একটি server আর তার access log. Dashboard হয়তো এখনো তৈরি হয়নি, বা সেটি আপনার প্রশ্নের উত্তর জানে না। তখন আপনার ভরসা shell: সেই program, যা আপনার লেখা command পড়ে অন্য program চালায়।

Server-এ ঢুকে কয়েক লাইনে আপনি উত্তর পেতে চান: কোন status code কতবার এসেছে? কোন IP সবচেয়ে বেশি request করছে? প্রতি মিনিটে কতগুলো 5xx? কোন path ধীর? p95 কত? এই পুরো পর্ব এই পাঁচটি প্রশ্নের উত্তর খুঁজবে।

আমরা একটি ২০ লাইনের নমুনা log ব্যবহার করব। প্রতিটি লাইন একটি HTTP request. শেষের তিনটি লাইন দেখুন:

203.0.113.52 - - [30/Oct/2026:14:04:21 +0600] "POST /api/orders HTTP/1.1" 504 157 5.003
203.0.113.18 - - [30/Oct/2026:14:04:35 +0600] "POST /api/orders HTTP/1.1" 500 182 2.118
203.0.113.60 - - [30/Oct/2026:14:04:52 +0600] "POST /api/orders HTTP/1.1" 500 182 2.092

Shell এই লাইনকে ফাঁকা জায়গায় ভাগ করে কয়েকটি ঘর (field) হিসেবে দেখতে পারে। প্রথম লাইনের ঘরগুলো এমন:

ঘরমানঅর্থ
$1203.0.113.52client-এর IP (এই পরিসর উদাহরণের জন্য সংরক্ষিত)
$4[30/Oct/2026:14:04:21সময়, স্থানীয় ঘড়িতে
$6, $7"POST, /api/ordersmethod আর path
$9504HTTP status code
$10157পাঠানো body-র bytes
$115.003request-এ লাগা সময়, সেকেন্ডে

Status code-এর প্রথম সংখ্যা বলে দেয় দায় কার: 2xx মানে সফল, 4xx মানে client-এর ভুল, 5xx মানে server-এর দিকের ভুল। তাই incident-এ 5xx-এর ওপর চোখ রাখা হয়।

এই log format-এর উৎস nginx, যা পর্ব ১৪-তে বিস্তারিত আসবে। nginx-এর ডিফল্ট combined format-এ সময়ের ঘর থাকে না। তাই নিচের মতো নিজের format বানাতে হয়:

log_format timed '$remote_addr - $remote_user [$time_local] '
                 '"$request" $status $body_bytes_sent $request_time';
access_log /var/log/nginx/access.log timed;

Field-এর নম্বর format-ভেদে বদলায়। $9 এখানে status, আপনার server-এ অন্য কিছু হতে পারে। তাই আগে tail -n 3 দিয়ে আসল লাইন দেখুন, তারপর command লিখুন। ভাঙা request-এ nginx "-" লিখতে পারে, তখন ঘরগুলো সরে যায়। ফলে ফাঁকা জায়গায় ভাগ করা হিসাব সেই লাইনে ভুল হয়।

২ · তিনটি stream, redirection আর exit status

ব্যাংকের একটি কাউন্টার ভাবুন। একটি জানালা দিয়ে কাগজ জমা পড়ে, আরেকটি দিয়ে রসিদ আসে, আর পাশে আছে অভিযোগ বই। রসিদ গ্রাহকের কাজে লাগে, অভিযোগ আলাদা থাকে। প্রতিটি Linux program-ও ঠিক তিনটি পথ নিয়ে জন্মায়, আর kernel প্রতিটিকে একটি ছোট সংখ্যা দেয়। এই সংখ্যার নাম file descriptor.

নম্বরনামকাজডিফল্টে কোথায়
0stdinইনপুট পড়াকীবোর্ড
1stdoutআসল ফলাফলterminal
2stderrভুল আর সতর্কবার্তাterminal

ফলাফল আর ভুল আলাদা পথে যায় বলেই pipe-এ ভুলের বার্তা ডেটার সঙ্গে মিশে যায় না। নিচের ছবিতে এই নকশাটি দেখুন।

Pipeline-এ তিনটি stream

stdout goes down the line, stderr goes to the terminal

grep, awk আর sort-এর pipeline-এ stdin, stdout আর stderr-এর পথ access.log stdin · fd 0 ফলাফল stdout · fd 1 grep awk sort pipe pipe stderr stderr Terminal: ভুলের বার্তা · fd 2 pipe বহন করে শুধু stdout stderr পাশ কাটিয়ে সরাসরি আপনার চোখে আসে
ছবি ৯.১ — ডেটা এগোয় নীল পথে, ভুলের বার্তা নামে লাল পথে। তাই মাঝের কোনো tool ভুল বললে সেই বার্তা পরের tool-এর ইনপুট নষ্ট করে না।

Redirection — পথ ঘুরিয়ে দেওয়া

Redirection মানে program শুরু হওয়ার আগেই shell তার কোনো পথকে অন্য জায়গায় জুড়ে দেয়। Program নিজে কিছুই জানে না।

লেখাঅর্থ
< in.txtstdin আসবে ফাইল থেকে
> out.txtstdout যাবে ফাইলে, পুরোনো লেখা মুছে
>> out.txtstdout ফাইলের শেষে যোগ হবে
2> err.txtstderr যাবে ফাইলে
2>&1fd 1 এখন যেখানে আছে, fd 2-কেও সেখানে পাঠানো
> /dev/nullআউটপুট ফেলে দেওয়া

সবচেয়ে বেশি গোলমাল হয় 2>&1-এর ক্রমে। Shell redirection পড়ে বাঁ থেকে ডানে, আর 2>&1 বলে “fd 1 এই মুহূর্তে যেখানে আছে, fd 2-কেও সেখানে পাঠান।” নিচের ধাপগুলো দেখুন।

1cmd > out.txt 2>&12cmd 2>&1 > out.txt
কোন পথ এখন কোথায়
fd 1
fd 2
  1. শুরু। কোনো redirection হয়নি। fd 1 আর fd 2, দুটোই terminal-এ।
  2. লাইন ১, > out.txt. fd 1 গেল out.txt-এ। fd 2 এখনো terminal-এ।
  3. লাইন ১, 2>&1. fd 1 এখন out.txt-এ, তাই fd 2-ও সেখানে গেল। দুটিই এক ফাইলে। এটিই সাধারণত আপনি চান।
  4. লাইন ২, 2>&1. নতুন command, সব আবার terminal-এ। fd 1 এখন terminal-এ, তাই fd 2 terminal-এই রইল। কিছুই বদলাল না।
  5. লাইন ২, > out.txt. শুধু fd 1 গেল ফাইলে। fd 2 আগের জায়গায়, terminal-এ। তাই ভুলের বার্তা ফাইলে ঢোকে না।

Exit status — program-এর শেষ কথা

প্রতিটি program শেষে একটি ছোট সংখ্যা ফেরত দেয়, যার নাম exit status. 0 মানে সফল, অন্য যেকোনো সংখ্যা মানে কোনো সমস্যা। Shell-এ শেষ command-এর মান থাকে $?-এ। && পরের command চালায় আগেরটি সফল হলে, || চালায় ব্যর্থ হলে। Script, cron আর CI, সবাই এই সংখ্যা দেখেই সিদ্ধান্ত নেয় (পর্ব ১০)।

কয়েকটি মান প্রচলিত: 127 মানে command পাওয়া যায়নি, 126 মানে চালানোর অনুমতি নেই, আর 128 + n মানে signal n-এ program থেমেছে। grep-এর নিয়ম আলাদা করে মনে রাখুন: 0 মানে মিলেছে, 1 মানে মেলেনি, 2 মানে আসল ভুল।

cd "$(mktemp -d)"
printf 'line one\nline two\n' > in.txt

echo "== stdin from a file =="
wc -l < in.txt

echo "== stdout and stderr go to different files =="
ls in.txt /no/such/file > out.txt 2> err.txt
echo "exit status: $?"
echo "out.txt: $(cat out.txt)"
echo "err.txt: $(cat err.txt)"

echo "== > overwrites, >> appends =="
echo first > f.txt; echo second > f.txt; echo third >> f.txt
cat f.txt

echo "== order of 2>&1 matters =="
ls /no/such/file > a.txt 2>&1
ls /no/such/file 2>&1 > b.txt | sed 's/^/pipe got: /'
wc -l < a.txt; wc -l < b.txt

echo "== grep status: 0 found, 1 not found, 2 error =="
grep -q 'two' in.txt; echo "$?"
grep -q 'three' in.txt; echo "$?"
grep -q 'x' /no/such/file 2> /dev/null; echo "$?"

echo "== a pipeline reports only its last command =="
grep 'three' in.txt | wc -l; echo "${PIPESTATUS[@]}"
set -o pipefail
grep 'three' in.txt | wc -l; echo "with pipefail: $?"

ls একটি ফাইল পেল, আরেকটি পেল না। তাই out.txt: in.txt এলো ঠিক পথে, আর err.txt: ls: cannot access '/no/such/file': No such file or directory এলো ভুলের পথে। exit status: 2 জানাল আংশিক ব্যর্থতাও ব্যর্থতা।

f.txt-এ আছে second আর third: > মুছে লেখে, >> যোগ করে। a.txt-এ ১ লাইন, b.txt-এ ০ লাইন, ঠিক উপরের stepper-এর মতো। দ্বিতীয় লাইনে stderr গেল pipe-এ, তাই ছাপা হলো pipe got: ls: cannot access ....

শেষ অংশটি সবচেয়ে গুরুত্বপূর্ণ। grep মেলাতে পারেনি, কিন্তু PIPESTATUS ছাপল 1 0: grep 1, wc 0. Pipeline-এর মোট মান শেষ command-এর, তাই ব্যর্থতা ঢাকা পড়ে। pipefail চালু করলে ছাপা হলো with pipefail: 1.

৩ · Pipe — কারখানার লাইন আর ছোট tool-এর দর্শন

গার্মেন্টস কারখানায় একজন কাটেন, পরের জন সেলাই করেন, তারপরের জন বোতাম লাগান, শেষে একজন ভাঁজ করেন। কেউ পুরো শার্ট বানান না। প্রত্যেকে একটি কাজ ভালো জানেন, আর আগের জনের হাত থেকে কাজ নেন। Pipe (|) ঠিক এই লাইন: আগের program-এর stdout সরাসরি পরের program-এর stdin হয়ে যায়।

একটি বড় পার্থক্য আছে। Pipe-এর প্রতিটি অংশ একই সময়ে চলে, আলাদা process হিসেবে। ডেটা বইতে বইতে কাজ হয়, আগে পুরো ফাইল শেষ হওয়ার অপেক্ষা থাকে না। মাঝে kernel-এর একটি ছোট buffer থাকে। পরের জন ধীর হলে buffer ভরে যায়, আর আগের জন লিখতে গিয়ে থেমে যায়। সেলাই লাইনে কাপড় জমে গেলে কাটার টেবিলও থামে, এখানেও তাই।

একটু ইতিহাস। ১৯৬৪ সালে Bell Labs-এর Doug McIlroy একটি memo-তে লেখেন, program জোড়ার উপায় থাকা উচিত, বাগানের পানির hose-এর মতো। প্রায় নয় বছর পরে, ১৯৭৩ সালের দিকে, Ken Thompson Unix-এ pipe যোগ করেন। পরে McIlroy নিজেই Unix-এর দর্শনকে এভাবে বলেন: এমন program লিখুন যা একটি কাজ ভালোভাবে করে; এমন program লিখুন যা একসাথে কাজ করে; এমন program লিখুন যা text stream সামলায়, কারণ সেটিই সর্বজনীন interface. শেষের সংক্ষিপ্ত রূপটি Peter Salus-এর A Quarter Century of Unix (১৯৯৪) বইয়ে পাওয়া যায়। একেই সাধারণত Unix philosophy বলা হয়।

এবার একটি আসল প্রশ্ন: “5xx পাওয়া client-দের মধ্যে কে সবচেয়ে বেশি ভুগছে?” নিচের pipeline-এর প্রতিটি অংশ পেরোনোর পর ডেটার চেহারা দেখুন। “পরে” চাপুন।

grep -E '" 5[0-9]{2} ' access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -3
grep-এর পরে২০ থেকে ৭ লাইন
203.0.113.33 - - [30/Oct/2026:14:03:04 +0600] "POST /api/orders HTTP/1.1" 500 182 2.104
203.0.113.60 - - [30/Oct/2026:14:03:22 +0600] "POST /api/orders HTTP/1.1" 500 182 2.087
203.0.113.41 - - [30/Oct/2026:14:03:49 +0600] "POST /api/orders HTTP/1.1" 500 182 2.131
203.0.113.33 - - [30/Oct/2026:14:04:02 +0600] "POST /api/orders HTTP/1.1" 500 182 2.095
203.0.113.52 - - [30/Oct/2026:14:04:21 +0600] "POST /api/orders HTTP/1.1" 504 157 5.003
203.0.113.18 - - [30/Oct/2026:14:04:35 +0600] "POST /api/orders HTTP/1.1" 500 182 2.118
203.0.113.60 - - [30/Oct/2026:14:04:52 +0600] "POST /api/orders HTTP/1.1" 500 182 2.092
awk-এর পরেশুধু IP, ৭ লাইন
203.0.113.33
203.0.113.60
203.0.113.41
203.0.113.33
203.0.113.52
203.0.113.18
203.0.113.60
sort-এর পরেএকই IP পাশাপাশি
203.0.113.18
203.0.113.33
203.0.113.33
203.0.113.41
203.0.113.52
203.0.113.60
203.0.113.60
uniq -c-এর পরে৫টি আলাদা IP, সামনে গণনা
      1 203.0.113.18
      2 203.0.113.33
      1 203.0.113.41
      1 203.0.113.52
      2 203.0.113.60
sort -rn-এর পরেবড় সংখ্যা ওপরে
      2 203.0.113.60
      2 203.0.113.33
      1 203.0.113.52
      1 203.0.113.41
      1 203.0.113.18
head -3-এর পরেচূড়ান্ত উত্তর
      2 203.0.113.60
      2 203.0.113.33
      1 203.0.113.52
  1. ছাঁকনি। grep ২০ লাইন থেকে রাখল সেই ৭ লাইন, যেখানে উদ্ধৃতি-চিহ্নের ঠিক পরে একটি ফাঁকা, তারপর 5 দিয়ে শুরু তিন অঙ্কের সংখ্যা। ৬টি 500, ১টি 504.
  2. কাটাই। awk প্রতিটি লাইনের প্রথম ঘর রাখল। বাকি সব বাদ। লাইনের সংখ্যা এখনো ৭।
  3. সাজানো। sort একই IP-কে পাশাপাশি আনল। পরের ধাপের জন্য এটি অপরিহার্য।
  4. গণনা। uniq -c পাশাপাশি একই লাইনকে এক লাইনে মিলিয়ে সামনে সংখ্যা বসাল। ৭ লাইন থেকে ৫ লাইন।
  5. র‍্যাংকিং। sort -rn সংখ্যা হিসেবে (-n) উল্টো ক্রমে (-r) সাজাল। দুই IP-র গণনা সমান, তাই পুরো লাইনের তুলনায় .60 এলো .33-এর আগে।
  6. উত্তর। head -3 ওপরের তিন লাইন রাখল। কোনো একজনই ভুগছেন না: সর্বোচ্চ গণনা মাত্র ২। সমস্যা ছড়ানো, তাই একজন client-কে দোষ দেওয়ার সুযোগ নেই।
ছাঁকনি | কাটাই | সাজানো | গণনা | র‍্যাংক | সেরা কয়েকটি
এই এক ছাঁচেই Ops-এর অর্ধেক প্রশ্নের উত্তর মেলে।
মূল ধারণা

Pipeline লেখার সেরা অভ্যাস হলো একটি করে অংশ যোগ করা। প্রথমে grep চালান, চোখে দেখুন। তারপর awk জুড়ুন, আবার দেখুন। মাঝের ধাপ ভুল হলে শেষের সংখ্যাও ভুল হবে, অথচ দেখতে ঠিকই লাগবে।

৪ · grep আর regular expression — ঠিক লাইনটি খুঁজে নেওয়া

grep ফাইলের প্রতিটি লাইন পড়ে, আর নকশার সঙ্গে মিলে গেলে সেই লাইন ছাপে। নকশাটি লেখা হয় regular expression (regex) দিয়ে। Ken Thompson-এর বানানো এই tool-এর নামটি এসেছে পুরোনো ed editor-এর g/re/p command থেকে: global, regular expression, print. Regex হলো খোঁজার ছাঁচ, যেমন “5 দিয়ে শুরু তিন অঙ্ক”।

চিহ্নঅর্থউদাহরণ
.যেকোনো একটি অক্ষরa.c মেলে abc-তে
\.আসল বিন্দু0\.5
^, $লাইনের শুরু, শেষ^203\.
[0-9]একটি অঙ্ক[45][0-9][0-9]
*, +, ?০ বা বেশি, ১ বা বেশি, ০ বা ১ বার[0-9]+
{2}ঠিক ২ বার5[0-9]{2}
(a|b)a অথবা b(GET|POST)

+, ?, |, () আর {}-কে এই অর্থে পেতে grep -E (extended regex) লাগে। প্রতিদিনের কাজে কয়েকটি option ঘুরেফিরে আসে:

Optionকাজ
-cলাইন না ছেপে কতটি মিলেছে তা গোনা
-vযা মেলে না, সেগুলো দেখানো (health check বাদ দিতে কাজের)
-iবড়-ছোট হাতের অক্ষরের পার্থক্য উপেক্ষা
-nলাইন নম্বরসহ
-oপুরো লাইন নয়, শুধু মেলা অংশ
-FRegex নয়, হুবহু লেখা খোঁজা
-A 2, -B 2মেলা লাইনের পরের, আগের ২ লাইনও
-rফোল্ডারের ভেতরে সব ফাইলে

নকশা যথেষ্ট কড়া হওয়া চাই। grep 500 শুধু status 500 ধরে না, bytes-এর ঘরে 1500-ও ধরে। আমাদের log-এ আসল 500 আছে ৬টি, অথচ grep -c 500 দেখাবে ১৩। একইভাবে grep 203.0.113.7 ধরে 203.0.113.70-ও, কারণ . যেকোনো অক্ষর, আর শেষে কোনো সীমা নেই। ভালো রূপ হলো grep -F '203.0.113.7 ' বা awk '$1 == "203.0.113.7"'.

৫ · cut, sort, uniq, wc, head, tail, tr — ছোট হাতিয়ারের বাক্স

এগুলো দেখতে সরল, কিন্তু প্রতিটির একটি করে নিয়ম না জানলে ভুল সংখ্যা আসবে।

Toolকাজযা মনে রাখবেন
cut -d' ' -f1নির্দিষ্ট ঘর কেটে নেওয়াদুই ফাঁকা পরপর থাকলে মাঝে খালি ঘর ধরে। তখন awk ভালো।
sortসাজানোডিফল্টে লেখা হিসেবে, তাই 9 আসে 10-এর পরে। সংখ্যায় -n, উল্টো -r, নির্দিষ্ট ঘরে -k.
uniq -cপাশাপাশি একই লাইন গোনাশুধু পাশাপাশি। তাই আগে sort.
wc -lলাইন গোনাআসলে newline গোনে। ফাইলের শেষে newline না থাকলে ১ কম আসে।
head -n 5, tail -n 5শুরুর বা শেষের লাইনtail -f নতুন লাইন অনুসরণ করে (অংশ ৭)।
trঅক্ষর বদলানো বা বাদ দেওয়াফাইলের নাম নেয় না, শুধু stdin. tr -s ' ' বাড়তি ফাঁকা চেপে দেয়।

এবার প্রথম তিনটি প্রশ্নের উত্তর। সব অংশই আগের ছাঁচের। Log-টি ব্লকের ভেতরেই তৈরি হয়, তাই ফল প্রতিবার একই।

export LC_ALL=C
cd "$(mktemp -d)"
cat > access.log <<'EOF'
203.0.113.7 - - [30/Oct/2026:14:01:05 +0600] "GET /api/products HTTP/1.1" 200 1500 0.041
203.0.113.7 - - [30/Oct/2026:14:01:12 +0600] "GET /api/products HTTP/1.1" 200 1500 0.038
203.0.113.25 - - [30/Oct/2026:14:01:33 +0600] "POST /api/login HTTP/1.1" 401 96 0.033
203.0.113.60 - - [30/Oct/2026:14:01:48 +0600] "POST /api/orders HTTP/1.1" 200 240 0.210
203.0.113.7 - - [30/Oct/2026:14:01:55 +0600] "GET /api/products HTTP/1.1" 200 1500 0.044
203.0.113.41 - - [30/Oct/2026:14:02:03 +0600] "GET /api/cart HTTP/1.1" 200 812 0.120
203.0.113.7 - - [30/Oct/2026:14:02:17 +0600] "GET /api/products HTTP/1.1" 200 1500 0.040
203.0.113.60 - - [30/Oct/2026:14:02:26 +0600] "POST /api/orders HTTP/1.1" 200 240 0.230
203.0.113.7 - - [30/Oct/2026:14:02:38 +0600] "GET /api/products HTTP/1.1" 200 1500 0.036
203.0.113.18 - - [30/Oct/2026:14:02:51 +0600] "GET /api/cart HTTP/1.1" 200 812 0.115
203.0.113.33 - - [30/Oct/2026:14:03:04 +0600] "POST /api/orders HTTP/1.1" 500 182 2.104
203.0.113.7 - - [30/Oct/2026:14:03:11 +0600] "GET /api/products HTTP/1.1" 200 1500 0.042
203.0.113.60 - - [30/Oct/2026:14:03:22 +0600] "POST /api/orders HTTP/1.1" 500 182 2.087
203.0.113.18 - - [30/Oct/2026:14:03:37 +0600] "GET /api/cart HTTP/1.1" 200 812 0.119
203.0.113.41 - - [30/Oct/2026:14:03:49 +0600] "POST /api/orders HTTP/1.1" 500 182 2.131
203.0.113.33 - - [30/Oct/2026:14:04:02 +0600] "POST /api/orders HTTP/1.1" 500 182 2.095
203.0.113.7 - - [30/Oct/2026:14:04:10 +0600] "GET /api/products HTTP/1.1" 200 1500 0.039
203.0.113.52 - - [30/Oct/2026:14:04:21 +0600] "POST /api/orders HTTP/1.1" 504 157 5.003
203.0.113.18 - - [30/Oct/2026:14:04:35 +0600] "POST /api/orders HTTP/1.1" 500 182 2.118
203.0.113.60 - - [30/Oct/2026:14:04:52 +0600] "POST /api/orders HTTP/1.1" 500 182 2.092
EOF
echo "== trap: grep 500 also matches bytes =="; grep -c 500 access.log
echo "== exact status 500 =="; grep -c '" 500 ' access.log
echo "== status codes =="; awk '{print $9}' access.log | sort | uniq -c | sort -rn
echo "== top IPs =="; cut -d' ' -f1 access.log | sort | uniq -c | sort -rn | head -3
echo "== 5xx per minute =="
awk '$9 ~ /^5/ {print substr($4, 14, 5)}' access.log | sort | uniq -c

grep -c 500 দিল 13, আর কড়া নকশা " 500 দিল 6. Status-এর হিসাব: 12 200, 6 500, 1 504, 1 401, মোট ২০। মোট মিলিয়ে দেখা নিজেই একটি যাচাই।

Top IP হলো 7 203.0.113.7, তারপর 4 203.0.113.60. মিনিটের হিসাব: 3 14:03, 4 14:04. substr($4, 14, 5) [30/Oct/2026:14:03:04-এর ১৪তম অক্ষর থেকে ৫টি অক্ষর নিল, অর্থাৎ ঘণ্টা আর মিনিট।

export LC_ALL=C ক্রমকে ভাষা-নির্ভর না রেখে byte-এর ক্রমে বাঁধে। ফলে একই ইনপুটে সব মেশিনে একই ক্রম আসে। বড় ফাইলে sort সাধারণত দ্রুতও হয়।

লক্ষ করুন, সবচেয়ে বেশি request করা IP হলো .7, কিন্তু সেটি 5xx-এর তালিকায় নেই। “সবচেয়ে বেশি” মানেই “দোষী” নয়। এই কথা অংশ ৭-এ কাজে লাগবে।

৬ · awk — ঘর, হিসাব আর p95

awk একটি ছোট ভাষা, যা লাইনে লাইনে চলে। ১৯৭৭ সালে Bell Labs-এ Alfred Aho, Peter Weinberger আর Brian Kernighan এটি বানান। নামটি তাঁদের পদবির আদ্যক্ষর। এর গঠন একটিই: শর্ত { কাজ }. প্রতিটি লাইনে শর্ত সত্য হলে কাজটি হয়।

লেখাঅর্থ
$1, $9, $0১ম ঘর, ৯ম ঘর, পুরো লাইন
NF, $NFঘরের সংখ্যা, শেষ ঘর
NRএখন পর্যন্ত পড়া লাইনের সংখ্যা
-F','ঘর ভাগের চিহ্ন বদলানো
$9 ~ /^5/৯ম ঘর নকশার সঙ্গে মেলে
END { ... }সব লাইন পড়ার পরে একবার চলে
n[key]++নাম ধরে গোনা (associative array)

খাতায় হিসাব রাখার মতো ভাবুন। দোকানি প্রতিটি মেমো পড়েন, পণ্যের নামের পাতায় যোগফল বসান, আর দিনের শেষে পাতা ধরে মোট লেখেন। awk-এর array সেই খাতা, END সেই দিনের শেষ।

Percentile — গড় কেন মিথ্যা বলে

“কোন path ধীর” প্রশ্নে গড় (average) প্রথম ধাপ, শেষ ধাপ নয়। আমাদের ২০টি request-এর গড় ০.৯৩৭ সেকেন্ড, অথচ ঠিক এই সময়ের কাছাকাছি একটি request-ও নেই। অর্ধেক request ০.১২ সেকেন্ডের মধ্যে শেষ হয়েছে, আর ব্যর্থগুলো ২ সেকেন্ডের বেশি। গড় দুই দলকে মিশিয়ে এমন একটি সংখ্যা বানিয়েছে, যা কারো অভিজ্ঞতা নয়।

p95 হলো সেই মান, যার সমান বা নিচে ৯৫% request আছে। সাজানো তালিকায় এটি বের করার সহজ উপায়ের নাম nearest-rank: rank = ⌈০.৯৫ × n⌉, অর্থাৎ ০.৯৫ × n-কে ওপরের পূর্ণসংখ্যায় তোলা। n = ২০ হলে rank = ⌈১৯⌉ = ১৯, মানে সাজানো তালিকার ১৯তম মান। Bash-এর পূর্ণসংখ্যার হিসাবে এটি লেখা যায় (n * 95 + 99) / 100: (১৯০০ + ৯৯) / ১০০ = ১৯।

export LC_ALL=C
cd "$(mktemp -d)"
cat > access.log <<'EOF'
203.0.113.7 - - [30/Oct/2026:14:01:05 +0600] "GET /api/products HTTP/1.1" 200 1500 0.041
203.0.113.7 - - [30/Oct/2026:14:01:12 +0600] "GET /api/products HTTP/1.1" 200 1500 0.038
203.0.113.25 - - [30/Oct/2026:14:01:33 +0600] "POST /api/login HTTP/1.1" 401 96 0.033
203.0.113.60 - - [30/Oct/2026:14:01:48 +0600] "POST /api/orders HTTP/1.1" 200 240 0.210
203.0.113.7 - - [30/Oct/2026:14:01:55 +0600] "GET /api/products HTTP/1.1" 200 1500 0.044
203.0.113.41 - - [30/Oct/2026:14:02:03 +0600] "GET /api/cart HTTP/1.1" 200 812 0.120
203.0.113.7 - - [30/Oct/2026:14:02:17 +0600] "GET /api/products HTTP/1.1" 200 1500 0.040
203.0.113.60 - - [30/Oct/2026:14:02:26 +0600] "POST /api/orders HTTP/1.1" 200 240 0.230
203.0.113.7 - - [30/Oct/2026:14:02:38 +0600] "GET /api/products HTTP/1.1" 200 1500 0.036
203.0.113.18 - - [30/Oct/2026:14:02:51 +0600] "GET /api/cart HTTP/1.1" 200 812 0.115
203.0.113.33 - - [30/Oct/2026:14:03:04 +0600] "POST /api/orders HTTP/1.1" 500 182 2.104
203.0.113.7 - - [30/Oct/2026:14:03:11 +0600] "GET /api/products HTTP/1.1" 200 1500 0.042
203.0.113.60 - - [30/Oct/2026:14:03:22 +0600] "POST /api/orders HTTP/1.1" 500 182 2.087
203.0.113.18 - - [30/Oct/2026:14:03:37 +0600] "GET /api/cart HTTP/1.1" 200 812 0.119
203.0.113.41 - - [30/Oct/2026:14:03:49 +0600] "POST /api/orders HTTP/1.1" 500 182 2.131
203.0.113.33 - - [30/Oct/2026:14:04:02 +0600] "POST /api/orders HTTP/1.1" 500 182 2.095
203.0.113.7 - - [30/Oct/2026:14:04:10 +0600] "GET /api/products HTTP/1.1" 200 1500 0.039
203.0.113.52 - - [30/Oct/2026:14:04:21 +0600] "POST /api/orders HTTP/1.1" 504 157 5.003
203.0.113.18 - - [30/Oct/2026:14:04:35 +0600] "POST /api/orders HTTP/1.1" 500 182 2.118
203.0.113.60 - - [30/Oct/2026:14:04:52 +0600] "POST /api/orders HTTP/1.1" 500 182 2.092
EOF
echo "== avg, max, count per path =="
awk '{ k = $6 " " $7; gsub(/"/, "", k); n[k]++; s[k] += $11; if ($11 > m[k]) m[k] = $11 }
END { for (k in n) printf "%.3f %.3f %d %s\n", s[k]/n[k], m[k], n[k], k }' access.log | sort -rn
echo "== slowest 3 requests =="
sort -t' ' -k11,11nr access.log | head -3 | awk '{print $11, $9, $7}'
echo "== p95 by sort =="
n=$(wc -l < access.log); rank=$(( (n * 95 + 99) / 100 ))
echo "n=$n rank=$rank"
awk '{print $11}' access.log | sort -n | sed -n "${rank}p"

প্রথম লাইন 2.008 5.003 9 POST /api/orders: গড় ২.০০৮ সেকেন্ড, সর্বোচ্চ ৫.০০৩, ৯টি request. বাকি path-গুলোর গড় ০.১২ সেকেন্ডের নিচে। ধীর জায়গা তাই একটিই।

সবচেয়ে ধীর তিনটি: 5.003 504 /api/orders, 2.131 500 /api/orders, 2.118 500 /api/orders. -k11,11nr মানে শুধু ১১তম ঘর ধরে সংখ্যার হিসাবে উল্টো ক্রমে সাজানো।

n=20 rank=19, আর উত্তর 2.131. অর্থাৎ ৯৫% request ২.১৩১ সেকেন্ডের মধ্যে শেষ হয়েছে। sed -n "19p" শুধু ১৯তম লাইন ছাপে। এখানে double quote জরুরি, যাতে shell $rank-এর মান বসাতে পারে। সংখ্যা মাত্র ২০টি, তাই এই p95 একটি মোটা আন্দাজ। পরিসংখ্যানে ভরসা পেতে অনেক বেশি request লাগে।

আরও গভীরে: ফাঁকায় ভাগ করা ঘর ভাঙা request-এ সরে যায়। তাই বড় log-এ awk -F'"' দিয়ে উদ্ধৃতি-চিহ্নে ভাগ করা নিরাপদ, তখন $3-এর ভেতরে থাকে status আর bytes. কোটি লাইনে sort ধীর হয়, কারণ পুরো ডেটা সাজাতে হয়, আর GNU sort দরকারে temp ফাইলে লেখে। Production-এ percentile সাধারণত histogram থেকে আন্দাজ করা হয় (পর্ব ৬৩), যাতে সব মান জমাতে না হয়। উল্লেখ্য, nginx-এর $request_time client-এর দিকের ধীর গতিও ধরে; backend-এর ভাগ আলাদা দেখতে $upstream_response_time লাগে।

৭ · একটি incident — রিপোর্ট থেকে কারণ পর্যন্ত

এবার সব হাতিয়ার একসাথে। ১৪:০৫-এ রিপোর্ট এলো: “checkout-এ 500 error.” Support-এর কথা অনুমান, log হলো প্রমাণ। ভালো তদন্তে প্রতিটি ধাপ একটি প্রশ্ন, আর প্রতিটি উত্তর পরের প্রশ্ন ঠিক করে। এগোনোর ক্রম: কী ঘটছে, কখন থেকে, কোথায়, কাকে, আর কী বদলেছে।

তদন্তের খাতা
প্রশ্ন
জানা গেল
status গণনা৪ রকম কোড
$ awk '{print $9}' access.log | sort | uniq -c | sort -rn
     12 200
      6 500
      1 504
      1 401
প্রতি মিনিটে 5xx১৪:০১ আর ১৪:০২ নেই
$ awk '$9 ~ /^5/ {print substr($4, 14, 5)}' access.log | sort | uniq -c
      3 14:03
      4 14:04
কোন pathএকটিই
$ awk '$9 ~ /^5/ {print $6, $7}' access.log | sort | uniq -c
      7 "POST /api/orders
কতজনtop IP-র status
$ awk '$9 ~ /^5/ {print $1}' access.log | sort -u | wc -l
5
$ awk '$1 == "203.0.113.7" {print $9}' access.log | sort | uniq -c
      7 200
/api/orders-এর সময়সময় · status · সেকেন্ড
$ awk '$7 == "/api/orders" {print substr($4, 14, 8), $9, $11}' access.log
14:01:48 200 0.210
14:02:26 200 0.230
14:03:04 500 2.104
14:03:22 500 2.087
14:03:49 500 2.131
14:04:02 500 2.095
14:04:21 504 5.003
14:04:35 500 2.118
14:04:52 500 2.092
app.logএকই সময়ের লাইন
$ grep -E 'ERROR|WARN' app.log | head -2
14:03:04 ERROR orders db pool timeout after 2000ms (pool_size=5)
14:03:22 ERROR orders db pool timeout after 2000ms (pool_size=5)
deploy.logপ্রথম error-এর ৩১ সেকেন্ড আগে
$ grep -n 'pool_size' deploy.log
2:14:02:33 config change pool_size: 50 -> 5
  1. আগে নিশ্চিত হওয়া। ২০ লাইনে ৭টি 5xx: ৬টি 500 আর ১টি 504. সমস্যা সত্যি, আর শুধু 500 নয়। grep ' 500 ' দিলে 504 বাদ পড়ত।
  2. সময় বাঁধা। ১৪:০৩-এ ৩টি, ১৪:০৪-এ ৪টি। ১৪:০১ আর ১৪:০২-তে একটিও নেই। অর্থাৎ শুরু ১৪:০২-এর শেষে বা ১৪:০৩-এর শুরুতে। এখন খোঁজার জায়গা ছোট হলো।
  3. জায়গা বাঁধা। ৭টি 5xx-ই POST /api/orders-এ। /api/cart বা /api/products ঠিক আছে। পুরো server পড়ে যায়নি।
  4. কে ভুগছে। ৫টি আলাদা IP, তাই এটি একজন client-এর ভুল নয়। সবচেয়ে বেশি request করা .7-এর ৭টি request-ই 200, তাই তাকে block করে লাভ নেই। এটি লোভনীয় কিন্তু ভুল পথ।
  5. ধরন বোঝা। আগে ০.২১ আর ০.২৩ সেকেন্ড, এখন প্রায় ২.১ সেকেন্ড। এত সমান সংখ্যা সাধারণত কোনো timeout-এর ইঙ্গিত। ১৪:০৪:২১-এর 504 আলাদা: এই কাল্পনিক setup-এ nginx ৫ সেকেন্ড অপেক্ষা করে নিজেই হাল ছেড়েছে।
  6. পেছনের সার্ভিস। upstream app-এর log বলছে: DB-র connection pool থেকে connection পেতে ২০০০ ms অপেক্ষা করে ব্যর্থ। ২ সেকেন্ডের timeout আর কিছু কাজ মিলে ২.১ সেকেন্ড। সংখ্যাটি মিলে গেল।
  7. কী বদলেছে। ১৪:০২:৩৩-এ deploy pool_size ৫০ থেকে ৫ করেছে। প্রথম error ১৪:০৩:০৪-এ। সময় মেলে, প্রক্রিয়াও মেলে। এটিই সবচেয়ে সম্ভাব্য মূল কারণ, তবে প্রমাণ হবে ঠিক করার পরে error থামলে।
মূল ধারণা

Incident-এ সবচেয়ে দামি প্রশ্ন: “ঠিক আগে কী বদলেছিল?” বেশিরভাগ তদন্তে প্রথম কাজ প্রভাব কমানো, যেমন rollback. গভীর কারণ খোঁজা চলে তারপর। পুরো প্রক্রিয়া আসবে পর্ব ৬৭-তে।

লাইভ দেখা — tail -f

ঠিক করার পরে আপনি নিজের চোখে দেখতে চাইবেন error থেমেছে কি না। tail -f ফাইলের শেষে বসে থাকে, আর নতুন লাইন এলেই ছাপে। এটি কখনো নিজে শেষ হয় না, তাই থামাতে Ctrl+C লাগে। সে কারণেই এটি এখানে চালানোর ব্লকে নেই।

tail -F /var/log/nginx/access.log | grep --line-buffered -E '" 5[0-9]{2} '

দুটি শব্দের দাম আছে। বড় হাতের -F ফাইলের নাম ধরে রাখে: log rotation-এ পুরোনো ফাইলের নাম বদলে নতুন ফাইল এলেও সে নতুনটি ধরে। ছোট -f পুরোনো ফাইলেই আটকে থাকতে পারে। আর --line-buffered ছাড়া grep আউটপুট জমিয়ে বড় টুকরোয় পাঠাতে পারে, কারণ তার stdout terminal নয়, pipe. একে stdout buffering বলে। তখন আপনি ভাববেন log থেমে গেছে, অথচ ডেটা buffer-এ বসে আছে। Pipeline-এর শেষ অংশ terminal-এ লিখলে সমস্যা কম, কিন্তু পরে আরেকটি tool জুড়লে এই সমস্যা দেখা দেয়।

৮ · sed, find, xargs আর quoting-এর ফাঁদ

sed (stream editor) লাইনে লাইনে লেখা বদলায়। ১৯৭০-এর দশকে Bell Labs-এ Lee McMahon এটি বানান। সবচেয়ে প্রচলিত রূপ s/খোঁজ/বদল/. শেষে g দিলে লাইনের সব মিল বদলায়, নইলে শুধু প্রথমটি। sed -n '5p' শুধু ৫ম লাইন ছাপে। -E দিলে extended regex চলে।

আমাদের incident-এর সমাধান হিসেবে app.conf-এ pool_size আবার ৫০ করতে হবে। Config বদলানো হলো ধারালো ছুরি, তাই ধাপগুলো ধীরে দেখুন।

app.confআগে দেখা
$ cat app.conf
# orders service
pool_size = 5
pool_timeout_ms = 2000
max_body_mb = 25
log_level = info
ঢিলা নকশাদুই জায়গা বদলে গেল
$ sed 's/5/50/' app.conf
# orders service
pool_size = 50
pool_timeout_ms = 2000
max_body_mb = 250
log_level = info
কড়া নকশা, শুধু দেখাফাইল অপরিবর্তিত
$ sed 's/^pool_size = 5$/pool_size = 50/' app.conf | diff app.conf -
2c2
< pool_size = 5
---
> pool_size = 50
প্রয়োগ, backup-সহ.bak রইল
$ sed -i.bak 's/^pool_size = 5$/pool_size = 50/' app.conf
$ diff app.conf.bak app.conf
2c2
< pool_size = 5
---
> pool_size = 50
awk দিয়ে যাচাই আর বদলঘর ধরে হুবহু তুলনা
$ awk -F' = ' '$1 == "pool_size" {print $2}' app.conf
50
$ awk -F' = ' -v OFS=' = ' \
    '$1 == "pool_timeout_ms" {$2 = 3000} {print}' \
    app.conf > app.conf.new
$ diff app.conf app.conf.new
3c3
< pool_timeout_ms = 2000
---
> pool_timeout_ms = 3000
  1. আগে পড়া। বদলানোর আগে ফাইলটি দেখুন। লক্ষ্য ২ নম্বর লাইন, pool_size = 5. লক্ষ করুন max_body_mb = 25-তেও একটি 5 আছে।
  2. ঢিলা নকশার ফল। s/5/50/ প্রতিটি লাইনের প্রথম 5 বদলায়। pool_size ঠিক হলো, কিন্তু max_body_mb হয়ে গেল 250. -i না থাকায় ফাইল বদলায়নি, আউটপুট দেখেই ভুল ধরা পড়ল।
  3. কড়া নকশা। ^ আর $ পুরো লাইন বেঁধে দিল। diff-এর শেষের - মানে stdin. একটিই লাইন বদলাবে, এটি দেখা গেল। ফাইল এখনো আগের মতো।
  4. প্রয়োগ। -i.bak ফাইল বদলায়, আর পুরোনোটি app.conf.bak নামে রাখে। GNU sed ভেতরে নতুন ফাইল লিখে আগেরটির জায়গায় বসায়। diff নিশ্চিত করল বদল ঠিক এক লাইন। ভুল হলে এক command-এ ফেরা যায়।
  5. awk-এর পথ। -F' = ' লাইনকে নাম আর মানে ভাগ করে। প্রথম command দেখাল মান এখন 50. দ্বিতীয়টি নাম ধরে হুবহু তুলনা করে শুধু timeout বদলাল, তাই আংশিক মিলের ভয় নেই। awk নিজের ফাইলে সরাসরি লেখে না, তাই ফল নতুন ফাইলে নেওয়া হলো।

Server-এ হাতে বদলানো config পরের deploy-তে মুছে যেতে পারে, আর Git-এর সত্যের সঙ্গে server-এর অমিল তৈরি হয়। একে drift বলে। জরুরি মুহূর্তে হাতে বদলালেও লিখে রাখুন, আর পরে আসল উৎসেই ঠিক করুন (পর্ব ৫১)। এখানে উৎস ছিল deploy, তাই rollback-ই প্রায়ই ভালো সমাধান। Bash script-এ এই কাজ নিরাপদে করার উপায় আসবে পর্ব ১০-এ।

find আর xargs — অনেক ফাইলে একসাথে

find ফোল্ডারের গাছ ঘুরে শর্ত মিলিয়ে ফাইল খোঁজে: -type f মানে সাধারণ ফাইল, -name '*.log' নামের নকশা, -newermt নির্দিষ্ট তারিখের পরে বদলানো, -size +100M আকার। xargs stdin-এর লেখা নিয়ে সেগুলোকে অন্য command-এর argument বানায়। কারণ grep বা rm-এর মতো tool ফাইলের নাম চায় argument-এ, stdin-এ নয়।

Quoting — shell কী দেখে

Command চালানোর আগে shell লেখাটি বদলায়। Quoting ঠিক করে সে কতটা বদলাতে পারবে।

লেখাShell যা করেযখন ব্যবহার করবেন
$nameমান বসায়, তারপর ফাঁকা জায়গায় ভাঙে, তারপর glob চালায়প্রায় কখনোই নয়
"$name"মান বসায়, একটি শব্দই রাখেসব variable-এ
'$name'কিছুই বদলায় না, হুবহু লেখাawk, sed, grep-এর নকশায়
*.logফাইলের নামে ভাঙে (globbing)find -name-এ quote ছাড়া নয়

এই ভাঙাকে বলে word splitting. awk '{print $1}'-তে single quote না দিলে shell $1-কে নিজের variable ভেবে বসিয়ে দিত।

export LC_ALL=C
cd "$(mktemp -d)"
mkdir -p logs/web1 logs/web2
echo '"GET /a" 200' > logs/web1/access.log
printf '"GET /b" 500\n"GET /c" 500\n' > "logs/web2/old access.log"
echo 'not a log' > logs/web1/notes.txt
touch -d '2026-10-30 12:00:00' logs/web1/access.log logs/web1/notes.txt
touch -d '2026-10-01 12:00:00' "logs/web2/old access.log"
name='logs/web2/old access.log'

echo "== unquoted: one name becomes two words =="
wc -l $name 2>&1 | sort
echo "== quoted: stays one word =="
wc -l "$name"
echo "== single quotes: no expansion =="
echo 'value is $name'

echo "== find: only .log files =="
find logs -type f -name '*.log' | sort
echo "== plain xargs breaks at the space =="
find logs -type f -name '*.log' | xargs grep -c ' 500' 2>&1 | sort
echo "== -print0 with xargs -0 is safe =="
find logs -type f -name '*.log' -print0 | xargs -0 grep -c ' 500' | sort
echo "== changed after 15 Oct 2026 =="
find logs -type f -newermt '2026-10-15' | sort

Quote ছাড়া wc পেল দুটি নাম: logs/web2/old আর access.log. দুটিই নেই, তাই দুটি No such file or directory এলো, মোট 0 total. Quote দিলে ঠিক উত্তর: 2 logs/web2/old access.log. Single quote-এ ছাপা হলো হুবহু value is $name.

সাধারণ xargs-ও ফাঁকায় ভাঙে, তাই একই ভুল আবার: ফাইলের দুই ভাগের জন্য grep-এর দুটি error, আর একটি আসল ফল logs/web1/access.log:0. আসল 500-গুলো গোনাই হলো না। Script-এ stderr কেউ না দেখলে এই ভুল চোখেই পড়বে না। -print0 নামগুলোকে null অক্ষরে আলাদা করে, xargs -0 সেভাবেই পড়ে। তাই ঠিক ফল এলো logs/web2/old access.log:2.

শেষ অংশে তারিখের শর্তে শুধু web1-এর দুটি ফাইল এলো, কারণ পুরোনো ফাইলের সময় ১ অক্টোবর।

মুছে ফেলার আগে শুধু তালিকা দেখুন। find ... -delete বা | xargs rm চালানোর আগে একই শর্তে -print চালিয়ে পুরো তালিকা পড়ুন। rm -rf "$DIR/"-এ DIR খালি হলে রূপ দাঁড়ায় rm -rf "/". পর্ব ১০-এর set -u ধরে শুধু সেট না-করা variable; খালি মান ধরে ${DIR:?} পাহারা।

৯ · কখন pipeline ছাড়বেন

Pipeline হলো পকেট-ছুরি। হাতের কাছের কাজে অসাধারণ, কিন্তু তা দিয়ে ঘর বানানো যায় না। কয়েকটি স্পষ্ট লক্ষণে অন্য হাতিয়ার নেওয়া উচিত।

লক্ষণকেন সমস্যাভালো পথ
Log আছে ৫০টি server-এএকেকটিতে ঢোকা অসম্ভব, autoscaling-এ server মুছেও যায়Centralized logging (পর্ব ৬৪)
গত মাসের ডেটা দরকারRotation-এ পুরোনো log মুছে যায়Log system-এর retention
প্রতিদিন একই প্রশ্নএটি toilMetric আর dashboard (পর্ব ৬৩)
সংখ্যা ছাড়ালে জানানো দরকারমানুষ সবসময় তাকিয়ে থাকে নাAlert (পর্ব ৬৬)
awk ১০ লাইন ছাড়াল, দুই ফাইল জোড়া লাগছেকেউ পরে পড়তে পারবে না, test নেইPython
Log JSON, বা stack trace বহু লাইনেরলাইন আর ঘরের ধারণা ভেঙে পড়েjq, Python, structured log

Log যদি JSON হয়, ঘর গোনার ঝামেলা থাকে না, নাম ধরে প্রশ্ন করা যায়:

jq -r 'select(.status >= 500) | .path' app.json | sort | uniq -c | sort -rn

এখানেও শেষের অংশ একই ছাঁচ। Pipeline-এর চিন্তা তাই log system-এও কাজে আসে: ছাঁকনি, ভাগ, গণনা, সাজানো। একবার কোনো প্রশ্নের উত্তর জরুরি বলে প্রমাণ হলে সেটিকে metric বা alert বানান। এটি ভালো Ops-এর স্বাভাবিক পথ।

পরীক্ষা ও ইন্টারভিউতে সাধারণত যে ধরনের প্রশ্ন আসে:

  • stdout আর stderr-এর পার্থক্য? fd 1 ফলাফলের জন্য, fd 2 ভুলের জন্য; pipe শুধু stdout নেয়।
  • uniq-এর আগে sort কেন? uniq শুধু পাশাপাশি একই লাইন মেলায়।
  • Log থেকে top 10 IP বের করুন। awk '{print $1}' log | sort | uniq -c | sort -rn | head.
  • Exit status 0 মানে? সফল। পাইপলাইনে শেষ command-এর মান পাওয়া যায়, pipefail সেটি বদলায়।
  • ফাঁকা-সহ নামের ফাইলে xargs? find -print0 | xargs -0.

১০ · AI-এর যুগে shell

AI এক সেকেন্ডে জটিল pipeline লিখে দেয়। Shell command-এ এটি সবচেয়ে কাজের, আবার সবচেয়ে বিপজ্জনকও: একটি ভুল লাইন সরাসরি production-এ চলে, undo নেই। Cloud আর DevOps-এর কাজে AI প্রায়ই * অনুমতি দেয়, 0.0.0.0/0 খুলে রাখে, storage public করে, command-এ secret বসায়, latest tag আর বাতিল API ব্যবহার করে, flag বানিয়ে নেয়, খরচ ভুলে যায়, আর rm -rf বা terraform destroy-এর মতো ধ্বংসাত্মক command সতর্কতা ছাড়াই দেয়।

AI-এর ভুলকী ঘটেযা করবেন
Log-এর format না দেখেই $9 ধরে নেওয়াভুল ঘর গোনে, সংখ্যা দেখতে ঠিক লাগে৫টি লাইন দিয়ে মিলিয়ে দেখা
grep 500 ধরনের ঢিলা নকশাবেশি গোনেমোট যোগফল মেলানো
GNU আর BSD-র পার্থক্য ভুলে যাওয়াmacOS-এ sed -i-এর রূপ আলাদানিজের মেশিনে man দেখা
নেই এমন option বানানোerror, অথবা অন্য অর্থে চলাঅফিসিয়াল man page দেখা
sed -i বা find -delete সরাসরিফেরার পথ নেইআগে -i ছাড়া, -print দিয়ে দেখা
chmod -R 777 “সমাধান”সবাই সব পারে, নিরাপত্তা শেষন্যূনতম অনুমতি (পর্ব ৮)

AI-কে সবসময় আসল ফরম্যাটের নমুনা দিন, তবে IP, token বা ব্যবহারকারীর তথ্য সরিয়ে। আর প্রতিটি অংশের ব্যাখ্যা চান, যাতে আপনি যাচাই করতে পারেন।

আমার nginx access log-এর তিনটি নমুনা লাইন নিচে দিলাম (IP আর token বদলে দিয়েছি): [এখানে ৩টি লাইন] প্রতি মিনিটে 5xx কতগুলো, সেটি বের করার একটি pipeline লিখে দাও। শর্ত: ১. পরিবেশ: Ubuntu, GNU awk, GNU sort. শুধু পড়বে, কোনো ফাইল বদলাবে না। ২. pipeline-এর প্রতিটি অংশ আলাদা লাইনে সহজ বাংলায় ব্যাখ্যা করো, আর কোন ঘর কেন নিয়েছ তা বলো। ৩. ভাঙা বা অন্য format-এর লাইনে কী ভুল হতে পারে, বলো। ৪. কীভাবে হাতে যাচাই করব, একটি ছোট উপায় দাও। ৫. কোনো option সম্পর্কে নিশ্চিত না হলে সেটি বলো, বানিয়ে লিখো না।

AI-এর command চালানোর আগে চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১Knuth-এর দশ পাতা বনাম McIlroy-র ছয় লাইন: ছোট tool জুড়ে লেখা shell কি সবসময় জেতে?

১৯৮৬ সালে Communications of the ACM-এ Jon Bentley-র “Programming Pearls” কলামে একটি বিখ্যাত ঘটনা ঘটে। Bentley, Donald Knuth-কে একটি কাজ দেন: একটি লেখায় সবচেয়ে বেশি ব্যবহৃত k-টি শব্দ, গণনাসহ ছাপা। Knuth তাঁর literate programming পদ্ধতিতে প্রায় দশ পাতার যত্নে লেখা program দেন, নিজস্ব data structure-সহ। সমালোচনা লিখতে গিয়ে Doug McIlroy দেন ছয় লাইনের একটি shell pipeline:

tr -cs A-Za-z '\n' |
tr A-Z a-z |
sort |
uniq -c |
sort -rn |
sed ${1}q

লক্ষ করুন, এটি এই পর্বের ঠিক সেই ছাঁচ: গুছিয়ে নেওয়া, সাজানো, গোনা, র‍্যাংক, সেরা কয়েকটি।

গল্পটিকে অনেকে “shell জিতেছে” বলে বলেন। আসল শিক্ষা আরও সূক্ষ্ম। McIlroy-র সমাধান কয়েক মিনিটে লেখা যায়, পড়া যায়, আর পরে বদলানো যায়। কিন্তু এটি পুরো ডেটা সাজায়। বিশাল ইনপুটে একটি বিশেষভাবে বানানো program অনেক দ্রুত আর কম memory-তে চলতে পারে। Knuth-এর লেখাটিও ছিল একটি কৌশল দেখানোর জন্য, একটি উপকারী utility বানানোর জন্য নয়। দুজন দুই ভিন্ন প্রশ্নের উত্তর দিচ্ছিলেন।

Ops-এর কাজে প্রশ্নটি প্রায়ই একবারের। ঘটনার সময় পাঁচ মিনিটে পাওয়া উত্তরের দাম একটি নিখুঁত program-এর চেয়ে বেশি। আবার যে হিসাব প্রতিদিন কোটি লাইনে চলবে, সেখানে pipeline ভুল হাতিয়ার।

মূল উপলব্ধি: শ্রেষ্ঠ সমাধান বলে কিছু নেই। প্রশ্ন হলো কাজটি কতবার চলবে, ডেটা কত বড়, আর ভুল হলে কত ক্ষতি। একবারের প্রশ্নে pipeline, স্থায়ী ব্যবস্থায় প্রকৌশল।

প্র ০২Text stream-কে “সর্বজনীন interface” বলা হয়। এর দাম কী, আর structured log কেন এলো?

Text-এর জাদু হলো কোনো চুক্তি লাগে না। grep-এর লেখক কখনো ভাবেননি nginx কী, তবু দুজন মিলে কাজ করে। পঞ্চাশ বছরের পুরোনো tool আজকের log-এ চলে। এটি সত্যিই অসাধারণ নকশা।

কিন্তু দাম আছে। Text-এ অর্থ নেই, আছে শুধু অবস্থান। $9 মানে “নবম টুকরো”, “status” নয়। Format-এ একটি ঘর যোগ হলে সব script চুপচাপ ভুল সংখ্যা দেবে, আর কোনো error আসবে না। এই পর্বে দেখেছেন, ভাঙা request-ও ঘর সরিয়ে দেয়। বহু-লাইনের stack trace “এক লাইন এক ঘটনা”-র ধারণা ভাঙে। লেখা থেকে সংখ্যা বের করা বারবার পড়তে হয় বলে মেশিনের ওপর চাপও বাড়ে।

এর জবাব হিসেবে দুটি ধারা এসেছে। Microsoft-এর PowerShell (প্রথম প্রকাশ ২০০৬) pipe দিয়ে লেখার বদলে object পাঠায়, তাই ঘর নামেই ধরা যায়। দ্বিতীয় ধারা হলো structured log: প্রতিটি লাইন একটি JSON object, যেমন {"status":500,"path":"/api/orders"}. তখন jq বা log system নাম ধরে প্রশ্ন করে, আর নতুন ঘর যোগ হলে পুরোনো প্রশ্ন ভাঙে না। Observability-র আধুনিক চর্চা এই দিকেই গেছে।

তবু text মরেনি। JSON মানুষের চোখে পড়া কঠিন, আকারে বড়, আর লিখতে খরচ বেশি। পুরোনো system আর tool-এর বিশাল জগৎও text-নির্ভর। বাস্তবে অনেক দল JSON লেখে, আর জরুরি মুহূর্তে সেই JSON-ই jq | sort | uniq -c-তে ঢেলে দেয়।

মূল উপলব্ধি: নমনীয়তা আর নিশ্চয়তার মধ্যে একটি লেনদেন আছে। Text কিছুই চায় না, তাই কিছুরই নিশ্চয়তা দেয় না। Schema প্রতিশ্রুতি দেয়, কিন্তু চুক্তি মানতে হয়।

প্র ০৩একটি regex কীভাবে পুরো সার্ভিস নামিয়ে দিতে পারে?

Regex নিরীহ দেখায়, কিন্তু তার পেছনে দুই রকম ইঞ্জিন আছে। তত্ত্বের দিক থেকে regular expression-এর ধারণা এসেছে ১৯৫০-এর দশকে Stephen Kleene-এর কাজ থেকে। ১৯৬৮ সালে Ken Thompson দেখান একে দক্ষভাবে মেলানো যায় একটি automaton-এর সব সম্ভাব্য অবস্থা একসাথে ধরে রেখে। এতে সময় বাড়ে মোটামুটি ইনপুটের দৈর্ঘ্যের অনুপাতে।

অন্যদিকে Perl-ধারার অনেক ইঞ্জিন backtracking করে: একটি পথ চেষ্টা করে, ব্যর্থ হলে পিছিয়ে অন্য পথ ধরে। এতে backreference-এর মতো শক্তিশালী সুবিধা মেলে, কিন্তু কিছু নকশায় চেষ্টার সংখ্যা ইনপুটের দৈর্ঘ্যে সূচকীয় হারে বা বহুপদী হারে বাড়ে। যেমন (a+)+$-ধরনের নকশা। একে catastrophic backtracking বলে। ইনপুট বাইরে থেকে এলে এটি নিরাপত্তারও সমস্যা, যার নাম ReDoS.

একটি প্রকাশ্য উদাহরণ: Cloudflare-এর নিজস্ব postmortem অনুযায়ী, ২০১৯ সালের জুলাইয়ে তাদের WAF-এর একটি নতুন নিয়মের regex এমন backtracking ঘটায় যে CPU ভরে যায়, আর বিশ্বজুড়ে প্রায় আধা ঘণ্টার মতো সেবা ব্যাহত হয়। লক্ষ করার বিষয়, নিয়মটি একসাথে সব জায়গায় ছড়ানো হয়েছিল। তাই সমস্যাটি শুধু regex-এর নয়, deployment-এরও (পর্ব ৫৯)।

GNU grep -E মূলত automaton-ধারায় চলে, তাই সাধারণ কাজে নিরাপদ। grep -P Perl-ধারার, শক্তিশালী কিন্তু সতর্কতা দরকার। আপনার application-এ regex ব্যবহারের সময় জেনে নিন ইঞ্জিন কোন ধরনের। Russ Cox-এর ২০০৭ সালের লেখা আর Google-এর RE2 লাইব্রেরি এই আলোচনাকে জনপ্রিয় করে।

মূল উপলব্ধি: ছোট একটি লেখার খরচ তার দৈর্ঘ্য দেখে বোঝা যায় না। যে কোড অবিশ্বস্ত ইনপুটে চলে, তার সবচেয়ে খারাপ অবস্থা ভাবতে হয়, আর বদলটি ধীরে ধীরে ছড়াতে হয়।

প্র ০৪set -o pipefail সবসময় চালু রাখলে কি সব সমস্যা মেটে? | head-এর সঙ্গে এর কী বিরোধ?

পর্ব ১০-এ দেখবেন, নিরাপদ script সাধারণত set -euo pipefail দিয়ে শুরু হয়। কারণ এটি ছাড়া grep ... missing.log | wc -l সফল দেখায়, আর script ভুল সংখ্যা নিয়ে এগিয়ে যায়। Backup বা deploy script-এ এই নীরব ব্যর্থতা ভয়ংকর।

কিন্তু এর একটি পরিচিত পার্শ্বপ্রতিক্রিয়া আছে। head -1 এক লাইন পেলেই শেষ হয়ে যায়, আর pipe-এর পড়ার মুখ বন্ধ হয়। আগের program তখনো লিখতে থাকলে kernel তাকে SIGPIPE signal পাঠায়, আর সে থেমে যায়। এটি আসলে সুন্দর নকশা: কারো দরকার নেই, তাই কাজ বন্ধ। তাই yes | head -1 অনন্ত লুপ হয় না।

সমস্যা হলো exit status. Signal 13-তে থামলে shell মান দেখায় 128 + 13 = 141. pipefail চালু থাকলে পুরো pipeline ব্যর্থ ধরা হয়, যদিও আপনি যা চেয়েছিলেন তাই পেয়েছেন। set -e-সহ script এখানে থেমে যেতে পারে। সমস্যাটি অনিয়মিত, কারণ এটি নির্ভর করে আগের program কতটা লিখেছিল। ছোট ফাইলে পুরোটা pipe-এর buffer-এ ঢুকে গেলে ব্যর্থতা দেখা যায় না। Linux-এ man page অনুযায়ী এই buffer সাধারণত ৬৫৫৩৬ bytes, তাই test-এ পাস, production-এ ব্যর্থ, এমন হতে পারে।

grep-এর 1-ও একই রকম ফাঁদ: “মেলেনি” কোনো ভুল নয়, অথচ pipefail একে ব্যর্থতা ধরে। সমাধান হলো উদ্দেশ্য স্পষ্ট লেখা, যেমন grep ... || true, বা ফলকে আলাদা ধাপে নেওয়া, বা PIPESTATUS দেখা।

মূল উপলব্ধি: কোনো safety flag জাদু নয়। প্রতিটি নিয়ম নতুন প্রান্তিক ঘটনা আনে। “ব্যর্থতা” কী, সেটি প্রতিটি command-এর জন্য আলাদা, আর সেই অর্থ আপনাকেই ঠিক করতে হয়।

প্র ০৫দুই server-এর p95-এর গড় কি পুরো সিস্টেমের p95? Latency বিচারে কোথায় ভুল হয়?

না। Percentile গড় করা যায় না, আর এটি dashboard-এর খুব প্রচলিত ভুল। একটি হিসাব দেখুন। Server A ১৯০০টি request সামলেছে, প্রতিটি ৫০ ms. Server B মাত্র ১০০টি, প্রতিটি ২০০০ ms. A-র p95 হলো ৫০ ms, B-র p95 হলো ২০০০ ms. গড় করলে (৫০ + ২০০০) / ২ = ১০২৫ ms.

এবার আসল হিসাব। মোট ২০০০ request, rank = ⌈০.৯৫ × ২০০০⌉ = ১৯০০. সাজালে প্রথম ১৯০০টিই ৫০ ms, তাই আসল p95 = ৫০ ms. গড়ের উত্তর প্রায় ২০ গুণ ভুল! আসল p95 পেতে হলে সব মান একসাথে সাজাতে হবে, অথবা server-গুলো থেকে histogram নিয়ে bucket যোগ করতে হবে। Prometheus-এর histogram ঠিক এই কারণে কাজে লাগে (পর্ব ৬৩)।

দ্বিতীয় ভুল: p95 ভালো মানেই সবার অভিজ্ঞতা ভালো নয়। একটি পাতা খুলতে যদি ২০টি request লাগে, আর প্রতিটির ৫% সম্ভাবনা থাকে ধীর হওয়ার, তাহলে অন্তত একটি ধীর হওয়ার সম্ভাবনা ১ − ০.৯৫২০, প্রায় ৬৪%. তাই বড় সিস্টেমে p99-ও দেখা হয়। Jeff Dean আর Luiz Barroso-র ২০১৩ সালের “The Tail at Scale” প্রবন্ধ এই ধারণাকে বিখ্যাত করে।

তৃতীয় ভুল: ব্যর্থ request বাদ দেওয়া। এক server দ্রুত 500 ফেরত দিলে তার latency দারুণ দেখায়! আমাদের log-এ উল্টোটি ঘটেছে, তবু status আর সময় একসাথে দেখাই নিরাপদ। চতুর্থত, load test-এ টুল নিজে আটকে গেলে ধীর সময়ের নমুনা কম আসে। Gil Tene এই সমস্যাকে coordinated omission নাম দেন।

মূল উপলব্ধি: একটি সংখ্যা কখনো পুরো গল্প বলে না। আগে জানুন সংখ্যাটি কীভাবে বানানো, কী বাদ পড়েছে, আর ঠিক কার অভিজ্ঞতা সে বলছে।

অনুশীলন

প্রথমটিতে উপরের status_ip_minute.sh ব্লকের log ব্যবহার করতে পারেন। ব্লকের কোড বদলে আবার চালান।

  1. Error rate হিসাব (command + numerical): ২০ লাইনের log-এ প্রতি মিনিটের 5xx হার বের করার একটি awk লিখুন। হাতে যাচাই করুন: ১৪:০৩-এ ৫টি request-এ ৩টি 5xx হলে হার কত?

    হার = ৩ / ৫ = ৬০%. একটি সমাধান:

    awk '{m = substr($4, 14, 5); total[m]++}
         $9 ~ /^5/ {bad[m]++}
         END {for (k in total)
           printf "%s %d/%d %.0f%%\n", k, bad[k], total[k], 100 * bad[k] / total[k]}' access.log | sort

    এই log-এ ফল: 14:01 0/5 0%, 14:02 0/5 0%, 14:03 3/5 60%, 14:04 4/5 80%. শুধু গণনার চেয়ে হার ভালো, কারণ ব্যস্ত সময়ে গণনা এমনিতেই বাড়ে। %% মানে printf-এ একটি আসল % চিহ্ন।

  2. আউটপুট অনুমান করুন: চালানোর আগে বলুন, এই দুটি লাইন কী ছাপবে, আর প্রথমটিতে কতটি লাইন আসবে?
    printf 'b\na\nb\nc\nb\na\n' | uniq -c
    printf 'b\na\nb\nc\nb\na\n' | sort | uniq -c

    প্রথমটিতে পাশাপাশি কোনো একই লাইন নেই, তাই ৬টি লাইন আসে, প্রতিটির গণনা ১: 1 b, 1 a, 1 b, 1 c, 1 b, 1 a.

    দ্বিতীয়টিতে সাজানোর পর একই অক্ষর পাশাপাশি আসে: 2 a, 3 b, 1 c. ভুলটি বিপজ্জনক, কারণ প্রথম আউটপুটেও কোনো error নেই। শুধু সংখ্যাগুলো অর্থহীন।

  3. সমালোচনা করুন (design): একজন সহকর্মী cron-এ এই লাইন রেখেছেন, আর সংখ্যা ১০০ ছাড়ালে alert পাঠাতে চান: cat /var/log/nginx/access.log | grep 500 | wc -l. অন্তত চারটি সমস্যা ধরুন, আর ভালো নকশা বলুন।
    • ঢিলা নকশা: grep 500 bytes, সময় বা path-এর 500-ও গোনে।
    • শুধু 500: 502, 503, 504 বাদ।
    • সময়ের জানালা নেই: ফাইল শুরু থেকে গোনে, তাই সংখ্যা সারাদিন শুধু বাড়ে, আর rotation-এর পর হঠাৎ কমে।
    • হার নেই: ১০০ বড় না ছোট, তা মোট request-এর ওপর নির্ভর করে।
    • এক server: বহু server-এ চলবে না, আর cron নিজে থেমে গেলে কেউ জানবে না।
    • বাড়তি cat: ক্ষতি সামান্য, তবে grep -c-ই যথেষ্ট।

    ভালো নকশা: status অনুযায়ী request গণনাকে metric বানান, আর error rate-এ alert রাখুন (পর্ব ৬৩, ৬৬)। সাময়িক ব্যবস্থা দরকার হলে শেষ পাঁচ মিনিটের লাইনে $9 ~ /^5/ দিয়ে হার গুনুন।

  4. Percentile (numerical): ৪০০টি request-এর সময় ছোট থেকে বড়ে সাজানো আছে sorted.txt-এ। nearest-rank পদ্ধতিতে p95 আর p99-এর rank কত? ফাইল থেকে p95 ছাপার command লিখুন। শেষে বলুন, ৮টি request-এর p95 কেন অর্থহীন।

    p95 rank = ⌈০.৯৫ × ৪০০⌉ = ৩৮০। p99 rank = ⌈০.৯৯ × ৪০০⌉ = ৩৯৬। Command: sed -n '380p' sorted.txt.

    ৮টি request-এ rank = ⌈৭.৬⌉ = ৮, অর্থাৎ p95 আসলে সর্বোচ্চ মানই। একটি request বদলালেই উত্তর পুরো বদলে যাবে। Percentile-এর জন্য যথেষ্ট নমুনা লাগে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রম“500 error” রিপোর্ট পাওয়ার পর log তদন্তের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Status গুনে নিশ্চিত হওয়া যে সমস্যা সত্যি
  2. প্রতি মিনিটের 5xx গুনে শুরুর সময় বের করা
  3. কোন path আর কোন client প্রভাবিত, তা বের করা
  4. সেই সময়ের application log পড়া
  5. ঠিক আগে কী বদলেছিল, deploy log-এ মেলানো
  6. সমাধানের পরে একই গণনা চালিয়ে error থেমেছে কি না যাচাই করা

cmd 2>&1 > out.txt চালালে cmd-এর ভুলের বার্তা কোথায় যাবে?

  • out.txt ফাইলে
  • Terminal-এ
  • /dev/null-এ, অর্থাৎ হারিয়ে যাবে
  • Terminal আর out.txt, দুই জায়গায়

printf 'a\nb\na\n' | uniq -c কতটি লাইন ছাপবে?

  • ১টি
  • ২টি
  • ৩টি
  • ৬টি

Access log-এ status 500 গুনতে grep -c 500 access.log চালানো হলো। প্রধান সমস্যা কী? (প্রতিটি লাইনে IP, সময়, request, status, bytes আর সময়কাল আছে।)

  • Status ছাড়া অন্য ঘরে থাকা 500-ও গোনা হবে
  • grep-এ -c নামে কোনো option নেই
  • grep সংখ্যা খুঁজতে পারে না
  • Log ফাইলটি বদলে যাবে

২০০টি request-এর সময় ছোট থেকে বড়ে সাজানো আছে। nearest-rank পদ্ধতিতে p95 কত নম্বর লাইনে?

  • ১৯০
  • ১৯৫
  • ১০
  • ৯৫

Bash-এ pipefail চালু নেই। grep x missing.log | wc -l চালানো হলো, আর missing.log ফাইলটি নেই। পুরো pipeline-এর exit status কত?

  • 2
  • 0
  • 1
  • 127

ফোল্ডারে কিছু ফাইলের নামে ফাঁকা আছে। সব .log ফাইলে নিরাপদে grep চালানোর সবচেয়ে ভালো উপায় কোনটি?

  • find . -name '*.log' | xargs grep ERROR
  • grep ERROR $(find . -name '*.log')
  • find . -name *.log | xargs grep ERROR
  • find . -name '*.log' -print0 | xargs -0 grep ERROR

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. তিনটি পথ: stdin 0, stdout 1, stderr 2. Redirection বাঁ থেকে ডানে পড়া হয়, আর pipe নেয় শুধু stdout.
  2. Exit status 0 মানে সফল। Pipeline শেষ command-এর মান দেয়, তাই script-এ pipefail.
  3. ছাঁচটি মনে রাখুন: ছাঁকনি | কাটাই | sort | uniq -c | sort -rn | head, আর প্রতিটি অংশ জুড়ে জুড়ে দেখুন।
  4. নকশা কড়া রাখুন, মোট যোগফল মেলান, আর গড়ের সঙ্গে p95 দেখুন। সবচেয়ে বেশি request মানেই দোষী নয়।
  5. বদলানোর আগে preview, variable-এ quote, নামে -print0. বহু server বা বারবারের প্রশ্নে pipeline ছেড়ে log system বা Python নিন।

পরের পর্বে: এতক্ষণ এক লাইনের command দেখলেন। পর্ব ১০-এ সেগুলোকে নিরাপদ Bash script বানাবেন: set -euo pipefail, function, cron আর systemd timer.

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান