使用 jenkins+docker 实现 git+springboot+gradle 及前端项目的持续集成和部署

0 前言

记录 jenkins 基于 docker 的前后端项目的构建及部署全流程。后端项目案例使用 SpringBoot+Gradle,前端项目案例框架不限

1 部署 jenkins

1.1 获取镜像

前往 https://hub.docker.com/r/jenkins/jenkins/tags 获取镜像:jenkins/jenkins:2.232-slim。也可以直接获取支持 jdk11 的镜像:jenkins/jenkins:2.232-jdk11,但该镜像体积较大,不选择。三种官方镜像信息如下:

[root@54s9uer src]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
jenkins/jenkins     2.232-centos        a355678ccb95        41 hours ago        782MB
jenkins/jenkins     2.232-jdk11         3ea7fe8adf57        41 hours ago        766MB
jenkins/jenkins     2.232-slim          803c6c58592b        41 hours ago        470MB

注: jenkins 有关 jdk11 兼容性的文档详见:jenkins-on-java-11

1.2 创建宿主机挂载目录

/usr/src/jenkins_home

1.2.1 挂载目录权限问题

若直接挂载上述目录,会因权限问题而无法启动容器。失败信息可使用 docker logs <容器ID> 命令查看容器日志:

touch: cannot touch '/var/jenkins_home/copy_reference_file.log': Permission denied
Can not write to /var/jenkins_home/copy_reference_file.log. Wrong volume permissions?

此时,先不进行挂载,使用 docker run -it --rm jenkins/jenkins:jdk11 /bin/bash 建立一个测试容器,执行 whoami && id 命令获取当前用户信息

jenkins@8dced799c01c:/$ whoami && id
jenkins
uid=1000(jenkins) gid=1000(jenkins) groups=1000(jenkins)

默认情况下,容器用户是 jenkins。进一步查看容器的 jenkins_home 目录权限,发现所有者也是 jenkins,如下所示:

jenkins@8dced799c01c:/$ ls -la /var/jenkins_home
total 20
drwxr-xr-x 2 jenkins jenkins 4096 Apr 12 00:54 .
drwxr-xr-x 1 root    root    4096 Apr  6 09:01 ..
-rw-r--r-- 1 jenkins jenkins   50 Apr 12 00:54 copy_reference_file.log
-rw-rw-r-- 1 root    root    7152 Apr  6 08:55 tini_pub.gpg

注:即使添加 -u root 参数进入容器,jenkins_home 的所有者依然是 jenkins

回到宿主机,查看 /usr/src/jenkins_home 权限,所有者是 root。因此当容器的 jenkins 用户进程访问宿主机的 /usr/src/jenkins_home 目录时,就出现 Permission denied 的问题。

1.2.2 解决方案

根据测试容器用户 uid 的结果,修改宿主机 /usr/src/jenkins_home 的所有者,执行 chown -R 1000 /usr/src/jenkins_home

注:该解决方案参考了 https://yq.aliyun.com/articles/53990

1.3 启动容器

docker run --name jenkins \
-p 8080:8080 \
--mount type=bind,src=/usr/src/jenkins_home,dst=/var/jenkins_home \
-d \
jenkins/jenkins:jdk11

1.4 添加项目构建依赖

用 root 身份进入容器

docker exec -it -u root <容器ID> /bin/bash

1.4.1 安装 git

(1)由于 jenkins/jenkins:2.232-slim 基础镜像为 Debian,因此使用 apt 工具安装

apt-get update
apt-get install git

注:通过该方法安装的版本不是最新

(2)查看执行路径

root@832c29984d24:/usr/bin# git --exec-path
/usr/lib/git-core

因此在 jenkins 的 全局工具配置 中需要的 Path to Git executable 就是 /usr/lib/git-core/git

附加内容:查看容器的系统版本。以 jenkins/jenkins:2.232-slim 镜像为例,进入容器后可执行如下命令查看

root@832c29984d24:/# cat /etc/issue
Debian GNU/Linux 10 \n \l

注:不可用 cat /proc/versionuname -a,得到的结果是是宿主机的系统信息。

附加内容:Debian 应用的安装路径通常在 /usr/share,可执行文件路径通常在 /usr/bin,配置文件路径通常在 /etc

1.4.2 安装 Amazon Corretto 11

后端项目案例使用 Amazon Corretto 11。jenkins 构建项目时也需要使用,与容器的自带 openJDK 不冲突,可以不在 /etc/profile 中设置 JAVA_HOME,只在 jenkins 的 全局工具配置 中指定即可。

注:若使用 jenkins/jenkins:2.232-jdk11 镜像,并且使用自带 openJDK,可使用 echo $JAVA_HOME 查看路径,直接复制到 jenkins 的 全局工具配置 中对应位置。

(1)在 下载页面 获取压缩包地址,下载到 /usr/src/ 目录下

wget -c https://d3pxv6yz143wms.cloudfront.net/11.0.3.7.1/amazon-corretto-11.0.3.7.1-linux-x64.tar.gz

注:若需要安装 wget 命令,执行 apt-get install wget 即可

(2)解压到 /usr/src/jdk11 目录

tar zxvf amazon-corretto-11.0.3.7.1-linux-x64.tar.gz -C /usr/src/jdk11/

因此在 jenkins 的 全局工具配置 中需要的 JAVA_HOME 就是 /usr/src/jdk11/amazon-corretto-11.0.3.7.1-linux-x64/

1.4.3 安装 NodeJs 插件

为构建前端项目,需安装 NodeJs 插件。

(1)前往 jenkins 的插件管理界面搜索安装

(2)进入 全局工具配置 中配置 nodejs 的版本和别名

附加内容:jenkins 插件及应用路径

  • 插件路径:$JENKINS_HOME/plugins,其中 $JENKINS_HOME 默认值为 /var/jenkins_home
  • nodejs 路径:$JENKINS_HOME/tools/jenkins.plugins.nodejs.tools.NodeJSInstallation

1.4.4 安装 Gradle

需要安装 Gradle Plugin 插件。进入 全局工具配置 选择自动安装,版本按需选择。

3 手动构建后端项目

新建 Freestyle project,输入任务名称后进入详细配置。

(1)General

填写项目地址,不带 tree/master 或 tree/branch 部分,如 https://github.com/username/repositoryname.git

(2)源码管理

选择 Git,在 Repository URL 栏填写 GitHub 仓库的 https 链接(如 https://github.com/username/repositoryname.git),Branchs to build 填写要被构建的分支,如 */master,源码库浏览器选择自动。

(3)构建

新增 Invoke Gradle 构建步骤,选择对应别名;点击高级,设置要执行的 Tasks 为 bootJar,其余栏位保持默认或空白。

4 手动构建前端项目

新建 Freestyle project,输入任务名称后进入详细配置。

(1)General

填写项目地址,不带 tree/master 或 tree/branch 部分,如 https://github.com/username/repositoryname.git

(2)源码管理

选择 Git,在 Repository URL 栏填写 GitHub 仓库的 https 链接(如 https://github.com/username/repositoryname.git),Branchs to build 填写要被构建的分支,如 */master,源码库浏览器选择自动。

(3)构建

新增 Executes a NodeJS script 构建步骤,选择 NodeJs 插件对应的别名;再新增 执行 shell 构建步骤,添加命令 node -v && npm -v && npm install --registry=http://registry.npm.taobao.org && npm run build

5 其他配置案例

5.1 GitHub 仓库分支有变动时(提交或合并到该分支)自动构建

5.1.1 配置 GitHub

(1)进入仓库的 Settings,侧边栏找到 Webhooks 并新建。输入 Payload URL,该地址默认为 http://<宿主机IP>:8080/github-webhook/,其余选项保持默认即可生成

注:webhook 是通知 Jenkins 的请求地址,GitHub 通过该地址通知 Jenkins 发生的事件

(2)配置 Personal access tokens,开放 admin:repo_hook(包含 write:repo_hook 和 read:repo_hook)即可

5.1.2 配置 jenkins

(1)主界面进入 Manager Jenkins,找到 Github 服务器

注:该配置项需安装 GitHub Plugin 插件

(2)名称 可任意输入,能辨识即可

(3)API URL 保持默认 https://api.github.com 即可

(4)添加凭据,类型选择 Secret text,在 Secret 栏填入在 GitHub 生成的 Personal access tokens;描述 栏可任意输入,能辨识即可

(5)凭据 下拉菜单选择新添加的凭据名称后,点击 连接测试,若成功则配置完成

注:凭据不能使用 Username with password 类型

附加内容:修改 jenkins 默认的 Webhooks 地址可在 Github 服务器 项右下角点击 高级,出现的 覆盖 Hook URL 即可重新指定。

5.1.3 任务详细配置

手动构建 基础上增加如下配置:

(1)在 构建触发器 项中勾选 GitHub hook trigger for GiTScm polling

(2)在 构建环境 项中勾选 Use secret text(s) or file(s),弹出菜单选择 指定凭据,选择之前步骤设定的凭据别名,变量栏 可空

完成上述设置后即可实现当 GitHub 库分支有变动时,如直接提交或从其他分支合并到该分支,就触发构建。

5.2 前端及后端项目构建成功后自动部署到远端容器

5.2.1 公共配置部分

5.2.1.1 开启 ssh 服务

(1)基础镜像 debian 默认不带 ssh,执行 apt-get install openssh-server 安装

(2)service ssh start 启动服务

(3)ps -e | grep ssh 验证安装

root@aa751ea246b2:/# ps -e | grep ssh
 2777 ?        00:00:00 sshd

注:若需要安装 ps 命令,执行 apt-get install procps 即可

5.2.1.2 root 账号密码设置

(1)开启允许 root 登陆,执行 vim /etc/ssh/sshd_config 修改配置文件,将 PermitRootLogin 项改为 yes(默认值 prohibit-password 阻止密码登陆;另有值 without-password 可不需密码登陆)

(2)执行 /etc/init.d/ssh restart 更新设置

(3)执行 passwd root 修改 root 密码

附加内容:安装 ssh 后添加普通权限用户并设置密码

useradd user
passwd user

5.2.1.3 root 免密登陆设置

(1)暂缺

备用:将 PasswordAuthentication 项改为 no(默认 yes 开启密码身份验证)

5.2.1.4 Publish over SSH 插件账号+密码连接设置

(1)进入 系统配置 找到 Publish over SSH 的配置项,前几项配置留空,直接展开高级设置,勾选Use password authentication, or use a different key,让每个连接使用独立设置的账户和密码。

(2)填写以下主要配置项,其余可留空或保持默认值

  • Name,随意填写
  • Hostname,远端容器的 IP 地址
  • Username,填入 root
  • Remote Directoty,远端容器接收传输文件的目录路径
  • Passphrase / Password,填入账户对应密码

(2)测试设置,提示 Success 说明连接正常

附加内容:查看容器 IP 地址

  • 方式一:在宿主机执行 docker inspect <容器ID>,找到 IPAddress 即可
  • 方式二:进入容器,执行 ifconfig 即可

附加内容:debian 安装网络命令

  • 安装 ifconfig:apt-get install net-tools
  • 安装 ping:apt-get install iputils-ping
  • 安装 ip:apt-get install iproute2

5.2.1.5 Publish over SSH 插件免密连接设置

(1)暂缺

5.2.3 前端项目自动部署详细配置

当 master 分支有变动时,jenkins 容器负责从 github 获取最新源代码并执行 npm build 构建,将构建结果,即 build 目录下内容发送到远端容器接收目录,发送之前清空接收目录。主要路径定义如下:

  • 远端容器接收目录为 /usr/src/static
  • jenkins 容器从 github 获得的源码目录为 /var/jenkins_home/workspace/testfrontend
  • jenkins 容器构建后,需要传输的目录为 /var/jenkins_home/workspace/testfrontend/build

(1)进入项目配置,在 构建 环节增加 Send files or execute commands over SSH

(2)填写以下主要配置项,其余可留空或保持默认值

  • Name,选择在 系统配置 中定义的名称
  • Source files,留空
  • Remove prefix,留空
  • Remote Directoty,留空
  • Exec command,填入以下命令:
cd /usr/src/
rm -rf static
mkdir static

(3)在 构建后操作 环节增加 Send build artifacts over SSH,并填写以下主要配置项,其余可留空或保持默认值

  • Name,选择在 系统配置 中定义的名称
  • Source files,填入 build/**
  • Remove prefix,填入 build
  • Remote Directoty,留空
  • Exec command,留空

附加内容:勾选 Send files or execute commands over SSH after the build runs 与在 构建后操作 中添加 Send build artifacts over SSH 的区别:前者无论构建是否成功都执行,后者需要构建成功才执行。

5.2.4 后端项目自动部署详细配置

5.2.4.1 建立应用容器

后端项目案例使用 Amazon Corretto 11,但官方镜像选择的基础镜像使用上多有不便,因此选用 debian 自行创建。

(1)基础镜像选择 debian:stable-20200414

(2)按照 官方文档 的安装方式

(3)安装 JDK 之前,请安装 java-common 软件包

apt-get update
apt-get install java-common

(4)从 下载 页面下载 Linux .deb 文件。

注:可能需要安装 wget 命令,执行 apt-get install wget 即可

(5)安装 .deb 文件。

dpkg --install java-11-amazon-corretto-jdk_11.0.3.7-1_amd64.deb

注:stable-slim 镜像无法用该方法安装 jdk11,原因暂时未知

5.2.4.2 配置

当 master 分支有变动时,jenkins 容器负责从 github 获取最新源代码,并使用 gradle 构建,将构建结果,即 build/libs 目录下 jar 包发送到远端容器接收目录,发送之前清空接收目录。主要路径定义如下:

  • 远端容器接收目录为 /usr/src/jdkapp
  • jenkins 容器从 github 获得的源码目录为 /var/jenkins_home/workspace/testbackend
  • jenkins 容器构建后,需要传输的文件为 /var/jenkins_home/workspace/testbackend/build/libs/demo.jar

(1)进入项目配置,在 构建 环节增加 Send files or execute commands over SSH

(2)填写以下主要配置项,其余可留空或保持默认值

  • Name,选择在 系统配置 中定义的名称
  • Source files,留空
  • Remove prefix,留空
  • Remote Directoty,留空
  • Exec command,填入以下命令:
cd /usr/src/
rm -rf jdkapp
mkdir jdkapp

(3)在 构建后操作 环节增加 Send build artifacts over SSH,并填写以下主要配置项,其余可留空或保持默认值

  • Name,选择在 系统配置 中定义的名称
  • Source files,填入 build/libs/*.jar
  • Remove prefix,填入 build/libs
  • Remote Directoty,留空
  • Exec in pty,勾选该项复选框,开启伪终端
  • Exec command,填入以下命令:
cd /usr/src/
cat /dev/null > nohup.out
nohup sh runjdkapp.sh

对应的 runjdkapp.sh 文件参考内容如下:

#!/bin/bash
port=3006
filter=$(netstat -ntlp | grep $port | awk '{print $7}' | awk -F '/' '{print $1}')
for var in $filter
do
  kill -9 $var
done
nohup java -jar /usr/src/jdkapp/demo.jar &

注:可能需要安装 netstat 命令,执行 apt-get install net-tools 即可

5.2.5 主要配置项释义

以前端项目为例:

  • Flatten files,扁平化文件。只上传文件,不上传文件所属文件夹。

  • Source files,需要传输的文件。若要传输目录,即相当于传输其下所有文件,例如:build/**,可参看 规则。注意,构建后所在路径位置默认指向工作空间,即 /var/jenkins_home/workspace/testfrontend,因此该配置项直接用目录名 build 即可,不用写长串内容。

  • Remove prefix,移除传输文件路径前缀。该配置项可结合以上 Source files 的例子来看。若留空,则远端最终结果是 /usr/src/static 目录下还有一层 build 目录,不符合需求,因此需要把「前缀」移除,则所需文件路径不带 build/ 前缀,就直接放到 /usr/src/static 目录下。

  • Remote directoty,在目标目录下创建一个新目录。该项若留空,则直接将文件放到本案例中的 /usr/src/static 下,假设填写 var,将文件放到本案例中的 /usr/src/static/var 下。

以 index.html 路径为例,上述配置效果可见下表:

Remote Directoty(系统配置) Source files Remove prefix Remote Directoty(项目配置) 结果
1 /usr/src/static build/** build /usr/src/static/index.html
2 /usr/src/static build/** /usr/src/static/build/index.html
3 /usr/src/static build/** var /usr/src/static/var/build/index.html
4 /usr/src/static build/** build var /usr/src/static/var/index.html

6 其他问题

6.1 提示内存不足,构建失败

进入 jenkins 查看失败的控制台输出,其中有如下信息:

OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory(0x00000000e0000000, 89456640, 0) failed; error='Not enough space' (errno=12)
#
# There is insufficient memory for the Java Runtime Environment to continue.
# Native memory allocation (mmap) failed to map 89456640 bytes for committing reserved memory.
# An error report file with more information is saved as:
# /var/jenkins_home/.gradle/daemon/5.6.2/hs_err_pid2363.log

进一步查看上述 gradle 的日志文件,其中有如下内容:

root@832c29984d24:/var/jenkins_home/.gradle/daemon/5.6.2# cat hs_err_pid2363.log
#
# There is insufficient memory for the Java Runtime Environment to continue.
# Native memory allocation (mmap) failed to map 89456640 bytes for committing reserved memory.
# Possible reasons:
# The system is out of physical RAM or swap space
# The process is running with CompressedOops enabled, and the Java Heap may be blocking the growth of the native heap
# Possible solutions:
# Reduce memory load on the system
# Increase physical memory or swap space
# Check if swap backing store is full
# Decrease Java heap size (-Xmx/-Xms)
# Decrease number of Java threads
# Decrease Java thread stack sizes (-Xss)
# Set larger code cache with -XX:ReservedCodeCacheSize=
# This output file may be truncated or incomplete.
#
# Out of Memory Error (os_linux.cpp:2718), pid=2363, tid=2364
#
# JRE version: (11.0.3+7) (build )
# Java VM: OpenJDK 64-Bit Server VM (11.0.3+7-LTS, mixed mode, aot, sharing, tiered, compressed oops, serial gc, linux-amd64)
# Core dump will be written. Default location: /var/jenkins_home/.gradle/daemon/5.6.2/core.2363

根据可能的原因进行排查,首先执行 free,发现 swap 大小为 0

total        used        free      shared  buff/cache   available
Mem: 1014728 570788 216884 6372 227056 297800
swap: 0 0 0

6.1.1 Linux swap 交换分区的配置

6.1.1.1 swap 交换分区的作用

Linux swap 交换分区,即为虚拟内存。当 Linux 系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到 swap 空间中,等到那些程序要运行时,再从 swap 中恢复保存的数据到内存中。这样,系统总是在物理内存不够时,才进行 swap 交换。避免应用程序内存不足错误的最简单方法之一是为服务器添加一些 swap 空间。

注:虽然建议对使用传统机械硬盘驱动器的系统进行交换,但对于 SSD 来说,使用 swap 可能会导致硬件随着时间的推移而出现问题。出于这种考虑,不建议在任何其他使用 SSD 存储上启用 swap。这样做会影响底层硬件的可靠性。

6.1.1.2 查看信息

可执行 swapon --show,若没有任何信息输出,说明没有可用的 swap 空间。也可以使用 free 查看活动的 swap

6.1.1.3 大小

一般来说可以按照如下规则设置大小:

  • 4G 以内的物理内存,swap 设置为内存的 2 倍。
  • 4-8G 的物理内存,swap 等于内存大小。
  • 8-64G 的物理内存,swap 设置为 8G。
  • 64-256G 物理内存,swap 设置为 16G。

实际上,系统中交换分区的大小并不取决于物理内存的量,而是取决于系统中内存的负荷,所以在安装系统时要根据具体的业务来设置 swap 的值。

6.1.1.4 设置

系统并不是等所有的物理内存都消耗完毕之后,才去使用 swap 的空间,什么时候使用是由 swappiness 参数值控制。

cat /proc/sys/vm/swappiness
60

该值默认值是 60

  • swappiness=0 的时候表示最大限度使用物理内存,然后才是 swap 空间,

  • swappiness = 100 的时候表示积极的使用 swap 分区,并且把内存上的数据及时的搬运到 swap 空间里面。

注:可以把这个参数值设置的低一些,让操作系统尽可能的使用物理内存,降低系统对 swap 的使用,从而提高系统的性能。与 swap 文件的交互是费时的,因为它们比与 RAM 的交互花费更长的时间,并且它们可能导致性能的显着降低。

  • 临时性修改:
sysctl vm.swappiness=10
vm.swappiness = 10

cat /proc/sys/vm/swappiness
10

这里我们的修改已经生效,但是如果我们重启了系统,又会变成 60.

  • 永久修改:

在 /etc/sysctl.conf 文件里添加如下参数:

vm.swappiness=10

然后重启系统

6.1.1.5 配置

(1)查看目前磁盘使用情况,执行 df -h

[root@54s9uer src]# df -h
文件系统 容量 已用 可用 已用% 挂载点
devtmpfs 486M 0 486M 0% /dev
tmpfs 496M 0 496M 0% /dev/shm
tmpfs 496M 57M 440M 12% /run
tmpfs 496M 0 496M 0% /sys/fs/cgroup
/dev/vda1 32G 7.8G 23G 26% /
tmpfs 100M 0 100M 0% /run/user/0
overlay 32G 7.8G 23G 26% /var/lib/docker/overlay2/c2b2dedef134be83b420347470f5bc0cf9ff0f/merged
overlay 32G 7.8G 23G 26% /var/lib/docker/overlay2/3bbd16437dc11ff7ed36ca4c6e60642ef8dbfa/merged

/dev/vda1 是实际使用的磁盘,swap 即可从这里设置容量

注:设置 swap 分区应在宿主机进行

注:通常,等于或双倍于系统内存的量是一个很好的选择。如果只是将其用作 RAM 后备,那么 swap 分区尽可能不要超过 4G。

(2)在根目录创建 swap 文件

fallocate -l 1G /swapfile

(3)查看是否分配了正确的大小

[root@54s9uer src]# ls -lh /swapfile
-rw------- 1 root root 1.0G 4 月 11 23:32 /swapfile

(4)设置权限。执行 chmod 600 /swapfile

(4)标记 swap 分区。执行 mkswap /swapfile

(5)启动 swap 分区。执行 swapon /swapfile

注:执行上述命令后可能出现以下提示,但是使用 swapon --showfree -m 命令,可见 swap 分区已经配置成功:

swapon: /swapfile:swapon 失败: 设备或资源忙

附加内容:关闭 swap

swapoff /swapfile

6.1.1.6 保持配置

上述配置在服务器重新启动后,将不会自动保留。可将交换文件添加到 /etc/fstab 来进行保留

(1)为了避免出现任何问题,先备份 /etc/fstab 文件

cp /etc/fstab /etc/fstab.bak

(2)将下列 swap 文件信息添加到 /etc/fstab 文件末尾

echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

6.2 自动部署后端项目时的问题

若将 runjdkapp.sh 代码直接填入 Exec command,会产生构建成功后能够运行但无法自行退出,最终超时的问题,jenkins 会判断构建失败。部分相关日志如下:

ERROR: Exception when publishing, exception message [Exec timed out or was interrupted after 120,000 ms]
Build step 'Send build artifacts over SSH' changed build result to UNSTABLE
Finished: UNSTABLE

必须勾选 Exec in pty 复选框,并将代码形成 sh 文件后再添加一个 nohup 才能够正常构建(参见前文步骤)。

有关讨论可详见:

进行上述设置后,项目可正常构建和部署,会出现以下信息:

nohup: ignoring input and appending output to 'nohup.out'
SSH: EXEC: completed after 200 ms

因此可在运行项目之前使用 cat /dev/null > nohup.out 清空该文件

附加内容:清空文件

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

推荐阅读更多精彩内容