人人網的系統架構
今天在網上看到人人網使用的開源軟體列表 ,人人網的架構師寫的,看完後,大概瞭解了人人網的架構資訊
資料層
使用MySQL ,同時使用Tokyo Cabinet(Key-value的儲存引擎,簡稱TC)做一個資料冗餘,TC代替MySQL做儲存,例如搜尋結果頁的使用者資料,但Tokyo Cabinet沒有網路處理能力,需要使用Tokyo Tyrant以提供網路接入能力,並調用Tokyo Cabinet的API進行持久化儲存
Tokyo Tyrant其實也是Tokyo Cabinet的作者開發的,主要是支援Memcached傳輸協議的網路介面,可以理解為Tokyo Tyrant處理網路連接,協議解析,然後調用Tokyo Cabinet的API來完成持久化儲存
服務端
Web Server : Nginx, 使用Nginx的代理能力,做跨IDC的請求代理,同時與Java Server–Resin搭配,解決Resin網路連接處理能力弱的問題
Java Server: Resin 替代Tomcat作為Java伺服器
Proxy 伺服器 : Squid 做圖片檔案的反向 Proxy緩衝
Linux伺服器叢集系統: LVS(Linux Virtual Server),使用它的4層的負載平衡,替代了很多硬體的負載平衡裝置
架構
Java web架構: Struts,王興同學一開始寫校內網果然是用structs,現在的人人網開始慢慢捨棄原有的架構,並自己開發了一個web架構,傳聞將來也會開源出來
搜尋引擎架構: 基於Lucence提供搜尋人的服務
網路架構:Netty,一個Java的網路架構,和apache的mina類似,但比mina更高效,用來提供一些小的服務
伺服器系統監控
ganglia Ganglia是一個跨平台可擴充的,高效能運算系統下的分布式監控系統,如叢集和網格
應用程式層緩衝
Memcached 一個純記憶體的key-value的cache系統,使用spymemcached作為java的Client
ICE : 一個跨語言的網路通訊架構,架構本身提供了強大的通訊能力,管理工具,負載平衡方案,其跨語言能力也是一個很大的亮點,基於這個架構之上,我們選用合適的語言來提供合適的服務,比如我們使用C++來開發Cache服務,使用Java來開發一些邏輯服務。架構本身可以很重,也可以很輕,具體要看你怎麼用:)
Memcached和ICE的快取服務區別
對Cache的操作粒度不一樣,Memcached對Cache對象以binary byte作為一個整體來操作,需要頻繁的序列化和還原序列化,我們使用ICE提供的Cache服務,可以以Cache對象的一個或者多個欄位來操作,比如一個使用者物件,我們可以只更新它的姓名。
人人網中介層:問題篇
由開源軟體組成的系統
與很多大型的網站一樣,人人網的系統全部是由開源軟體構建的。使用Nginx做前端接入,resin做容器,Memcached做通用cache,MySQL做資料庫,使用Linux作業系統。
除了上述的部分外,人人網還有一個與眾不同的中介層。中介層以服務的形式存在,位於MySQL和resin中間,提供高並發低成本的資料訪問層。
資料庫的壓力
在上述結構系統中,資料庫的效能往往成為系統瓶頸。人人網在發展的過程中不斷重構,改變最大的就是資料庫部分。大概的步驟是“最佳化SQL”,“業務拆分”,“垂直分割”和“水平分割”幾個階段,關於資料庫最佳化的細節將來再引用到這裡。
經過最佳化後的資料庫,單台可以承擔每秒3000次的主鍵查詢。再提高效能的最佳化,我們採用的方案是使用中介層。
效能目標
增加中介層可以在不增加伺服器數量的前提下,提高服務的整體效能,並且提高系統的可擴充性。這裡簡要列舉一些使用中介層服務最佳化的效果。
即時更新的資料
使用者的個人資訊資料,目前的寫操作500次/秒,讀操作2萬次/秒。這些資料分布在數十個資料表中,如果用資料庫做10次主鍵查詢,需要的時間將會非常可觀。中介層的快取服務把這個效能穩定在了99.9%的請求時間小於20ms。
判斷好友關係,讀操作900次/秒。這個操作現在使用6G記憶體儲存了所有的好友關係,在2ms內返回任意兩人的好友關係。
關聯查詢,僅好友名單就有1300次/秒。如果使用關聯查詢,資料庫需要同步很多無用的欄位。現在只需要兩次記憶體請求,並且衍生出很多種類的排序。
大量彙總的訪問
彙總的頁面在SNS中是訪問量最大的部分。首頁整合的功能多達17個模組,這些模組之間的關係相對獨立。為了快速的把這些資料集合在一起,就需要迅速擷取資料。
我們對整體技術架構的要求是,關鍵頁面執行時間要在100ms以內。
Session同步
眾多的resin伺服器之間,如何共用使用者身分識別驗證的結果,在各種session共用機制中,我們的方案是使用中介層服務來集中儲存的。
待續
問題篇只是開端,接下來的“求解篇”將會分析人人網中介層的主要應用情境。“實踐篇”將會舉例一個典型的中介層服務。
人人網中介層:求解篇
為了提高效能,在人人網的技術結構中,在資料庫和頁面之間,有中介層。中介層高效能的基礎是用記憶體代替磁碟。
用記憶體代替磁碟
資料庫系統的最大瓶頸在磁碟IO,大量的小資料請求不是磁碟的強項。人人網中介層服務就是利用了記憶體代替硬碟的方法來提高整體效能。有了這層服務以後,以前的資料庫關聯查詢被提前計算並緩衝,需要訪問時直接擷取。
通用的Memcached緩衝方案也有些不足,資料不能自己變化,也不能部分變化。於是人人網選擇了自己實現緩衝的方式。
在自己實現緩衝的過程中,管理記憶體相對容易,通訊協定是比較複雜的部分,我們在這方面選擇了開源的Ice通訊架構(http://www.zeroc.com)來完成繁瑣的工作,至今它都工作的很好。
Ice通訊架構在人人網完成了兩件事,通訊和定位。用戶端通過IceGrid組件定位到需要的服務地址,將請求發送到中介層服務,中介層服務將結果返回。用戶端只需要知道一個地址就可以找到所有的服務;同時,眾多服務也可以在不同的伺服器之間隨意遷移。在現在的人人網有超過500個Ice寫成的中介層服務在運行。
定製的記憶體資料
用Ice解決了通訊和部署的問題後,中介層服務就是核心的資料結構管理。概括的說,就是靈活變化,保證速度。下面列舉若干使用了中介層服務的情況
一份資料 多種排序
在人人網的好友頁,有很多排序方式可以顯示好友名單。每種列表都是從一個按ID排序的服務中擷取的,再經過排序,緩衝在各個順序的列表中。
隨時間變化的資料
在很多列表頁面,都會顯示“線上標誌”,這個標誌是冗餘在各個列表的緩衝當中,定期重新整理的。這些需要和cache一起實現的商務邏輯,在人人網中介層當中非常普遍。
特殊類型
我們用了一個bit儲存使用者的啟用狀態。200M記憶體可以儲存全部int範圍的狀態。並且查詢和更新速度飛快。
接下來的實踐篇將會用這個為例子展示中介層的實現。
人人網中介層:實踐篇
之前的問題篇和求解篇描述了人人網在發展過程中遇到的問題,並且介紹了我們採用中介層來提高效能的解決方案。今天的實踐篇將通過一個例子來實現一個中介層服務。
這個服務要達到的目的是快速的查詢使用者是否有效,資料將要使用bitset儲存在記憶體中,每個使用者一位,僅儲存正整數約21億,佔用記憶體256M。
開始編碼
下面的代碼都在這個位置儲存:http://gitorious.org/renren/bitserver。
介面定義
定義介面如下:
#include <Ice/BuiltinSequences.ice>
module renren {
struct BitSegment {
int begin;
int end;
Ice::ByteSeq data;
};
interface BitServer {
bool get(int offset);
Ice::BoolSeq gets(Ice::IntSeq offsets);
BitSegment getSegment(int begin, int end);
};
};
這個BitServer.ice檔案,通過slice2cpp命令編譯成為服務端的Skeleton檔案:
slice2cpp -I/opt/Ice-3.3/slice BitServer.ice
服務端
有了上面產生的服務端檔案後,就可以實現我們自己的業務功能了。
BitServerI.h和BitServerI.cpp,暫時只是實現了單個get的介面。
#ifndef __BitServerI_h__
#define __BitServerI_h__#include <BitServer.h>
#define SIZE_OF_BIT 2147483647
#include <bitset>
namespace renren
{
class BitServerI : virtual public BitServer
{
public:
void initialize();
virtual bool get(::Ice::Int,
const Ice::Current&);
virtual ::Ice::BoolSeq gets(const ::Ice::IntSeq&,
const Ice::Current&);
virtual ::renren::BitSegment getSegment(::Ice::Int,
::Ice::Int,
const Ice::Current&);
private:
std::bitset<SIZE_OF_BIT> bits_;
};
}
#endif
#include <BitServerI.h>
#include <Ice/Ice.h>int main(int argc, char** argv) {
int status = 0;
Ice::CommunicatorPtr ic;
try{
ic = Ice::initialize(argc, argv);
Ice::ObjectAdapterPtr adapter = ic->createObjectAdapter(“BitServer”);
renren::BitServerI* obj = new renren::BitServerI;
obj->initialize();
adapter->add(obj, ic->stringToIdentity(“BitServer”));
adapter->activate();
ic->waitForShutdown();
} catch (const Ice::Exception& e) {
std::cerr << e << std::endl;
status = 1;
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
status = 1;
} catch (…) {
std::cerr << “unknown exception” << std::endl;
status = 1;
}
if (ic) {
try {
ic->destroy();
} catch (const Ice::Exception& e) {
std::cerr << e << std::endl;
status = 1;
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
status = 1;
} catch (…) {
std::cerr << “unknown exception” << std::endl;
status = 1;
}
}
return status;
}
void
renren::BitServerI::initialize() {
for (int i=0; i<0xFFFFF;i=i+2) {
bits_[i]=true;
}
}
bool
renren::BitServerI::get(::Ice::Int offset,
const Ice::Current& current)
{
if(offset < 0) return false;
return bits_[offset];
}
::Ice::BoolSeq
renren::BitServerI::gets(const ::Ice::IntSeq& offsets,
const Ice::Current& current)
{
return ::Ice::BoolSeq();
}
::renren::BitSegment
renren::BitServerI::getSegment(::Ice::Int begin,
::Ice::Int end,
const Ice::Current& current)
{
return ::renren::BitSegment();
}
用戶端
我們使用Java作為用戶端,首先用slice2java工具產生Java的Proxy類。
slice2java -I/opt/Ice-3.3/slice BitServer.ice
然後自己實現用戶端代碼:
package renren;class BitServerAdapter {
private final String endpoints_;
private Ice.Communicator ic_;
private renren.BitServerPrx prx_;
public BitServerAdapter(String endpoints) {
this.endpoints_ = endpoints;
}
public void initialize() {
ic_ = Ice.Util.initialize();
prx_ = renren.BitServerPrxHelper.uncheckedCast(ic_.stringToProxy(endpoints_));
}
public boolean get(int id) {
return prx_.get(id);
}
public static void main(String[] args) {
BitServerAdapter adapter = new BitServerAdapter(args[0]);
adapter.initialize();
boolean ret = adapter.get(Integer.valueOf(args[1]));
System.out.println(ret);
System.exit(0);
}
}
效能測試
完成了代碼,來測試一下效能吧。
首先啟動伺服器
target/bitserver –Ice.Config=config
再啟動用戶端
java -cp /opt/Ice-3.3/lib/Ice.jar:target/bitclient.jar \
renren.BitServerAdapter “BitServer:default -p 10000” 1022
在用戶端調用增加迴圈50000次,單線程平均每秒處理一萬次。
在多線程的環境下,單個伺服器每秒可處理的請求8萬次左右,已經超過了目前的需要。