[譯] 將一箇舊的大型專案遷移到 Python 3

Starrier發表於2019-03-04

將一箇舊的大型專案遷移到 Python 3

一年半前,我們就決定使用 Python 3 了。我們已經討論了很長時間,現在是時候使用了!現在這個過程已經結束了,我們已經把生產環境的最後部署都遷移到了 Python 3

  • 整個程式碼庫大約有 240 k 行,不包括空行和註解。
  • 這是一個基於 Web 的批處理任務系統。並且只有一個生產,部署環境。
  • 程式碼庫大約有 15 年的歷史了。
  • 雖然這是一個 Django 應用程式,但部分程式碼是先於 Django 公佈之前寫的。

關於修改 Python 3 的一些基本統計資料,是基於對 git 提交歷史的粗略過濾產生的:

  • 275 次提交
  • 4080 次新增程式碼行
  • 3432 次刪除程式碼行

我發現有 109 個 jira 問題與這個專案相關。

Py2 → six → py3

我們的理念一直是 py2 →py2/py3 → py3 因為我們實在無法在實際生產中實現鉅變,這種直覺也以令人驚訝的方式被證明是正確的。這意味著 2 到 3 是不可能的,我認為這很常見。我們嘗試過使用 2 to 3 來檢測 Python 3 的相容性問題,但很快這也被發現無法成立。基本上,這樣的更改意味著在 Python 2 中的程式碼將被破壞。這樣的改變不可行。

結論是使用 six, 這是一個庫,可以方便的構建一個在 Python 2 和 3 中都有效的程式碼庫。

首當其衝的就是更新之前的依賴關係。這項工作需要立刻啟動,因為之後會有更多的內容要更新。

現代化

Python-modernize 是我們選擇進行遷移的工具。它是一個可以自動將 Py 2 程式碼庫轉換為可相容 six 程式碼庫的工具。我們首先引入一個測試,作為 CI 的一部分,來檢查基於 modernize 的新程式碼是否已經準備好相容 py3 了。這樣做最大的效果的是讓那些仍使用 Py 2 語法的人意識到新的處理方法,但這顯然對將現有的 240 k 行程式碼轉化到 six 作用不大。我們都有使用舊語法的壞習慣,這可以說是教學上的成功了,即使它對程式碼行的計數沒有什麼不同,它也被我們用於實驗分支:

實驗分支

我新建了一個名為“Python 3 ”的分支,並做了以下操作:

  • 在整個程式碼庫上執行“python-modernize -n -w” 。它會在合適的地方修改程式碼。我經常做完這步後沒有進行第一次提交就開始修復程式碼。這個錯誤步驟總是讓我後悔,不止一次地迫使我重新開始做整件事情。即使這個階段出錯,最好還是先把它提交。因此將機器和人要做的事情分開顯得尤為重要。
  • 將所有用於函式體的依賴項匯入到我們還沒有修復的 py3。

這裡的想法是“run ahead”,即看看如果我們沒有使用過時的依賴項,我們會遇到什麼問題。這個分支允許我在超級中斷狀態下可以非常快速地啟動應用程式,至少可以執行一些單元測試。 這個分支有很大的不同,但我還是找到了把它應用在適當場景的方法。我使用優秀的 GitUp 來拆分、組合和提交。當一個提交看起來不錯的時候,我會把它挑選到一個新的分支,然後發給程式碼審查。

沒有人可以在這個分支上工作,因為它被不斷地 rebase ,強制推送,濫用,但是它確實讓專案向前推進了,而不用等待所有的依賴項被更新。我強烈推薦使用這種方法!

靜態分析

我們新增了預提交鉤子,所以如果您編輯了一個檔案,就會收到建議將 Python 3 全部進行 modernize 更新的提示。

quote_plus 的手動靜態分析: 在處理 quote_plus 和 six 上有一些細微差別。最後,我們建立了自己的包裝器,預設程式碼強制執行使用這個包裝器,而不是使用標準庫中的包裝器,也不使用 six 中包裝器。我們還靜態檢查了您從未給 quote_plus 傳送過的位元組。

