最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

如何使用簡潔的架構(gòu)實現(xiàn)高性能讀服務(wù)

2023-06-15 12:46 作者:gzqhero  | 我要投稿

說明:讀業(yè)務(wù)這個名詞偏業(yè)務(wù)和產(chǎn)品,研發(fā)在做系統(tǒng)設(shè)計或日常溝通中,會采用偏技術(shù)實現(xiàn)的名詞,如讀接口或者讀服務(wù)。在本講的后續(xù)內(nèi)容中,我統(tǒng)一用讀服務(wù)代指讀業(yè)務(wù)。

讀服務(wù)的功能實現(xiàn)要求

讀服務(wù)在實現(xiàn)流程上,基本上是純粹地從存儲中一次或者多次獲取原始數(shù)據(jù),進行簡單的邏輯加工,或者直接返回給用戶/前端業(yè)務(wù)系統(tǒng)。它是無狀態(tài)的或者是無副作用的,也就是說每一次執(zhí)行都不會在存儲中記錄數(shù)據(jù)或者修改數(shù)據(jù),每一次讀請求都和上一次無關(guān)。

比如,打開資訊類 App,你會看到兩類場景,一類是業(yè)務(wù)后臺系統(tǒng)直接從存儲中獲取今日的新聞列表,另一類是推薦系統(tǒng)生成一個新聞推薦列表,給到業(yè)務(wù)前臺系統(tǒng)并展示給用戶。

亦如在電商 App 里,首頁展示的商品和促銷等信息,是運營根據(jù)營銷策略配置的,業(yè)務(wù)后臺接收到讀請求,然后直接去存儲獲取數(shù)據(jù)并進行加工后返回給業(yè)務(wù)前臺系統(tǒng)。其他業(yè)務(wù)亦然,此處不再贅述。

結(jié)合前面兩講和上述介紹,我們可以分析出讀服務(wù)在實現(xiàn)上需要滿足的功能要求,主要有以下 3 點。

  1. 保證高可用;其實不管是不是讀服務(wù),都需要滿足高可用。

  2. 保證高性能。我先定義一個較大的指標(biāo),TP 999 要在 100ms 以內(nèi)。比如你去瀏覽新聞和電商 App,如果首頁打開得非常慢,體驗一定非常差。

  3. 支持的 QPS 非常高(如上萬~百萬的峰值 QPS)。因為大部分業(yè)務(wù)場景都是“讀多寫少”。

針對這些技術(shù)功能性指標(biāo),下面將講解如何實現(xiàn)。

架構(gòu)盡量不要分層

之前我們介紹了如何利用拆分降低系統(tǒng)架構(gòu)復(fù)雜度,通過水平拆分將一些共性的、不易變的代碼邏輯單獨封裝成一個模塊對外服務(wù)。這樣能夠減少重復(fù),提升效率。此時的讀服務(wù)架構(gòu)如下圖 1 所示:

圖片
圖 1:分層架構(gòu)的讀服務(wù)

但在實際的應(yīng)用中,通過監(jiān)控圖可以發(fā)現(xiàn)此種架構(gòu)下,讀服務(wù)性能的平均值離 TP99 或 TP999 有較大的差距,通常在一倍以上。另外,性能的毛刺也比較多。產(chǎn)生這種情況有以下兩個原因。

  • 一方面是因為采用分層架構(gòu)之后,網(wǎng)絡(luò)傳輸相比不分層的架構(gòu)多了一倍。

  • 另一方面,讀服務(wù)的業(yè)務(wù)邏輯都比較簡單,性能主要消耗在網(wǎng)絡(luò)傳輸上。因此,請求查詢的數(shù)據(jù)越少,性能越好,假設(shè)為 10ms;數(shù)據(jù)多時,性能則較差,假設(shè)為 50ms。當(dāng)疊加上分層架構(gòu),性能就會翻倍。比如數(shù)據(jù)少時,從 10ms 變成 20ms;數(shù)據(jù)多時,從 50ms 變成 100ms。分層后,數(shù)據(jù)的多與少帶來的性能差距達到了 80ms,這也是產(chǎn)生毛刺差的原因。

