天天看點

Dubbo的負載均衡、叢集容錯、服務降級等機制詳解1. Dubbo與RPC的關系2. Dubbo的基本使用3. dubbo的REST協定4. dubbo的控制台5. dubbo的服務路由

文章目錄

  • 1. Dubbo與RPC的關系
  • 2. Dubbo的基本使用
    • 2.1 Dubbo是什麼?
    • 2.2 負載均衡
    • 2.3 服務逾時
    • 2.4 叢集容錯
    • 2.5 服務降級
    • 2.6 本地存根
    • 2.7 參數回調
    • 2.8 異步調用
    • 2.9 泛化調用、泛化服務
  • 3. dubbo的REST協定
  • 4. dubbo的控制台
  • 5. dubbo的服務路由

1. Dubbo與RPC的關系

1.1 什麼是RPC?

維基百科這樣解釋:

        遠端過程調用(英語:Remote Procedure Call,縮寫為 RPC)是一個計算機通信協定。該協定允許運作于一台計算機的程式調用另一個位址空間(通常為一個開放網絡的一台計算機)的子程式,而程式員就像調用本地程式一樣,無需額外地為這個互動作用程式設計(無需關注細節)。RPC是一種伺服器-用戶端(Client/Server)模式,經典實作是一個通過發送請求-接受回應進行資訊互動的系統。

        如果涉及的軟體采用面向對象程式設計,那麼遠端過程調用亦可稱作遠端調用或遠端方法調用,是以,對于Java程式員而言,RPC就是遠端方法調用。

        如何了解RPC是一個計算機通信協定呢?我們已經知道RPC是專注于遠端方法調用,如果實作遠端方法調用,基本的就是通過網絡,通過傳輸資料來進行調用。如下圖所示

Dubbo的負載均衡、叢集容錯、服務降級等機制詳解1. Dubbo與RPC的關系2. Dubbo的基本使用3. dubbo的REST協定4. dubbo的控制台5. dubbo的服務路由

        可以看到遠端方法A 想要調用遠端方法B,需要定義 資料類型 和 傳輸協定。而這些需要定義的東西作為一個協定存在于調用方和接收方,後續所有調用都遵守這個已制定的協定,這就是RPC通信協定。是以,我們其實可以看到RPC的自定義性是很高的,各個公司内部都可以實作自己的一套RPC架構,而Dubbo就是阿裡所開源出來的一套RPC架構。

        RPC和 HTTP、TCP的關系就是:RPC是基于HTTP、TCP協定來傳輸資料的,對于所傳輸的資料,可以交由RPC的雙方來協商定義,但基本都會包括:

  1. 調用的是哪個類或接口
  2. 調用的是哪個方法,方法名和方法參數類型(考慮方法重載)
  3. 調用方法的入參

1.2 Dubbo與RPC的關系

        上面說到實作RPC架構需要定義 資料類型 和 傳輸協定。而Dubbo作為阿裡開源出來的RPC架構,已經制定好了對應的 傳輸資料類型 和傳輸協定,使用Dubbo必須遵循Dubbo制定好的規則。

Dubbo的傳輸協定見下文!

3. 自定義RPC架構思路

服務端:

  1. 注冊服務到zk或redis。以map的形式儲存起來,key = 服務名,value = List<伺服器位址>。用戶端請求可以負載到value的某個位址上。

    注意:如果隻把服務放在本地緩存中,那麼其他的服務将調用失敗,因為不同的服務屬于不同的jvm,其他服務将無法感覺另一個服務中的本地緩存。

  2. 把服務和服務的實作類注冊到本地緩存。以map的形式儲存起來,key = 服務名,value = 服務的實作類。目的是:當服務端接受到用戶端請求,可以根據用戶端傳來的接口名,從本地緩存中拿到其實作類,然後通過反射調用用戶端想要調用的方法
  3. 根據不同的協定啟動不同的伺服器。如果是Http協定則啟動Tomcat,如果是Dubbo協定則啟動Netty。

用戶端:

  1. 指定傳輸的資料類型,包括接口(服務)名、方法名、參數類型、參數名,并封裝成一個類Invocation。
  2. 當用戶端調用某個接口時,采用jdk動态代理的方式,調用invoke代理方法,在代理方法中做增強邏輯。邏輯如下:

    2.1:填充資料類型Invocation

    2.2:從zk或redis中根據服務名拉取伺服器位址,并負載均衡到某一個伺服器位址下

    2.3:擷取用戶端協定(dubbo 或 http),并根據協定向服務端發送資料Invocation

  3. 用戶端DispartchServlet攔截到用戶端發過來的請求。通過JSON序列化二進制資料為Invocation 對象。根據對象中的接口名,從本地緩存中拿到對應的實作類,利用反射調用用戶端用戶端想要調用的方法,并輸出。完成了遠端服務調用!

2. Dubbo的基本使用

首先附上dubbo官方使用文檔:https://dubbo.apache.org/zh/docs/v2.7/user/examples/loadbalance/

2.1 Dubbo是什麼?

Dubbo的負載均衡、叢集容錯、服務降級等機制詳解1. Dubbo與RPC的關系2. Dubbo的基本使用3. dubbo的REST協定4. dubbo的控制台5. dubbo的服務路由

        Apache Dubbo 是一款高性能、輕量級的開源 Java 服務架構,提供了六大核心能力:面向接口代理的高性能RPC調用,智能容錯和負載均衡,服務自動注冊和發現,高度可擴充能力,運作期流量排程,可視化的服務治理與運維。

其中有以下幾個關鍵點

  • 注冊與發現

    :Dubbo使用zookeeper做服務的注冊中心,就是服務的提供者以臨時節點的形式将服務Server資訊注冊儲存到Zookeeper的dubbo目錄下的provider的節點下,供消費者發現調用。
  • 負載均衡

    : Dubbo支援負載均衡政策,就是同一個Dubbo服務被多台伺服器啟用後,會在在Zookeeper提供者節點下顯示多個相同接口名稱節點。消費者在調用Dubbo負載均衡服務時,采用權重的算法政策選擇具體某個伺服器上的服務,權重政策以*2倍數設定。
  • 容錯機制

    :Dubbo的提供者在Zookeeper上使用的是臨時節點,一旦提供者所在服務挂掉,該節點的客服端連接配接将會關閉,故節點自動消失。是以消費者調用接口時将不會輪詢到已經挂掉的接口上(延遲例外)。
  • Dubbo容器

    :Dubbo在java jvm中有自己的容器,和Spring IOC的bean一樣,将服務對象儲存到自己的容器中。
  • 監控中心

    :監控中心主要是用來服務監控和服務治理。服務治理包含:負載均衡政策、服務狀态、容錯、路由規則限定、服務降級等。具體可以下載下傳Dubbo監控中心用戶端檢視與設定。
  • Dubbo的協定

    :點選連結擷取更多協定的詳細資訊
①:dubbo協定: Dubbo預設協定是dubbo協定,采用單一長連接配接和 NIO 異步通訊,基于hessian作為序列化協定,适合于資料量小但并發高的服務調用,以及服務消費者機器數遠大于服務提供者機器數的情況。
②:hessian協定: Hessian底層采用Http通訊(同步),走hessian序列化協定。适用于提供者數量比消費者數量還多,适用于檔案的傳輸,一般較少用
③:http協定: 走json序列化,适用于浏覽器檢視,同時給應用程式和浏覽器JS使用的服務。
④:rmi協定:走java二進制序列化,多個短連接配接,适合消費者和提供者數量差不多,适用于檔案的傳輸,一般較少用
⑤:webservice協定:采用SOAP文本序列化,适用HTTP傳輸,常用于系統內建,跨語言調用
⑥:redis協定:基于 Redis實作的 RPC 協定。
⑦:rest協定:基于标準的Java REST API實作的REST調用支援

2.2 負載均衡

        生産者在為某個接口暴露服務時,可以根據協定、ip、端口号、服務、group、version等六要素暴露多個接口執行個體,達到類似于叢集的形式。如下所示:任意修改某個要素就算是這個接口已暴露的執行個體!在代碼中可以通過修改@Service注解的值來暴露不同的服務執行個體

@Service(interfaceName = "com.tuling.DemoService", version = "generic")

這樣就會暴露

http://ip:port/DemoService + generic

服務,消費時要根據生産者暴露的規則來進行消費。

Dubbo的負載均衡、叢集容錯、服務降級等機制詳解1. Dubbo與RPC的關系2. Dubbo的基本使用3. dubbo的REST協定4. dubbo的控制台5. dubbo的服務路由

        如果在

application.properties

配置檔案中,配置了多個協定,Dubbo會預設會根據配置暴露多個服務執行個體,如果做下面的配置,那麼上面的DemoService接口在zookeeper上就會有兩個服務執行個體,一個Http的,一個Dubbo的!

# 配置多協定

# dubbo協定
dubbo.protocols.p1.id=dubbo1
dubbo.protocols.p1.name=dubbo
dubbo.protocols.p1.port=20881
dubbo.protocols.p1.host=0.0.0.0
# http協定
dubbo.protocols.p2.id=dubbo2
dubbo.protocols.p2.name=http
dubbo.protocols.p2.port=20882
dubbo.protocols.p2.host=0.0.0.0
           
Dubbo的負載均衡、叢集容錯、服務降級等機制詳解1. Dubbo與RPC的關系2. Dubbo的基本使用3. dubbo的REST協定4. dubbo的控制台5. dubbo的服務路由

        那麼面對多個服務執行個體,消費端調用時是如何進行選擇的呢?Dubbo為我們提供了四種負載均衡政策,可以通過負載均衡政策來選擇一個服務執行個體進行調用!預設的負載政策為

random

随機調用。四種政策如下:

  • Random 随機

    :按權重設定随機機率,可通過配置權重修改機率
  • RoundRobin 輪詢

    :存在慢的提供者累積請求的問題,比如:第二台機器很慢,但沒挂,當請求調到第二台時就卡在那,久而久之,所有請求都卡在調到第二台上。
  • LeastActive 最少活躍數

    :活躍數是指調用前後的計數差,服務調用越快,活躍數越小。提供者越慢,接收的請求就越少,因為越慢的提供者的調用前後計數差會越大,活躍數也會變大
  • ConsistentHash 一緻性Hash

    :相同參數的請求總是發到同一提供者。

注意:比較難了解的是LeastActive 最少活躍數,理論上最少活躍數應該是在服務提供者端進行統計的,服務提供者統計有多少個請求正在執行中。但是Dubbo卻選擇

在消費端進行統計最少活躍數

,為什麼能在消費端進行統計?邏輯如下:

  1. 消費者會緩存所調用服務的所有提供者,比如記為p1、p2、p3三個服務提供者,每個提供者内都有一個屬性記為active,預設位0
  2. 消費者在調用次服務時,如果負載均衡政策是leastactive
  3. 消費者端會判斷緩存的所有服務提供者的active,選擇最小的,如果都相同,則随機選出某一個服務提供者後,假設位p2,Dubbo就會對p2.active+1
  4. 然後真正送出請求調用該服務
  5. 消費端收到響應結果後,對p2.active-1
  6. 這樣就完成了對某個服務提供者目前活躍調用數進行了統計,并且并不影響服務調用的性能,下次調用會再次判斷最小的active,這就解釋了為什麼服務提供者越慢,接收的請求就越少!因為它的active值大!

配置方式

  • Provider端配置:生産者通過在暴露服務的@Servic注解上進行配置:

    @Service(loadbalance = "roundrobin")

    ,配置時需要注意負載均衡方式均為小寫!
  • Consumer端配置:消費端通過

    @Reference(loadbalance = "leastactive ")

如果Provider和Consumer都配置,則以Consumer端配置的為準!

2.3 服務逾時

在服務提供者(服務端)和服務消費者上都可以配置服務逾時時間,這兩者是不一樣的。

@Service(version = "timeout", timeout = 4000)	//服務提供者端逾時時間
@Reference(version = "timeout", timeout = 3000,retries = 1)	//服務消費者端逾時時間
           

消費者調用一個服務,分為三步:

  1. 消費者發送請求(網絡傳輸)
  2. 服務端執行服務
  3. 服務端傳回響應(網絡傳輸)

如果在服務端和消費端隻在其中一方配置了timeout

         那麼沒有歧義,表示消費端調用服務的逾時時間,消費端如果超過時間還沒有收到響應結果,則消費端會抛逾時異常,但,服務端不會抛異常,服務端在執行服務後,會檢查執行該服務的時間,如果超過timeout,則會列印一個逾時日志。服務會正常的執行完。

如果在服務端和消費端各配了一個timeout

那情況就比較複雜了,假設

  • 服務執行為5s
  • 消費端timeout=3s
  • 服務端timeout=6s

那麼消費端調用服務時,消費端會收到逾時異常(因為消費端逾時了),服務端一切正常(服務端沒有逾時)。

如果

配置服務端timeout=4s,那麼由于服務執行為5s,是以服務端也會列印警告,辨別服務端也逾時了!

2.4 叢集容錯

         一個服務提供多個執行個體(叢集),叢集容錯是指:叢集容錯表示:服務消費者在調用某個服務時,這個服務有多個服務提供者,在經過負載均衡後選出其中一個服務提供者之後進行調用,但調用報錯後,Dubbo所采取的後續處理政策。如圖:如果服務執行個體1調用失敗,則會嘗試調用服務執行個體2或者3,預設重試2次。

Dubbo的負載均衡、叢集容錯、服務降級等機制詳解1. Dubbo與RPC的關系2. Dubbo的基本使用3. dubbo的REST協定4. dubbo的控制台5. dubbo的服務路由

叢集容錯可以在@Service 和 @Reference注解上進行配置:如果兩者都配置,以消費端為主!

@Service( cluster = "failfast")	//服務端超叢集容錯
@Reference(cluster = "failfast") //消費端叢集容錯
           

Dubbo提供了六種叢集容錯方案:

  1. Failover Cluster:失敗自動切換

    當出現失敗,重試其它伺服器。通常用于讀操作,但重試會帶來更長延遲。可通過 retries=“2” 來設定重試次數(不含第一次)。
  2. Failfast Cluster:快速失敗

    隻發起一次調用,失敗立即報錯。通常用于非幂等性的寫操作,比如新增記錄
  3. Failsafe Cluster:失敗安全

    出現異常時,不抛異常,直接忽略。通常用于寫入審計日志等操作
  4. Failback Cluster:失敗自動恢複

    背景記錄失敗請求,定時重發。通常用于消息通知操作
  5. Forking Cluster:并行調用多個伺服器

    隻要一個成功即傳回。通常用于實時性要求較高的讀操作,但需要浪費更多服務資源。可通過 forks=“2” 來設定最大并行數
  6. Broadcast Cluster:廣播調用所有提供者

    逐個調用,任意一台報錯則報錯。通常用于通知所有提供者更新緩存或日志等本地資源資訊

2.5 服務降級

         服務降級表示:服務消費者在調用某個服務提供者時,如果該服務提供者報錯了,所采取的措施。可以通過服務降級功能臨時屏蔽某個出錯的非關鍵服務,并定義降級後的傳回政策。叢集容錯和服務降級的差別在于:

  1. 叢集容錯是整個叢集範圍内的容錯
  2. 服務降級是單個服務提供者的自身容錯

服務降級可以在消費端的 @Reference注解上使用mock來指定降級方案

  • mock=force:return+null

    表示消費方對該服務的方法調用都直接傳回 null 值,不發起遠端調用。用來屏蔽不重要服務不可用時對調用方的影響。
  • mock=fail:return+null

    表示消費方對該服務的方法調用在失敗後,再傳回 null 值,不抛異常。用來容忍不重要服務不穩定時對調用方的影響。
//服務降級:如果調用失敗傳回123
 @Reference(version = "timeout", timeout = 1000, mock = "fail: return 123")  
           

更多服務降級方案可參考本地僞裝:https://dubbo.apache.org/zh/docs/v2.7/user/examples/local-mock/

本地僞裝其實也是對Mock的應用,便于服務端在用戶端執行容錯邏輯

2.6 本地存根

        消費端通過Dubbo遠端調用服務端,其業務實作基本都在服務端。但有些時候想在消費端也執行部分邏輯,比如:做

ThreadLocal 緩存(這個用處最大),提前驗證參數,調用失敗後僞造容錯資料

等等,此時就需要在@Reference中帶上 Stub,消費端生成服務的代理 Proxy 執行個體,會把 Proxy 通過構造函數傳給 Stub,然後把 Stub 暴露給使用者,Stub 可以決定要不要去調 Proxy。

//本地存根,開啟stub
@Reference(stub = "true")
或者
@Reference(stub = "com.foo.DemoServiceStub") //指定stub對象
           

還需要自定義一個類實作DemoService接口,表示為DemoService做的本地存根,這個類是放在消費端的

public class DemoServiceStub implements DemoService {

    private final DemoService demoService;

    // 構造函數傳入真正的遠端代理對象
    public DemoServiceStub(DemoService demoService){
        this.demoService = demoService;
    }

    @Override
    public String sayHello(String name) {
        // 此代碼在用戶端執行, 你可以在用戶端做ThreadLocal本地緩存,或預先驗證參數是否合法,等等
        try {
            return demoService.sayHello(name); // safe  null
        } catch (Exception e) {
            // 你可以容錯,可以做任何AOP攔截事項
            return "容錯資料";
        }
    }
}
           

注意:

實作類中必須有一個傳入遠端 DemoService 執行個體的構造函數

使用上述存根代碼執行後,如果調用失敗,則會執行DemoServiceStub中的容錯方案,控制台列印”容錯資料“!

2.7 參數回調

參數回調是指:當消費端調用服務成功後,希望服務端能夠回調一下消費端的邏輯

既然是服務端回調消費端的邏輯,那麼這個邏輯一定是存在消費端的!以DemoService服務為例

消費端調用:

@Reference(version = "callback")
    private DemoService demoService;

	//調用服務
	demoService.sayHello("aaa", "d1", new DemoServiceListenerImpl())
           

上述代碼

new DemoServiceListenerImpl()

中要包含着具體的回調邏輯

// 回調邏輯接口
public interface DemoServiceListener {
    void changed(String msg);
}
           
// 回調邏輯實作類
public class DemoServiceListenerImpl implements DemoServiceListener {

    @Override
    public void changed(String msg) {
        System.out.println("被回調了:"+msg);
    }
}
           

服務端回調

// DemoService接口
public interface DemoService {

    // 回調方法
    default String sayHello(String name, String key, DemoServiceListener listener) {
        return null;
    };
}
           
// @Method注明了是sayHello()中索引為2的參數參與了回調,以及最大同時支援3個回調,上述代碼隻有一個,如果寫4個就報錯
@Service(version = "callback", methods = {@Method(name = "sayHello", arguments = {@Argument(index = 2, callback = true)})}, callbacks = 3)
public class CallBackDemoService implements DemoService {
    @Override
    public String sayHello(String name, String key, DemoServiceListener callback) {
        
        callback.changed(); //代理對象直接回調消費端的changed方法
        
        return "";  // 正常通路
    }
}
           

在服務端回調時,需要注意:

  • sayHello()

    方法中的DemoServiceListener為代理對象,并不是消費端傳過來的DemoServiceListenerImpl對象
  • 需要在

    @Service

    中使用

    @Method

    注明是哪個方法中哪個參數參與了回調,以及最大同時支援幾個回調

結果:

在消費端列印的是changed的内容,但這個方法是在服務端被執行的

Dubbo的負載均衡、叢集容錯、服務降級等機制詳解1. Dubbo與RPC的關系2. Dubbo的基本使用3. dubbo的REST協定4. dubbo的控制台5. dubbo的服務路由

2.8 異步調用

         上文所講的内容都是依托于同步調用的,Dubbo也提供了異步調用方式,異步調用與同步調用的求别在于:

  • 服務端需要使用

    CompletableFuture.supplyAsync

    開啟一個線程執行任務
  • 用戶端需要使用

    CompletableFuture.whenComplete

    監聽異步線程執行完畢

消費端代碼示例:

@Reference(version = "async")
    private DemoService demoService;


    public static void main(String[] args) throws IOException {
        ConfigurableApplicationContext context = SpringApplication.run(AsyncDubboConsumerDemo.class);

        DemoService demoService = context.getBean(DemoService.class);

        // 調用直接傳回CompletableFuture
        CompletableFuture<String> future = demoService.sayHelloAsync("異步調用");  // 5

		//這個方法隻有等異步線程執行結束才會調用
        future.whenComplete((v, t) -> {
            if (t != null) {
                t.printStackTrace();
            } else {
                System.out.println("Response: " + v);
            }
        });

        System.out.println("結束了");

    }
           

服務端代碼示例

public interface DemoService {
    // 同步調用方法
    String sayHello(String name);

    // 異步調用方法
    default CompletableFuture<String> sayHelloAsync(String name) {
        return null;
    };

}

           
@Service(version = "async")
public class AsyncDemoService implements DemoService {

	//同步調用
    @Override
    public String sayHello(String name) {
        System.out.println("sayhello方法 " + name);
        return name;
    }

	// 主要關注這個異步調用
    @Override
    public CompletableFuture<String> sayHelloAsync(String name) {
        System.out.println("執行了異步服務" + name);

		//相當于在異步線程裡執行sayHello方法!
        return CompletableFuture.supplyAsync(() -> {
            return sayHello(name);
        });
    }
}
           

執行結果如下:

消費端:

Dubbo的負載均衡、叢集容錯、服務降級等機制詳解1. Dubbo與RPC的關系2. Dubbo的基本使用3. dubbo的REST協定4. dubbo的控制台5. dubbo的服務路由

服務端:

Dubbo的負載均衡、叢集容錯、服務降級等機制詳解1. Dubbo與RPC的關系2. Dubbo的基本使用3. dubbo的REST協定4. dubbo的控制台5. dubbo的服務路由

可以看到他們之間列印的順序也是異步的展現!

2.9 泛化調用、泛化服務

         泛化調用: 在Dubbo中,如果某個服務想要支援泛化調用,就可以将該服務的generic屬性設定為true,那對于服務消費者來說,就可以不用依賴該服務的接口,直接利用GenericService接口來進行服務調用。泛化調用可以用來做服務測試。

@EnableAutoConfiguration
public class GenericDubboConsumerDemo {

	//調用DemoService服務,并不需要注入DemoService,也不需要引入依賴
    @Reference(id = "demoService", version = "default", interfaceName = "com.tuling.DemoService", generic = true)
    private GenericService genericService;

    public static void main(String[] args) throws IOException {
        ConfigurableApplicationContext context = SpringApplication.run(GenericDubboConsumerDemo.class);
		
        GenericService genericService = (GenericService) context.getBean("demoService");

        Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{"周瑜"});
        System.out.println(result);
        
    }
}
           

         泛化服務: 可以不實作具體的某個接口,而是實作

GenericService

接口,并在

@Service

上标明接口名即可,在調用直接注入DemoService就可以使用!

@Service(interfaceName = "com.tuling.DemoService", version = "generic")
public class GenericDemoService implements GenericService {

    @Override
    public Object $invoke(String s, String[] strings, Object[] objects) throws GenericException {
        System.out.println("執行了generic服務");

        return "執行的方法是" + s;
    }
}

           

3. dubbo的REST協定

         dubbo支援多種遠端調用方式,例如dubbo RPC(二進制序列化 + tcp協定)、http invoker(二進制序列化 + http協定)、hessian(二進制序列化 + http協定)、WebServices (文本序列化 + http協定)、REST(文本序列化 + http協定)等等的支援。

         當我們用Dubbo提供了一個服務後,如果消費者沒有使用Dubbo也想調用服務,那麼這個時候就可以讓我們的服務支援REST協定,這樣消費者就可以通過REST形式調用我們的服務了。更多REST協定内容點選檢視官網!

①:服務端配置檔案修改協定為rest

dubbo.protocol.name=rest
           

②:服務端實作:使用@Path指定Rest風格的通路路徑(注意:所有暴露的服務都必須加@Path)

@Service(version = "rest")
@Path("demo")
public class RestDemoService implements DemoService {

    @GET
    @Path("say")
    @Produces({ContentType.APPLICATION_JSON_UTF_8, ContentType.TEXT_XML_UTF_8})
    @Override
    public String sayHello(@QueryParam("name") String name) {
        System.out.println("執行了rest服務" + name);

        URL url = RpcContext.getContext().getUrl();
        return String.format("%s: %s, Hello, %s", url.getProtocol(), url.getPort(), name);  // 正常通路
    }

}
           

這樣就可以通過浏覽器通路這個服務了,其他消費端也可以直接通過HttpCliet等非dubbo的形式去調用服務!

4. dubbo的控制台

5. dubbo的服務路由

經過服務路由可以配置黑名單、白名單、讀寫分離、隔離不同機房網段等等,這點在官網有很詳細的解釋 點選檢視官網詳情!!!

dubbo提供的标簽路由還可以用來釋出版本,什麼是藍綠釋出、灰階釋出?

繼續閱讀