Skip to content

[Bug][HStore][Kubernetes] Data writes remain unavailable after Store pod replacement due to stale DNS resolution #3124

Description

@bitflicker64

HugeGraph version

Current master, verified at commit b9710a7b0319ace41dd3c4ff5fb1165f53ee538e.

Backend and topology:

  • HStore backend
  • 3 PD nodes
  • 3 Store nodes
  • 3 Server nodes
  • Kubernetes deployment using stable per-Store DNS names

Problem

Replacing a Store pod during a rolling update can leave running HugeGraph Server pods permanently unable to write data to that Store.

The replacement Store pod has the same stable DNS name and Store node ID, but a different pod IP. PD reports the Store as Up with the correct node ID and address. The replacement Store is reachable from the Server pod, but HStore data writes continue to use the failed transport until the Server process is restarted.

This makes routine Store upgrades, node drains, and pod rescheduling capable of taking the graph data plane out of service while the Server pods remain Ready.

Observed behavior

After replacing a Store pod, every affected data write returned HTTP 500 after the HStore retry loop:

HgStoreClientException {sessionInfo: {storeNodeSession: {storeNode:
  {address: "hugegraph-store-1.hugegraph-store.<namespace>.svc:8500",
   nodeId: 2554185034686971778},
   graphName: "DEFAULT/hugegraph/g"}},
   reason: "UNAVAILABLE: io exception"}

Relevant stack frames:

NodeTxExecutor.retryingInvoke(NodeTxExecutor.java:404)
NodeTxExecutor.doCommit(NodeTxExecutor.java:120)
NodeTxExecutor.commitTx(NodeTxExecutor.java:100)
NodeTxSessionProxy.commit(NodeTxSessionProxy.java:110)
HstoreSessionsImpl$HstoreSession.commit(HstoreSessionsImpl.java:464)
HstoreStore.commitTx(HstoreStore.java:691)

At the time of failure:

  • PD reported all Stores as Up.
  • PD returned the same node ID shown in the exception.
  • hugegraph-store-1...:8500 accepted TCP connections from inside the Server pod.
  • The failure did not recover after more than six minutes.
  • Restarting the Server pods restored writes immediately; the same request returned HTTP 201.
  • Schema operations continued to work because they did not exercise this HStore commit path.
  • /versions and /graphs continued responding, so the Server remained Ready and continued receiving traffic.

Expected behavior

After a Store pod is replaced behind the same DNS name, existing Server processes should discard or recover the failed Store transport, resolve the current address, and resume writes without requiring a Server restart.

Source analysis

1. The normal container path enables a SecurityManager

bin/start-hugegraph.sh defaults to:

OPEN_SECURITY_CHECK="true"

The normal Server Docker entrypoint invokes:

./bin/start-hugegraph.sh -j "${JAVA_OPTS:-}" -t 120

It does not pass -s, so the default is retained. bin/hugegraph-server.sh then adds:

-Djava.security.manager=org.apache.hugegraph.security.HugeSecurityManager

The HStore Server image currently uses eclipse-temurin:11-jre-jammy.

There is no networkaddress.cache.ttl or sun.net.inetaddr.ttl setting in the packaged bin/ or conf/ files.

Under Java 11, when a SecurityManager is installed and no positive DNS TTL is explicitly configured, successful InetAddress resolutions are cached forever (-1). This behavior is specified by Java's InetAddress documentation:

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/InetAddress.html

The corresponding OpenJDK implementation is here:

https://github.com/openjdk/jdk11u/blob/master/src/java.base/share/classes/sun/net/InetAddressCachePolicy.java

The JDK uses this infinite default as protection against DNS spoofing or rebinding after an initial trusted resolution. HugeGraph does not explicitly select or document this DNS policy; it is an incidental consequence of enabling a SecurityManager to sandbox Gremlin execution.

2. HStore channels and stubs are retained by target address

AbstractGrpcClient stores channels in a static map keyed by the target string:

protected static Map<String, ManagedChannel[]> channels = new ConcurrentHashMap<>();

Channels are constructed with:

ManagedChannelBuilder.forTarget(target).usePlaintext().build();

No Store-client path removes or shuts down these cached channels when a transport call fails.

The separate channel-pool indexing defect in this class should be reported independently so it does not get buried in this DNS lifecycle issue.

Source:

https://github.com/apache/hugegraph/blob/b9710a7b0319ace41dd3c4ff5fb1165f53ee538e/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/AbstractGrpcClient.java

3. retryingInvoke retries the same cached state

NodeTxExecutor.retryingInvoke() invokes the same Supplier repeatedly after sleeping. It does not invalidate a Store node, session, stub, channel, or DNS state.

The transaction's Store sessions are cached by node ID for the duration of the complete retry loop and are replaced only in doCommit()'s final cleanup.

Source:

https://github.com/apache/hugegraph/blob/b9710a7b0319ace41dd3c4ff5fb1165f53ee538e/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/NodeTxExecutor.java

4. UNAVAILABLE is not handled distinctly

NotifyingExecutor catches all thrown transport failures, reports HgNodeStatus.NOT_WORK, and rethrows. It does not inspect StatusRuntimeException.getStatus().getCode() and has no UNAVAILABLE recovery path.

The resulting notification only calls pdClient.invalidPartitionCache(). It does not invalidate:

  • HgStoreNodeManager.nodeIdMap
  • the cached Store address
  • blocking or asynchronous stubs
  • cached ManagedChannels

Sources:

5. Re-querying PD does not refresh the existing Store node

Invalidating the partition cache allows the next retry to re-query PD for partition leadership. However, when PD returns the same Store node ID, HgStoreNodeManager.applyNode() immediately returns the existing nodeIdMap entry. It does not call the provider again to retrieve Store metadata or rebuild the Store node.

Source:

https://github.com/apache/hugegraph/blob/b9710a7b0319ace41dd3c4ff5fb1165f53ee538e/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/HgStoreNodeManager.java

6. gRPC attempts DNS refresh, but the JVM cache remains stale

The effective Server dependency resolves gRPC Java to 1.47.0. Its pick_first load balancer requests name resolution when a subchannel reaches TRANSIENT_FAILURE or IDLE:

https://github.com/grpc/grpc-java/blob/v1.47.0/core/src/main/java/io/grpc/internal/PickFirstLoadBalancer.java

However, DnsNameResolver ultimately resolves through InetAddress.getAllByName():

https://github.com/grpc/grpc-java/blob/v1.47.0/core/src/main/java/io/grpc/internal/DnsNameResolver.java

With the JVM positive DNS cache set effectively to forever by the active SecurityManager, a gRPC refresh receives the previously cached pod IP again. This also explains why restarting only the Server process fixes the failure: it clears the JVM DNS cache and all Store-client caches.

Reproduction outline

  1. Deploy PD, Store, and Server on Kubernetes with HStore enabled.
  2. Ensure Server resolves a Store through a stable per-pod/headless-Service DNS name.
  3. Successfully perform a graph data write.
  4. Replace that Store pod so the DNS name is unchanged but the pod IP changes.
  5. Wait for the Store to register as Up in PD.
  6. Confirm the Store DNS name and port are reachable from the Server pod.
  7. Perform another graph data write.

Actual result: writes repeatedly fail with UNAVAILABLE: io exception until Server is restarted.

Expected result: the existing Server reconnects to the replacement Store automatically.

Suggested fixes

Required runtime fix

Configure a finite positive DNS cache TTL for the official Server distribution/container when the SecurityManager is enabled. Prefer a Java security properties override containing, for example:

networkaddress.cache.ttl=30

An immediate OpenJDK 11 workaround is to pass:

-Dsun.net.inetaddr.ttl=30

This was validated on Temurin 11 with the following effective positive TTLs:

Runtime condition Effective positive DNS TTL
No SecurityManager, no TTL setting 30
SecurityManager, no TTL setting -1 (forever)
SecurityManager with -Dsun.net.inetaddr.ttl=30 30
SecurityManager with -Dnetworkaddress.cache.ttl=30 -1 (forever)

networkaddress.cache.ttl is a Java security property. Passing it only as an ordinary -D system property does not change Java 11's InetAddress cache policy. The supported distribution-level fix should set it through Java security properties; the chart-level JVM workaround should use -Dsun.net.inetaddr.ttl=30.

Java 11 checks the networkaddress.cache.ttl security property before the sun.net.inetaddr.ttl system-property fallback. Therefore, if the distribution later configures networkaddress.cache.ttl through Java security properties, that distribution value takes precedence and the chart's -Dsun.net.inetaddr.ttl=30 becomes a harmless no-op. It will not override or conflict with the upstream policy.

Compatibility and security tradeoff

For a healthy cluster whose Store addresses do not change, making the TTL finite does not tear down established channels or force working connections to re-resolve. DNS cache expiration is lazy: it permits a later resolution to consult DNS again, but does not itself schedule a DNS query every 30 seconds. In this client, gRPC requests name resolution when the channel encounters failure or enters idle state.

The intended observable change is that a new Store IP behind an unchanged DNS name can eventually be discovered instead of the old IP remaining cached for the lifetime of the Server JVM.

This is not entirely security-neutral. The SecurityManager-dependent infinite TTL is a DNS spoofing and rebinding defense, so changing it to a finite value reopens a bounded re-resolution window. The proposed tradeoff is appropriate here because:

  • HugeSecurityManager exists to sandbox Gremlin script execution; the repository contains no indication that its incidental infinite DNS cache is a deliberate Store-transport security control.
  • HStore names are obtained from PD metadata rather than arbitrary user-controlled request hostnames.
  • 30 seconds is OpenJDK 11's own default positive TTL when no SecurityManager is installed.
  • The negative DNS cache policy is separate (networkaddress.cache.negative.ttl, normally 10 seconds) and is not changed by this proposal.

If the project considers PD-provided Store addresses untrusted, that trust boundary should be addressed directly through PD authentication, transport security, and address validation rather than by permanently pinning the first DNS answer received by each Server process.

Optional Store-client hardening

  • Detect Status.Code.UNAVAILABLE explicitly.
  • Atomically evict the target's cached blocking stubs, asynchronous stubs, and ManagedChannels.
  • Shut down evicted ManagedChannels.
  • Invalidate or refresh the corresponding cached HgStoreNode so address changes for the same node ID can be observed.
  • Reset connection backoff or rebuild the target before the next application retry.

Channel invalidation alone will not solve the stable-DNS case while the JVM DNS cache remains infinite, so the finite TTL is required.

Kubernetes readiness impact

The official HStore image health check currently calls /versions. That endpoint remains healthy during this failure because the Server process and control-plane APIs are available even though HStore data writes are unavailable.

It would be useful to expose a non-mutating data-plane readiness endpoint that verifies the Server can reach the current HStore partition leaders. Helm charts could then use that endpoint instead of a process-only readiness check.

Operational severity

This failure is triggered by ordinary Kubernetes operations such as Store upgrades, pod rescheduling, and node drains. It does not self-heal, is invisible to the current readiness probe, and requires restarting otherwise healthy Server pods to restore writes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions