Các phương pháp hay nhất về tập lệnh Bash: Hướng dẫn nhanh về các tập lệnh Bash đáng tin cậy và hiệu suất

Các phương pháp hay nhất về tập lệnh Bash: Hướng dẫn nhanh về các tập lệnh Bash đáng tin cậy và hiệu suất
Hình nền Shell của manapi

Việc gỡ lỗi các tập lệnh bash giống như mò kim đáy bể, đặc biệt là khi các phần bổ sung mới xuất hiện trong cơ sở mã hiện có mà không xem xét kịp thời các vấn đề về cấu trúc, ghi nhật ký và độ tin cậy. Bạn có thể rơi vào những tình huống như vậy do lỗi của chính mình hoặc khi quản lý hàng đống tập lệnh phức tạp.

Đội Giải pháp đám mây Mail.ru đã dịch một bài viết với các đề xuất sẽ giúp bạn viết, gỡ lỗi và duy trì tập lệnh của mình tốt hơn. Dù bạn có tin hay không, không có gì có thể sánh được với sự hài lòng khi viết mã bash sạch, sẵn sàng sử dụng và hoạt động mọi lúc.

Trong bài viết, tác giả chia sẻ những gì ông đã học được trong vài năm qua, cũng như một số sai lầm phổ biến khiến ông mất cảnh giác. Điều này rất quan trọng vì mọi nhà phát triển phần mềm, tại một thời điểm nào đó trong sự nghiệp của họ, đều làm việc với các tập lệnh để tự động hóa các tác vụ công việc thường ngày.

Người xử lý bẫy

Hầu hết các tập lệnh bash mà tôi từng gặp không bao giờ sử dụng cơ chế dọn dẹp hiệu quả khi có điều gì đó không mong muốn xảy ra trong quá trình thực thi tập lệnh.

Những điều bất ngờ có thể nảy sinh từ bên ngoài, chẳng hạn như nhận được tín hiệu từ lõi. Việc xử lý những trường hợp như vậy là cực kỳ quan trọng để đảm bảo rằng các tập lệnh đủ tin cậy để chạy trên hệ thống sản xuất. Tôi thường sử dụng trình xử lý thoát để ứng phó với các tình huống như thế này:

function handle_exit() {
  // Add cleanup code here
  // for eg. rm -f "/tmp/${lock_file}.lock"
  // exit with an appropriate status code
}
  
// trap <HANDLER_FXN> <LIST OF SIGNALS TO TRAP>
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM

trap là lệnh tích hợp shell giúp bạn đăng ký chức năng dọn dẹp được gọi trong trường hợp có bất kỳ tín hiệu nào. Tuy nhiên, cần đặc biệt cẩn thận với những người xử lý như SIGINT, khiến tập lệnh bị hủy bỏ.

Ngoài ra, trong hầu hết các trường hợp bạn chỉ nên bắt EXIT, nhưng ý tưởng là bạn thực sự có thể tùy chỉnh hành vi của tập lệnh cho từng tín hiệu riêng lẻ.

Các chức năng được cài đặt sẵn - chấm dứt nhanh khi có lỗi

Điều rất quan trọng là phải phản hồi các lỗi ngay khi chúng xảy ra và ngừng thực thi một cách nhanh chóng. Không có gì tệ hơn việc tiếp tục chạy một lệnh như thế này:

rm -rf ${directory_name}/*

Xin lưu ý rằng biến directory_name không xác định.

Điều quan trọng là sử dụng các hàm dựng sẵn để xử lý các tình huống như vậy setchẳng hạn như set -o errexit, set -o pipefail hoặc set -o nounset ở đầu kịch bản. Các hàm này đảm bảo rằng tập lệnh của bạn sẽ thoát ngay khi nó gặp bất kỳ mã thoát nào khác XNUMX, sử dụng các biến không xác định, các lệnh không hợp lệ được truyền qua một đường dẫn, v.v.:

#!/usr/bin/env bash

set -o errexit
set -o nounset
set -o pipefail

function print_var() {
  echo "${var_value}"
}

print_var

$ ./sample.sh
./sample.sh: line 8: var_value: unbound variable

Lưu ý: các chức năng tích hợp như set -o errexit, sẽ thoát tập lệnh ngay khi có mã trả về "thô" (khác XNUMX). Vì vậy, tốt hơn là giới thiệu cách xử lý lỗi tùy chỉnh, ví dụ:

#!/bin/bash
error_exit() {
  line=$1
  shift 1
  echo "ERROR: non zero return code from line: $line -- $@"
  exit 1
}
a=0
let a++ || error_exit "$LINENO" "let operation returned non 0 code"
echo "you will never see me"
# run it, now we have useful debugging output
$ bash foo.sh
ERROR: non zero return code from line: 9 -- let operation returned non 0 code

Viết tập lệnh theo cách này buộc bạn phải cẩn thận hơn về hành vi của tất cả các lệnh trong tập lệnh và lường trước khả năng xảy ra lỗi trước khi nó khiến bạn bất ngờ.

ShellCheck để phát hiện lỗi trong quá trình phát triển

Thật đáng để tích hợp một cái gì đó như Kiểm tra vỏ vào quy trình phát triển và thử nghiệm của bạn để kiểm tra mã bash của bạn theo các phương pháp hay nhất.

Tôi sử dụng nó trong môi trường phát triển cục bộ của mình để nhận báo cáo về cú pháp, ngữ nghĩa và một số lỗi trong mã mà tôi có thể đã bỏ sót khi phát triển. Đây là một công cụ phân tích tĩnh cho các tập lệnh bash của bạn và tôi thực sự khuyên bạn nên sử dụng nó.

Sử dụng mã thoát của riêng bạn

Mã trả về trong POSIX không chỉ là 201 hoặc 254 mà còn là XNUMX hoặc giá trị khác XNUMX. Sử dụng các tính năng này để trả về mã lỗi tùy chỉnh (trong khoảng XNUMX-XNUMX) cho các trường hợp lỗi khác nhau.

Thông tin này sau đó có thể được sử dụng bởi các tập lệnh khác bao bọc tập lệnh của bạn để hiểu chính xác loại lỗi đã xảy ra và phản ứng tương ứng:

#!/usr/bin/env bash

SUCCESS=0
FILE_NOT_FOUND=240
DOWNLOAD_FAILED=241

function read_file() {
  if ${file_not_found}; then
    return ${FILE_NOT_FOUND}
  fi
}

Lưu ý: vui lòng đặc biệt cẩn thận với tên biến bạn xác định để tránh vô tình ghi đè các biến môi trường.

Chức năng ghi nhật ký

Việc ghi nhật ký đẹp và có cấu trúc là điều quan trọng để dễ dàng hiểu được kết quả tập lệnh của bạn. Cũng như các ngôn ngữ lập trình cấp cao khác, tôi luôn sử dụng các hàm ghi nhật ký gốc trong các tập lệnh bash của mình, chẳng hạn như __msg_info, __msg_error và như vậy.

Điều này giúp cung cấp cấu trúc ghi nhật ký được tiêu chuẩn hóa bằng cách thực hiện các thay đổi chỉ ở một nơi:

#!/usr/bin/env bash

function __msg_error() {
    [[ "${ERROR}" == "1" ]] && echo -e "[ERROR]: $*"
}

function __msg_debug() {
    [[ "${DEBUG}" == "1" ]] && echo -e "[DEBUG]: $*"
}

function __msg_info() {
    [[ "${INFO}" == "1" ]] && echo -e "[INFO]: $*"
}

__msg_error "File could not be found. Cannot proceed"

__msg_debug "Starting script execution with 276MB of available RAM"

Tôi thường cố gắng đưa một số loại cơ chế vào kịch bản của mình __init, trong đó các biến ghi nhật ký và các biến hệ thống khác được khởi tạo hoặc đặt thành giá trị mặc định. Các biến này cũng có thể được đặt từ các tùy chọn dòng lệnh trong quá trình gọi tập lệnh.

Ví dụ: một cái gì đó như:

$ ./run-script.sh --debug

Khi một tập lệnh như vậy được thực thi, nó đảm bảo rằng các cài đặt trên toàn hệ thống được đặt thành giá trị mặc định nếu chúng được yêu cầu hoặc ít nhất được khởi tạo thành giá trị nào đó phù hợp nếu cần.

Tôi thường đưa ra lựa chọn những gì nên khởi tạo và những gì không nên làm dựa trên sự cân bằng giữa giao diện người dùng và các chi tiết về cấu hình mà người dùng có thể/nên tìm hiểu kỹ.

Kiến trúc để tái sử dụng và làm sạch trạng thái hệ thống

Mã mô-đun/có thể tái sử dụng

├── framework
│   ├── common
│   │   ├── loggers.sh
│   │   ├── mail_reports.sh
│   │   └── slack_reports.sh
│   └── daily_database_operation.sh

Tôi giữ một kho lưu trữ riêng mà tôi có thể sử dụng để khởi tạo tập lệnh dự án/bash mới mà tôi muốn phát triển. Bất cứ thứ gì có thể tái sử dụng đều có thể được lưu trữ trong kho lưu trữ và được các dự án khác muốn sử dụng chức năng đó truy xuất. Việc tổ chức các dự án theo cách này giúp giảm đáng kể kích thước của các tập lệnh khác và cũng đảm bảo rằng cơ sở mã nhỏ và dễ kiểm tra.

Như trong ví dụ trên, tất cả các chức năng ghi nhật ký như __msg_info, __msg_error và những thứ khác, chẳng hạn như báo cáo Slack, được chứa riêng trong common/* và kết nối động trong các tình huống khác như daily_database_operation.sh.

Để lại một hệ thống sạch sẽ

Nếu bạn đang tải bất kỳ tài nguyên nào trong khi tập lệnh đang chạy, bạn nên lưu trữ tất cả dữ liệu đó trong một thư mục dùng chung có tên ngẫu nhiên, ví dụ: /tmp/AlRhYbD97/*. Bạn có thể sử dụng trình tạo văn bản ngẫu nhiên để chọn tên thư mục:

rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"

Sau khi hoàn thành công việc, việc dọn dẹp các thư mục đó có thể được cung cấp trong trình xử lý hook đã thảo luận ở trên. Nếu các thư mục tạm thời không được quan tâm, chúng sẽ tích tụ và ở một giai đoạn nào đó sẽ gây ra các sự cố không mong muốn trên máy chủ, chẳng hạn như đầy đĩa.

Sử dụng tập tin khóa

Thông thường, bạn cần đảm bảo rằng chỉ có một phiên bản của tập lệnh đang chạy trên máy chủ tại bất kỳ thời điểm nào. Điều này có thể được thực hiện bằng cách sử dụng các tập tin khóa.

Tôi thường tạo các tập tin khóa trong /tmp/project_name/*.lock và kiểm tra sự hiện diện của chúng ở đầu tập lệnh. Điều này giúp tập lệnh kết thúc một cách nhẹ nhàng và tránh những thay đổi không mong muốn đối với trạng thái hệ thống do một tập lệnh khác chạy song song. Không cần các tệp khóa nếu bạn cần thực thi song song cùng một tập lệnh trên một máy chủ nhất định.

Đo lường và cải thiện

Chúng ta thường cần làm việc với các tập lệnh chạy trong thời gian dài, chẳng hạn như các hoạt động cơ sở dữ liệu hàng ngày. Các hoạt động như vậy thường bao gồm một chuỗi các bước: tải dữ liệu, kiểm tra các điểm bất thường, nhập dữ liệu, gửi báo cáo trạng thái, v.v.

Trong những trường hợp như vậy, tôi luôn cố gắng chia tập lệnh thành các tập lệnh nhỏ riêng biệt và báo cáo trạng thái cũng như thời gian thực hiện của chúng bằng cách sử dụng:

time source "${filepath}" "${args}">> "${LOG_DIR}/RUN_LOG" 2>&1

Sau này tôi có thể thấy thời gian thực hiện với:

tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"

Điều này giúp tôi xác định các khu vực có vấn đề/chậm trong các tập lệnh cần tối ưu hóa.

Chúc may mắn!

Những gì khác để đọc:

  1. Go và bộ nhớ đệm GPU.
  2. Một ví dụ về ứng dụng hướng sự kiện dựa trên webhooks trong bộ lưu trữ đối tượng S3 của Giải pháp đám mây Mail.ru.
  3. Kênh telegram của chúng tôi về chuyển đổi kỹ thuật số.

Nguồn: www.habr.com

Thêm một lời nhận xét