一区二区久久-一区二区三区www-一区二区三区久久-一区二区三区久久精品-麻豆国产一区二区在线观看-麻豆国产视频

又拍網(wǎng)架構(gòu)中的分庫(kù)設(shè)計(jì)

  又拍網(wǎng)是一個(gè)照片分享社區(qū),從2005年6月至今積累了260萬用戶,1.1億張照片,目前的日訪問量為200多萬。5年的發(fā)展歷程里經(jīng)歷過許多起伏,也積累了一些經(jīng)驗(yàn),在這篇文章里,我要介紹一些我們?cè)诩夹g(shù)上的積累。

  又拍網(wǎng)和大多數(shù)Web2.0站點(diǎn)一樣,構(gòu)建于大量開源軟件之上,包括MySQL、php、nginx、Python、memcached、redis、Solr、Hadoop和RabbitMQ等等。又拍網(wǎng)的服務(wù)器端開發(fā)語言主要是phpPython,其中php用于編寫Web邏輯(通過HTTP和用戶直接打交道), 而Python則主要用于開發(fā)內(nèi)部服務(wù)和后臺(tái)任務(wù)。在客戶端則使用了大量的Javascript, 這里要感謝一下MooTools這個(gè)JS框架,它使得我們很享受前端開發(fā)過程。 另外,我們把圖片處理過程從php進(jìn)程里獨(dú)立出來變成一個(gè)服務(wù)。這個(gè)服務(wù)基于nginx,但是是作為nginx的一個(gè)模塊而開放REST API。

圖1:開發(fā)語言

  由于php的單線程模型,我們把耗時(shí)較久的運(yùn)算和I/O操作從HTTP請(qǐng)求周期中分離出來, 交給由Python實(shí)現(xiàn)的任務(wù)進(jìn)程來完成,以保證請(qǐng)求響應(yīng)速度。這些任務(wù)主要包括:郵件發(fā)送、數(shù)據(jù)索引、數(shù)據(jù)聚合和好友動(dòng)態(tài)推送(稍候會(huì)有介紹)等等。通常這些任務(wù)由用戶觸發(fā),并且,用戶的一個(gè)行為可能會(huì)觸發(fā)多種任務(wù)的執(zhí)行。 比如,用戶上傳了一張新的照片,我們需要更新索引,也需要向他的朋友推送一條新的動(dòng)態(tài)。php通過消息隊(duì)列(我們用的是RabbitMQ)來觸發(fā)任務(wù)執(zhí)行。

圖2:phpPython的協(xié)作

  數(shù)據(jù)庫(kù)一向是網(wǎng)站架構(gòu)中最具挑戰(zhàn)性的,瓶頸通常出現(xiàn)在這里。又拍網(wǎng)的照片數(shù)據(jù)量很大,數(shù)據(jù)庫(kù)也幾度出現(xiàn)嚴(yán)重的壓力問題。 因此,這里我主要介紹一下又拍網(wǎng)在分庫(kù)設(shè)計(jì)這方面的一些嘗試。

  分庫(kù)設(shè)計(jì)

  和很多使用MySQL的2.0站點(diǎn)一樣,又拍網(wǎng)的MySQL集群經(jīng)歷了從最初的一個(gè)主庫(kù)一個(gè)從庫(kù)、到一個(gè)主庫(kù)多個(gè)從庫(kù)、 然后到多個(gè)主庫(kù)多個(gè)從庫(kù)的一個(gè)發(fā)展過程。

圖3:數(shù)據(jù)庫(kù)的進(jìn)化過程

  最初是由一臺(tái)主庫(kù)和一臺(tái)從庫(kù)組成,當(dāng)時(shí)從庫(kù)只用作備份和容災(zāi),當(dāng)主庫(kù)出現(xiàn)故障時(shí),從庫(kù)就手動(dòng)變成主庫(kù),一般情況下,從庫(kù)不作讀寫操作(同步除外)。隨著壓力的增加,我們加上了memcached,當(dāng)時(shí)只用其緩存單行數(shù)據(jù)。 但是,單行數(shù)據(jù)的緩存并不能很好地解決壓力問題,因?yàn)閱涡袛?shù)據(jù)的查詢通常很快。所以我們把一些實(shí)時(shí)性要求不高的Query放到從庫(kù)去執(zhí)行。后面又通過添加多個(gè)從庫(kù)來分流查詢壓力,不過隨著數(shù)據(jù)量的增加,主庫(kù)的寫壓力也越來越大。

  在參考了一些相關(guān)產(chǎn)品和其它網(wǎng)站的做法后,我們決定進(jìn)行數(shù)據(jù)庫(kù)拆分。也就是將數(shù)據(jù)存放到不同的數(shù)據(jù)庫(kù)服務(wù)器中,一般可以按兩個(gè)緯度來拆分?jǐn)?shù)據(jù):

  垂直拆分:是指按功能模塊拆分,比如可以將群組相關(guān)表和照片相關(guān)表存放在不同的數(shù)據(jù)庫(kù)中,這種方式多個(gè)數(shù)據(jù)庫(kù)之間的表結(jié)構(gòu)不同

  水平拆分:而水平拆分是將同一個(gè)表的數(shù)據(jù)進(jìn)行分塊保存到不同的數(shù)據(jù)庫(kù)中,這些數(shù)據(jù)庫(kù)中的表結(jié)構(gòu)完全相同。

  拆分方式

  一般都會(huì)先進(jìn)行垂直拆分,因?yàn)檫@種方式拆分方式實(shí)現(xiàn)起來比較簡(jiǎn)單,根據(jù)表名訪問不同的數(shù)據(jù)庫(kù)就可以了。但是垂直拆分方式并不能徹底解決所有壓力問題,另外,也要看應(yīng)用類型是否合適這種拆分方式。如果合適的話,也能很好的起到分散數(shù)據(jù)庫(kù)壓力的作用。比如對(duì)于豆瓣我覺得比較適合采用垂直拆分, 因?yàn)?a >豆瓣的各核心業(yè)務(wù)/模塊(書籍、電影、音樂)相對(duì)獨(dú)立,數(shù)據(jù)的增加速度也比較平穩(wěn)。不同的是,又拍網(wǎng)的核心業(yè)務(wù)對(duì)象是用戶上傳的照片,而照片數(shù)據(jù)的增加速度隨著用戶量的增加越來越快。壓力基本上都在照片表上,顯然垂直拆分并不能從根本上解決我們的問題,所以,我們采用水平拆分的方式。

  拆分規(guī)則

  水平拆分實(shí)現(xiàn)起來相對(duì)復(fù)雜,我們要先確定一個(gè)拆分規(guī)則,也就是按什么條件將數(shù)據(jù)進(jìn)行切分。 一般2.0網(wǎng)站都以用戶為中心,數(shù)據(jù)基本都跟隨用戶,比如用戶的照片、朋友和評(píng)論等等。因此一個(gè)比較自然的選擇是根據(jù)用戶來切分。每個(gè)用戶都對(duì)應(yīng)一個(gè)數(shù)據(jù)庫(kù),訪問某個(gè)用戶的數(shù)據(jù)時(shí), 我們要先確定他/她所對(duì)應(yīng)的數(shù)據(jù)庫(kù),然后連接到該數(shù)據(jù)庫(kù)進(jìn)行實(shí)際的數(shù)據(jù)讀寫。

  那么,怎么樣對(duì)應(yīng)用戶和數(shù)據(jù)庫(kù)呢?我們有這些選擇:

  按算法對(duì)應(yīng)

  最簡(jiǎn)單的算法是按用戶ID的奇偶性來對(duì)應(yīng),將奇數(shù)ID的用戶對(duì)應(yīng)到數(shù)據(jù)庫(kù)A,而偶數(shù)ID的用戶則對(duì)應(yīng)到數(shù)據(jù)庫(kù)B。這個(gè)方法的最大問題是,只能分成兩個(gè)庫(kù)。另一個(gè)算法是按用戶ID所在區(qū)間對(duì)應(yīng),比如ID在0-10000之間的用戶對(duì)應(yīng)到數(shù)據(jù)庫(kù)A, ID在10000-20000這個(gè)范圍的對(duì)應(yīng)到數(shù)據(jù)庫(kù)B,以此類推。按算法分實(shí)現(xiàn)起來比較方便,也比較高效,但是不能滿足后續(xù)的伸縮性要求,如果需要增加數(shù)據(jù)庫(kù)節(jié)點(diǎn),必需調(diào)整算法或移動(dòng)很大的數(shù)據(jù)集, 比較難做到在不停止服務(wù)的前提下進(jìn)行擴(kuò)充數(shù)據(jù)庫(kù)節(jié)點(diǎn)。

  按索引/映射表對(duì)應(yīng)

  這種方法是指建立一個(gè)索引表,保存每個(gè)用戶的ID和數(shù)據(jù)庫(kù)ID的對(duì)應(yīng)關(guān)系,每次讀寫用戶數(shù)據(jù)時(shí)先從這個(gè)表獲取對(duì)應(yīng)數(shù)據(jù)庫(kù)。新用戶注冊(cè)后,在所有可用的數(shù)據(jù)庫(kù)中隨機(jī)挑選一個(gè)為其建立索引。這種方法比較靈活,有很好的伸縮性。一個(gè)缺點(diǎn)是增加了一次數(shù)據(jù)庫(kù)訪問,所以性能上沒有按算法對(duì)應(yīng)好。

  比較之后,我們采用的是索引表的方式,我們?cè)敢鉃槠潇`活性損失一些性能,更何況我們還有memcached, 因?yàn)樗饕龜?shù)據(jù)基本不會(huì)改變的緣故,緩存命中率非常高。所以能很大程度上減少了性能損失。

圖4:數(shù)據(jù)訪問過程

  索引表的方式能夠比較方便地添加數(shù)據(jù)庫(kù)節(jié)點(diǎn),在增加節(jié)點(diǎn)時(shí),只要將其添加到可用數(shù)據(jù)庫(kù)列表里即可。 當(dāng)然如果需要平衡各個(gè)節(jié)點(diǎn)的壓力的話,還是需要進(jìn)行數(shù)據(jù)的遷移,但是這個(gè)時(shí)候的遷移是少量的,可以逐步進(jìn)行。要遷移用戶A的數(shù)據(jù),首先要將其狀態(tài)置為遷移數(shù)據(jù)中,這個(gè)狀態(tài)的用戶不能進(jìn)行寫操作,并在頁(yè)面上進(jìn)行提示。 然后將用戶A的數(shù)據(jù)全部復(fù)制到新增加的節(jié)點(diǎn)上后,更新映射表,然后將用戶A的狀態(tài)置為正常,最后將原來對(duì)應(yīng)的數(shù)據(jù)庫(kù)上的數(shù)據(jù)刪除。這個(gè)過程通常會(huì)在凌晨進(jìn)行,所以,所以很少會(huì)有用戶碰到遷移數(shù)據(jù)中的情況。

  當(dāng)然,有些數(shù)據(jù)是不屬于某個(gè)用戶的,比如系統(tǒng)消息、配置等等,我們把這些數(shù)據(jù)保存在一個(gè)全局庫(kù)中。

  問題

  分庫(kù)會(huì)給你在應(yīng)用的開發(fā)和部署上都帶來很多麻煩。

  不能執(zhí)行跨庫(kù)的關(guān)聯(lián)查詢

  如果我們需要查詢的數(shù)據(jù)分布于不同的數(shù)據(jù)庫(kù),我們沒辦法通過JOIN的方式查詢獲得。比如要獲得好友的最新照片,你不能保證所有好友的數(shù)據(jù)都在同一個(gè)數(shù)據(jù)庫(kù)里。一個(gè)解決辦法是通過多次查詢,再進(jìn)行聚合的方式。我們需要盡量避免類似的需求。有些需求可以通過保存多份數(shù)據(jù)來解決,比如User-A和User-B的數(shù)據(jù)庫(kù)分別是DB-1和DB-2, 當(dāng)User-A評(píng)論了User-B的照片時(shí),我們會(huì)同時(shí)在DB-1和DB-2中保存這條評(píng)論信息,我們首先在DB-2中的photo_comments表中插入一條新的記錄,然后在DB-1中的user_comments表中插入一條新的記錄。這兩個(gè)表的結(jié)構(gòu)如下圖所示。這樣我們可以通過查詢photo_comments表得到User-B的某張照片的所有評(píng)論, 也可以通過查詢user_comments表獲得User-A的所有評(píng)論。另外可以考慮使用全文檢索工具來解決某些需求, 我們使用Solr來提供全站標(biāo)簽檢索和照片搜索服務(wù)。

圖5:評(píng)論表結(jié)構(gòu)

  不能保證數(shù)據(jù)的一致/完整性

  跨庫(kù)的數(shù)據(jù)沒有外鍵約束,也沒有事務(wù)保證。比如上面的評(píng)論照片的例子, 很可能出現(xiàn)成功插入photo_comments表,但是插入user_comments表時(shí)卻出錯(cuò)了。一個(gè)辦法是在兩個(gè)庫(kù)上都開啟事務(wù),然后先插入photo_comments,再插入user_comments, 然后提交兩個(gè)事務(wù)。這個(gè)辦法也不能完全保證這個(gè)操作的原子性。

  所有查詢必須提供數(shù)據(jù)庫(kù)線索

  比如要查看一張照片,僅憑一個(gè)照片ID是不夠的,還必須提供上傳這張照片的用戶的ID(也就是數(shù)據(jù)庫(kù)線索),才能找到它實(shí)際的存放位置。因此,我們必須重新設(shè)計(jì)很多URL地址,而有些老的地址我們又必須保證其仍然有效。我們把照片地址改成/photos/{username}/{photo_id}/的形式,然后對(duì)于系統(tǒng)升級(jí)前上傳的照片ID, 我們又增加一張映射表,保存photo_id和user_id的對(duì)應(yīng)關(guān)系。當(dāng)訪問老的照片地址時(shí),我們通過查詢這張表獲得用戶信息, 然后再重定向到新的地址。

  自增ID

  如果要在節(jié)點(diǎn)數(shù)據(jù)庫(kù)上使用自增字段,那么我們就不能保證全局唯一。這倒不是很嚴(yán)重的問題,但是當(dāng)節(jié)點(diǎn)之間的數(shù)據(jù)發(fā)生關(guān)系時(shí),就會(huì)使得問題變得比較麻煩。我們可以再來看看上面提到的評(píng)論的例子。如果photo_comments表中的comment_id的自增字段,當(dāng)我們?cè)贒B-2.photo_comments表插入新的評(píng)論時(shí), 得到一個(gè)新的comment_id,假如值為101,而User-A的ID為1,那么我們還需要在DB-1.user_comments表中插入(1, 101 ...)。 User-A是個(gè)很活躍的用戶,他又評(píng)論了User-C的照片,而User-C的數(shù)據(jù)庫(kù)是DB-3。 很巧的是這條新評(píng)論的ID也是101,這種情況很用可能發(fā)生。那么我們又在DB-1.user_comments表中插入一行像這樣(1, 101 ...)的數(shù)據(jù)。 那么我們要怎么設(shè)置user_comments表的主鍵呢(標(biāo)識(shí)一行數(shù)據(jù))?可以不設(shè)啊,不幸的是有的時(shí)候(框架、緩存等原因)必需設(shè)置。那么可以以u(píng)ser_id、 comment_id和photo_id為組合主鍵,但是photo_id也有可能一樣(的確很巧)。看來只能再加上photo_owner_id了, 但是這個(gè)結(jié)果又讓我們實(shí)在有點(diǎn)無法接受,太復(fù)雜的組合鍵在寫入時(shí)會(huì)帶來一定的性能影響,這樣的自然鍵看起來也很不自然。所以,我們放棄了在節(jié)點(diǎn)上使用自增字段,想辦法讓這些ID變成全局唯一。為此增加了一個(gè)專門用來生成ID的數(shù)據(jù)庫(kù),這個(gè)庫(kù)中的表結(jié)構(gòu)都很簡(jiǎn)單,只有一個(gè)自增字段id。 當(dāng)我們要插入新的評(píng)論時(shí),我們先在ID庫(kù)的photo_comments表里插入一條空的記錄,以獲得一個(gè)唯一的評(píng)論ID。 當(dāng)然這些邏輯都已經(jīng)封裝在我們的框架里了,對(duì)于開發(fā)人員是透明的。 為什么不用其它方案呢,比如一些支持incr操作的Key-Value數(shù)據(jù)庫(kù)。我們還是比較放心把數(shù)據(jù)放在MySQL里。 另外,我們會(huì)定期清理ID庫(kù)的數(shù)據(jù),以保證獲取新ID的效率。

  實(shí)現(xiàn)

  我們稱前面提到的一個(gè)數(shù)據(jù)庫(kù)節(jié)點(diǎn)為Shard,一個(gè)Shard由兩個(gè)臺(tái)物理服務(wù)器組成, 我們稱它們?yōu)镹ode-A和Node-B,Node-A和Node-B之間是配置成Master-Master相互復(fù)制的。 雖然是Master-Master的部署方式,但是同一時(shí)間我們還是只使用其中一個(gè),原因是復(fù)制的延遲問題, 當(dāng)然在Web應(yīng)用里,我們可以在用戶會(huì)話里放置一個(gè)A或B來保證同一用戶一次會(huì)話里只訪問一個(gè)數(shù)據(jù)庫(kù), 這樣可以避免一些延遲問題。但是我們的Python任務(wù)是沒有任何狀態(tài)的,不能保證和php應(yīng)用讀寫相同的數(shù)據(jù)庫(kù)。那么為什么不配置成Master-Slave呢?我們覺得只用一臺(tái)太浪費(fèi)了,所以我們?cè)诿颗_(tái)服務(wù)器上都創(chuàng)建多個(gè)邏輯數(shù)據(jù)庫(kù)。 如下圖所示,在Node-A和Node-B上我們都建立了shard_001和shard_002兩個(gè)邏輯數(shù)據(jù)庫(kù), Node-A上的shard_001和Node-B上的shard_001組成一個(gè)Shard,而同一時(shí)間只有一個(gè)邏輯數(shù)據(jù)庫(kù)處于Active狀態(tài)。 這個(gè)時(shí)候如果需要訪問Shard-001的數(shù)據(jù)時(shí),我們連接的是Node-A上的shard_001, 而訪問Shard-002的數(shù)據(jù)則是連接Node-B上的shard_002。以這種交叉的方式將壓力分散到每臺(tái)物理服務(wù)器上。 以Master-Master方式部署的另一個(gè)好處是,我們可以不停止服務(wù)的情況下進(jìn)行表結(jié)構(gòu)升級(jí), 升級(jí)前先停止復(fù)制,升級(jí)Inactive的庫(kù),然后升級(jí)應(yīng)用,再將已經(jīng)升級(jí)好的數(shù)據(jù)庫(kù)切換成Active狀態(tài), 原來的Active數(shù)據(jù)庫(kù)切換成Inactive狀態(tài),然后升級(jí)它的表結(jié)構(gòu),最后恢復(fù)復(fù)制。 當(dāng)然這個(gè)步驟不一定適合所有升級(jí)過程,如果表結(jié)構(gòu)的更改會(huì)導(dǎo)致數(shù)據(jù)復(fù)制失敗,那么還是需要停止服務(wù)再升級(jí)的。

圖6:數(shù)據(jù)庫(kù)布局

  前面提到過添加服務(wù)器時(shí),為了保證負(fù)載的平衡,我們需要遷移一部分?jǐn)?shù)據(jù)到新的服務(wù)器上。為了避免短期內(nèi)遷移的必要,我們?cè)趯?shí)際部署的時(shí)候,每臺(tái)機(jī)器上部署了8個(gè)邏輯數(shù)據(jù)庫(kù), 添加服務(wù)器后,我們只要將這些邏輯數(shù)據(jù)庫(kù)遷移到新服務(wù)器就可以了。最好是每次添加一倍的服務(wù)器, 然后將每臺(tái)的1/2邏輯數(shù)據(jù)遷移到一臺(tái)新服務(wù)器上,這樣能很好的平衡負(fù)載。當(dāng)然,最后到了每臺(tái)上只有一個(gè)邏輯庫(kù)時(shí),遷移就無法避免了,不過那應(yīng)該是比較久遠(yuǎn)的事情了。

  我們把分庫(kù)邏輯都封裝在我們的php框架里了,開發(fā)人員基本上不需要被這些繁瑣的事情困擾。下面是使用我們的框架進(jìn)行照片數(shù)據(jù)的讀寫的一些例子:

<?php
$Photos = new ShardedDBTable('Photos', 'yp_photos', 'user_id', array(
'photo_id' => array('type' => 'long', 'primary' => true, 'global_auto_increment' => true),
'user_id' => array('type' => 'long'),
'title' => array('type' => 'string'),
'posted_date' => array('type' => 'date'),
));

$photo = $Photos->new_object(array('user_id' => 1, 'title' => 'Workforme'));
$photo->insert();

// 加載ID為10001的照片,注意第一個(gè)參數(shù)為用戶ID
$photo = $Photos->load(1, 10001);

// 更改照片屬性
$photo->title = 'Database Sharding';
$photo->update();

// 刪除照片
$photo->delete();

// 獲取ID為1的用戶在2010-06-01之后上傳的照片
$photos = $Photos->fetch(array('user_id' => 1, 'posted_date__gt' => '2010-06-01'));
?>

it知識(shí)庫(kù)又拍網(wǎng)架構(gòu)中的分庫(kù)設(shè)計(jì),轉(zhuǎn)載需保留來源!

鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。

主站蜘蛛池模板: 久久国内精品自在自线400部o | 91视频国产精品 | 色黄网站在线观看 | 久久四虎 | 久久亚洲国产 | 色爱区综合激情五月综合色 | 欧美激情伊人 | 国内自拍第五一页 | 免费国产成人高清视频网站 | 97人人在线 | 欧美精品国产 | 久久2017| 999精品视频在线 | 六月天丁香婷婷 | 最新日本免费一区二区三区中文 | 色视频在线观看网站 | 国产在线麻豆自在拍91精品 | 欧美一级精品 | 在线看一区二区 | 欧美在线观看免费一区视频 | 亚洲人成依人成综合网 | 91极品视频在线观看 | 亚洲大片免费观看 | 久久夜色国产精品噜噜 | 国产色中色 | 中文字幕一区二区三区5566 | 国产一级免费在线观看 | 91免费观看视频 | 亚洲国产精久久小蝌蚪 | 91综合久久 | 久热99这里只有精品视频6 | 伊人网综合在线视频 | 亚洲视频毛片 | 欧美日韩国产码高清综合人成 | 欧美色鬼| 国产一级成人毛片 | 亚洲婷婷六月 | 中文字幕 亚洲一区 | www日本黄色 | 国产成人91精品 | 日本黄色美女网站 |