owt-server 的集群管理者、集群工作站、消息队列(一)

转载请注明:

原始地址: https://www.jianshu.com/p/9f2fb27062cb

原作者:wonder


1、写在前面

owt-server使用node.js开发,涉及node.js c++混合开发。

owt-server的目录结构如下:


root@ubuntu:/home/wonder/OWT/owt-server-master# ls

build  cert  doc  docker  LICENSE  node_modules  README.md  scripts  source  test  third_party

各种环境安装脚本在 scripts/ 下,参考README.md进行编译、安装、运行、测试即可


2、owt-server简要介绍

owt-server是集群式的媒体服务。每种功能模块可以是集群(cluster)的一个工作站(worker),多个worker由中心管理者(manager)管理,管理者有主(master)/备(slave)/候选者(candidate)之分。有些模块可以复用同一个worker。

worker、manager之间通过消息队列进行 任务传递/rpc调用。owt-server使用了node.js中的amqp库模块连接宿主机中运行的rabbitmq,以此作为消息队列的底层实现。


3、clusterManager集群管理者模块概述

查看目录 source/cluster_manager/


root@ubuntu:/home/wonder/OWT/owt-server-master# ls source/cluster_manager/

clusterManager.js  cluster_manager.toml  dist.json  index.js  log4js_configuration.json  matcher.js  package.json  scheduler.js  strategy.js

其中index.js,为该模块的启动入口。**index.js **代码前段是引用必要的库,重点库有:

1)amqp_client (库实现位于source/common/amqp_client.js):主要用是owt-server的 rpc 封装(利用amqp实现RPC角色的封装定义,如rpcClient、rpcServer等),后文会介绍

2)clusterManager(位于source/cluster_manager/clusterManager.js):主要定义了集群管理中心之管理者(manager)、候选者(candidate)自荐竞选、主(master)/备(slave)、同步、保活等方法。


3.1 clusterManager模块详细分解

clusterManager.js定义了主要的4内部函数变量(var ClusterManager、var runAsSlave、var runAsMaster和var runAsCandidate)和一个导出函数变量(exports.run)

