用Java構(gòu)建響應(yīng)式微服務(wù)4-構(gòu)建響應(yīng)式微服務(wù)系統(tǒng)

前一章節(jié)聚焦在構(gòu)建微服務(wù),這一章節(jié)全部是關(guān)于構(gòu)建系統(tǒng)。一個(gè)微服務(wù)不能成為一個(gè)服務(wù)系統(tǒng)。當(dāng)你擁抱微服務(wù)構(gòu)架,你將有成打的微服務(wù)。管理兩個(gè)微服務(wù),正如上一章節(jié)我們所做的,是容易的。你用的微服務(wù)越多,應(yīng)用就會(huì)變得更復(fù)雜。

首先,我們將學(xué)習(xí)服務(wù)發(fā)現(xiàn)怎樣被用來實(shí)現(xiàn)透明的、移動(dòng)的地址定位。然后,我們將討論可恢復(fù)性及其常見模式比如超時(shí)、熔斷器以及故障轉(zhuǎn)移。


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

當(dāng)你有一組微服務(wù)器,你不得不回答的第一個(gè)問題是:這些微服務(wù)彼此怎樣定位?為了與另一個(gè)通訊,一個(gè)微服務(wù)需要知道另一個(gè)的地址。正如我們在前一章節(jié)所做的,我們能夠在代碼中硬編碼地址(事件總線地址,URL,位置描述等等),或者放到一個(gè)外部的配置文件中。然而,這種解決方式不能夠移動(dòng)。你的應(yīng)用將會(huì)相當(dāng)固化,不同的部分不能夠移動(dòng),這與我們用微服務(wù)的意圖相抵觸。


客戶端和服務(wù)端的服務(wù)發(fā)現(xiàn)

微服務(wù)需要可移動(dòng)但是能定位。一個(gè)消費(fèi)者需要在事先不知道一個(gè)微服務(wù)的確切地址的前提下能夠與之通訊,特別是微服務(wù)的地址會(huì)隨著時(shí)間而變化。位置透明提供了彈性和活力:消費(fèi)者能夠用輪詢策略請求微服務(wù)的不同實(shí)例,兩次請求之間微服務(wù)可能發(fā)生移動(dòng)或更新。

位置透明定位通過一個(gè)稱為服務(wù)發(fā)現(xiàn)的模式實(shí)現(xiàn)。每一個(gè)微服務(wù)應(yīng)該聲明它能夠怎樣被請求,它的特征,當(dāng)然包括它的位置,而且包括一些別的元數(shù)據(jù)比如安全策略和版本。這些聲明被存儲(chǔ)在服務(wù)發(fā)現(xiàn)基礎(chǔ)設(shè)施里,通常通過執(zhí)行環(huán)境來提供服務(wù)注冊器。一個(gè)微服務(wù)也能夠決定從服務(wù)注冊器里撤消服務(wù)。一個(gè)微服務(wù)搜索服務(wù)注冊器來發(fā)現(xiàn)匹配的服務(wù)、選擇最佳的一個(gè)(用某種標(biāo)準(zhǔn)),并開始使用它。圖4-1描述這些交互:


圖4-1 與服務(wù)注冊器的交互?

消費(fèi)服務(wù)有兩種模式。當(dāng)使用客戶端服務(wù)發(fā)現(xiàn)時(shí),消費(fèi)者基于名稱和元數(shù)據(jù)在服務(wù)注冊器中查找服務(wù),選擇一個(gè)匹配的服務(wù)并使用它。從服務(wù)注冊器獲得的引用包括目標(biāo)微服務(wù)的直接鏈接。因?yàn)槲⒎?wù)是動(dòng)態(tài)實(shí)體,服務(wù)發(fā)現(xiàn)基礎(chǔ)設(shè)施必須不僅允許提供者發(fā)布服務(wù)、消費(fèi)者查找服務(wù),而且也提供關(guān)于服務(wù)的可達(dá)以及棄離信息。當(dāng)使用客戶端服務(wù)發(fā)現(xiàn)時(shí),服務(wù)注冊器能夠提供多種形式比如分布式數(shù)據(jù)結(jié)構(gòu),一個(gè)專用的基礎(chǔ)設(shè)施比如Consul,或者存儲(chǔ)在存儲(chǔ)服務(wù)中,比如Apache Zookeeper或Redis。

