浅析Kubernetes控制器

什么是控制器?

Kubernetes内拥有许多的控制器类型,用来控制pod的状态、行为、副本数量等等,控制器通过Pod的标签来控制Pod ,从而实现对应用的运维,如伸缩、升级等。

常用的控制器类型如下:

  1. ReplicationController 、ReplicaSet、Deployment:无状态服务,保证在任意时间运行Pod指定的副本数量,能够保证Pod总是可用的,支持滚动更新、回滚。典型用法:web服务。
  2. DaemonSet:确保集群内全部(或部分)node节点上都分配一个pod,如果新加node节点,也会自动再分配对应的pod。典型用法:filebeat日志收集、prometheus资源监控。
  3. StatefulSet:有状态服务,如各种数据存储系统。StatefullSet内的服务有着稳定的持久化存储和网络标识,有序部署,有序伸缩。
  4. Job:只运行一次的作业。
  5. CronJob:周期性运行的作业。典型用法:数据库定时备份。
  6. Horizontal Pod Autoscaling(HPA):按照期望的pod的cpu或内存来自动伸缩pod数量。

为什么需要控制器?

假如我们现在有一个Pod正在提供线上的服务,我们来想想一下我们可能会遇到的一些场景:

  • 某次运营活动非常成功,网站访问量突然暴增
  • 运行当前Pod的节点发生故障了,Pod不能正常提供服务了

针对第一种情况,可能比较好应对,一般活动之前我们会大概计算下会有多大的访问量,提前多启动几个Pod,活动结束后再把多余的Pod杀掉,虽然有点麻烦,但是应该还是能够应对这种情况的。

针对第二种情况,可能某天夜里收到大量报警说服务挂了,然后起来打开电脑在另外的节点上重新启动一个新的Pod,问题也很好的解决了。

如果我们都人工的去解决遇到的这些问题,似乎又回到了以前刀耕火种的时代了,是吧。要是有一种工具能够来帮助我们管理Pod就好了,Pod不够了自动帮我们新增一个,Pod挂了自动帮我们在合适的节点上重新启动一个Pod,如果这样的话,是不是遇到上面的问题,我们都不需要手动去解决了。

Pod分类

Pod分为自主式Pod和控制器管理的Pod这两类。

  • 自主式 Pod:Pod 退出后不会被自动创建,如图中的Pod A
  • 控制器管理的 Pod:在控制器的生命周期里,始终要维持 Pod 的副本数目,如图中的Pod B
    image.png

ReplicationController、ReplicaSet、Deployment

ReplicationController(RC)

简单来说,RC可以保证在任意时间运行Pod的副本数量,能够保证Pod总是可用的。如果实际Pod数量比指定的多,那就结束掉多余的,如果实际数量比指定的少,就新启动一些Pod。当Pod失败、被删除或者挂掉后,RC都会去自动创建新的Pod来保证副本数量,所以即使只有一个Pod,我们也应该使用RC来管理我们的Pod。

简而言之,RC用于确保其管控的Pod对象副本数满足期望的数值,它能实现以下功能:

  • 确保Pod的资源数量精确反应期望值
  • 确保Pod健康运行
  • 弹性伸缩

ReplicationController的示例如下所示:

apiVersion: v1
kind: ReplicationController
metadata:
  name: kubia
spec:
  replicas: 3
  selector:
    app: kubia
  template:
    metadata:
      labels:
        app: kubia
    spec:
      containers:
      - name: kubia
        image: luksa/kubia
        ports:
        - containerPort: 8080

一个ReplicationController有三个主要部分:

  • label selector ( 标签选择器):用于确定ReplicationController作用域中有哪些pod
  • replica count (副本个数): 指定应运行的Pod 数量
  • pod template (Pod模板): 用于创建新的Pod 副本

RC存在的问题:

大部分情况下,我们可以通过定义一个RC实现的Pod的创建和副本数量的控制
RC中包含一个完整的Pod定义模块(不包含apiversion和kind),RC是通过label selector机制来实现对Pod副本的控制的。通过改变RC里面的Pod副本数量,可以实现Pod的扩缩容功能。通过改变RC里面的Pod模板中镜像版本,可以实现Pod的滚动升级功能(但是不支持一键回滚,需要用相同的方法去修改镜像地址)

ReplicaSet(RS)

随着Kubernetes的高速发展,官方已经推荐我们使用RS和Deployment来代替RC了,实际上RS和RC的功能基本一致,
目前唯一的一个区别就是RC只支持基于等式的selector(如:env=dev或environment!=qa),但RS还支持基于集合的selector(如:version in (v1.0, v2.0)),这对复杂的运维管理就更方便了。

ReplicaSet的示例如下所示:

apiVersion: apps/v1beta2
kind: ReplicaSet
metadata:
  name: kubia
spec:
  replicas: 3
  selector:
    matchLabels:
      app: kubia
  template:
    metadata:
      labels:
        app: kubia
    spec:
      containers:
      - name: kubia
        image: luksa/kubia

ReplicationController和ReplicaSet唯一的区别就是在选择器中,不必在selector属性中直接列出pod需要的标签,
而是在selector.matchLabels下指定它们。
这是在ReplicaSet中定义标签选择器的更简单(也更不具表达力)的方式。

除了selector.matchLabels指定之外,还可以使用selector.matchExpressions指定,如下所示:

selector:
  matchExpressions:
    - key: app
      operator: In
      values:
        - kubia

使用ReplicaSet进行更富表达力的标签选择器有四个有效的运算符:

  • In : Label的值 必须与其中 一个指定的values 匹配。
  • NotIn : Label的值与任何指定的values 不匹配。
  • Exists : pod 必须包含 一个指定名称的标签(值不重要)。使用此运算符时,不应指定 values字段。
  • DoesNotExist : pod不得包含有指定名称的标签。values属性不得指定 。

如果你指定了多个表达式,则所有这些表达式都必须为true才能使选择器与pod匹配。如果同时指定matchLabels和matchExpressions, 则所有标签都必须匹配,并且所有表达式必须计算为true以使该pod与选择器匹配。

Deployment

不过我们也很少会去单独使用RS,它主要被Deployment这个更加高层的资源对象使用,除非用户需要自定义升级功能或根本不需要升级Pod,
Deployment 为 Pod 和 ReplicaSet 提供了一个申明式的定义方法。

典型的应用场景:

用来创建Pod和ReplicaSet、滚动更新和回滚、扩容和缩容、暂停与恢复。

在一般情况下,我们推荐使用Deployment而不直接使用Replica Set。

Deployment基于ReplicaSet之上,可为Pod和ReplicaSet资源提供声明式更新,它具有以下特性:

  • 事件和状态查看:可以查看Deployment对象升级的详细进度和状态
  • 回滚:当升级操作完成后发现问题时,支持将应用返回到指定的历史版本中
  • 版本记录:对Deployment 对象的每一次操作都予以保存
  • 暂停和启动:每一次升级,都可以随时暂停和启动
  • 多种自动更新方案:Recreate-重建更新、RollingUpdate-滚动更新

Deployment的更新策略描述如下:

  • RollingUpdate 策略:旧控制器的Pod数量不断减少,同时新控制器的Pod不断增加,以下两个属性:maxSurge:升级期间存在的总Pod数量最多可超过期望值的个数,可以是数值或百分比。maxUnavailabe:升级期间正常可用的Pod数(新旧版本)最多不能低于期望的个数,可以是数值或百分比。
  • Recreate 策略:在删除旧的 pod 之后才开始创建新的 pod。 如果你的应用程序不支持多个版本同时对外提供服务, 需要在启动新版本之前完全停用旧版本, 那么需要使用这种策略。 但是使用这种策略的话, 会导致应用程序出现短暂的不可用。

Deployment的示例如下所示:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: kubia
spec:
  replicas: 3
  template:
    metadata:
      name: kubia
      labels:
        app: kubia
    spec:
      containers:
      - image: luksa/kubia:v1
        name: nodejs

ReplicationController、ReplicaSet、Deployment的协调流程

image.png

ReplicationController、ReplicaSet、Deployment的区别

ReplicaSet是新一代的ReplicationController, 并推荐使用它替代ReplicationController来复制和管理 Pod。

同时,在使用 Deployment 时,实际的 Pod是由Deployment的Replicaset创建和管理的,而不是由Deployment直接创建和管理的。

image.png

StatefulSet

背景

Deployment不足以覆盖所有的应用编排问题,因为在它看来,一个应用的所有Pod,是完全一样的,所以它们之间就没有顺序,也无所谓运行在哪台宿主机上。需要时,Deployment就通过Pod模板创建新的Pod。不需要时,就“杀掉”任意一个Pod。但是在实际场景中,并不是所有应用都满足这样的要求。比如:主从关系,主备关系,还有就是数据存储类应用,多个实例通常会在本地磁盘上保存一份数据,而这些实例一旦被杀掉,即使重建出来,实例与数据之间的对应关系也已经丢失,从而导致应用失败。这种实例之间有不对等关系,或者有依赖关系的应用,被称为“有状态应用”。

StatefulSet简述

StatefulSet本质上是Deployment的一种变体,它为了解决有状态服务的问题,它所管理的Pod拥有固定的Pod名称,启停顺序,在StatefulSet中,Pod名字称为网络标识(hostname),还必须要用到共享存储。
在Deployment中,与之对应的服务是Service,而在StatefulSet中与之对应的Headless Service,Headless Service,即无头服务,与service的区别就是它没有Cluster IP,解析它的名称时将返回该Headless Service对应的全部Pod的Endpoint列表。
除此之外,StatefulSet在Headless Service的基础上又为StatefulSet控制的每个Pod副本创建了一个DNS域名,这个域名的格式为:
$(podname).(headless server name)
FQDN(全限定域名)格式为:$(podname).(headless server name).namespace.svc.cluster.local

image.png

StatefulSet将真实世界里的应用状态,抽象为了两种情况:

  • 拓扑状态:这种情况是说,应用的多个实例之间不是完全对等的关系。这些应用实例,必须按照某些顺序启动,比如某个应用的主节点A要先于B启动,那么当我把A和B两个节点删除之后,重新创建出来时,也要是这个顺序才行。并且,新创建出来的A和B,必须和原来的A和B网络标识一样,这样原先的访问者才能使用同样的方法,访问到这个新Pod。
  • 存储状态:这种情况是说,应用的多个实例分别绑定了不同的存储数据。对于这些应用实例来说,Pod A第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间Pod A被重新创建过。

所以,StatefulSet的核心功能就是通过某种方式,记录这些状态,然后在Pod被重新创建时,能够为新Pod恢复这些状态。

Headless Service

在深入了解StatefulSet之前,咱们先来讲讲Headless Service。

我们知道,Service是Kubernetes项目中用来将一组Pod暴露给外界访问的一种机制。比如,一个Deployment有3个Pod,那么我就可以定义一个Service,然后用户只要能访问到这个Service,就能访问到某个具体的Pod。但是,这个Service是怎么被访问到的呢?

  • 第一种方式,以Service的VIP(Virtual IP,即:虚拟IP)方式。比如:当我访问192.168.0.1这个Service的IP地址时,它就是一个VIP。在实际中,它会把请求转发到Service代理的具体Pod上。
  • 第二种方式,就是以Service的DNS方式。在这里又分为两种处理方法:第一种是Normal Service。这种情况下,当访问DNS记录时,解析到的是Service的VIP。第二种是Headless Service。这种情况下,访问DNS记录时,解析到的就是某一个Pod的IP地址。

可以看到,Headless Service不需要分配一个VIP,而是可以直接以DNS记录的方式解析出被代理Pod的IP地址。(Headless Service 是将Service的发布文件中的spec.clusterIP表示为None ,不让其获取clusterIP,DNS解析的时候直接走pod。) 这样设计有什么好处呢?

这样设计可以使Kubernetes为Pod分配唯一“可解析身份”。而有了这个身份之后,只要知道了一个Pod的名字以及它对应的Service的名字,就可以非常确定地通过这条DNS记录访问到Pod的IP地址。

创建StatefulSet

与ReplicaSet不同, 由StatefulSet创建的Pod拥有规则的名称(和主机名)。

image.png

StatefulSet的失败重启机制

StatefulSet使用标识完全一致的新的Pod替换, ReplicaSet则是使用一个不相干的新的Pod替换。

image.png

StatefulSet的示例如下所示:

apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: kubia
spec:
  serviceName: kubia
  replicas: 2
  template:
    metadata:
      labels:
        app: kubia
      spec:
        containers:
        - name: kubia
          image: luksa/kubia-pet
          ports:
          - name: http
            containerPort: 8080
          volumeMounts:
          - name: data
            mountPath: /var/data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      resources:
        requests:
          storage: 1Mi
      accessModes:
      - ReadWriteOnce

在这个YAML文件中,多了一个serviceName=kubia字段。这个字段的作用,就是告诉StatefulSet控制器,在执行控制循环时,要使用kubia这个Headless Service来保证Pod的“可解析身份”。这样,在创建Pod过程中,StatefulSet给它所管理的所有Pod名字,进行编号,使得每个Pod实例不重复。而更重要的是,这些Pod的创建,也是严格按照编号顺序来进行的。

StatefulSet和Deployment的区别

  • Deployment应用于无状态应用;StatefulSet应用于有状态应用。
  • Deployment的Pod之间没有顺序;StatefulSet的Pod部署、扩展、更新、删除都要有顺序。
  • Deployment的所有pod共享存储;StatefulSet的每个pod都有自己存储,所以都用volumeClaimTemplates,为每个pod都生成一个自己的存储,保存自己的状态。
  • Deployment的pod名字包含随机数字;StatefulSet的pod名字始终是固定的。
  • Deployment的Service都有ClusterIP,可以负载均衡;StatefulSet的Service没有ClusterIP,是Headless Service,所以无法负载均衡,返回的都是pod名,所以pod名字都必须固定,StatefulSet在Headless Service的基础上又为StatefulSet控制的每个Pod副本创建了一个DNS域名:$(podname).(headless server name).namespace.svc.cluster.local

DaemonSet

DaemonSet控制器确保集群中的每一个Node只运行一个特定的Pod副本,实现系统级的后台任务,也具有标签选择器。

也可以指定部分满足条件的Node运行一个Pod副本,比如监控具有ssd存储的Node节点。

常用来部署一些集群的日志、监控或者其他系统管理应用。典型的应用包括:

  • 日志收集,比如fluentd、logstash等。
  • 系统监控,比如Prometheus Node Exporter、collectd、New Relic agent、Ganglia gmond等。
  • 系统程序,比如kube-proxy、kube-dns、Glusterd、Ceph等。
image.png

DaemonSet的示例如下所示:

前提:

# 列出所用的节点
kubectl get node

# 给节点名为minikube的节点添加 disk=ssd 标签
kubectl label node minikube disk=ssd

下面创建一个模拟运行ssd-monitor 监控器进程的 DaemonSet, 该进程每5 秒会将 “SSD OK” 打印到标准输出。它将运行 一个基于 luksa/ssd-monitor 容器镜像的单容器 pod。 该 pod 的实例将在每个具有disk=ssd 标签的节点上创建。

apiVersion: apps/v1beta2
kind: DaemonSet
metadata:
  name: ssd-monitor
spec:
  selector:
    matchLabels:
      app: ssd-monitor
  template:
    metadata:
      labels:
        app: ssd-monitor
    spec:
      nodeSelector:
        disk: ssd
      containers:
      - name: main
        image: luksa/ssd-monitor

Job

到目前为止, 我们只谈论了需要持续运行的 pod。 你会遇到只想运行完成工作后就终止任务的清况。 ReplicationController 、 ReplicaSet和 DaemonSet 会持续运行任务, 永远达不到完成态。 这些 pod 中的进程在退出时会重新启动。 但是在一个可完成的任务中, 其进程终止后, 不应该再重新启动。

Job控制器用于配置Pod对象运行一次性任务,容器中的进程在正常运行结束后不会进行重启,而是将Pod对象置于“Completed”状态。若容器中的进程因为错误而终止,则需要配置确定是否要重启。

Job控制器对象主要有两种:

  • 单工作队列地串行式Job:多个一次性的作业方式串行执行多次作业,直到满足期望的次数
  • 多工作队列的并行式Job:多个工作队列并行运行多个一次性作业

配置项:

  • completions:总的执行的作业数
  • parallelism:作业执行的并行度
  • activeDeadlineSeconds:最大活动时间长度,超出此时长的作业将被终止
  • backoffLimit:将作业标记为失败状态之前的重试次数,默认为6
  • ttlSecondsAfterFinished:Completed的job默认不会清理。此配置项设置当job完成后的保留xx秒就自动清理这个job。当ttl controller清理job的时候是级联删除的,会把这个job下的pod一并删除。如果设置为0,job会被立即删除。如果不指定,job则不会被删除。

Job的示例如下所示:

定义了一个Job类型的资源,它将运行luksa/batch-job镜像,该镜像调用 一个运行120秒的进程,然后退出。

apiVersion: batch/v1
kind: Job
metadata:
  name: batch-job
spec:
  completions: 5
  parallelism: 2
  activeDeadlineSeconds: 100
  backoffLimit: 5
  ttlSecondsAfterFinished: 100
  template:
    metadata:
      labels:
        app: batch-job 
    spec:
      restartPolicy: OnFailure
      containers:
      - name: main
        image: luksa/batch-job

上面没有指定pod选择器,它将根据pod模板中的标签进行创建。

说明:

在一个pod的定义中,可以指定在容器中运行的进程结束时,Kubernetes应该做什么?

这是通过pod配置的属性restartPolicy完成的,默认为Always。Job类型的资源pod不能使用默认策略,因为它们不是要无限期地运行。因此,需要明确地将重启策略设置为OnFailure或Never。此设置防止容器在完成任务时重新启动。

CronJob

CronJob控制器执行周期性任务作业,控制其运行的时间点及重复运行的方式,类似于Linux操作系统的周期性任务作业计划的方式控制其运行的时间点和重复运行的方式。

配置项:

  • jobTemplate:Job控制器模板。
  • schedule:Cron格式的作业调度运行的时间点。
  • concurrencyPolicy:并发执行策略,用于定义前一次作业尚未完成时如何执行下一此任务。默认是Allow,即允许前后Job,甚至是属于同一个CrontJob的更多Job同时运行。如果设置为Forbid则禁止前后两个Job同时运行,如果前一个尚未结束,后一个不会启动(跳过),如果设置为Replace,则后一个Job会替代前一个Job,即终止前一个,启动后一个。
  • failedJobHistoryLimit:为失败的任务执行保留的历史记录数,默认是1。
  • successfulJobsHistoryLimit:为成功的任务执行保留的历史记录数,默认是3。
  • startingDeadlineSeconds:因各种原因缺乏执行作业的时间点所导致的启动作业错误的超时时长,会被记入错误历史记录
  • suspend:是否挂起后续的任务执行,默认是false。

CronJob的示例如下所示:

定义了一个CronJob类型的资源每15分钟运行一次批处理任务。

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: batch-job-every-fifteen-minutes
spec:
  schedule: "0,15,30,45 * * * *"
  jobTemplate:
    spec:
      template:
        metadata:
          labels:
            app: periodic-batch-job
        spec:
          restartPolicy: OnFailure
          containers:
          - name: main
            image: luksa/batch-job

配置时间表如下图所示:

image.png

时间表从左到右包含以下五个条目:

  • 分钟
  • 小时
  • 每月中的第几天
  • 星期几

了解调度Job如何运行

你可能发生 Job或Pod创建并运行得相对较晚的情况。你可能对这项作业有很高的要求,任务开始不能落后于预定的时间过多。

在这种情况下,可以通过指定CronJob规范中的startingDeadlineSeconds字段来指定截止日期。

apiVersion: batch/v1beta1
kind: CronJob
spec:
  schedule: "0,15,30,45 * * * *"
  startingDeadlineSeconds: 15

Pod 最迟必须在预定时间15秒后开始运行, 假如作业运行的时间应该是10:30:00。如果因为任何原因10:30:15不启动,任务将不会运行,并将显示为Failed。

在正常情况下,CronJob总是为schedule中配置的每个执行创建一个 Job, 但可能会同时创建两个Job, 或者根本没有建。
为了解决第一个问题,你的任务应该是幕等的(多次(而不是一次)运行不会得到不希望的结果)。
对于第二个问题,请确保下一个任务运行完成本应该由上一次(错过的) 运行完成的任何工作。

HorizontalPodAutoscaler(HPA)

应用的资源使用率通常都有高峰和低谷的时候,如何削峰填谷,提高集群的整体资源利用率,让Service中的Pod 个数自动调整呢?这就有赖于Horizontal Pod Autoscaling了,顾名思义,使Pod水平自动缩放(按照期望的pod的cpu或内存来自动伸缩pod数量)。

apiVersion: autoscaling/v1
# 资源类型是HPA
kind: HorizontalPodAutoscaler
metadata:
  name: hpa-test
  namespace: test
spec:
  # 最大副本数
  maxReplicas: 10 
  # 最小副本数
  minReplicas: 3   
  scaleTargetRef:   
    apiVersion: apps/v1beta1
    kind: Deployment  
    # 监控名为deploy-test的Deployment
    name: deploy-test   
  # cpu 阈值 CPU大于80会自动创建pod来分担服务压力,小于80则减少pod数量
  targetCPUUtilizationPercentage: 80 

基于多个Pod度量的自动伸缩(例如: CPU使用率和每秒查询率[QPS])的计算也并不复杂。 Autoscaler单独计算每个度量的副本数, 然后取最大值(例如:如果需要4个pod达到目标CPU使用率, 以及需要3个pod来达到目标QPS, 那么Autoscaler 将扩展到4个pod) 。

image.png

总结

我们通常将应用分为无状态应用、有状态应用、守护型应用、批处理应用这四种。Kubernetes针对各种类型的应用设计了相应的控制器。本文简单介绍了这几种控制器的不同用途。

控制器 用途
Deployment 无状态应用
StatefulSet 有状态应用
DaemonSet 守护进程应用
Job & CronJob 批处理作业

本文由博客一文多发平台 OpenWrite 发布!

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

推荐阅读更多精彩内容