Mô phỏng mutex/semaphore trên shell script
Linux
81
White

huydx viết ngày 26/09/2017

Why

Mutex/Semaphore là những khái niệm thông dụng trong những ngôn ngữ bậc cao, và support parallel programming như là C++, Java, Golang... Trong khi đó shell script lại là một ngôn ngữ vô cùng đơn giản, vậy tại sao lại cần dùng concept này cho shell script?
Về mặt lý thuyết, shell script cũng support việc chạy "song song" thông qua việc dùng toán tử "&" để chạy một command ở background của terminal đang bật. Do đó việc làm thế nào để "lock" một tài nguyên hoặc một xử lý là việc cần thiết.

Bài toán mình gặp phải trong thực tế là mình có 1 crontab chạy mỗi 1 minute để deploy một binary

* * * * * root /path/deploy-agent.sh >> /path/deploy-agent.log 2>&1

Nếu deploy-agent.sh chắc chắn hoàn thành trong 1 phút thì không sao, nhưng rất tiếc điều đó không đảm bảo trong tình huống của mình. Ngoài ra trong script này thì có rất nhiều thao tác nguy hiểm mà chỉ cần bị race condition thì sẽ dẫn đến 1 trạng thái không thể hồi phục.
Để tránh điều đó xảy ra thì việc lock lại tài nguyên để khi 1 round cron lần này chưa chạy xong, thì round kế tiếp sẽ phải chờ. Vậy làm thế nào để thực hiện thao tác lock trên shell script?

How

Cách đơn giản nhất chính là việc sử dụng 1 file thay cho lock. Việc file đó tồn tại nghĩa là tài nguyên đang được sử dụng:

if [ - f /tmp/deploy.lock ]; then
   touch /tmp/deploy.lock
   ( do_some_work )
fi
rm /tmp/deploy.lock 

Có 2 vẫn đề ở cách làm ở trên:

  • Đoạn code trên có race condition ở đoạn "touch" và check [ -f ]. Trong trường hợp này của tôi, khi mỗi cron cách nhau khá xa thì việc đó có lẽ không phải vấn đề, nhưng chúng ta cần một cách làm khác tốt hơn.
  • Nếu do_some_work mãi không kết thúc thì sao, khi đó lock sẽ bị giữ mãi. Việc này là hoàn toàn có thể xảy ra bởi network congestion khi thực hiện thao tác download.

Xử lý vấn đề đầu tiên khá đơn giản, bash có 1 option là "noclobber" để giúp tránh việc cùng ghi vào 1 file đã được ghi vào thông qua toán tử ">".
Ngoài ra chúng ta có thể sử dụng thêm trap (giống như defer trong golang) để giúp việc clean up lock file trở nên đơn giản hơn. Thêm nữa, chúng ta có thể lưu lại PID hiện tại vào file lock, việc này sẽ rất hữu ích trong việc xử lý vấn đề thứ 2 mà chúng ta chuẩn bị nói đến dưới đây.

if ( set -o noclobber; echo "$$" > "/tmp/deploy.lock") 2> /dev/null; then
  trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT
  (do_some_work)
fi

Xử lý vấn đề thứ 2 timeout phức tạp hơn, do công cụ có trong tay chúng ta không nhiều. Có 2 cách có thể làm

Cách 1 là sử dụng hàm sleep, chúng ta cho chạy một function mà sleep N giây, sau đó xoá đi file lock tại background.

function clean_up() {
 sleep 5
 current_pid = $$
 if [ current_pid -eq `cat /tmp/deploy.lock` ]; then
  rm  /tmp/deploy.lock
 fi
}

clean_up &
do_some_work

Điểm phức tạp của cách này là bạn phải chắc chắn thao tác clean_up đến từ cùng 1 process, việc này có thể thực hiện dễ dàng thông qua việc kiểm tra PID hiện tại và PID đã được ghi vào file lock.

Cách 2 là chúng ta có thể dùng utility stat để xem thời điểm tạo ra file lock, sau đó so sánh với thời điểm hiện tại

if [ -f /tmp/deploy.lock ]; then
    _stat=`stat --format='%Y' /tmp/deploy.lock`
    _date=`date '+%s'`
    _diff=`expr ${_date} - ${_stat}`
    if [ ${_diff} -lt 5 ]; then
        echo "retry next time.";
        quit 1
    fi

    echo "stop previous process..." 1>&2
    _pid=`cat /tmp/deploy.lock`
    kill ${_pid}
    rm -f /tmp/deploy.lock
    quit 1
fi
echo "$$" > /tmp/deploy.lock

