00:03Hey everyone, thank you for joining us today
00:05My name is Xinjiang, and I'm a senior engineer and personal addition to the engineering team
00:09Here's also my colleague Syndrome, and he's a senior software engineer on the real-time data infrastructure team
00:15We are so excited to be here today and share with you how we backfill Flink pipelines using network source and Netflix
00:23This gender, for today, our first talk about phone use cases and Netflix, and while we need to backfill Flink applications, and also work user existing approaches along with this pros and cons, then Signal will join us and discuss the
00:37technical aspects of designing iceberg source, and how he came up a solution to overcome event ordering challenges
00:44At the end, I'll come back and share with you how to enable expert backfill for your Flink application
00:51Let's get started
00:53First of all, I want to share with you how my team used blank
00:55I'm part of a personal addition to the engineering team, and my teens builds and owns many different data systems that power data analytics and machine learning algorithms
01:05One such system is the real-time merge impression app, which we usually call it RMI
01:11One of the main use cases of our minds taking is to calculate intake rate
01:16Ticker rate is literally the number of times the user watches the value is divided by the number of times the weight is shown to a user
01:21In other topical intake rate, RMI joins impression events and playback events in real time based on set of key to attribute place or impression
01:32RMI data is critical input to personalization algorithms and also AV experimentation analysis
01:38Because of large volume, the impression data and playback data, RMI is one of the largest states for Flink application and Netflix
01:48Just like any other types of software, Flink application can fail due to various Reasons, for example, it can encounter levels while reading from source or write to sync
01:56Dependency service field can happen and lead into incorrect output
02:04Upstream data changes can happen too, which break the assumption you make in your Flink logic
02:10Often times, the output of Flink applications serve as critical input to decision making for Netflix products
02:16After failures, we need to backfill to medicate downstream impacts
02:22Here are a few possible types of backfilling you might need, for example, correcting wrong data that was previously produced by a Flink application, vaccination data for the period
02:32times your Flink job was down, or using backfilling to bootstrap state in order to continuously consume live data from Kafka topics
02:43The next question is, how do we backfill the eating suite? Backfill is simply re-running the Flink job to reprocess source event from the problematic period
02:54It's easy, just redeploy the job for a different time stamp
02:56However, the biggest challenge here is Kafka topics come with limited retention, which means that when failures happen, you need to race with time to troubleshoot the failure, fix the problem, and replay events, all before the source events expire
03:05You might ask
03:14Why don't we just increase retention so we always have enough time? The biggest challenge is increasing Kafka retention is very expensive for huge data sets
03:24Let's take Netflix, for example, if we want to return 30 days of data generated by our application, it would cost Netflix around 93 million a year, not even mentioning that some applications will need more than 30 days of data
03:39If Kafka retentions cause the problem, can we possibly store the Kafka event somewhere else at a lower cost and backfill using that data? The answer is yes
03:50Netflix Keystone platform provides a rolling service that makes Kafka
03:54events available in other storage systems, such as Iceberg table on top of S3, so that they can be further processed by batch systems later
04:04What is Iceberg? For those of you who are not familiar with Iceberg, it is an open source table format for huge analytic data sets
04:11It offers many great features, such as schema evolution, file pruning, both based on partition and column level statistics, time traveling, and version rollback
04:22Last time, but not least, we compared the compression rate and storage cost for Parquet table on top of Procreative file in S3, and Iceberg table offers tall battery
04:36compression rate and 98 percent less storage cost, which is great for ops
04:45Let's take a look at how Kafka events look like
04:47The answer table on the left, we, on the left, we have a schematized stream of play by Kafka events, and there are the top level attributes such as account ID, show ID, and view duration seconds
04:58And when these events are brought to the Iceberg table, all those attributes become top level column in the Iceberg table, in addition to the original data containing Kafka events
05:10Kafka metadata, such as Kafka ingestion time, are automatically attached to the table as well to be used later, so we have a calculated event available in export table at lower cost, and can we buy free from export table? The also is yes
05:27Another breakfast approach is to build and maintain a separate batch-based application, such as Spark drop, that the equivalent to the Flink application, but rates from effort table instead
05:35When failures happen, you just go to your Spark job and run your Spark drop for the field period
05:44The biggest challenge with this approach is that the initial development of such Spark drop can take days or weeks, depends on the complexity of your logic, but also into do round and round of data validation to
05:55Make sure the two parallel applications produce the same results after the initial development
06:00For the future, you also need to continue to invest in engineering efforts to keep the Spark app up to date
06:10As you can see, this batch-based spectrum approach isn't ideal either
06:16To summarize, there are two common approaches for backfilling
06:18One is batch-based backfill, which is about maintaining a separate app equivalent to the Flink application, so one page is the data retention is at low cost, however, engineers have to maintain two applications in parallel
06:32Another approach, the real-time backfilling, which Is about, basically, we run the Flink application before Kafka sources expire
06:41The biggest advantage of this approach is that you can use exactly the same as for both production and backfilling, but the challenge here is increasing Cochrane retention is very expensive, as you can see
06:53Another approach here is ideal, which leads us to a question: can we possibly combine the best things from both worlds? And now, before you start to wonder what this magic solution is that can combine best of the both worlds, that is precisely what the Iceberg source connector provides us
07:14There are several nice properties about the Iceberg source connector
07:15Firstly, it provides a generic solution for backfilling Flink applications without tying itself to the business logic
07:24This makes it very easy to adopt it across a variety of Flink applications
07:28Secondly, it requires very few code changes to add it to a given Flink application
07:35Another excellent property about the Iceberg source connector is that since it's based on Flink's latest FLIP-27 source APIs, it can scale horizontally to the parallelism of the Flink application without causing any data skews on any one particular reader
07:47This allows the developer to backfill the application quickly by throwing resources onto it, so that he or she can jump onto live traffic quickly
07:56And finally, we've evaluated the Iceberg source connector in production deployments, which gives us the confidence that this is indeed a viable strategy
08:06Before we dive deep into the internals of the Iceberg source, let's try to understand the mechanics of backfilling using it
08:13As you can observe, when the application needs to process from the recent past, it reads this data from the Kafka cluster, which offers very low latency
08:22On the other hand, the application requires Historically old data, which is not covered under Kafka's retention policy, it is read from the Iceberg table, which offers high throughput and thus allows one to complete the backfill as quickly as possible
08:38Let's now look into the internals of the Iceberg source connector to understand how it satisfies some of our requirements
08:43We will also go over some of the fundamental challenges that we face when building it
08:49When we set out to build the Iceberg source connector, one of the first things that we looked into was check if there was already an existing integration with Iceberg that we could Potentially leverage, we observed that in the OSS Iceberg community, there existed code to read through Iceberg table that works for both bounded use cases, such as backfilling, as well as for continuously streaming use cases
09:09However, the existing OSS source still lagged certain properties that were critical to us
09:14One, the source that existed did not support stateful Flink applications that expect events to be ingested in a given order that is very similar to Kafka, for example
09:24Now, this was a critical requirement because the phase of this would result in backfill, backfilling producing incorrect
09:29Results
09:31Secondly, this was written using Flink's old source interfaces, which had its own set of limitations, making it hard to extend
09:38For example, to support something like data ordering, because the existing integration did not satisfy all of our requirements, we set out on this journey to build this Iceberg source connector ourselves that can be primarily used for backfilling, but can also be extended in the future for other purposes, such as bootstrapping from historical state
09:57We also were convinced that writing the connector on top of FLIP 27 would be the right path forward, as we realized that Coordination between the source connector and the reader was paramount to support something like ordering and alignment between sources
10:11First, let's look at what building a source based on FLIP-27 API entails, and then let's try to connect the dots, uh, to the Iceberg source
10:18Unlike Flink's old source APIs, where the developer was only expected to implement a simple source function that runs into a task manager, the new source API involves implementing two separate components: one, a split enumerator that runs on the job manager responsible for two functions: discovering new splits and assigning them to readers as and when they request it
10:43You might wonder what a split is, and the definition of it is very flexible
10:45For example, in the case of Kafka, it can represent a Kafka partition
10:50For something like the file source, for example, uh, it represents a portion of the file
10:56So, so that is the definition of a split, and we also have to implement a split reader as part of the FLIP-27 source that actually runs inside a task manager, and the responsibility of the split reader is that it should be capable of taking a split and reading emitting records by reading the split as into it, fitting this into the Iceberg source connector, the split eliminator becomes responsible for populating splits from the table that contain the source Kafka data
11:27The eliminator also is responsible for filtering the splits that just match the backfill period
11:33When the reader requests for a split, the split eliminator assigns its splits based on the set of splits that have not been assigned in the past, and it's the responsibility of the reader now to emit records by reading the splits in the source of, uh, in the Iceberg source context
11:48You can think of a split as essentially a portion of the data file, uh, which is stored in Parquet or Arc performance issuery, okay, I know what you're all thinking, this is all a little too abstract, and it would be nice to look at this from the perspective of the code, so let's do that
12:06Let's look at implementing the Icebox split determinator, which is the more interesting component in the Iceberg source
12:11The reader, after all, just emits the cost by reading a split, and there are not very many challenges in the component
12:17Flink has made it really simple to implement an eliminator
12:21All we need to do is implement a split inhibitor interface, and this interface, as you can see on the slide, has a number of methods to deal
12:30With various behaviors related to the numerator, for the purposes of this discussion, let's restrict ourselves to just the first two methods
12:36The start is a life cycle method to do any state investigation, while handle split request, as the name suggests, is used to handle the case where a reader requests for a new split to be assigned to it
12:48From the state perspective, we just need to keep track of the pending splits, and since this is bounded backfilling, we just have to initialize that once, and that can happen as part of the start method
12:58The Iceberg client library, as you can see, provides really nice APIs to filter just the data belonging to the backfill period from the table and organizes them, and organize them to different splits
13:10Now that we've initialized the set of splits that need to be read, when a reader requests for a split, if there is a pending split in the state, we can just return that, otherwise, we can tell the reader that there is no more splits so that the reader can shut itself down
13:23On that point, well, I know what you're all thinking, that should be fairly easy to productionize, what is he going to go over for the rest of this discussion? To answer that, I would say wait a bit, there are some highly challenges which you've not talked about yet, let's go over them
13:41One of the first challenges for the Iceberg source is that applications make assumptions around the ordering of data that's ingested into them
13:48Because we want these applications to adopt backfill without any major changes to their business logic, it's important to respect these ordering guarantees
13:56Failure to maintain them would result in application producing incorrect results, making the Iceberg source useless, we obviously don't want that
14:04To understand this completely, let's look at a real production Flink application, as you can
14:09See, in the slide, this Flink application reads, uh, from the playback source
14:13Playback source contains playback events collected from the user, and it essentially emits sessions from these, uh, playback source
14:22The playback sessions are emitted into something like a Kafka sync
14:29Ah, the most important operator of this application is the sessionizer operator, and let's closely look into it to understand the kind of, uh, ordering assumptions it makes
14:39The sessionizer is just implemented as a key process function, keeping track of two states, the start and end of the playback session
14:47If you look closely into The process element method, which gets invoked upon every new playback event belonging to a given user, you realize that what it does is very simple
14:55It checks if the current event, uh, represents the start of the session, if so, it updates the start state
15:02Similarly, checks if the current event extends the current session, in that case, it texts the, uh, end check, and finally, it sets itself a timer to invoke itself to, to see if the session has not been updated in a bit of time, so that it can basically emit it, uh, to the seek
15:21So the on timer method pretty much works like, no, as we had expected, which is that it checks if the session has not been updated in a given certain threshold, if so, it marks the session as complete and emits it to the output sync, as well as clearing the state
15:37Now, if you observe the code, you realize that it makes assumptions around how the data is ingested into the system
15:44If in light of the Iceberg source that we had built earlier, where splits are assigned in a random order, this would cause issues as the water box would progress incorrectly, resulting in invalid output
15:53So even though the Iceberg source could compile and work with the existing business logic and the Flink application, it could lead to Weird runtime behaviors, and that is bad
16:04Now you think about this and you realize, okay, I know how to fix this problem
16:05I am just going to order the splits based on their injection timestamp and passing them to the readers in that order
16:13Let's try to understand this visually
16:15Let's say the enumerator requests for splits from the Iceberg table, and let's say the table returns these five splits that belong to the backfill period
16:22So the user is essentially trying in this case to backfill from minutes 0 to 15, and t equal to 5 to 10 essentially represents the split that contains data from minutes 5 to 10, as you can expect
16:37Uh, now the enumerator, all it does is essentially order these splits, which just based on the injection time stamps and ascends them, uh, again based on the injection timestamp
16:46So, in this case, uh, when the readers request for splits, they remember ascends splits 0 to 5 to 2 readers, so that they can be read
16:55Once these splits are read and the reader lets the enumerator know that these splits have been completely, then this enumerator can assign the next set of splits, which basically belong to the backbone period of five to ten minutes, okay? So, this approach should solve the ordering problem and make the application produce the same output, uh, when using the Iceberg source, but there is one problem with this approach, and the problem is that it's going, the throughput is going to be limited to the number of splits that can be assigned in parallel, and this is going to be small
17:29And the issue is that even if the developer wants to throw more resources on the backfill so that he or she can complete the backfill quickly, uh, because the number of parallel splits that can be assigned in balance is limited, he or she is going to experience a very slow backfill, and that is not desirable
17:52The easiest way to address a Throughput problem is by relaxing the order constraint
17:56So far, we've assigned splits strictly in the injection timestamp order
17:58Although that is correct, results, Flink applications typically do not require such ordering guarantees and are usually returned with some lateness in mind
18:08Let's see how we can exploit that
18:11Let's consider the previous example where the numerator had to backfill data between minutes 0 and 15, so it had these five splits that it had to assign to readers
18:21Let's assume that this application can target a lateness of up to 10 minutes
18:26Now, as part of this approach, we are Introducing a new state called injection watermark that indicates a timestamp up to which data has been fully processed, so initially you can see that it is initialized to 0, indicating that Kafka data up to minute 0 has been fully processed
18:41Now, because the application can tolerate a lateness of up to 10 minutes, splits in the time window 0 to 5, as well as splits in the time window 5 to 10, can be read in parallel, as reading them will not violate the latest constraint
18:53However, a split from time window 10 to 15 cannot be assigned till the injection watermark moves further, as leading it would violate the latest
19:02Constraint, because we only have three parallel readers, in this case, we can only assign the first three splits, even though the fourth split is also ready to be assigned
19:14Now, once these three splits are completed and the reader lets the enumerator know of that, the enumerator can now move the watermark to 5, as all data till the 5th minute has been fully read
19:28Because the injection watermark has moved to the fifth minute, this implies that data till 15th minute, five plus 10, can be read without any, without violating any lateness constraint, thus splits 5 to 10, as well as split 10 to 15, will be assigned in parallel
19:43At this point, a nice property about this approach is that it solves the throughput problem of the previous approach, while maintaining correctness for applications that can tolerate some lateness
19:56You might wonder at this point, okay, this is all great, but does Kafka ordering even look like this, and do we need to, do we not need to mimic that, since that's the source we are trying to replace in the case of backfill? So let's first address the question of what type of ordering does Kafka actually provide
20:11If you go through the Apache Kafka docs to answer this, you realize Kafka guarantees strict ordering per Partition, what this means is that for partition one, offset five will always be delivered before offset six, and offset six will be delivered before offset seven, and so on
20:28Across partitions though, there are no strict, uh, there are no ordering guarantees
20:34Even though this is the ordering that Kafka provides, most Flink applications that deal with real-time analytics, and Netflix use event time semantics and are returned with lateness in mind
20:41For these applications, the rough ordering based on injection timestamp is more than good enough for both correctness, as well as for performance
20:49Considerations: Kafka ordering isn't required for these use cases
20:53However, if you do want to guarantee cuff coloring for other use cases, then what this implies is that on the read path, on the right path, the Iceberg data will have to be partitioned along Kafka's partitioning scheme
21:08This can lead to lots and lots of small files and can become exorbitantly expensive to store
21:14Apart from being expensive, it will also hurt read performance due to the need to do a lot of IOs
21:20Thus, use cases that require cuff coding are not under the purview of our Iceberg source connector
21:24Interestingly, we don't have such use cases at Netflix, there is one more challenge with the Iceberg source, which is interesting to think about
21:33Many Flink applications consume from multiple Kafka sources, thus these applications will need to read data from multiple Iceberg tables in the case of backfill
21:42However, if one of the sources is significantly larger than the other, and if we were to backfill both the sources at the same rate, then there could be a huge watermark skew, resulting in state size explosion
21:53This manifests itself in the form of either slow checkpoints or checkpoint failures, making the backfill stack hard to operate if It's hard to operate, developers will move to other backfilling means, such as using batch applications, which are easier to operate
22:06Thus, this is something that also needs to be resolved to make backfilling using Iceberg source a true success
22:13Let's try to understand this with a concrete example
22:18In this case, we are trying to backfill data between minutes 0 and 15 for two sources, one and two
22:22As you can observe, those two have 2x more data than source 1, and hence 2x more splits than source 1
22:32Thus, source 1 will progress at approximately 2x rate as those 2, causing watermark skews and thus state size
22:38exploration, so the question is, how can we solve this problem? As this is an important problem that needs to be resolved as well, one idea we can potentially explore is coordinating between the enumerators so that their injection water marks advance similarly
22:52Let's consider this idea
22:56So one change we need to make to the previous approach of injection watermark is that we need to make this state global instead of it being local to every source inhibitor, so what this would mean is that all the source eliminators will now have to coordinate with this global injection watermark tracker before they can
23:12Assign new splits, like in the previous example
23:14Let's assume that you know this application can tolerate a latency of 10 minutes for both the sources, thus splits from 0 to 10th minute can be assigned in the case of both the enumerators, and you can see that happening in the slide
23:32However, after source 1 splits, source it splits up right fully
23:37Uh, source one enumerator cannot assign more splits to its readers because those two is still lagging behind, and the global injection watermark is still stuck at zero
23:45Thus, source one splits will have to wait for this to be updated before they can be further assigned, and This is what happens after the second set of splits are completed by source 2
23:55In this case, you can see source 2 is trying to read splits up to minute 10, these three splits, so the globalization watermark is updated to, uh, five, uh, after these three splits are in by source two, and this gives both the eliminators room to allocate splits up to the 15th minute, and this pretty much solves the problem of alignment between the sources, and you can see both the sources pretty much updating their watermarks at a similar rate
24:28To summarize, the Icebox source involves three different, intermittently different, components, the split enumerator is responsible for discovering splits and assigning splits to the readers as and when they request them
24:39It also lets the watermark tracker know about new splits that have been discovered, so that the watermark tracker can keep track of the lowest injection watermark
24:51The industrial watermark tracker also lets the enumerator know in case of watermark updates, uh, so that the enumerator can go ahead and assign new splits based on these updates
25:03Finally, the reader is responsible for taking a split that has been assigned to it and emitting records from the split, thank You said that was a great technical overview
25:13For the next step, I'll show you how to enable Iceberg backfill for a Flink application
25:19Let's use our Mic for example
25:21Here, you have the impression and playback real-time events in Kafka topics, and the RMI production job reads from those topics and writes to RMI output
25:31At the same time, those real-time events are routed to Expert tables, and we set up a backfill stack that reads from those tables instead and writes to the same output
25:42Next, let's take a look at the code changes you needed to add Expert source to your application
25:47Here are some sample code from
25:52RMI app, and the impression source and playback source already defined here, all you need to do is to add a few lines of code to add Iceberg source
25:57As you can see, the in-memory representation of the expert source is consistent with the Kafka source, so you don't need to make any changes to the processing steps
26:04Later, you also need to make a few changes to your configuration
26:11Here, I define the backfill impression source to specify which aspect table to read from
26:16I also added a dynamic source selector, so I can easily switch between live source and background source later
26:24Here is how RMI backfill job looks like on
26:26Netflix internal Flink UI, as you can see here, I can easily switch between Kafka as a source through UI, and if I want to backfill, I just need to configure the start timestamp and end timestamp of the backfilling window
26:39I also have the option to choose to read from one or more regions as well when backfilling RMI data
26:45We were able to process 24-hour data within 5 hours
26:51We also did many rounds of data validation between production and backfill, and we found the backfill output matches up to 99.9% with production output
26:59The minor description here has to do with the duplication logic in RMI ER9, we merge
27:05Multiple impression records together if they share the same key and take the minimum event timestamp, therefore, depending on your backfield window, you might have a few impression records that have slightly different impression timestamp, but the mana description is very trivial to us
27:25Here are some lessons we learned from backfilling RMI
27:26The backfilling window depends on Flink logic
27:28In RMI's case, it locks up to one hour of lateness, and also we hold impression and playback data in state for six hours, so in order to backfill whatever RMI data, we need to process eight hours of data if you're flying
27:46application, have any even ordering requirement, you need to set max misalignment threshold
27:52For our match case, we set this configuration to be 15 minutes, so that the event will arrive no longer than 15 minutes late, and also backfilling job needs tuning separately from production job, because backfilling job will have source events arrive at higher volume per second
28:10Therefore, you might want to adjust your cluster setting, so that you can achieve higher throughput and backfill more quickly
28:20At the end, let's take a look at all the benefits that worksource has to offer, you can use the same Flink
28:25application both for production and backfilling, and you don't need to make any change to your business logic
28:30It's very easy to set up, just add a few lines of code to add aspect source to your application
28:37Because iceberg source scales horizontally, you can backfill large historical data very quickly
28:43Last one, but not least, is cost efficient
28:46Retaining 30 days of data previously cost about 93 million a year in Kafka, but in iceberg, it will only cost about 2 million a year
28:55At the end, I want to share a few items on the roadmap for iceberg source for those applications that do not require sub-second latency, we Want to provide support for continuous streaming aspect source, so that you can process incremental updates from source in real time in your Flink application
29:12Secondly, we want to support hybrid streaming, so you can bootstrap your application with historical data from Iceberg table and continue to consume live data
29:23There are still some blank applications that require strict Kafka ordering, such as CDC app, and we plan to support those use cases in the future as well
29:33That's everything we want to cover for today
29:35Thank you everyone for joining us, and please let us know if you have any questions.