或者,你能夠使用服務(wù)端服務(wù)發(fā)現(xiàn),讓一個(gè)負(fù)載均衡器、一個(gè)路由器、一個(gè)代理或者一個(gè)API Gateway為你管理發(fā)現(xiàn)(圖4-2)。消費(fèi)者仍然基于名稱和元數(shù)據(jù)查找服務(wù),但是得到的是一個(gè)虛擬地址。但消費(fèi)者請求服務(wù)時(shí),請求被路由到真實(shí)的地址,當(dāng)你使用Kubernetes或者AWS彈性負(fù)載均衡器時(shí)你就是使用了這種機(jī)制。


圖4-2 服務(wù)端服務(wù)發(fā)現(xiàn)?

Vert.X服務(wù)發(fā)現(xiàn)

Vert.X提供了一個(gè)可擴(kuò)展的服務(wù)發(fā)現(xiàn)機(jī)制。用同樣的API,你能夠使用客戶端或者服務(wù)端服務(wù)發(fā)現(xiàn)。Vert.X能夠從許多類型的服務(wù)發(fā)現(xiàn)基礎(chǔ)設(shè)施(比如Consul或者Kubernetes)導(dǎo)入或?qū)С龇?wù)(圖4-3)。它也能夠在沒有任何服務(wù)發(fā)現(xiàn)基礎(chǔ)設(shè)施的情況下使用,在這種情形下,它使用在Vert.X集群中共享的一個(gè)分布式的數(shù)據(jù)結(jié)構(gòu)。


圖4-3 從其他服務(wù)發(fā)現(xiàn)機(jī)制中導(dǎo)入服務(wù)、導(dǎo)出服務(wù)到其他服務(wù)發(fā)現(xiàn)機(jī)制

你可以通過類型查找服務(wù),獲得一個(gè)就緒的服務(wù)來使用。服務(wù)類型可以是一個(gè)http端點(diǎn)(endpoint)、一個(gè)事件總線地址、一個(gè)數(shù)據(jù)源等等。舉一個(gè)例子,如果你想查找我們在前面章節(jié)實(shí)現(xiàn)的命名為hello的http端點(diǎn),你將寫下面的代碼:

// We create an instance of servicediscovery

ServiceDiscovery discovery =ServiceDiscovery.create(vertx);

// As we know we want to use an HTTPmicroservice, we can

// retrieve a WebClient already configuredfor the service

HttpEndpoint.rxGetWebClient(discovery,

// This methodis a filter to select the service

rec ->rec.getName().endsWith("hello")

)

.flatMap(client ->

// We haveretrieved the WebClient, use it to call the service

client.get("/").as(BodyCodec.string()).rxSend()

)

.subscribe(response -> System.out.println(response.body()));

獲得的WebClient是被配上了服務(wù)的位置,這意味著你可以立刻用它來調(diào)用服務(wù)。如果你的環(huán)境是采用客戶端發(fā)現(xiàn),被配上的URL指向一個(gè)指定的服務(wù)實(shí)例;如果你采用的是服務(wù)端發(fā)現(xiàn),客戶端使用的是一個(gè)虛擬的URL。

取決于你的運(yùn)行時(shí)基礎(chǔ)設(shè)施,你可能不得不注冊你的服務(wù)。但是當(dāng)使用服務(wù)端服務(wù)發(fā)現(xiàn)時(shí),你通常不必做這個(gè),因?yàn)楫?dāng)服務(wù)被部署時(shí),你聲明了你的服務(wù)。否則,你需要顯示地發(fā)布你的服務(wù)。為了發(fā)布一個(gè)服務(wù),你需要?jiǎng)?chuàng)建一個(gè)記錄,包含服務(wù)名、位置和元數(shù)據(jù):

// We create the service discovery object

ServiceDiscovery discovery =ServiceDiscovery.create(vertx);

vertx.createHttpServer()

.requestHandler(req ->req.response().end("hello"))

.rxListen(8083)

.flatMap(

// Once the HTTP server is started (we are ready to serve)

// we publish the service.

server -> {

// We create a record describing the service and its

// location (for HTTP endpoint)

Record record = HttpEndpoint.createRecord(

"hello", // the name of the service

"localhost", // the host

server.actualPort(), // the port

"/" // the root of the endpoint

);

// We publish the service

return discovery.rxPublish(record);

}

)

