Redis的Sentinel

Sentinel

Note

本文檔翻譯自: http://redis.io/topics/sentinel

Redis 的 Sentinel 系統用於管理多個 Redis 服務器(instance),該系統執行以下三個任務:

  • 監控(Monitoring): Sentinel 會不斷地檢查你的主服務器和從服務器是否運作正常。
  • 提醒(Notification):當被監控的某個 Redis 服務器出現問題時, Sentinel 可以通過 API 向管理員或者其他應用程序發送通知。
  • 自動故障遷移(Automatic failover):當一個主服務器不能正常工作時, Sentinel 會開始一次自動故障遷移操作,它會將失效主服務器的其中一個從服務器升級爲新的主服務器,並讓失效主服務器的其他從服務器改爲複製新的主服務器;當客戶端試圖連接失效的主服務器時,集羣也會向客戶端返回新主服務器的地址,使得集羣可以使用新主服務器代替失效服務器。

Redis Sentinel 是一個分佈式系統,你可以在一個架構中運行多個 Sentinel 進程(progress),這些進程使用流言協議(gossip protocols)來接收關於主服務器是否下線的信息,並使用投票協議(agreement protocols)來決定是否執行自動故障遷移,以及選擇哪個從服務器作爲新的主服務器。

雖然 Redis Sentinel 釋出爲一個單獨的可執行文件 redis-sentinel ,但實際上它只是一個運行在特殊模式下的 Redis 服務器,你可以在啓動一個普通 Redis 服務器時通過給定 --sentinel 選項來啓動 Redis Sentinel 。

Warning

Redis Sentinel 目前仍在開發中,這個文檔的內容可能隨着 Sentinel 實現的修改而變更。

Redis Sentinel 兼容 Redis 2.4.16 或以上版本,推薦使用 Redis 2.8.0 或以上的版本。

獲取 Sentinel

目前 Sentinel 系統是 Redis 的 unstable 分支的一部分,你必須到 Redis 項目的 Github 頁面 克隆一份 unstable 分值,然後通過編譯來獲得 Sentinel 系統。

Sentinel 程序可以在編譯後的 src 文檔中發現,它是一個命名爲 redis-sentinel 的程序。

你也可以通過下一節介紹的方法,讓 redis-server 程序運行在 Sentinel 模式之下。

另外,一個新版本的 Sentinel 已經包含在了 Redis 2.8.0 版本的釋出文件中。

啓動 Sentinel

對於 redis-sentinel 程序,你可以用以下命令來啓動 Sentinel 系統:

redis-sentinel /path/to/sentinel.conf

對於 redis-server 程序,你可以用以下命令來啓動一個運行在 Sentinel 模式下的 Redis 服務器:

redis-server /path/to/sentinel.conf --sentinel

兩種方法都可以啓動一個 Sentinel 實例。

啓動 Sentinel 實例必須指定相應的配置文件,系統會使用配置文件來保存 Sentinel 的當前狀態,並在 Sentinel 重啓時通過載入配置文件來進行狀態還原。

如果啓動 Sentinel 時沒有指定相應的配置文件,或者指定的配置文件不可寫(not writable),那麼 Sentinel 會拒絕啓動。

配置 Sentinel

Redis 源碼中包含了一個名爲 sentinel.conf 的文件,這個文件是一個帶有詳細註釋的 Sentinel 配置文件示例。

運行一個 Sentinel 所需的最少配置如下所示:

sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

sentinel monitor resque 192.168.1.3 6380 4
sentinel down-after-milliseconds resque 10000
sentinel failover-timeout resque 180000
sentinel parallel-syncs resque 5

第一行配置指示 Sentinel 去監視一個名爲 mymaster 的主服務器,這個主服務器的 IP 地址爲 127.0.0.1 ,端口號爲 6379 ,而將這個主服務器判斷爲失效至少需要 2 個 Sentinel 同意(只要同意 Sentinel 的數量不達標,自動故障遷移就不會執行)。

不過要注意,無論你設置要多少個 Sentinel 同意才能判斷一個服務器失效, 一個 Sentinel 都需要獲得系統中多數(majority) Sentinel 的支持,才能發起一次自動故障遷移, 並預留一個給定的配置紀元(configuration Epoch ,一個配置紀元就是一個新主服務器配置的版本號)。

換句話說, 在只有少數(minority) Sentinel 進程正常運作的情況下, Sentinel 是不能執行自動故障遷移的。

其他選項的基本格式如下:

sentinel <選項的名字> <主服務器的名字> <選項的值>

各個選項的功能如下:

  • down-after-milliseconds 選項指定了 Sentinel 認爲服務器已經斷線所需的毫秒數。

    如果服務器在給定的毫秒數之內,沒有返回 Sentinel 發送的 PING 命令的回覆,或者返回一個錯誤,那麼 Sentinel 將這個服務器標記爲主觀下線(subjectively down,簡稱 SDOWN )。

    不過只有一個 Sentinel 將服務器標記爲主觀下線並不一定會引起服務器的自動故障遷移:只有在足夠數量的 Sentinel 都將一個服務器標記爲主觀下線之後,服務器纔會被標記爲客觀下線(objectively down, 簡稱 ODOWN ),這時自動故障遷移纔會執行。

    將服務器標記爲客觀下線所需的 Sentinel 數量由對主服務器的配置決定。

  • parallel-syncs 選項指定了在執行故障轉移時,最多可以有多少個從服務器同時對新的主服務器進行同步,這個數字越小,完成故障轉移所需的時間就越長。

    如果從服務器被設置爲允許使用過期數據集(參見對 redis.conf 文件中對 slave-serve-stale-data 選項的說明),那麼你可能不希望所有從服務器都在同一時間向新的主服務器發送同步請求,因爲儘管複製過程的絕大部分步驟都不會阻塞從服務器,但從服務器在載入主服務器發來的 RDB 文件時,仍然會造成從服務器在一段時間內不能處理命令請求:如果全部從服務器一起對新的主服務器進行同步,那麼就可能會造成所有從服務器在短時間內全部不可用的情況出現。

    你可以通過將這個值設爲 1 來保證每次只有一個從服務器處於不能處理命令請求的狀態。

本文檔剩餘的內容將對 Sentinel 系統的其他選項進行介紹,示例配置文件 sentinel.conf 也對相關的選項進行了完整的註釋。

主觀下線和客觀下線

前面說過, Redis 的 Sentinel 中關於下線(down)有兩個不同的概念:

  • 主觀下線(Subjectively Down, 簡稱 SDOWN)指的是單個 Sentinel 實例對服務器做出的下線判斷。
  • 客觀下線(Objectively Down, 簡稱 ODOWN)指的是多個 Sentinel 實例在對同一個服務器做出 SDOWN 判斷,並且通過 SENTINEL is-master-down-by-addr 命令互相交流之後,得出的服務器下線判斷。(一個 Sentinel 可以通過向另一個 Sentinel 發送 SENTINEL is-master-down-by-addr 命令來詢問對方是否認爲給定的服務器已下線。)

如果一個服務器沒有在 master-down-after-milliseconds 選項所指定的時間內,對向它發送 PING 命令的 Sentinel 返回一個有效回覆(valid reply),那麼 Sentinel 就會將這個服務器標記爲主觀下線。

服務器對 PING 命令的有效回覆可以是以下三種回覆的其中一種:

  • 返回 +PONG
  • 返回 -LOADING 錯誤。
  • 返回 -MASTERDOWN 錯誤。

如果服務器返回除以上三種回覆之外的其他回覆,又或者在指定時間內沒有回覆 PING 命令,那麼 Sentinel 認爲服務器返回的回覆無效(non-valid)。

注意,一個服務器必須在 master-down-after-milliseconds 毫秒內,一直返回無效回覆纔會被 Sentinel 標記爲主觀下線。

舉個例子,如果 master-down-after-milliseconds 選項的值爲 30000 毫秒(30 秒),那麼只要服務器能在每 29 秒之內返回至少一次有效回覆,這個服務器就仍然會被認爲是處於正常狀態的。

從主觀下線狀態切換到客觀下線狀態並沒有使用嚴格的法定人數算法(strong quorum algorithm),而是使用了流言協議:如果 Sentinel 在給定的時間範圍內,從其他 Sentinel 那裏接收到了足夠數量的主服務器下線報告,那麼 Sentinel 就會將主服務器的狀態從主觀下線改變爲客觀下線。如果之後其他 Sentinel 不再報告主服務器已下線,那麼客觀下線狀態就會被移除。

