Linux 中多任務(wù)寫入同一文件解決辦法

背景

二代宏基因組分析的自動(dòng)化流程的對數(shù)據(jù)質(zhì)控處理階段,在宏基因組的分析中,樣本可能會(huì)存在宿主污染的情況,這時(shí)候需要對數(shù)據(jù)進(jìn)行去宿主的操作,操作的方法就是用bowtie2軟件對宿主的序列建索引,使用雙端clean取mapping索引,確定宿主序列。在自動(dòng)化運(yùn)行中碰到了一個(gè)問題,目前一個(gè)項(xiàng)目自動(dòng)化都是在一個(gè)路徑啟動(dòng)的,不同樣本在分批下機(jī)的時(shí)候可以分批觸發(fā)自動(dòng)化,在每個(gè)樣本的路徑下進(jìn)行質(zhì)控操作。在含有加測樣本下機(jī)的時(shí)候,需要等待之前的樣本運(yùn)行完成之后,再次重新啟動(dòng)之前的原文庫和加測的文庫,這樣就完成了加測的質(zhì)控。但是發(fā)現(xiàn)在如果一個(gè)項(xiàng)目有宿主,在去宿主時(shí),三個(gè)質(zhì)控的任務(wù)在先后在同一個(gè)路徑下進(jìn)行建索引,建索引完成后對索引文件進(jìn)行md5計(jì)算,雖然每個(gè)任務(wù)在運(yùn)行開始判斷了md5的文件是否存在,如果存在會(huì)對md5進(jìn)行校驗(yàn),如果不存在就會(huì)在路徑下建索引。這個(gè)操作大部分都是沒問題的,只是我這個(gè)項(xiàng)目三個(gè)質(zhì)控的任務(wù)先后啟動(dòng),有兩個(gè)任務(wù)先進(jìn)行了md5檢驗(yàn),發(fā)現(xiàn)都沒有md5的文件,所以同時(shí)建了索引,導(dǎo)致有兩個(gè)任務(wù)對相同的文件進(jìn)行了寫入。

解決辦法

方法 1: 利用文件鎖定機(jī)制

在開始建索引之前,使用文件鎖定機(jī)制(如 flock 或基于文件系統(tǒng)的鎖定)確保同一時(shí)間只有一個(gè)任務(wù)進(jìn)行索引的構(gòu)建。
示例代碼(Bash腳本):

LOCKFILE=/path/to/lockfile.lock

# 嘗試獲取鎖
exec 200>$LOCKFILE
flock -n 200 || { echo "Another instance is running"; exit 1; }

# 檢查是否已經(jīng)存在索引文件
if [ ! -f /path/to/index.md5 ]; then
    # 開始建索引
    bowtie2-build /path/to/host_genome.fa /path/to/index_prefix
    # 計(jì)算md5
    md5sum /path/to/index_prefix.* > /path/to/index.md5
fi

# 釋放鎖
flock -u 200

方法 2: 檢查和等待

在開始建索引之前,檢查索引文件是否已經(jīng)存在。如果索引文件不存在,可以啟動(dòng)一個(gè)循環(huán),等待一段時(shí)間后再檢查,直到索引文件生成。
示例代碼(Bash腳本):

LOCKFILE=/path/to/index.lock

# 創(chuàng)建鎖文件
touch $LOCKFILE

# 檢查是否已經(jīng)存在索引文件
while [ ! -f /path/to/index.md5 ]; do
    sleep 10
    if [ ! -f $LOCKFILE ]; then
        # 如果鎖文件被刪除,說明索引構(gòu)建完成
        break
    fi
done

if [ ! -f /path/to/index.md5 ]; then
    # 開始建索引
    bowtie2-build /path/to/host_genome.fa /path/to/index_prefix
    # 計(jì)算md5
    md5sum /path/to/index_prefix.* > /path/to/index.md5
    # 刪除鎖文件
    rm -f $LOCKFILE
fi

方法 3: 使用分布式鎖服務(wù)

如果你的環(huán)境支持,你可以使用分布式鎖服務(wù),比如 etcd、Zookeeper 或 Redis 的分布式鎖來確保同一時(shí)間只有一個(gè)任務(wù)進(jìn)行索引構(gòu)建。
示例代碼(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:
        # 檢查是否已經(jīng)存在索引文件
        if not os.path.exists('/path/to/index.md5'):
            # 開始建索引
            subprocess.run(['bowtie2-build', '/path/to/host_genome.fa', '/path/to/index_prefix'])
            # 計(jì)算md5
            subprocess.run(['md5sum', '/path/to/index_prefix.*', '>', '/path/to/index.md5'])
    finally:
        lock.release()
else:
    print("Another instance is running")

方法 4: 使用數(shù)據(jù)庫記錄狀態(tài)

可以在數(shù)據(jù)庫中記錄索引構(gòu)建的狀態(tài),任務(wù)開始時(shí)先查詢數(shù)據(jù)庫中的狀態(tài),如果正在構(gòu)建或者已經(jīng)構(gòu)建完成,就不再重復(fù)構(gòu)建。
示例代碼(偽代碼):

import sqlite3
import time

conn = sqlite3.connect('/path/to/db.sqlite3')
cursor = conn.cursor()

# 檢查索引狀態(tài)
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'])
    # 計(jì)算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()

通過這些方法,可以確保在同一時(shí)間只有一個(gè)任務(wù)進(jìn)行索引構(gòu)建,避免多個(gè)任務(wù)同時(shí)寫入相同文件導(dǎo)致沖突的問題。

結(jié)論

根據(jù)實(shí)際情況方法2的方向比較適合。但是方法2中的鎖文件機(jī)制存在潛在的競態(tài)條件問題。如果兩個(gè)任務(wù)幾乎同時(shí)啟動(dòng),都發(fā)現(xiàn)鎖文件不存在,并且都嘗試去創(chuàng)建鎖文件和建索引,那么他們可能會(huì)同時(shí)進(jìn)行索引構(gòu)建。
可以改進(jìn)方法2,使其更加可靠。在此方案中,我們需要確保鎖文件的創(chuàng)建和檢查是原子的操作。一個(gè)常見的解決競態(tài)條件的方法是使用文件創(chuàng)建的原子性操作來確?;コ?。我們可以利用 mktemp 或 ln 命令來創(chuàng)建臨時(shí)文件作為鎖。

改進(jìn)的方案:使用 ln 命令創(chuàng)建鎖文件

下面是改進(jìn)后的腳本,它使用 ln 命令來創(chuàng)建鎖文件,確保只有一個(gè)任務(wù)能夠成功創(chuàng)建鎖文件并進(jìn)行索引構(gòu)建:

LOCKFILE=/path/to/index.lock
INDEX_MD5=/path/to/index.md5

# 嘗試創(chuàng)建鎖文件
if ln -s "$$" "$LOCKFILE" 2>/dev/null; then
    # 如果成功創(chuàng)建鎖文件,進(jìn)行索引構(gòu)建
    if [ ! -f "$INDEX_MD5" ]; then
        echo "Building index..."
        bowtie2-build /path/to/host_genome.fa /path/to/index_prefix
        # 計(jì)算md5
        md5sum /path/to/index_prefix.* > "$INDEX_MD5"
    fi
    # 完成后刪除鎖文件
    rm -f "$LOCKFILE"
else
    # 如果鎖文件已經(jīng)存在,等待索引構(gòu)建完成
    echo "Waiting for index to be built..."
    while [ -L "$LOCKFILE" ]; do
        sleep 10
    done
    echo "Index building completed by another process."
fi

核心思路:

使用 ln -s 命令嘗試創(chuàng)建一個(gè)軟鏈接作為鎖文件。$$ 是當(dāng)前進(jìn)程的 PID,這樣可以在鎖文件中記錄是哪個(gè)進(jìn)程持有鎖。
如果創(chuàng)建鎖文件成功,檢查索引文件是否存在。如果不存在,則進(jìn)行索引構(gòu)建并生成 MD5 文件。
如果創(chuàng)建鎖文件失敗,說明另一個(gè)進(jìn)程已經(jīng)持有鎖,當(dāng)前進(jìn)程等待鎖文件被刪除(即索引構(gòu)建完成)。

解釋:

ln -s "$$" "$LOCKFILE":嘗試創(chuàng)建一個(gè)軟鏈接作為鎖文件。如果鎖文件已經(jīng)存在,創(chuàng)建操作會(huì)失敗。
2>/dev/null:抑制錯(cuò)誤輸出。
while [ -L "$LOCKFILE" ]: 如果鎖文件存在,當(dāng)前進(jìn)程將進(jìn)入等待狀態(tài),每隔10秒檢查一次鎖文件的存在性,直到鎖文件被刪除。
通過這種方法,可以確保只有一個(gè)任務(wù)能夠創(chuàng)建鎖文件并進(jìn)行索引構(gòu)建,其他任務(wù)會(huì)等待索引構(gòu)建完成,從而避免競態(tài)條件問題。

看沒看懂都點(diǎn)個(gè)贊唄~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容