別了,MongoDB!

別了,MongoDB!

作者 | 衛報

譯者 | 薛命燈

為了獲得一個全託管的解決方案,英國衛報在 2018 年將 CMS 的數據存儲從一個自託管的 MongoDB 集群遷移到了 Amazon RDS 上的 PostgreSQL。團隊在沒有停機的情況下進行了基於 API 的遷移。

Guardian(英國衛報)網站的大部分內容——包括文章、博客、圖集和視頻——都是通過我們的內部 CMS 工具 Composer 生成的。用於存儲這些內容的是運行在 AWS 上的 MongoDB 數據庫。這個數據庫實際上是 Guardian 發佈的所有在線內容的“事實來源”——大約有 230 萬項內容,而在不久前,我們完成了從 MongoDB 到 Postgres SQL 的遷移。

Composer 及其數據庫最初起源於 Guardian Cloud——位於 Kings Cross 辦公室地下室的數據中心裡,並在倫敦的其他地方進行了失效備援。我們的失效備援機制在 2015 年 7 月一個炎熱的夏日經受了一次嚴峻的考驗。

別了,MongoDB!


倫敦南海岸酷熱的天氣適合小孩在噴泉中跳舞,但對數據中心絕對不是什麼好事

從那之後,將 Guardian 遷移到 AWS 這一需求變得更加緊迫。我們決定購買 OpsManager(MongoDB 的數據庫管理軟件)和 MongoDB 支持合同,希望給雲遷移帶來幫助。我們使用 OpsManager 來管理備份、處理編排問題,並對我們的數據庫集群進行監控。

由於編輯部的要求,我們需要在 AWS 上運行數據庫集群和 OpsManager,而不是使用 MongoDB 託管數據庫產品。這個很有難度,因為 MongoDB 並沒有提供任何可用於在 AWS 上搭建數據庫的工具——我們需要手動編寫 cloudformation 來定義所有的基礎設施。我們編寫了數百行 ruby 腳本,用來安裝監控和自動化代理以及編排新數據庫實例。我們不得不在團隊中進行與數據庫管理相關的知識分享——而我們原本希望 OpsManager 能夠輕鬆實現這些。

自從遷移到 AWS 以來,由於數據庫問題,我們經歷了兩次嚴重的中斷,每次都有一個小時的時間無法在 theguardian.com 上發佈任何內容。在這兩次事故中,OpsManager 和 MongoDB 的支持代理都沒能夠幫到我們,我們最終都是自己解決了這些問題——其中一次要感謝我們的一個同事,他在阿布扎比郊區的沙漠中接聽了我們打給他的救急電話。每個問題都可以寫成一篇博文,不過我們可以將這些問題概括如下。

  • 時鐘非常重要——不要將 VPC 鎖定到無法使用 NTP 的地步。
  • 在應用程序啟動時自動生成數據庫索引可能不是一個好主意。
  • 數據庫管理很重要而且很難——我們最好不要選擇自己做。


別了,MongoDB!


當時鐘不同步時,網絡將成為一場噩夢

OpsManager 並沒有真正兌現無障礙數據庫管理的承諾。例如,OpsManager 本身的管理就很費時——特別是從 OpsManager 1 升級到 OpsManager 2 時,並且需要掌握 OpsManager 的相關知識。由於不同版本 MongoDB 之間的身份驗證模式發生了變化,它也沒有實現“一鍵升級”的承諾。我們每年至少要花費兩個月的工程時間來完成數據庫管理工作。

所有這些問題,加上我們每年為支持合同和 OpsManager 支付的高額費用,我們開始尋找替代數據庫方案,並提出了以下要求。

  • 儘量減少數據庫管理相關的工作。
  • 支持靜態加密。
  • 可以從 MongoDB 遷移過來。

因為其他服務都運行在 AWS 上,所以最明顯的選擇應該是 DynamoDB——亞馬遜的 NoSQL 數據庫產品。然而,Dynamo 當時還不支持靜態加密。在等待亞馬遜開發這個特性大約 9 個月後,我們放棄了,最終選擇在 AWS RDS 上使用 Postgres。

你可能會說:“但是 Postgres 不是文檔數據庫!”。是的,它不是文檔數據庫,但它確實支持 JSONB 列類型,而且支持 JSON Blob 字段索引。我們希望通過使用 JSONB 類型將 MongoDB 遷移到 Postgres,只需要對數據模型做出最小的更改。此外,如果我們希望將來轉向關係型模型,還可以繼續使用 Postgres。另外,Postgres 已經很成熟了:我們遇到的大部分問題都能夠在 Stack Overflow 上找到解答。

從性能的角度來看,我們有信心 Postgres 可以應對 Composer,儘管 Composer 是一個寫入密集型的工具(每次用戶停止輸入時就會將內容寫入數據庫),但通常只有幾百個併發用戶,不需要高性能並行計算!

