ClickHouse 表引擎 & ClickHouse性能调优 - ClickHouse团队 Alexey Milovidov


https://clickhouse.com/

引子

什么是“更快”?

顺序读/写吞吐量?

随机读/写延迟?

特定并行性和工作负载下的IOPS。

显然RAM可能比磁盘慢,例如单个clnannel RAM与10倍 PCIe 4.0 SSD。

Why ClickHouse

Our feature rich and hardware efficient OLAP data management system is the right choice for your organization.

Performance

ClickHouse supports best in the industry query performance, while significantly reducing storage requirements through our innovative use of columnar storage and compression.

Scalability

Battle tested in production, with linear horizontal scalability from single-server deployments to clusters with many thousands of nodes.

Reliability

ClickHouse deployments feature best in class availability. There are no single points of failure, with the architecture supporting multi-master replication, performing effectively in multi-region configurations.

Security

ClickHouse comes with enterprise grade security features and fail-safe mechanisms protecting against data corruption from application bugs and human errors.

Quick Start

MacOS安装:

wget 'https://builds.clickhouse.com/master/macos/clickhouse'

chmod a+x ./clickhouse

./clickhouse

https://clickhouse.com/

ClickHouse 表引擎

引擎表决定:

数据的存储方式和存储位置:写入数据的位置&读取数据的位置

支持哪些请求以及如何支持

并行数据访问

如果有索引,请使用

是否可以执行多线程查询

数据复制

读取数据时,引擎只需要检索所需的列集。但是,在某些情况下,查询可能会在表引擎中部分处理。

MergeTree 系列的引擎用于最重要的任务。

小日志表引擎 TinyLog

最简单的表引擎,它将数据存储在磁盘上。每列都存储在一个单独的压缩文件中。在编写时,数据被附加到文件的末尾。

无并发数据访问限制:

如果从一个表中读取,在另一个查询中写入会报错

如果同时在多个查询中写入该表,数据将被破坏

使用该表的典型方法是一次写入:只写入一次数据,然后根据需要多次读取数据。请求在一个线程中执行。换句话说,这个引擎是为相对较小的表准备的(建议最多 100 万行)。如果你有很多小表,那么使用这个表引擎是有意义的,因为它比日志引擎更简单(需要打开的文件更少)。当你有大量的小表时,这种情况会导致效率低下。也不支持索引

TinyLog 表用于小批量处理的中间数据。

日志引擎

Log 和 TinyLog 的区别在于一个小的“标签”文件与一个列文件并存。

这些标签写在每个数据块上,并包含一个偏移量,指示从哪里开始读取文件以跳过指定的行数。这允许在多个线程中读取表数据。对于并发数据访问,读操作可以并发进行,而写操作则相互阻塞读和读。日志引擎不支持索引。同样,如果写入表失败,该表将被销毁并且从中读取数据将返回错误。注册机制适用于临时数据、写表、测试或演示。

内存引擎

内存引擎将未压缩的数据存储在 RAM 中。数据的存储方式与读取时接收到的数据完全相同。换句话说,从该表中读取是完全免费的。并行数据访问是同步的。锁很短:读和写操作不会互相阻塞。不支持索引。阅读是并行的。由于不会从磁盘读取、解压缩或反序列化数据,因此可以通过简单的查询实现最高性能(超过 10 Gbps)。(请注意,在许多情况下,MergeTree 引擎的性能几乎一样好。)

当服务器重新启动时,数据从表中消失,表变为空。通常,这个表引擎是不实用的。但是,它可以在相对较少的行数(约 100 万条)中用于测试和需要最大速度的任务

系统使用内存机制作为带有外部查询数据的临时表(参见“处理查询的外部数据”一节)并实现全局 IN(参见“运算符”一节)。

合并树MergeTree 引擎

MergeTree

The most universal and functional table engines for high-load tasks. The property shared by these engines is quick data insertion with subsequent background data processing. MergeTree family engines support data replication (with Replicated* versions of engines), partitioning, secondary data-skipping indexes, and other features not supported in other engines.

Engines in the family:

MergeTree

ReplacingMergeTree

SummingMergeTree

AggregatingMergeTree

CollapsingMergeTree

VersionedCollapsingMergeTree

GraphiteMergeTree

MergeTree 引擎支持主键和日期的索引,并提供实时更新数据的能力。它是 ClickHouse 中最先进的桌面引擎。不要将此与合并引擎混淆

