2020年5月2日 星期六

python與node.js RPC溝通 - 淺談zerorpc的一些坑

這一年玩Electron有做了幾個side project, 程式都是python(主功能) + electron (javacript, UI)架構:


  1. ChaldeaStockObservatory - 美股交易輔助工具
  2. PhotoMosaic-Artifact - 製作蒙太奇照片工具

ChaldeaStockObservatory是python作為RPC server端, UI會定時RPC到core server請求追蹤的股價以及條件策略; 而PhotoMosaic-Artifact則相反, 當UI端設定好要轉換的圖片以及相關素材後, 會啟動RPC server並叫起python程序執行工作, python端程序會持續送目前的進度狀態給UI端的RPC server。

因為都是很簡單的小應用, RPC溝通方面也不用太講究, 所以當初是選最簡單易用的zerorpc作為RPC溝通機制, 不過最近再做新的side project, POC階段卻踩到了zerorpc不少雷:

  1. 每次發RPC remote call到執行完回來會花5秒的時間才回來。
  2. 要是server端的remote function執行超過10秒, client端會跳Error: Lost remote after 10000ms的Error
  3. 用node.js的child_process跑python並執行python script, zerorpc server有機會掛掉。
因為這次想做的東西不像之前那樣簡單溝通就好, 必須讓UI跟core程序能即時溝通回饋給使用者顯示, 所以會需要即時反應core程序的結果, 所以才踩到上述的雷..., 以下是踩雷的分析結果:


1. 每次發RPC remote call到執行完回來會花5秒的時間才回來:

這原因是因為zerorpc的rpc溝通是以ZeroMQ為基底, 而zeromq的架構有個heaerbeat機制, 這個heartbeat機制可以確保目前的MQ server跟client目前是可連接的, 而zerorpc有個config heartbeatInterval預設是5秒, 每5秒才會檢查一次是否有message回來, 基本上有兩個方法可解:

  1. heartbeatInterval調小:
    只要將這個值調小就可以讓RPC call更早回來, 不過會更容易踩到其他雷, 這個等下會提到。
  2. 額外開個thread定期幫你做heartbeat動作:
    你可以不改heartbeatInterval, 直接多開一個thread定時幫你送heartbeat, 這樣rpc call就會在heartbeat後一併回來, 只要你的heartbeat thread開越快, 你的rpc call就會越及時。

 2. 要是server端的remote function執行超過10秒, client端會跳Error: Lost remote after 10000ms的Error:

前面提到zerorpc的heartbeatInterval預設是5秒, 而zerorpc也有自己的timeout機制, 而除了既有的timeout以外, heartbeat也有自己的timeout (_heartbeatExpirationTime), 這個值並沒有config化, 它是直接hardcode heartbeatInterval的兩倍 (10秒), 只要你的rpc function執行超過10秒, 就會跳這個error出來, 所以如果你的rpc function的工作是CPU bound類型, 那就很容易就碰到這個error, 而如果你之前為了加快rpc call回來的時間把heartbeatInterval調小, 那也會很容易踩到這個雷。 至於解決方法有以下:

  1. 把heartbeat設成none, 不啟動heartbeat功能(不推), 基本上最好不要, 因為一但沒有heartbeat機制, 會造成你沒辦法區分到底是rpc掛了, 還是你的rpc function還在跑, 這樣後續的error handling會超難處理。
  2. 把heartbeat改成超長, 這樣_heartbeatExpirationTime也會跟著超長, 而heartbeat太慢的問題可以靠(1.2)的另開heartbeat thread解決, 不過這方法也不推, 因為這樣當rpc掛掉的時候, 你會超長時間才看到error, 判斷到error以及handle的時間會被拉更遠。
  3. 直接改zerorpc的code, 把zerorpc channel的_heartbeatExpirationTime直接改成你要的數字, 不過也超級不推, 因為除了有上述(2.2)的問題外, 你之後要update library也都會被洗掉, 你要額外花effort去maintain它會非常累。
  4. 拆解你的長時間rpc function, 讓它定時做gevent.sleep(0), 這樣zerorpc就會有餘裕做heartbeat。 這方法其實超級瞎(有點像你在UI thread跑CPU bound的工作, 然後為了不讓使用者感覺UI hang所以不時的讓它處理UI工作, 那幹嘛不多開個thread做CPU bound的工作就好了...), 也是非常的不推薦。
  5. 依照zerorpc的特性設計自己的rpc function, rpc function不論是CPU bound還是IO bound, 本來就不該處理花太長時間的工作, 所以就是得自己根據需求設定rpc function, 不花資源快速的工作就照舊用它的sync rpc function, 而長時間的工作如果要簡單做就自己做個task queue開thread去處理, client端打完rpc call會拿到的task id, 在定時去query看什麼時候任務完成在把結果取回來; 而複雜做的話就是做成micro service, 你的rpc server只做dispatch task, 所有其他任務都是由其他對應的process去負責完成。
講那麼多其實真正的解法就是只有(5), (1~4)基本上你拿來幹一定之後一堆雷, 我自己也是簡單做成async task去執行花時間的工作。


3. 用node.js的child_process跑python並執行python script, zerorpc server有機會掛掉:

我自己在開發環境測試時, 用node.js的child_process.spawn()運行python script作為子程序, 如果remote rpc call太頻繁(100ms 以下), 會有機率造成Server端異常, client會再也無法RPC到server, 只能重啟server process, 這問題最玄的是如果不用node.js的child_process運行python而是直接開另一個terminal run python, 或是用node.js的child_process直接運行pyInstaller build好的exe, 就可以正常運行不管打再快都沒問題..., 這個我還真的是黑人問號...., 完全沒任何sense, 可是既然測試結果是發佈版不會有任何問題, 那我也不想花時間繼續找原因了, 畢竟發佈版不會有問題啊...(催眠自己)。


在來聊聊為什麼踩了上面那麼多雷還要繼續用zerorpc, 其實nodejs跟python rpc溝通還有不少library, 像是Thrift (Facebook開發, 現在由Apache軟體基金會接手), gRPC (Google), etc...。 最主要原因在於上述這兩個library都必須開rpc function interface spec, 在用他們的tool編譯成對應軟體的library才能使用, 而zerorpc則是不需要做這些直接支援動態rpc function, 這樣我想加個rpc function就不需要寫spec, 編譯, import library這些動作(當然這些可以用script自動化, 可是寫這個script又要花時間...), 另外一個理由就是不甘心吧, 想找出為什麼只有我踩到這麼多雷的原因, 可是真的去找後認真說zerorpc的文件超少, 官方網站(https://www.zerorpc.io/)提供的Document竟然是PyCon的Video以及Note..., 所以其實真的要找原因只能去啃zeromq的資料, 明明是為了簡單使用rpc才摸zerorpc, 實際上操作真的超簡單, 可是拆地雷竟然得去看zeromq的資料..., 這世界也太變態了吧!!

這次的分享大概就到這裏, 希望能幫到未來想用zerorpc去做python與node.js RPC溝通的人(還是反過來變成勸世文?), 最後照慣例分享目前正在填坑的動畫圖, 這就是每文一貼吧! (為了這個發文的動力大概佔了9成有XD)

最近開始填エロマンガ先生的動畫坑, 會填這坑是因為OP跟ED那時看了超香, 根本把宅宅的喜好通通包進去了, 沒想到拖到現在才入坑, 不過也完全不辜負我的期待, 動畫做超讚!!





附上歡樂的OP & ED XD



沒有留言:

張貼留言