mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-29 07:07:24 +08:00
87 lines
9.0 KiB
Markdown
87 lines
9.0 KiB
Markdown
# Managed client sample
|
|
|
|
This console sample shows the recommended way to connect an OPC UA client: build a single `ManagedSession` with `WithServerRedundancy()`. The same code works whether or not the target server is configured for redundancy, because a client is almost never aware of the server topology until it has connected.
|
|
|
|
- Against a redundant server, the managed session reads `Server.ServerRedundancy` / `Server.ServiceLevel`, discovers the redundant set from the connected server, and fails over transparently. There is no client-side failover-mode selection and no hand-maintained seed list — the peer set comes from the server.
|
|
- Against a server that is not configured for redundancy, the same session simply runs as a resilient, automatically reconnecting client (`RedundancySupport=None`).
|
|
|
|
## Run
|
|
|
|
```powershell
|
|
dotnet run --project samples\RedundantClient\RedundantClient.csproj -- `
|
|
--server opc.tcp://localhost:62543/RedundantServer --autoaccept --nosecurity --duration 00:05:00
|
|
```
|
|
|
|
| Option | Default | Description |
|
|
| --- | --- | --- |
|
|
| `--server`, `-s` | `opc.tcp://localhost:62543/RedundantServer` | Discovery URL of any server in the (optionally) redundant set. |
|
|
| `--nosecurity` | off | Select endpoints with `MessageSecurityMode.None`. |
|
|
| `--autoaccept` | off | Automatically accept untrusted server certificates (sample only). |
|
|
| `--duration`, `-d` | `00:02:00` | How long to monitor before exiting; `00:00:00` runs until Ctrl+C. |
|
|
| `--suite` | off | Run a browse/read/subscribe workload against the redundant `ISession`. |
|
|
|
|
The sample connects, logs the server's reported `RedundancySupport` (or notes that the server is not redundant), subscribes to `Server.ServerStatus.CurrentTime`, and logs the values together with any transparent connection-state changes (reconnect or failover). To observe failover, lower the active server's service level (for example with the `RedundantServer` sample's manual failover support) or stop the active server; the managed session reconnects to a healthy peer on its own.
|
|
|
|
See [HighAvailability.md](../../docs/HighAvailability.md) for the redundancy design and the [RedundantServer](../RedundantServer/README.md) sample for the server side.
|
|
|
|
## Run with docker compose (one file, the full HA matrix, env-driven)
|
|
|
|
A single [`docker-compose.yml`](docker-compose.yml) in this folder runs the whole matrix — a redundant server set and one or more clients — where **server consistency and client mode are chosen independently by an environment variable**, and **both sides log their failover / HA behavior, including data loss**. The adjacent [`.env`](.env) sets the default cell (eventual servers + independent clients), so a bare `up` needs no flags. docker compose auto-loads that `.env` even when you run from the repository root.
|
|
|
|
Selection is by Docker **compose profiles** (the `COMPOSE_PROFILES` environment variable) — pick one server profile and one client profile:
|
|
|
|
| Profile | Topology | Scales? |
|
|
| --- | --- | --- |
|
|
| `server-eventual` | Active/active servers, CRDT gossip + DNS discovery (the default) | yes (`--scale server=N`) |
|
|
| `server-strong` | Fixed 3-node active/passive Raft servers — no data loss | no (fixed odd quorum) |
|
|
| `client-independent` | N managed clients, each fails over on its own (the default) | yes (`--scale client=N`) |
|
|
| `client-coordinated` | Fixed 3-node client replica set — exactly one active client | no (fixed odd quorum) |
|
|
|
|
For `client-coordinated`, `CLIENT_CONSISTENCY` (`eventual` \| `strong`, default `eventual`) selects the shared-store flavor used to hand the session over to the promoted client. Only the eventual server and the independent clients are `--scale`-able; the strong server and the coordinated client are Raft quorums that need stable identities, so they are fixed 3-node named replicas. `SERVER_REPLICAS` (default 3) and `CLIENT_REPLICAS` (default 2) set the scalable counts.
|
|
|
|
Run from the repository root:
|
|
|
|
```bash
|
|
# DEFAULT: 3 eventual servers + 2 independent clients (failover + data loss demoable out of the box).
|
|
docker compose -f samples/RedundantClient/docker-compose.yml up --build
|
|
|
|
# Scale the eventual server and the independent clients independently:
|
|
docker compose -f samples/RedundantClient/docker-compose.yml up --build --scale server=5 --scale client=3
|
|
|
|
# STRONG server (no data loss) x independent clients:
|
|
COMPOSE_PROFILES=server-strong,client-independent \
|
|
docker compose -f samples/RedundantClient/docker-compose.yml up --build
|
|
|
|
# Any matrix cell, e.g. STRONG server x EVENTUAL coordinated client:
|
|
COMPOSE_PROFILES=server-strong,client-coordinated CLIENT_CONSISTENCY=eventual \
|
|
docker compose -f samples/RedundantClient/docker-compose.yml up --build
|
|
```
|
|
|
|
To trigger a server failover, stop the replica a client is on — for example scale the eventual servers down (`… up -d --scale server=1`) or `docker kill` one server container (for `server-strong`, stop the Raft leader, e.g. `… stop server-a`). Then watch the **client** log, which turns the transparent failover and its data impact into explicit lines:
|
|
|
|
| Client log line | Meaning |
|
|
| --- | --- |
|
|
| `Connection state: Connected -> Reconnecting` / `Failover` | The session lost its server and is selecting a healthy replica (independent client). |
|
|
| `CONNECTED: session (re)connected to …` | The session re-established a channel (transparent reconnect/failover). |
|
|
| `ACTIVE CLIENT: replica '<id>' is now the active client` | This coordinated replica was elected leader and (re)established monitoring. |
|
|
| `STANDBY: this replica is no longer the active client` | This coordinated replica lost leadership; a peer client took over. |
|
|
| `FAILOVER: now served by replica '<id>' (was '<other>')` | The `HighAvailability.ActiveReplica` value changed — a different server is now serving the session. |
|
|
| `DATA LOSS: CurrentTime jumped 6.0s (5 update(s) missed during failover)` | Gap in `ServerStatus.CurrentTime`: samples were missed while the subscription was re-established on the new server (or while a standby client was being promoted). |
|
|
| `DATA LOSS: Counter … across failover (state did not carry over)` | The replicated `Counter` regressed or diverged — the value was **not** preserved (see below). |
|
|
| `HA OK: Counter continued <n> -> <n+k> across failover (no data loss)` | The `Counter` value **was** preserved across the failover. |
|
|
|
|
The **server** log shows the other side of the same events: `HA: replica <id> became ACTIVE writer` / `… became STANDBY` on role changes, and a periodic `HA: replica <id> ACTIVE, Counter=<n>` heartbeat so you can see which replica produces which values.
|
|
|
|
Whether the `Counter` shows data loss or continuity depends on the **server** profile:
|
|
|
|
- **`server-eventual` (active/active, eventual consistency, the default).** Every replica writes its own independent `Counter`, so on failover the value does not carry over and the client logs **data loss** (`DATA LOSS: Counter jumped …`). This is the scalable topology.
|
|
- **`server-strong` (active/passive, strong Raft consistency).** The `Counter` rides a linearizable Raft store, so a promoted standby continues it and the client logs **no data loss** (`HA OK: Counter continued …`). Strong consistency needs a fixed odd quorum (3 nodes), so it is **not** `--scale`-able.
|
|
|
|
## Client replica set (high availability)
|
|
|
|
Run the client image in multiple containers so each replica is its own process — a single-process demo cannot coordinate a real deployment. The client mode is chosen with `CLIENT_MODE` (which the compose sets per profile); two process-per-replica models are supported:
|
|
|
|
- **Independent managed clients** (`CLIENT_MODE=independent`, the compose default, **2, scalable**): every container builds a `ManagedSession` with `WithServerRedundancy()` and reconnects to a healthy peer independently. Scale the client image with `--scale client=N`, or use the `clients` profile in [`RedundantServer/Scale/docker-compose.yml`](../RedundantServer/Scale/docker-compose.yml).
|
|
- **Coordinated single-active replica set** (`CLIENT_MODE=eventual` \| `strong`, the `client-coordinated` profile, **fixed odd quorum, default 3**): each container builds a `RedundantClientSession` over a real Raft cluster among the client replicas (`RaftLeaderElection` + a `RaftSharedKeyValueStore`, or a `HybridSharedKeyValueStore` over CRDT gossip for eventual) — the same building blocks the server uses, mirrored on the client. The processes elect one leader that holds the session and share its protected session secrets, so a follower takes over on leader loss and resumes monitoring. Each replica exposes a transparent `RedundantClientSession` (`ISession`). Because the election is Raft-quorum-based in both eventual and strong, a coordinated set needs an **odd ≥ 3 quorum** (a 2-member set is degenerate); the compose ships a fixed 3-node `client-a/b/c` set. A coordinated set mirrors session secrets through a networked store, so it requires a record protector: set `CLIENT_RECORD_KEY` to a shared base64 32-byte key in production, or `CLIENT_INSECURE=true` for an isolated demo (a well-known, non-secret demo key). See [HighAvailability.md](../../docs/HighAvailability.md).
|
|
|