该机制接受参数:包含日期的日期类型列的名称、选择表达式(可选)、定义表主键的元组以及索引的粒度。例如:

没有采样支持的示例:

MergeTree(EventDate, (CounterID, EventDate), 8192)

带采样支持的示例:

MergeTree(EventDate, intHash32(UserID), (CounterID, EventDate, intHash32(UserID)), 8192)

MergeTree 类型表必须有一个单独的日期列。在本例中,它是“EventDate”列。日期列类型必须是“DATE”(不是“DateTime”)

主键可以是任何表达式的元组(通常只是列的元组),也可以是单个表达式。

要检查 ClickHouse 在执行查询时是否可以使用此索引,请使用 force_index_by_date 和 force_primary_key 参数。

您可以使用一个大表并以小块的形式不断向其中添加数据 - 这就是 MergeTree 的目的

MergeTree族中所有的表类型都可以复制。

自定义分区键:

自定义节键:从 1.1.54310 版本开始,您可以在 MergeTree 系列中创建任何节表达式(不仅仅是按月)

分区键可以是表列表达式或此类表达式的集合(类似于主键)。分区键可以省略。创建表时,使用新语法在机制描述中指定部分键:

ENGINE [=] Name(...) [PARTITION BY expr] [ORDER BY expr] [SAMPLE BY expr] [SETTINGS name=value, ...]


对于MergeTree表,section表达式在section之后指定,主键在order之后,fetch key在fetch之后,参数可以指定索引的粒度(可选;默认为8192)等来自MergeTreeSettings.h。设置。

例如:

ENGINE = ReplicatedCollapsingMergeTree('/clickhouse/tables/name', 'replica1', Sign)

    PARTITION BY (toMonday(StartDate), EventType)

    ORDER BY (CounterID, StartDate, intHash32(UserID))

    样本由 intHash32(用户 ID)

要在 ALTER PARTITION 命令中指定节,请指定节(或元组)表达式的值。支持常量和常量表达式。例子:

ALTER TABLE table DROP PARTITION(toMonday(today()),1)

使用事件类型 1 删除本周的部分。优化查询也是如此。要在单分区表中指定单个节,请为节 () 指定一个元组。

段号含义:

之前:2014031720140323220(最小数据-最大数据-最小块-最大块系列)

之后:201403220(部分 ID - 最小块数 - 最大块级别)

节标识符是它的字符串标识符(如果可能是人类可读的),用于命名文件系统和 ZooKeeper 中的数据组件。您可以在 ALTER 查询中指定部分键。例如:section key toYYYYMM(EventDate); ALTER 可以指定节 201710 或节 ID“201710”

替换合并树ReplacingMergeTree

此引擎表与 MergeTree 的不同之处在于它删除具有相同主键值的重复记录。

表引擎的最后一个可选参数是版本列。连接时,所有具有相同主键值的行将减少为一行。如果指定了版本列,则保留版本最高的行,否则保留最后一行。

ReplacingMergeTree(EventDate, (OrderID, EventDate, BannerID, ...), 8192, ver)

版本列类型必须是UInt相关的Date,或者DateTime。

请注意,数据仅在合并过程中重复。合并发生在后台的未知时间,因此您无法安排它。部分数据仍无法处理

虽然您可以使用优化查询来执行计划外合并,但不要指望使用它们,因为优化查询会读取和写入大量数据。

因此,替换mergetree适合在后台去除重复数据以节省空间,但不能保证没有重复数据。

求和合并树 SummingMergeTree

这种机制与 MergeTree 的不同之处在于它在合并时收集数据。

SummingMergeTree(EventDate, (OrderID, EventDate, BannerID, ...), 8192)

总列数是隐式的。连接时,具有相同主键值(在本例中为 OrderId、EventDate、BannerID ...)的所有行都有自己的值,并且它们都不是主键的一部分。

SummingMergeTree(EventDate, (OrderID, EventDate, BannerID, ...), 8192, (Shows, Clicks, Cost, ...))

列的总数是明确设置的(最后一个参数是显示、点击、成本...)。连接时,所有具有相同主键值的行在指定列中都有它们的值。指定的列也必须是数字,并且不能是主键的一部分。

对于不属于主键的其他行,将选择串联中选择的第一个值。

这个桌面引擎不是特别有用。请记住,如果您保存预先聚合的数据,将会失去一些系统优势。

聚合合并树 AggregatingMergeTree

