Skip to content

Latest commit

 

History

History
144 lines (62 loc) · 5.95 KB

ParserArch.md

File metadata and controls

144 lines (62 loc) · 5.95 KB

设计目标

这是一个基础的解析架构(Parser-Architecture)。

前端有主要有两个相关功能:上传文件,然后触发后端解析功能,同时能够更新解析状态。

其中,后端的解析时间大概为20s/file,而且内存占用率比较高(一些模型文件等),CPU密集型计算

注意:这里前后端的交互是通过Thrift-RPC的。

变更历史

## version_0

    最初是从zhenqi那边接手过来的后端逻辑(应该也是abiao的后台逻辑)。大体设计如下:

    1) 前端upload文件,然后通过thrift接口call(json-interface)

    2) 后端thrift是一个threadServer,interface中接收到json_request,之后做了一个基本的

       format校验(尝试json_request_str能否decode成功),然后就起了一个解析子进程P1(multiprocess.Process) 同时返回res.

    3) 因为上面那个P1进程并没有join,因此,对于这个接口来说是一个异步的。P1进程是解析的耗时操作,

       每一步的解析结果都会写入DB,然后前端通过定时拉取数据库来同步状态。

    那这个设计有什么问题呢?
   
    答:没有并发控制。我在第一次做压力测试时,一下上传了200个文件,直接把服务器压挂了。
       
    然后我就想当然的说,那好办,搞个ThreadPoolServer。其实根本没解决问题。因为P1是异步的,
       
    一个线程接收到请求之后,立即fork了一个process进行异步处理,然后工作线程就收工,继续奔赴

    下一个战场了。所以线程池里面的线程数根本没有起到控制并发的作用。(这个我后面也会提到,就是

    我模糊的那个元问题。)
    
 ## version_1   
     
    然后我傻不愣登的想到一个神奇的方案。既然线程数控制不了异步P1,我就把P1的异步取消(添加join())

    这样的话,我不就能通过线程池大小来控制并发量了吗?

    是的,没错,肯定是这样的。

    然后,我就稍微勾划了一下方案,跟LongGe 和 huanjun去兜售,说要改,要这么改。

    但是,这里有个问题啊,你是爽了,前端呢?

    原来是异步接口,前端调用完之后,直接就返回。现在你要改成同步的,前端岂不是要被拖成狗?

    而且本来后面的流程就是异步的(往数据库里写)

    其实,我这么想的原因,还是因为那个元问题,我天真的:“非阻塞”<=>异步,“阻塞”<=>同步。

    所以,当我认为要取消 异步时,就等于 取消了 “非阻塞”。
   
    其实,在此之前,我跟LongGe聊的时候,LongGe还提醒我在服务侧加一个状态控制(有多少P1在跑)

    我当时还一本正经的强词夺理,说 不行啊,用的是Thrift,我没办法控制多线程的调度,除非我在

    Thrift那一层改啥的。。。。。。too young too simple,naive!

 ## version_2

    当我意识到我的问题时,是在我将要修改代码的时候。我跟LiGe聊了回天,我简单了说了我的方案,

    LiGe说前端最好不要用同步接口,他提到了他的RESTful(flask) + redis的架构。

    我当时想,如果不用thrift的话,我应该也是这么设计的,即 MQ + MultiProcess/MultiThread.

    我当时想,要不试试thrift的NoBlockingServer?

    从这时我突然意识到,不对,NoBlocking跟异步/同步并不是一回事!

    我重新梳理了一下同步/异步 and 阻塞/非阻塞的问题(这个的梳理下面有说)后,重新设计了方案:

    1) 前后端仍然是Thrift-RPC的异步接口,Server仍然采用ThreadServer。
    
    2) 后端的Handler Instance里面设置了一个MQ,每次接受到解析请求(即前端调用接口,传入json),

    handler首先校验json合法性,如果json不合法,返回给前端错误结果;如果json合法,返回给前端正确

    结果,并将json扔入MQ,等待处理(这里是异步化)。同时,handler里面启用了N个线程作为消费者,

    消费MQ中的json-request。那每个线程是如何进行处理的呢?很简单,每个线程去读MQ(block=True)

    获取到后,起一个进程(即上面的P1)进行解析处理,注意这里的P1是join(同步阻塞),这样即实现了并发

    控制,同时还保证了异步的要求。注意,每个线程中的处理使用try ...except原语包裹起来,防止异常

    导致线程退出,进而这个进程down掉。然后后端计算过程中的状态持久化到DB中,这样的前端通过主动请求

    来同步最新状态.

反思

   1)从一开始的我的思考中就存在一个问题,就是我把(异步/同步),(阻塞/非阻塞)给混了。

    首先温习一下这两组概念

异步/同步

    下面以这个P1的处理为例(P1耗时为10s)

    同步接口:在t_0秒caller调用接口,t_0 + 10秒接口返回(返回解析结果)

    异步接口:在t_0秒caller调用接口,t_0 + delta秒接口返回,(返回请求是否合法结果),真正的解析

              过程启动,并将结果写入数据库。(当然,真正的异步应该是回调caller的注册函数,

              这里因为caller 与 callee是前后端,后端没必要回调前端,直接把结果写到数据库里

              前端(caller)通过某些方式去同步结果(主动拉取,或者数据库通知caller等))

阻塞/非阻塞

    阻塞/非阻塞主要是体现在获取接口调用结果期间的状态。

    举个消息队列的例子,如果你是使用非阻塞的get方法,那如果MQ中没有消息,方法直接返回。

    如果是使用阻塞的get方法,调用一直等待,直到MQ中有数据。