ES大数据量性能优化调优方案

# 01. 选择合理的硬件配置:尽可能使用 SSD

使用SSD通常比机械硬盘查询速度快5~10倍,写入性能提升不明显。对于文档检索类查询性能要求较高的场景,建议考虑 SSD 作为存储,同时按照 1:10 的比例配置内存和硬盘。对于日志分析类查询并发要求较低的场景,可以考虑采用机械硬盘作为存储,同时按照 1:50 的比例配置内存和硬盘。单节点存储数据建议在2TB以内,不要超过5TB,以避免系统不稳定。

# 02. JVM配置机器一半的内存,但是不建议超过32G

修改conf/jvm.options配置,-Xms 和 -Xmx 设置为相同的值,推荐设置为机器内存的一半左右,剩余一半留给操作系统缓存使用。JVM 内存建议不要低于 2G,JVM 建议不要超过 32G,否则 JVM 会禁用内存对象指针压缩技术,造成内存浪费。机器内存大于 64G 内存时,推荐配置 -Xms30g -Xmx30g。JVM 堆内存较大时,内存垃圾回收暂停时间比较长,建议配置 ZGC(低延迟垃圾回收器) 或 G1(垃圾优先收集器) 垃圾回收算法。

# 03. 规模较大的集群配置专有主节点,避免脑裂问题

ES 主节点负责集群元信息管理、index 的增删操作、节点的加入剔除,定期将最新的集群状态广播至各个节点。在集群规模较大时,建议配置专有主节点只负责集群管理,不存储数据,不承担数据读写压力。

# 专有主节点配置(conf/elasticsearch.yml):

node.master:true

node.data: false

node.ingest:false

# 数据节点配置(conf/elasticsearch.yml):

node.master:false

node.data:true

node.ingest:true

Elasticsearch 默认每个节点既是候选主节点,又是数据节点。最小主节点数量参数minimum_master_nodes 推荐配置为候选主节点数量一半以上,该配置告诉 ES 当没有足够的 master 候选节点的时候,不进行 master 节点选举,等 master 节点足够进行选举。

例如对于 3 节点集群,最小主节点数量从默认值 1 改为 2。

# 最小主节点数量配置(conf/elasticsearch.yml):

discovery.zen.minimum_master_nodes: 2

# 04. Linux操作系统调优

关闭交换分区,防止内存置换降低性能。

# 将/etc/fstab 文件中包含swap的行注释掉

sed -i '/swap/s/^/#/' /etc/fstab

swapoff -a

# 单用户可以打开的最大文件数量,可以设置为官方推荐的65536或更大些

echo "* - nofile 655360" >> /etc/security/limits.conf

# 单用户线程数调大

echo "* - nproc 131072" >> /etc/security/limits.conf

# 单进程可以使用的最大map内存区域数量

echo "vm.max_map_count = 655360" >> /etc/sysctl.conf

# 参数修改立即生效

sysctl -p

# 05. 设置合理的索引分片数和副本数

索引分片数建议设置为集群节点的整数倍,初始数据导入时副本数设置为0,生产环境副本数建议设置为 1(设置 1 个副本,集群任意 1 个节点宕机数据不会丢失;设置更多副本会占用更多存储空间,操作系统缓存命中率会下降,检索性能不一定提升)。单节点索引分片数建议不要超过3个,每个索引分片推荐 10-40GB ,索引分片数设置后不可以修改,副本数设置后可以修改。

# 索引设置

curl -XPUT http://localhost:9200/fulltext001?pretty -H 'Content-Type: application/json'   

-d '{

    "settings": {

        "refresh_interval": "30s",

        "merge.policy.max_merged_segment": "1000mb",

        "translog.durability": "async",

        "translog.flush_threshold_size": "2gb",

        "translog.sync_interval": "100s",

        "index": {

            "number_of_shards": “15", //5节点+3索引分片单机

            "number_of_replicas": "0"

        }

    }

}'

# mapping 设置

curl -XPOST http://localhost:9200/fulltext001/doc/_mapping?pretty  -H 'Content-Type: application/json' 

-d '{

    "doc": {

        "_all": {

            "enabled": false

        },

        "properties": {

            "content": {

                "type": "text",

                "analyzer": "ik_max_word"

            },

            "id": {

                "type": "keyword"

            }

        }

    }

}'

# 写入数据示例

curl -XPUT 'http://localhost:9200/fulltext001/doc/1?pretty' -H 'Content-Type: application/json' 

-d '{

    "id": "https://www.huxiu.com/article/215169.html",

    "content": "“娃娃机,迷你KTV,VR体验馆,堪称商场三大标配‘神器’。”一家地处商业中心在过去的这几个月里,几乎所有的综合体都“标配”了三种“设备”…"

}'

# 修改副本数示例

curl -XPUT "http://localhost:9200/fulltext001/_settings" -H 'Content-Type: application/json' 

-d '{

    "number_of_replicas": 1

}'

# 06. 使用批量请求

使用批量请求将产生比单文档索引请求好得多的性能。写入数据时调用批量提交接口,推荐每批量提交5~15MB数据。如单条记录 1KB 大小,每批次提交 10000 条左右记录写入性能较优。

# 批量请求接口API

curl -XPOST "http://localhost:9200/_bulk" -H 'Content-Type: application/json' 

-d'

{ "index" : { "_index" : "test", "_type" : "_doc", "_id" : "1" } }{ "field1" : "value1" }

{ "delete" : { "_index" : "test", "_type" : "_doc", "_id" : "2" } }

{ "create" : { "_index" : "test", "_type" : "_doc", "_id" : "3" } }{ "field1" : "value3" }

{ "update" : {"_id" : "1", "_type" : "_doc", "_index" : "test"} }{ "doc" : {"field2" : "value2"} }'

# 07. 通过多进程/线程发送数据

单线程批量写入数据往往不能充分利用服务器 CPU 资源,可以尝试调整写入线程数或者在多个客户端上同时向 ES 服务器提交写入请求。通过测试确定最佳的 worker 数量。可以通过逐渐增加工作任务数量来测试,直到集群上的 I/O 或 CPU 饱和。

# 08. 调大写入的refresh interval

ES写入和打开一个新段的轻量过程叫做refresh,可以通过设置 refresh_interval,降低每个索引的刷新频率。

# 设置 refresh interval API

curl -XPUT "http://localhost:9200/index" -H 'Content-Type: application/json' 

-d'{

    "settings": {

        "refresh_interval": "30s"

    }

}'

refresh_interval 可以在已经存在的索引上进行动态更新,生产环境中建立一个大的新索引时,可以先关闭自动刷新,待开始使用该索引时再把它们调回来。

curl -XPUT "http://localhost:9200/index/_settings" -H 'Content-Type: application/json' 

-d'{ "refresh_interval": -1 }'

curl -XPUT "http://localhost:9200/index/_settings" -H 'Content-Type: application/json' 

-d'{ "refresh_interval": "30s" }'

# 09. 配置写入的事务日志参数

事务日志 translog 的设计目的是帮助 shard 恢复操作,否则数据可能会从内存 flush 到磁盘时发生意外而丢失。事务日志 translog 的落盘(fsync)是 ES 在后台自动执行的,默认每5秒钟提交到磁盘上,或者当 translog 文件大小大于 512MB 提交,或者在每个成功的索引、删除、更新或批量请求时提交。 索引创建时,可以调整默认日志刷新间隔index.translog.sync_interval: "60s"。创建索引后可以动态调整translog参数,"index.translog.durability":"async" 相当于关闭了 index、bulk 等操作的同步 flush translog 操作,仅使用默认的定时刷新、文件大小阈值刷新机制。

# 动态设置 translog API

curl -XPUT "http://localhost:9200/index" -H 'Content-Type: application/json' 

-d'{

    "settings": {

        "index.translog.durability": "async",

        "translog.flush_threshold_size": "2gb"

    }

}'

# 10. 设计mapping配置合适的字段类型

ES 写入文档时,如果请求中指定的索引名不存在,会自动创建新索引,并根据文档内容猜测可能的字段类型。但这不是最高效的,可以根据应用场景来设计合理的字段类型。根据业务场景设计索引配置合理的分片数、副本数,设置字段类型、分词器。如果不需要合并全部字段,禁用_all字段,通过copy_to来合并字段。

curl -XPOST "http://localhost:9200/twitter/doc/_mapping?pretty" -H 'Content-Type: application/json' 

-d'{

    "doc": {

        "_all": {

            "enabled": false

        },

        "properties": {

            "user": {

                "type": "keyword"

            },

            "post_date": {

                "type": "date"

            },

            "message": {

                "type": "text",

                "analyzer": "cjk"

            }

        }

    }

}’

document 模型设计

ES里复杂的关联查询尽量别用,最好是先在 Java 里就完成关联,将关联好的数据直接写入ES中。搜索时就不需要利用ES的搜索语法来完成 join 之类的关联搜索。所以document 模型设计是非常重要的,复杂操作尽量在 document 模型设计和写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的。

# 11. 使用过滤器缓存和分片查询缓存

默认情况下ES 的查询会计算返回的每条数据与查询语句的相关度,但对于非全文索引的使用场景只想精确地查找目标数据时,通过 filter 让 ES 不计算评分,并且尽可能地缓存 filter 的结果集,供后续包含相同 filter 的查询使用,提高查询效率。

# 普通查询

curl -XGET "http://localhost:9200/twitter/_search" -H 'Content-Type: application/json' 

-d'{

    "query": {

        "match": {

            "user": "kimchy"

        }

    }

}'

# 过滤器(filter)查询

curl -XGET "http://localhost:9200/twitter/_search" -H 'Content-Type: application/json' 

-d'{

    "query": {

        "bool": {

            "filter": {

                "match": {

                    "user": "kimchy"

                }

            }

        }

    }

}'

分片查询缓存的目的是缓存聚合、提示词结果和命中数(它不会缓存返回的文档,因此,它只在 search_type=count 时起作用)。分片缓存的大小默认情况下是 JVM 堆的 1% 大小,也可手动设置在config/elasticsearch.yml文件里。

indices.requests.cache.size: 1%

查看缓存占用内存情况(name 表示节点名, query_cache 表示过滤器缓存,request_cache 表示分片缓存,fielddata 表示字段数据缓存,segments 表示索引段)。

curl -XGET "http://localhost:9200/_cat/nodes?h=name,query_cache.memory_size,request_cache.memory_size,fielddata.memory_size,segments.memory&v" 

# 12. 使用路由 routing

ES写入文档时,文档会通过一个公式路由到一个索引中的一个分片上。默认的公式如下:

shard_num = hash(_routing) % num_primary_shards

_routing 字段的取值,默认是 _id 字段,可以根据业务场景设置经常查询的字段作为路由字段。例如可以考虑将用户 id、地区作为路由字段,查询时可以过滤不必要的分片,加快查询速度。

# 写入时指定路由

curl -XPUT "http://localhost:9200/my_index/my_type/1?routing=user1" -H 'Content-Type: application/json' 

-d'{

    "title": "This is a document",

    "author": "user1"

}'

# 查询时不指定路由,需要查询所有分片,查询时指定路由,只需要查询1个分片

curl -XGET "http://localhost:9200/my_index/_search?routing=user1" -H 'Content-Type: application/json' 

-d'{

    "query": {

        "match": {

            "title": "document"

        }

    }

}'

# 返回结果

{

    "took": 1,

    "timed_out": false,

    "_shards": {

        "total": 1,

        "successful": 1,

        "skipped": 0,

        "failed": 0

    }

    ... ...

}

# 13. 强制合并只读索引,关闭历史数据索引

只读索引可以从合并成一个单独的大segment中收益,减少索引碎片,减少 JVM 堆常驻内存。强制合并索引操作会耗费大量磁盘 IO,尽量配置在业务低峰期执行。历史数据索引如果业务上不再支持查询请求,可以考虑关闭索引,减少 JVM 内存占用。

# 索引forcemerge API

curl -XPOST "http://localhost:9200/abc20180923/_forcemerge?max_num_segments=1"

# 索引关闭API

curl -XPOST "http://localhost:9200/abc2017*/_close"

# 14. 配置合适的分词器

ES 内置了很多分词器,也可以安装自研/开源分词器。根据业务场景选择合适的分词器,避免全部采用默认 standard 分词器。

常用分词器:

cjk:根据二元索引对中日韩文分词,可以保证查全率。

IK:比较热门的中文分词,能按照中文语义切分,可以自定义词典。

pinyin:可以让用户输入拼音,就能查找到相关的关键词。

aliws:阿里巴巴自研分词,支持多种模型和分词算法,词库丰富,分词结果准确,适用于对查准要求高的场景。

# 分词效果测试API

curl -XPOST "http://localhost:9200/_analyze" -H 'Content-Type: application/json' 

-d'{

    "analyzer": "ik_max_word",

    "text": "南京市长江大桥"

}'

# 15. 配置查询聚合节点

查询聚合节点可以发送粒子查询请求到其他节点,收集和合并结果,以及响应发出查询的客户端。通过给查询聚合节点配置更高规格的 CPU 和内存,可以加快查询运算速度、提升缓存命中率。

# 查询聚合节点配置(conf/elasticsearch.yml):

node.master:false

node.data:false

node.ingest:false

# 16. 设置查询读取记录条数和字段

默认的查询请求通常返回排序后的前 10 条记录,最多一次读取 10000 条记录,通过 from 和 size 参数控制读取记录范围,避免一次读取过多的记录。通过 _source 参数可以控制返回字段信息,尽量避免读取大字段。

# 查询请求示例

curl -XGET http://localhost:9200/fulltext001/_search?pretty  -H 'Content-Type: application/json' 

-d '{

    "from": 0,

    "size": 10,

    "_source": "id",

    "query": {

        "bool": {

            "must": [

                {

                    "match": {

                        "content": "虎嗅"

                    }

                }

            ]

        }

    },

    "sort": [

        {

            "id": {

                "order": "asc"

            }

        }

    ]

}'

# 17. 设置 teminate_after 查询快速返回

teminate_after指定每个 shard 最多匹配 N 条记录后返回,设置查询超时时间 timeout。在查询结果中可以通过 “terminated_early” 字段标识是否提前结束查询请求。

# teminate_after 查询语法示例

curl -XGET "http://localhost:9200/twitter/_search" -H 'Content-Type: application/json' 

-d'{

    "from": 0,

    "size": 10,

    "timeout": "10s",

    "terminate_after": 1000,

    "query": {

        "bool": {

            "filter": {

                "term": {

                    "user": "elastic"

                }

            }

        }

    }

}'

# 18. 避免查询深度翻页

ES 默认只允许查看排序前 10000 条的结果,当翻页查看排序靠后的记录时,响应耗时一般较长。ES的分页陷阱-假如每页是10条数据,现在要查询第 100 页,实际上是会把每个 shard 上存储的前 1000 条数据都查到一个协调节点上,如果你有个5个shard,那么就有5000条数据,接着协调节点对这5000 条数据进行一些合并处理,再获取到最终第 100 页的 10 条数据。 分布式集群中,必须得从每个 shard 都查 1000 条数据过来,然后根据需求进行排序、筛选等操作,最后再次分页,拿到里面第 100 页的数据。所以翻页越深,每个 shard 返回的数据就越多,而且协调节点处理的时间越长,非常坑爹。所以用不允许深度分页。

如果是APP下拉可以用 scroll api,scroll 会一次性生成所有数据的一个快照,然后每次滑动向后翻页就是通过游标 scroll_id移动,获取下一页,性能会比分页要高很多,基本上都是毫秒级的。但是这个适合于下拉翻页的不能随意乱跳页的场景。所以现在很多产品都不允许随意翻页的。初始化时必须指定scroll参数,告诉ES要保存此次搜索的上下文多长时间。你需要确保用户不会持续不断翻页翻几个小时,否则可能因为超时而失败。

除了用 scroll api,也可用 search_after 来做,search_after 的思想是使用前一页的结果来帮助检索下一页的数据,显然,这种方式也不允许你随意翻页,你只能一页页往后翻。初始化时,需要使用一个唯一值的字段作为 sort 字段。使用search_after方式查询会更轻量级,如果每次只需要返回 10 条结果,则每个 shard 只需要返回 search_after 之后的 10 个结果即可,返回的总数据量只是和 shard 个数以及本次需要的个数有关,和历史已读取的个数无关。

# search_after查询语法示例

curl -XGET "http://localhost:9200/twitter/_search" -H 'Content-Type: application/json' 

-d'{

    "size": 10,

    "query": {

        "match": {

            "message": "Elasticsearch"

        }

    },

    "sort": [

        {

            "_score": {

                "order": "desc"

            }

        },

        {

            "_id": {

                "order": "asc"

            }

        }

    ],

    "search_after": [

        0.84290016,     //上一次response中某个doc的score

        "1024"          //上一次response中某个doc的id

    ]

}’

# 19. 避免前缀模糊匹配

Elasticsearch 默认支持通过 *? 正则表达式来做模糊匹配,如果在一个数据量较大规模的索引上执行模糊匹配,尤其是前缀模糊匹配,通常耗时会比较长,甚至可能导致内存溢出。尽量避免在高并发查询请求的生产环境执行这类操作。 

