数据库架构之【MinIO+Nginx+Keepalived】文件库集群方案

MinIO 是一个开源的(遵循Apache License v2.0协议)对象存储系统。它是为海量数据存储、人工智能、大数据分析而设计,单个对象最大可达5TB,适合存储海量图片、视频、日志文件、备份数据和容器/虚拟机镜像等。

本方案基于RockyLinux8系统设计,建议在RedHat/RockyLinux/CentOS系统中使用。

目录

  1. MinIO 的主要特性

  2. MinIO 下载和安装

  3. MinIO 单服务器单硬盘部署

  4. MinIO 单服务器多硬盘数据冗余集群部署

  5. MinIO+Nginx+Keepalived 高可用集群部署
    -- 5.1. 集群部署架构
    -- 5.2. MinIO 集群部署
    -- 5.3. MinIO 集群扩容
    -- 5.4. Nginx 集群部署
    -- 5.5. Keepalived 高可用

  6. MinIO 运维管理
    -- 6.1. 客户端简介
    -- 6.2. 从本地存储到 MinIO 的数据镜像
    -- 6.3. 从 MinIO 到 MinIO 的数据镜像

  7. Java SpringBoot 开发集成示例


1.MinIO 的主要特性

1、高性能:在标准硬件上,读/写速度高达 183 GB / 秒 和 171 GB / 秒。

2、简单:仅由一个参数极少的二进制可执行文件组成。无论是集群或者单点模式下,都可以通过一个简单指令完成服务的安装、升级和管理。升级命令可以无中断的完成 MinIO 的升级,并且不需要停机即可完成升级操作,降低总使用和运维成本。

3、可扩展:支持无限可伸缩的横向扩展,可以将服务器的硬盘作为一个单位节点,集群规模为服务器上硬盘的总数。

4、多平台:支持 intel,amd,arm,ppc64le,s390x 的 CPU 指令集架构的 Linux,Unix,Windows,MaxOS 操作系统。

5、云原生:符合原生云计算的架构和构建过程 其中包括支持 Kubernetes 、微服务和多租户的的容器技术。MinIO 在 DockerHub 上已经提供了官方镜像文件。

6、易用:提供一个中文可视化的 Web 应用控制台,通过控制台可以管理文件桶和上传、下载、删除文件。

7、多语言 SDK 支持:提供 JavaScript,Java,Python,Golang,.Net,Haskell 的客户端开发 SDK API。

8、冗余:使用纠删码、Checksum 来防止硬件错误和静默数据污染。在最高冗余度配置下,即使丢失 1/2 的硬盘也能恢复数据,但是需要有 1/2 + 1 个硬盘才能创建新的对象。

9、安全:提供服务器端文件加密方案,且对于性能的影响可忽略不计。


2.MinIO 下载和安装

1、下载 MinIO 服务器端的二进制可执行文件到【/usr/local/bin】目录中,并授予可执行权限。

各版本最新稳定版的 MinIO 服务器端下载镜像地址:

操作系统 CPU 架构 下载地址
Linux 64-bit Intel/AMD http://dl.minio.io/server/minio/release/linux-amd64/minio
Linux 64-bit ARM http://dl.minio.io/server/minio/release/linux-arm64/minio
Linux 64-bit PowerPC LE (ppc64le) http://dl.minio.io/server/minio/release/linux-ppc64le/minio
Linux 64-bit MIPS http://dl.minio.io/server/minio/release/linux-mips64/minio
Linux IBM Z-Series (S390X) http://dl.minio.io/server/minio/release/linux-s390x/minio
MaxOS 64-bit Intel/AMD http://dl.minio.io/server/minio/release/darwin-amd64/minio
MaxOS 64-bit ARM http://dl.minio.io/server/minio/release/darwin-arm64/minio
Windows 64-bit Intel/AMD http://dl.minio.io/server/minio/release/windows-amd64/minio.exe

以 64-bit Intel/AMD 的 CPU 架构为例:

[root@rocky ~]# wget http://dl.minio.io/server/minio/release/linux-amd64/minio -P /usr/local/bin
[root@rocky ~]# chmod +x /usr/local/bin/minio

2、下载 MinIO 客户端的二进制可执行文件到【/usr/local/bin】目录中,并授予可执行权限。

各版本最新稳定版的 MinIO 客户端下载镜像地址:

操作系统 CPU 架构 下载地址
Linux 64-bit Intel/AMD http://dl.minio.io/client/mc/release/linux-amd64/mc
Linux 64-bit ARM http://dl.minio.io/client/mc/release/linux-arm64/mc
Linux 64-bit PowerPC LE (ppc64le) http://dl.minio.io/client/mc/release/linux-ppc64le/mc
Linux 64-bit MIPS http://dl.minio.io/client/mc/release/linux-mips64/mc
Linux IBM Z-Series (S390X) http://dl.minio.io/client/mc/release/linux-s390x/mc
MaxOS 64-bit Intel/AMD http://dl.minio.io/client/mc/release/darwin-amd64/mc
MaxOS 64-bit ARM http://dl.minio.io/client/mc/release/darwin-arm64/mc
Windows 64-bit Intel/AMD http://dl.minio.io/client/mc/release/windows-amd64/mc.exe

以 64-bit Intel/AMD 的 CPU 架构为例:

[root@rocky ~]# wget http://dl.minio.io/client/mc/release/linux-amd64/mc -P /usr/local/bin
[root@rocky ~]# chmod +x /usr/local/bin/mc

3.MinIO 单服务器单硬盘部署

1、 下载和安装 MinIO 服务器端和客户端,操作步骤请参见“2.下载和安装”章节。

2、创建 MinIO 数据目录。

[root@rocky ~]# mkdir /vol

注意:该目录应当挂载到一块独立的物理硬盘上。

3、设置 MinIO 管理账号和口令。

[root@rocky ~]# export MINIO_ROOT_USER=admin
[root@rocky ~]# export MINIO_ROOT_PASSWORD=123456aA?

指定账号通过指令:export MINIO_ROOT_USER=<账号>
指定口令通过指令:export MINIO_ROOT_PASSWORD=<口令>
不指定时,默认的账号和口令为【minioadmin】。

4、启动 MinIO 服务。

1)前台启动(调试):

[root@rocky ~]# minio server --address :9000  --console-address :9001 /vol

2)后台启动(运行):

[root@rocky ~]# nohup minio server --address :9000  --console-address :9001 /vol > /var/log/minio.log  2>&1 &

指令格式:minio server <--address> <--console-address> [数据目录]
--address:API 监听地址和端口,可以设置【IP:PORT】或者【:PORT】的形式。不指定时,默认的监听端口为【:9000】。
--console-address:可视化管理应用的发布地址,可以设置【IP:PORT】或者【:PORT】的形式。不指定时,默认的监听端口为随机端口号。

5、开启远程访问策略。

设置防火墙端口(RockyLinux8默认安装firewall防火墙),允许"9000"、"9001"端口(MinIO 监听端口)访问服务器。

[root@rocky ~]$ firewall-cmd --zone=public --add-port=9000/tcp --permanent
[root@rocky ~]$ firewall-cmd --zone=public --add-port=9001/tcp --permanent
[root@rocky ~]$ firewall-cmd --reload

6、配置 MinIO 开机自启动。

使用文本编辑器修改 “/etc/rc.d/rc.local” 文件:

[root@rocky ~ ]$ vi /etc/rc.d/rc.local

追加文件内容并保存如下:

export MINIO_ROOT_USER="admin"
export MINIO_ROOT_PASSWORD="123456aA?"
nohup /usr/local/bin/minio server --address :9000 --console-address :9001 /vol > /var/log/minio.log  2>&1 &

授予可执行权限:

[root@rocky ~ ]$ chmod +x /etc/rc.d/rc.local

7、重新启动并通过浏览器访问控制台验证。

[root@rocky ~ ]$ reboot

在浏览器中访问【http://ip:9000】,输入用户名和口令登录。


4.MinIO 单服务器多硬盘数据冗余集群部署

单服务器多硬盘的是最简单、高性价比的数据冗余集群方案,作用是【当小于 1/2 块硬盘发生故障时,不会影响使用,不会丢失数据】,但是无法保障在服务器宕机时继续提供服务,也无法提升高并发的处理性能。

MinIO 要求集群至少有 4 个节点组成,因此服务器至少需要部署 4 块独立的物理硬盘,当 MinIO 数据目录存在于相同的硬盘时,将无法启动 MinIO 。

因此,当系统对于稳定性和性能需求不高,仅需保障数据冗余时,建议采用此方案。

1、 下载和安装 MinIO 服务器端和客户端,操作步骤请参见“2.下载和安装”章节。

2、创建 MinIO 数据目录。

[root@rocky ~]# mkdir /vol1 /vol2 /vol3 /vol4

注意:每个目录必须挂载到一块独立的物理硬盘上,当目录存在于相同的硬盘时,将无法启动 MinIO 集群。

3、设置 MinIO 管理账号和口令。

[root@rocky ~]# export MINIO_ROOT_USER=admin
[root@rocky ~]# export MINIO_ROOT_PASSWORD=123456aA?

指定账号通过指令:export MINIO_ROOT_USER=<账号>
指定口令通过指令:export MINIO_ROOT_PASSWORD=<口令>
不指定时,默认的账号和口令为【minioadmin】。

4、启动 MinIO 服务。

1)前台启动(调试):

[root@rocky ~]# minio server --address :9000  --console-address :9001 /vol1 /vol2 /vol3 /vol4

2)后台启动(运行):

[root@rocky ~]# nohup minio server --address :9000  --console-address :9001 /vol1 /vol2 /vol3 /vol4 \ > /var/log/minio.log  2>&1 &

指令格式:minio server <--address> <--console-address> [磁盘挂载目录]
--address:API 监听地址和端口,可以设置【IP:PORT】或者【:PORT】的形式。不指定时,默认的监听端口为【:9000】。
--console-address:可视化管理应用的发布地址,可以设置【IP:PORT】或者【:PORT】的形式。不指定时,默认的监听端口为随机端口号。

5、开启远程访问策略。

设置防火墙端口(RockyLinux8默认安装firewall防火墙),允许"9000"、"9001"端口(MinIO 监听端口)访问服务器。

[root@rocky ~]$ firewall-cmd --zone=public --add-port=9000/tcp --permanent
[root@rocky ~]$ firewall-cmd --zone=public --add-port=9001/tcp --permanent
[root@rocky ~]$ firewall-cmd --reload

6、配置 MinIO 开机自启动。

使用文本编辑器修改 “/etc/rc.d/rc.local” 文件:

[root@rocky ~ ]$ vi /etc/rc.d/rc.local

追加文件内容并保存如下:

export MINIO_ROOT_USER="admin"
export MINIO_ROOT_PASSWORD="123456aA?"
nohup /usr/local/bin/minio server --address :9000 --console-address :9001 /vol1 /vol2 /vol3 /vol4 > /var/log/minio.log 2>&1 &

授予可执行权限:

[root@rocky ~ ]$ chmod +x /etc/rc.d/rc.local

7、重新启动并通过浏览器访问控制台验证。

[root@rocky ~ ]$ reboot

在浏览器中访问【http://ip:9000】,输入用户名和口令登录。


5.MinIO+Nginx+Keepalived 高可用集群部署

高可用集群是最完整的集群方案,此方案具有集群化的全部特性:
1. 当小于 1/2 块硬盘发生故障时,不会影响使用,不会丢失数据;
2. 任一单服务器宕机时,不会影响使用;
3. 具有请求负载均衡能力,各服务器节点协同提供服务,可有效提升并发处理能力。

MinIO 要求集群至少有 4 个节点组成,因此至少需要部署 4 台 MinIO 服务器 。

高可用集群部署方案能够满足大多数对于稳定性和并发性能较高的应用场景,是生产级别系统的完整部署方案。

5.1. 集群部署架构

网络部署图:

MinIO 集群部署图

网络资源规划:

MinIO 服务器集群

节点名 主机名 IP:PORT 程序 操作系统
MinIO-1 minio-1 192.168.0.101:9000,9001 MinIO RockyLinux8
MinIO-2 minio-2 192.168.0.102:9000,9001 MinIO RockyLinux8
MinIO-3 minio-3 192.168.0.103:9000,9001 MinIO RockyLinux8
MinIO-4 minio-4 192.168.0.104:9000,9001 MinIO RockyLinux8

Nginx 服务器集群

节点名 主机名 IP:PORT 程序 操作系统
Nginx-1 nginx-1 192.168.0.201:9000,9001,112 Ngxin,Keepalived RockyLinux8
Nginx-2 nginx-2 192.168.0.202:9000,9001,112 Ngxin,Keepalived RockyLinux8

Keepalived Virtual IP:192.168.0.200。


5.2. MinIO 集群部署

1、 MinIO 服务器集群的各个节点下载和安装 MinIO 服务器端和客户端,操作步骤请参见“2.下载和安装”章节。

操作部署以“MinIO-1”节点为例,所有 MinIO 集群节点需全部完成一下操作:

2、创建 MinIO 数据目录。

[root@minio-1 ~]# mkdir /vol

3、创建 MinIO 集群服务器节点 DNS 映射记录。

注意:MinIO 集群启动的单元称为【区域】,每个【区域】必须由一组连续服务器 IP 或 DNS 和连续的硬盘挂载目录组成。而服务器 IP 地址通常为无序的,所以通过建立本地 DNS 映射记录,将无序的 IP 地址映射成有序的 DNS 名。如下所示:

minio server http://host{1...4}/vol{1...4} http://host{5...8}/vol{1...4}

使用文本编辑器打开 “/etc/hosts” 文件:

[root@minio-1 ~]# vi /etc/hosts

追加以下内容并保存:

192.168.0.101 minio-1
192.168.0.102 minio-2
192.168.0.103 minio-3
192.168.0.104 minio-4

4、设置 MinIO 管理账号和口令(注意,集群所有服务器节点的管理账号和口令必须是一致的)。

[root@minio-1 ~]# export MINIO_ROOT_USER=admin
[root@minio-1 ~]# export MINIO_ROOT_PASSWORD=123456aA?

指定账号通过指令:export MINIO_ROOT_USER=<账号>
指定口令通过指令:export MINIO_ROOT_PASSWORD=<口令>
不指定时,默认的账号和口令为【minioadmin】。

5、启动 MinIO 服务。

1)前台启动(调试):

[root@minio-1 ~]# minio server --address :9000 --console-address :9001 http://minio-{1...4}/vol

2)后台启动(运行):

[root@minio-1 ~]# nohup minio server --address :9000 --console-address :9001 http://minio-{1...4}/vol > /var/log/minio-cluster.log  2>&1 &

指令格式:minio server <--address> <--console-address> [节点位置]
--address:API 监听地址和端口,可以设置【IP:PORT】或者【:PORT】的形式。不指定时,默认的监听端口为【:9000】。
--console-address:可视化管理应用的发布地址,可以设置【IP:PORT】或者【:PORT】的形式。不指定时,默认的监听端口为随机端口号。

注意:各个服务器节点需要依次执行启动命令,才能完成整个集群的启动。启动集群过程中,将不断提示以下错误:

这是一个正常的现象,当已启动所有服务器节点后会自动关闭。

6、开启远程访问策略。

设置防火墙端口(RockyLinux8默认安装firewall防火墙),允许"9000"、"9001"端口(MinIO 监听端口)访问服务器。

[root@minio-1 ~]$ firewall-cmd --zone=public --add-port=9000/tcp --permanent
[root@minio-1 ~]$ firewall-cmd --zone=public --add-port=9001/tcp --permanent
[root@minio-1 ~]$ firewall-cmd --reload

7、配置 MinIO 开机自启动。

使用文本编辑器修改 “/etc/rc.d/rc.local” 文件:

[root@minio-1 ~ ]$ vi /etc/rc.d/rc.local

追加文件内容并保存如下:

export MINIO_ROOT_USER="admin"
export MINIO_ROOT_PASSWORD="123456aA?"
nohup minio server --address :9000 --console-address :9001 http://minio-{1...4}/vol > /var/log/minio-cluster.log  2>&1 &

授予可执行权限:

[root@minio-1 ~ ]$ chmod +x /etc/rc.d/rc.local

8、重新启动并通过浏览器访问控制台验证。

[root@minio-1 ~ ]$ reboot

在浏览器中访问任一节点的【http://ip:9000】,输入用户名和口令登录。

所有 MinIO 集群节点均需完成以上步骤。


5.3. MinIO 集群的扩容。

MinIO的极简设计理念使得MinIO分布式集群并不支持向集群中添加单个节点并进行自动调节的扩容方式,这是因为加入单个节点后所引发的数据均衡以及纠删组划分等问题会为整个集群带来复杂的调度和处理过程,并不利于维护。因此,MinIO提供了一种对等扩容的方式,即要求增加的节点数和磁盘数均需与原集群保持对等。

例如:原集群由 4 台服务器(每服务器 4 硬盘),则在扩容时必须同样增加 4 台服务器(每服务器 4 硬盘)或为其倍数,以便系统维持相同的数据冗余 SLA,从而极大地降低扩容的复杂性。

如上例,在扩容后,MinIO 集群并不会对全部的 8 个服务器节点进行完全的数据均衡,而是将原本的 4 个服务器节点视作一个区域,新加入的 4 个服务器节点视作另一区域,当有新对象上传时,集群将依据各区域的可用空间比例确定存放区域,在各区域内仍旧通过哈希算法确定对应的纠删组进行最终的存放。此外,集群进行一次对等扩容后,还可依据扩容规则继续进行对等扩容,但出于安全性考虑,集群的最大服务器节点数一般不得超过32个,也就是说可以扩容 2 次(第一次扩容 4 服务器,第二次扩容 8 服务器 ,第三次扩容 16 服务器)。

新增的服务器节点参见“5.2. MinIO 集群部署”章节部署服务器。但注意以下几点:

1、所有的服务器节点创建一致的 MinIO 集群服务器节点 DNS 映射记录。

注意:MinIO 集群启动的单元称为【区域】,每个【区域】必须由一组连续服务器 IP 或 DNS 和连续的硬盘挂载目录组成。而服务器 IP 地址通常为无序的,所以通过建立本地 DNS 映射记录,将无序的 IP 地址映射成有序的 DNS 名。如下所示:

minio server http://host{1...4}/vol{1...4} http://host{5...8}/vol{1...4}

使用文本编辑器打开 “/etc/hosts” 文件:

[root@minio-5 ~]# vi /etc/hosts

追加以下内容并保存:

192.168.0.101 minio-1
192.168.0.102 minio-2
192.168.0.103 minio-3
192.168.0.104 minio-4
192.168.0.105 minio-5
192.168.0.106 minio-6
192.168.0.107 minio-7
192.168.0.108 minio-8

2、新增服务器节点必须设置与原始集群设置一致的 MinIO 管理账号和口令。

[root@minio-5 ~]# export MINIO_ROOT_USER=admin
[root@minio-5 ~]# export MINIO_ROOT_PASSWORD=123456aA?

指定账号通过指令:export MINIO_ROOT_USER=<账号>
指定口令通过指令:export MINIO_ROOT_PASSWORD=<口令>
不指定时,默认的账号和口令为【minioadmin】。

3、所有服务器节点的集群启动命令改变(注意修改开机启动文件),原始集群需重新启动 MinIO 服务。

1)前台启动(调试):

[root@minio-5 ~]# minio server --address :9000 --console-address :9001 http://minio-{1...4}/vol  http://minio-{5...8}/vol

2)后台启动(运行):

[root@minio-5 ~]# nohup minio server --address :9000 --console-address :9001 http://minio-{1...4}/vol   http://minio-{5...8}/vol > /var/log/minio-cluster.log  2>&1 &

5.4. Nginx 集群部署

在各个 "Nginx " 集群节点(Nginx-1、Nginx-2)安装、配置 Nginx,以 "Nginx-1" 为例:

1、打开 Nginx 下载页面【http://nginx.org/en/download.html】,下载 Nginx 的源代码 tar.gz 包到用户主目录中。

Nginx 下载页面

2、验证并安装依赖软件。通过源代码编译的方式安装 Nginx,需要依赖软件"make"、"gcc"、"pcre"、"pcre-devel"、"zlib"、"zlib-devel"、"openssl"、"openssl-devel",验证或安装依赖软件。

[root@nginx-1 ~]# dnf install make gcc pcre pcre-devel zlib zlib-devel openssl openssl-devel

补充知识:

① "gcc"是一个C/C++、FORTRAN、JAVA、OBJC、ADA等多种语言的编译器,用来将源代码编译成可发布的软件程序。

② "make"是一个工程管理工具,能够根据 Makefile 中的脚本执行编译、安装流程。

③ "pcre"是一个正则表达式函数库;"pcre-devel"是它的开发库。

④ "zlib"是一个数据压缩函数库;"zib-devel"是它的开发库。

⑤ "openssl"是一个实现安全通信,避免窃听,同时确认另一端连接者身份的软件程序;"openssl-devel"是它的开发库。

3、解压缩 Nginx 的源代码 tar 包到用户主目录下。

[root@nginx-1 ~]# tar -zxvf nginx-1.18.0.tar.gz
[root@nginx-1 ~]# ll
drwxr-xr-x.  8 centos centos    4096 4月  21 22:09 nginx-1.18.0

4、安装 Nginx,进入源代码目录,配置、编译、安装程序。

[root@nginx-1 ~]# cd nginx-1.18.0
[root@nginx-1 nginx-1.18.0]# ./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module  --with-stream
Configuration summary
  + using system PCRE library
  + OpenSSL library is not used
  + using system zlib library

  nginx path prefix: "/usr/local/nginx"
  nginx binary file: "/usr/local/nginx/sbin/nginx"
  nginx modules path: "/usr/local/nginx/modules"
  nginx configuration prefix: "/usr/local/nginx/conf"
  nginx configuration file: "/usr/local/nginx/conf/nginx.conf"
  nginx pid file: "/usr/local/nginx/logs/nginx.pid"
  nginx error log file: "/usr/local/nginx/logs/error.log"
  nginx http access log file: "/usr/local/nginx/logs/access.log"
  nginx http client request body temporary files: "client_body_temp"
  nginx http proxy temporary files: "proxy_temp"
  nginx http fastcgi temporary files: "fastcgi_temp"
  nginx http uwsgi temporary files: "uwsgi_temp"
  nginx http scgi temporary files: "scgi_temp"

[root@nginx-1 nginx-1.18.0]# make
[root@nginx-1 nginx-1.18.0]# make install

[root@nginx-1 ~]# ll /usr/local/nginx
drwxr-xr-x. 2 root root 4096 5月  18 09:39 conf
drwxr-xr-x. 2 root root   40 5月  18 09:39 html
drwxr-xr-x. 2 root root    6 5月  18 09:39 logs
drwxr-xr-x. 2 root root   19 5月  18 09:39 sbin

程序安装目录是"/usr/local/nginx"。

5、设置 Nginx 配置文件参数。

使用文本编辑器打开配置文件:

[root@nginx-1 ~]# vi /usr/local/nginx/conf/nginx.conf

修改或验证文件中的以下参数并保存(七层代理):

http {
    
    # 负载均衡
    upstream minio-api {
        server 192.168.0.101:9000 weight=1;
        server 192.168.0.102:9000 weight=1;
        server 192.168.0.103:9000 weight=1;
        server 192.168.0.104:9000 weight=1;
    }

    # 负载均衡
    upstream minio-console {
        server 192.168.0.101:9001 weight=1;
        server 192.168.0.102:9001 weight=1;
        server 192.168.0.103:9001 weight=1;
        server 192.168.0.104:9001 weight=1;
    }
    
    server {
        # 监听端口
        listen       9000;
        
        # 服务器域名(主机头)
        server_name  localhost;

        # 代理 Web 服务的 Url 前缀,一般是 Web 服务的虚拟目录(可以是正则表达式)。
        location / {
            proxy_pass http://minio-api;
        }
    }

    server {
        # 监听端口
        listen       9001;
        
        # 服务器域名(主机头)
        server_name  localhost;

        # 代理 Web 服务的 Url 前缀,一般是 Web 服务的虚拟目录(可以是正则表达式)。
        location / {
            proxy_pass http://minio-console;
        }
    }
}

6、配置 Nginx 开机自启动。

使用文本编辑器创建配置文件:

[root@nginx-1 ~]# vi /usr/lib/systemd/system/nginx.service

编写文件内容并保存如下:

[Unit]
Description=Nginx
After=syslog.target network.target

[Service]
Type=forking
User=root
Group=root

ExecStart=/usr/local/nginx/sbin/nginx
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/usr/local/nginx/sbin/nginx -s quit
PrivateTmp=true

[Install]
WantedBy=multi-user.target

设置开机启动:

[root@nginx-1 ~]#  systemctl daemon-reload
[root@nginx-1 ~]#  systemctl enable nginx.service

7、启动 Nginx 服务。

[root@nginx-1 ~]#  systemctl start nginx.service

8、设置防火墙端口(CentOS8默认安装firewall防火墙),允许"9000","9001"端口(Nginx 默认端口)访问服务器。

[root@nginx-1 ~]# firewall-cmd --zone=public --add-port=9000/tcp --permanent
[root@nginx-1 ~]# firewall-cmd --zone=public --add-port=9001/tcp --permanent
[root@nginx-1 ~]# firewall-cmd --reload

注意:其他 "Nginx" 集群节点全部需要按照以上步骤配置。

9、Nginx 运维管理。

1)启动 Nginx 服务(任选一种方式)

[root@nginx-1 ~]# systemctl start nginx.service

或者

[root@nginx-1 ~]# /usr/local/nginx/sbin/nginx

2)停止 Nginx 服务(任选一种方式)

[root@nginx-1 ~]# systemctl stop nginx.service

或者

[root@nginx-1 ~]# /usr/local/nginx/sbin/nginx -s quit

3)重启 Nginx 服务

[root@nginx-1 ~]# systemctl restart nginx.service

或者

[root@nginx-1 ~]# /usr/local/nginx/sbin/nginx -s reload

4)查看 Nginx 服务状态

[root@nginx-1 ~]# systemctl status nginx.service

或者

[root@nginx-1 ~]# ps -ef | grep nginx
root     119777      1  0 10:16 ?        00:00:00 nginx: master process /usr/local/nginx/sbin/nginx

[root@nginx-1 ~]# netstat -ntap | grep nginx
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      119777/nginx: maste

[root@nginx-1 ~]# tail /usr/local/nginx/logs/error.log
[root@nginx-1 ~]# tail /usr/local/nginx/logs/access.log

5)启用 Nginx 服务开机自启动

[root@nginx-1 ~]# systemctl enable nginx.service

6)禁用 Nginx 服务开机自启动

[root@nginx-1 ~]# systemctl disable nginx.service

5.5. Keepalived 高可用

在各个 "Nginx" 集群节点 (Nginx-1、Nginx-2)安装、配置 Keepalived,以 "Nginx-1" 为例:

1、安装 EPEL 的 Yum源。

使用文本编辑器创建仓库配置文件:

[root@nginx-1 ~ ]# vi /etc/yum.repos.d/epel.repo

在文件中编写以下内容并保存:

[epel-modular]
name=Extra Packages for Enterprise Linux Modular $releasever - $basearch
baseurl=http://mirrors.aliyun.com/epel/$releasever/Modular/$basearch
enabled=1
gpgcheck=1
gpgkey=http://mirrors.aliyun.com/epel/RPM-GPG-KEY-EPEL-8

[epel]
name=Extra Packages for Enterprise Linux $releasever - $basearch
baseurl=http://mirrors.aliyun.com/epel/$releasever/Everything/$basearch
enabled=1
gpgcheck=1
gpgkey=http://mirrors.aliyun.com/epel/RPM-GPG-KEY-EPEL-8

更新 Yum 源:

[root@nginx-1 ~ ]# dnf clean all
[root@nginx-1 ~ ]# dnf makecache
Extra Packages for Enterprise Linux Modular 8 - 429 kB/s | 118 kB     00:00    
Extra Packages for Enterprise Linux 8 - x86_64  3.7 MB/s | 6.9 MB     00:01    
元数据缓存已建立。

EPEL(Extra Packages for Enterprise Linux)是企业级 Linux 操作系统的扩展包仓库,为 Redhat/CentOS系统提供大量的额外软件包。

2、安装 Keepalived。

[root@nginx-1 ~ ]# dnf install keepalived

程序安装目录是"/usr/sbin",配置文件目录是"/etc/keepalived"。

3、设置 Keepalived 配置文件参数。

使用文本编辑器打开配置文件:

[root@nginx-1 ~ ]# vi /etc/keepalived/keepalived.conf

在文件中编写以下内容并保存:

# 定义全局配置
global_defs {
    # 本地节点 ID 标识,一般设置为主机名。
    router_id nginx-1
}

# 定义周期性执行的脚本,脚本的退出状态码会被调用它的所有的 vrrp_instance 记录。
vrrp_script chk_nginx {
    # 执行脚本的路径。
    script "/etc/keepalived/nginx_check.sh"
    # 脚本执行的间隔(单位是秒)。默认为1s。
    interval 2
    # 当脚本调整优先级,从 -254 到 254。默认为2。
    # 1. 如果脚本执行成功(退出状态码为0),weight大于0,则priority增加。
    # 2. 如果脚本执行失败(退出状态码为非0),weight小于0,则priority减少。
    # 3. 其他情况下,priority不变。
    weight -20
    # 当脚本执行超过时长(单位是秒)则被认为执行失败。
    # 运行脚本的用户和组。
    user root root
    # timeout 30
    # 当脚本执行成功到设定次数时,才认为是成功。
    # rise 1
    # 当脚本执行失败到设定次数时,才认为是失败。
    # fall 3
}

# 定义虚拟路由,可以定义多个。
vrrp_instance VI_1 {
    # 本地节点初始状态,包括 MASTER(主节点) 和 BACKUP (备节点)。
    state MASTER
    # 本地节点绑定虚拟 IP 的网络接口。
    interface ens33
    # 本地节点优先级,优先级高的节点将动态变成 MASTER 节点,接管 VIP 。初始状态下,MASTER 节点的优先级必须高于 BACKUP 节点。
    priority 100
    # VRRP 实例 ID,范围是0-255。同一集群的所有节点应设置一致的值。
    virtual_router_id 216
    # 组播信息发送时间间隔。同一集群的所有节点必须设置一样,默认为1秒。
    advert_int 1
    # 设置验证信息。同一集群的所有节点必须一致
    authentication {
        # 指定认证方式。PASS 表示简单密码认证(推荐);AH:IPSEC认证(不推荐)。
        auth_type PASS
        # 指定认证所使用的密码,最多8位。
        auth_pass 1111
    }

    # 声明调用已定义的 vrrp_script 脚本。
    track_script {
        chk_nginx
    }

    # 定义虚拟 IP 地址。
    virtual_ipaddress {
        192.168.0.200
    }
}

初始化的主节点和备节点的区别体现在以下参数中:

  • 初始主节点
vrrp_instance VI_1 {
    # 必须设置为 MASTER 。
    state MASTER
    # 必须设置为最大值。
    priority 100
}
  • 初始备节点
vrrp_instance VI_1 {
    # 必须设置为 BACKUP 。
    state BACKUP
    # 必须设置为小于主节点的值。
    priority 90
}

4、创建或编辑 Nginx 检测脚本文件。文件路径对应配置文件中 vrrp_script 的 script 设置值。

使用文本编辑器创建脚本文件:

[root@nginx-1 ~ ]# vi /etc/keepalived/nginx_check.sh

在脚本文件中编写以下内容并保存:

#!/bin/bash
counter=$(ps -C nginx --no-heading|wc -l)
if [ "${counter}" = "0" ]; then
    /usr/local/nginx/sbin/nginx
    sleep 2
    counter=$(ps -C nginx --no-heading|wc -l)
    if [ "${counter}" = "0" ]; then
        killall -9 keepalived
    fi
fi

给脚本文件增加可执行权限:

[root@nginx-1 ~ ]# chmod 755 /etc/keepalived/nginx_check.sh

5、配置 Keepalived 系统服务。

使用文本编辑器创建配置文件:

[root@nginx-1 ~ ]# vi /usr/lib/systemd/system/keepalived.service

验证或修改文件内容并保存如下:

[Unit]
Description=LVS and VRRP High Availability Monitor
After=network-online.target syslog.target nginx.service
Wants=network-online.target
Requires=nginx.service

[Service]
Type=forking
User=root
Group=root
PIDFile=/var/run/keepalived.pid
KillMode=process
EnvironmentFile=-/etc/sysconfig/keepalived
ExecStart=/usr/sbin/keepalived $KEEPALIVED_OPTIONS
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill -HUP $MAINPID

[Install]
WantedBy=multi-user.target

重新加载系统服务管理器:

[root@nginx-1 ~ ]# systemctl daemon-reload

6、设置防火墙端口(CentOS8默认安装firewall防火墙),允许"112"端口(Keepalived 默认端口)访问服务器。

[root@nginx-1 ~ ]# firewall-cmd --zone=public --add-port=112/tcp --permanent
[root@nginx-1 ~ ]# firewall-cmd --reload

7、启动/重启 Keepalived 服务(不建议设置为开机自启动)。

启动 Keepalived 服务之前,应确保已正确启动了各节点的 Nginx 服务。各节点的启动或重启的顺序为:① 启动 Keepalived 主节点;② 依次启动 Keepalived 备节点。

[root@nginx-1 ~ ]# systemctl restart keepalived.service

注意:其他 "Nginx" 集群节点全部需要按照以上步骤配置。


6. MinIO 运维管理

6.1. 客户端简介

MinIO 提供两种客户端,一是基于 Shell 的【mc】,二是基于 Web 的【console】。

mc 客户端的安装参见 “2.MinIO 下载和安装” 章节,安装后可以通过以下指令来了解提供的功能:

[root@rocky ~]$ mc --help

在使用 mc 客户端操作 MinIO 之前,需要创建一个数据链接配置,指令如下:

[root@rocky ~]$ mc config host add minio http://192.168.0.101:9000 admin 123456aA?
Added `minio` successfully.

指令格式:mc config host add <别名> <MinIO API 地址:端口号> <账号> <口令>

其他功能可参见MinIO中文网站【http://docs.minio.org.cn/docs/master/minio-client-quickstart-guide】。

console 客户端在启动 MinIO 服务时跟随启动,可以通过在浏览器中访问【http://ip:9000】,输入用户名和口令登录。


6.2. 从本地存储到 MinIO 的数据镜像

mc 客户端提供从本地路径到远程 MinIO 文件桶的指令,可用于将本地数据一次性或者实时上传到指定的 MinIO 文件桶中。

1、登录 console 客户端创建一个目标文件桶,如:backup。

创建文件桶

2、在客户端本地通过 mc 客户端创建 MinIO 服务器连接配置(只需创建一次)。

[root@rocky ~]$ mc config host add minio http://192.168.0.101:9000 admin 123456aA?
Added `minio` successfully.

3、从客户端本地路径镜像到 MinIO 目标文件桶中。

指令格式:mc mirror <--overwrite> <-w> <本地目录> <MinIO 服务器别名 / 目标文件桶>

[root@rocky ~]$ mc mirror --overwrite /vol minio/backup

4、开启实时增量镜像。

实时增量镜像应当后台运行,启动后将监听镜像目录的变化,并将这些变化实时同步到 MinIO 的目标文件桶中:

[root@rocky ~]$ nohup mc mirror -w --overwrite /vol minio/backup  > /var/log/minio-mirror.log  2>&1 &

5、将实时增量镜像设置为开机启动服务。

使用文本编辑器修改 “/etc/rc.d/rc.local” 文件:

[root@rocky ~ ]$ vi /etc/rc.d/rc.local

追加文件内容并保存如下:

nohup mc mirror -w --overwrite /vol minio/backup  > /var/log/minio-mirror.log  2>&1 &

授予可执行权限:

[root@rocky ~ ]$ chmod +x /etc/rc.d/rc.local

6.3. 从 MinIO 到 MinIO 的数据镜像

console 客户端提供文件桶的发布订阅功能,可用于数据实时分发和同步的应用场景。

发布端能够以“文件桶”为管理单位,通过创建并启用分发规则,规则主要包括订阅:服务器的访问信息、目标文件桶、数据删除策略等。自发布规则生效之时起,发布端源文件桶所有增量操作都将同步到订阅端的目标文件桶中。

实时同步是从发布端到订阅端的单向同步过程,并且是基于事件触发方式的。因此,当订阅端修改数据时,不会与发布端相互影响,发布端也不会检查和修正订阅端的数据。

MinIO 文件同步

假设:

  • 源 MinIO 服务器接入点为【http://192.168.0.101:9000】,源文件桶叫做【origin】;
  • 目标 MinIO 服务器接入点为【http://192.168.0.102:9000】,目标文件桶叫做【dest】。

操作步骤如下:

1、分别登录源 MinIO 服务器和目标 MinIO 服务器的 console 客户端,创建源文件桶【origin】和目标文件桶【dest】,并开启这些文件桶的版本控制功能。以源 MinIO 服务器的操作为例:

第 1 步
第 2 步
第 3 步
第 4 步

注意:源 MinIO 服务器文件桶和目标 MinIO 服务器文件桶都需完成以上设置。

2、登录源 MinIO 服务器的 console 客户端,创建分发规则,操作步骤如下:

第 1 步
第 2 步
第 3 步
创建完成

3、登录源 MinIO 服务器的 console 客户端,在源文件桶中创建测试文件,查看是否同步到目标 MinIO 服务器的目标文件中。


7.Java SpringBoot 开发集成

1、Maven 引用。

<dependency>
  <groupId>io.minio</groupId>
  <artifactId>minio</artifactId>
  <version>7.0.2</version>
</dependency>

2、application.yml 配置文件。

# [note] MinIO settings。
minio:
  endpoint: http://<IP>:<PORT>
  accessKey: <username>
  secretKey: <password>
  secure: false

3、注入 MinIOClient 实例

package zhangy.minio;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import io.minio.MinioClient;
import lombok.SneakyThrows;

/**
 * MinIO 配置类
 * 
 * @author 张毅
 *
 */
@Configuration
public class MinIOClientConfig {

    /**
     * 接入点
     */
    @Value("${minio.endpoint}")
    private String endpoint;

    /**
     * 访问秘钥
     */
    @Value("${minio.accessKey}")
    private String accessKey;

    /**
     * 安全秘钥
     */
    @Value("${minio.secretKey}")
    private String secretKey;

    /**
     * 是否启用ssl协议
     */
    @Value("${minio.secure}")
    private boolean secure;

    /**
     * 注入配置。
     * 
     * @return 配置。
     */
    @Bean
    @SneakyThrows
    public MinioClient getMinioClient() {       
        return new MinioClient(endpoint, accessKey, secretKey, secure);
    }
}

4、MinIO 工具类。

package zhangy.minio;

import java.io.InputStream;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import io.minio.MinioClient;
import io.minio.ObjectStat;
import io.minio.PutObjectOptions;
import io.minio.Result;
import io.minio.messages.Bucket;
import io.minio.messages.Item;
import lombok.SneakyThrows;

/**
 * MinIO 远程文件系统客户端实现类
 * 
 * @author 张毅
 *
 */
@Component
public class MinIOClient {

    @Autowired
    protected MinioClient minioClient;

    /**
     * 配置文件桶
     */
    @Value("${minio.bucket:default}")
    protected String bucket;

    /**
     * 创建文件桶。
     *
     * @param bucketName 文件桶名称。
     */
    @SneakyThrows
    public void makeBucket(String bucketName) {
        if (!minioClient.bucketExists(bucketName)) {
            minioClient.makeBucket(bucketName);
        }
    }

    /**
     * 获取全部文件桶。
     * 
     */
    @SneakyThrows
    public List<Bucket> listBuckets() {
        return minioClient.listBuckets();
    }

    /**
     * 检查文件桶是否已创建。
     *
     * @param bucketName 文件桶名称。
     */
    @SneakyThrows
    public boolean existsBucket(String bucketName) {
        return minioClient.bucketExists(bucketName);
    }

    /**
     * 根据文件桶名称获取信息。
     *
     * @param bucketName 文件桶名称。
     */
    @SneakyThrows
    public Optional<Bucket> getBucket(String bucketName) {
        return minioClient.listBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
    }

    /**
     * 根据文件桶名称删除信息。
     *
     * @param bucketName 文件桶名称。
     */
    @SneakyThrows
    public void removeBucket(String bucketName) {
        minioClient.removeBucket(bucketName);
    }

    /**
     * 设置文件桶权限。
     *
     * @param bucketName 文件桶名称。
     * @param policy     文件桶授权。
     */
    @SneakyThrows
    public void setBucketPolicy(String bucketName, PolicyType policy) {
        if (minioClient.bucketExists(bucketName)) {
            minioClient.setBucketPolicy(bucketName, policy.value);
        }
    }

    /**
     * 设置文件桶权限(只读)。
     *
     * @param bucketName 文件桶名称。
     */
    @SneakyThrows
    public void setBucketPolicy(String bucketName) {
        this.setBucketPolicy(bucketName, PolicyType.READ_ONLY);
    }

    /**
     * 设置文件桶权限。
     *
     * @param bucketName 文件桶名称。
     * @param policy     文件桶授权。
     */
    @SneakyThrows
    public String getBucketPolicy(String bucketName) {
        if (minioClient.bucketExists(bucketName)) {
            return minioClient.getBucketLifeCycle(bucketName);
        }
        return null;
    }

    /**
     * 上传文件。
     *
     * @param bucketName 文件桶名称。
     * @param objectName 文件名称。
     * @param stream     文件流。
     * @param options    参数。
     */
    @SneakyThrows
    public void putObject(String bucketName, String objectName, InputStream stream, PutObjectOptions options) {
        if (StrUtil.isBlank(bucketName)) {
            bucketName = StrUtil.isNotBlank(bucketName) ? bucketName : bucket;
        }
        this.makeBucket(bucketName);
        minioClient.putObject(bucketName, objectName, stream, options);
    }

    /**
     * 上传文件。
     *
     * @param bucketName 文件桶名称。
     * @param objectName 文件名称。
     * @param stream     文件流。
     * @param size       文件长度。
     */
    @SneakyThrows
    public void putObject(String bucketName, String objectName, InputStream stream, long size) {
        if (StrUtil.isBlank(bucketName)) {
            bucketName = StrUtil.isNotBlank(bucketName) ? bucketName : bucket;
        }
        this.makeBucket(bucketName);
        PutObjectOptions options = new PutObjectOptions(size, size >= PutObjectOptions.MIN_MULTIPART_SIZE ? size : 0l);
        minioClient.putObject(bucketName, objectName, stream, options);
    }

    /**
     * 上传文件。
     *
     * @param bucketName 文件桶名称。
     * @param objectName 文件名称。
     * @param filename   文件名。
     * @param options    参数。
     */
    @SneakyThrows
    public void putObject(String bucketName, String objectName, String filename, PutObjectOptions options) {
        if (StrUtil.isBlank(bucketName)) {
            bucketName = StrUtil.isNotBlank(bucketName) ? bucketName : bucket;
        }
        this.makeBucket(bucketName);
        minioClient.putObject(bucketName, objectName, filename, options);
    }

    /**
     * 上传文件。
     *
     * @param bucketName 文件桶名称。
     * @param objectName 文件名称。
     * @param filename   文件名。
     */
    @SneakyThrows
    public void putObject(String bucketName, String objectName, String filename) {
        if (StrUtil.isBlank(bucketName)) {
            bucketName = StrUtil.isNotBlank(bucketName) ? bucketName : bucket;
        }
        this.makeBucket(bucketName);
        long size = FileUtil.size(FileUtil.file(filename));
        PutObjectOptions options = new PutObjectOptions(size, size >= PutObjectOptions.MIN_MULTIPART_SIZE ? size : 0l);
        minioClient.putObject(bucketName, objectName, filename, options);
    }

    /**
     * 根据文件前置查询文件。
     *
     * @param bucketName bucket名称。
     * @param prefix     前缀。
     * @param recursive  是否递归查询。
     * @return MinioItem 列表。
     */
    @SneakyThrows
    public List<Item> getObjects(String bucketName, String prefix, boolean recursive) {
        if (StrUtil.isBlank(bucketName)) {
            bucketName = StrUtil.isNotBlank(bucketName) ? bucketName : bucket;
        }
        this.makeBucket(bucketName);
        List<Item> list = new ArrayList<>();
        Iterable<Result<Item>> objectsIterator = minioClient.listObjects(bucketName, prefix, recursive);
        if (objectsIterator != null) {
            Iterator<Result<Item>> iterator = objectsIterator.iterator();
            if (iterator != null) {
                while (iterator.hasNext()) {
                    Result<Item> result = iterator.next();
                    Item item = result.get();
                    list.add(item);
                }
            }
        }

        return list;
    }

    /**
     * 获取文件
     *
     * @param bucketName 文件桶名称。
     * @param objectName 文件名称。
     * @return 二进制流。
     */
    @SneakyThrows
    public InputStream getObject(String bucketName, String objectName) {
        if (StrUtil.isBlank(bucketName)) {
            bucketName = StrUtil.isNotBlank(bucketName) ? bucketName : bucket;
        }
        this.makeBucket(bucketName);
        return minioClient.getObject(bucketName, objectName);
    }

    /**
     * 获取文件外链。
     *
     * @param bucketName bucket名称。
     * @param objectName 文件名称。
     * @param expires    过期时间 <=7。
     * @return url。
     */
    @SneakyThrows
    public String getObjectURL(String bucketName, String objectName, Integer expires) {
        if (StrUtil.isBlank(bucketName)) {
            bucketName = StrUtil.isNotBlank(bucketName) ? bucketName : bucket;
        }
        this.makeBucket(bucketName);
        return minioClient.presignedGetObject(bucketName, objectName, expires);
    }

    /**
     * 获取文件路径。
     * 
     * @param bucketName 文件桶名称。
     * @param fileName   文件名。
     * @return 文件路径。
     */
    @SneakyThrows
    public String getObjectURL(String bucketName, String fileName) {
        if (StrUtil.isBlank(bucketName)) {
            bucketName = StrUtil.isNotBlank(bucketName) ? bucketName : bucket;
        }
        this.makeBucket(bucketName);
        return minioClient.getObjectUrl(bucketName, fileName);
    }

    /**
     * 获取对象元数据。
     * 
     * @param bucketName 文件桶名称。
     * @param objectName 文件名称。
     * @return 对象元数据。
     */
    @SneakyThrows
    public ObjectStat statObject(String bucketName, String objectName) {
        if (StrUtil.isBlank(bucketName)) {
            bucketName = StrUtil.isNotBlank(bucketName) ? bucketName : bucket;
        }
        this.makeBucket(bucketName);
        return minioClient.statObject(bucketName, objectName);
    }

    /**
     * 删除文件。
     *
     * @param bucketName 文件桶名称。
     * @param objectName 文件名称,
     */
    @SneakyThrows
    public void removeObject(String bucketName, String objectName) {
        if (StrUtil.isBlank(bucketName)) {
            bucketName = StrUtil.isNotBlank(bucketName) ? bucketName : bucket;
        }
        this.makeBucket(bucketName);
        minioClient.removeObject(bucketName, objectName);
    }

    /**
     * 文件桶权限枚举。
     * 
     * @author 张毅
     *
     */
    public enum PolicyType {

        NONE("none"), READ_ONLY("readonly"), READ_WRITE("readwrite"), WRITE_ONLY("writeonly");

        private final String value;

        private PolicyType(final String value) {
            this.value = value;
        }

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

推荐阅读更多精彩内容