It is very hard to make use of sinked data if the topic name can change, since most consumers would expect that to be constant.
Proposed change:
Update sinks so the topic does not change on crash-restart, and they support “exactly-once” semantics (duplicates are not seen by downstream consumers)
also see: https://github.com/MaterializeInc/materialize/issues/1728
By default, Materialize saves only catalog metadata - enough information to restart computations from the beginning. To avoid re-writing an entire second copy of the data into a sinks, we currently take the approach of appending a random nonce to sink topic names on creation. Thus, if Materialize is re-started, the sinked results of its computations will be written, from the beginning, into fresh Kafka topics.
This causes a variety of operational issues; customers typically ask for us to help resolve these issues by writing to "the same" Kafka sink on restart. However, we would like to do this without sacrificing correctness; i.e., either without introducing duplicates into the stream, or if we must have duplicates, doing so in such a way that they can be deduplicated easily by downstream clients.
One possibly guiding request is for outage recovery - if the Materialize host goes down or needs to be restarted for whatever reason, we would like to be able to bring it back up without causing disruption to downstream consumers of the sinks; in particular, without having to manually reconfigure them with whatever the new topic name is and forcing them to restart their own workflows.
(There may be other reasons people want this as well, which might have their own set of requirements ...)
Re-ingest the entire old sink on startup, run the computation again, and subtract the old sink from the new computation. That way, what is written out will only be the new data. This could in theory work but would require O(n) startup time in the size of the historical output. Thus, the only way to really make this feasible would be to ask people to turn on compaction of sinks.
Check what was _last_ written to the sink, to avoid writing it again
For example: My consistency topic says timestamp t_0 is complete, I also have three messages "foo", "bar", and "quux" at t_0+1. Don't sink anything until the frontier has passed t_0, and also apply ("foo", -1), ("bar", -1), ("quux", -1) to the sinked collection to avoid duplicating those. There is probably a significant amount of messy logic involved, especially since we (currently) don't necessarily write sink outputs in order of timestamp.
A variant of 2. that accepts some duplicates in the un-committed timestamps would make things simpler. The "CDCv2" format Frank invented seems perfect for this - it is an envelope on top of Avro that encodes progress messages inline in streams in such a way that it's possible to correctly interpret streams with arbitrarily duplicated and reordered messages. However, we would have to either teach customers how to interpret this format, or just write the logic for them (maybe a simple "basic CDCv2 reader" library would be not too hard to write. The hard stuff has already been done in differential-dataflow, anyway).
Currently in real-time sources, timestamps are assigned arbitrarily, which will make possible solutions 2. or 3. above not work correctly, so we would need to fix that... (will elaborate further on this tomorrow).
@ruchirK, @frankmcsherry and @elindsey , I will continue writing more stuff about this tomorrow. Also, please add anything you think I've missed or forgotten about in the solution space.
Another option: Remember (durably) what was written to each sink. This means that when writing to sinks, we'd have to write down some metadata somewhere. This could be a "sink metadata" Kafka topic, where we write down what timestamp, offset, we've committed per sink topic/partition.
@rjnn We basically already do this, with the consistency topic. However - as I described in option 2 - you need to write some logic to deal with un-committed data. E.g., if you have committed everything at timestamp t_0, but then you write a few messages to timestamp t_1, then crash before finishing t_1 and writing "t_1 is done" to the consistency topic, then on restart you need to avoid writing anything <= t_0, _and_ any of the partial t_1 stuff that you wrote.
All known Materialize users are using what we call "real-time sources" in which the assignment of timestamps (and therefore, consistency boundaries) is arbitrary and based on wall-clock time. Thus, RT sources lack what one might call "durability"; that is, when one restarts a computation from scratch, it will not necessarily give the same answers, because timestamps may have changed.
Furthermore, different instantiations of the _same_ source might have _different_ timestamps, which makes the situation even worse. For example, if src is an RT source,
CREATE MATERIALIZED VIEW mv1 AS SELECT * FROM src;
CREATE MATERIALIZED VIEW mv2 AS SELECT * FROM src;
SELECT * FROM mv1 UNION ALL mv2;
might return rows with multiplicity 1, and SELECT * FROM mv1 EXCEPT ALL mv2 might be nonempty. Both of these results are impossible in standard SQL/relational algebra.
A lot of reasons, but for this particular task, anything we do requires detecting duplicate messages, which will be basically impossible if the new messages after restart have totally different timestamps from the "same" messages written before restart.
From this discussion emerges a set of properties that we would like Materialize to have:
Whenever we accept data for a given source and timestamp it, we should remember that binding of a timestamp to a set of Kafka offsets, and require all future reads of the same source to use it. IIRC, timestamp.rs is probably already set up to do this, but we stopped... for some reason?
This would give us property (1).
In order to also get property (2), there are a few possibilities. One is: whenever we make a timestamping decision, we could persist it before letting it flow to the rest of the system. In fact, this is what we used to do - I'm not sure why we stopped. The disadvantages include causing permanent O(n) growth of the catalog. Perhaps it would be better to write these to some Kafka topic, but then you would have to wait for that to be committed (for whatever notion of "committed" Kafka has) before allowing the data to leave the source and flow to the rest of the system.
Another option is to only store _one_ piece of data per partition: the latest timestamp (call it t) , and the latest offset x bound to it. Then, on restart, timestamp _all_ data up to and including x with timestamp t. After having read through x, continue timestamping based on wall-clock time as usual.
This is essentially equivalent to having compacted everything through t. It would _not_ give us property (2) for arbitrary AS OF queries, since all the distinctions before time t would have been lost. But, that should be fine, we already discard these distinctions when we do compaction. However, it _would_ give us property (2) for simple, non-AS OF queries: after restart, any new query would incorporate all the previous data and therefore be one possible continuation of it. The disadvantage of this is that the entire input (up to x) would need to be ingested before any queries can return - but that is probably fine (and in fact the more correct behavior) for a large class of users.
@rjnn We basically already do this, with the consistency topic. However - as I described in option 2 - you need to write some logic to deal with un-committed data. E.g., if you have committed everything at timestamp t_0, but then you write a few messages to timestamp t_1, then crash before finishing t_1 and writing "t_1 is done" to the consistency topic, then on restart you need to avoid writing anything <= t_0, and any of the partial t_1 stuff that you wrote.
Kafka has a commit protocol to do all of this atomically.
@rjnn Oh, nice, that could indeed make "option 2" somewhat simpler.
The other issue here is the current syntax is opposite to WYSIWYG. Could we keep the same topic name as in "CREATE SINK" rather than using it as a prefix?
CREATE SINK "materialize"."public"."sink_top_buy_categories" FROM "materialize"."public"."mv_top_buy_categories" INTO KAFKA BROKER '192.168.1.254' TOPIC 'sink_top_buy_categories' WITH ("consistency" = true) FORMAT AVRO USING CONFLUENT SCHEMA REGISTRY 'http://192.168.1.254:8081' WITH SNAPSHOT AS OF "now"();
krmanoha=> select * from mz_kafka_sinks;
sink_id | topic
---------+-------------------------------------------------------------
u53 | sink_top_buy_categories-u53-1605273997-9258380046558767833
@krishmanoh2 Indeed - once we come up with a way to preserve the topic name, we can probably just use the raw sink name, rather than appending a nonce.
Getting myself organized before I start on this - this is more like a concrete restatement of what Brennan's said with more specifics.
This will require customers' kafka consumers to be in read-committed mode. This is the default on many clients (such as all those based on rdkafka), but not for primary main Java client.
The best alternative is a similar scheme but using CDCv2 to remove the need for Kafka transactions. This is likely the best route when supporting other sinks that don't offer transactional support, but it does require us building out a small CDCv2 client in a few languages for customer ease of use. While a good option and something we will probably do in the future, given Kafka's prevalence it's worthwhile doing something specific for that sink type that imposes minimal format restrictions.
Most other alternatives are merely refinements or iterative improvements of this proposal.
Specific areas that will need careful testing:
Brief update: from the 'MZ Changes' list above, 3/4 are done and I'm starting on 5 soon; Ruchir's likely taking 1/2.
In the intervening time we had a long tangent discussing possible alternatives to 1/2 (timestamp persistence via the timestamper thread), but ultimately settled on the same design that was laid out here.
There is one additional customer requirement: we do need to curb the O(N) disk growth of timestamp persistence, and fairly soon. For the people currently interested it's okay to treat all historical data as a single "catch up" timestamp on restart, so the current plan is (roughly) to have the timestamper write down minted offset/timestamp pairs, have the sink record published timestamps, and periodically condense that to only the most recent published offset/timestamp pair.
and periodically condense that to only the most recent published offset/timestamp pair.
This seems sane. For which use cases is this not appropriate?
This seems sane. For which use cases is this not appropriate?
Talked over slack awhile back, but for posterity - I don't (currently) know of any use cases where this isn't appropriate, but I had been hoping to avoid it for v1 to get the basic version working end to end sooner. There's one quirk with ts condensing too: if there's still a large amount of historical data (ie. kafka topics aren't pruned), then it may land us in a state where we repeatedly oom on startup since the ts/offset granularity acts as a pacing mechanism today.
Down to four concrete tasks now:
Update: we've begun QA on two of the major PRs: https://github.com/MaterializeInc/materialize/pull/6532 and https://github.com/MaterializeInc/materialize/pull/6866
This has merged with experimental support
Most helpful comment
Brief update: from the 'MZ Changes' list above, 3/4 are done and I'm starting on 5 soon; Ruchir's likely taking 1/2.
In the intervening time we had a long tangent discussing possible alternatives to 1/2 (timestamp persistence via the timestamper thread), but ultimately settled on the same design that was laid out here.
There is one additional customer requirement: we do need to curb the O(N) disk growth of timestamp persistence, and fairly soon. For the people currently interested it's okay to treat all historical data as a single "catch up" timestamp on restart, so the current plan is (roughly) to have the timestamper write down minted offset/timestamp pairs, have the sink record published timestamps, and periodically condense that to only the most recent published offset/timestamp pair.