我們修復了每個 diango 應用程式中所有的 python 3 問題,並在 CI 環境中使用一個白名單強制執行了這一點,所以您無法破壞一個曾經修復過的應用程式。

依賴

對於我們來說,解決依賴是最困難的部分。我們有很多依賴,所以花了很多時間,其中有兩個依賴關係比較棘手:

  • splunk-lib. 我們依賴於 splunk,但是直到今天,他們仍然忽略所有要求為客戶端增加 py3 相容性的憤怒的客戶。我們團隊中的一個人 最後自己親自動手來解決這個問題。Splunk 處理得真的很糟糕,它甚至把這個評論區的這個問題鎖上了!這簡直讓人無法接受。
  • Cassandra. 我們的整個產品都在使用這個資料庫,但是我們使用了一個有以前 API 模組的舊的驅動程式。對於我們來說,py3 的遷移過程中,這佔據了很大的一部分,因此我們必須逐段重寫所有的這些程式碼。

測試

我們的程式碼測試覆蓋率大約有 65% 包括:單元、整合, 以及 UI 合併。 我們確實編寫了更多的測試,但總體數量並沒有發生太大的變化。考慮將覆蓋率從 65% 提高到 66% ,意味著編寫將近2000 行程式碼的測試,這一點也不奇怪。

我們必須跳過需要 Cassandra 的測試,同時修復這個依賴項。 我發明了一個有趣的小 hack 來使它發揮作用, 並寫了這方面的文章.

程式碼更改

關於程式碼更改的說明,在如何將 py2 遷移到 six 的文件中並未提及 (也許是我們錯過了):

StringIO

我們在程式碼中大量使用 StringIO 。第一反應就是使用 six。但對於 StringIO 來說,這在幾乎所有情況下 (但不是全部!)都被證明是錯。基本上,我們必須非常仔細地考慮每一個我們使用 StringIO 的地方,並試圖弄清楚我們是否應該用 io.StringIO, io.BytesIO 或者 six.StringIO 來替代它。這裡犯錯的表現通常為看起來像相容 py3 的程式碼準備好了,在 py2 中可以正常執行,卻實際上在 py3 中是失效的。

future 中匯入unicode_literals

這是一件好壞參半的事情。您可以通過將它新增到許多檔案中來發現 bug,但是有時會在 py2 中引入 bug。 當日志突然在奇怪的地方,比如在字串前寫”u”時,它也會變得令人困擾。總的來說,這顯然不是我所期望的效果。

str/bytes/unicode

這在很大程度上是您所期望的。我感到驚訝的是,在 py2 和 py3 中需要 str 。如果將來您使用 unicode_literals 匯入,那麼一些字串需要從 `foo` 修改為 str(`foo`)

six.moves

six.moves 的實現是一個非常奇怪的黑客行為,因此它不像它假裝的普通 Python 模組那樣執行。 我也不同意他們在 six.moves 中不包含 mock 的選擇。我們必須使用他們的 API 來自己新增它,但這讓我們很難開始工作,而且它要求我們將 from mock import patch 改為 from six.moves import mock 這也意味著 patch 現在變成了 mock.patch

CSV 的解析是不同的

如果你使用 csv 模組,你需要了解 csv342。在我看來,這應該是 six 的一部分。否則就意味著你沒有意識到有問題。不過我們在許多地方都沒有使用 csv342,所以您這裡要做的工作可能會有所不同。

釋出順序

我們首先進行測試:

  • 在 CI 中進行單元測試
  • 在 CI 中進行整合和UI測試(不包括 Cassandra)
  • 在 CI 中進行 Cassandra 測試 (這要晚於之前的步驟!)

接下來就是產品本身了。我們建立一臺擁有能一次性切換到 py3 的能力的批處理機器,並且至關重要地是將其切換回來。當在 py3 上發生中斷時,這一點就顯得很重要了。這對我們來說是很好的,因為我們可以重新排隊那些中斷的任務,但是我們不能中斷太多或者任何實際上是很關鍵的任務。我們使用 Sentry 來收集奔潰日誌,所以很容易檢視遷移到 py3 時遇到的所有問題,而且當我們修復了所有的問題時,我們需要再次遷移到 py3,直到我們得到一些問題,如此反覆。

我們有如下環境:

  • Devtest: 開發人員在內部使用,所以大多數情況下,這只是用來測試資料庫遷移。這個環境非常容易使用,所以這裡不經常出問題。
  • IAT (內部驗收測試):用於驗證更改,並在我們將更改推送到生產之前執行迴歸測試。
  • UAT (使用者接受度測試): 客戶可以訪問的測試環境。用於需要準備客戶系統的變更,或者讓客戶在上線前檢視變更。這個環境在資料庫遷移前幾天才會遷移。
  • 生產環境

我們按照以下順序將 Python 3 釋出到這些環境中:

  • Devtest 環境
  • 短期 IAT 環境
  • 長期 IAT 環境
  • 一臺短期的批處理生產機器
  • 在工作期間使用的一臺批處理生產機器
  • 生產 SFTP
  • 佔一半生產的批處理機器
  • 生產批次
  • 生產 Web (在測試環境的長時間手動測試執行之後)
  • 生產負載機器。這是批處理的一個特殊子集。它完成了我們產品中 CUP 和記憶體最多的部分。

負載機器暴露了與 Python 3 不相容的客戶資料配置,因此我們必須在 Python 2 中實現對這些情況的警告,並確保再次開啟 Python 3 之前已經修復了它們。這花了幾天時間,因為我們每天都會收到客戶資料,所以每次都會有一個警告,這又讓我們不得不再等一天。

生產中的驚喜

  • `ß`.upper() 在 py2 中是  `ß` 但是在 py3 中是  `SS` 。當產品的最後一部分遷移到 py3 時,最終導致了產品的崩潰!
  • 在 py2 中對不同型別的物件進行比較和排序是有效的,但這隱藏了大量的 bug 。我們得到了一些令人討厭的驚喜,因為這種行為以一些不明顯的方式從堆疊中洩露出來,特別是在一些排序列表中存在  None 的時候。總的來說,這是一個勝利,因為我們發現了相當多的 bug 。 None 在 py2 的列表中排在第一位,這可能會讓人感到驚訝(您可能會期望它被排序到接近於零的地方!), 現在我們只需要來處理它們。
  • `{}`.format(b`asd`) 在 Python 2 中是 `asd` , 但是在 Python 3 中是 "b`asd`" 。在 Python 3 中,這裡幾乎任何其他行為都會更好: 輸出為十六進位制 ( 結果明顯更不一樣 ) ,舊的行為 (之前的程式碼執行),或者丟擲異常 (最好的行為!)。
  • int(`1_0`) 在 py 3 中結果是 10 , 但是在 py2 中無效。這甚至在切換到 py3 之前就困擾了我們。因為這種錯配導致了另一個在我們之前使用 py3 的團隊給我們傳送了我們認為無效而他們認為有效的有效值。我個人認為這個決定是錯誤的:非常嚴格的解析是更好的預設方式,我擔心這將在未來幾年會繼續以微妙的方式困擾我們。

結論

最後,我們覺得在這件事上我們真的別無選擇: Python 2 的維護將在某個時刻停止,我們的依賴項僅限於 py3,最明顯的就是 Django。但是,無論如何,我們還是想要進行這種轉換,因為我們經常會被 bytes/Unicode 問題困擾,並且Python 3 僅僅是修復了 Python 2 中的許多小麻煩。這次遷移過程,我們已經在生產過程中發現了一些實際的漏洞/錯誤配置。我們也期待在任何地方都可以使用 f-string 和有序字典。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章