針對 Python SDK 的效能驅動型執行階段型別檢查

在這篇部落格文章中,我們將宣布即將發布 Beam Python SDK 的全新、可選擇加入的執行階段型別檢查系統,該系統針對開發和生產環境中的效能進行了最佳化。

但讓我們退一步想想 - 我們為什麼首先要在乎執行階段型別檢查?讓我們來看一個範例。

class MultiplyNumberByTwo(beam.DoFn):
    def process(self, element: int):
        return element * 2

p = Pipeline()
p | beam.Create(['1', '2'] | beam.ParDo(MultiplyNumberByTwo())

在這個程式碼中,我們將字串列表傳遞給一個明顯用於整數的 DoFn。幸運的是,此程式碼會在管道建構期間拋出錯誤,因為 beam.Create(['1', '2']) 的推斷輸出類型為 str,這與宣告的 MultiplyNumberByTwo.process 輸入類型 int 不相容。

但是,如果我們使用 no_pipeline_type_check 標誌關閉管道型別檢查會怎麼樣?或者更實際地說,如果 MultiplyNumberByTwo 的輸入 PCollection 來自資料庫,這意味著輸出資料類型只能在執行階段才知道,那又會如何呢?

在這兩種情況下,管道建構期間都不會拋出錯誤。即使在執行階段,此程式碼也能運作。每個字串都會乘以 2,產生 ['11', '22'] 的結果,但這肯定不是我們想要的結果。

那麼,您如何偵錯這種「隱藏」的錯誤?更廣泛地說,您如何在 Beam 中偵錯任何型別或序列化錯誤?

答案是使用執行階段型別檢查。

執行階段型別檢查 (RTC)

此功能的工作方式是在管道執行期間檢查實際的輸入和輸出值是否滿足宣告的型別限制。如果您之前執行了 runtime_type_check 開啟的程式碼,您將收到以下錯誤訊息

Type hint violation for 'ParDo(MultiplyByTwo)': requires <class 'int'> but got <class 'str'> for element

這是一個可操作的錯誤訊息 - 它告訴您,您的程式碼有錯誤,或者您宣告的型別提示不正確。聽起來很簡單,那陷阱是什麼?

它太慢了。 親自看看。

元素大小一般管道執行階段型別檢查管道
15.3 秒5.6 秒
2,0019.4 秒57.2 秒
10,00124.5 秒259.8 秒
18,00138.7 秒450.5 秒

在這個微型基準測試中,執行階段型別檢查的管道速度慢了 10 倍以上,並且隨著輸入 PCollection 大小的增加,差距只會越來越大。

那麼,有任何生產友善的替代方案嗎?

效能執行階段型別檢查

有的!我們開發了一個新的標誌,稱為 performance_runtime_type_check,它使用以下組合將其對管道時間複雜度的影響降到最低

  • 高效的 Cython 程式碼、
  • 智慧抽樣技術和
  • 最佳化的巨型型別提示。

那麼,新數字看起來如何?

元素大小一般RTC效能 RTC
15.3 秒5.6 秒5.4 秒
2,0019.4 秒57.2 秒11.2 秒
10,00124.5 秒259.8 秒25.5 秒
18,00138.7 秒450.5 秒39.4 秒

平均而言,新的效能 RTC 比一般管道慢 4.4%,而舊的 RTC 則慢了 900% 以上!此外,隨著輸入 PCollection 大小的增加,設定效能 RTC 系統的固定成本會分散到每個元素,從而降低對整體管道的相對影響。當有 18,001 個元素時,差異小於 1 秒。

它是如何運作的?

效能的升級有三個關鍵因素。

  1. 我們不檢查所有值的型別,而只檢查一部分值的型別,這在統計學中稱為樣本。最初,我們會抽樣大量元素,但隨著我們對元素類型不會隨時間變化的信心增加,我們會降低抽樣率(達到固定的最小值)。

  2. 舊的 RTC 系統使用繁重的包裝函式來執行型別檢查,而新的 RTC 系統則將型別檢查移至程式碼庫中經過 Cython 最佳化、未裝飾的部分。作為參考,Cython 是一種程式語言,可以為 Python 程式碼提供類似 C 的效能。

  3. 最後,我們使用單一巨型型別提示來檢查轉換的輸出值,而不是分別檢查輸入和輸出值。此巨型型別提示由原始轉換的輸出型別限制以及所有消費者轉換的輸入型別限制組成。使用此巨型型別提示可以減少額外負荷,同時讓我們拋出更多可操作的錯誤。例如,請考慮以下錯誤(從舊的 RTC 系統產生)

Runtime type violation detected within ParDo(DownstreamDoFn): Type-hint for argument: 'element' violated. Expected an instance of <class ‘str’>, instead found 9, an instance of <class ‘int’>.

此錯誤告訴我們 DownstreamDoFn 收到一個 int,但它預期的是一個 str,但沒有告訴我們是誰首先建立了這個 int。誰是產生此 int 的違規上游轉換?據推測,轉換的輸出型別提示過於廣泛(例如 Any),否則不存在,因為其輸出的執行階段型別檢查期間未拋出任何錯誤。

此處的問題歸結為缺少上下文。如果我們在檢查輸出型別時知道我們的消費者是誰,我們可以同時檢查我們的輸出值是否符合我們的輸出型別限制,以及每個消費者的輸入型別限制,以了解是否存在任何不符的可能性。這正是巨型型別提示所做的,它可以讓我們在宣告點而不是例外點拋出錯誤,從而在提供更高品質錯誤訊息的同時,節省您寶貴的時間。

那麼,使用效能 RTC 的相同錯誤會是什麼樣子?它與舊的錯誤訊息完全相同,但多了一行

[while running 'ParDo(UpstreamDoFn)']

這對於調查來說更具可操作性:)

後續步驟

去玩玩新的 performance_runtime_type_check 功能吧!

它處於實驗狀態,因此如果您遇到任何問題,請告訴我們