Zookeeper事務(wù)日志預(yù)分配空間解讀
前言
Zookeeper的通過快照日志和事務(wù)日志將內(nèi)存信息保存下來,記錄下來每次請求的具體信息。
尤其是其事務(wù)日志,每次處理事務(wù)請求時(shí)都需要將其記錄下來。
Zookeeper事務(wù)日志的默認(rèn)存儲方式是磁盤文件,那么Zookeeper的總體性能就受限與磁盤文件的寫入速度。
針對這個(gè)瓶頸,Zookeeper做了什么優(yōu)化操作呢,本文我們就一起來了解下。
1.事務(wù)日志的預(yù)分配
事務(wù)日志的添加,我們需要從FileTxnLog.append()方法看起
public class FileTxnLog implements TxnLog { ? ? volatile BufferedOutputStream logStream = null; ? ? volatile OutputArchive oa; ? ? volatile FileOutputStream fos = null; ? ?? ? ? // 追加事務(wù)日志 ? ? public synchronized boolean append(TxnHeader hdr, Record txn) ? ? ? ? throws IOException ? ? { ? ? ? ? if (hdr == null) { ? ? ? ? ? ? return false; ? ? ? ? } ? ? ? ? ? if (hdr.getZxid() <= lastZxidSeen) { ? ? ? ? ? ? LOG.warn("Current zxid " + hdr.getZxid() ? ? ? ? ? ? ? ? ? ? + " is <= " + lastZxidSeen + " for " ? ? ? ? ? ? ? ? ? ? + hdr.getType()); ? ? ? ? } else { ? ? ? ? ? ? lastZxidSeen = hdr.getZxid(); ? ? ? ? } ? ? ? ? ? // 默認(rèn)logStream為空 ? ? ? ? if (logStream==null) { ? ? ? ? ? ?if(LOG.isInfoEnabled()){ ? ? ? ? ? ? ? ? LOG.info("Creating new log file: " + Util.makeLogName(hdr.getZxid())); ? ? ? ? ? ?} ? ? ? ? ? ? ? // 以下代碼為創(chuàng)建事務(wù)日志文件 ? ? ? ? ? ? // 根據(jù)當(dāng)前事務(wù)ID來創(chuàng)建具體文件名,并寫入文件頭信息 ? ? ? ? ? ?logFileWrite = new File(logDir, Util.makeLogName(hdr.getZxid())); ? ? ? ? ? ?fos = new FileOutputStream(logFileWrite); ? ? ? ? ? ?logStream=new BufferedOutputStream(fos); ? ? ? ? ? ?oa = BinaryOutputArchive.getArchive(logStream); ? ? ? ? ? ?FileHeader fhdr = new FileHeader(TXNLOG_MAGIC,VERSION, dbId); ? ? ? ? ? ?fhdr.serialize(oa, "fileheader"); ? ? ? ? ? ?// Make sure that the magic number is written before padding. ? ? ? ? ? ?logStream.flush(); ? ? ? ? ? ?filePadding.setCurrentSize(fos.getChannel().position()); ? ? ? ? ? ?streamsToFlush.add(fos); ? ? ? ? } ? ? ? ? // 預(yù)分配代碼在這里 ? ? ? ? filePadding.padFile(fos.getChannel()); ? ? ? ? byte[] buf = Util.marshallTxnEntry(hdr, txn); ? ? ? ? if (buf == null || buf.length == 0) { ? ? ? ? ? ? throw new IOException("Faulty serialization for header " + ? ? ? ? ? ? ? ? ? ? "and txn"); ? ? ? ? } ? ? ? ? Checksum crc = makeChecksumAlgorithm(); ? ? ? ? crc.update(buf, 0, buf.length); ? ? ? ? oa.writeLong(crc.getValue(), "txnEntryCRC"); ? ? ? ? Util.writeTxnBytes(oa, buf); ? ? ? ? ? return true; ? ? } }
創(chuàng)建FileTxnLog對象時(shí),其logStream屬性為null,所以當(dāng)?shù)谝淮翁幚硎聞?wù)請求時(shí),會先根據(jù)當(dāng)前事務(wù)ID來創(chuàng)建一個(gè)文件。
1.1 事務(wù)日志預(yù)分配
public class FilePadding { ? ? long padFile(FileChannel fileChannel) throws IOException { ? ? ? ? // 針對新文件而言,newFileSize=64M ? ? ? ? long newFileSize = calculateFileSizeWithPadding(fileChannel.position(), currentSize, preAllocSize); ? ? ? ? if (currentSize != newFileSize) { ? ? ? ? ? ? // 將文件擴(kuò)充到64M,全部用0來填充 ? ? ? ? ? ? fileChannel.write((ByteBuffer) fill.position(0), newFileSize - fill.remaining()); ? ? ? ? ? ? currentSize = newFileSize; ? ? ? ? } ? ? ? ? return currentSize; ? ? } ? ?? ? ? // size計(jì)算 ? ? public static long calculateFileSizeWithPadding(long position, long fileSize, long preAllocSize) { ? ? ? ? // If preAllocSize is positive and we are within 4KB of the known end of the file calculate a new file size ? ? ? ? // 初始時(shí)候position=0,fileSize為0,preAllocSize為系統(tǒng)參數(shù)執(zhí)行,默認(rèn)為64M ? ? ? ? if (preAllocSize > 0 && position + 4096 >= fileSize) { ? ? ? ? ? ? // If we have written more than we have previously preallocated we need to make sure the new ? ? ? ? ? ? // file size is larger than what we already have ? ? ? ? ? ? // Q:這里確實(shí)沒看懂... ? ? ? ? ? ? if (position > fileSize) { ? ? ? ? ? ? ? ? fileSize = position + preAllocSize; ? ? ? ? ? ? ? ? fileSize -= fileSize % preAllocSize; ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? fileSize += preAllocSize; ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? ? return fileSize; ? ? } }
預(yù)分配的過程比較簡單,就是看下當(dāng)前文件的剩余空間是否<4096,如果是,則擴(kuò)容。
Q:
這里有一個(gè)不太明白的問題,position > fileSize的場景是怎樣的呢?
2.創(chuàng)建新的事務(wù)日志文件時(shí)機(jī)
通過上述代碼分析我們知道,當(dāng)logStream=null時(shí),就會創(chuàng)建一個(gè)新的事務(wù)日志文件,那么logStream對象什么時(shí)候?yàn)榭漳兀?/p>
搜索代碼,只看到FileTxnLog.rollLog()方法會主動(dòng)將logStream設(shè)置為null
public class FileTxnLog implements TxnLog { ? ? public synchronized void rollLog() throws IOException { ? ? ? ? if (logStream != null) { ? ? ? ? ? ? this.logStream.flush(); ? ? ? ? ? ? this.logStream = null; ? ? ? ? ? ? oa = null; ? ? ? ? } ? ? } }
那么根據(jù)這個(gè)線索,我們來搜索下rollLog的調(diào)用鏈
SyncRequestProcessor.run() -> ZKDatabase.rollLog() -> FileTxnSnapLog.rollLog() -> FileTxnLog.rollLog()
最終看到是在SyncRequestProcessor.run()方法中發(fā)起調(diào)用的,而且只有這一條調(diào)用鏈,我們來分析下
2.1 SyncRequestProcessor.run()
public class SyncRequestProcessor extends ZooKeeperCriticalThread implements RequestProcessor { ?? ?public void run() { ? ? ? ? try { ? ? ? ? ? ? int logCount = 0; ? ? ? ? ? ? ? setRandRoll(r.nextInt(snapCount/2)); ? ? ? ? ? ? while (true) { ? ? ? ? ? ? ? ? ... ? ? ? ? ? ? ? ? if (si != null) { ? ? ? ? ? ? ? ? ? ? // 追加事務(wù)日志 ? ? ? ? ? ? ? ? ? ? if (zks.getZKDatabase().append(si)) { ? ? ? ? ? ? ? ? ? ? ? ? logCount++; ? ? ? ? ? ? ? ? ? ? ? ? if (logCount > (snapCount / 2 + randRoll)) { ? ? ? ? ? ? ? ? ? ? ? ? ? ? setRandRoll(r.nextInt(snapCount/2)); ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 注意:在這里發(fā)起了rollLog ? ? ? ? ? ? ? ? ? ? ? ? ? ? zks.getZKDatabase().rollLog(); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ... ? ? ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? ? ? } else if (toFlush.isEmpty()) { ? ? ? ? ? ? ? ? ? ? ? ? ... ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? ? ? toFlush.add(si); ? ? ? ? ? ? ? ? ? ? if (toFlush.size() > 1000) { ? ? ? ? ? ? ? ? ? ? ? ? flush(toFlush); ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? } catch (Throwable t) { ? ? ? ? ? ? handleException(this.getName(), t); ? ? ? ? ? ? running = false; ? ? ? ? } ? ? ? ? LOG.info("SyncRequestProcessor exited!"); ? ? } }
需要注意下rollLog()方法執(zhí)行的條件,就是logCount > (snapCount / 2 + randRoll)
snapCount是一個(gè)系統(tǒng)參數(shù),System.getProperty("zookeeper.snapCount"),默認(rèn)值為100000
randRoll是一個(gè)隨機(jī)值
那么該條件觸發(fā)的時(shí)機(jī)為:處理的事務(wù)請求數(shù)至少要大于50000。
這時(shí)就出現(xiàn)了一個(gè)筆者無法理解的情況:
通過對事務(wù)日志的觀察可以看到其都是64M,而至少處理50000次事務(wù)請求后,Zookeeper才會分配一個(gè)新的事務(wù)日志文件,那么這個(gè)snapCount是一個(gè)經(jīng)驗(yàn)值嘛?
如果事務(wù)請求的value信息都很大,那么可能到不了50000次,就會超過64M,理論上應(yīng)該要?jiǎng)?chuàng)建一個(gè)新的文件了,但是貌似并沒有,這個(gè)該怎么處理呢?
如果事務(wù)請求value信息都很小,那么即使到了50000次,也不會超過64M,那么之前預(yù)分配的文件大小就浪費(fèi)了一部分。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
一文教你學(xué)會搭建SpringBoot分布式項(xiàng)目
這篇文章主要為大家詳細(xì)介紹了搭建SpringBoot分布式項(xiàng)目的相關(guān)知識,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-01-01Spring@Autowired與@Resource的區(qū)別有哪些
這篇文章主要為大家詳細(xì)介紹了@Autowired與@Resource的區(qū)別,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-02-02springboot基于IDEA環(huán)境熱加載與熱部署教程
這篇文章主要為大家介紹了springboot在IDEA環(huán)境下的熱加載與熱部署教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-03-03Java使用MulticastSocket實(shí)現(xiàn)群聊應(yīng)用程序
這篇文章主要為大家詳細(xì)介紹了Java使用MulticastSocket實(shí)現(xiàn)群聊應(yīng)用程序,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05