又拍网架构-又一个用到python的网站
2019獨(dú)角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
又拍網(wǎng)是一個(gè)照片分享社區(qū),從2005年6月至今積累了260萬(wàn)用戶,1.1億張照片,目前的日訪問(wèn)量為200多萬(wàn)。5年的發(fā)展歷程里經(jīng)歷過(guò)許多起伏,也積累了一些經(jīng)驗(yàn),在這篇文章會(huì)介紹一些在技術(shù)上的積累。
又拍網(wǎng)和大多數(shù)Web2.0站點(diǎn)一樣,構(gòu)建于大量開源軟件之上,包括MySQL、PHP、nginx、Python、memcached、redis、Solr、Hadoop和RabbitMQ等等。又拍網(wǎng)的服務(wù)器端開發(fā)語(yǔ)言主要是PHP和Python,其中PHP用于編寫Web邏輯(通過(guò)HTTP和用戶直接打交道), 而Python則主要用于開發(fā)內(nèi)部服務(wù)和后臺(tái)任務(wù)。在客戶端則使用了大量的Javascript, 這里要感謝一下MooTools這個(gè)JS框架,它使得我們很享受前端開發(fā)過(guò)程。 另外,我們把圖片處理過(guò)程從PHP進(jìn)程里獨(dú)立出來(lái)變成一個(gè)服務(wù)。這個(gè)服務(wù)基于nginx,但是是作為nginx的一個(gè)模塊而開放REST API。
圖1:開發(fā)語(yǔ)言
由于PHP的單線程模型,我們把耗時(shí)較久的運(yùn)算和I/O操作從HTTP請(qǐng)求周期中分離出來(lái), 交給由Python實(shí)現(xiàn)的任務(wù)進(jìn)程來(lái)完成,以保證請(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通過(guò)消息隊(duì)列(我們用的是RabbitMQ)來(lái)觸發(fā)任務(wù)執(zhí)行。
圖2:PHP和Python的協(xié)作
數(shù)據(jù)庫(kù)一向是網(wǎng)站架構(gòu)中最具挑戰(zhàn)性的,瓶頸通常出現(xiàn)在這里。又拍網(wǎng)的照片數(shù)據(jù)量很大,數(shù)據(jù)庫(kù)也幾度出現(xiàn)嚴(yán)重的壓力問(wè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ā)展過(guò)程。
最初是由一臺(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ù)的緩存并不能很好地解決壓力問(wèn)題,因?yàn)閱涡袛?shù)據(jù)的查詢通常很快。所以我們把一些實(shí)時(shí)性要求不高的Query放到從庫(kù)去執(zhí)行。后面又通過(guò)添加多個(gè)從庫(kù)來(lái)分流查詢壓力,不過(guò)隨著數(shù)據(jù)量的增加,主庫(kù)的寫壓力也越來(lái)越大。
在參考了一些相關(guān)產(chǎn)品和其它網(wǎng)站的做法后,我們決定進(jìn)行數(shù)據(jù)庫(kù)拆分。也就是將數(shù)據(jù)存放到不同的數(shù)據(jù)庫(kù)服務(wù)器中,一般可以按兩個(gè)緯度來(lái)拆分?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)起來(lái)比較簡(jiǎn)單,根據(jù)表名訪問(wèn)不同的數(shù)據(jù)庫(kù)就可以了。但是垂直拆分方式并不能徹底解決所有壓力問(wèn)題,另外,也要看應(yīng)用類型是否合適這種拆分方式。如果合適的話,也能很好的起到分散數(shù)據(jù)庫(kù)壓力的作用。比如對(duì)于豆瓣我覺(jué)得比較適合采用垂直拆分, 因?yàn)槎拱甑母骱诵臉I(yè)務(wù)/模塊(書籍、電影、音樂(lè))相對(duì)獨(dú)立,數(shù)據(jù)的增加速度也比較平穩(wěn)。不同的是,又拍網(wǎng)的核心業(yè)務(wù)對(duì)象是用戶上傳的照片,而照片數(shù)據(jù)的增加速度隨著用戶量的增加越來(lái)越快。壓力基本上都在照片表上,顯然垂直拆分并不能從根本上解決我們的問(wèn)題,所以,我們采用水平拆分的方式。
拆分規(guī)則
水平拆分實(shí)現(xiàn)起來(lái)相對(duì)復(fù)雜,我們要先確定一個(gè)拆分規(guī)則,也就是按什么條件將數(shù)據(jù)進(jìn)行切分。 一般2.0網(wǎng)站都以用戶為中心,數(shù)據(jù)基本都跟隨用戶,比如用戶的照片、朋友和評(píng)論等等。因此一個(gè)比較自然的選擇是根據(jù)用戶來(lái)切分。每個(gè)用戶都對(duì)應(yīng)一個(gè)數(shù)據(jù)庫(kù),訪問(wèn)某個(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的奇偶性來(lái)對(duì)應(yīng),將奇數(shù)ID的用戶對(duì)應(yīng)到數(shù)據(jù)庫(kù)A,而偶數(shù)ID的用戶則對(duì)應(yīng)到數(shù)據(jù)庫(kù)B。這個(gè)方法的最大問(wèn)題是,只能分成兩個(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)起來(lái)比較方便,也比較高效,但是不能滿足后續(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ù)訪問(wèn),所以性能上沒(méi)有按算法對(duì)應(yīng)好。
比較之后,我們采用的是索引表的方式,我們?cè)敢鉃槠潇`活性損失一些性能,更何況我們還有memcached, 因?yàn)樗饕龜?shù)據(jù)基本不會(huì)改變的緣故,緩存命中率非常高。所以能很大程度上減少了性能損失。
圖4:數(shù)據(jù)訪問(wèn)過(guò)程
索引表的方式能夠比較方便地添加數(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)置為正常,最后將原來(lái)對(duì)應(yīng)的數(shù)據(jù)庫(kù)上的數(shù)據(jù)刪除。這個(gè)過(guò)程通常會(huì)在臨晨進(jìn)行,所以,所以很少會(huì)有用戶碰到遷移數(shù)據(jù)中的情況。
當(dāng)然,有些數(shù)據(jù)是不屬于某個(gè)用戶的,比如系統(tǒng)消息、配置等等,我們把這些數(shù)據(jù)保存在一個(gè)全局庫(kù)中。
問(wèn)題
分庫(kù)會(huì)給你在應(yīng)用的開發(fā)和部署上都帶來(lái)很多麻煩。
· 不能執(zhí)行跨庫(kù)的關(guān)聯(lián)查詢
如果我們需要查詢的數(shù)據(jù)分布于不同的數(shù)據(jù)庫(kù),我們沒(méi)辦法通過(guò)JOIN的方式查詢獲得。比如要獲得好友的最新照片,你不能保證所有好友的數(shù)據(jù)都在同一個(gè)數(shù)據(jù)庫(kù)里。一個(gè)解決辦法是通過(guò)多次查詢,再進(jìn)行聚合的方式。我們需要盡量避免類似的需求。有些需求可以通過(guò)保存多份數(shù)據(jù)來(lái)解決,比如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)如下圖所示。這樣我們可以通過(guò)查詢photo_comments表得到User-B的某張照片的所有評(píng)論, 也可以通過(guò)查詢user_comments表獲得User-A的所有評(píng)論。另外可以考慮使用全文檢索工具來(lái)解決某些需求, 我們使用Solr來(lái)提供全站標(biāo)簽檢索和照片搜索服務(wù)。
圖5:評(píng)論表結(jié)構(gòu)
· 不能保證數(shù)據(jù)的一致/完整性
跨庫(kù)的數(shù)據(jù)沒(méi)有外鍵約束,也沒(méi)有事務(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)訪問(wèn)老的照片地址時(shí),我們通過(guò)查詢這張表獲得用戶信息, 然后再重定向到新的地址。
· 自增ID
如果要在節(jié)點(diǎn)數(shù)據(jù)庫(kù)上使用自增字段,那么我們就不能保證全局唯一。這倒不是很嚴(yán)重的問(wèn)題,但是當(dāng)節(jié)點(diǎn)之間的數(shù)據(jù)發(fā)生關(guān)系時(shí),就會(huì)使得問(wèn)題變得比較麻煩。我們可以再來(lái)看看上面提到的評(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也有可能一樣(的確很巧)。看來(lái)只能再加上photo_owner_id了, 但是這個(gè)結(jié)果又讓我們實(shí)在有點(diǎn)無(wú)法接受,太復(fù)雜的組合鍵在寫入時(shí)會(huì)帶來(lái)一定的性能影響,這樣的自然鍵看起來(lái)也很不自然。所以,我們放棄了在節(jié)點(diǎn)上使用自增字段,想辦法讓這些ID變成全局唯一。為此增加了一個(gè)專門用來(lái)生成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ù)制的延遲問(wèn)題, 當(dāng)然在Web應(yīng)用里,我們可以在用戶會(huì)話里放置一個(gè)A或B來(lái)保證同一用戶一次會(huì)話里只訪問(wèn)一個(gè)數(shù)據(jù)庫(kù), 這樣可以避免一些延遲問(wèn)題。但是我們的Python任務(wù)是沒(méi)有任何狀態(tài)的,不能保證和PHP應(yīng)用讀寫相同的數(shù)據(jù)庫(kù)。那么為什么不配置成Master-Slave呢?我們覺(jué)得只用一臺(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í)候如果需要訪問(wèn)Shard-001的數(shù)據(jù)時(shí),我們連接的是Node-A上的shard_001, 而訪問(wèn)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), 原來(lái)的Active數(shù)據(jù)庫(kù)切換成Inactive狀態(tài),然后升級(jí)它的表結(jié)構(gòu),最后恢復(fù)復(fù)制。 當(dāng)然這個(gè)步驟不一定適合所有升級(jí)過(guò)程,如果表結(jié)構(gòu)的更改會(huì)導(dǎo)致數(shù)據(jù)復(fù)制失敗,那么還是需要停止服務(wù)再升級(jí)的。
圖6:數(shù)據(jù)庫(kù)布局
前面提到過(guò)添加服務(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í),遷移就無(wú)法避免了,不過(guò)那應(yīng)該是比較久遠(yuǎn)的事情了。
我們把分庫(kù)邏輯都封裝在我們的PHP框架里了,開發(fā)人員基本上不需要被這些繁瑣的事情困擾。下面是使用我們的框架進(jìn)行照片數(shù)據(jù)的讀寫的一些例子:
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′));
?>
首先要定義一個(gè)ShardedDBTable對(duì)象,所有的API都是通過(guò)這個(gè)對(duì)象開放。第一個(gè)參數(shù)是對(duì)象類型名稱, 如果這個(gè)名稱已經(jīng)存在,那么將返回之前定義的對(duì)象。你也可以通過(guò)get_table(‘Photos’)這個(gè)函數(shù)來(lái)獲取之前定義的Table對(duì)象。 第二個(gè)參數(shù)是對(duì)應(yīng)的數(shù)據(jù)庫(kù)表名,而第三個(gè)參數(shù)是數(shù)據(jù)庫(kù)線索字段,你會(huì)發(fā)現(xiàn)在后面的所有API中全部需要指定這個(gè)字段的值。 第四個(gè)參數(shù)是字段定義,其中photo_id字段的global_auto_increment屬性被置為true,這就是前面所說(shuō)的全局自增ID, 只要指定了這個(gè)屬性,框架會(huì)處理好ID的事情。
如果我們要訪問(wèn)全局庫(kù)中的數(shù)據(jù),我們需要定義一個(gè)DBTable對(duì)象。
array(‘type’ => ‘long’, ‘primary’ => true, ‘a(chǎn)uto_increment’ => true),
‘username’ => array(‘type’ => ’string’),
));
?>
DBTable是ShardedDBTable的父類,除了定義時(shí)參數(shù)有些不同(DBTable不需要指定數(shù)據(jù)庫(kù)線索字段),它們提供一樣的API。
緩存
我們的框架提供了緩存功能,對(duì)開發(fā)人員是透明的。
load(1, 10001);
?>
比如上面的方法調(diào)用,框架先嘗試以Photos-1-10001為Key在緩存中查找,未找到的話再執(zhí)行數(shù)據(jù)庫(kù)查詢并放入緩存。當(dāng)更改照片屬性或刪除照片時(shí),框架負(fù)責(zé)從緩存中刪除該照片。這種單個(gè)對(duì)象的緩存實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單。稍微麻煩的是像下面這樣的列表查詢結(jié)果的緩存。
fetch(array(‘user_id’ => 1, ‘posted_date__gt’ => ‘2010-06-01′));
?>
我們把這個(gè)查詢分成兩步,第一步先查出符合條件的照片ID,然后再根據(jù)照片ID分別查找具體的照片信息。 這么做可以更好的利用緩存。第一個(gè)查詢的緩存Key為Photos-list-{shard_key}-{md5(查詢條件SQL語(yǔ)句)}, Value是照片ID列表(逗號(hào)間隔)。其中shard_key為user_id的值1。目前來(lái)看,列表緩存也不麻煩。 但是如果用戶修改了某張照片的上傳時(shí)間呢,這個(gè)時(shí)候緩存中的數(shù)據(jù)就不一定符合條件了。所以,我們需要一個(gè)機(jī)制來(lái)保證我們不會(huì)從緩存中得到過(guò)期的列表數(shù)據(jù)。我們?yōu)槊繌埍碓O(shè)置了一個(gè)revision,當(dāng)該表的數(shù)據(jù)發(fā)生變化時(shí)(調(diào)用insert/update/delete方法), 我們就更新它的revision,所以我們把列表的緩存Key改為Photos-list-{shard_key}-{md5(查詢條件SQL語(yǔ)句)}-{revision}, 這樣我們就不會(huì)再得到過(guò)期列表了。
revision信息也是存放在緩存里的,Key為Photos-revision。這樣做看起來(lái)不錯(cuò),但是好像列表緩存的利用率不會(huì)太高。因?yàn)槲覀兪且哉麄€(gè)數(shù)據(jù)類型的revision為緩存Key的后綴,顯然這個(gè)revision更新的非常頻繁,任何一個(gè)用戶修改或上傳了照片都會(huì)導(dǎo)致它的更新,哪怕那個(gè)用戶根本不在我們要查詢的Shard里。要隔離用戶的動(dòng)作對(duì)其他用戶的影響,我們可以通過(guò)縮小revision的作用范圍來(lái)達(dá)到這個(gè)目的。 所以revision的緩存Key變成Photos-{shard_key}-revision,這樣的話當(dāng)ID為1的用戶修改了他的照片信息時(shí), 只會(huì)更新Photos-1-revision這個(gè)Key所對(duì)應(yīng)的revision。
因?yàn)槿謳?kù)沒(méi)有shard_key,所以修改了全局庫(kù)中的表的一行數(shù)據(jù),還是會(huì)導(dǎo)致整個(gè)表的緩存失效。 但是大部分情況下,數(shù)據(jù)都是有區(qū)域范圍的,比如我們的幫助論壇的主題帖子, 帖子屬于主題。修改了其中一個(gè)主題的一個(gè)帖子,沒(méi)必要使所有主題的帖子緩存都失效。 所以我們?cè)贒BTable上增加了一個(gè)叫isolate_key的屬性。
array(‘type’ => ‘long’, ‘primary’ => true),
‘post_id’ => array(‘type’ => ‘long’, ‘primary’ => true, ‘a(chǎn)uto_increment’ => true),
‘a(chǎn)uthor_id’ => array(‘type’ => ‘long’),
‘content’ => array(‘type’ => ’string’),
‘posted_at’ => array(‘type’ => ‘datetime’),
‘modified_at’ => array(‘type’ => ‘datetime’),
‘modified_by’ => array(‘type’ => ‘long’),
), ‘topic_id’);
?>
注意構(gòu)造函數(shù)的最后一個(gè)參數(shù)topic_id就是指以字段topic_id作為isolate_key,它的作用和shard_key一樣用于隔離revision的作用范圍。
ShardedDBTable繼承自DBTable,所以也可以指定isolate_key。 ShardedDBTable指定了isolate_key的話,能夠更大幅度縮小revision的作用范圍。 比如相冊(cè)和照片的關(guān)聯(lián)表yp_album_photos,當(dāng)用戶往他的其中一個(gè)相冊(cè)里添加了新的照片時(shí), 會(huì)導(dǎo)致其它相冊(cè)的照片列表緩存也失效。如果我指定這張表的isolate_key為album_id的話, 我們就把這種影響限制在了本相冊(cè)內(nèi)。
我們的緩存分為兩級(jí),第一級(jí)只是一個(gè)PHP數(shù)組,有效范圍是Request。而第二級(jí)是memcached。這么做的原因是,很多數(shù)據(jù)在一個(gè)Request周期內(nèi)需要加載多次,這樣可以減少memcached的網(wǎng)絡(luò)請(qǐng)求。另外我們的框架也會(huì)盡可能的發(fā)送memcached的gets命令來(lái)獲取數(shù)據(jù), 從而減少網(wǎng)絡(luò)請(qǐng)求。
總結(jié)
這個(gè)架構(gòu)使得我們?cè)诤荛L(zhǎng)一段時(shí)間內(nèi)都不必再為數(shù)據(jù)庫(kù)壓力所困擾。我們的設(shè)計(jì)很多地方參考了netlog和flickr的實(shí)現(xiàn),因此非常感謝他們將一些實(shí)現(xiàn)細(xì)節(jié)發(fā)布出來(lái)。
關(guān)于作者:
周兆兆(Zola,不是你熟知的那個(gè)),又拍網(wǎng)架構(gòu)師。6年IT從業(yè)經(jīng)驗(yàn),不太專注于某項(xiàng)技術(shù),對(duì)很多技術(shù)都感興趣。
————————————————————
作為國(guó)內(nèi)最大的圖片服務(wù)提供商之一,Yupoo! 的 Alexa 排名大約在 5300 左右。同時(shí)收集到的一些數(shù)據(jù)如下:
帶寬:4000M/S (參考)
服務(wù)器數(shù)量:60 臺(tái)左右
Web服務(wù)器:Lighttpd, Apache, nginx
應(yīng)用服務(wù)器:Tomcat
其他:Python, Java, MogileFS 、ImageMagick 等
首先看一下網(wǎng)站的架構(gòu)圖:
該架構(gòu)圖給出了很好的概覽(點(diǎn)擊可以查看在 Yupoo! 上的大圖和原圖,請(qǐng)注意該圖版權(quán)信息)。
關(guān)于 Squid 與 Tomcat
Squid 與 Tomcat 似乎在 Web 2.0 站點(diǎn)的架構(gòu)中較少看到。我首先是對(duì) Squid 有點(diǎn)疑問(wèn),對(duì)此阿華的解釋是”目前暫時(shí)還沒(méi)找到效率比 Squid 高的緩存系統(tǒng),原來(lái)命中率的確很差,后來(lái)在 Squid 前又裝了層 Lighttpd, 基于 url 做 hash, 同一個(gè)圖片始終會(huì)到同一臺(tái) squid 去,所以命中率徹底提高了”
對(duì)于應(yīng)用服務(wù)器層的 Tomcat,現(xiàn)在 Yupoo! 技術(shù)人員也在逐漸用其他輕量級(jí)的東西替代,而 YPWS/YPFS 現(xiàn)在已經(jīng)用 Python 進(jìn)行開發(fā)了。
名次解釋:
YPWS–Yupoo Web Server YPWS 是用 Python開發(fā)的一個(gè)小型 Web 服務(wù)器,提供基本的 Web 服務(wù)外,可以增加針對(duì)用戶、圖片、外鏈網(wǎng)站顯示的邏輯判斷,可以安裝于任何有空閑資源的服務(wù)器中,遇到性能瓶頸時(shí)方便橫向擴(kuò)展。
YPFS–Yupoo File System 與 YPWS 類似,YPFS 也是基于這個(gè) Web 服務(wù)器上開發(fā)的圖片上傳服務(wù)器。
【Updated: 有網(wǎng)友留言質(zhì)疑 Python 的效率,Yupoo 老大劉平陽(yáng)在 del.icio.us 上寫到 “YPWS用Python自己寫的,每臺(tái)機(jī)器每秒可以處理294個(gè)請(qǐng)求, 現(xiàn)在壓力幾乎都在10%以下”】
圖片處理層
接下來(lái)的 Image Process Server 負(fù)責(zé)處理用戶上傳的圖片。使用的軟件包也是 ImageMagick,在上次存儲(chǔ)升級(jí)的同時(shí),對(duì)于銳化的比率也調(diào)整過(guò)了(我個(gè)人感覺(jué),效果的確好了很多)。”Magickd“ 是圖像處理的一個(gè)遠(yuǎn)程接口服務(wù),可以安裝在任何有空閑 CPU資源的機(jī)器上,類似 Memcached的服務(wù)方式。
我們知道 Flickr 的縮略圖功能原來(lái)是用 ImageMagick 軟件包的,后來(lái)被雅虎收購(gòu)后出于版權(quán)原因而不用了(?);EXIF 與 IPTC Flicke 是用 Perl 抽取的,我是非常建議 Yupoo! 針對(duì) EXIF 做些文章,這也是潛在產(chǎn)生受益的一個(gè)重點(diǎn)。
圖片存儲(chǔ)層
原來(lái) Yupoo! 的存儲(chǔ)采用了磁盤陣列柜,基于 NFS 方式的,隨著數(shù)據(jù)量的增大,”Yupoo! 開發(fā)部從07年6月份就開始著手研究一套大容量的、能滿足 Yupoo! 今后發(fā)展需要的、安全可靠的存儲(chǔ)系統(tǒng)“,看來(lái) Yupoo! 系統(tǒng)比較有信心,也是滿懷期待的,畢竟這要支撐以 TB 計(jì)算的海量圖片的存儲(chǔ)和管理。我們知道,一張圖片除了原圖外,還有不同尺寸的,這些圖片統(tǒng)一存儲(chǔ)在 MogileFS 中。
對(duì)于其他部分,常見(jiàn)的 Web 2.0 網(wǎng)站必須軟件都能看到,如 MySQL、Memcached 、Lighttpd 等。Yupoo! 一方面采用不少相對(duì)比較成熟的開源軟件,一方面也在自行開發(fā)定制適合自己的架構(gòu)組件。這也是一個(gè) Web 2.0 公司所必需要走的一個(gè)途徑。
來(lái)源:http://www.bopor.com/?p=652
轉(zhuǎn)載于:https://my.oschina.net/longhtml/blog/156338
總結(jié)
以上是生活随笔為你收集整理的又拍网架构-又一个用到python的网站的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 【MYSQL】总结MySQL中对表内容的
- 下一篇: 灰色系统与灰色预测模型