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又没有关掉。饶了一大圈,终于解决了。原来是前端的锅。

 

 

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