12 điểm bởi GN⁺ 2025-05-27 | 1 bình luận | Chia sẻ qua WhatsApp
  • Khi thực hiện các lần thử kết nối lặp lại để kiểm tra trạng thái máy chủ web trong script Bash, có thể phát sinh vấn đề máy chủ bất ngờ rơi vào vòng lặp vô hạn
  • Công cụ để giải quyết việc này là timeout, cho phép đặt giới hạn thời gian thực thi cho lệnh và khi vượt quá sẽ gửi tín hiệu để cố gắng kết thúc tiến trình
  • Không thể áp dụng trực tiếp cho các shell built-in như until, nên có thể xử lý bằng cách bọc trong tiến trình bash hoặc tách thành script riêng

Chờ máy chủ web trong script Bash và vấn đề vòng lặp vô hạn

  • Trong công việc thực tế, script Bash được dùng để thiết lập máy chủ web và kiểm tra trạng thái
  • Cấu trúc này sẽ tạm hoãn bước tiếp theo cho đến khi máy chủ khởi động xong, và về cơ bản hoạt động bình thường
  • Tuy nhiên, nếu máy chủ bị crash trong lúc khởi động thì script sẽ rơi vào vòng lặp vô hạn, nên cần có cách xử lý

Ví dụ dùng until và giới hạn của nó

  • Có thể lặp lại việc health check máy chủ web bằng cú pháp như sau
    until curl --silent --fail-with-body 10.0.0.1:8080/health; do  
    	sleep 1  
    done  
    
  • Khi máy chủ lỗi, tình huống sleep 1 lặp lại mãi mãi sẽ xảy ra

Đưa utility timeout vào sử dụng

  • Lệnh timeout sẽ gửi tín hiệu (SIGTERM, v.v.) để kết thúc nếu lệnh không hoàn thành trong thời gian đã chỉ định
  • Ví dụ: với timeout 1s sleep 5, sau 1 giây sẽ cố gắng kết thúc tiến trình sleep
  • Khi kết thúc, nó sẽ trả về mã thoát bất thường (ví dụ: 124)

Thử kết hợp timeout với until và vấn đề phát sinh

  • Tự nhiên sẽ muốn thử kết hợp timeout với until như bên dưới
    timeout 1m until curl ...; do  
    	sleep 1  
    done  
    
  • Tuy nhiên, timeout có thể gửi tín hiệu tới tiến trình, còn until là từ khóa built-in của shell nên không thể áp dụng trực tiếp

Cách giải quyết: bọc trong tiến trình Bash hoặc dùng script bên ngoài

  • Nếu bọc toàn bộ vòng lặp until bằng bash -c để chạy trong một tiến trình riêng, có thể áp dụng timeout
    timeout 1m bash -c "until curl ...; do sleep 1; done"  
    
  • Hoặc có thể tách phần vòng lặp thành script Bash bên ngoài, rồi áp dụng timeout cho script đó
    timeout 1m ./until.sh  
    
  • Dù không thể áp dụng timeout trực tiếp cho shell built-in, vẫn có thể đạt được hành vi mong muốn bằng các cách trên

