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

2020-07-25 15:21:28 +08:00
 abersheeran

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/

3472 次点击
所在节点    Python
45 条回复
treblex
2020-07-28 14:13:01 +08:00
学到了新的知识点😄
abersheeran
2020-07-28 14:45:05 +08:00
@suke971219 那就给 star 😀
CodeDrudgery
2020-07-28 17:02:39 +08:00
V 站的第一次留言给大佬,马上去贡献小星星
abersheeran
2020-07-29 11:02:33 +08:00
@CodeDrudgery 谢谢~
abersheeran
2020-08-12 13:10:00 +08:00
@swulling 现在有了。大佬可以看看
lusi1990
2020-08-28 17:07:41 +08:00
和 fastapi 的文档差距太大了,已 star
abersheeran
2020-08-29 22:24:38 +08:00
@lusi1990 谢谢~文档差距的确,一时半伙追不上,只能说不断完善吧。
wdhwg001
2020-10-04 23:34:55 +08:00
我仔细翻了一下你之前的讨论(在 7 月 13 日前后),实际上是没吵起来的。

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

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

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

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

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

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

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

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

反正微框架嘛,你还能指望有啥代码生态。Django 依旧是我们的主力框架,Index.py 也好,哪怕以后我走了他们打算换 fastapi 也好,都也只是非关系型数据库交互上用而已。Django 全家桶真香!
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
2020-10-05 14:00:08 +08:00
@wdhwg001 我的思路跟你不一样。ASGI 的 scope 本身就应该是可变的。就像是整个 HTTP 请求链路上,任何一层反向代理服务器都可以对这个 HTTP 请求的信息做出修改一样。这个我觉得问题不大。

我认为的问题在于没办法直接给 socket 写数据,比如发送一个静态文件,这种情况它的性能上就比较差,要拷贝很多次。前几天刚合并了一个相关的扩展过去。目前感觉应该不缺什么了。
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
2020-10-05 17:23:58 +08:00
@abersheeran 老实说比较底层的部分每个框架的实现都有问题,比如没有一个框架和 Server 实现了 Websocket 的 Set-Cookie,明明 2.1 标准里就有了…这就扯远了。

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

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

中间件这部分我还真没注意,我觉得这个申明有点脱裤子放屁的感觉。如果拷贝一份再把新对象传给下一层,那也太浪费内存了吧。可能 starlette 的 Session 实现跟我的想法差不多,所以没拷贝。
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
2020-10-05 19:30:33 +08:00
以及好像跑题了,说回来的话,我注意到你的实现实际上是个巨大的正则树。那么这样的话,这个正则树是否可以合并为一个巨大的单一正则以实现更大的性能提升和更少的内存占用呢?
abersheeran
2020-10-05 22:53:54 +08:00
@wdhwg001 Python 的 re 标准库没办法通过匹配到的 pattern 来获取对应的 endpoint 。这个我后续可能用 Rust 实现一个 Radix Tree 。

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

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

将上下文传入 receive 和 send 是有意义的,比如不然的话,你怎样使用纯 ASGI 中间件实现一个干净的 Session 呢? Session 的读取发生在__call__期间,而存储和 cookie 的更新则发生在 send 期间,而这之间则需要一个上下文去保存 Session 中的信息吧。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/693028

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX