(架构图源于参考书籍)
官方简介:Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例。 作为 Elastic Stack 的核心,它集中存储您的数据,帮助您发现意料之中以及意料之外的情况。
# 下载
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.15.1-linux-x86_64.tar.gz
# 解压
tar xzvf elasticsearch-7.15.1-linux-x86_64.tar.gz
# 以非 root 用户启动
cd /elasticsearch-7.15.1/bin/
./elasticsearch
# 检验是否启动成功,172.16.16.4 为 elasticsearch.yml 配置绑定的 IP 地址
curl 172.16.16.4:9200
若无法正常启动,则修改配置:
/home/sam/elasticsearch-7.15.1/config
修改 jvm.options 中内存配置:
-Xms256m
-Xmx256m
修改 vim elasticsearch.yml :
cluster.name: my-application
node.name: node-1
network.host: 172.16.16.4
http.port: 9200
discovery.seed_hosts: ["172.26.26.4", "::1"]
cluster.initial_master_nodes: ["node-1"]
若出现以下报错,则需修改系统配置:
# 报错
bootstrap check failure [1] of [1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
# 解决
sudo vim /etc/sysctl.conf
# 添加以下内容
vm.max_map_count=262144
# 重载配置
sudo sysctl -p
# 重新启动
sudo systemctl start elasticsearch
# 创建开机启动文件
vim /usr/lib/systemd/system/elasticsearch.service
内容如下:
[Unit]
Description=elasticsearch
[Service]
User=sam #启动用户
LimitNOFILE=100000
LimitNPROC=100000
ExecStart=/home/sam/elasticsearch-7.15.1/bin/elasticsearch #安装路径
[Install]
WantedBy=multi-user.target
# 重新加载文件配置
systemctl daemon-reload
# 设置开机启动
systemctl enable elasticsearch
# 关掉之前启动的 es
lsof -i tcp:9200
kill -9 pid
# 启动 es
systemctl start elasticsearch
# 放行端口
iptables -I INPUT 4 -p tcp -m state --state NEW -m tcp --dport 9200 -j ACCEPT
# 保存 iptables 规则
service iptables save
# 远程测试
curl 公网IP:9200
# github 地址
https://github.com/mobz/elasticsearch-head
# npm 启动方式
git clone git://github.com/mobz/elasticsearch-head.git
cd elasticsearch-head
npm install
npm run start
open http://localhost:9100/
# 搭建 node 环境
# 查看当前有那些可供选择的版本
# dnf module list node.js
# 选择一个版本
# dnf module edable nodejs:14
# 安装 nodejs
dnf install nodejs
# 查看当前的版本
node --version
npm --version
# 进入目录并安装
cd elasticsearch-head
npm install
vim elasticsearch.yml
# 配置跨域
http.cors.enabled: true
http.cors.allow-origin: "*"
# 重启 es 服务
sudo systemctl restart elasticsearch.service
# 放行端口
iptables -I INPUT 4 -p tcp -m state --state NEW -m tcp --dport 9100 -j ACCEPT
# 保存 iptables 规则
service iptables save
# 启动
npm run start
远程访问:
参考:官方 API 文档
创建 metadata 索引以及 objects 类型的映射:
curl -H "Content-Type: application/json" -XPUT 172.16.16.4:9200/metadata?include_type_name=true -d'{"mappings":{"objects":{"properties":{"name":{"type":"text","fielddata": true},"version":{"type":"integer"},"size":{"type":"integer"},"hash":{"type":"text"}}}}}'
在页面可以看到索引和集群信息,这里有一个 Unassigned 的节点是因为创建映射时默认副本为 1 ,而我们使用的是单节点,但是此时服务器依然可以使用。
该 ES 包封装了以 HTTP 访问 ES 的各种 API 的操作。
package es
/* 该 ES 包封装了以 HTTP 访问 ES 的各种 API 的操作 */
import (
"demo/sys"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
)
/* 元数据结构体 */
type Metadata struct {
Name string
Version int
Size int64
Hash string
}
type hit struct {
Source Metadata `json:"_source"`
}
type searchResult struct {
Hits struct {
Total int
Hits []hit
}
}
/*根据对象的名称和版本号来获取元数据*/
func getMetadata(name string, versionId int) (meta Metadata, e error) {
// 索引为 metadata ,类型为 objects,文档 id 为对象名称和版本号的拼接
url := fmt.Sprintf(sys.GetMetadataUrl, os.Getenv(sys.EsServer), name, versionId)
// 通过 GET URL 可以直接获取该对象的元数据,免除了耗时的搜索操作
r, e := http.Get(url)
if e != nil {
return
}
if r.StatusCode != http.StatusOK {
e = fmt.Errorf(sys.FailToGetMetadata, name, versionId, r.StatusCode)
return
}
result, _ := ioutil.ReadAll(r.Body)
// 将请求结果反序列化为元数据结构
json.Unmarshal(result, &meta)
return
}
/*根据对象名称获取最新版本的元数据*/
func SearchLatestVersion(name string) (meta Metadata, e error) {
// 构建 url 时需要将名称转移成 url 字符
url := fmt.Sprintf(sys.SearchLatestVersionUrl, os.Getenv(sys.EsServer), url.PathEscape(name))
r, e := http.Get(url)
if e != nil {
return
}
if r.StatusCode != http.StatusOK {
e = fmt.Errorf(sys.FailToSearchLatestMetadata, r.StatusCode)
return
}
result, _ := ioutil.ReadAll(r.Body)
var sr searchResult
// 请求结果反序列化
json.Unmarshal(result, &sr)
// 如果长度为 0 则没有搜索结果,直接返回
if len(sr.Hits.Hits) != 0 {
meta = sr.Hits.Hits[0].Source
}
return
}
/*根据对象的名称和版本号来获取元数据*/
func GetMetadata(name string, version int) (Metadata, error) {
// 没有指定版本号时默认返回最新版本的元数据
if version == 0 {
return SearchLatestVersion(name)
}
return getMetadata(name, version)
}
/*向 ES 服务上传一个新的元数据*/
func PutMetadata(name string, version int, size int64, hash string) error {
doc := fmt.Sprintf(sys.MetadataJson, name, version, size, hash)
client := http.Client{}
url := fmt.Sprintf(sys.PutMetadataUrl, os.Getenv(sys.EsServer), name, version)
request, _ := http.NewRequest(http.MethodPut, url, strings.NewReader(doc))
// 加入 header ,否则报 406
request.Header.Add("content-type","application/json")
r, e := client.Do(request)
if e != nil {
return e
}
if r.StatusCode == http.StatusConflict {
return PutMetadata(name, version+1, size, hash)
}
if r.StatusCode != http.StatusCreated {
result, _ := ioutil.ReadAll(r.Body)
return fmt.Errorf(sys.FailToPutMetadata, r.StatusCode, string(result))
}
return nil
}
/*版本号加一*/
func AddVersion(name, hash string, size int64) error {
// 获取目前最新的版本
version, e := SearchLatestVersion(name)
if e != nil {
return e
}
// 创建一个最新的版本号
return PutMetadata(name, version.Version+1, size, hash)
}
/*搜索对象的全部版本*/
func SearchAllVersions(name string, from, size int) ([]Metadata, error) {
// 不指定名字时则搜索全部对象的全部版本,指定名字时则搜索某个对象的全部版本
url := fmt.Sprintf(sys.SearchAllVersionsUrl, os.Getenv(sys.EsServer), from, size)
if name != "" {
url += "&q=name:" + name
}
r, e := http.Get(url)
if e != nil {
return nil, e
}
metas := make([]Metadata, 0)
result, _ := ioutil.ReadAll(r.Body)
var sr searchResult
json.Unmarshal(result, &sr)
for i := range sr.Hits.Hits {
metas = append(metas, sr.Hits.Hits[i].Source)
}
return metas, nil
}
/*删除指定的版本*/
func DelMetadata(name string, version int) {
client := http.Client{}
url := fmt.Sprintf(sys.DelMetadataUrl, os.Getenv(sys.EsServer), name, version)
request, _ := http.NewRequest(http.MethodDelete, url, nil)
client.Do(request)
}
type Bucket struct {
Key string
Doc_count int
Min_version struct {
Value float32
}
}
type aggregateResult struct {
Aggregations struct {
Group_by_name struct {
Buckets []Bucket
}
}
}
/*搜索版本状态*/
func SearchVersionStatus(min_doc_count int) ([]Bucket, error) {
client := http.Client{}
url := fmt.Sprintf(sys.SearchVersionStatusUrl, os.Getenv(sys.EsServer))
body := fmt.Sprintf(sys.SearchVersionStatusJson, min_doc_count)
request, _ := http.NewRequest(http.MethodGet, url, strings.NewReader(body))
r, e := client.Do(request)
if e != nil {
return nil, e
}
b, _ := ioutil.ReadAll(r.Body)
var ar aggregateResult
json.Unmarshal(b, &ar)
return ar.Aggregations.Group_by_name.Buckets, nil
}
func HasHash(hash string) (bool, error) {
url := fmt.Sprintf(sys.HasHashUrl, os.Getenv(sys.EsServer), hash)
r, e := http.Get(url)
if e != nil {
return false, e
}
b, _ := ioutil.ReadAll(r.Body)
var sr searchResult
json.Unmarshal(b, &sr)
return sr.Hits.Total != 0, nil
}
func SearchHashSize(hash string) (size int64, e error) {
url := fmt.Sprintf(sys.SearchHashSizeUrl, os.Getenv(sys.EsServer), hash)
r, e := http.Get(url)
if e != nil {
return
}
if r.StatusCode != http.StatusOK {
e = fmt.Errorf(sys.FailToSearchHashSize, r.StatusCode)
return
}
result, _ := ioutil.ReadAll(r.Body)
var sr searchResult
json.Unmarshal(result, &sr)
if len(sr.Hits.Hits) != 0 {
size = sr.Hits.Hits[0].Source.Size
}
return
}
package version
import (
"demo/es"
"encoding/json"
"log"
"net/http"
"strings"
)
/*处理版本搜索*/
func Handler(w http.ResponseWriter, r *http.Request) {
// 非 GET 方法时响应方法不允许
m := r.Method
if m != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// 其实是分页参数,一页最多有 1000 条记录,默认从第 0 条开始往后取数据
// 当返回值的长度不等于 1000 时,则说明后续没有数据了,直接返回
// 当返回值等于 1000 时,说明后续可能有数据, from 则从 1000 条开始往后取数据
from := 0
size := 1000
// 若未指定名字,则切割 URL 之后名字为空字符串
name := strings.Split(r.URL.EscapedPath(), "/")[2]
for {
metas, e := es.SearchAllVersions(name, from, size)
if e != nil {
log.Println(e)
// 服务器内部错误
w.WriteHeader(http.StatusInternalServerError)
return
}
// 遍历结果集
for i := range metas {
// 格式化为 json 返回
b, _ := json.Marshal(metas[i])
w.Write(b)
w.Write([]byte("\n"))
}
if len(metas) != size {
return
}
from += size
}
}
package utils
import (
"crypto/sha256"
"encoding/base64"
"io"
"net/http"
"strconv"
"strings"
)
/*从 header 获取偏移量*/
func GetOffsetFromHeader(h http.Header) int64 {
byteRange := h.Get("range")
if len(byteRange) < 7 {
return 0
}
if byteRange[:6] != "bytes=" {
return 0
}
bytePos := strings.Split(byteRange[6:], "-")
offset, _ := strconv.ParseInt(bytePos[0], 0, 64)
return offset
}
/*从 header 获取散列值*/
func GetHashFromHeader(h http.Header) string {
digest := h.Get("digest")
if len(digest) < 9 {
return ""
}
if digest[:8] != "SHA-256=" {
return ""
}
return digest[8:]
}
/*从 header 获取内容长度*/
func GetSizeFromHeader(h http.Header) int64 {
size, _ := strconv.ParseInt(h.Get("content-length"), 0, 64)
return size
}
/*计算散列值*/
func CalculateHash(r io.Reader) string {
h := sha256.New()
io.Copy(h, r)
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
package objects
import (
"demo/apiServer/heartbeat"
"demo/apiServer/locate"
"demo/apiServer/objectStream"
"demo/es"
"demo/sys"
"demo/utils"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
)
/*接口服务的 PUT 和 GET 请求是将 HTTP 请求转发到数据服务,实际上是调用数据服务的 PUT 和 GET 方法*/
func Handler(w http.ResponseWriter, r *http.Request) {
m := r.Method
// PUT 方法时,创建或者替换资源
if m == http.MethodPut {
put(w, r)
return
}
// GET 方法时,获取资源
if m == http.MethodGet {
get(w, r)
return
}
// 版本删除
if m == http.MethodDelete {
del(w,r)
return
}
// 其他方式时,返回状态码,方法不允许
w.WriteHeader(http.StatusMethodNotAllowed)
}
/*处理接口服务 PUT 请求*/
func put(w http.ResponseWriter, r *http.Request) {
// 按以前的步骤,这里应该获取存储对象名字,不过从 header 中取对象的散列值作为名字
hash := utils.GetHashFromHeader(r.Header)
if hash == "" {
log.Println(sys.MissingObjectHash)
w.WriteHeader(http.StatusBadRequest)
return
}
// 存储请求数据,散列值要作转义
httpStatus, e := storeObject(r.Body, url.PathEscape(hash))
if e != nil {
log.Println(e)
w.WriteHeader(httpStatus)
return
}
if httpStatus != http.StatusOK {
w.WriteHeader(httpStatus)
return
}
// 获取名字和大小,新增一个对象版本
name := strings.Split(r.URL.EscapedPath(), "/")[2]
size := utils.GetSizeFromHeader(r.Header)
e = es.AddVersion(name, hash, size)
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusInternalServerError)
}
// 返回结果
w.WriteHeader(httpStatus)
}
func storeObject(r io.Reader, obj string) (int, error) {
// 获取接口服务节点存储对象的流
stream, e := putStream(obj)
if e != nil {
return http.StatusServiceUnavailable, e
}
// 将请求数据体拷贝到流 stream
io.Copy(stream, r)
// 关闭流
e = stream.Close()
if e != nil {
return http.StatusInternalServerError, e
}
// 返回成功状态码
return http.StatusOK, nil
}
func putStream(obj string) (*objectStream.PutStream, error) {
// 随机选择一个数据服务节点
server := heartbeat.ChooseRandomDataServer()
// 若没有可用的数据服务节点则返回错误信息
if server == "" {
return nil, fmt.Errorf(sys.DataServerNotFound)
}
// 返回数据服务节点存储对象的流
return objectStream.NewPutStream(server, obj), nil
}
/*处理接口服务 GET 请求*/
func get(w http.ResponseWriter, r *http.Request) {
// 获取存储对象名称和版本号
name := strings.Split(r.URL.EscapedPath(), "/")[2]
versionId := r.URL.Query()["version"]
version := 0
var e error
if len(versionId) != 0 {
// 版本号字符串转数字
version, e = strconv.Atoi(versionId[0])
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusBadRequest)
return
}
}
// 根据名字和版本号来获取元数据
meta, e := es.GetMetadata(name, version)
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusInternalServerError)
return
}
// 元数据散列值为空则无该对象
if meta.Hash == "" {
w.WriteHeader(http.StatusNotFound)
return
}
// 散列值要作 URL 转移
object := url.PathEscape(meta.Hash)
// 根据散列值获取对象数据
stream, e := getStream(object)
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusNotFound)
return
}
// 将数据流拷贝到响应流 w
io.Copy(w, stream)
}
func getStream(obj string) (io.Reader, error) {
// 根据存储对象名称进行定位
server := locate.Locate(obj)
// 未找到该存储对象时返回定位失败错误
if server == "" {
return nil, fmt.Errorf(sys.DataServerLocateFail, obj)
}
// 定位到存储对象时,返回该对象的数据流
return objectStream.NewGetStream(server, obj)
}
/*处理接口服务 DELETE 请求*/
func del(w http.ResponseWriter, r *http.Request) {
// 获取名字
name := strings.Split(r.URL.EscapedPath(),"/")[2]
v,e := es.SearchLatestVersion(name)
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusInternalServerError)
return
}
// 插入一条新的元数据作删除标记
e = es.PutMetadata(name,v.Version + 1,0,"")
if e != nil {
log.Println(e)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
主要是散列值进行 URL 转义,传参时注意这点,否则找不到文件。如果这里不作处理也行,可以由前端传参时进行转义。若散列值有斜杠则必须先转义,再做 GET 请求,因为取文件名时是切割 URL 取第三个值作为名字。
package locate
import (
"demo/rabbitmq"
"demo/sys"
"log"
"net/url"
"os"
"strconv"
)
// 定位对象
func Locate(name string) bool {
// 访问磁盘上对应的文件名
_, e := os.Stat(name)
// 判读文件名是否存在
return !os.IsNotExist(e)
}
// 监听定位信息
func StartLocate() {
q := rabbitmq.New(os.Getenv(sys.RabbitmqServer))
defer q.Close()
// 绑定 data 网络层
q.Bind(sys.DataServersExchange)
// 获取信息管道
c := q.Consume()
// 从管道中遍历信息,msg 为需要定位的存储对象名字
for msg := range c {
// 去掉 json 序列化的双引号
obj, e := strconv.Unquote(string(msg.Body))
if e != nil {
log.Fatalln(e)
}
// 存储根目录拼接文件名,定位存储对象,名字需要 URL 转义
if Locate(os.Getenv(sys.StorageRoot) + url.PathEscape(obj)) {
// 如果存储对象存在,则回送本节点监听地址,已告知存储对象在该节点
q.Send(msg.ReplyTo, os.Getenv(sys.ListenAddress))
}
}
}
package main
import (
"demo/apiServer/heartbeat"
"demo/apiServer/locate"
"demo/apiServer/objects"
"demo/apiServer/version"
"demo/sys"
"net/http"
"os"
)
func main() {
// 监听数据服务节点心跳
go heartbeat.ListenHeartbeat()
// 处理对象请求,实际上是将对象请求转发给数据服务
http.HandleFunc("/handleObjs/", objects.Handler)
// 处理定位请求
http.HandleFunc("/locateObj/", locate.Handler)
// 处理版本信息
http.HandleFunc("/versions/",version.Handler)
// 启动并监听服务
http.ListenAndServe(os.Getenv(sys.ListenAddress), nil)
}
# 查看本机网络接口
ip a
# 数据服务节点 eth0:1~6
# IP范围 172.16.17.1 ~ 172.16.17.6
# 接口服务节点 eth0:7~8
# IP范围 172.16.18.1 ~ 172.16.18.2
# 网络接口绑定多个 IP
ifconfig eth0:1 172.16.17.1/20
ifconfig eth0:2 172.16.17.2/20
ifconfig eth0:3 172.16.17.3/20
ifconfig eth0:4 172.16.17.4/20
ifconfig eth0:5 172.16.17.5/20
ifconfig eth0:6 172.16.17.6/20
ifconfig eth0:7 172.16.18.1/20
ifconfig eth0:8 172.16.18.2/20
# rabbitmq-server 变量
export RABBITMQ_SERVER=amqp://yushanma:[email protected]:5672
# es-server 变量
export ES_SERVER=172.16.16.4:9200
# 启动数据服务节点
LISTEN_ADDRESS=172.16.17.1:12345 STORAGE_ROOT=/home/sam/files/1/objects/ go run dataServer/cmd/main.go &
LISTEN_ADDRESS=172.16.17.2:12345 STORAGE_ROOT=/home/sam/files/2/objects/ go run dataServer/cmd/main.go &
LISTEN_ADDRESS=172.16.17.3:12345 STORAGE_ROOT=/home/sam/files/3/objects/ go run dataServer/cmd/main.go &
LISTEN_ADDRESS=172.16.17.4:12345 STORAGE_ROOT=/home/sam/files/4/objects/ go run dataServer/cmd/main.go &
LISTEN_ADDRESS=172.16.17.5:12345 STORAGE_ROOT=/home/sam/files/5/objects/ go run dataServer/cmd/main.go &
LISTEN_ADDRESS=172.16.17.6:12345 STORAGE_ROOT=/home/sam/files/6/objects/ go run dataServer/cmd/main.go &
# 启动接口服务节点
LISTEN_ADDRESS=172.16.18.1:12346 go run apiServer/cmd/main.go &
LISTEN_ADDRESS=172.16.18.2:12346 go run apiServer/cmd/main.go &
选择第一个服务节点 172.16.18.1:12346 , PUT 一个名为 hello 的对象:
curl -v 172.16.18.1:12346/handleObjs/hello -XPUT -d"hello,yushanma"
因为我们没有提供散列值,因此会报 400 错误。我们可以通过 openssl 计算出这个对象的散列值:
echo -n "hello,yushanma" | openssl dgst -sha256 -binary | base64
将该散列值加入 PUT 请求的 header :
curl -v 172.16.18.1:12346/handleObjs/hello -XPUT -d"hello,yushanma" -H "digest:SHA-256=Ziupvid3V+SN3UJU3RjhrU3rZqykZPIN1JJCmT31vuo="
OK,我们已经上传了一个 hello 对象,也是第一个版本,接下来换第二个接口服务节点 172.16.18.2:12346 上传 hello 第二个版本:
# 计算散列值
echo -n "hello,yushanma,shirley" | openssl dgst -sha256 -binary | base64
YYmSQFej7Zs/82P2ZFe1NE8dtZQI1NCYOnlRj/EQi6w=
curl -v 172.16.18.1:12346/handleObjs/hello -XPUT -d"hello,yushanma,shirley" -H "digest:SHA-256=YYmSQFej7Zs/82P2ZFe1NE8dtZQI1NCYOnlRj/EQi6w="
至此,我们上传了两个 hello 对象,接下来用定位接口去查看它们分别被保存到哪个数据服务节点上:
Jack Ma@DESKTOP-L24D7IP MINGW64 ~/Desktop
$ echo -n "hello,yushanma" | openssl dgst -sha256 -binary | base64
Ziupvid3V+SN3UJU3RjhrU3rZqykZPIN1JJCmT31vuo=
Jack Ma@DESKTOP-L24D7IP MINGW64 ~/Desktop
$ echo -n "hello,yushanma,shirley" | openssl dgst -sha256 -binary | base64
YYmSQFej7Zs/82P2ZFe1NE8dtZQI1NCYOnlRj/EQi6w=
curl 172.16.18.1:12346/locateObj/Ziupvid3V+SN3UJU3RjhrU3rZqykZPIN1JJCmT31vuo=
curl 172.16.18.2:12346/locateObj/YYmSQFej7Zs%2F82P2ZFe1NE8dtZQI1NCYOnlRj%2FEQi6w=
能在对应的存储位置找到这两个文件。下面查看 hello 文件的版本:
curl 172.16.18.1:12346/versions/hello
可以看到有两个版本的 hello ,我们尝试 GET 请求 hello :
# 不传参数 version 时默认获取最新版本
curl 172.16.18.2:12346/handleObjs/hello?version=2
可以看到 GET 请求响应符合预期,最后尝试删除 hello :
curl -v 172.16.18.2:12346/handleObjs/hello -XDELETE
可以看到,删除时是将 hello 的最新版本标记为删除状态,大小为 0 ,没有散列值,再次请求最新的 hello 版本时响应 404 not found ,符合预期。但我们依然可以通过指定版本号来获取存在的 hello 。