下面就是 clusterManager.js,源码没有注释,笔者在走读源码后根据自己的理解添加了注释。涉及到对专业术语有疑问的,如rpc、主\备\候选者(master/slave/candidate)、以及 js 语法,请自行百度。

    推荐两篇介绍消息队列的网址,个人觉得很不错:

        【推荐看这篇,全面且有拓展】:[https://www.jianshu.com/p/a4d92d0d7e19](https://www.jianshu.com/p/a4d92d0d7e19)

          rabbitmq 对于AMQP的介绍:[https://www.rabbitmq.com/tutorials/amqp-concepts.html#message-acknowledge]

var ClusterManager = function (clusterName, selfId, spec) { //集群管理者定义**

     //省略一些定义

    ......

    //   var  ClusterManager  定义了集群管理中心(manager)的内部函数变量和返回值:**

    //    以下为内部函数变量,函数体均省略,请查看源码**

    var createScheduler = function (purpose);    /*创建某一类任务的调度器(Scheduler,记录\管理\执行)*/

    var checkAlive = function ();    /*检查该manager 所管理的工作站 (worker)的存活情况*/

    var workerJoin = function (purpose, worker, info);    /*执行某一类任务的worker加入该manager */

    var workerQuit = function (worker);    /*一个 worker 从该manager 退出该*/

    var keepAlive = function (worker, on_result);    /*一个 worker 向该 manager  申请保活*/

    var reportState = function (worker, state);    /* 该 manager  报告某 worker 的状态*/

    var reportLoad = function (worker, load);    /*该 manager 报告某 woker 的负载*/

    var pickUpTasks = function (worker, tasks);    /*令某 worker 执行某些任务*/

    var layDownTask = function (worker, task);    /*令某 worker 放弃执行某任务*/

    var schedule = function (purpose, task, preference, reserveTime, on_ok, on_error);    /*对某类型(purpose) 的任务 (task) 按照指定配置 (preference,reserveTime) 分配worker*/

    var unschedule = function (worker, task);    /*撤销某 worker 下分配的任务*/

    var getWorkerAttr = function (worker, on_ok, on_error);    /*获取某 worker 的属性*/

    var getWorkers = function (purpose, on_ok);    /*获取某类 workers*/

    var getTasks = function (worker, on_ok);    /*获取某 worker 的任务*/

    var getScheduled = function (purpose, task, on_ok, on_error);    /*获取某类型任务的 worker*/

    //    以下为返回值:

    var that = {name: clusterName,  id: selfId};    /*ClusterManager(...)的返回值that*/

    that.getRuntimeData = function (on_data);    /*收集该 manager 管理的每类Scheduler、每个 worker 、每个 task*/

    that.registerDataUpdate = function (on_updated_data);    /*向该 manager 注册消息同步实例*/

    that.setRuntimeData = function (data);    /*向该 manager 配置 data 中记录的 Scheduler、worker和 task*/

    that.setUpdatedData = function (data);    /*向该 manager 更新信息,data.type∈{"worker_join "," worker_quit"," worker_state"," worker_load","worker_pickup "," worker_laydown"," scheduled"," unscheduled" },data具体数据结构,请查看源码*/

    that.serve = function (monitoringTgt);    /*启用 manager 服务,并注册管理目标*/

     that.rpcAPI = {    /*rpc接口函数,以下函数体均省略,请查看源码。它们与内部函数变量是对应的*/

                join: function (purpose, worker, info, callback) { ...},

                quit: function (worker) { ...},

                keepAlive: function (worker, callback) { ...},

                reportState: function (worker, state) { ...},

                reportLoad: function (worker, load) { ...},

                pickUpTasks: function (worker, tasks) { ...},

               layDownTask: function (worker, task) { ...},

               schedule: function (purpose, task, preference, reserveTime, callback) { ...},

               unschedule: function (worker, task) { ...},

               getWorkerAttr: function (worker, callback) { ...},

                getWorkers: function (purpose, callback) {  ...},

               getTasks: function (worker, callback) {  ...},

                getScheduled: function (purpose, task, callback) { ...}

    }

}

var runAsSlave= function(topicChannel, manager) {    //集群管理者作为 “备份”(salve) 的身份运行**

    //省略一些定义 

    ......

    **//以下为内部函数变量,函数体均省略,请查看源码**

    var requestRuntimeData = function ();     /*向首要集群管理者 (master)  请求运行期间的数据,数据内容参阅上文var ClusterManager 返回值中的 getRuntimeData  函数*/

   var onTopicMessage = function(message);     /*接收到主题消息时的处理函数,message.type∈{ “runtimeData”,“updateData”,“declareMaster” },分别对应着 “收到运行期间的数据”,“收到数据更新”,“收到 master 的角色申明 ” */

    var superviseMaster = function ();    /*监督 master 的定时任务(30ms检查一次),若当前master失联(matster心跳超时大于2次),则该 salve 将进入候选者身份(candidate) 的状态*/

    **//以下为调用 runAsSlave  将执行的函数体**

    topicChannel.subscribe(     //在指定的主题信道下(基于消息队列) 订阅两种主题消息

        ['clusterManager.slave.#', 'clusterManager.*.' + manager.id] ,    //两种主题的关键id

        onTopicMessage ,    //主题消息处理函数

        function () {    //订阅成功后,执行的函数体

            requestRuntimeData();     //向 master 请求运行期间的数据

            superviseMaster();    //监督 master

        }

    )

**}**

**var runAsMaster = function(topicChannel, manager) {**

    //省略一些定义     

    ...... 

     topicChannel.bus.asRpcServer(    //启用远程调用服务

            manager.name,    //master 名称

            manager.rpcAPI,    //master的rpc接口

            function(rpcSvr) {     //rpc服务启用成功后执行的函数体

               topicChannel.bus.asMonitoringTarget(function(monitoringTgt) {     //启用worker监管服务,主要用于在worker远程调用登出master时的消息回传,可类比为消息确认ACK

                        manager.serve(monitoringTgt);     //启用 master 集群管理服务 

                        setInterval( //设置定时器

                                function () {    // 向消息队列的三种主题 发送 “declareMaster ”消息  

                                        topicChannel.publish(    //主题 'clusterManager.slave' 消息

                                            'clusterManager.slave', 

                                            {type: 'declareMaster', data: {id: manager.id, life_time: life_time}}         

                                        ); 

                                        topicChannel.publish(      //主题  'clusterManager.candidate' 消息

                                            'clusterManager.candidate', 

                                            {type: 'declareMaster', data: {id: manager.id, life_time: life_time}}

                                        );                

                                         topicChannel.publish(     //主题   'clusterManager.master' 消息 

                                            'clusterManager.master', 

                                            {type: 'declareMaster', data: {id: manager.id, life_time: life_time}}

                                        );   

                                 },

                                 20     //时间间隔20ms 

                         );

                        var onTopicMessage = function (message);  //消息处理函数

                        topicChannel.subscribe(     //订阅主题消息

                                ['clusterManager.master.#', 'clusterManager.*.' + manager.id],

                                onTopicMessage,    //消息处理函数

                                  function () {     //订阅成功执行的函数体

                                            manager.registerDataUpdate(    //注册通知slave的具体方法

                                                    topicChannel.publish(    //通过消息队列发送主题为 'clusterManager.slave' 的消息

                                                             'clusterManager.slave',

                                                              {type: 'updateData', data: data}  

                                                    )

                                            );

                                    }

                            );

                        },

                        function(reason) { process.exit();}; //asMonitoringTarget 失败

                   },

                    function(reason) {process.exit(); };    //as RPC server 失败

            }

    }

} 

var runAsCandidate = function(topicChannel, manager) {

     //省略一些定义    

     ......  

    var electMaster = function () { ...}     //该候选者决定自身身份:是 master 还是 slave

    var selfRecommend = function () { ...}    //该候选者自荐,每30ms向消息队列发送'clusterManager.candidate'  主题消息“selfRecommend”

      var onTopicMessage = function (message) { ...}    //消息处理函数。初始化后定时160ms决定自身身份;收到“selfRecommend”,若消息中id大于自身id,放弃晋升master;收到“declareMaster”,停止自荐,清除定时,成为slave身份

    topicChannel.subscribe(     //订阅 “clusterManager.candidate.#”主题消息

        ['clusterManager.candidate.#'],

        onTopicMessage,    //消息处理函数

        function () {     

            selfRecommend();      //订阅成功,该参与者开始自荐

        }

    );

} 

exports.run= function (topicChannel, clusterName, id, spec) {

    //该js库的导出函数

    var manager = new ClusterManager(clusterName, id, spec);    //生成一个集群管理者(manager)实例

    runAsCandidate(topicChannel, manager);    //该manager立即作为候选者(candiate)运行

}

思考:主/备方式的好处,在于:一定程度减少了中心式集群管理的风险,即中心管理者宕机造成集群失效的风险。其缺点也是存在的,即调度集中于主管理者,主管理者仅于备管理者进行同步,在调度请求非常频繁时,主管理者性能会成为瓶颈,这也是中心式网络应用的通病。

阅读clusterManager.js文件的总结:

a) 通过源码走读,可以明确管理者(manager)、主(master)/备(slave)/候选者(candidate)的分工以及竞选方式。

b) 这部分仅是对集群管理者(cluster_manager)的定义,对于集群工作站(worker)的定义还没有概念。目前仅知道,master暴露了一下rpc接口供调用。

