<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Netflix Technology Blog on Medium]]></title>
        <description><![CDATA[Stories by Netflix Technology Blog on Medium]]></description>
        <link>https://medium.com/@netflixtechblog?source=rss-c3aeaf49d8a4------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*BJWRqfSMf9Da9vsXG9EBRQ.jpeg</url>
            <title>Stories by Netflix Technology Blog on Medium</title>
            <link>https://medium.com/@netflixtechblog?source=rss-c3aeaf49d8a4------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 24 Dec 2024 11:47:31 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@netflixtechblog/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Introducing Configurable Metaflow]]></title>
            <link>https://netflixtechblog.com/introducing-configurable-metaflow-d2fb8e9ba1c6?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/d2fb8e9ba1c6</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[mlops]]></category>
            <category><![CDATA[metaflow]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Fri, 20 Dec 2024 07:10:37 GMT</pubDate>
            <atom:updated>2024-12-20T07:10:37.289Z</atom:updated>
            <content:encoded><![CDATA[<p><a href="https://www.linkedin.com/in/david-j-berg/"><em>David J. Berg</em></a>*<em>, </em><a href="https://www.linkedin.com/in/david-casler-05a5278/"><em>David Casler</em></a>^, <a href="https://www.linkedin.com/in/romain-cledat-4a211a5/"><em>Romain Cledat</em></a>*<em>, </em><a href="https://www.linkedin.com/in/qian-huang-emma/"><em>Qian Huang</em></a>*<em>, </em><a href="https://www.linkedin.com/in/rui-lin-483a83111/"><em>Rui Lin</em></a>*<em>, </em><a href="https://www.linkedin.com/in/nissanpow/"><em>Nissan Pow</em></a>*<em>, </em><a href="https://www.linkedin.com/in/nurcansonmez/"><em>Nurcan Sonmez</em></a>*<em>, </em><a href="https://www.linkedin.com/in/shashanksrikanth/"><em>Shashank Srikanth</em></a>*<em>, </em><a href="https://www.linkedin.com/in/chaoying-wang/"><em>Chaoying Wang</em></a>*<em>, </em><a href="https://www.linkedin.com/in/reginalw/"><em>Regina Wang</em></a>*<em>, </em><a href="https://www.linkedin.com/in/zitingyu/"><em>Darin Yu</em></a>*<br>*: Model Development Team, Machine Learning Platform<br>^: Content Demand Modeling Team</p><p>A month ago at QConSF, we showcased how <a href="https://qconsf.com/presentation/nov2024/supporting-diverse-ml-systems-netflix">Netflix utilizes Metaflow to power a diverse set of ML and AI use cases</a>, managing thousands of unique Metaflow flows. This followed a previous <a href="https://netflixtechblog.com/supporting-diverse-ml-systems-at-netflix-2d2e6b6d205d">blog</a> on the same topic. Many of these projects are under constant development by dedicated teams with their own business goals and development best practices, such as the system that <a href="https://netflixtechblog.com/supporting-content-decision-makers-with-machine-learning-995b7b76006f">supports our content decision makers</a>, or the system that ranks which language subtitles are most valuable for a specific piece of content.</p><p>As a central ML and AI platform team, our role is to empower our partner teams with tools that maximize their productivity and effectiveness, while adapting to their specific needs (not the other way around). This has been a guiding design principle with <a href="https://netflixtechblog.com/open-sourcing-metaflow-a-human-centric-framework-for-data-science-fa72e04a5d9">Metaflow since its inception</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XrOVl25ZLx8_4nHLRxNgDg.png" /><figcaption>Metaflow infrastructure stack</figcaption></figure><p>Standing on the shoulders of our extensive cloud infrastructure, Metaflow facilitates easy access to data, compute, and <a href="https://netflixtechblog.com/maestro-netflixs-workflow-orchestrator-ee13a06f9c78">production-grade workflow orchestration</a>, as well as built-in best practices for common concerns such as <a href="https://docs.metaflow.org/scaling/tagging">collaboration</a>, <a href="https://docs.metaflow.org/metaflow/basics#artifacts">versioning</a>, <a href="https://docs.metaflow.org/scaling/dependencies">dependency management</a>, and <a href="https://outerbounds.com/blog/metaflow-dynamic-cards">observability</a>, which teams use to setup ML/AI experiments and systems that work for them. As a result, Metaflow users at Netflix have been able to run millions of experiments over the past few years without wasting time on low-level concerns.</p><h3>A long standing FAQ: configurable flows</h3><p>While Metaflow aims to be un-opinionated about some of the upper levels of the stack, some teams within Netflix have developed their own opinionated tooling. As part of Metaflow’s adaptation to their specific needs, we constantly try to understand what has been developed and, more importantly, what gaps these solutions are filling.</p><p>In some cases, we determine that the gap being addressed is very team specific, or too opinionated at too high a level in the stack, and we therefore decide to not develop it within Metaflow. In other cases, however, we realize that we can develop an underlying construct that aids in filling that gap. Note that even in that case, we do not always aim to completely fill the gap and instead focus on extracting a more general lower level concept that can be leveraged by that particular user but also by others. One such recurring pattern we noticed at Netflix is the need to deploy sets of closely related flows, often as part of a larger pipeline involving table creations, ETLs, and deployment jobs. Frequently, practitioners want to <a href="https://docs.metaflow.org/production/coordinating-larger-metaflow-projects">experiment with variants</a> of these flows, testing new data, new parameterizations, or new algorithms, while keeping the overall structure of the flow or flows intact.</p><p>A natural solution is to make flows configurable using configuration files, so variants can be defined without changing the code. Thus far, there hasn’t been a built-in solution for configuring flows, so teams have built their bespoke solutions leveraging Metaflow’s <a href="https://docs.metaflow.org/metaflow/basics#advanced-parameters">JSON-typed Parameters</a>, <a href="https://docs.metaflow.org/scaling/data#data-in-local-files">IncludeFile</a>, and <a href="https://docs.metaflow.org/production/scheduling-metaflow-flows/scheduling-with-aws-step-functions#deploy-time-parameters">deploy-time Parameters</a> or deploying their own home-grown solution (often with great pain). However, none of these solutions make it easy to configure all aspects of the flow’s behavior, decorators in particular.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3f9q7PZgxYX8rRygIOWXyA.png" /><figcaption>Requests for a feature like Metaflow Config</figcaption></figure><p>Outside Netflix, we have seen similar frequently asked questions on the <a href="http://chat.metaflow.org">Metaflow community Slack</a> as shown in the user quotes above:</p><ul><li>how can I adjust <a href="https://docs.metaflow.org/scaling/remote-tasks/requesting-resources">the @resource requirements</a>, such as CPU or memory, without having to hardcode the values in my flows?</li><li>how to adjust <a href="https://docs.metaflow.org/production/scheduling-metaflow-flows/scheduling-with-argo-workflows#time-based-triggering">the triggering @schedule</a> programmatically, so our production and staging deployments can run at different cadences?</li></ul><h3>New in Metaflow: Configs!</h3><p>Today, to answer the FAQ, we introduce a new — small but mighty — feature in Metaflow: <a href="https://docs.metaflow.org/metaflow/configuring-flows/introduction">a Config object</a>. Configs complement the existing Metaflow constructs of artifacts and Parameters, by allowing you to configure all aspects of the flow, decorators in particular, prior to any run starting. At the end of the day, artifacts, Parameters and Configs are all stored as artifacts by Metaflow but they differ in when they are persisted as shown in the diagram below:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*L-klklqt1n9LKXG0jh-fTw.png" /><figcaption>Different data artifacts in Metaflow</figcaption></figure><p>Said another way:</p><ul><li>An<strong> artifact</strong> is resolved and persisted to the datastore at the end of each task.</li><li>A<strong> parameter</strong> is resolved and persisted at the start of a run; it can therefore be modified up to that point. One common use case is to use <a href="https://docs.metaflow.org/production/event-triggering">triggers</a> to pass values to a run right before executing. Parameters can only be used within your step code.</li><li>A<strong> config</strong> is resolved and persisted when the flow is deployed. When using a scheduler such as <a href="https://docs.metaflow.org/production/scheduling-metaflow-flows/scheduling-with-argo-workflows">Argo Workflows</a>, deployment happens when create’ing the flow. In the case of a local run, “deployment” happens just prior to the execution of the run — think of “deployment” as gathering all that is needed to run the flow. Unlike parameters, configs can be used more widely in your flow code, particularly, they can be used in step or flow level decorators as well as to set defaults for parameters. Configs can of course also be used within your flow.</li></ul><p>As an example, you can specify a Config that reads a pleasantly human-readable configuration file, formatted as <a href="https://toml.io/en/">TOML</a>. The Config specifies a triggering ‘@schedule’ and ‘@resource’ requirements, as well as application-specific parameters for this specific deployment:</p><pre>[schedule]<br>cron = &quot;0 * * * *&quot;<br><br>[model]<br>optimizer = &quot;adam&quot;<br>learning_rate = 0.5<br><br>[resources]<br>cpu = 1</pre><p>Using the newly released Metaflow 2.13, you can configure a flow with a Config like above, as demonstrated by this flow:</p><pre>import pprint<br>from metaflow import FlowSpec, step, Config, resources, config_expr, schedule<br><br>@schedule(cron=config_expr(&quot;config.schedule.cron&quot;))<br>class ConfigurableFlow(FlowSpec):<br>    config = Config(&quot;config&quot;, default=&quot;myconfig.toml&quot;, parser=&quot;tomllib.loads&quot;)<br><br>    @resources(cpu=config.resources.cpu)<br>    @step<br>    def start(self):<br>        print(&quot;Config loaded:&quot;)<br>        pprint.pp(self.config)<br>        self.next(self.end)<br><br>    @step<br>    def end(self):<br>        pass<br><br>if __name__ == &quot;__main__&quot;:<br>    ConfigurableFlow()</pre><p>There is a lot going on in the code above, a few highlights:</p><ul><li>you can refer to configs <em>before</em> they have been defined using ‘config_expr’.</li><li>you can define arbitrary <a href="https://docs.metaflow.org/metaflow/configuring-flows/parsing-configs">parsers</a> — using a string means the parser doesn’t even have to be present remotely!</li></ul><p>From the developer’s point of view, Configs behave like dictionary-like artifacts. For convenience, they support the dot-syntax (when possible) for accessing keys, making it easy to access values in a nested configuration. You can also unpack the whole Config (or a subtree of it) with Python’s standard dictionary unpacking syntax, ‘**config’. The standard dictionary subscript notation is also available.</p><p>Since Configs turn into dictionary artifacts, they get versioned and persisted automatically as artifacts. You can <a href="https://docs.metaflow.org/metaflow/client">access Configs of any past runs easily through the Client API</a>. As a result, your data, models, code, Parameters, Configs, and <a href="https://docs.metaflow.org/scaling/dependencies">execution environments</a> are all stored as a consistent bundle — neatly organized in <a href="https://docs.metaflow.org/scaling/tagging">Metaflow namespaces</a> — paving the way for easily reproducible, consistent, low-boilerplate, and now easily configurable experiments and robust production deployments.</p><h3>More than a humble config file</h3><p>While you can get far by accompanying your flow with a simple config file (stored in your favorite format, thanks to <a href="https://docs.metaflow.org/metaflow/configuring-flows/parsing-configs">user-definable parsers</a>), Configs unlock a number of advanced use cases. Consider these examples from the updated documentation:</p><ul><li>You can <a href="https://docs.metaflow.org/metaflow/configuring-flows/basic-configuration#mixing-configs-and-parameters"><strong>choose the right level of runtime configurability</strong></a> versus fixed deployments by mixing Parameters and Configs. For instance, you can use a Config to define a default value for a parameter which can be <a href="https://docs.metaflow.org/production/event-triggering/external-events#passing-parameters-in-events">overridden by a real-time event</a> as a run is triggered.</li><li>You can define a custom parser to <a href="https://docs.metaflow.org/metaflow/configuring-flows/parsing-configs#validating-configs-with-pydantic"><strong>validate the configuration</strong></a>, e.g. using the popular <a href="https://docs.pydantic.dev/latest/">Pydantic</a> library.</li><li>You are not limited to using a single file: you can leverage a configuration manager like <a href="https://omegaconf.readthedocs.io/en/2.3_branch/">OmegaConf</a> or <a href="https://hydra.cc/">Hydra</a> to <a href="https://docs.metaflow.org/metaflow/configuring-flows/parsing-configs#advanced-configurations-with-omegaconf"><strong>manage a hierarchy of cascading configuration files</strong></a>. You can also use a domain-specific tool for generating Configs, such as Netflix’s <em>Metaboost</em> which we cover below.</li><li>You can also <a href="https://docs.metaflow.org/metaflow/configuring-flows/custom-parsers#generating-configs-programmatically"><strong>generate configurations on the fly</strong></a>, e.g. fetch Configs from an external service, or inspect the execution environment, such as the current GIT branch, and include it as an extra piece of context in runs.</li></ul><p>A major benefit of Config over previous more hacky solutions for configuring flows is that they work seamlessly with other features of Metaflow: you can run steps remotely and deploy flows to production, even when relying on custom parsers, without having to worry about packaging Configs or parsers manually or keeping Configs consistent across tasks. Configs also work with the <a href="https://docs.metaflow.org/metaflow/managing-flows/runner">Runner</a> and <a href="https://docs.metaflow.org/metaflow/managing-flows/deployer">Deployer</a>.</p><h3>The Hollywood principle: don’t call us, we’ll call you</h3><p>When used in conjunction with a configuration manager like <a href="https://hydra.cc">Hydra</a>, Configs enable a pattern that is highly relevant for ML and AI use cases: orchestrating experiments over multiple configurations or sweeping over parameter spaces. While Metaflow has always supported <a href="https://docs.outerbounds.com/grid-search-with-metaflow/">sweeping over parameter grids</a> easily using foreaches, it hasn’t been easily possible to alter the flow itself, e.g. to change <a href="https://docs.metaflow.org/api/step-decorators/resources">@resources</a> or <a href="https://docs.metaflow.org/api/step-decorators/conda">@pypi/@conda</a> dependencies for every experiment.</p><p>In a typical case, you trigger a Metaflow flow that consumes a configuration file, changing <em>how</em> a run behaves. With Hydra, you can <a href="https://en.wikipedia.org/wiki/Inversion_of_control">invert the control</a>: it is Hydra that decides <em>what</em> gets run based on a configuration file. Thanks to Metaflow’s new <a href="https://docs.metaflow.org/metaflow/managing-flows/runner">Runner</a> and <a href="https://docs.metaflow.org/metaflow/managing-flows/deployer">Deployer</a> APIs, you can create a Hydra app that operates Metaflow programmatically — for instance, to deploy and execute hundreds of variants of a flow in a large-scale experiment.</p><p><a href="https://docs.metaflow.org/metaflow/configuring-flows/config-driven-experimentation">Take a look at two interesting examples of this pattern</a> in the documentation. As a teaser, this video shows Hydra orchestrating deployment of tens of Metaflow flows, each of which benchmarks PyTorch using a varying number of CPU cores and tensor sizes, updating a visualization of the results in real-time as the experiment progresses:</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2F4lj8iMvw7pU%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D4lj8iMvw7pU&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2F4lj8iMvw7pU%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/e1e6d120dc74e75d9e52956b6cee7efe/href">https://medium.com/media/e1e6d120dc74e75d9e52956b6cee7efe/href</a></iframe><h3>Metaboosting Metaflow — based on a true story</h3><p>To give a motivating example of what configurations look like at Netflix in practice, let’s consider <em>Metaboost</em>, an internal Netflix CLI tool that helps ML practitioners manage, develop and execute their cross-platform projects, somewhat similar to the open-source Hydra discussed above but with specific integrations to the Netflix ecosystem. Metaboost is an example of an opinionated framework developed by a team already using Metaflow. In fact, a part of the inspiration for introducing Configs in Metaflow came from this very use case.</p><p>Metaboost serves as a single interface to three different internal platforms at Netflix that manage ETL/Workflows (<a href="https://netflixtechblog.com/maestro-netflixs-workflow-orchestrator-ee13a06f9c78"><em>Maestro</em></a>), Machine Learning Pipelines (<a href="https://docs.metaflow.org"><em>Metaflow</em></a>) and Data Warehouse Tables (<em>Kragle</em>). In this context, having a single configuration system to manage a ML project holistically gives users increased project coherence and decreased project risk.</p><h4>Configuration in Metaboost</h4><p>Ease of configuration and templatizing are core values of Metaboost. Templatizing in Metaboost is achieved through the concept of <em>bindings</em>, wherein we can <em>bind</em> a Metaflow pipeline to an arbitrary label, and then create a corresponding bespoke configuration for that label. The binding-connected configuration is then merged into a global set of configurations containing such information as GIT repository, branch, etc. Binding a Metaflow, will also signal to Metaboost that it should instantiate the Metaflow flow once per binding into our orchestration cluster.</p><p>Imagine a ML practitioner on the Netflix Content ML team, sourcing features from hundreds of columns in our data warehouse, and creating a multitude of models against a <em>growing</em> suite of metrics. When a brand new content metric comes along, with Metaboost, the first version of the metric’s predictive model can easily be created by simply swapping the target column against which the model is trained.</p><p>Subsequent versions of the model will result from experimenting with hyper parameters, tweaking feature engineering, or conducting feature diets. Metaboost’s bindings, and their integration with Metaflow Configs, can be leveraged to scale the number of experiments as fast as a scientist can create experiment based configurations.</p><h4>Scaling experiments with Metaboost bindings — backed by Metaflow Config</h4><p>Consider a Metaboost ML project named `demo` that creates and loads data to custom tables (ETL managed by Maestro), and then trains a simple model on this data (ML Pipeline managed by Metaflow). The project structure of this repository might look like the following:</p><pre>├── metaflows<br>│   ├── custom                               -&gt; custom python code, used by<br>|   |   |                                       Metaflow<br>│   │   ├── data.py<br>│   │   └── model.py<br>│   └── training.py                          -&gt; defines our Metaflow pipeline<br>├── schemas<br>│   ├── demo_features_f.tbl.yaml             -&gt; table DDL, stores our ETL<br>|   |                                           output, Metaflow input<br>│   └── demo_predictions_f.tbl.yaml          -&gt; table DDL,<br>|                                               stores our Metaflow output<br>├── settings<br>│   ├── settings.configuration.EXP_01.yaml   -&gt; defines the additive<br>|   |                                           config for Experiment 1<br>│   ├── settings.configuration.EXP_02.yaml   -&gt; defines the additive<br>|   |                                           config for Experiment 2<br>│   ├── settings.configuration.yaml          -&gt; defines our global<br>|   |                                           configuration<br>│   └── settings.environment.yaml            -&gt; defines parameters based on<br>|                                               git branch (e.g. READ_DB)<br>├── tests<br>├── workflows<br>│   ├── sql<br>│   ├── demo.demo_features_f.sch.yaml        -&gt; Maestro workflow, defines ETL<br>│   └── demo.main.sch.yaml                   -&gt; Maestro workflow, orchestrates<br>|                                               ETLs and Metaflow<br>└── metaboost.yaml                           -&gt; defines our project for<br>                                                Metaboost</pre><p>The configuration files in the settings directory above contain the following YAML files:</p><pre># settings.configuration.yaml (global configuration)<br>model:<br>  fit_intercept: True<br>conda:<br>  numpy: &#39;1.22.4&#39;<br>  &quot;scikit-learn&quot;: &#39;1.4.0&#39;</pre><pre># settings.configuration.EXP_01.yaml<br>target_column: metricA<br>features:<br>  - runtime<br>  - content_type<br>  - top_billed_talent</pre><pre># settings.configuration.EXP_02.yaml<br>target_column: metricA<br>features:<br>  - runtime<br>  - director<br>  - box_office</pre><p>Metaboost will merge each experiment configuration (<em>*.EXP*.yaml</em>) into the global configuration (settings.configuration.yaml) <em>individually</em> at Metaboost command initialization. Let’s take a look at how Metaboost combines these configurations with a Metaboost command:</p><pre>(venv-demo) ~/projects/metaboost-demo [branch=demoX] <br>$ metaboost metaflow settings show --yaml-path=configuration<br><br>binding=EXP_01:<br>model:                     -&gt; defined in setting.configuration.yaml (global)<br>  fit_intercept: true<br>conda:                     -&gt; defined in setting.configuration.yaml (global)<br>  numpy: 1.22.4<br>  &quot;scikit-learn&quot;: 1.4.0<br>target_column: metricA     -&gt; defined in setting.configuration.EXP_01.yaml<br>features:                  -&gt; defined in setting.configuration.EXP_01.yaml<br>- runtime<br>- content_type<br>- top_billed_talent<br><br>binding=EXP_02:<br>model:                     -&gt; defined in setting.configuration.yaml (global)<br>  fit_intercept: true<br>conda:                     -&gt; defined in setting.configuration.yaml (global)<br>  numpy: 1.22.4<br>  &quot;scikit-learn&quot;: 1.4.0<br>target_column: metricA     -&gt; defined in setting.configuration.EXP_02.yaml<br>features:                  -&gt; defined in setting.configuration.EXP_02.yaml<br>- runtime<br>- director<br>- box_office</pre><p>Metaboost understands it should deploy/run two independent instances of training.py — one for the EXP_01 binding and one for the EXP_02 binding. You can also see that Metaboost is aware that the tables and ETL workflows are <em>not bound</em>, and should only be deployed once. These details of which artifacts to bind and which to leave unbound are encoded in the project’s top-level metaboost.yaml file.</p><pre>(venv-demo) ~/projects/metaboost-demo [branch=demoX] <br>$ metaboost project list<br><br>Tables (metaboost table list):<br>schemas/demo_predictions_f.tbl.yaml (binding=default):<br>    table_path=prodhive/demo_db/demo_predictions_f<br>schemas/demo_features_f.tbl.yaml (binding=default):<br>    table_path=prodhive/demo_db/demo_features_f<br><br>Workflows (metaboost workflow list):<br>workflows/demo.demo_features_f.sch.yaml (binding=default):<br>    cluster=sandbox, workflow.id=demo.branch_demox.demo_features_f<br>workflows/demo.main.sch.yaml (binding=default):<br>    cluster=sandbox, workflow.id=demo.branch_demox.main<br><br>Metaflows (metaboost metaflow list):<br>metaflows/training.py (binding=EXP_01): -&gt; EXP_01 instance of training.py<br>    cluster=sandbox, workflow.id=demo.branch_demox.EXP_01.training   <br>metaflows/training.py (binding=EXP_02): -&gt; EXP_02 instance of training.py<br>    cluster=sandbox, workflow.id=demo.branch_demox.EXP_02.training</pre><p>Below is a simple Metaflow pipeline that fetches data, executes feature engineering, and trains a LinearRegression model. The work to integrate Metaboost Settings into a user’s Metaflow pipeline (implemented using Metaflow Configs) is as easy as adding a single mix-in to the FlowSpec definition:</p><pre>from metaflow import FlowSpec, Parameter, conda_base, step<br>from custom.data import feature_engineer, get_data<br>from metaflow.metaboost import MetaboostSettings<br><br>@conda_base(<br>    libraries=MetaboostSettings.get_deploy_time_settings(&quot;configuration.conda&quot;)<br>)<br>class DemoTraining(FlowSpec, MetaboostSettings):<br>    prediction_date = Parameter(&quot;prediction_date&quot;, type=int, default=-1)<br><br>    @step<br>    def start(self):<br>        # get show_settings() for free with the mixin<br>        # and get convenient debugging info<br>        self.show_settings(exclude_patterns=[&quot;artifact*&quot;, &quot;system*&quot;])<br><br>        self.next(self.get_features)<br><br>    @step<br>    def get_features(self):<br>        # feature engineers on our extracted data<br>        self.fe_df = feature_engineer(<br>            # loads data from our ETL pipeline<br>            data=get_data(prediction_date=self.prediction_date),<br>            features=self.settings.configuration.features +<br>                [self.settings.configuration.target_column]<br>        )<br><br>        self.next(self.train)<br><br>    @step<br>    def train(self):<br>        from sklearn.linear_model import LinearRegression<br><br>        # trains our model<br>        self.model = LinearRegression(<br>            fit_intercept=self.settings.configuration.model.fit_intercept<br>        ).fit(<br>            X=self.fe_df[self.settings.configuration.features],<br>            y=self.fe_df[self.settings.configuration.target_column]<br>        )<br>        print(f&quot;Fit slope: {self.model.coef_[0]}&quot;)<br>        print(f&quot;Fit intercept: {self.model.intercept_}&quot;)<br><br>        self.next(self.end)<br><br>    @step<br>    def end(self):<br>        pass<br><br><br>if __name__ == &quot;__main__&quot;:<br>    DemoTraining()</pre><p>The Metaflow Config is added to the FlowSpec by mixing in the MetaboostSettings class. Referencing a configuration value is as easy as using the dot syntax to drill into whichever parameter you’d like.</p><p>Finally let’s take a look at the output from our sample Metaflow above. We execute experiment EXP_01 with</p><pre>metaboost metaflow run --binding=EXP_01</pre><p>which upon execution will merge the configurations into a single <em>settings</em> file (shown previously) and serialize it as a yaml file to the <em>.metaboost/settings/compiled/</em> directory.</p><p>You can see the actual command and args that were sub-processed in the <em>Metaboost Execution</em> section below. Please note the <strong>–config</strong> argument pointing to the serialized yaml file, and then subsequently accessible via <strong>self.settings</strong>. Also note the convenient printing of configuration values to stdout during the start step using a mixed in function named <strong>show_settings()</strong>.</p><pre>(venv-demo) ~/projects/metaboost-demo [branch=demoX] <br>$ metaboost metaflow run --binding=EXP_01<br><br>Metaboost Execution: <br> - python3.10 /root/repos/cdm-metaboost-irl/metaflows/training.py<br>   --no-pylint --package-suffixes=.py --environment=conda<br>   --config settings<br>   .metaboost/settings/compiled/settings.branch_demox.EXP_01.training.mP4eIStG.yaml<br>   run --prediction_date20241006<br><br>Metaflow 2.12.39+nflxfastdata(2.13.5);nflx(2.13.5);metaboost(0.0.27)<br>  executing DemoTraining for user:dcasler<br>Validating your flow...<br>    The graph looks good!<br>Bootstrapping Conda environment... (this could take a few minutes)<br>All packages already cached in s3.<br>All environments already cached in s3.<br><br>Workflow starting (run-id 50), see it in the UI at<br>https://metaflowui.prod.netflix.net/DemoTraining/50<br><br>[50/start/251640833] Task is starting.<br>[50/start/251640833] Configuration Values:<br>[50/start/251640833]   settings.configuration.conda.numpy            = 1.22.4<br>[50/start/251640833]   settings.configuration.features.0             = runtime<br>[50/start/251640833]   settings.configuration.features.1             = content_type<br>[50/start/251640833]   settings.configuration.features.2             = top_billed_talent<br>[50/start/251640833]   settings.configuration.model.fit_intercept    = True<br>[50/start/251640833]   settings.configuration.target_column          = metricA<br>[50/start/251640833]   settings.environment.READ_DATABASE            = data_warehouse_prod<br>[50/start/251640833]   settings.environment.TARGET_DATABASE          = demo_dev<br>[50/start/251640833] Task finished successfully.<br><br>[50/get_features/251640840] Task is starting.<br>[50/get_features/251640840] Task finished successfully.<br><br>[50/train/251640854] Task is starting.<br>[50/train/251640854] Fit slope: 0.4702672504331096<br>[50/train/251640854] Fit intercept: -6.247919678070083<br>[50/train/251640854] Task finished successfully.<br><br>[50/end/251640868] Task is starting.<br>[50/end/251640868] Task finished successfully.<br><br>Done! See the run in the UI at<br>https://metaflowui.prod.netflix.net/DemoTraining/50</pre><h4>Takeaways</h4><p>Metaboost is an integration tool that aims to ease the project development, management and execution burden of ML projects at Netflix. It employs a configuration system that combines git based parameters, global configurations and arbitrarily <em>bound</em> configuration files for use during execution against internal Netflix platforms.</p><p>Integrating this configuration system with the new Config in Metaflow is incredibly simple (by design), only requiring users to add a mix-in class to their FlowSpec — <a href="https://docs.metaflow.org/metaflow/configuring-flows/custom-parsers#including-default-configs-in-flows">similar to this example in Metaflow documentation</a> — and then reference the configuration values in steps or decorators. The example above templatizes a training Metaflow for the sake of experimentation, but users could just as easily use bindings/configs to templatize their flows across target metrics, business initiatives or any other arbitrary lines of work.</p><h3>Try it at home</h3><p>It couldn’t be easier to get started with Configs! Just</p><pre>pip install -U metaflow</pre><p>to get the latest version and <a href="https://docs.metaflow.org/metaflow/configuring-flows/introduction">head to the updated documentation</a> for examples. If you are impatient, you can find and execute <a href="https://github.com/outerbounds/config-examples">all config-related examples in this repository</a> as well.</p><p>If you have any questions or feedback about Config (or other Metaflow features), you can reach out to us at the <a href="http://chat.metaflow.org">Metaflow community Slack</a>.</p><h3>Acknowledgments</h3><p>We would like to thank <a href="https://outerbounds.co">Outerbounds</a> for their collaboration on this feature; for rigorously testing it and developing a repository of examples to showcase some of the possibilities offered by this feature.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d2fb8e9ba1c6" width="1" height="1" alt=""><hr><p><a href="https://netflixtechblog.com/introducing-configurable-metaflow-d2fb8e9ba1c6">Introducing Configurable Metaflow</a> was originally published in <a href="https://netflixtechblog.com">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Part 1: A Survey of Analytics Engineering Work at Netflix]]></title>
            <link>https://netflixtechblog.com/part-1-a-survey-of-analytics-engineering-work-at-netflix-d761cfd551ee?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/d761cfd551ee</guid>
            <category><![CDATA[analytics-engineering]]></category>
            <category><![CDATA[analytics]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Tue, 17 Dec 2024 23:15:23 GMT</pubDate>
            <atom:updated>2024-12-17T23:15:23.415Z</atom:updated>
            <content:encoded><![CDATA[<p><em>This article is the first in a multi-part series sharing a breadth of Analytics Engineering work at Netflix, recently presented as part of our annual internal Analytics Engineering conference. We kick off with a few topics focused on how we’re empowering Netflix to efficiently produce and effectively deliver high quality, actionable analytic insights across the company. Subsequent posts will detail examples of exciting analytic engineering domain applications and aspects of the technical craft.</em></p><p>At Netflix, we seek to entertain the world by ensuring our members find the shows and movies that will thrill them. Analytics at Netflix powers everything from understanding what content will excite and bring members back for more to how we should produce and distribute a content slate that maximizes member joy. Analytics Engineers deliver these insights by establishing deep business and product partnerships; translating business challenges into solutions that unblock critical decisions; and designing, building, and maintaining end-to-end analytical systems.</p><p>Each year, we bring the Analytics Engineering community together for an Analytics Summit — a 3-day internal conference to share analytical deliverables across Netflix, discuss analytic practice, and build relationships within the community. We covered a broad array of exciting topics and wanted to spotlight a few to give you a taste of what we’re working on across Analytics Engineering at Netflix!</p><h3>DataJunction: Unifying Experimentation and Analytics</h3><p><a href="https://www.linkedin.com/in/shyiann/">Yian Shang</a>, <a href="https://www.linkedin.com/in/anhqle/">Anh Le</a></p><p>At Netflix, like in many organizations, creating and using metrics is often more complex than it should be. Metric definitions are often scattered across various databases, documentation sites, and code repositories, making it difficult for analysts and data scientists to find reliable information quickly. This fragmentation leads to inconsistencies and wastes valuable time as teams end up reinventing metrics or seeking clarification on definitions that should be standardized and readily accessible.</p><p>Enter <a href="https://datajunction.io/">DataJunction</a> (DJ). DJ acts as a central store where metric definitions can live and evolve. Once a metric owner has registered a metric into DJ, metric consumers throughout the organization can apply that same metric definition to a set of filtered records and aggregate to any dimensional grain.</p><p>As an example, imagine an analyst wanting to create a “Total Streaming Hours” metric. To add this metric to DJ, they need to provide two pieces of information:</p><ul><li>The fact table that the metric comes from:</li></ul><p>SELECT<br> account_id, country_iso_code, streaming_hours<br>FROM streaming_fact_table</p><ul><li>The metric expression:</li></ul><p>`SUM(streaming_hours)`</p><p>Then metric consumers throughout the organization can call DJ to request either the SQL or the resulting data. For example,</p><ul><li>total_streaming_hours of each account:</li></ul><p>dj.sql(metrics=[“total_streaming_hours”], dimensions=[“account_id”]))</p><ul><li>total_streaming_hours of each country:</li></ul><p>dj.sql(metrics=[“total_streaming_hours”], dimensions=[“country_iso_code”]))</p><ul><li>total_streaming_hours of each account in the US:</li></ul><p>dj.sql(metrics=[“total_streaming_hours”], dimensions=[“country_iso_code”], filters=[“country_iso_code = ‘US’”]))</p><p>The key here is that DJ can perform the dimensional join on users’ behalf. If country_iso_code doesn’t already exist in the fact table, the metric owner only needs to tell DJ that account_id is the foreign key to an `users_dimension_table` (we call this process “<a href="https://datajunction.io/docs/0.1.0/data-modeling/dimension-links/">dimension linking</a>”). DJ then can perform the joins to bring in any requested dimensions from `users_dimension_table`.</p><p>The Netflix Experimentation Platform heavily leverages this feature today by treating cell assignment as just another dimension that it asks DJ to bring in. For example, to compare the average streaming hours in cell A vs cell B, the Experimentation Platform relies on DJ to bring in “cell_assignment” as a user’s dimension (no different from country_iso_code). A metric can therefore be defined once in DJ and be made available across analytics dashboards and experimentation analysis.</p><p>DJ has a strong pedigree–there are several prior <a href="https://benn.substack.com/p/bi-by-another-name">semantic layers</a> in the industry (e.g. <a href="https://medium.com/airbnb-engineering/how-airbnb-achieved-metric-consistency-at-scale-f23cc53dea70">Minerva</a> at Airbnb; dbt Transform, Looker, and AtScale as paid solutions). DJ stands out as an <a href="https://github.com/DataJunction/dj">open source</a> solution that is actively developed and stress-tested at Netflix. We’d love to see DJ easing <em>your</em> metric creation and consumption pain points!</p><h3>LORE: How we’re democratizing analytics at Netflix</h3><p><a href="https://www.linkedin.com/in/apurvakansara/">Apurva Kansara</a></p><p>At Netflix, we rely on data and analytics to inform critical business decisions. Over time, this has resulted in large numbers of dashboard products. While such analytics products are tremendously useful, we noticed a few trends:</p><ol><li>A large portion of such products have less than 5 MAU (monthly active users)</li><li>We spend a tremendous amount of time building and maintaining business metrics and dimensions</li><li>We see inconsistencies in how a particular metric is calculated, presented, and maintained across the Data &amp; Insights organization.</li><li>It is challenging to scale such bespoke solutions to ever-changing and increasingly complex business needs.</li></ol><p>Analytics Enablement is a collection of initiatives across Data &amp; Insights all focused on empowering Netflix analytic practitioners to efficiently produce and effectively deliver high-quality, actionable insights.</p><p>Specifically, these initiatives are focused on enabling analytics rather than on the activities that produce analytics (e.g., dashboarding, analysis, research, etc.).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/625/0*gUgNHuu6yqKdfbgg" /></figure><p>As part of broad analytics enablement across all business domains, we invested in a chatbot to provide real insights to our end users using the power of LLM. One reason LLMs are well suited for such problems is that they tie the versatility of natural language with the power of data query to enable our business users to query data that would otherwise require sophisticated knowledge of underlying data models.</p><p>Besides providing the end user with an instant answer in a preferred data visualization, LORE instantly learns from the user’s feedback. This allows us to teach LLM a context-rich understanding of internal business metrics that were previously locked in custom code for each of the dashboard products.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*onXkeBFPL44KYBQB" /></figure><p>Some of the challenges we run into:</p><ul><li>Gaining user trust: To gain our end users’ trust, we focused on our model’s explainability. For example, LORE provides human-readable reasoning on how it arrived at the answer that users can cross-verify. LORE also provides a confidence score to our end users based on its grounding in the domain space.</li><li>Training: We created easy-to-provide feedback using 👍 and 👎 with a fully integrated fine-tuning loop to allow end-users to teach new domains and questions around it effectively. This allowed us to bootstrap LORE across several domains within Netflix.</li></ul><p>Democratizing analytics can unlock the tremendous potential of data for everyone within the company. With Analytics enablement and LORE, we’ve enabled our business users to truly have a conversation with the data.</p><h3>Leveraging Foundational Platform Data to enable Cloud Efficiency Analytics</h3><p><a href="https://www.linkedin.com/in/jhan-104105/?utm_source=share&amp;utm_campaign=share_via&amp;utm_content=profile">J Han</a>, <a href="https://www.linkedin.com/in/pallavi-phadnis-75280b20/">Pallavi Phadnis</a></p><p>At Netflix, we use Amazon Web Services (AWS) for our cloud infrastructure needs, such as compute, storage, and networking to build and run the streaming platform that we love. Our ecosystem enables engineering teams to run applications and services at scale, utilizing a mix of open-source and proprietary solutions. In order to understand how efficiently we operate in this diverse technological landscape, the Data &amp; Insights organization partners closely with our engineering teams to share key efficiency metrics, empowering internal stakeholders to make informed business decisions.</p><p>This is where our team, Platform DSE (Data Science Engineering), comes in to enable our engineering partners to understand what resources they’re using, how effectively they utilize those resources, and the cost associated with their resource usage. By creating curated datasets and democratizing access via a custom insights app and various integration points, downstream users can gain granular insights essential for making data-driven, cost-effective decisions for the business.</p><p>To address the numerous analytic needs in a scalable way, we’ve developed a two-component solution:</p><ol><li>Foundational Platform Data (FPD): This component provides a centralized data layer for all platform data, featuring a consistent data model and standardized data processing methodology. We work with different platform data providers to get <em>inventory</em>, <em>ownership</em>, and <em>usage</em> data for the respective platforms they own.</li><li>Cloud Efficiency Analytics (CEA): Built on top of FPD, this component offers an analytics data layer that provides time series efficiency metrics across various business use cases. Once the foundational data is ready, CEA consumes inventory, ownership, and usage data and applies the appropriate <em>business logic</em> to produce <em>cost</em> and <em>ownership attribution</em> at various granularities.</li></ol><p>As the source of truth for efficiency metrics, our team’s tenants are to provide accurate, reliable, and accessible data, comprehensive documentation to navigate the complexity of the efficiency space, and well-defined Service Level Agreements (SLAs) to set expectations with downstream consumers during delays, outages, or changes.</p><p>Looking ahead, we aim to continue onboarding platforms, striving for nearly complete cost insight coverage. We’re also exploring new use cases, such as tailored reports for platforms, predictive analytics for optimizing usage and detecting anomalies in cost, and a root cause analysis tool using LLMs.</p><p>Ultimately, our goal is to enable our engineering organization to make efficiency-conscious decisions when building and maintaining the myriad of services that allows us to enjoy Netflix as a streaming service. For more detail on our modeling approach and principles, check out <a href="https://netflixtechblog.com/cloud-efficiency-at-netflix-f2a142955f83">this post</a>!</p><p>Analytics Engineering is a key contributor to building our deep data culture at Netflix, and we are proud to have a large group of stunning colleagues that are not only applying but advancing our analytical capabilities at Netflix. The 2024 Analytics Summit continued to be a wonderful way to give visibility to one another on work across business verticals, celebrate our collective impact, and highlight what’s to come in analytics practice at Netflix.</p><p>To learn more, follow the <a href="https://research.netflix.com/research-area/analytics">Netflix Research Site</a>, and if you are also interested in entertaining the world, have a look at <a href="https://explore.jobs.netflix.net/careers">our open roles</a>!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d761cfd551ee" width="1" height="1" alt=""><hr><p><a href="https://netflixtechblog.com/part-1-a-survey-of-analytics-engineering-work-at-netflix-d761cfd551ee">Part 1: A Survey of Analytics Engineering Work at Netflix</a> was originally published in <a href="https://netflixtechblog.com">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Cloud Efficiency at Netflix]]></title>
            <link>https://netflixtechblog.com/cloud-efficiency-at-netflix-f2a142955f83?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/f2a142955f83</guid>
            <category><![CDATA[cost]]></category>
            <category><![CDATA[data-modeling]]></category>
            <category><![CDATA[infrastructure]]></category>
            <category><![CDATA[cloud-efficiency]]></category>
            <category><![CDATA[engineering]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Tue, 17 Dec 2024 22:16:44 GMT</pubDate>
            <atom:updated>2024-12-17T22:29:05.500Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>By</strong> <a href="https://www.linkedin.com/in/jhan-104105?utm_source=share&amp;utm_campaign=share_via&amp;utm_content=profile">J Han</a>, <a href="https://www.linkedin.com/in/pallavi-phadnis-75280b20/">Pallavi Phadnis</a></p><h3><strong>Context</strong></h3><p>At Netflix, we use Amazon Web Services (AWS) for our cloud infrastructure needs, such as compute, storage, and networking to build and run the streaming platform that we love. Our ecosystem enables engineering teams to run applications and services at scale, utilizing a mix of open-source and proprietary solutions. In turn, our self-serve platforms allow teams to create and deploy, sometimes custom, workloads more efficiently. This diverse technological landscape generates extensive and rich data from various infrastructure entities, from which, data engineers and analysts collaborate to provide actionable insights to the engineering organization in a continuous feedback loop that ultimately enhances the business.</p><p>One crucial way in which we do this is through the democratization of highly curated data sources that sunshine usage and cost patterns across Netflix’s services and teams. The Data &amp; Insights organization partners closely with our engineering teams to share key efficiency metrics, empowering internal stakeholders to make informed business decisions.</p><h3><strong>Data is Key</strong></h3><p>This is where our team, Platform DSE (Data Science Engineering), comes in to enable our engineering partners to understand what resources they’re using, how effectively and efficiently they use those resources, and the cost associated with their resource usage. We want our downstream consumers to make cost conscious decisions using our datasets.</p><p>To address these numerous analytic needs in a scalable way, we’ve developed a two-component solution:</p><ol><li>Foundational Platform Data (FPD): This component provides a centralized data layer for all platform data, featuring a consistent data model and standardized data processing methodology.</li><li>Cloud Efficiency Analytics (CEA): Built on top of FPD, this component offers an analytics data layer that provides time series efficiency metrics across various business use cases.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*vDQJiJUttlRSpVBo" /></figure><p><strong>Foundational Platform Data (FPD)</strong></p><p>We work with different platform data providers to get <em>inventory</em>, <em>ownership</em>, and <em>usage</em> data for the respective platforms they own. Below is an example of how this framework applies to the <a href="https://spark.apache.org/">Spark</a> platform. FPD establishes<em> data contracts</em> with producers to ensure data quality and reliability; these contracts allow the team to leverage a common data model for ownership. The standardized data model and processing promotes scalability and consistency.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cln5xplS7lpdE0KOh0LE1Q.jpeg" /></figure><p><strong>Cloud Efficiency Analytics (CEA Data)</strong></p><p>Once the foundational data is ready, CEA consumes inventory, ownership, and usage data and applies the appropriate <em>business logic</em> to produce <em>cost</em> and <em>ownership attribution</em> at various granularities. The data model approach in CEA is to compartmentalize and be <em>transparent</em>; we want downstream consumers to understand why they’re seeing resources show up under their name/org and how those costs are calculated. Another benefit to this approach is the ability to pivot quickly as new or changes in business logic is/are introduced.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*bvD7xqAO9T9m4s4G" /></figure><p>* For cost accounting purposes, we resolve assets to a single owner, or distribute costs when assets are multi-tenant. However, we do also provide usage and cost at different aggregations for different consumers.</p><h3><strong>Data Principles</strong></h3><p>As the source of truth for efficiency metrics, our team’s tenants are to provide accurate, reliable, and accessible data, comprehensive documentation to navigate the complexity of the efficiency space, and well-defined Service Level Agreements (SLAs) to set expectations with downstream consumers during delays, outages or changes.</p><p>While ownership and cost may seem straightforward, the complexity of the datasets is considerably high due to the breadth and scope of the business infrastructure and platform specific features. Services can have multiple owners, cost heuristics are unique to each platform, and the scale of infra data is large. As we work on expanding infrastructure coverage to all verticals of the business, we face a unique set of challenges:</p><p><strong>A Few Sizes to Fit the Majority</strong></p><p>Despite data contracts and a standardized data model on transforming upstream platform data into FPD and CEA, there is usually some degree of customization that is unique to that particular platform. As the centralized source of truth, we feel the constant tension of where to place the processing burden. Decision-making involves ongoing transparent conversations with both our data producers and consumers, frequent prioritization checks, and alignment with business needs as <a href="https://jobs.netflix.com/culture">informed captains</a> in this space.</p><p><strong>Data Guarantees</strong></p><p>For data correctness and trust, it’s crucial that we have audits and visibility into health metrics at each layer in the pipeline in order to investigate issues and root cause anomalies quickly. Maintaining data completeness while ensuring correctness becomes challenging due to upstream latency and required transformations to have the data ready for consumption. We continuously iterate our audits and incorporate feedback to refine and meet our SLAs.</p><p><strong>Abstraction Layers</strong></p><p>We value <a href="https://jobs.netflix.com/culture">people over process</a>, and it is not uncommon for engineering teams to build custom SaaS solutions for other parts of the organization. Although this fosters innovation and improves development velocity, it can create a bit of a conundrum when it comes to understanding and interpreting usage patterns and attributing cost in a way that makes sense to the business and end consumer. With clear inventory, ownership, and usage data from FPD, and precise attribution in the analytical layer, we aim to provide metrics to downstream users regardless of whether they utilize and build on top of internal platforms or on AWS resources directly.</p><h3><strong>Future Forward</strong></h3><p>Looking ahead, we aim to continue onboarding platforms to FPD and CEA, striving for nearly complete cost insight coverage in the upcoming year. Longer term, we plan to extend FPD to other areas of the business such as security and availability. We aim to move towards proactive approaches via predictive analytics and ML for optimizing usage and detecting anomalies in cost.</p><p>Ultimately, our goal is to enable our engineering organization to make efficiency-conscious decisions when building and maintaining the myriad of services that allow us to enjoy Netflix as a streaming service.</p><h3>Acknowledgments</h3><p>The FPD and CEA work would not have been possible without the cross functional input of many outstanding colleagues and our dedicated team building these important data assets.</p><p>—</p><p>A bit about the authors:</p><p><em>JHan enjoys nature, reading fantasy, and finding the best chocolate chip cookies and cinnamon rolls. She is adamant about writing the SQL select statement with leading commas.</em></p><p><em>Pallavi enjoys music, travel and watching astrophysics documentaries. With 15+ years working with data, she knows everything’s better with a dash of analytics and a cup of coffee!</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f2a142955f83" width="1" height="1" alt=""><hr><p><a href="https://netflixtechblog.com/cloud-efficiency-at-netflix-f2a142955f83">Cloud Efficiency at Netflix</a> was originally published in <a href="https://netflixtechblog.com">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Title Launch Observability at Netflix Scale]]></title>
            <link>https://netflixtechblog.com/title-launch-observability-at-netflix-scale-c88c586629eb?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/c88c586629eb</guid>
            <category><![CDATA[netflix]]></category>
            <category><![CDATA[observability]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Tue, 17 Dec 2024 21:54:37 GMT</pubDate>
            <atom:updated>2024-12-17T23:06:50.596Z</atom:updated>
            <content:encoded><![CDATA[<h4>Part 1: Understanding The Challenges</h4><p><strong>By:</strong> <a href="https://www.linkedin.com/in/varun-khaitan/">Varun Khaitan</a></p><p>With special thanks to my stunning colleagues: <a href="https://www.linkedin.com/in/mallikarao/">Mallika Rao</a>, <a href="https://www.linkedin.com/in/esmir-mesic/">Esmir Mesic</a>, <a href="https://www.linkedin.com/in/hugodesmarques/">Hugo Marques</a></p><h3>Introduction</h3><p>At Netflix, we manage over a thousand global content launches each month, backed by billions of dollars in annual investment. Ensuring the success and discoverability of each title across our platform is a top priority, as we aim to connect every story with the right audience to delight our members. To achieve this, we are committed to building robust systems that deliver comprehensive observability, enabling us to take full accountability for every title on our service.</p><h3>The Challenge of Title Launch Observability</h3><p>As engineers, we’re wired to track system metrics like error rates, latencies, and CPU utilization — but what about metrics that matter to a title’s success?</p><p>Consider the following example of two different Netflix Homepages:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*B4iyOBZJZEo7eW-p" /><figcaption>Sample Homepage A</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*5F9ATQbyOp99jMwJ" /><figcaption>Sample Homepage B</figcaption></figure><p>To a basic recommendation system, the two sample pages might appear equivalent as long as the viewer watches the top title. Yet, these pages couldn’t be more different. Each title represents countless hours of effort and creativity, and our systems need to honor that uniqueness.</p><p>How do we bridge this gap? How can we design systems that recognize these nuances and empower every title to shine and bring joy to our members?</p><h3>The Operational Needs of a Personalization System</h3><p>In the early days of Netflix Originals, our launch team would huddle together at midnight, manually verifying that titles appeared in all the right places. While this hands-on approach worked for a handful of titles, it quickly became clear that it couldn’t scale. As Netflix expanded globally and the volume of title launches skyrocketed, the operational challenges of maintaining this manual process became undeniable.</p><p>Operating a personalization system for a global streaming service involves addressing numerous inquiries about why certain titles appear or fail to appear at specific times and places. <br>Some examples:</p><ul><li>Why is title X not showing on the Coming Soon row for a particular member?</li><li>Why is title Y missing from the search page in Brazil?</li><li>Is title Z being displayed correctly in all product experiences as intended?</li></ul><p>As Netflix scaled, we faced the mounting challenge of providing accurate, timely answers to increasingly complex queries about title performance and discoverability. This led to a suite of fragmented scripts, runbooks, and ad hoc solutions scattered across teams — an approach that was neither sustainable nor efficient.</p><p>The stakes are even higher when ensuring every title launches flawlessly. Metadata and assets must be correctly configured, data must flow seamlessly, microservices must process titles without error, and algorithms must function as intended. The complexity of these operational demands underscored the urgent need for a scalable solution.</p><h3>Automating the Operations</h3><p>It becomes evident over time that we need to automate our operations to scale with the business. As we thought more about this problem and possible solutions, two clear options emerged.</p><h3>Option 1: Log Processing</h3><p>Log processing offers a straightforward solution for monitoring and analyzing title launches. By logging all titles as they are displayed, we can process these logs to identify anomalies and gain insights into system performance. This approach provides a few advantages:</p><ol><li><strong>Low burden on existing systems:</strong> Log processing imposes minimal changes to existing infrastructure. By leveraging logs, which are already generated during regular operations, we can scale observability without significant system modifications. This allows us to focus on data analysis and problem-solving rather than managing complex system changes.</li><li><strong>Using the source of truth:</strong> Logs serve as a reliable “source of truth” by providing a comprehensive record of system events. They allow us to verify whether titles are presented as intended and investigate any discrepancies. This capability is crucial for ensuring our recommendation systems and user interfaces function correctly, supporting successful title launches.</li></ol><p>However, taking this approach also presents several challenges:</p><ol><li><strong>Catching Issues Ahead of Time:</strong> Logging primarily addresses post-launch scenarios, as logs are generated only after titles are shown to members. To detect issues proactively, we need to simulate traffic and predict system behavior in advance. Once artificial traffic is generated, discarding the response object and relying solely on logs becomes inefficient.</li><li><strong>Appropriate Accuracy:</strong> Comprehensive logging requires services to log both included and excluded titles, along with reasons for exclusion. This could lead to an exponential increase in logged data. Utilizing probabilistic logging methods could compromise accuracy, making it difficult to ascertain whether a title’s absence in logs is due to exclusion or random chance.</li><li><strong>SLA and Cost Considerations:</strong> Our existing online logging systems do not natively support logging at the title granularity level. While reengineering these systems to accommodate this additional axis is possible, it would entail increased costs. Additionally, the time-sensitive nature of these investigations precludes the use of cold storage, which cannot meet the stringent SLAs required.</li></ol><h3>Option 2: Observability Endpoints in Our Personalization Systems</h3><p>To prioritize title launch observability, we could adopt a centralized approach. By introducing observability endpoints across all systems, we can enable real-time data flow into a dedicated microservice for title launch observability. This approach embeds observability directly into the very fabric of services managing title launches and personalization, ensuring seamless monitoring and insights. Key benefits and strategies include:</p><ol><li><strong>Real-Time Monitoring: </strong>Observability endpoints enable real-time monitoring of system performance and title placements, allowing us to detect and address issues as they arise.</li><li><strong>Proactive Issue Detection: </strong>By simulating future traffic(an aspect we call “time travel”) and capturing system responses ahead of time, we can preemptively identify potential issues before they impact our members or the business.</li><li><strong>Enhanced Accuracy:</strong> Observability endpoints provide precise data on title inclusions and exclusions, allowing us to make accurate assertions about system behavior and title visibility. It also provides us with advanced debugability information needed to fix identified issues.</li><li><strong>Scalability and Cost Efficiency:</strong> While initial implementation required some investment, this approach ultimately offers a scalable and cost-effective solution to managing title launches at Netflix scale.</li></ol><p>Choosing this option also comes with some tradeoffs:</p><ol><li><strong>Significant Initial Investment: </strong>Several systems would need to create new endpoints and refactor their codebases to adopt this new method of prioritizing launches.</li><li><strong>Synchronization Risk: </strong>There would be a potential risk that these new endpoints may not accurately represent production behavior, thus necessitating conscious efforts to ensure all endpoints remain synchronized.</li></ol><h3>Up Next</h3><p>By adopting a comprehensive observability strategy that includes real-time monitoring, proactive issue detection, and source of truth reconciliation, we’ve significantly enhanced our ability to ensure the successful launch and discovery of titles across Netflix, enriching the global viewing experience for our members. In the next part of this series, we’ll dive into how we achieved this, sharing key technical insights and details.</p><p>Stay tuned for a closer look at the innovation behind the scenes!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c88c586629eb" width="1" height="1" alt=""><hr><p><a href="https://netflixtechblog.com/title-launch-observability-at-netflix-scale-c88c586629eb">Title Launch Observability at Netflix Scale</a> was originally published in <a href="https://netflixtechblog.com">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Content Drive]]></title>
            <link>https://netflixtechblog.medium.com/content-drive-919938544e4b?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/919938544e4b</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[filesystem]]></category>
            <category><![CDATA[hybrid-cloud]]></category>
            <category><![CDATA[cloud-services]]></category>
            <category><![CDATA[cloud-storage]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Mon, 25 Nov 2024 20:54:06 GMT</pubDate>
            <atom:updated>2024-11-26T03:15:18.080Z</atom:updated>
            <content:encoded><![CDATA[<h3>Content Drive — <em>How we organize and share billions of files in Netflix studio</em></h3><p>by <a href="https://www.linkedin.com/in/esha-palta/"><strong>Esha Palta</strong></a><strong>, </strong><a href="https://www.linkedin.com/in/khetrapal/"><strong>Ankur Khetrapal</strong></a><strong>, </strong><a href="https://linkedin.com/in/shannon-heh"><strong>Shannon Heh</strong></a><strong>, </strong><a href="https://www.linkedin.com/in/isabell-lin-951a81145"><strong>Isabell Lin</strong></a><strong>, </strong><a href="https://www.linkedin.com/in/shunfei-chen-40649918"><strong>Shunfei Chen</strong></a></p><h3>Introduction</h3><p>Netflix has pioneered the idea of a Studio in the Cloud, giving artists the ability to work from different corners of the world to create stories and assets to entertain the world. Starting at the point of ingestion where data is produced out of the camera, it goes through many stages, some of which are shown below. The media undergoes comprehensive backup routines at every stage and phase of this process with frequent uploads and downloads. In order to support these processes and studio applications, we need to provide a distributed, scalable, and performant media cloud storage infrastructure.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*YtQ_KpwZohYm77nR" /><figcaption>Fig 1: Lifecycle of studio content creation</figcaption></figure><p>Shifting gears towards assets storage, all these media files are securely delivered and stored within Amazon Simple Storage Service (S3) . Netflix maintains an identity of all these objects to be addressed by storage infrastructure layers along with other essential metadata about these objects.</p><p>At the edge, where artists work with assets, the artist applications and the artists themselves expect a file/folder interface so that there can be seamless access to these files without having agents for translating these files — we want to make working with studio applications a seamless experience for both our artists. This is not just restricted to artists, but also studio workflows. A great example is asset transformations that happen during the rendering of content.</p><p>We needed a system that could provide the ability to store, manage, and track billions of these media objects while keeping a familiar file/folder interface that lets users upload freeform files and provide management capabilities such as create, update, move, copy, delete, download, share, and fetch arbitrary tree structures.</p><p>In order to do this effectively, reliably, and securely to meet the requirements of a cloud-managed globally distributed studio, our media storage platform team has built a highly scalable metadata storage service — <strong><em>Content Drive</em></strong>.</p><p>Content Drive (or CDrive) is a cloud storage solution that provides file/folder interfaces for storing, managing, and accessing the directory structure of Netflix’s media assets in a scalable and secure way. It empowers applications such as Content Hub UI to import media content (upload to S3), manage its metadata, apply lifecycle policies, and provide access control for content sharing.</p><p>In this post we will share an overview of the CDrive service.</p><h3>Features</h3><ul><li>Storing, managing and tracking billions of files and folders while retaining folder structure. Provide a familiar Google Drive-like interface which lets users upload freeform files and provide management capabilities such as create, update, move, copy, delete, download, share, and fetch arbitrary tree structures.</li><li>Provide access control for viewing, uploads and downloads of files and folders.</li><li>Collaboration/Sharing — share work-in-progress files.</li><li>Data transfer manifest and token generation — Generate download manifest and tokens for requested files/folders after verifying authorization.</li><li>Files/folders notifications — Provide change notifications for files/folders. This enables live sharing and collaboration use cases in addition to supporting dependent backend applications to complete their business workflows around data ingestion.</li></ul><h3>Architecture</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*s__JX5jCDsibC5UNU6TNKA.png" /></figure><p><strong>CDrive Components</strong></p><ul><li>REST API and DGS (GraphQL) layer that provides endpoints to create/manage files/folders, manage shares for files/folders, and get authorization tokens for files/folders upload/download.</li><li>CDrive service layer that does the actual work of creating and managing tree structure (implements operations such as create, update, copy, move, rename, delete, checksum validation, etc on files/folder structures).</li><li>Access control layer that provides user and application-based authorization for files/folders managed in CDrive.</li><li>Data Transfer layer that proxies requests to other services for transfer tracking and transfer token generation after authorization.</li><li>Persistence layer that performs the metadata reads and updates in transactions for files/folders managed in CDrive.</li><li>Event Handler that produces event notifications for users and applications to consume and take action. For example, CDrive generates an event on upload completion for a folder.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0aIprJBxlp6dlchXP8zk3Q.png" /><figcaption>Fig 3: CDrive usage example</figcaption></figure><p>Fig 3 shows a sample CDrive usage example. We can see that different users access workspaces based on their user credentials. Users can perform all file/folder-level operations on data present in their workspaces and upload/download files/folders into their workspaces.</p><h3>Design and Concepts</h3><p>CDrive stores and manages files and folder metadata in hierarchical tree structures. It allows users and applications to group files into folders and files/folders into workspaces and supports features like create/update/delete/move/copy etc.</p><p>The tree structures belong to individual workspaces (partitions) and contain folders as branches and files as leaf nodes (a folder can also be a leaf node).</p><p>CDrive uses <a href="https://www.cockroachlabs.com/docs/">CockroachDB</a> to store its metadata and directory structure. There are a few reasons why we chose CockroachDB:</p><ul><li>To provide a strong consistency guarantee on operations. The type and correctness of data are very important. CDrive maintains an <strong><em>invariant</em></strong> of a unique file path for each file/folder. This means at any point in time a file path will represent a unique CDrive node.</li><li>Need for complex queries. CDrive needs to support a variety of complex file/folder operations such as create/merge/copy/move/updateMetadata/bulkGet etc., which requires a persistence layer to perform join queries in an optimized way.</li><li>Need for distributed transactions. CockroachDB provides distributed transaction support with its internal sharded architecture. CDrive data modeling enables it to perform metadata operations in a very efficient way.</li></ul><h3>CNode</h3><p>Each file, folder or workspace is represented by a node structure in CDrive. A file path always points to a unique CNode. This means any metadata operation that modifies the file path results in new CNode getting generated and older ones moving to the deleted status. For example: every time an artist copies a file, CDrive creates a new CNode for that file path.</p><p>A CDrive node can be of the following types -</p><ul><li><strong>Root/Workspace</strong>: This is the top-level partition for creating a file/folder hierarchy per application and project using CDrive. It is analogous to the disk partition on the OS.</li><li><strong>Folder</strong>: A container for other containers or files.</li><li><strong>File</strong>: A leaf node that has a reference to a data location this CNode represents.</li><li><strong>Sequence</strong>: A special container folder for file sequences. Sequence is a special container type in CDrive that can contain millions of files under it. This is created to represent special media files such as off-camera footage, which has a range of frames that form a small clip. All frames/files in a Sequence start with the same prefix and suffix but differ in the frame number, e.g. <em>frame.##.JPG</em>. A sequence can contain arbitrary lists of frame ranges (start index and end index). The sequence can provide a summary of millions of frames without looking into each file. CDrive provides APIs with the option to expand the sequence on a get operation. Whenever a folder is uploaded, the CDrive server inspects the folder to look for sequence files and groups them into a sequence.</li><li><strong>Snapshot</strong>: A Snapshot is a special container CNode that guarantees its subtree is <em>immutable</em>. The immutable subtree is a shallow copy of metadata from a folder that is not immutable. Typically applications create a snapshot to “lock” files/folders from further mutations to represent an asset.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*iFhwKbsMeTt_70A-" /><figcaption>Fig 4. CDrive nodes hierarchy representation</figcaption></figure><h4>CNode Metadata</h4><p>Each CNode contains the attributes associated with that node. The minimum metadata present with each node is UUID, Name, Parent Id, Path, Size, Checksums, Status, and Data location. For efficiency, CNode also contains its directory path (in terms of node UUIDs as well as filename path).</p><h3>Parent-Child Hierarchy</h3><p>All CNodes maintain a <strong><em>reference</em></strong> to their parent node Id. This is how CDrive maintains the hierarchical tree structure. Parent denotes the folder relation and root node has empty parent.</p><h3>Data Location</h3><p>Each file CNode contains a link or URL to the data location where the actual data bytes for that file are stored (e.g., in S3). Multiple CNodes can reference the same data location (in case a CNode is used in copy operation). And if a CNode is moved its data location remains unchanged. A file CNode can be present in multiple physical locations. CDrive provides information about the transfer state of files in these locations (Unknown/Created/Available/Failed).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/510/0*jVGqTmlB47v5eXBU" /><figcaption>Fig 5. Nodes and data locations representation</figcaption></figure><h3>Concurrency and Consistency</h3><p>CDrive allows multiple users/applications to access shared files/folders simultaneously. CDrive uses CockroachDB serializable transactions to support this and maintains the invariant of a unique file path for each node in CDrive.</p><p>An operation such as Copy or Move propagates changes to all the children in a subtree being modified. This involves updating metadata such as parent, file path, and/or partition for all nodes in that subtree.</p><p>If any operation results in a path conflict, CDrive provides a merge option to the user to decide whether to overwrite existing paths with new node information or preserve or fail the operation.</p><h3>Access Control</h3><p>In CDrive, authorization is driven based on the partition or workspace type, as mentioned in the workspace section. A workspace owned by an application can control access to those files/folders by integrating with authorization callbacks in CDrive. On the other hand, a user has complete control over files/folders that are part of their personal workspace.</p><p>CDrive allows users to collaborate by sharing files/folders with any set of permissions or user-based access control. If a folder or top-level CNode is shared with a set of permissions, such as read/download, then this access control applies to all the children in that subtree. CDrive also allows team folder creation for collaboration among artists in different geolocations. Changes made by one artist are visible to another based on the latest state of the folder being shared.</p><p>CDrive acts as a proxy layer for other Netflix services in the cloud because it provides user-level authorization on files and folders. For every operation, CDrive gets the initial user or application context from the request and verifies whether that user or application has the required access/permissions on the set of CNodes for that operation.</p><h3>Workspace</h3><p>All tree structures in CDrive belong to a unique workspace. Workspace in CDrive is an isolated file/folder logical partition. A workspace defines the authorization model, mutability, and data lifecycle for files and folders in that partition. A workspace can be of the following types.</p><h4>User/Personal Workspace</h4><p>User workspaces are used to store work-in-progress files per production for a user. Hence, files/folders within user workspaces are considered mutable. Data retention for all files/folders in a personal workspace is temporary, and simple purge data lifecycle policies can be applied to these temporary files once production has launched, as these temporary files won’t be needed. It uses a simple authorization model to which only that user has access. A user can provide access to these nodes through the shares feature.</p><h4>Application/Project Workspace</h4><p>Application or Project workspaces are used to store finalized assets that do not need further mutations. Hence, these are immutable tree structures. It uses a federated authorization model, delegating the auth to an owner application tied to that workspace. Data lifecycle policies are a bit complicated and cannot be applied at the whole workspace level here as these workspaces contain final delivery assets that need to be kept in storage forever. Data lifecycle decisions to archive or purge are taken at the individual file/folder level. We have a <a href="https://netflixtechblog.medium.com/netflixs-media-landscape-evolution-from-3-2-1-to-cloud-storage-optimization-77e9a19171ed">blog post</a> covering the intelligent data lifecycle solution in detail <a href="https://netflixtechblog.medium.com/netflixs-media-landscape-evolution-from-3-2-1-to-cloud-storage-optimization-77e9a19171ed">here</a>.</p><h4>Shared/Team Workspace</h4><p>A shared workspace is similar to a User workspace in terms of mutability. The only difference is that it is owned by CDrive and shared among users for collaboration in a project. Authorization for any files/folders under a shared workspace is based on an access control list associated with nodes. In these workspaces, data lifecycle management follows a similar principle as user workspaces. All files/folders belonging to shared workspaces are considered temporary and only kept while the show is in production.</p><h3>Stats and Numbers</h3><ul><li>As of 10/2024: CDrive is storing metadata for about 14.2+ <strong>billion</strong> files/folders</li><li>848k workspaces: user 70%, project: 27%, team: 3%</li><li>Averaging ~50+ million new CDrive nodes created every week</li></ul><figure><img alt="Total number of File CNodes: ~14 billion" src="https://cdn-images-1.medium.com/max/251/1*OFBIuY6eLQ67BvDcLyTLIw.png" /><figcaption>Total number of CNodes categorized by their functional type</figcaption></figure><ul><li>Total visits to ContentHub UI (built on top of Content Drive) weekly</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*TWXWrg9wbItQ-xFm" /></figure><ul><li>UI page visits by various studios and production departments further highlight the importance of Content Drive for business.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*srEmDph938VwmGwP" /></figure><ul><li>This graph provides a quick summary of server-level Requests per step and overall P90 Latencies for the endpoints taken over a one-day window.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*RvW9plAstwZEOBDI" /></figure><h3>Future posts</h3><p>We will come back with more posts on the following interesting problems we are working on at present:</p><ul><li>Search—CDrive has APIs to search based on file path under a partition, but we don’t have a search based on arbitrary attributes for a node. Search capability for an application or a user in a project is very useful for Machine Learning and user-facing workflows.</li><li>Sharding — With data growing exponentially, CDrive has a new challenge of serving read queries for a container with millions of files/folders. CDrive plans to address this by adding support for sharding. The idea is to divide the huge container into multiple shards. This can improve the container retrieval cost.</li><li>CDrive Versioning—Studio applications need the capability to support “artist’s file sessions,” where artists have access rights to view the changes that happened to files/folders in their workspaces, get change notifications, refresh the artist’s workstation, and revert to a point-in-time version. With this new requirement, CDrive needs to provide the versions/change tracking capabilities of a cloud-enabled file system.</li></ul><h3><strong>Acknowledgments</strong></h3><p>Special thanks to our stunning colleagues <a href="https://www.linkedin.com/in/rajnish-prasad-989a49/">Rajnish Prasad</a>, <a href="https://www.linkedin.com/in/josethomas1/">Jose Thomas</a>, <a href="https://www.linkedin.com/in/olofjohanson/">Olof Johansson</a>, <a href="https://www.linkedin.com/in/vyelevich/">Victor Yelevich</a>, <a href="https://www.linkedin.com/in/vinay-kawade/">Vinay Kawade</a>, <a href="https://www.linkedin.com/in/shengwei4721/overlay/about-this-profile/">Shengwei Wang</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=919938544e4b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Netflix’s Distributed Counter Abstraction]]></title>
            <link>https://netflixtechblog.com/netflixs-distributed-counter-abstraction-8d0c45eb66b2?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/8d0c45eb66b2</guid>
            <category><![CDATA[counter]]></category>
            <category><![CDATA[software-architecture]]></category>
            <category><![CDATA[system-design-interview]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[scalability]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Tue, 12 Nov 2024 20:34:59 GMT</pubDate>
            <atom:updated>2024-11-21T22:00:12.371Z</atom:updated>
            <content:encoded><![CDATA[<p>By: <a href="https://www.linkedin.com/in/rajiv-shringi/">Rajiv Shringi</a>, <a href="https://www.linkedin.com/in/oleksii-tkachuk-98b47375/">Oleksii Tkachuk</a>, <a href="https://www.linkedin.com/in/kartik894/">Kartik Sathyanarayanan</a></p><h3>Introduction</h3><p>In our previous blog post, we introduced <a href="https://netflixtechblog.com/introducing-netflix-timeseries-data-abstraction-layer-31552f6326f8">Netflix’s TimeSeries Abstraction</a>, a distributed service designed to store and query large volumes of temporal event data with low millisecond latencies. Today, we’re excited to present the <strong>Distributed Counter Abstraction</strong>. This counting service, built on top of the TimeSeries Abstraction, enables distributed counting at scale while maintaining similar low latency performance. As with all our abstractions, we use our <a href="https://netflixtechblog.medium.com/data-gateway-a-platform-for-growing-and-protecting-the-data-tier-f1ed8db8f5c6">Data Gateway Control Plane</a> to shard, configure, and deploy this service globally.</p><p>Distributed counting is a challenging problem in computer science. In this blog post, we’ll explore the diverse counting requirements at Netflix, the challenges of achieving accurate counts in near real-time, and the rationale behind our chosen approach, including the necessary trade-offs.</p><p><strong>Note</strong>: <em>When it comes to distributed counters, terms such as ‘accurate’ or ‘precise’ should be taken with a grain of salt. In this context, they refer to a count very close to accurate, presented with minimal delays.</em></p><h3>Use Cases and Requirements</h3><p>At Netflix, our counting use cases include tracking millions of user interactions, monitoring how often specific features or experiences are shown to users, and counting multiple facets of data during <a href="https://netflixtechblog.com/its-all-a-bout-testing-the-netflix-experimentation-platform-4e1ca458c15">A/B test experiments</a>, among others.</p><p>At Netflix, these use cases can be classified into two broad categories:</p><ol><li><strong>Best-Effort</strong>: For this category, the count doesn’t have to be very accurate or durable. However, this category requires near-immediate access to the current count at low latencies, all while keeping infrastructure costs to a minimum.</li><li><strong>Eventually Consistent</strong>: This category needs accurate and durable counts, and is willing to tolerate a slight delay in accuracy and a slightly higher infrastructure cost as a trade-off.</li></ol><p>Both categories share common requirements, such as high throughput and high availability. The table below provides a detailed overview of the diverse requirements across these two categories.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZjxKcMckMLrT_JqPUzP4MQ.png" /></figure><h3>Distributed Counter Abstraction</h3><p>To meet the outlined requirements, the Counter Abstraction was designed to be highly configurable. It allows users to choose between different counting modes, such as <strong>Best-Effort</strong> or <strong>Eventually Consistent</strong>, while considering the documented trade-offs of each option. After selecting a mode, users can interact with APIs without needing to worry about the underlying storage mechanisms and counting methods.</p><p>Let’s take a closer look at the structure and functionality of the API.</p><h3>API</h3><p>Counters are organized into separate namespaces that users set up for each of their specific use cases. Each namespace can be configured with different parameters, such as Type of Counter, Time-To-Live (TTL), and Counter Cardinality, using the service’s Control Plane.</p><p>The Counter Abstraction API resembles Java’s <a href="https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/util/concurrent/atomic/AtomicInteger.html">AtomicInteger</a> interface:</p><p><strong>AddCount/AddAndGetCount</strong>: Adjusts the count for the specified counter by the given delta value within a dataset. The delta value can be positive or negative. The <em>AddAndGetCount</em> counterpart also returns the count after performing the add operation.</p><pre>{<br>  &quot;namespace&quot;: &quot;my_dataset&quot;,<br>  &quot;counter_name&quot;: &quot;counter123&quot;,<br>  &quot;delta&quot;: 2,<br>  &quot;idempotency_token&quot;: { <br>    &quot;token&quot;: &quot;some_event_id&quot;,<br>    &quot;generation_time&quot;: &quot;2024-10-05T14:48:00Z&quot;<br>  }<br>}</pre><p>The idempotency token can be used for counter types that support them. Clients can use this token to safely retry or <a href="https://research.google/pubs/the-tail-at-scale/">hedge</a> their requests. Failures in a distributed system are a given, and having the ability to safely retry requests enhances the reliability of the service.</p><p><strong>GetCount</strong>: Retrieves the count value of the specified counter within a dataset.</p><pre>{<br>  &quot;namespace&quot;: &quot;my_dataset&quot;,<br>  &quot;counter_name&quot;: &quot;counter123&quot;<br>}</pre><p><strong>ClearCount</strong>: Effectively resets the count to 0 for the specified counter within a dataset.</p><pre>{<br>  &quot;namespace&quot;: &quot;my_dataset&quot;,<br>  &quot;counter_name&quot;: &quot;counter456&quot;,<br>  &quot;idempotency_token&quot;: {...}<br>}</pre><p>Now, let’s look at the different types of counters supported within the Abstraction.</p><h3>Types of Counters</h3><p>The service primarily supports two types of counters: <strong>Best-Effort</strong> and <strong>Eventually Consistent</strong>, along with a third experimental type: <strong>Accurate</strong>. In the following sections, we’ll describe the different approaches for these types of counters and the trade-offs associated with each.</p><h3>Best Effort Regional Counter</h3><p>This type of counter is powered by <a href="https://netflixtechblog.com/announcing-evcache-distributed-in-memory-datastore-for-cloud-c26a698c27f7">EVCache</a>, Netflix’s distributed caching solution built on the widely popular <a href="https://memcached.org/">Memcached</a>. It is suitable for use cases like A/B experiments, where many concurrent experiments are run for relatively short durations and an approximate count is sufficient. Setting aside the complexities of provisioning, resource allocation, and control plane management, the core of this solution is remarkably straightforward:</p><pre>// counter cache key<br>counterCacheKey = &lt;namespace&gt;:&lt;counter_name&gt;<br><br>// add operation<br>return delta &gt; 0<br>    ? cache.incr(counterCacheKey, delta, TTL)<br>    : cache.decr(counterCacheKey, Math.abs(delta), TTL);<br><br>// get operation<br>cache.get(counterCacheKey);<br><br>// clear counts from all replicas<br>cache.delete(counterCacheKey, ReplicaPolicy.ALL);</pre><p>EVCache delivers extremely high throughput at low millisecond latency or better within a single region, enabling a multi-tenant setup within a shared cluster, saving infrastructure costs. However, there are some trade-offs: it lacks cross-region replication for the <em>increment</em> operation and does not provide <a href="https://netflix.github.io/EVCache/features/#consistency">consistency guarantees</a>, which may be necessary for an accurate count. Additionally, idempotency is not natively supported, making it unsafe to retry or hedge requests.</p><p><strong><em>Edit</em>: A note on probabilistic data structures:</strong></p><p>Probabilistic data structures like <a href="https://en.wikipedia.org/wiki/HyperLogLog">HyperLogLog</a> (HLL) can be useful for tracking an approximate number of distinct elements, like distinct views or visits to a website, but are not ideally suited for implementing distinct increments and decrements for a given key. <a href="https://en.wikipedia.org/wiki/Count%E2%80%93min_sketch">Count-Min Sketch</a> (CMS) is an alternative that can be used to adjust the values of keys by a given amount. Data stores like <a href="https://redis.io/">Redis</a> support both <a href="https://redis.io/docs/latest/develop/data-types/probabilistic/hyperloglogs/">HLL</a> and <a href="https://redis.io/docs/latest/develop/data-types/probabilistic/count-min-sketch/">CMS</a>. However, we chose not to pursue this direction for several reasons:</p><ul><li>We chose to build on top of data stores that we already operate at scale.</li><li>Probabilistic data structures do not natively support several of our requirements, such as resetting the count for a given key or having TTLs for counts. Additional data structures, including more sketches, would be needed to support these requirements.</li><li>On the other hand, the EVCache solution is quite simple, requiring minimal lines of code and using natively supported elements. However, it comes at the trade-off of using a small amount of memory per counter key.</li></ul><h3>Eventually Consistent Global Counter</h3><p>While some users may accept the limitations of a Best-Effort counter, others opt for precise counts, durability and global availability. In the following sections, we’ll explore various strategies for achieving durable and accurate counts. Our objective is to highlight the challenges inherent in global distributed counting and explain the reasoning behind our chosen approach.</p><p><strong>Approach 1: Storing a Single Row per Counter</strong></p><p>Let’s start simple by using a single row per counter key within a table in a globally replicated datastore.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*X6k4-4N36IQ5yEPe" /></figure><p>Let’s examine some of the drawbacks of this approach:</p><ul><li><strong>Lack of Idempotency</strong>: There is no idempotency key baked into the storage data-model preventing users from safely retrying requests. Implementing idempotency would likely require using an external system for such keys, which can further degrade performance or cause race conditions.</li><li><strong>Heavy Contention</strong>: To update counts reliably, every writer must perform a Compare-And-Swap operation for a given counter using locks or transactions. Depending on the throughput and concurrency of operations, this can lead to significant contention, heavily impacting performance.</li></ul><p><strong>Secondary Keys</strong>: One way to reduce contention in this approach would be to use a secondary key, such as a <em>bucket_id</em>, which allows for distributing writes by splitting a given counter into <em>buckets</em>, while enabling reads to aggregate across buckets. The challenge lies in determining the appropriate number of buckets. A static number may still lead to contention with <em>hot keys</em>, while dynamically assigning the number of buckets per counter across millions of counters presents a more complex problem.</p><p>Let’s see if we can iterate on our solution to overcome these drawbacks.</p><p><strong>Approach 2: Per Instance Aggregation</strong></p><p>To address issues of hot keys and contention from writing to the same row in real-time, we could implement a strategy where each instance aggregates the counts in memory and then flushes them to disk at regular intervals. Introducing sufficient jitter to the flush process can further reduce contention.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*6iUKbxJ093jJTiYL" /></figure><p>However, this solution presents a new set of issues:</p><ul><li><strong>Vulnerability to Data Loss</strong>: The solution is vulnerable to data loss for all in-memory data during instance failures, restarts, or deployments.</li><li><strong>Inability to Reliably Reset Counts</strong>: Due to counting requests being distributed across multiple machines, it is challenging to establish consensus on the exact point in time when a counter reset occurred.</li><li><strong>Lack of Idempotency: </strong>Similar to the previous approach, this method does not natively guarantee idempotency. One way to achieve idempotency is by consistently routing the same set of counters to the same instance. However, this approach may introduce additional complexities, such as leader election, and potential challenges with availability and latency in the write path.</li></ul><p>That said, this approach may still be suitable in scenarios where these trade-offs are acceptable. However, let’s see if we can address some of these issues with a different event-based approach.</p><p><strong>Approach 3: Using Durable Queues</strong></p><p>In this approach, we log counter events into a durable queuing system like <a href="https://kafka.apache.org/">Apache Kafka</a> to prevent any potential data loss. By creating multiple topic partitions and hashing the counter key to a specific partition, we ensure that the same set of counters are processed by the same set of consumers. This setup simplifies facilitating idempotency checks and resetting counts. Furthermore, by leveraging additional stream processing frameworks such as <a href="https://kafka.apache.org/documentation/streams/">Kafka Streams</a> or <a href="https://flink.apache.org/">Apache Flink</a>, we can implement windowed aggregations.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*mQikuGyuzZ_lT7Y4" /></figure><p>However, this approach comes with some challenges:</p><ul><li><strong>Potential Delays</strong>: Having the same consumer process all the counts from a given partition can lead to backups and delays, resulting in stale counts.</li><li><strong>Rebalancing Partitions</strong>: This approach requires auto-scaling and rebalancing of topic partitions as the cardinality of counters and throughput increases.</li></ul><p>Furthermore, all approaches that pre-aggregate counts make it challenging to support two of our requirements for accurate counters:</p><ul><li><strong>Auditing of Counts</strong>: Auditing involves extracting data to an offline system for analysis to ensure that increments were applied correctly to reach the final value. This process can also be used to track the provenance of increments. However, auditing becomes infeasible when counts are aggregated without storing the individual increments.</li><li><strong>Potential Recounting</strong>: Similar to auditing, if adjustments to increments are necessary and recounting of events within a time window is required, pre-aggregating counts makes this infeasible.</li></ul><p>Barring those few requirements, this approach can still be effective if we determine the right way to scale our queue partitions and consumers while maintaining idempotency. However, let’s explore how we can adjust this approach to meet the auditing and recounting requirements.</p><p><strong>Approach 4: Event Log of Individual Increments</strong></p><p>In this approach, we log each individual counter increment along with its <strong>event_time</strong> and <strong>event_id</strong>. The event_id can include the source information of where the increment originated. The combination of event_time and event_id can also serve as the idempotency key for the write.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*0wKFK7xyTHnEKIhO" /></figure><p>However, <em>in its simplest form</em>, this approach has several drawbacks:</p><ul><li><strong>Read Latency</strong>: Each read request requires scanning all increments for a given counter potentially degrading performance.</li><li><strong>Duplicate Work</strong>: Multiple threads might duplicate the effort of aggregating the same set of counters during read operations, leading to wasted effort and subpar resource utilization.</li><li><strong>Wide Partitions</strong>: If using a datastore like <a href="https://cassandra.apache.org/_/index.html">Apache Cassandra</a>, storing many increments for the same counter could lead to a <a href="https://thelastpickle.com/blog/2019/01/11/wide-partitions-cassandra-3-11.html">wide partition</a>, affecting read performance.</li><li><strong>Large Data Footprint</strong>: Storing each increment individually could also result in a substantial data footprint over time. Without an efficient data retention strategy, this approach may struggle to scale effectively.</li></ul><p>The combined impact of these issues can lead to increased infrastructure costs that may be difficult to justify. However, adopting an event-driven approach seems to be a significant step forward in addressing some of the challenges we’ve encountered and meeting our requirements.</p><p>How can we improve this solution further?</p><h3>Netflix’s Approach</h3><p>We use a combination of the previous approaches, where we log each counting activity as an event, and continuously aggregate these events in the background using queues and a sliding time window. Additionally, we employ a bucketing strategy to prevent wide partitions. In the following sections, we’ll explore how this approach addresses the previously mentioned drawbacks and meets all our requirements.</p><p><strong>Note</strong>: <em>From here on, we will use the words “</em><strong><em>rollup</em></strong><em>” and “</em><strong><em>aggregate</em></strong><em>” interchangeably. They essentially mean the same thing, i.e., collecting individual counter increments/decrements and arriving at the final value.</em></p><p><strong>TimeSeries Event Store:</strong></p><p>We chose the <a href="https://netflixtechblog.com/introducing-netflix-timeseries-data-abstraction-layer-31552f6326f8">TimeSeries Data Abstraction</a> as our event store, where counter mutations are ingested as event records. Some of the benefits of storing events in TimeSeries include:</p><p><strong>High-Performance</strong>: The TimeSeries abstraction already addresses many of our requirements, including high availability and throughput, reliable and fast performance, and more.</p><p><strong>Reducing Code Complexity</strong>: We reduce a lot of code complexity in Counter Abstraction by delegating a major portion of the functionality to an existing service.</p><p>TimeSeries Abstraction uses Cassandra as the underlying event store, but it can be configured to work with any persistent store. Here is what it looks like:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ge4X7ywSmtizcNE5" /></figure><p><strong>Handling Wide Partitions</strong>: The <em>time_bucket</em> and <em>event_bucket</em> columns play a crucial role in breaking up a wide partition, preventing high-throughput counter events from overwhelming a given partition. <em>For more information regarding this, refer to our previous </em><a href="https://netflixtechblog.com/introducing-netflix-timeseries-data-abstraction-layer-31552f6326f8"><em>blog</em></a>.</p><p><strong>No Over-Counting</strong>: The <em>event_time</em>, <em>event_id</em> and <em>event_item_key</em> columns form the idempotency key for the events for a given counter, enabling clients to retry safely without the risk of over-counting.</p><p><strong>Event Ordering</strong>: TimeSeries orders all events in descending order of time allowing us to leverage this property for events like count resets.</p><p><strong>Event Retention</strong>: The TimeSeries Abstraction includes retention policies to ensure that events are not stored indefinitely, saving disk space and reducing infrastructure costs. Once events have been aggregated and moved to a more cost-effective store for audits, there’s no need to retain them in the primary storage.</p><p>Now, let’s see how these events are aggregated for a given counter.</p><p><strong>Aggregating Count Events:</strong></p><p>As mentioned earlier, collecting all individual increments for every read request would be cost-prohibitive in terms of read performance. Therefore, a background aggregation process is necessary to continually converge counts and ensure optimal read performance.</p><p><em>But how can we safely aggregate count events amidst ongoing write operations?</em></p><p>This is where the concept of <em>Eventually Consistent </em>counts becomes crucial. <em>By intentionally lagging behind the current time by a safe margin</em>, we ensure that aggregation always occurs within an immutable window.</p><p>Lets see what that looks like:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*EOpW-VnA_YZF7KOP" /></figure><p>Let’s break this down:</p><ul><li><strong>lastRollupTs</strong>: This represents the most recent time when the counter value was last aggregated. For a counter being operated for the first time, this timestamp defaults to a reasonable time in the past.</li><li><strong>Immutable Window and Lag</strong>: Aggregation can only occur safely within an immutable window that is no longer receiving counter events. The “acceptLimit” parameter of the TimeSeries Abstraction plays a crucial role here, as it rejects incoming events with timestamps beyond this limit. During aggregations, this window is pushed slightly further back to account for clock skews.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1008/0*DbtPCHPWoaauUkDr" /></figure><p>This does mean that the counter value will lag behind its most recent update by some margin (typically in the order of seconds). <em>This approach does leave the door open for missed events due to cross-region replication issues. See “Future Work” section at the end.</em></p><ul><li><strong>Aggregation Process</strong>: The rollup process aggregates all events in the aggregation window <em>since the last rollup </em>to arrive at the new value.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*oSHneX5BOi5VNGYM" /></figure><p><strong>Rollup Store:</strong></p><p>We save the results of this aggregation in a persistent store. The next aggregation will simply continue from this checkpoint.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*93S_a1YJ6zacuBnn" /></figure><p>We create one such Rollup table <em>per dataset</em> and use Cassandra as our persistent store. However, as you will soon see in the Control Plane section, the Counter service can be configured to work with any persistent store.</p><p><strong>LastWriteTs</strong>: Every time a given counter receives a write, we also log a <strong>last-write-timestamp</strong> as a columnar update in this table. This is done using Cassandra’s <a href="https://docs.datastax.com/en/cql-oss/3.x/cql/cql_reference/cqlInsert.html#cqlInsert__timestamp-value">USING TIMESTAMP</a> feature to predictably apply the Last-Write-Win (LWW) semantics. This timestamp is the same as the <em>event_time</em> for the event. In the subsequent sections, we’ll see how this timestamp is used to keep some counters in active rollup circulation until they have caught up to their latest value.</p><p><strong>Rollup Cache</strong></p><p>To optimize read performance, these values are cached in EVCache for each counter. We combine the <strong>lastRollupCount</strong> and <strong>lastRollupTs</strong> <em>into a single cached value per counter</em> to prevent potential mismatches between the count and its corresponding checkpoint timestamp.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*giCU1AtWUYMXHZcI" /></figure><p>But, how do we know which counters to trigger rollups for? Let’s explore our Write and Read path to understand this better.</p><p><strong>Add/Clear Count:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*wsxgnWH1yR0gHAEL" /></figure><p>An <em>add</em> or <em>clear</em> count request writes durably to the TimeSeries Abstraction and updates the last-write-timestamp in the Rollup store. If the durability acknowledgement fails, clients can retry their requests with the same idempotency token without the risk of overcounting.<strong> </strong>Upon durability, we send a <em>fire-and-forget </em>request to trigger the rollup for the request counter.</p><p><strong>GetCount:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*76pQR6OISx9yuRmi" /></figure><p>We return the last rolled-up count as<em> a quick point-read operation</em>, accepting the trade-off of potentially delivering a slightly stale count. We also trigger a rollup during the read operation to advance the last-rollup-timestamp, enhancing the performance of <em>subsequent</em> aggregations. This process also <em>self-remediates </em>a stale count if any previous rollups had failed.</p><p>With this approach, the counts<em> continually converge</em> to their latest value. Now, let’s see how we scale this approach to millions of counters and thousands of concurrent operations using our Rollup Pipeline.</p><p><strong>Rollup Pipeline:</strong></p><p>Each <strong>Counter-Rollup</strong> server operates a rollup pipeline to efficiently aggregate counts across millions of counters. This is where most of the complexity in Counter Abstraction comes in. In the following sections, we will share key details on how efficient aggregations are achieved.</p><p><strong>Light-Weight Roll-Up Event: </strong>As seen in our Write and Read paths above, every operation on a counter sends a light-weight event to the Rollup server:</p><pre>rollupEvent: {<br>  &quot;namespace&quot;: &quot;my_dataset&quot;,<br>  &quot;counter&quot;: &quot;counter123&quot;<br>}</pre><p>Note that this event does not include the increment. This is only an indication to the Rollup server that this counter has been accessed and now needs to be aggregated. Knowing exactly which specific counters need to be aggregated prevents scanning the entire event dataset for the purpose of aggregations.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Yusg6kC9Jj9ayjbi" /></figure><p><strong>In-Memory Rollup Queues:</strong> A given Rollup server instance runs a set of <em>in-memory</em> queues to receive rollup events and parallelize aggregations. In the first version of this service, we settled on using in-memory queues to reduce provisioning complexity, save on infrastructure costs, and make rebalancing the number of queues fairly straightforward. However, this comes with the trade-off of potentially missing rollup events in case of an instance crash. For more details, see the “Stale Counts” section in “Future Work.”</p><p><strong>Minimize Duplicate Effort</strong>: We use a fast non-cryptographic hash like <a href="https://xxhash.com/">XXHash</a> to ensure that the same set of counters end up on the same queue. Further, we try to minimize the amount of duplicate aggregation work by having a separate rollup stack that chooses to run <em>fewer</em> <em>beefier</em> instances.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*u3p0kGfuwvK5mP_j" /></figure><p><strong>Availability and Race Conditions: </strong>Having a single Rollup server instance can minimize duplicate aggregation work but may create availability challenges for triggering rollups. <em>If</em> we choose to horizontally scale the Rollup servers, we allow threads to overwrite rollup values while avoiding any form of distributed locking mechanisms to maintain high availability and performance. This approach remains safe because aggregation occurs within an immutable window. Although the concept of <em>now()</em> may differ between threads, causing rollup values to sometimes fluctuate, the counts will eventually converge to an accurate value within each immutable aggregation window.</p><p><strong>Rebalancing Queues</strong>: If we need to scale the number of queues, a simple Control Plane configuration update followed by a re-deploy is enough to rebalance the number of queues.</p><pre>      &quot;eventual_counter_config&quot;: {             <br>          &quot;queue_config&quot;: {                    <br>            &quot;num_queues&quot; : 8,  // change to 16 and re-deploy<br>...</pre><p><strong>Handling Deployments</strong>: During deployments, these queues shut down gracefully, draining all existing events first, while the new Rollup server instance starts up with potentially new queue configurations. There may be a brief period when both the old and new Rollup servers are active, but as mentioned before, this race condition is managed since aggregations occur within immutable windows.</p><p><strong>Minimize Rollup Effort</strong>: Receiving multiple events for the same counter doesn’t mean rolling it up multiple times. We drain these rollup events into a Set, ensuring <em>a given counter is rolled up only once</em> <em>during a rollup window</em>.</p><p><strong>Efficient Aggregation: </strong>Each rollup consumer processes a batch of counters simultaneously. Within each batch, it queries the underlying TimeSeries abstraction in parallel to aggregate events within specified time boundaries. The TimeSeries abstraction optimizes these range scans to achieve low millisecond latencies.</p><p><strong>Dynamic Batching</strong>: The Rollup server dynamically adjusts the number of time partitions that need to be scanned based on cardinality of counters in order to prevent overwhelming the underlying store with many parallel read requests.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*hoPpSmQeScn87q0U" /></figure><p><strong>Adaptive Back-Pressure</strong>: Each consumer waits for one batch to complete before issuing the rollups for the next batch. It adjusts the wait time between batches based on the performance of the previous batch. This approach provides back-pressure during rollups to prevent overwhelming the underlying TimeSeries store.</p><p><strong>Handling Convergence</strong>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*-hlw324cMUaC6pQJ" /></figure><p>In order to prevent <strong>low-cardinality</strong> counters from lagging behind too much and subsequently scanning too many time partitions, they are kept in constant rollup circulation. For <strong>high-cardinality</strong> counters, continuously circulating them would consume excessive memory in our Rollup queues. This is where the <strong>last-write-timestamp</strong> mentioned previously plays a crucial role. The Rollup server inspects this timestamp to determine if a given counter needs to be re-queued, ensuring that we continue aggregating until it has fully caught up with the writes.</p><p>Now, let’s see how we leverage this counter type to provide an up-to-date current count in near-realtime.</p><h3>Experimental: Accurate Global Counter</h3><p>We are experimenting with a slightly modified version of the Eventually Consistent counter. Again, take the term ‘Accurate’ with a grain of salt. The key difference between this type of counter and its counterpart is that the <em>delta</em>, representing the counts since the last-rolled-up timestamp, is computed in real-time.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*FVOlMO0VgrQoVBBi" /></figure><p>And then, <em>currentAccurateCount = lastRollupCount + delta</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*M3dbSof98dTfeuNe" /></figure><p>Aggregating this delta in real-time can impact the performance of this operation, depending on the number of events and partitions that need to be scanned to retrieve this delta. The same principle of rolling up in batches applies here to prevent scanning too many partitions in parallel. Conversely, if the counters in this dataset are<em> </em>accessed<em> </em>frequently, the time gap for the delta remains narrow, making this approach of fetching current counts quite effective.</p><p>Now, let’s see how all this complexity is managed by having a unified Control Plane configuration.</p><h3>Control Plane</h3><p>The <a href="https://netflixtechblog.medium.com/data-gateway-a-platform-for-growing-and-protecting-the-data-tier-f1ed8db8f5c6">Data Gateway Platform Control Plane</a> manages control settings for all abstractions and namespaces, including the Counter Abstraction. Below, is an example of a control plane configuration for a namespace that supports eventually consistent counters with low cardinality:</p><pre>&quot;persistence_configuration&quot;: [<br>  {<br>    &quot;id&quot;: &quot;CACHE&quot;,                             // Counter cache config<br>    &quot;scope&quot;: &quot;dal=counter&quot;,                                                   <br>    &quot;physical_storage&quot;: {<br>      &quot;type&quot;: &quot;EVCACHE&quot;,                       // type of cache storage<br>      &quot;cluster&quot;: &quot;evcache_dgw_counter_tier1&quot;   // Shared EVCache cluster<br>    }<br>  },<br>  {<br>    &quot;id&quot;: &quot;COUNTER_ROLLUP&quot;,<br>    &quot;scope&quot;: &quot;dal=counter&quot;,                    // Counter abstraction config<br>    &quot;physical_storage&quot;: {                     <br>      &quot;type&quot;: &quot;CASSANDRA&quot;,                     // type of Rollup store<br>      &quot;cluster&quot;: &quot;cass_dgw_counter_uc1&quot;,       // physical cluster name<br>      &quot;dataset&quot;: &quot;my_dataset_1&quot;                // namespace/dataset   <br>    },<br>    &quot;counter_cardinality&quot;: &quot;LOW&quot;,              // supported counter cardinality<br>    &quot;config&quot;: {<br>      &quot;counter_type&quot;: &quot;EVENTUAL&quot;,              // Type of counter<br>      &quot;eventual_counter_config&quot;: {             // eventual counter type<br>        &quot;internal_config&quot;: {                  <br>          &quot;queue_config&quot;: {                    // adjust w.r.t cardinality<br>            &quot;num_queues&quot; : 8,                  // Rollup queues per instance<br>            &quot;coalesce_ms&quot;: 10000,              // coalesce duration for rollups<br>            &quot;capacity_bytes&quot;: 16777216         // allocated memory per queue<br>          },<br>          &quot;rollup_batch_count&quot;: 32             // parallelization factor<br>        }<br>      }<br>    }<br>  },<br>  {<br>    &quot;id&quot;: &quot;EVENT_STORAGE&quot;,<br>    &quot;scope&quot;: &quot;dal=ts&quot;,                         // TimeSeries Event store<br>    &quot;physical_storage&quot;: {<br>      &quot;type&quot;: &quot;CASSANDRA&quot;,                     // persistent store type<br>      &quot;cluster&quot;: &quot;cass_dgw_counter_uc1&quot;,       // physical cluster name<br>      &quot;dataset&quot;: &quot;my_dataset_1&quot;,               // keyspace name<br>    },<br>    &quot;config&quot;: {                              <br>      &quot;time_partition&quot;: {                      // time-partitioning for events<br>        &quot;buckets_per_id&quot;: 4,                   // event buckets within<br>        &quot;seconds_per_bucket&quot;: &quot;600&quot;,           // smaller width for LOW card<br>        &quot;seconds_per_slice&quot;: &quot;86400&quot;,          // width of a time slice table<br>      },<br>      &quot;accept_limit&quot;: &quot;5s&quot;,                    // boundary for immutability<br>    },<br>    &quot;lifecycleConfigs&quot;: {<br>      &quot;lifecycleConfig&quot;: [<br>        {<br>          &quot;type&quot;: &quot;retention&quot;,                 // Event retention<br>          &quot;config&quot;: {<br>            &quot;close_after&quot;: &quot;518400s&quot;,<br>            &quot;delete_after&quot;: &quot;604800s&quot;          // 7 day count event retention<br>          }<br>        }<br>      ]<br>    }<br>  }<br>]</pre><p>Using such a control plane configuration, we compose multiple abstraction layers using containers deployed on the same host, with each container fetching configuration specific to its scope.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/853/0*4MdrlEjWg2MXU9S3" /></figure><h3>Provisioning</h3><p>As with the TimeSeries abstraction, our automation uses a bunch of user inputs regarding their workload and cardinalities to arrive at the right set of infrastructure and related control plane configuration. You can learn more about this process in a talk given by one of our stunning colleagues, <a href="https://www.linkedin.com/in/joseph-lynch-9976a431/">Joey Lynch</a> : <a href="https://www.youtube.com/watch?v=Lf6B1PxIvAs">How Netflix optimally provisions infrastructure in the cloud</a>.</p><h3>Performance</h3><p>At the time of writing this blog, this service was processing close to <strong>75K count requests/second</strong><em> globally</em> across the different API endpoints and datasets:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*1h_af4Kk3YrZrqlc" /></figure><p>while providing<strong> single-digit millisecond</strong> latencies for all its endpoints:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*UnI7eore6gvuqrrF" /></figure><h3>Future Work</h3><p>While our system is robust, we still have work to do in making it more reliable and enhancing its features. Some of that work includes:</p><ul><li><strong>Regional Rollups: </strong>Cross-region replication issues can result in missed events from other regions. An alternate strategy involves establishing a rollup table for each region, and then tallying them in a global rollup table. A key challenge in this design would be effectively communicating the clearing of the counter across regions.</li><li><strong>Error Detection and Stale Counts</strong>: Excessively stale counts can occur if rollup events are lost or if a rollup fails and isn’t retried. This isn’t an issue for frequently accessed counters, as they remain in rollup circulation. This issue is more pronounced for counters that aren’t accessed frequently. Typically, the initial read for such a counter will trigger a rollup,<em> self-remediating </em>the issue. However, for use cases that cannot accept potentially stale initial reads, we plan to implement improved error detection, rollup handoffs, and durable queues for resilient retries.</li></ul><h3>Conclusion</h3><p>Distributed counting remains a challenging problem in computer science. In this blog, we explored multiple approaches to implement and deploy a Counting service at scale. While there may be other methods for distributed counting, our goal has been to deliver blazing fast performance at low infrastructure costs while maintaining high availability and providing idempotency guarantees. Along the way, we make various trade-offs to meet the diverse counting requirements at Netflix. We hope you found this blog post insightful.</p><p>Stay tuned for <strong>Part 3 </strong>of Composite Abstractions at Netflix, where we’ll introduce our <strong>Graph Abstraction</strong>, a new service being built on top of the <a href="https://netflixtechblog.com/introducing-netflixs-key-value-data-abstraction-layer-1ea8a0a11b30">Key-Value Abstraction</a> <em>and</em> the <a href="https://netflixtechblog.com/introducing-netflix-timeseries-data-abstraction-layer-31552f6326f8">TimeSeries Abstraction</a> to handle high-throughput, low-latency graphs.</p><h3>Acknowledgments</h3><p>Special thanks to our stunning colleagues who contributed to the Counter Abstraction’s success: <a href="https://www.linkedin.com/in/joseph-lynch-9976a431/">Joey Lynch</a>, <a href="https://www.linkedin.com/in/vinaychella/">Vinay Chella</a>, <a href="https://www.linkedin.com/in/kaidanfullerton/">Kaidan Fullerton</a>, <a href="https://www.linkedin.com/in/tomdevoe/">Tom DeVoe</a>, <a href="https://www.linkedin.com/in/mengqingwang/">Mengqing Wang</a>, <a href="https://www.linkedin.com/in/varun-khaitan/">Varun Khaitan</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8d0c45eb66b2" width="1" height="1" alt=""><hr><p><a href="https://netflixtechblog.com/netflixs-distributed-counter-abstraction-8d0c45eb66b2">Netflix’s Distributed Counter Abstraction</a> was originally published in <a href="https://netflixtechblog.com">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[My Path Towards Data @ Netflix]]></title>
            <link>https://netflixtechblog.medium.com/my-non-linear-path-towards-data-netflix-feccbe9c3dae?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/feccbe9c3dae</guid>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Wed, 06 Nov 2024 16:26:07 GMT</pubDate>
            <atom:updated>2024-11-06T20:03:45.773Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>by Lisa Herzog</strong></p><p>Have you ever heard of the game “Two truths, one lie”? The rules are simple:</p><ul><li>Prepare three statements about yourself</li><li>Two true statements, one false statement</li><li>Ask your audience to guess which of your three statements is the lie</li></ul><p><strong>Are you ready? Let’s see if you can catch my lie:</strong></p><ol><li><strong>Childhood: </strong>I have grown up in a family of teachers: my grandmother, all my aunts and uncles are teachers and — guess what — my cousin is a teacher too. My father, a mathematics and science teacher, would get so enthusiastic about applied math that he would regularly try to convince my friends to do ‘fun’ DIY experiments when they were visiting me at home. My grandmother, on the other hand, was an excellent storyteller who would capture and inspire us with her stories (some fictional, some true and some a blend of both).</li><li><strong>Career Path: </strong>When I graduated from highschool in the early 2000’s in Germany, I knew exactly what type of career I wanted to pursue: my father’s passion for applied math inspired me to study Econometrics at Maastricht University in the Netherlands. Shortly after I commenced my studies in Econometrics, I discovered the world of Data Science, and it was love at first sight. After completing my Bachelor’s degree in Econometrics, I decided to specialise in “Data Science for Decision-Making” and shortly after graduating I landed a job in Data Science at Netflix.</li><li><strong>Analytics Engineering @ Netflix: </strong>If you asked me about my dream job as a kid, I would typically give one of two answers: “I want to become a detective” and “I want to become a writer”. While I have never pursued my childhood aspirations, I consider my current role as an Analytics Engineer to be a blend of both: we start with a question, collect and validate evidence, identify the “story” behind the evidence, and once we have made sense of it, we share our insights with our partners.</li></ol><p>Have you guessed which statement is false? Let’s find out if you are right.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/480/0*UaTrVWLXZj250wqC" /></figure><p>You might have guessed it — <strong>statement 2 </strong>is a lie!</p><p>I don’t have a quantitative degree in mathematics, statistics or computer science and have built most of my knowledge and experience through books, online courses, mentorship and hobby projects. So in case you are <strong>dreaming about a</strong> <strong>career in data</strong> but <strong>don’t have a degree in math or science</strong> — don’t be discouraged!</p><p>There are plenty of great resources that you can leverage to break into data.</p><p>In the next couple of sections I want to <strong>tell you my story</strong> and <strong>share my favourite data resources </strong>with you!</p><h3>My Path Into Data</h3><p>My path towards data science is non-linear; when I graduated from high school in a German small town in the early 2000’s, I had never heard of Data Science and Analytics, I had never heard of Silicon Valley, and — like many high school graduates — I had no clue what type of career I wanted to pursue. There was one thing I knew for sure, however: I could not see myself working in tech. Why? Working in tech would conjure up images of dark office rooms with (primarily male) programmers in hoodies and a working reality in which creativity, communication and social interaction did not have a place (oh boy was I wrong about this). After much consideration, I decided to study International Business with the hope that I could specialize later with more knowledge and experience below my belt.</p><p>I discovered the world of data science by accident during an open day at Maastricht University. My original plan was to visit information lectures about traditional business masters (process management looked like the most promising candidate) and then — I got lost (I do have a horrible sense of orientation). I sat down in a lecture hall expecting a information session on masters in process management and was therefore slightly baffled when the presenter kicked off with “Welcome to our lecture on Data Science in Decision-Making”. I did not want to be rude and leave early so I stayed. In less than 30 minutes, my view on Data Science and tech was reversed; I realised that:</p><ol><li><strong>Data-Powered Use Cases: </strong>Data Science enables many exciting use cases ranging from sentiment classification to what if scenario simulation models (GenAI was not a thing yet)</li><li><strong>Creativity &amp; Communication: </strong>Problem-Solving in tech requires creativity, a broad range of skills and exceptional communication skills (identifying and selling data-powered use cases, finding creative solutions to coding challenges, change management)</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/263/0*yMJXQ31bkJ1uAnS6" /></figure><p>And after only 30 minutes I decided to take a leap of faith and <strong>pivot into data. </strong>I am not going to lie, pivoting into data was tough in the beginning. The master program was designed to train business students to become “data translators”, someone who could serve as a bridge between business and tech. In the short time of a year, we covered data-powered use cases, quantitative methodologies and their applications, and unstructured data (e.g. text and image processing). But since I was brand new to the world of data, keeping up meant many evenings spent with digital mentors such as Josh Starmer’s <a href="https://www.youtube.com/@statquest">StatQuest</a>, Kirill Emerenko’s <a href="https://www.udemy.com/course/python-coding/?couponCode=OF53124">Python for Data Science</a> and many more. When I graduated, I was glad that the evening work had paid off — I had landed my first job in Data! My first job in Data gave me access to a large network of amazing mentors — one mentor spent hours and hours of her time reviewing my code and helped me to level up my coding skills, another mentor taught me statistics, and yet another mentor taught me to leverage personas when communicating to a non-technical audience.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/270/0*QMMcD8SQpja1lItb" /></figure><p>Fast-forward a couple of years and I could not believe my eyes when I spotted a message from a Netflix recruiter in my inbox inviting me to interview for an <strong>Analytics Engineering </strong>opportunity<strong> </strong>in <strong>Studio Production Data Science and Engineering. </strong>This opportunity felt like a dream coming true — ever since I can think, I would spend hours watching “Behind the Scenes” and the Oscars, and watching movies has always been a medium to explore unknown worlds, cultures through stories. Throughout the interview process, I was won over by the competence of my interviewers and the uniqueness of <a href="https://jobs.netflix.com/culture">Netflix’s culture</a> — and — the rest is history.</p><p><strong>Key Takeaways: </strong>you don’t need a quantitative degree to land a job in Data. There are plenty of great resources that can enable you to grow the skills you need.</p><p><strong>Which resources?</strong> Find out below ⬇️⬇️⬇️</p><h3><em>Analytics Practices and Resources</em></h3><h3><em>SQL</em></h3><p><strong>SQL </strong>is a database language that enables you to retrieve, combine and manipulate data. To give a concrete example, In Studio Production DSE we leverage SQL to answer questions about the operational health (time/cost/quality) of content production, for example:</p><ul><li>How many titles (movies or series) are we launching this year?</li><li>How much did it cost to produce X title, did we spend more than our budget?</li><li>Given X title, where did we spend the most?</li></ul><p><strong>Resources</strong></p><ul><li><strong>SQL: </strong>A good starting point is <a href="https://www.udemy.com/course/sql-mysql-for-data-analytics-and-business-intelligence">SQL for Data Analytics and Business Intelligence</a> by 365 careers which provides an overview of all essential SQL operations (aggregation, data table joins and window functions).</li><li><strong>Working with Real-Life Data: </strong>Once you have mastered SQL syntax, it is important to get your hands on real-life data (courses typically use very polished data sets). Leveraging real life data sets (see <a href="https://www.kaggle.com/datasets">Kraggle</a> for published datasets) enables you to build experience with cleaning your data and interpreting and resolving error messages. And rest assured, whatever error message you encounter, it is very likely that someone else has encountered it before and has found a solution, so you can rely on Google (and ChatGPT) to find an answer to your coding problem.</li></ul><h3><em>Data Preprocessing</em></h3><p>Preprocessing your data involves <strong>selecting information </strong>needed for your analysis (using SQL or Python), <strong>filtering </strong>your data, and <strong>data cleaning. </strong>In Studio Production DSE, the majority of data we work with is user entered which could result in missing data, and inconsistencies. Using SQL and Python enables us to identify and correct missing data and inconsistencies.</p><p><strong>Resources</strong></p><ul><li><strong>Data Cleaning: </strong>For an excellent data cleaning guide see Mahesh Tiwari’s <a href="https://medium.com/nerd-for-tech/data-cleaning-process-for-beginners-903aef7f6049">Guide for Data Cleaning</a>.</li><li><strong>Python: </strong>a good starting point is <a href="https://www.udemy.com/course/python-coding/learn/lecture/20493864?start=0#overview">Kirill Emerenko’s Python A-Z</a>, a very thorough course for Python fundamentals (loops, data types, metric manipulations and visualisation). For a specialisation in Data Preprocessing (using a library called Pandas), <a href="https://www.udemy.com/course/data-analysis-with-pandas/learn/lecture/40596672?start=0#overview">Data Analysis with Pandas</a> by Boris Paskhaver is a great resource.</li></ul><h3><em>Statistics</em></h3><p>Statistics enables you to decide to what extent you can <strong>generalise data </strong>beyond your sample, allows you to be cognisant of methodologies and their prerequisites towards the input data, and enables you to <strong>choose the best methodology </strong>to answer a question. To provide a concrete example, in Studio Production DSE, we leverage forecasting methodologies to predict cash flow per production which enables Production to anticipate spend and ensure that spend obligations are met throughout the production lifecycle.</p><p><strong>Resources</strong></p><ul><li><strong>Statistics:</strong> A resource that I have found incredibly useful is <a href="https://www.youtube.com/@statquest">StatQuest by Josh Starmer</a> — a channel focused on statistics and machine-learning which provides intuitive explanations and concrete examples for illustration. And many of the chapters have a themed song which will haunt you for weeks for example “Calculating p-values is kinda fun and not just when you are done”.</li></ul><h3><em>Defining Meaningful Metrics</em></h3><p>Working as an Analytics Engineer involves developing <strong>meaningful metrics </strong>for our cross-functional partners. In Studio Production DSE, we partner with Directors and VPs in Production Management and Content Operations to develop metrics that measure operational health (time/cost/quality) of content production for example: spend overages (actual spend vs. budget) per production or production slate, delays per content production phase (actual vs. planned milestones).</p><p><strong>Resources</strong></p><p>What defines a meaningful metric? You could ask yourself the following questions:</p><ul><li><strong>Relevant: </strong>is your metric aligned with the overall (business) objective?</li><li><strong>Actionable: </strong>are your partners able to influence this metric?</li><li><strong>Quantifiable: </strong>are you able to measure this metric?</li><li><strong>Simple: </strong>are you able to explain the metric in less than five minutes?</li></ul><p>Okay — this sounds great on paper but how do you build experience with setting meaningful metrics?</p><p>Something that I have found very useful is setting annual goals and developing metrics that help you track your progress towards the goal.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/292/0*8xgRTa7QIWzctyJ7" /></figure><p>Okay, okay — let me give you an example: let’s suppose you want to complete a half marathon by the end of the year. What metrics would help you track your progress towards your goal? There are two components to successfully completing a half marathon: mastering the distance, and mastering your speed. Knowing this you could set goals, metrics and targets:</p><ul><li><strong>Frequency: </strong>I want to run three times per week (metrics: # weekly runs)</li><li><strong>Weekly Distance Goals: </strong>I want to run 30 kilometers every week (metric: km per week)</li><li><strong>Speed: </strong>I want to run at a speed of 6:00 min per km (metric: speed per minute)</li></ul><p>Once you have set these goals, go through a mental checklist. Are your metrics: aligned, actionable, quantifiable and simple?</p><h3><em>Problem-Solving</em></h3><p>Problem-solving involves <strong>asking the right questions</strong> to understand the context and impact of a request, <strong>translating a vague question </strong>into a specific hypothesis and <strong>choosing the right </strong>type of methodology. In Studio Production, data projects typically start with a <strong>scoping session </strong>with our cross-functional partners. In scoping sessions, we ask questions to understand 1) what type of insights are needed 2) what use cases will be enabled/powered by the requested insights and 3) how the insights fit into the bigger picture (eg. company objectives). Once scoping is finalised, we typically prioritise this request against all other requests on our roadmap.</p><p><strong>Resources</strong></p><ul><li><strong>Prioritisation: </strong>prioritisation will depend on your problem space. Having said that, it is always useful to ask yourself: How will X insights influence my partners’ decisions and/or workflows? (it is useful to think through a couple what if scenarios, eg. if my metric showed X, how would this influence decisions/workflows). How does the above decision/workflow change impact the business? How does X insight fit into the bigger picture (eg annual company priorities and strategy).</li><li><strong>Problem-Solving: </strong>in case you are solving problems in a business context, a great resource is consultancy interview guides such as <a href="https://www.amazon.de/Case-Point-Complete-Interview-Preparation/dp/0971015880">Case In Point</a> by Cosentino, a comprehensive guide to common business problems and problem-solving approaches.</li></ul><h3><em>Data Storytelling</em></h3><p>Data storytelling involves crafting a narrative to your target audience and choosing the most effective visuals to corroborate your story. In Studio Production, Data Storytelling best practices enable us to talk to our cross functional partners in their own language. When pitching an idea, for example, we focus conversations on key questions that would be answered and use cases that could be enabled by specific insights (vs. providing a list of metrics or functionalities). When developing an insights tool, we leverage <a href="https://maze.co/guides/usability-testing/">usability testing</a> to catch data inaccuracies, identify usability issues and understand how the information fits into the user’s workflows and use cases.</p><p><strong>Resource</strong></p><ul><li><strong>Storytelling:</strong> An excellent resource for data storytelling is <a href="https://www.amazon.com/Storytelling-Data-Visualization-Business-Professionals/dp/1119002257?pd_rd_w=GOBtn&amp;content-id=amzn1.sym.7f0cf323-50c6-49e3-b3f9-63546bb79c92&amp;pf_rd_p=7f0cf323-50c6-49e3-b3f9-63546bb79c92&amp;pf_rd_r=PRDGPBNW9STWB7TQTHQW&amp;pd_rd_wg=v8uFy&amp;pd_rd_r=024055bd-f103-4273-9e7f-100ca4cb5886&amp;pd_rd_i=1119002257&amp;psc=1&amp;linkCode=sl1&amp;tag=swdbooks-20&amp;linkId=309f73640a0cb64e3b676e26561d07a0&amp;language=en_US&amp;ref_=as_li_ss_tl">Storytelling with Data</a> by Cole Nussbaumer Knaflic (see this link for a <a href="https://docs.google.com/presentation/d/1IvMNJ7Au67Su-r2ulU1cV4wD1DGYaCEEinI_zE1t77A/edit?usp=sharing">visual summary</a> of the key concepts). <a href="https://www.bol.com/nl/nl/f/don-t-make-me/30073530/">Don’t Make Me Think</a> by Steve Krug is an excellent resource to learn more about usability.</li></ul><h3><em>Summarising | Your Roadmap to Break Into Data</em></h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/738/0*rZyceDiQyojXTirF" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=feccbe9c3dae" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Streamlining Contract Management in Revenue Infrastructure]]></title>
            <link>https://netflixtechblog.medium.com/streamlining-contract-management-in-revenue-infrastructure-38340f3ec4b9?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/38340f3ec4b9</guid>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[financial-engineering]]></category>
            <category><![CDATA[event-based-architecture]]></category>
            <category><![CDATA[big-data]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Mon, 04 Nov 2024 22:42:58 GMT</pubDate>
            <atom:updated>2024-11-04T22:42:58.093Z</atom:updated>
            <content:encoded><![CDATA[<p>By <a href="https://www.linkedin.com/in/austingundry">Austin Gundry</a>, <a href="https://www.linkedin.com/in/travis-chun-6a594413">Travis Chun</a>, <a href="https://www.linkedin.com/in/zianhu">Zian Hu</a></p><h3><strong>Introduction</strong></h3><p>At Netflix, a core tenet on our mission to entertain the world is meeting customers where they are. This means building intuitive signup flows on their favorite devices, or bundling Netflix subscriptions with services they already know and trust. To do this, we invest heavily in our partner relationships to incentivize and compensate them for making these customer touch points as reliable as possible.</p><p>These incentive agreements can take many forms, but two common patterns are partners integrating Netflix SDKs to help drive signups on their devices or bundling Netflix subscriptions with their products or services. Netflix in turn compensates the partners for these efforts so that both Netflix and the partner benefit while growing the member base.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/894/0*LHPMfpk4KB4kcH8Y" /></figure><p>With the rapid expansion of our streaming business, we were left with several disjoint systems governing these partner agreements and their downstream financial transactions. To realign for the future, we recently built a new tool to store this contract information, automate all associated financial transactions, and add layers of innovation that will allow us to remain operationally excellent. Most importantly, these contracts are now centrally managed in one location, significantly reducing the complexity of contract maintenance. We’re proud to present this project as an example of how we use software to enable our business and delight our stakeholders.</p><h3>Motivation</h3><p>Partner contracts cover broad relationships that can have a wide variety of terms and conditions dictating the partner’s compensation. This wide variety is worth supporting because partner-originating subscriptions are a significant portion of our member base. With this significant portion of revenue comes rigid requirements for meeting security guidelines, government audit requirements, and ensuring complete and accurate transactions with our partners.</p><p>Every month we have to close our financial books, and we strive to close within 3 to 5 days while most companies of our size close in 7 to 10 days. To do this, we have an incredible team of revenue accountants, but they require tools that allow them to operate at the best of their ability. This blog post details how we built our new Agreements tool.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*zK8iCkKyWoB2MqKV" /></figure><h3>Requirements</h3><p>Netflix generates financial events for millions of subscribers every day, and all of these events need to be processed to determine the compensation commitment to our partners. Further, our system is in the signup path for new members so downtime must be avoided at all costs. The resulting financial impact is very much material, so auditing, observability, versioning, and security are mission-critical for the success of this tool. Finally, from a UX standpoint, making changes to agreements in this tool needs to be intuitive and approachable because the risk of misconfiguration carries significant financial consequences.</p><p>Below you’ll find a high-level diagram of the flow of information:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*mtC6ErCHQ5_kSYZf" /></figure><h3>Data Model</h3><p>Beginning with our data model, these contracts previously existed across 5 subsystems that were all built independently, so the first step was identifying a data model with extensibility in its DNA. To that end, we came up with 5 major components:</p><p><em>Metadata</em>: High-level attributes like IDs, expiration dates, or links to the original PDF contracts.</p><p><em>Obligation</em>: Identifies a product type through which we owe compensation</p><p><em>Eligibility Mechanic</em>: Criteria to evaluate which financial events are applicable to a given term</p><p><em>Quantification Mechanic</em>: Terms used to calculate partner compensation on the eligible financial event</p><p><em>Processor</em>: Defines any additional aggregate processing needed</p><p>An example agreement might look like the following:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*eDMnlNnD7gWMANTS" /></figure><h3>Data Storage</h3><p>With a data model that we’re confident will extend to future generations of partnerships, we moved to identifying a storage solution that met our functional requirements. While document storage systems might seem like the natural choice due to their flexibility, frequently repeating sub-terms of individual contracts gave our data more of a relational structure. ACID compliance of relational databases also provided all-or-nothing guarantees which resolved previous pain points where edits across the many contract sub-systems could occur out-of-sync. Finally, these contracts are only updated when partner agreements are renegotiated so write volume of the system is expected to remain very low relative to reads. In the end, we landed on using CockroachDB as Netflix’s paved path RDBMS technology of choice. Finally, our front-end team or downstream clients can then fetch this information over gRPC or GraphQL interfaces.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*x9dKksUUyj4SuG3R" /></figure><p>Contract versioning and approval was implemented at the database schema layer, but we have also been able to take advantage of <a href="https://netflixtechblog.com/data-mesh-a-data-movement-and-processing-platform-netflix-1288bcab2873">in-house Change-Data-Capture solutions</a> for additional observability on edits. Finally, we needed extra redundancy for one subset of agreements related to sign up promotional codes as the signup path at Netflix is mission critical. To do this, we periodically backup these agreements to S3. Even if our entire database cluster is unresponsive, the application can still startup and satisfy these specific promotional requests.</p><h3>Migration and Launch</h3><p>Putting these agreements into action, we have an event processing architecture that listens for financial events over Kafka, and uses our new contract data to calculate the partner compensation impact. As contracts are updated, our system can replay all association financial events within the recent period to self-correct for any discrepancies. We built migration utilities to aggregate the contract details in each of the legacy systems, translate them to the modern definitions, and write these definitions into the new tool. From there, we set up shadow writes in our calculation pipeline so that we could audit a comparison of three months’ worth of financial data to make sure there were no regressions. With sign-off from our internal audit team after a comprehensive review, we were ready to launch.</p><h3>Looking to the Future</h3><p>Now that the system is in production, we can start to explore exciting areas of contract enablement. Our design and front-end teams built an incredible UX and we want to add to that with features like backdating contract changes or preview utilities to estimate impacts of contract changes.</p><p>This innovation would not have been possible without significant time investment from our accounting, tax, legal, and business development teams. If this sort of work excites you, consider <a href="https://explore.jobs.netflix.net/careers/job/790298014083">joining the Revenue Infrastructure team</a> as this is just the tip of the iceberg. We are excited about the upcoming opportunities and our team will be publishing more blog posts soon detailing how we maintain Netflix’s financial data, so stay tuned!</p><h3>Acknowledgments</h3><p>Special thanks to our stunning colleagues who contributed to this project’s success: <a href="https://www.linkedin.com/in/ninglu-abbey-wang/">Abbey Wang</a>, <a href="https://www.linkedin.com/in/christine-kyauk/">Christine Kyauk</a>, <a href="https://www.linkedin.com/in/esnell/">Eric Snell</a>, <a href="https://www.linkedin.com/in/jesseejohnson/">Jessee Johnson</a>, <a href="https://www.linkedin.com/in/jessicaline/">Jéssica Joaquim</a>, <a href="https://www.linkedin.com/in/eugene-c-9b355834/">Eugene Chiriliuc</a>, <a href="https://www.linkedin.com/in/kpanayotova/">Kalina Panayotova</a>, <a href="https://www.linkedin.com/in/kamran-kotobi-741b8a78/">Kamran Kotobi</a>, <a href="https://www.linkedin.com/in/mario-camacho-chi/">Mario Camacho</a>, <a href="https://www.linkedin.com/in/nataliitzler/">Natali Itzler</a>, <a href="https://www.linkedin.com/in/nicholaspedroso/">Nicholas Pedroso</a>, <a href="https://www.linkedin.com/in/sripaul/">Sripaul Chidambaram Asokan</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=38340f3ec4b9" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Investigation of a Workbench UI Latency Issue]]></title>
            <link>https://netflixtechblog.com/investigation-of-a-workbench-ui-latency-issue-faa017b4653d?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/faa017b4653d</guid>
            <category><![CDATA[debugging]]></category>
            <category><![CDATA[cpu]]></category>
            <category><![CDATA[jupyter-notebook]]></category>
            <category><![CDATA[performance]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Mon, 14 Oct 2024 20:02:31 GMT</pubDate>
            <atom:updated>2024-10-14T20:02:31.194Z</atom:updated>
            <content:encoded><![CDATA[<p>By: <a href="https://www.linkedin.com/in/hechaoli/">Hechao Li</a> and <a href="https://www.linkedin.com/in/mayworm/">Marcelo Mayworm</a></p><p>With special thanks to our stunning colleagues <a href="https://www.linkedin.com/in/amer-ather-9071181/">Amer Ather</a>, <a href="https://www.linkedin.com/in/itaydafna">Itay Dafna</a>, <a href="https://www.linkedin.com/in/lucaepozzi/">Luca Pozzi</a>, <a href="https://www.linkedin.com/in/matheusdeoleao/">Matheus Leão</a>, and <a href="https://www.linkedin.com/in/yeji682/">Ye Ji</a>.</p><h3>Overview</h3><p>At Netflix, the Analytics and Developer Experience organization, part of the Data Platform, offers a product called Workbench. Workbench is a remote development workspace based on<a href="https://netflixtechblog.com/titus-the-netflix-container-management-platform-is-now-open-source-f868c9fb5436"> Titus</a> that allows data practitioners to work with big data and machine learning use cases at scale. A common use case for Workbench is running<a href="https://jupyterlab.readthedocs.io/en/latest/"> JupyterLab</a> Notebooks.</p><p>Recently, several users reported that their JupyterLab UI becomes slow and unresponsive when running certain notebooks. This document details the intriguing process of debugging this issue, all the way from the UI down to the Linux kernel.</p><h3>Symptom</h3><p>Machine Learning engineer <a href="https://www.linkedin.com/in/lucaepozzi/">Luca Pozzi</a> reported to our Data Platform team that their <strong>JupyterLab UI on their workbench becomes slow and unresponsive when running some of their Notebooks.</strong> Restarting the <em>ipykernel</em> process, which runs the Notebook, might temporarily alleviate the problem, but the frustration persists as more notebooks are run.</p><h3>Quantify the Slowness</h3><p>While we observed the issue firsthand, the term “UI being slow” is subjective and difficult to measure. To investigate this issue, <strong>we needed a quantitative analysis of the slowness</strong>.</p><p><a href="https://www.linkedin.com/in/itaydafna">Itay Dafna</a> devised an effective and simple method to quantify the UI slowness. Specifically, we opened a terminal via JupyterLab and held down a key (e.g., “j”) for 15 seconds while running the user’s notebook. The input to stdin is sent to the backend (i.e., JupyterLab) via a WebSocket, and the output to stdout is sent back from the backend and displayed on the UI. We then exported the <em>.har </em>file recording all communications from the browser and loaded it into a Notebook for analysis.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ltV3CYtNjLCzolXD" /></figure><p>Using this approach, we observed latencies ranging from 1 to 10 seconds, averaging 7.4 seconds.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/704/0*H7KW62J0jZKPTjQH" /></figure><h3>Blame The Notebook</h3><p>Now that we have an objective metric for the slowness, let’s officially start our investigation. If you have read the symptom carefully, you must have noticed that the slowness only occurs when the user runs <strong>certain</strong> notebooks but not others.</p><p>Therefore, the first step is scrutinizing the specific Notebook experiencing the issue. Why does the UI always slow down after running this particular Notebook? Naturally, you would think that there must be something wrong with the code running in it.</p><p>Upon closely examining the user’s Notebook, we noticed a library called <em>pystan</em> , which provides Python bindings to a native C++ library called stan, looked suspicious. Specifically, <em>pystan</em> uses <em>asyncio</em>. However, <strong>because there is already an existing <em>asyncio</em> event loop running in the Notebook process and <em>asyncio</em> cannot be nested by design, in order for <em>pystan</em> to work, the authors of <em>pystan</em> </strong><a href="https://pystan.readthedocs.io/en/latest/faq.html#how-can-i-use-pystan-with-jupyter-notebook-or-jupyterlab"><strong>recommend</strong></a><strong> injecting <em>pystan</em> into the existing event loop by using a package called </strong><a href="https://pypi.org/project/nest-asyncio/"><strong><em>nest_asyncio</em></strong></a>, a library that became unmaintained because <a href="https://github.com/erdewit/ib_insync/commit/ef5ea29e44e0c40bbadbc16c2281b3ac58aa4a40">the author unfortunately passed away</a>.</p><p>Given this seemingly hacky usage, we naturally suspected that the events injected by <em>pystan</em> into the event loop were blocking the handling of the WebSocket messages used to communicate with the JupyterLab UI. This reasoning sounds very plausible. However, <strong>the user claimed that there were cases when a Notebook not using <em>pystan</em> runs, the UI also became slow</strong>.</p><p>Moreover, after several rounds of discussion with ChatGPT, we learned more about the architecture and realized that, in theory, <strong>the usage of <em>pystan</em> and <em>nest_asyncio</em> should not cause the slowness in handling the UI WebSocket</strong> for the following reasons:</p><p>Even though <em>pystan</em> uses <em>nest_asyncio</em> to inject itself into the main event loop, <strong>the Notebook runs on a child process (i.e.</strong>,<strong> the <em>ipykernel</em> process) of the <em>jupyter-lab</em> server process</strong>, which means the main event loop being injected by <em>pystan</em> is that of the <em>ipykernel</em> process, not the <em>jupyter-server</em> process. Therefore, even if <em>pystan</em> blocks the event loop, it shouldn’t impact the <em>jupyter-lab</em> main event loop that is used for UI websocket communication. See the diagram below:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/738/0*DsQuZV5qnRXp5mVw" /></figure><p>In other words, <strong><em>pystan</em> events are injected to the event loop B in this diagram instead of event loop A</strong>. So, it shouldn’t block the UI WebSocket events.</p><p>You might also think that because event loop A handles both the WebSocket events from the UI and the ZeroMQ socket events from the <em>ipykernel</em> process, a high volume of ZeroMQ events generated by the notebook could block the WebSocket. However, <strong>when we captured packets on the ZeroMQ socket while reproducing the issue, we didn’t observe heavy traffic on this socket that could cause such blocking</strong>.</p><p>A stronger piece of evidence to rule out <em>pystan</em> was that we were ultimately able to reproduce the issue even without it, which I’ll dive into later.</p><h3>Blame Noisy Neighbors</h3><p>The Workbench instance runs as a <a href="https://netflixtechblog.com/titus-the-netflix-container-management-platform-is-now-open-source-f868c9fb5436">Titus container</a>. To efficiently utilize our compute resources, <strong>Titus employs a CPU oversubscription feature</strong>, meaning the combined virtual CPUs allocated to containers exceed the number of available physical CPUs on a Titus agent. <strong>If a container is unfortunate enough to be scheduled alongside other “noisy” containers — those that consume a lot of CPU resources — it could suffer from CPU deficiency.</strong></p><p>However, after examining the CPU utilization of neighboring containers on the same Titus agent as the Workbench instance, as well as the overall CPU utilization of the Titus agent, we quickly ruled out this hypothesis. Using the top command on the Workbench, we observed that when running the Notebook, <strong>the Workbench instance uses only 4 out of the 64 CPUs allocated to it</strong>. Simply put, <strong>this workload is not CPU-bound.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/892/0*YXsntKLiontnkNhf" /></figure><h3>Blame The Network</h3><p>The next theory was that the network between the web browser UI (on the laptop) and the JupyterLab server was slow. To investigate, we <strong>captured all the packets between the laptop and the server</strong> while running the Notebook and continuously pressing ‘j’ in the terminal.</p><p>When the UI experienced delays, we observed a 5-second pause in packet transmission from server port 8888 to the laptop. Meanwhile,<strong> traffic from other ports, such as port 22 for SSH, remained unaffected</strong>. This led us to conclude that the pause was caused by the application running on port 8888 (i.e., the JupyterLab process) rather than the network.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*c660xBwF4XuCA8KN" /></figure><h3>The Minimal Reproduction</h3><p>As previously mentioned, another strong piece of evidence proving the innocence of pystan was that we could reproduce the issue without it. By gradually stripping down the “bad” Notebook, we eventually arrived at a minimal snippet of code that reproduces the issue without any third-party dependencies or complex logic:</p><pre>import time<br>import os<br>from multiprocessing import Process<br><br>N = os.cpu_count()<br><br>def launch_worker(worker_id):<br>  time.sleep(60)<br><br>if __name__ == &#39;__main__&#39;:<br>  with open(&#39;/root/2GB_file&#39;, &#39;r&#39;) as file:<br>    data = file.read()<br>    processes = []<br>    for i in range(N):<br>      p = Process(target=launch_worker, args=(i,))<br>      processes.append(p)<br>      p.start()<br> <br>    for p in processes:<br>      p.join()</pre><p>The code does only two things:</p><ol><li>Read a 2GB file into memory (the Workbench instance has 480G memory in total so this memory usage is almost negligible).</li><li>Start N processes where N is the number of CPUs. The N processes do nothing but sleep.</li></ol><p>There is no doubt that this is the most silly piece of code I’ve ever written. It is neither CPU bound nor memory bound. Yet <strong>it can cause the JupyterLab UI to stall for as many as 10 seconds!</strong></p><h3>Questions</h3><p>There are a couple of interesting observations that raise several questions:</p><ul><li>We noticed that <strong>both steps are required in order to reproduce the issue</strong>. If you don’t read the 2GB file (that is not even used!), the issue is not reproducible. <strong>Why using 2GB out of 480GB memory could impact the performance?</strong></li><li><strong>When the UI delay occurs, the <em>jupyter-lab</em> process CPU utilization spikes to 100%</strong>, hinting at contention on the single-threaded event loop in this process (event loop A in the diagram before). <strong>What does the <em>jupyter-lab</em> process need the CPU for, given that it is not the process that runs the Notebook?</strong></li><li>The code runs in a Notebook, which means it runs in the <em>ipykernel</em> process, that is a child process of the <em>jupyter-lab</em> process. <strong>How can anything that happens in a child process cause the parent process to have CPU contention?</strong></li><li>The workbench has 64CPUs. But when we printed <em>os.cpu_count()</em>, the output was 96. That means <strong>the code starts more processes than the number of CPUs</strong>. <strong>Why is that?</strong></li></ul><p>Let’s answer the last question first. In fact, if you run <em>lscpu</em> and <em>nproc</em> commands inside a Titus container, you will also see different results — the former gives you 96, which is the number of physical CPUs on the Titus agent, whereas the latter gives you 64, which is the number of virtual CPUs allocated to the container. This discrepancy is due to the lack of a “CPU namespace” in the Linux kernel, causing the number of physical CPUs to be leaked to the container when calling certain functions to get the CPU count. The assumption here is that Python <strong><em>os.cpu_count()</em> uses the same function as the <em>lscpu</em> command, causing it to get the CPU count of the host instead of the container</strong>. Python 3.13 has <a href="https://docs.python.org/3.13/library/os.html#os.process_cpu_count">a new call that can be used to get the accurate CPU count</a>, but it’s not GA’ed yet.</p><p>It will be proven later that this inaccurate number of CPUs can be a contributing factor to the slowness.</p><h3>More Clues</h3><p>Next, we used <em>py-spy</em> to do a profiling of the <em>jupyter-lab</em> process. Note that we profiled the parent <em>jupyter-lab </em>process, <strong>not</strong> the <em>ipykernel</em> child process that runs the reproduction code. The profiling result is as follows:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ho2C4015Disa8aFv" /></figure><p>As one can see, <strong>a lot of CPU time (89%!!) is spent on a function called <em>__parse_smaps_rollup</em></strong>. In comparison, the terminal handler used only 0.47% CPU time. From the stack trace, we see that <strong>this function is inside the event loop A</strong>,<strong> so it can definitely cause the UI WebSocket events to be delayed</strong>.</p><p>The stack trace also shows that this function is ultimately called by a function used by a Jupyter lab extension called <em>jupyter_resource_usage</em>. <strong>We then disabled this extension and restarted the <em>jupyter-lab</em> process. As you may have guessed, we could no longer reproduce the slowness!</strong></p><p>But our puzzle is not solved yet. Why does this extension cause the UI to slow down? Let’s keep digging.</p><h3>Root Cause Analysis</h3><p>From the name of the extension and the names of the other functions it calls, we can infer that this extension is used to get resources such as CPU and memory usage information. Examining the code, we see that this function call stack is triggered when an API endpoint <em>/metrics/v1</em> is called from the UI. <strong>The UI apparently calls this function periodically</strong>, according to the network traffic tab in Chrome’s Developer Tools.</p><p>Now let’s look at the implementation starting from the call <em>get(jupter_resource_usage/api.py:42)</em> . The full code is <a href="https://github.com/jupyter-server/jupyter-resource-usage/blob/6f15ef91d5c7e50853516b90b5e53b3913d2ed34/jupyter_resource_usage/api.py#L28">here</a> and the key lines are shown below:</p><pre>cur_process = psutil.Process()<br>all_processes = [cur_process] + cur_process.children(recursive=True)<br><br>for p in all_processes:<br>  info = p.memory_full_info()</pre><p>Basically, it gets all children processes of the <em>jupyter-lab</em> process recursively, including both the <em>ipykernel</em> Notebook process and all processes created by the Notebook. Obviously, <strong>the cost of this function is linear to the number of all children processes</strong>. In the reproduction code, we create 96 processes. So here we will have at least 96 (sleep processes) + 1 (<em>ipykernel</em> process) + 1 (<em>jupyter-lab</em> process) = 98 processes when it should actually be 64 (allocated CPUs) + 1 (<em>ipykernel</em> process) + 1 <em>(jupyter-lab</em> process) = 66 processes, because the number of CPUs allocated to the container is, in fact, 64.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/971/0*sHTjycVMUk1yVAsk" /></figure><p>This is truly ironic. <strong>The more CPUs we have, the slower we are!</strong></p><p>At this point, we have answered one question: <strong>Why does starting many grandchildren processes in the child process cause the parent process to be slow? </strong>Because the parent process runs a function that’s linear to the number all children process recursively.</p><p>However, this solves only half of the puzzle. If you remember the previous analysis, <strong>starting many child processes ALONE doesn’t reproduce the issue</strong>. If we don’t read the 2GB file, even if we create 2x more processes, we can’t reproduce the slowness.</p><p>So now we must answer the next question: <strong>Why does reading a 2GB file in the child process affect the parent process performance, </strong>especially when the workbench has as much as 480GB memory in total?</p><p>To answer this question, let’s look closely at the function <em>__parse_smaps_rollup</em>. As the name implies, <a href="https://github.com/giampaolo/psutil/blob/c034e6692cf736b5e87d14418a8153bb03f6cf42/psutil/_pslinux.py#L1978">this function</a> parses the file <em>/proc/&lt;pid&gt;/smaps_rollup</em>.</p><pre>def _parse_smaps_rollup(self):<br>  uss = pss = swap = 0<br>  with open_binary(&quot;{}/{}/smaps_rollup&quot;.format(self._procfs_path, self.pid)) as f:<br>  for line in f:<br>    if line.startswith(b”Private_”):<br>    # Private_Clean, Private_Dirty, Private_Hugetlb<br>      s uss += int(line.split()[1]) * 1024<br>    elif line.startswith(b”Pss:”):<br>      pss = int(line.split()[1]) * 1024<br>    elif line.startswith(b”Swap:”):<br>      swap = int(line.split()[1]) * 1024<br>return (uss, pss, swap)</pre><p>Naturally, you might think that when memory usage increases, this file becomes larger in size, causing the function to take longer to parse. Unfortunately, this is not the answer because:</p><ul><li>First, <a href="https://www.kernel.org/doc/Documentation/ABI/testing/procfs-smaps_rollup"><strong>the number of lines in this file is constant</strong></a><strong> for all processes</strong>.</li><li>Second, <strong>this is a special file in the /proc filesystem, which should be seen as a kernel interface</strong> instead of a regular file on disk. In other words, <strong>I/O operations of this file are handled by the kernel rather than disk</strong>.</li></ul><p>This file was introduced in <a href="https://github.com/torvalds/linux/commit/493b0e9d945fa9dfe96be93ae41b4ca4b6fdb317#diff-cb79e2d6ea6f9627ff68d1342a219f800e04ff6c6fa7b90c7e66bb391b2dd3ee">this commit</a> in 2017, with the purpose of improving the performance of user programs that determine aggregate memory statistics. Let’s first focus on <a href="https://elixir.bootlin.com/linux/v6.5.13/source/fs/proc/task_mmu.c#L1025">the handler of <em>open</em> syscall</a> on this <em>/proc/&lt;pid&gt;/smaps_rollup</em>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/995/0*vGOD79Tleii7X22B" /></figure><p>Following through the <em>single_open</em> <a href="https://elixir.bootlin.com/linux/v6.5.13/source/fs/seq_file.c#L582">function</a>, we will find that it uses the function <em>show_smaps_rollup</em> for the show operation, which can translate to the <em>read</em> system call on the file. Next, we look at the <em>show_smaps_rollup</em> <a href="https://elixir.bootlin.com/linux/v6.5.13/source/fs/proc/task_mmu.c#L916">implementation</a>. You will notice <strong>a do-while loop that is linear to the virtual memory area</strong>.</p><pre>static int show_smaps_rollup(struct seq_file *m, void *v) {<br>  …<br>  vma_start = vma-&gt;vm_start;<br>  do {<br>    smap_gather_stats(vma, &amp;mss, 0);<br>    last_vma_end = vma-&gt;vm_end;<br>    …<br>  } for_each_vma(vmi, vma);<br>  …<br>}</pre><p>This perfectly <strong>explains why the function gets slower when a 2GB file is read into memory</strong>. <strong>Because the handler of reading the <em>smaps_rollup</em> file now takes longer to run the while loop</strong>. Basically, even though <strong><em>smaps_rollup</em></strong> already improved the performance of getting memory information compared to the old method of parsing the <em>/proc/&lt;pid&gt;/smaps</em> file, <strong>it is still linear to the virtual memory used</strong>.</p><h3>More Quantitative Analysis</h3><p>Even though at this point the puzzle is solved, let’s conduct a more quantitative analysis. How much is the time difference when reading the <em>smaps_rollup</em> file with small versus large virtual memory utilization? Let’s write some simple benchmark code like below:</p><pre>import os<br><br>def read_smaps_rollup(pid):<br>  with open(&quot;/proc/{}/smaps_rollup&quot;.format(pid), &quot;rb&quot;) as f:<br>    for line in f:<br>      pass<br><br>if __name__ == “__main__”:<br>  pid = os.getpid()<br>  <br>  read_smaps_rollup(pid)<br><br>  with open(“/root/2G_file”, “rb”) as f:<br>    data = f.read()<br><br>  read_smaps_rollup(pid)</pre><p>This program performs the following steps:</p><ol><li>Reads the <em>smaps_rollup</em> file of the current process.</li><li>Reads a 2GB file into memory.</li><li>Repeats step 1.</li></ol><p>We then use <em>strace</em> to find the accurate time of reading the <em>smaps_rollup</em> file.</p><pre>$ sudo strace -T -e trace=openat,read python3 benchmark.py 2&gt;&amp;1 | grep “smaps_rollup” -A 1<br><br>openat(AT_FDCWD, “/proc/3107492/smaps_rollup”, O_RDONLY|O_CLOEXEC) = 3 &lt;0.000023&gt;<br>read(3, “560b42ed4000–7ffdadcef000 — -p 0”…, 1024) = 670 &lt;0.000259&gt;<br>...<br>openat(AT_FDCWD, “/proc/3107492/smaps_rollup”, O_RDONLY|O_CLOEXEC) = 3 &lt;0.000029&gt;<br>read(3, “560b42ed4000–7ffdadcef000 — -p 0”…, 1024) = 670 &lt;0.027698&gt;</pre><p>As you can see, both times, the read <em>syscall</em> returned 670, meaning the file size remained the same at 670 bytes. However, <strong>the time it took the second time (i.e.</strong>,<strong> 0.027698 seconds) is 100x the time it took the first time (i.e.</strong>,<strong> 0.000259 seconds)</strong>! This means that if there are 98 processes, the time spent on reading this file alone will be 98 * 0.027698 = 2.7 seconds! Such a delay can significantly affect the UI experience.</p><h3>Solution</h3><p>This extension is used to display the CPU and memory usage of the notebook process on the bar at the bottom of the Notebook:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/524/0*bNYMYTc5QQAxLyya" /></figure><p>We confirmed with the user that disabling the <em>jupyter-resource-usage</em> extension meets their requirements for UI responsiveness, and that this extension is not critical to their use case. Therefore, we provided a way for them to disable the extension.</p><h3>Summary</h3><p>This was such a challenging issue that required debugging from the UI all the way down to the Linux kernel. It is fascinating that the problem is linear to both the number of CPUs and the virtual memory size — two dimensions that are generally viewed separately.</p><p>Overall, we hope you enjoyed the irony of:</p><ol><li>The extension used to monitor CPU usage causing CPU contention.</li><li>An interesting case where the more CPUs you have, the slower you get!</li></ol><p>If you’re excited by tackling such technical challenges and have the opportunity to solve complex technical challenges and drive innovation, consider joining our <a href="https://explore.jobs.netflix.net/careers?query=Data%20Platform&amp;pid=790298020581&amp;domain=netflix.com&amp;sort_by=relevance">Data Platform team</a>s. Be part of shaping the future of Data Security and Infrastructure, Data Developer Experience, Analytics Infrastructure and Enablement, and more. Explore the impact you can make with us!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=faa017b4653d" width="1" height="1" alt=""><hr><p><a href="https://netflixtechblog.com/investigation-of-a-workbench-ui-latency-issue-faa017b4653d">Investigation of a Workbench UI Latency Issue</a> was originally published in <a href="https://netflixtechblog.com">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Introducing Netflix’s TimeSeries Data Abstraction Layer]]></title>
            <link>https://netflixtechblog.com/introducing-netflix-timeseries-data-abstraction-layer-31552f6326f8?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/31552f6326f8</guid>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Tue, 08 Oct 2024 17:01:53 GMT</pubDate>
            <atom:updated>2024-10-13T03:41:58.855Z</atom:updated>
            <content:encoded><![CDATA[<p>By <a href="https://www.linkedin.com/in/rajiv-shringi">Rajiv Shringi</a>, <a href="https://www.linkedin.com/in/vinaychella/">Vinay Chella</a>, <a href="https://www.linkedin.com/in/kaidanfullerton/">Kaidan Fullerton</a>, <a href="https://www.linkedin.com/in/oleksii-tkachuk-98b47375/">Oleksii Tkachuk</a>, <a href="https://www.linkedin.com/in/joseph-lynch-9976a431/">Joey Lynch</a></p><h3><strong>Introduction</strong></h3><p>As Netflix continues to expand and diversify into various sectors like <strong>Video on Demand</strong> and <strong>Gaming</strong>, the ability to ingest and store vast amounts of temporal data — often reaching petabytes — with millisecond access latency has become increasingly vital. In previous blog posts, we introduced the <a href="https://netflixtechblog.com/introducing-netflixs-key-value-data-abstraction-layer-1ea8a0a11b30"><strong>Key-Value Data Abstraction Layer</strong></a> and the <a href="https://netflixtechblog.medium.com/data-gateway-a-platform-for-growing-and-protecting-the-data-tier-f1ed8db8f5c6"><strong>Data Gateway Platform</strong></a>, both of which are integral to Netflix’s data architecture. The Key-Value Abstraction offers a flexible, scalable solution for storing and accessing structured key-value data, while the Data Gateway Platform provides essential infrastructure for protecting, configuring, and deploying the data tier.</p><p>Building on these foundational abstractions, we developed the <strong>TimeSeries Abstraction</strong> — a versatile and scalable solution designed to efficiently store and query large volumes of temporal event data with low millisecond latencies, all in a cost-effective manner across various use cases.</p><p>In this post, we will delve into the architecture, design principles, and real-world applications of the <strong>TimeSeries Abstraction</strong>, demonstrating how it enhances our platform’s ability to manage temporal data at scale.</p><p><strong>Note: </strong><em>Contrary to what the name may suggest, this system is not built as a general-purpose time series database. We do not use it for metrics, histograms, timers, or any such near-real time analytics use case. Those use cases are well served by the Netflix </em><a href="https://netflixtechblog.com/introducing-atlas-netflixs-primary-telemetry-platform-bd31f4d8ed9a"><em>Atlas</em></a><em> telemetry system. Instead, we focus on addressing the challenge of storing and accessing extremely high-throughput, immutable temporal event data in a low-latency and cost-efficient manner.</em></p><h3>Challenges</h3><p>At Netflix, temporal data is continuously generated and utilized, whether from user interactions like video-play events, asset impressions, or complex micro-service network activities. Effectively managing this data at scale to extract valuable insights is crucial for ensuring optimal user experiences and system reliability.</p><p>However, storing and querying such data presents a unique set of challenges:</p><ul><li><strong>High Throughput</strong>: Managing up to 10 million writes per second while maintaining high availability.</li><li><strong>Efficient Querying in Large Datasets</strong>: Storing petabytes of data while ensuring primary key reads return results within low double-digit milliseconds, and supporting searches and aggregations across multiple secondary attributes.</li><li><strong>Global Reads and Writes</strong>: Facilitating read and write operations from anywhere in the world with adjustable consistency models.</li><li><strong>Tunable Configuration</strong>: Offering the ability to partition datasets in either a single-tenant or multi-tenant datastore, with options to adjust various dataset aspects such as retention and consistency.</li><li><strong>Handling Bursty Traffic</strong>: Managing significant traffic spikes during high-demand events, such as new content launches or regional failovers.</li><li><strong>Cost Efficiency</strong>: Reducing the cost per byte and per operation to optimize long-term retention while minimizing infrastructure expenses, which can amount to millions of dollars for Netflix.</li></ul><h3>TimeSeries Abstraction</h3><p>The TimeSeries Abstraction was developed to meet these requirements, built around the following core design principles:</p><ul><li><strong>Partitioned Data</strong>: Data is partitioned using a unique temporal partitioning strategy combined with an event bucketing approach to efficiently manage bursty workloads and streamline queries.</li><li><strong>Flexible Storage</strong>: The service is designed to integrate with various storage backends, including <a href="https://cassandra.apache.org/_/index.html">Apache Cassandra</a> and <a href="https://www.elastic.co/elasticsearch">Elasticsearch</a>, allowing Netflix to customize storage solutions based on specific use case requirements.</li><li><strong>Configurability</strong>: TimeSeries offers a range of tunable options for each dataset, providing the flexibility needed to accommodate a wide array of use cases.</li><li><strong>Scalability</strong>: The architecture supports both horizontal and vertical scaling, enabling the system to handle increasing throughput and data volumes as Netflix expands its user base and services.</li><li><strong>Sharded Infrastructure</strong>: Leveraging the <strong>Data Gateway Platform</strong>, we can deploy single-tenant and/or multi-tenant infrastructure with the necessary access and traffic isolation.</li></ul><p>Let’s dive into the various aspects of this abstraction.</p><h3>Data Model</h3><p>We follow a unique event data model that encapsulates all the data we want to capture for events, while allowing us to query them efficiently.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*jl30Jl559Fnd29in" /></figure><p>Let’s start with the smallest unit of data in the abstraction and work our way up.</p><ul><li><strong>Event Item</strong>: An event item is a key-value pair that users use to store data for a given event. For example: <em>{“device_type”: “ios”}</em>.</li><li><strong>Event</strong>: An event is a structured collection of one or more such event items. An event occurs at a specific point in time and is identified by a client-generated timestamp and an event identifier (such as a UUID). This combination of <strong>event_time</strong> and <strong>event_id</strong> also forms part of the unique idempotency key for the event, enabling users to safely retry requests.</li><li><strong>Time Series ID</strong>: A <strong>time_series_id</strong> is a collection of one or more such events over the dataset’s retention period. For instance, a <strong>device_id</strong> would store all events occurring for a given device over the retention period. All events are immutable, and the TimeSeries service only ever appends events to a given time series ID.</li><li><strong>Namespace</strong>: A namespace is a collection of time series IDs and event data, representing the complete TimeSeries dataset. Users can create one or more namespaces for each of their use cases. The abstraction applies various tunable options at the namespace level, which we will discuss further when we explore the service’s control plane.</li></ul><h3>API</h3><p>The abstraction provides the following APIs to interact with the event data.</p><p><strong>WriteEventRecordsSync</strong>: This endpoint writes a batch of events and sends back a durability acknowledgement to the client. This is used in cases where users require a guarantee of durability.</p><p><strong>WriteEventRecords</strong>: This is the fire-and-forget version of the above endpoint. It enqueues a batch of events without the durability acknowledgement. This is used in cases like logging or tracing, where users care more about throughput and can tolerate a small amount of data loss.</p><pre>{<br>  &quot;namespace&quot;: &quot;my_dataset&quot;,<br>  &quot;events&quot;: [<br>    {<br>      &quot;timeSeriesId&quot;: &quot;profile100&quot;,<br>      &quot;eventTime&quot;: &quot;2024-10-03T21:24:23.988Z&quot;,<br>      &quot;eventId&quot;: &quot;550e8400-e29b-41d4-a716-446655440000&quot;,<br>      &quot;eventItems&quot;: [<br>        {<br>          &quot;eventItemKey&quot;: &quot;deviceType&quot;,  <br>          &quot;eventItemValue&quot;: &quot;aW9z&quot;<br>        },<br>        {<br>          &quot;eventItemKey&quot;: &quot;deviceMetadata&quot;,<br>          &quot;eventItemValue&quot;: &quot;c29tZSBtZXRhZGF0YQ==&quot;<br>        }<br>      ]<br>    },<br>    {<br>      &quot;timeSeriesId&quot;: &quot;profile100&quot;,<br>      &quot;eventTime&quot;: &quot;2024-10-03T21:23:30.000Z&quot;,<br>      &quot;eventId&quot;: &quot;123e4567-e89b-12d3-a456-426614174000&quot;,<br>      &quot;eventItems&quot;: [<br>        {<br>          &quot;eventItemKey&quot;: &quot;deviceType&quot;,  <br>          &quot;eventItemValue&quot;: &quot;YW5kcm9pZA==&quot;<br>        }<br>      ]<br>    }<br>  ]<br>}</pre><p><strong>ReadEventRecords</strong>: Given a combination of a namespace, a timeSeriesId, a timeInterval, and optional eventFilters, this endpoint returns all the matching events, sorted descending by event_time, with low millisecond latency.</p><pre>{<br>  &quot;namespace&quot;: &quot;my_dataset&quot;,<br>  &quot;timeSeriesId&quot;: &quot;profile100&quot;,<br>  &quot;timeInterval&quot;: {<br>    &quot;start&quot;: &quot;2024-10-02T21:00:00.000Z&quot;,<br>    &quot;end&quot;:   &quot;2024-10-03T21:00:00.000Z&quot;<br>  },<br>  &quot;eventFilters&quot;: [<br>    {<br>      &quot;matchEventItemKey&quot;: &quot;deviceType&quot;,<br>      &quot;matchEventItemValue&quot;: &quot;aW9z&quot;<br>    }<br>  ],<br>  &quot;pageSize&quot;: 100,<br>  &quot;totalRecordLimit&quot;: 1000<br>}</pre><p><strong>SearchEventRecords</strong>: Given a search criteria and a time interval, this endpoint returns all the matching events. These use cases are fine with eventually consistent reads.</p><pre>{<br>  &quot;namespace&quot;: &quot;my_dataset&quot;,<br>  &quot;timeInterval&quot;: {<br>    &quot;start&quot;: &quot;2024-10-02T21:00:00.000Z&quot;,<br>    &quot;end&quot;: &quot;2024-10-03T21:00:00.000Z&quot;<br>  },<br>  &quot;searchQuery&quot;: {<br>    &quot;booleanQuery&quot;: {<br>      &quot;searchQuery&quot;: [<br>        {<br>          &quot;equals&quot;: {<br>            &quot;eventItemKey&quot;: &quot;deviceType&quot;,<br>            &quot;eventItemValue&quot;: &quot;aW9z&quot;<br>          }<br>        },<br>        {<br>          &quot;range&quot;: {<br>            &quot;eventItemKey&quot;: &quot;deviceRegistrationTimestamp&quot;,<br>            &quot;lowerBound&quot;: {<br>              &quot;eventItemValue&quot;: &quot;MjAyNC0xMC0wMlQwMDowMDowMC4wMDBa&quot;,<br>              &quot;inclusive&quot;: true<br>            },<br>            &quot;upperBound&quot;: {<br>              &quot;eventItemValue&quot;: &quot;MjAyNC0xMC0wM1QwMDowMDowMC4wMDBa&quot;<br>            }<br>          }<br>        }<br>      ],<br>      &quot;operator&quot;: &quot;AND&quot;<br>    }<br>  },<br>  &quot;pageSize&quot;: 100,<br>  &quot;totalRecordLimit&quot;: 1000<br>}</pre><p><strong>AggregateEventRecords</strong>: Given a search criteria and an aggregation mode (e.g. DistinctAggregation) , this endpoint performs the given aggregation within a given time interval. Similar to the Search endpoint, users can tolerate eventual consistency and a potentially higher latency (in seconds).</p><pre>{<br>  &quot;namespace&quot;: &quot;my_dataset&quot;,<br>  &quot;timeInterval&quot;: {<br>    &quot;start&quot;: &quot;2024-10-02T21:00:00.000Z&quot;,<br>    &quot;end&quot;: &quot;2024-10-03T21:00:00.000Z&quot;<br>  },<br>  &quot;searchQuery&quot;: {...some search criteria...},<br>  &quot;aggregationQuery&quot;: {<br>    &quot;distinct&quot;: {<br>      &quot;eventItemKey&quot;: &quot;deviceType&quot;,<br>      &quot;pageSize&quot;: 100<br>    }<br>  }<br>}</pre><p>In the subsequent sections, we will talk about how we interact with this data at the storage layer.</p><h3>Storage Layer</h3><p>The storage layer for TimeSeries comprises a primary data store and an optional index data store. The primary data store ensures data durability during writes and is used for primary read operations, while the index data store is utilized for search and aggregate operations. At Netflix, <strong>Apache Cassandra</strong> is the preferred choice for storing durable data in high-throughput scenarios, while <strong>Elasticsearch</strong> is the preferred data store for indexing. However, similar to our approach with the API, the storage layer is not tightly coupled to these specific data stores. Instead, we define storage API contracts that must be fulfilled, allowing us the flexibility to replace the underlying data stores as needed.</p><h3>Primary Datastore</h3><p>In this section, we will talk about how we leverage <strong>Apache Cassandra</strong> for TimeSeries use cases.</p><h4>Partitioning Scheme</h4><p>At Netflix’s scale, the continuous influx of event data can quickly overwhelm traditional databases. Temporal partitioning addresses this challenge by dividing the data into manageable chunks based on time intervals, such as hourly, daily, or monthly windows. This approach enables efficient querying of specific time ranges without the need to scan the entire dataset. It also allows Netflix to archive, compress, or delete older data efficiently, optimizing both storage and query performance. Additionally, this partitioning mitigates the performance issues typically associated with <a href="https://thelastpickle.com/blog/2019/01/11/wide-partitions-cassandra-3-11.html">wide partitions</a> in Cassandra. By employing this strategy, we can operate at much higher disk utilization, as it reduces the need to reserve large amounts of disk space for compactions, thereby saving costs.</p><p>Here is what it looks like :</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*MxuEH6_pOVDcAMie" /></figure><p><strong>Time Slice: </strong>A<strong> </strong>time slice is the unit of data retention and maps directly to a Cassandra table. We create multiple such time slices, each covering a specific interval of time. An event lands in one of these slices based on the <strong>event_time</strong>. These slices are joined with <em>no time gaps</em><strong> </strong>in between, with operations being <em>start-inclusive</em> and <em>end-exclusive</em>, ensuring that all data lands in one of the slices. By utilizing these time slices, we can efficiently implement retention by dropping entire tables, which reduces storage space and saves on costs.</p><p><strong>Why not use row-based Time-To-Live (TTL)?</strong></p><p>Using TTL on individual events would generate a significant number of <a href="https://thelastpickle.com/blog/2016/07/27/about-deletes-and-tombstones.html">tombstones</a> in Cassandra, degrading performance, especially during range scans. By employing discrete time slices and dropping them, we avoid the tombstone issue entirely. The tradeoff is that data may be retained slightly longer than necessary, as an entire table’s time range must fall outside the retention window before it can be dropped. Additionally, TTLs are difficult to adjust later, whereas TimeSeries can extend the dataset retention instantly with a single control plane operation.</p><p><strong>Time Buckets</strong>: Within a time slice, data is further partitioned into time buckets. This facilitates effective range scans by allowing us to target specific time buckets for a given query range. The tradeoff is that if a user wants to read the entire range of data over a large time period, we must scan many partitions. We mitigate potential latency by scanning these partitions in parallel and aggregating the data at the end. In most cases, the advantage of targeting smaller data subsets outweighs the read amplification from these scatter-gather operations. Typically, users read a smaller subset of data rather than the entire retention range.</p><p><strong>Event Buckets</strong>: To manage extremely high-throughput write operations, which may result in a burst of writes for a given time series within a short period, we further divide the time bucket into event buckets. This prevents overloading the same partition for a given time range and also reduces partition sizes further, albeit with a slight increase in read amplification.</p><p><strong>Note</strong>: <em>With Cassandra 4.x onwards, we notice a substantial improvement in the performance of scanning a range of data in a wide partition. See </em><strong><em>Future Enhancements</em></strong><em> at the end to see the </em><strong><em>Dynamic Event bucketing</em></strong><em> work that aims to take advantage of this.</em></p><h4>Storage Tables</h4><p>We use two kinds of tables</p><ul><li><strong>Data tables</strong>: These are the time slices that store the actual event data.</li><li><strong>Metadata table</strong>: This table stores information about how each time slice is configured <em>per namespace</em>.</li></ul><h4>Data tables</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ktuEBzveeK4f1mWH" /></figure><p>The partition key enables splitting events for a <strong>time_series_id</strong> over a range of <strong>time_bucket(s)</strong> and <strong>event_bucket(s)</strong>, thus mitigating hot partitions, while the clustering key allows us to keep data sorted on disk in the order we almost always want to read it. The <strong>value_metadata</strong> column stores metadata for the <strong>event_item_value</strong> such as compression.</p><p><strong>Writing to the data table:</strong></p><p>User writes will land in a given time slice, time bucket, and event bucket as a factor of the <strong>event_time</strong> attached to the event. This factor is dictated by the control plane configuration of a given namespace.</p><p>For example:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*P4IThIE_PE9F8KYi" /></figure><p>During this process, the writer makes decisions on how to handle the data before writing, such as whether to compress it. The <strong>value_metadata</strong> column records any such post-processing actions, ensuring that the reader can accurately interpret the data.</p><p><strong>Reading from the data table:</strong></p><p>The below illustration depicts at a high-level on how we scatter-gather the reads from multiple partitions and join the result set at the end to return the final result.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*a805txbeIDqYP73d" /></figure><h4>Metadata table</h4><p>This table stores the configuration data about the time slices for a given namespace.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*asJFOjl1iwlSajJc" /></figure><p>Note the following:</p><ul><li><strong>No Time Gaps</strong>: The end_time of a given time slice overlaps with the start_time of the next time slice, ensuring all events find a home.</li><li><strong>Retention</strong>: The status indicates which tables fall inside and outside of the retention window.</li><li><strong>Flexible</strong>: This metadata can be adjusted per time slice, allowing us to tune the partition settings of future time slices based on observed data patterns in the current time slice.</li></ul><p>There is a lot more information that can be stored into the <strong>metadata</strong> column (e.g., compaction settings for the table), but we only show the partition settings here for brevity.</p><h3>Index Datastore</h3><p>To support secondary access patterns via non-primary key attributes, we index data into Elasticsearch. Users can configure a list of attributes per namespace that they wish to search and/or aggregate data on. The service extracts these fields from events as they stream in, indexing the resultant documents into Elasticsearch. Depending on the throughput, we may use Elasticsearch as a reverse index, retrieving the full data from Cassandra, or we may store the entire source data directly in Elasticsearch.</p><p><strong>Note</strong>:<em> Again, users are never directly exposed to Elasticsearch, just like they are not directly exposed to Cassandra. Instead, they interact with the Search and Aggregate API endpoints that translate a given query to that needed for the underlying datastore.</em></p><p>In the next section, we will talk about how we configure these data stores for different datasets.</p><h3>Control Plane</h3><p>The data plane is responsible for executing the read and write operations, while the control plane configures every aspect of a namespace’s behavior. The data plane communicates with the TimeSeries control stack, which manages this configuration information. In turn, the TimeSeries control stack interacts with a sharded <strong>Data Gateway Platform Control Plane</strong> that oversees control configurations for all abstractions and namespaces.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*aB6OKXoG-mT65Vh1" /></figure><p>Separating the responsibilities of the data plane and control plane helps maintain the high availability of our data plane, as the control plane takes on tasks that may require some form of schema consensus from the underlying data stores.</p><h3>Namespace Configuration</h3><p>The below configuration snippet demonstrates the immense flexibility of the service and how we can tune several things per namespace using our control plane.</p><pre>&quot;persistence_configuration&quot;: [<br>  {<br>    &quot;id&quot;: &quot;PRIMARY_STORAGE&quot;,<br>    &quot;physical_storage&quot;: {<br>      &quot;type&quot;: &quot;CASSANDRA&quot;,                  // type of primary storage<br>      &quot;cluster&quot;: &quot;cass_dgw_ts_tracing&quot;,     // physical cluster name<br>      &quot;dataset&quot;: &quot;tracing_default&quot;          // maps to the keyspace<br>    },<br>    &quot;config&quot;: {<br>      &quot;timePartition&quot;: {<br>        &quot;secondsPerTimeSlice&quot;: &quot;129600&quot;,    // width of a time slice<br>        &quot;secondPerTimeBucket&quot;: &quot;3600&quot;,      // width of a time bucket<br>        &quot;eventBuckets&quot;: 4                   // how many event buckets within<br>      },<br>      &quot;queueBuffering&quot;: {<br>        &quot;coalesce&quot;: &quot;1s&quot;,                   // how long to coalesce writes<br>        &quot;bufferCapacity&quot;: 4194304           // queue capacity in bytes<br>      },<br>      &quot;consistencyScope&quot;: &quot;LOCAL&quot;,          // single-region/multi-region<br>      &quot;consistencyTarget&quot;: &quot;EVENTUAL&quot;,      // read/write consistency<br>      &quot;acceptLimit&quot;: &quot;129600s&quot;              // how far back writes are allowed<br>    },<br>    &quot;lifecycleConfigs&quot;: {<br>      &quot;lifecycleConfig&quot;: [                  // Primary store data retention<br>        {<br>          &quot;type&quot;: &quot;retention&quot;,<br>          &quot;config&quot;: {<br>            &quot;close_after&quot;: &quot;1296000s&quot;,      // close for reads/writes<br>            &quot;delete_after&quot;: &quot;1382400s&quot;      // drop time slice<br>          }<br>        }<br>      ]<br>    }<br>  },<br>  {<br>    &quot;id&quot;: &quot;INDEX_STORAGE&quot;,<br>    &quot;physicalStorage&quot;: {<br>      &quot;type&quot;: &quot;ELASTICSEARCH&quot;,              // type of index storage<br>      &quot;cluster&quot;: &quot;es_dgw_ts_tracing&quot;,       // ES cluster name<br>      &quot;dataset&quot;: &quot;tracing_default_useast1&quot;  // base index name<br>    },<br>    &quot;config&quot;: {<br>      &quot;timePartition&quot;: {<br>        &quot;secondsPerSlice&quot;: &quot;129600&quot;         // width of the index slice<br>      },<br>      &quot;consistencyScope&quot;: &quot;LOCAL&quot;,<br>      &quot;consistencyTarget&quot;: &quot;EVENTUAL&quot;,      // how should we read/write data<br>      &quot;acceptLimit&quot;: &quot;129600s&quot;,             // how far back writes are allowed<br>      &quot;indexConfig&quot;: {<br>        &quot;fieldMapping&quot;: {                   // fields to extract to index<br>          &quot;tags.nf.app&quot;: &quot;KEYWORD&quot;,<br>          &quot;tags.duration&quot;: &quot;INTEGER&quot;,<br>          &quot;tags.enabled&quot;: &quot;BOOLEAN&quot;<br>        },<br>        &quot;refreshInterval&quot;: &quot;60s&quot;            // Index related settings<br>      }<br>    },<br>    &quot;lifecycleConfigs&quot;: {<br>      &quot;lifecycleConfig&quot;: [<br>        {<br>          &quot;type&quot;: &quot;retention&quot;,              // Index retention settings<br>          &quot;config&quot;: {<br>            &quot;close_after&quot;: &quot;1296000s&quot;,<br>            &quot;delete_after&quot;: &quot;1382400s&quot;<br>          }<br>        }<br>      ]<br>    }<br>  }<br>]</pre><h3>Provisioning Infrastructure</h3><p>With so many different parameters, we need automated provisioning workflows to deduce the best settings for a given workload. When users want to create their namespaces, they specify a list of <em>workload</em> <em>desires</em>, which the automation translates into concrete infrastructure and related control plane configuration. We highly encourage you to watch this <a href="https://www.youtube.com/watch?v=2aBVKXi8LKk">ApacheCon talk</a>, by one of our stunning colleagues <strong>Joey Lynch,</strong> on how we achieve this. We may go into detail on this subject in one of our future blog posts.</p><p>Once the system provisions the initial infrastructure, it then scales in response to the user workload. The next section describes how this is achieved.</p><h3>Scalability</h3><p>Our users may operate with limited information at the time of provisioning their namespaces, resulting in best-effort provisioning estimates. Further, evolving use-cases may introduce new throughput requirements over time. Here’s how we manage this:</p><ul><li><strong>Horizontal scaling</strong>: TimeSeries server instances can auto-scale up and down as per attached scaling policies to meet the traffic demand. The storage server capacity can be recomputed to accommodate changing requirements using our <a href="https://github.com/Netflix-Skunkworks/service-capacity-modeling/tree/main/service_capacity_modeling">capacity planner</a>.</li><li><strong>Vertical scaling</strong>: We may also choose to vertically scale our TimeSeries server instances or our storage instances to get greater CPU, RAM and/or attached storage capacity.</li><li><strong>Scaling disk</strong>: We may attach <a href="https://aws.amazon.com/ebs/">EBS</a> to store data if the capacity planner prefers infrastructure that offers larger storage at a lower cost rather than SSDs optimized for latency. In such cases, we deploy jobs to scale the EBS volume when the disk storage reaches a certain percentage threshold.</li><li><strong>Re-partitioning data</strong>: Inaccurate workload estimates can lead to over or under-partitioning of our datasets. TimeSeries control-plane can adjust the partitioning configuration for upcoming time slices, once we realize the nature of data in the wild (via partition histograms). In the future we plan to support re-partitioning of older data and dynamic partitioning of current data.</li></ul><h3>Design Principles</h3><p>So far, we have seen how TimeSeries stores, configures and interacts with event datasets. Let’s see how we apply different techniques to improve the performance of our operations and provide better guarantees.</p><h4>Event Idempotency</h4><p>We prefer to bake in idempotency in all mutation endpoints, so that users can retry or hedge their requests safely. <a href="https://research.google/pubs/the-tail-at-scale/">Hedging</a> is when the client sends an identical competing request to the server, if the original request does not come back with a response in an expected amount of time. The client then responds with whichever request completes first. This is done to keep the tail latencies for an application relatively low. This can only be done safely if the mutations are idempotent. For TimeSeries, the combination of <strong>event_time</strong>, <strong>event_id</strong> and <strong>event_item_key</strong> form the idempotency key for a given <strong>time_series_id</strong> event.</p><h4>SLO-based Hedging</h4><p>We assign Service Level Objectives (SLO) targets for different endpoints within TimeSeries, as an indication of what we think the performance of those endpoints should be <em>for a given namespace</em>. We can then hedge a request if the response does not come back in that configured amount of time.</p><pre>&quot;slos&quot;: {<br>  &quot;read&quot;: {               // SLOs per endpoint<br>    &quot;latency&quot;: {<br>      &quot;target&quot;: &quot;0.5s&quot;,   // hedge around this number<br>      &quot;max&quot;: &quot;1s&quot;         // time-out around this number<br>    }<br>  },<br>  &quot;write&quot;: {<br>    &quot;latency&quot;: {<br>      &quot;target&quot;: &quot;0.01s&quot;,<br>      &quot;max&quot;: &quot;0.05s&quot;<br>    }<br>  }<br>}</pre><h4>Partial Return</h4><p>Sometimes, a client may be sensitive to latency and willing to accept a partial result set. A real-world example of this is real-time frequency capping. Precision is not critical in this case, but if the response is delayed, it becomes practically useless to the upstream client. Therefore, the client prefers to work with whatever data has been collected so far rather than timing out while waiting for all the data. The TimeSeries client supports partial returns around SLOs for this purpose. Importantly, we still maintain the latest order of events in this partial fetch.</p><h4>Adaptive Pagination</h4><p>All reads start with a default fanout factor, scanning 8 partition buckets in parallel. However, if the service layer determines that the time_series dataset is dense — i.e., most reads are satisfied by reading the first few partition buckets — then it dynamically adjusts the fanout factor of future reads in order to reduce the read amplification on the underlying datastore. Conversely, if the dataset is sparse, we may want to increase this limit with a reasonable upper bound.</p><h4>Limited Write Window</h4><p>In most cases, the active range for writing data is smaller than the range for reading data — i.e., we want a range of time to become immutable as soon as possible so that we can apply optimizations on top of it. We control this by having a configurable “<strong>acceptLimit</strong>” parameter that prevents users from writing events older than this time limit. For example, an accept limit of 4 hours means that users cannot write events older than <em>now() — 4 hours</em>. We sometimes raise this limit for backfilling historical data, but it is tuned back down for regular write operations. Once a range of data becomes immutable, we can safely do things like caching, compressing, and compacting it for reads.</p><h4>Buffering Writes</h4><p>We frequently leverage this service for handling bursty workloads. Rather than overwhelming the underlying datastore with this load all at once, we aim to distribute it more evenly by allowing events to coalesce over short durations (typically seconds). These events accumulate in in-memory queues running on each instance. Dedicated consumers then steadily drain these queues, grouping the events by their partition key, and batching the writes to the underlying datastore.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*pMVe_h3daBDLWdis" /></figure><p>The queues are tailored to each datastore since their operational characteristics depend on the specific datastore being written to. For instance, the batch size for writing to Cassandra is significantly smaller than that for indexing into Elasticsearch, leading to different drain rates and batch sizes for the associated consumers.</p><p>While using in-memory queues does increase JVM garbage collection, we have experienced substantial improvements by transitioning to JDK 21 with ZGC. To illustrate the impact, ZGC has reduced our tail latencies by an impressive 86%:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*hj98LMk1UddaaDs-" /></figure><p>Because we use in-memory queues, we are prone to losing events in case of an instance crash. As such, these queues are only used for use cases that can tolerate some amount of data loss .e.g. tracing/logging. For use cases that need guaranteed durability and/or read-after-write consistency, these queues are effectively disabled and writes are flushed to the data store almost immediately.</p><h4>Dynamic Compaction</h4><p>Once a time slice exits the active write window, we can leverage the immutability of the data to optimize it for read performance. This process may involve re-compacting immutable data using optimal compaction strategies, dynamically shrinking and/or splitting shards to optimize system resources, and other similar techniques to ensure fast and reliable performance.</p><p>The following section provides a glimpse into the real-world performance of some of our TimeSeries datasets.</p><h3>Real-world Performance</h3><p>The service can write data in the order of low single digit milliseconds</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*VWrQj2ya5PQWusBq" /></figure><p>while consistently maintaining stable point-read latencies:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*23F_CzqsjMoI8GHB" /></figure><p>At the time of writing this blog, the service was processing close to <em>15 million events/second</em> across all the different datasets at peak globally.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1022/0*dZFDUVX35Cj1MPOj" /></figure><h3>Time Series Usage @ Netflix</h3><p>The TimeSeries Abstraction plays a vital role across key services at Netflix. Here are some impactful use cases:</p><ul><li><strong>Tracing and Insights: </strong>Logs traces across all apps and micro-services within Netflix, to understand service-to-service communication, aid in debugging of issues, and answer support requests.</li><li><strong>User Interaction Tracking</strong>: Tracks millions of user interactions — such as video playbacks, searches, and content engagement — providing insights that enhance Netflix’s recommendation algorithms in real-time and improve the overall user experience.</li><li><strong>Feature Rollout and Performance Analysis</strong>: Tracks the rollout and performance of new product features, enabling Netflix engineers to measure how users engage with features, which powers data-driven decisions about future improvements.</li><li><strong>Asset Impression Tracking and Optimization</strong>: Tracks asset impressions ensuring content and assets are delivered efficiently while providing real-time feedback for optimizations.</li><li><strong>Billing and Subscription Management:</strong> Stores historical data related to billing and subscription management, ensuring accuracy in transaction records and supporting customer service inquiries.</li></ul><p>and more…</p><h3>Future Enhancements</h3><p>As the use cases evolve, and the need to make the abstraction even more cost effective grows, we aim to make many improvements to the service in the upcoming months. Some of them are:</p><ul><li><strong>Tiered Storage for Cost Efficiency: </strong>Support moving older, lesser-accessed data into cheaper object storage that has higher time to first byte, potentially saving Netflix millions of dollars.</li><li><strong>Dynamic Event Bucketing: </strong>Support real-time partitioning of keys into optimally-sized partitions as events stream in, rather than having a <em>somewhat</em> static configuration at the time of provisioning a namespace. This strategy has a huge advantage of <em>not</em> partitioning time_series_ids that don’t need it, thus saving the overall cost of read amplification. Also, with Cassandra 4.x, we have noted major improvements in reading a subset of data in a wide partition that could lead us to be less aggressive with partitioning the entire dataset ahead of time.</li><li><strong>Caching: </strong>Take advantage of immutability of data and cache it intelligently for discrete time ranges.</li><li><strong>Count and other Aggregations: </strong>Some users are only interested in counting events in a given time interval rather than fetching all the event data for it.</li></ul><h3>Conclusion</h3><p>The TimeSeries Abstraction is a vital component of Netflix’s online data infrastructure, playing a crucial role in supporting both real-time and long-term decision-making. Whether it’s monitoring system performance during high-traffic events or optimizing user engagement through behavior analytics, TimeSeries Abstraction ensures that Netflix operates seamlessly and efficiently on a global scale.</p><p>As Netflix continues to innovate and expand into new verticals, the TimeSeries Abstraction will remain a cornerstone of our platform, helping us push the boundaries of what’s possible in streaming and beyond.</p><p>Stay tuned for Part 2, where we’ll introduce our <strong>Distributed Counter Abstraction</strong>, a key element of <strong>Netflix’s Composite Abstractions</strong>, built on top of the TimeSeries Abstraction.</p><h3>Acknowledgments</h3><p>Special thanks to our stunning colleagues who contributed to TimeSeries Abstraction’s success: <a href="https://www.linkedin.com/in/tomdevoe/">Tom DeVoe</a> <a href="https://www.linkedin.com/in/mengqingwang/">Mengqing Wang</a>, <a href="https://www.linkedin.com/in/kartik894/">Kartik Sathyanarayanan</a>, <a href="https://www.linkedin.com/in/jordan-west-8aa1731a3/">Jordan West</a>, <a href="https://www.linkedin.com/in/matt-lehman-39549719b/">Matt Lehman</a>, <a href="https://www.linkedin.com/in/cheng-wang-10323417/">Cheng Wang</a>, <a href="https://www.linkedin.com/in/clohfink/">Chris Lohfink</a> .</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=31552f6326f8" width="1" height="1" alt=""><hr><p><a href="https://netflixtechblog.com/introducing-netflix-timeseries-data-abstraction-layer-31552f6326f8">Introducing Netflix’s TimeSeries Data Abstraction Layer</a> was originally published in <a href="https://netflixtechblog.com">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>