因此,為了提高查詢的性能減少毛刺同時降低部署機器的數(shù)量,可以將水平拆分的數(shù)據(jù)訪問層代碼工程保留獨立,但在實際編譯時,直接編譯到讀服務(wù)里。以 Java 舉例,直接將數(shù)據(jù)訪問層編譯為 JAR 包并由讀服務(wù)進行依賴。這樣在部署時,它們在同一個進程里,去掉網(wǎng)絡(luò)傳輸升級后的架構(gòu)如下圖 2 所示:

圖片
圖 2:內(nèi)嵌的讀服務(wù)架構(gòu)

在實際的案例中,當(dāng)進行此項升級后。性能有了較明顯變化,TP999 基本降了一半,平均性能降了 20%~30% 。下圖 3 展示了一個大致效果圖,你可以感受一下:

圖片
圖 3:內(nèi)嵌后的性能效果圖

上述優(yōu)化的隱含原則是,讀服務(wù)要盡可能和數(shù)據(jù)靠近,減少網(wǎng)絡(luò)傳輸。此項原則其實已經(jīng)應(yīng)用在很多類似的場景里了。比如:

  1. 現(xiàn)在很多瀏覽器都自帶本地緩存的功能,你瀏覽一個網(wǎng)頁后,在一段時間內(nèi)再次訪問時,此網(wǎng)頁的數(shù)據(jù)就是從瀏覽器緩存里獲取的,直觀感覺就是網(wǎng)頁打開速度非???。

  2. 另外 CDN 也是一樣的道理,把你需要的數(shù)據(jù)推送到和你最近的機房里,縮短網(wǎng)絡(luò)傳輸?shù)木嚯x。

這兩種場景里提升性能的方式又稱為 數(shù)據(jù)前置。但數(shù)據(jù)前置也會帶來另一個問題——數(shù)據(jù)更新不及時。關(guān)于這塊內(nèi)容,我會在“ 04 講”里詳細介紹,并給出有效的解決方案。

代碼盡可能簡單

讀服務(wù)的實現(xiàn)流程非常簡單,為了方便你理解,這里我把讀服務(wù)執(zhí)行的大致流程畫在了一張大圖里,如下圖 4 所示:

圖片
圖 4:讀服務(wù)執(zhí)行流程圖

結(jié)合圖 4 所展示的內(nèi)容,我們再來看一下讀服務(wù)的執(zhí)行步驟,接口在接受請求時:

  1. 首先會將外部的入?yún)⒔馕龀蓛?nèi)部模型并進行校驗;

  2. 之后會根據(jù)具體的存儲類型使用內(nèi)部模型構(gòu)建查詢條件并請求對應(yīng)的存儲;

  3. 獲取到數(shù)據(jù)后,進行反序列化轉(zhuǎn)換為內(nèi)部模型;

  4. 根據(jù)業(yè)務(wù)情況進行適當(dāng)?shù)靥幚恚?/p>

  5. 將處理后的數(shù)據(jù)轉(zhuǎn)換為對外 SDK 的模型并返回。

在上述流程里,有大量的模型映射,比如將外部的模型映射為代碼中的內(nèi)部模型、將內(nèi)部模型映射為查詢條件等。這些不同層次的模型,字段都差不多。為了提升映射效率,有時可能會借助一些框架對相同的字段進行自動化的轉(zhuǎn)換。

對于其他能夠提升效率的地方,你可能也會引入一些框架。在讀服務(wù)對于性能要求非常嚴(yán)格的情況下,要盡可能地減少引入框架。如果一定要引入,必須經(jīng)過嚴(yán)格的壓測。在實際的應(yīng)用中,很多能夠提升效率的框架性能都非常差。比如 Java 中的 Bean.copyProperties,它采用了反射的機制進行字段 copy,在數(shù)據(jù)量較大時,性能較低。

另外,在讀服務(wù)的處理鏈路上,為了方便排查問題,經(jīng)常會直接將請求的入?yún)⒓皬拇鎯χ蝎@取的數(shù)據(jù),使用JSON進行序列化為字符串,并進行日志打印。這種粗暴的方式,對性能也會有非常大的消耗,建議不要直接全量序列化,而是精細化地按需打印。

最后。對于讀取的內(nèi)容要在存儲處按需取,而不是全量取回后再在服務(wù)內(nèi)部進行過濾。如果存儲為 MySQL,則不要使用 select *,需要手動指定你要查詢的字段。如果存儲為 Redis,則使用 Redis 的 hash 結(jié)構(gòu)存儲數(shù)據(jù),因為 hash 結(jié)構(gòu)可以讓你在查詢時指定需要返回哪些字段。其他存儲結(jié)構(gòu),如 ElasticSearch 等亦然。

