文章目錄
- 1 分布式文件存儲
- 1_分布式文件存儲的由來
- 2_常見的分布式存儲框架
- 2 FastDFS介紹
- 3 FastDFS安裝
- 1_拉取鏡像文件
- 2_構建Tracker服務
- 3_構建Storage服務
- 4_測試圖片上傳
- 4 客戶端操作
- 1_Fastdfs-java-client
- 2_文件上傳
- 3_文件下載
- 4_獲取文件信息
- 5_問題
- 5 SpringBoot整合
1 分布式文件存儲
1_分布式文件存儲的由來
在我們的項目中有很多需要存儲的內容出現,比如圖片,視頻,文件等等,在早期的時候用戶量不大,產生的文件也不是很多,這時我們可以把文件和服務程序放在一個服務器中。
后面隨著文件越來越多,服務器的資源會被文件資源大量占據,從而影響到服務器的穩定,這時我們可以單獨的把文件服務器拆出來。
拆解出來后,文件服務的使用不會影響到我們的系統服務的穩定,但是當用戶量越來越大,存儲的文件就會越來越多,這時如果還是單臺的文件服務,比如100T的文件,這時是存儲不下去的,這時就產生了我們將的分布式文件存儲,
也就是我們解決如何將這100T的文件分散的存儲到各個節點上,然后當我們需要讀取文件的時候又能非常快的幫我們把文件找到。這個就是分布式文件系統幫我們解決的問題了。
2_常見的分布式存儲框架
接下來我們看看在國內常用的分布式存儲的框架選擇有哪些
分布式框架 | 說明 |
---|---|
FastDFS | 我們介紹的主角,國產 |
HDFS | Hadoop組件中分布式存儲框架 |
MinIO | MinIO是在Apache下的產品,最適合存儲非結構化的數據,比如照片,視頻,日志文件,備份和容器等。 |
阿里云對象存儲 | 當然我們還可以花費一點費用來使用其他廠商提供的對象存儲服務 |
2 FastDFS介紹
FastDFS是余慶(國人,淘寶)開發的一個開源的輕量級分布式文件系統,它對文件進行管理,功能包括:文件存儲、文件同步、文件訪問(文件上傳、文件下載)等,解決了大容量存儲和負載均衡的問題。
特別適合以文件為載體的在線服務,如相冊網站、視頻網站等等。
FastDFS為互聯網量身定制,充分考慮了冗余備份、負載均衡、線性擴容等機制,并注重高可用、高性能等指標,使用FastDFS很容易搭建一套高性能的文件服務器集群提供文件上傳、下載等服務。
FastDFS的特點:
- FastDFS是一個輕量級的開源分布式文件系統
- FastDFS主要解決了大容量的文件存儲和高并發訪問的問題,文件存取時實現了負載均衡
- FastDFS實現了軟件方式的RAID,可以使用廉價的IDE硬盤進行存儲
- 支持存儲服務器在線擴容
- 支持相同內容的文件只保存一份,節約磁盤空間
- FastDFS只能通過Client API訪問,不支持POSIX訪問方式
- FastDFS特別適合大中型網站使用,用來存儲資源文件(如:圖片、文檔、音頻、視頻等等)
架構圖:
相關術語講解:
名詞 | 描述 |
---|---|
Tracker Server | 跟蹤服務器,主要做調度工作,在訪問上起負載均衡的作用。記錄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之間不會相互通信。
3 FastDFS安裝
FastDFS的安裝我們還是通過Docker來安裝實現吧,直接在Linux上還裝還是比較繁瑣的,但就學習而言Docker安裝還是非常高效的。
Docker環境請自行安裝哦,不清楚的可以看看我的Docker專題的內容。
1_拉取鏡像文件
首先我們可以通過 docker search fastdfs
來查詢下有哪些鏡像文件。
我們看到搜索到的鏡像還是蠻多的,這里我們使用 delron/fastdfs
你也可以嘗試使用其他的鏡像來安裝,你也可以制作自己的鏡像來給別人使用哦,只是不同的鏡像在使用的時候配置會有一些不一樣,有些鏡像沒有提供Nginx的相關配置,使用的時候會繁瑣一點。
接下來通過 docker pull delron/fastdfs
命令把鏡像拉取下來(YGQYGQ2/fastdfs-nginx)。
2_構建Tracker服務
準備基本環境
mkdir -p /mydata/fastdfs/tracker
mkdir -p /mydata/fastdfs/storage
首先我們需要通過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.200.129: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 |
當然如果你發現這些相關的端口被占用了,或者想要修改對應的端口信息也可以的。
不過這需要配置,你可以先進入容器中查看下相關的配置文件信息。
# 進入容器
docker exec -it storage bash
# 進入目錄
cd /etc/fdfs
# 查看配置問價
ls
然后查看storage.conf文件(可以設置group、storage端口和nginx端口)
這個是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.200.129:8888/group1/M00/00/00/wKjIgWf19-mAWO23AABSIMuaaeU816.jpg(如果訪問不到可以嘗試關閉防火墻)
到這兒FastDFS的服務安裝成功了。
4 客戶端操作
1_Fastdfs-java-client
首先我們來看下如何實現FastDFS中提供的JavaAPI來直接實現對應的文件上傳和下載操作。
創建一個普通的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.200.129:22122
然后導入對應的工具類,在工具類中完成了StorageClient的實例化,并提供了相關的上傳和下載的方法。
package org.duration.fastdfs.config;import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.*;import java.io.*;
import java.util.Objects;@Slf4j
public class FastDFSClient {private static final String CONF_FILENAME = Objects.requireNonNull(Thread.currentThread().getContextClassLoader().getResource("")).getPath() + "fastdfs-config.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) {log.error(e.getMessage(), e);}}/*** @param inputStream 上傳的文件輸入流* @param fileName 上傳的文件原始名* @return 【group,file_path】*/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.available() > 0) {// 查看文件的長度int len = inputStream.available();// 創建對應長度的字節數組file_buff = new byte[len];// 將輸入流中的字節內容,讀到字節數組中。int read = inputStream.read(file_buff);}// 上傳文件。參數含義:要上傳的文件的內容(使用字節數組傳遞),上傳的文件的類型(擴展名),元數據return storageClient.upload_file(file_buff, getFileExt(fileName), meta_list);} catch (Exception ex) {log.error(ex.getMessage(), ex);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;int len = fis.available();if (len > 0) {file_buff = new byte[len];int read = fis.read(file_buff);}return storageClient.upload_file(file_buff, getFileExt(fileName), meta_list);} catch (Exception ex) {return null;} finally {if (fis != null) {try {fis.close();} catch (IOException e) {log.error(e.getMessage(), e);}}}}/*** 根據組名和遠程文件名來刪除一個文件** @param groupName 例如 "group1" 如果不指定該值,默認為group1* @param remoteFileName 例如"M00/00/00/wKgxgk5HbLvfP86RAAAAChd9X1Y736.jpg"* @return 0為成功,非0為失敗,具體為錯誤代碼*/public static int deleteFile(String groupName, String remoteFileName) {try {return storageClient.delete_file(groupName == null ? "group1" : groupName, remoteFileName);} 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);return new ByteArrayInputStream(bytes);} catch (Exception ex) {return null;}}public static NameValuePair[] getMetaDate(String groupName, String remoteFileName) {try {return storageClient.get_metadata(groupName, remoteFileName);} catch (Exception ex) {log.error(ex.getMessage(), ex);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); // 不帶最后的點}}}
然后我們就可以來測試上傳的操作了。
2_文件上傳
先來看下文件上傳的流程
上傳流程的文字梳理為:
- 客戶端訪問Tracker
- Tracker 返回Storage的IP和端口
- 客戶端直接訪問Storage,把文件內容和元數據發送過去。
- Storage返回文件存儲id。包含了組名和文件名
//不可獲取元數據信息
public static void fileUpload() {File file = new File("D:\\WorkSpace\\IdeaProject\\fastdfs_demo\\src\\main\\resources\\10937845.jpg");String[] strings = FastDFSClient.uploadFile(file, file.getName());if (strings == null) {return;}System.out.println("文件上傳成功" + Arrays.asList(strings));
}
//可獲取元數據
public static void fileUploadByMetaData() {File file = new File("D:\\WorkSpace\\IdeaProject\\fastdfs_demo\\src\\main\\resources\\10937845.jpg");try (InputStream is = new FileInputStream(file)) {String[] strings = FastDFSClient.uploadFile(is, file.getName());if (strings == null) {return;}System.out.println("文件上傳成功" + Arrays.asList(strings));} catch (Exception e) {log.error(e.getMessage(), e);}
}
訪問即可:http://192.168.200.129:8888/group1/M00/00/00/wKjIgWf2Ju-AZEFGAAAqSN2jiTQ329.jpg
返回后的字符串的結構說明
3_文件下載
文件下載的流程,如下
文件下載的流程為:
- client詢問tracker需要下載的文件的storage,參數為文件的標識(group加文件名)。
- tracker根據客戶端的參數返回一臺可用的storage。
- client根據返回的storage直接完成對應的文件的下載。
有了上面的基礎,文件下載就非常簡單了,我們只需要根據前面上傳的文件的group和文件的存儲路徑就可以通過StorageClient中提供的downloadFile方法把對應的文件下載下來了,具體的代碼如下
public static void downloadFile() {String[] params = {"group1", "M00/00/00/wKjIgWf2Ju-AZEFGAAAqSN2jiTQ329.jpg"};try (InputStream is = FastDFSClient.downloadFile(params[0], params[1]);OutputStream os = new FileOutputStream("D:\\WorkSpace\\IdeaProject\\fastdfs_demo\\src\\main\\resources\\12.jpg")) {if (is == null) {return;}int index = 0;while ((index = is.read()) != -1) {os.write(index);}os.flush();} catch (IOException e) {log.error(e.getMessage(), e);}
}
4_獲取文件信息
獲取文件元數據信息
public static void getFileInfo() {String[] params = {"group1", "M00/00/00/wKjIgWf2NPqAamQdAAAqSN2jiTQ216.jpg"};NameValuePair[] metaDate = FastDFSClient.getMetaDate(params[0], params[1]);if (metaDate == null) {return;}for (NameValuePair nameValuePair : metaDate) {log.info("信息如下:{}={}", nameValuePair.getName(), nameValuePair.getValue());}
}
還有一些其他接口比如獲取文件擴展名,可自行測試
5_問題
注意:StorageClient是線程不安全的。那么我們的解決方案
- 對文件的操作的每個方法我們做同步處理
- 每次操作文件的時候我們都獲取一個新的StorageClient對象(全局變局部)
第一種方式效率肯定是最低的,第二種方式每次都要建立新的連接效率同樣的會受到影響,這時最好的方式其實是把StorageClient交給我們自定義的連接池來管理
5 SpringBoot整合
我們在實際工作中基本都是和SpringBoot整合在一起來使用的,那么我們就來看看FastDFS是如何在SpringBoot項目中來使用的。
首先創建一個普通的SpringBoot項目,然后導入fastdfs-spring-boot-starter
這個依賴。
<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>
既然是一個starter,那么必然會在spring.factories文件中提供對應的自動配置類。
可以看到給我們提供的配置類為FdfsAutoConfiguration進入后可以看到幫我們注入了很多的核心對象。
然后可以看到系統提供的配置信息,前綴為 fdfs
然后我們就可以在application.yaml
中配置FastDFS的配置信息了。
fdfs:connect-timeout: 1000so-timeout: 1000tracker-list: 192.168.200.129:22122
配置完成后我們就可以測試文件的上傳下載操作了
@SpringBootTest
class FastDfsSpringBootApplicationTests {@Autowiredpublic FastFileStorageClient storageClient;@Testvoid contextLoads() throws Exception{File file = new ClassPathResource("10937845.jpg").getFile();StorePath path = storageClient.uploadFile(null, new FileInputStream(file), file.length(), file.getName().substring(file.getName().lastIndexOf(".") + 1));System.out.println(path.getFullPath());}}
文件操作成功