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

·  阅读 3132

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/...
复制代码


1.  $ go list github.com/elastic/go-elasticsearch/v8/...
2.  github.com/elastic/go-elasticsearch/v8
3.  github.com/elastic/go-elasticsearch/v8/esapi
4.  github.com/elastic/go-elasticsearch/v8/estransport
5.  github.com/elastic/go-elasticsearch/v8/esutil
6.  github.com/elastic/go-elasticsearch/v8/internal/version


复制代码

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

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

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

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

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

go doc -short github.com/elastic/go-elasticsearch/v8
复制代码


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


复制代码

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

go doc -short github.com/elastic/go-elasticsearch/v8.Client
复制代码


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

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


复制代码

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

esapi 包

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



1.  res, err := client.Index(
2.    "my-index",
3.    strings.NewReader(`{"title":"Test"}`),
4.    client.Index.WithDocumentID("1"))
5.  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() 方法。 完全可以自己创建实例并调用方法 — 相应的代码如下所示:



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


复制代码

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

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

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

go doc -short github.com/elastic/go-elasticsearch/v8/esapi.Create
复制代码
`

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

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

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

11.  func (f Create) WithContext(v context.Context) func(*CreateRequest)
12.  func (f Create) WithErrorTrace() func(*CreateRequest)
13.  func (f Create) WithFilterPath(v ...string) func(*CreateRequest)
14.  func (f Create) WithHeader(h map[string]string) func(*CreateRequest)
15.  func (f Create) WithHuman() func(*CreateRequest)
16.  func (f Create) WithOpaqueID(s string) func(*CreateRequest)
17.  func (f Create) WithPipeline(v string) func(*CreateRequest)
18.  func (f Create) WithPretty() func(*CreateRequest)
19.  func (f Create) WithRefresh(v string) func(*CreateRequest)
20.  func (f Create) WithRouting(v string) func(*CreateRequest)
21.  func (f Create) WithTimeout(v time.Duration) func(*CreateRequest)
22.  func (f Create) WithVersion(v int) func(*CreateRequest)
23.  func (f Create) WithVersionType(v string) func(*CreateRequest)
24.  func (f Create) WithWaitForActiveShards(v string) func(*CreateRequest)

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
复制代码

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

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

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



1.  client.Reindex(
2.    strings.NewReader(`{...}`),
3.    client.Reindex.WithRequestsPerSecond(100),
4.    client.Reindex.WaitForCompletion(true),
5.  )


复制代码

注意:该包声明了 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
复制代码


1.  $ go doc -short github.com/elastic/go-elasticsearch/v7/estransport.Interface
2.  type Interface interface {
3.  	Perform(*http.Request) (*http.Response, error)
4.  }
5.      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
复制代码


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

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


复制代码

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

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

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

`

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

4.    var filteredConns []*estransport.Connection

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

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

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

`![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
复制代码

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

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

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



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

4.    for _, c := range conns {
5.      if _, ok := c.Attributes["attribute-name"]; ok {
6.        if c.Attributes["attribute-name"] == "attribute-value" {
7.          filteredConns = append(filteredConns, c)
8.        }
9.      }
10.    }

12.    // ...
13.  }


复制代码

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

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

下一步

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

我们将在下一篇文章 “Elasticsearch:运用 Go 语言实现 Elasticsearch 搜索 - 8.x” 中详细介绍如何使用 Golang 代码连接到 Elasticsearch 机器,写入文档并搜索文档。

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改