Analisis statis volume besar kode Python: pengalaman Instagram. Bagian 1

Kode server Instagram ditulis secara eksklusif dalam Python. Yah, pada dasarnya itu. Kami menggunakan sedikit Cython, dan dependensi menyertakan banyak C ++ - kode yang dapat dioperasikan dari Python seperti halnya dengan C-extension.



Aplikasi server kami adalah monolith, yang merupakan satu basis kode besar, yang terdiri dari beberapa juta baris dan termasuk beberapa ribu titik akhir Django (di sini adalah presentasi tentang penggunaan Django di Instagram). Semua ini dimuat dan disajikan sebagai satu kesatuan. Beberapa layanan telah dialokasikan dari monolit, tetapi rencana kami tidak termasuk pemisahan monolit yang kuat.

Sistem server kami adalah monolith yang sangat sering berubah. Setiap hari, ratusan pemrogram membuat ratusan kode yang dikomit. Kami terus menerapkan perubahan ini, melakukan ini setiap tujuh menit. Akibatnya, proyek ini dikerahkan dalam produksi sekitar seratus kali sehari. Kami berusaha keras untuk memastikan bahwa kurang dari satu jam berlalu antara mendapatkan komit ke cabang master dan menyebarkan kode yang sesuai dalam produksi (di sini adalah pembicaraan tentang ini dibuat di PyCon 2019).

Sangat sulit untuk mempertahankan basis kode monolitik yang besar ini, membuat ratusan komit setiap hari, dan pada saat yang sama tidak membawanya ke keadaan kekacauan total. Kami ingin menjadikan Instagram tempat para pemrogram bisa produktif dan dapat dengan cepat menyiapkan fitur-fitur baru yang bermanfaat dari sistem.

Materi ini berfokus pada bagaimana kami menggunakan linting dan refactoring otomatis untuk membuatnya lebih mudah untuk mengelola basis kode Python.

Jika Anda tertarik untuk mencoba beberapa ide yang disebutkan dalam materi ini, maka Anda harus tahu bahwa kami baru-baru ini ditransfer ke kategori proyek sumber terbuka LibCST , yang mendasari banyak alat internal kami untuk penyaringan dan refactoring kode otomatis.

Bagian kedua

Linting: dokumentasi yang muncul jika diperlukan


Linting membantu programmer menemukan dan mendiagnosis masalah dan antipattern yang mungkin tidak diketahui oleh pengembang sendiri tanpa memperhatikannya dalam kode. Ini penting bagi kami karena fakta bahwa ide-ide yang relevan mengenai desain kode semakin sulit untuk didistribusikan, semakin banyak programmer yang mengerjakan proyek. Dalam kasus kami, kami berbicara tentang ratusan spesialis.


Varietas linting

Linting hanyalah salah satu dari banyak jenis analisis kode statis yang kami gunakan di Instagram.

Cara paling primitif untuk menerapkan aturan linting adalah dengan menggunakan ekspresi reguler. Ekspresi reguler mudah ditulis, tetapi Python bukan bahasa "biasa" . Akibatnya, sangat sulit (dan kadang-kadang tidak mungkin) untuk secara andal mencari pola dalam kode Python menggunakan ekspresi reguler.

Jika kita berbicara tentang cara paling rumit dan canggih untuk mengimplementasikan linter, maka ada alat seperti mypy dan Pyre . Ini adalah dua sistem untuk memeriksa jenis kode Python secara statis yang dapat melakukan analisis program yang mendalam. Instagram menggunakan Pyre. Ini adalah alat yang ampuh, tetapi sulit untuk diperluas dan dikustomisasi.

Ketika kita berbicara tentang linting di Instagram, kita biasanya bermaksud bekerja dengan aturan sederhana berdasarkan pohon sintaksis abstrak. Justru sesuatu seperti ini yang mendasari aturan linting kita sendiri untuk kode server.

