elasticsearch理论、集群、分词器、head插件、常用命令

概述

elasticsearch是一个分布式可扩展的实时搜索和分析引擎,一个建立在全文搜索引擎Apache Lucene基础上的搜索引擎,它不仅可以进行全文搜索,还可以进行以下工作:

  • 分布式实时文件存储,并将每一个字段都编入索引,使其可以被搜索
  • 实时分析的分布式搜索引擎
  • 可以扩展到上百台服务器,吹PB级别的结构化或非及结构化数据
  • 提供了REST API的操作接口,开箱即用

存储单位:
1KB=1024B
1MB=1024KB
1GB=1024MB
1TB=1024GB
1PB=1024TB
1EB=1024PB
1ZB=1024EB
1YB=1024ZB

基本概念

elasticsearch是面向文档型数据库,一条数据在这里就是一个文档

  1. 索引(Index)
    索引是具有相似特性的文档集合。例如,可以为客户数据提供索引,为产品目录建立另一个索引,以及为订单数据建立另一个索引。索引由名称(必须全部为小写)标识,该名称用于在对其中的文档执行索引、搜索、更新和删除操作时引用索引。在单个群集中,您可以定义尽可能多的索引。

  2. 文档(document)
    Elasticsearch文档是一个存储在索引中的JSON文档。每个文档都有一个类型和对应的ID,这是惟一的。
    如:

 	{
			  "_index" : "packtpub",
			  "_type" : "elk",
			  "_id" : "1",
			  "_version" : 1,
			  "found" : true,
			  "_source":{
				book_name : "learning elk",
				book_author:"鲁迅"
			  }
		}
  1. 字段(Field)
    文档内的一个基本单位,键值对形式(book_name : “learning elk”)

  2. 类型(Type)
    类型是index下的一个逻辑分类。比如weather这个index里,可以按照城市分组,也可以按照气候类型分组,这种分组就叫做类型

  3. 映射(Mapping)
    映射用于映射文档的每个field及其对应的数据类型,例如字符串、整数、浮点数、双精度数、日期等等。

  4. 分片(Shard)
    shard:单台机器无法存储大量数据,es可以将一个索引中的数据切分为多个shard,分布在多台服务器上存储。有了shard就可以横向扩展,存储更多数据,让搜索和分析等操作分布到多台服务器上去执行,提升吞吐量和性能。

  5. 分词
    把一段文本中的词按一定规则进行切分

  6. 主分片(Primary shard)与复制分片(replica shard)
    复制分片通常驻留在一个不同的节点上,而不是主碎片,在故障转移和负载平衡的情况下,可以满足多个请求。

  7. 集群(Cluster)
    集群是存储索引数据的节点集合。elasticsearch提供了水平的可伸缩性用以存储集群中的数据。每个集群都由一个集群名称来表示,不同的节点指明集群名称连接在一起。

  8. 节点(Node)
    节点是一个单独运行的elasticsearch实例,它属于一个集群。默认情况下,elasticsearch中的每个节点都加入名为“elasticsearch”的集群。每个节点都可以在elasticsearch中使用自己的elasticsearch.yml,它们可以对内存和资源分配有不同的设置。

ES集群

集群分类

  1. 数据节点(Data Node)
    数据节点索引文档并对索引文档执行搜索。建议添加更多的数据节点,以提高性能或扩展集群。通过在elasticsearch中设置这些属性,可以使节点成为一个数据节点。elasticsearch.yml配置
    node.master = false
    node.data=true
  2. 管理节点(Master Node)
    主节点负责集群的管理。对于大型集群,建议有三个专用的主节点(一个主节点和两个备份节点),它们只作为主节点,不存储索引或执行搜索。在elasticsearch.yml配置声明节点为主节点:
    node.master = true
    node.data=false
  3. 路由节点亦称负载均衡节点(Routing Node or load balancer node)
    这些节点不扮演主或数据节点的角色,但只需执行负载平衡,或为搜索请求路由,或将文档编入适当的节点。这对于高容量搜索或索引操作非常有用。
    node.master = false
    node.data=false
  4. 提取节点(lngest节点)
    可以创建多个预处理管道,用以修改传入文档

Zendiscovery通信

默认ES进程会绑定在自己的回环地址上,然后扫描本机的9300-9305号端口,尝试跟其他端口上的启动的es进程进行通信,然后自动形成一个集群。如果修改了监听地址为非回环地址,ES按照配置文件里指定的地址或自动扫描当前网段其他节点,自动跟其他节点上的es node进行通信

Master选举

如下图所示
在这里插入图片描述

说明:下边的判断框(是否本地节点),是说master是否为本机,如果master不是本机,则发送加入集群的请求,如果master是本机,等待加入的节点数量达到规定数量以完成选举,选举完成之后宣布自己是master,发布clusterstate。要说明的是,当临时master选举出来的时候只是选举master的第一步,master是集群概念,所以要等其他节点加入进来才算真正意义的master

脑裂

因为网络或者其他故障,导致一个集群被划分成了两伙或者多方势力,这些群伙都有多个node以及一个master,那么原来的集群就出现了多个master。master主宰了集群状态的维护以及shard的分配,因此如果有多个master,可能会导致数据被破坏

容错机制

状态 意义
green 所有主分片和从分片都可用
yellow 所有主分片可用,但存在不可用的从分片
red 存在不可用的主分片
  • 宕机瞬间

master node宕机的一瞬间,该节点的primary shard(主分片)就没有了,此时状态就不是active status,那么集群中就不是所有的主分片都是active的了

  • 容错步骤一

master选举,es自动选举另一个node成为master,承担起master的责任

  • 容错步骤二

新master将丢失掉的主分片的某个复制分片提升为主分片,此时cluster status会变成yellow,因为所有的主分片都变成active status了,但是少了一个复制分片

  • 容错步骤三

重启node,新master会将确实的副本都copy一份到该节点,而且该节点会使用之前已有的分片数据,只是同步一下宕机的修改,cluster status变为green

集群部署

服务器规划

主机名 ip 作用
es1 192.168.43.249 ES节点1
es2 192.168.43.74 ES节点2
es3 192.168.43.253 ES节点3

环境准备

内存分配

每台2G

目录规划

目录 作用
/usr/local/es 程序应用目录
/es/data 数据目录
/es/log 日志目录

用户及权限

建立用户并进行目录授权(es不建议使用root运行)

mkdir -p /es/{data,log}
groupadd es
useradd es -g es -p es
chown -R es:es /es/data
chown -R es:es /es/log

hosts绑定

所有节点均绑定hosts记录

vim /etc/hosts
192.168.43.249 es1
192.168.43.74 es2
192.168.43.253 es3

系统设定

#可打开的文件描述符

echo '* soft nofile 819200' >> /etc/security/limits.conf
echo '* hard nofile 819200' >> /etc/security/limits.conf

#用户可用进程数量

echo '* soft nproc 2048' >> /etc/security/limits.conf
echo '* hard nproc 4096' >> /etc/security/limits.conf

#JVM能开启的最大线程数
echo 'vm.max_map_count=655360' >> /etc/sysctl.conf
#重载生效
sysctl -p

JDK环境

所有节点均安装JDK1.8以后版本

tar -zxf jdk-8u201-linux-x64.tar.gz -C /usr/local/
echo 'export JAVA_HOME=/usr/local/jdk1.8.0_201' >> /etc/profile
echo 'export JRE_HOME=/usr/local/jdk1.8.0_201/jre' >> /etc/profile
echo 'export CLASSPATH=$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar' >> /etc/profile
echo 'export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH' >> /etc/profile
sysctl -p

ES集群安装

所有节点安装ES

[root@localhost ~]# wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.3.2.tar.gz
[root@localhost ~]# tar -zxf elasticsearch-6.3.2.tar.gz -C /usr/local/
[root@localhost ~]# ln -s /usr/local/elasticsearch-6.3.2/ /usr/local/es
[root@localhost ~]# chown -R es:es /usr/local/es/

ES集群配置

#JVM参数可以根据服务器配置情况适当调整
[root@localhost root]$ vim /usr/local/es/config/jvm.options 
-Xms2g
-Xmx2g
#主配置文件严格按照yml语法
[root@localhost root]$ vim /usr/local/es/config/elasticsearch.yml 

ES1 节点设置

[root@localhost root]$ vim /usr/local/es/config/elasticsearch.yml 
cluster.name: bdqn-cluster 
node.name: es1
node.master: true #是否为主节点(有资格被选为主节点)
node.data: true  #是否作为数据节点
path.data: /es/data
path.logs: /es/log
network.host: 192.168.43.249  #网络地址
http.port: 9200
transport.tcp.port: 9300  #内部节点间的沟通端口
discovery.zen.minimum_master_nodes: 2 #至少有几个存货的候选节点才进行master选举
discovery.zen.ping_timeout: 120s #超时时间放长,防止脑裂
client.transport.ping_timeout: 60s
 #配置有机会参与master选举的节点
discovery.zen.ping.unicast.hosts: ["192.168.43.249:9300","192.168.43.74:9300","192.168.43.253:9300"] 
gateway.recover_after_nodes: 3  #达到该数量数据节点启动复制
gateway.recover_after_time: 5m  #在达到期待的节点数之前,recovery过程会等待的时间
gateway.recover_after_data_nodes: 2  #如果超过等待时间,达到多少数据节点,启动recovery

gateway.recover_after_nodes和gateway.recover_after_data_nodes要结合gateway.revocer_after_time来使用,例如,上述配置表明,如果5分钟还没有达到足够数量的节点则超时,超时后如果节点数量达到3个就启动recovery,或者数据节点数量达到2个,就启动recovery

ES2 节点配置

es2节点的配置跟节点一基本相同,需要改动的项有

node.name: es2
network.host: 192.168.43.74

ES3 节点配置

es3节点的配置跟节点一基本相同,需要改动的项有

node.name: es3
network.host: 192.168.43.253

启动

启动之前需要清空防火墙

#切换启动用户
su es
#查看帮助说明
/usr/local/es/bin/elasticsearch -h
#后台运行启动
/usr/local/es/bin/elasticsearch -d

查看集群状态

如果遇到下边这样的报错,说明程序没有启动起来,看一下chown命令是否生效

[root@localhost ~]# curl -XGET 'http://192.168.43.249:9200/_cluster/health?pretty'
curl: (7) Failed connect to 192.168.43.249:9200; Connection refused
[root@localhost ~]# ll /usr/local/
total 0
drwxr-xr-x. 2 root root   6 Nov  5  2016 bin
drwxr-xr-x. 8 es   es   143 Jul 20  2018 elasticsearch-6.3.2
lrwxrwxrwx. 1 root root  31 Feb  3 15:14 es -> /usr/local/elasticsearch-6.3.2/

可以看出es目录的属主和属组没有改过来,重新改一遍属主和属组,三个节点都执行
[root@localhost ~]# chown -R es:es /usr/local/es

改完属主和属组之后再运行
[root@localhost ~]# su es
[es@localhost root]$ /usr/local/es/bin/elasticsearch -d
[es@localhost root]$ curl -XGET 'http://192.168.43.249:9200/_cluster/health?pretty'
{
  "cluster_name" : "bdqn-cluster",
  "status" : "red",
  "timed_out" : false,
  "number_of_nodes" : 3,
  "number_of_data_nodes" : 3,
  "active_primary_shards" : 0,
  "active_shards" : 0,
  "relocating_shards" : 0,
  "initializing_shards" : 0,
  "unassigned_shards" : 0,
  "delayed_unassigned_shards" : 0,
  "number_of_pending_tasks" : 0,
  "number_of_in_flight_fetch" : 0,
  "task_max_waiting_in_queue_millis" : 0,
  "active_shards_percent_as_number" : "NaN"
}

查看节点及master
[es@localhost root]$ curl -XGET 'http://192.168.43.249:9200/_cat/master'
4TB13oyNS7qcfEebaOaFyw 192.168.43.74 192.168.43.74 es2
[es@localhost root]$ curl -XGET 'http://192.168.43.249:9200/_cat/nodes'
192.168.43.249 11 84 1 0.27 0.17 0.19 mdi - es1
192.168.43.74  11 97 0 0.08 0.04 0.05 mdi * es2
192.168.43.253 10 96 0 0.12 0.07 0.06 mdi - es3

在这里插入图片描述
也可以在节点本身看一下elasticsearch是否启动成功
在这里插入图片描述

插件安装

IK分词器

自动安装方式

[es@localhost root]$ /usr/local/es/bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.3.2/elasticsearch-analysis-ik-6.3.2.zip

注意,版本要严格匹配,安装完之后要重启节点

手动安装

需要在每个节点都暗转ik才可以

[root@localhost ~]# wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.3.2/elasticsearch-analysis-ik-6.3.2.zip
[root@localhost ~]# mkdir /usr/local/es/plugins/ik
[root@localhost ~]# unzip elasticsearch-analysis-ik-6.3.2.zip -d /usr/local/es/plugins/ik/
[root@localhost ~]# chown -R es:es /usr/local/es/plugins/ik/

上述操作完成后,重启es

验证分词

#创建索引
[es@localhost root]$ curl -XPUT http://192.168.43.249:9200/index
{"acknowledged":true,"shards_acknowledged":true,"index":"index"}

#使用index索引创建mapping(对字段'content'进行中文分词)
> }'
[es@es1 root]$ curl -XPOST "http://192.168.43.249:9200/index/fulltext/_mapping" -H 'Content-Type: application/json' -d'
> {
>         "properties": {
>             "content": {
>                  "type": "text",
>                  "analyzer": "ik_max_word",
>                  "search_analyzer": "ik_max_word"
>              }
>         }
> }'
{"acknowledged":true}[es@es1 root]$ 

#添加记录
[es@es1 root]$ curl -H "Content-Type: application/json" -XPOST http://192.168.43.249:9200/index/fulltext/1 -d'{"content":"燃烧你的卡路里"}'
{"_index":"index","_type":"fulltext","_id":"1","_version":1,"result":"created","_shards":{"total":2,"successful":2,"failed":0},"_seq_no":0,"_primary_term":4}

#查找记录
[es@es1 root]$ curl -XPOST "http://192.168.43.249:9200/index/fulltext/_search" -H 'Content-Type: application/json' -d'
> {
>     "query" : { "match" : { "content" : "燃烧"}},
>     "highlight" : {
>         "pre_tags" : ["<tag1>", "<tag2>"],
>         "post_tags" : ["</tag1>", "</tag2>"],
>         "fields" : {
>             "content" : {}
>         }
>     }
> }'
{"took":588,"timed_out":false,"_shards":{"total":5,"successful":5,"skipped":0,"failed":0},"hits":{"total":1,"max_score":0.2876821,"hits":[{"_index":"index","_type":"fulltext","_id":"1","_score":0.2876821,"_source":{"content":"燃烧你的卡路里"},"highlight":{"content":["<tag1>燃烧</tag1>你的卡路里"]}}]}}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Head插件

修改配置文件,开启跨域rest请求,所有节点都这样操作

[es@es1 root]$ vim /usr/local/es/config/elasticsearch.yml 
末尾加两行 然后重启es
http.cors.enabled: true
http.cors.allow-origin: "*"

安装配置head插件

#下载淘宝10.6的npm镜像
wget https://npm.taobao.org/mirrors/node/latest/node-v10.6.0-linux-x64.tar.gz

#解压
tar -zxf node-v10.6.0-linux-x64.tar.gz -C /usr/local/

#配置环境变量/etc/profile  尾部添加内容
vim /etc/profile
export NODE_HOME=/usr/local/node-v10.6.0-linux-x64/
export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$NODE_HOME/bin:$PATH

#重新加载
source /etc/profile

#使用淘宝NPM镜像站(别名方式)
alias cnpm="npm --registry=https://registry.npm.taobao.org --cache=$HOME/.npm/.cache/cnpm --disturl=https://npm.taobao.org/dist --userconfig=$HOME/.cnpmrc"

#下载head插件
git clone git://github.com/mobz/elasticsearch-head.git

#安装
cd elasticsearch-head/
cnpm install

#运行
cnpm run start

#访问
firefox http://192.168.43.249:9100

关于数据复制

参数配置

举例来说,对于一个有10个数据节点的集群,如果有以下设置

gateway.expected_data_nodes: 10
gateway.recover_after_time: 5m
gateway.recover_after_data_nodes: 8

那么集群5分钟内10个数据节点都加入,或者5分钟后有8个及以上的数据节点加入,都会立即启动recovery过程

减少主副本之间的数据复制

如果不是full restart,而是重启单个数据节点,仍然会造成数据在不同节点之间来回复制。为了避免这个问题,可以在重启之前先关闭集群的 shard allocation,尽量从本地直接回复数据,然后,在节点重启完成加入集群后,再重新打开。

常用命令

状态相关

_cat

[root@es1 elasticsearch-head]# curl 192.168.43.249:9200/_cat
=^.^=
/_cat/allocation
/_cat/shards
/_cat/shards/{index}
/_cat/master
/_cat/nodes
/_cat/tasks
/_cat/indices
/_cat/indices/{index}
/_cat/segments
/_cat/segments/{index}
/_cat/count
/_cat/count/{index}
/_cat/recovery
/_cat/recovery/{index}
/_cat/health
/_cat/pending_tasks
/_cat/aliases
/_cat/aliases/{alias}
/_cat/thread_pool
/_cat/thread_pool/{thread_pools}
/_cat/plugins
/_cat/fielddata
/_cat/fielddata/{fields}
/_cat/nodeattrs
/_cat/repositories
/_cat/snapshots/{repository}
/_cat/templates

verbose 详细信息
每个命令都支持使用?v参数,来显示详细的信息

[root@es1 elasticsearch-head]# curl http://192.168.43.249:9200/_cat/master?v
id                     host          ip            node
4TB13oyNS7qcfEebaOaFyw 192.168.43.74 192.168.43.74 es2

help
每个命令都支持使用help参数,来输出可以显示的列

[root@es1 elasticsearch-head]# curl http://192.168.43.249:9200/_cat/master?help
id   |   | node id    
host | h | host name  
ip   |   | ip address 
node | n | node name  

headers
通过h参数,可以执行输出的字段

[root@es1 elasticsearch-head]# curl http://192.168.43.249:9200/_cat/master?h=ip,node
192.168.43.74 es2

索引相关

查看所有索引

[root@es1 elasticsearch-head]# curl http://192.168.43.249:9200/_cat/indices?v
health status index uuid                   pri rep docs.count docs.deleted store.size pri.store.size
green  open   index WgQXJcVDSf6BiVAogsKk5g   5   1          1            0      8.2kb          4.1kb

创建索引

[root@es1 elasticsearch-head]# curl -XPUT 'http://192.168.43.249:9200/bdqntest'
{"acknowledged":true,"shards_acknowledged":true,"index":"bdqntest"}

关闭索引

[root@es1 elasticsearch-head]# curl -XPOST 'http://192.168.43.249:9200/bdqntest/_close?pretty'
{
  "acknowledged" : true
}

打开索引

[root@es1 elasticsearch-head]# curl -XPOST 'http://192.168.43.249:9200/bdqntest/_open?pretty'
{
  "acknowledged" : true,
  "shards_acknowledged" : true
}

删除索引

[root@es1 elasticsearch-head]# curl -XDELETE 'http://192.168.43.249:9200/bdqntest?pretty'
{
  "acknowledged" : true
}

数据相关

插入数据

[root@es1 elasticsearch-head]# curl -H 'Content-Type: application/json' -XPUT '192.168.43.249:9200/bdqntest/external/1?pretty' -d'
> {
>     "name": "tiechui"
> }'
{
  "_index" : "bdqntest",
  "_type" : "external",
  "_id" : "1",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}

取出external类型id为1的数据

[root@es1 elasticsearch-head]# curl -XGET '192.168.43.249:9200/bdqntest/external/1?pretty'
{
  "_index" : "bdqntest",
  "_type" : "external",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "_source" : {
    "name" : "tiechui"
  }
}

更新文档

[root@es1 elasticsearch-head]# curl -H 'Content-Type: application/json' -XPOST '192.168.43.249:9200/bdqntest/external/1/_update?pretty' -d'
{
    "doc": { "name": "wangtiechui" }
}'
{
  "_index" : "bdqntest",
  "_type" : "external",
  "_id" : "1",
  "_version" : 2,
  "result" : "updated",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 1,
  "_primary_term" : 1
}

删除文档

[root@es1 elasticsearch-head]# curl -XDELETE '192.168.43.249:9200/bdqntest/external/1?pretty'
{
  "_index" : "bdqntest",
  "_type" : "external",
  "_id" : "1",
  "_version" : 4,
  "result" : "deleted",
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_seq_no" : 3,
  "_primary_term" : 1
}

查询所有记录

[root@es1 elasticsearch-head]# curl -H "Content-Type: application/json" -XPOST '192.168.43.249:9200/bdqntest/external/_search?pretty' -d'
> {
>     "query": { "match_all":{}}
> }'
{
  "took" : 266,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 0,
    "max_score" : null,
    "hits" : [ ]
  }
}

查询符合记录

[root@es1 elasticsearch-head]# curl -H "Content-Type: application/json" -XPOST '192.168.43.249:9200/bdqntest/external/_search?pretty' -d'
{
    "query": { "match":{"name": "tiechui"}}
}'
{
  "took" : 94,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 0,
    "max_score" : null,
    "hits" : [ ]
  }
}

缓存相关

clear cache
下边的命令可以清空所有的内存缓存

[root@es1 elasticsearch-head]# curl -H "Content-Type: application/json" -XPOST '192.168.43.249:9200/bdqntest/_cache/clear?pretty'
{
  "_shards" : {
    "total" : 10,
    "successful" : 10,
    "failed" : 0
  }
}

flush
flush api可以让我们去强制flush多个索引,索引flush后,就会释放这个索引占用的内存,因为会将缓存中的数据强制写到磁盘,同时还会清理掉translog中的日志。默认情况下es会不定时自动触发flush操作

ES为了数据的安全,在接受写入文档的时候,在写入内存buffer的同时,会写一份translog日志,从而在出现程序故障或磁盘异常时,保证数据的安全。

[root@es1 elasticsearch-head]# curl -H "Content-Type: application/json" -XPOST '192.168.43.249:9200/bdqntest/_flush?pretty'
{
  "_shards" : {
    "total" : 10,
    "successful" : 10,
    "failed" : 0
  }
}

refresh
refresh用来刷新一个index,可以让立即实现数据从内存到磁盘的缓存过程,从而使文档可以被立即搜到

[root@es1 elasticsearch-head]# curl -H "Content-Type: application/json" -XPOST '192.168.43.249:9200/bdqntest/_refresh?pretty'
{
  "_shards" : {
    "total" : 10,
    "successful" : 10,
    "failed" : 0
  }
}

flush与refresh的区别

当一个文档进入ES的初期, 文档是被存储到内存里的,默认经过1s之后, 会被写入文件系统缓存,这样该文档就可以被搜索到了,注意,此时该索引数据被没有最终写入到磁盘上。如果你对这1s的时间间隔还不满意, 调用_refresh就可以立即实现内存->文件系统缓存, 从而使文档可以立即被搜索到。
ES为了数据的安全, 在接受写入的文档的时候, 在写入内存buffer的同时, 会写一份translog日志,从而在出现程序故障/磁盘异常时, 保证数据的完整和安全。flush会触发lucene commit,并清空translog日志文件。 translog的flush是ES在后台自动执行的,默认情况下ES每隔5s会去检测要不要flush translog,默认条件是:每 30 分钟主动进行一次 flush,或者当 translog 文件大小大于 512MB主动进行一次 flush

猜你喜欢

转载自blog.csdn.net/weixin_43557605/article/details/86751201
今日推荐