Replies: 4 comments
|
Thanks for taking the time to write this up. The two issues you filed (#1252, #1257) already made the library better, and the crash recovery observation is worth a detailed answer, so here it is. One correction first, because it changes the picture: GoAkt does not enforce
There was a real hole behind your concern until recently, though. Before Olric v0.3.13, after a crash the survivor's only copy of a partition sat in its backup fragment, where the balancer could ship it away before promoting it. A second failure could destroy data even at So with v0.3.13 and
What I'm going to change in core:
On question 1, the placement journal: with the new default, the gap it closes narrows to the three cases above. They're real, but they don't justify an external storage contract in core's default path. GoAkt's value is that it runs on nothing but itself. A store the developer has to provide, even optionally, moves the reliability story outside the library for everyone who reads about it. So: the journal belongs in a companion library, per the boundary from #1245, but I'd rather upstream the hooks than the feature. You said re-landing it after the relocation rewrite cost two one-line hooks and one wiring change. File a feature request describing exactly those integration points. I expect they come down to a notification on relocatable spawn and stop, plus a way to hand in a On question 2, items 2 through 5 above are my answer: the default has to be safe, deviating from it has to warn, partial recovery has to be observable, and the docs shouldn't require reverse-engineering the quorum rules. |
|
Thank you for the correction - and it is a correction we needed. We read The five changes in #1260 answer our second question completely, and the olric v0.3.13 work behind them is more than we hoped for. On the journal: we have filed #1262 with the exact integration points, per your request. It comes down to the two you predicted - placement notifications on relocatable spawn/stop, and a chance to hand in a departed node's relocation set before the registry derivation runs - plus a note on the one wire-type question they raise. If those land, our journal moves onto the public API and the fork goes away entirely, which is the outcome we want too. Separately: the two capabilities that had no business being in core are now out. |
|
@StringKe I am closing this discussion. For feedback on how GoAkt is performing at your organisation, you can kindly use the feedback issue. Thank you very much for helping enhance the framework. |
Uh oh!
There was an error while loading. Please reload this page.
What we run
An internal analytics platform (self-hosted, Google-Analytics-shaped) that collects behavioural and telemetry data from clients across several of our product lines. One Go application, deliberately kept a monolith, running on Kubernetes at roughly 31 pods in steady state, with about 16 new pods joining the existing cluster simultaneously during a rolling update. GoAkt is the entire distribution layer: no MQTT or AMQP broker, no service mesh, no external actor registry. The only stateful things next to the app are a database and Redis.
How the workload maps onto the actor model
Four shapes, each answered by a primitive rather than by a piece of infrastructure:
WithRelocationDisabledplus a long-lived passivation strategy), so "deliver this to user X" is a location-transparentTellinstead of a Redis lookup plus a routing table we maintain ourselves.How it performs
The headline is that the actor core has not been a source of surprises. Message passing, supervision, placement, and grain single activation behave exactly as documented under steady state, and none of our production incidents in this stack originated there. Everything that bit us lived at the edges of the lifecycle: how the system starts, and what happens when a node goes away. That is a good place for the sharp edges to be, and it is why the two issues below were worth digging into.
The two edges we hit
1. Remoting inherited cancelation from the
Startcontext (#1252, merged in #1253).We passed our DI framework's bounded OnStart context to
ActorSystem.Start- the ordinary "give startup 30 seconds" pattern. The node started fine and served traffic. Then the startup budget elapsed: because the startup context was retained as the remoting server's base context, every inbound handler context from that moment on was born alreadyDeadlineExceeded. All cross-node traffic to that node failed permanently, while the process itself looked perfectly healthy. The fix is a one-linecontext.WithoutCancelon the server's base context. The more general lesson: a library that holds a caller's context for the lifetime of a server creates a footgun no amount of care at the call site can defuse.2. Cluster bootstrap had no in-process retry (#1257, PR #1258).
During a rolling update, about 16 new pods join a 31-node cluster that already holds data. Partition redistribution plus routing-table convergence can exceed the 10s bootstrap timeout, at which point
Startfails and the process exits - so Kubernetes CrashLoopBackOff becomes the de facto retry loop: several restarts and a few minutes of alert noise for a condition that heals in seconds. We raised the bootstrap timeout on our side as a stopgap, but the real fix is a bounded in-process retry with a full engine teardown between attempts. Olric's own join retries do not cover this, because they only retry the memberlist join, not the initial sync and dmap creation that follow it.Crash recovery on the default
replicaCount: an observation we would like to open upRegistry-derived crash recovery is a genuinely nice addition and we adopted it immediately. What we want to share is how it behaves in combination with the default configuration - we did read the note in the docs, and we still had to work around it in practice, which is why we think it is worth putting on the table.
The mechanism. Registry records are keyed by actor name (
PutActor->dmap.Put(composeKey(namespaceActors, addr.Name()), ...)), so a record's partition is chosen byhash(actorName)and has nothing to do with which node the actor actually runs on.ReplicaCountdefaults to 1, which in Olric means one primary copy and no backups. So when a node crashes, the partitions it owned are gone with it - and those partitions hold a share of every node's records, including its own.The consequence.
deriveRelocationSetFromRegistryscans the surviving registry and finds the crashed node's records that happened to hash onto other nodes' partitions - statistically about (N-1)/N of them - and silently misses the rest: no error, no warning, and the rebalance still reports success. Silent partial recovery is harder to notice than no recovery at all.Why "just raise the replica count" is not a free answer. The docs recommend
replica count > 1, but GoAkt enforcesreadQuorum + writeQuorum > replicaCount, so moving to 2 forceswriteQuorum = 2(orreadQuorum = 2). WithwriteQuorum = 2, every registry write must reach two replicas synchronously - so while a node is down, registry writes fail, and grain activation writes the registry. To make recovery-after-crash work, you make the registry fail-during-crash. For a registry that is inherently rebuildable (an actor re-registers on spawn), that is a trade we did not feel able to make. Memory is not the objection - our registry is a few hundred records - the quorum coupling is.What we do instead. We run an opt-in placement journal on our fork: relocatable placements are recorded to a store we own (Redis, which we already run), and on an unexpected
NodeLeftthe leader replays that node's journal to synthesize the samePeerStatea graceful shutdown would have produced. It needs no quiescence wait, so it runs synchronously in theNodeLefthandler, and it is complete atreplicaCount = 1because it does not depend on Olric's partition replication at all. It also survives somethingreplicaCount = 2does not: a full-cluster restart wipes the in-memory DMap registry entirely, while the journal is still there.The proposal. We would be glad to offer this upstream as a purely additive feature: a
placement.Journalinterface plus aClusterConfig.WithPlacementJournal(store)option, defaulting to nil so behavior is unchanged for anyone who does not opt in; when configured, relocation prefers the journal's complete record and otherwise falls back to registry derivation exactly as today. The interface is storage-agnostic (an in-memory implementation ships with it; ours in production is Redis-backed), so no specific store enters core. It has been running in production, and it has already survived a full upstream rewrite of the relocation machinery - re-landing it on the new dispatcher/worker architecture cost two one-line hooks and one wiring change, which is a fair proxy for its long-term maintenance weight.If that shape does not belong in core, we understand - it sits close to the boundary drawn in #1245, and the storage contract is precisely the debatable part. Even without the feature, a startup warning when
replicaCount == 1and relocation is enabled might save someone a bad afternoon.What we are building on top
Per the boundary write-up in #1245: the runtime-shaped capabilities we need beyond the actor core (durable at-least-once jobs with retry and DLQ, a cluster gateway that terminates TLS for the whole cluster, a KV/lock surface, rate limiting) live on our fork today and will move into a companion library built on GoAkt's public API, in the eGo mould. We agreed with that rubric then and we still do: the core staying small is exactly what let us embed it in our own process instead of adopting an entire platform.
Two questions we would like your view on
All reactions