转:Yupoo(又拍网)的系统架构
Yupoo!(又拍網) 是目前國內最大的圖片服務提供商,整個網站構建于大量的開源軟件之上。以下為其使用到的開源軟件信息:
-
操作系統:CentOS、MacOSX、Ubuntu
-
服務器:Apache、Nginx、Squid
-
數據庫:MySQLmochiweb、MySQLdb
-
服務器監控:Cacti、Nagios、
-
開發語言:PHP、Python、Erlang、Java、Lua
-
分布式計算:Hadoop、Mogilefs、
-
日志分析:AWStats
-
任務管理:Redmine
-
消息系統:RabbitMQ、php-amqp
-
前端框架:Mootools
-
緩存系統:Memcached、php-memcached、libmemcached、pylibmc、XCache、RedisRiak、Predis
-
圖片處理:GraphicsMagick、gmagick
-
FTP工具:vsftpd
-
開發工具:VIM、Readline
-
調試工具:Firebug、Xdebug
-
版本控制:Mercurial
-
搜索服務:Solr
-
郵件服務:Postfix
-
網絡編程:Twisted、cURL、libevent、Net-SNMP、NTP
-
可用性測試:ibrowse
-
集群系統:Heartbeat
-
并行開發:gevent
-
負載均衡:IPVS
-
Python框架:bottle
-
虛擬通道:OpenVPN
(信息來源:http://www.yupoo.com/info/about/)。
一、Yupoo的整體架構
二、程序語言的選擇
Yupoo 的服務器端開發語言主要是PHP和Python,其中PHP用于編寫Web邏輯(通過HTTP和用戶直接打交道), 而Python則主要用于開發內部服務和后臺任務。在客戶端則使用了大量基于MooTools框架的Javascript。 另外,Yupoo把圖片處理過程從PHP進程里獨立出來變成一個服務。這個服務基于nginx,作為nginx的一個模塊開放REST API。
三、服務器的選用
選用Squid的原因是“目前暫時還沒找到效率比 Squid 高的緩存系統,原來命中率的確很差,后來在 Squid 前又裝了層 Lighttpd, 基于 url 做 hash, 同一個圖片始終會到同一臺 squid 去,所以命中率徹底提高了。”
同時Yupoo也使用Python開發了YPWS/YPFS:
-
YPWS-Yupoo Web Server 是用 Python開發的一個小型 Web 服務器,提供基本的 Web 服務外,可以增加針對用戶、圖片、外鏈網站顯示的邏輯判斷,可以安裝于任何有空閑資源的服務器中,遇到性能瓶頸時方便橫向擴展。
-
YPFS-Yupoo File System 與 YPWS 類似,也是基于這個 Web 服務器上開發的圖片上傳服務器。
有網友留言質疑 Python 的效率,Yupoo 老大劉平陽在 del.icio.us 上寫到 “YPWS用Python自己寫的,每臺機器每秒可以處理294個請求, 現在壓力幾乎都在10%以下”
四、Yupoo的消息系統
由 于PHP的單線程模型,Yupoo把耗時較久的運算和I/O操作從HTTP請求周期中分離出來, 交給由Python實現的任務進程來完成,以保證請求響應速度。這些任務主要包括:郵件發送、數據索引、數據聚合和好友動態推送等等。PHP通過消息隊列 (Yupoo用的是RabbitMQ)來觸發任務執行。這些任務的主要特點為:
-
由用戶或者定時觸發的
-
耗時比較長的
-
需要異步執行的
整個任務系統主要分為以消息分發、進程管理和工作進程組成。
五、數據庫的設計
數 據庫一向是網站架構中最具挑戰性的,瓶頸通常出現在這里。又拍網的照片數據量很大,數據庫也幾度出現嚴重的壓力問題。和很多使用MySQL的2.0站點一 樣,又拍網的MySQL集群經歷了從最初的一個主庫一個從庫、到一個主庫多個從庫、 然后到多個主庫多個從庫的一個發展過程。
最 初是由一臺主庫和一臺從庫組成,當時從庫只用作備份和容災,當主庫出現故障時,從庫就手動變成主庫,一般情況下,從庫不作讀寫操作(同步除外)。隨著壓力 的增加,加上了memcached,當時只用其緩存單行數據。 但是,單行數據的緩存并不能很好地解決壓力問題,因為單行數據的查詢通常很快。所以把一些實時性要求不高的Query放到從庫去執行。后面又通過添加多個 從庫來分流查詢壓力,不過隨著數據量的增加,主庫的寫壓力也越來越大。在參考了一些相關產品和其它網站的做法后,進了行數據庫拆分。也就是將數據存放到不 同的數據庫服務器中。
如何進行數據庫拆分?
-
垂直拆分:是指按功能模塊拆分,比如可以將群組相關表和照片相關表存放在不同的數據庫中,這種方式多個數據庫之間的表結構不同。
-
水平拆分:而水平拆分是將同一個表的數據進行分塊保存到不同的數據庫中,這些數據庫中的表結構完全相同。
一 般都會先進行垂直拆分,因為這種方式拆分方式實現起來比較簡單,根據表名訪問不同的數據庫就可以了。但是垂直拆分方式并不能徹底解決所有壓力問題,另外, 也要看應用類型是否合適這種拆分方式。如果合適的話,也能很好的起到分散數據庫壓力的作用。比如對于豆瓣我比較適合采用垂直拆分, 因為豆瓣的各核心業務/模塊(書籍、電影、音樂)相對獨立,數據的增加速度也比較平穩。不同的是,又拍網的核心業務對象是用戶上傳的照片,而照片數據的增 加速度隨著用戶量的增加越來越快。壓力基本上都在照片表上,顯然垂直拆分并不能從根本上解決我們的問題,所以,Yupoo采用水平拆分的方式。
水 平拆分實現起來相對復雜,我們要先確定一個拆分規則,也就是按什么條件將數據進行切分。 一般2.0網站都以用戶為中心,數據基本都跟隨用戶,比如用戶的照片、朋友和評論等等。因此一個比較自然的選擇是根據用戶來切分。每個用戶都對應一個數據 庫,訪問某個用戶的數據時, 要先確定他/她所對應的數據庫,然后連接到該數據庫進行實際的數據讀寫。那么,怎么樣對應用戶和數據庫呢?Yupoo有這些選擇:
1、按算法對應
最 簡單的算法是按用戶ID的奇偶性來對應,將奇數ID的用戶對應到數據庫A,而偶數ID的用戶則對應到數據庫B。這個方法的最大問題是,只能分成兩個庫。另 一個算法是按用戶ID所在區間對應,比如ID在0-10000之間的用戶對應到數據庫A, ID在10000-20000這個范圍的對應到數據庫B,以此類推。按算法分實現起來比較方便,也比較高效,但是不能滿足后續的伸縮性要求,如果需要增加 數據庫節點,必需調整算法或移動很大的數據集, 比較難做到在不停止服務的前提下進行擴充數據庫節點。
2、按索引/映射表對應
這 種方法是指建立一個索引表,保存每個用戶的ID和數據庫ID的對應關系,每次讀寫用戶數據時先從這個表獲取對應數據庫。新用戶注冊后,在所有可用的數據庫 中隨機挑選一個為其建立索引。這種方法比較靈活,有很好的伸縮性。一個缺點是增加了一次數據庫訪問,所以性能上沒有按算法對應好。
比較之后,Yupoo采用的是索引表的方式,我們愿意為其靈活性損失一些性能,更何況我們還有memcached, 因為索引數據基本不會改變的緣故,緩存命中率非常高。所以能很大程度上減少了性能損失。
索 引表的方式能夠比較方便地添加數據庫節點,在增加節點時,只要將其添加到可用數據庫列表里即可。 當然如果需要平衡各個節點的壓力的話,還是需要進行數據的遷移,但是這個時候的遷移是少量的,可以逐步進行。要遷移用戶A的數據,首先要將其狀態置為遷移 數據中,這個狀態的用戶不能進行寫操作,并在頁面上進行提示。 然后將用戶A的數據全部復制到新增加的節點上后,更新映射表,然后將用戶A的狀態置為正常,最后將原來對應的數據庫上的數據刪除。這個過程通常會在臨晨進 行,所以,所以很少會有用戶碰到遷移數據中的情況。當然,有些數據是不屬于某個用戶的,比如系統消息、配置等等,把這些數據保存在一個全局庫中。
分庫帶來的問題如何解決?
分庫會給在應用的開發和部署上都帶來很多麻煩。
1、不能執行跨庫的關聯查詢
如 果我們需要查詢的數據分布于不同的數據庫,沒辦法通過JOIN的方式查詢獲得。比如要獲得好友的最新照片,不能保證所有好友的數據都在同一個數據庫里。一 個解決辦法是通過多次查詢,再進行聚合的方式。所以需要盡量避免類似的需求。有些需求可以通過保存多份數據來解決,比如User-A和User-B的數據 庫分別是DB-1和DB-2, 當User-A評論了User-B的照片時,我們會同時在DB-1和DB-2中保存這條評論信息,我們首先在DB-2中的photo_comments表 中插入一條新的記錄,然后在DB-1中的user_comments表中插入一條新的記錄。這兩個表的結構如下圖所示。這樣我們可以通過查詢 photo_comments表得到User-B的某張照片的所有評論, 也可以通過查詢user_comments表獲得User-A的所有評論。另外可以考慮使用全文檢索工具來解決某些需求, 使用Solr來提供全站標簽檢索和照片搜索服務。
2、不能保證數據的一致/完整性
跨 庫的數據沒有外鍵約束,也沒有事務保證。比如上面的評論照片的例子, 很可能出現成功插入photo_comments表,但是插入user_comments表時卻出錯了。一個辦法是在兩個庫上都開啟事務,然后先插入 photo_comments,再插入user_comments, 然后提交兩個事務。這個辦法也不能完全保證這個操作的原子性。
3、所有查詢必須提供數據庫線索
比 如要查看一張照片,僅憑一個照片ID是不夠的,還必須提供上傳這張照片的用戶的ID(也就是數據庫線索),才能找到它實際的存放位置。因此,必須重新設計 很多URL地址,而有些老的地址我們又必須保證其仍然有效。Yupoo把照片地址改成/photos/{username}/{photo_id}/的形 式,然后對于系統升級前上傳的照片ID, 又增加一張映射表,保存photo_id和user_id的對應關系。當訪問老的照片地址時,通過查詢這張表獲得用戶信息, 然后再重定向到新的地址。
4、自增ID重復的問題
如 果要在節點數據庫上使用自增字段,那么我們就不能保證全局唯一。這倒不是很嚴重的問題,但是當節點之間的數據發生關系時,就會使得問題變得比較麻煩。再來 看看上面提到的評論的例子。如果photo_comments表中的comment_id的自增字段,當我們在DB-2.photo_comments表 插入新的評論時, 得到一個新的comment_id,假如值為101,而User-A的ID為1,那么我們還需要在DB-1.user_comments表中插入(1, 101 …)。 User-A是個很活躍的用戶,他又評論了User-C的照片,而User-C的數據庫是DB-3。 很巧的是這條新評論的ID也是101,這種情況很用可能發生。那么我們又在DB-1.user_comments表中插入一行像這樣(1, 101 …)的數據。 那么我們要怎么設置user_comments表的主鍵呢(標識一行數據)?可以不設啊,不幸的是有的時候(框架、緩存等原因)必需設置。那么可以以 user_id、 comment_id和photo_id為組合主鍵,但是photo_id也有可能一樣(的確很巧)。看來只能再加上photo_owner_id了, 但是這個結果又讓我們實在有點無法接受,太復雜的組合鍵在寫入時會帶來一定的性能影響,這樣的自然鍵看起來也很不自然。所以,Yupoo放棄了在節點上使 用自增字段,想辦法讓這些ID變成全局唯一。為此增加了一個專門用來生成ID的數據庫,這個庫中的表結構都很簡單,只有一個自增字段id。 當我們要插入新的評論時,我們先在ID庫的photo_comments表里插入一條空的記錄,以獲得一個唯一的評論ID。 當然這些邏輯都已經封裝在我們的框架里了,對于開發人員是透明的。 為什么不用其它方案呢,比如一些支持incr操作的Key-Value數據庫。Yupoo還是比較放心把數據放在MySQL里。 另外,Yupoo會定期清理ID庫的數據,以保證獲取新ID的效率。
數據庫優化的實現
前 面提到的一個數據庫節點為Shard,一個Shard由兩個臺物理服務器組成, 可以理解為Node-A和Node-B,Node-A和Node-B之間是配置成Master-Master相互復制的。 雖然是Master-Master的部署方式,但是同一時間還是只使用其中一個,原因是復制的延遲問題, 當然在Web應用里,可以在用戶會話里放置一個A或B來保證同一用戶一次會話里只訪問一個數據庫, 這樣可以避免一些延遲問題。但是Python任務是沒有任何狀態的,不能保證和PHP應用讀寫相同的數據庫。那么為什么不配置成Master-Slave 呢?Yupoo覺得只用一臺太浪費了,所以在每臺服務器上都創建多個邏輯數據庫。 如下圖所示,在Node-A和Node-B上我們都建立了shard_001和shard_002兩個邏輯數據庫, Node-A上的shard_001和Node-B上的shard_001組成一個Shard,而同一時間只有一個邏輯數據庫處于Active狀態。 這個時候如果需要訪問Shard-001的數據時,我們連接的是Node-A上的shard_001, 而訪問Shard-002的數據則是連接Node-B上的shard_002。以這種交叉的方式將壓力分散到每臺物理服務器上。 以Master-Master方式部署的另一個好處是,可以不停止服務的情況下進行表結構升級, 升級前先停止復制,升級Inactive的庫,然后升級應用,再將已經升級好的數據庫切換成Active狀態, 原來的Active數據庫切換成Inactive狀態,然后升級它的表結構,最后恢復復制。 當然這個步驟不一定適合所有升級過程,如果表結構的更改會導致數據復制失敗,那么還是需要停止服務再升級的。
前 面提到過添加服務器時,為了保證負載的平衡,需要遷移一部分數據到新的服務器上。為了避免短期內遷移的必要,在實際部署的時候,每臺機器上部署了8個邏輯 數據庫, 添加服務器后,只要將這些邏輯數據庫遷移到新服務器就可以了。最好是每次添加一倍的服務器, 然后將每臺的1/2邏輯數據遷移到一臺新服務器上,這樣能很好的平衡負載。當然,最后到了每臺上只有一個邏輯庫時,遷移就無法避免了,不過那應該是比較久 遠的事情了。
Yupoo把分庫邏輯都封裝在我們的PHP框架里了,開發人員基本上不需要被這些繁瑣的事情困擾。下面是使用框架進行照片數據的讀寫的一些例子:
<?php$Photos= newShardedDBTable('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的照片,注意第一個參數為用戶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'));?>首 先要定義一個ShardedDBTable對象,所有的API都是通過這個對象開放。第一個參數是對象類型名稱, 如果這個名稱已經存在,那么將返回之前定義的對象。你也可以通過get_table(‘Photos’)這個函數來獲取之前定義的Table對象。 第二個參數是對應的數據庫表名,而第三個參數是數據庫線索字段,你會發現在后面的所有API中全部需要指定這個字段的值。 第四個參數是字段定義,其中photo_id字段的global_auto_increment屬性被置為true,這就是前面所說的全局自增ID, 只要指定了這個屬性,框架會處理好ID的事情。
如果我們要訪問全局庫中的數據,我們需要定義一個DBTable對象。
<?php$Users= newDBTable('Users', 'yp_users', array('user_id' => array('type'=> 'long', 'primary'=> true, 'auto_increment'=> true),'username'=> array('type'=> 'string'),));?>DBTable是ShardedDBTable的父類,除了定義時參數有些不同(DBTable不需要指定數據庫線索字段),它們提供一樣的API。
六、緩存方案的選擇
Yupoo使用的框架自帶緩存功能,對開發人員是透明的。
<?php$photo= $Photos->load(1, 10001);?>比如上面的方法調用,框架先嘗試以Photos-1-10001為Key在緩存中查找,未找到的話再執行數據庫查詢并放入緩存。當更改照片屬性或刪除照片時,框架負責從緩存中刪除該照片。這種單個對象的緩存實現起來比較簡單。稍微麻煩的是像下面這樣的列表查詢結果的緩存。
<?php$photos= $Photos->fetch(array('user_id'=> 1, 'posted_date__gt'=> '2010-06-01'));?>Yupoo 把這個查詢分成兩步,第一步先查出符合條件的照片ID,然后再根據照片ID分別查找具體的照片信息。 這么做可以更好的利用緩存。第一個查詢的緩存Key為Photos-list-{shard_key}-{md5(查詢條件SQL語句)}, Value是照片ID列表(逗號間隔)。其中shard_key為user_id的值1。目前來看,列表緩存也不麻煩。 但是如果用戶修改了某張照片的上傳時間呢,這個時候緩存中的數據就不一定符合條件了。所以,需要一個機制來保證我們不會從緩存中得到過期的列表數據。我們 為每張表設置了一個revision,當該表的數據發生變化時(調用insert/update/delete方法), 我們就更新它的revision,所以我們把列表的緩存Key改為Photos-list-{shard_key}-{md5(查詢條件SQL語 句)}-{revision}, 這樣我們就不會再得到過期列表了。
revision 信息也是存放在緩存里的,Key為Photos-revision。這樣做看起來不錯,但是好像列表緩存的利用率不會太高。因為我們是以整個數據類型的 revision為緩存Key的后綴,顯然這個revision更新的非常頻繁,任何一個用戶修改或上傳了照片都會導致它的更新,哪怕那個用戶根本不在我 們要查詢的Shard里。要隔離用戶的動作對其他用戶的影響,我們可以通過縮小revision的作用范圍來達到這個目的。 所以revision的緩存Key變成Photos-{shard_key}-revision,這樣的話當ID為1的用戶修改了他的照片信息時, 只會更新Photos-1-revision這個Key所對應的revision。
因 為全局庫沒有shard_key,所以修改了全局庫中的表的一行數據,還是會導致整個表的緩存失效。 但是大部分情況下,數據都是有區域范圍的,比如幫助論壇的主題帖子, 帖子屬于主題。修改了其中一個主題的一個帖子,沒必要使所有主題的帖子緩存都失效。 所以在DBTable上增加了一個叫isolate_key的屬性。
<?php$GLOBALS['Posts'] = newDBTable('Posts', 'yp_posts', array('topic_id' => array('type'=> 'long', 'primary'=> true),'post_id' => array('type'=> 'long', 'primary'=> true, 'auto_increment'=> true),'author_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');?>注意構造函數的最后一個參數topic_id就是指以字段topic_id作為isolate_key,它的作用和shard_key一樣用于隔離revision的作用范圍。
ShardedDBTable 繼承自DBTable,所以也可以指定isolate_key。 ShardedDBTable指定了isolate_key的話,能夠更大幅度縮小revision的作用范圍。 比如相冊和照片的關聯表yp_album_photos,當用戶往他的其中一個相冊里添加了新的照片時, 會導致其它相冊的照片列表緩存也失效。如果指定這張表的isolate_key為album_id的話, 我們就把這種影響限制在了本相冊內。
緩 存分為兩級,第一級只是一個PHP數組,有效范圍是Request。而第二級是memcached。這么做的原因是,很多數據在一個Request周期內 需要加載多次,這樣可以減少memcached的網絡請求。另外Yupoo的框架也會盡可能的發送memcached的gets命令來獲取數據, 從而減少網絡請求。
參考文章:http://www.infoq.com/cn/articles/yupoo-partition-database
另外推薦學習:Facebook圖片存儲架構Yupoo(又拍網)的系統架構
轉載于:https://www.cnblogs.com/dasn/articles/3209639.html
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的转:Yupoo(又拍网)的系统架构的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 重新注册dll
- 下一篇: 超短的判断IE javascript代码