start_time: 2024-04-25 13:50:24 +0800

Openresty测试框架--Test::Nginx

96
醇雾
IP属地: 北京
0.3 2019.08.25 00:32 字数 2600

Test::Nginx使用说明

1. 简介

1.1 组件介绍

Test::Nginx是一个由Perl编写的nginx测试组件,继承自Test::Base。

1.2 安装方法

sudo cpan Test::Nginx

1.3 测试目录结构

测试目录最好命名为t,因为在测试过程中会在t中创建conf目录存储当前测试的配置和日志(默认路径为t/),应该可以提供参数来指定目录,尚未研究。

cc_cache/
├── go.sh
└── t
    ├── 001-test.t
    ├── 002-strategy.t
    ├── 003-rule.t
    └── ANYU.pm

1.4 测试文件格式

Test::Nginx遵循一种特殊的设计。

#codes...

__DATA__

#suites...

如代码片段所示,使用DATA将每个测试文件分割成代码和测试用例两部分,Perl解释器将只运行代码部分内容。

__DATA__
其实这是Perl相关的语法,在代码中使用<STDIN>可以从标准输入接收数据,类似地使用<DATA>文件句柄可以直接从脚本自身获取数据(脚本中特殊常量DATA之后的内容),同时它也标志着脚本的逻辑代码的结束。
注意:第二次使用句柄时,需要修改偏移量才能重新从头读取数据。

1.4.1 代码部分

# 调用对应的测试模块,一般情况使用'no plan'即可
use Test::Nginx::Socket 'no plan';
# 运行测试用例
run_tests();

1.4.2 用例部分

由一系列测试块(BLOCK)组成,每个测试块由标题、可选描述和一组用例组成。

# 标题名称,合理标题前缀有利于测试结果分析
# 可以使用reindex命令自动生成TEST编号前缀
=== title TEST01: NAME
optional description

# value1包含换行符在内多行数据
# 可以使用多个filter对value1进行预处理
--- section1 (filter ...)
value1

# value2默认使用了chmop去掉了两边的空白符
--- section2 : value2

常用sections举例。

=== TEST 1: hello, world
This is just a simple demonstration

# 增加配置到server{}
--- config
location = /t {
    echo "hello, world!";
}

# 发起测试请求
--- request
GET /t

# 确认响应body
# chomp用来去掉value两边的空白符
--- response_body chomp
hello, world!

# 确认响应状态码,不显示指定该section时默认200
--- error_code: 200

# 仅运行此block
--- ONLY

# 跳过此block
--- SKIP

# eval将value作为代码执行并获取对应结果
-- response_body eval
"a" x 4096

1.4.3 更多Sections

Section 说明 备注
config 插入server块配置
http_config 插入http块配置
main_config 插入顶层配置
request 发起请求 默认HTTP/1.1,可以后缀指定
pipelined_requests 多个请求 request好像会自动转换
response_body 响应body 可以用eval支持qr//格式的正则
response_body_like 响应正则 response_body好像会自动转换
error_code 响应码 默认200
more_headers 请求头
response_headers 响应头检查
error_log 错误日志包含
no_error_log 错误日志不包含 使用[error]确定没异常
grep_error_log 筛选日志条件 结果与grep_error_log_out匹配
grep_error_log_out 匹配筛选日志 类res可以是多行内容
wait 日志等待时间 单位秒,支持float
must_die 测试启动失败 标记,启动失败则测试通过
ONLY 只测试当前block 标记
SKIP 跳过当前block 标记
ignore_response 不检查响应完整性 默认HTTP/1.1协议格式
timeout 连接超时时间 默认3秒

测试时将为每个block生成一个配置文件t/servroot/conf/nginx.conf进行测试。
我们还可以通过编写perl代码的方式来新增section或扩展现有的section。

1.5 通用代码封装

通常使用prove来启动每个测试perl脚本来获取测试结果,我们可以将通用的测试启动参数、测试配置和函数封装调用。

-- 封装模块 --
package t::ANYUTester;

# 启动HUP测试模式
BEGIN {
    if ($ENV{TEST_NGINX_CHECK_LEAK}) {
        $SkipReason = "unavailable for the hup tests";
    } else {
        $ENV{TEST_NGINX_USE_HUP} = 1;
        undef $ENV{TEST_NGINX_USE_STAP};
    }
}

# -Base部分在调用t::ANYUTester时传入
use Test::Nginx::Socket -Base;

repeat_each(2);
log_level('info');

# 控制block测试顺序与代码一致(默认是随机测试)
no_shuffle();

# 使用diff风格的字符串对比(response_body/grep_error_log_out等)
no_long_string();

# 在每个block启动前调用进行预处理。
add_block_preprocessor(sub {
    my ($block) = @_;

    # 修改http部分配置
    my $http_config = $block->http_config;
    # EOCH为perl的多行字符串语法
    $http_config .= <<'_EOCH_';
    lua_package_path "/home/work/cc/cc_nginx/luafiles/?.lua;;";
    lua_shared_dict cache_status 100m;
    lua_shared_dict cache_status_lock 100k;
_EOCH_
    $block->set_value("http_config", $http_config);

    # 修改server部分配置
    my $config = $block->config;
    $config .= <<'_EOCS_';
    location /cc_cache {
        rewrite_by_lua_file /home/work/cc/cc_nginx/luafiles/cc/api/cache.lua;
    }

    location /rule_check {
        rewrite_by_lua_block {
            local host = ngx.var.arg_host
            local module = ngx.var.arg_module
            local uri = ngx.var.arg_uri
            local infos = {
                host = host,
                uri = uri,
            }

            local rule_api = require "cc.rule.cache"
            local strategy_api = require "cc.strategy.cache"

            local key = module .. ":" .. host
            local rules = strategy_api.lru_get(key)
            if rules then
                ngx.print(rule_api.match(infos, rules))
            else
                ngx.print("miss")
            end
        }
    }
_EOCS_
    $block->set_value("config", $config);

});

1;
__END__

-- 主模块 --
use t::ANYUTester 'no_plan';
run_tests();

__DATA__
# test suites...

1.6 测试启动器

可以将系统环境变量等启动时需要设置的内容进行封装,避免环境污染,方便操作。

#!/usr/bin/env bash

export PATH=/home/work/cc/cc_nginx/nginx/sbin:$PATH

# 部分系统perl环境会提示(Can't locate t/.pm in @INC)
export PERL5LIB=$(pwd):$PERL5LIB

exec prove "$@"

使用shell命令$ sh go.sh t/进行测试,可以指定某个.t文件进行单独测试。

-v 查看每个测试点的情况
-r 遍历指定的目录

如果使用vim编辑器进行测试用例.t文件的编写,可以直接使用:!prove %快速测试查看结果。

2. Test:Nginx进阶

2.1 知识补充

2.1.1 Perl变量的引用

# codes...
our $after_10_min = time()+600;
run_tests();
# datas...
'prefix "ttl":' . $::after_10_min . 'suffix'
# datas...

只有在支持eval(作为代码通过perl解释器运行)的section(request/response_body)才支持变量引入,因为eval和run_tests前的代码部分运行很可能不在一个模块内(变量的引入可能在block处理时完成),所以需要使用our定义为包全局变量。

Perl中our和my的区别
my声明的是真正的词法变量,只能在闭合块中访问。
our声明的是包全局变量,在符号表中存储(可以通过全限定在任何地方访问)。
ps. $::var即$main::var,取包全局变量

变量的引用严格使用perl语法,可以通过.连接字符串或在""包闭的串中直接引用。

注意:标准json中是用的双引号"data",不支持'data'

2.1.2 多种lua流程模块

init_by_lua
nginx启动时的配置加载阶段
init_worker_by_lua
worker进程启动时的配置加载阶段
set_by_lua
用于简单的计算设置变量,适用于server/location
rewrite_by_lua
在标准HtpRewriteModule之后进行,适用于http/server/location
access_by_lua
请求访问阶段,在标准HttpAccessModule之后进行,用于访问控制,适用于http/server/location
content_by_lua
内容处理器,接收请求并输出响应,适用于location
header_filter_by_lua
body_filter_by_lua
log_by_lua

参考链接:https://www.cnblogs.com/JohnABC/p/6206622.html

2.1.3 Response为table时的比较

通过在Json请求中增加了一个变量来将预定的结果table传入,然后调用table序列化函数(有序模式),将实际结果和预期结果都转为字符串进行比较。

if data.check_result then
    local check_result = data.check_result
    if is_table(check_result) then
        -- table2string有序将table内数据转为统一格式的字符串
        check_result = util.table2string(check_result, true)
        result = util.table2string(json_decode(result), true)
    else
        check_result = tostring(check_result)
    end

    ngx_log(ngx_ERR, "result: " .. result)
    ngx_log(ngx_ERR, "check_result: " .. check_result)

    if result == check_result then
        ngx_exit(200, 'true')
    else
        ngx_exit(400, 'false')
    end
else
    ngx_exit(200, result)
end

2.1.4 跳过某个.t文件

use Test::Nginx::Socket skip_all => "some reasons";

2.2 超时测试技巧

2.2.1 连接超时

可以通过在某个IP的服务器链路上的防火墙设置相应的规则,丢弃所有的SYN包,就会产生访问超时的现象。下面是官方示例的代码和相应的"黑洞"地址。

=== TEST 1: connect timeout
--- config
    # 配置DNS解析使用的服务器IP
    resolver 8.8.8.8;
    resolver_timeout 1s;

    location = /t {
        content_by_lua_block {
            local sock = ngx.socket.tcp()
            sock:settimeout(100) -- ms
            local ok, err = sock:connect("agentzh.org", 12345)
            if not ok then
                ngx.log(ngx.ERR, "failed to connect: ", err)
                return ngx.exit(500)
            end
            ngx.say("ok")
        }
    }
--- request
GET /t
--- response_body_like: 500 Internal Server Error
--- error_code: 500
--- error_log
failed to connect: timeout

一般情况下在测试中需要把timeout时间相应的缩小,有利于加快测试进度。需要注意的是,Test::Nginx默认的连接超时时间是3秒。

2.2.2 读取超时

可以通过修改server代码,使用sleep等操作长时间不中断连接即可。

=== TEST 1: read timeout
--- main_config
    # TCP伪造服务器
    stream {
        server {
            listen 5678;
            content_by_lua_block {
                ngx.sleep(10)  -- 10 sec
            }
        }
    }
--- config
    lua_socket_log_errors off;
    location = /t {
        content_by_lua_block {
            local sock = ngx.socket.tcp()
            sock:settimeout(100) -- ms
            assert(sock:connect("127.0.0.1", 5678))
            ngx.say("connected.")
            local data, err = sock:receive()  -- try to read a line
            if not data then
                ngx.say("failed to receive: ", err)
            else
                ngx.say("received: ", data)
            end
        }
    }
--- request
GET /t
--- response_body
connected.
failed to receive: timeout
--- no_error_log
[error]

2.2.3 发送超时

使用mockeagain配合lua测试代码完成。
参考链接:https://openresty.gitbooks.io/programming-openresty/content/testing/testing-erroneous-cases.html

2.2.4 Client中断

使用timeout配合abort处理,在server中使用sleep延长响应时间,造成客户端中断,观察server日志确定测试结果。

=== TEST 1: abort processing in the Lua callback on client aborts
--- config
    location = /t {
        # 检查客户端中止事件
        lua_check_client_abort on;

        content_by_lua_block {
            # 设置回调函数在Client中断时停止客户端处理
            local ok, err = ngx.on_abort(function ()
                ngx.log(ngx.NOTICE, "on abort handler called!")
                ngx.exit(444)
            end)

            if not ok then
                error("cannot set on_abort: " .. err)
            end

            ngx.sleep(0.7)  -- sec
            ngx.log(ngx.NOTICE, "main handler done")
        }
    }
--- request
    GET /t
--- timeout: 0.2
# 控制Test::Nginx忽略超时错误信息
--- abort
--- ignore_response
--- no_error_log
[error]
main handler done
--- error_log
client prematurely closed connection
on abort handler called!

2.3 丰富的测试模式

2.3.1 Benchmark模式

在运行prove前设定系统变量TEST_NGINX_BENCHMARK即可开启基准测试。如果使用HTTP/1.1协议,将调用weighttp工具,如果使用HTTP/1.0将会调用ab工具,这些工具需要自己安装的哈。

# 将会发起2000个请求,并发数为2
# weighttp -c2 -k -n2000 $url
# ab -r -d -S -c2 -k -n2000 $url
export TEST_NGINX_BENCHMARK='2000 2'
prove t/foo.t

测试结果中可以展示出测试结果供参考。

---weighttp---
finished in 2 sec, 652 millisec and 752 microsec, 75393 req/s, 12218 kbyte/s
requests: 200000 total, 200000 started, 200000 done, 200000 succeeded, 0 failed, 0 errored
status codes: 200000 2xx, 0 3xx, 0 4xx, 0 5xx

---ab---
Time taken for tests:   3.001 seconds
Complete requests:      200000
Failed requests:        0
Requests per second:    66633.75 [#/sec] (mean)
Transfer rate:          10798.70 [Kbytes/sec] received

run_tests()前加入以下命令,可以开启多进程测试。

master_on();
workers(4);

2.3.2 HUP模式

为了保证测试块间的环境隔离,Test::Nginx默认在每个测试块启动独立的Nginx实例。当我们需要在测试块间共享一个实例时(保持共享内存),就需要在测试块之间使用HUP信号的方式切换。

# Blocks间使用HUP方式重启
export TEST_NGINX_USE_HUP=1

# Files间也使用HUP方式重启
export TEST_NGINX_NO_CLEAN=1

也可以在测试文件.t中的代码启动部分使用perl进行处理。

# 使用HUP方式进行BLOCK间的切换测试,共享内存数据等。
BEGIN {
    # 使用weghttpd/ab+ps进行简单内存泄漏测试
    if ($ENV{TEST_NGINX_CHECK_LEAK}) {
        $SkipReason = "unavailable for the hup tests";
    } else {
        # 使用HUP方式在block间切换nginx,但是repeat_each时好像不是
        $ENV{TEST_NGINX_USE_HUP} = 1;
        undef $ENV{TEST_NGINX_USE_STAP};
    }
}

use Test::Nginx::Socket 'no_plan';

# test codes and suites

如果想要在Block内的测试中重新加载openresty,只能通过lua代码来控制了。

local f = assert(io.open("t/servroot/logs/nginx.pid", "r"))
local master_pid = assert(f:read())
assert(f:close())
assert(os.execute("kill -HUP " .. master_pid) == 0)

Nginx的几种信号说明。

kill -HUP nginx进程号("/var/run/nginx.pid")
当nginx接收到HUP信号时,它会尝试先解析配置文件(如果指定文件,就使用指定的,否则使用默认的),如果成功,就应用新的配置文件(例如:重新打开日志文件或监听的套接字),之后,nginx运行新的工作进程并从容关闭旧的工作进程,通知工作进程关闭监听套接字,但是继续为当前连接的客户提供服务,所有客户端的服务完成后,旧的工作进程就关闭,如果新的配置文件应用失败,nginx再继续使用早的配置进行工作。</br>
TERM,INT 快速关闭,关闭信号源(stop)
QUIT 从容关闭,会等待请求结束(quit)
HUP 平滑重启,重新加载配置文件(使用reload会加载两次配置)
USR1 重新打开日志文件,切割日志时用途较大
USR2 平滑升级可执行程序(reopen)
WINCH 从容关闭工作进程(配合USR2进行版本升级)

当然,使用HUP加载的方式会带来一些影响,比如HUP加载会有一个新旧worker交替时间,导致某些情况下测试结果与预期不符。

2.3.3 Valgrind模式

openresty中内存相关问题一般出现在较底层的代码(Nginx/LuaJIT/FFI),Lua代码的内存问题(无限制得对全局表和变量操作)无法使用Valgrind检查,因为它们被LuaJIT垃圾收集机制管理。

export TEST_NGINX_USE_VALGRIND=1

贴一个官方的测试用栗。

=== TEST 1: C strlen()
--- config
    location = /t {
        content_by_lua_block {
            local ffi = require "ffi"
            local C = ffi.C

            # 避免每次都重新声明这个函数,使用pcall进行验证
            if not pcall(function () return C.strlen end) then
                ffi.cdef[[
                    size_t strlen(const char *s);
                ]]
            end

            local buf = ffi.new("char[3]", {48, 49, 0})
            local len = tonumber(C.strlen(buf))
            ngx.say("strlen: ", len)
        }
    }
--- request
    GET /t
--- response_body
strlen: 2
--- no_error_log
[error]

在直接以valgrind模式进行测试时,会产生一些假性的错误告警,如下所示:

t/a.t .. TEST 1: C strlen()
==7366== Invalid read of size 4
==7366==    at 0x546AE31: str_fastcmp (lj_str.c:57)
==7366==    by 0x546AE31: lj_str_new (lj_str.c:166)
==7366==    by 0x547903C: lua_setfield (lj_api.c:903)
==7366==    by 0x4CAD18: ngx_http_lua_cache_store_code (ngx_http_lua_cache.c:119)
==7366==    by 0x4CAB25: ngx_http_lua_cache_loadbuffer (ngx_http_lua_cache.c:187)
==7366==    by 0x4CB61A: ngx_http_lua_content_handler_inline (ngx_http_lua_contentby.c:300)

这是由于LuaJIT的内存优化导致的,在LuaJIT的代码仓库中有一个文件lj.supp列出了所有已知的valgrind会产生的误报,我们可以直接复制到工作目录并重命名为valgrind.suppress供测试模式调用valgrind使用。

cp -i /path/to/luajit-2.0/src/lj.supp ./valgrind.suppress

再次运行测试时,将会收到正确的测试结果。

t/a.t .. TEST 1: C strlen()
t/a.t .. ok
All tests successful.
Files=1, Tests=3,  2 wallclock secs ( 0.01 usr  0.00 sys +  1.51 cusr  0.06 csys =  1.58 CPU)
Result: PASS

在测试过程中还可能会出现nginx内核和openssl库等相关的误报,一般情况下框架会自动识别并在测试结果结尾展示可以添加到valgrind.suppress中的内容,如下所示。

{
   <insert_a_suppression_name_here>
   Memcheck:Addr4
   fun:str_fastcmp
   fun:lj_str_new
   fun:lua_setfield
   fun:ngx_http_lua_cache_store_code
   fun:ngx_http_lua_cache_loadbuffer
   fun:ngx_http_lua_content_handler_inline
   fun:ngx_http_core_content_phase
   fun:ngx_http_core_run_phases
   fun:ngx_http_process_request
   fun:ngx_http_process_request_line
   fun:ngx_epoll_process_events
   fun:ngx_process_events_and_timers
   fun:ngx_single_process_cycle
   fun:main
}

如果要测试一些由于lua分配的对象导致的内存泄漏情况,我们需要在编译luajit的时候加入参数关闭luajit的内存分配策略。

make CCDEBUG=-g XCFLAGS='-DLUAJIT_USE_VALGRIND -DLUAJIT_USE_SYSMALLOC'

或者编译openresty的时候加入特定参数。

./configure \
    --prefix=/opt/openresty-valgrind \
    --with-luajit-xcflags='-DLUAJIT_USE_VALGRIND -DLUAJIT_USE_SYSMALLOC' \
    --with-debug \
    -j4
make -j4
sudo make install

# 或者openresty提供了一个更为直接的参数来编译关闭内存管理的openresty版本
--with-no-pool-patch

nginx1.9.13之后的版本,提供了一个宏用来设置实现类似"无池补丁"的效果,但是效果并没有编译时关闭内存管理那么干脆利落。

#define NGX_DEBUG_PALLOC 

Valgrind无法识别堆栈和应用级别的内存问题。
Google团队开发的AddressSanitizer也可以用来识别内存泄漏,但各有千秋。

2.3.4 CheckLeak模式

Valgrind非常擅长检测各种内存泄漏和内存无效访问,但在面对应用程序级的垃圾收集器(GC)和内存池等内存管理器中的泄漏方面也受到限制,这在现实中很常见。所以Test::Nginx::Socket中新增了一种测试模式来检测内存问题。

export TEST_NGINX_CHECK_LEAK=1

这种测试模式下,框架将会周期性的使用ps命令来获取当前占用的系统内存大小,计算出一个拟合的斜率k表示内存的增长速度,可以通过k来确认是否有内存泄漏问题出现。通过在Lua中定期调用垃圾收集函数,可以确保k参数的有效性。

collectgarbage()

然而,这种测试模式存在一个很大的缺点,它无法提供有关泄漏(可能)发生的位置的任何详细信息。它所报告的只是数据样本和其他指标,只能验证是否存在泄漏(至少在某种程度上)。

在Wiki中应该还有更多的模式来解决这个问题,等有时间去研究一下。

3. Wiki学习链接

使用说明:https://openresty.gitbooks.io/programming-openresty/content/
详细内容:https://metacpan.org/release/Test-Nginx

最后编辑于 :2019-08-25 00:32:55
©著作权归作者所有,转载或内容合作请联系作者
序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
783c64db 45e5 48d7 82e4 95736f50533e 沈念sama 阅读 158560 评论 4 赞 361
序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
783c64db 45e5 48d7 82e4 95736f50533e 沈念sama 阅读 67104 评论 1 赞 291
文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
8 开封第一讲书人 阅读 108297 评论 0 赞 243
文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
8 开封第一讲书人 阅读 43869 评论 0 赞 204
正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
388e473c fe2f 40e0 9301 e357ae8f1b41 茶点故事 阅读 52275 评论 3 赞 287
文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
8 开封第一讲书人 阅读 40563 评论 1 赞 216
那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
783c64db 45e5 48d7 82e4 95736f50533e 沈念sama 阅读 31833 评论 2 赞 312
文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
8 开封第一讲书人 阅读 30543 评论 0 赞 197
序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
783c64db 45e5 48d7 82e4 95736f50533e 沈念sama 阅读 34245 评论 1 赞 241
正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
388e473c fe2f 40e0 9301 e357ae8f1b41 茶点故事 阅读 30512 评论 2 赞 244
正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
388e473c fe2f 40e0 9301 e357ae8f1b41 茶点故事 阅读 32011 评论 1 赞 258
序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
783c64db 45e5 48d7 82e4 95736f50533e 沈念sama 阅读 28359 评论 2 赞 253
正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
388e473c fe2f 40e0 9301 e357ae8f1b41 茶点故事 阅读 33006 评论 3 赞 235
文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
8 开封第一讲书人 阅读 26062 评论 0 赞 8
文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
8 开封第一讲书人 阅读 26825 评论 0 赞 194
我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
783c64db 45e5 48d7 82e4 95736f50533e 沈念sama 阅读 35590 评论 2 赞 273
正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
388e473c fe2f 40e0 9301 e357ae8f1b41 茶点故事 阅读 35501 评论 2 赞 268
end_time: 2024-04-25 13:50:24 +0800 Completed in 146.594765ms