現(xiàn)在的學(xué)生太厲害了!一位同學(xué)冒著掛科的風(fēng)險(xiǎn)給大家寫得 Spring Cloud 入門總結(jié)

作者:FrancisQ

來源:juejin.im/post/5de2553e5188256e885f4fa3

馬上要考試了!!!

作為一個(gè)苦逼的在讀大學(xué)生,又要面臨半年一度的期末考試了,因?yàn)樯险n沒聽,我啥都不會(huì),什么通信原理,單片機(jī)。。。饒了我吧!!!

給你們看看我上課在干啥你就知道我為啥啥都不會(huì)了。

上課筆記。。

emmm,字比較丑??。我還記得那是一堂英語(yǔ)課,老師不讓用電子設(shè)備,我只能手寫我這篇文章的思路。。。

所以,冒著期末要掛科的風(fēng)險(xiǎn)??,我也得把這篇文章寫完,給大家分享知識(shí),自己也能重新復(fù)習(xí)和認(rèn)識(shí)一下?Spring Cloud。

我女朋友說,要是這篇文章能有 50 個(gè)贊就給我買個(gè) SSD ??????

首先我給大家看一張圖,如果大家對(duì)這張圖有些地方不太理解的話,我希望你們看完我這篇文章會(huì)恍然大悟。

# 什么是Spring cloud

構(gòu)建分布式系統(tǒng)不需要復(fù)雜和容易出錯(cuò)。Spring Cloud 為最常見的分布式系統(tǒng)模式提供了一種簡(jiǎn)單且易于接受的編程模型,幫助開發(fā)人員構(gòu)建有彈性的、可靠的、協(xié)調(diào)的應(yīng)用程序。Spring Cloud 構(gòu)建于 Spring Boot 之上,使得開發(fā)者很容易入手并快速應(yīng)用于生產(chǎn)中。

官方果然官方,介紹都這么有板有眼的。

我所理解的 Spring Cloud 就是微服務(wù)系統(tǒng)架構(gòu)的一站式解決方案,在平時(shí)我們構(gòu)建微服務(wù)的過程中需要做如 服務(wù)發(fā)現(xiàn)注冊(cè) 、配置中心 、消息總線 、負(fù)載均衡 、斷路器 、數(shù)據(jù)監(jiān)控 等操作,而 Spring Cloud 為我們提供了一套簡(jiǎn)易的編程模型,使我們能在 Spring Boot 的基礎(chǔ)上輕松地實(shí)現(xiàn)微服務(wù)項(xiàng)目的構(gòu)建。

# Spring Cloud 的版本

當(dāng)然這個(gè)只是個(gè)題外話。

Spring Cloud 的版本號(hào)并不是我們通常見的數(shù)字版本號(hào),而是一些很奇怪的單詞。這些單詞均為英國(guó)倫敦地鐵站的站名。同時(shí)根據(jù)字母表的順序來對(duì)應(yīng)版本時(shí)間順序,比如:最早 的 Release 版本 Angel,第二個(gè) Release 版本 Brixton(英國(guó)地名),然后是 Camden、 Dalston、Edgware、Finchley、Greenwich、Hoxton。

# Spring Cloud 的服務(wù)發(fā)現(xiàn)框架——Eureka

Eureka是基于REST(代表性狀態(tài)轉(zhuǎn)移)的服務(wù),主要在AWS云中用于定位服務(wù),以實(shí)現(xiàn)負(fù)載均衡和中間層服務(wù)器的故障轉(zhuǎn)移。我們稱此服務(wù)為Eureka服務(wù)器。Eureka還帶有一個(gè)基于Java的客戶端組件Eureka Client,它使與服務(wù)的交互變得更加容易。客戶端還具有一個(gè)內(nèi)置的負(fù)載平衡器,可以執(zhí)行基本的循環(huán)負(fù)載平衡。在Netflix,更復(fù)雜的負(fù)載均衡器將Eureka包裝起來,以基于流量,資源使用,錯(cuò)誤條件等多種因素提供加權(quán)負(fù)載均衡,以提供出色的彈性。

總的來說,Eureka 就是一個(gè)服務(wù)發(fā)現(xiàn)框架。何為服務(wù),何又為發(fā)現(xiàn)呢?

舉一個(gè)生活中的例子,就比如我們平時(shí)租房子找中介的事情。

在沒有中介的時(shí)候我們需要一個(gè)一個(gè)去尋找是否有房屋要出租的房東,這顯然會(huì)非常的費(fèi)力,一你找憑一個(gè)人的能力是找不到很多房源供你選擇,再者你也懶得這么找下去(找了這么久,沒有合適的只能將就)。

這里的我們就相當(dāng)于微服務(wù)中的 Consumer ,而那些房東就相當(dāng)于微服務(wù)中的 Provider 。消費(fèi)者 Consumer 需要調(diào)用提供者 Provider 提供的一些服務(wù),就像我們現(xiàn)在需要租他們的房子一樣。

但是如果只是租客和房東之間進(jìn)行尋找的話,他們的效率是很低的,房東找不到租客賺不到錢,租客找不到房東住不了房。

所以,后來房東肯定就想到了廣播自己的房源信息(比如在街邊貼貼小廣告),這樣對(duì)于房東來說已經(jīng)完成他的任務(wù)(將房源公布出去),但是有兩個(gè)問題就出現(xiàn)了。

第一、其他不是租客的都能收到這種租房消息,這在現(xiàn)實(shí)世界沒什么,但是在計(jì)算機(jī)的世界中就會(huì)出現(xiàn)資源消耗的問題了。

第二、租客這樣還是很難找到你,試想一下我需要租房,我還需要東一個(gè)西一個(gè)地去找街邊小廣告,麻不麻煩?