如查询请求"角色:*A8848*"查询时,往往导致整个集群负载较高。通过对数据预处理,增加冗余字段 "角色.keyword",并事先将所有角色按照1、2、3...7分词后存储至该字段,字段存储内容示例:京,A,8,4,京A,A8,88,84,48,京A8...京A88488。通过查询"角色.keyword:A8848"即可解决原来的性能问题。

# 20. 避免索引稀疏

Elasticsearch7.X 版本只允许 type 值为“_doc”,在一个索引下面创建多个字段不一样的 type,或者将几百个字段不一样的索引合并到一个索引中,会导致索引稀疏问题。 建议每个索引下只创建一个 type,字段不一样的数据分别独立创建index,不要合并成一个大索引。每个查询请求根据需要去读取相应的索引,避免查询大索引扫描全部记录,加快查询速度。

# 21. 扩容集群节点个数,升级节点规格

# 22. 数据预热

 后台监控系统每隔一段时间去系统搜索一下热数据,刷到 filesystem cache 里去。对于访问高频数据,最好做一个专门的缓存预热子系统,就是对热数据每隔一段时间,就提前访问一下,让数据进入 filesystem cache 里面去,这样二次访问性能提升很多。

# 23. 数据冷热分离

  ES可以做类似于mysql的水平拆分,即将低频冷数据和高频热数据分别写不同的索引,这样可确保热数据在被预热之后,尽量都留在 filesystem os cache 里,不受冷数据冲掉。比如有6台机器,两个索引,一个放冷数据,一个放热数据,每个索引 3 个shard。3台机器放热数据 index,另外3台机器放冷数据 index。这样大量的时间是在访问热数据 index,热数据量的占比小就能保证其保留在 filesystem cache 里,确保热数据的访问高性能。由于冷热索引的隔离,即使访问冷数据都在磁盘上,此时性能差点,但访问低频就能接受。

# 24. 合理选取搜索字段

比如现在有一行数据,id,name,age .... 30 个字段。但是现在搜索只需要根据 id,name,age 三个字段来搜索。仅仅写入 es 中要用来检索的少数几个字段就可以了,比如说就写入ES id,name,age 三个字段,然后你可以把其他的字段数据存在mysql/hbase 里,用 ES+ hbase 架构伸缩性更好。因为Hbase的特点是适用于海量数据的在线存储,就是对 Hbase 可以写入海量数据,但是不要做复杂的搜索,而是做很简单的一些根据 id 或者范围进行查询的操作。从 es 中根据 name 和 age 去搜索,拿到的结果可能就 20 个 doc id,然后根据 doc id 到 Hbase 里去查询每个 doc id 对应的完整的数据,查出来返回给前端。写入ES的数据最好小于等于或者是略微大于ES的 filesystem cache 的内存容量。先从ES检索,然后再根据 ES返回的 id 去 hbase 里查询。

注25:性能优化的关键点——filesystem cache

往ES写的数据,实际上都写到磁盘文件里去了,查询时操作系统会将磁盘文件里的数据自动缓存到 filesystem cache 里。ES搜索引擎严重依赖于底层的filesystem cache,如果给 filesystem cache 更多的内存,尽量让内存可以容纳所有的 idx segment file 索引数据文件,那么搜索时基本都是走内存的,性能会非常高。性能差距根据之前很多的测试和压测,如果走磁盘一般肯定上秒,搜索性能绝对是秒级别的,如果是走 filesystem cache,是走纯内存的,那么一般来说性能比走磁盘要高一个数量级,基本上就是毫秒级的,从几毫秒到几百毫秒不等。

如原先公司ES节点有 3 台机器,每台机器内存64G,总内存就是 64 * 3 = 192G。每台机器给ES jvm heap 是 32G,那么留给 filesystem cache的就是每台机器32G,总共集群里filesystem cache 的内存就是 32 * 3 = 96G。而此时整个磁盘上索引数据文件,在 3 台机器上共占用了1T 磁盘容量,ES数据量是 1T,那么每台机器的数据量是 300G。filesystem cache 的内存才 100G,十分之一的数据可以放内存,其他的都在磁盘,然后你执行搜索操作,大部分操作都是走磁盘,性能肯定差。要让ES性能要好,最佳的情况下,就是你的机器的内存,至少可以容纳你的总数据量的50%。

最佳的情况是仅仅在ES中就存少量的数据,就是你要用来搜索的那些索引,如果内存留给 filesystem cache 的是 100G,那么你就将索引数据控制在 100G 以内,这样的话,你的数据几乎全部走内存来搜索,性能很高1 秒以内。

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