升級 Python 3 (上) — Porting bus

Rilak, Zhou
iCHEF
Published in
10 min readSep 6, 2021

iCHEF 去年底順利地從 Python2.7 升級到了 Python 3.6 (緊接著升了 3.8)。

將整個流程分成六個部分,這篇文章講的是 1~3,4~6 在下集。

升級流程

  1. 決定 Py3 版本與升級套件。
  2. 建置 Py3 CI,並讓 Code 可以執行測試(不用通過測試)。
  3. 在 Py3 分次通過所有的單元測試。
  4. 測試與部署 Py3 service。
  5. 使用 Load Balancer (AWS ALB) 分次切換流量上版。
  6. 確認流量都導向 Py3 後,關閉 Py2 service。

其中「3. 通過單元測試」和「5. 切換流量」都是分次慢慢完成,避免一次變動太多而增加品質工程師測試的壓力。

什麼是 Py3 Porting ?

非常建議先參考以下 Guide:

  1. Python 官方建議指南: Porting Python 2 Code to Python 3
  2. Six (Python 2 and 3 Compatibility Library)
  3. Python3 porting book
  4. CheatSheet

想像目前有一個專案正在用 Python2 執行,因為族繁不及備載的理由(新套件、新功能、招募、效能以及不斷逼近的 End-of-life) 想要改成用 Python3 執行。
所以我們先嘗試安裝了某一個 Py3 版本(3.6, 3.7, …),然後執行 pip-compile 或 pip install 想設定好 dependencies,然後發現套件版本不支援
搞定版本與套件後嘗試執行程式,首先遇到 print "" 跳 Syntax Error。於是把所有 print 都改成了 print()。
然後發現除法回傳浮點數、字串編碼都變 unicode 了。所以你在每一個檔案的前面都加上了起手式:

from __future__ import absolute_import, division, print_function, unicode_literals

然後開始處理被棄用/改名的 function:

# 2, 3 不相容
for key, value in some_dict.iteritems():
print key, value
# 2, 3 相容
for key, value in some_dict.items():
print(key, value)

或是發現因為型別不同而錯得比較隱晦:

# map() return list in py2, iterator in py3
numbers = map(int, ['2', '5', '3', '4'])
print(min(numbers))
print(sum(numbers)) # print 14 in Py2, 0 in py3
# Fix: numbers = list(map(int, ['2', '5', '3', '4']))

當上面這些簡單的語法題出現在數萬行的專案,出現很 Legacy 的 code,出現在各種角落,重複解這類問題還是滿讓人崩潰的。
又因為有大量的 Code change,所以必須建立 Py2 和 Py3 的自動化測試與 CI 來確認 Py3 行爲正確以及 Py2 行為不變。
可能也需要在進 release 前回顧有什麼地方改得特別多,寫清單交給偉大的品質工程師請他們協助驗證。
至於在開心的完成一個小目標後,發現與其他團隊的 PR 大量 conflict 又是另一個故事了…。

辦公室的 Py3 porting 吉祥物: 負電拍拍與正電拍拍,寶可夢第世代的加油寶可夢。

決定 Python3 版本與升級套件:

當時(2019年) Python 的最高版本是 3.8,期望上當然是能升多高升多高,如果能升到 3.8 是最好。但還要考慮套件支援版本。
以 Pandas 為例,0.24.2 版本支援 Py 2.7 但最高只支援 Py 3.7,0.25.0 支援 Py 3.8 但就不支援 Py2 了。

最後在 H 同事努力研究套件版本之下,決定先升級到 Python 3.6。
(後來我們又做了 3.6 ➡ 3.8 的升級,相比 2.7 ➡ 3.6 超過半年的工程,在不需要考慮 2.7 之後,3.6 ➡ 3.8 只用了不到兩週。)

建置 Py3 環境與 CI

在 H 同事和 T 同事的努力之下,Code 被調整到了可以執行單元測試的狀態,此時測試(不意外的)幾乎全部 fail,只做到程式沒有 Syntax Error 以及安裝套件。
他們在這個階段也寫了文件和 Make commands 來方便地建立 Py3 環境、安裝套件等等,讓其他同事能無痛開始嘗試 Py3,功德無量,好人同事一生平安。

CI 上線與 Porting Bus

iCHEF 後端團隊的每個 PR 都必須在 Jenkins 上通過單元測試。
感謝 K 同事在 2018 年就已經建立了平行測試的架構 (參考: 再戰 Jenkins CI Pipeline: Parallel 加速),為了跑兩倍的 Py2 + Py3 CI,我們將 4 個平行增加到 8 個平行。

但考慮到 Py3 還不能通過測試,一開始新的 4 個平行幾乎什麼都沒跑。

在 Jenkins file 內用來指定 4 個平行分別要跑哪些測試,最初幾乎是空的。

H 同事把圖片內的 py3Parallel 稱為 Porting Bus,公車每到一站都會讓乘客上車,不需要等到車滿才出發。這邊則是 app 通過測試後才放入 CI 。好處是讓 feature team 必須在開發時考慮到 2、3 相容,不能把已經改好的 Py3 測試弄壞。

為了開始分工 Porting,我們在 Notion 上開了一個認養清單列出所有 app 給大家認領工作,每個人有空檔時就在清單上登記自己要改哪個 app。

App 名稱,負責人,doing or done

Porting Camp 與測試覆蓋率

在 Porting Bus 發車後一個月,剛好遇到公司例行的團隊重組,部分後端成員有大約一週的空擋可以一起參與 Porting,這週就命名為 Porting Camp (相信大家已經發現我們很喜歡幫每件事取名了)。

在這五天內我們做了…
1. 請每位後端成員安裝好 Py3 環境,選擇要修正的 App、以 Coverage 100% 為目標補測試、修正 Py3 錯誤
2. 改好 16 個 App (總共 6x 個),並每個 App 都有高於 90% 的覆蓋率
3. Py3 coverage 來到 36% (Py2 83%)

除了推進進度之外,在 Camp 後每個人都建好了 Py3 環境,都有能力 review porting PR 和修正開發中弄壞的 Py3 測試,Porting 不再只是一個人的事。
還有不確定算不算好處,這個 Camp 是後端團隊難得全體投入同一件事,覺得也算滿有趣的經驗 (但一次就夠了)。

壞處則是因為一週內連開了 16 個 PR,很多都有改到共用的程式 (例如 GraphQL schema, TestCaseBase, DB interface, Common libs, …),彼此 Conflict 到崩潰。若有一個人先改好這些共用程式,應該會舒服許多。

最後這邊不推薦大家以 100% Coverage 為目標,我們從不算低的 83% 開始補測試也增加了很多 Porting 成本,幫不是自己負責的功能補測試的成本比預期還高,如果是 Legacy 的話會更痛苦。
如果測試涵蓋率一開始就不高,比起短期內硬補上去,更推薦在 Deploy 時分批人工驗證,並隨時保留退回 Py2 的彈性。

The Porting Must Go On

韶光荏苒,輪到我接任後端 Infra 職務,與 Mentor 和主管討論後決定 2020 下半年主要負責完成 Py3 porting,於是每天的工作就是跑測試、修正、跑測試、發 PR、寫注意事項給品質工程師。
原本估算需要兩個月到一季,但一回生二回熟,由一個人專門負責 Porting 真的會越做越快,加上一千多個 Fail TestCase 內也有很多是共同的錯誤,工程量比想像中的少。最後用了三週的時間修完所有的 Py3 單元測試。

可喜可賀、可喜可賀

特別感謝各團隊的同事撥冗幫忙 Review PR。

做得好的與可以更好的

✅ 行前教育
大家都在各自的專案內水深火熱時,寫好文件和輔助工具(例如 Makefile)真的很重要。2 3 相容的改法也有很多種,有好有壞 (很多時候沒有絕對的好,但可以統一用大家都能接受的),我們有示範用 PR 與共編的常見問題列表。

✅ CI 的品質
Coverage: 良好的測試內容與涵蓋率能過濾掉絕大部分的錯誤。
效率: 因為 Porting 特別仰賴 CI 執行結果,所以執行速度, Flaky test 數量與機器穩定性等等都決定了 Porting 的效率。
所以我個人認為能順利完成 Porting 階段,真的要感謝大家日常的積累。

✅ 分工
Porting Bus 和分工表有效公開、追蹤進度。Camp 的舉辦則讓每位工程師都有能力參與 Porting。再次感謝每一位參與的同事。

✅ 風險揭露
每個 PR merge 都有開 task 給品質工程師告知修正範圍。

✅ 人力投入
長期來看,Py3 是團隊繼續前進不可或缺的更新。但對公司來說則是短期內沒有急需、沒有回報、需要很多人力還可能會增加 Bug 的項目,所以特別感謝公司和技術長支持,願意不斷投入人力專職負責 Porting 和辦 Camp。

❌ Conflict
在 Porting Camp 時大家一起改不同 App,結果彼此衝突導致 Merge PR 時卡來卡去。還有每次 release 會限制不要進太多 Porting PR,導致 PR 會放很久,增加 Conflict 的機會。(事後諸葛是只要改動範圍有被測到,並且明確告知品質工程師,也許當時可以不用進的這麼保守) 結果 PR 就一直放著,放兩週就忘記在修什麼了,放一個月直接忘記它存在,建議早點寫好改動範圍與注意事項。

❌ 要求 100% Coverage
新寫的 Feature 以100% 測試涵蓋率為目標當然沒問題。
但要幫以前的 Feature 測試到 100% 就要考慮清楚成本了。要加測試首先要確定 Spec,過程大概就是在公司內進行田野調查,起手式先 git blame 看當時開發的工程師是誰(如果是已離職員工,問一下資深同事有沒有印象),如果順利找到當時的專案 board,在裡面翻翻找找,有機會得到一份不確定有沒有過時的 Spec…。
比較實際的做法是只補有被改動且沒有測試的 Code。

下集內容

我們順利安裝完 Py3 套件並通過 Py3 的單元測試,但 Production 和測試環境都還是用 Py2 執行 Service。
下集會接著描述我們如何 Deploy Py3 Service 並透過切換流量的方式來減輕人工測試的負擔與保留退回 Py2 Service 的彈性。

--

--