需求场景
有一个python上线部署脚本,兼具重启和健康检查功能,当上线重启的时候希望定时任务的健康检查不要触发任何重启操作。
同时,如果执行重启的时候碰到正在例行的健康检查,等待,直到健康检查结束。
即: 当一个脚本被多次调用时(非多线程),使用flock控制其内同一个函数的调用控制。
由于是多进程,线程锁是无效的,简单的使用python的fcntl模块的文件锁可以实现这个需求
关于文件锁
linux内核提供的一种进程之间资源防竞争机制。防止多进程间使用同一个共享资源时同时操作造成错乱。
鉴于linux中一切皆文件,所以文件锁有很多使用的空间。
锁分为劝告锁和强制锁。劝告锁只是一个非强制的约定规则,即可以不遵守。所以需要不同进程间约定协调。而强制锁则由内核进行强制约束。
此外,两种锁都可以有共享和排他的分类,分别是共享锁(读锁)和排他锁(写锁)
- 共享锁:
我在它身上上了一把锁,你也可以过来读取它 - 排他锁:
我在它身上上了一把锁,我要写东西进去,这期间你不能读也不能写
关于两种锁的不同进程间的兼容关系
进程B共享锁 | 进程B排他锁 | |
---|---|---|
进程A锁 | ||
无 | 是 | 是 |
共享锁 | 是 | 否 |
排他锁 | 否 | 否 |
强制锁由内核进行强制约束,当文件加有共享锁后,其他进程对文件对写操作会被内核阻止,当文件加有排他锁后,其他进程任何操作都会被阻止塞。阻止包括是堵塞-等待,非堵塞-立刻返回错误信号EAGAIN。
详细如下表
当前锁 | 堵塞读 | 堵塞写 | 非堵塞读 | 非堵塞写 |
---|---|---|---|---|
共享锁 | 正常读 | 堵塞 | 正常读 | EAGAIN |
排他锁 | 堵塞 | 堵塞 | EAGAIN | EAGAIN |
注意:
- 一个进程可以对同一文件同时上共享锁和排他锁
- 劝告锁只有劝告作用,需要进程间自我约束协调
- 强制锁后的阻止包括堵塞和非堵塞,堵塞会持续等待直到锁解除,非堵塞则会直接返回错误信号并退出
shell的flock命令
shell提供了flock命令实现文件锁。flock只可以添加劝告锁,并且只能针对整个文件进行加锁,无法对文件的部分进行加锁。
flock对强制锁的实现
进程A调用了flock对文件t.txt加LOCK.EX的前提下,进程B对t.txt的访问情况
进程B | 访问情况 |
---|---|
直接访问t.txt | 可以访问 |
先调用flock加锁,再访问t.txt | 不可访问 |
也就是说,具体使用的时候不同进程必须先调用flock加锁实现强制锁的功能。
flock的参数
Usage:
flock [-sxun][-w #] fd# #第一种模式直接对文件描述符加锁,解锁。其实当程序执行完毕,文件描述符自动关闭后锁也会自动施放
flock [-sxon][-w #] file [-c] command... #第二种模式先对文件加锁,然后执行命令
flock [-sxon][-w #] directory [-c] command... #第三种模式先对目录加锁,然后执行命令
Options:
-s --shared Get a shared lock # 共享锁
-x --exclusive Get an exclusive lock #排他锁
-u --unlock Remove a lock # 解锁
-n --nonblock Fail rather than wait # 非堵塞,直接报错
-w --timeout Wait for a limited amount of time #堵塞,等待时间
-o --close Close file descriptor before running command #第二/三种模式下先关闭文件描述符,再执行命令
-c --command Run a single command string through the shell #加锁后执行的shell命令
-h --help Display this text #显示帮助
-V --version Display version # 显示版本信息
两种使用例子:
shell scripts:
#!/bin/bash
#/opt/test.sh
(
flock -xn 110 && echo success || echo failed ;
) 110>/tmp.lock
crontab:
* * * * * flock -xn /tmp/crontab.lock -c 'bash /opt/test.sh'
前者封装在脚本里需要判断flock的执行结果$?,然后根据执行结果判断下一步操作。
crontab中可以利用flock的-c参数,加锁成功才会执行-c后面的cmd。
python的fcntl模块
fcntl模块的flock()跟shell的flock类似,是对unix系统flock()的封装。只是无法实现等待时间(flock -w)的操作。堵塞状态下会一直等待下去,非堵塞状态会抛出OSError异常。
lockf()是对unix系统fcntl()的封装,不仅仅能实现对文件整体的lock,还能实现对指定文件位置的lock。
看下文档中对于flock()几种锁的介绍
man 2 flock
FLOCK(2) Linux Programmer's Manual FLOCK(2)
NAME
flock - apply or remove an advisory lock on an open file
SYNOPSIS
#include <sys/file.h>
int flock(int fd, int operation);
DESCRIPTION
Apply or remove an advisory lock on the open file specified by fd. The argument operation is one of the following:
LOCK_SH Place a shared lock. More than one process may hold a shared lock for a given file at a given time.
LOCK_EX Place an exclusive lock. Only one process may hold an exclusive lock for a given file at a given time.
LOCK_UN Remove an existing lock held by this process.
A call to flock() may block if an incompatible lock is held by another process. To make a nonblocking request, include LOCK_NB (by ORing) with any of the above operations.
A single file may not simultaneously have both shared and exclusive locks.
其中提及三种锁操作,LOCK_SH,LOCK_EX,LOCK_UN,分别是共享锁,排他锁,解锁。同时LOCK_NB是非堵塞机制,可以与共享锁或者排他锁共存配置
与shell类似,python使用文件锁可以很容易实现对文件的控制。文章前面需求背景中实现对脚本上线和健康检查的两个使用场景下重启方法的调用控制就是通过文件锁的方式实现的。
示例伪代码
#!/usr/bin/env python
# _*_ coding: utf-8 _*_
import sys
from subprocess import call
import fcntl
...
def do_stop_start(tag):
if tag == "kill":
do_cmd = kill_cmd
else:
do_cmd = start_cmd
for cmd in do_cmd:
call(cmd,shell=True)
...
if __name__ == "__main__":
#get FD of lock_file
deploy_file = open("/tmp/.lock","r")
if sys.argv[1] == "health":
fcntl.flock(deploy_file,fcntl.LOCK_EX|fcntl.LOCK_NB)
...
do_stop_start("start")
else:
fcntl.flock(deploy_file,fcntl.LOCK_EX)
...
do_stop_start("stop")
do_stop_start('start')
...
fcntl.flock(deploy_file,fcntl.LOCK_UN)
deploy_file.close()
这样脚本执行上线部署的时候,会尝试添加一个排他锁,而如果此前已经有健康检查执行的话,文件已经加了排他锁,脚本会直接进入堵塞状态,等待健康检查完成,再进行进程重启操作。
同样,如果健康检查执行之前,已经有上线部署,此时健康检查想给文件添加一个排他锁和非堵塞锁,由于此前上线部署已经加了排他锁,上锁失败,同时是非堵塞锁,直接抛出异常退出。
参考文档