Categories
程式開發

數據庫連接從15000降到100以下:DigitalOcean如何解決技術債


本文最初發佈於DigitalOcean官方博客,經原作者授權由InfoQ中文站翻譯並分享。

最近,一位新員工在午餐時問我:“DigitalOcean的技術債務是什麼情況?”

聽到這個問題時,我忍不住笑了。軟件工程師詢問一家公司的技術債務就相當於詢問其信用評分。這是他們衡量一家公司過去的遺留問題以及他們所要背負的包袱的方法。 DigitalOcean對技術包袱並不陌生。

作為雲提供商,我們管理著自己的服務器和硬件,我們面臨著許多其他初創公司在這個雲計算的新時代尚未遇到的複雜性。這些艱難的情況最終迫使我們不得不早做權衡。任何快速成長的公司都知道,你早期做出的技術決定往往會在以後影響到你。

盯著桌子對面的新員工,我深吸了一口氣,然後開始說。 “讓我告訴你,我們有15000個與數據庫的直接連接……”

數據庫連接從15000降到100以下:DigitalOcean如何解決技術債 1

我給這位新員工講的故事是DigitalOcean迄今為止最大的技術重組。這是整個公司多年來的努力,教會了我們很多東西。我希望,講述這個故事可以幫助未來的DigitalOcean開發者——或任何發現自己陷入棘手的技術債務問題的開發者。

故事緣起

DigitalOcean從一開始就痴迷於簡單性。這是我們的核心價值觀之一:力求簡單而優雅的解決方案。這不僅適用於我們的產品,也適用於我們的技術決策。這一點在我們最初的系統設計中最為明顯。

與GitHub、Shopify和Airbnb一樣,DigitalOcean也是在2011年從一個Rails應用程序開始的。我們內部稱為Cloud的Rails應用程序管理著UI和公共API中的所有用戶交互。 Rails服務有兩個Perl編寫的輔助服務:調度器和DOBE(DigitalOcean的後端)。調度器將調度並分配Droplet給管理程序,而DOBE負責創建實際的Droplet虛擬機。 Cloud和調度器作為獨立的服務運行,而DOBE則運行在fleet中的每台服務器上。

無論是Cloud、調度器,還是DOBE,彼此之間都不能直接對話。他們通過MySQL數據庫通信。這個數據庫有兩個作用:存儲數據和代理通信。這三個服務都使用單個數據庫表作為消息隊列來傳遞信息。

每當用戶新建一個Droplet時,Cloud就會向隊列插入一個新的事件記錄。調度器每秒一次不斷地在數據庫中輪詢新的Droplet事件,並將它們的創建工作安排給一個可用的管理程序。最後,每個DOBE實例將等待新調度的Droplet創建並完成任務。為了能檢測到所有新的更改,這些服務器都需要在數據庫中輪詢表中的新記錄。

數據庫連接從15000降到100以下:DigitalOcean如何解決技術債 2

雖然在系統設計方面,無限循環和讓每個服務器直接連接到數據庫可能比較低級,但它很簡單,而且很有效——特別是在人手短缺的技術團隊面臨緊迫的期限和快速增長的用戶群時。

四年來,數據庫消息隊列一直是DigitalOcean技術的支柱。在此期間,我們採用了微服務體系結構,用gRPC代替HTTPS進行內部通信,並取消了Perl,代之以Golang作為後端服務。然而,所有的方法都指向MySQL數據庫。

值得注意的是,不能僅僅因為某些東西是“遺產”就認為它功能不完善,應該被取代。 Bloomberg和IBM都有用Fortran和COBOL編寫的遺留服務,這些服務產生的收入超過整個公司。另一方面,每個系統都有一個擴展限制。我們就要達到這個限制了。

從2012年到2016年,DigitalOcean的用戶流量增長超過了1000%。我們在目錄中添加了更多的產品,在基礎設施中添加了更多的服務。這增加了數據庫消息隊列上進入的事件。對Droplet需求增加意味著調度器要加班加點地把它們分配到服務器上。不幸的是,對於調度器,可用服務器的數量不是靜態的。

數據庫連接從15000降到100以下:DigitalOcean如何解決技術債 3

為了滿足Droplet需求的不斷增長,我們不斷地增加服務器來處理流量。每個新的管理程序都意味著一個新的數據庫持久連接。截至2016年初,該數據庫擁有超過1.5萬個直接連接,每一個連接每1到5秒就查詢一次新事件。如果這還不夠糟糕的話,每個管理程序用來獲取新Droplet事件的SQL查詢也變得越來越複雜。它已經變成了一個巨人,有150多行,18張表關聯在一起。令人印象深刻的是,它不穩定,而且很難維持。