c) 这部分对rpc的调用是比较高层次的,确实在owt-server的代码中,amqp_client.js文件对node.js中的amqp进行了封装,以amqp为基础实现了底层的消息收发、通知机制。

d) 这部分提到了Scheduler,它是作为某种类型的任务的管理器,供clusterManager.js使用的。它内部实现了task的记录、worker的记录、超时管理、task与worker的关联、task和worker的调度分配细节。对于记录、关联、超时管理等功能,下文不做详细描述,因为相关的接口基本与clusterManager.js 文件中 var ClusterManager 提供的接口一致。

因此,下文将仅对scheduler.js中的任务调度部分做详细分解。

       ( 消息队列重要文件amqp_client.js将在下一篇《owt-server 的集群管理、集群工作站、消息队列(二)》进行分解; 集群工作站(worker)将在《owt-server 的集群管理、集群工作站、消息队列(三)》结合具体应用类型进行分解)

3.2 scheduler模块---任务调度部分详细分解

话不多说,上干货。

首先,放两个scheduler 模块---任务调度部分需要使用的模块。

1) strategy.js

调度策略模块,描述了不同的调度准则:最近使用、最常使用、最少使用、roundRobin(轮询)、随机选取

这里贴两个(最常使用、roundRobin)进行说明


var mostUsed = function () {    

    this.allocate = function (workers, candidates, on_ok, on_error) {    //获取该策略选中的某个 work 在 workers 中的标号,candidates中存放标号

        var most = 0, found = undefined;

        for (var i in candidates) {    //在提前筛选出的候选candidates中搜索,(提前筛选好处是缩小策略算法运算的空间范围)

            var id = candidates[i];

            if (workers[id].load >= most) {    //检查id所对应work的负载,选取最大负载的work的标号

                most = workers[id].load;

                found = id;

            }

        }

        on_ok(found);    //回调选中的标号

    };};


var roundRobin = function () {

    var latest_used = 65536 * 65536;

    this.allocate = function (workers, candidates, on_ok, on_error) {    

        var i = candidates.indexOf(latest_used);    //初始返回-1

        if (i === -1) {

            latest_used = candidates[0];    //初始选第一个候选

        } else {

            latest_used = (i === candidates.length - 1) ? candidates[0] : candidates[i + 1];  //选择下一个candidates[xxx]中存放的标号

        }

        on_ok(latest_used);     //回调选中的标号

    };};

2) matcher.js

条件匹配模块,描述了不同类型work的匹配准则。owt-server提供了多种类型的服务:portal、webrtc、video、audio、analytics、conference、recording、streaming。其中有些服务需要有独特的任务task和工作站worker的匹配准则。

举两个栗子(上干货):


var webrtcMatcher = function () {

    this.match = function (preference, workers, candidates) {    //参数1是配置喜好

        var result = [],

            found_sweet = false;    //找到甜心?!?!?!?!!!,源码作者有点意思的(奸笑~)

        for (var i in candidates) {

            var id = candidates[i];

            var capacity = workers[id].info.capacity;    //每个worker在向master登记时,都会把自身能力带上

            if (is_isp_applicable(capacity.isps, preference.isp)) {    //这个isp是什么作用还不清楚,直译是“运营商”?懂的朋友可以交流一下

                if (is_region_suited(capacity.regions, preference.region)) {    //这个region也不太明确,根据字面直觉上和域控相关

                    if (!found_sweet) {

                        found_sweet = true;

                        result = [id];

                    } else {

                        result.push(id);

                    }

                } else {    //不在region里,并且没有找到甜心,强行指定甜心吗?有点迷

                    if (!found_sweet) {

                        result.push(id);

                    }

                }

            }

        }

        return result;

    };};


