{"id":22285,"date":"2024-07-15T08:59:25","date_gmt":"2024-07-15T15:59:25","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/microsoft365dev\/?p=22285"},"modified":"2024-07-15T08:59:25","modified_gmt":"2024-07-15T15:59:25","slug":"azure-fluid-relay-leveraging-azure-blob-storage-to-scale-git","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/microsoft365dev\/azure-fluid-relay-leveraging-azure-blob-storage-to-scale-git\/","title":{"rendered":"Azure Fluid Relay: Leveraging Azure Blob Storage to scale Git"},"content":{"rendered":"<p>In this post, we will learn how to leverage Git as a storage mechanism behind the globally available Azure Fluid Relay (AFR) service. AFR is an Azure-hosted implementation of the open-source <a href=\"https:\/\/fluidframework.com\/\">Fluid Framework<\/a> reference server, <a href=\"https:\/\/github.com\/microsoft\/FluidFramework\/tree\/main\/server\/routerlicious#routerlicious\">Routerlicious<\/a>. This service is designed to facilitate real-time collaboration by ordering and broadcasting <a href=\"https:\/\/fluidframework.com\/docs\/concepts\/tob\/#fluid-data-operations-all-the-way-down\">Fluid operations<\/a> across connected clients.<\/p>\n<h2>Summaries and Git<\/h2>\n<p>To understand how we leverage Git, it&#8217;s essential to grasp the concept of <a href=\"https:\/\/fluidframework.com\/docs\/concepts\/summarizer\/\">summaries<\/a>. Summaries are snapshots of the collaborative state at a given point. They allow us to efficiently manage and retrieve the state without replaying the entire history of operations. Conveniently, summaries are built and stored as blob trees, which are very similar to the data structures behind Git.<\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171817.png\"><img decoding=\"async\" class=\"aligncenter wp-image-22299 size-large\" src=\"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171817-1024x552.png\" alt=\"Image Screenshot 2024 07 15 171817\" width=\"640\" height=\"345\" srcset=\"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171817-1024x552.png 1024w, https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171817-300x162.png 300w, https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171817-768x414.png 768w, https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171817.png 1144w\" sizes=\"(max-width: 640px) 100vw, 640px\" \/><\/a><\/p>\n<p>By using Git to store Fluid summaries, we get several benefits:<\/p>\n<ol>\n<li>1:1 mapping of Summary Tree and Blob nodes to Git Tree and Blob objects.<\/li>\n<li>Immutability across summary versions.<\/li>\n<li>Efficient storage use by reusing blobs that do not change across versions.<\/li>\n<\/ol>\n<p>To dive deeper into how we use Git for storing summary trees, look at the open source <a href=\"https:\/\/github.com\/microsoft\/FluidFramework\/tree\/main\/server\/gitrest\">Gitrest service implementation<\/a>.<\/p>\n<h3>Challenge 1: Scalability and reliability<\/h3>\n<p>While Git is excellent for version control, using it for long-term data storage at scale presents a major challenge: a high volume of data that needs to be managed reliably. As the number of collaborative sessions grows, so does the number of Git repositories and summary versions, leading to performance bottlenecks and rapidly increasing storage costs, particularly when using a local filesystem.<\/p>\n<p>In the early days of AFR, Gitrest was deployed to a Kubernetes cluster along with 2 other key components: the <a href=\"https:\/\/github.com\/microsoft\/FluidFramework\/blob\/main\/server\/gitssh\/Dockerfile\">GitSSH microservice<\/a> and a <a href=\"https:\/\/kubernetes.io\/docs\/concepts\/storage\/persistent-volumes\/\">Persistent Volume Claim<\/a> (PVC). We then used the built-in Node.js filesystem module along with the <a href=\"https:\/\/www.nodegit.org\/\">nodegit<\/a> package to store summaries as Git repositories in the PVC via local SSH through the GitSSH microservice.<\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171838.png\"><img decoding=\"async\" class=\"aligncenter size-full wp-image-22300\" src=\"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171838.png\" alt=\"Image Screenshot 2024 07 15 171838\" width=\"1499\" height=\"365\" srcset=\"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171838.png 1499w, https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171838-300x73.png 300w, https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171838-1024x249.png 1024w, https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171838-768x187.png 768w\" sizes=\"(max-width: 1499px) 100vw, 1499px\" \/><\/a><\/p>\n<p>As you can imagine, this didn&#8217;t scale well for a few reasons:<\/p>\n<ol>\n<li>Access to a particular container&#8217;s summaries was bound to 1 Kubernetes cluster forever. This is a complete non-starter for global scalability and reliability, especially regarding disaster recovery.<\/li>\n<li><a href=\"https:\/\/kubernetes.io\/docs\/concepts\/storage\/persistent-volumes\/#expanding-persistent-volumes-claims\">Increasing the storage available in a Persistent Volume Claim<\/a> can cause availability outages and scaling them out horizontally introduces the additional challenge of mapping containers to volumes.<\/li>\n<li>Gitrest pods couldn&#8217;t be reliably scaled out to handle increased traffic when they all had to be mounted to 1 PVC.<\/li>\n<\/ol>\n<h3>Solution: Scaling Git with Azure Blob Storage as a Filesystem<\/h3>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171859.png\"><img decoding=\"async\" class=\"aligncenter size-full wp-image-22301\" src=\"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171859.png\" alt=\"3\" width=\"1447\" height=\"425\" srcset=\"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171859.png 1447w, https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171859-300x88.png 300w, https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171859-1024x301.png 1024w, https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171859-768x226.png 768w\" sizes=\"(max-width: 1447px) 100vw, 1447px\" \/><\/a><\/p>\n<p>To address these challenges, we leverage <a href=\"https:\/\/azure.microsoft.com\/en-us\/products\/storage\/blobs\">Azure Blob Storage<\/a> to scale Git summary storage globally by plugging a custom filesystem implementation using Azure Blob Storage APIs into the <a href=\"https:\/\/isomorphic-git.org\/\">isomorphic-git<\/a> package. This approach gives us the best of both worlds: the version control and immutability of Git along with the availability, scalability, and reliability of Azure Blob Storage.<\/p>\n<h3>Challenge 2: Performance and cost<\/h3>\n<p>Before we began migrating to Azure Blob Storage as a remote filesystem for Git summary storage, we ran some initial tests by uploading our codebase&#8217;s Git repository to Azure Blob. Immediately, we knew we had problems: network overhead and traffic volume.<\/p>\n<h3>Challenge 2.1: Network overhead<\/h3>\n<p>Uploading 1 large file to Azure Blob is <em>very fast<\/em> (milliseconds) but uploading hundreds or even thousands of smaller files sequentially is <em>very slow<\/em> (seconds). In practice, this means that summary read\/write performance degrades proportionally to<\/p>\n<p>a) the height and width of the tree, and<\/p>\n<p>b) the physical distance between the Kubernetes cluster and the Azure Blob Storage datacenter<\/p>\n<p>To understand why network overhead is such a big deal when using a remote filesystem in this way, you must understand a little about how isomorphic-git works out-of-the-box. A Git tree must be written bottom up, as files are named as a SHA value of their contents. Specifically, a tree referencing a tree and a blob must be written <em>after<\/em> the child tree and blob have their SHA values computed. To compute those, we write them to storage using the isomorphic-git writeTree and writeBlob APIs, which incidentally involves writing the files to Azure Blob. From here, you can see how a tall tree can involve many sequential layers of writing to storage, thus amplifying the impact of network overhead and latency.<\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171915.png\"><img decoding=\"async\" class=\"aligncenter size-full wp-image-22302\" src=\"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171915.png\" alt=\"4\" width=\"1499\" height=\"925\" srcset=\"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171915.png 1499w, https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171915-300x185.png 300w, https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171915-1024x632.png 1024w, https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171915-768x474.png 768w\" sizes=\"(max-width: 1499px) 100vw, 1499px\" \/><\/a><\/p>\n<p>For example, if network latency is 10ms for a file, then storing a tree of height 10 takes 100ms; however, if latency is 70ms, writing the same tree would take 700ms.<\/p>\n<p>Note: it takes ~70ms for a network packet to cross the United States from coast to coast.<\/p>\n<p>The obvious solution here is to perform all SHA hash computation locally, then write all files to storage in parallel. Then, the network latency would no longer stack with itself. Unfortunately, network overhead was not the only problem here.<\/p>\n<h3>Challenge 2.2: Traffic volume<\/h3>\n<p>Azure Fluid Relay serves millions of collaborative sessions per day, which means a lot of new summaries. Even at the highest performance, premium dedicated tier of Azure Blob Storage, we were quickly running out of storage access allowance across our many Azure Storage Accounts. That meant we were sending so many requests that we could no longer, even temporarily, mitigate this scalability bottleneck with additional provisioning (i.e. money). Thankfully, we knew this day would come and had already been experimenting with workarounds. Enter: <strong>Low-IO Write<\/strong><\/p>\n<h3>Solution: Low-IO write mode<\/h3>\n<p>To address the issue of exceeding storage access limits in Azure Blob Storage, the current workaround involves computing the entire Git summary in memory first. This is achieved by using the isomorphic-git package in conjunction with <a href=\"https:\/\/www.npmjs.com\/package\/memfs\">memfs<\/a>, a memory-based file system, to create a single JSON filesystem payload for each summary, which is then stored with a commit and tree pointing to it. By handling the Git filesystem computation of the summary tree in memory, we minimize the network overhead and traffic volume that typically comes with writing multiple blobs to storage.<\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171954.png\"><img decoding=\"async\" class=\"size-full wp-image-22304 alignnone\" src=\"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171954.png\" alt=\"5\" width=\"1202\" height=\"1346\" srcset=\"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171954.png 1202w, https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171954-268x300.png 268w, https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171954-914x1024.png 914w, https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-content\/uploads\/sites\/73\/2024\/07\/Screenshot-2024-07-15-171954-768x860.png 768w\" sizes=\"(max-width: 1202px) 100vw, 1202px\" \/><\/a><\/p>\n<p>As a result, the number of blobs written to Azure Blob Storage for each summary is significantly reduced, down to just four key components: the ref, commit, tree, and single memfs blob. This streamlined approach leads to fewer storage access requests and helps to alleviate the scalability bottleneck associated with high traffic volume.<\/p>\n<h3>Trade-offs<\/h3>\n<p>However, this method introduces a couple substantial trade-offs: decreased storage efficiency and increased memory usage. Since each summary is freshly computed and stored as new blobs, it prevents the reuse of unchanged blobs from previous summaries. This leads to an increase in cost for the additional required storage capacity. Also, because we must build a fully self-contained Git file system every time, we must load the previous summary into memory to allow the new Git tree to reference previously written, unchanged objects. This leads to an increase in the cost for memory capacity within the Gitrest service.<\/p>\n<p>Despite this downside, the overall storage costs are still reduced. The savings stem from the decreased volume of access requests, which vastly outweighs the minor increase in capacity. This balance between increased storage capacity costs and reduced access volume ultimately results in a net reduction in total storage expenses for the Azure Fluid Relay service, not to mention the additional room for scalability.<\/p>\n<h2>Future work<\/h2>\n<p>Going forward, we hope to find a feasible solution to performance and cost challenge that does not have as many major trade-offs as the Low-IO write solution. However, in the meantime we are continuing to leverage our current solution\u2019s extensibility to power other needs, such as using Redis as a remote filesystem to store ephemeral containers.<\/p>\n<p>Fluid Framework 2 is <a href=\"https:\/\/aka.ms\/fluid\/release_blog\">production-ready now<\/a>! We\u2019re excited to see all the collaborative experiences that you\u2019ll build with it.<\/p>\n<p>Visit the following resources to learn more:<\/p>\n<ul>\n<li><a href=\"https:\/\/aka.ms\/fluid\/start\">Get Started<\/a>\u00a0with Fluid Framework 2<\/li>\n<li><a href=\"https:\/\/aka.ms\/fluid\/samples\">Sample app code<\/a>\u00a0using SharedTree DDS and SharePoint Embedded<\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/azure-fluid-relay\/how-tos\/connect-fluid-azure-service\">Connect your app to Azure Fluid Relay<\/a> for production workflows<\/li>\n<li>For questions or feedback,\u00a0<a href=\"https:\/\/aka.ms\/fluid\/discuss\">reach out via GitHub<\/a><\/li>\n<li><a href=\"https:\/\/aka.ms\/fluid\/connect\">Connect directly with the Fluid team<\/a>, we would love to hear what you are building!<\/li>\n<li>Follow\u00a0<a href=\"http:\/\/twitter.com\/fluidframework\">@FluidFramework on X (Twitter)<\/a>\u00a0to stay updated<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Learn how to leverage Git as a storage mechanism behind the globally available Azure Fluid Relay (AFR) service.<\/p>\n","protected":false},"author":165615,"featured_media":22293,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[61,345,169],"class_list":["post-22285","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-microsoft-365-developer","tag-azure","tag-azure-blob-storage","tag-azure-fluid-relay"],"acf":[],"blog_post_summary":"<p>Learn how to leverage Git as a storage mechanism behind the globally available Azure Fluid Relay (AFR) service.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-json\/wp\/v2\/posts\/22285","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-json\/wp\/v2\/users\/165615"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-json\/wp\/v2\/comments?post=22285"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-json\/wp\/v2\/posts\/22285\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-json\/wp\/v2\/media\/22293"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-json\/wp\/v2\/media?parent=22285"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-json\/wp\/v2\/categories?post=22285"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/microsoft365dev\/wp-json\/wp\/v2\/tags?post=22285"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}