【gperftools】1——CPU profiler

1 gperftools 簡介

gperftools 是一款 Google 的開源高性能內(nèi)存相關工具集奠蹬,包括 tcmalloc 內(nèi)存管理工具做个,還有一些例如 cpu profilerheap profiler 等性能分析工具,本系列將逐一介紹。

2 CPU profiler 簡介

CPU profiler 主要是通過采樣的的方式,給出一段時間內(nèi)程序?qū)嶋H占用cpu時間偏進行統(tǒng)計和分析刚盈,優(yōu)點是使用起來簡潔方便。

性能分析通過抽樣方法完成道媚,默認是1秒100個樣本扁掸,一個樣本是10毫秒,所以如果程序運行時間不到10ms最域,那么得到的結(jié)果可能會和開始執(zhí)行的時候不同谴分。

3 本次測試簡介

  1. 使用的是 ubuntu 20.04 系統(tǒng)。
  2. 采用直接調(diào)用提供的 API(在需要測試的代碼的前后分別調(diào)用 ProfilerStart()ProfilerStop())的方式進行測試镀脂。

4 安裝環(huán)境并測試

安裝 unwind

sudo apt install libunwind-dev

安裝 gperftools

cd ~/Download
git clone https://github.com/gperftools/gperftools.git
cd gperftools
sh autogen.sh
./configure
make all
sudo make install

編寫測試代碼牺蹄,監(jiān)控開始,參數(shù)為需要生成的文件名:

/* Start profiling and write profile info into fname, discarding any
 * existing profiling data in that file.
 *
 * This is equivalent to calling ProfilerStartWithOptions(fname, NULL).
 */
PERFTOOLS_DLL_DECL int ProfilerStart(const char* fname);

監(jiān)控結(jié)束:

/* Stop profiling. Can be started again with ProfilerStart(), but
 * the currently accumulated profiling data will be cleared.
 */
PERFTOOLS_DLL_DECL void ProfilerStop(void);

如果開啟了新的線程薄翅,需要在線程起始添加如下函數(shù)進行線程的的注冊沙兰,但測試發(fā)現(xiàn)有無該語句并不造成影響。

/* Routine for registering new threads with the profiler.
 */
PERFTOOLS_DLL_DECL void ProfilerRegisterThread(void);

使用時需要包含的頭文件:

#include <gperftools/profiler.h>

測試代碼
https://github.com/ixiaolonglong/memory_tool/blob/master/gperftools/tests/cpu_profiler/cpu_profiler_test.cpp

編譯測試代碼

g++ cpu_profiler_test.cpp -o cpu_profiler_test --lprofiler

執(zhí)行 cpu_profiler_test翘魄,生成 .profile 文件鼎天。

5 無環(huán)境情況下測試

有時候并不能要求所有編譯程序的環(huán)境都安裝一遍 gperftoolsunwind,更多的是直接編譯運行暑竟,所以需要在編譯可執(zhí)行文件時就要準備好相關的源文件斋射。

在這個項目中本人將源碼作為 submodule,編譯時本地生成 lib 文件但荤,可以直接進行鏈接罗岖。

工程如下,使用流程詳見 README.md 文件:
https://github.com/ixiaolonglong/memory_tool/tree/master/gperftools

在編譯工程時需要添加如下編譯選項腹躁,避免被編譯器優(yōu)化:

# tcmalloc options
add_compile_options(
    -fno-builtin-malloc
    -fno-builtin-calloc
    -fno-builtin-realloc
    -fno-builtin-free)

這里記錄一下靜態(tài)庫與動態(tài)庫鏈接桑包,對于靜態(tài)庫來說,需要描述所有遞歸用到的 lib:

target_link_libraries(cpu_profiler_test PRIVATE
                                profiler
                                fake_stacktrace_scope
                                sysinfo
                                spinlock
                                maybe_threads
                                logging
                                unwind
                                pthread)

而如果是動態(tài)庫纺非,則鏈接就會簡單很多:

target_link_libraries(cpu_profiler_test PRIVATE
                                profiler
                                pthread)

https://www.zhihu.com/question/277160878
這個不是cmake的坑哑了,應該是你glog庫的坑赘方,我猜想glog庫是你自行編譯,而glog庫編譯時沒有動態(tài)鏈接gflags導致的垒手。如果你生成動態(tài)庫時蒜焊,就使用target link library生成倒信,再配合上rpath尋找路徑科贬,是可以支持a依賴b,b依賴c鳖悠,而你在a中只要寫b的依賴而不用寫c的依賴榜掌。

6 報告

執(zhí)行程序的環(huán)境不一定非要安裝 gperftools,但生成的 profile 文件時必須要安裝 gperftools 使用其 pprof 工具進行解析乘综。

需要安裝圖形工具 Graphviz

sudo apt-get install graphviz

生成不同類型的報告命令:

# 生成性能報告(層次調(diào)用節(jié)點有向圖)輸出到web瀏覽器顯示
pprof cpu_profiler_test cpu_test.profile --web

# 生成pdf格式的性能報告(層次調(diào)用節(jié)點有向圖)
pprof cpu_profiler_test cpu_test.profile --pdf > prof.pdf

# 生成文本格式的性能報告輸出到控制臺
pprof cpu_profiler_test cpu_test.profile --text
6.1 文本
Total: 30 samples
       6  20.0%  20.0%       16  53.3% psiginfo
       5  16.7%  36.7%        5  16.7% __nss_database_lookup
       4  13.3%  50.0%        4  13.3% _IO_default_xsputn
       4  13.3%  63.3%       28  93.3% __snprintf
       3  10.0%  73.3%        3  10.0% _IO_enable_locks
       3  10.0%  83.3%       24  80.0% vscanf
       2   6.7%  90.0%        2   6.7% _IO_str_pbackfail
       1   3.3%  93.3%        1   3.3% cuserid
       1   3.3%  96.7%        9  30.0% test_main_thread
       1   3.3% 100.0%       21  70.0% test_other_thread
       0   0.0% 100.0%       21  70.0% RunFunctionInThread
       0   0.0% 100.0%        9  30.0% __libc_start_main
       0   0.0% 100.0%        9  30.0% _start
       0   0.0% 100.0%       21  70.0% clone
       0   0.0% 100.0%        9  30.0% main
       0   0.0% 100.0%       21  70.0% start_thread

上面文本中輸出的內(nèi)容是對程序中每一個函數(shù)的CPU使用時間分析憎账,數(shù)據(jù)有兩大列:

  • 左:不包含內(nèi)部其他函數(shù)調(diào)用所消耗的CPU時間(內(nèi)聯(lián)函數(shù)除外)如果函數(shù)內(nèi)部沒有任何調(diào)用,那么就和右列相等
  • 右:整個函數(shù)消耗的CPU時間卡辰,包括函數(shù)內(nèi)部其他函數(shù)調(diào)用所消耗的CPU時間

每行按照數(shù)據(jù)順序:

  1. 分析樣本數(shù)量(不包含其他函數(shù)調(diào)用)
  2. 分析樣本百分比(不包含其他函數(shù)調(diào)用)
  3. 目前為止的分析樣本百分比(不包含其他函數(shù)調(diào)用)
  4. 分析樣本數(shù)量(包含其他函數(shù)調(diào)用)
  5. 分析樣本百分比(包含其他函數(shù)調(diào)用)
  6. 函數(shù)名
6.2 圖形
profile.png

每個節(jié)點代表一個函數(shù)胞皱,節(jié)點數(shù)據(jù)格式:

  • Class Name
  • Method Name
  • local (percentage) ,不包含內(nèi)部其他函數(shù)調(diào)用所消耗的CPU時間(內(nèi)聯(lián)函數(shù)除外)
  • of cumulative (percentage) 九妈,整個函數(shù)消耗的CPU時間反砌,包括函數(shù)內(nèi)部其他函數(shù)調(diào)用所消耗的CPU時間,如果與local相同萌朱,則不打印
  • 有向邊由調(diào)用者指向被調(diào)用者宴树,有向邊上的時間表示被調(diào)用者所消耗的CPU時間

meta 信息(圖左上角):

  • Total samples,總采樣數(shù)
  • Focusing on晶疼,--focus option 所包含的采樣數(shù)
  • Dropped nodes酒贬,忽略的節(jié)點
  • Dropped edges,忽略的邊

focus 某些函數(shù):

pprof --gv --focus=vsnprintf cpu_profiler_test cpu_test.profile

ignore 某些函數(shù):

pprof --gv --ignore=snprintf cpu_profiler_test cpu_test.profile

更多操作可參考:https://gperftools.github.io/gperftools/cpuprofile.html

6.3 Kcachegrind

安裝 Kcachegrind

sudo apt-get install kcachegrind

生成 .callgrind 文件

pprof --callgrind cpu_profiler_test cpu_test.profile > cpu_test.callgrind

分析命令

kcachegrind cpu_test.callgrind

分析結(jié)果如下圖所示翠霍,相對來說功能比較強:
callgrind.png

7 控制監(jiān)控開關

如果是server上的程序锭吨,啟動后一般不會主動退出,即使退出寒匙,也一般不會正常退出零如,而 gperftools 必須在程序正常退出的情況下才能夠正常收集或者收集完整數(shù)據(jù)。

7.1 請求服務
#include <gperftools/profiler.h>

void on_request(Request* req) {
    static bool is_profile_started = false;
    if (req->type == START_PROFILE && !is_profile_started) {
        ProfilerStart("xxx.profile");
        is_profile_started = true;
    } else if (req->type == STOP_PROFILE && is_profile_started) {
        ProfilerStop();
        is_profile_started = false;
    } else {
        // normal request processing here
    }
}
7.2 信號
static void gprof_callback(int signum) {
    if (signum == SIGUSR1) {
        printf("Catch the signal ProfilerStart\n");
        ProfilerStart("bs.prof");
    }
    else if (signum == SIGUSR2) {
        printf("Catch the signal ProfilerStop\n");
        ProfilerStop();
    }
}

static void setup_signal() {
    struct sigaction profstat;
    profstat.sa_handler = gprof_callback;
    profstat.sa_flags = 0;
    sigemptyset(&profstat.sa_mask);                                        
    sigaddset(&profstat.sa_mask, SIGUSR1);
    sigaddset(&profstat.sa_mask, SIGUSR2);

    if (sigaction(SIGUSR1, &profstat,NULL) < 0)
        fprintf(stderr, "SIGUSR1 Fail !");

    if (sigaction(SIGUSR2, &profstat,NULL) < 0)
        fprintf(stderr, "SIGUSR2 Fail !");
}

8 原理

如果只關心如何使用蒋情,則到這里就可以編寫自己的工程了埠况,下面對 CPU profiler 的源碼進行簡單的剖析。

站在巨人的肩膀上:http://www.tealcode.com/gperftool_source_analysis/

入口:

extern “C” PERFTOOLS_DLL_DECL int ProfilerStart(const char* fname) {
    return CpuProfiler::instance_.Start(fname, NULL);
}

bool CpuProfiler::Start(const char* fname, const ProfilerOptions* options) {
    collector_.Start(fname, collector_options);
    // Setup handler for SIGPROF interrupts
    EnableHandler();
    return true;
}

CPU profiler 啟動的時候棵癣,核心功能就是啟動數(shù)據(jù)收集器(collector_)辕翰,這個數(shù)據(jù)收集器的 Start() 函數(shù)的功能就是初始化數(shù)據(jù)收集需要的數(shù)據(jù)結(jié)構(gòu),并創(chuàng)建數(shù)據(jù)收集文件:

bool ProfileData::Start(const char* fname, const ProfileData::Options& options) {
    // Open output file and initialize various data structures
    int fd =open(fname, O_CREAT | O_WRONLY | O_TRUNC, 0666);
    start_time_ = time(NULL);
    fname_ = strdup(fname);
    // Reset counters
    num_evicted_ = 0;
    count_ = 0;
    evictions_ = 0;
    total_bytes_ = 0;
    hash_ = new Bucket[kBuckets];
    evict_ = new Slot[kBufferLength];
    memset(hash_, 0, sizeof(hash_[0]) * kBuckets);
    // Record special entries
    evict_[num_evicted_++] = 0; // count for header
    evict_[num_evicted_++] = 3; // depth for header
    evict_[num_evicted_++] = 0; // Version number
    CHECK_NE(0, options.frequency());
    int period =1000000/ options.frequency();
    evict_[num_evicted_++] = period; // Period (microseconds)
    evict_[num_evicted_++] = 0; // Padding
    out_ = fd;
    return true;
}

然后就是開啟 CPU profiler 的一個處理函數(shù)狈谊,這個函數(shù)就是把 prof_handler() 注冊到了某個地方:

void CpuProfiler::EnableHandler() {
    prof_handler_token_ = ProfileHandlerRegisterCallback(prof_handler, this);
}

ProfileHandlerToken* ProfileHandlerRegisterCallback(
    ProfileHandlerCallback callback, void* callback_arg) {
    return ProfileHandler::Instance()->RegisterCallback(callback, callback_arg);
}

功能都在 ProfileHandler 里面喜命,其為一個單例類沟沙,構(gòu)造函數(shù)如下:

ProfileHandler::ProfileHandler() {
    timer_type_ = (getenv(“CPUPROFILE_REALTIME”) ? ITIMER_REAL : ITIMER_PROF);
    signal_number_ = (timer_type_ == ITIMER_PROF ? SIGPROF : SIGALRM);

    // Get frequency of interrupts (if specified)
    char junk;
    constchar* fr =getenv(“CPUPROFILE_FREQUENCY”);

    if (fr != NULL && (sscanf(fr, “%u%c”, &frequency_, &junk) == 1) && (frequency_ > 0)) {
        // Limit to kMaxFrequency
        frequency_ = (frequency_ > kMaxFrequency) ? kMaxFrequency : frequency_;
    } else {
        frequency_ = kDefaultFrequency;
    }

    // Install the signal handler.

    structsigaction sa;
    sa.sa_sigaction = SignalHandler;
    sa.sa_flags = SA_RESTART | SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    sigaction(signal_number_, &sa, NULL);
}

構(gòu)造函數(shù)中,根據(jù)環(huán)境變量 CPUPROFILE_REALTIME 的配置壁榕,來決定讓 SIGPROF 還是 SIGALRM 信號來觸發(fā) SignalHandler 信號處理函數(shù)矛紫,并根據(jù)環(huán)境變量 CPUPROFILE_FREQUENCY 的配置來設置自己的一個頻率變量 frequency_ ,如果沒有設置牌里,就使用默認值颊咬,這個默認值是100,而最大值是4000牡辽。

然后 ProfileHandlerRegisterCallback() 函數(shù)的實現(xiàn)如下:

ProfileHandlerToken* ProfileHandler::RegisterCallback(ProfileHandlerCallback callback, void* callback_arg) {
    ProfileHandlerToken* token = new ProfileHandlerToken(callback, callback_arg);
    SpinLockHolder cl(&control_lock_);
    DisableHandler();
    {
        SpinLockHolder sl(&signal_lock_);
        callbacks_.push_back(token);
    }

    // Start the timer if timer is shared and this is a first callback.
    if ((callback_count_ == 0) && (timer_sharing_ == TIMERS_SHARED)) {
        StartTimer();
    }

    ++callback_count_;
    EnableHandler();
    return token;
}

這個函數(shù)就如其函數(shù)名字喳篇,把指定的回調(diào)函數(shù)添加到 callbacks_里面去,然后在加入第一個 callback 的時候調(diào)用 StartTimer() 函數(shù)來啟動定時器态辛,然后調(diào)用 EnableHander() 函數(shù)來開啟回調(diào)麸澜。StartTimer() 的實現(xiàn)如下:

void ProfileHandler::StartTimer() {
    struct itimerval timer;
    timer.it_interval.tv_sec = 0;
    timer.it_interval.tv_usec = 1000000 / frequency_;
    timer.it_value = timer.it_interval;
    setitimer(timer_type_, &timer, 0);
}

EnableHandler() 的實現(xiàn)如下:

void ProfileHandler::EnableHandler() {
    struct sigaction sa;
    sa.sa_sigaction = SignalHandler;
    sa.sa_flags = SA_RESTART | SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    const int signal_number = (timer_type_ == ITIMER_PROF ? SIGPROF : SIGALRM);
    RAW_CHECK(sigaction(signal_number, &sa, NULL) == 0, “sigprof (enable)”);
}

到這里,這個工具的基本工作原理已經(jīng)可以猜出個大概了奏黑。它用 setitimer() 啟動一個系統(tǒng)定時器炊邦,這個定時器會每秒鐘執(zhí)行觸發(fā) frequencySIGPROF 或者 SIGALRM 信號,從而去觸發(fā)上面注冊的信號處理函數(shù)熟史。那么猜想馁害,信號處理函數(shù)里面應該會用 backtrace 去檢查一下目標程序執(zhí)行到什么位置了。信號處理函數(shù)如下:

void CpuProfiler::prof_handler(int sig, siginfo_t*, void* signal_ucontext, void* cpu_profiler) {
    CpuProfiler* instance = static_cast<CpuProfiler*>(cpu_profiler);
    if (instance->filter_==NULL||(*instance->filter_)(instance->filter_arg_)) {
    void* stack[ProfileData::kMaxStackDepth];
    // Under frame-pointer-based unwinding at least on x86, the
    // top-most active routine doesn’t show up as a normal frame, but
    // as the “pc” value in the signal handler context.
    stack[0] = GetPC(*reinterpret_cast<ucontext_t*>(signal_ucontext));
    // We skip the top three stack trace entries (this function,
    // SignalHandler::SignalHandler and one signal handler frame)
    // since they are artifacts of profiling and should not be
    // measured. Other profiling related frames may be removed by
    // “pprof” at analysis time. Instead of skipping the top frames,
    // we could skip nothing, but that would increase the profile size
    // unnecessarily.
    int depth = GetStackTraceWithContext(stack +1, arraysize(stack) -1, 3, signal_ucontext);
    void**used_stack;
    if (depth >0&& stack[1] == stack[0]) {
        // in case of non-frame-pointer-based unwinding we will get
        // duplicate of PC in stack[1], which we don’t want
        used_stack = stack + 1;
    } else {
        used_stack = stack;
        depth++; // To account for pc value in stack[0];
    }
        instance->collector_.Add(depth, used_stack);
    }
}

果然是獲取backtrace以故,然后記錄到collector_里面去蜗细。

總結(jié)

  1. 這個工具是用系統(tǒng)定時器定時產(chǎn)生信號的方式,在信號處理函數(shù)里面獲取當前的調(diào)用堆棧來確定當前落在哪個函數(shù)里面的怒详。獲取頻率默認是每10ms采樣一次炉媒,參數(shù)是可調(diào)的,但是最大頻率是4000昆烁,也就是支持的最小采樣間隔是250微秒吊骤;
  2. 這個工具獲取到的性能數(shù)據(jù)是基于統(tǒng)計數(shù)據(jù)的,也就是他并不真正跟蹤函數(shù)的每一次調(diào)用過程静尼,而是均勻地采樣并記錄采樣點所落在的函數(shù)調(diào)用位置白粉,用這些統(tǒng)計數(shù)據(jù)來計算每個函數(shù)的執(zhí)行時間占比。這個數(shù)據(jù)并不是準確的數(shù)據(jù)鼠渺,但是只要運行時間相對比較長鸭巴,統(tǒng)計數(shù)據(jù)還是能比較準確地說明問題的。而這也是為什么說這個工具是比較好的服務器程序性能分析工具拦盹,而對一些客戶端程序鹃祖,比如游戲客戶端并不是非常合適。因為游戲客戶端上普舆,相比長時間的統(tǒng)計數(shù)據(jù)恬口,它們通常更加關心的是某些幀內(nèi)的具體負載情況校读。
  3. 這個工具不工作的時候,就會把系統(tǒng)定時器取消掉祖能,不會定時產(chǎn)生中斷信號歉秫,不會觸發(fā)中斷處理程序,所以對運行程序的影響真的是很小养铸,運行效率上可以說完全沒有影響雁芙。而對產(chǎn)品的影響只是多占用一些鏈接 profiler 庫的內(nèi)存而已。

參考鏈接:

https://gperftools.github.io/gperftools/cpuprofile.html
http://airekans.github.io/cpp/2014/07/04/gperftools-profile
https://blog.csdn.net/aganlengzi/article/details/62893533
https://blog.csdn.net/10km/article/details/83820080
https://www.zhihu.com/question/277160878
http://www.tealcode.com/gperftool_source_analysis/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末揭厚,一起剝皮案震驚了整個濱河市却特,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌筛圆,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件椿浓,死亡現(xiàn)場離奇詭異太援,居然都是意外死亡,警方通過查閱死者的電腦和手機扳碍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門提岔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人笋敞,你說我怎么就攤上這事碱蒙。” “怎么了夯巷?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵赛惩,是天一觀的道長。 經(jīng)常有香客問我趁餐,道長喷兼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任后雷,我火速辦了婚禮季惯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘臀突。我一直安慰自己勉抓,他們只是感情好,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布候学。 她就那樣靜靜地躺著藕筋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪盒齿。 梳的紋絲不亂的頭發(fā)上念逞,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天困食,我揣著相機與錄音,去河邊找鬼翎承。 笑死硕盹,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的叨咖。 我是一名探鬼主播瘩例,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼甸各!你這毒婦竟也來了垛贤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤趣倾,失蹤者是張志新(化名)和其女友劉穎聘惦,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體儒恋,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡善绎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了诫尽。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片禀酱。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖牧嫉,靈堂內(nèi)的尸體忽然破棺而出剂跟,到底是詐尸還是另有隱情,我是刑警寧澤酣藻,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布曹洽,位于F島的核電站,受9級特大地震影響臊恋,放射性物質(zhì)發(fā)生泄漏衣洁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一抖仅、第九天 我趴在偏房一處隱蔽的房頂上張望坊夫。 院中可真熱鬧,春花似錦撤卢、人聲如沸环凿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽智听。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間到推,已是汗流浹背考赛。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留莉测,地道東北人颜骤。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像捣卤,于是被迫代替她去往敵國和親忍抽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容