manueljishi commented on issue #2708:
URL: 
https://github.com/apache/apisix-ingress-controller/issues/2708#issuecomment-5265451571

   We hit this and tracked it to a reproducible root cause. Posting in full 
since the issue was just marked stale, and because I think #2689 and #2699 are 
the same bug.
   
   **TL;DR:** it is not the controller and not APISIX. It is the **adc sidecar 
version boundary at v0.22.0**, combined with adc's *update* path being unable 
to migrate a service from the old inline-upstream format to the new split 
format.
   
   ## Environment
   
   | | before | after |
   |---|---|---|
   | apisix helm chart | 2.12.4 | 2.16.0 |
   | APISIX | 3.14.1 | 3.17.0 |
   | apisix-ingress-controller | 2.0.0-rc5 | 2.1.0 |
   | **adc** | **0.21.2** | **0.26.0** |
   
   Provider is etcd/traditional (`deployment.role: traditional`, 
`config_provider: etcd`), external etcd, ~218 services, Kubernetes 1.33 on EKS.
   
   ## Symptom
   
   After the chart upgrade, services whose pods were replaced started returning 
502 permanently. Everything else looked healthy:
   
   - controller logs `"status":"success"`
   - Argo shows Synced / Healthy
   - `ApisixRoute` status is `Accepted=True`
   - the controller sends the **correct** node IPs
   
   Only services whose pods happened to cycle after the upgrade were affected, 
so it degrades gradually rather than failing loudly.
   
   ## Root cause
   
   adc changed how service config is stored:
   
   - **[api7/adc#354](https://github.com/api7/adc/pull/354)** (v0.22.0) 
`feat(apisix): separate inline upstream`
   - **[api7/adc#384](https://github.com/api7/adc/pull/384)** (v0.23.0) 
`fix(apisix): both inline and referenced upstreams coexist within the service` 
— *"This PR will commit the removal of the inline upstream upon service 
updates."*
   
   So:
   
   - adc **≤ 0.21.x** → service holds an embedded `"upstream": {nodes: [...]}`
   - adc **≥ 0.22.0** → service holds `"upstream_id"`, nodes live in a separate 
`/apisix/upstreams/<id>`
   
   The apisix helm chart defaults the adc sidecar to **0.21.2 in chart 2.12.x** 
and **0.26.0 in chart 2.16.0**, so a routine chart upgrade crosses this 
boundary. Neither the APISIX nor the apisix-ingress-controller release notes 
mention it.
   
   **Existing services are not migrated.** adc's update path reads a service 
that still has an inline upstream, assumes the separate upstream object already 
exists, and emits only the service write. APISIX rejects it:
   
   ```
   PUT /apisix/admin/services/<id> -> 400 Bad Request
   error_msg: failed to fetch upstream info by upstream id [<id>], response 
code: 404
   ```
   
   One rejection fails the whole ADC batch, so unrelated services in the same 
batch also stop converging. In our cluster this was 28 failures per sync.
   
   ## Evidence chain
   
   Names below are redacted as `<ns>` / `<route>`; IDs are the real 
deterministic hashes for a single service.
   
   Controller is sending the right thing — from the `prepared request body` log 
line:
   
   ```json
   "upstream":{"labels":{"managed-by":"apisix-ingress-controller"},
               "nodes":[{"host":"10.0.17.10","port":3000,"weight":100}]}
   ```
   
   etcd still holds the old value:
   
   ```console
   $ etcdctl get /apisix/services/<svc-id> --print-value-only
   {"upstream":{"nodes":[{"weight":100,"port":3000,"host":"10.0.30.68"}],...}}
   ```
   
   Real endpoint at the time was `10.0.17.10`; `10.0.30.68` belonged to no 
running pod. Data plane confirms:
   
   ```
   balancer.lua:376: run(): proxy request to 10.0.30.68:3000 while connecting 
to upstream
   connect() failed (113: No route to host) while connecting to upstream, 
upstream: "http://10.0.30.68:3000/";
   ```
   
   State was split roughly in half — **218 services, only 107 upstream 
objects** — so over 100 services referenced an `upstream_id` that was never 
created.
   
   Notably, the controller payload contains `services`, `routes` and 
`consumers` but **no `upstreams` array** — the inline→split conversion happens 
entirely inside adc.
   
   ## What did not work
   
   - **Restarting the controller / APISIX.** No effect — the stale values are 
in etcd.
   - **`ingressClassName: apisix` on the CRs** (the resolution on #2699). 
Tested directly; the service stayed on the inline format. Those routes were 
already `Accepted=True`, so that fix addresses a different failure.
   - **Forcing a service rewrite** by appending a throwaway host to the route. 
This *does* migrate a service — but only where the upstream object already 
exists. It migrated 45 of 215 and then stalled with no error.
   - **Upgrading adc forward.** Checked every release 0.26.0 → 0.29.0. The 
differ v4 work in 0.27.1/0.28.0 is explicitly *"only a code refactoring; you 
should expect no difference in behavior"*. Nothing addresses upstream creation 
ordering.
   
   ## What did work
   
   **adc's create path is fine — only the update path is broken.** Deleting the 
service object makes adc treat it as new, and it creates both objects correctly.
   
   Single-service proof (note the service and its upstream share the same id):
   
   ```console
   $ etcdctl del /apisix/services/<svc-id>
   1
   
   $ etcdctl get /apisix/upstreams/<svc-id> --print-value-only
   {"labels":{"managed-by":"apisix-ingress-controller"},"retries":0,
    "name":"<ns>_<route>_0",
    "scheme":"http","nodes":[{"host":"10.0.47.0","port":3000,"weight":100}],
    
"type":"roundrobin","timeout":{"read":10,"send":5,"connect":2},"id":"<svc-id>"}
   ```
   
   The service came back with `upstream_id`, the upstream object was created 
with the correct nodes, and the `ApisixUpstream` timeout/retries settings were 
preserved.
   
   We scripted this in small batches, verifying after each that the service 
returned **and** its upstream object exists. Final state across 218 services:
   
   - 218/218 on `upstream_id`, 0 inline
   - 0 sync failures (from 28 per sync)
   - 0 referenced upstreams pointing at dead pods
   - pod restart → APISIX picks up the new IP within one `syncPeriod`
   
   Cost is a short unroutable window per service, between the delete and the 
controller recreating it — about one `syncPeriod`. Only the current batch is 
affected.
   
   ## Suggestions
   
   1. **adc should migrate inline services on update** — when it reads a 
service with an inline `upstream` and intends to write `upstream_id`, it needs 
to create the upstream object first. Right now it emits a service referencing 
an object that does not exist.
   2. **Ordering**: if the upstream create and service update are in the same 
batch, the upstream must be applied first, otherwise APISIX's reference 
validation rejects the service.
   3. **Document the v0.22.0 format change as an upgrade note in the chart**, 
since the sidecar version is a chart default that most users never set 
explicitly. A chart bump silently crosses it.
   4. Worth checking whether a partial failure should really poison an entire 
ADC batch — one invalid resource stopping unrelated services from converging is 
what turned a single bad route into a wider outage for us.
   
   Happy to provide full controller/adc logs or test anything against a patch.


-- 
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