1. 程式人生 > 實用技巧 >暢購商城(二):分散式檔案系統FastDFS

暢購商城(二):分散式檔案系統FastDFS

好好學習,天天向上

本文已收錄至我的Github倉庫DayDayUP:github.com/RobodLee/DayDayUP,歡迎Star,更多文章請前往:目錄導航

FastDFS介紹

1. 簡介

FastDFS是一個開源的輕量級分散式檔案系統,它對檔案進行管理,功能包括:檔案儲存、檔案同步、檔案訪問(檔案上傳、檔案下載)等,解決了大容量儲存和負載均衡的問題。特別適合以檔案為載體的線上服務,如相簿網站、視訊網站等等。FastDFS為網際網路量身定製,充分考慮了冗餘備份、負載均衡、線性擴容等機制,並注重高可用、高效能等指標,使用FastDFS很容易搭建一套高效能的檔案伺服器叢集提供檔案上傳、下載等服務。

2. 架構

FastDFS由跟蹤伺服器(Tracker Server)、儲存伺服器(Storage Server)和客戶端(Client)構成。

  • TrackerStorageClient

Tracker不負責檔案的儲存,它主要負責檔案管理負載均衡操作,就像是一箇中介,為Client提供Storage的資訊。而Storage才是負責檔案上傳,修改,刪除等工作。

Client:“嗨,Tracker!你那裡還有沒有正在閒著的Storage呀?我現在有些檔案需要儲存。”

Tracker:“我來看看,嗯.... 找到了,Storage5現在正好閒著,他的地址是192.168.200:8888/group5,你直接去找他吧。”

Client:“OK!”

Client:“嗨,Storage5!我這裡現在有一堆檔案,就放你這裡吧。”

Storage5:“好的,老闆,幫你存好了,放在我這兒你就放心吧,我還讓我家二弟幫你做了備份,不會丟的。”

Client:“好的,謝謝啦!”

......

通過上面的對話,就應該能明白這三者之間的關係了吧。

從圖中可以看出,Storage是一個叢集,叢集裡面分為一個一個的組,每個組裡面有不止一臺Storage(也可以是一臺),這麼設計有什麼好處呢?首先並不是只有一臺Storage為客戶端提供服務,而是由Tracker動態地分配相對空閒的Storage給客戶端提供服務,這樣就做到了負載均衡

,使得不會有個別Storage壓力過大。還有就是每個組裡面不止一個Storage,一個組裡面的所有Storage都是同步備份資料的,為的就是實現容災,一臺Storage掛了資料也不會丟失,除非全掛了。還有一個好處就是可以很方便地實現線性擴容,如果哪天Storage的空間不夠用了,就可以直接新增一組Storage,然後註冊到Tracker中。同樣的,Tracker也是叢集。

3. 檔案上傳流程


首先Storage會定時向Tracker上報自己的狀態資訊,這樣Tracker就會知道Storage還是不是活著;當Client給Tracker傳送請求的時候,Tracker會查詢是否有可用的Storage;有的話就將Storage的資訊給Client,Client在得到Storage的資訊後,就將檔案上傳到StorageStorage先是生成一個file_id,再將檔案寫入磁碟中儲存,最後將file_id返回給Client

4. file_id的格式


group1是Storage所在的組名,M00是Storage的虛擬路徑,02/04是兩級目錄,後面的就是檔名了,這個檔名是自動生成的。

FastDFS搭建

我們要把FastDFS安裝到docker中,首先要有docker的環境,資料提供的虛擬機器裡面已經安裝好docker了。

1. 下載FastDFS

接下來將FastDFS的映象下載到本地

docker pull morunchang/fastdfs

下載完成之後看一下有沒有:

docker images


已經下載好了,接下來就開始安裝吧。

2. 安裝Tracker和Storage

安裝Tracker

