model repository項目踩坑記錄

一、model repository介紹

1、開發工具

  • python3.7
  • svn
  • bash腳本
  • gunicorn
  • fastapi
  • sqlalchemy
  • docker
  • k8s
  • git
  • mysql

2、model repository是什麼?

model repository是一個類似於github的模型版本管理網站,實現以項目爲單位的現在協作的模型開發,主要實現的功能有:

  • 模型版本管理(pygit2)
  • 用戶權限管理(涉及多種權限,pycasbin)
  • 項目管理
  • 模型管理
  • 模型版本管理
  • 模型註冊
  • 成員管理
  • 消息中心(mysql+websocket+異步)
  • 用戶權限申請

使用model repository。首先用戶會被賦予訪問的權限,權限分爲網站admin,擁有所有權限,也是用來創建用戶以及創建項目的角色。在用戶創建了項目會指定相應的此項目下的admin管理員,我們稱此爲p-admin。那麼p-admin就擁有次個項目下的所有權限,那麼p-admin就可以去管理此項目,爲項目指定dev用戶、普通用戶、管理員用戶,創建模型、上傳模型版本等操作,即可組織相應的開發人員進行項目開發工作。

3、model repository作用是什麼?

model repository其實可以理解爲一個服務於多項目、多用戶等代碼開發管理的git平臺。每個項目的開發是隔離的,大家互不影響且不能相互訪問,當然一個用戶可以擔任多個項目的角色,這時候這個用戶就具備了訪問多個項目的權限。爲了靈活性,也可以臨時提高用戶權限,將某個資源針對某個人開放,這裏的資源是指的倉庫中任何一個文件或者路徑。

4、技術難點

4.1、權限管理

在設計中,我們要對網站的每一個路徑、每一個文件都可以進行控制,意思就是我們把某一類資源甚至某一個資源決定是否開放給某一個用戶或者某一類用戶。這裏使用的pycasbin,基於RBAC模式。總體來說pycasbin還是比較好用的,只是後期隨着用戶量的增大,以及網站資源的增多,數據庫存儲的數據增速會越來越大。

權限管理還是有很多概念需要去了解的。有興趣可以再循着pycasbin這條線多去查詢一下相關知識。

4.2 版本管理

版本管理的對象主要有:模型代碼版本的管理、以及模型說明文檔的管理。目標是:用戶可以上傳任意多的模型版本、文檔版本,我們會給用戶記錄下所有版本,並且在頁面上行程版本歷史,供用戶下載。當然這其中穿插了權限管理,那些人可以上傳、那些人可以下載等。

這個版塊實現使用pygit2。pygit2是一個直接與git打交道的python的包,他提供了很多api可以供我們使用。將用戶上傳的文件存儲在git中,以及從git中遍歷用戶的上傳歷史。但是有一點比較坑:就是畢竟是別人寫的api,在使用的時候,若個性化的要求比較多,必然會造成api使用起來不方便,我們在開發中就遇到了一些坑,最後對api進行了重構、代理才解決。

4.3 fork功能以及消息中心

fork功能其實就是用來將某個資源開放給某個用戶來設計的,比如,隔壁項目的一個成員需要用到本項目的某個模型作爲參考,這時候他可以提交一個fork申請,本項目的管理員收到申請後,就可以賦予其fork的權限了。

但是這一切都是要通過消息中心的,因爲它涉及消息的交互。

消息中心爲了做到實時性,採用websocket的形式,所有登錄的用戶全部連接到wenbcket中,然後由掃面mysql的協程程序向用戶推送消息。如下所示:

 

4.4 模型版本註冊

這個功能不是我來做的,但是知道其中的原理。

模型註冊達到的目的是什麼?

當開發者把模型做出來以後,並且有了很多的測試數據後,就可以得到相應的測試結果。但是這些怎樣呈現給用呢?直接給他看預測的數據,估計對方會一點懵逼,這時候模型註冊的功能就被做出來了。按照模型預測中常用的圖以及輸入的參數(當然不同的模型的參數是不同的,這些在網站都有詳細的規定),待開發者導入後,模型註冊後會生成一個頁面,這個頁面將預測結果動態的顯示出來,用戶通過調整參數,非常直觀的理解模型。

 

二、開發中遇到的坑

1、websocket自動斷開後報錯,捕捉異常

由於websocket斷開後會報錯,後端的日誌上打印出很多的錯誤信息。計劃捕捉到這種websocket.exception.ConnectionCloseEroor。

方案:使用fastapi提供的exception_handler裝飾器自定義一個異常處理方法,用作全局捕捉websocket斷開異常。編寫方式如下:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from websockets.exceptions import ConnectionClosedError

app = FastAPI()

@app.exception_handler(ConnectionClosedError)
async def handle_ws_close_exc(request: Request, exc: ConnectionClosedError):
    logging.warning("webscoket closed")

坑:發現使用這個全局的handler,怎麼也捕捉不到websoket斷開的異常。於是查看源碼,發現handler明明註冊到了middleware中了,但是就是不知道爲什麼?

後來自己寫了一些小的demo,發現都是可以捕獲的。

最後發現:發送的http異常,都是可以捕獲的,但是websocket異常通過fastapi自帶的exception_handler是無法捕捉到的。

然後,就換了方案,改寫了websocket一個裝飾器方法,在裏面加入了捕捉ConnectionCloseEroor的邏輯,並且將其打印到warnning級別的日誌當中,不再報錯。

2、git自動去重踩坑記

在使用pygit控制git倉庫時,當兩次提交的文件內容完全一致時。 git中並不會重新新建一個文檔來存儲這個文件,但是他會生成一個新的commit_id,來指向舊的commit。在我們提取版本記錄時,其實pygit的接口已經對這種不同commit_id指向相同文件的情況做了去重處理。

所以前端頁面會出現怪異的情況,那就是我明明上傳了一個版本(和上個版本相同的版本),也顯示上傳成功了,但是顯示版本記錄時卻沒有這條信息,自然認爲是系統出了bug。

 

解決問題:

第一個方案:在用戶上傳文檔後,我在存入倉庫後,與它以前的所有版本內容做dif比較,如果出現了相同版本,則返回給前端信息,提示用戶上傳失敗:原因是此版本和上個版本重複了。

但是這種方案有個很大的問題:針對於小文件是沒問題,基本上點擊了上傳按鈕就能返回是否上傳失敗的信息;但是對於model_dist這種,一個文件就幾百兆,用戶花了一分鐘上傳上去了,你告訴用戶上傳失敗了,原因是和上一個版本相同。體驗不好。並且需求方也傾向於:你不要管我上傳什麼樣的版本,我上傳重複的我願意,你給我顯示就好了。

 

第二個方案:

不用官方提供的log接口了,自己去實現一個。難點:我們得到所有的commit對象後,如何確定這個commit對應的是哪個文件提交時生成的commit對象,因爲pygit中的commit對象是隻會記錄commit信息的,如message、author、commit_time這些。

解決方案:把commit的message給結構化。message不再是隻存用戶的commit_message,而是會存入一個json字符串。包含commit_message以及entry,而這個entry會指向相應的文件路徑,就解決了commit無法指向對應的文件的問題。

這個大問題解決了,還有一個小問題,由於我們的版本控制邏輯都是在versionCtrol文件下實現的,我們把commit_message結構修改了,那麼上層端口拿到commit對象時,使用commit.message卻發現多了一個entry字符串,這是不好的。所以我們要保證,commit這個對象對於上層用戶來講,使用起來要和以前是一樣的,上層使用時,我得commit.message就是傳我得commit信息,entry什麼的我不管;我用commit.message就是得到我當初提交的commit信息。

解決方法:寫了一個類,commit_proxy,用來代理commit對象,外界訪問的commit對象,其實就是我這個commit'_proxy

寫法如下:

class CommitProxy:
    def __init__(self, commit):
        self.commit = commit
        structured = StructuredCommitMessage.parse_raw(commit.message)  #生成一個格式化信息對象
        self.message = structured.provided   #provided指的用戶當時提交的commit信息
        self.meta_for_artifact = structured.meta_for_artifact   #meta_for_artifact即entry
    def __getattr__(self, name):
        return getattr(self.commit, name) 

這個類寫的非常巧妙,commit的屬性代理到commit對象了。

 

 

3、websocket斷開後自動重連報錯踩坑

這個鍋其實歸結爲前端,主要是有兩個方面的影響:刪除用戶後不同報錯,mysql連接超額

 

先說第一個影響吧,主要體現在測試環境下。由於前期我們要經常修復bug,對代碼做一些修改,因此數據庫裏面存的數據就會不符合要求,我們就需要進行清理數據庫的操作,這時候就會把user表也清理掉,這時候問題就出現了。

由於ws的機制是,我如果發現斷開了,我就一直重連,直到連接上才罷休。那麼我把user表清空後,ws連接時,就會出現沒有權限,因此就斷開了,但是斷開了,我就一直重連,導致後臺就會一直刷ws重新無權限的信息。

這時候肯定有人問,ws檢測到無權限就讓返回登錄界面不就ok了?對的,我開始也是這麼想的,並且在http請求中我是這麼做的,沒有問題,用戶無權限後,就直接跳轉到登陸界面了。但是我們現在面對的是ws,和前端對接後,它告訴我,你給我一個狀態碼,我不能重新跳到login界面。

於是換了一種方案:斷開後,嘗試連接20次,如果仍然無法連接,就不再重連,當用戶刷新頁面,會重新嘗試連接ws。

 

第二個坑:

這個坑讓我費了很的勁。

因爲大家在測試時,經常會報錯: queue pool size limit 0f 5 overflow 10,意思就是說mysql的連接數量超額了。第一反應是,是不是我那裏用了session後,沒close,於是找遍了所有代碼用到的session的地方,發現沒問題,因爲我都是用的with session上下文,最後都是close掉了。

第二反應,是不是queue pool size設置太小了,於是到網上查,果然有人說模型的pool只有5個大小,他們很多都設置到了100,於是乎增大了poolsize。哇塞,效果不錯,沒事了。可但是過了一會又報這個錯誤了。這就說明了根本不是poolsize的問題,有些連接肯定沒斷開,持續的佔用的,纔會到這個問題。

第三反應,直接登錄到mysql的服務器,去看看到底是那些連接一直存在。我發現隨着刷新頁面,連接越來越來多,最後就超額了。但是呢,我在前端頁面訪問普通的sql查詢時就不會多連接。這讓我很奇怪。

第四反應:使用python的stack方法,去記錄當發生poolsize超額時,到底是哪些連接還一直存在,是誰調用的這個連接。然後就找到了代碼,全都是消息中心裏面的代碼佔用着這些鏈接。

第5反應:我刷新就會有新連接,而直接點擊查詢的路由,就不會發生這種情況。然後我去找了前端,

問:刷新時,你是不是操作了websocket的連接。

答:是的

問:那你不是一刷新,就新建的了一個ws的連接,但是原來的ws連接還沒有關掉

答:不知道耶,稍後確認回覆你。

 

最終,果然是前端ws直接創建了一個新的,舊的ws又沒有關掉。饒了一大圈,終於解決了。原來是前端的鍋。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章