A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.html 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang="fr">
  4. <!-- Has to be within the first 1024 bytes, hence before the `title` element
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset="utf-8">
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name="viewport" content="width=device-width,initial-scale=1">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>Surviving and thriving through the 2022-11-05 meltdown (archive) — David Larlet</title>
  13. <meta name="description" content="Publication mise en cache pour en conserver une trace.">
  14. <!-- That good ol' feed, subscribe :). -->
  15. <link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
  16. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  17. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
  18. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
  19. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
  20. <link rel="manifest" href="/static/david/icons2/site.webmanifest">
  21. <link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
  22. <link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
  23. <meta name="msapplication-TileColor" content="#f7f7f7">
  24. <meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
  25. <meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
  26. <meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
  27. <!-- Documented, feel free to shoot an email. -->
  28. <link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
  29. <!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
  30. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  31. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  32. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  33. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  34. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  35. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  36. <script>
  37. function toggleTheme(themeName) {
  38. document.documentElement.classList.toggle(
  39. 'forced-dark',
  40. themeName === 'dark'
  41. )
  42. document.documentElement.classList.toggle(
  43. 'forced-light',
  44. themeName === 'light'
  45. )
  46. }
  47. const selectedTheme = localStorage.getItem('theme')
  48. if (selectedTheme !== 'undefined') {
  49. toggleTheme(selectedTheme)
  50. }
  51. </script>
  52. <meta name="robots" content="noindex, nofollow">
  53. <meta content="origin-when-cross-origin" name="referrer">
  54. <!-- Canonical URL for SEO purposes -->
  55. <link rel="canonical" href="https://blog.freeradical.zone/post/surviving-thriving-through-2022-11-05-meltdown/">
  56. <body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">
  57. <article>
  58. <header>
  59. <h1>Surviving and thriving through the 2022-11-05 meltdown</h1>
  60. </header>
  61. <nav>
  62. <p class="center">
  63. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  64. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  65. </svg> Accueil</a> •
  66. <a href="https://blog.freeradical.zone/post/surviving-thriving-through-2022-11-05-meltdown/" title="Lien vers le contenu original">Source originale</a>
  67. </p>
  68. </nav>
  69. <hr>
  70. <h2 id="background">Background</h2>
  71. <p>After Elon Musk bought Twitter and started making a bizarre series of decisions about how to run it, people started logging into Mastodon to see what it’s all about. Lots of them. So, so many of them. In real numbers, Free Radical grew by <em>20% in the last week</em>. Which is awesome, because it’s wonderful to see new faces excited and eager to join the fun. The downside is that new users, being new to it, tend to be understandably excited and exploratory, with lots of posting, following other new people, and doing the kinds of things that require server hardware to wake up and earn its living. I don’t have hard stats to back it up, but from eyeballing the logs, I estimate that the server load was about 4 times greater than it was 2 weeks ago.</p>
  72. <h2 id="rumblings">Rumblings</h2>
  73. <p>I woke up yesterday, wondered what was happening online, and saw a few messages asking why people weren’t seeing the things they expected to see, like new toots their friends had made on other servers, notifications on their mobile apps, and the like. Huh. That’s interesting, and a little alarming. The site indeed “felt” slow. Then I noticed a stream of other admins asking questions like “hey, is anyone else seeing their server catching on fire?”</p>
  74. <p>Uh-oh.</p>
  75. <h2 id="time-travel">Time travel</h2>
  76. <p>Mastodon runs a lot of little work tasks in the background, such as “add user A’s new toot to the database”, or “notify server B that user C replied to someone there”, or “let user D’s phone know that there’s a new notification for them”. As these tasks come in, they’re added to a “queue” of work to be done, and a program comes along to act on each of those tasks. Ideally, and in normal operation, tasks are completed as quickly as they’re being added to the queue, and the site <em>feels</em> like it’s operating as soon as a user asks it to do something. I don’t know if there’s another word for it, but I describe it as “realtime”. When I checked the queue yesterday, it was running about 45 minutes behind realtime. Every action a user took had to wait for nearly an hour before its effects were visible. That’s not good.</p>
  77. <h2 id="old-architecture">Old architecture</h2>
  78. <p>Let’s take a moment to talk about how Free Radical was set up. A Mastodon server has a few components:</p>
  79. <ul>
  80. <li>A PostgreSQL database server.</li>
  81. <li>A Redis caching server that holds a lot of working information in fast RAM.</li>
  82. <li>A “streaming” service (running in Node.js) that serves long-running HTTP and WebSocket connections to clients.</li>
  83. <li>The Mastodon website, a Ruby on Rails app, for people using the service in their web browser.</li>
  84. <li>“Sidekiq”, another Ruby on Rails service that processes the background housekeeping tasks we talked about earlier.</li>
  85. <li>Amazon’s S3 storage service that handles images, videos, and all those other shiny things.</li>
  86. </ul>
  87. <p>When I first launched the service in 2017, all of these services ran on the same DigitalOcean server with 4GB of RAM. I ran out of disk space pretty quickly because all of those delightful cat pictures people post take up a lot of hard drive, so I offloaded that to S3. The PostgreSQL database also grew rapidly, and I relocated that to a server running in my own house. (That has nice privacy implications, too. US courts have ruled that it requires more effort for law enforcement to subpoena data stored in your residence than in a cloud server.) We ran that way with minor occasional adjustments for a couple of years:</p>
  88. <ul>
  89. <li>PostgreSQL is hosted in my house.</li>
  90. <li>Media is in S3.</li>
  91. <li>Everything else ran in the 4GB cloud server.</li>
  92. </ul>
  93. <h2 id="more-about-sidekiq">More about Sidekiq</h2>
  94. <p>When the queue is lagging behind realtime, whatever the root cause, the result is that Sidekiq isn’t working fast enough. The default Mastodon settings tell Sidekiq to use 5 worker threads, meaning that it can process 5 queued tasks at the same time. I turned that knob as Free Radical grew over the years, and had settled on having 25 worker threads. That is, it could handle queued tasks about 5 times as quickly as an untuned Mastodon instance. That worked well for years. Sometimes the instance would get flooded with a short burst of traffic, but those busy little workers would chew their way through the queue and most users would probably never notice that it was temporarily slow.</p>
  95. <h2 id="ill-melt-with-you">I’ll melt with you</h2>
  96. <p>I noticed something worrisome when I looked at the 45-minute old tasks: many of them were second (or third or fourth) attempts to interact with other servers. That’s unusual in normal operation. Sure, there are often a couple of servers temporarily down for service, but it’s uncommon to see many of them at once. And wow, there sure were many of them yesterday.</p>
  97. <p>I have an unproven hypothesis. Suppose that Free Radical had several worker threads trying to contact instance Foo. Foo was running slowly, so those connections eventually timed out after many seconds, and Free Radical added a retry task to the end of the queue. However, while it was waiting for those connections to give up, it was responding to other servers slowly. Somewhere out there, server Bar was trying to deliver messages to Free Radical, and those connections were timing out because Free Radical was stuck waiting for Foo. That made Bar run slowly. Meanwhile, Foo is trying to contact Bar, but can’t because Bar is so loaded up. In other words, lots of servers were running slowly because they were waiting on all their neighbors to start running quickly.</p>
  98. <p>Again, I can’t prove this. It would explain the traffic and queue patterns we saw yesterday, though, and I’d bet that a variation of this was happening.</p>
  99. <h2 id="back-to-sidekiq">Back to Sidekiq</h2>
  100. <p>The Sidekiq server is written in Ruby on Rails. That means that there are lots of people who understand it and can contribute to developing and improving it. That’s good. It also means that it’s kind of a slow-running resource hog. That’s not good. Other server software is written in languages much better suited for running many background processes at once. For example, Pleroma is written in Elixir, and Elixir is all like “oh, you want me to do 473,000 things at once? OK!”</p>
  101. <p>Ruby on Rails isn’t Elixir. It’s not easy to just turn up the number of worker threads and go back to eating breakfast. That didn’t stop me from trying. And in any case, I had to run more threads <em>somehow</em> if we ever wanted to get back to realtime. These things happened quickly:</p>
  102. <p>I increased the number of worker threads.</p>
  103. <p>Since each one of them insists on connecting to the database at all times, the PgBouncer connection pooler ran out of available connections.</p>
  104. <p>I increased the number of PgBouncer’s allowed connections.</p>
  105. <p>Now we had lots of running threads, but the server was almost out of RAM.</p>
  106. <p>We needed to get rid of <em>something</em>.</p>
  107. <h2 id="moving-redis">Moving Redis</h2>
  108. <p>Remember that bit about Redis caching things in RAM? That’s good under normal circumstances, but now Redis and Sidekiq were fighting over RAM. And that database server was just sitting there like a slacker running PostgreSQL and sipping espresso like a smug hipster. I launched a Redis service on that hardware, configured an encrypted tunnel for it, and told Sidekiq to use the new Redis server. Then I crossed my fingers, restarted Sidekiq… and it worked! The extra RAM let the worker threads start zooming along.</p>
  109. <p>However, suddenly all of my Mastodon timelines were empty. Oh, rats. Turns out they’re all cached in Redis, and when I switched Sidekiq to the new server, it lost track of the old cached data. Mastodon conveniently has a command (<code>tootctl feeds build</code>) to recreate all that data. I ran that command, it started working, and then the queue started filling up again faster than the workers could clear it. That’s the opposite of what I was working for.</p>
  110. <h2 id="raspberry-jammin">Raspberry jammin'</h2>
  111. <p>Out of the corner of my eye, I saw my little lonely Raspberry Pi 4, sad because it was waiting to be picked last at recess. Hey there, little buddy! Are you up to running Sidekiq? Yes, yes it was. Now, building the Sidekiq Docker image wasn’t a quick process. Docker running on a Raspberry Pi, using NFS for storage because the RPi’s own SD card is too slow and fragile, is about as sluggish as you might think. But it worked! And once the service launched, it wouldn’t be using much drive I/O anyway, and the RPi’s CPU is surprisingly capable.</p>
  112. <p>I configured Sidekiq to point at the existing database server and the new Redis service, fired it up, refreshed the Sidekiq web UI, and saw that a huge new flood of fast workers was online and tearing through tasks like my dog goes through dropped potato chips. That… worked?! Yeah, it worked!</p>
  113. <p>A short while later, we were back to realtime. I ran the timeline rebuilding command again, and the worker threads temporarily got as far back as 5 minutes behind realtime, but then caught back up and stayed there. We were back in business.</p>
  114. <h2 id="where-we-are-now">Where we are now</h2>
  115. <p>I feel like Free Radical turned a corner in this exercise. Until yesterday, aside from the database server, Free Radical was tightly bound to a single cloud server. Now we have:</p>
  116. <ul>
  117. <li>PostgreSQL and Redis running on a large, fast server.</li>
  118. <li>Media in S3.</li>
  119. <li>The web and streaming services, and 1 Sidekiq service, running on a 4GB cloud server.</li>
  120. <li>Another Sidekiq service sharing the work equally from a separate hunk of hardware.</li>
  121. </ul>
  122. <p>I could move back to the old architecture now that the short-term burst of traffic is likely over, but why? Free Radical is in a great place, where the most resource-intensive part of the system can be horizontally scaled to a cluster of additional servers on a moment’s notice without reconfiguring or restarting anything. Even without that, we now have 160 workers instead of the previous 25.</p>
  123. <p>The process was a little hectic, but I sure like where we ended up.</p>
  124. <h2 id="technical-details">Technical details</h2>
  125. <p>The above went a <em>long</em> way toward bringing Free Radical back. A couple of days later I noticed that the queue was still filling up faster than expected sometimes, and that the worker process on each server was running at 100%. After research it seemed that it’s much better to run multiple Sidekiq processes, each with fewer worker threads, to take advantage of multi-CPU servers. Here’s how I enabled that.</p>
  126. <p>First, I created up a much simpler <code>config/sidekiq-helper.yml</code> file:</p>
  127. <p>This is similar to the full <code>config/sidekiq.yml</code>, but without the <code>scheduler</code> queue (because you should only have <a href="https://docs.joinmastodon.org/admin/scaling/" target="_blank">one scheduler queue worker running, ever</a>) or any of its related scheduled jobs.</p>
  128. <p>Next, I updated my <code>docker-compose.yml</code> on the main server to have multiple <code>sidekiq</code> blocks, like:</p>
  129. <p>See how <code>sidekiq_2</code>’s <code>command</code> line specifies the new <code>sidekiq-helper.yml</code> file mentioned above? With this setup, <code>sidekiq_1</code> runs <em>all</em> of the queues, including <code>scheduler</code>. <code>sidekiq_2</code> runs all of them <em>except</em> <code>scheduler</code>.</p>
  130. <p>On the Raspberry Pi, I created <code>sidekiq_1</code> through <code>sidekiq_4</code>, each using the <code>sidekiq-helper.yml</code> config so that <em>none</em> of them were running the <code>scheduler</code> queue.</p>
  131. <p>Also note the <code>cpu_shares</code> settings? Docker compose uses that to adjust <a href="https://docs.docker.com/compose/compose-file/#cpu_shares" target="_blank">each container’s CPU usage</a> compared to the other containers. The default value is <code>1024</code>, so this runs the worker processes at a lower CPU priority than the <code>web</code> and <code>streaming</code> containers, which helps keep the web interface and mobile apps nicely responsive.</p>
  132. <p>Finally, I ran <code>docker compose build</code> to create new Docker images incorporating the <code>config/sidekiq-helper.yml</code> files and restarted the services on each server.</p>
  133. <p>After I made these changes, the worker threads are no longer CPU bound and are completing queued tasks faster than ever.</p>
  134. </article>
  135. <hr>
  136. <footer>
  137. <p>
  138. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  139. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  140. </svg> Accueil</a> •
  141. <a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
  142. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
  143. </svg> Suivre</a> •
  144. <a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
  145. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
  146. </svg> Pro</a> •
  147. <a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
  148. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
  149. </svg> Email</a> •
  150. <abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
  151. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
  152. </svg> Légal</abbr>
  153. </p>
  154. <template id="theme-selector">
  155. <form>
  156. <fieldset>
  157. <legend><svg class="icon icon-brightness-contrast">
  158. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
  159. </svg> Thème</legend>
  160. <label>
  161. <input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
  162. </label>
  163. <label>
  164. <input type="radio" value="dark" name="chosen-color-scheme"> Foncé
  165. </label>
  166. <label>
  167. <input type="radio" value="light" name="chosen-color-scheme"> Clair
  168. </label>
  169. </fieldset>
  170. </form>
  171. </template>
  172. </footer>
  173. <script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
  174. <script>
  175. function loadThemeForm(templateName) {
  176. const themeSelectorTemplate = document.querySelector(templateName)
  177. const form = themeSelectorTemplate.content.firstElementChild
  178. themeSelectorTemplate.replaceWith(form)
  179. form.addEventListener('change', (e) => {
  180. const chosenColorScheme = e.target.value
  181. localStorage.setItem('theme', chosenColorScheme)
  182. toggleTheme(chosenColorScheme)
  183. })
  184. const selectedTheme = localStorage.getItem('theme')
  185. if (selectedTheme && selectedTheme !== 'undefined') {
  186. form.querySelector(`[value="${selectedTheme}"]`).checked = true
  187. }
  188. }
  189. const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
  190. window.addEventListener('load', () => {
  191. let hasDarkRules = false
  192. for (const styleSheet of Array.from(document.styleSheets)) {
  193. let mediaRules = []
  194. for (const cssRule of styleSheet.cssRules) {
  195. if (cssRule.type !== CSSRule.MEDIA_RULE) {
  196. continue
  197. }
  198. // WARNING: Safari does not have/supports `conditionText`.
  199. if (cssRule.conditionText) {
  200. if (cssRule.conditionText !== prefersColorSchemeDark) {
  201. continue
  202. }
  203. } else {
  204. if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
  205. continue
  206. }
  207. }
  208. mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
  209. }
  210. // WARNING: do not try to insert a Rule to a styleSheet you are
  211. // currently iterating on, otherwise the browser will be stuck
  212. // in a infinite loop…
  213. for (const mediaRule of mediaRules) {
  214. styleSheet.insertRule(mediaRule.cssText)
  215. hasDarkRules = true
  216. }
  217. }
  218. if (hasDarkRules) {
  219. loadThemeForm('#theme-selector')
  220. }
  221. })
  222. </script>
  223. </body>
  224. </html>