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 ,所以请大家放心。后续应该不会有再像这种的破坏性更改了,并且在我和朋友一起创建的公司倒闭之前,我们都会对这个项目进行维护。
如果不幸,我创业失败,我个人也会接着维护这个框架。这也是我在大学里创造的第一个我很满意的项目,也是最后一个(今年毕业了)。说着说着有点伤感……希望我创业有成,不求深圳一套房,能赚几百万我就心满意足。
1
swulling 2020-07-25 15:22:47 +08:00 via iPad
有没有 benchmark
|
2
abersheeran OP @swulling 暂时没这个时间去做,主要是工作忙。周末搞这个,女朋友已经很不满了,待会得陪她出门玩。大佬有兴趣的话,可以帮忙做一下?
|
3
BBCCBB 2020-07-25 15:29:34 +08:00
支持. 加油.
starlette 是基于什么方面的考量不想使用 Radix tree 的呢. |
4
BBCCBB 2020-07-25 15:30:56 +08:00
还有就是是不是 fork 一下 starlette 改下路由实现, 然后 fork fastapi 改下依赖来的快些呢?
|
5
abersheeran OP |
6
abersheeran OP @BBCCBB 你想太多了,路由系统是 web 框架最重要的部分,跟其他部件都是强耦合的。你要么重写一个,要么就只能用遍历路由。我觉得小项目其实也还好,执行几十个正则表示式查路由,对于小项目也算不得什么。面对有上千个路由的大项目,starlette(fastapi) 这种遍历就很致命了。
|
7
secondwtq 2020-07-25 17:05:24 +08:00
你可以把女朋友换给 V 站大佬,自己就可以安心做 benchmark 了
|
8
eudore 2020-07-25 18:07:29 +08:00
Radix 是基操,算不上啥宣传亮点。
|
10
AlexaZhou 2020-07-25 18:51:05 +08:00
需要先跑下 benchmark,看现有框架里面,路由查找消耗的时间,占整理处理时间的比例是怎样。
只有在路由查找本身是瓶颈的情况下,这个改进才是有意义的 |
11
abersheeran OP @eudore 在 Python web 框架里,是首个。虽然我也知道是基操。毕竟我玩了一个晚上就实现完了。
|
12
abersheeran OP |
13
ericls 2020-07-25 22:01:14 +08:00
框架的性能测试无非就是比 overhead 谁小。除非出了大错误,框架带来的 overhead 都不会成为瓶颈
要想速度快的话,不用框架最快。 性能测试还是得看 server 意义大一些 |
14
abersheeran OP |
15
ericls 2020-07-25 22:07:04 +08:00
我觉得这个项目还是挺有意思的
|
16
abersheeran OP @ericls 谢谢~后续文档会越来越详细的,目前文档很粗糙。
|
17
newlife 2020-07-25 23:06:58 +08:00
单纯路由映射提升感觉没有多大意义,
|
18
abersheeran OP |
19
eudore 2020-07-27 21:22:27 +08:00
@abersheeran 只是有点难以置信,py 发展怎么多年 flask/django 等主流框架居然不是高性能路由匹配,太菜了。。。。
|
20
abersheeran OP @eudore 我没看过的时候,我也不信。
|
21
treblex 2020-07-28 14:13:01 +08:00
学到了新的知识点😄
|
22
abersheeran OP @suke971219 那就给 star 😀
|
23
CodeDrudgery 2020-07-28 17:02:39 +08:00
V 站的第一次留言给大佬,马上去贡献小星星
|
24
abersheeran OP @CodeDrudgery 谢谢~
|
25
abersheeran OP @swulling 现在有了。大佬可以看看
|
26
lusi1990 2020-08-28 17:07:41 +08:00
和 fastapi 的文档差距太大了,已 star
|
27
abersheeran OP @lusi1990 谢谢~文档差距的确,一时半伙追不上,只能说不断完善吧。
|
28
wdhwg001 2020-10-04 23:34:55 +08:00
我仔细翻了一下你之前的讨论(在 7 月 13 日前后),实际上是没吵起来的。
社区成员 erewok 表达的意思仅仅是之前关于这件事就有讨论了,所以更希望有实际的实现和性能对比,而不是在要求一项功能。并且后面的意见也是希望能开 PR 出来,用性能实测数据说话。 Tom 的意见是简洁优先,但没有明确拒绝一项性能改善。所以你注释充足结构清晰,拿代码说话的话也是可以 PR 的,性能始终是 Encode 社区的很重要的考量。 |
29
wdhwg001 2020-10-04 23:44:13 +08:00
所以总之就是,如果有心力的话,可以考虑把实际的性能对比拿出来,提一下 PR,测量点应该主要有这几个:
模拟一个简单的路由情况,提供树实现和遍历实现的性能对比(非 WSL 的 Linux 下内存、吞吐、延迟)。 模拟一个常规的大型网站的路由情况,提供性能对比。 模拟一个深层次的路由情况,提供性能对比,看一下你的实现是否会在深层次的时候性能跑不过遍历。 模拟一个树被压成单层扁平的路由情况,提供性能对比,看一下你的实现在做普通遍历的时候性能会不会跑不过当前的遍历实现。 如果能实现全面的性能提升,并且代码风格良好的话,我觉得 encode 社区会很乐意接纳的。但如果因为误解而没有心力了的话也不勉强。 |
30
abersheeran OP @wdhwg001 是的。毕竟只是说代码,大家说话都比较克制,没有真的吵起来,这里是一个夸张的说法(而且 encode 的一个人,话里的意思感觉是误解了我在说 starlette 这个框架不够高效)。Tom 的意思是他更喜欢简洁的实现,在那个上下文里,显然他是拒绝了我的想法。他这个话,是在我说我打算去开个 ISSUE 详细的讨论这件事之后发的。意思不用更明显了。
至于代码实现上的性能对比,上面我已经追加了。理论性能对比,这个文章我都没必要写,任何人一搜就能搜到。Radix Tree 是目前 web 路由匹配的普适最优解。 |
31
abersheeran OP |
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 标准的问题,因为基于性能的考量,一个上下文状态本就应该是单例,可变,不可复制的。 |
33
abersheeran OP @wdhwg001 我的思路跟你不一样。ASGI 的 scope 本身就应该是可变的。就像是整个 HTTP 请求链路上,任何一层反向代理服务器都可以对这个 HTTP 请求的信息做出修改一样。这个我觉得问题不大。
我认为的问题在于没办法直接给 socket 写数据,比如发送一个静态文件,这种情况它的性能上就比较差,要拷贝很多次。前几天刚合并了一个相关的扩展过去。目前感觉应该不缺什么了。 |
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,就导致了一系列问题。 |
35
wdhwg001 2020-10-05 17:23:58 +08:00
@abersheeran 老实说比较底层的部分每个框架的实现都有问题,比如没有一个框架和 Server 实现了 Websocket 的 Set-Cookie,明明 2.1 标准里就有了…这就扯远了。
但是 Socket 的问题我感觉不是特别大,因为目前其实也不推荐裸跑 ASGI 或者用 ASGI 传静态文件,大家都是外面套一个 Nginx 用的。 ASGI 还有一个问题就是它只是个纸面标准,没有针对每一项纸面规定的测试…也是比较遗憾了。 |
36
abersheeran OP @wdhwg001 呃,我们现在就是不带前置服务器直接跑 ASGI Server 😀。
中间件这部分我还真没注意,我觉得这个申明有点脱裤子放屁的感觉。如果拷贝一份再把新对象传给下一层,那也太浪费内存了吧。可能 starlette 的 Session 实现跟我的想法差不多,所以没拷贝。 |
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 里。 |
38
wdhwg001 2020-10-05 19:30:33 +08:00
以及好像跑题了,说回来的话,我注意到你的实现实际上是个巨大的正则树。那么这样的话,这个正则树是否可以合并为一个巨大的单一正则以实现更大的性能提升和更少的内存占用呢?
|
39
abersheeran OP @wdhwg001 Python 的 re 标准库没办法通过匹配到的 pattern 来获取对应的 endpoint 。这个我后续可能用 Rust 实现一个 Radix Tree 。
直接把 scope 传到 receive 和 send 里是什么意思?我没懂这个有什么用。 |
40
wdhwg001 2020-10-05 23:58:17 +08:00
@abersheeran 我试了一下,结果倒是挺遗憾的,Python 里的正则的性能与捕获组的数量是呈线性关系的,与捕获组之间的包含关系无关。所以,将树展开成正则组的操作应该是仅在树的同级节点的时候可以通过合并获得性能提升,超过同级的情况下就是 Python 的语言性能和正则引擎的执行效率之间的撕逼取舍了。
我个人不倾向于使用换语言的方式实现这个,因为你似乎还没完全榨干 Python 呢。 将上下文传入 receive 和 send 是有意义的,比如不然的话,你怎样使用纯 ASGI 中间件实现一个干净的 Session 呢? Session 的读取发生在__call__期间,而存储和 cookie 的更新则发生在 send 期间,而这之间则需要一个上下文去保存 Session 中的信息吧。 |
41
abersheeran OP @wdhwg001 ASGI3 里面 __call__ 直接做完所有工作了,完全可以共享到一个 session 对象。
```python async def __call__(self, scope, receive, send): session = {} scope["session"] = session ... send(...session...) ``` |
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 的引用,也就不会存在复制的问题了。 |
43
abersheeran OP @wdhwg001 你为啥要从 scope["session"] 读数据,直接用 session 变量不就行了?
|
44
wdhwg001 2020-10-07 21:04:29 +08:00
@abersheeran 但是也同样无法解决闭包不可靠的问题啊,你只能规定禁止 scope 深拷贝,并且规定禁止 scope["session"] = {}这样的操作。
|
45
abersheeran OP @wdhwg001 这就只能靠约定了。
|