12 คะแนน โดย GN⁺ 2025-05-27 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • เมื่อสคริปต์ Bash พยายามเชื่อมต่อซ้ำเพื่อตรวจสอบ สถานะของเว็บเซิร์ฟเวอร์ อาจเกิดปัญหาที่เซิร์ฟเวอร์เข้าสู่ ลูปไม่สิ้นสุด โดยไม่คาดคิด
  • timeout ซึ่งเป็นเครื่องมือสำหรับแก้ปัญหานี้ จะกำหนดเวลาจำกัดในการรันคำสั่ง และเมื่อเกินเวลาจะส่งสัญญาณเพื่อพยายาม ยุติโพรเซส
  • ไม่สามารถใช้กับ shell built-in อย่าง until ได้โดยตรง จึงต้องแก้ด้วยการ ห่อด้วยโพรเซส bash หรือแยกเป็นสคริปต์ต่างหาก

การรอเว็บเซิร์ฟเวอร์และปัญหาลูปไม่สิ้นสุดในสคริปต์ Bash

  • ในการทำงานจริง มีการใช้สคริปต์ Bash เพื่อทำ การตั้งค่าเว็บเซิร์ฟเวอร์และตรวจสอบสถานะ
  • โครงสร้างจะพักงานถัดไปไว้ก่อนระหว่างที่เซิร์ฟเวอร์กำลังเริ่มทำงาน ซึ่งโดยปกติจะทำงานได้ไม่มีปัญหา
  • แต่หากเกิด crash ระหว่างการเริ่มต้นเซิร์ฟเวอร์ ก็จะเข้าสู่ลูปไม่สิ้นสุด ทำให้ต้องหาวิธีแก้ไข

ตัวอย่างการใช้ until และข้อจำกัด

  • มีการทำ Health check ของเว็บเซิร์ฟเวอร์ ซ้ำด้วยรูปแบบดังนี้
    until curl --silent --fail-with-body 10.0.0.1:8080/health; do  
    	sleep 1  
    done  
    
  • เมื่อเซิร์ฟเวอร์ล้มเหลว จะเกิดสถานการณ์ที่ sleep 1 ถูกทำซ้ำไปตลอดกาล

การนำยูทิลิตี timeout มาใช้

  • คำสั่ง timeout จะยุติคำสั่งโดยส่ง สัญญาณ (เช่น SIGTERM) หากคำสั่งไม่เสร็จภายในเวลาที่กำหนด
  • ตัวอย่าง: ในกรณี timeout 1s sleep 5 เมื่อผ่านไป 1 วินาที จะพยายาม ยุติโพรเซส sleep
  • เมื่อยุติ จะคืนค่าเป็น รหัสออกแบบผิดปกติ (เช่น 124)

การพยายามใช้ timeout ร่วมกับ until และปัญหาที่พบ

  • จึงลองนำ timeout มาใช้ร่วมกับ until แบบด้านล่างอย่างเป็นธรรมชาติ
    timeout 1m until curl ...; do  
    	sleep 1  
    done  
    
  • แต่ timeout สามารถส่งสัญญาณได้กับ เป้าหมายที่เป็นโพรเซส เท่านั้น ขณะที่ until เป็นคีย์เวิร์ดแบบ built-in ของ shell จึง ไม่สามารถใช้ได้โดยตรง

วิธีแก้: ห่อด้วยโพรเซส Bash หรือใช้สคริปต์ภายนอก

  • หากห่อทั้งลูป until ด้วย bash -c เพื่อให้รันเป็นโพรเซสแยก ก็จะสามารถใช้ timeout ได้
    timeout 1m bash -c "until curl ...; do sleep 1; done"  
    
  • หรือจะแยกส่วนลูปออกเป็น สคริปต์ Bash ภายนอก แล้วค่อยใช้ timeout กับสคริปต์นั้นก็ได้
    timeout 1m ./until.sh  
    
  • แม้จะไม่สามารถใช้ timeout กับ shell built-in ได้โดยตรง แต่ก็สามารถบรรลุพฤติกรรมที่ต้องการได้ด้วยวิธีข้างต้น

