爲Hadoop存儲層增加對OpenStack Swift的支持

原文鏈接:爲 Hadoop 的存儲層增加對 OpenStack Swift 的支持

編者按:爲Hadoop的存儲層增加對OpenStack Swift的支持後,即可直接使用Hadoop MapReduce及其相關工具直接分析存儲在Swift中的數據。本文探討了通過編寫 Swift 適配器,將 OpenStack Swift 對象存儲作爲 Hadoop 的底層存儲,爲 Hadoop 的存儲層增加對 OpenStack Swift 的支持,最終達到功能驗證(Functional POC)的目標。以下爲原文:


背景


在 Hadoop 中有一個抽象文件系統的概念,它有多個不同的子類實現,由 DistributedFileSystem 類代表的 HDFS 便是其中之一。在 Hadoop 的 1.x 版本中,HDFS 存在 NameNode 單點故障,並且它是爲大文件的流式數據訪問而設計的,不適合隨機讀寫大量的小文件。本文將探討通過使用其他的存儲系統,例如 OpenStack Swift 對象存儲,作爲 Hadoop 的底層存儲,爲 Hadoop 的存儲層增加對 OpenStack Swift 的支持,並給出測試結果,最終達到功能驗證(Functional POC)的目標。值得一提的是,爲 Hadoop 增加對 OpenStack Swift 的支持並非要取代 HDFS,而是爲使用 Hadoop MapReduce 及其相關的工具直接分析存儲在 Swift 中的數據提供了方便;本文作爲一個階段性的嘗試,目前尚未考慮數據局部性(Data Locality),這部分將作爲未來的工作。另外,Hadoop 2.x 提供了高可用 HDFS 的解決辦法,不在本文的討論範圍之內。

本文面向的讀者爲對 Hadoop 和 OpenStack Swift 感興趣的軟件開發者和管理員,並假設讀者已經對它們有基本的瞭解。本文使用的 Hadoop 的版本爲 1.0.4,OpenStack Swift 的版本爲 1.7.4,Swift Java Client API 的版本爲 1.8,用於認證的 Swauth 的版本爲 1.0.4。



Hadoop 與 OpenStack Swift 對象存儲的整合


設想以下情形,如果已經在 Swift 中存儲了大量數據,但是想要使用 Hadoop 對這些數據進行分析,挖掘出有用的信息。此時可能的做法是,先將 Swift 集羣中的數據導出到中間服務器,再將這些數據導入到 HDFS 中,才能通過運行 MapReduce 作業來分析這些數據。如果數據量非常大,那麼整個導入數據的過程會很長,並且要使用更多的存儲空間。

如果能將 Hadoop 和 OpenStack Swift 進行整合,使得 Hadoop 能夠直接訪問 Swift 對象存儲,並能運行 MapReduce 作業來分析存儲在 Swift 中的數據,那麼將提高效率,減少硬件成本。


Hadoop 抽象文件系統 API


org.apache.hadoop.fs.FileSystem 是 Hadoop 中的一個通用文件系統的抽象基類,它抽象出了文件系統對文件和目錄的各種操作,例如:創建、拷貝、移動、重命名、刪除文件和目錄、讀寫文件、讀寫文件元數據等基本的文件系統操作,以及文件系統的一些其他通用操作。 Hadoop官方API中可以看到FileSystem抽象類中的方法和含義。



FileSystem 抽象類有多個不同的子類實現,包括:本地文件系統實現、分佈式文件系統實現、內存文件系統實現、FTP 文件系統實現、非 Apache 提供的第三方存儲系統實現,以及通過 HTTP 和 HTTPS 協議訪問分佈式文件系統的實現。其中,LocalFileSystem 類代表了進行客戶端校驗和的本地文件系統,在未對 Hadoop 進行配置時是默認的文件系統。分佈式文件系統實現是 DistributedFileSystem 類,即 HDFS,用來存儲海量數據,典型的應用是存儲大小超過了單臺機器的磁盤總容量的大數據集。第三方存儲系統實現是由非 Apache 的其他廠商提供的開源實現,如 S3FileSystem 和 NativeS3FileSystem 類,它們是使用 Amazon S3 作爲底層存儲的文件系統實現。

