天天看點

分布式檔案存儲系統FastDFS

分布式檔案存儲系統FastDFS

一、分布式檔案存儲

1.分布式檔案存儲的由來

  在我們的項目中有很多需要存儲的内容出現,比如圖檔,視訊,檔案等等,在早期的時候使用者量不大,産生的檔案也不是很多,這時我們可以把檔案和服務程式放在一個伺服器中。

分布式檔案存儲系統FastDFS

  後面随着檔案越來越多,伺服器的資源會被檔案資源大量占據,進而影響到伺服器的穩定,這時我們可以單獨的把檔案伺服器拆出來。

分布式檔案存儲系統FastDFS

  拆解出來後,檔案服務的使用不會影響到我們的系統服務的穩定,但是當使用者量越來越大,存儲的檔案就會越來越多,這時如果還是單台的檔案服務,比如100T的檔案,這時是存儲不下去的,這時就産生了我們将的分布式檔案存儲,

分布式檔案存儲系統FastDFS

  也就是我們解決如何将這100T的檔案分散的存儲到各個節點上,然後當我們需要讀取檔案的時候又能非常快的幫我們把檔案找到。這個就是分布式檔案系統幫我們解決的問題了。

2.常見的分布式存儲架構

  接下來我們看看在國内常用的分布式存儲的架構選擇有哪些

分布式架構 說明
FastDFS 我們介紹的主角,國産
HDFS Hadoop元件中分布式存儲架構
MinIO MinIO是在Apache下的産品,最适合存儲非結構化的資料,<br />比如照片,視訊,日志檔案,備份和容器等。
阿裡雲對象存儲 當然我們還可以花費一點費用來使用其他廠商提供的對象存儲服務

  好了就介紹這麼幾個,其他的我們也用不到了。

二、FastDFS介紹

  FastDFS是餘慶國人開發的一個開源的輕量級分布式檔案系統,它對檔案進行管理,功能包括:檔案存儲、檔案同步、檔案通路(檔案上傳、檔案下載下傳)等,解決了大容量存儲和負載均衡的問題。特别适合以檔案為載體的線上服務,如相冊網站、視訊網站等等。

  FastDFS為網際網路量身定制,充分考慮了備援備份、負載均衡、線性擴容等機制,并注重高可用、高性能等名額,使用FastDFS很容易搭建一套高性能的檔案伺服器叢集提供檔案上傳、下載下傳等服務。

FastDFS的特點:

  • FastDFS是一個輕量級的開源分布式檔案系統
  • FastDFS主要解決了大容量的檔案存儲和高并發通路的問題,檔案存取時實作了負載均衡
  • FastDFS實作了軟體方式的RAID,可以使用廉價的IDE硬碟進行存儲
  • 支援存儲伺服器線上擴容
  • 支援相同内容的檔案隻儲存一份,節約磁盤空間
  • FastDFS隻能通過Client API通路,不支援POSIX通路方式
  • FastDFS特别适合大中型網站使用,用來存儲資源檔案(如:圖檔、文檔、音頻、視訊等等)

架構圖:

分布式檔案存儲系統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

  FastDFS的安裝我們還是通過Docker來安裝實作吧,直接在Linux上還裝還是比較繁瑣的,但就學習而言Docker安裝還是非常高效的。Docker環境請自行安裝哦,不清楚的可以看看我的Docker專題的内容。​​https://blog.csdn.net/qq_38526573/category_9619681.html​​

1.拉取鏡像檔案

  首先我們可以通過 ​

​docker search fastdfs​

​ 來查詢下有哪些鏡像檔案。

分布式檔案存儲系統FastDFS

  我們看到搜尋到的鏡像還是蠻多的,這裡我們使用 ​

​delron/fastdfs​

​​ 你也可以嘗試使用其他的鏡像來安裝,你也可以制作自己的鏡像來給别人使用哦,隻是不同的鏡像在使用的時候配置會有一些不一樣,有些鏡像沒有提供Nginx的相關配置,使用的時候會繁瑣一點。接下來通過 ​

​docker pull delron/fastdfs​

​指令把鏡像拉取下來。

分布式檔案存儲系統FastDFS

2.建構Tracker服務

  首先我們需要通過Docker指令來建立Tracker服務。指令為

