Share ประสบการณ์ Upgrade Python 3.7

Share ประสบการณ์ Upgrade Python 3.7

ถ้าจะมีงานหนึ่งที่ผมทำแล้วรู้สึกสนุกและท้าทายทุกครั้งที่อยู่ที่ Pronto Tools ก็คือการอัพเกรดเวอร์ชั่น Python นี่แหละครับ ตอนผมเข้ามาทำงานใหม่ๆ ทีมใช้ Python 3.5 อยู่ปีที่แล้วเลยอัพเกรดเป็น 3.6 และในปีนี้เนื่องในโอกาส Python 3.7 ออก เราก็รอแปปนึงจนคิดว่าอะไรๆ พร้อมแล้ว ถึงจะอัพเกรด

ทำไมถึงต้องอัพเกรด

เพราะเราอยากใช้ของใหม่ๆ ครับ อันนี้เหตุผลสั้นๆ เลย เราอยากใช้ breakpoint() ตัวใหม่ เราอยากใช้ dataclasses เราอยากใช้ from __future__ import annotations และอื่นๆ อีกมากมาย นี่ยังไม่รวมถึง Library ต่างๆ ที่เกี่ยวข้องที่บาง Library ทำมาเพื่อ Python 3.7+ แล้ว ถึงแม้ว่า Python 3.6 กว่าจะ end of life ก็ตั้งปี 2021 แต่รีบๆ อัพเกรดไว้ก็ไม่เสียหายครับ

ข้อดีอีกอย่างของการอัพเกรดในช่วงนี้คือ หา Issues ง่ายครับหลายๆ Library ที่เราพึ่งพาอยู่ในโปรเจ็คเราก็ยังมีอีกหลายคนที่ต้องการอัพเกรดเหมือนกัน แต่ถ้าหาไม่เจอ นี่ก็เป็นโอกาสอันดีครับที่จะได้ไป Contribute ให้ Library หลายๆ ตัวให้รองรับ Python 3.7 ในตัว

How to upgrade แบบไม่เจ็บตัว

อย่างแรกเลยครับ ถ้าอยากอุ่นใจ codebase เราควรจะมี Unit test ครับ ซึ่งสิ่งหนึ่งที่ผมเรียนรู้จากช่วงอัพเกรดที่ผ่านมาคือ codebase ของ SimpleSat ที่เราดูแลอยู่บาง service มี coverage อยู่ที่ประมาณ 97% files, 98% lines covered เลยครับ ส่วนตัวผมเองเลยค่อนข้างอุ่นใจไประดับนึงละ

เนื่องจากว่า environment เราอยู่บน Docker 100% โนะ วิธีอัพเกรดคือแค่เปลี่ยน base image จาก Python 3.6 เป็น Python 3.7 แล้วก็ลอง Build images ของทุก microservice service ใหม่ดู จากนั้นก็รัน docker-compose stack ใน local แล้วดูว่าเกิดอะไรขึ้น ข้างล่างนี่คือ List ของปัญหาที่เราเจอกับ Library ที่เราใช้อยู่ครับ

uWSGI

เมื่อประมาณ 2 เดือนที่แล้วเราก็เพิ่งอัพเกรด uWSGI เป็นเวอร์ชั่น 2.0.17 พอมาถึงวันที่เราเปลี่ยนมาใช้ Python 3.7 ระเบิดครับ เหตุผลเพราะว่า C-API PyOS_AfterFork มันดัน Deprecate ใน Python 3.7 ครับ โชคดีที่ Issue นี้มีคนแก้ไว้แล้วและ uWSGI ได้ออก release 2.0.17.1 มาแก้ปัญหานี้แล้วเรียบร้อยครับ

Python 3.7.0: PyOS_AfterFork is deprecated · Issue #1813 · unbit/uwsgi
Since Python 3.7.0 the PyOS_AfterFork is deprecated. uwsgi is using this function in python_plugin.c.

PyToolz