.subscribe(rec ->System.out.println("Service published"));

在微服務(wù)基礎(chǔ)設(shè)施里,服務(wù)發(fā)現(xiàn)是一個(gè)關(guān)鍵組件。它能夠動(dòng)態(tài)、透明定位、可移動(dòng)。當(dāng)處理少量服務(wù)時(shí),服務(wù)發(fā)現(xiàn)可能看起來顯得笨重,但是,當(dāng)你的系統(tǒng)不斷增長時(shí),它是必須的。Vert.X服務(wù)發(fā)現(xiàn)提供你統(tǒng)一的API,與你所采用的基礎(chǔ)設(shè)施、服務(wù)發(fā)現(xiàn)類型不相關(guān)。然而,當(dāng)你的系統(tǒng)增長時(shí),會(huì)有另一種可變因素呈指數(shù)增長---失敗。


穩(wěn)定性和可恢復(fù)模式

當(dāng)處理分布式系統(tǒng)時(shí),失敗是頭第公民,你不得不與他們打交道。你的微服務(wù)必須知道他們請求的服務(wù)可能因?yàn)楹芏喾N原因而失敗。每個(gè)微服務(wù)間的交互將以某種方式失敗,你需要對這些失敗有所準(zhǔn)備。失敗能夠以不同的形式,從多種網(wǎng)絡(luò)錯(cuò)誤到語義錯(cuò)誤。


在響應(yīng)式微服務(wù)里管控失敗

響應(yīng)式微服務(wù)是有責(zé)任管控本地的失敗的。他們必須避免傳播失敗到別的微服務(wù)。換言之,你不應(yīng)該傳遞燙手山竽給別的微服務(wù)。因此,響應(yīng)式微服務(wù)把失敗作為頭等公民。

Vert.X開發(fā)模型把失敗作為一個(gè)重要的內(nèi)容。當(dāng)使用回調(diào)開發(fā)模型時(shí),處理器(Handler)常常接收一個(gè)AsyncResult作為參數(shù),這個(gè)結(jié)構(gòu)封裝了異常操作的結(jié)果:成功時(shí),你可以得到結(jié)果;失敗時(shí),它包含一個(gè)Throwable描述失?。?/p>

client.get("/").as(BodyCodec.jsonObject())

.send(ar -> {

if (ar.failed()) {

Throwable cause = ar.cause();

// You need to manage the failure.

} else {

// It's a success

JsonObject json = ar.result().body();

}

});

當(dāng)使用RxJava API時(shí),失敗管理能夠在subscribe方法里做:

client.get("/").as(BodyCodec.jsonObject())

.rxSend()

.map(HttpResponse::body)

.subscribe(

json -> { /* success */ },

err -> { /* failure */ }

);

如果失敗產(chǎn)生于一個(gè)被訂閱的流,錯(cuò)誤處理器會(huì)被調(diào)用。你也可以更早地處理失敗、以避免subscribe方法里的錯(cuò)誤處理器:

client.get("/").as(BodyCodec.jsonObject())

.rxSend()

.map(HttpResponse::body)

.onErrorReturn(t -> {

// Called if rxSend produces a failure

// We can return a default value

return new JsonObject();

})

.subscribe(

json -> {

// Always called, either with the actual result

// or with the default value.

}

);

管控錯(cuò)誤不好玩但它必須做。當(dāng)面對失敗的時(shí)候,響應(yīng)式微服務(wù)的代碼有責(zé)任做出合乎需要的決定。它也需要有所準(zhǔn)備,清楚它對其他微服務(wù)請求可能會(huì)失敗。


使用超時(shí)

當(dāng)處理分布式交互時(shí),我們經(jīng)常使用超時(shí)。超時(shí)是一個(gè)簡單的機(jī)制,允許你停止等待響應(yīng),一旦你認(rèn)為響應(yīng)不會(huì)到來時(shí)。恰當(dāng)?shù)某瑫r(shí)提供了失敗隔離,確保失敗被限制在它所影響的微服務(wù)、允許你處理超時(shí)繼續(xù)以降級模式執(zhí)行。

client.get(path)

.rxSend() // Invoke the service

// We need to be sure to use the Vert.xevent loop

.subscribeOn(RxHelper.scheduler(vertx))

// Configure the timeout, if no response,it publishes

// a failure in the Observable

.timeout(5, TimeUnit.SECONDS)

// In case of success, extract the body

.map(HttpResponse::bodyAsJsonObject)

// Otherwise use a fallback result

.onErrorReturn(t -> {

// timeout or another exception

return new JsonObject().put("message", "D'oh!Timeout");

})

.subscribe(

json -> {

System.out.println(json.encode());

}

);

超時(shí)經(jīng)常和重試一起使用,當(dāng)一個(gè)超時(shí)發(fā)生時(shí),我們可以重試一次。失敗之后立刻重試一個(gè)操作有多種作用,但是僅僅一些是有益的。如果操作失敗是因?yàn)樵谡埱笪⒎?wù)方面顯著的問題,立刻重試很可能再一次失敗。一些暫時(shí)的失敗能夠用重試來解決,特別是網(wǎng)絡(luò)失敗比如丟失消息。你可以像下面這樣來決定是否重試操作:

client.get(path)

.rxSend()

.subscribeOn(RxHelper.scheduler(vertx))

.timeout(5, TimeUnit.SECONDS)

// Configure the number of retries

// here we retry only once.

.retry(1)

.map(HttpResponse::bodyAsJsonObject)

.onErrorReturn(t -> {

return newJsonObject().put("message", "D'oh! Timeout");

})

.subscribe(

json ->System.out.println(json.encode())

);

這是重要的,記住超時(shí)并不表明操作失敗。在分布式系統(tǒng),存在很多失敗原因。讓我們看一個(gè)例子,你有兩個(gè)微服務(wù),A和B,A正發(fā)送一個(gè)請求給B,但是響應(yīng)沒有及時(shí)過來、A獲得一個(gè)超時(shí),在這個(gè)情況里,三種類型的失敗可能發(fā)生:

[if !supportLists]1)??????[endif]A至B之間的消息丟失了:操作沒有執(zhí)行;

[if !supportLists]2)??????[endif]B的操作失敗了:操作沒有完成它的執(zhí)行;

[if !supportLists]3)??????[endif]B至A的響應(yīng)消息丟失了:操作被成功執(zhí)行了,但是A沒有收到響應(yīng)。

最后一種case經(jīng)常被忽略,它可能是有害的。在這種情況下,結(jié)合超時(shí)和重試可能破壞了系統(tǒng)的完整性。重試僅僅可以用于冪等操作:這種操作你可以調(diào)用多次也不會(huì)改變最初調(diào)用的結(jié)果。使用重試之前,總是檢查確定你的系統(tǒng)能夠優(yōu)雅地處理重試操作。

重試也會(huì)使消費(fèi)者等待更長時(shí)間獲得響應(yīng),這也不是一件好事情。相比重試許多次,返回一個(gè)回退(fallback)通常是更好的。另外,持續(xù)地請求一個(gè)失敗的服務(wù),可能給跟蹤帶來不便。這兩個(gè)關(guān)系可被另一個(gè)可恢復(fù)模式所管控:熔斷器。


熔斷器(Circuit Breaker)

熔斷器是一種模式,用來處理重復(fù)的失敗。它保護(hù)微服務(wù)反復(fù)請求一個(gè)失敗的服務(wù)。熔斷器是管理交互的三種狀態(tài)自動(dòng)流轉(zhuǎn)(圖4-4)。它開始是關(guān)閉狀態(tài),在這種狀態(tài)下,熔斷器正常地執(zhí)行操作,如果交互成功,沒有任何事情發(fā)生,如果失敗,熔斷器記下了一次失敗,一旦失敗的次數(shù)(或者失敗的頻度)超過了閥值,熔斷器切換到打開狀態(tài),在這種狀態(tài)下,請求熔斷器立刻失敗、根本不執(zhí)行交互。代替執(zhí)行操作,熔斷器可能執(zhí)行回退(fallback)、提供一個(gè)缺省的結(jié)果。過了設(shè)置的時(shí)間后,熔斷器判斷操作有成功的可能,因此它進(jìn)入半開狀態(tài),在這種狀態(tài)下,下一個(gè)請求將執(zhí)行交互,取決于這個(gè)請求的結(jié)果,熔斷器恢復(fù)到關(guān)閉狀態(tài)、或者返回到打開狀態(tài)直到另一次超時(shí)。


圖4-4熔斷器狀態(tài)?

用Java實(shí)現(xiàn)的最有名的熔斷器是Hystrix(https://github.com/Netflix/Hystrix)。當(dāng)你在Vert.X微服務(wù)里使用Hystrix時(shí),你需要顯示地切換到Vert.X事件輪詢器、執(zhí)行不同的回調(diào)。或者,對于異常操作你可以用Vert.X內(nèi)置的熔斷器,強(qiáng)化Vert.X非阻塞的異步開發(fā)模型。

讓我們想象一個(gè)脆弱的hello微服務(wù)。消費(fèi)者應(yīng)該保護(hù)與這個(gè)服務(wù)的交互、象下面這樣使用熔斷器:

CircuitBreaker circuit =CircuitBreaker.create("my-circuit",

vertx,

new CircuitBreakerOptions()

.setFallbackOnFailure(true) // Call thefallback

// on failures

.setTimeout(2000) // Set the operationtimeout

.setMaxFailures(5) // Number of failuresbefore

// switching to

// the 'open' state

.setResetTimeout(5000) // Time beforeattempting

// to reset

// the circuit breaker

);

// ...

circuit.rxExecuteCommandWithFallback(future->

client.get(path)

.rxSend()

.map(HttpResponse::bodyAsJsonObject)

.subscribe(future::complete, future::fail),

t -> new JsonObject().put("message", "D'oh!Fallback")

).subscribe(

json -> {

// Get the actual json or the fallback value

System.out.println(json.encode());

}

);

在這段代碼里,http交互被熔斷器保護(hù)。當(dāng)失敗次數(shù)達(dá)到配置的閥值時(shí),熔斷器將停止請求微服務(wù)、取而替之調(diào)用一個(gè)回退。一段時(shí)間后,熔斷器將讓一個(gè)請求通過來檢查跟蹤微服務(wù)是否恢復(fù)。這個(gè)例子使用一個(gè)web客戶端,任何交互都能夠被熔斷器管理,以保護(hù)脆弱的服務(wù)、異常以及其它的失敗。

運(yùn)維團(tuán)隊(duì)需要監(jiān)控熔斷器切換到open狀態(tài),Hystrix和Vert.X熔斷器都有監(jiān)控能力。


健康檢查和故障轉(zhuǎn)移

超時(shí)和熔斷器允許消費(fèi)者在它們那一側(cè)處理失敗,崩潰呢?面對崩潰,故障轉(zhuǎn)移策略重啟失敗的部分。但是實(shí)現(xiàn)這個(gè)之前,我們必須能夠檢測微服務(wù)死掉了。

健康檢查是微服務(wù)提供的一個(gè)指示它的狀態(tài)的API。它告訴請求者服務(wù)是否是健康的。調(diào)用常常使用http交互但不是必須的。一套檢查被執(zhí)行、整體狀態(tài)被計(jì)算和返回。當(dāng)一個(gè)微服務(wù)被檢測出是不健康時(shí),它不應(yīng)當(dāng)再被請求,因?yàn)榻Y(jié)果很可能是失敗。注意請求一個(gè)健康的微服務(wù)也不確保成功。健康檢查僅僅表明服務(wù)正在運(yùn)行,它不保證精確地處理請求、網(wǎng)絡(luò)傳遞響應(yīng)。

取決于你的環(huán)境,你可能有不同層級的健康檢查。舉例來說,你可能有就緒檢查(readiness check),用于部署時(shí)確定微服務(wù)是否就緒可以接收請求(一切都被正確地初始化);活著檢查(liveness check)用于檢查不正常、表明微服務(wù)是否能夠成功地處理請求。當(dāng)目標(biāo)微服務(wù)不能響應(yīng)、活著檢查不能被執(zhí)行時(shí),微服務(wù)很可能崩潰了。

在Vert.X應(yīng)用里,這有幾種方式實(shí)現(xiàn)健康檢查。你可以簡單地實(shí)現(xiàn)route返回狀態(tài),或者甚至使用一個(gè)真實(shí)的請求。在你的應(yīng)用中你也可能使用Vert.X健康檢查模塊去實(shí)現(xiàn)幾種健康檢查、合成不同的結(jié)果。下面的代碼提供了一個(gè)提供兩個(gè)層級的健康檢查的例子:

Router router = Router.router(vertx);

HealthCheckHandler hch =HealthCheckHandler.create(vertx);

// A procedure to check if we can get adatabase connection

hch.register("db-connection",future -> {

client.rxGetConnection()

.subscribe(c -> {

future.complete();

c.close();

},

future::fail

);

});

// A second (business) procedure

hch.register("business-check",future -> {

// ...

});

// Map /health to the health check handler

router.get("/health").handler(hch);

// ...

完成健康檢查之后,你可以實(shí)現(xiàn)失敗轉(zhuǎn)移策略。一般地,策略是僅僅重啟系統(tǒng)死掉的部分。另外,失敗轉(zhuǎn)移常常是被你的運(yùn)行時(shí)基礎(chǔ)設(shè)施提供的。Vert.X提供了一個(gè)內(nèi)置的失敗轉(zhuǎn)移,當(dāng)集群中的一個(gè)節(jié)點(diǎn)死掉時(shí)會(huì)被觸發(fā)。使用內(nèi)置的Vert.X故障轉(zhuǎn)移,你不需要一個(gè)傳統(tǒng)的健康檢查比如周期性地ping Vert.X集群節(jié)點(diǎn)。當(dāng)Vert.X失去對一個(gè)節(jié)點(diǎn)的跟蹤時(shí),Vert.X挑選集群中一個(gè)健康的節(jié)點(diǎn)來重新部署死掉的部分。

失敗轉(zhuǎn)移保證你的系統(tǒng)運(yùn)行但是不解決根本原因---這是你的工作。當(dāng)應(yīng)用意外地死掉時(shí),應(yīng)該做一下剖析。


小結(jié)

這一章節(jié)定位于當(dāng)你的微服務(wù)系統(tǒng)增長時(shí)你需要面對的幾個(gè)關(guān)鍵問題。正如我們所學(xué)的,在任何微服務(wù)系統(tǒng)里,服務(wù)發(fā)現(xiàn)是必須有的,確保位置透明。然后,因?yàn)槭∈遣豢杀苊獾?,我們討論了一組模式來提升系統(tǒng)的可恢復(fù)性和穩(wěn)定性。

Vert.X包含了一個(gè)可插拔的服務(wù)發(fā)現(xiàn)設(shè)施,它能夠用同樣的API處理客戶端服務(wù)發(fā)現(xiàn)和服務(wù)端服務(wù)發(fā)現(xiàn)。Vert.X服務(wù)發(fā)現(xiàn)也能夠從其它服務(wù)發(fā)現(xiàn)基礎(chǔ)設(shè)施中導(dǎo)入服務(wù)、導(dǎo)出服務(wù)到其它服務(wù)發(fā)現(xiàn)基礎(chǔ)設(shè)施。Vert.X包含了一套恢復(fù)模式比如超時(shí)、熔斷器和失敗轉(zhuǎn)移。我們看到了這些模式的不同例子。不幸地,處理失敗,我們?nèi)圆坏貌蛔霾糠止ぷ鳌?/p>

在下一章節(jié),我們將學(xué)習(xí)怎樣部署響應(yīng)式微服務(wù)到OpenShift,解釋怎樣做服務(wù)發(fā)現(xiàn)、熔斷器,失敗轉(zhuǎn)移能夠讓你的系統(tǒng)幾乎是”防彈的”。這些課題是特別重要的,不要低估處理微服務(wù)時(shí)需要處理的其他方面,比如安全、部署、聚合日志、測試等等。

關(guān)于這些課題,如果你想了解更多,查看下面的資源:

. 響應(yīng)式微服務(wù)構(gòu)架(https://info.lightbend.com/COLL-20XX-Reactive-Microservices-Architecture-RES-LP.html)

. Vert.X服務(wù)發(fā)現(xiàn)文檔(http://vertx.io/docs/vertx-service-discovery/java/)

. 發(fā)布它!設(shè)計(jì)和部署生產(chǎn)就緒的軟件(http://shop.oreilly.com/product/9780978739218.do)

. Netflix Hystrix(https://github.com/Netflix/Hystrix)

. Vert.X服務(wù)熔斷器文檔(http://vertx.io/docs/vertx-circuit-breaker/java/)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容