docker run -d --name tracker --network=host -v /mydata/fastdfs/tracker:/var/fdfs delron/fastdfs tracker      

  tracker服務預設的端口為22122,-v 實作了容器和本地目錄的挂載操作。

分布式檔案存儲系統FastDFS

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位址。

分布式檔案存儲系統FastDFS

  預設情況下在Storage服務中是幫我們安裝了Nginx服務的,相關的端口為

服務 預設端口
tracker 22122
storage 23000
Nginx 8888

  當然如果你發現這些相關的端口被占用了,或者想要對應的修改端口資訊也可以的。要修改你可以先進入容器中檢視下相關的配置檔案資訊。

分布式檔案存儲系統FastDFS

  然後檢視storage.conf檔案

分布式檔案存儲系統FastDFS

  這個是storage監聽的Nginx的端口8888,如果要修改那麼我們還需要修改Nginx中的服務配置,這塊的配置在 ​

​/usr/local/nginx/conf​

​目錄下

分布式檔案存儲系統FastDFS

  檢視下檔案

分布式檔案存儲系統FastDFS

是以要修改端口号的話,這兩個位置都得修改了。當然本文我們就使用預設的端口号來使用了。

4.測試圖檔上傳

  好了,安裝我們已經完成了,那麼到底是否可以使用呢?我們來測試下。首先在虛拟機的/mydata/fastdfs/storage下儲存一張圖檔。

分布式檔案存儲系統FastDFS

  然後我們再進入到storage容器中。并且進入到 ​

​/var/fdfs​

​目錄下,可以看到我們挂載的檔案了

分布式檔案存儲系統FastDFS

  然後執行如下指令即可完成圖檔的上傳操作

/usr/bin/fdfs_upload_file /etc/fdfs/client.conf 1.jpg      
分布式檔案存儲系統FastDFS

  通過上面的提示我們看到檔案上傳成功了,而且傳回了檔案在storage中存儲的資訊。這時我們就可以通過這個資訊來拼接通路的位址在浏覽器中通路了:http://192.168.56.100:8888/group1/M00/00/00/wKg4ZGHcKLSAXibaAAezMuUrlS8235.jpg

分布式檔案存儲系統FastDFS

  好了到這兒FastDFS的服務安裝成功了。

四、用戶端操作

1.Fastdfs-java-client

  首先我們來看下如何實作FastDFS中提供的JavaAPI來直接實作對應的檔案上傳和下載下傳操作。

1.1 檔案上傳

  先來看下檔案上傳的流程

分布式檔案存儲系統FastDFS

  上傳流程的文字梳理為:

  1. 用戶端通路Tracker
  2. Tracker 傳回Storage的ip和端口
  3. 用戶端直接通路Storage,把檔案内容和中繼資料發送過去。
  4. 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      
分布式檔案存儲系統FastDFS

  然後導入對應的工具類,在工具類中完成了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();
        }
    }      
分布式檔案存儲系統FastDFS

通路即可:http://192.168.56.100:8888/group1/M00/00/00/wKg4ZGHcUE6AZA2UAAW8dIX5p50374.jpg

  傳回後的字元串的結構說明

分布式檔案存儲系統FastDFS

1.2 檔案下載下傳

  檔案下載下傳的流程,如下

分布式檔案存儲系統FastDFS

  檔案下載下傳的流程為:

  1. client詢問tracker需要下載下傳的檔案的storage,參數為檔案的辨別(group加檔案名)。
  2. tracker根據用戶端的參數傳回一台可用的storage。
  3. 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是線程不安全的。那麼我們的解決方案

  1. 對檔案的操作的每個方法我們做同步處理
  2. 每次操作檔案的時候我們都擷取一個新的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檔案中提供對應的自動配置類。

分布式檔案存儲系統FastDFS

  可以看到給我們提供的配置類為FdfsAutoConfiguration進入後可以看到幫我們注入了很多的核心對象。

分布式檔案存儲系統FastDFS

  然後可以看到系統提供的配置資訊,字首為 ​

​fdfs​

分布式檔案存儲系統FastDFS

  然後我們就可以在application.properties中配置FastDFS的配置資訊了。

分布式檔案存儲系統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());
    }

}