สำหรับคนที่ไม่รู้จักนะครับ PyToolz เป็น Library functional programming ที่ได้รับความนิยมมากตัวหนึ่งใน Python และเราใช้มันในหลายๆ ส่วนของ codebase เราครับ เวอร์ชั่นที่เราใช้อยู่คือ 0.8.2 พออัพเกรดมาใช้ Python 3.7 สิ่งที่เราเจอคือ

SyntaxError: Generator expression must be parenthesized

ซึ่งโค้ดบางส่วนใน Library ยังเขียน Generator expression แบบไม่มีวงเล็บอยู่นั่นเองครับ ซึ่งเป็น Change in behavior หนึ่งใน Python 3.7 นะครับ

Python 3.7 now correctly raises a SyntaxError, as a generator expression always needs to be directly inside a set of parentheses and cannot have a comma on either side, and the duplication of the parentheses can be omitted only on calls. (Contributed by Serhiy Storchaka in bpo-32012and bpo-32023.)

แต่ก็โชคดีเหมือนกันครับที่ Library ออก patch มาแก้ไขเรื่องนี้แล้วเพียงแค่อัพไป release 0.9.0 ก็ไม่มีปัญหานี้แล้วครับ

python 3.7 compatibility bug · Issue #404 · pytoolz/toolz
In the conda's vendored version of toolz, when testing against python 3.7, I got this error Traceback (most recent call last): File "/opt/conda-py37/lib/python3.7/site-packages/conda/excep...

Django

เนื่องจากสถาปัตยกรรมของเราเป็น Microservice โนะแล้ว แต่ละ service ก็เกิดก่อนหลังไม่พร้อมกันเลยมีบางตัวที่ใช้ Django ไม่ตรงกับตัวอื่น ปัญหานี้เจอใน Django 1.11.x ครับและเป็นปัญหาเดียวกับ PyToolz เลยคือ Generator expression must be parenthesized ซึ่งก็มีคนเปิด issue ไว้แล้วทีนี่

Refs #28814 -- Fixed “SyntaxError: Generator expression must be paren… · django/django@931c60c
…thesized” on Python 3.7. Due to https://bugs.python.org/issue32012.

แต่ตัวนี้โชคร้ายหน่อยตรงที่ Maintainer ของ Django ไม่อัพเดทให้กับ 1.11 แล้วเนื่องจากหมด Mainstream support ไปแล้วครับ

Per the FAQ, Django 1.11.x is not compatible with Python 3.7.

Django 1.11.x reached end of mainstream support on December 2, 2017 and it receives only data loss and security fixes until its end of life.

เพราะฉะนั้นในเคสนี้ เราเลยเหลือทางเลือกทางเดียวคือ อัพเกรด Django เลยครับจาก 1.11 กระโดดไปเป็น 2.1 ซึ่งเป็นเวอร์ชั่นล่าสุดเลย แต่เนื่องจากเราอัพเกรด Django เพราะฉะนั้น Library ที่เราใช้ที่เกี่ยวข้องกับ Django อย่างเช่น Django REST Framework, django-filter, etc. ก็เลยต้องอัพตามไปด้วย แต่โดยรวมแล้วก็ไม่มีปัญหาอะไรครับ นอกจากต้องเพิ่มอาร์กิวเม้นต์ on_delete เข้าไปใน ForeignKey ของ Django ด้วย

Freezegun

Freezegun ก็เป็น Library ตัวนึงที่เราใช้งานสูงมากครับ ช่วยให้เราเขียน Unit test ที่มีเวลาเข้ามาเกี่ยวข้องได้ง่ายขึ้นมาก เวอร์ชั่นที่เราใช้อยู่ปัจจุบันคือ 0.3.9 ครับ ซึ่งพออัพเกรดมาใช้ Python 3.7 ก็เจอปัญหาเลยว่า

AttributeError: module 'uuid' has no attribute '_uuid_generate_time'