存儲的選型和架構(gòu)

讀服務(wù)最主要依賴的中間件是存儲,因此存儲的性能很大程度上決定了讀服務(wù)的性能。對于 MySQL、HBase等數(shù)據(jù)庫,即使使用分庫分表、讀寫分離、索引優(yōu)化等手段,在并發(fā)量大時,性能也很難達到 200ms 以內(nèi)。

為了提升性能,實戰(zhàn)中的架構(gòu)通常選用基于內(nèi)存的、性能更好的 Redis 作為主存儲,MySQL 作為兜底來構(gòu)建,如下圖 5 所示的架構(gòu):

圖片
圖 5:緩存+數(shù)據(jù)庫的讀服務(wù)架構(gòu)

此架構(gòu)稱為懶加載模式。在初始的時候,所有數(shù)據(jù)都存儲在數(shù)據(jù)庫中。當(dāng)讀服務(wù)接受請求時,會先去緩存中查詢數(shù)據(jù),如果沒有查詢到數(shù)據(jù),就會降級到數(shù)據(jù)庫中查詢,并將查詢結(jié)果保存在 Redis 中,以供下一次請求進行查詢。保存在 Redis 中的數(shù)據(jù)會設(shè)置一個過期時間,防止數(shù)據(jù)庫的數(shù)據(jù)變更了,請求還一直讀取緩存中的臟數(shù)據(jù)。

上述的架構(gòu)設(shè)計簡單清晰且實現(xiàn)成本較低,但還存在一些潛在的問題,不能滿足本講第一小節(jié)里提到的高可用及完全高性能的要求。主要有以下幾大類問題:

1. 存在緩存穿透的風(fēng)險

如果惡意請求不斷使用緩存中不存在的數(shù)據(jù)發(fā)送請求,就會導(dǎo)致該請求每次都會被降級到數(shù)據(jù)庫中。因為數(shù)據(jù)庫能夠支持的并發(fā)有限,如果請求量很大,可能會把數(shù)據(jù)庫打掛,進而引起讀服務(wù)不可用。這也就不滿足高可用這個要求。

針對數(shù)據(jù)庫中沒有的數(shù)據(jù),可以在緩存中設(shè)置一個占位符。在第二次請求處理時,讀取緩存中的占位符即可識別數(shù)據(jù)庫中沒有此數(shù)據(jù),然后直接返回給業(yè)務(wù)前臺系統(tǒng)即可。

使用占位符雖然解決了穿透的問題,但也帶來了另外一個問題。如果惡意請求不斷變換請求的條件,同時這些條件對應(yīng)的數(shù)據(jù)在數(shù)據(jù)庫中均不存在,那么緩存中存儲的表示無數(shù)據(jù)的占位符也會把整個緩存撐爆,進而導(dǎo)致有效數(shù)據(jù)被緩存清理策略清除或者整個讀服務(wù)宕機。

對于此種惡意請求,就需要在業(yè)務(wù)上著手處理。對于請求的參數(shù)可以內(nèi)置一些 token 或者一些驗證數(shù)據(jù),在讀服務(wù)中前置進行校驗并攔截,而不是透傳到緩存或數(shù)據(jù)庫中。

2. 緩存集中過期導(dǎo)致雪崩

對存儲在緩存中的數(shù)據(jù)設(shè)置過期時間是為了定期獲取數(shù)據(jù)庫中的變更,但如果設(shè)置不合理,可能會導(dǎo)致緩存集中過期,進而所有的讀請求都會因緩存未命中,而直接請求到數(shù)據(jù)庫。因緩存支持的量級至少是數(shù)據(jù)庫的十倍以上,此類瞬間高并發(fā)的流量會直接將數(shù)據(jù)庫打掛,進而宕機。

對于數(shù)據(jù)庫的過期時間,可以在設(shè)置時進行加鹽操作。假設(shè)原先統(tǒng)一是 2 個小時過期,設(shè)置時根據(jù)隨機算法在一個區(qū)間內(nèi)獲取一個隨機值,在 2 個小時的過期時間上再加上此隨機值,這就做到了各個緩存的過期時間不一致,同時過期的緩存數(shù)量最可控。

3. 懶加載無法感知實時變更