FileSystem 抽象類有多個不同的子類實現,包括:本地文件系統實現、分佈式文件系統實現、內存文件系統實現、FTP 文件系統實現、非 Apache 提供的第三方存儲系統實現,以及通過 HTTP 和 HTTPS 協議訪問分佈式文件系統的實現。其中,LocalFileSystem 類代表了進行客戶端校驗和的本地文件系統,在未對 Hadoop 進行配置時是默認的文件系統。分佈式文件系統實現是 DistributedFileSystem 類,即 HDFS,用來存儲海量數據,典型的應用是存儲大小超過了單臺機器的磁盤總容量的大數據集。第三方存儲系統實現是由非 Apache 的其他廠商提供的開源實現,如 S3FileSystem 和 NativeS3FileSystem 類,它們是使用 Amazon S3 作爲底層存儲的文件系統實現。

通過閱讀 Hadoop 的文件系統相關的源代碼和 Javadoc,並藉助於工具,可以分析出 FileSystem 抽象類的各個抽象方法的含義和用法,以及 FileSystem API 中各類之間的繼承、依賴關係。org.apache.hadoop.fs 包中包括了 Hadoop 文件系統相關的接口和類,如文件輸入流 FSDataInputStream 類和輸出流 FSDataOutputStream 類,文件元數據 FileStatus 類,所有的輸入/輸出流類都分別和 FSDataInputStream 類和 FSDataOutputStream 類是組合關係,所有的文件系統子類實現均繼承自 FileSystem 抽象類。Hadoop FileSystem API 的類圖如下圖所示。

 

Hadoop FileSystem API 的類圖 

以 S3FileSystem 爲例,它使用的底層存儲系統是 Amazon S3,繼承了 FileSystem 抽象類,是它的一個具體實現,並實現了針對 Amazon S3 的輸入/輸出流。用戶可以在 Hadoop 的配置文件 core-site.xml 中爲 fs.default.name 屬性指定 Amazon S3 存儲系統的 URI,就可以使 Hadoop 得以訪問 Amazon S3,並在其上運行 MapReduce 作業。

Swift 的 Java 客戶端 API

Swift 通過 HTTP 協議對外提供存儲服務,有一個 REST 風格的 API。Swift 本身是用 Python 語言實現的,但是也提供了多種編程語言的客戶端 API,例如:Python、Java、PHP、C#、Ruby 等。這些客戶端 API 都通過發起 HTTP 請求和接收 HTTP 響應來與 Swift 集羣的代理節點進行交互,Swift 客戶端 API 在 REST API 之上提供了更高層次的對容器和對象的操作,使得程序員編寫訪問 Swift 的程序變得更爲方便。

Swift 的 Java 客戶端 API 名叫 java-cloudfiles,也是一個開源項目。其中的 FilesClient 類提供了對 Swift 對象存儲的各種操作,包括:登錄 Swift、創建和刪除 Account、容器、對象,獲得 Account、容器、對象的元數據,以及讀寫對象的方法。其他相關的類包括:FilesContainer、FilesObject、FilesContainerInfo、FilesObjectMetaData 等,它們分別代表 Swift 中的容器和對象以及對應的元數據,如容器包含的對象個數,對象的大小、修改時間等。版本號爲 1.8 的 java-cloudfiles 能夠和開源版本的 Swift 兼容。Filesclient 類中主要的方法和含義見下表。

方法簽名含義
FilesClient(String, String, String, String, int)構造方法,參數包括代理節點的 URL、account、username、password,timeout
boolean login()登錄 Swift
void createContainer(String)創建容器
boolean deleteContainer(String)刪除容器
boolean containerExists (String)判斷容器是否存在
boolean storeObject(String, byte[], String, String, Map<String,String>)把字節數組中的值存儲到對象中,把元數據存儲到擴展屬性中
byte[] getObject (String, String)從 Swift 獲取對象內容並存入字節數組
List<FilesContainer> listContainers()列出某個賬戶包含的所有容器
List<FilesObject> listObjects(String)列出某個容器包含的所有對象
FilesContainerInfo getContainerInfo (String)獲取容器的元數據
FilesObjectMetaData getObjectMetaData (String, String)獲取對象的元數據


FilesClient 類中的主要方法和含義


綜上所述,Hadoop FileSystem API 能夠接受新的文件系統實現的機制,以及能夠用 Java 語言編寫應用程序與 Swift 進行交互操作,這兩點使得擴展 Hadoop 抽象文件系統是可行的。