客觀下線條件只適用於主服務器:對於任何其他類型的 Redis 實例, Sentinel 在將它們判斷爲下線前不需要進行協商,所以從服務器或者其他 Sentinel 永遠不會達到客觀下線條件。

只要一個 Sentinel 發現某個主服務器進入了客觀下線狀態,這個 Sentinel 就可能會被其他 Sentinel 推選出,並對失效的主服務器執行自動故障遷移操作。

每個 Sentinel 都需要定期執行的任務

  • 每個 Sentinel 以每秒鐘一次的頻率向它所知的主服務器、從服務器以及其他 Sentinel 實例發送一個 PING 命令。
  • 如果一個實例(instance)距離最後一次有效回覆 PING 命令的時間超過 down-after-milliseconds 選項所指定的值,那麼這個實例會被 Sentinel 標記爲主觀下線。一個有效回覆可以是: +PONG -LOADING 或者 -MASTERDOWN
  • 如果一個主服務器被標記爲主觀下線,那麼正在監視這個主服務器的所有 Sentinel 要以每秒一次的頻率確認主服務器的確進入了主觀下線狀態。
  • 如果一個主服務器被標記爲主觀下線,並且有足夠數量的 Sentinel (至少要達到配置文件指定的數量)在指定的時間範圍內同意這一判斷,那麼這個主服務器被標記爲客觀下線。
  • 在一般情況下,每個 Sentinel 會以每 10 秒一次的頻率向它已知的所有主服務器和從服務器發送 INFO 命令。當一個主服務器被 Sentinel 標記爲客觀下線時, Sentinel 向下線主服務器的所有從服務器發送 INFO 命令的頻率會從 10 秒一次改爲每秒一次。
  • 當沒有足夠數量的 Sentinel 同意主服務器已經下線,主服務器的客觀下線狀態就會被移除。當主服務器重新向 Sentinel 的 PING 命令返回有效回覆時,主服務器的主管下線狀態就會被移除。

自動發現 Sentinel 和從服務器

一個 Sentinel 可以與其他多個 Sentinel 進行連接,各個 Sentinel 之間可以互相檢查對方的可用性,並進行信息交換。

你無須爲運行的每個 Sentinel 分別設置其他 Sentinel 的地址,因爲 Sentinel 可以通過發佈與訂閱功能來自動發現正在監視相同主服務器的其他 Sentinel ,這一功能是通過向頻道 __sentinel__:hello 發送信息來實現的。

與此類似,你也不必手動列出主服務器屬下的所有從服務器,因爲 Sentinel 可以通過詢問主服務器來獲得所有從服務器的信息。

  • 每個 Sentinel 會以每兩秒一次的頻率,通過發佈與訂閱功能,向被它監視的所有主服務器和從服務器的 __sentinel__:hello 頻道發送一條信息,信息中包含了 Sentinel 的 IP 地址、端口號和運行 ID (runid)。
  • 每個 Sentinel 都訂閱了被它監視的所有主服務器和從服務器的 __sentinel__:hello 頻道,查找之前未出現過的 sentinel (looking for unknown sentinels)。當一個 Sentinel 發現一個新的 Sentinel 時,它會將新的 Sentinel 添加到一個列表中,這個列表保存了 Sentinel 已知的,監視同一個主服務器的所有其他 Sentinel 。
  • Sentinel 發送的信息中還包括完整的主服務器當前配置(configuration)。如果一個 Sentinel 包含的主服務器配置比另一個 Sentinel 發送的配置要舊,那麼這個 Sentinel 會立即升級到新配置上。
  • 在將一個新 Sentinel 添加到監視主服務器的列表上面之前, Sentinel 會先檢查列表中是否已經包含了和要添加的 Sentinel 擁有相同運行 ID 或者相同地址(包括 IP 地址和端口號)的 Sentinel ,如果是的話, Sentinel 會先移除列表中已有的那些擁有相同運行 ID 或者相同地址的 Sentinel ,然後再添加新 Sentinel 。

Sentinel API

在默認情況下, Sentinel 使用 TCP 端口 26379 (普通 Redis 服務器使用的是 6379 )。

Sentinel 接受 Redis 協議格式的命令請求,所以你可以使用 redis-cli 或者任何其他 Redis 客戶端來與 Sentinel 進行通訊。

有兩種方式可以和 Sentinel 進行通訊:

  • 第一種方法是通過直接發送命令來查詢被監視 Redis 服務器的當前狀態,以及 Sentinel 所知道的關於其他 Sentinel 的信息,諸如此類。
  • 另一種方法是使用發佈與訂閱功能,通過接收 Sentinel 發送的通知:當執行故障轉移操作,或者某個被監視的服務器被判斷爲主觀下線或者客觀下線時, Sentinel 就會發送相應的信息。

Sentinel 命令

以下列出的是 Sentinel 接受的命令:

  • PING :返回 PONG
  • SENTINEL masters :列出所有被監視的主服務器,以及這些主服務器的當前狀態。
  • SENTINEL slaves <master name> :列出給定主服務器的所有從服務器,以及這些從服務器的當前狀態。
  • SENTINEL get-master-addr-by-name <master name> :返回給定名字的主服務器的 IP 地址和端口號。如果這個主服務器正在執行故障轉移操作,或者針對這個主服務器的故障轉移操作已經完成,那麼這個命令返回新的主服務器的 IP 地址和端口號。
  • SENTINEL reset <pattern> :重置所有名字和給定模式 pattern 相匹配的主服務器。 pattern 參數是一個 Glob 風格的模式。重置操作清除主服務器目前的所有狀態,包括正在執行中的故障轉移,並移除目前已經發現和關聯的,主服務器的所有從服務器和 Sentinel 。
  • SENTINEL failover <master name> :當主服務器失效時,在不詢問其他 Sentinel 意見的情況下,強制開始一次自動故障遷移(不過發起故障轉移的 Sentinel 會向其他 Sentinel 發送一個新的配置,其他 Sentinel 會根據這個配置進行相應的更新)。

發佈與訂閱信息

客戶端可以將 Sentinel 看作是一個只提供了訂閱功能的 Redis 服務器:你不可以使用 PUBLISH 命令向這個服務器發送信息,但你可以用 SUBSCRIBE 命令或者 PSUBSCRIBE 命令,通過訂閱給定的頻道來獲取相應的事件提醒。

一個頻道能夠接收和這個頻道的名字相同的事件。比如說,名爲 +sdown 的頻道就可以接收所有實例進入主觀下線(SDOWN)狀態的事件。

通過執行 PSUBSCRIBE * 命令可以接收所有事件信息。

以下列出的是客戶端可以通過訂閱來獲得的頻道和信息的格式:第一個英文單詞是頻道/事件的名字,其餘的是數據的格式。

注意,當格式中包含 instance details 字樣時,表示頻道所返回的信息中包含了以下用於識別目標實例的內容:

<instance-type> <name> <ip> <port> @ <master-name> <master-ip> <master-port>

@ 字符之後的內容用於指定主服務器,這些內容是可選的,它們僅在 @ 字符之前的內容指定的實例不是主服務器時使用。

  • +reset-master <instance details> :主服務器已被重置。
  • +slave <instance details> :一個新的從服務器已經被 Sentinel 識別並關聯。
  • +failover-state-reconf-slaves <instance details> :故障轉移狀態切換到了 reconf-slaves 狀態。
  • +failover-detected <instance details> :另一個 Sentinel 開始了一次故障轉移操作,或者一個從服務器轉換成了主服務器。
  • +slave-reconf-sent <instance details> :領頭(leader)的 Sentinel 向實例發送了 SLAVEOF 命令,爲實例設置新的主服務器。
  • +slave-reconf-inprog <instance details> :實例正在將自己設置爲指定主服務器的從服務器,但相應的同步過程仍未完成。
  • +slave-reconf-done <instance details> :從服務器已經成功完成對新主服務器的同步。
  • -dup-sentinel <instance details> :對給定主服務器進行監視的一個或多個 Sentinel 已經因爲重複出現而被移除 —— 當 Sentinel 實例重啓的時候,就會出現這種情況。
  • +sentinel <instance details> :一個監視給定主服務器的新 Sentinel 已經被識別並添加。
  • +sdown <instance details> :給定的實例現在處於主觀下線狀態。
  • -sdown <instance details> :給定的實例已經不再處於主觀下線狀態。
  • +odown <instance details> :給定的實例現在處於客觀下線狀態。
  • -odown <instance details> :給定的實例已經不再處於客觀下線狀態。
  • +new-epoch <instance details> :當前的紀元(epoch)已經被更新。
  • +try-failover <instance details> :一個新的故障遷移操作正在執行中,等待被大多數 Sentinel 選中(waiting to be elected by the majority)。
  • +elected-leader <instance details> :贏得指定紀元的選舉,可以進行故障遷移操作了。
  • +failover-state-select-slave <instance details> :故障轉移操作現在處於 select-slave 狀態 —— Sentinel 正在尋找可以升級爲主服務器的從服務器。
  • no-good-slave <instance details> :Sentinel 操作未能找到適合進行升級的從服務器。Sentinel 會在一段時間之後再次嘗試尋找合適的從服務器來進行升級,又或者直接放棄執行故障轉移操作。
  • selected-slave <instance details> :Sentinel 順利找到適合進行升級的從服務器。
  • failover-state-send-slaveof-noone <instance details> :Sentinel 正在將指定的從服務器升級爲主服務器,等待升級功能完成。
  • failover-end-for-timeout <instance details> :故障轉移因爲超時而中止,不過最終所有從服務器都會開始複製新的主服務器(slaves will eventually be configured to replicate with the new master anyway)。
  • failover-end <instance details> :故障轉移操作順利完成。所有從服務器都開始複製新的主服務器了。
  • +switch-master <master name> <oldip> <oldport> <newip> <newport> :配置變更,主服務器的 IP 和地址已經改變。 這是絕大多數外部用戶都關心的信息。
  • +tilt :進入 tilt 模式。
  • -tilt :退出 tilt 模式。

故障轉移

一次故障轉移操作由以下步驟組成:

  • 發現主服務器已經進入客觀下線狀態。
  • 對我們的當前紀元進行自增(詳情請參考 Raft leader election ),並嘗試在這個紀元中當選。
  • 如果當選失敗,那麼在設定的故障遷移超時時間的兩倍之後,重新嘗試當選。如果當選成功,那麼執行以下步驟。
  • 選出一個從服務器,並將它升級爲主服務器。
  • 向被選中的從服務器發送 SLAVEOF NO ONE 命令,讓它轉變爲主服務器。
  • 通過發佈與訂閱功能,將更新後的配置傳播給所有其他 Sentinel ,其他 Sentinel 對它們自己的配置進行更新。
  • 向已下線主服務器的從服務器發送 SLAVEOF 命令,讓它們去複製新的主服務器。
  • 當所有從服務器都已經開始複製新的主服務器時,領頭 Sentinel 終止這次故障遷移操作。

Note

每當一個 Redis 實例被重新配置(reconfigured) —— 無論是被設置成主服務器、從服務器、又或者被設置成其他主服務器的從服務器 —— Sentinel 都會向被重新配置的實例發送一個 CONFIG REWRITE 命令,從而確保這些配置會持久化在硬盤裏。

Sentinel 使用以下規則來選擇新的主服務器:

  • 在失效主服務器屬下的從服務器當中,那些被標記爲主觀下線、已斷線、或者最後一次回覆 PING 命令的時間大於五秒鐘的從服務器都會被淘汰。
  • 在失效主服務器屬下的從服務器當中,那些與失效主服務器連接斷開的時長超過 down-after 選項指定的時長十倍的從服務器都會被淘汰。
  • 在經歷了以上兩輪淘汰之後剩下來的從服務器中,我們選出複製偏移量(replication offset)最大的那個從服務器作爲新的主服務器;如果複製偏移量不可用,或者從服務器的複製偏移量相同,那麼帶有最小運行 ID 的那個從服務器成爲新的主服務器。

Sentinel 自動故障遷移的一致性特質

Sentinel 自動故障遷移使用 Raft 算法來選舉領頭(leader) Sentinel ,從而確保在一個給定的紀元(epoch)裏,只有一個領頭產生。

這表示在同一個紀元中,不會有兩個 Sentinel 同時被選中爲領頭,並且各個 Sentinel 在同一個紀元中只會對一個領頭進行投票。

更高的配置紀元總是優於較低的紀元,因此每個 Sentinel 都會主動使用更新的紀元來代替自己的配置。

簡單來說,我們可以將 Sentinel 配置看作是一個帶有版本號的狀態。一個狀態會以最後寫入者勝出(last-write-wins)的方式(也即是,最新的配置總是勝出)傳播至所有其他 Sentinel 。

舉個例子,當出現網絡分割(network partitions)時,一個 Sentinel 可能會包含了較舊的配置,而當這個 Sentinel 接到其他 Sentinel 發來的版本更新的配置時, Sentinel 就會對自己的配置進行更新。

如果要在網絡分割出現的情況下仍然保持一致性,那麼應該使用 min-slaves-to-write 選項,讓主服務器在連接的從實例少於給定數量時停止執行寫操作,與此同時,應該在每個運行 Redis 主服務器或從服務器的機器上運行 Redis Sentinel 進程。

Sentinel 狀態的持久化

Sentinel 的狀態會被持久化在 Sentinel 配置文件裏面。

每當 Sentinel 接收到一個新的配置,或者當領頭 Sentinel 爲主服務器創建一個新的配置時,這個配置會與配置紀元一起被保存到磁盤裏面。

這意味着停止和重啓 Sentinel 進程都是安全的。

Sentinel 在非故障遷移的情況下對實例進行重新配置

即使沒有自動故障遷移操作在進行, Sentinel 總會嘗試將當前的配置設置到被監視的實例上面。特別是:

  • 根據當前的配置,如果一個從服務器被宣告爲主服務器,那麼它會代替原有的主服務器,成爲新的主服務器,並且成爲原有主服務器的所有從服務器的複製對象。
  • 那些連接了錯誤主服務器的從服務器會被重新配置,使得這些從服務器會去複製正確的主服務器。

不過,在以上這些條件滿足之後, Sentinel 在對實例進行重新配置之前仍然會等待一段足夠長的時間,確保可以接收到其他 Sentinel 發來的配置更新,從而避免自身因爲保存了過期的配置而對實例進行了不必要的重新配置。

TILT 模式

Redis Sentinel 嚴重依賴計算機的時間功能:比如說,爲了判斷一個實例是否可用, Sentinel 會記錄這個實例最後一次相應 PING 命令的時間,並將這個時間和當前時間進行對比,從而知道這個實例有多長時間沒有和 Sentinel 進行任何成功通訊。

不過,一旦計算機的時間功能出現故障,或者計算機非常忙碌,又或者進程因爲某些原因而被阻塞時, Sentinel 可能也會跟着出現故障。

TILT 模式是一種特殊的保護模式:當 Sentinel 發現系統有些不對勁時, Sentinel 就會進入 TILT 模式。

因爲 Sentinel 的時間中斷器默認每秒執行 10 次,所以我們預期時間中斷器的兩次執行之間的間隔爲 100 毫秒左右。 Sentinel 的做法是,記錄上一次時間中斷器執行時的時間,並將它和這一次時間中斷器執行的時間進行對比:

  • 如果兩次調用時間之間的差距爲負值,或者非常大(超過 2 秒鐘),那麼 Sentinel 進入 TILT 模式。
  • 如果 Sentinel 已經進入 TILT 模式,那麼 Sentinel 延遲退出 TILT 模式的時間。

當 Sentinel 進入 TILT 模式時,它仍然會繼續監視所有目標,但是:

  • 它不再執行任何操作,比如故障轉移。
  • 當有實例向這個 Sentinel 發送 SENTINEL is-master-down-by-addr 命令時, Sentinel 返回負值:因爲這個 Sentinel 所進行的下線判斷已經不再準確。

如果 TILT 可以正常維持 30 秒鐘,那麼 Sentinel 退出 TILT 模式。

處理 -BUSY 狀態

Warning

該功能尚未實現

當 Lua 腳本的運行時間超過指定時限時, Redis 就會返回 -BUSY 錯誤。

當出現這種情況時, Sentinel 在嘗試執行故障轉移操作之前,會先向服務器發送一個 SCRIPT KILL 命令,如果服務器正在執行的是一個只讀腳本的話,那麼這個腳本就會被殺死,服務器就會回到正常狀態。

Sentinel 的客戶端實現

關於 Sentinel 客戶端的實現信息可以參考 Sentinel 客戶端指引手冊

發佈了46 篇原創文章 · 獲贊 5 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章