Linux环境安装Elasticsearch集群高可用及使用

0.873字数 2484阅读 567

Elasticsearch优势

横向可扩展性:只需要增加台服务器,做一点儿配置,启动一下Elasticsearch就可以并入集群。
分片机制提供更好的分布性:同一个索引分成多个分片(sharding), 这点类似于HDFS的块机制;分而治之的方式可提升处理效率。
高可用:提供复制( replica) 机制,一个分片可以设置多个复制,使得某台服务器在宕机的情况下,集群仍旧可以照常运行,并会把服务器宕机丢失的数据信息复制恢复到其他可用节点上。
口使用简单:共需一条命令就可以下载文件,然后很快就能搭建一一个站内搜索引擎。

Elasticsearch应用场景

大型分布式日志分析系统ELK elasticsearch(存储日志)+logstash(收集日志)+kibana(展示数据)
大型电商商品搜索系统、网盘搜索引擎等。

Elasticsearch存储结构

Elasticsearch是文件存储,Elasticsearch是面向文档型数据库,一条数据在这里就是一个文档,用JSON作为文档序列化的格式,比如下面这条用户数据:

{
    "name" :     "AncientJazz",
    "age" :      18
}

关系数据库 ⇒ 数据库 ⇒ 表 ⇒ 行 ⇒ 列(Columns)
Elasticsearch ⇒ 索引(Index) ⇒ 类型(type) ⇒ 文档(Docments) ⇒ 字段(Fields)

一.Java环境变量

  • 卸载Centos7自带的OpenJDK,安装JDK8
1.卸载OpenJDK的命令
rpm -e --nodeps java-1.8.0-openjdk-1.8.0.102-4.b14.el7.x86_64
rpm -e --nodeps java-1.8.0-openjdk-headless-1.8.0.102-4.b14.el7.x86_64
rpm -e --nodeps java-1.7.0-openjdk-1.7.0.111-2.6.7.8.el7.x86_64
rpm -e --nodeps java-1.7.0-openjdk-headless-1.7.0.111-2.6.7.8.el7.x86_64
2.在/usr/local/src路径下创Java文件夹
cd /usr/local/src
mkdir Java
3.进入/usr/local/src/Java上传jdk-8u51-linux-x64.tar.gz并解压
cd /usr/local/src/Java
tar -zxvf jdk-8u51-linux-x64.tar.gz
4.配置环境变量

进入profile文件编辑模式

vim /etc/profile

在profile最后添加如下内容

JAVA_HOME=/usr/local/src/Java/jdk1.8.0_51
JAVA_BIN=/usr/local/src/Java/jdk1.8.0_51/bin
PATH=$JAVA_HOME/bin:$PATH
CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export JAVA_HOME JAVA_BIN PATH CLASSPATH

运行此命令让环境变量生效

source /etc/profile

再次查询jdk版本是否生效

二.Elasticsearch安装

1.执行下载命令
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.4.3.tar.gz
2.解压Elasticsearch
tar -zxf elasticsearch-6.4.3.tar.gz -C /usr/local/src/
3.进入Elasticsearch修改配置文件
cd /usr/local/src/elasticsearch-6.4.3/config

我们可以看到目录下jvm.options文件,那么他是做什么用的?

执行文本输出命令,查看内容

cat jvm.options

里面有配置jvm内存的设置,可以看到默认初始内存和启动内存是1g,比较占内存,我们可以改成512m(暂未测试)

修改elasticsearch配置文件

vim elasticsearch.yml

修改为虚拟机的实际ip地址



修改端口号:Elasticsearch有两个端口号是 9200和9300,那他们有什么区别?
9200:ES节点和外部通信的端口号‘

9300是TCP协议端口号,ES集群之间通讯端口号
9200是http协议端口号,暴露ES RESTful接口端口号

后续查看集群健康度:添加这行是因为elasticsearch服务与elasticsearch-head之间可能存在跨越,修改elasticsearch配置即可,在elastichsearch.yml中添加如下命名即可:

#allow origin
http.cors.enabled: true
http.cors.allow-origin: "*"
4.启动Elasticsearch
cd /usr/local/src/elasticsearch-6.4.3/bin
./elasticsearch

启动报错:root用户无法启动


can not run elasticsearch as root
  • 解决方案:
  1. 因为安全问题Elasticsearch

  2. 不让用root用户直接运行,所以要创建新用户
    第一步:liunx创建新用户 adduser XXX 然后给创建的用户加密码 passwd XXX 输入两次密码。
    第二步:切换刚才创建的用户 su XXX 然后执行elasticsearch 会显示Permission denied 权限不足。
    第三步:给新建的XXX赋权限,chmod 777 * 这个不行,因为这个用户本身就没有权限,肯定自己不能给自己付权限。所以要用root用户登录付权限。
    第四步:root给XXX赋权限,chown -R XXX /你的elasticsearch安装目录。
    然后执行成功。

  3. 创建一个分组

groupadd esroot
useradd esadmin -g esroot -p root
chown -R esadmin:esroot /usr/local/src/elasticsearch-6.4.3/
su esadmin 
5.再次启动Elasticsearch
./elasticsearch

启动报错:直接停止运行报错


bootstrap checks failed
  • 解决方案:
    切回root用户
su root
vi /etc/sysctl.conf
vm.max_map_count=655360
cd /etc
sysctl -p
6.修改完成后再次启动Elasticsearch

再切到esadmin用户,再次启动

su esadmin
./elasticsearch

启动报错:依旧启动直接停止报错


max file descriptors [4096] for elasticsearch process is too low, increase to at least [65536]
  • 解决方案:记得用root用户
vi /etc/security/limits.conf

在最后添加如下内容

* soft nofile 65536
* hard nofile 131072
* soft nproc 2048 
* hard nproc 4096

再次启动的话也会报错

7.需要重启虚拟机使用esadmin启动即可运行成功
cd /usr/local/src/elasticsearch-6.4.3/bin
su esadmin
./elasticsearch

启动成功,不再报错


8.外部访问Elasticsearch

记得使用root用户关闭

临时关闭防火墙

systemctl stop firewalld

永久防火墙开机自启动

systemctl disable firewalld

查看防火墙状态

systemctl status firewalld

http://虚拟机ip地址:9200



Kibana可视化界面

1.执行下载命令
wget https://artifacts.elastic.co/downloads/kibana/kibana-6.4.3-linux-x86_64.tar.gz
2.解压Kibana
tar -zxvf kibana-6.4.3-linux-x86_64.tar.gz -C /usr/local/src/
3.进入Kibana修改配置文件
cd /usr/local/src/kibana-6.4.3-linux-x86_64/config

1.开放端口号
2.设置虚拟机地址
3.设置Elasticsearch的协议、ip地址、端口号,如果是集群就写集群的地址


image.png
4.启动Kibana

进入bin目录

cd /usr/local/src/kibana-6.4.3-linux-x86_64/bin

执行启动命令

./kibana

在windows上访问: http://虚拟机ip地址:5601


5.Kibana实现增删改查

点击Dev Tools

  • 创建索引:主要不能用大写否则会报错
PUT /ancientjazz

点击运行,右侧提示创建成功


  • 查询索引
GET /ancientjazz

可以用查询到刚刚创建的索引


  • 添加文档 /索引名称/类型/id
PUT /ancientjazz/user/1
{
  "name":"gujinhang",
  "sex":0,
  "age":21
}

添加成功后可以看到有索引、类型、id、以及版本号,比如多次执行或者把name属性修改了再次执行可以发现他的版本号是会自增长


  • 查询文档
GET /ancientjazz/user/1

查询到刚刚添加的数据是一致的


  • 删除索引
DELETE /ancientjazz

删除成功



再次查询是会报错的


Elasticsearch版本控制

  1. 为什么要进行版本控制
    为了保证数据再多线程操作下的准确性

  2. 悲观锁和乐观锁
    悲观锁:假设会发生并发冲突,屏蔽一切可能违反数据准确性的操作
    乐观锁:假设不会发生并发冲突,只在提交操作是检查是否违反数据完整性。

  3. 内部版本控制和外部版本控制
    内部版本控制:_version自增长,修改数据后,_version会自动的加1

外部版本控制:为了保持_version与外部版本控制的数值一致
使用version_type=external检查数据当前的version值是否小于请求中的version值

PUT /ancientjazz/user/1?version=2
{
  "name":"gujinhang",
  "sex":0,
  "age":21
}

ik分词器安装

因为Elasticsearch中默认的标准分词器分词器对中文分词不是很友好,会将中文词语拆分成一个一个中文的汉子。因此引入中文分词器-es-ik插件

下载地址: https://github.com/medcl/elasticsearch-analysis-ik/releases
注意: es-ik分词插件版本一定要和es安装的版本对应

先停掉Elasticsearch
将解压好的文件通过FileZilla上传到此路径 /usr/local/src/elasticsearch-6.4.3/plugins

上传成功,重启Elasticsearch即可


cd /usr/local/src/elasticsearch-6.4.3/bin
./elasticsearch
自定义扩展字典

先停掉Elasticsearch

/usr/local/src/elasticsearch-6.4.3/plugins/ik/config
mkdir AncientJazz
vi AncientJazz/word.dic

修改配置文件指定词典

vi IKAnalyzer.cfg.xml

启动Elasticsearch即可

测试配置结果

这个默认分词器不太友好,把每个中文字都分开了



下面这个是配置过后的效果



集群搭建

克隆三台之前安装好Elasticsearch的虚拟机:我自己用的Hyper-v,VM的自己操作克隆

1.首先将原来的名字改掉


2.导出虚拟机


3.选择导出位置


4.导出成功后进行导入操作


5.选中导出的文件目录——下一步


6.直接下一步


7.选中导入类型——复制虚拟机——下一步


8.设置虚拟机文件存放位置——下一步


9.设置虚拟机硬盘的存放位置——下一步


10.点击完成


11.克隆成功:将其改名为Elasticsearch-1



其他两台重复操作即可,把三台都开起来设置ip地址


虚拟机名称 IP地址 服务器名称
Elasticsearch-1 192.168.137.151 node-1
Elasticsearch-2 192.168.137.152 node-2
Elasticsearch-3 192.168.137.153 node-3

集群的准备工作已经做完

服务集群配置

修改配置文件

cd /usr/local/src/elasticsearch-6.4.3/config/
vi elasticsearch.yml
cluster.name: es     #保证三台服务器节点集群名称相同
node.name: node-1     #每个节点名称不一样 其他两台为node-2 ,node-3
network.host: XXX.XXX.XXX.XXX     #实际虚拟机服务器ip地址
discovery.zen.ping.unicast.hosts: ["192.168.137.151", "192.168.137.152","192.168.137.153"]   #多个服务集群ip
discovery.zen.minimum_master_nodes: 1

修改完成后启动三台虚拟机的服务

验证集群: http://虚拟机ip地址:9200/_cat/nodes?pretty



注意克隆data文件会导致数据不同步
报该错误解决办法
failed to send join request to master
因为克隆导致data文件也克隆呢,直接清除每台服务器data文件

cd /usr/local/src/elasticsearch-6.4.3/data
rm -rf nodes/

解决完以上错误再次启动

启动可能会比较慢,最好成功了再起下一台,不然可能出现脑裂现象

cd /usr/local/src/elasticsearch-6.4.3/bin
./elasticsearch -d #后台守护线程运行

启动成功
*号是主


Elasticsearch-head插件——监控集群健康度(自行参考网上教学安装)生病写不动了~~~~


检索-SpringBoot整合Jest操作ES

  1. 创建项目
  1. 选择模块

  1. pom.xml文件分析
    SpringBoot默认支持两种技术来和ES交互;
    1.Jest(默认不生效)
    需要导入jest的工具包(io.searchbox.client.JestClient)
    2.SpringData ElasticSearch(版本不适配会超时连接)
    1)、Client节点信息clusterNodes;clusterName
    2)、ElasticSearchTemplate操作es
    3)、编写一个ElasticSearchRepository的子接口来操作ES

4.配置
集群以及连接超时时间的设置
版本适配说明:https://github.com/spring-projects/spring-data-elasticsearch
如果版本不适配:
1)、升级SpringBoot版本
2)、安装对应版本的ES

5.Jest测试

package com.ancientjazz.springbootes;

import com.ancientjazz.springbootes.bean.student;
import io.searchbox.client.JestClient;

import io.searchbox.core.Index;
import io.searchbox.core.Search;
import io.searchbox.core.SearchResult;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.IOException;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootEsApplicationTests {

    @Autowired
    private JestClient jestClient;

    //存储
    @Test
    public void contextLoads() {
        student student = new student();
        student.setId(2);
        student.setName("张三");
        student.setAge(18);
        student.setSex("男");

        Index index = new Index.Builder(student).index("shoocl").type("student").build();
        try {
            jestClient.execute(index);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //获取
    @Test
    public void contextLcads1(){
        String json="{\n" +
                "  \"query\": {\n" +
                "  \t\"match\":{\n" +
                "  \t\t\"name\":\"张三\"\n" +
                "  \t}\n" +
                "  }\n" +
                "}";

        Search build = new Search.Builder(json).addIndex("shoocl").addType("student").build();
        try {
            SearchResult execute = jestClient.execute(build);
            System.out.println(execute.getJsonString());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

测试实体类

package com.ancientjazz.springbootes.bean;

import lombok.Data;

/**
 * @author Ancient Jazz
 * @create 2019-02-16 14:03
 **/
@Data
public class student {
    private Integer id;
    private String name;
    private Integer age;
    private String sex;
}

5.1.SpringData ElasticSearch测试
测试类

package com.ancientjazz.springbootes;

import com.ancientjazz.springbootes.bean.Book;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootEsApplicationTests {

    @Autowired
    private BookMapper bookMapper;

    @Test
    public void contextLcads2(){
        Book book = new Book();
        book.setId(1);
        book.setName("西游记");
        bookMapper.index(book);
    }

    @Test
    public void contextLcads3(){
        for (Book book : bookMapper.findByNameLike("西")) {
            System.out.println(book);
        }
    }
}

接口继承ElasticsearchRepository

package com.ancientjazz.springbootes;

import com.ancientjazz.springbootes.bean.Book;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

import java.util.List;

/**
 * @author Ancient Jazz
 * @create 2019-02-16 15:31
 **/
public interface BookMapper extends ElasticsearchRepository<Book,Integer>{
    //自定义查询条件
    List<Book> findByNameLike(String bookName);
}

测试实体类

package com.ancientjazz.springbootes.bean;

import lombok.Data;
import org.springframework.data.elasticsearch.annotations.Document;

/**
 * @author Ancient Jazz
 * @create 2019-02-16 15:28
 **/
@Document(indexName = "ancientjazz",type = "book")
@Data
public class Book {
    private Integer id;
    private String name;
}

项目运用

搜索框from表单提交

<div class th:fragment="html" xmlns:th="http://www.w3.org/1999/xhtml">
    <a th:href="${application.contextPath}">
        <img id="logo" src="img/site/logo.gif" class="logo">
    </a>
    <form action="search" method="get" >
        <div class="searchDiv">
            <input name="keyword" type="text" placeholder="时尚男鞋  太阳镜 ">
            <button  type="submit" class="searchButton">搜索</button>
            <div class="searchBelow">
                <span th:each="c,status: ${application.categories_below_search}" th:if="${status.index>=5 && status.index<=8}">
                    <a th:href="@{'category?cid='+${c.id}}" th:text="${c.name}" ></a>
                    <span th:if="${status.index!=8}">|</span>
                </span>

        </div>
        </div>
    </form>
</div>

1. 通过getUrlParms 拿到keyword参数
2. 然后访问通过 axios.js 访问
3. 获取返回之后显示出来

<div class th:fragment="html" xmlns:th="http://www.w3.org/1999/xhtml">
    <script>
        $(function(){
            var keyword = getUrlParms("keyword");
            console.log(keyword)
            var data4Vue = {
                    uri:'foresearch',
                    products:[]
            };
            //ViewModel
            var vue = new Vue({
                el: '#workingArea',
                data: data4Vue,
                mounted:function(){ //mounted 表示这个 Vue 对象加载成功了
                    this.load();
                },
                methods: {
                    load:function(){
                        var url =  this.uri+"?keyword="+keyword;
                        axios.post(url).then(function(response) {
                            vue.products = response.data;
                            vue.$nextTick(function(){
                                linkDefaultActions();
                            })
                        });
                    }
                }
            });
        })
    </script>

    <div id="searchResult">
        <div class="searchResultDiv">
            <div th:replace="include/fore/productsBySearch::html" ></div>   
        </div>
    </div>
</div>
  1. 获取参数keyword
  2. 根据keyword进行模糊查询,获取满足条件的前20个产品
  3. 为这些产品设置销量和评价数量
  4. 返回这个产品集合
@PostMapping("foresearch")
    public Object search( String keyword){
        if(null==keyword) {
            keyword = "";
        }
        List<Product> ps= productService.search(keyword,0,20);
        productImageService.setFirstProdutImages(ps);
        productService.setSaleAndReviewNumber(ps);
        return ps;
    }

最初是以模糊查询完成的商品搜索功能,ProductService 做了如下改动

package com.tmall.service;

import com.tmall.dao.ProductDAO;
import com.tmall.es.ProductESDAO;
import com.tmall.pojo.Category;
import com.tmall.pojo.Product;
import com.tmall.util.Page4Navigator;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class ProductService  {
    
    @Autowired ProductDAO productDAO;
    @Autowired CategoryService categoryService;
    @Autowired ProductImageService productImageService;
    @Autowired OrderItemService orderItemService;
    @Autowired ReviewService reviewService;
    @Autowired ProductESDAO productESDAO;

    //1. 是增加,删除,修改的时候,除了通过 ProductDAO 对数据库产生影响之外,还要通过 ProductESDAO 同步到 es.
    public void add(Product bean) {
        productDAO.save(bean);
        productESDAO.save(bean);
    }

    public void delete(int id) {
        productDAO.delete(id);
        productESDAO.delete(id);
    }

    public Product get(int id) {
        return productDAO.findOne(id);

    }

    public void update(Product bean) {
        productDAO.save(bean);
        productESDAO.save(bean);
    }

    public Page4Navigator<Product> list(int cid, int start, int size,int navigatePages) {
        Category category = categoryService.get(cid);
        Sort sort = new Sort(Sort.Direction.DESC, "id");
        Pageable pageable = new PageRequest(start, size, sort);     
        Page<Product> pageFromJPA =productDAO.findByCategory(category,pageable);
        return new Page4Navigator<>(pageFromJPA,navigatePages);
    }

    public void fill(List<Category> categorys) {
        for (Category category : categorys) {
            fill(category);
        }
    }
    public void fill(Category category) {
        List<Product> products = listByCategory(category);
        productImageService.setFirstProdutImages(products);
        category.setProducts(products);
    }


    public void fillByRow(List<Category> categorys) {
        int productNumberEachRow = 8;
        for (Category category : categorys) {
            List<Product> products =  category.getProducts();
            List<List<Product>> productsByRow =  new ArrayList<>();
            for (int i = 0; i < products.size(); i+=productNumberEachRow) {
                int size = i+productNumberEachRow;
                size= size>products.size()?products.size():size;
                List<Product> productsOfEachRow =products.subList(i, size);
                productsByRow.add(productsOfEachRow);
            }
            category.setProductsByRow(productsByRow);
        }
    }

    public List<Product> listByCategory(Category category){
        return productDAO.findByCategoryOrderById(category);
    }

    public void setSaleAndReviewNumber(Product product) {
        int saleCount = orderItemService.getSaleCount(product);
        product.setSaleCount(saleCount);


        int reviewCount = reviewService.getCount(product);
        product.setReviewCount(reviewCount);

    }


    public void setSaleAndReviewNumber(List<Product> products) {
        for (Product product : products)
            setSaleAndReviewNumber(product);
    }
    //3. 查询的修改。 以前查询是模糊查询,现在通过 ProductESDAO 到 elasticsearch 中进行查询了。
    public List<Product> search(String keyword, int start, int size) {
        initDatabase2ES();
        FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery()
                //查询条件,但是并未使用,放在这里,为的是将来使用,方便参考,知道如何用
                .add(QueryBuilders.matchPhraseQuery("name", keyword),
                        ScoreFunctionBuilders.weightFactorFunction(100))
                //设置权重分求和模式
                .scoreMode("sum")
                //设置权重分最低分
                .setMinScore(10);

        //设置分页
        Sort sort  = new Sort(Sort.Direction.DESC,"id");
        Pageable pageable = new PageRequest(start, size,sort);
        SearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withPageable(pageable)
                .withQuery(functionScoreQueryBuilder).build();

        Page<Product> page = productESDAO.search(searchQuery);
        return page.getContent();
    }

    //2. 初始化数据到es. 因为数据刚开始都在数据库中,不在es中,所以刚开始查询,先看看es有没有数据,如果没有,就把数据从数据库同步到es中。
    public void initDatabase2ES(){
        PageRequest pageable = new PageRequest(0, 5);
        Page<Product> page = productESDAO.findAll(pageable);
        if(page.getContent().isEmpty()){
            Iterable<Product> products = productESDAO.findAll();
            for (Product product : products) {
                productESDAO.save(product);
            }

        }
    }
}

继承ElasticsearchRepository接口

package com.tmall.es;

import com.tmall.pojo.Product;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

/**
 * @author Ancient Jazz
 * @create 2019-02-17 15:40
 **/
public interface ProductESDAO extends ElasticsearchRepository<Product,Integer>{
}

在 Application 中,为es和jpa分别指定不同的包名,否则会出错

package com.tmall;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
@EnableCaching
@EnableElasticsearchRepositories(basePackages = "com.tmall.es")
@EnableJpaRepositories(basePackages = {"com.tmall.dao","com.tmall.pojo"})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);     
    }
}

application.properties配置

spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300

推荐阅读更多精彩内容