RPC框架的可靠性設計

weixin_33766168發表於2019-01-31

1. 背景

1.1 分散式呼叫引入的故障

在傳統的單體架構中,業務服務呼叫都是本地方法呼叫,不會涉及到網路通訊、協議棧、訊息序列化和反序列化等,當使用RPC框架將業務由單體架構改造成分散式系統之後,本地方法呼叫將演變成跨程式的遠端呼叫,會引入一些新的故障點,如下所示:

\"\"

圖1 RPC呼叫引入的潛在故障點

新引入的潛在故障點包括:

1.訊息的序列化和反序列化故障,例如,不支援的資料型別。

2.路由故障:包括服務的訂閱、釋出故障,服務例項故障之後沒有及時重新整理路由表,導致RPC呼叫仍然路由到故障節點。

3.網路通訊故障,包括網路閃斷、網路單通、丟包、客戶端浪湧接入等。

1.2 第三方服務依賴

RPC服務通常會依賴第三方服務,包括資料庫服務、檔案儲存服務、快取服務、訊息佇列服務等,這種第三方依賴同時也引入了潛在的故障:

1.網路通訊類故障,如果採用BIO呼叫第三方服務,很有可能被阻塞。

2.“雪崩效用”導致的級聯故障,例如服務端處理慢導致客戶端執行緒被阻塞。

3.第三方不可用導致RPC呼叫失敗。

典型的第三方依賴示例如下:

\"\"

圖2 RPC服務端的第三方依賴

2. 通訊層的可靠性設計

2.1 鏈路有效性檢測

當網路發生單通、連線被防火牆Hang住、長時間GC或者通訊執行緒發生非預期異常時,會導致鏈路不可用且不易被及時發現。特別是異常發生在凌晨業務低谷期間,當早晨業務高峰期到來時,由於鏈路不可用會導致瞬間的大批量業務失敗或者超時,這將對系統的可靠性產生重大的威脅。

從技術層面看,要解決鏈路的可靠性問題,必須週期性的對鏈路進行有效性檢測。目前最流行和通用的做法就是心跳檢測。

心跳檢測機制分為三個層面:

1.TCP層面的心跳檢測,即TCP的Keep-Alive機制,它的作用域是整個TCP協議棧。

2.協議層的心跳檢測,主要存在於長連線協議中。例如MQTT協議。

3.應用層的心跳檢測,它主要由各業務產品通過約定方式定時給對方傳送心跳訊息實現。

心跳檢測的目的就是確認當前鏈路可用,對方活著並且能夠正常接收和傳送訊息。做為高可靠的NIO框架,Netty也提供了心跳檢測機制,下面我們一起熟悉下心跳的檢測原理。

心跳檢測的原理示意圖如下:

\"\"

圖3 鏈路心跳檢測

不同的協議,心跳檢測機制也存在差異,歸納起來主要分為兩類:

1.Ping-Pong型心跳:由通訊一方定時傳送Ping訊息,對方接收到Ping訊息之後,立即返回Pong應答訊息給對方,屬於請求-響應型心跳。

2.Ping-Ping型心跳:不區分心跳請求和應答,由通訊雙方按照約定定時向對方傳送心跳Ping訊息,它屬於雙向心跳。

心跳檢測策略如下:

1.連續N次心跳檢測都沒有收到對方的Pong應答訊息或者Ping請求訊息,則認為鏈路已經發生邏輯失效,這被稱作心跳超時。

2.讀取和傳送心跳訊息的時候如何直接發生了IO異常,說明鏈路已經失效,這被稱為心跳失敗。

無論發生心跳超時還是心跳失敗,都需要關閉鏈路,由客戶端發起重連操作,保證鏈路能夠恢復正常。

Netty的心跳檢測實際上是利用了鏈路空閒檢測機制實現的,它的空閒檢測機制分為三種:

1.讀空閒,鏈路持續時間t沒有讀取到任何訊息。

2.寫空閒,鏈路持續時間t沒有傳送任何訊息。

3.讀寫空閒,鏈路持續時間t沒有接收或者傳送任何訊息。

Netty的預設讀寫空閒機制是發生超時異常,關閉連線,但是,我們可以定製它的超時實現機制,以便支援不同的使用者場景,鏈路空閒介面定義如下:

protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {        ctx.fireUserEventTriggered(evt);    }

鏈路空閒的時候並沒有關閉鏈路,而是觸發IdleStateEvent事件,使用者訂閱IdleStateEvent事件,用於自定義邏輯處理,例如關閉鏈路、客戶端發起重新連線、告警和列印日誌等。利用Netty提供的鏈路空閒檢測機制,可以非常靈活的實現鏈路空閒時的有效性檢測。

2.2 客戶端斷連重連

當發生如下異常時,客戶端需要釋放資源,重新發起連線:

1.服務端因為某種原因,主動關閉連線,客戶端檢測到鏈路被正常關閉。

2.服務端因為當機等故障,強制關閉連線,客戶端檢測到鏈路被Rest掉。

3.心跳檢測超時,客戶端主動關閉連線。

4.客戶端因為其它原因(例如解碼失敗),強制關閉連線。

5.網路類故障,例如網路丟包、超時、單通等,導致鏈路中斷。

客戶端檢測到鏈路中斷後,等待INTERVAL時間,由客戶端發起重連操作,如果重連失敗,間隔週期INTERVAL後再次發起重連,直到重連成功。

為了保證服務端能夠有充足的時間釋放控制程式碼資源,在首次斷連時客戶端需要等待INTERVAL時間之後再發起重連,而不是失敗後就立即重連。

為了保證控制程式碼資源能夠及時釋放,無論什麼場景下的重連失敗,客戶端都必須保證自身的資源被及時釋放,包括但不限於SocketChannel、Socket等。重連失敗後,需要列印異常堆疊資訊,方便後續的問題定位。

利用Netty Channel提供的CloseFuture,可以非常方便的檢測鏈路狀態,一旦鏈路關閉,相關事件即被觸發,可以重新發起連線操作,程式碼示例如下:

future.channel().closeFuture().sync();   } finally {       // 所有資源釋放完成之後,清空資源,再次發起重連操作       executor.execute(new Runnable() {      public void run() {          try {         TimeUnit.SECONDS.sleep(3);//3秒之後發起重連,等待控制程式碼釋放         try {                // 發起重連操作             connect(NettyConstant.PORT, NettyConstant.REMOTEIP);         } catch (Exception e) {          ......異常處理相關程式碼省略      }       });

2.3 快取重發

當我們呼叫訊息傳送介面的時候,訊息並沒有真正被寫入到Socket中,而是先放入NIO通訊框架的訊息傳送佇列中,由Reactor執行緒掃描待傳送的訊息佇列,非同步的傳送給通訊對端。假如很不幸,訊息佇列中積壓了部分訊息,此時鏈路中斷,這會導致部分訊息並沒有真正傳送給通訊對端,示例如下:

\"\"

圖4 鏈路中斷導致積壓訊息沒有傳送

發生此故障時,我們希望NIO框架能夠自動實現訊息快取和重新傳送,遺憾的是作為基礎的NIO通訊框架,無論是Mina還是Netty,都沒有提供該功能,需要通訊框架自己封裝實現,基於Netty的實現策略如下:

1.呼叫Netty ChannelHandlerContext的write方法時,返回ChannelFuture物件,我們在ChannelFuture中註冊傳送結果監聽Listener。

2.在Listener的operationComplete方法中判斷操作結果,如果操作不成功,將之前傳送的訊息物件新增到重發佇列中。

3.鏈路重連成功之後,根據策略,將快取佇列中的訊息重新傳送給通訊對端。

需要指出的是,並非所有場景都需要通訊框架做重發,例如服務框架的客戶端,如果某個服務提供者不可用,會自動切換到下一個可用的服務提供者之上。假定是鏈路中斷導致的服務提供者不可用,即便鏈路重新恢復,也沒有必要將之前積壓的訊息重新傳送,因為訊息已經通過FailOver機制切換到另一個服務提供者處理。所以,訊息快取重發只是一種策略,通訊框架應該支援鏈路級重發策略。

2.4 客戶端超時保護

在傳統的同步阻塞程式設計模式下,客戶端Socket發起網路連線,往往需要指定連線超時時間,這樣做的目的主要有兩個:

1.在同步阻塞I/O模型中,連線操作是同步阻塞的,如果不設定超時時間,客戶端I/O執行緒可能會被長時間阻塞,這會導致系統可用I/O執行緒數的減少。

2.業務層需要:大多數系統都會對業務流程執行時間有限制,例如WEB互動類的響應時間要小於3S。客戶端設定連線超時時間是為了實現業務層的超時。

對於NIO的SocketChannel,在非阻塞模式下,它會直接返回連線結果,如果沒有連線成功,也沒有發生I/O異常,則需要將SocketChannel註冊到Selector上監聽連線結果。所以,非同步連線的超時無法在API層面直接設定,而是需要通過使用者自定義定時器來主動監測。

Netty在建立NIO客戶端時,支援設定連線超時引數。Netty的客戶端連線超時引數與其它常用的TCP引數一起配置,使用起來非常方便,上層使用者不用關心底層的超時實現機制。這既滿足了使用者的個性化需求,又實現了故障的分層隔離。

2.5 針對客戶端的併發連線數流控

以Netty的HTTPS服務端為例,針對客戶端的併發連線數流控原理如下所示:

\"\"

圖5 服務端HTTS連線數流控

基於Netty的Pipeline機制,可以對SSL握手成功、SSL連線關閉做切面攔截(類似於Spring的AOP機制,但是沒采用反射機制,效能更高),通過流控切面介面,對HTTPS連線做計數,根據計數器做流控,服務端的流控演算法如下:

1.獲取流控閾值。

2.從全域性上下文中獲取當前的併發連線數,與流控閾值對比,如果小於流控閾值,則對當前的計數器做原子自增,允許客戶端連線接入。

3.如果等於或者大於流控閾值,則丟擲流控異常給客戶端。

4.SSL連線關閉時,獲取上下文中的併發連線數,做原子自減。

在實現服務端流控時,需要注意如下幾點:

1.流控的ChannelHandler宣告為@ChannelHandler.Sharable,這樣全域性建立一個流控例項,就可以在所有的SSL連線中共享。

2.通過userEventTriggered方法攔截SslHandshakeCompletionEvent和SslCloseCompletionEvent事件,在SSL握手成功和SSL連線關閉時更新流控計數器。

3.流控並不是單針對ESTABLISHED狀態的HTTP連線,而是針對所有狀態的連線,因為客戶端關閉連線,並不意味著服務端也同時關閉了連線,只有SslCloseCompletionEvent事件觸發時,服務端才真正的關閉了NioSocketChannel,GC才會回收連線關聯的記憶體。

4.流控ChannelHandler會被多個NioEventLoop執行緒呼叫,因此對於相關的計數器更新等操作,要保證併發安全性,避免使用全域性鎖,可以通過原子類等提升效能。

2.6 記憶體保護

NIO通訊的記憶體保護主要集中在如下幾點:

1.鏈路總數的控制:每條鏈路都包含接收和傳送緩衝區,鏈路個數太多容易導致記憶體溢位。

2.單個緩衝區的上限控制:防止非法長度或者訊息過大導致記憶體溢位。

3.緩衝區記憶體釋放:防止因為緩衝區使用不當導致的記憶體洩露。

4.NIO訊息傳送佇列的長度上限控制。

當我們對訊息進行解碼的時候,需要建立緩衝區。緩衝區的建立方式通常有兩種:

1.容量預分配,在實際讀寫過程中如果不夠再擴充套件。

2.根據協議訊息長度建立緩衝區。

在實際的商用環境中,如果遇到畸形碼流攻擊、協議訊息編碼異常、訊息丟包等問題時,可能會解析到一個超長的長度欄位。筆者曾經遇到過類似問題,報文長度欄位值竟然是2G多,由於程式碼的一個分支沒有對長度上限做有效保護,結果導致記憶體溢位。系統重啟後幾秒內再次記憶體溢位,幸好及時定位出問題根因,險些釀成嚴重的事故。

Netty提供了編解碼框架,因此對於解碼緩衝區的上限保護就顯得非常重要。下面,我們看下Netty是如何對緩衝區進行上限保護的:

首先,在記憶體分配的時候指定緩衝區長度上限:

/**     * Allocate a {@link ByteBuf} with the given initial capacity and the given     * maximal capacity. If it is a direct or heap buffer depends on the actual     * implementation.     */    ByteBuf buffer(int initialCapacity, int maxCapacity);

其次,在對緩衝區進行寫入操作的時候,如果緩衝區容量不足需要擴充套件,首先對最大容量進行判斷,如果擴充套件後的容量超過上限,則拒絕擴充套件:

 @Override    public ByteBuf capacity(int newCapacity) {        ensureAccessible();        if (newCapacity \u0026lt; 0 || newCapacity \u0026gt; maxCapacity()) {            throw new IllegalArgumentException(\u0026quot;newCapacity: \u0026quot; + newCapacity);        }

在訊息解碼的時候,對訊息長度進行判斷,如果超過最大容量上限,則丟擲解碼異常,拒絕分配記憶體,以LengthFieldBasedFrameDecoder的decode方法為例進行說明:

if (frameLength \u0026gt; maxFrameLength) {            long discard = frameLength - in.readableBytes();            tooLongFrameLength = frameLength;            if (discard \u0026lt; 0) {                in.skipBytes((int) frameLength);            } else {                discardingTooLongFrame = true;                bytesToDiscard = discard;                in.skipBytes(in.readableBytes());            }            failIfNecessary(true);            return null;        }

3. RPC呼叫層的可靠性設計

3.1 RPC呼叫異常場景

RPC呼叫過程中除了通訊層的異常,通常也會遇到如下幾種故障:

  • 服務路由失敗。

  • 服務端超時。

  • 服務端呼叫失敗。

RPC框架需要能夠針對上述常見的異常做容錯處理,以提升業務呼叫的可靠性。

3.1.1 服務路由失敗

RPC客戶端通常會基於訂閱/釋出的機制獲取服務端的地址列表,並將其快取到本地,RPC呼叫時,根據負載均衡策略從本地快取的路由表中獲取到一個唯一的服務端節點發起呼叫,原理如下所示:

\"\"

圖6 基於訂閱釋出機制的RPC呼叫

通過快取的機制能夠提升RPC呼叫的效能,RPC客戶端不需要每次呼叫都向註冊中心查詢目標服務的地址資訊,但是也可能會發生如下兩類潛在故障:

1.某個RPC服務端發生故障,或者下線,客戶端沒有及時重新整理本地快取的服務地址列表,就會導致RPC呼叫失敗。

2.RPC客戶端和服務端都工作正常,但是RPC客戶端和服務端的連線或者網路發生了故障,如果沒有鏈路的可靠性檢測機制,就會導致RPC呼叫失敗。

3.1.2 服務端超時

當服務端無法在指定的時間內返回應答給客戶端,就會發生超時,導致超時的原因主要有:

1.服務端的I/O執行緒沒有及時從網路中讀取客戶端請求訊息,導致該問題的原因通常是I/O執行緒被意外阻塞或者執行長週期操作。

2.服務端業務處理緩慢,或者被長時間阻塞,例如查詢資料庫,由於沒有索引導致全表查詢,耗時較長。

3.服務端發生長時間Full GC,導致所有業務執行緒暫停執行,無法及時返回應答給客戶端。

3.1.3 服務端呼叫失敗

有時會發生服務端呼叫失敗,導致服務端呼叫失敗的原因主要有如下幾種:

1.服務端解碼失敗,會返回訊息解碼失敗異常。

2.服務端發生動態流控,返回流控異常。

3.服務端訊息佇列積壓率超過最大閾值,返回系統擁塞異常。

4.訪問許可權校驗失敗,返回許可權相關異常。

5.違反SLA策略,返回SLA控制相關異常。

6.其他系統異常。

需要指出的是,服務呼叫異常不包括業務層面的處理異常,例如資料庫操作異常、使用者記錄不存在異常等。

3.2 RPC呼叫可靠性方案

3.2.1註冊中心與鏈路檢測雙保險機制

因為註冊中心有叢集內所有RPC客戶端和服務端的例項資訊,因此通過註冊中心向每個服務端和客戶端傳送心跳訊息,檢測對方是否線上,如果連續N次心跳超時,或者心跳傳送失敗,則判斷對方已經發生故障或者下線(下線可以通過優雅停機的方式主動告知註冊中心,實時性會更好)。註冊中心將故障節點的服務例項資訊通過心跳訊息傳送給客戶端,由客戶端將故障的服務例項資訊從本地快取的路由表中刪除,後續訊息呼叫不再路由到該節點。

在一些特殊場景下,儘管註冊中心與服務端、客戶端的連線都沒有問題,但是服務端和客戶端之間的鏈路發生了異常,由於發生鏈路異常的服務端仍然在快取表中,因此訊息還會繼續排程到故障節點上,所以,利用RPC客戶端和服務端之間的雙向心跳檢測,可以及時發現雙方之間的鏈路問題,利用重連等機制可以快速的恢復連線,如果重連N次都失敗,則服務路由時不再將訊息傳送到連線故障的服務節點上。

利用註冊中心對服務端的心跳檢測和通知機制、以及服務端和客戶端針對鏈路層的雙向心跳檢測機制,可以有效檢測出故障節點,提升RPC呼叫的可靠性,它的原理如下所示:

\"\"

圖7 註冊中心與鏈路雙向心跳檢測機制原理

3.2.2 叢集容錯策略

常用的叢集容錯策略包括:

1.失敗自動切換(Failover)。

2.失敗通知(Failback)。

3.失敗快取(Failcache)。

4.快速失敗(Failfast)。

失敗自動切換策略:服務呼叫失敗自動切換策略指的是當發生RPC呼叫異常時,重新選路,查詢下一個可用的服務提供者。

服務釋出的時候,可以指定服務的叢集容錯策略。消費者可以覆蓋服務提供者的通用配置,實現個性化的容錯策略。

Failover策略的設計思路如下:消費者路由操作完成之後,獲得目標地址,呼叫通訊框架的訊息傳送介面傳送請求,監聽服務端應答。如果返回的結果是RPC呼叫異常(超時、流控、解碼失敗等系統異常),根據消費者叢集容錯的策略進行容錯路由,如果是Failover,則重新返回到路由Handler的入口,從路由節點繼續執行。選路完成之後,對目標地址進行比對,防止重新路由到故障服務節點,過濾掉上次的故障服務提供者之後,呼叫通訊框架的訊息傳送介面傳送請求訊息。

RPC框架提供Failover容錯策略,但是使用者在使用時需要自己保證用對地方,下面對Failover策略的應用場景進行總結:

1.讀操作,因為通常它是冪等的。

2.冪等性服務,保證呼叫1次與N次效果相同。

需要特別指出的是,失敗重試會增加服務呼叫時延,因此框架必須對失敗重試的最大次數做限制,通常預設為3,防止無限制重試導致服務呼叫時延不可控。

失敗通知(Failback):在很多業務場景中,客戶端需要能夠獲取到服務呼叫失敗的具體資訊,通過對失敗錯誤碼等異常資訊的判斷,決定後續的執行策略,例如非冪等性的服務呼叫。

Failback的設計方案如下:RPC框架獲取到服務提供者返回的RPC異常響應之後,根據策略進行容錯。如果是Failback模式,則不再重試其它服務提供者,而是將RPC異常通知給客戶端,由客戶端捕獲異常進行後續處理。

失敗快取(Failcache):Failcache策略是失敗自動恢復的一種,在實際專案中它的應用場景如下:

1.服務有狀態路由,必須定點傳送到指定的服務提供者。當發生鏈路中斷、流控等服務暫時不可用時,RPC框架將訊息臨時快取起來,等待週期T,重新傳送,直到服務提供者能夠正常處理該訊息。

2.對時延要求不敏感的服務。系統服務呼叫失敗,通常是鏈路暫時不可用、服務流控、GC掛住服務提供者程式等,這種失敗不是永久性的失敗,它的恢復是可預期的。如果客戶端對服務呼叫時延不敏感,可以考慮採用自動恢復模式,即先快取,再等待,最後重試。

3.通知類服務。例如通知粉絲積分增長、記錄介面日誌等,對服務呼叫的實時性要求不高,可以容忍自動恢復帶來的時延增加。

為了保證可靠性,Failcache策略在設計的時候需要考慮如下幾個要素:

1.快取時間、快取物件上限數等需要做出限制,防止記憶體溢位。

2.快取淘汰演算法的選擇,是否支援使用者配置。

3.定時重試的週期T、重試的最大次數等需要做出限制並支援使用者指定。

4.重試達到最大上限仍失敗,需要丟棄訊息,記錄異常日誌。

快速失敗(Failfast):在業務高峰期,對於一些非核心的服務,希望只呼叫一次,失敗也不再重試,為重要的核心服務節約寶貴的執行資源。此時,快速失敗是個不錯的選擇。快速失敗策略的設計比較簡單,獲取到服務呼叫異常之後,直接忽略異常,記錄異常日誌。

4. 第三方服務依賴故障隔離

4.1 總體策略

儘管很多第三方服務會提供SLA,但是RPC服務本身並不能完全依賴第三方服務自身的可靠性來保障自己的高可靠,第三方服務依賴隔離的總體策略如下:

1.第三方依賴隔離可以採用執行緒池 + 響應式程式設計(例如RxJava)的方式實現。

2.對第三方依賴進行分類,每種依賴對應一個獨立的執行緒/執行緒池。

3.服務不直接呼叫第三方依賴的API,而是使用非同步封裝之後的API介面。

4.非同步呼叫第三方依賴API之後,獲取Future物件。利用響應式程式設計框架,

可以訂閱後續的事件,接收響應,針對響應進行程式設計。

4.2 非同步化

如果第三方服務提供的是標準的HTTP/Restful服務,則利用非同步HTTP客戶端,例如Netty、Vert.x、非同步RestTemplate等發起非同步服務呼叫,這樣無論是服務端自身處理慢還是網路慢,都不會導致呼叫方被阻塞。

如果對方是私有或者定製化的協議,SDK沒有提供非同步介面,則需要採用執行緒池或者利用一些開源框架實現故障隔離。

非同步化示例圖如下所示:

\"\"

圖8 非同步化原理示意圖

非同步化的幾個關鍵技術點:

1.非同步具有依賴和傳遞性,如果想在某個業務流程的某個過程中做非同步化,則入口處就需要做非同步。例如如果想把Redis服務呼叫改造成非同步,則呼叫Redis服務之前的流程也需要同時做非同步化,否則意義不大(除非呼叫方不需要返回值)。

2.通常而言,全棧非同步對於業務效能和可靠性提升的意義更大,全棧非同步會涉及到內部服務呼叫、第三方服務呼叫、資料庫、快取等平臺中介軟體服務的呼叫,非同步化改造成本比較高,但是收益也比較明顯。

3.不同框架、服務的非同步程式設計模型儘量保持一致,例如統一採取RxJava風格的介面、或者JDK8的CompletableFuture。如果不同服務SDK的非同步API介面風格差異過大,會增加業務的開發成本,也不利用執行緒模型的歸併和整合。

4.3.基於Hystrix的第三方依賴故障隔離

整合Netflix開源的Hystrix框架,可以非常方便的實現第三方服務依賴故障隔離,它提供的主要功能包括:

1.依賴隔離。

2.熔斷器。

3.優雅降級。

4.Reactive程式設計。

5.訊號量隔離。

建議的整合策略如下:

1.第三方依賴隔離:使用HystrixCommand做一層非同步封裝,實現業務的RPC服務呼叫執行緒和第三方依賴的執行緒隔離。

2.依賴分類管理:對第三方依賴進行分類、分組管理,根據依賴的特點設定熔斷策略、優雅降級策略、超時策略等,以實現差異化的處理。

總體整合檢視如下所示:

\"\"

圖9 基於Hystrix的第三方故障隔離框架

基於Hystrix可以非常方便的實現第三方依賴服務的熔斷降級,它的工作原理如下:

1.熔斷判斷:服務呼叫時,對熔斷開關狀態進行判斷,當熔斷器開關關閉時, 請求被允許通過熔斷器。

2.熔斷執行:當熔斷器開關開啟時,服務呼叫請求被禁止通過,執行失敗回撥介面。

3.自動恢復:熔斷之後,週期T之後允許一條訊息通過,如果成功,則取消熔斷狀態,否則繼續處於熔斷狀態。

流程如下所示:

\"\"

圖10 基於Hystrix的熔斷降級

5. 作者簡介

李林鋒,10年Java NIO、平臺中介軟體設計和開發經驗,精通Netty、Mina、分散式服務框架、API Gateway、PaaS等,《Netty進階之路》、《分散式服務框架原理與實踐》作者。目前在華為終端應用市場負責業務微服務化、雲化、全球化等相關設計和開發工作。

聯絡方式:新浪微博 Nettying 微信:Nettying

Email:neu_lilinfeng@sina.com

相關文章