對hadoop RPC的理解

因爲公司hadoop集羣出現了一些瓶頸,在機器不增加的情況下需要進行優化,不管是存儲還是處理性能,更合理的利用現有集羣的資源,所以來學習了一波hadoop的rpc相關的知識和hdfs方面的知識,以及yarn相關的優化,學完之後確實明白了可以在哪些方面進行優化,可以對哪些參數進行調整,有點恍然大悟的感覺,本文的大部分的內容來於《Hadoop 2.x HDFS源碼剖析》,自認爲這本書寫的挺好,確實能學到很多東西,看了本篇博客如果不懂,還是可以繼續學習這本書,講的很詳細,很清晰。本篇文章主要從RPC的原理、hdfs通信協議和Hadoop RPC的實現這三部分進行闡述。

一、RPC原理

1.1、RPC框架

  RPC(Pemote Procedure CallProtocol)是一種通過網絡調用遠程計算機服務協議,RPC協議假定存在某些網絡傳輸協議,如TCP,UDP,並通過這些傳輸協議爲通信程序之間傳遞訪問請求或者應答信息。在OSI網絡通信模型中,RPC跨越了傳輸層和應用層。RPC 的主要功能目標是讓構建分佈式計算(應用)更容易,在提供強大的遠程調用能力時不損失本地調用的語義簡潔性。爲實現該目標,RPC 框架需提供一種透明調用機制讓使用者不必顯式的區分本地調用和遠程調用。

  RPC通常採用客戶機/服務器模型。請求程序是一個客戶機,而服務提供程序則是一個服務器。客戶端首先會發送一個有參數的調用請求到服務端,等待服務端的響應消息,在服務端,服務提供程序會保持睡眠狀態直到有調用請求到達爲止,當接收到請求,服務端會對請求就行調用,計算結果,最後返回給客戶端。RPC的框架圖如下圖所示:

  上圖所示,RPC主要包括如下幾個部分:

1、通信模塊:傳輸RPC請求和響應的網絡通信模塊,可以基於TCP協議,也可以基於UDP協議,它們在客戶和服務器之間傳遞請求和應答消息,一般不會對數據包進行任何處理。請求–應答協議的實現方式有同步方式和異步方式兩種,如下圖所示。

2、客戶端的stub程序:在客戶端stub程序表現爲像本地程序一樣,但底層會將調用參數和請求序列化並通過通信模塊發送給服務器。之後stub會等待服務器的響應信息,並將響應信息反序列化給請求程序。
3、服務端stub程序:stub程序會將客戶端發送的調用請求和參數反序列化,根據調用信息觸發對應的服務程序,然後將服務程序的響應信息徐麗華併發回給客戶端。
4、請求程序:請求程序像本地調用方法一樣調用客戶端stub程序,然後接收stub程序的返回響應信息。
5、服務程序:服務端會接收來自stub的調用請求,執行對應的邏輯並返回執行結果。

1.2、RPC特點

  1. 透明性:遠程調用其他機器上的程序,對用戶來說就像調用本地方法一樣。
  2. 高性能:RPC Server能夠併發處理多個來自Client的請求。
  3. 可控性:JDK中已經提供了一個RPC框架——RMI,但是該PRC框架過於重量級並且可控之處比較少,所以Hadoop RPC實現了自定義的RPC框架。

1.3、RPC請求基本步驟

  1. 客戶程序以本地方式調用系統產生的客戶端的Stub程序;
  2. 該Stub程序將函數調用信息序列化並按照網絡通信模塊的要求封裝成消息包,並交給通信模塊發送到遠程服務器端。
  3. 遠程服務器端接收此消息後,將此消息發送給相應的服務端的Stub程序;
  4. Stub程序拆封消息,並反序列化,形成被調過程要求的形式,並調用對應函數;
  5. 被調用函數按照所獲參數執行,並將結果返回給服務端的Stub程序;
  6. Stub程序將此結果封裝成消息,通過網絡通信模塊逐級地傳送給客戶程序。

二、HDFS的通信協議

HDFS通信協議抽象了HDFS各個節點之間的調用接口,主要分爲hadoop RPC接口和流式接口

2.1、hadoop RPC接口

