高性能 Java 應(yīng)用層網(wǎng)關(guān)設(shè)計(jì)實(shí)踐
前言
上文我們簡(jiǎn)單闡述了一下接入層網(wǎng)關(guān)的實(shí)現(xiàn)原理
不少人對(duì) Java 網(wǎng)關(guān)的實(shí)現(xiàn)也比較感興趣,所以這篇文章我們來(lái)簡(jiǎn)單談?wù)?Java 應(yīng)用網(wǎng)關(guān)設(shè)計(jì),本文將會(huì)從以下幾個(gè)方面來(lái)闡述 Java 應(yīng)用層網(wǎng)關(guān)的設(shè)計(jì)
-
Java 應(yīng)用層網(wǎng)關(guān)的必要性 -
核心網(wǎng)關(guān)技術(shù)選型 -
嵌入式網(wǎng)關(guān) 設(shè)計(jì)
Java 應(yīng)用層網(wǎng)關(guān)的必要性
我們的 Java 網(wǎng)關(guān)分為應(yīng)用層網(wǎng)關(guān)和業(yè)務(wù)嵌入式網(wǎng)關(guān)兩部分,架構(gòu)圖如下

Java 網(wǎng)關(guān)分為核心網(wǎng)關(guān)和業(yè)務(wù)嵌入式網(wǎng)關(guān)服務(wù)兩部分,主要工作原理如下
-
接入層流量首先進(jìn)入 Java 核心網(wǎng)關(guān),經(jīng)過(guò)一系列的 pipeline 處理(風(fēng)控,路由協(xié)議轉(zhuǎn)換、流控、降級(jí)等操作)后發(fā)起泛化調(diào)用再打入業(yè)務(wù)層網(wǎng)關(guān) -
業(yè)務(wù)層網(wǎng)關(guān)也會(huì)經(jīng)過(guò)一系列的 pipeline(接口校驗(yàn),驗(yàn)簽,session 校驗(yàn)等)進(jìn)入最終的業(yè)務(wù)邏輯,然后再調(diào)用相關(guān) dubbo 服務(wù)最終完成本次 Java 請(qǐng)求的響應(yīng)。
核心網(wǎng)關(guān)與嵌入式業(yè)務(wù)網(wǎng)關(guān)的功能如下

其中嵌入式網(wǎng)關(guān)是以 jar 包的形式集成到業(yè)務(wù)的工程里的,具體為啥要這樣設(shè)計(jì),后文會(huì)詳述。
首先來(lái)看 Java 網(wǎng)關(guān)為啥要分成核心網(wǎng)關(guān)和嵌入式業(yè)務(wù)網(wǎng)關(guān)兩部分,直接從接入層打到業(yè)務(wù)網(wǎng)關(guān)不是更省事嗎,何必多此一舉再加一層核心網(wǎng)關(guān),多加一層不是多了一個(gè)損耗嗎。
這里有三個(gè)原因
-
核心網(wǎng)關(guān)主要起著風(fēng)控,鑒權(quán)、路由協(xié)議轉(zhuǎn)換、流控、降級(jí),打點(diǎn)統(tǒng)計(jì)(請(qǐng)求報(bào)錯(cuò)等)等作用,這些功能對(duì)每一個(gè)層請(qǐng)求來(lái)說(shuō)都是通用的,統(tǒng)一將這些功能抽離放在核心網(wǎng)關(guān)實(shí)現(xiàn)更合理。 -
當(dāng)然了,可以統(tǒng)一把第一點(diǎn)所述的這些功能放在接入層實(shí)現(xiàn),但這樣會(huì)讓接入層顯得很臃腫,另外第一點(diǎn)中有一個(gè)很重要的功能,路由協(xié)議轉(zhuǎn)換(將 http 轉(zhuǎn)成 dubbo),由于我們的接入層用的是 OpenResty,它是不支持這種協(xié)議轉(zhuǎn)換的,除非基于 OpenResty 做二次開(kāi)發(fā),這樣費(fèi)時(shí)費(fèi)力,也無(wú)必要,這樣看來(lái)抽出一個(gè) Java 核心網(wǎng)關(guān)來(lái)?yè)?dān)任第一點(diǎn)所述的功能是更合理的,計(jì)算機(jī)界不有一句話(huà)么:任何問(wèn)題,在計(jì)算機(jī)界都可以通過(guò)加入一個(gè)中間層來(lái)解決。加一個(gè) Java 核心網(wǎng)關(guān)符合單一職責(zé),分層的設(shè)計(jì)理念。 -
加入一個(gè)核心網(wǎng)關(guān),確實(shí)多了一層,也多了一個(gè)損耗,不過(guò)核心網(wǎng)關(guān)并不處理具體的邏輯,它主要起著流量轉(zhuǎn)發(fā)的作用,而且在下文我們可以看到,它采用了 webflux 這種反應(yīng)式編程框架,帶來(lái)的損耗比起引入它帶來(lái)的優(yōu)勢(shì)可以忽略不計(jì)。
接下來(lái)我們簡(jiǎn)單談?wù)労诵木W(wǎng)關(guān)和業(yè)務(wù)網(wǎng)關(guān)的設(shè)計(jì)思路。
核心網(wǎng)關(guān)技術(shù)選型
同步阻塞 VS 異步非阻塞
上節(jié)介紹可知 Java 核心網(wǎng)關(guān)承擔(dān)著所有的流量入口,本身會(huì)調(diào)用大量的業(yè)務(wù)接口(打到業(yè)務(wù)網(wǎng)關(guān)里),所以 IO 操作會(huì)很頻繁,在技術(shù)選型上是有要求的, 首先來(lái)看看傳統(tǒng)的 Spring MVC(servlet 3.0之前)

顯然我們應(yīng)該采用異步非阻塞的編程模型,它是如何工作的呢,如下圖示
工作原理如下
-
只有一個(gè) request 線(xiàn)程負(fù)責(zé) accept 所有的請(qǐng)求,每個(gè)請(qǐng)求都有一個(gè) Event handler 和回調(diào),request 線(xiàn)程接收到 request 請(qǐng)求后,首先會(huì)為此請(qǐng)求在 Event Loop 中注冊(cè)一個(gè)回調(diào)函數(shù),緊接著馬上把這個(gè)請(qǐng)求丟給線(xiàn)程池中的某個(gè)線(xiàn)程處理,然后此 request 線(xiàn)程立馬返回,馬上就可以處理另外的請(qǐng)求了。 -
線(xiàn)程池中的線(xiàn)程處理完請(qǐng)求的 Event Handler(DB,網(wǎng)絡(luò)IO等邏輯) 后,會(huì)去調(diào)用之前注冊(cè)好的回調(diào)函數(shù)返回請(qǐng)求結(jié)果
從以上的工作原理可以看出,負(fù)責(zé)處理請(qǐng)求的 request 線(xiàn)程只需求一個(gè),線(xiàn)程數(shù)大大減少!更少的線(xiàn)程意味著更高的內(nèi)存利用,也意味著線(xiàn)程間的切換開(kāi)銷(xiāo)大大減少!所以顯然應(yīng)該使用這種編程模型。
打個(gè)簡(jiǎn)單的比方,相信大家都有去酒店就餐的經(jīng)歷,對(duì)于酒店來(lái)說(shuō),怎么才能最大化地提高接客效率呢
-
一種方式是對(duì)每一個(gè)客人,都安排一位接待員,這名接待員負(fù)責(zé)客人的接待,入座,上菜等所有流程,顯然如果這樣安排的話(huà)有多少位客人就等安排多少位接待員。 -
第二種方式是只安排一位接待員,這名接待員在接待客人入座后,立刻回到門(mén)口迎接客人,剩下的交給上菜服務(wù)員(線(xiàn)程池工作),這樣的話(huà)接待員的人數(shù)就大大減少了,能極大地提升效率。
最終我們選擇了 Spring WebFlux 這種反應(yīng)式(Reactive),基于事件驅(qū)動(dòng)的異步非阻塞框架。
反應(yīng)式編程與 Spring WebFlux 簡(jiǎn)介
反應(yīng)式編程簡(jiǎn)介
反應(yīng)式編程 (reactive programming) 是一種基于數(shù)據(jù)流 (data stream) 和 變化傳遞 (propagation of change) 的 聲明式 (declarative) 的編程范式。它是一種編程思想,能夠基于數(shù)據(jù)流中的事件(變化)進(jìn)行相關(guān)反應(yīng)處理,舉個(gè)簡(jiǎn)單的例子:在 a = b + c 這個(gè)語(yǔ)句中,要得到 a 的值,如果用傳統(tǒng)的編程模型,每次 b 或 c 變化后都需要重新計(jì)算以獲得 a,而在反應(yīng)式編程中,我們把 b,c 當(dāng)作數(shù)據(jù)流,a 會(huì)對(duì) b,c 作出的變化實(shí)時(shí)響應(yīng)。
反應(yīng)式編程有以下幾個(gè)特點(diǎn)
1、事件驅(qū)動(dòng)
在事件驅(qū)動(dòng)的程序中,組件之間通過(guò)松藕合的生產(chǎn)者(也稱(chēng)被訂閱者,即 Publisher)和訂閱者模式(Subscriber)來(lái)實(shí)現(xiàn),這些事件是以異步和非阻塞的方式來(lái)接收和發(fā)送的,基于事件驅(qū)動(dòng)的編程有啥好處呢,簡(jiǎn)單地說(shuō)它是依靠推模式而不是拉模式來(lái)動(dòng)作的,也就是說(shuō)只有生產(chǎn)者有消息(變化)時(shí)才會(huì)通知消費(fèi)者作出響應(yīng),也就意味著消費(fèi)者不需要輪詢(xún)也不需要等待數(shù)據(jù)。
2、實(shí)時(shí)響應(yīng)
以我們的網(wǎng)關(guān)為例, request 線(xiàn)程接收請(qǐng)求后,快速返回存儲(chǔ)結(jié)果的上下文,把具體執(zhí)行交給線(xiàn)程池里的線(xiàn)程(可以認(rèn)為是后臺(tái)線(xiàn)程),處理完成后,異步地將調(diào)用結(jié)果封裝到結(jié)果的上下文中,可以看到此過(guò)程是完全異步的,也就是說(shuō)實(shí)時(shí)響應(yīng)必須通過(guò)異步編程實(shí)現(xiàn),在 Java 8 中,發(fā)起調(diào)用后可以快速返回 CompletableFuture 對(duì)象。
3、彈性機(jī)制
事件驅(qū)動(dòng)的松散耦合提供了組件在失敗下可以抓獲完全隔離的上下文場(chǎng)景,作為消息封裝,發(fā)送到其他組件時(shí),在具體編程時(shí)可以檢查錯(cuò)誤比如是否接受到,接受的命令是否可執(zhí)行等等,并決定如何應(yīng)對(duì)。
反應(yīng)式編程主要工作流程如下
-
被訂閱者主動(dòng)推送數(shù)據(jù)給訂閱者,在異步或完成時(shí)觸發(fā)另外的兩個(gè)方法 -
被訂閱者發(fā)生異常,會(huì)觸發(fā) onError -
所有的推送完成無(wú)異常,最終會(huì)執(zhí)行 onSuccess 方法
還有一個(gè)問(wèn)題,如果 Publisher 發(fā)送消息過(guò)快超過(guò) Subscriber 的處理速度了怎么辦,所以就得提一下背壓(BackPressure)的概念了,知乎網(wǎng)友扔物線(xiàn)對(duì)此概念解釋我認(rèn)為非常到位:
backpressure 是源自工程學(xué)中的概念:在管道運(yùn)輸中,氣流或液流由于管道突然變細(xì)、急彎等原因?qū)е掠赡程幊霈F(xiàn)了下游向上游的逆向壓力,這種情況稱(chēng)為「backpressure」,相應(yīng)的在反應(yīng)式編程中,在數(shù)據(jù)流從上游生產(chǎn)者向下游消費(fèi)者傳輸?shù)倪^(guò)程中,上游生產(chǎn)速度大于下游消費(fèi)速度,導(dǎo)致下游的 Buffer 溢出,這種現(xiàn)象就叫做 Backpressure 出現(xiàn),這里的重點(diǎn)在于「Buffer 溢出」,為什么需要 buffer, 因?yàn)?Publisher 生產(chǎn)速度大于 Subscriber 的消費(fèi)速度,所以需要 Buffer, 因?yàn)橥獠織l件限制,顯然 Buffer 是有上限的,如果生產(chǎn)速度超過(guò) buffer, 則 backpressure 產(chǎn)生,超過(guò) buffer 的話(huà),唯一的選擇就是丟掉新事件。
這就好比,比如你的 server 只能承受 5000~6000 的請(qǐng)求,如果你把 buffer 設(shè)置為 5000,則一旦請(qǐng)求數(shù)超過(guò) 5000,則背壓產(chǎn)生,超過(guò)的請(qǐng)求數(shù)丟棄,這樣保證了機(jī)器不會(huì)被源源不斷的 Publisher 生產(chǎn)事件壓垮,有效提升了網(wǎng)關(guān)的可用性。
Spring WebFlux 簡(jiǎn)介
為了更好地促進(jìn)反應(yīng)式編程的應(yīng)用,在 Java 平臺(tái)上,Netflix(開(kāi)發(fā)了 RxJava)、TypeSafe(開(kāi)發(fā)了 Scala、Akka)、Pivatol(開(kāi)發(fā)了 Spring、Reactor)共同制定了一個(gè)被稱(chēng)為 Reactive Streams 項(xiàng)目(規(guī)范),用于制定反應(yīng)式編程相關(guān)的規(guī)范以及接口。
Reactor 基于 Reactive Stream 定制了一套反應(yīng)式編程框架,而 WebFlux 則是以 Reactor 為基礎(chǔ)實(shí)現(xiàn)了 Web 領(lǐng)域的反應(yīng)式編程框架,由于反應(yīng)式編程的異步非阻塞特性,所以 WebFlux 運(yùn)行于 Netty , Undertow 等支持異步編程模型的 server 之上,當(dāng)然也可運(yùn)行于支持 Servlet 3.1 的 Server 容器上(Servlet 3.1 開(kāi)始支持異步)

為了讓大家更好利用 webflux 編程,Spring 貼心地兼容了 @Controller 等 Spring MVC 的注解在 webflux 的使用,能讓使用者更好地過(guò)渡到 webflux 編程中來(lái),不過(guò)在底層實(shí)現(xiàn)中,與 Spring MVC 的實(shí)現(xiàn)的請(qǐng)求 InputStream 和響應(yīng) OutputStream 不同,webflux 實(shí)現(xiàn)了一套反應(yīng)式的請(qǐng)求(ServerHttpRequest) 和響應(yīng)(ServerHttpResponse),這兩個(gè)類(lèi)將請(qǐng)求體與響應(yīng)體以 Flux
通過(guò)介紹可以看到 webflux 實(shí)現(xiàn)了從請(qǐng)求到響應(yīng),到渲染,事件發(fā)送等一整套反應(yīng)式事件的支持,是的,要最大程度地發(fā)揮 webflux 的性能,中間所有的事件都應(yīng)該以 Mono 或 Flux 響應(yīng)式事件流的形式存在!
WebFlux 的底層實(shí)現(xiàn)其實(shí)是基于 Reactor 實(shí)現(xiàn)的,在 Reactor 的核心類(lèi)中,以下兩個(gè)類(lèi)代表了發(fā)布者
-
Mono: 代表 0 到 1 個(gè)元素的發(fā)布者 -
Flux:代表 0 到 N 個(gè)元素的發(fā)布者
這玩意怎么用呢,如下圖示
@RequestMapping("/demo")
@RestController
public class DemoController {
@RequestMapping(value = "/foobar")
public Mono<Foobar> foobar() {
return Mono.just(new Foobar());
}
}
本來(lái)是要返回 foobar 對(duì)象的,結(jié)果最終以 Mono
在我們的網(wǎng)關(guān)設(shè)計(jì)中,當(dāng)收到請(qǐng)求后,使用了 Mono 來(lái)充當(dāng)發(fā)布者,如果中間出現(xiàn)了問(wèn)題,會(huì)調(diào)用 onError, 最終成功后會(huì)調(diào)用 onSuccess,以下是網(wǎng)關(guān)實(shí)現(xiàn)采用的總體框架。