เคสนี้ก็ยังโชคดีอยู่ครับที่ตัว Library ออก patch มาแก้ไขเรียบร้อยแล้วเพียงแค่อัพเกรดเป็น 0.3.10 ก็ไม่เจอปัญหานี้แล้วครับ แต่เคสนี้เป็นเคสที่แปลกมากอย่างนึงคือ ผมพยายามหาใน Changelog ของ Python 3.7 แต่ไม่มีที่ไหนพูดถึง Remove attribute _uuid_generate_time เลยครับ

AttributeError: module ‘uuid’ has no attribute ‘_uuid_generate_time’ on Python 3.7.0b1 · Issue #225 · spulec/freezegun
The upcoming Python 3.7 version drops the internal _uuid_generate_time function in the uuid module, which makes freezegun fail on import: Python 3.7.0b1+ (heads/3.7:7f38637853, Feb 1 2018, 13:09:53...

Dropbox

ในเคสของ Dropbox SDK ปัญหาค่อนข้างจะตรงไปตรงมาครับคือ ใน codebase ของตัว Library ยังมีตัวแปรบางตัวใช้คำว่า async ซึ่งใน Python 3.7 กลายมาเป็น Reserved keywords แล้ว อันนี้ก็อัพเดทไปใช้เวอร์ชั่นล่าสุด 9.0.0 ก็ไม่มีปัญหานี้แล้วครับ

Failure to import on Python 3.7.0b4 · Issue #145 · dropbox/dropbox-sdk-python
async is now a keyword leading to: import: 'dropbox' Traceback (most recent call last): File "/opt/conda/conda-bld/dropbox_1525570948590/test_tmp/run_test.py", line 2, in <modu...

Flake8

พอเราเปลี่ยนมาใช้ Python 3.7 ตัว Flake8 จากที่รันเงียบๆ อยู่ดีๆ ก็มี Warning เพิ่มขึ้นมาว่า

FutureWarning: Possible nested set at position 1
 EXTRANEOUS_WHITESPACE_REGEX = re.compile(r'[[({] | []}),;:]')

ซึ่งก็มีคนไปเปิด issues ไว้แล้วครับที่ Pycodestyle (ตัว Flake8 ขี่อยู่บน Library อีกสามตัวหนึ่งในนั้นคือ Pycodestyle ระหว่างนี้เราก็ได้แต่รอ Flake8 ออกเวอร์ชั่นใหม่ ก็ทนรำคาญไปอีกนิดหน่อย แต่โดยรวมแล้วก็ยังทำงานได้ปกติครับ

FutureWarning: Possible nested set for EXTRANEOUS_WHITESPACE_REGEX · Issue #728 · PyCQA/pycodestyle
When running pycodestyle 2.3.1 with Python 3.7, I get: /home/florian/proj/qutebrowser/git/.tox/flake8/lib/python3.7/site-packages/pycodestyle.py:113: FutureWarning: Possible nested set at position ...

Celery

ตัว Celery นี่เป็นเคสที่ปวดหัวที่สุดละครับ คือตัว Package หลักที่อยู่ใน PyPI ยังไม่ support Python 3.7 จนมีคนไปโวยวายเยอะมาก ว่าทำไมไม่ support ฟะ ก็น่าจะมีคนใช้เยอะนิ ซึ่งจริงๆ ใน Master branch ของตัว Library เองก็มีคนแก้ปัญหา compatibility กับ Python 3.7 แล้วนะครับ ปัญหาเล็กมากแค่เรื่อง async keywords เหมือนเคส Dropbox เลย แต่ทาง Maintainer ของ Celery ก็ไม่ยอมออก Release เล็กๆ ออกมาแก้เรื่องนี้ (คงจะรอจน release 4.3 ออกอย่างไวสุดก็เดือนหน้า) ครับ

Support `async` keyword in python 3.7 · Issue #4500 · celery/celery
Checklist I have included the output of celery -A proj report in the issue. (if you are not able to do this, then at least specify the Celery version affected). I have verified that the issue exist...

ระหว่างนี้ ถ้าอยากใช้ Celery กับ Python 3.7 ก็แนะนำให้ pip จาก GitHub ที่ commit นี้ไปก่อนนะครับจนกว่า Celery 4.3.x จะออก

pip install git+https://github.com/celery/celery.git@ced86ea58859e9f704cc781c59ea3e137b199638

ส่วนถ้าใครอยากอ่านดราม่าใน Celery ก็ติดตามได้ที่ issues นี้เลยครับ

Celery release cycle · Issue #4957 · celery/celery
There is no reasonable release cycle for Celery. If something is broken, it could be broken in stable version for a very long time, even if it's actually fixed at master. Steps to reproduce Fix...

หลังจากอัพเกรดจนทุก Library ใช้งานได้ปกติแล้ว Unit test ผ่านหมด เราก็ยังไม่ไว้ใจ 100% ครับ วันต่อมาหลังจากเรา Deploy ตัว Docker image ใหม่ที่อัพเกรดแล้วไปเครื่อง development เราทำ Manual testing ทุกฟีเจอร์ที่เกี่ยวข้องกับแอพเลย ซึ่งโชคดีที่ไม่เจอปัญหาอะไรที่ร้ายแรง หรือเกี่ยวข้องโดยตรงจากการอัพเดทครับ เราเลยอัพเกรดขึ้น Production ในบ่ายวันนั้นเลย ซึ่งก็ยังไม่เจอปัญหาอะไรมาจนถึงปัจจุบัน

Upgrade แล้วได้อะไรบ้าง

อันนี้เป็นสิ่งที่ผมนึกได้สุดท้ายก่อนที่เราจะกดอัพเกรดเครื่อง Development ครับคือพยายามเก็บ Resource ทุกอย่างว่าก่อนและหลัง Deploy เปลี่ยนไปมากแค่ไหนและข้างล่างนี่คือ สิ่งที่เกิดขึ้นครับ

จะเห็นว่า Memory Usage ลดลงอย่างเห็นได้ชัดมากๆ เกือบ 3 GB เลยทีเดียว ถึงแม้วันต่อมามันจะพุ่งมาเฉลี่ยอยู่ที่ ~5GB แต่ก็ไม่ขึ้นไปแตะ 6.75GB เหมือนก่อนอัพเกรดเลยครับ นอกจากนั้นแล้วตอนที่ดู Monitor ใน Datadog สิ่งที่เห็นอีกอย่างนึงคือ Load Average โดยรวมต่อ container ลดลงอย่างเห็นได้ชัดครับ ซึ่งค่อนข้างจะสอดคล้องกับผลที่ได้จาก Benchmark ใน Blog ข้างล่างนี้ครับ

Which is the fastest version of Python? | HackerNoon
Of course, “it depends”, but what does it depend on and how can you assess which is the fastest version of Python for your application? Is Python 3 slower than Python 2? Which version of Python 3 is the fastest and what other options do you have for speed? Using the performance suite utility The cor…

สรุป

การอัพเกรดมีความเสี่ยงนะครับ โปรดใช้ความระมัดระวังในการอัพเกรด แต่ความคุ้มค่าที่ได้มาก็คุ้มที่จะเหนื่อยอยู่ครับทั้งฟีเจอร์ใหม่ๆ ของภาษาและ Library ที่เกี่ยวข้อง

อีกอย่างที่สำคัญและอยากจะฝากไว้คือ Unit Test ครับ เราเขียน Unit Test ไม่ใช่แค่ให้มันผ่านๆ ไป แต่หัวใจของมันคือทำให้เรามั่นใจว่า ตัว Unit ของ Software เราจะยังทำงานเหมือนเดิม ถ้าเราทำการเปลี่ยนแปลงอะไรๆ ใน codebase ครับ

ก็ขอบคุณทุกคนที่อ่านมาจนถึงบรรทัดนี้ครับ หวังว่าจะเป็นประโยชน์แล้วก็ช่วยให้คนที่กำลังลังเลจะใช้ Python 3.7 สบายใจขึ้น แล้วพบกันใหม่ Entry หน้าสวัสดีครับ


Original post at: https://yothinix.medium.com/share-ประสบการณ์-upgrade-python-3-7-3f27300d966d