1 ความคิดเห็น

 
GN⁺ 2025-05-27
ความคิดเห็นบน Hacker News
  • ทริกที่ฉันชอบมากที่สุดอย่างหนึ่งซึ่งไม่ค่อยมีใครรู้จัก คือการแนะนำวิธีทดสอบความล้มเหลวของ system call แบบต่าง ๆ ด้วย strace fault injection

    $ strace -e trace=clone -e fault=clone:error=EAGAIN
    

    มีคำอธิบายรายละเอียดเพิ่มเติมในลิงก์ที่เกี่ยวข้อง

    • รู้สึกว่าฟีเจอร์นี้น่าทึ่งมาก และแชร์ประสบการณ์ว่าถ้ารู้เรื่องนี้ตั้งแต่ก่อนก็คงดี
      เพราะไม่มีวิธีทดสอบเส้นทางเมื่อเกิดความล้มเหลว จึงเคยต้องแทนที่บางส่วนของฟังก์ชันด้วยโค้ดชั่วคราว แต่ทริกนี้ทำให้มีแนวทางที่กระชับกว่านั้น

    • มีความเห็นว่าวิธีนี้ดูมีประโยชน์มาก
      และสงสัยว่าบน Windows มีฟังก์ชันคล้าย ๆ กันหรือไม่

  • มีข้อเสนอว่าวิธีที่ดีที่สุดสำหรับ service health check คือกำหนดทั้งเวลา timeout สูงสุดและจำนวนครั้ง retry สูงสุด
    โดยทั่วไปจะลอง retry ได้ถึง X ครั้ง และตัดสินว่า failed ภายในเวลาไม่เกิน Y
    เน้นว่าควรตัดสินให้ล้มเหลวให้เร็วที่สุด แทนที่จะรอนานเกินไป
    ในบริการมาตรฐาน มักเริ่ม health check เฉพาะหลังจากรับประกัน dependency ของคอนเทนเนอร์ครบถ้วนและพร้อมทำงานแล้ว
    ใน Kubernetes ให้ดู Init Container, ใน AWS ECS ให้ดู dependsOn, และใน Docker Compose ให้ดูการตั้งค่า depends_on
    มีการยกตัวอย่าง POSIX shell script
    แต่ก็มีการกล่าวว่า curl เองมีฟังก์ชันนี้ในตัวอยู่แล้ว จึงสามารถใช้แบบด้านล่างได้โดยไม่ต้องมีสคริปต์แยก

    curl --silent --fail-with-body --connect-timeout 5 --retry-all-errors --retry-delay 1 --retry-max-time 300 --retry 300 10.0.0.1:8080/health
    
  • มีการแชร์ประสบการณ์ว่าใน Mac ไม่มีคำสั่ง timeout มาให้โดยปริยาย จึงเคยลองหลายวิธีเพื่อทำ timeout ด้วย bash builtin เพียงอย่างเดียว
    พร้อมอธิบายว่าคำสั่ง sleep เป็นมาตรฐานใน POSIX จึงสามารถใช้ได้
    และยกตัวอย่างการทำฟังก์ชัน timeout ดังนี้

    # TIMEOUT SYSTEM(สรุป)
    # function timeout <num_seconds> <command>
    # ทริกเกอร์ <command> หลังจากเวลาที่กำหนดผ่านไป
    

    ใช้ฟังก์ชันชื่อ times_up เพื่อจัดการ timeout
    และมีตัวอย่างทดสอบด้วยการวน for 20 ครั้งพร้อม timeout 10 วินาที

    • มีการแชร์ว่าตนเคยทำวิธีคล้ายกันตามคำแนะนำใน Stack Overflow เมื่อ 12 ปีก่อน
      สามารถดูรายละเอียดได้ในลิงก์อ้างอิง
      เน้นว่าใช้แค่ shell builtins กับ sleep และโค้ดนั้นจำเป็นต้องเข้ากันได้กับ POSIX
      พร้อมเตือนว่าไวยากรณ์ {1..20} ของ bash ในตัวอย่างไม่ใช่ POSIX
      ส่วนที่ตนปรับปรุงคือ ถ้าไม่เกิด timeout ให้คืนค่า true แต่ถ้าเกิด timeout ให้คืนค่า false เพื่อให้จัดการ error ในสคริปต์ได้ง่ายขึ้น

    • มีการแชร์วิธีที่ง่ายมาก โดยรันคำสั่งกับ sleep แบบขนานกัน แล้วส่งสัญญาณเพื่อจบคำสั่งเมื่อครบเวลาที่กำหนด

      <command> & sleep <timeout>; kill -SIGALRM %1
      
    • มีการแชร์ตัวอย่างสคริปต์เมื่อ 13 ปีก่อนที่ใช้ read -t เพื่อทำ timeout
      ลิงก์

  • มีการแนะนำว่า curl มีแฟล็ก --retry-connrefused อยู่แล้ว จึงสามารถใช้ความสามารถนี้ได้ทันทีโดยไม่ต้องเขียน shell loop

  • หากต้องส่งตัวแปรตอนใช้ bash -c ก็แนะนำให้เพิ่มอาร์กิวเมนต์ดังนี้

    bash -c 'some command "$1" "$2"' -- "$var1" "$var2"
    

    พร้อมอธิบายเหตุผลที่ใช้ "--" และบทบาทของ argv[0]
    แม้จะใช้ printf %q ได้เช่นกัน แต่มีการระบุว่าชอบวิธีที่เข้ากันได้กับ Bourne มากกว่า

    • อธิบายว่า "--" มีความหมายชัดเจนมากในฐานะสัญญาณสิ้นสุดออปชันทั้งใน bash และ CLI ของ Unix/Linux ส่วนใหญ่
      อ้างอิงที่เกี่ยวข้อง

    • มีการแชร์ว่า Busybox ใช้ค่า argv[0] เพื่อตัดสินว่าจะรันโปรแกรมใด จึงสามารถกำหนดเป็น "ls", "mv", "cp" หรือคำสั่งอื่นที่ต้องการได้

  • เวลาต้องมีตรรกะ retry วิธีที่ฉันมักใช้มีดังนี้

    for i in {0..60}; do
      true -- "$i"
      if eventually_succeeds; then break; fi
      sleep 1s
    done
    

    แม้จะไม่หรูหรานัก แต่โดยทั่วไปค่อนข้างแม่นยำ และในขั้นที่ซับซ้อนขึ้นก็สามารถใช้ exponential backoff ได้
    อีกทั้งยังมีข้อดีในด้านการขยายต่อยอด

    • shellcheck แนะนำให้จัดการกรณีนี้โดยใช้ตัวแปร _
      ลิงก์อ้างอิง

    • มีการเน้นว่าฟังก์ชัน eventually_succeeds อาจต้องมี timeout หรือ defensive coding เพิ่มเติมตามสถานการณ์
      พร้อมย้ำว่าควรเขียนโค้ดเชิงป้องกันเสมอในเรื่อง POSIX/process/IO

  • มีการแชร์ประสบการณ์ว่าเมื่อก่อนตอนลูก ๆ ยังเล็ก เคยใช้คำสั่งด้านล่างเป็นเหมือนเครื่องมือ parental control เพื่อให้ดูรายการได้แค่ 30 นาที

    timeout 1800 mplayer show.mp4 ; sudo pm-suspend
    

    พร้อมบอกว่าเป็นไอเดียที่นำไปใช้ประโยชน์ได้ดีมาก

    • มีความเห็นเสริมว่านี่เป็นกรณีใช้งานที่อธิบายได้เท่มากที่สุด
  • มีการระบุว่าไม่ค่อยชอบใช้คำสั่งแบบ inline หรือไฟล์สคริปต์ชั่วคราว เพราะต้องส่งสัญญาณไปยัง subprocess
    วิธีที่ตนชอบคือทำตรรกะที่ซับซ้อนตามต้องการเป็นฟังก์ชัน แล้ว export ออกมา จากนั้นห่อด้วย timeout bash -c
    เกี่ยวข้องกับวิธีส่งอาร์กิวเมนต์อย่างปลอดภัยที่ aidenn0 กล่าวไว้

    #!/usr/bin/env bash
    
    long_fn () { # ใส่ตรรกะที่ต้องการ
     sleep $1
    }
    to () {
     local duration="$1"; shift
     local fn_name="$1"; shift
     export -f "$fn_name"
     timeout "$duration" bash -c "$fn_name"' "$@"' _ $@
    }
    
    time to 1s long_fn 5
    
    • มีการชี้ว่าท้ายสุดต้องใส่ "$@" เสมอ
      ไม่เช่นนั้นจะเกิดปัญหาส่งอาร์กิวเมนต์ที่มีช่องว่างได้ไม่ถูกต้อง
      พร้อมแชร์ตัวอย่าง long_fn ที่ใช้ตรวจสอบจุดนี้ได้
  • มีการรำลึกถึงบทความบล็อกเก่าที่เคยพูดถึง timeout
    หากสนใจมากขึ้นในแง่ภาษาโปรแกรมทั่วไปหรือกลไกภายในที่ไม่ใช่ shell ก็แนะนำให้อ่านบล็อกที่เกี่ยวข้อง

  • มีการแชร์ประสบการณ์ว่าเคยเพิ่ม command timeout ในการตั้งค่า Kubernetes
    และระบุว่าสคริปต์ POSIX shell อย่าง await-cmd.sh, await-http.sh, await-tcp.sh มีความสมบูรณ์ดีและใช้งานได้ค่อนข้างมีประโยชน์ในบางสถานการณ์
    ลิงก์โปรเจกต์ที่เกี่ยวข้อง