不出所料,正是在這個時期,問題開始顯現。單點故障和成千上萬的依賴關係攫取了共享資源,不可避免地導致了一段時間的混亂。表鎖和查詢積壓會導致停機和性能下降。

由於系統的緊耦合,我們沒能找到一個清晰簡單的解決方案來解決這個問題。 Cloud、調度器和DOBE都是瓶頸。如果只修補其中一兩個組件,就會將負載轉移到其餘的瓶頸組件上。因此,經過深思熟慮,工程師們想出了一個三管齊下的方案來解決這個問題:

  1. 減少數據庫上直接連接的數量;
  2. 重構調度器的排名算法,改進可用性;
  3. 移除數據庫的消息隊列職責。

開始重構

為了解決數據庫依賴,DigitalOcean的工程師創建了事件路由器。事件路由器充當區域代理,代表每個數據中心中的每個DOBE實例輪詢數據庫。這樣,就只有少數代理在做查詢,而不是數以千計的服務器。每個事件路由器代理將獲取特定區域中的所有活動事件,並將每個事件委派給適當的管理程序。事件路由器還將龐大的輪詢查詢拆分成更小、更容易維護的輪詢查詢。

數據庫連接從15000降到100以下:DigitalOcean如何解決技術債 4

當事件路由器上線後,數據庫連接的數量從15000個減少到不足100個。

接下來,工程師們把目光投向了下一個目標:調度器。如前所述,調度器是一個Perl腳本,它決定哪個管理程序將託管一個創建好的Droplet。它使用一系列查詢來對服務器進行排序。每當用戶創建一個Droplet時,調度器就用最好的機器更新表行。

雖然聽起來很簡單,但該調度器有一些缺陷。它的邏輯很複雜,很難處理。它是單線程的,在流量高峰期間性能會受影響。最後,該調度器只有一個實例,卻必須為整個fleet服務。這是一個不可避免的瓶頸。為了解決這些問題,工程團隊創建了調度器V2。

更新後的調度器徹底修改了排名系統。它不從數據庫中查詢服務器指標,而是從管理程序中聚合它們,並將它們存儲在自己的數據庫中。此外,調度器團隊使用並發和復制保證新服務的負載性能。

事件路由器和調度器V2取得了許多了不起的成果,消除了當時的許多架構缺陷。儘管如此,還是有一個明顯的障礙。到2017年初,集中式MySQL消息隊列仍然在使用——甚至還很頻繁。它每天處理40萬條新記錄,每秒更新20次。

遺憾的是,移除數據庫消息隊列並不容易。第一步是避免服務直接訪問它。數據庫需要一個抽象層。它還需要一個API來聚合請求並代它執行查詢。任何服務想要創建一個新事件,都需要通過API來實現。於是,Harpoon誕生了。

不過,為事件隊列構建接口這部分比較容易。事實證明,從其他團隊那裡獲得支持更加困難。與Harpoon集成意味著團隊將不得不放棄他們的數據庫訪問,重寫他們的部分代碼庫,並最終改變他們一直以來做事的方式。這可不是件容易的事。

Harpoon的工程師們逐個團隊、逐個服務地將整個代碼庫遷移到他們的新平台上。這花了差不多一年的時間,但到2017年底,Harpoon成為數據庫消息隊列的唯一發布者。

現在真正的工作開始了。對事件系統的完全控制意味著Harpoon可以自由地重新設計Droplet工作流了。

Harpoon的第一個任務是將消息隊列職責從數據庫提取到自身。為此,Harpoon創建了自己的內部消息隊列,由RabbitMQ和異步worker組成。當Harpoon在一側把新事件推入隊列時,worker在另一側拉取它們。由於RabbitMQ取代了數據庫隊列,worker可以方便地直接與調度器和事件路由器交互。因此,調度器V2和事件路由器不是通過輪詢從數據庫獲取新變化,而是由Harpoon直接將更新推送給它們。截止到2019年本文撰寫時,這就是Droplet事件架構所處的位置。

數據庫連接從15000降到100以下:DigitalOcean如何解決技術債 5

一路向前

在過去的7年裡,DigitalOcean已經從一家小型創業公司成長為今天這樣的成熟的雲提供商。與其他轉型中的科技公司一樣,DigitalOcean會定期處理遺留代碼和技術債務。無論是拆分單體應用、創建多區域服務,還是消除單點故障,我們DigitalOcean的工程師們始終致力於打造優雅而簡單的解決方案。

這就是我們的基礎設施如何隨著用戶群的發展而擴展的故事,希望你覺得這是個有趣而又有啟發性的故事。我很樂意在下面的評論中看到你的想法!

原文鏈接:

From 15,000 database connections to under 100: DigitalOcean’s tale of tech debt