12437字,帶你深入探究RPC通訊原理

為什么要學(xué)習(xí)RPC

如下是Http請求案例:

file

請求過程會有3次握手4次揮手:

1:瀏覽器請求服務(wù)器(訂單服務(wù)),請求建立鏈接                              1次握手
2:服務(wù)器(訂單服務(wù))響應(yīng)瀏覽器锅尘,可以建立鏈接继薛,并詢問瀏覽器是否可以建立鏈接      2次握手
3:瀏覽器響應(yīng)服務(wù)器(訂單服務(wù)),可以建立鏈接                               3次握手

------開始傳輸數(shù)據(jù)------

1:瀏覽器向服務(wù)端(訂單服務(wù))發(fā)起請求钉凌,要求斷開鏈接                          1次揮手
2:服務(wù)器(訂單服務(wù))回應(yīng)瀏覽器,數(shù)據(jù)還在傳輸中                              2次揮手
3:服務(wù)器(訂單服務(wù))接收完數(shù)據(jù)后,向?yàn)g覽器發(fā)消息要求斷開鏈接                  3次揮手
4:瀏覽器收到服務(wù)器消息后,回復(fù)服務(wù)器(訂單服務(wù))同意斷開鏈接                  4次揮手

1.1 PRC概述

RPC 的主要功能目標(biāo)是讓構(gòu)建分布式計(jì)算(應(yīng)用)更容易,在提供強(qiáng)大的遠(yuǎn)程調(diào)用能力時(shí)不損失本地調(diào)用的語義簡潔性捂人。為實(shí)現(xiàn)該目標(biāo)御雕,RPC 框架需提供一種透明調(diào)用機(jī)制,讓使用者不必顯式的區(qū)分本地調(diào)用和遠(yuǎn)程調(diào)用滥搭。

RPC的優(yōu)點(diǎn):

  • 分布式設(shè)計(jì)

  • 部署靈活

  • 解耦服務(wù)

  • 擴(kuò)展性強(qiáng)

RPC框架優(yōu)勢:

  • RPC框架一般使用長鏈接酸纲,不必每次通信都要3次握手,減少網(wǎng)絡(luò)開銷瑟匆。
  • RPC框架一般都有注冊中心闽坡,有豐富的監(jiān)控管理、發(fā)布脓诡、下線接口无午、動態(tài)擴(kuò)展等,對調(diào)用方來說是無感知祝谚、統(tǒng)一化的操作、協(xié)議私密酣衷,安全性較高
  • RPC 協(xié)議更簡單內(nèi)容更小交惯,效率更高,服務(wù)化架構(gòu)穿仪、服務(wù)化治理席爽,RPC框架是一個(gè)強(qiáng)力的支撐。
  • RPC基于TCP實(shí)現(xiàn)啊片,也可以基于Http2實(shí)現(xiàn)

1.2 RPC框架

主流RPC框架:

  • Dubbo:國內(nèi)最早開源的 RPC 框架只锻,由阿里巴巴公司開發(fā)并于 2011 年末對外開源,僅支持 Java 語言紫谷。
  • Motan:新浪微博內(nèi)部使用的 RPC 框架齐饮,于 2016 年對外開源捐寥,僅支持 Java 語言。
  • Tars:騰訊內(nèi)部使用的 RPC 框架祖驱,于 2017 年對外開源握恳,僅支持 C++ 語言。
  • Spring Cloud:國外 Pivotal 公司 2014 年對外開源的 RPC 框架捺僻,提供了豐富的生態(tài)組件乡洼。
  • gRPC:Google 于 2015 年對外開源的跨語言 RPC 框架,支持多種語言匕坯。
  • Thrift:最初是由 Facebook 開發(fā)的內(nèi)部系統(tǒng)跨語言的 RPC 框架束昵,2007 年貢獻(xiàn)給了 Apache 基金,成為 Apache 開源項(xiàng)目之一葛峻,支持多種語言妻怎。

1.3 應(yīng)用場景

應(yīng)用例舉:

  • 分布式操作系統(tǒng)的進(jìn)程間通訊
    進(jìn)程間通訊是操作系統(tǒng)必須提供的基本設(shè)施之一,分布式操作系統(tǒng)必須提供分布于異構(gòu)的結(jié)點(diǎn)機(jī)上進(jìn)程間的通訊機(jī)制,RPC是實(shí)現(xiàn)消息傳送模式的分布式進(jìn)程間通訊方式之一泞歉。
  • 構(gòu)造分布式設(shè)計(jì)的軟件環(huán)境
    由于分布式軟件設(shè)計(jì)逼侦,服務(wù)與環(huán)境的分布性, 它的各個(gè)組成成份之間存在大量的交互和通訊, RPC是其基本的實(shí)現(xiàn)方法之一。Dubbo分布式服務(wù)框架基于RPC實(shí)現(xiàn)腰耙,Hadoop也采用了RPC方式實(shí)現(xiàn)客戶端與服務(wù)端的交互榛丢。
  • 遠(yuǎn)程數(shù)據(jù)庫服務(wù)
    在分布式數(shù)據(jù)庫系統(tǒng)中,數(shù)據(jù)庫一般駐存在服務(wù)器上挺庞,客戶機(jī)通過遠(yuǎn)程數(shù)據(jù)庫服務(wù)功能訪問數(shù)據(jù)庫服務(wù)器晰赞,現(xiàn)有的遠(yuǎn)程數(shù)據(jù)庫服務(wù)是使用RPC模式的。例如选侨,Sybase和Oracle都提供了存儲過程機(jī)制掖鱼,系統(tǒng)與用戶定義的存儲過程存儲在數(shù)據(jù)庫服務(wù)器上,用戶在客戶端使用RPC模式調(diào)用存儲過程援制。
  • 分布式應(yīng)用程序設(shè)計(jì)
    RPC機(jī)制與RPC工具為分布式應(yīng)用程序設(shè)計(jì)提供了手段和方便, 用戶可以無需知道網(wǎng)絡(luò)結(jié)構(gòu)和協(xié)議細(xì)節(jié)而直接使用RPC工具設(shè)計(jì)分布式應(yīng)用程序戏挡。
  • 分布式程序的調(diào)試
    RPC可用于分布式程序的調(diào)試。使用反向RPC使服務(wù)器成為客戶并向它的客戶進(jìn)程發(fā)出RPC晨仑,可以調(diào)試分布式程序褐墅。例如,在服務(wù)器上運(yùn)行一個(gè)遠(yuǎn)端調(diào)試程序洪己,它不斷接收客戶端的RPC妥凳,當(dāng)遇到一個(gè)調(diào)試程序斷點(diǎn)時(shí),它向客戶機(jī)發(fā)回一個(gè)RPC答捕,通知斷點(diǎn)已經(jīng)到達(dá)逝钥,這也是RPC用于進(jìn)程通訊的例子。

2. 深入RPC原理

2.1 設(shè)計(jì)與調(diào)用流程

file

具體調(diào)用過程:

  1. 服務(wù)消費(fèi)者(client客戶端)通過本地調(diào)用的方式調(diào)用服務(wù)拱镐。
  2. 客戶端存根(client stub)接收到請求后負(fù)責(zé)將方法艘款、入?yún)⒌刃畔⑿蛄谢ńM裝)成能夠進(jìn)行網(wǎng)絡(luò)傳輸?shù)南Ⅲw持际。
  3. 客戶端存根(client stub)找到遠(yuǎn)程的服務(wù)地址,并且將消息通過網(wǎng)絡(luò)發(fā)送給服務(wù)端磷箕。
  4. 服務(wù)端存根(server stub)收到消息后進(jìn)行解碼(反序列化操作)选酗。
  5. 服務(wù)端存根(server stub)根據(jù)解碼結(jié)果調(diào)用本地的服務(wù)進(jìn)行相關(guān)處理。
  6. 本地服務(wù)執(zhí)行具體業(yè)務(wù)邏輯并將處理結(jié)果返回給服務(wù)端存根(server stub)岳枷。
  7. 服務(wù)端存根(server stub)將返回結(jié)果重新打包成消息(序列化)并通過網(wǎng)絡(luò)發(fā)送至消費(fèi)方芒填。
  8. 客戶端存根(client stub)接收到消息,并進(jìn)行解碼(反序列化)空繁。
  9. 服務(wù)消費(fèi)方得到最終結(jié)果殿衰。