var videoMatcher = function () {

    this.match = function (preference, workers, candidates) {

        if (!preference || !preference.video)    

            return candidates;

        var formatContain = function (listA, listB) {    //函数,统计B在A中的数量

            var count = 0;

            listB.forEach((fmtB) => {

                if (listA.indexOf(fmtB) > -1)

                    count++;

            });

            return (count === listB.length);

        };

        var result = candidates.filter(function(cid) {    //筛选结果

            var capacity = workers[cid].info.capacity;

            var encodeOk = false;

            var decodeOk = false;

            if (capacity.video) {    

                encodeOk = formatContain(capacity.video.encode, preference.video.encode);    //判断偏好的视频编码器是否在worker的能力中

                decodeOk = formatContain(capacity.video.decode, preference.video.decode);    //判断偏好的视频解码器是否在worker的能力中

            }

            if (!encodeOk) {    //编码不匹配

                log.warn('No available workers for encoding:', JSON.stringify(preference.video.encode));

            }

            if (!decodeOk) {    //解码不匹配

                log.warn('No available workers for decoding:', JSON.stringify(preference.video.decode));

            }

            return (encodeOk && decodeOk);   //编解码都匹配才行嘛!

        });

        return result;

    };};

终于,

3)scheduler.js

代码不多,就是淦~


exports.Scheduler = function(spec) {

    /*State <- [0 | 1 | 2]*/        //官方注释最为致命,这种魔数是看代码最大的障碍之一,尤其是表示状态的魔数

    /*{WorkerId: {state: State, load: Number, info: info, tasks:[Task]}*/     // Scheduler  中worker表中的属性,以及tasks表属性

    var workers = {};

    /*{Task: {reserve_timer: TimerId,

              reserve_time: Number,

              worker: WorkerId}      }*/

    var tasks = {};

    var matcher = Matcher.create(spec.purpose),     //根据指定的应用类型名称创建对应matcher 

        strategy = Strategy.create(spec.strategy),     //根据指定的策略创建对应matcher 

        schedule_reserve_time = spec.scheduleReserveTime; 

that.schedule = function (task, preference, reserveTime, on_ok, on_error) {    //参数1是需要调度的任务编号,参数2是该任务的偏好配置

        if (tasks[task]) {    //该任务编号在处理记录中

            var newReserveTime = reserveTime && tasks[task].reserve_time < reserveTime ? reserveTime : tasks[task].reserve_time,  //更新保留时长

                worker = tasks[task].worker;    //正在处理该 task 的 worker

            if (workers[worker]) {    // worker还在记录中

                if (isTaskInExecution(task, worker)) {    //该任务正在执行

                    tasks[task].reserve_time = newReserveTime;    //更新任务保留时长

                } else {    //任务没在执行

                    reserveWorkerForTask(task, worker, newReserveTime);    //向worker申请该task执行时长

                }

                return on_ok(worker, workers[worker].info);    //回调指定的 worker 和 它的信息,并返回

            } else {    //如果 worker 没了

                repealTask(task);      //清理该任务记录,准备重新分派

            }

        }

        var candidates = [];

        for (var worker in workers) {

            if (isWorkerAvailable(workers[worker])) {    //衡量worker负载和状态

                candidates.push(worker);    //加入候选

            }

        }

        if (candidates.length < 1) {    

            return on_error('No worker available, all in full load.');

        }

        candidates = matcher.match(preference, workers, candidates);    //matcher它来了,基于任务偏好、matcher类型筛选候选者

        if (candidates.length < 1) {

            return on_error('No worker matches the preference.');

        } else {

            strategy.allocate(workers, candidates, function (worker) {    //strategy它来了,基于strategy 策略选择合适的 worker

                reserveWorkerForTask(task, worker, (reserveTime && reserveTime > 0 ? reserveTime : schedule_reserve_time));    //为该task分配记录

                on_ok(worker, workers[worker].info);    //回调

            }, on_error);

        }

    };

}

总结:scheduler 使用了策略方式,结合strategy 和matcher 模块,将不同配置对应的策略与策略的执行进行解耦,方便扩展,是良好设计模式的体现。


