V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
abersheeran
V2EX  ›  Python

第一个基于 Radix Tree 进行路由查找的 Python web 框架发布了!

  •  1
     
  •   abersheeran · 2020-07-25 15:21:28 +08:00 · 3502 次点击
    这是一个创建于 1613 天前的主题,其中的信息可能已经有所发展或是发生改变。

    https://github.com/abersheeran/index.py

    经过我和 encode 组织(也就是 starlette / django rest framework 的创造者)的交流,他们并不想把基数树查找纳入 starlette 。

    本来如果他们愿意纳入,那么 fastapi 也可以获得基数树路由的加持,性能可以获得再次上升。

    现在没办法了,我自己整。之前 index.py 使用的是文件树映射路由,虽然很高效,但是很受限。现在我把基数树纳入路由系统,把 index.py 的路由能力提升一大截,任何风格的路由均可以在里面找到。

    无论是喜欢 flask/bottle 这种装饰器路由,还是 Django 这种列表式均可以被 index.py 支持。并且无论再复杂的路由都是在代码加载时展开,不会增加服务的任何运行时开销!

    什么叫快,这就是快! 官方文档里我不好写,但是既然是论坛,我就这么说了,index.py 目前应该是所有 Python web 框架里最快的,没有之一。fastapi 快在使用了 ASGI + uvicorn,而 index.py 不仅有这两,而且路由系统更高效。

    由于我公司有不少项目已经使用了 index.py ,所以请大家放心。后续应该不会有再像这种的破坏性更改了,并且在我和朋友一起创建的公司倒闭之前,我们都会对这个项目进行维护。

    如果不幸,我创业失败,我个人也会接着维护这个框架。这也是我在大学里创造的第一个我很满意的项目,也是最后一个(今年毕业了)。说着说着有点伤感……希望我创业有成,不求深圳一套房,能赚几百万我就心满意足。

    欢迎大家来拍砖 https://index-py.abersheeran.com/route/

    第 1 条附言  ·  2020-07-25 22:10:07 +08:00
    加个附言:我为 asgiref / uvicorn / starlette 都贡献过实际有用的代码,修过它们的 bug 。倒不用担心 index.py 是个垃圾,不能兼容现有 ASGI 生态,基本上 starlette/fastapi 能用的生态,都可以无缝引入 index.py 里。整体设计上我个人还是挺满意的,但是部分代码的确是为了实现不择手段,后续会优化。
    第 2 条附言  ·  2020-08-12 13:09:39 +08:00
    https://github.com/abersheeran/asgi-benchmark

    各大 ASGI 框架的性能测试对比。
    45 条回复    2020-10-08 17:25:56 +08:00
    swulling
        1
    swulling  
       2020-07-25 15:22:47 +08:00 via iPad
    有没有 benchmark
    abersheeran
        2
    abersheeran  
    OP
       2020-07-25 15:25:08 +08:00
    @swulling 暂时没这个时间去做,主要是工作忙。周末搞这个,女朋友已经很不满了,待会得陪她出门玩。大佬有兴趣的话,可以帮忙做一下?
    BBCCBB
        3
    BBCCBB  
       2020-07-25 15:29:34 +08:00
    支持. 加油.

    starlette 是基于什么方面的考量不想使用 Radix tree 的呢.
    BBCCBB
        4
    BBCCBB  
       2020-07-25 15:30:56 +08:00
    还有就是是不是 fork 一下 starlette 改下路由实现, 然后 fork fastapi 改下依赖来的快些呢?
    abersheeran
        5
    abersheeran  
    OP
       2020-07-25 15:33:44 +08:00
    @BBCCBB 刚翻了一下 gitter.im 被刷下去了,就不截图了。大致是 encode 的其中一个人认为是没必要的,然后我们吵了一会,出来一个支持我的(不是 encode 的人)。还没说两句,我准备提交一个 issue 去正式讨论一番的时候,Tom 出来说 starlette 的路由实现不光以性能为主,简单也很重要。他都发话了,我也没必要挣扎了,就回了一个原来如此。记录你在 gitter 应该都能查到。
    abersheeran
        6
    abersheeran  
    OP
       2020-07-25 15:35:48 +08:00
    @BBCCBB 你想太多了,路由系统是 web 框架最重要的部分,跟其他部件都是强耦合的。你要么重写一个,要么就只能用遍历路由。我觉得小项目其实也还好,执行几十个正则表示式查路由,对于小项目也算不得什么。面对有上千个路由的大项目,starlette(fastapi) 这种遍历就很致命了。
    secondwtq
        7
    secondwtq  
       2020-07-25 17:05:24 +08:00
    你可以把女朋友换给 V 站大佬,自己就可以安心做 benchmark 了
    eudore
        8
    eudore  
       2020-07-25 18:07:29 +08:00
    Radix 是基操,算不上啥宣传亮点。
    est
        9
    est  
       2020-07-25 18:45:05 +08:00
    @BBCCBB 我觉得主要原因是 菜。
    AlexaZhou
        10
    AlexaZhou  
       2020-07-25 18:51:05 +08:00
    需要先跑下 benchmark,看现有框架里面,路由查找消耗的时间,占整理处理时间的比例是怎样。

    只有在路由查找本身是瓶颈的情况下,这个改进才是有意义的
    abersheeran
        11
    abersheeran  
    OP
       2020-07-25 21:56:31 +08:00
    @eudore 在 Python web 框架里,是首个。虽然我也知道是基操。毕竟我玩了一个晚上就实现完了。
    abersheeran
        12
    abersheeran  
    OP
       2020-07-25 21:59:36 +08:00
    @AlexaZhou ……你这是面对一个已经存在的项目进行分析的路子,不适合于框架。

    另外,这个路由优化都不需要我来证明什么,你去看看 golang 的框架,哪个不是 Radix Tree ?
    ericls
        13
    ericls  
       2020-07-25 22:01:14 +08:00
    框架的性能测试无非就是比 overhead 谁小。除非出了大错误,框架带来的 overhead 都不会成为瓶颈
    要想速度快的话,不用框架最快。

    性能测试还是得看 server 意义大一些
    abersheeran
        14
    abersheeran  
    OP
       2020-07-25 22:04:01 +08:00
    @ericls 是的。只能说同样 uvicorn 下,index.py 的裸性能是比其他 starlette 系(包括 responder/fastapi )快的。但一旦引入业务代码,基本上就看业务代码写的水平了。
    ericls
        15
    ericls  
       2020-07-25 22:07:04 +08:00
    我觉得这个项目还是挺有意思的
    abersheeran
        16
    abersheeran  
    OP
       2020-07-25 22:11:30 +08:00
    @ericls 谢谢~后续文档会越来越详细的,目前文档很粗糙。
    newlife
        17
    newlife  
       2020-07-25 23:06:58 +08:00
    单纯路由映射提升感觉没有多大意义,
    abersheeran
        18
    abersheeran  
    OP
       2020-07-27 09:52:56 +08:00
    @newlife index.py 在很多方面都是和 flask/django 这种传统框架不同的,而也不像 fastapi 这个拼接怪全搬的 starlette 。有更多的便于开发的修改点,你用用就知道了
    eudore
        19
    eudore  
       2020-07-27 21:22:27 +08:00
    @abersheeran 只是有点难以置信,py 发展怎么多年 flask/django 等主流框架居然不是高性能路由匹配,太菜了。。。。
    abersheeran
        20
    abersheeran  
    OP
       2020-07-28 11:43:32 +08:00
    @eudore 我没看过的时候,我也不信。
    treblex
        21
    treblex  
       2020-07-28 14:13:01 +08:00
    学到了新的知识点😄
    abersheeran
        22
    abersheeran  
    OP
       2020-07-28 14:45:05 +08:00
    @suke971219 那就给 star 😀
    CodeDrudgery
        23
    CodeDrudgery  
       2020-07-28 17:02:39 +08:00
    V 站的第一次留言给大佬,马上去贡献小星星
    abersheeran
        24
    abersheeran  
    OP
       2020-07-29 11:02:33 +08:00
    @CodeDrudgery 谢谢~
    abersheeran
        25
    abersheeran  
    OP
       2020-08-12 13:10:00 +08:00
    @swulling 现在有了。大佬可以看看
    lusi1990
        26
    lusi1990  
       2020-08-28 17:07:41 +08:00
    和 fastapi 的文档差距太大了,已 star
    abersheeran
        27
    abersheeran  
    OP
       2020-08-29 22:24:38 +08:00
    @lusi1990 谢谢~文档差距的确,一时半伙追不上,只能说不断完善吧。
    wdhwg001
        28
    wdhwg001  
       2020-10-04 23:34:55 +08:00
    我仔细翻了一下你之前的讨论(在 7 月 13 日前后),实际上是没吵起来的。

    社区成员 erewok 表达的意思仅仅是之前关于这件事就有讨论了,所以更希望有实际的实现和性能对比,而不是在要求一项功能。并且后面的意见也是希望能开 PR 出来,用性能实测数据说话。

    Tom 的意见是简洁优先,但没有明确拒绝一项性能改善。所以你注释充足结构清晰,拿代码说话的话也是可以 PR 的,性能始终是 Encode 社区的很重要的考量。
    wdhwg001
        29
    wdhwg001  
       2020-10-04 23:44:13 +08:00
    所以总之就是,如果有心力的话,可以考虑把实际的性能对比拿出来,提一下 PR,测量点应该主要有这几个:

    模拟一个简单的路由情况,提供树实现和遍历实现的性能对比(非 WSL 的 Linux 下内存、吞吐、延迟)。

    模拟一个常规的大型网站的路由情况,提供性能对比。

    模拟一个深层次的路由情况,提供性能对比,看一下你的实现是否会在深层次的时候性能跑不过遍历。

    模拟一个树被压成单层扁平的路由情况,提供性能对比,看一下你的实现在做普通遍历的时候性能会不会跑不过当前的遍历实现。

    如果能实现全面的性能提升,并且代码风格良好的话,我觉得 encode 社区会很乐意接纳的。但如果因为误解而没有心力了的话也不勉强。
    abersheeran
        30
    abersheeran  
    OP
       2020-10-05 01:00:29 +08:00
    @wdhwg001 是的。毕竟只是说代码,大家说话都比较克制,没有真的吵起来,这里是一个夸张的说法(而且 encode 的一个人,话里的意思感觉是误解了我在说 starlette 这个框架不够高效)。Tom 的意思是他更喜欢简洁的实现,在那个上下文里,显然他是拒绝了我的想法。他这个话,是在我说我打算去开个 ISSUE 详细的讨论这件事之后发的。意思不用更明显了。

    至于代码实现上的性能对比,上面我已经追加了。理论性能对比,这个文章我都没必要写,任何人一搜就能搜到。Radix Tree 是目前 web 路由匹配的普适最优解。
    abersheeran
        31
    abersheeran  
    OP
       2020-10-05 01:07:42 +08:00
    @wdhwg001 现在也没那么多心力放讨论这种无聊的事上面,我直接去 asgiref 搞事去了。Index.py 基本各部分都独立出来了,现在也就几个通用的 ASGI 中间件和 Request 、Response 对象用了 starlette 的代码。路由系统每一行都是原创,我们自己用的满意就是最好的。

    反正微框架嘛,你还能指望有啥代码生态。Django 依旧是我们的主力框架,Index.py 也好,哪怕以后我走了他们打算换 fastapi 也好,都也只是非关系型数据库交互上用而已。Django 全家桶真香!
    wdhwg001
        32
    wdhwg001  
       2020-10-05 13:22:32 +08:00
    @abersheeran ASGI 标准我感觉没什么前途,标准已经推到 3.0 了,但是各个实现都还很不完整,设计上也有点问题。

    举例子说的话,比如 scope 明显应该是一个单例,但是 ASGI 里却将 Scope 定义为了一种 immutable,并且是在中间件之间可随意复制的。这埋下的一个巨坑就是对于一个中间件来说,根本没办法获取到最终的 Scope 引用,从而不能在 send 期间访问到一个稳定的上下文。

    也就是说,这个半残的中间件机制甚至连一个稳定的,不需要在请求里操心存取的 Session 都实现不了。Starlette 里的 Session 实现是有坑的,他们把 Session 存到了 scope 里,违背了 immutable,然后使用了 ASGI 明确声明不可靠的闭包 scope 引用去在 send 期间检查和存储 scope 中的 session,这就使得 scope 一旦被复制,session 的管理就变得完全不可靠了。

    而这根本就是一个 ASGI 标准的问题,因为基于性能的考量,一个上下文状态本就应该是单例,可变,不可复制的。
    abersheeran
        33
    abersheeran  
    OP
       2020-10-05 14:00:08 +08:00
    @wdhwg001 我的思路跟你不一样。ASGI 的 scope 本身就应该是可变的。就像是整个 HTTP 请求链路上,任何一层反向代理服务器都可以对这个 HTTP 请求的信息做出修改一样。这个我觉得问题不大。

    我认为的问题在于没办法直接给 socket 写数据,比如发送一个静态文件,这种情况它的性能上就比较差,要拷贝很多次。前几天刚合并了一个相关的扩展过去。目前感觉应该不缺什么了。
    wdhwg001
        34
    wdhwg001  
       2020-10-05 17:16:18 +08:00
    @abersheeran ASGI 标准要求 Scope 是不可变的,每次修改都要复制 Scope 以避免污染上层 Middleware:
    https://asgi.readthedocs.io/en/latest/specs/main.html#middleware

    但是这样一来,Session 实际上是无法以纯 ASGI Middleware 可靠实现的,因为它需要一个上下文,在请求传至 Router 之前初始化上下文中的 Session (从 Cookie / 数据库中读取 Session 放入上下文),在 Endpoint 处理完之后将上下文中 Session 的最终状态进行存储。

    而 ASGI 中不存在这样一个上下文存储,所以 Starlette 才会把 Scope 当作上下文,而最终的 Scope 又不会被传递至 send,就导致了一系列问题。
    wdhwg001
        35
    wdhwg001  
       2020-10-05 17:23:58 +08:00
    @abersheeran 老实说比较底层的部分每个框架的实现都有问题,比如没有一个框架和 Server 实现了 Websocket 的 Set-Cookie,明明 2.1 标准里就有了…这就扯远了。

    但是 Socket 的问题我感觉不是特别大,因为目前其实也不推荐裸跑 ASGI 或者用 ASGI 传静态文件,大家都是外面套一个 Nginx 用的。

    ASGI 还有一个问题就是它只是个纸面标准,没有针对每一项纸面规定的测试…也是比较遗憾了。
    abersheeran
        36
    abersheeran  
    OP
       2020-10-05 18:21:48 +08:00
    @wdhwg001 呃,我们现在就是不带前置服务器直接跑 ASGI Server 😀。

    中间件这部分我还真没注意,我觉得这个申明有点脱裤子放屁的感觉。如果拷贝一份再把新对象传给下一层,那也太浪费内存了吧。可能 starlette 的 Session 实现跟我的想法差不多,所以没拷贝。
    wdhwg001
        37
    wdhwg001  
       2020-10-05 19:06:37 +08:00
    @abersheeran 不仅如此,Starlette 早期是直接拿 Scope 当 Context 用的,Auth 、Session 、App 实例一类的都塞进 Scope 里,后来又有了 app.state 和 Request.state 两种 state,但是受限于 ASGI 标准,没有任何办法可以在中间件里访问到这个 Request.state…

    所以如果真的玩 ASGI 标准的话,倒不如把这件事扔上台面,把 scope 变成一个 mutable dict 的单例,然后决定一下上下文是存到哪,是新开一个 dict 叫 state 或者 context,还是直接就存到 scope 里。

    然后还得决定一下怎样让这个上下文在整个请求周期内都是可用的,怎样把它传到 receive 和 send 里。
    wdhwg001
        38
    wdhwg001  
       2020-10-05 19:30:33 +08:00
    以及好像跑题了,说回来的话,我注意到你的实现实际上是个巨大的正则树。那么这样的话,这个正则树是否可以合并为一个巨大的单一正则以实现更大的性能提升和更少的内存占用呢?
    abersheeran
        39
    abersheeran  
    OP
       2020-10-05 22:53:54 +08:00
    @wdhwg001 Python 的 re 标准库没办法通过匹配到的 pattern 来获取对应的 endpoint 。这个我后续可能用 Rust 实现一个 Radix Tree 。

    直接把 scope 传到 receive 和 send 里是什么意思?我没懂这个有什么用。
    wdhwg001
        40
    wdhwg001  
       2020-10-05 23:58:17 +08:00
    @abersheeran 我试了一下,结果倒是挺遗憾的,Python 里的正则的性能与捕获组的数量是呈线性关系的,与捕获组之间的包含关系无关。所以,将树展开成正则组的操作应该是仅在树的同级节点的时候可以通过合并获得性能提升,超过同级的情况下就是 Python 的语言性能和正则引擎的执行效率之间的撕逼取舍了。

    我个人不倾向于使用换语言的方式实现这个,因为你似乎还没完全榨干 Python 呢。

    将上下文传入 receive 和 send 是有意义的,比如不然的话,你怎样使用纯 ASGI 中间件实现一个干净的 Session 呢? Session 的读取发生在__call__期间,而存储和 cookie 的更新则发生在 send 期间,而这之间则需要一个上下文去保存 Session 中的信息吧。
    abersheeran
        41
    abersheeran  
    OP
       2020-10-06 00:33:24 +08:00
    @wdhwg001 ASGI3 里面 __call__ 直接做完所有工作了,完全可以共享到一个 session 对象。

    ```python
    async def __call__(self, scope, receive, send):
    session = {}
    scope["session"] = session
    ...

    send(...session...)
    ```
    wdhwg001
        42
    wdhwg001  
       2020-10-06 19:25:13 +08:00
    @abersheeran 但是你的中间件需要调用内层的 Application,而 send 则是一层一层传递下去,直到最后一层 Application 才调用这个 callable 的,所以你不应该在中间件里直接执行这个 send,而是要把它传递到更深层的 ASGI Application 里:

    ```python
    async def __call__(self, scope, receive, send):
    session = {}
    scope["session"] = session

    await self.app(scope, receive, send)
    ```

    这样一来,你就只能存一个 scope 的闭包:

    ```python
    async def __call__(self, scope, receive, send):
    session = {}
    scope["session"] = session

    async def send_wrapper(message):
    # do something to store session
    scope["session"]["saved"] = True # 这里的闭包是不可靠的
    await send(message) # 但是这里是可靠的

    await self.app(scope, receive, send_wrapper)
    ```

    但是就像 ASGI 标准里描述的那样,Scope 是会被下一层 Application 复制的,这就使得内层如果真的复制了 Scope 的话,外层对 Scope 的闭包引用读取到的只会是脏数据。我仔细思考过了,觉得这里如果 ASGI 不做调整,ASGI 的实现也不保存 Application 栈的话(因为像 Django 的 Daphne 一样保存栈是很消耗资源的),这里应该是没有解决方案的。

    可如果 ASGI 提供一个单例的上下文的话:


    ```python
    async def __call__(self, scope, receive, send):
    session = {}
    scope["session"] = session # 把它存到一个请求期间的上下文变量,不一定是 Scope

    async def send_wrapper(message, scope): # 这里接收一个请求期间的上下文变量,不一定是 Scope
    # do something to store session
    scope["session"]["saved"] = True # 这里就不是闭包而是普通地用参数了,所以是可靠的
    await send(message) # 这里本来就是可靠的

    await self.app(scope, receive, send_wrapper)
    ```

    或者,如果修改 ASGI 的规定,使得 Scope 由一个 immutable 变为一个 mutable single instance 的话,任何一个地方都可以取到一个可靠的 Scope 的引用,也就不会存在复制的问题了。
    abersheeran
        43
    abersheeran  
    OP
       2020-10-07 14:15:32 +08:00
    @wdhwg001 你为啥要从 scope["session"] 读数据,直接用 session 变量不就行了?
    wdhwg001
        44
    wdhwg001  
       2020-10-07 21:04:29 +08:00
    @abersheeran 但是也同样无法解决闭包不可靠的问题啊,你只能规定禁止 scope 深拷贝,并且规定禁止 scope["session"] = {}这样的操作。
    abersheeran
        45
    abersheeran  
    OP
       2020-10-08 17:25:56 +08:00
    @wdhwg001 这就只能靠约定了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5267 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 05:46 · PVG 13:46 · LAX 21:46 · JFK 00:46
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.