春暖花开四月初

问题

最近做的工作,似乎没有太大难度,以至于让我在技术方面有些迷茫。而且,基于阿里的平台,很多技术方面的事情变得很容易。有时甚至可以轻轻松松地搭建起一个分布式应用,能支撑起RT在5ms以内,QPS在10w以上的访问。

另外一个问题是,有很多框架,各种层出不穷的新东西,学也学不尽。那么,怎么才能算是掌握核心技术呢?

回顾

首先回顾一下最近半年内的技术经历。

去年9月中旬,转岗到另一个部门。从Python技术转到Java方向,一切都是新奇的。

校招时本打算找Java方面的工作,但一直以来没有Java方面的项目经验,而对Java的了解也就只在Leetcode上刷题和基本的语法方面,于是直接被阿里刷掉了。后来找到了Python方向的工作,但也担心Python以后的发展,甚至因为这个事情给一些比较有名的Python工程师发过邮件咨询。其中一位这么给我说:

首先不要想着自己是一个Python工程师,应该想着自己是一个后端工程师,然后才是Python工程师。

觉得他说的在理,对以后用Python做主力语言也不是那么抗拒了。但9月份因为一些不可抗力因素要转岗,原以为也是Python,不过转岗面试时都问的是Java,然后我基础还可以,于是就这么转到新部门,技术也切到了Java方向。

1. 迁移

最开始接触的是一个后台系统,Mentor说是前不久离职的两个同事留下来的项目,以后就交我维护。那是第一个真正接触的Spring MVC项目。之前我用的是Python的Django/Flask/Tornado框架,虽然也听说过Spring MVC,但很是嫌弃Java框架一直以来笨重的xml配置(相比Django/Ruby On Rails/Grails那种惯例优于配置的轻便易用)。但是,既然上了Java的贼船,就不能不用Spring MVC,可等我在本地把那个系统用Tomcat运行起来,一个更重要的项目开始了。

之前离职的同事还负责另外一个项目,就是数据入库系统,而这些从任务分配上来讲都该由我接手。同时,因为大项目的需要,我们这个系统要迁移到另外一个平台上,于是我就开始了这段近四个月的迁移历程。

原系统架构

如图,算法组在HDFS文件系统中储算法数据,这些数据需要存入Hbase/Redis/Memcache中,提供给线上服务使用。大项目将算法迁移到了ODPS平台(阿里的OLAP平台,学堂在线上有专门对这个进行介绍),也将数据存储到了ODPS上,但是我们的服务还是要从Hbase/Redis/Memcache里取数据,这需要我们把数据由ODPS上存到具体的数据库中。

1.1 插件攻城狮

集团有自己的异构数据传输工具Datax(该工具在Github上已经开源),可以将数据在各种不同的数据库之间进行传输。该工具采用插件的方式使用,例如我需要向Hbase写数据,则必须向Datax提供Hbase的写插件。集团的Datax已经默认提供了Hbase1.1和Hbase0.94的写插件,但是但是,我们的Hbase是0.98版本的,经过测试发现不兼容。另外,Datax没有Redis和Memcache插件。本来,这个事情由另一团队负责支持我们,但由于各种原因,最后他们提供了Redis的写插件,我们则自己开发了Hbase0.98和Memcache的写插件。

插件的开发过程是不连续的,跟大项目的推进进度有关,需得算法提供ODPS上已经就绪了的数据,我才可以迁移。最开始给我的是入Hbase的数据,但是Datax上Hbase能用的插件版本跟我们的Hbase不匹配。如果不解决这个问题,我们根本迁移不了。最终我们决定自己开发Datax的Hbase0.98版本写插件。

开发插件这个事情应该是我主动要求来做的,因为当时数据已经有了,然而我却入不了Hbase,心里比较着急。不记得当时是否跟组长说过这个事情我来做,反正我主动去看了下Hbase0.94的代码,查看了Datax插件的开发文档,又去网上查了下0.94和0.98的区别,撸起袖子就开干。其实0.98的主要是底层依赖的库与0.94的代码不一样,其他层面只需要对0.94的代码做些改动即可,而1.1版本的区别就大了,这也是我选择0.94来参考的原因。这时也是我第一次接触Java的构建工具Maven。

代码改好之后,让部署的同学帮忙部署。然而,测试的时候遇到了一个奇怪的错误:有个方法找不到。由于在Java上比较没有经验,死活没找出来是啥问题,就去寻找Mentor的帮助。他看了一下错误信息,然后弄了个jd-gui的小工具,将那Hbase和Hadoop的依赖包反编译了下,在里面发现找不到的那个方法存在,但是没有符合那个参数的。当时我也不明白这是啥问题---那个错误的异常我没完全看懂,因为参数的部分是我从没见过的信息,当时Mentor又找了好久,可能他有其他方面的怀疑,反正我没看懂。

夜晚回家的路上就想,如果这个方法这个包里面没有,那么很有可能是依赖的包不对,正确的包应该在哪呢?要被迁移的入库任务也是用Java从HDFS上读数据写入Hbase,那么那些代码里面肯定有。第二天我去了,然后就看了那个pom文件,把开发的插件的依赖的Hadoop版本给换掉了,再一部署测试,没问题了!

特别感谢我的Mentor,他帮我很多,要不是他我也不晓得可能是依赖包出了问题。这个事情让我觉得Java的依赖有时候会出现很大的问题,做好依赖管理可能就是开发Java程序时比较重要的一环,也是让人感觉超级蛋疼的一环。

Hbase0.98就这么开发出来了,感觉我组长还有整个大项目的负责人都比较开心,后者还专门找我问了一下这个插件的情况。这也是我第一次真正用Java做个东西出来。

接下来我受到了鼓励,觉得开发这个插件也不是很难嘛,决定自己做Redis的写插件开发。我先调研了原先那个入库代码里面对Redis的写入情况以及对应的API,然后花了一个周末的时间把东西开发出来,并且测试、部署都没问题。但是周一上班的时候,我把这事情告诉组长,他说Redis插件和Memcache插件就另一个团队来开发,就不用我的了。话说,当时确实有点沮丧...

后来就跟另一个团队的开发同事沟通Redis写插件的需求问题,由他来给我们提供Redis写插件,但我负责在我们的环境中测试他的插件。他是一个很好哥们,哈哈,和他合作很愉快,虽然这个插件不用我的了,可我也没啥话可说。

但是在Memcache写插件的测试过程中出了点问题。由于他提供的是在Datax的另一个插件的基础上修改的插件,底层依赖的是spymemcached包,而我们这边用的是xmemcached包……最开始我也不知道我们用的是这个,后来看了代码才知道,但这时候他已经结束了Memcache插件的开发。他的插件也可以使用,但是效率低的出奇---我得开1000个线程才能满足传输速率。当时就想,很可能就是两边的依赖的库不一样导致的。然后一个周末的下午和夜晚,我用了大概8个小时,开发出基于xmemcached包的Memcache写插件,那天我还记得,最后是学校自习室的保安把我赶走了,因为待的太晚……

第二天部署测试,效率杠杠的:传输速率提高了100多倍,花费时间减少了100多倍。后来我们就果断采用我这个插件了。

当然,开发还是很开心的,尤其是做了比较有用的事情,这使我们这个迁移项目推进的速度加快了,且风险降低了。

这里面还有其他故事。由于其他组的业务需要,我还给他们开发了Redis读插件。另外,还有一个其他部门的高级工程师,经常找我咨询Hbase的事情,因为他负责他们部门的数据迁移,而Hbase标准插件不满足他的需求,然后就找我来帮他开发。当然,我们也有自己的需求,后来我给Hbase0.98写插件增加了一些功能,例如支持动态列,支持列名可空。可能是帮他太多了,那哥们说要请我吃饭,哈哈,我是那么好意思的人吗?果断说他太客气了,不需要请吃饭的。而且那阵子经常收到来自杭州的电话,因为有一个部门需要Hbase0.98的读插件,需要我支持……然后花了点时间给他们写了个读的。现在还在做他们的支持,因为动不动就找我。

这事情让我感觉还是很好玩,一是开发比较开心,另外被需要会让人感觉自己很重要,我也会有这种迷之虚幻感,哈哈。不过最重要的是让我感觉到,我终于在Java方面迈出了第一步。

1.2 简陋的入库系统
原系统架构

在上面这个架构图上,三台入库机里面的入库任务和调度,都是用shell写的。话说我当时看到这个很简陋的入库系统时,真想要把它拆了重写才好,至少要弄个Web界面,配置什么的直接在界面上操作,而不需要每次都要登录入库机,修改xml配置文件---那么长的配置文件,蛋疼啊。不过我们的全部入库任务都会迁移到集团的调度平台,重写也没啥用处。于是迁移过程中我一直在梳理这些入库Shell的流程,然后把业务逻辑迁移到集团的调度平台(D2)上。