那怎么辦呢?

我們當(dāng)然不會(huì)那么傻乎乎的,第一時(shí)間就是去找 中介 呀,它為我們提供了統(tǒng)一房源的地方,我們消費(fèi)者只需要跑到它那里去找就行了。

而對(duì)于房東來說,他們也只需要把房源在中介那里發(fā)布就行了。

那么現(xiàn)在,我們的模式就是這樣的了。

但是,這個(gè)時(shí)候還會(huì)出現(xiàn)一些問題。

1.房東注冊(cè)之后如果不想賣房子了怎么辦?我們是不是需要讓房東定期續(xù)約?如果房東不進(jìn)行續(xù)約是不是要將他們從中介那里的注冊(cè)列表中移除。

2.租客是不是也要進(jìn)行注冊(cè)呢?不然合同乙方怎么來呢?

3.中介可不可以做連鎖店呢?如果這一個(gè)店因?yàn)槟承┎豢煽沽σ蛩囟鵁o法使用,那么我們是否可以換一個(gè)連鎖店呢?

針對(duì)上面的問題我們來重新構(gòu)建一下上面的模式圖

好了,舉完這個(gè)??我們就可以來看關(guān)于 Eureka 的一些基礎(chǔ)概念了,你會(huì)發(fā)現(xiàn)這東西理解起來怎么這么簡(jiǎn)單。??????

服務(wù)發(fā)現(xiàn):其實(shí)就是一個(gè)“中介”,整個(gè)過程中有三個(gè)角色:服務(wù)提供者(出租房子的)、服務(wù)消費(fèi)者(租客)、服務(wù)中介(房屋中介)。

服務(wù)提供者:就是提供一些自己能夠執(zhí)行的一些服務(wù)給外界。

服務(wù)消費(fèi)者:就是需要使用一些服務(wù)的“用戶”。

服務(wù)中介:其實(shí)就是服務(wù)提供者和服務(wù)消費(fèi)者之間的“橋梁”,服務(wù)提供者可以把自己注冊(cè)到服務(wù)中介那里,而服務(wù)消費(fèi)者如需要消費(fèi)一些服務(wù)(使用一些功能)就可以在服務(wù)中介中尋找注冊(cè)在服務(wù)中介的服務(wù)提供者。

服務(wù)注冊(cè) Register:

官方解釋:當(dāng) Eureka 客戶端向 Eureka Server 注冊(cè)時(shí),它提供自身的元數(shù)據(jù),比如IP地址、端口,運(yùn)行狀況指示符URL,主頁(yè)等。

結(jié)合中介理解:房東 (提供者 Eureka Client Provider)在中介 (服務(wù)器 Eureka Server) 那里登記房屋的信息,比如面積,價(jià)格,地段等等(元數(shù)據(jù) metaData)。

服務(wù)續(xù)約 Renew:

官方解釋:Eureka 客戶會(huì)每隔30秒(默認(rèn)情況下)發(fā)送一次心跳來續(xù)約。通過續(xù)約來告知 Eureka Server 該 Eureka 客戶仍然存在,沒有出現(xiàn)問題。正常情況下,如果 Eureka Server 在90秒沒有收到 Eureka 客戶的續(xù)約,它會(huì)將實(shí)例從其注冊(cè)表中刪除。

結(jié)合中介理解:房東 (提供者 Eureka Client Provider) 定期告訴中介 (服務(wù)器 Eureka Server) 我的房子還租(續(xù)約) ,中介 (服務(wù)器Eureka Server) 收到之后繼續(xù)保留房屋的信息。

獲取注冊(cè)列表信息 Fetch Registries:

官方解釋:Eureka 客戶端從服務(wù)器獲取注冊(cè)表信息,并將其緩存在本地。客戶端會(huì)使用該信息查找其他服務(wù),從而進(jìn)行遠(yuǎn)程調(diào)用。該注冊(cè)列表信息定期(每30秒鐘)更新一次。每次返回注冊(cè)列表信息可能與 Eureka 客戶端的緩存信息不同, Eureka 客戶端自動(dòng)處理。如果由于某種原因?qū)е伦?cè)列表信息不能及時(shí)匹配,Eureka 客戶端則會(huì)重新獲取整個(gè)注冊(cè)表信息。Eureka 服務(wù)器緩存注冊(cè)列表信息,整個(gè)注冊(cè)表以及每個(gè)應(yīng)用程序的信息進(jìn)行了壓縮,壓縮內(nèi)容和沒有壓縮的內(nèi)容完全相同。Eureka 客戶端和 Eureka 服務(wù)器可以使用JSON / XML格式進(jìn)行通訊。在默認(rèn)的情況下 Eureka 客戶端使用壓縮 JSON 格式來獲取注冊(cè)列表的信息。

結(jié)合中介理解:租客(消費(fèi)者 Eureka Client Consumer) 去中介 (服務(wù)器 Eureka Server) 那里獲取所有的房屋信息列表 (客戶端列表 Eureka Client List) ,而且租客為了獲取最新的信息會(huì)定期向中介 (服務(wù)器 Eureka Server) 那里獲取并更新本地列表。

服務(wù)下線 Cancel:

官方解釋:Eureka客戶端在程序關(guān)閉時(shí)向Eureka服務(wù)器發(fā)送取消請(qǐng)求。發(fā)送請(qǐng)求后,該客戶端實(shí)例信息將從服務(wù)器的實(shí)例注冊(cè)表中刪除。該下線請(qǐng)求不會(huì)自動(dòng)完成,它需要調(diào)用以下內(nèi)容:DiscoveryManager.getInstance().shutdownComponent();