1 bình luận

 
GN⁺ 2025-05-27
Ý kiến trên Hacker News
  • Mẹo ít người biết mà tôi thích nhất là dùng strace fault injection để kiểm tra các trường hợp lỗi của nhiều system call khác nhau

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

    Liên kết liên quan giải thích chi tiết hơn

    • Chia sẻ rằng tính năng này thực sự đáng kinh ngạc và ước gì đã biết sớm hơn
      Trước đây vì không có cách kiểm tra nhánh thất bại nên thường phải tạm thời thay thế một phần của hàm bằng mã thử nghiệm, nhưng mẹo này mở ra khả năng tiếp cận gọn gàng hơn

    • Ý kiến cho rằng cách này trông rất hữu ích
      Đồng thời tò mò không biết trên Windows có tính năng tương tự hay không

  • Đề xuất rằng cách tối ưu cho service health check là đặt cả thời gian timeout tối đa lẫn số lần retry tối đa
    Thông thường sẽ thử retry tối đa X lần và coi là thất bại trong tối đa Y thời gian
    Nhấn mạnh cần quyết định thất bại càng sớm càng tốt thay vì chờ quá lâu
    Với các service tiêu chuẩn, chỉ nên bắt đầu health check sau khi dependency của container đã được đảm bảo đầy đủ và sẵn sàng hoạt động
    Trên Kubernetes có Init Container, trên AWS ECS có dependsOn, trong Docker Compose có thiết lập depends_on
    Có đưa ra ví dụ shell script POSIX
    Tuy nhiên cũng nhắc rằng curl đã tích hợp sẵn tính năng này nên có thể dùng như sau mà không cần script riêng

    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
    
  • Chia sẻ kinh nghiệm từng thử nhiều cách để tự triển khai timeout chỉ bằng bash builtins vì trên Mac lệnh timeout không được cung cấp sẵn mặc định
    Giải thích rằng lệnh sleep là tiêu chuẩn trong POSIX nên có thể sử dụng
    Đưa ra ví dụ triển khai chức năng timeout như dưới đây

    # TIMEOUT SYSTEM(tóm tắt)
    # function timeout <num_seconds> <command>
    # kích hoạt <command> sau khi hết một khoảng thời gian nhất định
    

    Dùng hàm times_up để xử lý timeout
    Có ví dụ kiểm tra bằng vòng lặp for chạy 20 lần với timeout 10 giây

    • Chia sẻ rằng 12 năm trước đã triển khai cách tương tự theo lời khuyên trên Stack Overflow
      Có thể xem chi tiết tại liên kết tham khảo
      Nhấn mạnh rằng chỉ dùng shell builtins và sleep, và đoạn mã đó bắt buộc phải tương thích POSIX
      Lưu ý rằng cú pháp {1..20} của bash trong ví dụ không phải POSIX
      Điểm cải tiến của tôi là nếu không bị timeout thì trả về true, còn nếu timeout xảy ra thì trả về false để có thể xử lý lỗi trong script đơn giản hơn

    • Chia sẻ một cách cực kỳ đơn giản như dưới đây: chạy lệnh và sleep song song, rồi sau thời gian chỉ định thì dùng signal để dừng lệnh

      <command> & sleep <timeout>; kill -SIGALRM %1
      
    • Chia sẻ ví dụ script từ 13 năm trước từng dùng read -t để triển khai timeout
      Liên kết

  • Thông báo rằng curl đã có sẵn cờ --retry-connrefused, nên có thể tận dụng ngay tính năng này mà không cần vòng lặp shell

  • Nếu cần truyền biến khi dùng bash -c, khuyến nghị thêm đối số như sau

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

    Giải thích lý do dùng "--" và vai trò của argv[0]
    Cũng có thể dùng printf %q, nhưng có nói rằng vẫn thích cách tương thích Bourne hơn

    • Giải thích rằng "--" có ý nghĩa rất rõ ràng là dấu kết thúc tùy chọn trong bash và hầu hết CLI Unix/Linux
      Tham khảo liên quan

    • Chia sẻ rằng Busybox quyết định chương trình sẽ chạy dựa trên giá trị của argv[0], nên có thể gán thành các lệnh mong muốn như ls, mv, cp v.v.

  • Khi cần logic thử lại nhiều lần, đây là cách tôi thường dùng

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

    Không quá bóng bẩy nhưng nhìn chung khá chính xác, và ở mức nâng cao hơn có thể áp dụng exponential backoff
    Cũng có lợi thế về khả năng mở rộng

    • shellcheck khuyến nghị xử lý vấn đề này bằng cách dùng biến _
      Liên kết tham khảo

    • Nhấn mạnh rằng hàm eventually_succeeds tùy tình huống có thể vẫn cần timeout hoặc mã phòng thủ bổ sung
      Nhắc lại rằng với POSIX/process/IO luôn cần viết mã theo hướng phòng thủ

  • Chia sẻ rằng trước đây khi con cái còn nhỏ, đã dùng lệnh dưới đây như một dạng công cụ kiểm soát của phụ huynh để chỉ cho xem một chương trình trong 30 phút

    timeout 1800 mplayer show.mp4 ; sudo pm-suspend
    

    Đánh giá rằng ý tưởng này đã được áp dụng rất hữu ích

    • Có thêm ý kiến rằng đây là ví dụ giải thích cách dùng hay nhất
  • Nói rằng bản thân không thích dùng command inline hay file script tạm khi cần gửi signal tới subprocess
    Cách tôi thích là viết logic phức tạp mong muốn thành một hàm, export nó rồi bọc bằng timeout bash -c
    Liên quan tới cách xử lý truyền đối số an toàn mà aidenn0 đã nhắc tới

    #!/usr/bin/env bash
    
    long_fn () { # triển khai logic mong muốn
     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
    
    • Chỉ ra rằng ở cuối bắt buộc phải dùng "$@"
      Nếu không, các đối số có chứa khoảng trắng sẽ không được truyền đúng
      Có chia sẻ ví dụ long_fn để kiểm chứng điểm này
  • Nhắc lại một bài blog trước đây từng đề cập đến timeout
    Nếu tò mò hơn về ngôn ngữ lập trình thông thường hoặc cơ chế hoạt động bên trong thay vì shell, có thể tham khảo blog liên quan

  • Chia sẻ kinh nghiệm từng thêm timeout cho lệnh trong môi trường Kubernetes
    Các shell script POSIX như await-cmd.sh, await-http.sh, await-tcp.sh đã khá hoàn thiện và có thể rất hữu ích trong một số tình huống nhất định
    Liên kết dự án liên quan