Dubbo 3 前瞻之應用級服務發現

Dubbo 與開源中國共同策劃   【Dubbo 雲原生之路】系列文章,和你們一塊兒回顧 Apache Dubbo 社區的發展。這是系列第二篇。系列文章主要涵蓋 Dubbo 技術解讀、社區運營、應用案例解析三大部分。在這裏,咱們也向全部的    Dubbo 用戶和開發者發出投稿邀請,若是你正在使用 Dubbo,或是正在爲 Dubbo 貢獻力量,歡迎和咱們分享你的開發使用經驗,優質文章也會收錄進【Dubbo 雲原生之路】系列。
 
投稿地址:xuyijun@oschina.cn

系列開篇:Dubbo 雲原生之路html

本文做者:劉軍
花名陸龜,Github 帳號 Chickenlj,Apache Dubbo PMC,項目核心開發,見證了 Dubbo 重啓開源,到從 Apache 基金會畢業的整個過程。現任職阿里云云原生應用平臺團隊,參與服務框架、微服務相關工做,目前主要在推進 Dubbo 3.0 - Dubbo 雲原生。

1、服務發現(Service Discovery) 概述

從 Internet 剛開始興起,如何動態感知後端服務的地址變化就是一個必需要面對的問題,爲此人們定義了 DNS 協議,基於此協議,調用方只須要記住由固定字符串組成的域名,就能輕鬆完成對後端服務的訪問,而不用擔憂流量最終會訪問到哪些機器 IP,由於有代理組件會基於 DNS 地址解析後的地址列表,將流量透明的、均勻的分發到不一樣的後端機器上。apache

在使用微服務構建複雜的分佈式系統時,如何感知 backend 服務實例的動態上下線,也是微服務框架最須要關心並解決的問題之一。業界將這個問題稱之爲 -  微服務的地址發現(Service Discovery),業界比較有表明性的微服務框架如 SpringCloud、Microservices、Dubbo 等都抽象了強大的動態地址發現能力,而且爲了知足微服務業務場景的需求,絕大多數框架的地址發現都是基於本身設計的一套機制來實現,所以在能力、靈活性上都要比傳統 DNS 豐富得多。如 SpringCloud 中經常使用的 Eureka, Dubbo 中經常使用的 Zookeeper、Nacos 等,這些註冊中心實現不止可以傳遞地址(IP + Port),還包括一些微服務的 Metadata 信息,如實例序列化類型、實例方法列表、各個方法級的定製化配置等。編程

下圖是微服務中 Service Discovery 的基本工做原理圖,微服務體系中的實例大概可分爲三種角色:服務提供者(Provider)、服務消費者(Consumer)和註冊中心(Registry)。而不一樣框架實現間最主要的區別就體如今註冊中心數據的組織:地址如何組織、以什麼粒度組織、除地址外還同步哪些數據?後端

咱們今天這篇文章就是圍繞這三個角色展開,重點看下 Dubbo 中對於服務發現方案的設計,包括以前老的服務發現方案的優點和缺點,以及 Dubbo 3.0 中正在設計、開發中的全新的面向應用粒度的地址發現方案,咱們期待這個新的方案能作到:api

  • 支持幾十萬/上百萬級集羣實例的地址發現
  • 與不一樣的微服務體系(如 Spring Cloud)實如今地址發現層面的互通

2、Dubbo 地址發現機制解析

咱們先以一個 DEMO 應用爲例,來快速的看一下 Dubbo 「接口粒度」服務發現與「應用粒度」服務發現體現出來的區別。這裏咱們重點關注 Provider 實例是如何向註冊中心註冊的,而且,爲了體現註冊中心數據量變化,咱們觀察的是兩個 Provider 實例的場景。架構

應用 DEMO 提供的服務列表以下:app

<dubbo:service interface="org.apache.dubbo.samples.basic.api.DemoService" ref="demoService"/>

<dubbo:service interface="org.apache.dubbo.samples.basic.api.GreetingService" ref="greetingService"/>

咱們示例註冊中心實現採用的是 Zookeeper ,啓動 192.168.0.103 和 192.168.0.104 兩個實例後,如下是兩種模式下注冊中心的實際數據。負載均衡

2.1 「接口粒度」 服務發現

192.168.0.103  實例註冊的數據:框架

dubbo://192.168.0.103:20880/org.apache.dubbo.samples.basic.api.DemoService?anyhost=true&application=demo-provider&default=true&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.samples.basic.api.DemoService&methods=testVoid,sayHello&pid=995&release=2.7.7&side=provider×tamp=1596988171266

dubbo://192.168.0.103:20880/org.apache.dubbo.samples.basic.api.GreetingService?anyhost=true&application=demo-provider&default=true&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.samples.basic.api.GreetingService&methods=greeting&pid=995&release=2.7.7&side=provider×tamp=1596988170816

192.168.0.104  實例註冊的數據:運維

dubbo://192.168.0.104:20880/org.apache.dubbo.samples.basic.api.DemoService?anyhost=true&application=demo-provider&default=true&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.samples.basic.api.DemoService&methods=testVoid,sayHello&pid=995&release=2.7.7&side=provider×tamp=1596988171266

dubbo://192.168.0.104:20880/org.apache.dubbo.samples.basic.api.GreetingService?anyhost=true&application=demo-provider&default=true&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.samples.basic.api.GreetingService&methods=greeting&pid=995&release=2.7.7&side=provider×tamp=1596988170816

2.2 「應用粒度」 服務發現

192.168.0.103 實例數據:

{

    "name": "demo-provider",

    "id": "192.168.0.103:20880",

    "address": "192.168.0.103",

    "port": 20880,

  "metadata": {

    "dubbo.endpoints": "[{\"port\":20880,\"protocol\":\"dubbo\"}]",

    "dubbo.metadata.storage-type": "local",

    "dubbo.revision": "6785535733750099598"

  },

    "time": 1583461240877

}

192.168.0.104  實例數據:

{

"name": "demo-provider",

"id": "192.168.0.104:20880",

"address": "192.168.0.104",

"port": 20880,

"metadata": {

"dubbo.endpoints": "[{"port":20880,"protocol":"dubbo"}]",

"dubbo.metadata.storage-type": "local",

"dubbo.revision": "7829635812370099387"

},

"time": 1583461240947

}

對比以上兩種不一樣粒度的服務發現模式,從 「接口粒度」 升級到 「應用粒度」 後咱們能夠總結出最大的區別是:註冊中心數據量再也不與接口數成正比,不論應用提供有多少接口,註冊中心只有一條實例數據。

那麼接下來咱們詳細看下這個變化給 Dubbo 帶來了哪些好處。

3、Dubbo 應用級服務發現的意義

咱們先說結論,應用級服務發現給 Dubbo 帶來如下優點:

  • 與業界主流微服務模型對齊,好比 SpringCloud、Kubernetes Native Service 等
  • 提高性能與可伸縮性。註冊中心數據的從新組織(減小),能最大幅度的減輕註冊中心的存儲、推送壓力,進而減小 Dubbo Consumer 側的地址計算壓力;集羣規模也開始變得可預測、可評估(與 RPC 接口數量無關,只與實例部署規模相關)

3.1 對齊主流微服務模型

自動、透明的實例地址發現(負載均衡)是全部微服務框架須要解決的事情,這能讓後端的部署結構對上游微服務透明,上游服務只須要從收到的地址列表中選取一個,發起調用就能夠了。要實現以上目標,涉及兩個關鍵點的自動同步:

  • 實例地址,服務消費方須要知道地址以創建連接
  • RPC 方法定義,服務消費方須要知道 RPC 服務的具體定義,不論服務類型是 rest 或 rmi 等

對於 RPC 實例間藉助註冊中心的數據同步,REST 定義了一套很是有意思的成熟度模型,感興趣的朋友能夠參考這裏:點擊查看, 按照文章中的 4 級成熟度定義,Dubbo 當前基於接口粒度的模型能夠對應到 L4 級別。

接下來,咱們看看 Dubbo、SpringCloud 以及 Kubernetes 分別是怎麼圍繞自動化的實例地址發現這個目標設計的。

a) Spring Cloud

Spring Cloud 經過註冊中心只同步了應用與實例地址,消費方能夠基於實例地址與服務提供方創建連接,可是消費方對於如何發起 http 調用(SpringCloud 基於 rest 通訊)一無所知,好比對方有哪些 http endpoint,須要傳入哪些參數等。

RPC 服務這部分信息目前都是經過線下約定或離線的管理系統來協商的。這種架構的優缺點總結以下。

優點:部署結構清晰、地址推送量小。

缺點:地址訂閱須要指定應用名, provider 應用變動(拆分)需消費端感知;RPC 調用沒法全自動同步。

b) Dubbo

Dubbo 經過註冊中心同時同步了實例地址和 RPC 方法,所以其能實現 RPC 過程的自動同步,面向 RPC 編程、面向 RPC 治理,對後端應用的拆分消費端無感知,其缺點則是地址推送數量變大,和 RPC 方法成正比。

c) Dubbo + Kubernetes

Dubbo 要支持 Kubernetes native service,相比以前自建註冊中心的服務發現體系來講,在工做機制上主要有兩點變化:

  • 服務註冊由平臺接管,provider 再也不須要關心服務註冊
  • consumer 端服務發現將是 Dubbo 關注的重點,經過對接平臺層的 API-Server、DNS 等,Dubbo client 能夠經過一個 Service Name(一般對應到 Application Name)查詢到一組 Endpoints(一組運行 provider 的 pod),經過將 Endpoints 映射到 Dubbo 內部地址列表,以驅動 Dubbo 內置的負載均衡機制工做

Kubernetes Service 做爲一個抽象概念,怎麼映射到 Dubbo 是一個值得討論的點:

  • Service Name - > Application Name,Dubbo 應用和 Kubernetes 服務一一對應,對於微服務運維和建設環節透明,與開發階段解耦
apiVersion: v1

kind: Service metadata:   name: provider-app-name spec:   selector:     app: provider-app-name   ports:     - protocol: TCP       port:       targetPort: 9376
  • Service Name - > Dubbo RPC Service,Kubernetes 要維護調度的服務與應用內建 RPC 服務綁定,維護的服務數量變多
---

apiVersion: v1

kind: Service

metadata:

  name: rpc-service-1

spec:

  selector:

    app: provider-app-name

  ports: ##

...

---

apiVersion: v1

kind: Service

metadata:

  name: rpc-service-2

spec:

  selector:

    app: provider-app-name

  ports: ##

...

---

apiVersion: v1

kind: Service

metadata:

  name: rpc-service-N

spec:

  selector:

    app: provider-app-name

  ports: ##

...

結合以上幾種不一樣微服務框架模型的分析,咱們能夠發現,Dubbo 與 SpringCloud、Kubernetes 等不一樣產品在微服務的抽象定義上仍是存在很大不一樣的。SpringCloud 和 Kubernetes 在微服務的模型抽象上仍是比較接近的,二者基本都只關心實例地址的同步,若是咱們去關心其餘的一些服務框架產品,會發現它們絕大多數也是這麼設計的,即 REST 成熟度模型中的 L3 級別。

對比起來 Dubbo 則相對是比較特殊的存在,更多的是從 RPC 服務的粒度去設計的。對應 REST 成熟度模型中的 L4 級別。

如咱們上面針對每種模型作了詳細的分析,每種模型都有其優點和不足。而咱們最初決定 Dubbo 要作出改變,往其餘的微服務發現模型上的對齊,是咱們最先在肯定  Dubbo 的雲原生方案時,咱們發現要讓 Dubbo 去支持 Kubernetes Native Service,模型對齊是一個基礎條件;另外一點是來自用戶側對 Dubbo 場景化的一些工程實踐的需求,得益於 Dubbo 對多註冊、多協議能力的支持,使得 Dubbo 聯通不一樣的微服務體系成爲可能,而服務發現模型的不一致成爲其中的一個障礙,這部分的場景描述請參見如下文章:點擊查看

3.2 更大規模的微服務集羣 - 解決性能瓶頸

這部分涉及到和註冊中心、配置中心的交互,關於不一樣模型下注冊中心數據的變化,以前原理部分咱們簡單分析過。爲更直觀的對比服務模型變動帶來的推送效率提高,咱們來經過一個示例看一下不一樣模型註冊中心的對比:

圖中左邊是微服務框架的一個典型工做流程,Provider 和  Consumer 經過註冊中心實現自動化的地址通知。其中,Provider 實例的信息如圖中表格所示:應用 DEMO 包含三個接口 DemoService 1 2 3,當前實例的 ip 地址爲 10.210.134.30。

  • 對於 Spring Cloud 和 Kubernetes 模型,註冊中心只會存儲一條 DEMO - 10.210.134.30+metadata 的數據
  • 對於老的 Dubbo 模型,註冊中心存儲了三條接口粒度的數據,分別對應三個接口 DemoService 1 2 3,而且不少的址數據都是重複的

能夠總結出,基於應用粒度的模型所存儲和推送的數據量是和應用、實例數成正比的,只有當咱們的應用數增多或應用的實例數增加時,地址推送壓力纔會上漲。

而對於基於接口粒度的模型,數據量是和接口數量正相關的,鑑於一個應用一般發佈多個接口的現狀,這個數量級自己比應用粒度是要乘以倍數的;另一個關鍵點在於,接口粒度致使的集羣規模評估的不透明,相對於實i例、應用增加都一般是在運維側的規劃之中,接口的定義更多的是業務側的內部行爲,每每能夠繞過評估給集羣帶來壓力。

以 Consumer 端服務訂閱舉例,根據我對社區部分 Dubbo 中大規模頭部用戶的粗略統計,根據受統計公司的實際場景,一個 Consumer 應用要消費(訂閱)的 Provier 應用數量每每要超過 10 個,而具體到其要消費(訂閱)的的接口數量則一般要達到 30 個,平均狀況下 Consumer 訂閱的 3 個接口來自同一個 Provider 應用,如此計算下來,若是以應用粒度爲地址通知和選址基本單位,則平均地址推送和計算量將降低 60% 還要多。

而在極端狀況下,也就是當 Consumer 端消費的接口更多的來自同一個應用時,這個地址推送與內存消耗的佔用將會進一步獲得下降,甚至能夠超過 80% 以上。

一個典型的幾段場景便是 Dubbo 體系中的網關型應用,有些網關應用消費(訂閱)達 100+ 應用,而消費(訂閱)的服務有 1000+ ,平均有 10 個接口來自同一個應用,若是咱們把地址推送和計算的粒度改成應用,則地址推送量從原來的 n 1000 變爲 n 100,地址數量下降可達近 90%。

4、應用級服務發現工做原理

4.1 設計原則

上面一節咱們從服務模型支撐大規模集羣的角度分別給出了 Dubbo 往應用級服務發現靠攏的好處或緣由,但這麼作的同時接口粒度的服務治理能力仍是要繼續保留,這是 Dubbo 框架編程模型易用性、服務治理能力優點的基礎。

如下是我認爲咱們作服務模型遷移仍要堅持的設計原則:

  • 新的服務發現模型要實現對原有 Dubbo 消費端開發者的無感知遷移,即 Dubbo 繼續面向 RPC 服務編程、面向 RPC 服務治理,作到對用戶側徹底無感知
  • 創建 Consumer 與 Provider 間的自動化 RPC 服務元數據協調機制,解決傳統微服務模型沒法同步 RPC 級接口配置的缺點

4.2 基本原理詳解

應用級服務發現做爲一種新的服務發現機制,和之前 Dubbo 基於 RPC 服務粒度的服務發如今核心流程上基本上是一致的:即服務提供者往註冊中心註冊地址信息,服務消費者從註冊中心拉取&訂閱地址信息。

這裏主要的不一樣有如下兩點:

4.2.1 註冊中心數據以「應用 - 實例列表」格式組織,再也不包含 RPC 服務信息

如下是每一個 Instance metadata 的示例數據,總的原則是 metadata 只包含當前 instance 節點相關的信息,不涉及 RPC 服務粒度的信息。

整體信息歸納以下:實例地址、實例各類環境標、metadata service 元數據、其餘少許必要屬性。

{

    "name": "provider-app-name",

    "id": "192.168.0.102:20880",

    "address": "192.168.0.102",

    "port": 20880,

    "sslPort": null,

    "payload": {

        "id": null,

        "name": "provider-app-name",

        "metadata": {

            "metadataService": "{\"dubbo\":{\"version\":\"1.0.0\",\"dubbo\":\"2.0.2\",\"release\":\"2.7.5\",\"port\":\"20881\"}}",

            "endpoints": "[{\"port\":20880,\"protocol\":\"dubbo\"}]",

            "storage-type": "local",

            "revision": "6785535733750099598",

        }

    },

    "registrationTimeUTC": 1583461240877,

    "serviceType": "DYNAMIC",

    "uriSpec": null

}

4.2.2 Client – Server 自行協商 RPC 方法信息

在註冊中心再也不同步 RPC 服務信息後,服務自省在服務消費端和提供端之間創建了一條內置的 RPC 服務信息協商機制,這也是「服務自省」這個名字的由來。服務端實例會暴露一個預約義的 MetadataService RPC 服務,消費端經過調用 MetadataService 獲取每一個實例 RPC 方法相關的配置信息。

當前 MetadataService 返回的數據格式以下:

[

  "dubbo://192.168.0.102:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=demo-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=9585&release=2.7.5&side=provider×tamp=1583469714314",  "dubbo://192.168.0.102:20880/org.apache.dubbo.demo.HelloService?anyhost=true&application=demo-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=9585&release=2.7.5&side=provider×tamp=1583469714314",   "dubbo://192.168.0.102:20880/org.apache.dubbo.demo.WorldService?anyhost=true&application=demo-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=9585&release=2.7.5&side=provider×tamp=1583469714314" ]

熟悉 Dubbo 基於 RPC 服務粒度的服務發現模型的開發者應該能看出來,服務自省機制機制將之前註冊中心傳遞的 URL 一拆爲二:

  • 一部分和實例相關的數據繼續保留在註冊中心,如 ip、port、機器標識等
  • 另外一部分和 RPC 方法相關的數據從註冊中心移除,轉而經過 MetadataService 暴露給消費端

理想狀況下是能達到數據按照實例、RPC 服務嚴格區分開來,但明顯能夠看到以上實現版本還存在一些數據冗餘,有些也數據還未合理劃分。尤爲是 MetadataService 部分,其返回的數據還只是簡單的 URL 列表組裝,這些 URL實際上是包含了全量的數據。

如下是服務自省的一個完整工做流程圖,詳細描述了服務註冊、服務發現、MetadataService、RPC 調用間的協做流程。

  • 服務提供者啓動,首先解析應用定義的「普通服務」並依次註冊爲 RPC 服務,緊接着註冊內建的 MetadataService 服務,最後打開 TCP 監聽端口
  • 啓動完成後,將實例信息註冊到註冊中心(僅限 ip、port 等實例相關數據),提供者啓動完成
  • 服務消費者啓動,首先依據其要「消費的 provider 應用名」到註冊中心查詢地址列表,並完成訂閱(以實現後續地址變動自動通知)
  • 消費端拿到地址列表後,緊接着對 MetadataService 發起調用,返回結果中包含了全部應用定義的「普通服務」及其相關配置信息
  • 至此,消費者能夠接收外部流量,並對提供者發起 Dubbo RPC 調用

在以上流程中,咱們只考慮了一切順利的狀況,但在更詳細的設計或編碼實現中,咱們還須要嚴格約定一些異常場景下的框架行爲。好比,若是消費者 MetadataService 調用失敗,則在重試知道成功以前,消費者將不能夠接收外部流量。

4.3 服務自省中的關鍵機制

4.3.1 元數據同步機制

Client 與 Server 間在收到地址推送後的配置同步是服務自省的關鍵環節,目前針對元數據同步有兩種具體的可選方案,分別是:

  • 內建 MetadataService
  • 獨立的元數據中心,經過中細化的元數據集羣協調數據

內建 MetadataService

MetadataService 經過標準的 Dubbo 協議暴露,根據查詢條件,會將內存中符合條件的「普通服務」配置返回給消費者。這一步發生在消費端選址和調用前。

元數據中心

複用 2.7 版本中引入的元數據中心,provider 實例啓動後,會嘗試將內部的 RPC 服務組織成元數據的格式到元數據中心,而 consumer 則在每次收到註冊中心推送更新後,主動查詢元數據中心。

注意 consumer 端查詢元數據中心的時機,是等到註冊中心的地址更新通知以後。也就是經過註冊中心下發的數據,咱們能明確的知道什麼時候某個實例的元數據被更新了,此時才須要去查元數據中心。

4.3.2 RPC 服務 < - > 應用映射關係

回顧上文講到的註冊中心關於「應用 - 實例列表」結構的數據組織形式,這個變更目前對開發者並非徹底透明的,業務開發側會感知到查詢/訂閱地址列表的機制的變化。具體來講,相比以往咱們基於 RPC 服務來檢索地址,如今 consumer 須要經過指定 provider 應用名才能實現地址查詢或訂閱。

老的 Consumer 開發與配置示例:

<!-- 框架直接經過 RPC Service 1/2/N 去註冊中心查詢或訂閱地址列表 -->

<dubbo:registry address="zookeeper://127.0.0.1:2181"/>

<dubbo:reference interface="RPC Service 1" />

<dubbo:reference interface="RPC Service 2" />

<dubbo:reference interface="RPC Service N" />

新的 Consumer 開發與配置示例:

<!-- 框架須要經過額外的 provided-by="provider-app-x" 才能在註冊中心查詢或訂閱到地址列表 -->

<dubbo:registry address="zookeeper://127.0.0.1:2181?registry-type=service"/>

<dubbo:reference interface="RPC Service 1" provided-by="provider-app-x"/>

<dubbo:reference interface="RPC Service 2" provided-by="provider-app-x" />

<dubbo:reference interface="RPC Service N" provided-by="provider-app-y" />

以上指定 provider 應用名的方式是 Spring Cloud 當前的作法,須要 consumer 端的開發者顯示指定其要消費的 provider 應用。

以上問題的根源在於註冊中心不知道任何 RPC 服務相關的信息,所以只能經過應用名來查詢。

爲了使整個開發流程對老的 Dubbo 用戶更透明,同時避免指定 provider 對可擴展性帶來的影響(參見下方說明),咱們設計了一套 RPC 服務到應用名的映射關係,以嘗試在 consumer 自動完成 RPC 服務到 provider 應用名的轉換。

Dubbo 之因此選擇創建一套「接口-應用」的映射關係,主要是考慮到 service - app 映射關係的不肯定性。一個典型的場景便是應用/服務拆分,如上面提到的配置<dubbo:reference interface="RPC Service 2" provided-by="provider-app-x" />,PC Service 2 是定義於 provider-app-x 中的一個服務,將來它隨時可能會被開發者分拆到另一個新的應用如 provider-app-x-1 中,這個拆分要被全部的 PC Service 2 消費方感知到,並對應用進行修改升級,如改成<dubbo:reference interface="RPC Service 2" provided-by="provider-app-x-1" />,這樣的升級成本不能否認仍是挺高的。

究竟是 Dubbo 框架幫助開發者透明的解決這個問題,仍是交由開發者本身去解決,固然這只是個策略選擇問題,而且 Dubbo 2.7.5+ 版本目前是都提供了的。其實我我的更傾向於交由業務開發者經過組織上的約束來作,這樣也可進一步下降 Dubbo 框架的複雜度,提高運行態的穩定性。

5、總結與展望

應用級服務發現機制是 Dubbo 面向雲原生走出的重要一步,它幫 Dubbo 打通了與其餘微服務體系之間在地址發現層面的鴻溝,也成爲 Dubbo 適配 Kubernetes Native Service 等基礎設施的基礎。咱們指望 Dubbo 在新模型基礎上,能繼續保留在編程易用性、服務治理能力等方面強大的優點。可是咱們也應該看到應用粒度的模型一方面帶來了新的複雜性,須要咱們繼續去優化與加強;另外一方面,除了地址存儲與推送以外,應用粒度在幫助 Dubbo 選址層面也有進一步挖掘的潛力。

相關文章
相關標籤/搜索