【Zookeeper】原始碼分析之持久化(三)之FileTxnSnapLog

leesf發表於2017-01-14

一、前言

  前面分析了FileSnap,接著繼續分析FileTxnSnapLog原始碼,其封裝了TxnLog和SnapShot,其在持久化過程中是一個幫助類。

二、FileTxnSnapLog原始碼分析

  2.1 類的屬性  

public class FileTxnSnapLog {
    //the direcotry containing the 
    //the transaction logs
    // 日誌檔案目錄
    private final File dataDir;
    //the directory containing the
    //the snapshot directory
    // 快照檔案目錄
    private final File snapDir;
    // 事務日誌
    private TxnLog txnLog;
    // 快照
    private SnapShot snapLog;
    // 版本號
    public final static int VERSION = 2;
    // 版本
    public final static String version = "version-";

    // Logger
    private static final Logger LOG = LoggerFactory.getLogger(FileTxnSnapLog.class);
}

  說明:類的屬性中包含了TxnLog和SnapShot介面,即對FileTxnSnapLog的很多操作都會轉發給TxnLog和SnapLog進行操作,這是一種典型的組合方法。

  2.2 內部類

  FileTxnSnapLog包含了PlayBackListener內部類,用來接收事務應用過程中的回撥,在Zookeeper資料恢復後期,會有事務修正過程,此過程會回撥PlayBackListener來進行對應的資料修正。其原始碼如下 

public interface PlayBackListener {
    void onTxnLoaded(TxnHeader hdr, Record rec);
}

  說明:在完成事務操作後,會呼叫到onTxnLoaded方法進行相應的處理。

  2.3 建構函式

  FileTxnSnapLog的建構函式如下 

    public FileTxnSnapLog(File dataDir, File snapDir) throws IOException {
        LOG.debug("Opening datadir:{} snapDir:{}", dataDir, snapDir);
        // 在datadir和snapdir下生成version-2目錄
        this.dataDir = new File(dataDir, version + VERSION);
        this.snapDir = new File(snapDir, version + VERSION);
        if (!this.dataDir.exists()) { // datadir存在但無法建立目錄,則丟擲異常
            if (!this.dataDir.mkdirs()) {
                throw new IOException("Unable to create data directory "
                        + this.dataDir);
            }
        }
        if (!this.snapDir.exists()) { // snapdir存在但無法建立目錄,則丟擲異常
            if (!this.snapDir.mkdirs()) {
                throw new IOException("Unable to create snap directory "
                        + this.snapDir);
            }
        }
        // 給屬性賦值
        txnLog = new FileTxnLog(this.dataDir);
        snapLog = new FileSnap(this.snapDir);
    }

  說明:對於建構函式而言,其會在傳入的datadir和snapdir目錄下新生成version-2的目錄,並且會判斷目錄是否建立成功,之後會建立txnLog和snapLog。

  2.4 核心函式分析

  1. restore函式 

    public long restore(DataTree dt, Map<Long, Integer> sessions, 
            PlayBackListener listener) throws IOException {
        // 根據snap檔案反序列化dt和sessions
        snapLog.deserialize(dt, sessions);
        // 
        FileTxnLog txnLog = new FileTxnLog(dataDir);
        // 獲取比最後處理的zxid+1大的log檔案的迭代器
        TxnIterator itr = txnLog.read(dt.lastProcessedZxid+1);
        // 最大的zxid
        long highestZxid = dt.lastProcessedZxid;
        
        TxnHeader hdr;
        try {
            while (true) {
                // iterator points to 
                // the first valid txn when initialized
                // itr在read函式呼叫後就已經指向第一個合法的事務
                // 獲取事務頭
                hdr = itr.getHeader();
                if (hdr == null) { // 事務頭為空
                    //empty logs 
                    // 表示日誌檔案為空
                    return dt.lastProcessedZxid;
                }
                if (hdr.getZxid() < highestZxid && highestZxid != 0) { // 事務頭的zxid小於snapshot中的最大zxid並且其不為0,則會報錯
                    LOG.error("{}(higestZxid) > {}(next log) for type {}",
                            new Object[] { highestZxid, hdr.getZxid(),
                                    hdr.getType() });
                } else { // 重新賦值highestZxid
                    highestZxid = hdr.getZxid();
                }
                try {
                    // 在datatree上處理事務
                    processTransaction(hdr,dt,sessions, itr.getTxn());
                } catch(KeeperException.NoNodeException e) {
                   throw new IOException("Failed to process transaction type: " +
                         hdr.getType() + " error: " + e.getMessage(), e);
                }
                // 每處理完一個事務都會進行回撥
                listener.onTxnLoaded(hdr, itr.getTxn());
                if (!itr.next()) // 已無事務,跳出迴圈
                    break;
            }
        } finally {
            if (itr != null) { // 迭代器不為空,則關閉
                itr.close();
            }
        }
        // 返回最高的zxid
        return highestZxid;
    }

  說明:restore用於恢復datatree和sessions,其步驟大致如下

  ① 根據snapshot檔案反序列化datatree和sessions,進入②

  ② 獲取比snapshot檔案中的zxid+1大的log檔案的迭代器,以對log檔案中的事務進行迭代,進入③

  ③ 迭代log檔案的每個事務,並且將該事務應用在datatree中,同時會呼叫onTxnLoaded函式進行後續處理,進入④

  ④ 關閉迭代器,返回log檔案中最後一個事務的zxid(作為最高的zxid)

  其中會呼叫到FileTxnLog的read函式,read函式在FileTxnLog中已經進行過分析,會呼叫processTransaction函式,其原始碼如下 

    public void processTransaction(TxnHeader hdr,DataTree dt,
            Map<Long, Integer> sessions, Record txn)
        throws KeeperException.NoNodeException {
        // 事務處理結果
        ProcessTxnResult rc;
        switch (hdr.getType()) { // 確定事務型別
        case OpCode.createSession: // 建立會話
            // 新增進會話
            sessions.put(hdr.getClientId(),
                    ((CreateSessionTxn) txn).getTimeOut());
            if (LOG.isTraceEnabled()) {
                ZooTrace.logTraceMessage(LOG,ZooTrace.SESSION_TRACE_MASK,
                        "playLog --- create session in log: 0x"
                                + Long.toHexString(hdr.getClientId())
                                + " with timeout: "
                                + ((CreateSessionTxn) txn).getTimeOut());
            }
            // give dataTree a chance to sync its lastProcessedZxid
            // 處理事務
            rc = dt.processTxn(hdr, txn);
            break;
        case OpCode.closeSession: // 關閉會話
            // 會話中移除
            sessions.remove(hdr.getClientId());
            if (LOG.isTraceEnabled()) {
                ZooTrace.logTraceMessage(LOG,ZooTrace.SESSION_TRACE_MASK,
                        "playLog --- close session in log: 0x"
                                + Long.toHexString(hdr.getClientId()));
            }
            // 處理事務
            rc = dt.processTxn(hdr, txn);
            break;
        default:
            // 處理事務
            rc = dt.processTxn(hdr, txn);
        }

        /**
         * Snapshots are lazily created. So when a snapshot is in progress,
         * there is a chance for later transactions to make into the
         * snapshot. Then when the snapshot is restored, NONODE/NODEEXISTS
         * errors could occur. It should be safe to ignore these.
         */
        if (rc.err != Code.OK.intValue()) { // 忽略處理結果中可能出現的錯誤
            LOG.debug("Ignoring processTxn failure hdr:" + hdr.getType()
                    + ", error: " + rc.err + ", path: " + rc.path);
        }
    }

  說明:processTransaction會根據事務頭中記錄的事務型別(createSession、closeSession、其他型別)來進行相應的操作,對於createSession型別而言,其會將會話和超時時間新增至會話map中,對於closeSession而言,會話map會根據客戶端的id號刪除其會話,同時,所有的操作都會呼叫到dt.processTxn函式,其原始碼如下  

    public ProcessTxnResult processTxn(TxnHeader header, Record txn)
    {
        // 事務處理結果
        ProcessTxnResult rc = new ProcessTxnResult();

        try {
            // 從事務頭中解析出相應屬性並儲存至rc中
            rc.clientId = header.getClientId();
            rc.cxid = header.getCxid();
            rc.zxid = header.getZxid();
            rc.type = header.getType();
            rc.err = 0;
            rc.multiResult = null;
            switch (header.getType()) { // 確定事務型別
                case OpCode.create: // 建立結點
                    // 顯示轉化
                    CreateTxn createTxn = (CreateTxn) txn;
                    // 獲取建立結點路徑
                    rc.path = createTxn.getPath();
                    // 建立結點
                    createNode(
                            createTxn.getPath(),
                            createTxn.getData(),
                            createTxn.getAcl(),
                            createTxn.getEphemeral() ? header.getClientId() : 0,
                            createTxn.getParentCVersion(),
                            header.getZxid(), header.getTime());
                    break;
                case OpCode.delete: // 刪除結點
                    // 顯示轉化
                    DeleteTxn deleteTxn = (DeleteTxn) txn;
                    // 獲取刪除結點路徑
                    rc.path = deleteTxn.getPath();
                    // 刪除結點
                    deleteNode(deleteTxn.getPath(), header.getZxid());
                    break;
                case OpCode.setData: // 寫入資料
                    // 顯示轉化
                    SetDataTxn setDataTxn = (SetDataTxn) txn;
                    // 獲取寫入資料結點路徑
                    rc.path = setDataTxn.getPath();
                    // 寫入資料
                    rc.stat = setData(setDataTxn.getPath(), setDataTxn
                            .getData(), setDataTxn.getVersion(), header
                            .getZxid(), header.getTime());
                    break;
                case OpCode.setACL: // 設定ACL
                    // 顯示轉化
                    SetACLTxn setACLTxn = (SetACLTxn) txn;
                    // 獲取路徑
                    rc.path = setACLTxn.getPath();
                    // 設定ACL
                    rc.stat = setACL(setACLTxn.getPath(), setACLTxn.getAcl(),
                            setACLTxn.getVersion());
                    break;
                case OpCode.closeSession: // 關閉會話
                    // 關閉會話
                    killSession(header.getClientId(), header.getZxid());
                    break;
                case OpCode.error: // 錯誤
                    // 顯示轉化
                    ErrorTxn errTxn = (ErrorTxn) txn;
                    // 記錄錯誤
                    rc.err = errTxn.getErr();
                    break;
                case OpCode.check: // 檢查
                    // 顯示轉化
                    CheckVersionTxn checkTxn = (CheckVersionTxn) txn;
                    // 獲取路徑
                    rc.path = checkTxn.getPath();
                    break;
                case OpCode.multi: // 多個事務
                    // 顯示轉化
                    MultiTxn multiTxn = (MultiTxn) txn ;
                    // 獲取事務列表
                    List<Txn> txns = multiTxn.getTxns();
                    rc.multiResult = new ArrayList<ProcessTxnResult>();
                    boolean failed = false;
                    for (Txn subtxn : txns) { // 遍歷事務列表
                        if (subtxn.getType() == OpCode.error) {
                            failed = true;
                            break;
                        }
                    }

                    boolean post_failed = false;
                    for (Txn subtxn : txns) { // 遍歷事務列表,確定每個事務型別並進行相應操作
                        // 處理事務的資料
                        ByteBuffer bb = ByteBuffer.wrap(subtxn.getData());
                        Record record = null;
                        switch (subtxn.getType()) {
                            case OpCode.create:
                                record = new CreateTxn();
                                break;
                            case OpCode.delete:
                                record = new DeleteTxn();
                                break;
                            case OpCode.setData:
                                record = new SetDataTxn();
                                break;
                            case OpCode.error:
                                record = new ErrorTxn();
                                post_failed = true;
                                break;
                            case OpCode.check:
                                record = new CheckVersionTxn();
                                break;
                            default:
                                throw new IOException("Invalid type of op: " + subtxn.getType());
                        }
                        assert(record != null);
                        // 將bytebuffer轉化為record(初始化record的相關屬性)
                        ByteBufferInputStream.byteBuffer2Record(bb, record);
                       
                        if (failed && subtxn.getType() != OpCode.error){ // 失敗並且不為error型別
                            int ec = post_failed ? Code.RUNTIMEINCONSISTENCY.intValue() 
                                                 : Code.OK.intValue();

                            subtxn.setType(OpCode.error);
                            record = new ErrorTxn(ec);
                        }

                        if (failed) { // 失敗
                            assert(subtxn.getType() == OpCode.error) ;
                        }
                        
                        // 生成事務頭
                        TxnHeader subHdr = new TxnHeader(header.getClientId(), header.getCxid(),
                                                         header.getZxid(), header.getTime(), 
                                                         subtxn.getType());
                        // 遞迴呼叫處理事務
                        ProcessTxnResult subRc = processTxn(subHdr, record);
                        // 儲存處理結果
                        rc.multiResult.add(subRc);
                        if (subRc.err != 0 && rc.err == 0) {
                            rc.err = subRc.err ;
                        }
                    }
                    break;
            }
        } catch (KeeperException e) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Failed: " + header + ":" + txn, e);
            }
            rc.err = e.code().intValue();
        } catch (IOException e) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Failed: " + header + ":" + txn, e);
            }
        }
        /*
         * A snapshot might be in progress while we are modifying the data
         * tree. If we set lastProcessedZxid prior to making corresponding
         * change to the tree, then the zxid associated with the snapshot
         * file will be ahead of its contents. Thus, while restoring from
         * the snapshot, the restore method will not apply the transaction
         * for zxid associated with the snapshot file, since the restore
         * method assumes that transaction to be present in the snapshot.
         *
         * To avoid this, we first apply the transaction and then modify
         * lastProcessedZxid.  During restore, we correctly handle the
         * case where the snapshot contains data ahead of the zxid associated
         * with the file.
         */
        // 事務處理結果中儲存的zxid大於已經被處理的最大的zxid,則重新賦值
        if (rc.zxid > lastProcessedZxid) {
            lastProcessedZxid = rc.zxid;
        }

        /*
         * Snapshots are taken lazily. It can happen that the child
         * znodes of a parent are created after the parent
         * is serialized. Therefore, while replaying logs during restore, a
         * create might fail because the node was already
         * created.
         *
         * After seeing this failure, we should increment
         * the cversion of the parent znode since the parent was serialized
         * before its children.
         *
         * Note, such failures on DT should be seen only during
         * restore.
         */
        if (header.getType() == OpCode.create &&
                rc.err == Code.NODEEXISTS.intValue()) { // 處理在恢復資料過程中的結點建立操作
            LOG.debug("Adjusting parent cversion for Txn: " + header.getType() +
                    " path:" + rc.path + " err: " + rc.err);
            int lastSlash = rc.path.lastIndexOf('/');
            String parentName = rc.path.substring(0, lastSlash);
            CreateTxn cTxn = (CreateTxn)txn;
            try {
                setCversionPzxid(parentName, cTxn.getParentCVersion(),
                        header.getZxid());
            } catch (KeeperException.NoNodeException e) {
                LOG.error("Failed to set parent cversion for: " +
                      parentName, e);
                rc.err = e.code().intValue();
            }
        } else if (rc.err != Code.OK.intValue()) {
            LOG.debug("Ignoring processTxn failure hdr: " + header.getType() +
                  " : error: " + rc.err);
        }
        return rc;
    }