所涉及的技術(shù):

  1. 動態(tài)代理

    生成Client Stub(客戶端存根)和Server Stub(服務(wù)端存根)的時(shí)候需要用到j(luò)ava動態(tài)代理技術(shù)。

  2. 序列化
    在網(wǎng)絡(luò)中盛泡,所有的數(shù)據(jù)都將會被轉(zhuǎn)化為字節(jié)進(jìn)行傳送闷祥,需要對這些參數(shù)進(jìn)行序列化和反序列化操作。

    目前主流高效的開源序列化框架有Kryo傲诵、fastjson凯砍、Hessian、Protobuf等拴竹。

  3. NIO通信

    Java 提供了 NIO 的解決方案悟衩,Java 7 也提供了更優(yōu)秀的 NIO.2 支持∷ò荩可以采用Netty或者mina框架來解決NIO數(shù)據(jù)傳輸?shù)膯栴}座泳。開源的RPC框架Dubbo就是采用NIO通信,集成支持netty幕与、mina挑势、grizzly。

  4. 服務(wù)注冊中心

    通過注冊中心啦鸣,讓客戶端連接調(diào)用服務(wù)端所發(fā)布的服務(wù)潮饱。主流的注冊中心組件:Redis、Nacos赏陵、Zookeeper饼齿、Consul 、Etcd蝙搔。Dubbo采用的是ZooKeeper提供服務(wù)注冊與發(fā)現(xiàn)功能。

  5. 負(fù)載均衡

    在高并發(fā)的場景下考传,需要多個(gè)節(jié)點(diǎn)或集群來提升整體吞吐能力吃型。

  6. 健康檢查

    健康檢查包括,客戶端心跳和服務(wù)端主動探測兩種方式僚楞。

2.2 RPC深入解析

2.2.1 序列化技術(shù)

file
  • 序列化的作用

    在網(wǎng)絡(luò)傳輸中勤晚,數(shù)據(jù)必須采用二進(jìn)制形式枉层, 所以在RPC調(diào)用過程中, 需要采用序列化技術(shù)赐写,對入?yún)ο蠛头祷刂祵ο筮M(jìn)行序列化與反序列化鸟蜡。

  • 序列化原理

    自定義的二進(jìn)制協(xié)議來實(shí)現(xiàn)序列化:


    file

    一個(gè)對象是如何進(jìn)行序列化? 下面以User對象例舉講解:

    User對象:

    package com.itcast;
    public class User {
    
        /**
         * 用戶編號
         */
        private String userNo = "0001";
    
        /**
         * 用戶名稱
         */
        private String name = "zhangsan";
    
    }
    
    

    包體的數(shù)據(jù)組成:

    業(yè)務(wù)指令為0x00000001占1個(gè)字節(jié),類的包名com.itcast占10個(gè)字節(jié), 類名User占4個(gè)字節(jié);

    屬性UserNo名稱占6個(gè)字節(jié)挺邀,屬性類型string占2個(gè)字節(jié)表示揉忘,屬性值為0001占4個(gè)字節(jié);

    屬性name名稱占4個(gè)字節(jié)端铛,屬性類型string占2個(gè)字節(jié)表示泣矛,屬性值為zhangsan占8個(gè)字節(jié);

    包體共計(jì)占有1+10+4+6+2+4+4+2+8 = 41字節(jié)禾蚕。

    包頭的數(shù)據(jù)組成:

    版本號v1.0占4個(gè)字節(jié)您朽,消息包體實(shí)際長度為41占4個(gè)字節(jié)表示,序列號0001占4個(gè)字節(jié)换淆,校驗(yàn)碼32位表示占4個(gè)字節(jié)哗总。

    包頭共計(jì)占有4+4+4+4 = 16字節(jié)。

    包尾的數(shù)據(jù)組成:

    通過回車符標(biāo)記結(jié)束\r\n倍试,占用1個(gè)字節(jié)讯屈。

    整個(gè)包的序列化二進(jìn)制字節(jié)流共41+16+1 = 58字節(jié)。這里講解的是整個(gè)序列化的處理思路易猫, 在實(shí)際的序列化處理中還要考慮更多細(xì)節(jié)耻煤,比如說方法和屬性的區(qū)分,方法權(quán)限的標(biāo)記准颓,嵌套類型的處理等等哈蝇。

  • 序列化的處理要素

    1. 解析效率:序列化協(xié)議應(yīng)該首要考慮的因素,像xml/json解析起來比較耗時(shí)攘已,需要解析dom樹炮赦,二進(jìn)制自定義協(xié)議解析起來效率要快很多软能。
    2. 壓縮率:同樣一個(gè)對象心俗,xml/json傳輸起來有大量的標(biāo)簽冗余信息,信息有效性低母谎,二進(jìn)制自定義協(xié)議占用的空間相對來說會小很多峡眶。
    3. 擴(kuò)展性與兼容性:是否能夠利于信息的擴(kuò)展,并且增加字段后舊版客戶端是否需要強(qiáng)制升級峭拘,這都是需要考慮的問題鸡挠,在自定義二進(jìn)制協(xié)議時(shí)候,要做好充分考慮設(shè)計(jì)彭沼。
    4. 可讀性與可調(diào)試性:xml/json的可讀性會比二進(jìn)制協(xié)議好很多备埃,并且通過網(wǎng)絡(luò)抓包是可以直接讀取姓惑,二進(jìn)制則需要反序列化才能查看其內(nèi)容。
    5. 跨語言:有些序列化協(xié)議是與開發(fā)語言緊密相關(guān)的瓜喇,例如dubbo的Hessian序列化協(xié)議就只能支持Java的RPC調(diào)用挺益。
    6. 通用性:xml/json非常通用,都有很好的第三方解析庫乘寒,各個(gè)語言解析起來都十分方便望众,二進(jìn)制數(shù)據(jù)的處理方面也有Protobuf和Hessian等插件,在做設(shè)計(jì)的時(shí)候盡量做到較好的通用性伞辛。
  • 常用的序列化技術(shù)

    1. JDK原生序列化

      代碼:

      ...
          public static void main(String[] args) throws IOException, ClassNotFoundException {
              String basePath =  "D:/TestCode";
              FileOutputStream fos = new FileOutputStream(basePath + "tradeUser.clazz");
              TradeUser tradeUser = new TradeUser();
              tradeUser.setName("Mirson");
              ObjectOutputStream oos = new ObjectOutputStream(fos);
              oos.writeObject(tradeUser);
              oos.flush();
              oos.close();
              FileInputStream fis = new FileInputStream(basePath + "tradeUser.clazz");
              ObjectInputStream ois = new ObjectInputStream(fis);
              TradeUser deStudent = (TradeUser) ois.readObject();
              ois.close();
              System.out.println(deStudent);
          }
      ...
      

      (1) 在Java中烂翰,序列化必須要實(shí)現(xiàn)java.io.Serializable接口。

      (2) 通過ObjectOutputStream和ObjectInputStream對象進(jìn)行序列化及反序列化操作蚤氏。

      (3) 虛擬機(jī)是否允許反序列化甘耿,不僅取決于類路徑和功能代碼是否一致佳恬,一個(gè)非常重要的一點(diǎn)是兩個(gè)類的序列化 ID 是否一致
      (也就是在代碼中定義的序列ID private static final long serialVersionUID)

      (4) 序列化并不會保存靜態(tài)變量贰剥。

      (5) 要想將父類對象也序列化,就需要讓父類也實(shí)現(xiàn)Serializable 接口。

      (6) Transient 關(guān)鍵字的作用是控制變量的序列化乖菱,在變量聲明前加上該關(guān)鍵字帆锋,可以阻止該變量被序列化到文件中,在被反序列化后,transient 變量的值被設(shè)為初始值摄乒,如基本類型 int為 0梨水,封裝對象型Integer則為null。

      (7) 服務(wù)器端給客戶端發(fā)送序列化對象數(shù)據(jù)并非加密的,如果對象中有一些敏感數(shù)據(jù)比如密碼等,那么在對密碼字段序列化之前奕短,最好做加密處理, 這樣可以一定程度保證序列化對象的數(shù)據(jù)安全日杈。

    2. JSON序列化

      一般在HTTP協(xié)議的RPC框架通信中,會選擇JSON方式填硕。

      優(yōu)勢:JSON具有較好的擴(kuò)展性姻檀、可讀性和通用性狭莱。

      缺陷:JSON序列化占用空間開銷較大,沒有JAVA的強(qiáng)類型區(qū)分,需要通過反射解決,解析效率和壓縮率都較差痕檬。

      如果對并發(fā)和性能要求較高唁桩,或者是傳輸數(shù)據(jù)量較大的場景,不建議采用JSON序列化方式碍现。

    3. Hessian2序列化

      Hessian 是一個(gè)動態(tài)類型躏升,二進(jìn)制序列化佃却,并且支持跨語言特性的序列化框架瘤泪。

      Hessian 性能上要比 JDK、JSON 序列化高效很多惶洲,并且生成的字節(jié)數(shù)也更小。有非常好的兼容性和穩(wěn)定性,所以 Hessian 更加適合作為 RPC 框架遠(yuǎn)程通信的序列化協(xié)議。

      代碼示例:

      ...
      TradeUser tradeUser = new TradeUser();
      tradeUser.setName("Mirson");
      
      //tradeUser對象序列化處理
      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      Hessian2Output output = new Hessian2Output(bos);
      output.writeObject(tradeUser);
      output.flushBuffer();
      byte[] data = bos.toByteArray();
      bos.close();
      
      //tradeUser對象反序列化處理
      ByteArrayInputStream bis = new ByteArrayInputStream(data);
      Hessian2Input input = new Hessian2Input(bis);
      TradeUser deTradeUser = (TradeUser) input.readObject();
      input.close();
      
      System.out.println(deTradeUser);
      ...
      

      Dubbo Hessian Lite序列化流程:


      file

      Dubbo Hessian Lite反序列化流程:

    file

    Hessian自身也存在一些缺陷耘分,大家在使用過程中要注意:

    • 對Linked系列對象不支持计盒,比如LinkedHashMap拔第、LinkedHashSet 等,但可以通過CollectionSerializer類修復(fù)。

    • Locale 類不支持,可以通過擴(kuò)展 ContextSerializerFactory 類修復(fù)拷呆。

    • Byte/Short 在反序列化的時(shí)候會轉(zhuǎn)成 Integer啥供。

Dubbo2.7.3通訊序列化源碼實(shí)現(xiàn)分析

  • 序列化實(shí)現(xiàn)流程:
file
ExchangeCodec的encode方法:

```java
    @Override
    public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws IOException {
        if (msg instanceof Request) {
            encodeRequest(channel, buffer, (Request) msg);
        } else if (msg instanceof Response) {
            encodeResponse(channel, buffer, (Response) msg);
        } else {
            super.encode(channel, buffer, msg);
        }
    }
```
  • 反序列化實(shí)現(xiàn)流程:
file
源碼:

ExchangeCodec的decode方法:

```java
    @Override
    public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
        int readable = buffer.readableBytes();
        byte[] header = new byte[Math.min(readable, HEADER_LENGTH)];
        buffer.readBytes(header);
        return decode(channel, buffer, readable, header);
    }

```

ExchangeCodec的decodeBody方法:

```java
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
    ...
    } else {
            // decode request.
            Request req = new Request(id);
            req.setVersion(Version.getProtocolVersion());
            req.setTwoWay((flag & FLAG_TWOWAY) != 0);
            if ((flag & FLAG_EVENT) != 0) {
                req.setEvent(true);
            }
            try {
                ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is,                 proto);   
            }
    ...
}
```
  1. Protobuf序列化

    Protobuf 是 Google 推出的開源序列庫,它是一種輕便、高效的結(jié)構(gòu)化數(shù)據(jù)存儲格式属划,可以用于結(jié)構(gòu)化數(shù)據(jù)序列化,支持 Java、Python菱农、C++、Go 等多種語言舅巷。

    Protobuf 使用的時(shí)候需要定義 IDL(Interface description language)飒房,然后使用不同語言的 IDL 編譯器褥芒,生成序列化工具類,它具備以下優(yōu)點(diǎn):

    • 壓縮比高,體積小颜及,序列化后體積相比 JSON、Hessian 小很多爱葵;
    • IDL 能清晰地描述語義辆雾,可以幫助并保證應(yīng)用程序之間的類型不會丟失,無需類似 XML 解析器惭墓;
    • 序列化反序列化速度很快钧萍,不需要通過反射獲取類型;
    • 消息格式的擴(kuò)展、升級和兼容性都不錯(cuò),可以做到向后兼容挖炬。

    代碼示例:

    Protobuf腳本定義:

    // 定義Proto版本
    syntax = "proto3";
    // 是否允許生成多個(gè)JAVA文件
    option java_multiple_files = false;
    // 生成的包路徑
    option java_package = "com.itcast.bulls.stock.struct.netty.trade";
    // 生成的JAVA類名
    option java_outer_classname = "TradeUserProto";
    
    
    // 預(yù)警通知消息體
    message TradeUser {
    
        /**
         * 用戶ID
         */
        int64 userId = 1 ;
        
        /**
         * 用戶名稱
         */
        string userName = 2 ;
    }
    
    

    代碼操作:

    // 創(chuàng)建TradeUser的Protobuf對象
    TradeUserProto.TradeUser.Builder builder = TradeUserProto.TradeUser.newBuilder();
    builder.setUserId(101);
    builder.setUserName("Mirson");
    
    //將TradeUser做序列化處理
    TradeUserProto.TradeUser msg = builder.build();
    byte[] data = msg.toByteArray();
    
    //反序列化處理, 將剛才序列化的byte數(shù)組轉(zhuǎn)化為TradeUser對象
    TradeUserProto.TradeUser deTradeUser = TradeUserProto.TradeUser.parseFrom(data);
    System.out.println(deTradeUser);
    

2.2.2 動態(tài)代理

file
  • 內(nèi)部接口如何調(diào)用實(shí)現(xiàn)?

    RPC的調(diào)用對用戶來講是透明的钓猬,那內(nèi)部是如何實(shí)現(xiàn)呢?內(nèi)部核心技術(shù)采用的就是動態(tài)代理,RPC 會自動給接口生成一個(gè)代理類,當(dāng)我們在項(xiàng)目中注入接口的時(shí)候,運(yùn)行過程中實(shí)際綁定的是這個(gè)接口生成的代理類面褐。在接口方法被調(diào)用的時(shí)候闻蛀,它實(shí)際上是被生成代理類攔截到了薪棒,這樣就可以在生成的代理類里面,加入其他調(diào)用處理邏輯,比如連接負(fù)載管理吨述,日志記錄等等冰啃。

    JDK動態(tài)代理:

    被代理對象必須實(shí)現(xiàn)1個(gè)接口
    
  • JDK動態(tài)代理的如何實(shí)現(xiàn)?

    實(shí)例代碼:

    public class JdkProxyTest {
    
        /**
         * 定義用戶的接口
         */
        public interface User {
            String job();
        }
    
    
        /**
         * 實(shí)際的調(diào)用對象
         */
        public static class Teacher {
    
            public String invoke(){
                return "i'm Teacher";
            }
        }
    
    
        /**
         * 創(chuàng)建JDK動態(tài)代理類
         */
        public static class JDKProxy implements InvocationHandler {
            private Object target;
    
            JDKProxy(Object target) {
                this.target = target;
            }
    
            @Override
            public Object invoke(Object proxy, Method method, Object[] paramValues) {
                return ((Teacher)target).invoke();
            }
        }
    
    
            public static void main(String[] args){
                // 構(gòu)建代理器
                JDKProxy proxy = new JDKProxy(new Teacher());
                ClassLoader classLoader = ClassLoaderUtils.getClassLoader();
    
                // 生成代理類
                User user = (User) Proxy.newProxyInstance(classLoader, new Class[]{User.class}, proxy);
    
                // 接口調(diào)用
                System.out.println(user.job());
            }
    }
    
    

    JDK動態(tài)代理的實(shí)現(xiàn)原理:

file

JDK內(nèi)部如何處理?

反編譯生成的代理類可以知道汪榔,代理類 $Proxy里面會定義相同簽名的接口(也就是上面代碼User的job接口)士聪,然后內(nèi)部會定義一個(gè)變量綁定JDKProxy代理對象曼库,當(dāng)調(diào)用User.job接口方法,實(shí)質(zhì)上調(diào)用的是JDKProxy.invoke()方法,從而實(shí)現(xiàn)了接口的動態(tài)代理。

  • 為什么要加入動態(tài)代理?

    第一, 如果沒有動態(tài)代理, 服務(wù)端大量的接口將不便于管理,需要大量的if判斷讥电,如果擴(kuò)展了新的接口纠炮,需要更改調(diào)用邏輯耕肩, 不利于擴(kuò)展維護(hù)梳虽。

    第二, 是可以攔截竖螃,添加其他額外功能画拾, 比如連接負(fù)載管理,日志記錄等等。

  • 動態(tài)代理開源技術(shù)

    (1) Cglib 動態(tài)代理

    Cglib是一個(gè)強(qiáng)大的、高性能的代碼生成包汽畴,它廣泛被許多AOP框架使用旧巾,支持方法級別的攔截。它是高級的字節(jié)碼生成庫忍些,位于ASM之上鲁猩,ASM是低級的字節(jié)碼生成工具是尔,ASM的使用對開發(fā)人員要求較高,相比較來講, ASM性能更好。


    file

    (2) Javassist 動態(tài)代理

    一個(gè)開源的分析、編輯和創(chuàng)建Java字節(jié)碼的類庫。javassist是jboss的一個(gè)子項(xiàng)目漂佩,它直接使用java編碼的形式熟尉,不需要了解虛擬機(jī)指令,可以動態(tài)改變類的結(jié)構(gòu),或者動態(tài)生成類。Javassist 的定位是能夠操縱底層字節(jié)碼典鸡,所以使用起來并不簡單搂蜓,Dubbo 框架的設(shè)計(jì)者為了追求性能花費(fèi)了不少精力去適配javassist伪煤。

    (3) Byte Buddy 字節(jié)碼增強(qiáng)庫

    Byte Buddy是致力于解決字節(jié)碼操作和 簡化操作復(fù)雜性的開源框架。Byte Buddy 目標(biāo)是將顯式的字節(jié)碼操作隱藏在一個(gè)類型安全的領(lǐng)域特定語言背后。它屬于后起之秀,在很多優(yōu)秀的項(xiàng)目中碘饼,像 Spring隘冲、Jackson 都用到了 Byte Buddy 來完成底層代理辐马。相比 Javassist努释,Byte Buddy 提供了更容易操作的 API炬搭,編寫的代碼可讀性更高丘薛。

幾種動態(tài)代理性能比較:

單位是納秒。大括號內(nèi)代表的是樣本標(biāo)準(zhǔn)差,綜合結(jié)果:

file

Byte Buddy > CGLIB > Javassist> JDK拦坠。

源碼剖析

file

核心源碼:

file

2.2.3 服務(wù)注冊發(fā)現(xiàn)

  • 服務(wù)注冊發(fā)現(xiàn)的作用

    在高可用的生產(chǎn)環(huán)境中,一般都以集群方式提供服務(wù)栏赴,集群里面的IP可能隨時(shí)變化竖瘾,也可能會隨著維護(hù)擴(kuò)充或減少節(jié)點(diǎn)沟突,客戶端需要能夠及時(shí)感知服務(wù)端的變化,獲取集群最新服務(wù)節(jié)點(diǎn)的連接信息准浴。

  • 服務(wù)注冊發(fā)現(xiàn)功能

file

服務(wù)注冊:在服務(wù)提供方啟動的時(shí)候事扭,將對外暴露的接口注冊到注冊中心內(nèi),注冊中心將這個(gè)服務(wù)節(jié)點(diǎn)的 IP 和接口等連接信息保存下來乐横。為了檢測服務(wù)的服務(wù)端的有效狀態(tài)求橄,一般會建立雙向心跳機(jī)制。

服務(wù)訂閱:在服務(wù)調(diào)用方啟動的時(shí)候葡公,客戶端去注冊中心查找并訂閱服務(wù)提供方的 IP罐农,然后緩存到本地,并用于后續(xù)的遠(yuǎn)程調(diào)用催什。如果注冊中心信息發(fā)生變化涵亏, 一般會采用推送的方式做更新。

  • 服務(wù)注冊發(fā)現(xiàn)的具體流程

    主流服務(wù)注冊工具有Nacos蒲凶、Consul气筋、Zookeeper等,

    基于 ZooKeeper 的服務(wù)發(fā)現(xiàn):

    ZooKeeper 集群作為注冊中心集群旋圆,服務(wù)注冊的時(shí)候只需要服務(wù)節(jié)點(diǎn)向 ZooKeeper 節(jié)點(diǎn)寫入注冊信息即可宠默,利用 ZooKeeper 的 Watcher 機(jī)制完成服務(wù)訂閱與服務(wù)下發(fā)功能。

file

A. 先在 ZooKeeper 中創(chuàng)建一個(gè)服務(wù)根路徑灵巧,可以根據(jù)接口名命名(例如:/dubbo/com.itcast.xxService)搀矫,在這個(gè)路徑再創(chuàng)建服務(wù)提供方與調(diào)用方目錄(providers、consumers)刻肄,分別用來存儲服務(wù)提供方和調(diào)用方的節(jié)點(diǎn)信息瓤球。

B. 服務(wù)端發(fā)起注冊時(shí),會在服務(wù)提供方目錄中創(chuàng)建一個(gè)臨時(shí)節(jié)點(diǎn)敏弃,節(jié)點(diǎn)中存儲注冊信 息卦羡,比如IP,端口麦到,服務(wù)名稱等等虹茶。

C. 客戶端發(fā)起訂閱時(shí),會在服務(wù)調(diào)用方目錄中創(chuàng)建一個(gè)臨時(shí)節(jié)點(diǎn)隅要,節(jié)點(diǎn)中存儲調(diào)用方的信息蝴罪,同時(shí)watch 服務(wù)提供方的目錄(/dubbo/com.itcast.xxService/providers)中所有的服務(wù)節(jié)點(diǎn)數(shù)據(jù)。當(dāng)服務(wù)端產(chǎn)生變化時(shí)步清,比如下線或宕機(jī)等要门,ZooKeeper 就會通知給訂閱的客戶端虏肾。

ZooKeeper方案的特點(diǎn):

ZooKeeper 的一大特點(diǎn)就是強(qiáng)一致性,ZooKeeper 集群的每個(gè)節(jié)點(diǎn)的數(shù)據(jù)每次發(fā)生更新操作欢搜,都會通知其它 ZooKeeper 節(jié)點(diǎn)同時(shí)執(zhí)行更新封豪。它要求保證每個(gè)節(jié)點(diǎn)的數(shù)據(jù)能夠?qū)崟r(shí)的完全一致,這樣也就會導(dǎo)致ZooKeeper 集群性能上的下降炒瘟,ZK是采用CP模式(保證強(qiáng)一致性)吹埠,如果要注重性能, 可以考慮采用AP模式(保證最終一致)的注冊中心組件疮装, 比如Nacos等缘琅。

  • 源碼剖析

    Dubbo Spring Cloud 訂閱的源碼(客戶端):


    file

    核心源碼:

    RegistryProtocol的doRefer方法:

    private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
            RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
            directory.setRegistry(registry);
            directory.setProtocol(protocol);
            // all attributes of REFER_KEY
            Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
            URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
            if (!ANY_VALUE.equals(url.getServiceInterface()) && url.getParameter(REGISTER_KEY, true)) {
                directory.setRegisteredConsumerUrl(getRegisteredConsumerUrl(subscribeUrl, url));
                registry.register(directory.getRegisteredConsumerUrl());
            }
            directory.buildRouterChain(subscribeUrl);
            directory.subscribe(subscribeUrl.addParameter(CATEGORY_KEY,
                    PROVIDERS_CATEGORY + "," + CONFIGURATORS_CATEGORY + "," + ROUTERS_CATEGORY));
    
            Invoker invoker = cluster.join(directory);
            ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
            return invoker;
        }
    

    Dubbo Spring Cloud 注冊發(fā)現(xiàn)的源碼(服務(wù)端):


    file

    核心源碼:

    RegistryProtocol的export方法:

    public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
            // 獲取注冊信息
          URL registryUrl = getRegistryUrl(originInvoker);
            // 獲取服務(wù)提供方信息
            URL providerUrl = getProviderUrl(originInvoker);
    
            // Subscribe the override data
            // FIXME When the provider subscribes, it will affect the scene : a certain JVM exposes the service and call
            //  the same service. Because the subscribed is cached key with the name of the service, it causes the
            //  subscription information to cover.
            final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
            final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
            overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
    
            providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
            //export invoker
            final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
    
            // 獲取訂閱注冊器
            final Registry registry = getRegistry(originInvoker);
            final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl);
            ProviderInvokerWrapper<T> providerInvokerWrapper = ProviderConsumerRegTable.registerProvider(originInvoker,
                    registryUrl, registeredProviderUrl);
            //to judge if we need to delay publish
            boolean register = registeredProviderUrl.getParameter("register", true);
            if (register) {
                // 進(jìn)入服務(wù)端信息注冊處理
                register(registryUrl, registeredProviderUrl);
                providerInvokerWrapper.setReg(true);
            }
    
            // Deprecated! Subscribe to override rules in 2.6.x or before.
          // 服務(wù)端信息訂閱處理
            registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
    
            exporter.setRegisterUrl(registeredProviderUrl);
            exporter.setSubscribeUrl(overrideSubscribeUrl);
            //Ensure that a new exporter instance is returned every time export
            return new DestroyableExporter<>(exporter);
        }
    

2.2.4 網(wǎng)絡(luò)IO模型

  • 有哪些網(wǎng)絡(luò)IO模型

    分為五種:

    • 同步阻塞 IO(BIO)
    • 同步非阻塞 IO(NIO)
    • IO 多路復(fù)用
    • 信號驅(qū)動IO
    • 異步非阻塞 IO(AIO)

    常用的是同步阻塞 IO 和 IO 多路復(fù)用模型。

  • 什么是阻塞IO模型

file

通常由一個(gè)獨(dú)立的 Acceptor 線程負(fù)責(zé)監(jiān)聽客戶端的連接廓推。一般通過在while(true) 循環(huán)中服務(wù)端會調(diào)用 accept() 方法等待接收客戶端的連接的方式監(jiān)聽請求刷袍,請求一旦接收到一個(gè)連接請求,就可以建立通信套接字,在這個(gè)通信套接字上進(jìn)行讀寫操作樊展,此時(shí)不能再接收其他客戶端連接請求呻纹,直到客戶端的操作執(zhí)行完成。
系統(tǒng)內(nèi)核處理 IO 操作分為兩個(gè)階段——等待數(shù)據(jù)和拷貝數(shù)據(jù)专缠。而在這兩個(gè)階段中雷酪,應(yīng)用進(jìn)程中 IO 操作的線程會一直都處于阻塞狀態(tài),如果是基于 Java 多線程開發(fā)涝婉,那么每一個(gè) IO 操作都要占用線程哥力,直至 IO 操作結(jié)束。

  • IO多路復(fù)用

    概念: 服務(wù)端采用單線程過select/epoll機(jī)制嘁圈,獲取fd列表, 遍歷fd中的所有事件, 可以關(guān)注多個(gè)文件描述符蟀淮,使其能夠支持更多的并發(fā)連接最住。

    IO多路復(fù)用的實(shí)現(xiàn)主要有select,poll和epoll模式怠惶。

    文件描述符:

    在Linux系統(tǒng)中一切皆可以看成是文件涨缚,文件又可分為:普通文件、目錄文件策治、鏈接文件和設(shè)備文件脓魏。

    文件描述符(file descriptor)是內(nèi)核為了高效管理已被打開的文件所創(chuàng)建的索引,用來指向被打開的文件通惫。文件描述符的值是一個(gè)非負(fù)整數(shù)茂翔。

    下圖說明(左邊是進(jìn)程、中間是內(nèi)核履腋、右邊是文件系統(tǒng)):

    1) A的文件描述符1和30都指向了同一個(gè)打開的文件句柄珊燎, 代表進(jìn)程多次執(zhí)行打開操作惭嚣。

    2) A的文件描述符2和B的文件描述符2都指向文件句柄(#73),代表A和程B可能是父子進(jìn)程或者A和進(jìn)程B打開了同一個(gè)文件(低概率)悔政。

    3) (時(shí)間緊張可不講)A的描述符0和B的描述符3分別指向不同的打開文件句柄晚吞,但這些句柄均指向i-node表的相同條目(#1936),這種情況是因?yàn)槊總€(gè)進(jìn)程各自對同一個(gè)文件發(fā)起了打開請求谋国。

file

程序剛剛啟動的時(shí)候槽地,0是標(biāo)準(zhǔn)輸入,1是標(biāo)準(zhǔn)輸出芦瘾,2是標(biāo)準(zhǔn)錯(cuò)誤捌蚊。如果此時(shí)去打開一個(gè)新的文件,它的文件描述符會是3旅急。

三者的區(qū)別:

select poll epoll
操作方式 遍歷 遍歷 回調(diào)
底層實(shí)現(xiàn) bitmap 數(shù)組 紅黑樹
IO效率 每次調(diào)用都進(jìn)行線性遍歷逢勾,時(shí)間復(fù)雜度為O(n) 每次調(diào)用都進(jìn)行線性遍歷,時(shí)間復(fù)雜度為O(n) 事件通知方式藐吮,每當(dāng)fd就緒溺拱,系統(tǒng)注冊的回調(diào)函數(shù)就會被調(diào)用,將就緒fd放到readyList里面谣辞,時(shí)間復(fù)雜度O(1)
最大連接數(shù) 1024(x86)或2048(x64) 無上限 無上限
fd拷貝 每次調(diào)用select迫摔,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài) 每次調(diào)用poll,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài) 調(diào)用epoll_ctl時(shí)拷貝進(jìn)內(nèi)核并保存泥从,之后每次epoll_wait不拷貝

select/poll處理流程:

此處是動圖

epoll的處理流程:

此處是動圖

當(dāng)連接有I/O流事件產(chǎn)生的時(shí)候句占,epoll就會去告訴進(jìn)程哪個(gè)連接有I/O流事件產(chǎn)生,然后進(jìn)程就去處理這個(gè)進(jìn)程躯嫉。這樣性能相比要高效很多纱烘!

epoll 可以說是I/O 多路復(fù)用最新的一個(gè)實(shí)現(xiàn),epoll 修復(fù)了poll 和select絕大部分問題, 比如

epoll 是線程安全的祈餐。
epoll 不僅告訴你sock組里面的數(shù)據(jù)擂啥,還會告訴你具體哪個(gè)sock連接有數(shù)據(jù),不用進(jìn)程獨(dú)自輪詢查找帆阳。

  • select 模型

    使用示例:

     while (1) {
        // 阻塞獲取
        // 每次需要把fd從用戶態(tài)拷貝到內(nèi)核態(tài)
        nfds = select(max + 1, &read_fd, &write_fd, NULL, &timeout);
        // 每次需要遍歷所有fd哺壶,判斷有無讀寫事件發(fā)生
        for (int i = 0; i <= max && nfds; ++i) {
          if (i == listenfd) {
             --nfds;
             // 這里處理accept事件
             FD_SET(i, &read_fd);//將客戶端socket加入到集合中
          }
          if (FD_ISSET(i, &read_fd)) {
            --nfds;
            // 這里處理read事件
          }
          if (FD_ISSET(i, &write_fd)) {
             --nfds;
            // 這里處理write事件
          }
        }
      }
    
    

    缺點(diǎn):

    • 單個(gè)進(jìn)程所打開的FD最大數(shù)限制為1024。
    • 每次調(diào)用select蜒谤,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài)尚粘,fd數(shù)據(jù)較大時(shí)影響性能嫉称。
    • 對socket掃描時(shí)是線性掃描搔预,效率較低(高并發(fā)場景)
  • POLL模型

    int max  = 0;  // 隊(duì)列的實(shí)際長度
    while (1) {
        // 阻塞獲取
        // 每次需要把fd從用戶態(tài)拷貝到內(nèi)核態(tài)
        nfds = poll(fds, max+1, timeout);
        if (fds[0].revents & POLLRDNORM) {
            // 這里處理accept事件
            connfd = accept(listenfd);
            //將新的描述符添加到讀描述符集合中
        }
        // 每次需要遍歷所有fd首昔,判斷有無讀寫事件發(fā)生
        for (int i = 1; i < max; ++i) {     
            if (fds[i].revents & POLLRDNORM) { 
                sockfd = fds[i].fd
                    if ((n = read(sockfd, buf, MAXLINE)) <= 0) {
                        // 這里處理read事件
                        if (n == 0) {
                            close(sockfd);
                            fds[i].fd = -1;
                        }
                    } else {
                        // 這里處理write事件     
                    }
                if (--nfds <= 0) {
                    break;       
                }   
            }
        }
    }
    

    缺點(diǎn):

    • poll與select相比,只是沒有fd的限制阶祭,都存在相同的缺陷台妆。
  • EPOLL模型

    使用示例:

    // 需要監(jiān)聽的socket放到ep中
    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); 
    while(1) {
         // 阻塞獲取
         nfds = epoll_wait(epfd,events,20,0);
         for(i=0;i<nfds;++i) {
             if(events[i].data.fd==listenfd) {
                 // 這里處理accept事件
                 connfd = accept(listenfd);
                 // 接收新連接寫到內(nèi)核對象中
                 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
             } else if (events[i].events&EPOLLIN) {
                 // 這里處理read事件
                 read(sockfd, BUF, MAXLINE);
                 //讀完后準(zhǔn)備寫
                 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
             } else if(events[i].events&EPOLLOUT) {
                 // 這里處理write事件
                 write(sockfd, BUF, n);
                 //寫完后準(zhǔn)備讀
                 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
             }
         }
     }
    

    缺點(diǎn):

    • 目前只能工作在linux環(huán)境下
    • 數(shù)據(jù)量很小的時(shí)候沒有性能優(yōu)勢

    epoll下的兩種模式(拓展了解):

    EPOLLLT和EPOLLET兩種觸發(fā)模式翎猛,LT是默認(rèn)的模式,ET是“高速”模式接剩。

    LT(水平觸發(fā))模式下切厘,只要這個(gè)fd還有數(shù)據(jù)可讀,每次 epoll_wait都會返回它的事件懊缺,提醒用戶程序去操作

    ET(邊緣觸發(fā))模式下疫稿,它只會提示一次,直到下次再有數(shù)據(jù)流入之前都不會再提示了鹃两,無論fd中是否還有數(shù)據(jù)可讀遗座。所以在ET模式下,read一個(gè)fd的時(shí)候一定要把它的buffer讀完俊扳,或者遇到EAGAIN錯(cuò)誤

file
  • 三種模式對比

    對比項(xiàng) select poll epoll
    數(shù)據(jù)結(jié)構(gòu) bitmap 數(shù)組 紅黑樹
    最大連接數(shù) 1024 無上限 無上限
    fd拷貝 每次調(diào)用select拷貝 每次調(diào)用poll拷貝 fd首次調(diào)用epoll_ctl拷貝途蒋,每次調(diào)用epoll_wait不拷貝
    工作效率 輪詢:O(n) 輪詢:O(n) 回調(diào):O(1)
  • 為什么阻塞 IO 和 IO 多路復(fù)用最為常用?

    在實(shí)際的網(wǎng)絡(luò) IO 的應(yīng)用中馋记,需要的是系統(tǒng)內(nèi)核的支持以及編程語言的支持『牌拢現(xiàn)在大多數(shù)系統(tǒng)內(nèi)核都會支持阻塞 IO、非阻塞 IO 和 IO 多路復(fù)用梯醒,但像信號驅(qū)動 IO宽堆、異步 IO,只有高版本的 Linux 系統(tǒng)內(nèi)核才會支持茸习。

    同步阻塞IO畜隶、同步非阻塞IO、同步IO多路復(fù)用與異步IO區(qū)別:

    • 同步阻塞IO(實(shí)質(zhì)上号胚, 每個(gè)請求不管成功或失敗籽慢, 都會阻塞)
    file
    • 同步非阻塞IO(相比第一種的完全阻塞,如果數(shù)據(jù)沒準(zhǔn)備好猫胁,會返回EWOULDBLOCK箱亿, 這樣就不會造成阻塞)
    file
    • 同步IO多路復(fù)用

      (kernel會根據(jù)select/epoll等機(jī)制, 監(jiān)聽所有select接入的socket杜漠,當(dāng)任何一個(gè)socket中的數(shù)據(jù)準(zhǔn)備好了极景,select就會返回察净, 使得一個(gè)進(jìn)程能同時(shí)等待多個(gè)文件描述符驾茴。)

file
  • 異步IO
file
  • RPC 框架采用哪種網(wǎng)絡(luò) IO 模型?

    1. IO 多路復(fù)用應(yīng)用特點(diǎn):

    IO 多路復(fù)用更適合高并發(fā)的場景氢卡,可以用較少的進(jìn)程(線程)處理較多的 socket 的 IO 請求锈至,但使用難度比較高。

    1. 阻塞 IO應(yīng)用特點(diǎn):

    與 IO 多路復(fù)用相比译秦,阻塞 IO 每處理一個(gè) socket 的 IO 請求都會阻塞進(jìn)程(線程)峡捡,但使用難度較低击碗。在并發(fā)量較低、業(yè)務(wù)邏輯只需要同步進(jìn)行 IO 操作的場景下们拙,阻塞 IO 已經(jīng)滿足了需求稍途,并且不需要發(fā)起 大量的select 調(diào)用,開銷上要比 IO 多路復(fù)用低砚婆。

    1. RPC框架應(yīng)用:

    RPC 調(diào)用在大多數(shù)的情況下械拍,是一個(gè)高并發(fā)調(diào)用的場景, 在 RPC 框架的實(shí)現(xiàn)中装盯,一般會選擇 IO 多路復(fù)用的方式坷虑。在開發(fā)語言的網(wǎng)絡(luò)通信框架的選型上,我們最優(yōu)的選擇是基于 Reactor 模式實(shí)現(xiàn)的框架埂奈,如 Java 語言迄损,首選的框架便是 Netty 框架(目前 Netty 是應(yīng)用最為廣泛的框架),并且在 Linux 環(huán)境下账磺,也要開啟 epoll 來提升系統(tǒng)性能(Windows 環(huán)境下是無法開啟 epoll 的芹敌,因?yàn)橄到y(tǒng)內(nèi)核不支持)。

2.2.5 時(shí)間輪

  • 為什么需要時(shí)間輪绑谣?

    在Dubbo中党窜,為增強(qiáng)系統(tǒng)的容錯(cuò)能力,會有相應(yīng)的監(jiān)聽判斷處理機(jī)制借宵。比如RPC調(diào)用的超時(shí)機(jī)制的實(shí)現(xiàn)幌衣,消費(fèi)者判斷RPC調(diào)用是否超時(shí),如果超時(shí)會將超時(shí)結(jié)果返回給應(yīng)用層壤玫。在Dubbo最開始的實(shí)現(xiàn)中豁护,是將所有的返回結(jié)果(DefaultFuture)都放入一個(gè)集合中,并且通過一個(gè)定時(shí)任務(wù)欲间,每隔一定時(shí)間間隔就掃描所有的future楚里,逐個(gè)判斷是否超時(shí)。

    這樣的實(shí)現(xiàn)方式雖然比較簡單猎贴,但是存在一個(gè)問題就是會有很多無意義的遍歷操作開銷班缎。比如一個(gè)RPC調(diào)用的超時(shí)時(shí)間是10秒,而設(shè)置的超時(shí)判定的定時(shí)任務(wù)是2秒執(zhí)行一次她渴,那么可能會有4次左右無意義的循環(huán)檢測判斷操作达址。

    為了解決上述場景中的類似問題,Dubbo借鑒Netty趁耗,引入了時(shí)間輪算法沉唠,減少無意義的輪詢判斷操作。

  • 時(shí)間輪原理

    對于以上問題苛败, 目的是要減少額外的掃描操作就可以了满葛。比如說一個(gè)定時(shí)任務(wù)是在5 秒之后執(zhí)行径簿,那么在 4.9 秒之后才掃描這個(gè)定時(shí)任務(wù),這樣就可以極大減少 CPU開銷嘀韧。這時(shí)我們就可以利用時(shí)鐘輪的機(jī)制了篇亭。

file

時(shí)鐘輪的實(shí)質(zhì)上是參考了生活中的時(shí)鐘跳動的原理,那么具體是如何實(shí)現(xiàn)呢锄贷?

在時(shí)鐘輪機(jī)制中暗赶,有時(shí)間槽和時(shí)鐘輪的概念,時(shí)間槽就相當(dāng)于時(shí)鐘的刻度肃叶;而時(shí)鐘輪就相當(dāng)于指針跳動的一個(gè)周期蹂随,我們可以將每個(gè)任務(wù)放到對應(yīng)的時(shí)間槽位上。

如果時(shí)鐘輪有 10 個(gè)槽位因惭,而時(shí)鐘輪一輪的周期是 10 秒岳锁,那么我們每個(gè)槽位的單位時(shí)間就是 1 秒,而下一層時(shí)間輪的周期就是 100 秒蹦魔,每個(gè)槽位的單位時(shí)間也就是 10 秒激率,這就好比秒針與分針, 在秒針周期下勿决, 刻度單位為秒乒躺, 在分針周期下, 刻度為分低缩。

file

假設(shè)現(xiàn)在我們有 3 個(gè)任務(wù)嘉冒,分別是任務(wù) A(0.9秒之后執(zhí)行)、任務(wù) B(2.1秒后執(zhí)行)與任務(wù) C(12.1秒之后執(zhí)行)咆繁,我們將這 3 個(gè)任務(wù)添加到時(shí)鐘輪中讳推,任務(wù) A 被放到第 0 槽位,任務(wù) B 被放到第 2槽位玩般,任務(wù) C 被放到下一層時(shí)間輪的第2個(gè)槽位银觅,如下圖所示:

file

通過這個(gè)場景我們可以了解到,時(shí)鐘輪的掃描周期仍是最小單位1秒坏为,但是放置其中的任務(wù)并沒有反復(fù)掃描究驴,每個(gè)任務(wù)會按要求只掃描執(zhí)行一次, 這樣就能夠很好的解決CPU 浪費(fèi)的問題匀伏。

疊加時(shí)鐘輪洒忧, 無限增長, 效率會不斷下降帘撰,該如何解決跑慕?設(shè)定三個(gè)時(shí)鐘輪万皿, 小時(shí)輪摧找, 分鐘輪核行, 秒級輪

Dubbo中的時(shí)間輪原理是如何實(shí)現(xiàn)?

主要是通過Timer蹬耘,Timeout芝雪,TimerTask幾個(gè)接口定義了一個(gè)定時(shí)器的模型,再通過HashedWheelTimer這個(gè)類實(shí)現(xiàn)了一個(gè)時(shí)間輪定時(shí)器(默認(rèn)的時(shí)間槽的數(shù)量是512综苔,可以自定義這個(gè)值)惩系。它對外提供了簡單易用的接口,只需要調(diào)用newTimeout接口如筛,就可以實(shí)現(xiàn)對只需執(zhí)行一次任務(wù)的調(diào)度堡牡。通過該定時(shí)器,Dubbo在響應(yīng)的場景中實(shí)現(xiàn)了高效的任務(wù)調(diào)度杨刨。

  • Dubbo源碼剖析

    時(shí)間輪核心類HashedWheelTimer結(jié)構(gòu):