docker run -d --name tracker --net=host morunchang/fastdfs sh tracker.sh
  • docker run:執行容器
  • -d:後臺執行
  • --name trackr:起個名字,這裡叫tracker,也可以用等號連線 --name=tracker
  • --net=host:host網路模式,容器與宿主機共享IP
  • morunchang/fastdfs:映象
  • sh tracker.sh:執行一個名為tracker.sh的shell指令碼

現在Tracker就安裝好了。

安裝Storage

docker run -d --name storage --net=host -e TRACKER_IP=192.168.31.200:22122 -e GROUP_NAME=group1 morunchang/fastdfs sh storage.sh
  • -e:將Storage新增到環境變數
  • TRACKER_IP:Tracker的IP以及埠
  • GROUP_NAME=group1:當前Storage的組名

Tracker和Storage開機自啟動

現在我們的Tracker和Storage已經安裝好了,但是如果每次開機都要去手動啟動的話還是太麻煩了,所以設定一下開機自啟動:

docker update --restart=always tracker
docker update --restart=always storage

需要注意的是這裡的“tracker”和“storage”是前面--name設定的名字,前面設定的是什麼,這裡就寫什麼。到此為止,FastDFS就已經安裝好了。

3. 配置ngx_fastdfs_module


當我們訪問Storage中的檔案資源的時候,中間是經過了Nginx,然後通過ngx_fastdfs_module去訪問Storage的,ngx_fastdfs_module在安裝Tracker和Storage的時候已經自動幫我們安裝好了,我們來看一下。

docker exec -it storage  /bin/bash			//進入到storage容器中
vim /etc/nginx/conf/nginx.conf			    //修改/etc/nginx/conf/目錄下的nginx。conf檔案

裡面會有這麼一段內容

location ~ /M00 {
	root /data/fast_data/data;
	ngx_fastdfs_module;
}

上面這段內容就說明ngx_fastdfs_module已經配置好了。

4. 設定禁止快取

當瀏覽器訪問過Storage中的資源後,即便將Storage中的資源已經被刪除了,瀏覽器還是會訪問快取中資料,那如果我們不想這樣,就可以配置一下禁止快取。還是上面的步驟,在裡面新增一行內容:

location ~ /M00 {
	add_header Cache-Control no-store;	#告訴瀏覽器不要快取資料
	root /data/fast_data/data;
    ngx_fastdfs_module;
}

5. 檔案位置

nginx配置檔案

前面我們說過,訪問Storage先是通過了Nginx,那麼Nginx的配置檔案在哪呢?

docker exec -it storage  /bin/bash			//進入到storage容器中
cd etc/nginx/conf


可以看到,這裡有個nginx.conf檔案,這個就是nginx的配置檔案。裡面配置了Nginx的埠等資訊。

Tracker和Storage配置檔案

cd etc/fsds		//切換到etc/fsds/conf目錄下


storage.conf和tracker.conf分別是Storage和Tracker配置檔案。

6. 儲存的檔案位置

上一節中提到了file_id的格式,其中有個虛擬目錄M00,看配置檔案中的M00,其實就是/data/fast_data/data目錄。切換進來看看:

cd data/fast_data/data


這些都是目錄,前面不是說了file_id的虛擬目錄後面跟著二級目錄麼,再進入00目錄看一下:

還是一堆目錄,再進入00目錄:

這個jpg檔案是我剛才傳上去的。

檔案儲存微服務

1. 建立微服務工程changgou-service-file

在changgou-service下新建一個Module叫做changgou-service-file,因為用到了fastdfs,自然需要新增依賴

<dependencies>
    <!-- FastDFS客戶端程式包-->
    <dependency>
        <groupId>net.oschina.zcx7878</groupId>
        <artifactId>fastdfs-client-java</artifactId>
        <version>1.27.0.0</version>
    </dependency>
</dependencies>

接下來就需要在resource目錄下建立FastDFS的配置檔案fdfs_client.conf

#連線超時,單位是秒
connect_timeout=60
#通訊超時時間,傳送或接收資料時。假設在超時時間後還不能傳送或接收資料,則本次網路通訊失敗
network_timeout=60
#字符集
charset=UTF-8
#Tracker的http埠
http.tracker_http_port=8080
#Tracker伺服器IP和埠設定
tracker_server=192.168.31.200:22122