Ketika Python mengeksekusi modul, itu dimulai dengan memulai parser dan meneruskan kode sumbernya. Berkat ini, pohon parse dibuat - semacam pohon sintaksis beton (CST). Pohon ini adalah representasi tanpa kehilangan dari kode sumber input. Setiap detail disimpan di pohon ini, seperti komentar, tanda kurung dan koma. Berdasarkan CST, Anda dapat sepenuhnya mengembalikan kode asli.


Python Parse Tree (variasi CST) yang dihasilkan oleh lib2to3

Sayangnya, pendekatan semacam itu mengarah pada penciptaan pohon yang kompleks, yang membuatnya sulit untuk mengekstrak informasi semantik yang menarik bagi kita darinya.

Python mengkompilasi pohon parse menjadi pohon sintaksis abstrak (AST). Beberapa informasi tentang kode sumber hilang selama konversi ini. Kita berbicara tentang "informasi sintaksis tambahan" - seperti komentar, tanda kurung, koma. Namun, semantik kode dalam AST dipertahankan.


Pohon sintaksis python abstrak yang dihasilkan oleh modul ast

Kami mengembangkan LibCST - perpustakaan yang memberi kami yang terbaik dari dunia CST dan AST. Ini memberikan representasi kode di mana semua informasi tentang itu disimpan (seperti dalam CST), tetapi mudah untuk mengekstrak informasi semantik tentangnya dari representasi kode tersebut (seperti ketika bekerja dengan AST).


Representasi pohon sintaks LibCST tertentu

Aturan linting kami menggunakan pohon sintaks LibCST untuk menemukan pola dalam kode. Pohon sintaks ini, pada tingkat tinggi, mudah dijelajahi, memungkinkan Anda untuk menyingkirkan masalah yang menyertai pekerjaan dengan bahasa "tidak teratur".

Misalkan dalam modul tertentu ada ketergantungan siklik karena jenis impor. Python memecahkan masalah ini dengan menempatkan ketik perintah impor di blok if TYPE_CHECKING . Ini adalah perlindungan terhadap impor apa pun saat runtime.

 #    from typing import TYPE_CHECKING from util import helper_fn #    if TYPE_CHECKING:    from circular_dependency import CircularType 

Kemudian, seseorang menambahkan impor jenis lain dan lainnya if memblokir ke kode. Namun, orang yang melakukan ini mungkin tidak tahu bahwa mekanisme seperti itu sudah ada dalam modul.

 #    from typing import TYPE_CHECKING from util import helper_fn #    if TYPE_CHECKING:    from circular_dependency import CircularType if TYPE_CHECKING: #   !    from other_package import OtherType 

Anda dapat menyingkirkan redundansi ini menggunakan aturan linter!

Mari kita mulai dengan menginisialisasi penghitung blok "pelindung" yang ditemukan dalam kode.

 class OnlyOneTypeCheckingIfBlockLintRule(CstLintRule):    def __init__(self, context: Context) -> None:        super().__init__(context)        self.__type_checking_blocks = 0 

Kemudian, memenuhi kondisi yang sesuai, kami menambah penghitung, dan memeriksa bahwa tidak akan ada lebih dari satu blok dalam kode. Jika kondisi ini tidak terpenuhi, kami menghasilkan peringatan di tempat yang sesuai dalam kode, memanggil mekanisme tambahan yang digunakan untuk menghasilkan peringatan tersebut.

 def visit_If(self, node: cst.If) -> None:    if node.test.value == "TYPE_CHECKING":        self.__type_checking_blocks += 1        if self.__type_checking_blocks > 1:            self.context.report(                node,                "More than one 'if TYPE_CHECKING' section!"            ) 

Aturan linting yang serupa bekerja dengan melihat pohon LibCST dan mengumpulkan informasi. Dalam linter kami, ini diterapkan menggunakan pola Pengunjung. Seperti yang mungkin telah Anda perhatikan, aturan menimpa metode visit dan meninggalkan metode yang terkait dengan jenis simpul. "Pengunjung" ini dipanggil dalam urutan tertentu.

 class MyNewLintRule(CstLintRule):    def visit_Assign(self, node):        ... #      def visit_Name(self, node):        ... #        def leave_Assign(self, name):        ... #      


Metode kunjungan dipanggil sebelum mengunjungi keturunan node. Metode cuti dipanggil setelah mengunjungi semua keturunan

Kami mematuhi prinsip kerja, sesuai dengan tugas sederhana yang diselesaikan terlebih dahulu. Aturan linter pertama kami sendiri diterapkan dalam satu file, berisi satu "pengunjung" dan menggunakan status bersama.


Satu file, satu "pengunjung", menggunakan status bersama

Kelas Single Visitor harus memiliki informasi tentang keadaan dan logika semua aturan penyatuan kami yang tidak terkait dengannya. Selain itu, tidak selalu jelas negara mana yang sesuai dengan aturan tertentu. Pendekatan ini menunjukkan dirinya dengan baik dalam situasi di mana ada beberapa aturan Anda sendiri, tetapi kami memiliki sekitar seratus aturan ini, yang sangat menyulitkan dukungan pola single-visitor .


Sulit untuk mengetahui keadaan dan logika apa yang dikaitkan dengan masing-masing cek.

Tentu saja, sebagai salah satu solusi yang mungkin untuk masalah ini, orang dapat mempertimbangkan definisi dari beberapa "pengunjung" dan organisasi dari skema kerja sedemikian rupa sehingga masing-masing dari mereka akan melihat seluruh pohon setiap kali. Namun, ini akan menyebabkan penurunan serius dalam produktivitas, dan linter adalah program yang harus bekerja dengan cepat.


Setiap aturan linter berulang kali dapat melintasi pohon. Saat memproses file, aturan dijalankan secara berurutan. Namun, pendekatan ini, yang sering melintasi pohon, akan menyebabkan penurunan kinerja yang serius.

Alih-alih menyadari sesuatu seperti ini, kami terinspirasi oleh linter yang digunakan dalam ekosistem bahasa pemrograman lain - seperti ESLint dari JavaScript, dan membuat daftar "pengunjung" terpusat (Visitor Registry).


Daftar "pengunjung" terpusat. Kita dapat secara efektif menentukan simpul mana yang tertarik pada setiap aturan linter, menghemat waktu pada node yang tidak tertarik padanya.

Ketika aturan linter diinisialisasi, semua menimpa metode aturan disimpan dalam registri. Ketika kita berkeliling pohon, kita melihat semua "pengunjung" yang terdaftar dan memanggil mereka. Jika metode ini tidak diterapkan, itu berarti Anda tidak perlu memanggilnya.

Ini mengurangi konsumsi sistem sumber daya komputasi ketika aturan linting baru ditambahkan padanya. Kami biasanya memeriksa dengan sedikit file yang baru saja dimodifikasi. Tetapi kita dapat memeriksa semua aturan di seluruh basis kode server Instagram secara paralel hanya dalam 26 detik.

Setelah kami menyelesaikan masalah kinerja, kami menciptakan kerangka pengujian yang ditujukan untuk mematuhi teknik pemrograman tingkat lanjut, yang memerlukan pengujian dalam situasi di mana sesuatu harus memiliki kualitas dan dalam situasi di mana sesuatu tidak harus memiliki kualitas seharusnya.

 class MyCustomLintRuleTest(CstLintRuleTest):    RULE = MyCustomLintRule       VALID = [        Valid("good_function('this should not generate a report')"),        Valid("foo.bad_function('nor should this')"),    ]       INVALID = [        Invalid("bad_function('but this should')", "IG00"),    ] 

Lanjutan → bagian kedua

Pembaca yang budiman! Apakah Anda menggunakan linter?


Source: https://habr.com/ru/post/id473766/


All Articles