Cách này cũng không quá khó, tuy nhiên chúng ta vẫn có cảm giác là phải có 1 cách nào đó tốt hơn.

Cám ơn bạn đã đọc đến đây :D, thực ra để giải quyết cả 2 vấn đề: lock và timeout, linux cung cấp cho chúng ta một tiện ích 2 trong 1, với tên gọi là flock. Interface và cách sử dụng flock thể hiện ở đoạn sample code dưới đây

LOCKFILE="/tmp/example.lock"
PIDFILE="/tmp/example.pid"

set -e

function doDeploy() {
  echo "execute"
  sleep 10
}

if [ -x "$(command -v flock)" ]; then
(
if [ ! -f "${PIDFILE}" ]; then
   echo "$$" > "${PIDFILE}"
fi
flock -w 5 -x 200 || {
      echo "ERROR: lock timeout" 1>&2
      if [ -f ${PIDFILE} ]; then
        p=`cat ${PIDFILE}`
        echo "kill slow process ${p}"
        kill -9 "${p}"
        rm -rf "${PIDFILE}"
      fi
      echo "exit, wait to next cron turn to execute"
      exit 1;
    }
    doDeploy
    rm -rf "${PIDFILE}"
) 200> "${LOCKFILE}"
fi

Parameter quan trọng nhất ở đây là -w có nghĩa là timeout. Flock sẽ đợi cho đến hết timeout, bằng không sẽ trả về error code. Chúng ta dùng toán tử or || để check error code và chạy đoạn code xử lý khi timeout.
Một điểm khá thú vị mà bạn có thể thấy ở trên là pattern khi gọi flock là

( 
flock -s 200 
# ... commands executed under lock ... 
) 200>/deploy.lock

Trong unix thì () nghĩa là subshell, tức là đoạn code trong đó sẽ được fork ra xử lý trong 1 shell mới. Flock lấy đầu vào là 1 file descriptor number, và ở đây 200 cũng chỉ là 1 cái fd bất kì, không có ý nghĩa gì cả, và con số đó được lưu vào trong deploy.lock file để flock trong lần gọi tiếp theo sẽ biết là mình cần lock cùng 1 file, cùng 1 fd. Chi tiết về con số 200 cũng như pattern gọi này bạn có thể xem ở đây.

Chúng ta cũng cần check flock có tồn tại hay không thông qua phép check -x (if [ -x $(command -v flock) ]).

Có một điểm cần chú ý là: trong block doDeploy ở ví dụ ở trên, nếu bạn thực hiện một thao tác fork một process, mà process đó sẽ tiếp tục chạy ngay cả khi quá trình deploy đã kết thúc, thì flock sẽ bị giữ mãi mãi, vì khi đó người giữ flock sẽ là process con chứ không phải là process thực hiện doDeploy nữa. Để test hành vi này thì bạn chỉ cần cho hàm sleep ở trên thành background, và để thời gian sleep lớn lớn 1 chút.

function doDeploy() {
  echo "execute"
  sleep 1000 &
}

Take away

Vậy bạn sẽ take away được gì từ bài viết này?

  • Thực hiện lock trên bash mệt hơn nhiều so với trên một ngôn ngữ bậc cao
  • Cách sử dụng flock để thực hiện lock trên bash
Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

huydx

116 bài viết.
928 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
148 14
Introduction (Link) là một cuộc thi ở Nhật, và cũng chỉ có riêng ở Nhật. Đây là một cuộc thi khá đặc trưng bởi sự thú vị của cách thi của nó, những...
huydx viết gần 2 năm trước
148 14
White
117 15
Happy programmer là gì nhỉ, chắc ai đọc xong title của bài post này cũng không hiểu ý mình định nói đến là gì :D. Đầu tiên với cá nhân mình thì hap...
huydx viết 3 năm trước
117 15
White
94 10
(Ảnh) Mở đầu Chắc nhiều bạn đã nghe đến khái niệm oauth. Về cơ bản thì oauth là một phương thức chứng thực, mà nhờ đó một web service hay một ap...
huydx viết gần 3 năm trước
94 10
Bài viết liên quan
White
1 0
sudo du sh
t viết 2 năm trước
1 0
White
34 10
Thời kỳ mới đi làm tôi nghĩ cứ phải gõ thật nhiều cho quen cho nhớ nhưng lâu dần việc đó cho cảm giác thật nhàm chán. Hiện giờ, những gì tôi hay là...
manhdung viết 3 năm trước
34 10
White
1 0
Sử dụng option I với xargs Với option I thì bạn có thể sử dụng place holder với biến được lấy ra từ xargs man của option này: I replacestr R...
LinhPT viết 2 năm trước
1 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
116 bài viết.
928 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!