Instagram是一家基于iOS和Android的社交圖片照片分享應用開發商。憑借著獨特的運營理念,自2010年3月成立以來,短短一年的時間就吸引了1400萬用戶。而后隨著手機相機改動、圖像處理升級、與Facebook等社交靈活交互、支持Android等服務不斷升級,用戶量迅速沖擊3000萬,于2012年9月被Facebook以7.15億美元收購。而截止到今年2月底,其活躍用戶成功突破1億。
Instagram兩位創始人
與高速增長相背離的是,從成立之初僅有凱文·希斯特羅姆(Kevin Systrom)和邁克·克里格(Mike Krieger)兩位創始人,到2011年獲得A輪風投700萬美元的4位員工,再到被收購時的13人團隊,Instagram人員組織一直極為精簡。
如此小規模的團隊居然可以如此自如地應對飛速增長的用戶數并提供創新服務,這不能不說是硅谷的又一個財富傳奇。以至于Instagram技術團隊撰寫的《Instagram:數百的實例 大量的技術》一經發布,就獲得了創業企業CTO們的熱烈回應。彼時,Instagram的團隊還在尋找一個“可以馴服EC2 實例群的DevOps”。
沒有想到,收購如此來勢洶洶。2012年4月10日,Facebook宣布收購Instagram。兩天之后,Instagram的聯合創始人Mike Krieger公開發表《如何成為十億美元公司》演講,第一次向外界全面地展現了Instagram創業歷程以及其中不得不說的技術“秘密.”。本文為演講PPT全文翻譯,有助于創新技術團隊更好認識和了解Instagram13人團隊創造奇跡所依賴的技術:
Instagram技術團隊:
- 2010年: 2位工程師
- 2011年: 3位工程師
- 2012年: 5位工程師
Instagram核心原則:
- 1 simplicity(簡潔主義)
- 2 optimize for minimal operational burden(為盡量減少運維負擔而優化)
- 3 instrument everything(監控一切)
一、初創階段:
兩名沒有任何后端的實戰經驗的創始人;
通過托管在洛杉磯某處的一臺機器(甚至性能都沒有MacBook Pro強);
存儲采用CouchDB(Apache CouchDB 是一個面向文檔的數據庫管理系統);
產品上線第一天有25000注冊用戶。
二、上線階段:
因為忘記favicon.ico圖標文件,在Django上引起大量404錯誤。
這是第一個經驗教訓。后面還有:
ulimit -n ,設置Linux內核可以同時打開的文件描述符的最大值,例如size為4092。
memcached -t 4,設置用于處理請求的線程數。
prefork/postfork 線程的預加載還是后加載。
顯然,絕大多數系統擴展問題繁瑣而困難。而在不斷出現問題并解決問題的過程中,Instagram決定遷往AWS的EC2。
三、遷移階段:
“let’s move to EC2”就像是“對100碼速度行駛的汽車更換所有部件”。
具體分析:
1. 數據庫擴展
早期:django ORM+postgresql(PostGIS)
因為PostGIS而選擇了postgresql,PostGIS在對象關系型數據庫PostgreSQL上增加了存儲管理空間數據的能力,相當于Oracle的spatial部分,數據庫可部署在獨立服務器上。
隨著照片數量的爆發式增長,最大內存為68G的EC2顯然無法支持。
改變:進行vertical partitioning(垂直分區),并通過django db routers使垂直分區更加容易。
如:照片則映射到photodb
def db_for_read(self, model):<br> if app_label == 'photos':<br> return 'photodb'
幾個月以后,photodb>60G的時候,采用horizontal partitioning(水平分區,用“分片”sharding實現)
但sharding也帶來諸多問題:
1). 數據檢索(多數情況下,很難知道用戶的主訪問模式)
2). 當有分片變得太大的時候怎么辦?
可以采用,基于范圍的分片策略(如MongoDB一樣)
3). 性能下降,特別是由EC2實例磁盤IO導致,解決方法是:預先切分(pre-split),即預先切分上千個邏輯切片,將它們映射到較少的物理分區節點中去。
2. 選擇合適工具
進行緩存/反規范化數據設計
用戶上傳圖片時:
1). 用戶上傳帶有標題信息和地理位置信息(可選)的照片;
2). 同步寫到這個用戶對應的數據庫(分片)中;
3). 進行隊列化處理
a 如果帶有地理位置信息,通過異步的POST請求,將這個圖片的信息送到Solr(Instagram 用于geo-search API的全文檢索服務器)。
b 跟隨者的信息分發(follower delivery),即告訴我的follower ,我發布了新的照片。如何來實現的呢?每個用戶都有一個follower 列表,新照片上傳時會把照片ID發送給列表中的每一個用戶,用Redis 來處理這一業務簡直太棒了,快速插入,快速子集化。
c 當需要生成feed的時候,我們通過ID+#的格式,直接在memcached中查找信息
Redis適合什么樣的場景?
1).數據結構相對有限;
2).對頻繁GET的地方,對復雜對象進行緩存;
不要將自己綁定在非得以內存數據庫為主要存儲策略的方案上。
關于Follow圖譜
第一版:簡單的數據庫表格(source_ id, target_id, status)
需要來回答如下查詢:我關注誰?誰關注我?我是否關注某人?某人是否關注我?
當數據庫的壓力變大時,Instagram開始在Redis中并發存儲關注圖譜,但這也帶來了內容一致性(consistency)的問題。而不一致性一度會帶來緩存失效問題。
PostGIS結合輕量的memcached緩存,可以支撐上萬的請求量。
需要注意點:
1). 核心數據存儲部分有一個萬能的組件支撐,就像:Redis;
2).千萬不要試想用兩種工具去做同一個工作;
3. 保持敏捷
1). 廣泛的單元測試和功能測試
2). 堅持DRY(Don’t Repeat Yourself)原則
3). 使用通知/信號機制實現解耦
4). 我們大部分工作使用Python來完成,只有逼不得已的時候,才會用C
5). 頻繁的代碼復查,盡量保持“智慧共享”。
6). 廣泛的系統監控
4. 往Android平臺擴展
12小時增加100萬新用戶的關鍵:
1). 偉大的工具可以使讀取更具擴展性,例如:redis: slaveof <host> <port>(SLAVEOF 命令用于在 Redis 運行時動態地修改復制(replication)功能的行為);
2). 更短的迭代周期;
3). 不要重復發明輪子,例如想開發一個系統監控的守護進程,完全沒有必要,HAProxy完全能勝任這一工作;
4). 找強大的技術顧問;
5). 技術團隊保持開放的氛圍并積極回饋開源世界;
6). 關注優化,想辦法讓系統速度快上一倍。
7). 保持敏捷;
8).使用最少部件,最干凈的解決方案;
9). 不要過度的優化,除非你提前知道自己的系統將如何擴展
PPT很好的保持了Instagram“simplicity”的哲學,即使提到技術,也精簡到了極致。為了讓更多朋友明了,特別從其工程師博客上選擇更多細節來補足,而這里,是這5位工程師實踐經驗的總結,其中不乏那些極為實用的開源工具。
四、其他細節技術
1. 操作系統/主機
在Amazon EC2上跑Ubuntu Linux 11.04 (“Natty Narwhal”),這個版本經過驗證在 EC2 上夠穩定。但之前的版本在EC2上高流量的時候都會出現各種不可預測的問題。
2. 負載均衡
每一個對Instagram 服務器的訪問都會通過負載均衡服務器;我們使用2臺Nginx機器做DNS輪詢。這種方案的缺點是當其中一臺退役時,需要花時間更新DNS。最近,轉而使用Amazon的ELB(Elastic Load Balancer)負載均衡器,使用3個Nginx 實例可以實現調入調出(而當某個Nginx實例通不過故障檢測,系統會自動將其從循環中抽離);同時在 ELB 層停掉了 SSL , 以緩解nginx的 CPU 壓力。使用Amazon的Route53服務作為DNS服務,這是AWS控制臺上增加的一套很好的GUI工具。
3. 應用服務器
在Amazon的High-CPU Extra-Large機器上運行了Django ,隨著用戶的增長,已經在上面跑了25個Django實例了(幸運地,因為是無狀態的,所以非常便于橫向擴展)。但發現個別工作負載是屬于計算密集型而非IO密集型,因此High-CPU Extra-Large類型的實例剛好提供了合適的比重(CPU和內存)。
為此,使用 Gunicorn 作為 WSGI 服務器。過去曾用過 Apache 下的 mod_wsgi 模塊,不過發現 Gunicorn 更容易配置并且節省 CPU 資源。使用 Fabric 加速部署。Fabric最近增加了并行模式,因此部署只需要花費幾秒鐘。
4. 數據存儲
大部分數據(用戶信息,照片的元數據、標簽等)存儲在PostgreSQL中;并基于不同的Postgres 實例進行切分的。主要分片集群包含12個四倍超大內存云主機(且12個副本在不同的區域);
而亞馬遜的網絡磁盤系統(EBS)每秒的尋道能力不夠,因此,將所有工作放到內存中就變得尤為重要。為了獲得合理的性能,創建了軟 RAID 以提升 IO 能力,使用的 Mdadm 工具進行 RAID 管理;
這里,vmtouch用來管理內存數據是個極好的工具,尤其是在故障轉移時,從一臺機器到另一臺機器,甚至沒有活動的內存概要文件的情況。這里是腳本,用來解析運行于一臺機器上的vmtouch 輸出并打印出相應vmtouch命令,在另一臺機器上執行,用于匹配他當前的內存狀態;
所有的PostgreSQL實例都是運行于主-備模式(Master-Replica),基于流復制,并且使用EBS快照經常備份我們的系統。為了保證快照的一致性(原始靈感來源于ec2-consistent-snapshot)使用XFS作為我們的文件系統,通過XFS,當進行快照時,可以凍結&解凍RAID陣列。為了進行流復制,我們最愛的工具是repmgr 。
對于從應用服務器連接到數據,我們很早就使用了Pgbouncer做連接池,此舉對性能有巨大的影響。我們發現Christophe Pettus的博客 有大量的關于Django、PostgreSQL 和Pgbouncer 秘訣的資源。
照片直接存儲在亞馬遜的S3,當前已經存儲了幾T的照片數據。使用亞馬遜的CloudFront作為我們的CDN,這加快了全世界用戶的照片加載時間。
為了geo-search API,我們一直使用PostgreSQL了很多個月,不過后來遷移到了Apache Solr.他有一套簡單的JSON接口,這樣我們的應用程序相關的,只是另一套API而已。
最后,和任何現代Web服務一樣,使用了Memcached 做緩存,并且當前已經使用了6個Memcached 實例,我們使用pylibmc & libmemcached進行連接。Amzon最近啟用了一個靈活的緩存服務(Elastic Cache service),但是它并不比運行我們自己的實例便宜,因此我們并沒有切換上去;
5. 任務隊列&推送通知
當一個用戶決定分享一張Instagram 的照片到Twitter 或Facebook,或者是當我們需要通知一個 實時訂閱者有一張新的照片貼出,我們將這個任務推到 Gearman,一個任務隊列系統能夠寫于Danga。這樣做的任務隊列異步通過意味著媒體上傳可以盡快完成,而“重擔”可以在后臺運行。我們大概有200個工作實例(都用Python寫的)在給定的時間內對隊列中的任務進行消費,并分發給不同的服務。我們的feed feed fan-out也使用了Gearman,這樣posting就會響應新用戶,因為他有很多followers。
對于消息推送,找到的最劃算的方案是,一個開源的Twisted 服務,已經為我們處理了超過10億條通知,并且絕對可靠。
6. 監控
基于 Python-Munin,寫了很多Munin 插件,使用Munin進行圖形化度量。用于圖形化度量非系統級的東西(例如,每秒的簽入人數,每條照片發布數等),使用Pingdom作為外部監控服務,PagerDuty 用于事件通知。
Python錯誤報告,是使用的Sentry,一個Disqus的工程師寫的令人敬畏的開源的Django app。在任何時間,都可以實時的查看系統發生了什么錯誤。
文章到這里,并沒有結束。對今天已經跨越“億”線的Instagram而言,“為了最小的運營負擔而優化程序,利用一切能用到的(開源)工具與云平臺,極簡的技術主張”未變,不信,看看最近的新聞,《Instagram的負載均衡武器:Eureka填補了Amazon Web Services的大缺口》還在續寫屬于他自己的技術傳奇。(@紅心李的分析對本文也有貢獻,審校/仲浩)
鏈接:Mike Krieger《如何成為十億美元公司》演講PPT