Elasticsearch:Go 客户端简介 - 8.x

news/2024/6/3 6:15:11 标签: elasticsearch, 大数据

Elasticsearch 的官方 Go 客户端是由 Elastic 开发、维护和支持的客户端系列的最新成员之一。 初始版本于 2019 年初发布,并在过去一年中逐渐成熟,获得了重试请求、发现集群节点和各种辅助组件等功能。 我们还提供了全面的示例,以方便使用客户端。

在本系列中,我们将探讨 Go 客户端的架构和设计,重点介绍具体的实现细节,并提供示例和使用指南。

在这篇博文中,我们将重点关注客户端的整体架构以及包和存储库布局。

值得指出的是,在使用 golang 进行客户端开发时,开发者也可以选择 GitHub - olivere/elastic: Deprecated: Use the official Elasticsearch client for Go at https://github.com/elastic/go-elasticsearch 这个开源项目,但是由于这个项目不能承诺持续的维护性,现在的很多开发者转而选择 Elasticsearch 的官方 Go 客户端。

客户端架构

每个 Elasticsearch 客户端库都包含很多功能。 因此,为了便于维护和开发,需要适当地分离关注点。 从鸟瞰的角度来看,Elasticsearch 客户端有两个主要问题:

  1. 以相应的编程语言公开 Elasticsearch API
  2. 从集群发送和接收数据

自然,画面在细节上更复杂(数据发送和接收到底是如何进行的?数据是如何暴露给调用代码的?)但整体画面很简单。

elasticsearch 包使这种关注点分离变得透明:它在物理上将两层分离到不同的包中,并使用一个伞形包将它们联系在一起。 (此模式也在 Ruby 客户端实现。)

go list github.com/elastic/go-elasticsearch/v8/...
$ go list github.com/elastic/go-elasticsearch/v8/...
github.com/elastic/go-elasticsearch/v8
github.com/elastic/go-elasticsearch/v8/esapi
github.com/elastic/go-elasticsearch/v8/estransport
github.com/elastic/go-elasticsearch/v8/esutil
github.com/elastic/go-elasticsearch/v8/internal/version

esapiestransport 包直接对应了上面提到的两个关注点。

这对客户端的可维护性、可扩展性和灵活性具有重大影响。 首先,将代码、测试和其他只与一个关注点相关的实验分开是很简单的; 如果你查看 estransport 单元和集成测试,很明显它们根本不关心 Elasticsearch API。 其次,只使用一个单独的包很简单:例如,只使用与 API 相关的包。 一个常见的问题是:“为什么有人会那样做?” 正确答案退后一步:程序包不想做出阻止用户实现特定目标的决定,无论该目标多么罕见或不寻常。

这种思维轨迹说明了官方 Elasticsearch 客户端的总体方法:提供最佳的开箱即用体验,但不要阻止用户以无法预料的方式修改库。 多年来,我们喜欢看到人们使用各种客户端扩展点在深奥的环境中实现特定目标。 例如,今年早些时候,The Guardian 发表了一篇关于用于Java 客户端集群节点发现的自定义组件的文章。

注意:你可能想知道为什么这些包没有简单地命名为 apitransport。 其动机是防止从用户那里 “窃取” 合法的包或变量名 — 毕竟,名为 api 的包或变量是经常发生的事情。

那么这些包是如何捆绑在一起的呢? 这是 elasticsearch 包的责任。

go doc -short github.com/elastic/go-elasticsearch/v8
$ go doc -short github.com/elastic/go-elasticsearch/v8
const Version = version.Client
type Client struct{ ... }
    func NewClient(cfg Config) (*Client, error)
    func NewDefaultClient() (*Client, error)
type Config struct{ ... }

这个包的占用空间很小,它最重要的组件是 ClientConfig 类型; 后者提供了一种配置客户端的方法,前者嵌入了 Elasticsearch API 和 HTTP 传输。

go doc -short github.com/elastic/go-elasticsearch/v8.Client
$ go doc -short github.com/elastic/go-elasticsearch/v8.Client
type Client struct {
	*esapi.API // Embeds the API methods
	Transport  estransport.Interface
}
    Client represents the Elasticsearch client.

func NewClient(cfg Config) (*Client, error)
func NewDefaultClient() (*Client, error)
func (c *Client) DiscoverNodes() error
func (c *Client) Metrics() (estransport.Metrics, error)
func (c *Client) Perform(req *http.Request) (*http.Response, error)

该包导出客户端初始化方法:NewDefaultClient()NewClient()。 我们将在下一篇博文中重点介绍客户端的配置和定制。

esapi 包

esapi 包通过 Go 编程语言数据类型提供对 Elasticsearch API 的访问。 例如,要索引文档,你可以在客户端调用相应的方法:

res, err := client.Index(
  "my-index",
  strings.NewReader(`{"title":"Test"}`),
  client.Index.WithDocumentID("1"))
fmt.Println(res, err)

Go 包提供与其他语言的客户端相同的 API,允许跨编程语言的一致用户体验,并促进多语言团队的沟通。 因此,Elasticsearch API 的各种命名空间可用作客户端上的命名空间。 例如,要检查集群健康状况,你可以在客户端调用 Cluster.Health() 方法; 要创建索引,你可以调用 Indices.Create() 方法。

该方法返回 esapi.Response 和 error。 后者在请求失败时返回; 例如,当端点无法访问或请求超时时。 esapi.Response 类型是 *http.Response 的轻量级包装器。 除了公开响应状态、标头和正文之外,它还提供了一些辅助方法,例如 IsError()。 请注意,500 Internal Server Error 响应仍然是有效响应,因此在这种情况下不会返回任何错误 — 调用代码必须检查响应状态才能正确处理响应。 esapi.Response 类型还实现了 fmt.Stringer 接口,以允许在开发和调试期间打印响应,如上例所示。

如果仔细观察,你会发现方法调用创建了 IndexRequest 结构的新实例,并调用了它的 Do() 方法。 完全可以自己创建实例并调用方法 — 相应的代码如下所示:

req := esapi.IndexRequest{
  Index:      "my-index",
  DocumentID: "1",
  Body:       strings.NewReader(`{"title":"Test"}`),
}
req.Do(context.Background(), client)

两种变体在功能上是等效的。 面向方法的 API 的开销几乎可以忽略不计,但也有一定的优势。

首先,它可以说更易于人类阅读和编写,因为代码流更流畅,命名更简洁,界面更紧凑。

同样重要的是,它清楚地定义了哪些 API 参数是必需的(在上面的示例中,它们是索引名称和 JSON 有效负载)以及哪些是可选的(文档 ID)。 与 Create() API 进行比较,后者在方法签名中明确表示需要文档 ID:

go doc -short github.com/elastic/go-elasticsearch/v8/esapi.Create
$ go doc -short github.com/elastic/go-elasticsearch/v8/esapi.Create
type Create func(index string, id string, body io.Reader, o ...func(*CreateRequest)) (*Response, error)
    Create creates a new document in the index.

    Returns a 409 response when a document with a same ID already exists in the
    index.

    See full documentation at
    https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-index_.html.

func (f Create) WithContext(v context.Context) func(*CreateRequest)
func (f Create) WithErrorTrace() func(*CreateRequest)
func (f Create) WithFilterPath(v ...string) func(*CreateRequest)
func (f Create) WithHeader(h map[string]string) func(*CreateRequest)
func (f Create) WithHuman() func(*CreateRequest)
func (f Create) WithOpaqueID(s string) func(*CreateRequest)
func (f Create) WithPipeline(v string) func(*CreateRequest)
func (f Create) WithPretty() func(*CreateRequest)
func (f Create) WithRefresh(v string) func(*CreateRequest)
func (f Create) WithRouting(v string) func(*CreateRequest)
func (f Create) WithTimeout(v time.Duration) func(*CreateRequest)
func (f Create) WithVersion(v int) func(*CreateRequest)
func (f Create) WithVersionType(v string) func(*CreateRequest)
func (f Create) WithWaitForActiveShards(v string) func(*CreateRequest)

这对于在其 IDE 和编辑器中使用代码完成的开发人员特别有用。

面向方法的 API 提供的另一个便利与接受布尔值和数值的参数有关。 因为在 Go 中所有类型都有一个默认值,所以包无法判断调用代码是否设置了 false 值,或者它是否只是 bool 类型的默认值; int 类型和值为 0 时也存在类似的问题。这在 Go 中通常通过接受指向该值的指针作为参数来解决,但这会使调用代码相当冗长。

因此,不必声明一个变量并将指针传递给它,调用代码只需将值传递给适当的方法,它就会自动创建指针:

client.Reindex(
  strings.NewReader(`{...}`),
  client.Reindex.WithRequestsPerSecond(100),
  client.Reindex.WaitForCompletion(true),
)

注意:该包声明了 esapi.BoolPtr()esapi.IntPtr() 辅助函数,以便在直接使用请求结构时更容易使用接受指针的字段。

Elasticsearch 有超过 300 个不同的、不断发展的 API,手动管理代码库几乎是不可能的 — 唯一可持续的维护模式是完全生成的代码库。 幸运的是,Elasticsearch 存储库包含每个 API 的综合规范,作为 JSON 文档的集合。 这是确保客户端一致性并跟上 Elasticsearch API 发展的主要工具。

Go 生成器是内部包集合的一部分,将 API 定义从 JSON 转换为 Go 源代码,生成单独的文件,使用 gofmt 格式化它们,并生成带有 “map” 的 esapi.API 类型的文件所有的 API,在 Go 中使用反射。然后将这种类型嵌入到客户端中。

Elasticsearch 存储库也包含相应的资源:一组集成测试,编码为 YAML 文件。 Go 客户端的另一个内部包将 YAML 定义转换为常规的 Go 测试文件。 这些测试在持续集成环境中定期运行,捕获任何缺失的 API 或参数。

如上所述,Elasticsearch 客户端的一个关注点是发送和接收数据,默认情况下是 JSON 负载。 Go 客户端将请求和响应主体简单地公开为 io.Reader,将任何编码和解码留给调用代码。 这种实施有多种原因。 首先,对于极其多变的 API 负载没有正式的规范,因此代码生成是不可能的。

另一个原因与性能和可伸缩性有关。 通过将编码和解码留给调用代码,它和客户端之间有一个清晰的界限,使性能推理更容易。 根据经验,JSON 编码和解码对客户端的性能影响最大,通常高于网络传输的成本。 从另一个角度来看,通过将编码和解码卸载到调用代码,可以直接使用第三方 JSON 包,这在大多数情况下都大大超过了标准库。 要查看各种 JSON 包的示例和基准,请参阅存储库中的 _examples/encoding 文件夹。

注意:为了更轻松地将自定义负载传递给 API,客户端提供了 esutil.JSONReader 类型。 我们将在本系列的下一篇文章中重点介绍 esutil 包。

estransport 

到目前为止,我们一直关注客户端的首要职责:将 Elasticsearch API 公开为 Go 编程语言中的类型。 让我们将注意力转移到负责通过网络发送和接收数据的组件:estransport 包。

该包暴露的主要类型是 estransport.Client,它实现了estransport.Interface。 它定义了一个方法 Perform(),它接受一个 *http.Request 并返回一个 *http.Response

go doc -short github.com/elastic/go-elasticsearch/v7/estransport.Interface
$ go doc -short github.com/elastic/go-elasticsearch/v7/estransport.Interface
type Interface interface {
	Perform(*http.Request) (*http.Response, error)
}
    Interface defines the interface for HTTP client.

此方法的默认实现是传输组件的核心,不仅处理发送和接收数据,还处理连接池管理、重试请求、存储客户端指标、记录操作等。 让我们仔细看看。

在客户端可以向集群发送请求之前,它首先需要知道将请求发送到哪里。 对于本地开发,这非常简单:你只需保留默认值 (http://localhost:9200),或使用单个地址配置客户端。 在这种情况下,使用 “单个” 连接池,它只返回一个连接。 在 Elastic Cloud 上使用 Elasticsearch Service 时也是如此,它只为集群提供单个端点,因为它有自己的负载平衡逻辑。 这同样适用于负载均衡器或代理后面的任何集群。

但是,本地生产集群与多个节点一起运行,并且仅将请求发送到单个节点并不是最佳选择,这使其成为瓶颈。 这就是包导出 estransport.ConnectionPool 接口的原因,该接口允许从列表中选择连接:

go doc -short github.com/elastic/go-elasticsearch/v7/estransport.ConnectionPool
$ go doc -short github.com/elastic/go-elasticsearch/v7/estransport.ConnectionPool
type ConnectionPool interface {
	Next() (*Connection, error)  // Next returns the next available connection.
	OnSuccess(*Connection) error // OnSuccess reports that the connection was successful.
	OnFailure(*Connection) error // OnFailure reports that the connection failed.
	URLs() []*url.URL            // URLs returns the list of URLs of available connections.
}
    ConnectionPool defines the interface for the connection pool.

func NewConnectionPool(conns []*Connection, selector Selector) (ConnectionPool, error)

每当客户端配置了多个集群地址时,就会使用 “status” 连接池实现,它保留健康和不健康连接的列表,并具有检查不健康连接是否再次健康的机制。 与客户端的一般扩展性一致,需要时可以在配置中传递连接池的自定义实现。

“status” 连接池的 Next() 方法委托给另一个接口:estransport.Selector,具有循环选择器的默认实现,这通常是跨集群节点分布负载的最有效方式。 同样,当在复杂的网络拓扑中需要更复杂的连接选择机制时,可以在配置中传递选择器的自定义实现。

例如,一个简化的 “hostname” 选择器实现可能如下所示:

func (s *HostnameSelector) Select(conns []*estransport.Connection) (*estransport.Connection, error) {
  // Access locking ommited

  var filteredConns []*estransport.Connection

  for _, c := range conns {
    if strings.Contains(c.URL.String(), "es1") {
      filteredConns = append(filteredConns, c)
    }
  }

  if len(filteredConns) > 0 {
    s.current = (s.current + 1) % len(filteredConns)
    return filteredConns[s.current], nil
  }

  return nil, errors.New("No connection with hostname [es1] available")
}

然而,自定义选择器对于客户端的另一个功能更有用:发现集群中节点的能力,通俗地称为“ 嗅探(sniffing)”。 它使用节点信息 API来检索有关集群中节点的信息,并根据集群状态动态更新客户端配置。 例如,这允许你将客户端仅指向集群协调节点,并让它自动发现数据或摄取节点。 (阅读相应博客文章中有关该功能的更多信息。)

Go 客户端公开了一个方法 DiscoverNodes() 以手动执行操作,并公开了 DiscoverNodesIntervalDiscoverNodesOnStart 配置选项以在客户端初始化时定期执行操作。 除了获取节点列表并使客户端配置动态化之外,它还存储附加到节点的元数据,例如角色或属性。

回到上面提到的 The Guardian 报告的用例,解决这个问题意味着提供一个自定义选择器,该选择器将过滤连接的 Attributes 字段以获得特定值,对应于在特定可用性区域中运行的特定客户端实例:

func (s *AttributeSelector) Select(conns []*estransport.Connection) (*estransport.Connection, error) {
  // ...

  for _, c := range conns {
    if _, ok := c.Attributes["attribute-name"]; ok {
      if c.Attributes["attribute-name"] == "attribute-value" {
        filteredConns = append(filteredConns, c)
      }
    }
  }

  // ...
}

注意:需要再次强调的是,只有当客户端直接连接到集群时,节点发现才有用,而不是当集群在代理后面时,在 Elastic Cloud 上使用 Elasticsearch Service 时也是如此。

传输组件的另一个有用特性是重试失败请求的能力,这在 Elasticsearch 等分布式系统中尤为重要。 默认情况下,当它收到网络错误或状态代码为 502503504 的 HTTP 响应时,它最多重试请求 3 次。重试次数和 HTTP 响应代码以及可选的退避延迟是可配置的。 可以完全禁用此功能。

下一步

正如我们所见,发送请求和获得响应的简单操作实际上非常复杂。 为了了解到底发生了什么,客户端提供了多种类型的日志记录组件,从对本地开发有用的彩色记录器到适用于生产的基于 JSON 的记录器,以及标准化的指标输出。

我们将在下一篇文章中详细介绍如何使用 Golang 代码连接到 Elasticsearch 机器,写入文档并搜索文档。敬请期待!


http://www.niftyadmin.cn/n/25839.html

相关文章

第一行代码Androiod第三版 笔记 第九章丰富你的程序,运用手机多媒体

文章目录前言一、通知渠道是什么?二、快速入门1. 书写通知2.通知有了 ,点击也没动静啊- PendingIntent3 点击完之后,通知不消失怎么办4 通过setStyle() 来是实现长文字通知5 还想放张图6 不同通知之间也有等级差异三、 使用相机7 调用相机8 从…

Hi3861鸿蒙物联网项目实战:智能安防报警

华清远见FS-Hi3861开发套件,支持HarmonyOS 3.0系统。开发板主控Hi3861芯片内置WiFi功能,开发板板载资源丰富,包括传感器、执行器、NFC、显示屏等,同时还配套丰富的拓展模块。开发板配套丰富的学习资料,包括全套开发教程…

【Maven自动化构建工具】 | 项目管理工具

目录 第1章:Maven简介 1. 传统项目开发存在的问题 2. Maven 概述 3. Maven核心概念 4. 安装 Maven 环境 第2 章 Maven 的核心概念 1. Maven 工程约定目录结构 2. 仓库概念 3. POM文件 4. 坐标 5. 依赖 6. Maven的生命周期、命令和插件 第 3 章 Maven…

C++11之后的decltype类型指示符

C11之后的decltype类型指示符一、什么是decltype类型指示符二、typeid运算符三、使用decltype指示符四、decltype和引用五、decltype(auto)六、本章代码汇总一、什么是decltype类型指示符 有时会遇到这种情况:希望从表达式的类型推断出要定义的变量的类型&#xff…

Netty源码性能分析 - ThreadLocal PK FastThreadLocal

序 既然jdk已经有ThreadLocal,为何netty还要自己造个FastThreadLocal?FastThreadLocal快在哪里?这需要从jdk ThreadLocal的本身说起。在java线程中,每个线程都有一个ThreadLocalMap实例变量(如果不使用ThreadLocal&…

Runtime、ProcessBuilder的区别(Java中,两种方法来启动其他程序)

目录 ■Runtime、ProcessBuilder 区别: ■Java中提供了两种方法来启动其他程序 ■代码 ・Runtime ・ProcessBuilder ■类的方法 ・Process.waitFor()方法 ・Process.getErrorStream()方法 ・Process.redirectErrorStream(true)方法: ■可运行代码 ・java…

【计算机网络】网络基础

目录前言一、计算机网络发展二、初识“协议”1. 协议的概念2. 协议分层三、OSI七层模型四、TCP/IP五层(四层)模型五、网络传输基本流程1. 网络传输流程图2.数据包封装和分用六、网络中的地址管理1. IP地址2. MAC地址前言 本文是博主首次学习网络知识后进行的总结,文…

Java学习(60)Java多态——向上转型与向下转型

Java多态——向上转型与向下转型向上转型1. 案例2. 向上转型的特点向下转型1. 案例2. 向下转型的特点3. 必须满足转型条件才能进行强转向上转型 1. 案例 public abstract class Animal {//方法:吃东西public void eat("动物都有吃东西的能力"); }public…