性能优化:SRS为何能做到同类的三倍

性能无疑是服务器的核心能力,几乎每个开源服务器的介绍都是”高性能XXX服务器“。视频服务器由于业务的超复杂度,特别是WebRTC服务器,要做到高性能是非常有挑战的难点。

为何性能很重要?完备的功能需要用性能交换,安全性需要用性能交换,成本需要用性能交换,产品体验需要用性能交换,甚至系统弹性都需要性能交换。有了基础性能,就有了竞争力的资本;基础性能若有问题,举步维艰,想要干点啥都不容易,就像天生羸弱的身子板。

SRS虽然是单进程单线程模型,性能一直都很高,比如:

  • 单进程能跑满千兆或万兆网卡,一般的场景完全能覆盖。
  • 性能是NginxRTMP或Janus的三倍左右,目前还没有更高性能的开源同类产品。
  • 提供集群能力,水平扩展性能,在开源项目中也不多见。

这并不是终点,这个性能基准只够5年左右,随着业务发展一定会有更高性能的需求。目前服务器属于第二代高并发架构,也就是单线程架构:

  • 第一代高并发架构,1990~2010年,多线程架构,一般比较老的服务器都是这种架构,一般无法解决C10K问题,比如Adobe AMSApache HTTP ServerJanus WebRTC Server。核心问题是多线程的水平扩展性问题,并发越多,线程之间的同步和竞争开销就越大(这个问题也是现代语言Go在性能方面的硬伤,特别是在超多CPU比如64核或128核时,多线程的损耗会更大)。
  • 第二代高并发架构,2010~2020年,单线程架构,多进程或单进程单线程都是这种架构,C10K问题得到比较好的解决,比如NginxSRSMediaSoup。核心问题是单线程引入的异步回调问题,新的语言比如Go引入轻量线程goroutine(协程)解决这个问题,老的语言比如C++20、JS await等都有对应的机制。另外多进程的进程间通信也会引入额外复杂性,比如直播和RTC的流的跨进程回源和拉流问题。
  • 第三代高并发架构,隔离的多线程架构,比如云原生的数据面反向代理Envoy线程模型,还有比Nginx更高性能的反向代理么,这就是Envoy了,如下图所示。其实Envoy和Nginx都是事件驱动,但是Envoy是完全非阻塞。而Envoy的多线程实际上和第一代的多线程也不同,线程之间几乎没有交互,可以看作是隔离的进程。由于是线程,所以它们之间的(少量)通信也很容易。同样,多线程也可以和轻量线程结合使用。
Chart of Requests per Second over HTTP by Load Balancer and Concurrency Level

SRS目前还属于第二代架构,第三代架构验证过是可行的(#2188),由于目前SRS性能还不是短板,所以没有合并到主干分支,有需要可以自己合并feature/threads。目前SRS的性能数据如下:

SFU Clients CPU Memory 线程 VM
SRS 4000 players ~94% x1 419MB 1 G5 8CPU
NginxRTMP 2400 players ~92% x1 173MB 1 G5 8CPU
SRS 2300 publishers ~89% x1 1.1GB 1 G5 8CPU
NginxRTMP 1300 publishers ~84% x1 198MB 1 G5 8CPU
SFU Clients CPU Memory 线程 VM
SRS 1000 players ~90% x1 180MB 1 G5 2CPU
Janus 700 players ~93% x2 430MB 24 G5 2CPU
SRS 950 publishers ~92% x1 132MB 1 G5 2CPU
Janus 350 publishers ~93% x2 405MB 23 G5 2CPU

Note: CentOS7, 600Kbps, ECS/G5-2.5GHZ(SkyLake), SRS/v4.0.105, NginxRTMP/v1.2.1。虽然系统有8CPU但只能使用单个CPU,选择8CPU是因为只有8CPU的内网带宽才能到10Gbps。

当我们提到性能,一般隐含条件是“满足业务场景的体验”下的性能优化,比如直播要求卡顿率低、延迟在3秒之内,比如WebRTC要求端到端延迟400ms之内(服务器0延迟),一般现在服务器的内存可以是4GB、8GB、16GB、32GB或64GB,这意味着我们可以尽量用内存Cache来降低CPU运算。

SRS Protocol VP6 H.264 VP6+MP3 H.264+MP3
2.0.72 RTMP 0.1s 0.4s 0.8s 0.6s
2.0.70 RTMP 0.1s 0.4s 1.0s 0.9s
1.0.10 RTMP 0.4s 0.4s 0.9s 1.2s
4.0.87 WebRTC x 80ms x x

Note: 在音视频服务器的性能优化中,延迟是必须要考虑的一个因素,特别是在RTC服务器,性能优化不能以增大延迟为方法。

性能基准

如果没有压测能力,就无法优化性能。

SRS的基准是并发流,比如使用srs-bench推流可以获得支持的最高推流(发布)并发,和最高拉流(播放)并发。压测工具一般读取文件,可以选择典型的业务场景,录制成样本文件,这样压测可以尽量模拟线上场景。

性能优化前,必须使用压测获得目前的性能基准,分析目前的性能瓶颈和优化思路,然后修改代码获得新的性能基准,如此反复不断提升性能。如下图所示:

上图就是性能分析的主面板,左三右二加一个浏览器:

  • 左上:服务器的top图,命令是 top -H -p $(cat objs/srs.pid) ,看CPU和内存。还有每个CPU的消耗情况(进入top后按数字1),比如us是用户空间函数,sy是内核函数,si是网卡软中断。
  • 左中:系统的网络带宽图,命令是 dstat ,看出入口带宽。比如视频平均码率是600kbps,那么900个推流时,网卡的recv流量应该是600*900/8000.0KBps也就是67.5MBps,如果网卡吞吐率达不到预期,那么肯定会出现卡顿等问题,比如可能是系统的网卡队列缓冲区太小导致丢包。
  • 左下:服务器关键日志,命令是 tail -f objs/srs.log |grep -e 'RTC: Server' -e Hybrid ,查看RTC的连接数和关键日志,以及进程的CPU等信息。如果连接数达不到预期,或者CPU接近100%,也是有问题的。
  • 右上:服务器热点函数列表,命令是 perf top -p $(cat objs/srs.pid) ,可以看到当前主要的热点函数列表,以及每个函数所占用的百分比。性能优化一般的思路,就是根据这个表,优化掉排名在前面的热点函数。
  • 右下:压测客户端的top图,如果压测服务器的CPU满载,也一样达不到预期,会出现卡顿等情况。同样也需要先检查系统的网卡队列缓冲区,避免系统丢包。
  • 浏览器:在浏览器中播放流,比如webrtc://8.126.115.13:1985/live/livestream100?eip=8.126.115.13,可以通过eip指定外网ip,这样压测工具可以推内网地址,而浏览器观看可以看外网的地址。浏览器观看可以随机抽查某个流,判断是否播放流畅,声音和画面是否正常等。

Note: 我们在左上的图中,截图时加上了标注,可以更快的看出这个性能图的摘要,比如ECS/C5 2CPU说明是ECS的C5机型一共是2个CPU,900 publish streams是RTC推流一共900个并发流。

工具链

没有工具链就无法做性能优化,前一章我们分享了压测工具srs-bench,查看网络带宽工具dstat,查看热点函数工具perf,查看CPU工具top

还有一些工具链,总结在SRS性能(CPU)、内存优化工具用法,我们挑一些和性能优化相关的工具重点介绍。包括:

  • sysctl:修改内核UDP缓冲区,防止内核丢包。
  • GPERF: GCP:使用GCP分析热点函数的调用链,图形化展示。
  • taskset:进程绑核后,避免软中断干扰,便于查看数据。

对于RTC,很重要的是需要把内核协议栈的缓冲区改大,默认只有200KB,必须改成16MB以上,否则会导致丢包:

sysctl net.core.rmem_max=16777216
sysctl net.core.rmem_default=16777216
sysctl net.core.wmem_max=16777216
sysctl net.core.wmem_default=16777216

可以直接修改文件/etc/sysctl.conf,重启也能生效:

# vi /etc/sysctl.conf
net.core.rmem_max=16777216
net.core.rmem_default=16777216
net.core.wmem_max=16777216
net.core.wmem_default=16777216

如果perf热点函数比较通用,比如是malloc,那我们可能需要分析调用链路,看是哪个执行分支导致malloc热点,由于SRS使用的协程,perf无法正确获取堆栈,我们可以用GPERF: GCP工具:

# Build SRS with GCP
./configure --with-gperf --with-gcp && make

# Start SRS with GCP
./objs/srs -c conf/console.conf

# Or CTRL+C to stop GCP
killall -2 srs

# To analysis cpu profile
./objs/pprof --text objs/srs gperf.srs.gcp*

图形化展示时,需要安装依赖graphviz

yum install -y graphviz

然后就可以生成SVG图,用浏览器打开就可以看了:

./objs/pprof --svg ./objs/srs gperf.srs.gcp >t.svg

还可以使用taskset绑定进程到某个核,这样避免在不同的核跳动,和软中断跑在一个核后干扰性能,比如一般软中断会在CPU0,我们绑定SRS到CPU1:

taskset -pc 1 $(cat objs/srs.pid)

Note:如果是多线程模式,可以增加参数-a绑定所有线程到某个核,或者在配置文件中,配置cpu_affinity指定线程的核。

内存交换性能

现代服务器的内存都很大,平均每个核有2GB内存,比如:

还有其他型号的,比如G5每个核是4GB内存,比如R5更是每个核高达8GB内存。这么多内存,对于无磁盘缓存型的网络服务器,直播转发或者SFU转发,一般内存是用不了这么多的,收包然后转发,几乎不需要缓存很久的数据。

因此,线上的视频服务器一般内存都是很充足的,有些情况下可以用内存来优化性能的地方,就可以果断的上内存缓存(Cache)策略。

比如,在直播播放时,SRS有个配置项叫合并写入(发送):

vhost __defaultVhost__ {
    play {
        # Set the MW(merged-write) min messages.
        # default: 0 (For Real-Time, min_latency on)
        # default: 1 (For WebRTC, min_latency off)
        # default: 8 (For RTMP/HTTP-FLV, min_latency off).
        mw_msgs         8;
    }
}

如果是非低延迟(默认)模式是8,也就是收到了8个音视频包后,才会转发给播放器。关键代码如下:

srs_error_t SrsRtmpConn::do_playing(SrsLiveSource* source, SrsLiveConsumer* consumer, SrsQueueRecvThread* rtrd)
{
    mw_msgs = _srs_config->get_mw_msgs(req->vhost, realtime);
    mw_sleep = _srs_config->get_mw_sleep(req->vhost);

    while (true) {
        consumer->wait(mw_msgs, mw_sleep);

        if ((err = consumer->dump_packets(&msgs, count)) != srs_success) {
            return srs_error_wrap(err, "rtmp: consumer dump packets");
        }

        if (count > 0 && (err = rtmp->send_and_free_messages(msgs.msgs, count, info->res->stream_id)) != srs_success) {
            return srs_error_wrap(err, "rtmp: send %d messages", count);
        }

如果是25fps,那么8个包大约是在320ms,考虑音频包大约是160ms延迟,这个队列的额外延迟在直播中也是可以接受的。

如果是8个包一次发送,按照平均码率1Mbps,差不多是300Mb也就是40KB的数据。如果按照峰值5Mbps码率计算,那就是一次发送200KB的数据。我们可以用writev一次发送这些数据,就可以极大的提高分发的性能了。

每个连接我们需要的内存按照1MB来计算,那么4000个连接需要4GB内存。如果是7000个连接,需要7GB的内存。可以认为直播分发的性能优化,是典型的内存(加少量延迟)来换更低的CPU使用。

Note: 当然服务器引入额外的160ms延迟对于RTC场景就是不可以接受的,只能在直播中使用这种优化。

Note: RTC的UDP发送是否能使用类似的优化?我们调研过UDP/sendmmsgUDP/GSO是可以提升一部分,但是由于UDP每个连接可合并发送的数据很少,目前压测分析热点也不在这里,所以优化有限,目前SRS并没有做这两个优化。

查找优化

STL的vector和map的查找算法,已经优化得很好了,实际上还是会成为性能瓶颈。

比如,RTC由于实现了端口复用,需要根据每个UDP包的五元组(或其他信息),查找到对应的Session处理包;Session需要根据SSRC找到对应的track,让track处理这个包。

比如,SRS的日志是可追溯的,打印时会打印出上下文ID,可以将多个会话的日志分离。这个Context ID是存储在全局的map中的,每次切换上下文需要根据协程ID查找出对应的上下文ID。

如果每个包都需要这么运算一次,那开销也是相当可观的。考虑根据UDP包查找Session,如下图:

int SrsUdpMuxSocket::recvfrom(srs_utime_t timeout)
{
    nread = srs_recvfrom(lfd, buf, nb_buf, (sockaddr*)&from, &fromlen, timeout);
    getnameinfo((sockaddr*)&from, fromlen,  (char*)&address_string, 64, (char*)&port_string
    peer_ip = std::string(address_string);
    peer_port = atoi(port_string); 

srs_error_t SrsUdpMuxListener::cycle()
{
    while (true) {
        SrsUdpMuxSocket skt(lfd);
        int nread = skt.recvfrom(SRS_UTIME_NO_TIMEOUT);
        err = handler->on_udp_packet(&skt);

srs_error_t SrsRtcServer::on_udp_packet(SrsUdpMuxSocket* skt)
{
    string peer_id = skt->peer_id();
    ISrsResource* conn = _srs_rtc_manager->find_by_id(peer_id);
    session = dynamic_cast<SrsRtcConnection*>(conn);

ISrsResource* SrsResourceManager::find_by_id(std::string id)
{
    map<string, ISrsResource*>::iterator it = conns_id_.find(id);
    return (it != conns_id_.end())? it->second : NULL;
}

这个逻辑有几个地方会有热点,通过压测可以在perf上看到:

  • 每个UDP包都调用getnameinfo将sockaddr转成字符串的ip:port,也就是地址标识,会有大量的string开辟和释放。
  • 每个UDP包都需要根据ip:port,在map中查找出对应的Session(Resource或Conneciton),字符串查找的速度是很慢的。

改进其实也容易,查找时不转成string,而是生成uint64_t的地址,目前支持的是IPv4地址只需要6字节就可以表达ip:port(如果是IPv6则需要两个uint64_t),如下所示:

int SrsUdpMuxSocket::recvfrom(srs_utime_t timeout)
{
    nread = srs_recvfrom(lfd, buf, nb_buf, (sockaddr*)&from, &fromlen, timeout);
    sockaddr_in* addr = (sockaddr_in*)&from;
    fast_id_ = uint64_t(addr->sin_port)<<48 | uint64_t(addr->sin_addr.s_addr);

srs_error_t SrsRtcServer::on_udp_packet(SrsUdpMuxSocket* skt)
{
    uint64_t fast_id = skt->fast_id();
    session = (SrsRtcConnection*)_srs_rtc_manager->find_by_fast_id(fast_id);

虽然解决了string查找的热点,随着并发的提升,map<key: uint64_t>的查找也变成了热点,在perf上可以看到map的不断平衡,我们还可以改成vector查找:

ISrsResource* SrsResourceManager::find_by_fast_id(uint64_t id)
{
    SrsResourceFastIdItem* item = &conns_level0_cache_[(id | id>>32) % nn_level0_cache_];
    if (item->available && item->fast_id == id) {
        return item->impl;
    }

    map<uint64_t, ISrsResource*>::iterator it = conns_fast_id_.find(id);
    return (it != conns_fast_id_.end())? it->second : NULL;
}

Note: 首先我们在vector中取余查找,如果取余碰撞了(两个不同id但是取余后一样,比如1001%1000和2001%1000是一样的),那么就用map查找。

Note:快速查找,可以考虑C++11的unordered_set,实现原理是类似的。

通过不同的查找方式,string变uint64_t优化了查找速度,而更快的优化是不用map查找,直接使用数组取余就是无查找了。

当然,这样的优化,让逻辑变得复杂了。

无代码优化

当我们优化完明显的热点,优化完头部热点,会发现perf显示已经没有明显的热点,有时候有些不太明显的函数也会排在前头,比如拷贝RTP Packet:

SrsRtpPacket* SrsRtpPacket::copy()
{
    SrsRtpPacket* cp = new SrsRtpPacket();

    cp->header = header;
    cp->payload_ = payload_? payload_->copy():NULL;
    cp->payload_type_ = payload_type_;

    cp->nalu_type = nalu_type;
    cp->shared_buffer_ = shared_buffer_? shared_buffer_->copy2() : NULL;
    cp->actual_buffer_size_ = actual_buffer_size_;
    cp->frame_type = frame_type;

    cp->cached_payload_size = cached_payload_size;
    // For performance issue, do not copy the unused field.
    cp->decode_handler = decode_handler;

    return cp;
}

这个函数有啥可以优化的么?没有什么可以优化的,全都是赋值和拷贝(无深拷贝),但是在perf上它就是排名在前头。

Note: 这时候千万别怀疑perf有问题,确实热点是这个拷贝是没有错的,perf不会出错,perf不会出错,perf不会出错,千万不要把焦点挪开去优化其他函数。

并不是函数性能高,就不会成为瓶颈,有个公式如下:

性能热点 = 函数执行效率 x 函数执行次数

一般我们会优先优化函数执行效率,让函数更高效。但是我们也不能忽略了函数的执行次数,如果一个高效的函数被反复的执行,一样也会变成性能热点。这时候我们的优化思路就是:如何让代码不执行,或明显减少执行次数

通过分析可以发现,这个SrsRtpPacket::copy调用点有:

  • 从Publisher拷贝到每个Consumer,函数是SrsRtcSource::on_rtp
  • 包发送后,放到Track的NACK队列,函数是SrsRtcRecvTrack::on_nackSrsRtcSendTrack::on_nack

上面第二个拷贝可以省略,由于每个Player都有NACK,所以可以减少一倍的调用,优化后这个热点也就不排在前头了:

srs_error_t SrsRtcRecvTrack::on_nack(SrsRtpPacket** ppkt)
{
    rtp_queue_->set(seq, pkt);
    *ppkt = NULL;

同样的,这个优化的代价就是增加了风险,参数也从指针,变成了指针的指针,一路都改成了指针的指针,可以犯错的概率就大太多了。

UDP协议栈

在直播优化中,我们使用writev一次写入大量的数据,大幅提高了播放的性能。

其实UDP也有类似的函数,UDP的sendto对应TCP的write,UDP的sendmmsg对应TCP的writev,我们调研过UDP/sendmmsg是可以提升一部分性能,不过它的前提是:

  • 在Perf中必须看到UDP的相关函数成为热点,如果有其他的热点比UDP更耗性能,那么上sendmmsg也不会有改善。
  • 一般并发要到2000以上,UDP协议栈才可能出现在perf的热点,较低并发时收发的包,还不足以让UDP的函数成为热点。
  • 由于不能增加延迟,需要改发送结构,集中发给多个地址的UDP包统一发送。这对可维护性上是比较大的影响。

还有一种优化是GSO,延迟分包。我们调研过UDP/GSO,比sendmmsg提升还要大一些,它的前提是:

  • sendmmsg一样,只有当UDP相关函数成为perf的热点,优化才有效。
  • GSO只能对一个客户端的包延迟组包,所以他的作用取决于要发给某个客户端的包数目,由于RTC的实时性要求,一般2到3个比较常见。

Note: 从上图可见,开启Padding后,UDP组包效能可以提升10%左右。GSO虽然不能减少实际网络上UDP包的数目,但是可以让内核延迟到最后才组UDP包,可以把GSO发送的多个包认为是一个包,相当于减少了发送UDP包的次数。

还有一种优化的可能,就是ZERO_COPY,其实TCP的零拷贝支持得比较早,但是UDP的支持得比较晚一些。收发数据时,需要从用户空间到内核空间不断拷贝,不过之前测试没有明显收益,参考ZERO-COPY

多线程

文章开头我们提到,第三代高并发架构,将是隔离的多线程架构,比如云原生的数据面反向代理Envoy线程模型

我们也调研过SRS可能的多线程架构,参考#2188。和Envoy不同,SRS涉及到了TCP和UDP,API和媒体服务,级联和QoS等问题,可能的架构也比较多。

SRS 1/2/3/4一直都是单线程(第二代架构),如下图所示:

Note:这个架构的风险一直都存在,写磁盘可能是阻塞的,DNS解析可能是阻塞的,RTC无法使用多CPU的能力(直播可以用集群或REUSE_PORT扩展多核能力)。

很显然,写磁盘应该由单独线程完成,可以避免阻塞,这就是SRS 5.0使用的架构:

Note: 其实DVR和HLS也是写磁盘操作,未来也会由写磁盘的线程实现,目前还没有实现。

Note: DNS解析也是阻塞的,和写磁盘不同,DNS解析本质上是UDP请求,是可以自己实现协议解析,不需要用多线程做。

针对RTC的多核扩展能力,有一种很自然(也是改动较小)的思路,就是将更多的能力拆分到线程中。比如SRTP加解密,占用了30%的CPU,如果能拆分到独立线程肯定对并发能力有提升。比如UDP收发也可以放到独立线程,也可以避免内核UDP收发效率不高的问题。如下图所示:

Note: 这个架构是被标记为废弃,原因就是SRTP和UDP确实效率不高,但是Hybrid里面的QoS算法是瓶颈所在,而这部分不方便拆分多线程。

Note: 另外,就算QoS拆分成多线程,这个架构最多用到大约3~4CPU,并不能用到32或64核CPU,也就是并发能力还是受限。

Note: 最后,这种多线程架构,线程之间交互较多,所以会有锁的开销,也不算第三代服务器架构。

最终的多线程架构,是能水平扩展的多线程架构,实现的原型参考feature/threads分支,如下图所示:

Note: 我们实现的一个版本是多端口模型,通过端口分割不同的Hybrid线程,Hybrid线程之间独立不需要交互。实际上多线程之间也是可以复用同样端口的,只是切网时需要考虑新五元组的绑定。

Note: 这个架构完全解决了水平扩展的问题,也避免了线程之间需要交互数据,压测在32核C5机器上,能跑到10K左右并发(可以在更多核的机器上扩展)。

当然,最后这个架构也并非没有问题,目前看还需要解决以下问题,才能在线上使用:

  • API必须非常简单,如果是Janus那种复杂的API,就无法使用这种结构。SRS目前的API是比较合适实现这种架构。API实际上承担了调度的能力。
  • 直播需要改进,适配这种多线程结构。直播Edge相对比较容易改造,可以用REUSE_PORT,相当于多个进程。而直播Origin改造比较麻烦。
  • 统计和API需要改造,系统的CPU使用率,告警和水位统计,限流策略,都会因为多线程有所不同。比如在线人数,需要汇总每个线程的连接数。
  • 全局变量和局部静态变量,必须仔细Check,保障是线程安全(thread-safe),或者是线程局部(thread-local),虽然我们在原型中已经改造得差不多,但还是需要更多确认。

由于多线程本质上和集群的能力是有一部分重合的,只是多线程的效率更高。比如直播其实可以用多个Edge部署在一台机器上(Edge后面挂一堆Edge),实现多核的扩展。RTC如果实现了级联,也一样是可以扩展多核能力,比如单核支持800个并发,每个SRS跑在一个Pod中,也可以用级联扩展能力(当然会造成进程之间的带宽比多线程要高,多线程是不走带宽)。

硬件加速

SRS没有使用硬件加速,但这个是一个很不错的思路,包括:

  • CPU指令集优化:加解密和编解码的算法,有些可以用到CPU的特殊指令,增加批处理的能力,比如AVX512
  • 专用加解密硬件卡,加解密是比较通用的算法,有专门硬件,可以调研看看。
  • UDP收发优化,不经过内核协议栈,直接从用户空间和网卡交互:DPDK

总结

还有 0% 的精彩内容
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
支付 ¥1.00 继续阅读
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容