Swift 適配器的設計

由上述內容得知,要擴展 Hadoop 的抽象文件系統,需要做以下兩項工作:繼承並實現 FileSystem 抽象類,並在實現類中使用 Swift 的 Java 客戶端 API 以進行各種文件操作。因此,擴展系統的設計應遵循軟件設計模式當中的對象適配器模式(Adapter Pattern)。對象適配器模式的作用是進行接口適配,就是將一個類的接口轉換成客戶程序希望的另一個接口,使得原本由於接口不兼容而不能一起工作的那些類可以一起工作。

在擴展系統中,Swift 適配器調用 Swift 的 Java 客戶端 API,實現了對 Swift 對象存儲的操作,Hadoop MapReduce API 調用 Hadoop FileSystem API,對於 MapReduce 來說,底層的 HDFS 和 Swift 都是透明的。與 HDFS 相比,Swift 適配器所在的 API 層次結構如下圖所示。

 

API 層次結構 

Swift 適配器的詳細設計如下:SwiftAdapter 是一個適配器類,FilesClient 是一個被適配類,SwiftAdapter 類繼承了 FileSystem 抽象類,它和 FilesClient 類是組合關係,包含了 FilesClient 類的一個引用。其中,FilesClient 類是 Swift 的 Java 客戶端 API 中的一個類。Swift 輸入/輸出流如下:SwiftInputStream 是針對 Swift 的輸入流,SwiftByteArrayInputStream 是一個包含字節數組緩存的輸入流,SwiftInputStream 包含了 SwiftByteArrayInputStream 的一個引用,SwiftOutputStream 是針對 Swift 的輸出流,它們繼承了相應的文件系統輸入/輸出流基類或接口,輸入流具有 seek 等功能,輸出流具有 flush 等功能。Swift 適配器中的類圖如下圖所示。

Swift適配器類圖

下表爲Swift 適配器中類的詳細關係: 

類名
父接口/父類
依賴類 
SwiftAdapter FileSystem FilesClient, SwiftInputStream, SwiftOutputStream 等 
SwiftInputStream FSInputStream FilesClient, SwiftByteArrayInputStream 
SwiftByteArrayInputStream ByteArrayInputStream, Seekable 
SwiftByteOutputStream ByteArrayOutputStreamFilesClient 
SwiftFileStatus(SwiftAdapter 的內部類) FileStatus 


Swift 適配器中類的詳細關係

Swift 適配器的實現

實現細節

在與 Swift 進行交互之前需要首先登錄 Swift,因此要使用 Swift 中預先創建的某個賬戶、用戶名和密碼,實現的細節如下。

調用 Swift 的 Java 客戶端 API,實現針對 Swift 的輸入/輸出流。

在 Hadoop 中,所有的輸入流類都需要繼承並實現 FSInputStream 抽象類,重點是實現 read 方法和 seek 方法。read 方法從輸入流中讀取下一個字節,是輸入流類最基本的方法,seek 方法設置輸入流的讀取位置,如果使用一個字節數組作爲緩衝則能實現隨機定位到某一字節。SwiftByteArrayInputStream 類繼承了 ByteArrayInputStream 類和 Seekable 接口,它使用了一個字節數組作爲緩衝。SwiftInputStream 類繼承 FSInputStream 抽象類,幷包含 SwiftByteArrayInputStream 類的一個引用,它調用 Swift 的 Java 客戶端 API,將 Swift 中的對象讀入到字節數組的緩衝。通過這樣的實現,針對 Swift 的輸入流類 SwiftInputStream 就具有了 read 和 seek 這些輸入流的基本操作。

在 Hadoop 中,輸出流類只需要是 OutputStream 抽象類的子類即可,重點是實現 write 方法和 flush 方法,它可以選擇是否實現 Syncable 接口的 sync 方法,sync 方法使得緩衝的數據與底層存儲設備同步。write 方法向輸出流中寫入一個字節,是輸出流類最基本的方法。SwiftOutputStream 類繼承了 OutputStream 抽象類的子類 ByteArrayOutputStream,在 flush 方法中調用 Swift 的 Java 客戶端 API,將緩衝中的所有字節存儲到 Swift 中的對象。通過這樣的實現,針對 Swift 的輸出流類 SwiftOutputStream 就具有了 write 和 flush 這些輸出流的基本操作。