1、ClientProtoclo

  該接口定義客戶端與namenode節點間的接口,用於客戶端對文件系統的所有操作,讀寫都需要與該接口交互。
  該接口定義了由客戶端發起,namenode響應的操作,主要包括hdfs文件讀寫的相關操作,hdfs命名空間,快照,緩存相關的操作。
  (客戶端與Namenode交互)一般性操作,客戶端會通過getBlockLocations()方法向Namenode獲取文件的具體位置信息(指的是存儲這個數據塊副本的所有datanode的信息),還會使用reportBadBlocks()方式向Namenode彙報錯誤的數據塊。
  (寫,追寫數據)首先會使用create()方法通過hdfs文件系統目錄樹中創建一個新的空文件,然後調用addBlock()方法獲取存儲文件的數據塊的位置信息,最後客戶端根據位置信息與datanode建立數據流管道,寫入數據。追寫略有不同,首先通過append()方法獲取最後一個可寫數據塊的位置信息並打開一個已有的文件(沒有寫滿的),然後建立好數據管道流,並向節點中追寫數據,當寫滿後,則會像create()方法一樣,客戶端會調用addBlock()方法獲取新的數據塊。當客戶端完成了整個文件的寫入操作後,會調用complete()方法通知Namenode,這個操作會提交新寫入的HDFS文件的所有數據塊,當數據塊滿則副本數時,則返回true,否則返回false;會重試。

  如上是順利完成,如果在客戶端獲取到一個新申請的數據塊時,無法建立連接,會調用abandonBlock()方法放棄喝個數據塊,客戶端會再次通過addBlock()方法獲取新的數據塊。
  在客戶端寫某個數據塊時,如果副本節點出了錯誤,客戶端會調用getAdditionalDatanode()方法向Namenode申請一個新的datanode來替代故障datanode。然後客戶端調用updateBlockForPipeline()方法向Namenode申請爲這個數據塊分配新的時間戳,這樣故障節點的數據塊的時間戳就會過期,回本刪除,最後客戶端就可以使用新的時間戳建立新的數據管道流近些寫數據了。
如果在寫的過程中客戶端發生了故障,爲了防止故障,對於任意的一個client打開的文件都需要client定期調用clientProtocol.renewLease()方法更新租約,如果Namenode長時間沒有收到client的租約更新信息,就會認爲client故障,觸發一次租約恢復操作,關閉文件並且同步所有數據節點上這個文件數據塊的狀態,確保hdfs系統中這個文件正確且保持一致。
  如果在寫數據的過程中Namenode發生故障呢,則需要HA發揮作用了。

2、ClientDatanodeProtocol

  客戶端與datanode間的接口,主要用戶客戶端獲取datanode節點信息是調用,真正的讀寫是通過流式接口進行的。其中主要定義兩部分,一部分是支持HDFS文件讀寫的操作,例如調用getReplicaVisibleLength()獲取datanode節點某個數據塊的真實數據長度和getBlockLocalPathInfo()方法等,另一部分是支持DFSAdmin中與datanode節點管理相關的命令。

3、DatanodeProtocol

  datanode與namenode間的通信接口,包括namenode通過該接口中的方法返回向datanode下發指令。datanode則是通過該接口向namenode進行註冊,彙報塊信息和緩存信息。DataNode使用這個接口與Namenode握手、註冊、發送心跳、進行全量以及增量的數據塊彙報,NameNode會在Datanode的心跳響應中攜帶Namenode的指令。該接口主要的方法分爲三種類型,Datanode的啓動相關,心跳相關,數據塊的讀寫相關。
  (Datanode啓動相關)Datanode啓動會與NameNode進行4次交互,通過versionRequest()與NameNode進行握手操作,然後調用refisterDatanode()向NameNode註冊當前的datanode,接着調用blockReport()彙報datanode上存儲的所有數據塊信息,最後調用cacheReport()彙報datanode緩存的所有數據塊。
握手主要是返回namenode的一些命名空間ID,集羣ID,hdfs的版本號等,datanode收到信息後進行校驗對比,判斷是否能夠與該namenode協同工作,能否註冊。
  塊彙報後會根據datanode上報數據塊存儲情況建立數據塊與datanode節點的對應關係。blockReport()在啓動的時候和指定時間間隔的情況下發生。cacheReport()和blockReport()方法完全一致。只不過是彙報當前datanode上的緩存的所有數據塊。
  (心跳相關)datanode會定期的向namenode發送心跳(dfs.heartbeat.interval=3s),如果namenode很長時間沒有收到datanode的心跳信息,則認爲該datanode失效。每次心跳都會包含datanode節點上的存儲的狀態,緩存狀態,正在寫文件數據的連接數,讀寫數據使用的線程等。在開啓了HA的狀態下,datanode需要向兩個namenode同時發送心跳信息,不過只有active纔會向datanode發送指令。
  (數據塊讀寫相關)datanode會向namenode彙報損壞的數據塊,以及定期性namenode彙報datanode新接手的數據塊或者刪除的數據塊。

4、InterDatanodeProtocol

  datanode和datanode間的接口,主要用於數據塊的恢復操作,以及同步datanode節點上存儲數據塊副本的信息。主要用於租約恢復操作。
客戶端打開一個文件進行操作時,首先要獲取這個文件的租約,並且還需要定期更新這個租約,不然,namenode則會認爲該client異常,namenode就會觸發租約恢復操作(同步數據管道中所有datanode上該文件數據庫的狀態,並強制關閉這個文件)。
  租約恢復不是由namenode控制的負責的,而是namenode在數據管道中選擇出一個datanode的恢復主節點,然後下發恢復指令觸發這個數據節點控制租約恢復操作,也就是有這個恢復主節點協調整個租約恢復操作的的過程。租約恢復操作就是將數據管道中所有的datanode節點保存同一的數據塊狀態(時間戳和數據塊長度)同步一致。

5、NamenodePortocol

  主要用於namenode的ha機制,或者單節點的情況下是secondaryNamenode也namenode之間的通信接口

2.2、流式接口

  流式接口是HDFS中基於TCP或者HTTP實現的接口,在HDFS中,流式接口包括基於TCP的DataTransferProtocol接口,以及HA架構中Active Namenode和Standby Namenode之間的HTTP接口。

1、DataTransferProtocol

DataTransferProtocol是用來描述寫入或者讀出Datanode上數據的基於TCP的流式接口,HDFS客戶端與數據節點以及數據節點與數據節點之間的數據塊的傳輸就是基於DataTransferProtocol接口實現的。

2、Active Namenode和Standby Namenode間的HTTP接口

  Namenode會定期將文件系統的命名空間保存在一個fsimage文件中,以及會將Namenode的命名空間的修改操作先寫入到editlog文件中,定期的合併fsimage和editlog文件。這個合併操作由Secondary Namenode或者Standby Namenode去實現,合併完之後又要同步給Active Namenode,在Active Namenode和Standby Namenode之間的HTTP接口就是用來傳輸的fsimage文件的。

二、Hadoop RPC的實現

  Hadoop作爲一個分佈式的存儲系統,各個節點之間的通信和交互是必不可少的,所以在hadoop有一套節點之間的通信交互機制。RPC(Pemote Procedure CallProtocol,遠程過程調用協議)允許本地程序像調用本地方法一樣調用遠程機器上的應用程序提供服務,Hadop RPC機制是基於IPC實現的,主要用到了java的動態代理,java NIO以及protobuf等基礎技術(沒有基於java的RMI)。

  Hadoop RPC框架主要由三個類組成:RPC、Client和Server類,RPC類用於對外提供一個使用Hadoop RPC框架的接口,Client類用於實現客戶端功能,Server類用於實現服務端功能。

3.1、RPC類的實現

  客戶端調用程序可以通過RPC類提供的waitForProxy()和getProxy()方法獲取指定RPC協議的代理對象,然後RPC客戶端就可以調用代理對象的方法發送RPC請求到服務器了。
  在服務端,服務端程序調用RPC內部的Builder.build()方法構造一個RPC.Server類,然後調用RPC.server.start()方法啓動Server對象監聽並響應RPC請求。