不停機遷移二十年的網站內容


別了,MongoDB!


Postgres 咬了一口 Mongo

遷移計劃

大多數數據庫遷移都涉及相同的步驟,我們的也不例外。以下是我們遷移數據庫的步驟。

  • 創建新數據庫。
  • 創建一種寫入新數據庫的方法(新 API)。
  • 創建一個代理,將流量發送到舊數據庫和新數據庫,並使用舊數據庫作為主數據庫。
  • 將記錄從舊數據庫遷移到新數據庫。
  • 使新數據庫成為主數據庫。
  • 移除舊數據庫。

因為遷移的數據庫正在為我們的 CMS 提供支持,所以在遷移過程中應該儘可能減小對用戶造成的影響。畢竟,新聞不能一刻停止更新。

新 API

2017 年 7 月下旬,我們開始開發基於 Postgres 的新 API。所以,我們的遷移旅程開始了。但要了解整個旅程,首先需要了解我們是從哪裡開始的。

我們的 CMS 架構大概是這樣的:一個數據庫、一組 API 和與 API 交互的一些應用程序(例如 Web 前端)。技術棧一直是 Scala、Scalatra Framework 和 Angular.js,四年來沒有變過。

經過一些調查,我們得出結論,在遷移現有內容之前,我們需要找到一種方法與新的 PostgreSQL 數據庫通信,並且仍然可以像往常一樣使用舊 API。畢竟,MongoDB 仍然是事實來源。在試驗新 API 時,它為我們提供了一張安全毯。

這就是為什麼基於舊 API 開發不是一個好的選擇。在舊 API 中幾乎沒有關注點分離,甚至在控制器級別都能夠找到 MongoDB 相關的東西。因此,基於現有 API 添加另一個數據庫存風險太大。

