opensearch-go v5.1.3
UPGRADE NOTE - v5.1.3 changes one transport signature.
opensearchtransport.Transport.LookupShardreturns every copy of a shard. Its signature changes fromLookupShard(index, routing string) (ShardTarget, bool)toLookupShard(index, key string, dst *[]*ShardTarget) error. It writes the shard's copies, primary first and then the ready replicas, into a slice the caller owns, and reports why it cannot answer as an error. Callers ofNewBulkIndexerandClient.Doc.MGetare unaffected.
v5.1.3 cuts the client's allocations and CPU on the bulk and mget paths, moves every request and response onto the encoding/json/v2 engine on Go 1.26 as well as Go 1.27, and raises the default transport's idle connection pool so concurrent requests stop opening new connections. It also adds an opt-in direct driver for DocumentClient.MGet, which splits each mget by shard in the client and sends every node only the documents it holds.
Requires Go 1.26.0 or later.
Full Changelog: v5.1.2...v5.1.3
Highlights
Less client work per document
In a benchmark against a fake 3-node cluster that answers every request at once, so that only the client's own cost is measured, v5.1.3 compared with v5.1.2 as follows:
| Workload | Throughput | CPU per document | Allocations per document | Bytes per document |
|---|---|---|---|---|
BulkIndexer, 500k documents
| 1.2x | -28% | -56% | -32% |
MGet, 1000 documents per call
| 2.0x | -61% | -92% | -49% |
A 1000-document mget also had a 43% lower p50 and a 60% lower p99. Mgets of 100 documents are bound by the wait for each response rather than by client CPU, so their throughput stayed within 7% of v5.1.2's, with 46% less CPU and 87% fewer allocations per document and a 36% lower p99.
Against v5.0.3, v5.1.3 indexed 3.7x as many documents per second with 44% less CPU per document, and read 1000-document mgets at nearly twice the rate.
The benchmark ran on one machine over loopback with 8 concurrent readers, and every build used the same transport with 64 idle connections per node. It is a small, controlled test, not a production workload. The gain on your cluster depends on its size, shard layout, document shape and concurrent load.
Connection reuse under concurrency
The default transport keeps up to 64 idle connections per node and 4096 in total. net/http keeps 2 per node, so with more concurrent requests than that, most requests opened a new connection, and over TLS each one is a full handshake. In the benchmark above with v5.1.2's default transport, a 100-document mget's p50 was about 30% higher and its p99 about twice as high. A Transport you supply is unchanged.
encoding/json/v2 on every toolchain
The client encodes and decodes on the encoding/json/v2 engine on every supported Go. Go 1.27 uses the standard library's encoding/json, which already runs on it. Go 1.26 runs a copy of Go 1.27.3's encoding/json/v2 in internal/third_party/jsonv2, which adds no dependency and goes once the module requires Go 1.27. Semantics and error types stay encoding/json's.
Unions and enums gain MarshalJSONTo and, where the payload decides the branch, UnmarshalJSONFrom. The client's codec uses them to write a union straight into the encoder and to decode it in one pass. A search body of 200 query clauses encodes 22% faster with 80% fewer bytes. MarshalJSON and UnmarshalJSON are unchanged for callers who use them directly.
On Go 1.26 without GOEXPERIMENT=jsonv2, three details change:
- A Go string holding invalid UTF-8 is written with U+FFFD, as Go 1.27 does, rather than as a
\uescape. - A few failures that were untyped errors, such as a
,stringfield holding an unquoted value, are reported as*json.UnmarshalTypeError. - A
*json.MarshalerErrormay name the pointer type of the value whose method failed.
Direct mget driver (opt-in)
Sent as one request, an mget makes the receiving node a coordinator: it splits the documents by shard, asks each shard's node for its share, and waits for the slowest. The direct driver does that split in the client, using the shard map the transport already keeps. It sends each node only the documents it holds, as concurrent requests answered from the node's own copies (preference=_local), and puts the answers back in the order they were asked for. The response, raw body included, is the one the coordinator would have given.
- Select it with
Config.MGet.Driver,OPENSEARCH_GO_MGET=direct|coordinator, or per call withWithMGetDriver.opensearchutil.MGetuses it whatever the configuration says. The default is stillcoordinator. MGetConfig.FanOutchooses the copies it reads.MGetFanOutFewestNodes, the default, reads each mget from as few nodes as hold its shards.MGetFanOutLeastLoadedreads each document from the least loaded copy.- A realtime read of a segment-replicated index goes to the primary. A document the client cannot place, such as one named through an alias, goes through a coordinating node, as does every document of an mget with a
preference,filter_path,formatorsource, a rawBodyReader, orids. - Each node's requests share a window that halves on a 429 and grows back as the node keeps up.
MGetConfig.Observerreports each request and each mget's fan-out, retries and coordinator fallbacks.
The direct driver saves the coordinating node's work, so its gain shows on the cluster rather than in the client.
The opensearchapi.MGetDriver documentation has sequence diagrams of both drivers and a table of the requests and get thread pool tasks each one makes.
Other changes
- Add
NodeStatsIntervaltoopensearch.Config, so the node statistics poll interval can be set in code instead of throughOPENSEARCH_GO_NODE_STATS_INTERVAL(#1237) - Add
opensearch.NewBufferedResponse, which builds aResponsethat holds its body in memory, soRawBodyreturns it (#1236) - Add
opensearchtransport.WithReadNode, which sends a get or mget to a chosen node and accounts it to the node's get thread pool, andConnection.GetPool, which reports that pool's window.ShardTargetgainsReplication, read from each index'sindex.replication.type(#1236) - Decode bulk items and mget documents into storage allocated up to 64 at a time, size an mget's
Docsand a bulk response'sItemsfor the documents requested, read a response with aContent-Lengthinto one buffer of that length, and build query strings without aurl.Values. AGetResultorBulkRespItemkept from a response keeps alive the storage it shares with up to 63 others (#1243) - Write the bulk indexer's action lines and the direct driver's sub-request bodies without reflection when every field is a plain ASCII string, with the same bytes as before (#1240)
Fixes
opensearchtransporttakes a node's thread pool counters on its first statistics poll as a baseline. The counters count from the node's start, so a node that had rejected writes at any time before the client connected was marked overloaded and had its pool window halved, and a queue or wait time already there read as congestion (#1235)- Fix
TestMGetAllocationsandTestSchedulerPublishLoadAllocationsfailing on a busy machine, where the client's background health check and discovery were counted in the test's allocations (#1243)
Breaking changes
opensearchtransport.Transport.LookupShard(index, routing string) (ShardTarget, bool)is nowLookupShard(index, key string, dst *[]*ShardTarget) error. It returns every copy of the shard, primary first, into a slice the caller owns (#1236)
Dependencies
- Bump golangci-lint from v2.14.0 to v2.15.0 (#1236)
See CHANGELOG.md for the full list.
Contributors
Thanks to everyone who contributed to this release: @iamrajiv, @sean- and @ryanyuan.
Thanks to @iamrajiv for the test that runs the bulk indexer over the real transport, which keeps the write pool window from capping the indexer again.