[譯] 將一箇舊的大型項目遷移到 Python 3

將一箇舊的大型項目遷移到 Python 3

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

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

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

  • 275 次提交
  • 4080 次添加代碼行
  • 3432 次刪除代碼行

我發現有 109 個 jira 問題與這個項目相關。python

Py2 → six → py3

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

結論是使用 six, 這是一個庫,能夠方便的構建一個在 Python 2 和 3 中都有效的代碼庫。ios

首當其衝的就是更新以前的依賴關係。這項工做須要馬上啓動,由於以後會有更多的內容要更新。git

現代化

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

實驗分支

我新建了一個名爲「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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索