網(wǎng)關(guān)的責(zé)任鏈設(shè)計(jì)
不管是核心網(wǎng)關(guān)還是嵌入式網(wǎng)關(guān)我們都采用了責(zé)任鏈模式來(lái)實(shí)現(xiàn)網(wǎng)關(guān)的核心處理流程,將每個(gè)處理邏輯看成一個(gè)slot,每個(gè) slot 按照預(yù)先設(shè)定的順序先后執(zhí)行,與開(kāi)源kong,zuul等類(lèi)似,我們也采用了PRPE模式(Pre、Routing、Post、Error)

Pre 階段:
-
initParamsSlot 初始化組裝請(qǐng)求上下文參數(shù) -
sentinelSlot 流控組件引入 ,做集群限流、降級(jí)、熔斷使用 -
riskSlot 風(fēng)控處理
-
dubboSlot 通過(guò) dubbo 泛化調(diào)用轉(zhuǎn)換成 dubbo 協(xié)議進(jìn)行遠(yuǎn)程調(diào)用
-
APMMonitorSlot APM 監(jiān)控處理,請(qǐng)求出錯(cuò)等打點(diǎn)監(jiān)控
采用這樣的設(shè)計(jì)方式,各個(gè) slot 各司其職,也有較好的可擴(kuò)展性,如果還想加什么 slot,定義好此 slot 功能,指定好其在調(diào)用鏈中的位置即可。
需要注意的是有些 Slot 的請(qǐng)求結(jié)果依賴(lài)于前面 Slot 的執(zhí)行結(jié)果,這種情況下需要對(duì)前面的執(zhí)行事件用 Mono 的形式封裝起來(lái),這樣這些 slot 就構(gòu)成了一個(gè)個(gè)的響應(yīng)式事件流,保證了這些 Slot 都是異步執(zhí)行的,不會(huì)阻塞主線(xiàn)程。