file
  • 時(shí)間輪在RPC的應(yīng)用

    • 調(diào)用超時(shí)與重試處理: 上面所講的客戶端調(diào)用超時(shí)的處理晤柄,就可以應(yīng)用到時(shí)鐘輪,我們每發(fā)一次請求妖胀,都創(chuàng)建一個(gè)處理請求超時(shí)的定時(shí)任務(wù)放到時(shí)鐘輪里芥颈,在高并發(fā)、高訪問量的情況下赚抡,時(shí)鐘輪每次只輪詢一個(gè)時(shí)間槽位中的任務(wù)爬坑,這樣會節(jié)省大量的 CPU

      源碼:

      FailbackRegistry涂臣, 代碼片段:

      // 構(gòu)造方法
      public FailbackRegistry(URL url) {
              super(url);
              this.retryPeriod = url.getParameter(REGISTRY_RETRY_PERIOD_KEY, DEFAULT_REGISTRY_RETRY_PERIOD);
      
              // since the retry task will not be very much. 128 ticks is enough.
              // 重試器的時(shí)間槽數(shù)量盾计, 設(shè)定為128
              retryTimer = new HashedWheelTimer(new NamedThreadFactory("DubboRegistryRetryTimer", true), retryPeriod, TimeUnit.MILLISECONDS, 128);
          }
      
      // 失敗時(shí)間任務(wù)注冊器
      private void addFailedRegistered(URL url) {
              FailedRegisteredTask oldOne = failedRegistered.get(url);
              if (oldOne != null) {
                  return;
              }
              FailedRegisteredTask newTask = new FailedRegisteredTask(url, this);
              oldOne = failedRegistered.putIfAbsent(url, newTask);
              if (oldOne == null) {
                  // never has a retry task. then start a new task for retry.
                  // 舊任務(wù)不存在, 則放置時(shí)間輪赁遗,開啟新一個(gè)任務(wù)
                  retryTimer.newTimeout(newTask, retryPeriod, TimeUnit.MILLISECONDS);
              }
          }
      
  • 定時(shí)心跳檢測: RPC 框架調(diào)用端定時(shí)向服務(wù)端發(fā)送的心跳檢測闯估,來維護(hù)連接狀態(tài),我們可以將心跳的邏輯封裝為一個(gè)心跳任務(wù)吼和,放到時(shí)鐘輪里涨薪。心跳是要定時(shí)重復(fù)執(zhí)行的,而時(shí)鐘輪中的任務(wù)執(zhí)行一遍就被移除了炫乓,對于這種需要重復(fù)執(zhí)行的定時(shí)任務(wù)我們該如何處理呢刚夺?我們在定時(shí)任務(wù)邏輯結(jié)束的最后,再加上一段邏輯末捣, 重設(shè)這個(gè)任務(wù)的執(zhí)行時(shí)間侠姑,把它重新丟回到時(shí)鐘輪里。這樣就可以實(shí)現(xiàn)循環(huán)執(zhí)行箩做。

    源碼:

    HeaderExchangeServer代碼片段:

    ...
    // 建立心跳時(shí)間輪莽红, 槽位數(shù)默認(rèn)為128
    private static final HashedWheelTimer IDLE_CHECK_TIMER = new HashedWheelTimer(new NamedThreadFactory("dubbo-server-idleCheck", true), 1,
                TimeUnit.SECONDS, TICKS_PER_WHEEL);
    ...
        // 啟動心跳任務(wù)檢測
        private void startIdleCheckTask(URL url) {
            if (!server.canHandleIdle()) {
                AbstractTimerTask.ChannelProvider cp = () -> unmodifiableCollection(HeaderExchangeServer.this.getChannels());
                int idleTimeout = getIdleTimeout(url);
                long idleTimeoutTick = calculateLeastDuration(idleTimeout);
                CloseTimerTask closeTimerTask = new CloseTimerTask(cp, idleTimeoutTick, idleTimeout);
                this.closeTimerTask = closeTimerTask;
    
                // init task and start timer.
                // 開啟心跳檢測任務(wù)
                IDLE_CHECK_TIMER.newTimeout(closeTimerTask, idleTimeoutTick, TimeUnit.MILLISECONDS);
            }
        }
    ...
    

    連接檢測, 會不斷執(zhí)行, 加入時(shí)間輪中安吁。

    AbstractTimerTask源碼:

        @Override
        public void run(Timeout timeout) throws Exception {
            Collection<Channel> c = channelProvider.getChannels();
            for (Channel channel : c) {
                if (channel.isClosed()) {
                    continue;
                }
                // 調(diào)用心跳檢測任務(wù)
                doTask(channel);
            }
            // 重新放入時(shí)間輪中
            reput(timeout, tick);
        }
    

    還可以參考HeartbeatTimerTask醉蚁、ReconnectTimerTask源碼實(shí)現(xiàn)。

3. RPC的高級機(jī)制

3.1 異步處理機(jī)制

  • 為什么要采用異步鬼店?

    如果采用同步調(diào)用网棍, CPU 大部分的時(shí)間都在等待而沒有去計(jì)算,從而導(dǎo)致 CPU 的利用率不夠妇智。

    RPC 請求比較耗時(shí)的原因主要是在哪里滥玷?

    在大多數(shù)情況下,RPC 本身處理請求的效率是在毫秒級的巍棱。RPC 請求的耗時(shí)大部分都是業(yè)務(wù)耗時(shí)惑畴,比如業(yè)務(wù)邏輯中有訪問數(shù)據(jù)庫執(zhí)行慢 SQL 的操作,核心是在I/O瓶頸航徙。所以說桨菜,在大多數(shù)情況下,影響到 RPC 調(diào)用的吞吐量的原因也就是業(yè)務(wù)邏輯處理慢了捉偏,CPU 大部分時(shí)間都在等待資源倒得。

  • 調(diào)用端如何實(shí)現(xiàn)異步?

    常用的方式就是Future 方式夭禽,它是返回 Future 對象霞掺,通過GET方式獲取結(jié)果;或者采用入?yún)?Callback 對象的回調(diào)方式讹躯,處理結(jié)果菩彬。

    從DUBBO框架, 來看具體是如何實(shí)現(xiàn)異步調(diào)用潮梯?

file
  • 服務(wù)端如何實(shí)現(xiàn)異步骗灶?

    為了提升性能,連接請求與業(yè)務(wù)處理不會放在一個(gè)線程處理秉馏, 這個(gè)就是服務(wù)端的異步化耙旦。服務(wù)端業(yè)務(wù)處理邏輯加入異步處理機(jī)制。

    在RPC 框架提供一種回調(diào)方式萝究,讓業(yè)務(wù)邏輯可以異步處理免都,處理完之后調(diào)用 RPC 框架的回調(diào)接口。

RPC 框架的異步策略主要是調(diào)用端異步與服務(wù)端異步帆竹。調(diào)用端的異步就是通過 Future 方式绕娘。

服務(wù)端異步則需要一種回調(diào)方式,讓業(yè)務(wù)邏輯可以異步處理栽连。這樣就實(shí)現(xiàn)了RPC調(diào)用的全異步化险领。

  • RPC框架的異步實(shí)現(xiàn)

    RPC 框架的異步策略主要是調(diào)用端異步與服務(wù)端異步。調(diào)用端的異步就是通過 Future 方式實(shí)現(xiàn)異步,調(diào)用端發(fā)起一次異步請求并且從請求上下文中拿到一個(gè) Future绢陌,之后通過 Future 的 get 方法獲取結(jié)果挨下,如果業(yè)務(wù)邏輯中同時(shí)調(diào)用多個(gè)其它的服務(wù),則可以通過 Future 的方式減少業(yè)務(wù)邏輯的耗時(shí)下面,提升吞吐量。

    服務(wù)端異步則需要一種回調(diào)方式绩聘,讓業(yè)務(wù)邏輯可以異步處理沥割,之后調(diào)用 RPC 框架提供的回調(diào)接口,將最終結(jié)果異步通知給調(diào)用端凿菩。這樣就實(shí)現(xiàn)了RPC調(diào)用的全異步机杜。

    Dubbo源碼:

    異步調(diào)用: AsyncToSyncInvoker.invoke方法

    獲取結(jié)果:ChannelWrappedInvoker.doInvoke方法

3.2 路由與負(fù)載均衡(了解)

我們后面會講解灰度發(fā)布機(jī)制,基于Nginx+Lua衅谷、擴(kuò)展SpringCloud Gateway源碼灰度發(fā)布和負(fù)載均衡椒拗,只要項(xiàng)目集群、分布式應(yīng)用就會涉及到路由與負(fù)載均衡获黔。

  • 為什么要采用路由蚀苛?

    真實(shí)的環(huán)境中一般是以集群的方式提供服務(wù),對于服務(wù)調(diào)用方來說玷氏,一個(gè)接口會有多個(gè)服務(wù)提供方同時(shí)提供服務(wù)堵未,所以 RPC 在每次發(fā)起請求的時(shí)候,都需要從多個(gè)服務(wù)節(jié)點(diǎn)里面選取一個(gè)用于處理請求的服務(wù)節(jié)點(diǎn)盏触。

    這就需要在RPC應(yīng)用中增加路由功能渗蟹。

  • 如何實(shí)現(xiàn)路由?

    服務(wù)注冊發(fā)現(xiàn)方式:

    通過服務(wù)發(fā)現(xiàn)的方式從邏輯上看是可行赞辩,但注冊中心是用來保證數(shù)據(jù)的一致性雌芽。通過服務(wù)發(fā)現(xiàn)方式來實(shí)現(xiàn)請求隔離并不理想。

    RPC路由策略:

    從服務(wù)提供方節(jié)點(diǎn)集合里面選擇一個(gè)合適的節(jié)點(diǎn)(負(fù)載均衡)辨嗽,把符合我們要求的節(jié)點(diǎn)篩選出來世落。這個(gè)就是路由策略:

    接收請求-->請求校驗(yàn)-->路由策略-->負(fù)載均衡-->

    使用了 IP 路由策略后,整個(gè)集群的調(diào)用拓?fù)淙缦聢D所示:

file

有些場景下糟需,可能還需要更細(xì)粒度的路由方式岛心,比如說根據(jù)SESSIONID要落到相同的服務(wù)節(jié)點(diǎn)上以保持會話的有效性;

可以考慮采用參數(shù)化路由

file
  • RPC框架中的負(fù)載均衡

    RPC 的負(fù)載均衡是由 RPC 框架自身提供實(shí)現(xiàn),自主選擇一個(gè)最佳的服務(wù)節(jié)點(diǎn)篮灼,發(fā)起 RPC 調(diào)用請求忘古。

file