这种机制与 MergeTree 的不同之处在于合并将存储在表中的聚合函数的状态组合成具有相同主键值的行。为了使其工作,它在聚合和聚合数据类型上使用 -State 和 -Merge 修饰符。

请注意,在大多数情况下,使用聚合合并树是不切实际的,因为查询可以有效地在非聚合数据上运行。

折叠合并树CollapsingMergeTree

这个引擎是专门为 Yandex.Metrica 设计的

它与 MergeTree 的不同之处在于,它允许在连接时自动删除或折叠某些行。

Yandex.Metrica 具有正常日志(例如,命中日志)和更改日志。更改日志用于逐步计算数据更改统计信息。例如会话更改日志或记录用户历史的日志。在 Yandex.Metrica 中,对话不断变化。例如,每个会话的点击次数增加。我们称任何对象的变化为一对(“旧值,新值”)。如果创建了对象,则旧值可能会丢失。如果对象被删除,新值可能会丢失。如果对象已被修改,但之前存在且未被删除

CollapsingMergeTree(EventDate, (CounterID, EventDate, intHash32(UniqID), VisitID), 8192, Sign)

这里的 Sign 是一列,其中包含 -1 代表“旧”值和 1 代表“新”值

拼接时,每组顺序主键值(用于对数据进行排序的列)减少到不超过一行,“signcolumn = -1”(负行)列的值减少到no多于一行,且列值“signcolumn = 1”(“正线”)。换句话说,更改日志中的条目将被折叠。

————————————————————————————————————————

数据复制

复制仅支持来自 MergeTree 系列的表。复制工作在单个表的级别,而不是整个服务器。服务器可以存储复制表和非复制表。

插入和修改被复制(有关更多信息,请参阅 ALTER)。复制压缩数据,而不是请求文本。CREATE、DROP、ATTACH、DETACH 和重命名请求。它们不会被复制。换句话说,它们属于同一台服务器。CREATE TABLE 查询在运行查询的服务器上创建一个新的复制表。如果此表已存在于其他服务器上,它将添加一个新副本。DROP TABLE 查询删除运行该查询的服务器上的副本。RENAME 查询重命名副本中的表。换句话说,复制的表可能有

复制是异步和多主的。插入(和 ALTER)请求可以发送到任何可用的服务器。数据插入到这个服务器,然后发送到其他服务器。由于数据是异步的,最近插入的数据会滞留在其他副本上。如果副本的一部分不可用,那么当它们可用时,它们的数据将被写入。如果副本可用,则延迟是通过网络传输压缩数据块所需的时间。

如果您将一个数据包写入副本,并且在该数据有时间到达其他副本之前,拥有该数据的服务器已不复存在,则数据将丢失。

在复制过程中,只有粘贴的原始数据通过网络传输。进一步的数据转换(合并)是一致的,并以相同的方式对所有副本执行。这将最大限度地减少网络使用,这意味着当副本位于不同的数据中心时,复制可以很好地工作。(请注意,跨不同数据中心复制数据是复制的主要目的。)

创建复制表

故障后恢复

如果报告异常,系统会检查本地文件系统中的数据集是否与预期的数据集匹配(ZooKeeper 存储了此信息)。如果存在小的不一致,系统会通过将数据与副本同步来纠正它们。

如果系统检测到损坏的数据片段(错误的文件大小)或无法识别的片段(部分写入文件系统,但未写入 ZooKeeper),它会将它们移动到“单独的”子目录(它们不会被删除)。任何丢失的片段从副本中复制

请注意,ClickHouse 不会执行任何破坏性操作,例如自动删除大量数据。

如果本地数据与预期数据偏差太大,则会触发安全机制。服务器将其输入日志并拒绝启动。这是因为这种情况可能表示配置错误,例如,如果一个段的副本被意外配置为另一个段的副本。但是,此机制的阈值设置得足够低,以至于它可以在正常恢复过程中发生。在这种情况下,数据会通过“按下按钮”自动恢复

数据完全丢失后的恢复

如果服务器上的所有数据和元数据都消失了,请按照以下步骤进行恢复:

    1.在服务器上安装 ClickHouse。如果您正在使用它,请在包含分段标识符和副本的配置文件中正确定义替换。

   2.如果你有非复制表,你必须手动复制服务器,从复制中复制它们的数据(在/var/lib/clickhouse/data/db_name/table_name/目录下)

    3.复制表定义位于/var/lib/clickhouse/metadata/replica。如果在表定义中明确定义了段或副本 ID,请更正它以匹配该副本。(还要启动服务器并允许对表进行任何其他查询,它应该在 /var/lib/clickhouse/metadata/.sql 中。)

    4.运行恢复,使用任何创建管理节点/path_to_table/replicant_name/flag/force_restore_data 或运行此命令来恢复所有复制的表:sudo -u clickhouse touch /var/lib/clickhouse/flags/force_restore_data