在緩存中設(shè)置過期時間,雖然可以讓用戶感知到數(shù)據(jù)的變更。但感知并不是實時的,會有一定延遲。在某些對于數(shù)據(jù)變更不敏感的場景是可以的,比如編輯新發(fā)布了一個新聞,但你沒有看到,因為你都不知道編輯新發(fā)布了一個新聞。

如果想要做到實時看到數(shù)據(jù)的變更,可以將架構(gòu)升級。升級后的架構(gòu)如下圖 6 所示:

圖片
圖 6:主動推送變更的架構(gòu)

在每次修改完數(shù)據(jù)之后,主動將數(shù)據(jù)更新至緩存里。此種方案下,緩存里的數(shù)據(jù)均和數(shù)據(jù)庫保持一致。

但在細節(jié)上,還是存在一些問題。如果你修改完了數(shù)據(jù)庫再更新緩存,在異常情況下,可能出現(xiàn)數(shù)據(jù)庫更新成功了,但緩存更新失敗了的情況。因為數(shù)據(jù)庫和緩存是兩個存儲,如果沒有分布式事務(wù)的機制,緩存更新失敗了,數(shù)據(jù)庫的數(shù)據(jù)是不會回滾的。此時,緩存和數(shù)據(jù)庫中的數(shù)據(jù)依然不一致,因此這個方案并沒有完美解決問題。如果先更新緩存,再更新數(shù)據(jù)庫,同樣會因為沒有分布式事務(wù)的保障,出現(xiàn)緩存中存在臟數(shù)據(jù)的問題。

另外, 在更新數(shù)據(jù)庫后主動更新緩存的模式,在實際的實施層面很容易出現(xiàn)遺漏。因為你需要在所有更新數(shù)據(jù)庫的地方都加上主動更新緩存的代碼,當(dāng)開發(fā)人員不斷變更時,很容易出現(xiàn)遺漏的情況,比如在某一個需求里,開發(fā)人員只更新了數(shù)據(jù)庫而沒有更新緩存。

除了容易遺漏之外,在所有更新數(shù)據(jù)庫的地方,都利用緩存和數(shù)據(jù)庫的分布式事務(wù)來保證數(shù)據(jù)完全一致的成本較高,在實際工作中,成本也是一個必須要考慮的問題。

4. 懶加載無法擺脫毛刺的困擾

使用懶加載的緩存過期方案,還有一個無法避免的問題,就是性能毛刺。當(dāng)緩存過期時,讀服務(wù)的請求都會穿透到數(shù)據(jù)庫中,對于穿透請求的性能和使用緩存的性能差距非常大,時常是毫秒和秒級別的差異。

大部分普通業(yè)務(wù)場景可以容忍此問題,但在一些對性能要求極高的場景里,比如 App 首頁,毛刺問題仍需重視和解決。

至此,懶加載架構(gòu)的四個問題及對應(yīng)的潛在解決方案已講解完畢。雖然懶加載架構(gòu)存在一些問題,但在實際應(yīng)用中,此方案及其變種方案因為實現(xiàn)簡單、成本低,仍是使用較多的解決方案。

總結(jié)

在本講里,我們介紹了讀服務(wù)在實現(xiàn)時應(yīng)該滿足的技術(shù)功能性要求,由此確定了讀服務(wù)實現(xiàn)時應(yīng)該滿足的目標(biāo),應(yīng)該遵守兩個基本原則:架構(gòu)盡量不要分層、代碼盡可能簡單。在此原則之上,我們提供了一個在實戰(zhàn)中常見的架構(gòu)方案,指出了此方案存在的四點不足,并提供了相對應(yīng)的應(yīng)對方案。


如何使用簡潔的架構(gòu)實現(xiàn)高性能讀服務(wù)的評論 (共 條)

分享到微博請遵守國家法律
望江县| 吉水县| 平顶山市| 芜湖县| 衡阳县| 贵南县| 富源县| 双鸭山市| 喜德县| 青铜峡市| 宜都市| 昭平县| 安徽省| 于田县| 泰兴市| 马鞍山市| 禹城市| 霍林郭勒市| 莱阳市| 翁源县| 乌审旗| 灵川县| 道孚县| 关岭| 盐山县| 汶上县| 南溪县| 乌鲁木齐市| 黄浦区| 德钦县| 平潭县| 博乐市| 廉江市| 阿图什市| 响水县| 乐业县| 河池市| 涞源县| 赤城县| 天长市| 西充县|