結(jié)合中介理解:房東 (提供者 Eureka Client Provider) 告訴中介 ?(服務(wù)器 Eureka Server) 我的房子不租了,中介之后就將注冊(cè)的房屋信息從列表中剔除。

服務(wù)剔除 Eviction:

官方解釋:在默認(rèn)的情況下,當(dāng)Eureka客戶端連續(xù)90秒(3個(gè)續(xù)約周期)沒有向Eureka服務(wù)器發(fā)送服務(wù)續(xù)約,即心跳,Eureka服務(wù)器會(huì)將該服務(wù)實(shí)例從服務(wù)注冊(cè)列表刪除,即服務(wù)剔除。

結(jié)合中介理解:房東(提供者 Eureka Client Provider) 會(huì)定期聯(lián)系 中介 ?(服務(wù)器 Eureka Server) 告訴他我的房子還租(續(xù)約),如果中介 ?(服務(wù)器 Eureka Server) 長(zhǎng)時(shí)間沒收到提供者的信息,那么中介會(huì)將他的房屋信息給下架(服務(wù)剔除)。

下面就是 Netflix 官方給出的 Eureka 架構(gòu)圖,你會(huì)發(fā)現(xiàn)和我們前面畫的中介圖別無二致。

當(dāng)然,可以充當(dāng)服務(wù)發(fā)現(xiàn)的組件有很多:Zookeeper ,Consul , Eureka 等。

更多關(guān)于 Eureka 的知識(shí)(自我保護(hù),初始注冊(cè)策略等等)可以自己去官網(wǎng)查看

# 負(fù)載均衡之 Ribbon

什么是 RestTemplate?

不是講 Ribbon 么?怎么扯到了 RestTemplate 了?你先別急,聽我慢慢道來。

我不聽我不聽我不聽??????。

我就說一句!RestTemplate是Spring提供的一個(gè)訪問Http服務(wù)的客戶端類,怎么說呢?就是微服務(wù)之間的調(diào)用是使用的 RestTemplate 。比如這個(gè)時(shí)候我們 消費(fèi)者B 需要調(diào)用 提供者A 所提供的服務(wù)我們就需要這么寫。如我下面的偽代碼。

@Autowiredprivate RestTemplate restTemplate;// 這里是提供者A的ip地址,但是如果使用了 Eureka 那么就應(yīng)該是提供者A的名稱private static final String SERVICE_PROVIDER_A = "http://localhost:8081";?@PostMapping("/judge")public boolean judge(@RequestBody Request request) {? ? String url = SERVICE_PROVIDER_A + "/service1";? ? return restTemplate.postForObject(url, request, Boolean.class);}

如果你對(duì)源碼感興趣的話,你會(huì)發(fā)現(xiàn)上面我們所講的 Eureka 框架中的 注冊(cè)、續(xù)約 等,底層都是使用的 RestTemplate 。

為什么需要 Ribbon?

Ribbon ?是 Netflix 公司的一個(gè)開源的負(fù)載均衡 項(xiàng)目,是一個(gè)客戶端/進(jìn)程內(nèi)負(fù)載均衡器,運(yùn)行在消費(fèi)者端。

我們?cè)倥e個(gè)??,比如我們?cè)O(shè)計(jì)了一個(gè)秒殺系統(tǒng),但是為了整個(gè)系統(tǒng)的 高可用 ,我們需要將這個(gè)系統(tǒng)做一個(gè)集群,而這個(gè)時(shí)候我們消費(fèi)者就可以擁有多個(gè)秒殺系統(tǒng)的調(diào)用途徑了,如下圖。

如果這個(gè)時(shí)候我們沒有進(jìn)行一些 均衡操作 ,如果我們對(duì) 秒殺系統(tǒng)1 進(jìn)行大量的調(diào)用,而另外兩個(gè)基本不請(qǐng)求,就會(huì)導(dǎo)致 秒殺系統(tǒng)1 崩潰,而另外兩個(gè)就變成了傀儡,那么我們?yōu)槭裁催€要做集群,我們高可用體現(xiàn)的意義又在哪呢?

所以 Ribbon 出現(xiàn)了,注意我們上面加粗的幾個(gè)字——運(yùn)行在消費(fèi)者端。指的是,Ribbon 是運(yùn)行在消費(fèi)者端的負(fù)載均衡器,如下圖。

其工作原理就是 Consumer 端獲取到了所有的服務(wù)列表之后,在其內(nèi)部使用負(fù)載均衡算法,進(jìn)行對(duì)多個(gè)系統(tǒng)的調(diào)用。

Nginx 和 Ribbon 的對(duì)比

提到 負(fù)載均衡 就不得不提到大名鼎鼎的 Nignx 了,而和 Ribbon 不同的是,它是一種集中式的負(fù)載均衡器。

何為集中式呢?簡(jiǎn)單理解就是 將所有請(qǐng)求都集中起來,然后再進(jìn)行負(fù)載均衡。如下圖。

我們可以看到 Nginx 是接收了所有的請(qǐng)求進(jìn)行負(fù)載均衡的,而對(duì)于 Ribbon 來說它是在消費(fèi)者端進(jìn)行的負(fù)載均衡。如下圖。

請(qǐng)注意 Request 的位置,在 Nginx 中請(qǐng)求是先進(jìn)入負(fù)載均衡器,而在 Ribbon 中是先在客戶端進(jìn)行負(fù)載均衡才進(jìn)行請(qǐng)求的。

Ribbon 的幾種負(fù)載均衡算法

負(fù)載均衡,不管 Nginx 還是 Ribbon 都需要其算法的支持,如果我沒記錯(cuò)的話 Nginx 使用的是 輪詢和加權(quán)輪詢算法。而在 Ribbon 中有更多的負(fù)載均衡調(diào)度算法,其默認(rèn)是使用的 RoundRobinRule 輪詢策略。