調用 Swift 的 Java 客戶端 API,實現 SwiftAdapter 的各種文件操作。

實現的操作包括:打開文件並返回輸入流,創建文件並返回輸出流,刪除路徑,判斷路徑是否存在,獲得路徑的元數據,獲得文件系統的 URI,獲得工作目錄,創建目錄等等。目錄對應 Swift 中的容器,文件對應 Swift 中的對象。在實現的過程中,有幾個問題需要進行特殊處理。

首先,由於在 Swift 對象存儲中,名稱空間是扁平的,沒有目錄層次結構,所以在路徑上需要進行特殊處理,具體的做法是允許文件名稱包含斜槓(/)。在一般的 POSIX 兼容的文件系統中,斜槓不能作爲文件名的一部分,屬於非法字符,而在 Swift 中是允許的。通過這種方式,可以實現虛擬的目錄層次結構。此時,根路徑作爲容器的名稱,根目錄之後的整個路徑都作爲對象的名稱。

其次,由於 Swift 對象存儲不是一個真正的文件系統,與一般的文件系統不同,不包含用戶、用戶組以及其他使用者的可讀、可寫、可執行的權限信息,所以在權限上需要進行特殊處理,具體的做法是將這些權限信息存儲在對象的擴展屬性中。FilesClient 類的 storeObject 方法有一個 java.util.Map 類型的參數,可以把用戶、用戶組以及其他使用者的權限信息作爲 java.util.Map 對象中的元素,以代表權限類型的字符串作爲鍵,以權限對應的數字作爲值,例如用戶、用戶組以及其他使用者的權限信息分別爲<"Acl-User", "6">、<"Acl-Group", "4">、<"Acl-Others", "4">。把包含權限信息的 java.util.Map 對象作爲參數傳遞給 storeObject 方法,就可以將權限信息存儲到擴展屬性中了。

SwiftAdapter 類中接口轉換的對應關係如表 4 所示,下表列出了 SwiftAdapter 類與 FilesClient 類的方法之間的對應關係。

SwiftAdapter 類的方法FilesClient 類被轉換的方法
initialize調用 FilesClient 類的構造方法,初始化 FilesClient 類的實例
opengetObject 返回的字節數組作爲 SwiftInputStream 中的緩衝存儲
createstoreObject 將 SwiftOutputStream 中緩衝存儲中的字節保存到 Swift 的對象中
append不支持此操作
renamedeleteObject, storeObject
delete目錄對應 deleteObject 和 deleteContainer,文件對應 deleteObject
mkdirscreateContainer,storeObject
getFileStatus目錄對應 getContainerInfo, 文件對應 getObjectMetaData
initialize調用 FilesClient 類的構造方法,初始化 FilesClient 類的實例
opengetObject 返回的字節數組作爲 SwiftInputStream 中的緩衝存儲
createstoreObject 將 SwiftOutputStream 中緩衝存儲中的字節保存到 Swift 的對象中


SwiftAdapter 類中接口轉換的對應關係


編譯源代碼並打包成 JAR 文件,再將 JAR 文件及其依賴的類庫部署到 Hadoop 集羣中所有節點的$HADOOP_PREFIX/share/hadoop/lib 目錄中。

使用 RPM 文件安裝的 Hadoop 的類庫默認目錄是/usr/share/hadoop/lib。這就像將插件安裝到 Hadoop 中一樣,沒有對原有軟件進行修改。

修改 Hadoop 集羣中所有節點的配置文件 core-site.xml,使文件系統的 URI 指向 Swift 的代理節點,並指定 Swift 中的某個 Account、用戶名和密碼。

這些屬性會被 Swift 適配器讀取。在 Swift 集羣中部署多臺代理節點,還可以使用專門的負載均衡器(Load Balancer)或輪轉 DNS(Round-robin DNS)指向這些代理節點,並在 core-site.xml 中使文件系統的 URI 指向負載均衡器或輪轉 DNS。配置文件 core-site.xml 的屬性如下表所示。

