bitflicker64 opened a new issue, #3123:
URL: https://github.com/apache/hugegraph/issues/3123

   ### Bug Type (问题类型)
   
   server status (启动/运行异常)
   
   ### Before submit
   
   - [x] I have confirmed and searched that there are no similar problems in 
the historical issues and documents
   
   ### Environment (环境信息)
   
   - Server Version: master (verified at b9710a7), also present in 1.7.0
   - Backend: hstore, 3 PD x 3 Store x 3 Server 
(docker/docker-compose-3pd-3store-3server.yml)
   - OS: Ubuntu 22.04 containers on Docker 29.4.0
   - Data Size: n/a, fails before init
   
   ### Expected & Actual behavior (期望与实际表现)
   
   In a multi-PD deployment, `bin/wait-storage.sh` picks one PD and stays on it 
for the whole wait, even when that PD is the only one that cannot answer 
correctly. The other PDs are never contacted again.
   
   The relevant part of 
`hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-storage.sh`:
   
   ```bash
   check_any_pd() {
     for peer in $(echo "$PD_REST_LIST" | tr ',' ' '); do
       if curl ${PD_AUTH_ARGS} -f -s http://${peer}/v1/health >/dev/null 2>&1; 
then
         echo "$peer"
         return 0
       fi
     done
     return 1
   }
   
   until PD_REST=$(check_any_pd); do
     log 'No PD peer ready yet, retrying in 5s'
     sleep 5
   done
   log "PD health check PASSED via $PD_REST"
   
   until curl ${PD_AUTH_ARGS} -f -s \
         http://${PD_REST}/v1/stores 2>/dev/null | \
         grep -qi '"state"[[:space:]]*:[[:space:]]*"Up"'; do
     log 'No Up store yet, retrying in 5s'
     sleep 5
   done
   ```
   
   `PD_REST` is assigned once. The second loop then polls that single peer 
until the 300s timeout expires.
   
   The catch is that `/v1/health` doesn't tell you whether the PD is a working 
member of the raft group. In 
`hugegraph-pd/hg-pd-service/.../rest/StoreAPI.java`:
   
   ```java
   @GetMapping(value = "/health", produces = MediaType.TEXT_PLAIN_VALUE)
   public Serializable checkHealthy() {
       return "";
   }
   ```
   
   It returns 200 as soon as the Spring listener is up. Meanwhile `/v1/stores` 
reads the node's own raft-backed metadata (`PDRestService.getStores` to 
`StoreNodeService.getStores` to `StoreInfoMeta.getStores`, which does a 
`scanPrefix` over local KV). There is no leader redirect in the PD REST layer. 
So a PD that is listening but has no raft state returns HTTP 200 with an empty 
store list. `curl -f` succeeds, the grep never matches, and the loop spins 
until timeout.
   
   I confirmed this against a real PD started with a peer list whose other two 
members do not exist:
   
   ```
   /v1/health   -> HTTP 200          (passes the curl -f gate in the script)
   /v1/stores   -> {"stores":[],"numOfService":0,"numOfNormalService":0,...}   
HTTP 200
   /v1/members  -> HTTP 500 Internal Server Error
   ```
   
   Docker reported that container as `Up (healthy)` the whole time.
   
   Since `docker-entrypoint.sh` runs with `set -euo pipefail` and calls the 
script unguarded, the timeout kills the entrypoint and the container restarts.
   
   ### Reproduction
   
   I couldn't get this to happen from a plain cold `docker compose up`, since 
the `depends_on: service_healthy` ordering usually gives the raft group enough 
time to form. It shows up when a PD is slow to join, or when the selected PD 
goes away partway through the wait.
   
   To get a deterministic case I ran the unmodified script against three PD 
REST endpoints on a docker network, with the first peer serving `/v1/health` 
normally but reporting an empty store list, and the other two reporting a store 
in state `Up`:
   
   | peer list | result |
   | --- | --- |
   | `pd0,pd1,pd2` (pd0 storeless) | exit 1 after 301s |
   | `pd1,pd0,pd2` (same cluster, order swapped) | exit 0 in 1s |
   | `pd0,pd1,pd2`, pd0 killed 30s into the wait | exit 1 after 300s |
   
   Request counts on the PD side during the failing run:
   
   ```
   pd0: 61 GET /v1/stores
   pd1:  0 GET /v1/stores
   pd2:  0 GET /v1/stores
   ```
   
   Peer ordering is the only difference between the passing and failing runs.
   
   The restart does not help as much as it looks like it should. Running the 
same setup under `restart: unless-stopped`, with the first PD joining the group 
at t+330s:
   
   ```
   ### wait-storage attempt starting 18:23:27
   [wait-storage] PD health check PASSED via pd0:8620
   [wait-storage] ERROR: Timeout waiting for storage backend
   ### wait-storage attempt starting 18:28:28
   [wait-storage] PD health check PASSED via pd0:8620
   [wait-storage] Store registration check PASSED
   [wait-storage] Storage backend is VIABLE
   
   RestartCount=1  Running=true  ExitCode=0
   ```
   
   The second attempt re-runs selection but picks the same peer, because the 
list order is fixed. It only passed because the PD had caught up by then. If 
that PD stays up and storeless, every cycle fails the same way and the server 
never starts. The error text points at the storage backend, which is the one 
component that was fine, so this looks like slow storage rather than a peer 
selection problem. That's probably why it hasn't been noticed.
   
   ### Suggested fix
   
   Probe `/v1/stores` across every peer on each attempt instead of binding to 
one. `/v1/stores` succeeding is strictly stronger than `/v1/health`, so the 
separate health loop can go:
   
   ```diff
   -              check_any_pd() {
   +              check_any_pd_stores() {
                    for peer in \$(echo \"\$PD_REST_LIST\" | tr ',' ' '); do
   -                  if curl ${PD_AUTH_ARGS} -f -s http://\${peer}/v1/health 
>/dev/null 2>&1; then
   +                  if curl ${PD_AUTH_ARGS} -f -s http://\${peer}/v1/stores 
2>/dev/null | \
   +                     grep -qi '\"state\"[[:space:]]*:[[:space:]]*\"Up\"'; 
then
                        echo \"\$peer\"
                        return 0
                      fi
                    done
                    return 1
                  }
    
   -              until PD_REST=\$(check_any_pd); do
   +              until PD_REST=\$(check_any_pd_stores); do
   ```
   
   with the second `until` loop and its log line removed. This gives both 
all-peer probing and re-selection on every retry. With this change the two 
failing cases above exit 0 in under a second.
   
   The java client already does the right thing here: 
`PDClient.newLeaderStub()` iterates every host, uses `getMembers` to find the 
actual raft leader, and moves on when a host throws. Only the shell layer lacks 
that.
   
   ### Related, lower priority
   
   `checkHealthy()` returning an unconditional 200 also means the `depends_on: 
service_healthy` gates in all three compose files do not check anything beyond 
the port being open. The Store health endpoint has the same body. Splitting 
liveness from readiness would help, with readiness backed by something raft 
aware such as `RaftEngine.getLeaderGrpcAddress()`. I would not make a PD exit 
when it cannot reach quorum, since a node waiting for its peers to come back is 
often the correct one and exiting turns a recoverable partition into a crash 
loop.
   
   `bin/wait-partition.sh` has a similar single endpoint binding, and 
`STORE_REST` is pinned to `store0:8520` for all three servers in the compose 
file. Lower impact, since the entrypoint calls it with `|| log "WARN: 
partitions not assigned yet"`.
   
   Two smaller things in the same file:
   
   - Line 78 reads `PD_PEERS="${hugegraph_pd_peers:-}"`, but the entrypoint 
exports `hugegraph.pd.peers` with dots, which bash cannot expand, and nothing 
sets the underscore name. It works only because the env to config loop rewrites 
`pd.peers` into the properties file and the line 80 fallback picks it up.
   - Line 32 sets `WAIT_STORAGE_TIMEOUT_S=300` unconditionally, so the `[ -n 
... ]` guard at line 76 is always true and the value cannot be overridden from 
the environment. `WAIT_STORAGE_TIMEOUT_S="${WAIT_STORAGE_TIMEOUT_S:-300}"` 
would match how wait-partition.sh handles its timeout.
   
   Happy to send a PR for the wait-storage.sh change if that's useful.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to