这个系统主要承担以下几方面的功能:

  • 创建新的入库任务:主要是通过增加xml配置文件、Shell任务文件、Crontab执行项实现
  • 调度任务:执行时检查HDSF上的数据是否存在,如果不存在则等待数据,按照一定的等待时间重试;且如果机器上的资源不足,则等待再执行
  • 报警:入库失败则给相关负责人员发邮件

那么这些是怎么实现的呢?

首先crontab里面会设定某个入库任务的执行时间,以及对应的shell脚本。然后shell脚本里面会有检查HDFS文件是否存在的逻辑,有等待逻辑,还有失败发送邮件的逻辑,但最关键的是给出了入库的Java的执行命令及参数---这儿会依赖一个jar包,该包里面实现具体的入库逻辑。另外,还需要配置xml文件。Java代码会读取xml文件里面配置的executor,producer、processor和output。executor包含producer、processor和output三个参数,它表示一个完整的入库处理流程。producer表示产生数据,这个数据可能是HDFS上的数据,也可能是本地数据。processor表示处理过程,可能把数据转换成一定的格式,也可能不做处理。output则是将数据入到某个数据库中,可能是Hbase的入库代码,也可能是Redis或者Memcache。

xml文件中的配置

xml文件中需要为每个任务需要配置一个executor,一个executor里面需要配置好producer、processor和output。比较好的是依赖的jar包里面已经把相关的代码写了,很少需要更新jar包---除非有了新的需求,比如更改了数据格式,这就要增加processor的处理类,在该类里面实现具体的数据格式处理。整个老入库系统维护过程中,我只处理了一次这样的需求,升级了一次jar包,这说明这个jar包的代码写的还是比较有扩展性的。

这个系统的问题在于以下几个方面:

  • 每个任务都要增加shell文件、xml中的executor配置,时间久了,shell文件会非常多,xml长的非常长,crontab也会非常长
  • 没有机器的资源使用监控,有一次有个算法同事改了代码,不知道哪里写错了,开了三万个线程并且不关闭,其他任务没资源执行,且因为没有资源,也没有给我们发报警邮件,还是API调用部分发现没数据我们才发现这个问题……
  • 手工操作太多,有一次修改xml,文件格式弄错了,最后弄的很多入库任务都入库失败……

其实这只需要一个管理系统而已。如果这个系统没有迁移,我会做如下开发:

  • 任务脚本改用Python,不用shell,shell可读性弱,且Python还有很多库可以使用
  • 任务脚本根据配置自动生成
  • 配置改用MySQL管理,开发Web界面
  • 增加机器资源监控
  • 支持增加机器及指定机器执行,之前系统的3台机器是随着任务的增多一台台增加的
  • 再往后面就是往分布式调度方面开发了

哈哈,都是我的假想,并没有做这样的一个东西。但是迁移到D2平台已经基本满足的这样的需求,但D2也有一点不好,每次整体上的一点小变化,都要修改成百上千个任务节点,没有批量改的操作接口,真是累啊。在维护这些任务的过程中,我改过好几次每次上百个文件的任务节点配置。这让我体会到,工作上的有很多事情都是重复性的,没有开发创造那么有意思。

1.3 MR的虚幻感

这个迁移项目还有后面一步关键,就是数据转换。

入库过程

这是原始的入库流程。当算法的数据迁移到ODPS上了之后,我们即使有了Datax和对应的写插件,也还存在一个巨大的问题,就是数据处理过程。API从Hbase/Memcache/Redis取得的数据都是有一定的格式,而算法给的数据却是原始的数据。同时,ODPS支持通过SQL对其之中的数据进行查询,也支持通过MapReduce对其进行处理。于是我们迁移之后的流程就成了这样:

迁移后的流程

于是我迁移过程就变成了:

  1. 在入库的Java程序中寻找每一个shell任务对应的executor和它的具体配置,然后查看processor配置的类里面的具体处理方法
  2. 根据1中的处理方法,改写成MapReduce,在ODPS上处理

嗯,总共179个入库任务,虽然迁移过程中砍掉了一些,最终也有上百个。另外,ODPS的MapReduce感觉是给Hadoop的MapReduce增加了一层包装,所以写法略有不同。

当然,这个过程中并不是MapReduce有多难写,而是梳理Shell的处理流程,查找数据Parser的方法,才是特别耗费心力的。感觉就是苦力劳动吧。其中一个shell文件写的特别复杂,我只好打印出来从纸上看,总计有6页代码。

Hadoop的MapReduce在研究生时代就写过,并且用实验室的机器搭建过Hadoop集群。另外,研究生毕业设计是用Spark来做推荐系统的数据处理,里面写Map/Reduce真是简单的都不知道怎么说好了,没有对比就没有伤害。

由于是使用MapReduce,这让我有一种“MapReduce也就是这样子啊,也没刚出来的时候那么高大上”的感觉。我想可能是写那种简单的处理写疲惫了……

其实Hadoop的HDFS/MapReduce的细节,包括设计,都是非常值得研究的,可惜现在没时间去做这个了。也不止这个,因为我们的数据存储主要是使用Hbase,这三样正好对应Google的那三篇论文,这可就不只是值得去好好研究的了,而是一定要去研究的。且待我有时间。

1.4 总结

花了四个月的时间,磕磕碰碰,从无到有,总算是独自把这个迁移任务完成。感谢我的Mentor和我的组长,是他们在我遇到问题的时候给我提供信息,帮我理清思路,建议并指导我怎么去处理。这一个过程中感觉真的收获了不少,让我有了一定的Java开发经验。但这个过程中同样暴露了我在Java技术方面的短板,即项目经验太少,需要掌握的东西很多,有时候处理的思路也有偏移,而且对于底层有点雾里看花的感觉---可能是写的不够多吧。这四个月中,大致做了这么些事情:

  • 开发Datax插件、自己测试,并给需要的同学提供相关服务,包括Datax的使用方法
  • 梳理旧的入库系统逻辑,改造成MapReduce
  • D2平台上创建任务,包括入库、MR处理等
  • 推动算法组同学提供ODPS上的相关数据(因为这个事情阻塞不少时间)
2. 后台维护
2.1 管理系统迁移

年后过来,接受了三个后台管理系统往集团平台上迁移的任务,Mentor协助我。这三个系统都是之前同事来维护的,顺理成章,这些也该由我来迁移。其中Mentor帮我迁移了一个,自个儿完成了剩下的两个。这三个系统都是由Spring MVC写的,于是这次我才有功夫接触一点Spring MVC方面的知识。迁移的关键点在于:

  • 将原有的系统前端和后端合并成一个应用
  • 后台数据库的切换,web.xml中修改MySQL的配置
  • 旧MySQL数据同步到集团的MySQL平台中(idb4)
  • 接入SSO替换原有的CAS系统,修改原有的权限逻辑
  • 集成平台(Aone)部署测试

这个过程中,遇到了一些问题,但是都被解决了,也对Spring MVC的整个项目的构造以及容器、注解、AOP和配置有所了解了。如果现在让我写一个CURD的Spring MVC项目,我想应该没多大难度。但总感觉不踏实,因为我没有把这个东西掌握到我十分熟悉的那种地步。

2.2 审核系统

迁移过程中审核系统变得无法使用,主要是算法那边没有提供审核信息。于是我们几个相关环节的负责人把审核流程通了一遍。

流程

如上图所示,整个流程就是数据通过ODPS入到MySQL,审核人员通过Web界面审核数据,把修改存入到MySQL中,然后定时将全量的MySQL审核数据同步到ODPS上,最后生成的审核数据,会被算法用于过滤。这样,同事提供了数据,我配置好了相关的Datax任务,整个系统就运行起来了。这里面流程虽然简单,但是各方面都要通知到,尤其是使用最后的数据的同学。

2.3 用户变身

用户变身是为了给算法测试提供的一个功能,实际上就是在小结UID替换器的实现里面介绍的功能。

用户变身

本身功能是对特定用户的ID进行替换,如果请求中带有我们定义的ID,则替换成我们定义的该ID的替换ID,推荐算法根据替换后的ID计算出推荐结果。这实际上就起到了用户变身的效果。

功能要求:

  • 要有管理界面,能对我们要替换的ID和最终替换成的ID进行管理,一个这样的映射对定为一个规则
  • 要能停止和启用某个规则
  • 要能使修改后的规则立即在线上起作用,也就是要添加这样的一个按钮:使规则生效

如上图所示,在管理后台修改了规则之后,可以点击使规则生效按钮。这个按钮会调用线上API的每台机器的同一个接口,也就是图中的刷新接口。刷新接口又会调用管理系统的数据接口(HTTP),加载所有启用了的规则信息到线上API中的一个全局HashMap中。线上的请求打过来的时候,会根据请求中的ID在HashMap中做匹配,如果匹配上了,则替换掉,如果没有,则略过。

这是我用到的第一个规范的使用MySQL数据影响高访问量API的设计。如果不这样做,而是每次请求进行MySQL查询,再替换,则MySQL肯定会挂。 之所以能这么设计,是因为少量的过滤数据是可以加载到内存中,而且使用HTTP协议加载数据,实现了两个系统之间的松耦合。

这个功能是独自参考系统原有的黑名单功能做出来的,前前后后,包括写管理系统、修改线上API、测试部署等花了近5天。当然,代码本身写的不多,但在项目的哪个地方写,该写哪些东西才是花时间比较多的。

3. 实时跟踪系统

实时跟踪系统是大系统架构换新的一个关键服务,已经由另一个团队的同事们写好,交由我们使用,且当前由我负责。这个系统里面用了集团的很多产品,好在开源界有很多替换方案,此处就用开源方案给出设计图:

实时跟踪系统

系统本身功能是对用户的点击、浏览记录进行跟踪,跟踪数据要实时提供给算法使用。点击等日志会通过Kafka的发布和订阅服务,传递给我们的实时跟踪系统。实时跟踪系统中取得日志中的数据,解析某些关键数据,存储到Redis集群中---Redis中的数据量十分大,预估是5T左右。同时,实时跟踪系统通过Dubbo分布式服务框架提供RPC服务接口,返回Redis集群中存储的数据。

通过RPC服务使用Redis数据的消费者是算法组的同学,他们使用用户的实时数据进行推荐。这个系统对性能要求比较高,要求RPC调用数据响应时间在5ms以内,并且支持7w每秒的QPS。

这个服务运行的还不错,但很快就接到了新的需求:日志结构更改和提供HTTP的数据访问接口。

日志结构更改就是通过Kafka订阅的日志结构跟之前的不一样,需要重新写解析方法---当然我是看了源代码之后才知道要改解析方法。提供HTTP接口的原因是RPC服务在算法平台上可能无法使用。

大概花了两天半的时间完成了这个任务。当然,最开始还是有点慌张的,因为对这个东西一无所知,仅仅知道它叫“实时跟踪系统”,用了RPC服务框架(因为是Java菜鸟,RPC服务本身原理我没深入研究过,仅仅知道这个是做什么的)。但我答应了组长三天之内把这个东西修改好,部署并测试,还要跑成功。所谓的跑成功就是在我们的APP上有点击浏览记录,我能通过RPC服务和HTTP服务看到我的点击足迹。

其实看了代码之后就有了思路。但是对于集团的一些产品不熟悉(设计图中给的全部是开源的产品,集团内部用的不是这些,另外还有些开关和限流服务,都是第一次接触),以及测试完全是在生产环境下的,抓包找自己的设备的ID花了很长时间。好在比较快速地完成了这个任务,当时给组长说已经没问题了时,他说:好,非常好。

这个服务的代码不是我写的,但是写的不错,接下来我会把它的代码再仔细看一遍。但我感觉到,这个用8台机器运行的应用,并有5台机器的缓存(类Redis)集群,在集团提供了好用的平台的情况下,增加功能和维护变得容易,但也屏蔽了我对具体架构的了解和学习。就像这种感觉:如果一件事变得容易,那么肯定是有其他东西帮助你做好了很多事情。这也会造成对这个平台更加依赖。

思考

经历回顾带来了什么

  • 让我知道自己为什么会有一种虚的感觉
  • 代码写的不多。没有一次完整的开发整个项目的经验,基本零敲碎打,而自己也没有总结一些代码的良好的框架设计。但是,在大型的项目中,基本上每个人都负责一部分模块的开发,要一个人完全把所有功能做好,也不太现实。
  • 同时,好的平台,让人可以专注于业务开发,但也屏蔽了对更深层技术的感知。这是好事还是坏事呢?如果有一天,需要从零开始搭建一个基础平台,只会业务代码,能Hold住吗?
  • 让我知道过去的半年内到底做了哪些东西

以上的工作历程,本质上是干活和学习结合在一起的。这样的学习是有帮助的,比如后台系统中的一个处理我把它用在实时跟踪系统的改造中了。同时,这个总结也让我梳理了一下在Java技术方向半年以来做了哪些事情。当然,这些事情有些有价值,有些不一定有价值,但对我来说也是比较重要的成长经历,对自己的技术经历要清晰。

  • 让我开始去思考接下来要处理哪些事情

什么是项目经验

  • 开发经验
  • 维护和扩展经验

什么是核心技术

接下来该怎么做

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

推荐阅读更多精彩内容