另外注意高亮的 dubboSlot 階段,在 dubbo 2.7 之前 dubbo 底層返回 Future(會(huì)一直占用一個(gè)線(xiàn)程輪詢(xún)結(jié)果),對(duì)異步編程不友好,2.7 之后返回了 CompleteFuture,與 webflux 的異步編程模型完美結(jié)合(發(fā)起調(diào)用嵌入式網(wǎng)關(guān)后立馬返回,等調(diào)用完成后才執(zhí)行,是真正的異步)。
嵌入式網(wǎng)關(guān)設(shè)計(jì)
首先我們要明白為啥會(huì)有嵌入式網(wǎng)關(guān)的需求,主要有三個(gè)原因
-
目前有 H5, 小程序,app 端,各端的 session 存儲(chǔ)不一樣,需要根據(jù)請(qǐng)求的各端來(lái)查找 session 對(duì)應(yīng)的 uid,這個(gè)操作顯然應(yīng)該在網(wǎng)關(guān)層面來(lái)做,放在嵌入式網(wǎng)關(guān)來(lái)實(shí)現(xiàn)更合理 -
每個(gè)請(qǐng)求進(jìn)入業(yè)務(wù)層之后,我們需要對(duì)其時(shí)間戳,app 簽名,小程序簽名等進(jìn)行校驗(yàn),這些校驗(yàn)對(duì)每個(gè)端的請(qǐng)求都是必要的,所以顯然應(yīng)該在網(wǎng)關(guān)來(lái)做 -
有些業(yè)務(wù)需要在執(zhí)行業(yè)務(wù)前后做一些擴(kuò)展,比如執(zhí)行前后需要打點(diǎn)分析等,對(duì)擴(kuò)展的實(shí)現(xiàn)網(wǎng)關(guān)也應(yīng)該支持
那么嵌入式網(wǎng)關(guān)如何實(shí)現(xiàn)呢,業(yè)務(wù)服務(wù)是以 dubbo 服務(wù)的形式存在的,而在 dubbo 中有一個(gè) Filter 機(jī)制,是專(zhuān)門(mén)為服務(wù)提供方和服務(wù)消費(fèi)方調(diào)用過(guò)程進(jìn)行攔截設(shè)計(jì)的,每次遠(yuǎn)程方法執(zhí)行,該攔截都會(huì)被執(zhí)行。這樣就為開(kāi)發(fā)者提供了非常方便的擴(kuò)展性,所以嵌入式網(wǎng)關(guān)的主要設(shè)計(jì)思路就是自定義 dubbo 的 filter,然后在此 filter 中執(zhí)行相關(guān)的擴(kuò)展邏輯即可,偽代碼如下:

這樣通過(guò)自定義 filter 的方式我們解決了擴(kuò)展性的問(wèn)題,注意我們使用了Activate注解,這樣 dubbo 就會(huì)把注釋的Filter 作為 dubbo 原生的 Filter 自動(dòng)加載,而不需要顯示的配置 provider 或者 consumer 的 filter,也就避免了對(duì)代碼的侵入性。
這里的業(yè)務(wù)邏輯執(zhí)行前后的擴(kuò)展也是通過(guò)責(zé)任鏈的模式來(lái)執(zhí)行一個(gè)個(gè)的的 slot, 我們先定義好時(shí)間戳校驗(yàn),簽名校驗(yàn),Session轉(zhuǎn)id等 slot, 然后在 xml 中指定這些 slot 的執(zhí)行順序

每個(gè)業(yè)務(wù)都有一個(gè) gateway.xml 文件,可以在此文件中配置 H5, app, 小程序需要執(zhí)行的 slot。
以對(duì) app 請(qǐng)求配置需要執(zhí)行的前置 slot 和后置處理 slot 為例 ,偽代碼如下

這樣只要在啟動(dòng)函數(shù)中引入(ImportResource)需要支持的 gateway 的 xml 文件,配置的 bean 就能生效,然后在 filter 中會(huì)分別取 bizChannel(請(qǐng)求必傳,代表是業(yè)務(wù)哪一端標(biāo)識(shí),如 biz_h5, biz_app, biz_小程序)對(duì)應(yīng)的 slotBizList 即可執(zhí)行業(yè)務(wù)邏輯前后的擴(kuò)展。

通過(guò)這樣的方式就有效地指定了業(yè)務(wù)邏輯執(zhí)行前后需要執(zhí)行的 slot,每個(gè)業(yè)務(wù)如果想在業(yè)務(wù)邏輯執(zhí)行前后進(jìn)行擴(kuò)展,只要定義好自己的 slot 邏輯,在 xml 文件中指定此 slot 的位置即可生效。
嵌入式網(wǎng)關(guān)按以上思路實(shí)現(xiàn)后,就通過(guò) jar 包分發(fā)到各個(gè)業(yè)務(wù)系統(tǒng)。好處是:穩(wěn)定性提升,每個(gè)業(yè)務(wù)集成一個(gè)穩(wěn)定版本的網(wǎng)關(guān) Jar,某一個(gè)業(yè)務(wù)系統(tǒng)做網(wǎng)關(guān) Jar 升級(jí)時(shí),其他業(yè)務(wù)系統(tǒng)都不受干擾
總結(jié)
本文詳細(xì)介紹了網(wǎng)關(guān)的實(shí)踐思路,相信大家對(duì)反應(yīng)式編程,dubbo filter 等應(yīng)該有了一定的了解,首先 Java 核心網(wǎng)關(guān)作為承載所有流量的入口,必然對(duì)其性能有較高的要求,而使用反應(yīng)式編程的異步非阻塞編程模型能很好地滿(mǎn)足我們的需求(關(guān)于反應(yīng)式編程的介紹如有不明白的,可以再看看文末的參考鏈接,介紹的清晰明了),其次不同業(yè)務(wù)在業(yè)務(wù)邏輯執(zhí)行前后需要做各種各樣的擴(kuò)展,所以我們使用自定義的 filter 實(shí)現(xiàn)了這種需求,這種需求顯然放在嵌入式網(wǎng)關(guān)實(shí)現(xiàn)更合理,而讓嵌入式網(wǎng)關(guān)以 jar 包的形式嵌入業(yè)務(wù)服務(wù)中,做到了對(duì)業(yè)務(wù)層的無(wú)侵入,也有較強(qiáng)的可擴(kuò)展性。
巨人的肩膀
https://juejin.im/post/6844903631133622285#heading-16 https://blog.csdn.net/qiangcuo6087/article/details/79024646
https://www.zhihu.com/question/49618581
https://howtodoinjava.com/spring-webflux/spring-webflux-tutorial/
特別推薦一個(gè)分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒(méi)關(guān)注的小伙伴,可以長(zhǎng)按關(guān)注一下:
長(zhǎng)按訂閱更多精彩▼
如有收獲,點(diǎn)個(gè)在看,誠(chéng)摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!