註釋的部分請刪掉,我在使用的過程中總是連線失敗,然後把註釋刪了就好了。

微服務工程自然也需要配置,在resource目錄下建立application.yml

spring:
  servlet:
    multipart:
      max-file-size: 10MB       #上傳檔案最大大小
      max-request-size: 10MB    #請求資料最大大小
  application:
    name: file                  #該微服務的名字
server:
  port: 18082                   #該微服務的埠
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
feign:
  hystrix:
    enabled: true

最後再建立一個啟動類即可,在com.robod包下新建一個類FileApplication.class

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableEurekaClient
public class FileApplication {
    public static void main(String[] args) {
        SpringApplication.run(FileApplication.class,args);
    }
}

這裡有個地方需要強調一下,就是(exclude = {DataSourceAutoConfiguration.class}),它的作用是取消資料來源自動匯入。SpringBoot會自動從配置檔案中查詢spring.datasource.相關屬性並自動配置單資料來源。因為在這個微服務工程並沒有配置資料庫的相關屬性,所以不加exclude的話就會報錯。

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class

Action:

Consider the following:
	If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
	If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).

因此我們需要新增這行內容來取消資料來源自動匯入。

接下來我們先啟動Eureka工程後再來啟動一下該專案試試,啟動後訪問http://127.0.0.1:7001

OK! 專案成功啟動,接下來就實現一下檔案上傳,刪除等功能。

2. 檔案操作功能的實現

為了方便管理,我們將檔案資訊封裝成一個JavaBean,在com.robod.file包下新建一個類FastDFSFile

@Data	//不要忘了匯入Lombok的依賴
public class FastDFSFile {

    //檔名
    private String name;
    //檔案內容
    private byte[] content;
    //副檔名
    private String ext;
    //檔案MD5摘要
    private String md5;
    //檔案作者
    private String author;

    public FastDFSFile(String name, byte[] content, String ext) {
        this.name = name;
        this.content = content;
        this.ext = ext;
    }

    public FastDFSFile
            (String name, byte[] content, String ext, String md5, String author) {
        this.name = name;
        this.content = content;
        this.ext = ext;
        this.md5 = md5;
        this.author = author;
    }
}

現在再建立一個工具類來實現對檔案的一些操作,在com.robod.utils包下新建一個類FastDFSUtils

public class FastDFSUtils {

    private static StorageClient storageClient;
    private static TrackerClient trackerClient;
    private static TrackerServer trackerServer;

    static {
        try {
            String path = new ClassPathResource("fdfs_client.conf").getPath();
            //載入Tracker連線資訊
            ClientGlobal.init(path);
            //建立一個Tracker的客戶端物件
            trackerClient = new TrackerClient();
            //通過TrackerClient訪問TrackerServer服務,獲取連線物件
            trackerServer = trackerClient.getConnection();
            //通過TrackerServer的連線資訊去獲取Storage的連線資訊,儲存進StorageClient物件中
            storageClient = new StorageClient(trackerServer, null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 上傳檔案
     *
     * @param file
     */
    public static String[] upload(FastDFSFile file) throws Exception {
        //上傳檔案
        return storageClient.upload_file(file.getContent(), file.getExt(), null);
    }

    /**
     * 下載檔案
     * @param groupName         檔案所在的組名     group1
     * @param remoteFileName    檔案的路徑及名字   M00/00/00/wKgfyF8AjpSADeBLABXmlvc7iOY701.jpg
     */
    public static InputStream downloadFile(String groupName, String remoteFileName) throws Exception {
        byte[] bytes = storageClient.download_file(groupName, remoteFileName);
        return new ByteArrayInputStream(bytes);
    }

    /**
     * 刪除檔案
     * @param groupName         檔案所在的組名     group1
     * @param remoteFileName    檔案的路徑及名字   M00/00/00/wKgfyF8AjpSADeBLABXmlvc7iOY701.jpg
     */
    public static int deleteFile(String groupName, String remoteFileName) throws Exception {
        return storageClient.delete_file(groupName, remoteFileName);
    }

    /**
     * 獲取檔案資訊
     * @param groupName         檔案所在的組名     group1
     * @param remoteFileName    檔案的路徑及名字   M00/00/00/wKgfyF8AjpSADeBLABXmlvc7iOY701.jpg
     */
    public static FileInfo getFileInfo(String groupName, String remoteFileName) throws Exception {
        return storageClient.get_file_info(groupName,remoteFileName);
    }

    /**
     * 獲取storage資訊
     * @return  store_path_index
     */
    public static StorageServer getStorage() throws IOException {
        return trackerClient.getStoreStorage(trackerServer);
    }

    /**
     * 獲取Storage的IP和埠資訊
     * @param groupName
     * @param fileName
     * @return ServerInfo:ip_addr,port
     * @throws IOException
     */
    public static ServerInfo[] getStorageInfo(String groupName, String fileName) throws IOException {
        return trackerClient.getFetchStorages(trackerServer,groupName,fileName);
    }

    /**
     * 獲取Tracker資訊
     * @return
     */
    public static String getTrackerInfo() {
        String ip = trackerServer.getInetSocketAddress().getHostString();
        int port = ClientGlobal.getG_tracker_http_port();
        return new StringBuilder(ip).append(":").append(port).toString();
    }

}

在這個類中我們封裝了對檔案的一些列操作,現在就來寫個入口,在com.robod.controller包下新建一個類FileController

@RestController
@CrossOrigin
@RequestMapping("/file")
public class FileController {

    @PostMapping("/upload")
    public Result<String> upload(@RequestParam("file") MultipartFile multipartFile) throws Exception{
        FastDFSFile fastDFSFile = new FastDFSFile(
                multipartFile.getOriginalFilename(),
                multipartFile.getBytes(),
                StringUtils.getFilenameExtension(multipartFile.getOriginalFilename()));
        System.out.println(fastDFSFile.toString() );
        String[] upload = FastDFSUtils.upload(fastDFSFile);
        String groupName = upload[0];
        String fileName = upload[1];
        System.out.println(groupName);
        System.out.println(fileName);
        System.out.println("------------------");
        System.out.println("獲取檔案資訊");
        FileInfo fileInfo = FastDFSUtils.getFileInfo(groupName, fileName);
        System.out.println(fileInfo.getSourceIpAddr());
        System.out.println(fileInfo.getFileSize());
        System.out.println(fileInfo.getCreateTimestamp());
        System.out.println(fileInfo.getCrc32());
        System.out.println("----------------------");
        System.out.println("獲取Storage資訊");
        StorageServer storage = FastDFSUtils.getStorage();
        System.out.println(storage.getStorePathIndex());
        System.out.println("-----------------");
        System.out.println("獲取Storage的IP和埠資訊");
        ServerInfo[] storageInfo = FastDFSUtils.getStorageInfo(groupName, fileName);
        for (ServerInfo serverInfo : storageInfo) {
            System.out.println(serverInfo.getIpAddr());
            System.out.println(serverInfo.getPort());
        }
        System.out.println("--------------------");
        System.out.println("獲取Tracker資訊");
        String trackerInfo = FastDFSUtils.getTrackerInfo();
        System.out.println(trackerInfo);
        return new Result<>(true, StatusCode.OK,"檔案上傳成功","---");
    }
}

將專案啟動起來,用postman傳送請求。


從控制檯的列印情況中可以看出,檔案成功上傳到了伺服器中,而且前面寫的一些獲取資訊的方法也正常運行了。

寫在最後

這篇文章先是介紹了FastDFS的一些知識,接著就是講了FastDFS的安裝以及檔案路徑和配置的內容,最後搭建了檔案微服務並實現了對於檔案的操作功能。如果我的文章對你有些幫助,不要忘了點贊,收藏,轉發,關注。要是有什麼好的意見歡迎在下方留言。讓我們下期再見!