分布式檔案存儲系統FastDFS
一、分布式檔案存儲
1.分布式檔案存儲的由來
在我們的項目中有很多需要存儲的内容出現,比如圖檔,視訊,檔案等等,在早期的時候使用者量不大,産生的檔案也不是很多,這時我們可以把檔案和服務程式放在一個伺服器中。
後面随着檔案越來越多,伺服器的資源會被檔案資源大量占據,進而影響到伺服器的穩定,這時我們可以單獨的把檔案伺服器拆出來。
拆解出來後,檔案服務的使用不會影響到我們的系統服務的穩定,但是當使用者量越來越大,存儲的檔案就會越來越多,這時如果還是單台的檔案服務,比如100T的檔案,這時是存儲不下去的,這時就産生了我們将的分布式檔案存儲,
也就是我們解決如何将這100T的檔案分散的存儲到各個節點上,然後當我們需要讀取檔案的時候又能非常快的幫我們把檔案找到。這個就是分布式檔案系統幫我們解決的問題了。
2.常見的分布式存儲架構
接下來我們看看在國内常用的分布式存儲的架構選擇有哪些
分布式架構 | 說明 |
FastDFS | 我們介紹的主角,國産 |
HDFS | Hadoop元件中分布式存儲架構 |
MinIO | MinIO是在Apache下的産品,最适合存儲非結構化的資料,<br />比如照片,視訊,日志檔案,備份和容器等。 |
阿裡雲對象存儲 | 當然我們還可以花費一點費用來使用其他廠商提供的對象存儲服務 |
好了就介紹這麼幾個,其他的我們也用不到了。
二、FastDFS介紹
FastDFS是餘慶國人開發的一個開源的輕量級分布式檔案系統,它對檔案進行管理,功能包括:檔案存儲、檔案同步、檔案通路(檔案上傳、檔案下載下傳)等,解決了大容量存儲和負載均衡的問題。特别适合以檔案為載體的線上服務,如相冊網站、視訊網站等等。
FastDFS為網際網路量身定制,充分考慮了備援備份、負載均衡、線性擴容等機制,并注重高可用、高性能等名額,使用FastDFS很容易搭建一套高性能的檔案伺服器叢集提供檔案上傳、下載下傳等服務。
FastDFS的特點:
- FastDFS是一個輕量級的開源分布式檔案系統
- FastDFS主要解決了大容量的檔案存儲和高并發通路的問題,檔案存取時實作了負載均衡
- FastDFS實作了軟體方式的RAID,可以使用廉價的IDE硬碟進行存儲
- 支援存儲伺服器線上擴容
- 支援相同内容的檔案隻儲存一份,節約磁盤空間
- FastDFS隻能通過Client API通路,不支援POSIX通路方式
- FastDFS特别适合大中型網站使用,用來存儲資源檔案(如:圖檔、文檔、音頻、視訊等等)
架構圖:
相關術語講解:
名詞 | 描述 |
Tracker Server | 跟蹤伺服器,主要做排程工作,在通路上起負載均衡的作用。<br />記錄storage server的狀态,是連接配接Client和Storage server的樞紐 |
Storage Server | 存儲伺服器,檔案和meta data都儲存到存儲伺服器上 |
group | 組,也可稱為卷。同組内伺服器上的檔案是完全相同的 |
檔案辨別 | 包括兩部分:組名和檔案名(包含路徑) |
meta-data | 檔案相關屬性,鍵值對(Key Value Pair)方式,如:width=1024,heigth=768 |
架構解讀:
- 隻有兩個角色,tracker server和storage server,不需要存儲檔案索引資訊。
- 所有伺服器都是對等的,不存在Master-Slave關系。
- 存儲伺服器采用分組方式,同組記憶體儲伺服器上的檔案完全相同(RAID 1)。
- 不同組的storage server之間不會互相通信。
- 由storage server主動向tracker server報告狀态資訊,tracker server之間不會互相通信。
三、FastDFS安裝
FastDFS的安裝我們還是通過Docker來安裝實作吧,直接在Linux上還裝還是比較繁瑣的,但就學習而言Docker安裝還是非常高效的。Docker環境請自行安裝哦,不清楚的可以看看我的Docker專題的内容。https://blog.csdn.net/qq_38526573/category_9619681.html
1.拉取鏡像檔案
首先我們可以通過
docker search fastdfs
來查詢下有哪些鏡像檔案。
我們看到搜尋到的鏡像還是蠻多的,這裡我們使用
delron/fastdfs
你也可以嘗試使用其他的鏡像來安裝,你也可以制作自己的鏡像來給别人使用哦,隻是不同的鏡像在使用的時候配置會有一些不一樣,有些鏡像沒有提供Nginx的相關配置,使用的時候會繁瑣一點。接下來通過
docker pull delron/fastdfs
指令把鏡像拉取下來。
2.建構Tracker服務
首先我們需要通過Docker指令來建立Tracker服務。指令為
docker run -d --name tracker --network=host -v /mydata/fastdfs/tracker:/var/fdfs delron/fastdfs tracker
tracker服務預設的端口為22122,-v 實作了容器和本地目錄的挂載操作。
3.建構Storage服務
接下來建立Storage服務,具體的執行指令如下
docker run -d --name storage --network=host -e TRACKER_SERVER=192.168.56.100:22122 -v /mydata/fastdfs/storage:/var/fdfs -e GROUP_NAME=group1 delron/fastdfs storage
在執行上面指令的時候要注意對應的修改下,其中TRACKER_SERVER中的ip要修改為你的Tracker服務所在的服務IP位址。
預設情況下在Storage服務中是幫我們安裝了Nginx服務的,相關的端口為
服務 | 預設端口 |
tracker | 22122 |
storage | 23000 |
Nginx | 8888 |
當然如果你發現這些相關的端口被占用了,或者想要對應的修改端口資訊也可以的。要修改你可以先進入容器中檢視下相關的配置檔案資訊。
然後檢視storage.conf檔案
這個是storage監聽的Nginx的端口8888,如果要修改那麼我們還需要修改Nginx中的服務配置,這塊的配置在
/usr/local/nginx/conf
目錄下
檢視下檔案
是以要修改端口号的話,這兩個位置都得修改了。當然本文我們就使用預設的端口号來使用了。
4.測試圖檔上傳
好了,安裝我們已經完成了,那麼到底是否可以使用呢?我們來測試下。首先在虛拟機的/mydata/fastdfs/storage下儲存一張圖檔。
然後我們再進入到storage容器中。并且進入到
/var/fdfs
目錄下,可以看到我們挂載的檔案了
然後執行如下指令即可完成圖檔的上傳操作
/usr/bin/fdfs_upload_file /etc/fdfs/client.conf 1.jpg
通過上面的提示我們看到檔案上傳成功了,而且傳回了檔案在storage中存儲的資訊。這時我們就可以通過這個資訊來拼接通路的位址在浏覽器中通路了:http://192.168.56.100:8888/group1/M00/00/00/wKg4ZGHcKLSAXibaAAezMuUrlS8235.jpg
好了到這兒FastDFS的服務安裝成功了。
四、用戶端操作
1.Fastdfs-java-client
首先我們來看下如何實作FastDFS中提供的JavaAPI來直接實作對應的檔案上傳和下載下傳操作。
1.1 檔案上傳
先來看下檔案上傳的流程
上傳流程的文字梳理為:
- 用戶端通路Tracker
- Tracker 傳回Storage的ip和端口
- 用戶端直接通路Storage,把檔案内容和中繼資料發送過去。
- Storage傳回檔案存儲id。包含了組名和檔案名
首先建立一個普通的maven項目,然後引入對應的依賴
<dependencies>
<dependency>
<groupId>cn.bestwu</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.27</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
</dependencies>
然後編寫FastDFS的配置檔案,内容如下:注意ip修改為你自己對應的ip即可
connect_timeout = 10
network_timeout = 30
charset = UTF-8
http.tracker_http_port = 8080
tracker_server = 192.168.56.100:22122
然後導入對應的工具類,在工具類中完成了StorageClient的執行個體化,并提供了相關的上傳和下載下傳的方法。
package com.bobo.fastdfs.config;
import org.apache.commons.lang3.StringUtils;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.*;
import java.io.*;
public class FastDFSClient {
private static final String CONF_FILENAME = Thread.currentThread().getContextClassLoader().getResource("").getPath() + "fdfs_client.conf";
private static StorageClient storageClient = null;
/**
* 隻加載一次.
*/
static {
try {
ClientGlobal.init(CONF_FILENAME);
TrackerClient trackerClient = new TrackerClient(ClientGlobal.g_tracker_group);
TrackerServer trackerServer = trackerClient.getConnection();
StorageServer storageServer = trackerClient.getStoreStorage(trackerServer);
storageClient = new StorageClient(trackerServer, storageServer);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
*
* @param inputStream
* 上傳的檔案輸入流
* @param fileName
* 上傳的檔案原始名
* @return
*/
public static String[] uploadFile(InputStream inputStream, String fileName) {
try {
// 檔案的中繼資料
NameValuePair[] meta_list = new NameValuePair[2];
// 第一組中繼資料,檔案的原始名稱
meta_list[0] = new NameValuePair("file name", fileName);
// 第二組中繼資料
meta_list[1] = new NameValuePair("file length", inputStream.available()+"");
// 準備位元組數組
byte[] file_buff = null;
if (inputStream != null) {
// 檢視檔案的長度
int len = inputStream.available();
// 建立對應長度的位元組數組
file_buff = new byte[len];
// 将輸入流中的位元組内容,讀到位元組數組中。
inputStream.read(file_buff);
}
// 上傳檔案。參數含義:要上傳的檔案的内容(使用位元組數組傳遞),上傳的檔案的類型(擴充名),中繼資料
String[] fileids = storageClient.upload_file(file_buff, getFileExt(fileName), meta_list);
return fileids;
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
/**
*
* @param file
* 檔案
* @param fileName
* 檔案名
* @return 傳回Null則為失敗
*/
public static String[] uploadFile(File file, String fileName) {
FileInputStream fis = null;
try {
NameValuePair[] meta_list = null; // new NameValuePair[0];
fis = new FileInputStream(file);
byte[] file_buff = null;
if (fis != null) {
int len = fis.available();
file_buff = new byte[len];
fis.read(file_buff);
}
String[] fileids = storageClient.upload_file(file_buff, getFileExt(fileName), meta_list);
return fileids;
} catch (Exception ex) {
return null;
}finally{
if (fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 根據組名和遠端檔案名來删除一個檔案
*
* @param groupName
* 例如 "group1" 如果不指定該值,預設為group1
* @param remoteFileName
* 例如"M00/00/00/wKgxgk5HbLvfP86RAAAAChd9X1Y736.jpg"
* @return 0為成功,非0為失敗,具體為錯誤代碼
*/
public static int deleteFile(String groupName, String remoteFileName) {
try {
int result = storageClient.delete_file(groupName == null ? "group1" : groupName, remoteFileName);
return result;
} catch (Exception ex) {
return 0;
}
}
/**
* 修改一個已經存在的檔案
*
* @param oldGroupName
* 舊的組名
* @param oldFileName
* 舊的檔案名
* @param file
* 新檔案
* @param fileName
* 新檔案名
* @return 傳回空則為失敗
*/
public static String[] modifyFile(String oldGroupName, String oldFileName, File file, String fileName) {
String[] fileids = null;
try {
// 先上傳
fileids = uploadFile(file, fileName);
if (fileids == null) {
return null;
}
// 再删除
int delResult = deleteFile(oldGroupName, oldFileName);
if (delResult != 0) {
return null;
}
} catch (Exception ex) {
return null;
}
return fileids;
}
/**
* 檔案下載下傳
*
* @param groupName 卷名
* @param remoteFileName 檔案名
* @return 傳回一個流
*/
public static InputStream downloadFile(String groupName, String remoteFileName) {
try {
byte[] bytes = storageClient.download_file(groupName, remoteFileName);
InputStream inputStream = new ByteArrayInputStream(bytes);
return inputStream;
} catch (Exception ex) {
return null;
}
}
public static NameValuePair[] getMetaDate(String groupName, String remoteFileName){
try{
NameValuePair[] nvp = storageClient.get_metadata(groupName, remoteFileName);
return nvp;
}catch(Exception ex){
ex.printStackTrace();
return null;
}
}
/**
* 擷取檔案字尾名(不帶點).
*
* @return 如:"jpg" or "".
*/
private static String getFileExt(String fileName) {
if (StringUtils.isBlank(fileName) || !fileName.contains(".")) {
return "";
} else {
return fileName.substring(fileName.lastIndexOf(".") + 1); // 不帶最後的點
}
}
}
然後我們就可以來測試上傳的操作了。
public static void main(String[] args) {
try {
File file = new File("D:/2.jpg");
InputStream is = new FileInputStream(file);
String fileName = UUID.randomUUID().toString()+".jpg";
String[] result = FastDFSClient.uploadFile(is, fileName);
System.out.println(Arrays.toString(result));
} catch (Exception e) {
e.printStackTrace();
}
}
通路即可:http://192.168.56.100:8888/group1/M00/00/00/wKg4ZGHcUE6AZA2UAAW8dIX5p50374.jpg
傳回後的字元串的結構說明
1.2 檔案下載下傳
檔案下載下傳的流程,如下
檔案下載下傳的流程為:
- client詢問tracker需要下載下傳的檔案的storage,參數為檔案的辨別(group加檔案名)。
- tracker根據用戶端的參數傳回一台可用的storage。
- client根據傳回的storage直接完成對應的檔案的下載下傳。
有了上面的基礎,檔案下載下傳就非常簡單了,我們隻需要根據前面上傳的檔案的group和檔案的存儲路徑就可以通過StorageClient中提供的downloadFile方法把對應的檔案下載下傳下來了,具體的代碼如下
/**
* 檔案下載下傳
*/
public static void downloadFile(){
try {
InputStream is = FastDFSClient
.downloadFile("group1", "M00/00/00/wKg4ZGHcUE6AZA2UAAW8dIX5p50374.jpg");
OutputStream os = new FileOutputStream(new File("D:/12.jpg"));
int index = 0 ;
while((index = is.read())!=-1){
os.write(index);
}
os.flush();
os.close();
is.close();
} catch (Exception e) {
e.printStackTrace();
}
}
注意:StorageClient是線程不安全的。那麼我們的解決方案
- 對檔案的操作的每個方法我們做同步處理
- 每次操作檔案的時候我們都擷取一個新的StorageClient對象
第一種方式效率肯定是最低的,第二種方式每次都要建立新的連接配接效率同樣的會受到影響,這時最好的方式其實是把StorageClient交給我們自定義的連接配接池來管理
2.SpringBoot整合
我們在實際工作中基本都是和SpringBoot整合在一起來使用的,那麼我們就來看看FastDFS是如何在SpringBoot項目中來使用的。首先建立一個普通的SpringBoot項目,然後導入fastdfs-spring-boot-starter這個依賴。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.luhuiguo</groupId>
<artifactId>fastdfs-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>
</dependencies>
既然是一個starter,那麼必然會在spring.factories檔案中提供對應的自動配置類。
可以看到給我們提供的配置類為FdfsAutoConfiguration進入後可以看到幫我們注入了很多的核心對象。
然後可以看到系統提供的配置資訊,字首為
fdfs
然後我們就可以在application.properties中配置FastDFS的配置資訊了。
配置完成後我們就可以測試檔案的上傳下載下傳操作了
@SpringBootTest
class FastDfsSpringBootApplicationTests {
@Autowired
public FastFileStorageClient storageClient;
@Test
void contextLoads() throws Exception{
File file = new File("d:\\2.jpg");
StorePath path = storageClient.uploadFile(null,new FileInputStream(file),file.length(),file.getName());
System.out.println(path.getFullPath());
}
}