3.2、Client類的實現

  HDFS Client會獲取一個ClientProtocolPB協議的代理對象,並在這個代理對象上調用RPC方法,代理對象會調用RPC.Client.call()方法將序列化之後的RPC請求發送到服務器。

  Client發送請求與接收響應的流程圖如下所示:Client類只有一個入口,即call()方法,代理類會調用Client.call()方法將RPC請求發送到遠程服務器,然後等待遠程服務器的響應。

 Client發送請求與接收響應的流程圖如上所示:主要可以分爲如下幾個步驟:

  1. Client.call()方法將RPC請求封裝成一個Call對象,其中保存了RPC調用的完成標誌,返回值信息以及異常信息,然後call()方法會創建一個connection對象,用於管理client和server的socket連接。用ConnectionId作爲key,將新建的Connection對象放入到Client.connections字段值保存,以callId作爲key,將構造的Call對象放入Connection.calls字段中保存。
  2. Client.call()方法調用Connection.setupIOstreams()方法建立與server的socket連接,setupIOstreams()方法還會啓動connection線程,這些線程會監聽socket並讀取server發回的響應信息。
  3. Client.call()方法調用Connection.sendRpcRequest()方法發送RPC請求到Server。
  4. Client.call()方法調用Call.wait()在Call對象上等待,等待server返回響應信息。
  5. Connection線程收到Server發回的響應信息,根據響應中的信息找到對應的call對象,然後設置Call對象的返回字段,並調用call.notify()喚醒Client.call()方法的線程讀取Call對象的返回值。

RPC.Client中發送請求和接收響應的是由兩個獨立的線程進行的,發送請求線程就是調用Client.call()方法的線程,而接收響應線程則是call()啓動的connect線程。

內部類Connection是一個線程類,提供建立Client到Server的Socket連接,發送RPC請求以及讀取RPC響應信息等功能。Client與每個Server之間維護一個通信連接,與該連接相關的基本信息及操作被封裝到Connection類中,基本信息主要包括通信連接唯一標識(remoteId)、與Server端通信的Socket(socket)、網絡輸入數據流(in)、網絡輸出數據流(out)、保存RPC請求的哈希表(calls)等。

Call類封裝了一個RPC請求,它包含5個成員變量,分別是唯一標識id、函數調用信息param、函數執行返回值value、出錯或者異常信息error和執行完成標識符done。由於Hadoop RPC Server採用異步方式處理客戶端請求,這使遠程過程調用的發生順序與結果返回順序無直接關係,而Client端正是通過id識別不同的函數調用的。當客戶端向服務器端發送請求時,只需填充id和param兩個變量,而剩下的3個變量(value、error和done)則由服務器端根據函數執行情況填充。

3.3、Server類的實現

爲了提高性能,Server類採用了很多技術提高併發能力,包括線程池,javaNIO提供的Reactor模式等。爲了更好的理解Server類的設計,我們一步一步的推進:

3.3.1、Reactor模式

  RPC服務端的處理流程和所有網絡程序服務端處理的流程類似:1、讀取請求;2、反序列化請求;3、處理請求;4、序列化響應;5、發回響應。

  Reactor模式是一種廣泛應用在服務器端的設計模式,也是一種基於事件驅動的設計模式;Reactor的處理流程是:應用程序向一箇中間人註冊IO事件,當中間人監聽到這個IO時間發生後,會通知並喚醒應用程序處理這個事件,這裏所說的中間人其實是一個不斷等待和循環的線程,它接收所以的應用程序的註冊,並肩擦應用程序註冊的IO事件是否就緒,如果就緒了則通知應用程序進行處理。

  一個簡單的基於Reactor模式的網絡服務器設計如下圖所示:主要包括reactor、acceptor以及hadndler等模塊,其中reactor負責監聽所有的IO事件,當檢測到一個新的IO事件發生時,reactor就睡喚醒這個事件對應的模塊處理。acceptor負責響應socket連接請求事件,會接收請求建立連接,之後構造handler對象,handler負責向reactor註冊IO讀事件,然後進行對應的業務邏輯處理,最後發回響應。

  主要的步驟如下:

  1. 客戶端發送socket連接請求到服務端,服務端的reactor對象監聽到了這個IO請求,由於acceptor對象在reactor對象上註冊了socket連接請求的iO事件,所以reactor會出發acceptor對象響應socket連接請求。
  2. acceptor對象會接收到來自客戶端的socket連接請求,併爲這個連接創建一個handler對象,handler對象的構造方法在reactor對象上註冊IO讀事件。
  3. 客戶端建立連接後,會通過socket發送RPC請求,RPC請求達到reactor後,會有reactor對象分發到對應的handler對象處理。
  4. handler對象會從網絡上讀取RPC請求,然後反序列化請求並執行請求對應的邏輯,最後將響應信息序列化並通過socket發回給客戶端。

由於上述的設計中服務端只有一個線程,所以就要求handler中讀取請求、執行請求以及發送響應的流程必須能夠迅速處理完成,如果在一個環節中發生了阻塞,則整個服務器邏輯全部阻塞。所以接下來看多線程的Reactor模式的網絡服務器結構。

  3.3.2、多線程的Reactor模式

  在基礎的Reactor模式的基礎上,把佔用時間比較長的讀取請求部分也業務邏輯處理部分進行分開,交給兩個獨立的線程池處理,分別爲readers的線程池和handler的線程池。readers線程池中包含若干個執行讀取RPC請求任務的Reader線程。它們會在Reactor上註冊讀取RPC請求IO事件,然後從網絡中讀取RPC請求,並將RPC請求封裝在一個Call對象中,最後將Call對象放入共享消息隊列MQ中。而handers線程池包含很多個handler線程,它們會不斷的從共享消息隊列MQ中取出RPC請求,然後執行業務邏輯並向客戶端發送響應。這樣就保證了IO事件的監聽和分發,RPC請求的讀取和響應是在不同的線程中執行,大大提高了服務器的併發性能。具體的架構圖如下:

 上圖就是多線程的Reactor模式版本,IO事件的監聽、RPC請求的讀取和處理就可以併發的進行了,但是像hadoop的Namenode這樣的對象,同一時間會存在很多個socket連接請求以及RPC請求的道道,這樣就會造成Reactor在處理和分發這些IO事件時出現阻塞,導致服務器性能下降,在這個的基礎上可以拓展爲多個reactor的模式。

3.3.3、多個Reactor多線程模式

多個Reactor多線程模式結構如下圖所示:

      這裏mainReactor負責監聽socket連接事件,readReactor負責監聽IO讀事件,respondSelector負責監聽IO寫事件,這裏會構造多個readReactor降低系統的負載,不同的Reader線程會根據一定的邏輯到不同的readReactor上註冊IO讀事件。當acceptor建立了socket連接後,會從readers線程池中取出一個reader線程去出發RPC請求的流程。Reader線程會根據一定的邏輯選出一個readReactor對象並在這個對象上註冊讀取RPC請求的IO事件。之後就會由該readReactor在網絡監聽是否有RPC請求到達,並出發Reader線程讀取,當handler成功處理一個RPC請求後,就會向respondSelector註冊寫RPC響應IO事件,當socket輸出流管道可以寫數據時,sender類就可以將響應發送個客戶端了。

3.3.4、server類的設計

server類的設計結構如下所示,基本和多個reactor多線程版本的設計模式類似。

  1.  Listener:類似於Reactor模式中的mainReactor,Listener對象中存在一個Selector對象acceptSelector,負責監聽來自客戶端的Socket請求,當acceptSelector監聽到連接請求後,Listener對象會初始化這個連接,之後採用輪詢的方式從readers線程池中選出一個reader線程處理RPC請求的讀取操作。
  2. Reader:與Reactor模式中的Reader線程相同,用於RPC讀取請求,Reader線程類中存在一個Selector對象readSelector,類似Reactor模式中的readReactor,這個對象用於監聽網絡中是否可以讀取的RPC請求。當readSelector堅挺到有可讀的RPC請求後,會喚醒Reader線程讀取這個請求,並將請求封裝在一個Call對象中,然後將這個Call對象放入CallQueue隊列中。
  3. Handler:與Reactor模式中的Handler類似,用於處理RPC請求併發迴響應,Handler對象會從CallQueue中不停的取出RPC請求,然後執行RPC請求對應的本地函數進行處理,最後封裝響應發回給客戶端。
  4. Responder:用於向客戶端發送RPC響應,會在Responder內部的respondSelector上註冊一個寫響應事件,這裏的respondSelector和Reactor中的respondSelector概念相同,當respondSelector堅挺到網絡情況具備寫響應的條件時,會通知Responder將剩餘的響應發回給客戶端

server類處理RPC請求的處理流程:

  1. Listener線程的acceptSelector在ServerSocketChannel上註冊OP_ACCEPT事件,並且創建readers線程池,每個Reader的readSelector此時並不監控任何的Channel。
  2. Client發送socket連接請求,出發Listener的acceptSelector喚醒Listener線程。
  3. Listener調用ServerSocketChannel.accept()創建一個新的SocketChannel
  4. Listener從readers線程池中挑選一個線程,並在Reader的readSelector上註冊OP_READ事件
  5. Client發送RPC請求數據報,出發Reader的selector喚醒Reader線程
  6. Reader從socketChannel中讀取數據,封裝成Call對象,然後放入共享對壘CallQueue中
  7. handlers線程池的線程都在CallQueue上阻塞,當有Call對象被放入後,其中一個Handler線程被喚醒,然後根據Call對象的信息滴哦用BlockingServer對象的callBlockingMethod()方法,然後Handler將響應寫入SocketChannel中。
  8. 如果handler發現無法將響應完全寫入到SocketChannel中,將在Responder的respondSelector上註冊一個OP_WRITE時間,當socket恢復正常,Responder將被喚醒繼續寫響應。

Server類的內部類介紹:

  • Listener類:是一個線程類,整個Server中只會有一個Listener線程,用於監聽來自客戶端的Socket連接請求,對於每一個新到達的socket連接請求,Listener都會從readers線程池中選擇一個Reader線程處理,Listener中定義了一個Selector對象,負責監聽SelectionKey.OP_ACCEPT事件。
  • Reader類:是一個線程類,每個Reader線程都會負責讀取若干個客戶端連接發來的RPC請求,而在Server類中會存在多個Reader線程構成一個readers線程池,readers線程池併發的讀取RPC請求,提高了Server處理RPC請求速度,Reader類定義了自己的readSelector字段,用於箭筒SelectionKey.OP_READ事件。
  • Connection類:維護Server和Client之間的Socket連接,Reader線程會調用readAndProcess()方法從IO流中讀取一個RPC請求
  • Handler類:一個線程類,負責執行RPC請求對應的本地函數,然後將結果返回給客戶端。Handler線程類的主方法會循環從共享隊列CallQueue中取出待處理的Call對象,然後調用Server.call()方法執行RPC調用對應的本地函數。
  • Responder類:一個線程類,一個Server中只有一個Responder對象,Responder內部包含一個Selector對象responseSelector,用於監聽SelectionKey.OP_WRITE事件。responseSelector會循環監控網絡環境中是否具備發送數據的條件,之後responseSelector會觸發Responder線程發送未完成的響應結果到客戶端。

3.4、基於RPC的優化

知道了RPC的原理後,下面的優化自然而然就懂了。

  • Handler線程數目。在Hadoop中,ResourceManager和NameNode分別是YARN和HDFS兩個子系統中的RPC Server,其對應的Handler數目分別由參數yarn.resourcemanager.resource-tracker.client.thread-count和dfs.namenode.service.handler.count指定,默認值分別爲50和10,當集羣規模較大時,這兩個參數值會大大影響系統性能
  • 客戶端最大重試次數。在分佈式環境下,因網絡故障或者其他原因迫使客戶端重試連接是很常見的,但嘗試次數過多可能不利於對實時性要求較高的應用。客戶端最大重試次數由參數ipc.client.connect.max.retries指定,默認值爲10,也就是會連續嘗試10次(每兩次之間相隔1秒)
  • 每個Handler線程對應的最大Call數目。由參數ipc.server.handler.queue.size指定,默認是100,也就是說,默認情況下,每個Handler線程對應的Call隊列長度爲100。比如,如果Handler數目爲10,則整個Call隊列(即共享隊列callQueue)最大長度爲:100×10=1000
  • ipc.server.listen.queue.size控制了服務端socket的監聽隊列長度,即backlog長度,默認值是128。而Linux的參數net.core.somaxconn默認值同樣爲128。當服務端繁忙時,如NameNode,128是遠遠不夠的。這樣就需要增大backlog,例如3000臺集羣就將ipc.server.listen.queue.size設成了32768,爲了使得整個參數達到預期效果,同樣需要將kernel參數net.core.somaxconn設成一個大於等於32768的值。

 

參考:《Hadoop 2.x HDFS源碼剖析》《Hadoop技術內幕 :深入解析YARN架構與實現原理》

https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-common/SingleCluster.html

https://blog.csdn.net/lemon89/article/details/17354887

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