Jack Jiang

          我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
          posts - 503, comments - 13, trackbacks - 0, articles - 1

          1、引言

          當(dāng)你在瀏覽器輸入 qq.com 按下回車鍵,到頁面呈現(xiàn)在你面前,整個(gè)過程發(fā)生了什么?我以前思考過這個(gè)問題,從最前面的瀏覽器到最后的 db 都梳理的一遍,觸發(fā)了一次技術(shù)頓悟,將很多散落的知識(shí)點(diǎn)貫通起來了。

          本文將拋棄千篇一律的計(jì)網(wǎng)知識(shí)理論,從現(xiàn)實(shí)的互聯(lián)網(wǎng)技術(shù)實(shí)踐角度,一步步為你分享一次網(wǎng)絡(luò)請(qǐng)求背后的技術(shù)秘密。

           

          2、系列文章

          本文是系列文章中的第 17篇,本系列文章的大綱如下:

          3、 IP、DNS 和 CDN

          如果面試時(shí)問你「局域網(wǎng) IP 有哪些 IP 段」,你怎么答?

          3.1為什么不是每個(gè)設(shè)備一個(gè)公網(wǎng) IP?

          先說個(gè) QQ 的小故事。QQ 剛開發(fā)時(shí)也沒想到 QQ 會(huì)發(fā)展成中國(guó)互聯(lián)網(wǎng)基礎(chǔ)設(shè)施,就用4字節(jié)整形表示 QQ 號(hào)了。早期內(nèi)部的一些項(xiàng)目有用 int 表示 QQ 號(hào),能表示的最大值是2^31-1,即21億多。在 QQ 號(hào)發(fā)放近20億時(shí),即通搞了個(gè)22億 QQ 號(hào)的專項(xiàng),通知每個(gè)項(xiàng)目檢查修改,使用 unsigned int 表示 QQ 號(hào),以支持21億以上的 QQ 號(hào)。

          可以看出在底層和協(xié)議設(shè)計(jì)中,字段的擴(kuò)大是非常麻煩的。IP 地址也有類似問題。

          目前廣泛使用的是 IPv4,一個(gè) IP 地址4個(gè)字節(jié),理論上共有2^32個(gè) IP 地址,接近43億。這個(gè)數(shù)量還不到人均一個(gè),遠(yuǎn)遠(yuǎn)不夠,自然也不能每個(gè)設(shè)備一個(gè)公網(wǎng) IP 了,所以 Internet 規(guī)定了 IPv4 地址空間的一部分供專用地址使用,這些地址永遠(yuǎn)不會(huì)被當(dāng)做公用地址來分配,局域網(wǎng)內(nèi)部 IP 就是使用這些專用地址。

          常見專用地址有:10.x.x.x,172.16-31.x.x,192.168.x.x,另外127.0.0.1表示本地回環(huán)地址,代表設(shè)備的本地虛擬接口。

          了解這個(gè)后,如果你發(fā)現(xiàn)你在公司的 IP 是192.168.0.100,在家里的IP也是192.168.0.100時(shí),就不會(huì)詫異了。局域網(wǎng)內(nèi)部 IP 只用于局域網(wǎng)內(nèi)部通訊,如果要連接廣域網(wǎng),還要用到 NAT(網(wǎng)絡(luò)地址轉(zhuǎn)換)技術(shù)(詳見《通俗易懂:快速理解P2P技術(shù)中的NAT穿透原理》)。

          NAT 常用于局域網(wǎng)內(nèi)部 IP 和局域網(wǎng)分配的公網(wǎng) IP 之間進(jìn)行轉(zhuǎn)換,使用最多的是端口多路復(fù)用(PAT)方式,簡(jiǎn)單的描述就是,你在局域網(wǎng)內(nèi)訪問 qq 時(shí),路由器會(huì)記錄你的內(nèi)網(wǎng) IP 和端口(假設(shè)是192.168.0.100:12345),用路由器的公網(wǎng)IP和一個(gè)未使用的端口向公網(wǎng)發(fā)網(wǎng)絡(luò)包(假設(shè)是202.96.134.133:23456),路由器還會(huì)把TCP~202.96.134.133:23456~192.168.0.100:12345配對(duì)保存起來。當(dāng) qq 的響應(yīng)發(fā)到202.96.134.133:23456后,路由器通過查找配對(duì)表就知道是發(fā)給192.168.0.100:12345。

          PS:相關(guān)資料可進(jìn)一步查閱:

          1. 腦殘式網(wǎng)絡(luò)編程入門(六):什么是公網(wǎng)IP和內(nèi)網(wǎng)IP?NAT轉(zhuǎn)換又是什么鬼?
          2. P2P技術(shù)詳解(一):NAT詳解——詳細(xì)原理、P2P簡(jiǎn)介
          3. IPv6技術(shù)詳解:基本概念、應(yīng)用現(xiàn)狀、技術(shù)實(shí)踐(上篇)

          3.2DNS 有話說

          搞網(wǎng)絡(luò)管理的同學(xué)對(duì) DNS 比較熟悉,程序員也需要了解,不管是前端還是后端。

          IP 地址不好記,于是就有了域名。瀏覽器訪問 qq.com 時(shí),會(huì)先做一次域名解析,把 qq.com 這個(gè)域名解析成 IP 地址,然后才能發(fā)出 IP 包。

          在 windows 和 linux 下解析域名前,會(huì)先從本地 hosts 文件里查找網(wǎng)址映射關(guān)系,如果有,就先調(diào)用這個(gè) IP 地址映射,完成域名解析。早期做 Web 開發(fā)時(shí)會(huì)用這種方式來切換開發(fā)環(huán)境、測(cè)試環(huán)境、預(yù)發(fā)布環(huán)境和正式環(huán)境。

          比如你在騰訊云注冊(cè)了一個(gè)個(gè)人用的域名,一般會(huì)直接用騰訊云的 DNS 服務(wù)器來管理該域名的解析。對(duì)于門戶網(wǎng)站,則會(huì)自己搭建 DNS 服務(wù)器來管理域名。自建 DNS 服務(wù)器既方便管理,又能提高安全等級(jí),防范 DDOS 域名攻擊。

          如果域名的訪問量比較大,可以讓域名對(duì)應(yīng)多個(gè) IP 地址,操作系統(tǒng)會(huì)隨機(jī)選擇其中一個(gè),這是早期的 Web 負(fù)載均衡方式。但因?yàn)橛行?DNS Server 不按 TTL 指示緩存,導(dǎo)致 DNS 變更生效時(shí)間最長(zhǎng)可達(dá)到24~48小時(shí)。一旦某個(gè) IP 的機(jī)器故障,而 DNS 又不能立即刷新,會(huì)讓部分用戶服務(wù)不可用。于是就有了用 LVS/nginx 來動(dòng)態(tài)負(fù)載均衡的方式,LVS 的負(fù)載均衡基于 IP 層,nginx 則是用 HTTP 層的反向代理機(jī)制。

          此外還要考慮到電信/聯(lián)通/移動(dòng)三大運(yùn)營(yíng)商跨網(wǎng)的問題。運(yùn)營(yíng)商之間的通訊帶寬有限,如果你的服務(wù)器在電信機(jī)房,那么聯(lián)通用戶訪問就會(huì)比較慢。所以就有了多線機(jī)房的出現(xiàn)。多線機(jī)房實(shí)際是一個(gè)機(jī)房有電信、聯(lián)通、移動(dòng)等多條線路接入。通過多線機(jī)房?jī)?nèi)部路由器設(shè)置,及 BGP 自動(dòng)路由的分析,實(shí)現(xiàn)電信用戶訪問電信線路,聯(lián)通用戶訪問聯(lián)通線路,這樣實(shí)現(xiàn)電信聯(lián)通均可以快速訪問 。

          市面上的各種云(比如騰訊云、阿里云)的 CLB 選用 BGP 多線,就可實(shí)現(xiàn)電信/聯(lián)通/移動(dòng)多網(wǎng)統(tǒng)一接入和自動(dòng)負(fù)載均衡。

          PS:相關(guān)資料可進(jìn)一步查閱:

          1. 不為人知的網(wǎng)絡(luò)編程(九):理論聯(lián)系實(shí)際,全方位深入理解DNS
          2. 全面了解移動(dòng)端DNS域名劫持等雜癥:原理、根源、HttpDNS解決方案等
          3. 美圖App的移動(dòng)端DNS優(yōu)化實(shí)踐:HTTPS請(qǐng)求耗時(shí)減小近半
          4. 百度APP移動(dòng)端網(wǎng)絡(luò)深度優(yōu)化實(shí)踐分享(一):DNS優(yōu)化篇

          3.3CDN 登場(chǎng)

          為了給用戶提供更快的訪問速度,人們發(fā)明了 CDN(Content Delivery Network,內(nèi)容分發(fā)網(wǎng)絡(luò))。簡(jiǎn)單的說就是,一個(gè)域名對(duì)應(yīng)有多個(gè) IP,這些 IP 分布在全國(guó)各地,用戶訪問域名時(shí),DNS 服務(wù)器根據(jù)用戶的來源 IP,返回一個(gè)就近的 IP 給用戶,從而實(shí)現(xiàn)更快的訪問速度。

          從上面的描述可以知道,CDN 要求各節(jié)點(diǎn)的內(nèi)容是一致的,這樣才能讓各地用戶訪問到一致的內(nèi)容,所以 CDN 主要用于 Web 靜態(tài)資源的分發(fā)和加速。

          Web 性能優(yōu)化方案一般會(huì)有一條動(dòng)靜分離,即把靜態(tài)資源使用的域名和動(dòng)態(tài)腳本的域名分開,有了 CDN 后,一般會(huì)把靜態(tài)資源的域名托管給 CDN 以提高更快的訪問速度和更低的成本。

          現(xiàn)在也有動(dòng)態(tài) CDN,針對(duì)接口這種請(qǐng)求也能做就近接入,但原理是轉(zhuǎn)入云廠商的內(nèi)部鏈路做網(wǎng)絡(luò)加速,一般不把數(shù)據(jù)緩存在邊緣節(jié)點(diǎn)上。

          4、TCP、消息分包和協(xié)議設(shè)計(jì)

          一次網(wǎng)絡(luò)請(qǐng)求經(jīng)過 DNS 解析知道了目的 IP,現(xiàn)在就要發(fā)出網(wǎng)絡(luò)包了。

          4.1TCP 是一種流式協(xié)議

          講網(wǎng)絡(luò)編程的教科書一般都會(huì)對(duì) TCP 的可靠傳輸做詳細(xì)說明,但對(duì)于 TCP 是一種流式協(xié)議講解的不多,但這背后隱藏著很重要的一個(gè)知識(shí)點(diǎn)。先做個(gè)名詞定義方便交流,這里的 “消息”是指應(yīng)用層的一個(gè)完整的協(xié)議包。

          流式協(xié)議的特點(diǎn)是什么?就像流水連續(xù)不斷那樣,消息之間沒有邊界。例如 send 了3條消息,分別是100字節(jié)、50字節(jié)、80字節(jié),recv 時(shí)可能收到的是230字節(jié),就是說一次 recv 收到了3條消息,需要應(yīng)用邏輯自己對(duì) recv 到的數(shù)據(jù)進(jìn)行分析,得出完整的消息。

          能一次 recv 到多個(gè)消息,也可能一次 recv 到一個(gè)半消息或半個(gè)消息,都是有可能的,這就是流式協(xié)議的特點(diǎn)。有的文章講的粘包也是這個(gè)概念。

          4.2消息分包

          既然 TCP 是一種流式協(xié)議,需要應(yīng)用層自己來分析出完整的消息,那有哪些方式來確定一個(gè)完整消息呢?這個(gè)就是應(yīng)用層通訊協(xié)議設(shè)計(jì)的工作了。

          先看看最常見的 HTTP 協(xié)議是如何來分包的。HTTP 協(xié)議是一種文本協(xié)議(非二進(jìn)制協(xié)議),用 \r\n\r\n 來分割消息頭和消息體,HTTP 請(qǐng)求的消息頭中有 Content-Length 來告知消息體有多大,如果沒有該字段就表示無消息體,GET 請(qǐng)求大多是這樣。HTTP 響應(yīng)的消息頭中,或者有 Content-Length,或者有 Transfer-Encoding: chunked 告知以 chunk 模式分析消息體。

          HTTP 用 \r\n\r\n 來分割消息頭和消息體,這種用特定字符“/”字符串來分割或分包的方式,還有不少協(xié)議用到。例如 FTP/SMTP/POP3 都是用 \n 來作為一個(gè)命令結(jié)束的標(biāo)志。

          這種消息分包的方式,需要應(yīng)用層去掃描已 recv 到的數(shù)據(jù),性能上還不夠高效,代碼不嚴(yán)謹(jǐn)?shù)倪€容易被攻擊。在需要自定義協(xié)議的項(xiàng)目中,不少選擇用二進(jìn)制協(xié)議,解析高效,安全性更好些。

          最簡(jiǎn)單的二進(jìn)制協(xié)議分包方式是消息的頭4個(gè)字節(jié)表示消息的總長(zhǎng)度。這種方式還需要對(duì)最大消息長(zhǎng)度做個(gè)限制,例如 64K 或 1024K 大小,避免超大數(shù)據(jù)包對(duì)接收方緩沖區(qū)的破壞。更進(jìn)一步的,可以加入簡(jiǎn)單校驗(yàn)方法。例如消息頭1個(gè)字節(jié)固定式0x2,消息的最后1個(gè)字節(jié)固定式0x3,消息總長(zhǎng)度放在第2~5字節(jié)。這樣收到完整消息后,如果頭尾不是0x2和0x3,就直接異常處理。

          4.3協(xié)議設(shè)計(jì)

          消息分包是協(xié)議設(shè)計(jì)的一個(gè)工作,協(xié)議設(shè)計(jì)的話題還不少,這里以 HTTP 協(xié)議為例,簡(jiǎn)要的說說里面設(shè)計(jì)的點(diǎn),自己設(shè)計(jì)的協(xié)議也可以對(duì)照著有選擇的使用,原理是共通的。

          具體是:

          • 1)由消息頭+消息體組成:空行分割 HTTP head 和 body,HTTP 頭的每一行以 \r\n 結(jié)尾,空行就是 \r\n\r\n ;
          • 2)消息分包:如上所述,HTTP 用 Content-Length 和 Transfer-Encodeing 來分包;
          • 3)消息壓縮:請(qǐng)求中有 Accept-Encoding 字段,響應(yīng)中用 Content-Encoding 字段表明壓縮方式,一般采用 gzip 壓縮;
          • 4)消息加密:https (SSL: Secure Socket Layer);
          • 5)消息 ID:URL 就是消息 ID ;
          • 6)響應(yīng)的狀態(tài)碼:第一個(gè)數(shù)字定義了響應(yīng)的類別:   1xx:指示信息--表示請(qǐng)求已接收,繼續(xù)處理  2xx:成功--表示請(qǐng)求已被成功接收、理解、接受  3xx:重定向--要完成請(qǐng)求必須進(jìn)行更進(jìn)一步的操作  4xx:客戶端錯(cuò)誤--請(qǐng)求有語法錯(cuò)誤或請(qǐng)求無法實(shí)現(xiàn)  5xx:服務(wù)器端錯(cuò)誤--服務(wù)器未能實(shí)現(xiàn)合法的請(qǐng)求
          • 7)協(xié)議版本號(hào):HTTP/1.1中的1.1就是 HTTP 1.1 版本;
          • 8)長(zhǎng)連接:請(qǐng)求中 Connection: keep-alive 表示希望服務(wù)器保持連接,減少 TCP 連接的開銷;
          • 9)字符集:Content-Type 字段表明了字符集,例如: Content-Type: text/html; charset=gb2312;
          • 10)字符轉(zhuǎn)義:URL 中的參數(shù)需要做 URL 轉(zhuǎn)義處理,例如 http://xx.com/do?name=t%2F%3F%23%3Daa 表示 name 為 t/?#=aa。

          在我們自己設(shè)計(jì)協(xié)議時(shí),可以有選擇的使用:

          • 1)如果消息比較大,可以采用支持壓縮;
          • 2)如果要兼容多個(gè)版本的協(xié)議,那版本號(hào)必不可少;
          • 3)如果采用二進(jìn)制協(xié)議,字符集和字符轉(zhuǎn)義的用處不大。

          4.4HTTP 協(xié)議和二進(jìn)制協(xié)議的對(duì)比

          HTTP 協(xié)議是一種文本協(xié)議,也是一種 Name-Based 協(xié)議,就從這兩方面來說。

          文本協(xié)議 vs 二進(jìn)制協(xié)議。

          文本協(xié)議的特點(diǎn):

          • 1)便于人;
          • 2)易于閱讀、理解、調(diào)試、構(gòu)造;
          • 3)解析復(fù)雜、冗余多;
          • 4)需要考慮字符轉(zhuǎn)義。

          二進(jìn)制協(xié)議的特點(diǎn):

          • 1)便于機(jī)器。

          Name-Based vs Position-Based。

          Name-Based 協(xié)議的特點(diǎn):

          • 1)協(xié)議字段都用 Name 標(biāo)識(shí);
          • 2)協(xié)議字段與位置無關(guān);
          • 3)協(xié)議字段可缺省;
          • 4)新增協(xié)議字段比較方便;
          • 5)解析復(fù)雜;
          • 6)需要考慮字符轉(zhuǎn)義。

          Position-Based 協(xié)議的特點(diǎn):

          • 1)每個(gè)協(xié)議字段都有特定的位置;
          • 2)新增協(xié)議字段需要做好協(xié)議版本管理(protobuf 這類就挺好,詳見《Protobuf從入門到精通,一篇就夠!》);
          • 3)解析更高效。

          5、CGI 和 FastCGI

          消息經(jīng)過網(wǎng)絡(luò)傳輸,到達(dá)了服務(wù)器端,最常見的服務(wù)器是 Web 服務(wù)器,做 PHP 的同學(xué)都知道 FastCGI 模式的 PHP 比普通 PHP 更高效,其中的原理是什么呢?

          5.1古老但常見的 CGI

          Web 服務(wù)器能解析 HTTP 請(qǐng)求,返回靜態(tài)資源(HTML 頁、圖片等),但要輸出動(dòng)態(tài)內(nèi)容,必須得 PHP/C#/Ruby/Java/Python/C/C++ 這些外部程序來實(shí)現(xiàn)。

          早期有個(gè)技術(shù)叫 CGI(Common Gateway Interface,通用網(wǎng)關(guān)接口),是用于 Web 服務(wù)器和外部程序之間傳輸數(shù)據(jù)的一種標(biāo)準(zhǔn)。

          一個(gè)簡(jiǎn)單的 CGI 程序(C++ 語言)如下:

          #include <stdio.h>

          #include <stdlib.h>

          int main()

          {

                 printf("Content-type: text/html\r\n\r\n");

                 printf("your name is:%s\n", getenv("QUERY_STRING"));

                 return 0;

          }

          瀏覽器訪問這個(gè) CGI 程序,就會(huì)顯示:your name is:name=xxx 。

          CGI 規(guī)定了 Web 服務(wù)器如何和 CGI 程序之間傳輸數(shù)據(jù)。

          具體過程大體是這樣:

          • 1)Web 服務(wù)器收到的請(qǐng)求信息后,啟動(dòng) CGI 程序(apache 是 fork 進(jìn)程 exec CGI 程序);
          • 2)Web 服務(wù)器通過環(huán)境變量和標(biāo)準(zhǔn)輸入把請(qǐng)求信息傳遞給 CGI 程序;
          • 3)CGI 程序執(zhí)行業(yè)務(wù)邏輯后,通過標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤把響應(yīng)數(shù)據(jù)返回給 Web 服務(wù)器,CGI 程序 exit;
          • 4)Web 服務(wù)器再組織成 HTTP 響應(yīng)包發(fā)給瀏覽器。

          在上面的例子中,第一行 printf 是輸出 HTTP 頭(還記得 HTTP Header 和 Body 是用 \r\n\r\n 分割的么?),getenv("QUERY_STRING")是從環(huán)境變量獲取 URL,printf 是通過標(biāo)準(zhǔn)輸出返回內(nèi)容。

          Web 服務(wù)器會(huì)把哪些信息通過環(huán)境變量傳遞給 CGI 程序?

          常用的有這些:

          • 1)CONTENT_LENGTH :向標(biāo)準(zhǔn)輸入發(fā)送的數(shù)據(jù)的字節(jié)數(shù)(POST);
          • 2)QUERY_STRING:實(shí)際存放發(fā)送給 CGI 程序的數(shù)據(jù)(GET);
          • 3)REQUEST_METHOD:傳送數(shù)據(jù)所用的 CGI 方法(GET或POST);
          • 4)HTTP_COOKIE:cookie 值;
          • 5)REMOTE_ADDR:用戶 IP;
          • 6)SCRIPT_NAME:請(qǐng)求的 CGI。

          可以看到 CGI 只是一種標(biāo)準(zhǔn),可以用任何一種語言編寫 CGI 程序,只要這種語言具有標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和環(huán)境變量,比如:C/C++,perl,PHP、ruby。按照 CGI 標(biāo)準(zhǔn)要求,就能和 Web 服務(wù)器交互起來。

          5.2FastCGI 應(yīng)運(yùn)而生

          CGI 是通過環(huán)境變量/標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出/標(biāo)準(zhǔn)錯(cuò)誤來傳輸數(shù)據(jù),運(yùn)行性能比較低。

          主要有兩點(diǎn):

          • 1)每個(gè)請(qǐng)求都需要 Web 服務(wù)器去 fork 出 CGI 程序,頻繁 fork 進(jìn)程比較耗時(shí);
          • 2)CGI 程序每次都是從頭運(yùn)行,讀配置、連接其他服務(wù)都得重新來,也比較耗時(shí)。

          FastCGI 是對(duì) CGI 的改進(jìn)。

          FastCGI 模式下,Web 服務(wù)器和 FastCGI 程序傳輸數(shù)據(jù)的過程大體是:

          • 1)Web 服務(wù)器收到的請(qǐng)求信息后,按 FastCGI 協(xié)議把請(qǐng)求信息通過 socket 發(fā)給 FastCGI 程序;
          • 2)FastCGI 程序執(zhí)行業(yè)務(wù)邏輯后,通過 socket 把響應(yīng)數(shù)據(jù)返回給 Web 服務(wù)器,F(xiàn)astCGI 程序不 exit;
          • 3)Web 服務(wù)器再組織成 HTTP 響應(yīng)包發(fā)給瀏覽器。

          對(duì)比 CGI 的通過,可以發(fā)現(xiàn)主要是少了每次 fork 的過程,并且用 socket 來傳輸數(shù)據(jù),這是 FastCGI 接口更高效的原因。

          FastCGI 有這些特點(diǎn):

          • 1)FastCGI 程序常駐內(nèi)存,啟動(dòng)后可以反復(fù)處理請(qǐng)求;
          • 2)FastCGI 就是進(jìn)程池/線程池模型的通用同步服務(wù)器框架。

          FastCGI 程序處理請(qǐng)求后不會(huì)退出,可以反復(fù)處理請(qǐng)求,那么在啟動(dòng)后就把配置解析、與其他后臺(tái)的連接建立好,不用每次請(qǐng)求時(shí)搞一邊,自然更快了。

          至于這個(gè) FastCGI 內(nèi)部如何實(shí)現(xiàn)進(jìn)城池/線程池,就是 FastCGI 進(jìn)程管理器(FastCGI 引擎)的事情了。C/C++ FastCGI 常用 apache 的 mod_fastcgi 模塊,PHP 常用 spawn-fcgi 和 PHP-FPM。

          5.3nginx 的反向代理

          現(xiàn)在更多是用 nginx 的反向代理功能,把 HTTP 請(qǐng)求轉(zhuǎn)發(fā)到后端的 trpc 服務(wù)直接處理。這里的 trpc 服務(wù)就有點(diǎn) FastCGI 的感覺,但不用與 nginx 部署在一起了。

          6、服務(wù)器后端架構(gòu)模型

          上節(jié)講到 Web 服務(wù)器和 CGI/FastCGI 能動(dòng)態(tài)輸出內(nèi)容,從而提供更強(qiáng)大的業(yè)務(wù)處理能力。Web 服務(wù)器這種架構(gòu),我稱之為 Web 模式,與之相對(duì)的是 Svr 模式。Web 模式和 Svr 模式是互聯(lián)網(wǎng)項(xiàng)目的后臺(tái)最常見的兩種模式。先介紹幾個(gè)概念。

          6.1同步通訊 vs 異步通訊

          同步通訊是指在一個(gè)連接中,一個(gè)請(qǐng)求的應(yīng)答沒回來前,不能發(fā)送下一個(gè)請(qǐng)求,整個(gè)通訊過程是:請(qǐng)求1-應(yīng)答1-請(qǐng)求2-應(yīng)答2…… 這種。異步通訊與同步通訊相反,在一個(gè)連接中,可以隨意發(fā)送請(qǐng)求,而且收到應(yīng)答的順序可能與發(fā)送請(qǐng)求的順序不一致。

          從描述上就能理解,同步通訊的通訊性能比異步低,但好處是簡(jiǎn)單,不用考慮亂序應(yīng)答的復(fù)雜情況。

          6.2同步邏輯 vs 異步邏輯

          同步邏輯是指在代碼中遇到需要等待的調(diào)用時(shí)(例如向數(shù)據(jù)庫查詢數(shù)據(jù)),阻塞著,一直等待調(diào)用完成。異步邏輯則是不阻塞,繼續(xù)執(zhí)行后續(xù)代碼。

          我們常見的文件 IO 接口 read/write,網(wǎng)絡(luò) IO 接口 send/recv 默認(rèn)都是同步的,需要執(zhí)行特別的設(shè)置 API 才能變成非阻塞的。同步邏輯符合人腦的思維模式,寫異步邏輯需要處理各種非阻塞和異常情況,極其挑戰(zhàn)智力,就算采用有限狀態(tài)機(jī),也是件很具挑戰(zhàn)的工作。

          6.3無狀態(tài) vs 有狀態(tài)

          CGI/FastCGI 每次執(zhí)行時(shí),會(huì)從數(shù)據(jù)層(db 或數(shù)據(jù) cache)獲得數(shù)據(jù),修改后再寫回到數(shù)據(jù)層,也就是說 CGI/FastCGI 并不會(huì)緩存數(shù)據(jù)。這就是無狀態(tài)。

          無狀態(tài)的架構(gòu)中,請(qǐng)求是這臺(tái) Web 服務(wù)器處理,還是那臺(tái)處理,都沒有區(qū)別,因?yàn)閿?shù)據(jù)都是從數(shù)據(jù)層獲得的。這種架構(gòu)的擴(kuò)容非常方便,但需注意,要防范一個(gè)請(qǐng)求同時(shí)多并發(fā)時(shí),可能出現(xiàn)的數(shù)據(jù)不一致的漏洞,即要做防并發(fā)處理。

          有狀態(tài)是與無狀態(tài)相對(duì)的概念,是指服務(wù)器中緩存了數(shù)據(jù)。這種架構(gòu)中,因?yàn)椴恍枰磸?fù)的從數(shù)據(jù)層取數(shù)據(jù),性能會(huì)高很多,但因?yàn)榉?wù)器緩存了數(shù)據(jù),為了保持?jǐn)?shù)據(jù)一致性,只能把該數(shù)據(jù)的請(qǐng)求都分發(fā)到這臺(tái)服務(wù)器來處理。對(duì)于游戲來說,每個(gè)區(qū)的用戶數(shù)據(jù)是獨(dú)立的,對(duì)交互的實(shí)時(shí)性要求高,采用有狀態(tài)的架構(gòu)正好合適。

          6.4Web 模式 vs Svr 模式

          早期的瀏覽器如果要實(shí)現(xiàn)在線聊天,需要瀏覽器定時(shí)請(qǐng)求服務(wù)器獲取聊天信息,Web 服務(wù)器無法主動(dòng)給客戶端推送消息。到 WebSocket 出來后才具備實(shí)時(shí)推送的能力。

          Web 模式業(yè)務(wù)一般有這些特點(diǎn):

          • 1)是請(qǐng)求-應(yīng)答式:即先客戶端請(qǐng)求,才會(huì)有服務(wù)器應(yīng)答(少數(shù)場(chǎng)景可借助 WebSocket 主動(dòng)推送);
          • 2)是同步通訊:一個(gè)連接里,只有收到應(yīng)答后才能發(fā)下一個(gè)請(qǐng)求(HTTP2 可多路復(fù)用);
          • 3)是同步邏輯:Web 模式較少采用異步邏輯;
          • 4)是無狀態(tài)架構(gòu):CGI/FastCGI 每次從數(shù)據(jù)層獲取數(shù)據(jù),修改后再寫回到數(shù)據(jù)層。

          Svr 模式就是與之相對(duì)的,客戶端和服務(wù)器之間采用長(zhǎng)連接,客戶端的請(qǐng)求不一定會(huì)有應(yīng)答,服務(wù)器還可以主動(dòng)推送消息到客戶端,通訊也不限定是同步的,客戶端可以不斷的發(fā)送請(qǐng)求,服務(wù)器的應(yīng)答甚至可能與請(qǐng)求的順序不一致。

          Svr 模式相對(duì) Web 模式來說,通訊性能更強(qiáng),因?yàn)椴捎昧碎L(zhǎng)連接和異步通訊,還能主動(dòng)推送消息,這是優(yōu)勢(shì)。但也因?yàn)椴捎昧碎L(zhǎng)連接和異步通訊,對(duì)客戶端開發(fā)的要求就更高些,需要處理好斷線重連和支持響應(yīng)亂序。

          Web 模式因?yàn)槟J胶?jiǎn)單,Web 服務(wù)器自己實(shí)現(xiàn)了 HTTP 協(xié)議處理和 FastCGI 進(jìn)程管理等通用操作,F(xiàn)astCGI 這些外部程序只需要處理業(yè)務(wù)邏輯就行,降低了很多門檻。而且因?yàn)槭菬o狀態(tài)的,擴(kuò)容非常方便,直接加機(jī)器就能搞定,這個(gè)平滑擴(kuò)容的優(yōu)勢(shì)在 Web 時(shí)代的作用非常大——搞性能優(yōu)化、架構(gòu)優(yōu)化的時(shí)間成本比較大,而且不可控,加硬件就能快速抗住,是個(gè)好的方案。

          7、數(shù)據(jù)層的演進(jìn)

          我們用一個(gè)做手游的故事來聊聊數(shù)據(jù)層不斷優(yōu)化提升的演進(jìn)過程。

          7.1簡(jiǎn)單設(shè)計(jì)

          有一天,老板突然說做個(gè)山寨版的糖果傳奇手游,你接到任務(wù)后,分析出游戲的交互頻率不大,都是點(diǎn)查詢,用 MySQL 能簡(jiǎn)單搞定。建個(gè)表,設(shè)好主鍵和索引,你輕松搞定數(shù)據(jù)庫設(shè)計(jì),愜意的泡了杯茶邊喝邊敲代碼。

          這里說的“點(diǎn)查詢”,是指基于指定主鍵的查詢,例如查詢指定用戶的信息,因?yàn)槭腔谥付ㄖ麈I,查詢結(jié)果有限且較少,點(diǎn)查詢的效率非常高。另一種叫“面查詢”,是基于主鍵或索引的范圍查詢,例如查詢昨天所有的訂單,這種查詢雖然有主鍵或索引,但結(jié)果數(shù)量不確定,有時(shí)處理不好時(shí)會(huì)出現(xiàn)嚴(yán)重性能問題。

          游戲刪檔內(nèi)測(cè)上線了,用戶數(shù)不多,請(qǐng)求的響應(yīng)也很及時(shí),老板拍了拍你的肩膀。

          7.2數(shù)據(jù)庫調(diào)優(yōu)

          游戲上線反響不錯(cuò),精美的畫面給了玩家不少驚喜,更多玩家蜂擁而入,你從監(jiān)控上發(fā)現(xiàn) MySQL 的壓力有點(diǎn)大,當(dāng)初只是對(duì)數(shù)據(jù)庫表結(jié)構(gòu)做了設(shè)計(jì),現(xiàn)在你開始 review 數(shù)據(jù)庫優(yōu)化了:修改 MySQL 參數(shù)加大 InnoDB 的 cache,不使用事務(wù)提交。

          做了這些優(yōu)化后,db 性能提升明顯,整個(gè)系統(tǒng)跑得很歡,你又愜意的去泡茶了。

          7.3分庫分表

          你們游戲山寨得比較牛叉,用戶持續(xù)增加,作為有風(fēng)險(xiǎn)意識(shí)的你,肯定不會(huì)等到系統(tǒng)告警了才去優(yōu)化,于是你在想更大訪問量時(shí)怎么辦?

          單臺(tái) db 的性能有極限,必須有擴(kuò)展到多臺(tái) db 的能力,于是你重新修改了數(shù)據(jù)庫表結(jié)構(gòu)和后臺(tái)代碼,把主鍵按規(guī)則做了分庫分表,目前用戶增長(zhǎng)迅猛,假定單臺(tái) db 存放500萬用戶,最終可能有上億用戶,那么可能有20臺(tái) DB,于是你分了32個(gè)庫,每個(gè)庫里有32張表,共1024張表。

          初始時(shí)這1024張表都在一臺(tái) db 上,當(dāng)用戶數(shù)增加時(shí),分裂成2臺(tái)、4臺(tái)、8臺(tái)、16臺(tái)。涉及好分庫分表策略后,db 壓力能通過擴(kuò)容來解決,你放心了。

          PS:雖然使用 TDSQL 可以隱含的幫你分表,但有追求的你還是需要了解原始的分庫分表做法。

          7.4關(guān)于讀寫分離

          有不少介紹 MySQL 讀寫分離已提升 MySQL 并發(fā)性能的文章,在游戲項(xiàng)目中用得比較少,主要是讀寫比例的原因。像網(wǎng)站那種讀多寫少的應(yīng)用場(chǎng)景可以采用讀寫分離,而游戲的讀和寫差不多多,讀寫分離的用處不大;而且用戶可能是海量的,分多臺(tái) db 是常事,如果分庫后再搞讀寫分離,整個(gè) db 就過于復(fù)雜了。

          MySQL 讀寫分離是基于 MySQL 主從復(fù)制功能的,現(xiàn)在的項(xiàng)目實(shí)用的 db 基本都是一主多備。如果項(xiàng)目有 OLAP 需求要直接查詢 MySQL ,往往也是在 slave 上查詢,不直接操作 master,避免低效查詢降低 master 性能影響業(yè)務(wù)。這種做法也是 OLDP(On-Line Transaction Processing,聯(lián)機(jī)事務(wù)處理)和 OLAP(On-Line Analytical Processing,聯(lián)機(jī)分析處理)分離的常見做法。

          7.5緩存

          有了分庫分表來平滑擴(kuò)容,項(xiàng)目安穩(wěn)了較長(zhǎng)一段時(shí)間,直到某一天,運(yùn)維說db機(jī)器增長(zhǎng)比較快,4個(gè)月就增加到了64臺(tái)(master+slave),希望后臺(tái)能提升單臺(tái)db的性能,以應(yīng)對(duì)后續(xù)的業(yè)務(wù)增長(zhǎng)。

          OK,你祭出你留的后手——Redis,麻利的操起機(jī)械鍵盤,咔噠咔噠的改起后臺(tái)的代碼,加入緩存邏輯:讀的時(shí)候從 Redis 讀,如果沒有就從 MySQL 查詢并寫入 Redis。寫的時(shí)候同時(shí)寫入 MySQL 和 Redis。

          好吧,你終于使用了 NoSQL。搞定這個(gè)問題后,成本下降了,項(xiàng)目收入可觀,happy!

          7.6為什么 Redis 性能比 MySQL 高?

          首要因素是 Redis 的數(shù)據(jù)是在內(nèi)存中,而用 MySQL 一般是希望數(shù)據(jù)持久化到磁盤的。從 IO 速度來說,內(nèi)存 IO 比磁盤 IO 會(huì)快幾個(gè)數(shù)量級(jí),Redis 也就比 MySQL 性能更高。

          架構(gòu)和性能優(yōu)化做到后面,會(huì)發(fā)現(xiàn)最終限制性能的是硬件瓶頸。例如 nginx 做靜態(tài) Webserver 時(shí),出口流量往往能達(dá)到網(wǎng)卡的最大值或出口帶寬的最大值。MySQL 是個(gè)性能不錯(cuò)的 db,但它的數(shù)據(jù)持久化在磁盤上,自然就受限于磁盤 IO 速度。

          次要因素是 MySQL 支持完全的增刪查改 SQL 操作,還得支持 where 條件組合,這種復(fù)雜性讓 MySQL 的緩存機(jī)制有較大挑戰(zhàn),不像 Redis 這種 key-value 類型的操作是點(diǎn)查詢,所以 MySQL 的緩存機(jī)制不如 Redis 那么簡(jiǎn)單有效。

          總結(jié)一點(diǎn)就是,性能、通用性、成本三者難以同時(shí)滿足。在成本一定的情況下,專用的高效,通用的低效。如何同時(shí)滿足性能和通用性呢?那自然就是提高成本,最簡(jiǎn)單的方案是以前將 db 的機(jī)械硬盤升級(jí)到固態(tài)硬盤,性能提升很明顯。

          至此,從瀏覽器發(fā)起請(qǐng)求,到從 db 中獲取數(shù)據(jù)并返回的整個(gè)鏈路,都簡(jiǎn)要分析了一遍,希望能幫你搭建一個(gè)知識(shí)框架,貫通你過往的知識(shí)點(diǎn) 

          8、參考資料

          [1] TCP/IP詳解 - 第17章·TCP:傳輸控制協(xié)議

          [2] 假如你來設(shè)計(jì)TCP協(xié)議,會(huì)怎么做?

          [3] 深入淺出,全面理解HTTP協(xié)議

          [4] 什么是公網(wǎng)IP和內(nèi)網(wǎng)IP?NAT轉(zhuǎn)換又是什么鬼?

          [5] 一文讀懂高性能網(wǎng)絡(luò)編程中的I/O模型

          [6] 到底什么是高并發(fā)?一文即懂!

          [7] 深入操作系統(tǒng),徹底理解I/O多路復(fù)用

          [8] 深入操作系統(tǒng),徹底理解同步與異步

          [9] 騰訊資深架構(gòu)師干貨總結(jié):一文讀懂大型分布式系統(tǒng)設(shè)計(jì)的方方面面

          [10] 新手入門:零基礎(chǔ)理解大型分布式架構(gòu)的演進(jìn)歷史、技術(shù)原理、最佳實(shí)踐

          [11] 一篇讀懂分布式架構(gòu)下的負(fù)載均衡技術(shù):分類、原理、算法、常見方案等

          [12] 從新手到架構(gòu)師,一篇就夠:從100到1000萬高并發(fā)的架構(gòu)演進(jìn)之路

          [13] IM通訊協(xié)議專題學(xué)習(xí)(一):Protobuf從入門到精通,一篇就夠!


          (本文已同步發(fā)布于:http://www.52im.net/thread-4723-1-1.html?_dsign=ab3ce5a3



          作者:Jack Jiang (點(diǎn)擊作者姓名進(jìn)入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時(shí)通訊開發(fā)交流群 215891622
          討論:http://www.52im.net/
          Jack Jiang同時(shí)是【原創(chuàng)Java Swing外觀工程BeautyEye】【輕量級(jí)移動(dòng)端即時(shí)通訊框架MobileIMSDK】的作者,可前往下載交流。
          本博文 歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明出處(也可前往 我的52im.net 找到我)。


          只有注冊(cè)用戶登錄后才能發(fā)表評(píng)論。


          網(wǎng)站導(dǎo)航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯(lián)系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 布尔津县| 清镇市| 渝北区| 汝州市| 通山县| 淮北市| 策勒县| 舞阳县| 连云港市| 桐城市| 名山县| 仲巴县| 宣武区| 清河县| 和顺县| 河东区| 铜陵市| 方正县| 鸡泽县| 和硕县| 陆良县| 台州市| 本溪市| 华池县| 利川市| 尚志市| 开远市| 松阳县| 丰原市| 枣庄市| 乐都县| 嵊州市| 黄陵县| 石城县| 思茅市| 富宁县| 顺平县| 铜山县| 拜泉县| 安远县| 周口市|