RoundRobinRule:輪詢策略。Ribbon 默認(rèn)采用的策略。若經(jīng)過一輪輪詢沒有找到可用的 provider,其最多輪詢 10 輪。若最終還沒有找到,則返回 null。

RandomRule: 隨機(jī)策略,從所有可用的 provider 中隨機(jī)選擇一個(gè)。

RetryRule: 重試策略。先按照 RoundRobinRule 策略獲取 provider,若獲取失敗,則在指定的時(shí)限內(nèi)重試。默認(rèn)的時(shí)限為 500 毫秒。

?????? 還有很多,這里不一一舉??了,你最需要知道的是默認(rèn)輪詢算法,并且可以更換默認(rèn)的負(fù)載均衡算法,只需要在配置文件中做出修改就行。

providerName:? ribbon:????NFLoadBalancerRuleClassName:?com.netflix.loadbalancer.RandomRule

當(dāng)然,在 Ribbon 中你還可以自定義負(fù)載均衡算法,你只需要實(shí)現(xiàn) IRule 接口,然后修改配置文件或者自定義 Java Config 類。

# 什么是 Open Feign

有了 Eureka,RestTemplate,Ribbon 我們就可以??愉快地進(jìn)行服務(wù)間的調(diào)用了,但是使用 RestTemplate 還是不方便,我們每次都要進(jìn)行這樣的調(diào)用。

這樣每次都調(diào)用 RestRemplate 的 API 是否太麻煩,我能不能像調(diào)用原來代碼一樣進(jìn)行各個(gè)服務(wù)間的調(diào)用呢?

??????聰明的小朋友肯定想到了,那就用 映射 呀,就像域名和IP地址的映射。我們可以將被調(diào)用的服務(wù)代碼映射到消費(fèi)者端,這樣我們就可以 “無縫開發(fā)”啦。

OpenFeign 也是運(yùn)行在消費(fèi)者端的,使用 Ribbon 進(jìn)行負(fù)載均衡,所以 OpenFeign 直接內(nèi)置了 Ribbon。

在導(dǎo)入了 Open Feign 之后我們就可以進(jìn)行愉快編寫 ?Consumer 端代碼了。

然后我們?cè)?Controller 就可以像原來調(diào)用 Service 層代碼一樣調(diào)用它了。

# 必不可少的 Hystrix

什么是 Hystrix之熔斷和降級(jí)

在分布式環(huán)境中,不可避免地會(huì)有許多服務(wù)依賴項(xiàng)中的某些失敗。Hystrix是一個(gè)庫(kù),可通過添加等待時(shí)間容限和容錯(cuò)邏輯來幫助您控制這些分布式服務(wù)之間的交互。Hystrix通過隔離服務(wù)之間的訪問點(diǎn),停止服務(wù)之間的級(jí)聯(lián)故障并提供后備選項(xiàng)來實(shí)現(xiàn)此目的,所有這些都可以提高系統(tǒng)的整體彈性。

總體來說 Hystrix 就是一個(gè)能進(jìn)行 熔斷 和 降級(jí) 的庫(kù),通過使用它能提高整個(gè)系統(tǒng)的彈性。

那么什么是 熔斷和降級(jí) 呢?再舉個(gè)??,此時(shí)我們整個(gè)微服務(wù)系統(tǒng)是這樣的。服務(wù)A調(diào)用了服務(wù)B,服務(wù)B再調(diào)用了服務(wù)C,但是因?yàn)槟承┰颍?wù)C頂不住了,這個(gè)時(shí)候大量請(qǐng)求會(huì)在服務(wù)C阻塞。

服務(wù)C阻塞了還好,畢竟只是一個(gè)系統(tǒng)崩潰了。但是請(qǐng)注意這個(gè)時(shí)候因?yàn)榉?wù)C不能返回響應(yīng),那么服務(wù)B調(diào)用服務(wù)C的的請(qǐng)求就會(huì)阻塞,同理服務(wù)B阻塞了,那么服務(wù)A也會(huì)阻塞崩潰。

請(qǐng)注意,為什么阻塞會(huì)崩潰。因?yàn)檫@些請(qǐng)求會(huì)消耗占用系統(tǒng)的線程、IO 等資源,消耗完你這個(gè)系統(tǒng)服務(wù)器不就崩了么。

這就叫 服務(wù)雪崩。媽耶,上面兩個(gè) 熔斷 和 降級(jí) 你都沒給我解釋清楚,你現(xiàn)在又給我扯什么 服務(wù)雪崩 ???????

別急,聽我慢慢道來。

不聽我也得講下去!

所謂 熔斷 就是服務(wù)雪崩的一種有效解決方案。當(dāng)指定時(shí)間窗內(nèi)的請(qǐng)求失敗率達(dá)到設(shè)定閾值時(shí),系統(tǒng)將通過 斷路器 直接將此請(qǐng)求鏈路斷開。

也就是我們上面服務(wù)B調(diào)用服務(wù)C在指定時(shí)間窗內(nèi),調(diào)用的失敗率到達(dá)了一定的值,那么 Hystrix 則會(huì)自動(dòng)將 服務(wù)B與C 之間的請(qǐng)求都斷了,以免導(dǎo)致服務(wù)雪崩現(xiàn)象。

其實(shí)這里所講的 熔斷 就是指的 Hystrix 中的 斷路器模式 ,你可以使用簡(jiǎn)單的 @HystrixCommand 注解來標(biāo)注某個(gè)方法,這樣 Hystrix 就會(huì)使用 斷路器 來“包裝”這個(gè)方法,每當(dāng)調(diào)用時(shí)間超過指定時(shí)間時(shí)(默認(rèn)為1000ms),斷路器將會(huì)中斷對(duì)這個(gè)方法的調(diào)用。

當(dāng)然你可以對(duì)這個(gè)注解的很多屬性進(jìn)行設(shè)置,比如設(shè)置超時(shí)時(shí)間,像這樣。

@HystrixCommand(? ? commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1200")})public List<Xxx> getXxxx() {? ? // ...省略代碼邏輯}

但是,我查閱了一些博客,發(fā)現(xiàn)他們都將 熔斷 和 降級(jí) 的概念混淆了,以我的理解,降級(jí)是為了更好的用戶體驗(yàn),當(dāng)一個(gè)方法調(diào)用異常時(shí),通過執(zhí)行另一種代碼邏輯來給用戶友好的回復(fù)。

這也就對(duì)應(yīng)著 Hystrix 的 后備處理 模式。

你可以通過設(shè)置 fallbackMethod 來給一個(gè)方法設(shè)置備用的代碼邏輯。比如這個(gè)時(shí)候有一個(gè)熱點(diǎn)新聞出現(xiàn)了,我們會(huì)推薦給用戶查看詳情,然后用戶會(huì)通過id去查詢新聞的詳情,但是因?yàn)檫@條新聞太火了(比如最近什么*易對(duì)吧),大量用戶同時(shí)訪問可能會(huì)導(dǎo)致系統(tǒng)崩潰,那么我們就進(jìn)行 服務(wù)降級(jí) ,一些請(qǐng)求會(huì)做一些降級(jí)處理比如當(dāng)前人數(shù)太多請(qǐng)稍后查看等等。

什么是Hystrix之其他

我在閱讀 《Spring微服務(wù)實(shí)戰(zhàn)》這本書的時(shí)候還接觸到了一個(gè)艙壁模式的概念。

在不使用艙壁模式的情況下,服務(wù)A調(diào)用服務(wù)B,這種調(diào)用默認(rèn)的是使用同一批線程來執(zhí)行的,而在一個(gè)服務(wù)出現(xiàn)性能問題的時(shí)候,就會(huì)出現(xiàn)所有線程被刷爆并等待處理工作,同時(shí)阻塞新請(qǐng)求,最終導(dǎo)致程序崩潰。

關(guān)注公眾號(hào)「程序員的成長(zhǎng)之路」后回復(fù)「2048」關(guān)鍵字,免費(fèi)獲取5T技術(shù)學(xué)習(xí)資源!包括但不限于:C/C++,Linux,Python,Java,PHP,人工智能,單片機(jī),樹莓派,等等。

而艙壁模式會(huì)將遠(yuǎn)程資源調(diào)用隔離在他們自己的線程池中,以便可以控制單個(gè)表現(xiàn)不佳的服務(wù),而不會(huì)使該程序崩潰。

具體其原理我推薦大家自己去了解一下,本篇文章中對(duì)艙壁模式不做過多解釋。當(dāng)然還有 Hystrix 儀表盤,它是用來實(shí)時(shí)監(jiān)控 Hystrix 的各項(xiàng)指標(biāo)信息的,這里我將這個(gè)問題也拋出去,希望有不了解的可以自己去搜索一下。

# 微服務(wù)網(wǎng)關(guān)——Zuul

ZUUL 是從設(shè)備和 web 站點(diǎn)到 Netflix 流應(yīng)用后端的所有請(qǐng)求的前門。作為邊界服務(wù)應(yīng)用,ZUUL 是為了實(shí)現(xiàn)動(dòng)態(tài)路由、監(jiān)視、彈性和安全性而構(gòu)建的。它還具有根據(jù)情況將請(qǐng)求路由到多個(gè) Amazon Auto Scaling Groups(亞馬遜自動(dòng)縮放組,亞馬遜的一種云計(jì)算方式) 的能力

在上面我們學(xué)習(xí)了 Eureka 之后我們知道了 服務(wù)提供者 是 消費(fèi)者 通過 Eureka Server 進(jìn)行訪問的,即 Eureka Server 是 服務(wù)提供者 的統(tǒng)一入口。那么整個(gè)應(yīng)用中存在那么多 消費(fèi)者 需要用戶進(jìn)行調(diào)用,這個(gè)時(shí)候用戶該怎樣訪問這些 消費(fèi)者工程 呢?當(dāng)然可以像之前那樣直接訪問這些工程。但這種方式?jīng)]有統(tǒng)一的消費(fèi)者工程調(diào)用入口,不便于訪問與管理,而 Zuul 就是這樣的一個(gè)對(duì)于 消費(fèi)者 的統(tǒng)一入口。

如果學(xué)過前端的肯定都知道 Router 吧,比如 Flutter 中的路由,Vue,React中的路由,用了 Zuul 你會(huì)發(fā)現(xiàn)在路由功能方面和前端配置路由基本是一個(gè)理。?? 我偶爾擼擼 Flutter。

大家對(duì)網(wǎng)關(guān)應(yīng)該很熟吧,簡(jiǎn)單來講網(wǎng)關(guān)是系統(tǒng)唯一對(duì)外的入口,介于客戶端與服務(wù)器端之間,用于對(duì)請(qǐng)求進(jìn)行鑒權(quán)、限流、 路由、監(jiān)控等功能。

沒錯(cuò),網(wǎng)關(guān)有的功能,Zuul 基本都有。而 Zuul 中最關(guān)鍵的就是 路由和過濾器 了,在官方文檔中 Zuul 的標(biāo)題就是

Router and Filter : Zuul

Zuul 的路由功能

簡(jiǎn)單配置

本來想給你們復(fù)制一些代碼,但是想了想,因?yàn)楦鱾€(gè)代碼配置比較零散,看起來也比較零散,我決定還是給你們畫個(gè)圖來解釋吧。

請(qǐng)不要因?yàn)槲疫@么好就給我點(diǎn)贊 ?? 。瘋狂暗示。

比如這個(gè)時(shí)候我們已經(jīng)向 Eureka Server 注冊(cè)了兩個(gè) Consumer 、三個(gè) Provicer ,這個(gè)時(shí)候我們?cè)偌觽€(gè) Zuul 網(wǎng)關(guān)應(yīng)該變成這樣子了。

emmm,信息量有點(diǎn)大,我來解釋一下。關(guān)于前面的知識(shí)我就不解釋了?? 。

首先,Zuul 需要向 Eureka 進(jìn)行注冊(cè),注冊(cè)有啥好處呢?

你傻呀,Consumer 都向 Eureka Server 進(jìn)行注冊(cè)了,我網(wǎng)關(guān)是不是只要注冊(cè)就能拿到所有 Consumer 的信息了?

拿到信息有什么好處呢?

我拿到信息我是不是可以獲取所有的 Consumer 的元數(shù)據(jù)(名稱,ip,端口)?

拿到這些元數(shù)據(jù)有什么好處呢?拿到了我們是不是直接可以做路由映射?比如原來用戶調(diào)用 Consumer1 的接口 localhost:8001/studentInfo/update 這個(gè)請(qǐng)求,我們是不是可以這樣進(jìn)行調(diào)用了呢?localhost:9000/consumer1/studentInfo/update 呢?你這樣是不是恍然大悟了?

這里的url為了讓更多人看懂所以沒有使用 restful 風(fēng)格。

上面的你理解了,那么就能理解關(guān)于 Zuul 最基本的配置了,看下面。

server:? port: 9000eureka:? client:? ? service-url:? ? ? # 這里只要注冊(cè) Eureka 就行了? ? ? defaultZone: http://localhost:9997/eureka

然后在啟動(dòng)類上加入 @EnableZuulProxy 注解就行了。沒錯(cuò),就是那么簡(jiǎn)單??。

統(tǒng)一前綴

這個(gè)很簡(jiǎn)單,就是我們可以在前面加一個(gè)統(tǒng)一的前綴,比如我們剛剛調(diào)用的是 localhost:9000/consumer1/studentInfo/update,這個(gè)時(shí)候我們?cè)?yaml 配置文件中添加如下。

zuul:? prefix: /zuul

這樣我們就需要通過 localhost:9000/zuul/consumer1/studentInfo/update 來進(jìn)行訪問了。

路由策略配置

你會(huì)發(fā)現(xiàn)前面的訪問方式(直接使用服務(wù)名),需要將微服務(wù)名稱暴露給用戶,會(huì)存在安全性問題。所以,可以自定義路徑來替代微服務(wù)名稱,即自定義路由策略。

zuul:? routes:? ? consumer1: /FrancisQ1/**? ? consumer2: /FrancisQ2/**

這個(gè)時(shí)候你就可以使用 localhost:9000/zuul/FrancisQ1/studentInfo/update 進(jìn)行訪問了。

服務(wù)名屏蔽

這個(gè)時(shí)候你別以為你好了,你可以試試,在你配置完路由策略之后使用微服務(wù)名稱還是可以訪問的,這個(gè)時(shí)候你需要將服務(wù)名屏蔽。

zuul:? ignore-services: "*"

路徑屏蔽

Zuul 還可以指定屏蔽掉的路徑 URI,即只要用戶請(qǐng)求中包含指定的 URI 路徑,那么該請(qǐng)求將無法訪問到指定的服務(wù)。通過該方式可以限制用戶的權(quán)限。

zuul:? ignore-patterns: **/auto/**

這樣關(guān)于 auto 的請(qǐng)求我們就可以過濾掉了。

** 代表匹配多級(jí)任意路徑

*代表匹配一級(jí)任意路徑

敏感請(qǐng)求頭屏蔽

默認(rèn)情況下,像 Cookie、Set-Cookie 等敏感請(qǐng)求頭信息會(huì)被 zuul 屏蔽掉,我們可以將這些默認(rèn)屏蔽去掉,當(dāng)然,也可以添加要屏蔽的請(qǐng)求頭。

Zuul 的過濾功能

如果說,路由功能是 Zuul 的基操的話,那么過濾器就是 Zuul的利器了。畢竟所有請(qǐng)求都經(jīng)過網(wǎng)關(guān)(Zuul),那么我們可以進(jìn)行各種過濾,這樣我們就能實(shí)現(xiàn) 限流,灰度發(fā)布,權(quán)限控制 等等。

簡(jiǎn)單實(shí)現(xiàn)一個(gè)請(qǐng)求時(shí)間日志打印

要實(shí)現(xiàn)自己定義的 Filter 我們只需要繼承 ZuulFilter 然后將這個(gè)過濾器類以 @Component 注解加入 Spring 容器中就行了。

在給你們看代碼之前我先給你們解釋一下關(guān)于過濾器的一些注意點(diǎn)。

過濾器類型:Pre、Routing、Post。前置Pre就是在請(qǐng)求之前進(jìn)行過濾,Routing路由過濾器就是我們上面所講的路由策略,而Post后置過濾器就是在 Response 之前進(jìn)行過濾的過濾器。你可以觀察上圖結(jié)合著理解,并且下面我會(huì)給出相應(yīng)的注釋。

// 加入Spring容器@Componentpublic class PreRequestFilter extends ZuulFilter {? ? // 返回過濾器類型 這里是前置過濾器? ? @Override? ? public String filterType() {? ? ? ? return FilterConstants.PRE_TYPE;? ? }? ? // 指定過濾順序 越小越先執(zhí)行,這里第一個(gè)執(zhí)行? ? // 當(dāng)然不是只真正第一個(gè) 在Zuul內(nèi)置中有其他過濾器會(huì)先執(zhí)行? ? // 那是寫死的 比如 SERVLET_DETECTION_FILTER_ORDER = -3? ? @Override? ? public int filterOrder() {? ? ? ? return 0;? ? }? ? // 什么時(shí)候該進(jìn)行過濾? ? // 這里我們可以進(jìn)行一些判斷,這樣我們就可以過濾掉一些不符合規(guī)定的請(qǐng)求等等? ? @Override? ? public boolean shouldFilter() {? ? ? ? return true;? ? }? ? // 如果過濾器允許通過則怎么進(jìn)行處理? ? @Override? ? public Object run() throws ZuulException {? ? ? ? // 這里我設(shè)置了全局的RequestContext并記錄了請(qǐng)求開始時(shí)間? ? ? ? RequestContext ctx = RequestContext.getCurrentContext();? ? ? ? ctx.set("startTime", System.currentTimeMillis());? ? ? ? return null;? ? }}

// lombok的日志@Slf4j// 加入 Spring 容器@Componentpublic class AccessLogFilter extends ZuulFilter {? ? // 指定該過濾器的過濾類型? ? // 此時(shí)是后置過濾器? ? @Override? ? public String filterType() {? ? ? ? return FilterConstants.POST_TYPE;? ? }? ? // SEND_RESPONSE_FILTER_ORDER 是最后一個(gè)過濾器? ? // 我們此過濾器在它之前執(zhí)行? ? @Override? ? public int filterOrder() {? ? ? ? return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;? ? }? ? @Override? ? public boolean shouldFilter() {? ? ? ? return true;? ? }? ? // 過濾時(shí)執(zhí)行的策略? ? @Override? ? public Object run() throws ZuulException {? ? ? ? RequestContext context = RequestContext.getCurrentContext();? ? ? ? HttpServletRequest request = context.getRequest();? ? ? ? // 從RequestContext獲取原先的開始時(shí)間 并通過它計(jì)算整個(gè)時(shí)間間隔? ? ? ? Long startTime = (Long) context.get("startTime");? ? ? ? // 這里我可以獲取HttpServletRequest來獲取URI并且打印出來? ? ? ? String uri = request.getRequestURI();? ? ? ? long duration = System.currentTimeMillis() - startTime;? ? ? ? log.info("uri: " + uri + ", duration: " + duration / 100 + "ms");? ? ? ? return null;? ? }}

上面就簡(jiǎn)單實(shí)現(xiàn)了請(qǐng)求時(shí)間日志打印功能,你有沒有感受到 Zuul 過濾功能的強(qiáng)大了呢?

沒有?好的、那我們?cè)賮怼?/p>

令牌桶限流

當(dāng)然不僅僅是令牌桶限流方式,Zuul 只要是限流的活它都能干,這里我只是簡(jiǎn)單舉個(gè)??。

我先來解釋一下什么是 令牌桶限流 吧。

首先我們會(huì)有個(gè)桶,如果里面沒有滿那么就會(huì)以一定 固定的速率 會(huì)往里面放令牌,一個(gè)請(qǐng)求過來首先要從桶中獲取令牌,如果沒有獲取到,那么這個(gè)請(qǐng)求就拒絕,如果獲取到那么就放行。

下面我們就通過 Zuul 的前置過濾器來實(shí)現(xiàn)一下令牌桶限流。

@Component@Slf4jpublic class RouteFilter extends ZuulFilter {? ? // 定義一個(gè)令牌桶,每秒產(chǎn)生2個(gè)令牌,即每秒最多處理2個(gè)請(qǐng)求? ? private static final RateLimiter RATE_LIMITER = RateLimiter.create(2);? ? @Override? ? public String filterType() {? ? ? ? return FilterConstants.PRE_TYPE;? ? }?? ? @Override? ? public int filterOrder() {? ? ? ? return -5;? ? }?? ? @Override? ? public Object run() throws ZuulException {? ? ? ? log.info("放行");? ? ? ? return null;? ? }?? ? @Override? ? public boolean shouldFilter() {? ? ? ? RequestContext context = RequestContext.getCurrentContext();? ? ? ? if(!RATE_LIMITER.tryAcquire()) {? ? ? ? ? ? log.warn("訪問量超載");? ? ? ? ? ? // 指定當(dāng)前請(qǐng)求未通過過濾? ? ? ? ? ? context.setSendZuulResponse(false);? ? ? ? ? ? // 向客戶端返回響應(yīng)碼429,請(qǐng)求數(shù)量過多? ? ? ? ? ? context.setResponseStatusCode(429);? ? ? ? ? ? return false;? ? ? ? }? ? ? ? return true;? ? }}

這樣我們就能將請(qǐng)求數(shù)量控制在一秒兩個(gè),有沒有覺得很酷?

關(guān)于 Zuul ?的其他

Zuul 的過濾器的功能肯定不止上面我所實(shí)現(xiàn)的兩種,它還可以實(shí)現(xiàn) 權(quán)限校驗(yàn),包括我上面提到的 灰度發(fā)布 等等。

當(dāng)然,Zuul 作為網(wǎng)關(guān)肯定也存在 單點(diǎn)問題 ,如果我們要保證 Zuul 的高可用,我們就需要進(jìn)行 Zuul 的集群配置,這個(gè)時(shí)候可以借助額外的一些負(fù)載均衡器比如 Nginx 。

# Spring Cloud配置管理——Config

為什么要使用進(jìn)行配置管理?

當(dāng)我們的微服務(wù)系統(tǒng)開始慢慢地龐大起來,那么多 Consumer 、Provider 、Eureka Server 、Zuul 系統(tǒng)都會(huì)持有自己的配置,這個(gè)時(shí)候我們?cè)陧?xiàng)目運(yùn)行的時(shí)候可能需要更改某些應(yīng)用的配置,如果我們不進(jìn)行配置的統(tǒng)一管理,我們只能去每個(gè)應(yīng)用下一個(gè)一個(gè)尋找配置文件然后修改配置文件再重啟應(yīng)用。

首先對(duì)于分布式系統(tǒng)而言我們就不應(yīng)該去每個(gè)應(yīng)用下去分別修改配置文件,再者對(duì)于重啟應(yīng)用來說,服務(wù)無法訪問所以直接拋棄了可用性,這是我們更不愿見到的。

那么有沒有一種方法既能對(duì)配置文件統(tǒng)一地進(jìn)行管理,又能在項(xiàng)目運(yùn)行時(shí)動(dòng)態(tài)修改配置文件呢?

那就是我今天所要介紹的 Spring Cloud Config 。

能進(jìn)行配置管理的框架不止 Spring Cloud Config 一種,大家可以根據(jù)需求自己選擇(disconf,阿波羅等等)。而且對(duì)于 Config 來說有些地方實(shí)現(xiàn)的不是那么盡人意。

Config 是什么

Spring Cloud Config 為分布式系統(tǒng)中的外部化配置提供服務(wù)器和客戶端支持。使用 Config 服務(wù)器,可以在中心位置管理所有環(huán)境中應(yīng)用程序的外部屬性。

簡(jiǎn)單來說,Spring Cloud Config 就是能將各個(gè) 應(yīng)用/系統(tǒng)/模塊 的配置文件存放到 統(tǒng)一的地方然后進(jìn)行管理(Git 或者 SVN)。

你想一下,我們的應(yīng)用是不是只有啟動(dòng)的時(shí)候才會(huì)進(jìn)行配置文件的加載,那么我們的 Spring Cloud Config 就暴露出一個(gè)接口給啟動(dòng)應(yīng)用來獲取它所想要的配置文件,應(yīng)用獲取到配置文件然后再進(jìn)行它的初始化工作。就如下圖。

當(dāng)然這里你肯定還會(huì)有一個(gè)疑問,如果我在應(yīng)用運(yùn)行時(shí)去更改遠(yuǎn)程配置倉(cāng)庫(kù)(Git)中的對(duì)應(yīng)配置文件,那么依賴于這個(gè)配置文件的已啟動(dòng)的應(yīng)用會(huì)不會(huì)進(jìn)行其相應(yīng)配置的更改呢?

答案是不會(huì)的。

什么?那怎么進(jìn)行動(dòng)態(tài)修改配置文件呢?這不是出現(xiàn)了 配置漂移 嗎?你個(gè)渣男??,你又騙我!

別急嘛,你可以使用 Webhooks ,這是 ?github 提供的功能,它能確保遠(yuǎn)程庫(kù)的配置文件更新后客戶端中的配置信息也得到更新。

噢噢,這還差不多。我去查查怎么用。

慢著,聽我說完,Webhooks 雖然能解決,但是你了解一下會(huì)發(fā)現(xiàn)它根本不適合用于生產(chǎn)環(huán)境,所以基本不會(huì)使用它的。

而一般我們會(huì)使用 Bus 消息總線 + Spring Cloud Config 進(jìn)行配置的動(dòng)態(tài)刷新。

# 引出 Spring Cloud Bus

用于將服務(wù)和服務(wù)實(shí)例與分布式消息系統(tǒng)鏈接在一起的事件總線。在集群中傳播狀態(tài)更改很有用(例如配置更改事件)。

你可以簡(jiǎn)單理解為 Spring Cloud Bus 的作用就是管理和廣播分布式系統(tǒng)中的消息,也就是消息引擎系統(tǒng)中的廣播模式。當(dāng)然作為 消息總線 的 Spring Cloud Bus 可以做很多事而不僅僅是客戶端的配置刷新功能。

而擁有了 Spring Cloud Bus 之后,我們只需要?jiǎng)?chuàng)建一個(gè)簡(jiǎn)單的請(qǐng)求,并且加上 @ResfreshScope 注解就能進(jìn)行配置的動(dòng)態(tài)修改了,下面我畫了張圖供你理解。

# 總結(jié)

這篇文章中我?guī)Т蠹页醪搅私饬?Spring Cloud 的各個(gè)組件,他們有

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

Ribbon 進(jìn)程內(nèi)負(fù)載均衡器

Open Feign 服務(wù)調(diào)用映射

Hystrix 服務(wù)降級(jí)熔斷器

Zuul 微服務(wù)網(wǎng)關(guān)

Config 微服務(wù)統(tǒng)一配置中心

Bus 消息總線

如果你能這個(gè)時(shí)候能看懂下面那張圖,也就說明了你已經(jīng)對(duì) Spring Cloud 微服務(wù)有了一定的架構(gòu)認(rèn)識(shí)。

END

本文發(fā)于 微星公眾號(hào)「程序員的成長(zhǎng)之路」,回復(fù)「1024」你懂得,給個(gè)贊唄。

回復(fù) [ 256 ] Java 程序員成長(zhǎng)規(guī)劃

回復(fù) [ 777 ] 接私活的七大平臺(tái)利器

回復(fù) [ 2048 ] 免費(fèi)領(lǐng)取C/C++,Linux,Python,Java,PHP,人工智能,單片機(jī),樹莓派,等 5T 學(xué)習(xí)資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評(píng)論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,676評(píng)論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,730評(píng)論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,873評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,266評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評(píng)論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,482評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,036評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,846評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,025評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,279評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評(píng)論 1 289
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,751評(píng)論 3 394
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,016評(píng)論 2 375

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