導讀:今天分享的題目是Alluxio元數據和數據的同步,從設計實現和優化的角度進行讨論。主要包括以下幾個方面的内容:
01
Alluxio簡介
Alluxio是雲原生的數據編排平台,通過解耦計算和存儲層,在中間産生了一個數據編排層,負責對上層計算應用隐藏底層的時間細節。Alluxio提供了統一的存儲命名空間,在中間層提供了緩存和其他數據管理功能。在下圖可以看到有Spark、Hive、Map reduce這一類傳統的Hadoop大數據計算應用、Presto 這種OLAP類型的數據分析,還有像Tensorflow、Pytorch這樣的AI應用。存儲層比較豐富,包括各種各樣的存儲。
圖1 Alluxio簡介
下面是Alluxio用戶列表,這些公司都公開展示了Alluxio的使用場景。通過粗略分類,看到非常多的行業,包括互聯網、金融、電子商務、娛樂、電信等。感興趣的同學可以關注公衆号,上面有相關文章的彙總。
圖2 Alluxio的用戶展示
--
02
Alluxio數據挂載
這部分将首先回顧Alluxio如何通過數據挂載實現統一編排層;之後讨論Alluxio如何和底層存儲保持一緻;介紹元數據和數據同步功能;Alluxio的時間原理和優化;最後對不同場景的推薦配置給出建議。
1. Alluxio統一的數據命名空間
首先介紹數據挂載這個功能。Alluxio通過把底層存儲挂載到Alluxio層上,實現了統一的數據命名空間。
圖3 Alluxio統一命名空間
上圖的例子中Alluxio挂載了HDFS和對象存儲。Alluxio的文件系統樹就是由左右兩棵樹合成,形成了一個虛拟文件系統的文件系統樹。它可以支持非常多的底層存儲系統,統一把它們稱作Under File System。稱為Under是因為它們都處于Alluxio的抽象層下。Alluxio支持各種各樣不同的底層存儲系統,比如不同版本的HDFS,支持NFS, Ceph, Amazon S3, Google Cloud之類不同的對象存儲。除此之外還支持非常多其他類型的對象存儲,比如微軟Azure、阿裡、華為、騰訊,也包括國内其他供應商,如七牛對象存儲。左下圖中的例子是在自己的電腦上運行Alluxio,可以挂載不同的存儲,比如挂載HDFS,另外還可以挂載不同版本的HDFS,挂載對象存儲,挂載網盤。
2. Alluxio挂載點
Alluxio的統一命名空間,實際就是把挂載合成了一個Alluxio的虛拟層。Alluxio的挂載點可以粗略分成兩種:
圖4 Alluxio挂載點
根挂載點直接挂在根節點上,組成了Alluxio的根節點。如果沒有根節點,無法産生,繼續形成下面的結構。所以要求在配置文件裡面定義根挂載點,系統啟動的時候就進行挂載,不挂載就沒有辦法啟動。
嵌套挂載點比較靈活,可以通過指令進行挂載。通過這個命令行,發出通知,做挂載的操作。同樣地,可以挂載,也可以卸載,就是把Mount換成Unmount。嵌套挂載點是嵌套在目錄的下面,可以挂在某個部分下面,不一定挂載在根節點下面。這裡有個要求,即兩個嵌套點的樹不能互相覆蓋,這樣帶來的好處是比較靈活。如果根挂載點将來需要更換,為了避免需要改配置和重啟服務,可以使用一個dummy的根挂載點,比如就挂載在本地路徑下面,不使用它,且不在它下面創建任何文件,它存在的唯一目的就是可以啟動Alluxio服務。然後在此基礎上,把所有要管理的存儲,都以嵌套挂載點的方式挂載上去。之後如果要改變,就直接卸載更換為其它挂載點,這樣就很靈活。所有挂載和挂載操作,都會記錄在日志裡,重啟系統,并重啟服務之後,無需再手動操作。
3. Alluxio策略化數據管理
圖5 Alluxio策略化數據管理
挂載操作有一個進階版操作,目前隻包含在商業版本裡面。所做的事情就是讓用戶可以把兩個存儲挂載到同一個路徑下,可以互相覆蓋。同時通過配置讀寫策略,定義讀寫文件到哪個存儲裡,并給出操作的先後順序。同時Alluxio有一個遷移策略,讓文件可以自動在Alluxio的管理下,在多個存儲之間進行遷移。例如,把HDFS和對象存儲同時挂載到同一路徑下,上層用戶隻能看到這樣一棵樹,但是實際上背後有兩個不同的存儲。通過配置,讓Alluxio把HDFS的數據,根據一些規則,定期遷移進S3,例如規定将超過七天的數據,認定是不常用到的冷數據之後,把它從HDFS的集群拿出來,遷移到S3,節省HDFS的存儲空間。
--
03
Alluxio底層存儲一緻性
在把底層存儲挂載到Alluxio的統一命名空間上之後,如何保持Alluxio和底層存儲的一緻性?我們在這一部分進行分析。
圖6 Alluxio與一緻性
Alluxio和底層存儲的一緻性,要從Alluxio命名空間中文件的來源說起。文件的操作分為兩類:
一緻性可以分為兩個部分:
下面先看寫數據的一緻性。
1. Alluxio寫文件流程
首先Alluxio寫文件的流程可以把它抽象成兩步。第一步是客戶端到Alluxio,第二步是Alluxio到UFS。
圖7 Alluxio寫文件流程
其中的每一步都可以抽象成下面的三個步驟:
同樣Alluxio到存儲系統也可以同樣地抽象提取。
客戶端和Alluxio之間,主要流程分三步:
同樣Alluxio到存儲系統也抽象成三步。不同存儲系統的抽象和具備的一緻性,都不同,此處進行抽象隻是為了便于理解。比如要求強一緻性保證,但是很多對象存儲,給的一緻性保證會弱很多,比如寫進去之後不能馬上讀到這個數據。在這裡,不考慮這種本身的不一緻性的問題。假設Alluxio向存儲提交了之後,就能保證存儲端的文件就是需要的樣子。
Alluxio為了滿足不同的需求,設計了幾種不同的寫策略,下面逐一分析寫策略的流程以及帶來的數據一緻性保證。
2. Must-Catch寫模式
圖8 Alluxio:MUST_CACHE寫模式
首先是常用的MUST_CACHE模式。在這種模式下,隻會寫Alluxio緩存不寫UFS。這個模式分為三步:
首先客戶端會向Alluxio 發出創建文件請求,創建的文件隻是一個空文件,作為一個占位;
之後Alluxio Worker實現具體數據的寫操作,具體數據會被分割成多個數據塊,作為Block存在于Alluxio Storage裡面。
在緩存寫之後,客戶端對Master做提交文件的請求,告訴Master寫了這些數據塊,寫到Worker,然後更新對應的元數據,也知道了這些數據塊和Worker所對應的位置。
在這樣的流程之後,Alluxio不會向UFS創建這個文件,也不會寫這個文件,所以Alluxio和UFS之間的元數據和數據都不一緻。
3. Through寫模式
圖9 Alluxio:Through寫模式
THROUGH的寫模式有所不同,這裡同樣的 createfile() 發出一個請求,然後找Worker寫數據。Worker會做三件事:
在第二步結束後,客戶端會向Alluxio 提交這個文件。因為Alluxio 的提交是發生在文件寫完了之後,所以,Alluxio和UFS此時的元數據是一緻的。因為沒有數據緩存,所以也不存在數據一緻性的問題。
Alluxio的緩存是在需要讀之後才會産生,而這種THROUGH模式是比較适合用來寫已知不再會被Alluxio讀取的數據。所以在這種情況下,元數據是一緻的,也不存在數據不一緻的問題。
4. CACHE_THROUGH寫模式
下面的CACHE_THROUGH模式就是前面兩種模式的結合。
圖10 Alluxio:CATCH_THROUGH寫模式
唯一的不同點是在第二步,寫緩存的同時又寫了UFS。在這兩個都成功之後,第二步才會成功,之後客戶端才會做提交操作。同樣的道理,因為Alluxio在UFS更新之後才更新,所以兩者的元數據和數據都是一緻的。
5. ASYNC_THROUGH寫模式
最後是ASYNC_THROUGH異步寫模式,和前面的模式唯一的區别是第二步中的UFS寫變成了異步,放在了第四步。
圖11 Alluxio:ASYNC_THROUGH寫模式
在Alluxio寫緩存之後,首先創建了文件之後,在第二步寫了Alluxio緩存;在第二步緩存寫完之後,Worker就向客戶端返回成功;然後由客戶端向Master提交文件。注意在這個時候,Worker還沒有去UFS創建這個文件,也沒有向UFS寫文件。在Alluxio向客戶端返回請求成功之後,在之後的某個時間,由Job Service把這個文件創建到時裡面,并且持久化。
需要注意:在異步的模式下,持久化由于某些原因失敗了,比如Alluxio成功之後,突然有人直接向裡面創建了一個同名的文件,在第四步的時候,由于緩存和之間産生了不一緻,導緻這個文件無法創建、無法寫入。這個時候,Alluxio會有不一緻的問題,此時需要人工介入來解決這個沖突。
6. 讀文件流程
前文介紹以上四種不同的寫模式以及一緻性保證,現在來看Alluxio的讀文件流程。讀文件也可以粗略分成兩種:冷讀和熱讀。
圖12 Alluxio:讀文件流程
簡單來說,冷讀情況下Alluxio不知道這個文件,需要從加載元數據和數據。熱讀的時候,緩存命中,不需要加載元數據和數據。
① 冷讀文件
圖13 Alluxio:冷讀文件
在冷讀流程裡,客戶端向Alluxio請求元數據,此時Master還沒有這個元數據,所以會向UFS發出一個請求,并從UFS加載這個元數據,這也稱之為元數據同步。
在客戶端具體讀數據的時候,客戶端找到Worker,Worker此時還沒有緩存,于是Worker會向UFS做緩存的加載,這就是常說的緩存冷讀。在做完這兩個步驟之後,緩存是和元數據一緻的。
② 熱讀文件
圖14 Alluxio:熱讀文件
在熱讀的情況下,元數據可以在緩存裡面找到,數據可以在Worker裡面找到;此時不會有對UFS的讀請求。
在緩存命中的時候,如何保證緩存與和是一緻的?這裡包括元數據的一緻和數據的一緻。這個簡單的來說,就是通過Alluxio的元數據和數據的同步機制,也就是下一部分的内容。
--
04
Alluxio和UFS元數據和數據同步
1. 檢查Alluxio元數據/數據一緻性
首先考慮這個問題:在什麼時候需要檢查Alluxio的元數據和數據的一緻性?
首先在寫數據的時候需要檢查。如果這個文件在已經存在了,作為緩存,除了放棄這個操作之外,也沒有其他的選項。因為不能在用戶不知情的情況下,覆蓋掉用戶在裡面的數據。
在讀數據的時候,同樣需要考慮如果文件在裡面已經更新了,那緩存也需要對應進行更新,需要UFS考慮裡面的文件是否發生了變化。
圖15 檢查Alluxio:元數據/數據一緻性
2. 保證Alluxio元數據/數據一緻性
元數據和數據的一緻性分成兩步來逐個讨論。首先讨論如何保證Alluxio元數據和一緻。Alluxio通過兩種方式來保證:
圖16 保證Alluxio:元數據/數據一緻性
① 通過基于時間的假設
第一種是通過基于時間的假設,在裡面的文件,在一段時間内它是不變的。判斷方法是在每一次文件源信息、元數據請求的時候,檢查Alluxio的元數據是否足夠新。
這個判斷分成了兩個不同的部分:
這種元數據的同步機制是惰性的,隻有在請求的時候才會進行檢查。這樣設計的理念是盡量避免訪問慢的操作、昂貴的操作。這樣的事情越少越好、越懶越好。如果Alluxio所知的文件信息足夠新,就假設Alluxio和UFS是一緻的。如果不夠新,就放棄這個假設再做一次同步;同時更新Alluxio裡面的元數據。
② 基于通知
另外一個思路就是抛棄基于時間的假設,基于通知,依賴文件更改的告知。這個不是假設,是一個确定的信息。如果沒有通知文件有變化,就确定Alluxio和現在的文件是一緻的。
其次是如何保證Alluxio和UFS的數據保持一緻。思路也非常簡單:保持數據的一緻,隻需要确定元數據及是否一緻。這裡做出的假設是:如果UFS的數據,文件内容有所改變,那這個改變一定會反映在文件的元數據上。要麼是文件的長度改變,要麼是這個文件的Hash,也就是哈希值會發生改變。通過觀察這個Alluxio和的元數據,可以發現這些變化點。
如果基于這個假設,Alluxio的元數據和UFS保持一緻時緩存和UFS也會一緻。如果觀察元數據發現内容有變化,那麼就更新元數據并抛棄已有緩存。在下一次讀的時候,重新加載緩存。如果發現文件的内容沒有變化,隻做必要的元數據更新,不抛棄數據緩存。
3. 數據同步機制
Alluxio提供兩種同步機制,這裡先介紹時間戳機制,再介紹基于消息的同步機制。
① 基于元數據時間戳的同步機制
下面先看一下第一種機制,基于元數據時間戳的同步機制。
圖17 基于元數據時間戳同步
時間戳主要是通過配置項alluxio.user.file.metadata.sync.interval,通常稱之為sync.interval或者interval。比較同步數據上次同步的時間戳和配置項。配置項中有幾種不同的配置方式:
圖18 元數據同步的開銷
② 同步時間間隔配置
圖19 同步時間間隔配置
這個時間間隔具體配置有三個,優先級是由低到高,後面的配置可以覆蓋前面的配置。
Alluxio還提供一些語法糖指令,比如:loadMetadata指令就是專門為了觸發元數據同步。如果加上-F選項,實際上的意思就是把sync.interval設成0,相當于強制進行一次元數據的刷新。ls和metadata這兩個指令的區别:ls把文件展示在面前,ls的RPC有網絡開銷, 會把信息發給你,客戶端要保存下來,并且展示出來。這個不是每一次都需要,假如隻是想要觸發一次元數據的同步,隻需loadMetadata就可以,返回值隻有成功或者失敗,可以節約很多網絡帶寬和的内存開銷。
③ 基于消息的同步機制
以上是基于時間的同步機制,下面看一下另外一種思路,就是基于消息同步機制。
圖20 基于消息同步
首先需要Alluxio 2.0版本以上,以及Hadoop 2.6.1版本以上,因為HDFS底層的inotify機制是在2.6版本加進去的。實際上發生的就是從HDFS的namenode直接讀取HDFS有哪些文件發生了變化。實現原理就是維護了一個Alluxio和namenode之間通過HDFS的inotify機制保持了一個信息流。Alluxio定期心跳從這個信息流裡面去讀,有哪些文件發生了變化,然後根據這個變化的具體的類型,Alluxio決定要不要再去namenode觸發一次元數據的同步。
這樣的元數據同步是有的放矢,不再是基于時間猜測。每一次同步都是有理有據,知道文件已經改變,稱為Active Sync,也是因為在這裡化被動為主動,不再被動去猜,而是主動知道了有哪些變化,然後主動去觸發同步。但是這個inotify隻能告訴告訴我們哪些文件發生了變化以及變化是什麼類型,包含的信息很少,具體要做同步還需要一個元數據同步的機制,所以這一步是繞不開的。具體的使用也非常簡單,可以通過指令來開啟它,也可以關閉,或者查看現在有哪些HDFS路徑或挂載點開啟了這個功能,這些指令也受Journal日志保護,當開啟了Active Sync功能之後,就會被記錄在Journal日志裡面,在重啟集群之後,無需重敲一遍指令。
4. Active Sync性能取舍
Active Sync也不是萬能藥,它做了一些功能的取舍。每種設計都有它的取舍,因此也有适合和不适合的場景。
圖21 Active Sync性能取舍
Active Sync在确定了文件更改之後,再去做同步操作,它省掉了那些沒有變化、無用的同步操作。但是每一個文件的更改,都會觸發同步操作。具體文件的更改并不一定是客戶端的請求,雖然是主動加載它,但實際上并不一定用到,有可能是多餘的操作。
Active Sync和基于時間戳的同步機制,各有利弊。具體選擇時需要進行考量,在後面的章節會總結分析哪種場景适合的配置。同時要注意Active Sync隻支持HDFS,原因是隻有HDFS提供API,其他存儲沒有機制可以知道有哪些文件發生了變化,所以沒有辦法來實現。
--
05
元數據同步實現與優化
在了解了機制後,現在了解一下元數據同步的實現原理,然後再看元數據同步的優化。
1. 元數據同步原理
目前元數據的同步粗略分為左下角的這幾個步驟。左上角我們列出了元數據同步的參與者。包括了RPC線程,就是Master端,來自于RPC的線程池。第2個參與者稱作同步線程池(sync thread pool)。第3個參與者稱作預取線程池(prefetch thread pool)。這裡有一個InodeTree,就是Alluxio裡面維護的所有文件的元數據,形成了一個樹狀的結構。圖中的UFS代表着實際的底層存儲。所有對UFS的連線都是一個對外部系統的RPC,是比較昂貴的操作。
圖22 元數據同步原理
同步步驟分三步:
① RPC線程向UFS讀取文件的元信息。在處理請求ls -R 時,對根節點全量的文件樹進行一次ls操作。此時基于時間戳,需要觸發元數據同步, RPC線程就會讀取文件根節點的信息,檢查挂載HDFS://alluxio根節點的信息。如果發現信息有更新,就去InodeTree裡面找根節點,則RPC線程對應地更新/創建/删除Alluxio的Inode元信息,并且這個節點進行更新。
② 如果在處理遞歸的時候,發現下面還有其他目錄,整棵樹都需要進行一次搜索,此時把遞歸文件/文件夾提交到同步線程池(sync thread pool)。用廣度優先搜索的方式進行遍曆,也就是常說的BFS。當提交這個任務後。會去同步線程池,在BFS的過程中,按照一定的順序遍曆這棵樹,data -> user -> others -> report …
在這個過程中,主要是做兩件事情:
就把任務提前交給預取線程池,告訴它去讀HDFS裡面相關的這些路徑,這也就是稱為預取的原因。在真正處理這個路徑之前,就提前告訴它,因為操作會需要很長的時間,保證性能最好的方式就是提前做這個事情;等任務處理到的時候,已經準備好了。這樣最高地利用了多線程的并發。
在處理每個節點的時候,确認預取線程池準備好的結果;如果有更新,就更新自己的InodeTree。
③ 預取線程池負責從讀取文件或者文件夾信息,把結果交給這個同步線程池,來加速這個具體的同步過程。
2. 性能優化-緩存
在這個時間原理的基礎上,進行了一系列的優化。
圖23 性能優化-緩存
首先就是做了緩存的優化。主要的設計理念就是因為UFS操作非常貴、非常慢,希望盡可能多的地緩存結果,節省的操作。
緩存優化涉及下面這幾種緩存:
3. 算法優化-鎖優化
在緩存優化之外,基于Alluxio的版本叠代,也做了各種各樣的元數據同步優化,其中比較大的就是算法的優化。
圖24 算法優化-鎖優化
在Alluxio2.3版本之前,所有的RPC線程是自己進行元數據的同步。如果遞歸基于DFS自己遍曆這棵樹,從頭到尾都持有路徑的寫鎖。如果是遞歸,實際上是寫鎖鎖住了整個子樹。在鎖住子樹的過程中,其他線程就無法讀取子樹裡的内容,并發度低,由于是單線程,做了非常多需要Block時間長的操作。
在Alluxio2.3裡面非常大的優化,就是把它改成了多線程并發同步的算法,更多利用線程池操作,DFS改成了BFS。根據需要,讀鎖升級,而非從頭至尾持有寫鎖。讀寫鎖的好處就是讓不同的線程之間有了更高的并發度。這樣在同步某一棵子樹的過程中,其他線程還有機會可以讀到這裡面的内容,所以整體的并發會更高。
4. 性能優化-調整并行度
第三個優化來自于用戶對線程池的配置。
圖25 性能優化-并行度
在Alluxio 2.3裡面加入了線程池之後,可以通過配置參數調整元數據同步的并行度。之前單線程沒有配置可言。調整并行度一般是通過這三個參數。
兩個線城池的大小,建議配置成CPU數的倍數,具體比例以及系統應該配多大,取決于這兩個線程所做工作的時間,以及中間的配合方式。所以沒有給出統一建議,建議根據具體程序運行的時候,CPU比如可以做一些火焰圖、分析延遲以及inodetree更新的延遲,做一個更加合理的配比。默認的配比都是CPU的倍數,如果沒有在無數據同步看到非常明顯的瓶頸,不需要進行特别細粒度的調節。
--
06
對不同場景的推薦配置
最後針對不同的場景具體分析問題,推薦比較好的配置。
1. 場景1
圖26 場景一配置
最簡單的場景就是:所有的寫和讀全都經過了Alluxio。無疑Alluxio所有的元數據都是最新,此時無需做元數據同步。可以關閉元數據同步,提升性能。
2. 場景2
第二個場景稍微複雜一些。大部分的操作經過Alluxio,但是不排除還有一些場景,會繞過Alluxio直接改動原來裡面的文件。
圖27 場景二配置
在這種場景下,為了保證Alluxio和UFS元數據和數據的一緻,還是要保留一部分元數據同步的操作。在這裡分HDFS和非HDFS進行考慮,主要是因為這些HDFS比别人多一個選項Active Sync。
HDFS的情況下,需要考慮這幾個點:如果HDFS更新非常頻繁,就是繞過Alluxio的更新非常頻繁,或者HDFS的namenode的壓力比較大,不建議使用Active Sync,而是使用基于時間的被動同步。使用被動同步的原因是Active Sync是基于Alluxio心跳,去namenode拉取具體有哪些文件發生了改動,每一次拉取都是一次RPC,給namenode施加的壓力。同時如果文件發生了改動,Alluxio就又會向namenode發起RPC去同步這些文件。因此希望盡量減少這些操作來降低namenode的壓力。
如果基于時間的被動同步,能給予減少元數據同步操作的機會,則使用基于時間的被動同步;反之要使用Active Sync,因為更加主動,時效性更高。如果不會帶來性能負擔,完全可以去嘗試一下。
如果不是HDFS,隻能用基于時間的被動同步。
建議盡量節約元數據同步的操作,具體做法考慮每個數據、每個路徑更新的頻率大概是如何。基于這樣的考量,盡量給sync interval設一個比較大的值,盡量減少元數據的觸發。
3. 場景3
第三個場景稍微有些不同。大部分或全部更新都不經過Alluxio,而更新又非常頻繁。
圖28 場景三配置
在這種情況下,使用HDFS而且時效性又非常重要,使用Active Sync是唯一方案。因為如果把sync interval調到一個特别低的值,甚至可能觸發比Active Sync更多的同步。如果把它設成零之後,每一次都不會觸發同步,會進一步加大同步的操作開銷。
如果時效性并不十分重要,建議使用這個基于時間的被動同步。通過節省開銷,減少元數據同步的操作,就能提升系統的性能。同樣如果不是HDFS,則使用基于時間的被動同步。
以上是三個場景的分析,也是今天分享的全部内容。
--
07
答疑
Q:在ASYNC_THROUGH mode當中,因UFS原因導緻ASYNC上傳始終失敗,這種情況怎麼優雅地處理,以避免不一緻?
A:這個問題非常好,問到了痛點。目前沒有一個特别優雅的解決方案。
在異步方式下沒有call back,或者hook的自動介入,隻能人為介入。Alluxio2.4商業版中加入了一個job service dashboard查看異步任務的狀态。如果異步持久化失敗了,這個地方可以看到所有失敗的異步任務。目前隻能通過這個方式觀察究竟有哪些東西失敗了,然後再人為進行幹預。
我們已經注意到job service的問題,打算重新設計整個job service,從幾個方向,可擴展Scalability, Fault-tolerance,包括monitoring, failure mode, 恢複等。未來版本裡面會做出更好的設計,有很多場景,并加入人工幹預模式。通過開放一些接口,可以自動化地讀到這些東西,或是可能會增加界面,讓用戶定義一些方式。
Alluxio到了一個新的階段,要好好思考,用更加scalable的方案去解決這些問題。這些異步的操作已經提上了議事日程。
Q:和HDFS可以不同路徑設置不同的同步時間嗎?
A:可以用配置參數的方法去實現。
今天的分享就到這裡,謝謝大家。
閱讀更多技術幹貨文章、下載講師PPT,請關注微信公衆号“DataFunTalk”。
分享嘉賓:劉嘉承 Alluxio 核心組研發工程師
編輯整理:曾新宇 對外經貿大學
出品平台:DataFunTalk
分享嘉賓:
關于我們:
DataFun:專注于大數據、人工智能技術應用的分享與交流。發起于2017年,在北京、上海、深圳、杭州等城市舉辦超過100 線下和100 線上沙龍、論壇及峰會,已邀請超過2000位專家和學者參與分享。其公衆号 DataFunTalk 累計生産原創文章700 ,百萬 閱讀,14萬 精準粉絲。
歡迎轉載分享評論,轉載請私信。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!