背景
二代宏基因组分析的自动化流程的对数据质控处理阶段,在宏基因组的分析中,样本可能会存在宿主污染的情况,这时候需要对数据进行去宿主的操作,操作的方法就是用bowtie2软件对宿主的序列建索引,使用双端clean取mapping索引,确定宿主序列。在自动化运行中碰到了一个问题,目前一个项目自动化都是在一个路径启动的,不同样本在分批下机的时候可以分批触发自动化,在每个样本的路径下进行质控操作。在含有加测样本下机的时候,需要等待之前的样本运行完成之后,再次重新启动之前的原文库和加测的文库,这样就完成了加测的质控。但是发现在如果一个项目有宿主,在去宿主时,三个质控的任务在先后在同一个路径下进行建索引,建索引完成后对索引文件进行md5计算,虽然每个任务在运行开始判断了md5的文件是否存在,如果存在会对md5进行校验,如果不存在就会在路径下建索引。这个操作大部分都是没问题的,只是我这个项目三个质控的任务先后启动,有两个任务先进行了md5检验,发现都没有md5的文件,所以同时建了索引,导致有两个任务对相同的文件进行了写入。
解决办法
方法 1: 利用文件锁定机制
在开始建索引之前,使用文件锁定机制(如 flock 或基于文件系统的锁定)确保同一时间只有一个任务进行索引的构建。
示例代码(Bash脚本):
LOCKFILE=/path/to/lockfile.lock
# 尝试获取锁
exec 200>$LOCKFILE
flock -n 200 || { echo "Another instance is running"; exit 1; }
# 检查是否已经存在索引文件
if [ ! -f /path/to/index.md5 ]; then
# 开始建索引
bowtie2-build /path/to/host_genome.fa /path/to/index_prefix
# 计算md5
md5sum /path/to/index_prefix.* > /path/to/index.md5
fi
# 释放锁
flock -u 200
方法 2: 检查和等待
在开始建索引之前,检查索引文件是否已经存在。如果索引文件不存在,可以启动一个循环,等待一段时间后再检查,直到索引文件生成。
示例代码(Bash脚本):
LOCKFILE=/path/to/index.lock
# 创建锁文件
touch $LOCKFILE
# 检查是否已经存在索引文件
while [ ! -f /path/to/index.md5 ]; do
sleep 10
if [ ! -f $LOCKFILE ]; then
# 如果锁文件被删除,说明索引构建完成
break
fi
done
if [ ! -f /path/to/index.md5 ]; then
# 开始建索引
bowtie2-build /path/to/host_genome.fa /path/to/index_prefix
# 计算md5
md5sum /path/to/index_prefix.* > /path/to/index.md5
# 删除锁文件
rm -f $LOCKFILE
fi
方法 3: 使用分布式锁服务
如果你的环境支持,你可以使用分布式锁服务,比如 etcd、Zookeeper 或 Redis 的分布式锁来确保同一时间只有一个任务进行索引构建。
示例代码(Python使用Redis分布式锁):
import redis
import time
import subprocess
r = redis.Redis(host='localhost', port=6379, db=0)
lock = r.lock('index_lock', timeout=600)
if lock.acquire(blocking=True):
try:
# 检查是否已经存在索引文件
if not os.path.exists('/path/to/index.md5'):
# 开始建索引
subprocess.run(['bowtie2-build', '/path/to/host_genome.fa', '/path/to/index_prefix'])
# 计算md5
subprocess.run(['md5sum', '/path/to/index_prefix.*', '>', '/path/to/index.md5'])
finally:
lock.release()
else:
print("Another instance is running")
方法 4: 使用数据库记录状态
可以在数据库中记录索引构建的状态,任务开始时先查询数据库中的状态,如果正在构建或者已经构建完成,就不再重复构建。
示例代码(伪代码):
import sqlite3
import time
conn = sqlite3.connect('/path/to/db.sqlite3')
cursor = conn.cursor()
# 检查索引状态
cursor.execute("SELECT status FROM index_status WHERE id = 1")
status = cursor.fetchone()
if status == 'completed':
print("Index already built")
elif status == 'building':
while status == 'building':
time.sleep(10)
cursor.execute("SELECT status FROM index_status WHERE id = 1")
status = cursor.fetchone()
else:
cursor.execute("UPDATE index_status SET status = 'building' WHERE id = 1")
conn.commit()
# 开始建索引
subprocess.run(['bowtie2-build', '/path/to/host_genome.fa', '/path/to/index_prefix'])
# 计算md5
subprocess.run(['md5sum', '/path/to/index_prefix.*', '>', '/path/to/index.md5'])
cursor.execute("UPDATE index_status SET status = 'completed' WHERE id = 1")
conn.commit()
通过这些方法,可以确保在同一时间只有一个任务进行索引构建,避免多个任务同时写入相同文件导致冲突的问题。
结论
根据实际情况方法2的方向比较适合。但是方法2中的锁文件机制存在潜在的竞态条件问题。如果两个任务几乎同时启动,都发现锁文件不存在,并且都尝试去创建锁文件和建索引,那么他们可能会同时进行索引构建。
可以改进方法2,使其更加可靠。在此方案中,我们需要确保锁文件的创建和检查是原子的操作。一个常见的解决竞态条件的方法是使用文件创建的原子性操作来确保互斥。我们可以利用 mktemp 或 ln 命令来创建临时文件作为锁。
改进的方案:使用 ln 命令创建锁文件
下面是改进后的脚本,它使用 ln 命令来创建锁文件,确保只有一个任务能够成功创建锁文件并进行索引构建:
LOCKFILE=/path/to/index.lock
INDEX_MD5=/path/to/index.md5
# 尝试创建锁文件
if ln -s "$$" "$LOCKFILE" 2>/dev/null; then
# 如果成功创建锁文件,进行索引构建
if [ ! -f "$INDEX_MD5" ]; then
echo "Building index..."
bowtie2-build /path/to/host_genome.fa /path/to/index_prefix
# 计算md5
md5sum /path/to/index_prefix.* > "$INDEX_MD5"
fi
# 完成后删除锁文件
rm -f "$LOCKFILE"
else
# 如果锁文件已经存在,等待索引构建完成
echo "Waiting for index to be built..."
while [ -L "$LOCKFILE" ]; do
sleep 10
done
echo "Index building completed by another process."
fi
核心思路:
使用 ln -s 命令尝试创建一个软链接作为锁文件。$$ 是当前进程的 PID,这样可以在锁文件中记录是哪个进程持有锁。
如果创建锁文件成功,检查索引文件是否存在。如果不存在,则进行索引构建并生成 MD5 文件。
如果创建锁文件失败,说明另一个进程已经持有锁,当前进程等待锁文件被删除(即索引构建完成)。
解释:
ln -s "$$" "$LOCKFILE":尝试创建一个软链接作为锁文件。如果锁文件已经存在,创建操作会失败。
2>/dev/null:抑制错误输出。
while [ -L "$LOCKFILE" ]: 如果锁文件存在,当前进程将进入等待状态,每隔10秒检查一次锁文件的存在性,直到锁文件被删除。
通过这种方法,可以确保只有一个任务能够创建锁文件并进行索引构建,其他任务会等待索引构建完成,从而避免竞态条件问题。
看没看懂都点个赞呗~