RPC 負(fù)載均衡策略一般包括輪詢、隨機(jī)诅诱、權(quán)重髓堪、最少連接等。Dubbo默認(rèn)就是使用隨機(jī)負(fù)載均衡策略。

  • 自適應(yīng)的負(fù)載均衡策略

    RPC 的負(fù)載均衡完全由 RPC 框架自身實(shí)現(xiàn)干旁,服務(wù)調(diào)用方發(fā)起請求時(shí)驶沼,會通過所配置的負(fù)載均衡組件,自主地選擇合適服務(wù)節(jié)點(diǎn)争群。調(diào)用方如果能知道每個(gè)服務(wù)節(jié)點(diǎn)處理請求的能力回怜,再根據(jù)服務(wù)節(jié)點(diǎn)處理請求的能力來判斷分配相應(yīng)的流量,集群資源就能夠得到充分的利用换薄, 當(dāng)一個(gè)服務(wù)節(jié)點(diǎn)負(fù)載過高或響應(yīng)過慢時(shí)玉雾,就少給它發(fā)送請求,反之則多給它發(fā)送請求轻要。這個(gè)就是自適應(yīng)的負(fù)載均衡策略复旬。

    具體如何實(shí)現(xiàn)?

    這就需要判定服務(wù)節(jié)點(diǎn)的處理能力冲泥。


    file

    主要步驟:

    (1)添加計(jì)分器和指標(biāo)采集器驹碍。

    (2)指標(biāo)采集器收集服務(wù)節(jié)點(diǎn) CPU 核數(shù)、CPU 負(fù)載以及內(nèi)存占用率等指標(biāo)凡恍。

    (3)可以配置開啟哪些指標(biāo)采集器志秃,并設(shè)置這些參考指標(biāo)的具體權(quán)重。

    (4)通過對服務(wù)節(jié)點(diǎn)的綜合打分嚼酝,最終計(jì)算出服務(wù)節(jié)點(diǎn)的實(shí)際權(quán)重洽损,選擇合適的服務(wù)節(jié)點(diǎn)。

3.3 熔斷限流(了解)

我們后面課程會詳細(xì)講解熔斷限流組件Sentinel高級用法革半、源碼剖析碑定、策略機(jī)制,但是RPC需要考慮熔斷限流機(jī)制又官,我們一起來了解一下延刘。

  • 為什么要進(jìn)行限流?

    在實(shí)際生產(chǎn)環(huán)境中六敬,每個(gè)服務(wù)節(jié)點(diǎn)都可能由于訪問量過大而引起一系列問題碘赖,就需要業(yè)務(wù)提供方能夠進(jìn)行自我保護(hù),從而保證在高訪問量外构、高并發(fā)的場景下普泡,系統(tǒng)依然能夠穩(wěn)定,高效運(yùn)行审编。

  • 服務(wù)端的自我保護(hù)實(shí)現(xiàn)

    file

在Dubbo框架中撼班, 可以通過Sentinel來實(shí)現(xiàn)更為完善的熔斷限流功能,服務(wù)端是具體如何實(shí)現(xiàn)限流邏輯的垒酬?

方法有很多種砰嘁, 最簡單的是計(jì)數(shù)器件炉,還有平滑限流的滑動窗口、漏斗算法以及令牌桶算法等等矮湘。Sentinel采用的是滑動窗口來實(shí)現(xiàn)的限流斟冕。

file

windowStart: 時(shí)間窗口的開始時(shí)間,單位是毫秒

windowLength: 時(shí)間窗口的長度缅阳,單位是毫秒

value: 時(shí)間窗口的內(nèi)容

初始的時(shí)候arrays數(shù)組中只有一個(gè)窗口磕蛇,每個(gè)時(shí)間窗口的長度是500ms,這就意味著只要當(dāng)前時(shí)間與時(shí)間窗口的差值在500ms之內(nèi)十办,時(shí)間窗口就不會向前滑動秀撇。

file

時(shí)間繼續(xù)往前走,當(dāng)超過500ms時(shí)橘洞,時(shí)間窗口就會向前滑動到下一個(gè)捌袜,這時(shí)就會更新當(dāng)前窗口的開始時(shí)間:

file

在當(dāng)前時(shí)間點(diǎn)中進(jìn)入的請求说搅,會被統(tǒng)計(jì)到當(dāng)前時(shí)間所對應(yīng)的時(shí)間窗口中炸枣。

  • 調(diào)用方的自我保護(hù)

    一個(gè)服務(wù) A 調(diào)用服務(wù) B 時(shí),服務(wù) B 的業(yè)務(wù)邏輯又調(diào)用了服務(wù) C弄唧,這時(shí)服務(wù) C 響應(yīng)超時(shí)适肠,服務(wù) B 就可能會因?yàn)槎逊e大量請求而導(dǎo)致服務(wù)宕機(jī)映企,由此產(chǎn)生服務(wù)雪崩的問題计呈。

    熔斷處理流程:

file

熔斷機(jī)制:

熔斷器的工作機(jī)制主要是關(guān)閉渴语、打開和半打開這三個(gè)狀態(tài)之間的切換锭碳。

Sentinel 熔斷降級組件它可以支持以下降級策略:

  • 平均響應(yīng)時(shí)間 (DEGRADE_GRADE_RT):當(dāng) 1s 內(nèi)持續(xù)進(jìn)入 N 個(gè)請求灶挟,對應(yīng)時(shí)刻的平均響應(yīng)時(shí)間(秒級)均超過閾值(count熊尉,以 ms 為單位)季稳,那么在接下的時(shí)間窗口(DegradeRule 中的 timeWindow馋嗜,以 s 為單位)之內(nèi)麸俘,對這個(gè)方法的調(diào)用都會自動地熔斷(拋出 DegradeException)辩稽。注意 Sentinel 默認(rèn)統(tǒng)計(jì)的 RT 上限是 4900 ms,超出此閾值的都會算作 4900 ms从媚,若需要變更此上限可以通過啟動配置項(xiàng) -Dcsp.sentinel.statistic.max.rt=xxx 來配置逞泄。
  • 異常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):當(dāng)資源的每秒請求量 >= N(可配置),并且每秒異嘲菪В總數(shù)占通過量的比值超過閾值(DegradeRule 中的 count)之后喷众,資源進(jìn)入降級狀態(tài),即在接下的時(shí)間窗口(DegradeRule 中的 timeWindow紧憾,以 s 為單位)之內(nèi)到千,對這個(gè)方法的調(diào)用都會自動地返回。異常比率的閾值范圍是 [0.0, 1.0]赴穗,代表 0% - 100%父阻。
  • 異常數(shù) (DEGRADE_GRADE_EXCEPTION_COUNT):當(dāng)資源近 1 分鐘的異常數(shù)目超過閾值之后會進(jìn)行熔斷愈涩。注意由于統(tǒng)計(jì)時(shí)間窗口是分鐘級別的,若 timeWindow 小于 60s加矛,則結(jié)束熔斷狀態(tài)后仍可能再進(jìn)入熔斷狀態(tài)履婉。

更多資料,參考Sentinel官方文檔斟览。

本文由育博學(xué)谷狂野架構(gòu)師發(fā)布
如果本文對您有幫助毁腿,歡迎關(guān)注和點(diǎn)贊;如果您有任何建議也可留言評論或私信苛茂,您的支持是我堅(jiān)持創(chuàng)作的動力
轉(zhuǎn)載請注明出處已烤!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市妓羊,隨后出現(xiàn)的幾起案子胯究,更是在濱河造成了極大的恐慌,老刑警劉巖躁绸,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件裕循,死亡現(xiàn)場離奇詭異,居然都是意外死亡净刮,警方通過查閱死者的電腦和手機(jī)剥哑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來淹父,“玉大人株婴,你說我怎么就攤上這事∈钊希” “怎么了困介?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蘸际。 經(jīng)常有香客問我座哩,道長,這世上最難降的妖魔是什么捡鱼? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任八回,我火速辦了婚禮,結(jié)果婚禮上驾诈,老公的妹妹穿的比我還像新娘缠诅。我一直安慰自己,他們只是感情好乍迄,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布管引。 她就那樣靜靜地躺著,像睡著了一般闯两。 火紅的嫁衣襯著肌膚如雪褥伴。 梳的紋絲不亂的頭發(fā)上谅将,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天,我揣著相機(jī)與錄音重慢,去河邊找鬼饥臂。 笑死,一個(gè)胖子當(dāng)著我的面吹牛似踱,可吹牛的內(nèi)容都是我干的隅熙。 我是一名探鬼主播,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼核芽,長吁一口氣:“原來是場噩夢啊……” “哼囚戚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起轧简,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤驰坊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后哮独,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拳芙,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年借嗽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了态鳖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片转培。...
    茶點(diǎn)故事閱讀 40,013評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡恶导,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出浸须,到底是詐尸還是另有隱情惨寿,我是刑警寧澤,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布删窒,位于F島的核電站裂垦,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏肌索。R本人自食惡果不足惜蕉拢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望诚亚。 院中可真熱鬧晕换,春花似錦、人聲如沸站宗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽梢灭。三九已至夷家,卻和暖如春蒸其,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背库快。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工摸袁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人义屏。 一個(gè)月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓但惶,卻偏偏與公主長得像,于是被迫代替她去往敵國和親湿蛔。 傳聞我的和親對象是個(gè)殘疾皇子膀曾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容