在恢复期间,网络带宽不受限制。如果您要同时恢复多个副本,请记住这一点。

从 MergeTree 转换为 ReplicatedMergeTree

如果不同副本上的数据不同,请先同步数据或删除除一个副本外的所有副本上的数据。

从 ReplicatedMergeTree 转换为 MergeTree

创建一个具有不同名称的 MergeTree 表。将合并树表的复制数据中的所有数据移动到新表的数据目录中。然后删除复制的mergetree表并重启服务器。

删除.sql文件对应的元数据目录

删除ZooKeeper中对应的路径(/pathtotable/replicaname)。

之后,您可以启动服务器,创建 MergeTree 表,将数据移动到其目录,然后重新启动服务器。

ZooKeeper 集群中的元数据丢失或损坏时的恢复

如果 ZooKeeper 数据丢失或损坏,您可以通过将数据移动到上述非重做表来保存数据。

如果其他副本具有相同的部分,请将它们添加到工作集中。如果不是,那么这些部分将从它们所属的副本中加载。

分布式表引擎 DistributedTableEngine

分布式:分布式引擎本身不存储数据,但允许跨多个服务器进行分布式查询处理,查询是自动并行的。在读取数据期间,如果有的话,将使用远程服务器上的表索引。

分布式引擎接受参数:

服务器配置文件中的集群名称、远程数据库名称、远程表名称和(可选)分片键

Distributed(logs, default, hits[, sharding_key])

默认情况下,将从日志集群中的所有服务器读取数据。命中位于集群中的每台服务器上。数据不仅会被读取,还会在远程服务器上进行部分处理(在某种程度上,这是可能的)。例如,对于 GROUP BY 查询,数据将在远程服务器上聚合,聚合函数的中间状态将发送到请求服务器。然后将数据进一步聚合。

有两种方式将数据写入集群:

首先,您可以定义哪些服务器要写入哪些数据,并直接对每个块执行写入操作。换句话说,插入操作是在表的分布式表“视图”上执行的。这是最灵活的解决方案 - 您可以使用由于域的需要而可能不重要的任何拆分解决方案。这也是一个最佳解决方案,因为数据可以完全独立地写入不同的段。

其次,您可以对分布式表执行插入操作。在这种情况下,表会将插入的数据传播到服务器本身。要将其写入分布式表,它必须设置一个分片键(最后一个参数)。另外,如果只有一个split,写操作不指定segment key,因为在这个例子中没有意义。

每个分片都可以在配置文件中定义其权重。默认情况下,权重为 1。数据分布在分片之间,与分片的权重成正比。例如,如果有两个分区,第一个的权重是 9,第二个是 10,那么第一个将在字符串的 9/19 部分上发送,第二个将在 10/19 上发送。

每个片段可以在配置文件中定义“internal_replication_system”参数。

如果此参数设置为true,则写入操作将选择第一个健康副本并将数据写入其中。如果分布式表“查找”复制的表,则使用此替代方法。换句话说,用于记录数据的表将被自己复制。

如果设置为 false(默认值),数据将写入所有副本。基本上,这意味着分布式表会复制数据本身。这比使用副本表更糟糕。由于副本没有经过一致性检查,它们会随着时间的推移而略有不同。

请求使用特定键连接到数据(IN 或 JOIN)。如果数据用这个key分隔,可以使用local IN或JOIN代替GLOBAL IN或GLOBAL JOIN,效率更高

大量服务器(数百个或更多)使用大量小请求(来自一个客户、网站、广告商或合作伙伴的请求)。为了防止小查询影响整个集群,将一个客户端的数据放在一个段中是有意义的。或者就像我们在 Yandex 中所做的那样。您可以设置双向分片:将整个集群划分为“层”,其中一层可以由多个分片组成。一个客户的数据位于一层,但可以根据需要在该层中添加切片,数据随机分布。

数据是异步写入的。插入分布式表,数据块只写入本地文件系统。数据会尽快发送到后台远程服务器。您应该检查文件列表(数据等待发送)检查数据是否发送成功

如果服务器不存在,或者插入分布式表后发生暴力重启(例如设备故障),插入的数据可能会丢失。如果在表目录中发现损坏的数据块,则将其移动到“损坏”的子目录中,不再使用。

合并机制(不要与 MergeTree 混淆)本身不存储数据,但允许您同时读取任意数量的其他表。阅读是自动并行的。不支持写入表。读取时,如果存在,将使用正在读取的表的索引。合并机制采用参数:数据库名称和表正则表达式。

Merge(hits,'^WatchLog')

数据将从“matches”数据库中的表中读取,这些表的名称匹配正则表达式,正则表达式区分大小写。

除了数据库名称之外,您还可以使用返回字符串的常量表达式。例如currentDatabase()

合并机制的一个典型用途是使用大量的 TinyLog 表,就像使用单个表一样。

虚拟列

虚拟列:虚拟列是表引擎提供的列,与表定义无关。换句话说,这些列未列在 CREATE TABLE 中,但它们是可选的。

虚拟列和常规列的区别如下:

它们未列在表定义中

无法将数据添加到 INSERT

当使用 INSERT 而不指定列列表时,虚拟列将被忽略

使用星号 (SELECT) 时,它们不会被选中

虚拟列不会出现在 SHOW CREATE TABLE 和 DESC 表查询中

缓冲

缓存:缓冲数据以写入 RAM 并定期将其刷新到另一个表。在读操作期间,数据同时从缓冲区和另一个表中读取。

Buffer(database, table, num_layers, min_time, max_time, min_rows, max_rows, min_bytes, max_bytes)

 Buffer 参数说明:

database,table:数据更新到哪个库表。除了数据库名称之外,您还可以使用返回字符串的常量表达式。

num_layers :并行层数。在物理上,该表将在单独的缓冲区中显示为“num_layers”。推荐值:16. 

mintime, maxtime, minrows, maxrows, minbytes 和 maxbytes 都是用于从缓冲区更新数据的条件。

如果满足所有“最小”条件或至少一个“最大”条件,则从缓冲区更新数据并写入目标表。从第一次写入缓冲区的时间,从 seconds 到 seconds, minrows , maxrows - 缓冲区中行数的条件。最小字节,maxbytes - 缓冲区中字节数的条件。

在写操作期间,数据被插入到一个随机的 numlayers 缓冲区中。或者,如果插入的数据块足够大(超过 maxrows 或 maxbytes),则直接写入目标表,跳过缓冲区。

例如:

CREATETABLEmerge.hits_bufferASmerge.hitsENGINE= Buffer(merge, hits,16,10,100,10000,1000000,10000000,100000000)

创建合并。与“合并”具有相同结构的 Hitsbuffer 表。单击并使用缓冲引擎。写入此表时,数据将缓存在 RAM 中,然后写入“联合”。敲桌子。已创建 16 个缓冲区。如果写入超过 100 秒或 100 MB 的数据或 100 MB 的数据,则将更新所有数据。或者,如果同时过去了 10 秒并且写入了 1000 行和 10 MB 的数据。如果只记录一行,100秒后会更新。如果写了很多行,数据很快就会更新。

当服务器使用 DROP TABLE 或单独的表停止时,缓冲的数据也将在目标表中更新。

您可以为数据库和表名称设置空单引号字符串。这表明没有目标表。在这种情况下,当达到数据更新条件时,缓冲区将被清除。这对于将数据窗口保存在内存中很有用。

从缓冲区表中读取数据时,无论是从缓冲区还是从目标表(如果有),都必须对数据进行处理。请注意,缓冲表不支持索引。换句话说,缓冲区中的数据被完全扫描,这对于大缓冲区来说可能很慢。(从属表中的数据将使用它支持的索引。

如果服务器异常重启,缓冲区中的数据就会丢失。如果您需要对从属表和缓冲区表运行 ALTER,我们建议您先删除缓冲区表,在从属表上运行 ALTER,然后重新创建缓冲区表。如果缓冲表中的列集与从属表中的列集不匹配,则在两个表中插入列的子集。

当数据添加到缓冲区时,其中一个缓冲区被阻塞。如果同时从表中执行读操作,会造成延迟。

MergeTree vs Memory

ClickHouse中有多个表引擎:

MergeTree表将数据存储在磁盘上。

内存表将数据存储在内存中。

RAM比磁盘快, 那么内存表比MergeTree快吗?

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

推荐阅读更多精彩内容