
簡介這是基于Java與Moosefs的分布式文件系統完整項目資料面向高校學生與Java開發人員可用于課程設計、畢業設計及分布式存儲技術實戰學習。壓縮包共200個文件以jar依賴庫、java源碼、class編譯產物、html頁面為主體附帶sql數據庫腳本、jsp動態頁面、doc/ppt說明文檔及Eclipse工程配置整體僅14.52MB目錄結構清晰方便直接導入IDE運行。源碼已注明經過測試校正、可百分百成功運行覆蓋文件上傳、用戶認證、目錄列表、XML配置生成等核心功能能夠幫助讀者理解Moosefs與Java整合的基本流程與工程實現。當前已有273人學習下載適合需要一套可運行的分布式文件系統參考實現并結合文檔快速掌握設計與排錯思路的開發者。1. 基于Java和MooseFS的分布式文件系統解決的不只是存儲問題做課程設計或內部系統驗證時大家經常遇到一個尷尬單機文件系統扛不住并發直接上HDFS又太重開一臺NFS服務器又缺副本冗余。MooseFS的定位恰好落在中間——它在底層把文件切塊分散到多臺chunkserver上對外暴露的卻是一個標準POSIX掛載點。而當你不想讓所有客戶端都去裝內核模塊、掛FUSE的時候用Java把這層能力重新封裝一套Web化的文件管理接口反而是更接地氣的做法。我拆的這套源碼就是走這個路線底層借助MooseFS做數據冗余和橫向擴容Java層負責處理用戶會話、目錄枚舉、文件上傳和元數據XML化。適合兩類人一是做Java Web方向課設、畢設需要一套完整前后端鏈路的同學二是想快速搭一個私有化文件服務做二次開發的工程師。2. MooseFS的存儲原理與Java側選型考量2.1 元數據和數據分離的架構MooseFS最核心的設計是master節點和chunkserver分離。master只保存文件名、目錄結構、文件到chunk的映射關系真正的數據塊分散在各臺chunkserver上。一個文件默認切成64MB的chunk每個chunk可以設置多個副本。這在Java開發者的視角里很接近“目錄樹是索引、數據是對象存儲”的模型只不過MooseFS把它包裝成了普通文件系統。所以從這個源碼的命名空間來看它的設計思路是Java服務先通過掛載點或者API訪問MooseFS拿到文件和目錄信息然后組裝成自定義數據格式返給前端。這樣的好處是前端完全不感知底層是分布式存儲只看到一個個帶路徑的文件對象。安裝完MooseFS后常規做法是先啟master再啟chunkserver# 初始化master的元數據存儲目錄 mkdir -p /var/lib/mfs /usr/local/mfs/sbin/mfsmaster -a # 啟動chunkserver配置文件指向master地址 /usr/local/mfs/sbin/mfschunkserver start # 客戶端掛載 /usr/local/mfs/bin/mfsmount /mnt/mfs -H mfsmaster.example.com-a參數不是后臺運行而是讓master在啟動時自動創建或恢復元數據文件。掛載時-H指定master主機也可以加-P指定端口默認9419。掛載成功后再寫文件才真正進入MooseFS的chunk分配流程。很多同學會在這里犯一個認知錯誤以為MooseFS掛載點和普通磁盤掛載一樣沒有副本概念。實際上mfssetgoal才是控制副本數的命令如果不設置默認goal是1意味著數據只有一份。生產環境至少設2mfs setgoal 2 /mnt/mfs/upload_dir mfs getgoal /mnt/mfs/upload_dir命令作用常用參數mfsgetgoal查看文件或目錄的副本目標-r遞歸mfssetgoal設置副本目標2表示雙副本mfsdirinfo查看目錄的chunk和大小統計無mfsfileinfo查看具體文件落在哪些chunk上無2.2 Java封裝MooseFS的常見路徑Java程序操作MooseFS常見的有三條路。第一條是直接用JNI調用libmfs性能最好但維護成本高這個項目沒有走。第二條是運行時調用mfstools命令行工具通過ProcessBuilder執行mfsdirinfo、mfsfileinfo等命令解析輸出。第三條是把MooseFS掛載為本地目錄Java用標準的java.io或java.nio去訪問。這套源碼里listDir、listPath、uploadfile這些類的能力邊界決定了它是掛在第二條和第三條之間文件操作走本地掛載路徑統計和元數據巡檢走命令解析。這么做的好處是代碼穩定不依賴MooseFS內部協議升級MooseFS版本時Java側不用跟著改。用Java執行MooseFS命令的標準姿勢是封裝一層CommandRunnerpublic class MfsCommandRunner { private static final String MFS_TOOLS /usr/local/mfs/bin/; public ListString exec(String tool, String... args) { ListString command new ArrayList(); command.add(MFS_TOOLS tool); command.addAll(Arrays.asList(args)); try { Process process new ProcessBuilder(command) .redirectErrorStream(true) .start(); try (BufferedReader reader new BufferedReader( new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { return reader.lines().collect(Collectors.toList()); } } catch (IOException e) { throw new RuntimeException(調用MooseFS命令失敗: tool, e); } } }redirectErrorStream(true)很關鍵把stderr合并到stdout否則執行mfsdirinfo時如果有告警輸出你會漏掉真正的結果數據。第二個關鍵點是字符集必須顯式指定UTF-8MooseFS命令輸出的文件名如果包含中文直接用平臺默認編碼解析會產生亂碼后面ParseEncoding類要處理的本質上就是這個問題。2.3 這塊源碼為什么值得過一遍項目里出現的類名基本反映了完整請求鏈AuthRequest處理身份、User管理會話、listDir和listPath處理目錄讀取、uploadfile處理寫入、CreateFileInfoXml和XmlGeneratorDemo負責結果序列化。這套分層不是花架子它對應的是一個真實網盤系統的最小閉環。你把類名翻譯成職責就能得到一份模塊劃分圖。真正值得學習的不是某一個類的代碼而是“把系統調用包裝成Web接口”的全局思路。MooseFS有自己的CLI和API但瀏覽器端不可能直接執行中間這層Java服務要解決的正是協議轉換、鑒權、參數校驗和錯誤兜底。這套代碼的架構取向很務實不重造分布式存儲的輪子只做存儲能力的產品化封裝。3. 核心類職責拆解與目錄枚舉實現3.1 從class名反推模塊邊界拿到資源包后第一件事不是去看代碼內容而是先看class清單建立整體認知。這堆class可以歸成四組。分組類名職責推斷實體層User、NetDiskFile用戶對象與網盤文件對象通信層AuthRequest、Response請求認證與統一響應目錄服務listDir、listPath目錄掃描與路徑轉換文件服務uploadfile、CreateFileInfoXml上傳與元數據生成NetDiskFile是最核心的實體它把所有來自MooseFS的文件信息統一成Java對象。這個類一般會包含文件名、絕對路徑、是否是目錄、文件大小、修改時間、副本數等字段。listDir和listPath的區別值得細講前者是給定一個目錄ID或路徑枚舉該目錄下的直接子項后者處理的是“多級路徑定位”比如前端傳過來/work/docs/2025需要把它拆解成逐級目錄驗證。ParseEncoding的存在說明原作者在文件名編碼上吃過虧。MooseFS底層以字節存儲文件名不同客戶端寫入時的編碼可能不一致Java讀取時如果直接new String(bytes)會出現亂碼。正確做法是先用CharsetDecoder配合REPLACE策略解碼對無法映射的字節打上標記而不是直接拋出異常。3.2 listDir的默認實現邏輯listDir在MooseFS場景下的實現通常是掃描掛載目錄核心代碼如下public ListNetDiskFile listDir(String path) { File dir new File(MOUNT_ROOT normalize(path)); File[] children dir.listFiles(); if (children null) { return Collections.emptyList(); } ListNetDiskFile result new ArrayList(children.length); for (File child : children) { NetDiskFile netDiskFile new NetDiskFile(); netDiskFile.setName(child.getName()); netDiskFile.setPath(relativePath(child.getAbsolutePath())); netDiskFile.setDirectory(child.isDirectory()); netDiskFile.setSize(child.isDirectory() ? 0 : child.length()); netDiskFile.setLastModified(new Date(child.lastModified())); result.add(netDiskFile); } // 目錄優先然后按名稱排序 result.sort(Comparator.comparing(NetDiskFile::isDirectory) .reversed() .thenComparing(NetDiskFile::getName)); return result; }normalize(path)方法要做三件事把反斜杠統一替換成正斜杠、去掉路徑末尾多余的/、防止..跳級穿越根目錄。這段代碼容易踩坑的地方是new File(MOUNT_ROOT path)如果path來自前端請求參數必須做路徑穿越校驗否則用戶傳../../etc/passwd就能讀到掛載點之外的內容。child.isDirectory()這個調用在MooseFS掛載點上實際會觸發一次元數據stat操作如果目錄層級深、文件數量大這個方法的性能會明顯變差。優化做法是用Files.newDirectoryStream配合Files.readAttributes批量取屬性減少系統調用次數。3.3 listPath處理多級路徑導航listPath解決的是面包屑導航問題。前端需要知道用戶在哪個層級每一層有哪些兄弟目錄。常規實現是遞歸拆分路徑public MapString, Object listPath(String path) { MapString, Object result new HashMap(); // 規范化后的路徑去掉開頭的斜杠 String normalized normalize(path); String[] segments normalized.split(/); ListNetDiskFile breadcrumb new ArrayList(); StringBuilder current new StringBuilder(); for (String segment : segments) { if (segment.isEmpty()) { continue; } current.append(/).append(segment); NetDiskFile dirInfo new NetDiskFile(); dirInfo.setName(segment); dirInfo.setPath(current.toString()); dirInfo.setDirectory(true); breadcrumb.add(dirInfo); } result.put(currentPath, normalized); result.put(parents, breadcrumb); result.put(children, listDir(normalized)); return result; }segments normalized.split(/)這里注意split函數對連續分隔符的處理路徑如果是/a//b中間會產出一個空字符串所以必須加isEmpty()判斷。listDir是復用上一節的方法這體現了“底層方法只做一件事上層方法做組合”的分層思想。這塊代碼對面試答“分布式文件系統如何做目錄瀏覽”這個問題很有用你可以直接說目錄服務接收路徑參數通過本地掛載點讀取目錄將文件和目錄區分后序列化為JSON元數據來源是MooseFS的master節點。回答關鍵點在于講清楚你的Java服務并不直接訪問chunkserver而是通過掛載點間接訪問這樣的解耦讓上層更穩定。4. AuthRequest與User協同的登錄鑒權流程4.1 會話建立與令牌生成User類在這個項目中承擔的是登錄態載體AuthRequest負責把HTTP請求里的憑證解析成User對象。常見的實現方式是基于token的用戶登錄成功后服務端生成一個隨機串把用戶信息寫入session或內存Map同時把token返回給前端。后續請求都在Header里帶Authorization: Bearer token。public class AuthRequest { private MapString, String tokenStore new ConcurrentHashMap(); private SecureRandom secureRandom new SecureRandom(); public User login(String username, String password) { User user userService.validate(username, password); if (user null) { throw new AuthException(用戶名或密碼錯誤); } // 生成128位隨機令牌 byte[] bytes new byte[16]; secureRandom.nextBytes(bytes); String token Base64.getUrlEncoder().withoutPadding() .encodeToString(bytes); tokenStore.put(token, user.getUsername()); user.setToken(token); return user; } public User validate(String token) { String username tokenStore.get(token); if (username null) { throw new AuthException(token無效或已過期); } return userService.findByUsername(username); } }SecureRandom比Math.random()更適合做token生成因為后者可預測。Base64.getUrlEncoder().withoutPadding()生成的是URL安全的token不會包含和/字符適合放在HTTP頭里。token存儲用ConcurrentHashMap在單機部署下夠了但如果你要多實例部署這段代碼必須換成Redis否則用戶在一臺機器登錄請求被負載均衡轉發到另一臺就會掉線。4.2 密碼存儲不能只做MD5課程設計里常見的錯誤是MD5(password)直接存庫這在面試里一定會被追問。正確做法是加鹽哈希用PBKDF2或BCryptpublic String hashPassword(String password, String salt) { try { PBEKeySpec spec new PBEKeySpec(password.toCharArray(), salt.getBytes(StandardCharsets.UTF_8), 10000, 256); SecretKeyFactory factory SecretKeyFactory.getInstance(PBKDF2WithHmacSHA256); byte[] hash factory.generateSecret(spec).getEncoded(); return Base64.getEncoder().encodeToString(hash); } catch (GeneralSecurityException e) { throw new RuntimeException(密碼哈希失敗, e); } }迭代次數10000是底線現在推薦120000以上但考慮到課設項目的性能10000到50000之間可以接受。salt每個用戶獨立長度16字節以上存庫時把salt和hash拼在一起存形如salt$hash校驗時拆開重新計算。這個點值得多說一句的原因在于User類如果設計了password字段那就要配套設計密碼策略。原項目里User如果只存用戶名和明文密碼建議改造成上面的方案再擴展。面試八股里經常考“分布式會話如何共享”你可以從tokenStore出發引申到Redis集中式存儲的方案。4.3 攔截器的路由保護鑒權不能只在登錄接口做需要對受保護的路徑統一攔截。Java Web項目里最常見的做法是實現一個HandlerInterceptor或Filterpublic class AuthInterceptor implements HandlerInterceptor { private AuthRequest authRequest; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登錄接口 if (request.getRequestURI().endsWith(/login)) { return true; } String authHeader request.getHeader(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { response.setStatus(HttpStatus.SC_UNAUTHORIZED); return false; } String token authHeader.substring(7); try { User user authRequest.validate(token); request.setAttribute(currentUser, user); return true; } catch (AuthException e) { response.setStatus(HttpStatus.SC_UNAUTHORIZED); return false; } } }substring(7)是把Bearer前綴去掉這里要注意Bearer和token之間是有空格的。request.setAttribute(currentUser, user)把用戶對象塞進request作用域后面的Controller就能直接取到當前操作者身份做權限判定。這個攔截器配置在Spring MVC里通常是registry.addInterceptor(new AuthInterceptor()).addPathPatterns(/api/**).excludePathPatterns(/api/login)。鑒權不健壯的表現是只在前端隱藏按鈕、后端接口不校驗。你可以在所有寫入接口里以NetDiskFile為粒度做ACL判斷例如判斷文件owner和當前用戶是否一致這個邏輯放在攔截器之后的Controller或Service層。5. XmlGeneratorDemo與ParseEncoding的元數據交換5.1 XML格式的文件信息協議XmlGeneratorDemo的作用是把Java對象序列化成XMLCreateFileInfoXml則是專門構造文件信息節點。這和現代項目直接用JSON不太一樣但XML在Java世界里仍然有大量遺留系統在使用而且MooseFS的.meta文件和部分管理命令的輸出天然接近XML風格。閱讀這套代碼的重點是理解序列化的對稱性。文件信息XML的標準結構通常是?xml version1.0 encodingUTF-8? files file name課程設計報告.docx/name path/work/docs/課程設計報告.docx/path typefile/type size2048000/size modified2025-03-15 10:24:33/modified goal2/goal /file file nameimages/name path/work/docs/images/path typedir/type size0/size modified2025-03-14 09:01:12/modified goal2/goal /file /filesgoal字段是MooseFS特有的表示副本目標數這個字段是值錢的。前端可以用它來顯示文件的可靠程度運維也可以用它在管理頁面上快速找出goal為1的高風險文件。5.2 Java XML序列化的常見實現用DOM方式生成XML在數據量小的時候簡單直觀在這個項目里直接拼字符串反而更實用public String toXml(ListNetDiskFile files) { StringBuilder sb new StringBuilder(); sb.append(?xml version\1.0\ encoding\UTF-8\?); sb.append(files); for (NetDiskFile file : files) { sb.append(file); sb.append(name).append(escapeXml(file.getName())).append(/name); sb.append(path).append(escapeXml(file.getPath())).append(/path); sb.append(type).append(file.isDirectory() ? dir : file).append(/type); sb.append(size).append(file.getSize()).append(/size); sb.append(modified).append(formatDate(file.getLastModified())).append(/modified); sb.append(/file); } sb.append(/files); return sb.toString(); } private String escapeXml(String text) { if (text null) { return ; } return text.replace(, amp;) .replace(, lt;) .replace(, gt;) .replace(\, quot;) .replace(, apos;); }escapeXml是必須寫的文件名里如果包含或不轉義生成的XML就是非法的前端解析會直接報錯。替換順序上必須先處理否則lt;會被二次轉義成amp;lt;。formatDate方法建議用SimpleDateFormat(yyyy-MM-dd HH:mm:ss)但注意SimpleDateFormat線程不安全如果這個方法被多線程調用要么每次new要么用ThreadLocal包一層。5.3 ParseEncoding解決中文文件名亂碼MooseFS掛載點通過FUSE向Java暴露文件名時文件名是以字節數組形式存儲的。如果之前有客戶端用GBK編碼寫入之后用UTF-8讀取中文目錄就會顯示成亂碼。ParseEncoding的思路是檢測字節序列并做編碼轉換。常見做法是先用CharsetDetector嘗試探測探測失敗時做啟發式判斷public String parse(byte[] bytes) { String utf8 new String(bytes, StandardCharsets.UTF_8); // 如果UTF-8解碼沒有替換字符說明是合法UTF-8 if (!utf8.contains(\uFFFD)) { return utf8; } // 否則嘗試GBK try { return new String(bytes, Charset.forName(GBK)); } catch (UnsupportedCharsetException e) { return new String(bytes, StandardCharsets.ISO_8859_1); } }\uFFFD是Unicode的替換字符UTF-8解碼遇到非法字節序列時會插入它。但這個方法有誤判情況如果文件名本身是GBK編碼的中文按UTF-8解碼很可能不產生替換符而是解出錯誤漢字。這時候更可靠的策略是維護一份“已知文件名緩存”優先用數據庫里存過的規范名稱去匹配。實際項目中更多遇到的情況是同一目錄下混有UTF-8和GBK兩種編碼的文件名這就需要在寫入NetDiskFile對象時將原始字節序列也暫存等前端以參數方式明確charset后再轉換。如果你重寫這個模塊建議把編碼探測放在文件讀取入口做一次而不是在展示階段反復做。6. uploadfile落盤策略與MooseFS副本聯動6.1 分片上傳與臨時文件處理uploadfile類承載的是文件上傳能力。直接multipart解析后寫MooseFS掛載點不算難但要處理大文件。常見做法是先落本地臨時目錄再以流方式寫入MooseFS掛載點這樣任意一步失敗都不會在掛載點上留下半截文件。public String upload(MultipartFile multipartFile, String targetPath) throws IOException { String tempFile System.getProperty(java.io.tmpdir) /mfs_ UUID.randomUUID() .tmp; File localFile new File(tempFile); multipartFile.transferTo(localFile); File target new File(MOUNT_ROOT normalize(targetPath) / multipartFile.getOriginalFilename()); try (FileInputStream input new FileInputStream(localFile); FileOutputStream output new FileOutputStream(target)) { byte[] buffer new byte[1 20]; int len; while ((len input.read(buffer)) ! -1) { output.write(buffer, 0, len); } } finally { localFile.delete(); } // 設置MooseFS副本數為2 mfsCommandRunner.exec(mfssetgoal, 2, target.getAbsolutePath()); return target.getAbsolutePath(); }這里localFile.delete()放finally保證臨時文件一定被清理但更健壯的做法是捕獲delete失敗場景做日志告警。分片大小1MB是通用選擇如果網絡棧和文件系統都支持可以提到4MB減少讀寫次數。寫入完成后調mfssetgoal 2是通知MooseFS master對這塊新文件觸發復制流程復制是異步的如果有監控需求可以輪詢mfsfileinfo看副本是否已經達到目標數。6.2 文件鎖與并發寫入沖突MooseFS的POSIX鎖對多客戶端寫入同一個文件有保護但Java進程內多線程寫同一路徑還是可能出現互相覆蓋。常見的處理方式是用目標路徑作為鎖粒度private final ConcurrentHashMapString, ReentrantLock lockMap new ConcurrentHashMap(); public void uploadWithLock(String path, InputStream data) { ReentrantLock lock lockMap.computeIfAbsent(path, k - new ReentrantLock()); lock.lock(); try { // 執行寫入 } finally { lock.unlock(); lockMap.remove(path); } }lockMap.remove(path)在解鎖后執行避免鎖對象無限堆積。但這里有個并發縫隙兩個線程同時computeIfAbsent得到不同鎖對象解決方法是使用ConcurrentHashMap.compute在單個原子操作內完成獲取和加鎖或者直接不刪鎖對象以少量內存換并發安全。6.3 上傳接口的限流與目錄配額上傳接口如果沒有限制幾個大文件就能把chunkserver的磁盤塞滿。MooseFS本身支持配額quota配置通過mfsmaster.cfg的QUOTA相關配置或用mfs quota參數生效。Java側也要做校驗上傳前檢查目標目錄已用容量與文件大小之和是否超過約定值。mfs getquota /mnt/mfs/work/docs mfs setquota -o 102400000 -s 204800000 /mnt/mfs/work/docs-o是soft quota-s是hard quota單位字節。hard quota是硬上限超過后寫入直接報錯soft quota在超過后進入grace period可以在這段時間內清理文件。Setquota需要master上開啟quota功能否則命令會靜默失敗。6.4 上傳成功后的元數據刷新上傳文件后前端如果立刻刷新目錄列表走的是listDir此時MooseFS掛載點看到的是最新數據Java層不用做緩存清理。容易出錯的是把文件信息放進了進程內緩存而MooseFS是分布式的別的客戶端可能已經改了文件列表你的緩存還在展示舊版本。如果項目里加了緩存建議用lastModified時間戳做失效判斷。上一步上傳寫入的文件mtime是確定的目錄列表請求時對比當前目錄的mtime是否比緩存時間戳新新則重新掃描MooseFS。7. 調試MooseFS鏈路的一套可復用方法拿到這套源碼想讓它在自己機器上跑通可以按“單機模擬→日志追蹤→接口驗證”三步走不涉及MooseFS集群也能把uploadfile和listDir這條主鏈路完整調通。7.1 單機部署MooseFS的最小配置在單機上裝一個MooseFS master、一個chunkserver、一個掛載點即可。用Docker方式更快docker network create mfs-net docker run -d --name mfs-master --network mfs-net \ -v /data/mfs-master:/var/lib/mfs -p 9419:9419 \ moosefs/master docker run -d --name mfs-chunk --network mfs-net \ -v /data/mfs-chunk:/var/lib/mfs -e MASTER_HOSTmfs-master \ moosefs/chunkserver mkdir -p /mnt/mfs sudo mfsmount /mnt/mfs -H 127.0.0.1掛載成功后先寫入幾個中文名文件測試ParseEncoding是否正常工作。如果在容器里跑掛載點需要使用--privileged或者--cap-add SYS_ADMIN并加--device /dev/fuse。7.2 驗證上傳接口返回的路徑正確性啟動Java服務后用curl模擬前端POST請求觀察響應中的XML結構是否完整curl -X POST -H Authorization: Bearer ${TOKEN} \ -F file/tmp/test中文.txt \ http://localhost:8080/api/upload重點檢查返回的name字段是否不亂碼、goal是否為2。如果name亂碼說明ParseEncoding沒生效檢查掛載點所在系統的locale。如果goal是1檢查mfssetgoal的路徑是否正確。驗證文件真實落入MooseFS分布層mfsfileinfo /mnt/mfs/upload/test中文.txt輸出會顯示chunk編號和每個副本所在chunkserver的IP。這里可以看到文件其實被切成了chunk而不是原樣存儲這是面試里講分布式存儲設計的最佳佐證。Chunkserver上的數據塊是44字節頭部加上數據體直接用cat是讀不出原文件的只有通過MooseFS協議才能還原這個特性也是面試官喜歡追問的點。7.3 常見異常現象與排查順序現象排查方向uploadfile寫文件卡死檢查掛載點是否斷連執行mfsmount -t或用df -h /mnt/mfs確認listDir返回空但掛載點有文件檢查Java進程的掛載權限是否以root身份掛載、應用以普通用戶運行中文文件名一半亂碼ParseEncoding的檢測順序導致誤判手動指定charset重試mfssetgoal報no such file確認路徑是MooseFS掛載點內的絕對路徑而不是Java服務所在機器的本地路徑上傳大文件內存溢出multipart解析時Spring默認有大小限制檢查max-file-size配置其中“掛載點斷連”最容易迷惑人。MooseFS master重啟后客戶端掛載點不會自動恢復Java進程里所有文件操作都會返回IO異常。常規做法是在Java服務里加一個定時心跳任務定期向目標目錄寫入探針文件然后刪除失敗就觸發重新掛載。7.4 并發上傳性能驗證用wrk或jmeter壓一下上傳接口關注兩個指標chunkserver的磁盤寫入帶寬和master的元數據操作速率。Java側Service層如果用synchronized包了寫入路徑并發會被鎖到一個線程上整個上傳能力就廢了。去掉不必要鎖之后再觀察100并發下有鎖和無鎖的吞吐差距。最終把接口吞吐壓到3000QPS左右時master節點的日志會輸出chunkserver的連接數。超過這個量說明單master架構到了瓶頸這時候可以橫向加chunkserver但master還是單點。MooseFS的master主從切換需要額外配置Metalogger和Carp有興趣可以再深入。看著MooseFS上被切成chunk、分布在不同節點上的文件你就能直觀理解為什么分布式文件系統敢說自己不怕單機磁盤損壞——因為同一個chunk的副本根本不在同一臺物理機上。本文還有配套的精品資源點擊獲取