Node.js 任务状态监控

在实际生产环境中,避免不了有很多后台运行的任务和定时任务,对任务状态的监控与及时告警可以尽量减少程序出错时对用户造成的影响。针对常见的两种任务类型:定时任务、守护进程内批处理任务,利用 Node.js child_process 实现了任务状态的监控、重启与邮件告警。

思路

现在的互联网已经不是单机作战的时代了,分布式部署是非常常见的方式,一个项目中的任务可能运行在多台服务器上,我们的监控平台要做到重启某个任务就需要知道任务运行的具体服务器,针对这一个问题我们需要获取到任务与服务器关系的确切信息,所以每台运行任务的服务器需要在启动任务时向任务状态管理平台注册自己的信息。

任务状态的维护依赖于任务运行服务器的心跳上报,每个任务设置一个超时时间,在任务启动时向任务状态管理平台发送开始运行信号,在任务运行结束后向管理平台发送运行完成信号。任务管理平台根据任务设置的超时时间,在超时后仍然没有接收到任务完成信号则判定任务失败,将任务失败信号发送回任务运行的服务器。再有任务运行服务器自行处理,如重启任务或者结束任务等。

根据以上的逻辑,实际需要就是在任务运行的服务器实现一个任务调度功能与 HTTP 服务器用来监听管理平台发送的信号;在管理平台这边实现任务服务器信息注册、任务状态监管与超时告警。文字表述比较晦涩,具体流程可以参考一下的流程图。


定时任务主进程调度系统流程图.png

定时任务状态管理平台流程图.png

实现代码

后续会把关键信息从代码中抽离出来放到配置文件中,然后放到 GitHub 上,暂时以贴代码的形式简单展示一下。

// 任务运行服务器调度系统
'use strict';

// 内建模块
const fork = require('child_process').fork;
const path = require('path');

// 第三方模块
const _ = require('lodash');
const CronJob = require('cron').CronJob;
const bodyParser = require('body-parser');
const express = require('express');
const request = require('request');
const uuid = require('uuid');

class TaskStatusManagementClient {
    /**
     * 初始化 TaskStatusClient
     * @param {Object} taskClientConfig 服务器配置信息
     */
    constructor(taskClientConfig) {
        this.taskClientConfig = taskClientConfig;
        this.taskHomePath = taskClientConfig.taskHomePath;
        this.childsInstance = {};
        this.crontabJobsInstance = {};
    }

    /**
     * start TaskStatusClient
     */
    start() {
        this._process();
    }

    _process() {
        let self = this;

        // 根据服务器配置信息启动所有任务
        for(let taskConfig of self.taskClientConfig.tasks) {
            switch(taskConfig.type) {
                case 'daemon': {
                    self._daemonTaskHandler(taskConfig);
                    break;
                }
                case 'crontab': {
                    if(taskConfig.crontabRule) {
                        self._crontabTaskHanlder(taskConfig);
                    }
                    break;
                }
                default: {
                    console.log('unknow task type');
                    break;
                }
            }
        }

        // 在程序退出时结束所有子进程任务
        process.on('exit', function (code) {
            for(let child in self.childsInstance) {
                if(self.childsInstance.hasOwnProperty(child)) {
                    self.childsInstance[child].kill('SIGHUP');
                }
            }
        });

        // 启动 HTTP 服务器,监听任务状态监控平台发回的信号
        let app = express();
        app.use(bodyParser.json());
        app.use(bodyParser.urlencoded({ extended: false }));
        app.post('/', function (req, res) {
            let body;
            try {
                body = typeof req.body !== 'object' ? JSON.parse(req.body) : req.body;
            } catch (error) {
                return res.status(400).json({ message: 'invalid json' });
            }
            res.status(200).json({ message: 'ok' });
            
            // 收到任务状态监控平台发回的信号后重启任务
            let taskConfig = _.find(self.taskClientConfig.tasks, { name: body.name });
            let taskIdentifier = '';
            // daemon 类型的任务在 childsInstance 中的 key 是任务名
            // crontab 类型的任务在 childsInstance 中的 key 是任务名 + 任务 ID
            switch (taskConfig.type) {
                case 'daemon': {
                    taskIdentifier = taskConfig.name;
                    self.childsInstance[taskIdentifier].kill('SIGHUP');
                    delete self.childsInstance[taskIdentifier];
                    if(self.handlers.daemonTaskHandler) {
                        self.handlers.daemonTaskHandler(taskConfig);
                    } else {
                        self._daemonTaskHandler(taskConfig);
                    }
                    break;
                }
                case 'crontab': {
                    taskIdentifier = taskConfig.name + taskConfig.id;
                    self.childsInstance[taskIdentifier].kill('SIGHUP');
                    delete self.childsInstance[taskIdentifier];
                    if(self.handlers.crontabTaskHandler) {
                        self.handlers.crontabTaskHandler(taskConfig);
                    } else {
                        self._crontabTaskHanlder(taskConfig);
                    }
                    break;
                }
                default: {
                    console.log('unknow task type');
                    break;
                }
            }

        });
        app.listen(self.taskClientConfig.server.port, function () {
            console.log('server start at: ' + self.taskClientConfig.server.port);
            self._registerServer(self.taskClientConfig, function (error, result) {
                if (error) {
                    console.log(error);
                }
            });
        });
    }

    _daemonTaskHandler(taskConfig) {
        let self = this;
        let taskInfo = {
            appName: self.appName,
            name: taskConfig.name,
            expires: taskConfig.timeout
        };
        let child = fork(path.join(self.taskHomePath,taskConfig.file));
        self.childsInstance[taskInfo.name] = child;
        child.on('message', function (message) {
            switch (message.signal) {
                case 'start': {
                    taskInfo.id = message.id;
                    self._startTask(taskInfo, function (error) {
                        if (error) {
                            console.log(error);
                        }
                    });
                    break;
                }
                case 'end': {
                    taskInfo.id = message.id;
                    self._endTask(taskInfo, function (error) {
                        if (error) {
                            console.log(error)
                        }
                    });
                    break;
                }
                default: {
                    console.log('unknow signal');
                    break;
                }
            }
        });
    }

    _crontabTaskHanlder(taskConfig) {
        let self = this;
        let taskInfo = {
            appName: self.appName,
            name: taskConfig.name,
            id: uuid.v4(),
            expires: taskConfig.timeout
        };
        self.crontabJobsInstance[taskInfo.name + taskInfo.id] = new CronJob(taskConfig.crontabRule, function () {
            self._startTask(taskInfo, function (error) {
                if(error) {
                    console.log(error);
                } else {
                    let child = fork(path.join(self.taskHomePath, taskConfig.file));
                    self.childsInstance[taskInfo.name + taskInfo.id] = child;
                    child.on('exit', function (code) {
                        // 子进程退出 code 为 0,代表正常退出,这时可以向监控平台发送任务已完成信号
                        if(code === 0) {
                            self._endTask(taskInfo, function (error) {
                                if (error) {
                                    console.log(error);
                                }
                            });
                        }
                    });
                }
            });
        }, undefined, true);
    }

    _startTask(taskInfo, callback) {
        let requestOptions = {
            uri: this.taskClientConfig.management.host + ':' + this.taskClientConfig.management.port + '/tasks/status/start',
            method: 'POST',
            timeout: 5000,
            form: taskInfo
        };
        request(requestOptions, function (error, response, body) {
            return callback(error, body);
        });
    }

    _endTask(taskInfo, callback) {
        let requestOptions = {
            uri: this.taskClientConfig.management.host + ':' + this.taskClientConfig.management.port + '/tasks/status/end',
            method: 'POST',
            timeout: 5000,
            form: taskInfo
        };
        request(requestOptions, function (error, response, body) {
            return callback(error, body);
        });
    }

    _registerServer(taskClientConfig, callback) {
        let requestOptions = {
            uri: this.taskClientConfig.management.host + ':' + this.taskClientConfig.management.port + '/tasks/servers',
            method: 'POST',
            timeout: 5000,
            form: taskClientConfig
        };
        request(requestOptions, function (error, response, body) {
            return callback(error, body);
        });
    }
}

module.exports = TaskStatusManagementClient;
// 调度系统的使用
'use strict';

const config = {
    // 监控平台信息,必须
    management: {
        host: 'http://127.0.0.1',
        port: 3000
    },
    // 当前服务器信息,必须
    server: {
        host: 'http://127.0.0.1',
        port: 3001
    },
    // 当前服务器任务文件地址(绝对路径),必须
    taskHomePath: path.join(__dirname, 'tasks'),
    // 任务配置信息,必须
    tasks:[{
        name: 'exampleTaskOne',
        type: 'daemon',
        file: 'example_task_one.js',
        timeout: 10000
    }, {
        name: 'exampleTaskTwo',
        type: 'crontab',
        file: 'example_task_two.js',
        crontabRule: '*/20 * * * * *',  // 任务类型为 crontab 是此字段为必须
        timeout: 10000
    }]
};
let taskStatusManagementClient = new TaskStatusManagementClient(config);
taskStatusManagementClient.start();

监控平台 HTTP 服务器比较简单,三个 API,用来将服务器信息、任务开始状态、任务结束状态写入数据库,这里就不在赘述。

// 监控平台任务状态监管
'use strict';

const async = require('async');
const config = require('config');
const mail = require('nodemailer').createTransport({
    service: 'your email service',
    auth: {
        user: 'your email username',
        pass: 'your email password'
    }
});
const request = require('request');

const logger = require('../log/logger');
const moment = require('../libs/moment');
// 一下三个为数据库操作连接,不需要关注内部代码,不影响代码阅读
const TaskResult = require('../models/task_result');
const TaskStatus = require('../models/task_status');
const TaskServer = require('../models/task_server');

/**
 * 发送告警邮件
 * @param {String} to 邮件接收者
 * @param {String} subject 邮件主题
 * @param {String} message 邮件内容
 * @param {Function} callback
 */
function sendWarningMessage(to, subject, message, callback) {
    var options = {
        'from': 'xxx@xxx.xxx',
        'to': to,
        'subject': subject,
        'text': message,
        'encoding': 'UTF-8'
    };
    mail.sendMail(options, function(error) {
        console.log('send message to: ' + to);
        return callback(error);
    });
}

/**
 * 处理执行成功任务
 * @param {String} task 任务状态对象
 * @param {Function} callback
 */
function handleSingleSucceedTask(task, callback) {
        TaskResult.increaseTaskSuccessCount(task.name, function (error) {
            return callback(error);
        });
}

/**
 * 处理执行失败的任务
 * @param {String} task 任务状态对象
 * @param {Function} callback
 */
function handleSingleErrorTask(task, callback) {
    async.waterfall([
        // 增加任务执行失败次数
        async.apply(TaskResult.increaseTaskErrorCount.bind(TaskResult), task.name),
        async.apply(TaskResult.getTaskErrorCount.bind(TaskResult), task.name),
        function (count, callback) {
            callback(undefined);
            // 给调度系统发信号
            TaskServer.findTaskServerHost(task.name, function (error, serverHost) {
                // 超过限制失败次数,发送告警邮件
                if (count >= config.get('task.limitErrorCount')) {
                    sendWarningMessage(config.get('task.noticeUserEmailAddress'), '定时任务告警', `${ serverHost }: "${ task.name }" 执行失败超过预定失败次数`,  function (error) {
                        if (error) {
                            logger.error(error);
                        }
                    });
                    TaskResult.resetTaskErrorCount(task.name, function (error) {
                        if (error) {
                            logger.error(error);
                        }
                    });
                }
                if (!error && serverHost) {
                    // send 'error' signal to task server
                    request({
                        uri: serverHost,
                        method: 'POST',
                        timeout: 5000,
                        form: {
                            name: task.name,
                            id: task.taskId,
                            pid: task.pid
                        }
                    }, function (error, response, body) {
                        if (error) {
                            logger.error(error);
                        }
                    });
                }
            });
        }
    ], function (error) {
        return callback(error);
    });
}

/**
 * 获取所有已经到达预定超时时间的任务并处理
 */
function process() {
    let currentTime = new Date().getTime();
    async.waterfall([
        async.apply(TaskStatus.getExpiredTasks.bind(TaskStatus), currentTime),
        // 并行处理每个任务
        function (tasks, callback) {
            if (tasks.length > 0) {
                async.parallel(tasks.map(function (task) {
                    return function (callback) {
                        // 超时后任务状态仍然为 running,代表任务执行失败
                        if (task.status === 'running') {
                            handleSingleErrorTask(task, function (error) {
                                return callback(error);
                            });
                        } else {
                            handleSingleSucceedTask(task, function (error) {
                                return callback(error);
                            });
                        }
                    }
                }), function (error) {
                    return callback(error);
                });
            } else {
                return callback(new Error('no tasks need to exec'));
            }
        },
        // 删除已经处理完成的任务
        async.apply(TaskStatus.removeExpiredTasks.bind(TaskStatus), currentTime)
    ], function (error) {
        if (error && error.message === 'no tasks need to exec') {
            let delay = moment.millisecondToDayMinuteHourSecond(config.get('task.noTaskDelayTime'));
            console.log(`no tasks, delay ${ delay }`);
            setTimeout(process, config.get('task.noTaskDelayTime'));
        } else if (error) {
            logger.error(error);
            process();
        } else {
            process();
        }
    });
}