屬性說明
fs.default.nameswift://proxy.swiftcluster.net:8080proxy.swiftcluster.net 是預先設置的輪轉 DNS 的域名
fs.swift.implswift.SwiftAdapter完整的類名
fs.swift.accountAUTH_5248434a-4066-407e-b5e3-0bec4fdbfc71Swift 中的一個 Account 名稱
fs.swift.usernametest:rootSwift 中的一個用戶名
fs.swift.passwordtesting上述用戶名對應的密碼
fs.swift.auth.urlhttp://proxy.swiftcluster.net:8080/auth認證服務器的 URL,此處使用 Swauth

配置文件 core-site.xml 的屬性

拓撲結構

Hadoop 集羣中部署了 1 臺 JobTracker 節點,以及多臺運行 TaskTracker 的 slave 節點,所有節點均加入了 Swift 適配器 JAR 文件及其依賴的類庫。Swift 集羣中部署了多個 Proxy 節點和 Storage 節點,並且部署了 1 臺輪轉 DNS 服務器,它指向這些 Swift 集羣中的代理節點。整個擴展系統的拓撲結構如下圖所示。

 

擴展系統拓撲結構圖 

流程

在 Swift 適配器中,以初始化文件系統實例、打開文件並讀取數據、以及創建文件並寫入數據的操作爲例,分別敘述它們的流程,並使用 UML 時序圖展示出來。

Hadoop 的文件系統客戶端命令行程序對應的是 org.apache.hadoop.fs.FsShell 類。在使用該命令行程序與文件系統進行交互的時候,Hadoop 首先會根據配置文件中指定的 scheme 尋找對應的文件系統實現類,並進行初始化操作。org.apache.hadoop.fs.FileSystem 類有一個靜態內部類 FileSystem.Cache,它使用一個 Java 的 Map 類型緩存了文件系統的實例對象,鍵是文件系統的 scheme 名稱,例如”hdfs”,值是對應的文件系統對象實例,例如 DistributedFileSystem 類的實例。在本文的實現中,Swift 適配器的 scheme 名稱是”swift”,對應的文件系統類是 swift.SwiftAdapter,並且在配置文件中設置屬性 fs.swift.impl 爲 swift.SwiftAdapter。初始化文件系統實例的詳細流程如下:如果名稱爲”swift”的 scheme 存在於該緩存中,則 FileSystem.Cache 直接通過 get 方法返回 swift.SwiftAdapter 的對象實例。否則,FileSystem 類調用靜態方法 createFileSystem,接着調用 ReflectionUtils 類的 newInstance 方法,最終調用 Constructor 類的 newInstance 方法,以反射的方式獲得 Swift 適配器類的對象實例,最後調用 initialize 方法進行必要的初始化操作。初始化文件系統實例的 UML 時序圖如下圖所示。

 

初始化文件系統實例的 UML 時序圖 

打開文件並讀取數據的詳細流程如下:在打開文件的時候,客戶程序調用 SwiftAdapter 類的 open 方法,SwiftAdapter 對象首先初始化 Swift 輸入流類 SwiftInputStream 的實例,然後 SwiftInputStream 對象會調用 FilesClient 對象的 getObject 方法向 Swift 集羣中的代理服務器發起 HTTP 請求獲取 Swift 中的對象,把數據存入 SwiftByteArrayInputStream 對象內部的字節數組緩衝中,之後客戶端程序調用 SwiftInputStream 對象的 read 方法讀取緩衝存儲中的字節,讀取數據的操作完成之後再調用 close 方法關閉 Swift 輸入流。打開文件並讀取數據的 UML 時序圖如下圖所示。

打開文件並讀取數據的 UML 時序圖

創建文件並寫入數據的詳細流程如下:在創建文件的時候,客戶程序調用 SwiftAdapter 類的 create 方法,SwiftAdapter 對象首先初始化 Swift 輸出流類 SwiftOutputStream 的實例,然後客戶程序調用 SwiftOutputStream 對象的 write 方法把數據寫入到它內部的字節數組緩衝中,直到調用它的 flush 方法或 close 方法,SwiftOutputStream 對象纔會調用 FilesClient 對象的 storeObject 方法,向 Swift 集羣中的代理服務器發起 HTTP 請求將緩衝存儲中的字節寫入 Swift 中的對象。創建文件並寫入數據的 UML 時序圖如下圖所示。

打開文件並讀取數據的 UML 時序圖



未來的工作

通過 Swift 適配器,將高可用的 Swift 對象存儲作爲 Hadoop 的底層存儲系統,使得 Hadoop 在存儲層面具有了高可用性。把 Swift 適配器部署到已有的 Hadoop 集羣中是簡單快捷的。原本用來分析存儲在 HDFS 中的數據的 MapReduce 應用程序,也無需修改即可分析存儲在 Swift 中的數據。

但是,使用 Swift 適配器將 Hadoop 與 Swift 對象存儲整合之後,整個系統的缺點是失去了數據局部性(Data Locality)的優勢。在 HDFS 中,NameNode 節點知道每一個文件塊存儲在哪一個 DataNode 節點上。因此在運行 MapReduce 作業的過程中,用戶編寫的 MapReduce 應用程序的二進制文件會被 MapReduce 框架調度發送至儘可能離數據最近的節點,最好的情況是在文件塊所在的 DataNode 節點上的 TaskTracker 進程啓動 Map 任務,此時 Map 任務從本地文件系統讀取輸入文件,這樣可以避免大量的數據在 Hadoop 集羣的不同節點之間傳輸,節省了網絡帶寬,也能加速 MapReduce 作業在 map 階段的運行速度。

通過 Swift 適配器,將 Swift 對象存儲作爲 Hadoop 的底層存儲系統,對 Hadoop 集羣來說,Swift 是一個外部存儲系統,TaskTracker 和文件不在同一個節點上,因此在 MapReduce 作業運行的 map 階段,所有的讀取文件操作都通過網絡傳輸數據。Swift 對象存儲對於 Hadoop 集羣來說是一個黑盒,MapReduce 框架無法知道存儲系統的內部細節。

本文的目的是爲 Hadoop 的存儲層增加對 OpenStack Swift 的支持,並非要取代 HDFS。作爲一個階段性的嘗試,目前並未考慮和解決數據局部性的問題,這部分將作爲未來的工作。

測試結果

Swift 適配器使得 Swift 對象存儲可以作爲 Hadoop 的底層存儲系統,實現的效果包括兩個方面:第一,使用 Hadoop 的文件系統命令行訪問 Swift 對象存儲。第二,運行 MapReduce 作業分析存儲在 Swift 中的數據。

使用 Hadoop 的文件系統命令行訪問 Swift 對象存儲

ls 列出某個目錄下的文件,在實現時未讀取文件的實際修改時間,因此默認爲 1970-1-1。如圖所示。

Figure xxx. Requires a heading

cat 查看某個文件的內容,如圖所示。

Figure xxx. Requires a heading

mkdir 創建目錄,如創建成功則無提示信息,否則提示該目錄已存在的信息,如圖所示。

Figure xxx. Requires a heading

put 將本地文件存入 Swift 對象存儲中,如操作成功則無提示信息,如圖所示。

Figure xxx. Requires a heading

get 將 Swift 對象存儲中的對象存到本地文件中,如操作成功則無提示信息,如圖所示。

Figure xxx. Requires a heading

rm, rmr 前者刪除文件,後者級聯刪除目錄,操作不論成功與否都有提示信息,如圖所示。

Figure xxx. Requires a heading

du 顯示某個目錄下的目錄和文件的大小,即字節長度,如圖所示。

Figure xxx. Requires a heading

運行 MapReduce 作業分析存儲在 Swift 中的數據

首先在 Hadoop 集羣中提交一個 MapReduce 作業,然後通過如下 URL 訪問 JobTracker 節點的 MapReduce 管理的頁面:http://<jobtracker-ip-address>:50030/jobtracker.jsp,點擊具體的作業鏈接進入查看運行結果的頁面,從頁面上的文件 Scheme(swift://)可以看出 Hadoop 已經在 Swift 對象存儲之上運行 MapReduce 作業了,運行結果頁面如圖 8 所示。

 在 Swift 對象存儲之上運行 MapReduce 作業示例圖

在 Swift 對象存儲之上運行 MapReduce 作業示例圖




總結

本文分析了 Hadoop FileSystem API 和 Swift Java client API,以及 Hadoop 與 OpenStack Swift 整合的可行性,介紹了 Swift 適配器的設計和實現細節,最終將 OpenStack Swift 對象存儲作爲 Hadoop 的底層存儲,使得它們能夠協同工作,爲 Hadoop 的存儲層增加了對 OpenStack Swift 的支持。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章