3.3 index.js --- cluster_manager 模块入口

在节3中,提到了该模块下的index.js是入口程序,下面简单介绍一下,以形成模块到进程(程序)的概念。

首先是引用的库


var amqper = require('./amqp_client'));    //amqp封装模块

var logger = require('./logger').logger;    //日志

var log = logger.getLogger('Main');    

var ClusterManager = require('./clusterManager');    //cluster_manager模块

var toml = require('toml');    //配置文件模块

var fs = require('fs');    //文件系统模块

其次,配置文件读取、配置设置


var config;

try {  

config = toml.parse(fs.readFileSync('./cluster_manager.toml'));      //可以读一下配置文件,更加清晰

} catch (e) {

  log.error('Parsing config error on line ' + e.line + ', column ' + e.column + ': ' + e.message);

  process.exit(1);

}

config.manager = config.manager || {};    //manager配置

config.manager.name = config.manager.name || 'owt-cluster';    //manager名字

config.manager.initial_time = config.manager.initial_time || 10 * 1000;    //启动时间

config.manager.check_alive_interval = config.manager.check_alive_interval || 1000;    //manager 检查 worker 失联的时间间隔

config.manager.check_alive_count = config.manager.check_alive_count || 10;    //manager 剔除失联 worker 前的最大检查次数

config.manager.schedule_reserve_time = config.manager.schedule_reserve_time || 60 * 1000;    //调度默认保留时间(仅当调度请求没有该字段时)

config.strategy = config.strategy || {};    //调度策略, 以下为各种应用类型的默认调度策略

config.strategy.general = config.strategy.general || 'round-robin';

config.strategy.portal = config.strategy.portal || 'last-used';

config.strategy.conference = config.strategy.conference || 'last-used';

config.strategy.webrtc = config.strategy.webrtc || 'last-used';

config.strategy.sip = config.strategy.sip || 'round-robin';

config.strategy.streaming = config.strategy.streaming || 'round-robin';

config.strategy.recording = config.strategy.recording || 'randomly-pick';

config.strategy.audio = config.strategy.audio || 'most-used';

config.strategy.video = config.strategy.video || 'least-used';

config.strategy.analytics = config.strategy.analytics || 'least-used';

config.rabbit = config.rabbit || {};    //rabbitmq配置

config.rabbit.host = config.rabbit.host || 'localhost';    //rabbitmq地址

config.rabbit.port = config.rabbit.port || 5672;    //rabbitmq端口

最后,


function startup () {

    var enableService = function () {

        var id = Math.floor(Math.random() * 1000000000);    //生成随机id

        var spec = {initialTime: config.manager.initial_time,

                    checkAlivePeriod: config.manager.check_alive_interval,

                    checkAliveCount: config.manager.check_alive_count,

                    scheduleKeepTime: config.manager.schedule_reserve_time,

                    strategy: config.strategy

                   };

        amqper.asTopicParticipant(config.manager.name + '.management', function(channel) {  //利用amqp封装库加入主题,得到句柄channel

            log.info('Cluster manager up! id:', id);    

            ClusterManager.run(channel, config.manager.name, id, spec);    //使用配置、随机、句柄,启动ClusterManager

        }, function(reason) {

            log.error('Cluster manager initializing failed, reason:', reason);

            process.exit();

        });

    };

    amqper.connect(config.rabbit, function () {    //amqp封装库连接rabbitmq消息队列

        enableService();    //启动上述服务

    }, function(reason) {

        log.error('Cluster manager connect to rabbitMQ server failed, reason:', reason);

        process.exit();

    });

}

startup();    //启动

...    //省略其他系统信号设置


至此,集群管理者模块,即cluster_manager模块以及基本分解完毕。下一步就是需要理清集群工作站,以及二者如何协调工作。此外就是二者所依赖的底层消息队列究竟做了些什么,还有怎么实现的。

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