module.exports = process;
定时任务状态管理平台流程图.png

实现代码

后续会把关键信息从代码中抽离出来放到配置文件中,然后放到 GitHub 上,暂时以贴代码的形式简单展示一下。

// 任务运行服务器调度系统
'use strict';

// 内建模块
const fork = require('child_process').fork;
const path = require('path');

// 第三方模块
const _ = require('lodash');
const CronJob = require('cron').CronJob;
const bodyParser = require('body-parser');
const express = require('express');
const request = require('request');
const uuid = require('uuid');

class TaskStatusManagementClient {
    /**
     * 初始化 TaskStatusClient
     * @param {Object} taskClientConfig 服务器配置信息
     */
    constructor(taskClientConfig) {
        this.taskClientConfig = taskClientConfig;
        this.taskHomePath = taskClientConfig.taskHomePath;
        this.childsInstance = {};
        this.crontabJobsInstance = {};
    }

    /**
     * start TaskStatusClient
     */
    start() {
        this._process();
    }

    _process() {
        let self = this;

        // 根据服务器配置信息启动所有任务
        for(let taskConfig of self.taskClientConfig.tasks) {
            switch(taskConfig.type) {
                case 'daemon': {
                    self._daemonTaskHandler(taskConfig);
                    break;
                }
                case 'crontab': {
                    if(taskConfig.crontabRule) {
                        self._crontabTaskHanlder(taskConfig);
                    }
                    break;
                }
                default: {
                    console.log('unknow task type');
                    break;
                }
            }
        }

        // 在程序退出时结束所有子进程任务
        process.on('exit', function (code) {
            for(let child in self.childsInstance) {
                if(self.childsInstance.hasOwnProperty(child)) {
                    self.childsInstance[child].kill('SIGHUP');
                }
            }
        });

        // 启动 HTTP 服务器,监听任务状态监控平台发回的信号
        let app = express();
        app.use(bodyParser.json());
        app.use(bodyParser.urlencoded({ extended: false }));
        app.post('/', function (req, res) {
            let body;
            try {
                body = typeof req.body !== 'object' ? JSON.parse(req.body) : req.body;
            } catch (error) {
                return res.status(400).json({ message: 'invalid json' });
            }
            res.status(200).json({ message: 'ok' });
            
            // 收到任务状态监控平台发回的信号后重启任务
            let taskConfig = _.find(self.taskClientConfig.tasks, { name: body.name });
            let taskIdentifier = '';
            // daemon 类型的任务在 childsInstance 中的 key 是任务名
            // crontab 类型的任务在 childsInstance 中的 key 是任务名 + 任务 ID
            switch (taskConfig.type) {
                case 'daemon': {
                    taskIdentifier = taskConfig.name;
                    self.childsInstance[taskIdentifier].kill('SIGHUP');
                    delete self.childsInstance[taskIdentifier];
                    if(self.handlers.daemonTaskHandler) {
                        self.handlers.daemonTaskHandler(taskConfig);
                    } else {
                        self._daemonTaskHandler(taskConfig);
                    }
                    break;
                }
                case 'crontab': {
                    taskIdentifier = taskConfig.name + taskConfig.id;
                    self.childsInstance[taskIdentifier].kill('SIGHUP');
                    delete self.childsInstance[taskIdentifier];
                    if(self.handlers.crontabTaskHandler) {
                        self.handlers.crontabTaskHandler(taskConfig);
                    } else {
                        self._crontabTaskHanlder(taskConfig);
                    }
                    break;
                }
                default: {
                    console.log('unknow task type');
                    break;
                }
            }

        });
        app.listen(self.taskClientConfig.server.port, function () {
            console.log('server start at: ' + self.taskClientConfig.server.port);
            self._registerServer(self.taskClientConfig, function (error, result) {
                if (error) {
                    console.log(error);
                }
            });
        });
    }

    _daemonTaskHandler(taskConfig) {
        let self = this;
        let taskInfo = {
            appName: self.appName,
            name: taskConfig.name,
            expires: taskConfig.timeout
        };
        let child = fork(path.join(self.taskHomePath,taskConfig.file));
        self.childsInstance[taskInfo.name] = child;
        child.on('message', function (message) {
            switch (message.signal) {
                case 'start': {
                    taskInfo.id = message.id;
                    self._startTask(taskInfo, function (error) {
                        if (error) {
                            console.log(error);
                        }
                    });
                    break;
                }
                case 'end': {
                    taskInfo.id = message.id;
                    self._endTask(taskInfo, function (error) {
                        if (error) {
                            console.log(error)
                        }
                    });
                    break;
                }
                default: {
                    console.log('unknow signal');
                    break;
                }
            }
        });
    }

    _crontabTaskHanlder(taskConfig) {
        let self = this;
        let taskInfo = {
            appName: self.appName,
            name: taskConfig.name,
            id: uuid.v4(),
            expires: taskConfig.timeout
        };
        self.crontabJobsInstance[taskInfo.name + taskInfo.id] = new CronJob(taskConfig.crontabRule, function () {
            self._startTask(taskInfo, function (error) {
                if(error) {
                    console.log(error);
                } else {
                    let child = fork(path.join(self.taskHomePath, taskConfig.file));
                    self.childsInstance[taskInfo.name + taskInfo.id] = child;
                    child.on('exit', function (code) {
                        // 子进程退出 code 为 0,代表正常退出,这时可以向监控平台发送任务已完成信号
                        if(code === 0) {
                            self._endTask(taskInfo, function (error) {
                                if (error) {
                                    console.log(error);
                                }
                            });
                        }
                    });
                }
            });
        }, undefined, true);
    }

    _startTask(taskInfo, callback) {
        let requestOptions = {
            uri: this.taskClientConfig.management.host + ':' + this.taskClientConfig.management.port + '/tasks/status/start',
            method: 'POST',
            timeout: 5000,
            form: taskInfo
        };
        request(requestOptions, function (error, response, body) {
            return callback(error, body);
        });
    }

    _endTask(taskInfo, callback) {
        let requestOptions = {
            uri: this.taskClientConfig.management.host + ':' + this.taskClientConfig.management.port + '/tasks/status/end',
            method: 'POST',
            timeout: 5000,
            form: taskInfo
        };
        request(requestOptions, function (error, response, body) {
            return callback(error, body);
        });
    }

    _registerServer(taskClientConfig, callback) {
        let requestOptions = {
            uri: this.taskClientConfig.management.host + ':' + this.taskClientConfig.management.port + '/tasks/servers',
            method: 'POST',
            timeout: 5000,
            form: taskClientConfig
        };
        request(requestOptions, function (error, response, body) {
            return callback(error, body);
        });
    }
}

module.exports = TaskStatusManagementClient;
// 调度系统的使用
'use strict';

const config = {
    // 监控平台信息,必须
    management: {
        host: 'http://127.0.0.1',
        port: 3000
    },
    // 当前服务器信息,必须
    server: {
        host: 'http://127.0.0.1',
        port: 3001
    },
    // 当前服务器任务文件地址(绝对路径),必须
    taskHomePath: path.join(__dirname, 'tasks'),
    // 任务配置信息,必须
    tasks:[{
        name: 'exampleTaskOne',
        type: 'daemon',
        file: 'example_task_one.js',
        timeout: 10000
    }, {
        name: 'exampleTaskTwo',
        type: 'crontab',
        file: 'example_task_two.js',
        crontabRule: '*/20 * * * * *',  // 任务类型为 crontab 是此字段为必须
        timeout: 10000
    }]
};
let taskStatusManagementClient = new TaskStatusManagementClient(config);
taskStatusManagementClient.start();

监控平台 HTTP 服务器比较简单,三个 API,用来将服务器信息、任务开始状态、任务结束状态写入数据库,这里就不在赘述。

// 监控平台任务状态监管
'use strict';

const async = require('async');
const config = require('config');
const mail = require('nodemailer').createTransport({
    service: 'your email service',
    auth: {
        user: 'your email username',
        pass: 'your email password'
    }
});
const request = require('request');

const logger = require('../log/logger');
const moment = require('../libs/moment');
// 一下三个为数据库操作连接,不需要关注内部代码,不影响代码阅读
const TaskResult = require('../models/task_result');
const TaskStatus = require('../models/task_status');
const TaskServer = require('../models/task_server');

/**
 * 发送告警邮件
 * @param {String} to 邮件接收者
 * @param {String} subject 邮件主题
 * @param {String} message 邮件内容
 * @param {Function} callback
 */
function sendWarningMessage(to, subject, message, callback) {
    var options = {
        'from': 'xxx@xxx.xxx',
        'to': to,
        'subject': subject,
        'text': message,
        'encoding': 'UTF-8'
    };
    mail.sendMail(options, function(error) {
        console.log('send message to: ' + to);
        return callback(error);
    });
}

/**
 * 处理执行成功任务
 * @param {String} task 任务状态对象
 * @param {Function} callback
 */
function handleSingleSucceedTask(task, callback) {
        TaskResult.increaseTaskSuccessCount(task.name, function (error) {
            return callback(error);
        });
}

/**
 * 处理执行失败的任务
 * @param {String} task 任务状态对象
 * @param {Function} callback
 */
function handleSingleErrorTask(task, callback) {
    async.waterfall([
        // 增加任务执行失败次数
        async.apply(TaskResult.increaseTaskErrorCount.bind(TaskResult), task.name),
        async.apply(TaskResult.getTaskErrorCount.bind(TaskResult), task.name),
        function (count, callback) {
            callback(undefined);
            // 给调度系统发信号
            TaskServer.findTaskServerHost(task.name, function (error, serverHost) {
                // 超过限制失败次数,发送告警邮件
                if (count >= config.get('task.limitErrorCount')) {
                    sendWarningMessage(config.get('task.noticeUserEmailAddress'), '定时任务告警', `${ serverHost }: "${ task.name }" 执行失败超过预定失败次数`,  function (error) {
                        if (error) {
                            logger.error(error);
                        }
                    });
                    TaskResult.resetTaskErrorCount(task.name, function (error) {
                        if (error) {
                            logger.error(error);
                        }
                    });
                }
                if (!error && serverHost) {
                    // send 'error' signal to task server
                    request({
                        uri: serverHost,
                        method: 'POST',
                        timeout: 5000,
                        form: {
                            name: task.name,
                            id: task.taskId,
                            pid: task.pid
                        }
                    }, function (error, response, body) {
                        if (error) {
                            logger.error(error);
                        }
                    });
                }
            });
        }
    ], function (error) {
        return callback(error);
    });
}

/**
 * 获取所有已经到达预定超时时间的任务并处理
 */
function process() {
    let currentTime = new Date().getTime();
    async.waterfall([
        async.apply(TaskStatus.getExpiredTasks.bind(TaskStatus), currentTime),
        // 并行处理每个任务
        function (tasks, callback) {
            if (tasks.length > 0) {
                async.parallel(tasks.map(function (task) {
                    return function (callback) {
                        // 超时后任务状态仍然为 running,代表任务执行失败
                        if (task.status === 'running') {
                            handleSingleErrorTask(task, function (error) {
                                return callback(error);
                            });
                        } else {
                            handleSingleSucceedTask(task, function (error) {
                                return callback(error);
                            });
                        }
                    }
                }), function (error) {
                    return callback(error);
                });
            } else {
                return callback(new Error('no tasks need to exec'));
            }
        },
        // 删除已经处理完成的任务
        async.apply(TaskStatus.removeExpiredTasks.bind(TaskStatus), currentTime)
    ], function (error) {
        if (error && error.message === 'no tasks need to exec') {
            let delay = moment.millisecondToDayMinuteHourSecond(config.get('task.noTaskDelayTime'));
            console.log(`no tasks, delay ${ delay }`);
            setTimeout(process, config.get('task.noTaskDelayTime'));
        } else if (error) {
            logger.error(error);
            process();
        } else {
            process();
        }
    });
}

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

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,800评论 6 13
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • 本文转自运维之路(id:HuashengPeng001)订阅号 近年来,随着计算机技术的飞速发展,以及行业信息的共...
    大数据之心阅读 5,521评论 0 34
  • 明月皎皎,蝉鸣喈喈。 既见良人,云胡不夷? 明月昭昭,蝉鸣嘁嘁。 既见良人,云胡不瘳? 明月皓皓,蝉鸣不已。 既见...
    布瓜先生阅读 447评论 3 2
  • 坚强是什么呢?每个人都认为不一样吧…… 我认为很简单,有一天你可以笑着讲述那些曾经让你哭的过往
    韩昕阅读 194评论 0 0