View Code

  說明:processTxn用於處理事務,即將事務操作應用到DataTree記憶體資料庫中,以恢復成最新的資料。

  2. save函式  

    public void save(DataTree dataTree,
            ConcurrentHashMap<Long, Integer> sessionsWithTimeouts)
        throws IOException {
        // 獲取最後處理的zxid
        long lastZxid = dataTree.lastProcessedZxid;
        // 生成snapshot檔案
        File snapshotFile = new File(snapDir, Util.makeSnapshotName(lastZxid));
        LOG.info("Snapshotting: 0x{} to {}", Long.toHexString(lastZxid),
                snapshotFile);
        // 序列化datatree、sessionsWithTimeouts至snapshot檔案
        snapLog.serialize(dataTree, sessionsWithTimeouts, snapshotFile);
        
    }

  說明:save函式用於將sessions和datatree儲存至snapshot檔案中,其大致步驟如下

  ① 獲取記憶體資料庫中已經處理的最新的zxid,進入②

  ② 根據zxid和快照目錄生成snapshot檔案,進入③

  ③ 將datatree(記憶體資料庫)、sessionsWithTimeouts序列化至快照檔案。

  其他的函式或多或少都是呼叫TxnLog和SnapLog中的相應函式,之前已經進行過分析,這裡不再累贅。

三、總結

  本篇博文分析了FileTxnSnapLog的原始碼,其主要封裝了TxnLog和SnapLog來進行相應的處理,其提供了從snapshot檔案和log檔案中恢復記憶體資料庫的介面,原始碼相對而言較為簡單,也謝謝各位園友的觀看~

相關文章