我們另闢蹊徑,複製了舊 API,於是就有了 APIV2。它包含了與舊 API 相同的端點和功能。我們使用了 doobie(https://tpolecat.github.io/doobie/)——一個 Scala JDBC 層,並使用 Docker 在本地運行和測試,改進了日誌和關注點分離。APIV2 將成為更快的現代 API。

到了 2017 年 8 月底,我們部署了一組新 API,使用 PostgreSQL 作為後端數據庫。但這只是一個開始。保存在 MongoDB 中的文章最早是在二十年前創建的,所有這些文章都需要遷移到 Postgres 數據庫中。

開始遷移

我們需要能夠編輯網站上的文章,無論它們是什麼時候發佈的,因此所有文章都作為單一的“事實來源”存在數據庫中。

雖然通過 Guardian 的 Content API(CAPI)可以操作所有的文章,為應用程序和網站提供支持,但遷移才是關鍵,因為數據庫是“事實來源”。如果 CAPI 的 Elasticsearch 集群出現任何問題,可以從 Composer 的數據庫重新索引數據。

因此,在關閉 MongoDB 之前,我們必須確信發給基於 Postgres 的 API 和基於 MongoDB 的 API 的相同請求能夠返回相同的響應。

為此,我們需要將所有內容複製到新的 Postgres 數據庫中。這一步使用了直接調用新舊 API 的腳本來完成。這樣做的好處是,API 已經提供了一組經過良好測試的接口,用於從數據庫讀取文章和寫入文章,而不是編寫直接訪問數據庫的代碼。

遷移的基本流程是這樣的:

  • 從 MongoDB 獲取內容。
  • 將內容發佈到 Postgres。
  • 從 Postgres 獲取內容。
  • 檢查第一步和第三步的響應是否相同。

如果最終用戶完全沒有意識到後端正在進行遷移,這說明遷移進行得很順利,而一個好的遷移腳本是進行順利遷移的重要組成部分。

因此,我們需要一個腳本,它可以:

  • 發出 HTTP 請求。
  • 確保在遷移一部分內容後,兩個 API 返回的響應是相匹配的。
  • 如果出現錯誤則停止。
  • 生成詳細日誌以幫助診斷問題。
  • 發生錯誤後從正確的點重啟。

我們開始使用 Ammonite(http://ammonite.io/)。Ammonite 允許你使用 Scala 編寫腳本,Scala 是我們團隊使用的主要語言。這是一個很好的機會,我們可以嘗試之前沒有用過的東西,看看它對我們是否有用。不過,雖然 Ammonite 允許我們使用熟悉的語言,但它也有一些缺點。雖然 Intellij 現在支持 Ammonite,但在當時是不支持的,所以當時我們無法使用自動完成和自動導入功能,而且也不可能長時間運行 Ammonite 腳本。

最終,我們放棄使用 Ammonite,並選擇了一個 sbt 項目來進行遷移。這種方法讓我們可以使用的我們認為可靠的語言,並可以進行多次“測試遷移”,直到我們有信心在生產環境中運行。

讓我們感到驚喜的是,這對測試 Postgres API 有很大好處。我們在新 API 中發現了一些之前沒有發現的細微錯誤和邊緣情況。

快進到 2018 年 1 月,是時候在我們的預生產環境 CODE 中測試完整的遷移了。

與我們的大多數系統類似,CODE 和 PROD 之間唯一相同的地方是應用程序的版本。用於 CODE 環境的 AWS 基礎設施遠沒有 PROD 那麼強大,因為它的使用率要低得多。

在 CODE 上運行遷移將有助於我們:

  • 估計遷移到 PROD 需要多長時間。
  • 評估遷移對性能的影響(如果有的話)。

為了準確衡量這些指標,我們必須讓這兩個環境相匹配。我們需要將 PROD 環境的 MongoDB 備份還原到 CODE 環境,並更新 AWS 基礎設施。

遷移 200 多萬項內容需要很長時間,我們的腳本運行了一整夜。

為了衡量遷移的進度,我們將結構化日誌發送到 ELK。我們可以創建儀表盤,跟蹤成功遷移的文章數量、失敗的數量和總體進度。此外,儀表盤顯示在離團隊很近的大屏幕上,讓大家都能看到。

別了,MongoDB!


顯示遷移進度的儀表盤

遷移完成後,我們採用相同的技術檢查 Postgres 與 MongoDB 中的每個文檔是否匹配。

代理和在生產環境中運行


別了,MongoDB!


MongoDB 到 Postgres 的遷移:代理

代理

現在,基於 Postgres 的新 API 已經在運行,我們需要使用真實的流量和數據訪問模式對其進行測試,以確保它的可靠性和穩定性。我們可以使用兩種方法:修改客戶端,讓原先的 Mongo API 客戶端與新舊兩個 API 通信,或者使用代理。我們使用 Akka Streams 開發了一個代理。

代理的操作相當簡單:

  • 接收來自負載均衡器的流量。
  • 將流量轉發到主 API 並返回。
  • 將相同的流量異步轉發到從 API。
  • 計算兩個響應之間的差異並記錄它們。

一開始,代理記錄了兩個 API 響應之間的很多差異,這說明 API 有一些細小但卻很重要的行為差異,我們需要修補這些差異。

結構化日誌

在 Guardian,我們使用 ELK 來記錄日誌。Kibana 為我們提供了靈活顯示日誌的方式。Kibana 採用了簡單易學的 lucene 查詢語法。但我們很快就發現無法過濾掉日誌或對其進行分組。例如,我們無法過濾出與 GET 請求相關的日誌。

我們的解決方案是向 Kibana 發送更多結構化日誌,而不只是發送消息。一個日誌條目包含了多個字段,例如時間戳、發送日誌的應用程序名。我們可以非常容易地通過編程的方式來添加新字段。這些結構化字段被稱為標記(marker),可以使用 logstash-logback-encoder 庫(https://github.com/logstash/logstash-logback-encoder)來實現。我們從請求消息中提取了有用的信息(例如路徑、方法、狀態碼),併為這些信息創建一個 map,將它們記錄到日誌中。請看下面的例子。

import akka.http.scaladsl.model.HttpRequest
import ch.qos.logback.classic.{Logger => LogbackLogger}
import net.logstash.logback.marker.Markers
import org.slf4j.{LoggerFactory, Logger => SLFLogger}
import scala.collection.JavaConverters._
object Logging {
val rootLogger: LogbackLogger = LoggerFactory.getLogger(SLFLogger.ROOT_LOGGER_NAME).asInstanceOf[LogbackLogger]
private def setMarkers(request: HttpRequest) = {
val markers = Map(
"path" -> request.uri.path.toString(),
"method" -> request.method.value
)
Markers.appendEntries(markers.asJava)
}
def infoWithMarkers(message: String, akkaRequest: HttpRequest) =
rootLogger.info(setMarkers(akkaRequest), message)
}

日誌的附加結構讓我們可以構建有用的儀表盤,併為差異添加更多的上下文信息,這樣有助於我們識別 API 之間的一些細微的不一致。

複製流量和代理重構

在將內容遷移到 CODE 數據庫後,我們最終得到了幾乎與 PROD 數據庫相同的副本,主要區別是 CODE 現在沒有流量。為了將真實流量複製到 CODE 環境中,我們使用了一個叫作 GoReplay(gor,https://goreplay.org/)的開源工具。它的設置非常簡單,並且可以根據你的需求進行定製。

因為訪問 API 的所有流量會先達到代理,所以有必要在代理服務器上安裝 gor。你可以像下面這樣下載 gor,捕獲 80 端口的流量,然後將其發送到另一臺服務器。

wget https://github.com/buger/goreplay/releases/download/v0.16.0.2/gor_0.16.0_x64.tar.gz
tar -xzf gor_0.16.0_x64.tar.gz gor
sudo gor --input-raw :80 --output-http http://apiv2.code.co.uk

一切都運行良好,但一段時間之後,出現代理會在幾分鐘內不可用的情況,導致生產環境出現中斷。經過調查,我們發現運行代理的三臺服務器同時重啟。我們懷疑 gor 使用了太多資源,導致代理出現故障。經過進一步調查,我們在 AWS 控制檯中發現這些服務器會定期重啟,但不是在同一時間。

我們試圖找到一種方法,即在運行 gor 的同時不給代理施加任何壓力,於是我們使用了第二個技術棧。這套方案只在發生緊急時使用,而且我們的生產環境監控工具會不斷對其進行測試。這次,將流量從這個技術棧以雙倍的速度複製到 CODE,沒有出現任何問題。

但新發現帶來了很多問題。我們在設計代理時只是將它作為暫時方案,所以它可能沒有像其他應用程序那樣經過精心設計。此外,它是用 Akka Http 構建的,而團隊成員之前並沒有使用過它。代碼很混亂,到處都是臨時修復代碼。我們決定啟動一項重大的重構工作,以提高代碼可讀性,並添加更多的日誌標記。

我們希望通過簡化代碼邏輯來防止服務器崩潰重啟,但並沒有奏效。經過大約兩個星期的嘗試,我們感覺越陷越深。我們不得不痛下決心放棄,因為將時間花在實際的遷移工作上比試圖修復一個月之後就會消失的軟件要好得多。做出這個決定的代價就是我們又經歷了兩次生產環境中斷,每次中斷持續大約兩分鐘,但總體來說這是正確的做法。

快進到 2018 年 3 月,我們現在已經完成了 CODE 遷移,對 API 的性能或 CMS 的用戶體驗沒有任何不利影響。我們現在可以開始考慮停用 CODE 中的代理。

第一階段是改變 API 的優先級,讓代理先與 Postgres 通信。雖然這一步是可配的,但仍然有一點複雜。

Composer 會在一個文檔被更新後向 Kinesis 流發送消息。為了避免消息重複,只應該有一個 API 發送消息。API 為此提供了一個配置標誌,值為 true 表示基於 MongoDB 的 API,值為 false 表示基於 Postgres 的 API。但只是簡單地讓代理與 Postgres 通信還不夠,因為在請求到達 MongoDB 之前,消息不會被髮到 Kinesis 流,這樣延遲太嚴重了。

為了解決這個問題,我們創建了 HTTP 端點,用於即時修改負載均衡器所有實例的內存配置。這樣我們就能非常快地切換 API,而無需編輯配置文件並重新部署。此外,這個可以通過腳本來實現,減少人為干預和人為錯誤。

現在,所有的請求都先到達 Postgres,我們可以通過配置和重新部署讓更改永久生效。

下一步是完全移除代理,讓客戶端直接與 Postgres API 通信。由於我們有很多客戶端,逐個更新每個客戶端並不是個好辦法。因此,我們考慮在 DNS 層面做一些修改。我們最開始在 DNS 中創建了一個 CNAME,指向了代理的 ELB,現在將其改為指向 API 的 ELB。這樣就可以只在一個地方做出更改,而不是更新每一個客戶端。

現在到了 PROD 遷移的時候了。這個過程相對簡單,因為一切都是基於配置的。此外,因為我們在日誌中添加了 stage 標記,所以我們可以通過更新 Kibana 過濾器來重用之前構建的儀表盤。

關閉代理和 MongoDB

歷經 10 個月,遷移了 240 萬篇文章,我們終於可以關閉所有與 MongoDB 相關的基礎設施。但首先是我們一直在等待的那一刻:關掉代理。

別了,MongoDB!


日誌顯示了代理在消減

代理給我們帶來了很多問題,我們迫不及待想要把它關掉!我們所要做的就是更新 CNAME 記錄,將它直接指向 APIV2 負載均衡器。

出乎意料的是,刪除舊的 MongoDB API 成了我們的另一項挑戰。在瘋狂刪除舊代碼時,我們發現集成測試並沒有使用新 API。所運的是,大多數問題都只與配置相關,因此這些問題很容易就解決了。

之後發生的一切都很順利。我們將所有 MongoDB 實例從 OpsManager 中分離出來,然後關掉它們。剩下的事情就是慶祝,然後睡個好覺。

英文原文

https://www.theguardian.com/info/2018/nov/30/bye-bye-mongo-hello-postgres


分享到:


相關文章: