<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          騰訊云十億級 Node.js 網(wǎng)關(guān)的架構(gòu)設(shè)計與工程實踐

          共 14663字,需瀏覽 30分鐘

           ·

          2022-01-23 20:06

          作者|王偉嘉
          編輯|孫瑞瑞

          本文由 InfoQ 整理自訊云 CloudBase 前端負(fù)責(zé)人王偉嘉在 GMTC 全球大前端技術(shù)大會(深圳站)2021 上的演講《十億級 Node.js 網(wǎng)關(guān)的架構(gòu)設(shè)計與工程實踐》。

          大廠技術(shù)??高級前端??Node進(jìn)階

          點(diǎn)擊上方?程序員成長指北,關(guān)注公眾號

          回復(fù)1,加入高級Node交流群

          大家好,我今天的演講主題主要是講我們業(yè)務(wù)上用 Node.js 寫的一個網(wǎng)關(guān)。先做個簡單的自我介紹,我叫王偉嘉,現(xiàn)在是騰訊云云開發(fā) CloudBase 的前端負(fù)責(zé)人,說 CloudBase 可能很多人不太知道,但是我們旗下其實有挺多產(chǎn)品的,可能或多或少聽說過,比如說小程序·云開發(fā),寫小程序的同學(xué)應(yīng)該會知道吧。

          當(dāng)然我今天不是來推銷產(chǎn)品的,今天是開門見山的講一講網(wǎng)關(guān)是一個怎么樣的組件,網(wǎng)關(guān)在做什么事情。網(wǎng)關(guān)這個詞其實到處都在用,它可以工作在一個硬件的層面,可以工作在網(wǎng)絡(luò)層,也可以工作在應(yīng)用層。

          網(wǎng)關(guān)快速入門

          網(wǎng)關(guān)在做什么?

          我們今天講的實際上是一個工作在 HTTP 七層協(xié)議的網(wǎng)關(guān),它主要做的有幾件事情:

          第一,公網(wǎng)入口。它作為我們公有云服務(wù)的一個入口,可以把公有云過來的請求定向到用戶的資源上面去。

          第二,對接后端資源。我們云開發(fā)有很多內(nèi)部的資源,像云函數(shù)、容器引擎這樣的資源,便可以把請求對接到這樣的云資源上面去。

          第三,身份鑒權(quán)。云開發(fā)有自己的一套賬號身份體系,請求里如果是帶有身份信息的,那么網(wǎng)關(guān)會對身份進(jìn)行鑒權(quán)。

          所以網(wǎng)關(guān)這個東西聽起來好像是很底層的一個組件,大家可能會覺得很復(fù)雜,實際上并沒有。我們就花幾行代碼,就可以實現(xiàn)一個非常簡單的 HTTP 網(wǎng)關(guān)的邏輯。

          import express from 'express'import { requestUpstream, resolveUpstream } from './upstream'

          const app = express()

          app.all('*', (req, res) => { console.log(req.method, req.path, req.headers) const upstream = await resolveUpstream(req.method, req.path, req.headers) const response = await requestUpstream(upstream, req.body) console.log(response.statusCode, response.headers) res.send(response)})

          const port = 3000app.listen(port, () => { console.log(`App listening at ${port}`)})

          這段示例代碼在做的事情很簡單,即我們收到一個請求之后,會根據(jù)請求的方法或者路徑進(jìn)行解析,找出它的上游是什么,然后再去請求上游,這樣就完成一個網(wǎng)關(guān)的邏輯。

          當(dāng)然這是最簡的一個代碼了,實際上里面有很多東西是沒有考慮到的,比如技術(shù)框架以及內(nèi)部架構(gòu)模塊的治理,比如性能優(yōu)化、海量的日志系統(tǒng)、高可用保障、DevOps 等等。當(dāng)然這樣展開就非常大了,所以我今天也不會面面俱到,會選其中幾個方向來講的比較深一點(diǎn),這樣我覺得會對大家比較有收獲。

          云開發(fā) CloudBase(TCB) 是個啥?

          說到這個,順便介紹一下我們云開發(fā) CloudBase 是什么,要介紹我們網(wǎng)關(guān)肯定要知道我們業(yè)務(wù),像小程序·云開發(fā)、Web 應(yīng)用托管、微搭低代碼平臺,還有微信云托管這樣的服務(wù)都是在我們體系內(nèi)的。

          這些服務(wù)它的資源都會過我們的網(wǎng)關(guān)來進(jìn)行鑒權(quán),你可以在云開發(fā)體系下的控制臺上,看到我們 URL 的入口,實際上這些 URL 它的背后就是我們的網(wǎng)關(guān)。

          整個網(wǎng)關(guān)最簡版的一個架構(gòu)如上圖所示,我們會給用戶免費(fèi)提供一個公網(wǎng)的默認(rèn)域名,這個域名它背后實際上是一套 CDN 的分發(fā)網(wǎng)絡(luò),然后 CDN 回源到核心網(wǎng)關(guān)上面來。我們網(wǎng)關(guān)本身是無狀態(tài)的服務(wù),收到請求之后,它需要知道如何把請求分發(fā)到后端的云資源上去,所以有一個旁路的后端服務(wù)可以讀取這樣一套數(shù)據(jù)。

          網(wǎng)關(guān)的后面就是用戶自己的云上資源了,你在云開發(fā)用到任何資源幾乎都可以通過這樣的鏈路來進(jìn)行訪問。

          網(wǎng)關(guān)內(nèi)部是基于 Nest.js 來做的,選 Nest.js 是因為它本身自帶一套設(shè)計模式,很 Spring 那一套,更多的是做 IOC 容器這一套設(shè)計模式。

          從上圖可以看到,我們把它的內(nèi)部架構(gòu)分成了兩層,一層是 Controller,一層是 Service。Controller 主要是控制各種訪問資源的邏輯。比如說你去訪問一個云函數(shù)(SCF),和你去訪問一個靜態(tài)托管的資源,它所需要的訪問信息肯定是不一樣的,所以這也就是分成了幾種 Controller 來實現(xiàn)。

          底層的話 Service 這一層是非?!昂瘛钡?,Service 內(nèi)部又分成邏輯模塊和功能性模塊。

          首先第一大塊是我們的邏輯模塊,邏輯模塊主要是處理我們內(nèi)部服務(wù)模塊的很多東西,最上面這一層主要就是處理跟資源訪問相關(guān)的一些請求的邏輯,跟各種資源使用不同的協(xié)議、方法來對接。然后中間這一層,更多的是做我們內(nèi)部的一些集群的邏輯。比如集群管理,作為一個公有云的服務(wù),我們對于客戶也是會分等級的,像 VIP 客戶可能就需要最后來發(fā)布,我們肯定是先驗證一些灰度的流量,像這塊邏輯就屬于中間這一層來管理。最下面這一層就會有各種負(fù)責(zé) I/O 的 Client,我這一次只畫了一個 HTTP 的 Client,實際上還會有一些別的 Client。

          除此之外還有一些旁路的功能性模塊,包括像怎么打日志、配置、本地緩存的管理、錯誤處理,還有本地配置管理 DNS、調(diào)用鏈追蹤等這些旁路的服務(wù)。

          這一套設(shè)計其實就是老生常談的高內(nèi)聚低耦合,業(yè)務(wù)邏輯和真正的 I/O 實現(xiàn)要解耦開。因為只有解耦開,你才能夠針對你的業(yè)務(wù)邏輯進(jìn)行單元化的測試,可以很方便的把它底層的這種 I/O 讀寫邏輯給 Mook 起來,保證核心業(yè)務(wù)邏輯模塊的可測試性。

          上圖是網(wǎng)關(guān)整體的鏈路架構(gòu),稍微更全面一點(diǎn)。最上層是分布在邊緣的 CDN 節(jié)點(diǎn),然后這些 CDN 節(jié)點(diǎn)會回源到我們部署在各地的集群,然后這些集群它又可以訪問后面不同區(qū)域的資源,因為公有云它的資源其實也是按區(qū)域來劃分的,所以這就講到了我們網(wǎng)關(guān)的兩個核心的要求,“快”和“穩(wěn)”

          首先作為一個網(wǎng)關(guān)肯定是要快的,因為網(wǎng)關(guān)作為 CloudBase 云函數(shù)、云托管、靜態(tài)資源的公網(wǎng)出口,性能要求極高,需要承接 C 端流量,應(yīng)對各種地域、各種設(shè)備的終端接入場景。如果說過你網(wǎng)關(guān)這一層就可能就花了幾百毫秒甚至一兩秒鐘的時間,對于客戶來講是不可接受的。因為客戶他自己的一個函數(shù)可能只跑了 20 毫秒,如果網(wǎng)關(guān)也引入 20 毫秒的延遲,對于客戶來講他就覺得你這條鏈路不行。

          其次就是要穩(wěn),我們是大租戶模式,要扛住海量的 C 端請求,我們需要極高的可用性。作為數(shù)據(jù)面的核心組件,需要極高的可用性,任何故障將會直接影響下游客戶的業(yè)務(wù)穩(wěn)定性。如果在座的有四川或者云南的同學(xué),你回家每次打開健康碼掃碼,其實請求都會經(jīng)過我這個網(wǎng)關(guān)的。

          所以今天主要就講兩個部分,既然是快和穩(wěn),分別對應(yīng)性能優(yōu)化和可用性,所以我現(xiàn)在從性能優(yōu)化開始講起。

          性能優(yōu)化

          網(wǎng)關(guān)性能優(yōu)化思路

          性能優(yōu)化的思路,首先是看時間都花在哪個地方了,網(wǎng)關(guān)是一個網(wǎng)絡(luò)的組件,大部分時間都是耗在 I/O 上,而不是本身的計算,所以它是一個高 I/O、低計算的一個組件。

          網(wǎng)關(guān)有幾個技術(shù)特點(diǎn):首先,它的自身業(yè)務(wù)邏輯多,是重 I/O、輕計算的一個組件;其次它的請求模式是比較固定的,模式固定我們可以理解為,你的一個客戶他發(fā)送過來的請求實際上就是那么幾種,它的路徑、包體大小、請求頭等這些都是比較趨向于固定的,很難會有一個客戶他的請求是完全隨機(jī)生成的,這對我們后面針對這種情況做緩存設(shè)計會有一些幫助。最后,網(wǎng)關(guān)的核心鏈路很長,涉及到多個網(wǎng)絡(luò)平面。

          那么我們就找到了我們的一些優(yōu)化方向:減少整個 IO 的消耗,并且優(yōu)化核心鏈路。所以優(yōu)化部分我就分成了兩塊內(nèi)容來講,第一塊是網(wǎng)關(guān)自身核心服務(wù)優(yōu)化,第二塊是整體架構(gòu)鏈路優(yōu)化。

          核心服務(wù)性能優(yōu)化

          第一部分,核心的服務(wù)怎么做優(yōu)化?先提幾個方向。

          第一,網(wǎng)關(guān)自身的業(yè)務(wù)邏輯很多,調(diào)用很多外部服務(wù),其中有一些是不需要同步阻塞調(diào)用的,因此我們會把部分業(yè)務(wù)邏輯做異步化,讓到后臺去異步運(yùn)行。比如說自定義域名來源的請求,我們要先確定這個域名是不是合法綁定的,這里的校驗就會放到后臺異步來進(jìn)行,網(wǎng)關(guān)只是讀取校驗的結(jié)果。

          第二,網(wǎng)關(guān)類似代理,轉(zhuǎn)發(fā)請求響應(yīng)體,這里我們使用了流式的傳輸方式。其實 Node 原生的 HTTP 模塊里,HTTP Body 已經(jīng)是一個流對象了(Stream),并不需要額外的引入類似 body-parser 這樣的組件把 Stream 轉(zhuǎn)成一個 JavaScript 對象。為此我們在網(wǎng)關(guān)的設(shè)計上就盡量避免把請求相關(guān)的元數(shù)據(jù)放到 Body 里,于是網(wǎng)關(guān)就可以只解析請求頭,而不解析用戶的請求體,原封不動地流式轉(zhuǎn)給后端就可以了。

          第三,我們請求后端資源的時候,改用長連接,減少短連接帶來的握手消耗。像 Nginx 這樣的組件,它通常都是短連接的模式,因為我們這些業(yè)務(wù)情況比較特殊一點(diǎn),是一個大租戶的模式,類似于所有用戶共用同個 Nginx,那么你再啟用短連接模式的話,就會有一個 TCP TIME_WAIT 的問題,下面會詳細(xì)討論。

          最后,我們的請求模式比較固定,我們會針對實際情況設(shè)計一些比較合理的緩存的機(jī)制。

          優(yōu)化點(diǎn):啟用長連接機(jī)制

          首先,為什么短連接會有問題?

          我們?nèi)フ埱笥脩糍Y源的時候,網(wǎng)關(guān)所在的網(wǎng)絡(luò)平面是內(nèi)部服務(wù)的平面,但是每個用戶的公有云資源實際上是另一個網(wǎng)絡(luò)平面。那么這兩個網(wǎng)絡(luò)平面之間是需要通過一個穿透網(wǎng)關(guān)來通信的。這個穿透網(wǎng)關(guān)可以理解為是一種網(wǎng)絡(luò)層虛擬設(shè)備,或者你可以理解為它就是一個四層轉(zhuǎn)發(fā)的 Nginx,作為代理客戶端,單個實例可以最大承載 6.5W 的 TCP 的連接數(shù)。

          如果做過一些傳輸層協(xié)議的同學(xué)應(yīng)該會知道,一個 TCP 連接斷開之后,客戶端會進(jìn)入一個 TIME_WAIT 的階段,然后在 Linux 內(nèi)核里面它會等待兩倍的時間,默認(rèn)是 60 秒,兩倍是 120 秒,120 秒之后才會釋放這個連接,也就是說在 120 秒內(nèi),客戶端的端口實際上都是處于被占用的狀態(tài)的。所以我們很容易能算出來單個傳統(tǒng)網(wǎng)關(guān)它能夠承載的最大的額定 QPS 大概就是不到 600 的樣子,這個肯定是不能滿足用戶需求的。

          那么我們怎么去解決短連接 TIME_WAIT 這個問題?其實有好幾種方法。

          第一種是修改 Linux 的 TCP 傳輸層的內(nèi)核參數(shù),去啟用重用、快速回收等機(jī)制。但對于我們的服務(wù)來說并不合適,這需要定制這樣一個系統(tǒng)內(nèi)核,維護(hù)成本會非常高。

          第二種,云上類似的組件怎么解決?比如騰訊內(nèi)部的負(fù)載均衡,其實很簡單,就是直接擴(kuò)張集群內(nèi)的VM數(shù)量。比如一臺反向代理服務(wù)器加上TIME_WAIT快速回收,可以承載5000多QPS,但想要二十萬QPS 怎么辦,做40個虛擬實例就行了。但這種做法,一是需要內(nèi)核定制,二是需要我們付出很大的虛擬實例成本,就沒有選擇這種經(jīng)典方案。

          最后一種就是我們改成長連接的機(jī)制,類似 Nginx 的 Upstream Keepalive 這樣的機(jī)制。改成這樣一個機(jī)制之后,其實效果還挺好的,單個穿透網(wǎng)關(guān)就可以最大承載 6.5W 個連接數(shù),相當(dāng)于幾乎 6.5W 個并發(fā)。對于同一個目標(biāo) IP PORT,它可以直接復(fù)用連接,所以它穿透網(wǎng)關(guān)的連接數(shù)限制就不再是瓶頸了。

          長連接的問題

          那么是不是長連接就是完美的?其實并不是。長連接會導(dǎo)致另外一個問題,競態(tài)問題(keep-alive race condition),如果在座里有用 HTTP 長連接的方式做 RPC 調(diào)用的同學(xué),應(yīng)該經(jīng)常會看到這個問題。

          客戶端與服務(wù)端成功建立了長連接連接靜默一段時間(無 HTTP 請求)服務(wù)端因為在一段時間內(nèi)沒有收到任何數(shù)據(jù),主動關(guān)閉了 TCP 連接客戶端在收到 TCP 關(guān)閉的信息前,發(fā)送了一個新的 HTTP 請求服務(wù)端收到請求后拒絕,客戶端報錯 ECONNRESET

          所以怎么解決?

          第一種方案,就是把客戶端的 keep-alive 超時時間設(shè)置得短一些(短于服務(wù)端即可)。這樣就可以保證永遠(yuǎn)是客戶端這邊超時關(guān)閉的 TCP 連接,消除了錯誤的暫態(tài)。

          但這樣在實際生產(chǎn)環(huán)境中是沒法 100% 解決問題的,因為無論把客戶端超時時間如何設(shè)置到多少,因為網(wǎng)絡(luò)延遲的存在,始終無法保證所有的服務(wù)端的 keep-alive 超時時間都長于客戶端的值;如果把客戶端超時時間設(shè)置得太?。ū热?1 秒),又失去了意義。

          那么正確方法就是用短連接去重新試一次。遇到這個錯誤,并且它是長連接的,那么你就用短連接來發(fā)起一次重試。這個也是參考了 Chrome 的做法,Chromium 自己的內(nèi)核里面處理了這樣一種情況,瀏覽器里它其實這種長連接也是時刻存在的,下圖是一段它自己里面的內(nèi)核的代碼。

          2019 年的時候,社區(qū)里常用的 agentkeepalive 不支持識別當(dāng)前請求是否開啟 keepalive,我們給社區(qū)提交過一個 PR,支持了這個特性。也就說你只要使用了 agentkeepalive 這樣一個包,就可以寫一段代碼來識別出這種情況,并且進(jìn)行重試。

          這是我們一個日常統(tǒng)計的量,大概萬分之 1.3 的概率,會命中這樣一個競態(tài)的情況。

          小結(jié)

          1. 非必要情況,不要用 HTTP 協(xié)議作為 RPC 底層協(xié)議。因為 HTTP 本身最適合的場景是瀏覽器跟服務(wù)端來做的,而不是一個服務(wù)端和服務(wù)端之間的一個 IPC 協(xié)議,盡量使用 gRPC 或者類似的這樣的協(xié)議來做。

          2. 如果不得已使用 HTTP,你的后端可能非常老舊,開啟長連接是一種較好的方案。

          3. 長連接需要解決 Keep Alive 的競態(tài)問題。如果你用長連接,記得一定要處理這個問題,不然這個問題會成為一個幽靈一樣存在。像剛才說的,萬分之 1.3 非常難復(fù)現(xiàn),但是這個錯誤又會不停地出現(xiàn)在你業(yè)務(wù)里。

          優(yōu)化點(diǎn):設(shè)計緩存機(jī)制

          緩存在后臺設(shè)計里是個萬金油,“哪里慢了抹哪里”,但是如何設(shè)計緩存其實也是一門學(xué)問。

          前面提到我們的請求模式都是非常固定的,我們可以根據(jù)請求模式來決定緩存數(shù)據(jù)。緩存都是些什么東西呢?是路由配置,像域名配置、環(huán)境信息、臨時密鑰等這些信息。

          這些數(shù)據(jù)有哪些特點(diǎn)?首先是活躍數(shù)據(jù)占比小,這確實也是現(xiàn)狀。假設(shè)我們?nèi)康挠脩衾锩婷刻熘挥写蟾?5%~10% 的用戶才是活躍的,這個數(shù)據(jù)才是真的會經(jīng)過你的網(wǎng)關(guān)。其次是模式比較固定。第三是對實時性的要求不高。比如說變更了路由之后,客戶通常是能夠接受有 1~3 分鐘不定的延遲的,并不要求說變更了路由之后就即刻生效。

          因此我們可以針對以上這些特點(diǎn)來設(shè)計緩存。第一是因為我們的活躍數(shù)據(jù)占比很小,所以我們是緩存局部數(shù)據(jù),從來不會緩存全量的數(shù)據(jù)。第二是我們會選取域名、環(huán)境這種幾乎是固定的信息作為緩存 Key,這樣緩存的覆蓋面就可以得到保證。第三是讀時緩存要大于寫時緩存,這個后續(xù)會提到為什么會選用讀時緩存,而不是寫入數(shù)據(jù)的時候把緩存推到我們的網(wǎng)關(guān)里。

          本地緩存的局限性

          最早的時候,實際上我們是有一個最簡單的設(shè)計,就是加了一個非常簡單的本地緩存,它可能就是以域名或以路徑作為緩存的 Key,這樣實現(xiàn)簡單但有很多局限性:

          首先,要寫大量這樣的代碼,要去先讀本地有沒有緩存,有緩存就緩存,沒緩存去后臺要數(shù)據(jù)。

          其次,因為網(wǎng)關(guān)不是一個單獨(dú)的實例,它不是一個單進(jìn)程的 Node,單進(jìn)程的 Node 是扛不了這么多量的,我們是有很多很多實例,大概是有幾千核,也就是說有幾千個 Node 進(jìn)程,如果這些進(jìn)程它本身都有一份自己獨(dú)有的內(nèi)存,也就導(dǎo)致它這個緩存沒有辦法在所有實例上生效。因此當(dāng)我們的網(wǎng)關(guān)規(guī)模變得越來越大的時候,緩存也就永遠(yuǎn)都只能出現(xiàn)在局部。

          為了解決這樣的問題,我們加入了 Redis 中心化的緩存。我們是本地內(nèi)存 +Redis 兩層緩存,本地內(nèi)存主要是為了降低 Redis 負(fù)載。當(dāng) Redis 故障的時候也可以降級到本地緩存,這樣可以避免緩存擊穿問題。Redis 作為一個中心化的緩存,使緩存可以在所有實例上生效,也就是說只要請求過了一次網(wǎng)關(guān),Redis 緩存就會生效,并且所有的網(wǎng)關(guān)實例上都會讀到這樣一個緩存。

          既然有了緩存,那必然有緩存淘汰的機(jī)制,怎么樣合理地淘汰你的緩存?這里是用了 TTL + LRU 兩重的機(jī)制來保證,針對不同的數(shù)據(jù)類別,單獨(dú)設(shè)置參數(shù),為什么是 TTL + LRU?后面在容災(zāi)部分會進(jìn)行解釋。

          最后就是抽象出數(shù)據(jù)加載層,它是專門用來封裝讀操作,包括緩存的管理、請求、刷新、容災(zāi)這樣一套機(jī)制,我們內(nèi)部會有一個專門模塊來處理。

          有了 Redis 之后,我們的緩存是中心化的了,只要你的請求經(jīng)過了我們之后,你的東西就可以在所有的實例上生效。但是這樣會引來另一個問題,因為淘汰機(jī)制是 TTL 的,必然遇到緩存過期。假設(shè)是每秒鐘都會回頭發(fā)起一次請求,那么緩存是一定是會過期的,一分鐘或兩分鐘之后你的緩存就過期了,在過期之后的請求一定是不會命中緩存的,這導(dǎo)致了請求毛刺的問題。這對于在持續(xù)流量的下游業(yè)務(wù)上,體現(xiàn)非常明顯,下圖是我們的一個截圖。

          可以看到圖上有很多毛刺,這些毛刺的尖尖就是它沒有命中緩存的時候,為了解決緩存的毛刺問題,我們加入了 Refresh-Ahead 這樣一個機(jī)制,就是說每次請求進(jìn)來的時候,我們首先會去 Redis 里去讀,使用緩存的數(shù)據(jù)來運(yùn)行邏輯。

          同時我們也會判斷,如果緩存剩余 TTL 小于一定值,它就會即觸發(fā)異步刷新的邏輯,這時候我們會去請求后端服務(wù),并且把更新鮮一點(diǎn)的數(shù)據(jù)刷新到 Redis 里,這就是我們數(shù)據(jù)加載層內(nèi)實現(xiàn) Refresh-Ahead 機(jī)制的大概邏輯。

          Refresh-Ahead 其實非常簡單,字面意義就是說提前去刷新緩存,緩存數(shù)據(jù)快到 TTL 了,那么就去提前更新一下。

          能夠這樣設(shè)計,更多是基于一個先驗的邏輯,就是說當(dāng)下這一刻被訪問的數(shù)據(jù),大概率在未來的一段時間內(nèi)會再次被訪問。

          下圖是我們加入了 Refresh-Ahead 之后的一個效果,紅色箭頭處是上線時間,上線完之后發(fā)現(xiàn)毛刺就明顯變少了。但是為什么還會有一點(diǎn)毛刺?因為有一些數(shù)據(jù)它可能真的就是很長的時間,刷新了之后它也依然過期了,依然會產(chǎn)生這樣的毛刺。

          最后解釋一下,為什么我們是網(wǎng)關(guān)去后臺讀數(shù)據(jù),而不是后臺把數(shù)據(jù)推給網(wǎng)關(guān)?或者說,為什么是“拉”而不是“推”?

          這其實有幾個考量點(diǎn),第一,因為是數(shù)據(jù)局部緩存,所以我們?nèi)繑?shù)據(jù)完全推過來體積很大,大概有幾十個 G,而活躍占比很小,如果完全存在內(nèi)存里,其實也是一種反模式的做法,不太經(jīng)濟(jì)。

          第二,后臺能不能只推局部活躍的數(shù)據(jù)給到網(wǎng)關(guān)呢?其實也是不太合適的,后臺很難去識別哪些數(shù)據(jù)是活躍的,哪些數(shù)據(jù)不是活躍的,這樣實現(xiàn)復(fù)雜,難度很大。

          第三,網(wǎng)關(guān)和它的持久化的后臺之間會產(chǎn)生一個緩存 Key 上的耦合,所謂的 Key 上的耦合就是說雙方要約定一組 Key,我這個數(shù)據(jù)是在 Key 上面去讀,然后你后臺要把數(shù)據(jù)推到 Key 上面。那么就會帶來另一個問題,一旦 Key 寫錯了,或者說出現(xiàn)了一些不可預(yù)料的問題,那就會產(chǎn)生一些比較災(zāi)難性的后果,所以我們就沒有使用“推”這樣一種方式。

          小結(jié)

          1. 在現(xiàn)代大規(guī)模服務(wù)里,緩存是必選項,不是可選項。

          2. 緩存系統(tǒng)本質(zhì)是一個小型的分布式系統(tǒng),無法逾越 CAP 理論。

          3. 根據(jù)業(yè)務(wù)場景,合理地權(quán)衡性能、一致性和可用性。

          架構(gòu)、鏈路性能優(yōu)化

          前面講的是服務(wù)跟服務(wù)自己核心的優(yōu)化,接下來講一講架構(gòu)和鏈路上的一些性能優(yōu)化。

          上圖是我們整體的一個架構(gòu),可以看到一個請求,它從前面的接入層一直走到后端云資源之間,其實整個鏈路是很長的,這里分析一下。

          首先鏈路很長,涉及邊緣節(jié)點(diǎn)、核心業(yè)務(wù)、后端資源。其次網(wǎng)關(guān)是承接 C 端流量的,它其實對終端的性能是很敏感的。第三個就是網(wǎng)絡(luò)環(huán)境復(fù)雜,它涉及到數(shù)個網(wǎng)絡(luò)平面的打通。因此我們就有了優(yōu)化方向,第一個是讓鏈路更快更短。第二個是核心服務(wù) Set 化,便于多地域鋪設(shè),終端用戶可以就近接入。第三個就是我們在網(wǎng)絡(luò)平面之間會做一些針對性的優(yōu)化,針對性優(yōu)化怎么做,后面會提。

          前置鏈路:CDN 就近回源

          首先先講一下我們就近接入是怎么做的,在網(wǎng)關(guān)最開始上線的時候,其實會存在一個問題,你的 CDN 節(jié)點(diǎn)它其實是通過公網(wǎng)回源的,那為什么是公網(wǎng)回源?

          其實這涉及到國內(nèi)這幾家大廠的一個網(wǎng)絡(luò)架構(gòu),簡單地說就是,諸多的 CDN 節(jié)點(diǎn)中,有部分可能不是騰訊自建的,所處的網(wǎng)絡(luò)可能不是騰訊的內(nèi)網(wǎng),它可能是某個運(yùn)營商,比如說電信、聯(lián)通或者網(wǎng)通這樣的邊緣節(jié)點(diǎn),然后它是要走公網(wǎng)回源到騰訊的入口的,這里的公網(wǎng)回源就非常慢。

          比如說廣州的節(jié)點(diǎn)回源到上海,并且走 HTTPS 協(xié)議,那就是 60~100 毫秒,但問題在于 CDN 節(jié)點(diǎn)是有很多的,HTTPS 握手之后,這個鏈接還是沒有辦法復(fù)用的,等于說每次請求都要跟源站之間進(jìn)行一次 HTTPS 握手,這個延遲是不可接受的。

          最后我們在網(wǎng)關(guān)的回源接入點(diǎn)上做了一層就近接入,也就是說你 CDN 在廣州的節(jié)點(diǎn),可以很就近地接入到我們在部署在廣州的網(wǎng)關(guān),然后網(wǎng)關(guān)內(nèi)部再進(jìn)行跨地域的訪問,因為這個時候就已經(jīng)是內(nèi)網(wǎng)了,速度就會很快。

          為了能更好地鋪設(shè)網(wǎng)關(guān)多地接入點(diǎn),我們就把網(wǎng)關(guān)改造成了地域無感的,即業(yè)務(wù)邏輯和它所在的地域是解耦的。其次,網(wǎng)關(guān)支持跨地域訪問后端資源。最后,配置收歸統(tǒng)一,所有地域用同樣的后端資源配置,減少了我們不同地域的配置發(fā)散的問題。

          服務(wù)本體:SET 化部署

          把這些事情做了之后,網(wǎng)關(guān)其實達(dá)到了“SET 化部署”的概念,降低就近接入成本,任意集群能訪問任意地域的后端資源。相當(dāng)于網(wǎng)關(guān)在所有地域的集群,服務(wù)能力都是一模一樣的。你可以使用任意域名去任意網(wǎng)關(guān)訪問,獲得到結(jié)果都是一樣,這樣 SET 化部署帶來很多好處:

          • 新地域接入點(diǎn)的部署、維護(hù)成本極大下降

          • 便于鋪設(shè)就近接入點(diǎn),加速 CDN 接入

          • 不同地域的集群之間服務(wù)能力完全等價,帶來容災(zāi)能力上的提升:流量拆分、故障隔離

          也就是說全網(wǎng)只要只剩一個地域的網(wǎng)關(guān)可用,我們的服務(wù)就可以正常的運(yùn)行。

          底層組件同可用區(qū)部署

          接下來涉及到網(wǎng)絡(luò)平面之間的部署,剛才提到了我們在訪問用戶的資源的時候,其實會經(jīng)過一個穿透網(wǎng)關(guān),這個是不可避免的,因為它涉及到兩個網(wǎng)絡(luò)平面的打通,在穿透網(wǎng)關(guān)的這一條鏈路也是可以優(yōu)化的。

          我們可以看一個數(shù)據(jù),就是像這種穿透網(wǎng)關(guān)和我們云上的資源,它通常是部署在不同的機(jī)房的。

          舉個例子,像圖上的上海二區(qū),它實際上是在上海的花橋機(jī)房,穿透網(wǎng)關(guān)因為它是網(wǎng)絡(luò)層提供的設(shè)備,它會部署在上海六區(qū)。查一下地理位置可以看到,二區(qū)到六區(qū)之間其實相隔了可能有七八十公里,后端資源是在上海三區(qū)的,寶信。在地理位置上講,它整個請求就經(jīng)過了下圖這樣一段鏈路。

          但實際上這也是完全沒有必要的,我們可以將網(wǎng)關(guān)和穿透網(wǎng)關(guān)部署在同樣一個區(qū)域,這樣就會極大降低從網(wǎng)關(guān)到后端資源這樣的一個延遲。當(dāng)然這個事情我們正在慢慢地鋪設(shè)中,現(xiàn)在還在驗證可行性的階段,我們設(shè)想是這樣來做。

          最后來看效果,我們總體的緩存命中率大概有 99.98%。你可以自己部署一個很簡單的服務(wù)到我們的平臺上,然后跑一下測速,你會發(fā)現(xiàn)全國其實都是綠的,這個也是我們覺得做的還不錯的一個證明。網(wǎng)關(guān)自身的耗時,其實 99% 的請求都會在 14 毫秒內(nèi)被處理完畢。當(dāng)然你說平均值能不能進(jìn)一步降低,我覺得是可以的。但是你再進(jìn)一步降低的話,可能就涉及到 Node.js 本身事件驅(qū)動模型這樣一個調(diào)度的問題。

          小結(jié)

          1. 大規(guī)模服務(wù)不能只考慮自身性能,前置 / 后置鏈路都可能成為性能瓶頸。

          2. 前置 / 后置鏈路通常與公司基建、網(wǎng)絡(luò)架構(gòu)密切相關(guān),服務(wù)研發(fā)團(tuán)隊需要深刻理解。

          3. Node.js 受限于自身異步模型,很難精細(xì)化地控制、調(diào)度異步 IO,并非萬金油。

          高可用保障

          講完性能優(yōu)化,最后一個部分就是可用性保障,那么我們通常的服務(wù)怎么來做可用性保障?

          第一,不要出事故。服務(wù)的健壯性,你本身服務(wù)要足夠的健壯,這里有很多機(jī)制,包括灰度發(fā)布、熱更新、流量管理、限流、熔斷、防緩存擊穿,還有緩存容災(zāi)、特性開關(guān)、柔性降級……這些東西。

          第二,出事故了能感知到。事故永遠(yuǎn)是不可避免的,每天都會發(fā)生亂七八糟的各種事故,出了事故的時候你是要能夠感知到,并且能夠讓你的系統(tǒng)自修復(fù),或者說你自己人員上來修復(fù)。這就涉及到監(jiān)控告警系統(tǒng),還有像外部的撥測,用戶反饋監(jiān)控,社群里面的一些監(jiān)控。

          第三,能立刻修復(fù)事故。出了事故的時候,能夠有機(jī)制去立刻修復(fù),比如快速擴(kuò)容,當(dāng)然最好的是整個系統(tǒng)它能夠自愈。比如說有個節(jié)點(diǎn)它出問題了,你的系統(tǒng)可以自動剔除它,但如果做不到的話,你可以去做一些人工介入的故障隔離,還有多實例災(zāi)備切換、邏輯降級等。

          上圖是我們網(wǎng)關(guān)整體的架構(gòu),哪些地方容易出現(xiàn)問題?其實每一層都會出問題,所以每一層其實都要相應(yīng)的去做容災(zāi),比如 CDN 到 CLB 這一層,CLB 是不是有多個實例的災(zāi)備?像 CLB 到網(wǎng)關(guān)這一層,是不是網(wǎng)關(guān)也是有同樣的多實例,還有一些監(jiān)控的指標(biāo)。當(dāng)然這個篇幅就非常大了,所以我今天只講我們最核心業(yè)務(wù)層的容災(zāi)。

          核心業(yè)務(wù)層

          先講講我們核心業(yè)務(wù)層面臨的一些挑戰(zhàn)。

          1. 下游客戶業(yè)務(wù)隨時有突發(fā)大流量,要能抗住沖擊。因為我們是承載公有云流量的,大概有上萬的客戶他的服務(wù)是部署在這里的,我們永遠(yuǎn)不知道這些客戶什么時候會突然來一個秒殺活動,他可能也從來不給我們報備,這個客戶的流量可能隨時就會翻個幾千倍甚至幾萬倍,所以這時候我們要能扛住這樣一個沖擊。

          2. 網(wǎng)關(guān)本身依賴服務(wù)多,穩(wěn)定性差異大,要有足夠的自動容錯兜底機(jī)制。

          3. 能應(yīng)對多個可用區(qū)故障,需要流量調(diào)度、災(zāi)備、多地多活等機(jī)制。

          4. 我們能先于客戶發(fā)現(xiàn)問題,需要業(yè)務(wù)維度的監(jiān)控告警機(jī)制。

          核心業(yè)務(wù)層:應(yīng)對大流量沖擊

          那我們怎么樣去應(yīng)對一個大流量的沖擊?實際上對于一個系統(tǒng)來講,它其實是非常具有破壞性的,它有可能直接把你的緩存還有你的 DB 擊穿,導(dǎo)致你的 DB 直接就夯住了,CPU 被打滿。下圖是我們一次真實的例子,我也不是很排斥說出來。


          這是我們今年年初 1 月份的時候,有一個客戶他的流量突然翻了 100 多倍,你可以看到圖上它的量就突然提升,這造成一個什么問題?它的緩存都是冷的,也就是說訪問量突然提升 100 倍,這 100 倍的請求,可能都要去后臺讀它的一些數(shù)據(jù),導(dǎo)致直接把后臺數(shù)據(jù)庫的 CPU 打滿了,也導(dǎo)致這個災(zāi)難進(jìn)一步擴(kuò)散,擴(kuò)散到到所有用戶的數(shù)據(jù)都讀不出來了。

          后來我們就反思了一下這個是不是有問題的?對,是有問題。我們要做什么事情來防止這樣的問題出現(xiàn)的?

          1. 提升服務(wù)承載能力

          大流量來了,你自己本身要能扛得住,這個時候要去提升你整個服務(wù)快速擴(kuò)容的能力。我們的網(wǎng)關(guān)實際上當(dāng)時已經(jīng)是完全容器化的,所以這一點(diǎn)還好,它可以快速做到橫向擴(kuò)容,瞬間擴(kuò)出幾百核幾千核的資源,可以在幾分鐘之內(nèi)完成。

          其次,我們是使用了單 POD 多 Node 進(jìn)程,就是我們 1 個 POD 會帶有 8 核,每個核心跑一個進(jìn)程。這個在 Kubernetes 里面實際上是一個反模式,因為 Kubernetes 要求 POD 要盡量得小,然后里面就只跑 Node 一個進(jìn)程。但在工程實踐的時候,我們發(fā)現(xiàn)這樣跑雖然沒有問題,但是它擴(kuò)容速度非常慢,因為每次實例擴(kuò)出來,都是批量的。比如說我們內(nèi)部的容器系統(tǒng),只能說一次擴(kuò) 100 個實例,也就是說一批也就擴(kuò) 100 核,并且這 100 核都要分配內(nèi)部的虛擬 IP,可能會導(dǎo)致內(nèi)部的 IP 池被耗盡了。最后我們做了合并,起碼一個 POD 能擴(kuò)出 8 核的資源出來。

          1. 保證服務(wù)健壯性,不被打垮

          當(dāng)然,除了提升自己抗沖擊的能力以外,還要保證你的后端,保護(hù)好你后面的服務(wù)。

          比如說我們要保證服務(wù)的健壯性,在大量沖擊的時候不會被打垮。首先單個實例會做一個限頻限流,防止雪崩,流量限頻是說你一個實例最多可能只能承載 1000QPS 的流量,再多你這個實例就直接放棄掉,就不請求了,這樣可以防止你的整個后臺雪崩,不至于說一個 POD 崩了,然后其他 POD 請求又更多,把其他 POD 全部帶崩。

          其次,我們要做 DB 的旁路化,網(wǎng)關(guān)它讀的永遠(yuǎn)是緩存,緩存里面讀不到,那就是讀不到,它永遠(yuǎn)不會直接把請求請求到 DB 里面去。當(dāng)有數(shù)據(jù)寫入或者說數(shù)據(jù)變更的時候,后臺同學(xué)會先落 DB,然后再把 DB 的數(shù)據(jù)推送到緩存里面,大概就是下圖這樣一個邏輯,防止緩存擊穿問題。

          第三,服務(wù)降級機(jī)制。假設(shè)真的出現(xiàn)問題了,比如說你緩存也出問題了,我們可以做一些服務(wù)的降級。它可能有一些功能沒有了,比如說有些特殊的 HTTP 請求,響應(yīng)頭可能沒有了,但是它不會干擾你的主干邏輯,這個也是可以做的。

          核心業(yè)務(wù)層:應(yīng)對外部事故

          本身服務(wù)構(gòu)建狀態(tài)是沒有用的,依賴的外部組件服務(wù)也一定會出問題,而且它們的可用性說不定遠(yuǎn)遠(yuǎn)比你想象的要低,那要怎么做?

          首先,我們內(nèi)部是有一套集群控制系統(tǒng)的,我們內(nèi)部分成了主機(jī)群、VIP 集群和灰度集群這三個集群。每次發(fā)布的時候永遠(yuǎn)是會先發(fā)灰度集群,驗證一段時間之后才會全讓到其他集群上。這樣的集群隔離也給我們帶來另一個好處,一旦其中有一個集群出現(xiàn)了問題,比如說灰度集群的 DB 掛了,或者 DB 被寫滿了等其他的事故,我們可以很快速地把流量切換到主機(jī)群和 VIP 集群上面去,這得益于我們內(nèi)部其實有一套集群管理的快速切換機(jī)制。

          服務(wù)降級:容災(zāi)緩存

          其次,做容災(zāi)緩存,假設(shè)依賴的服務(wù)全掛,服務(wù)自動啟用容災(zāi)緩存,使用舊數(shù)據(jù)保證基本的可用性。

          年初我們有一次這樣的事故,整個機(jī)房停電,機(jī)房就相當(dāng)于消失了,導(dǎo)致后臺服務(wù)全部都沒有了,這種情況怎么做?這時候就只能是啟用緩存容災(zāi)。我們網(wǎng)關(guān)本地的緩存是永遠(yuǎn)不會主動清除的,因為你使用舊數(shù)據(jù)也比直接報錯要好,這時候我們就會使用一個舊數(shù)據(jù)來保證它的可用性。

          這個怎么理解呢?我們網(wǎng)關(guān)內(nèi)部的數(shù)據(jù)它永遠(yuǎn)不會被清理,它只會說通過 LRU 的形式被清理掉,比如說我的內(nèi)存里有可能會有很老的數(shù)據(jù),昨天或者前天的數(shù)據(jù),但是你在災(zāi)難發(fā)生的時候,即使是昨天還是前天數(shù)據(jù)它依然是有用的,它依然可以拿出來保證你最基本的可用性,下面是我們一個邏輯圖,大家可以了解。

          服務(wù)降級:跳過非核心鏈路

          你的服務(wù)有可能會降級,我剛提到我們網(wǎng)關(guān)有鑒權(quán)的功能,鑒權(quán)功能其實依賴我們騰訊內(nèi)部的一個組件。這樣一個組件,它其實也是不穩(wěn)定的,有時候會出問題,那么遇到這種問題怎么辦?鑒權(quán)都沒有辦法鑒權(quán)了。這個時候我們在一些場景允許的情況下,會直接把鑒權(quán)的邏輯給跳過,我們不鑒權(quán)了,先放過一段時間,總比說我直接拒絕掉,直接報錯這個請求要好得多。

          核心業(yè)務(wù)層:網(wǎng)關(guān)自身災(zāi)備、異地多活

          最后一點(diǎn)就是我剛剛提到的,因為我們網(wǎng)關(guān)做了服務(wù) SET 化改造、部署后,天然獲得了跨 AZ、跨地域熱切換的能力。簡單來講,只要全網(wǎng)還剩一個網(wǎng)關(guān)可用區(qū),業(yè)務(wù)流量就可以切換,網(wǎng)關(guān)的服務(wù)就不會宕機(jī),當(dāng)然切換現(xiàn)在還沒有做到完全自動化,因為涉及到跨地域的切換,這個是需要人工介入的,不過說實話我們還沒有遇到過這么大的災(zāi)難。

          做個小結(jié),我們做了多集群切換、緩存容災(zāi)、柔性降級這些事情之后可以達(dá)到怎樣一個效果:

          • 容許后臺最多 (N-1) 個集群長時間故障

          • 容許后臺全部集群短時間故障

          • 容許內(nèi)部 DNS 全網(wǎng)故障

          核心業(yè)務(wù)層:還有什么能做的?

          我們還有什么能做的?如果某天,容器平臺全網(wǎng)故障,怎么辦?其實也是我們現(xiàn)在構(gòu)思的一個東西,我們是不是可以做到一個異構(gòu)部署這樣一個形態(tài)。

          服務(wù)異構(gòu)部署,即使容器平臺全地域全可用區(qū)故障,也能切換到基于虛擬機(jī)的架構(gòu)上,這也是我們正在籌劃的一個事情。

          最后說完了容災(zāi),接下來說怎么做監(jiān)控告警?做監(jiān)控告警其實比較老生常談了,但是也可以在這里稍微掃個盲,我們的所有網(wǎng)關(guān),它會把自己所有的訪問日志推送到我們的 ES(elasticsearch)的集群上,然后我們會有一個專門的 TCB Alarm 這樣一個模塊,它會去定期的輪詢這樣的日志,去檢查這些日志里面有沒有一些異常,比如說某個用戶的流量突然高了,或者某個錯誤碼突然增多,它會把這樣的信息通過電話或者企業(yè)微信推送給我們。

          因為是基于 ES 的,所以監(jiān)控可以做得非常精細(xì),甚至可以做到感知到某個接口,今天的耗時比昨天要高超過 50%,那這個接口是不是今天做什么變更讓它變慢了?

          我們也可以做針對下游重點(diǎn)客戶、業(yè)務(wù)的一些監(jiān)控,比如說幾個省的健康碼,都可以做重點(diǎn)的監(jiān)控。

          總結(jié)

          其實我只是選取了整個 Node 服務(wù)里面非常小的兩個切面來講,性能優(yōu)化和高可用保障??赡芎茈y覆蓋到很全面,但是我想講的稍微深一點(diǎn),能夠讓大家有些足夠的益處。

          首先,服務(wù)核心優(yōu)化這里講了長連接和緩存機(jī)制,可能是大部分服務(wù)或多或少都會遇到的問題。然后,鏈路架構(gòu)優(yōu)化這里講了就近接入和 Set 化部署這樣一個機(jī)制。高可用保障我主要是介紹了核心業(yè)務(wù)層的一些高可用保障,包括應(yīng)對大流量沖擊,怎么做緩存容災(zāi),柔性降級,多可用區(qū)、多地域切換,監(jiān)控告警這些東西。

          最后我想就今天的演講做一個總結(jié)。

          第一,Node.js 服務(wù)與其它后臺服務(wù)并無二致,遵循同一套方法論。

          Node.js 服務(wù)本質(zhì)上也是做后臺開發(fā)的,與其它后臺服務(wù)并無二致,遵循同一套方法論。我今天的演講如果把 Node.js 改成 Golang 改成 Java,我就不站在這里了,可能我就去 Golang 的會上講,實際上是一樣的。

          第二,Node.js 足以承載核心大規(guī)模服務(wù),無須妄自菲薄。

          我們這套網(wǎng)關(guān)其實也現(xiàn)網(wǎng)驗證兩年了,它跟別的技術(shù)棧的這種后臺服務(wù)來講,其實并沒有太大的缺點(diǎn)。所以大家在拿 Node.js 做這種海量服務(wù)的時候,可以不用覺得 Node.js 好像只是個前端的小玩具,好像不是很適合這種成熟的業(yè)務(wù),成熟業(yè)務(wù)是不是還是用 Java 來寫,拿 C++ 來寫,其實是沒有必要的。

          當(dāng)然,如果你真的需要對你的 IO 調(diào)度非常精細(xì)的時候,那么你可能得選用 C++ 或者 Rust,這樣可以直接調(diào)度 IO 的方案。

          第三,前端處在技術(shù)的十字路口,不應(yīng)自我局限于“Web 前端”領(lǐng)域。

          最后一個也是我今天想提的,可能我講這么多,大家覺得我不是一個前端工程師對不對?但實際上我在公司內(nèi)部的職級確實是個前端工程師。我一直覺得前端它是站在一個技術(shù)的十字路口的,所以大家工作中也好,還是學(xué)習(xí)中也好,不用把自己局限在“Web 前端”這樣一個領(lǐng)域。這次 GMTC 大會也可以看到,前端現(xiàn)在也不只是大家傳統(tǒng)意義上的可能就是寫頁面這樣一個領(lǐng)域。

          這是一個當(dāng)年喬布斯演講用的一個圖,他說蘋果是站在技術(shù)和人文的十字路口,實際上前端也是站在很多技術(shù)的十字路口上。

          那么我的演講就到此結(jié)束,謝謝大家。

          嘉賓介紹:

          王偉嘉:騰訊云 CloudBase 前端負(fù)責(zé)人

          畢業(yè)于復(fù)旦大學(xué),現(xiàn)任騰訊云 CloudBase 前端負(fù)責(zé)人,Node.js Core Collaborator,騰訊 TC39 代表。目前在騰訊云 CloudBase 團(tuán)隊負(fù)責(zé)小程序·云開發(fā)、Webify 等公有云產(chǎn)品的核心設(shè)計和研發(fā),服務(wù)了下游數(shù)十萬開發(fā)者和用戶,對 Node.js 服務(wù)架構(gòu)、全棧開發(fā)、云原生開發(fā)、Serverless 有較豐富的經(jīng)驗,先后在阿里 D2、GMTC、騰訊 TWeb 等大會上發(fā)表過技術(shù)演講。?

          Node 社群


          我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。


          ???“分享、點(diǎn)贊在看” 支持一波??

          瀏覽 38
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美一区二区三区四区五区视频 | 伊人自拍 | 亚洲欧美久久翔田千里 | 日夲A级片网站 | 操逼做爱视频 |