前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >「推荐」从openresty谈到rust

「推荐」从openresty谈到rust

作者头像
MikeLoveRust
发布2019-10-31 11:39:14
1.9K0
发布2019-10-31 11:39:14
举报

本文转载自知乎:https://zhuanlan.zhihu.com/p/87922545?utm_source=wechat_session&utm_medium=social&utm_oi=870758040972959744&from=singlemessage

本文小编觉得讲得极好,是作者多年一线实践及细心思考的结果。故在此强烈推荐仔细阅读。

大概是2015年,我开始关注nginx,在这之前,我一直从事C++的网络开发工作(通信网的信令协议栈研发,还有CORBA框架的实现),大概有七八年吧,都沉浸在C++的世界里,没有接触过什么更高级更现代的语言。开眼看世界也是最近三四年的事情,惭愧。

接触到nginx,很自然注意到了openresty,觉得很不错。nginx代码我拜读过,觉得实现得很优化,例如http解析就用了2000多行来做,充分考虑了时空性能。当时候nginx是声名在外。openresty引入了lua,封装了cosocket,使得能在nginx的基础上很简单地做二次开发,并且因为luajit,二次开发的性能代价很小。总而言之,当时候觉得openresty十分得惊艳,进而也膜拜章亦春大神。

当时工作有点乏味,然后也有点心思想跳槽,但是想到自己这七年来都是独孤一味地钻研C++相关的底层项目,感觉自己缺乏竞争力,所以很想学点东西,于是想到可不可以我也写一个http框架呢?luajit本身的ffi很厉害,不需要codegen就可以动态加载并访问任何C库函数,它的jit性能也很高,luajit的作者,Mike Pall也是编译器的翘楚,所以我想,可否我连nginx本身也用lua来重写呢?同时我对上提供openresty一样的api,这样所有*-resty的第三方库就可以直接拿来用了。这种思路类似于linus当年编写linux内核一样,对内重写,对外兼容POSIX,使得app可以直接拿来重用,例如bash。

重写的工作很有趣,有很多挑战,例如我要用纯luajit来实现cosocket。openresty的cosocket,非阻塞和select都在nginx的C层面,所以每次陷入阻塞读写的时候,会先yield到C层面。另外,openresty的协程是有父子关系的,表现在一次http请求由一个父协程来处理,它生成的其他协程(一般用来访问外部资源,例如redis),则是其子协程。父协程可以等待(或者同时等待多个)子协程,而父协程退出后,子协程也会退出。纯luajit没有C的承托,所以只能通过lua的exception来做,通过特殊的异常抛出和捕获来实现openresty的cosocket。还有一个有趣的地方,就是nginx的热重启,是通过保留文件描述符并且通过父子进程的环境变量来透传重现的,用纯lua来做,并且还考虑linux的信号处理,则要费了一点心思了,但最后还是做出来了,当时候心情很愉悦。

这个重写最终发布的开源项目就是luajit.io,这个名字也很有意思,一方面,这是一个我申请的io后缀的域名,另一方面它也是项目的名字,io框架嘛,一语双关。各种实现细节肯定不如nginx这般精致,所以性能不会达到nginx这么好,有80%就足够了,做出来后也符合我的预期。用20%的性能换取更简单的代码实现,我觉得已经很有意义了。试想,nginx和openresty的C代码加起来这么多,而我用lua重写,只有区区5000多行代码。

发布后,收到了不少关注。不过,我也就是当一个玩具工程来练手罢了。我后来再反思,其实cosocket虽然惊艳,但是并非一枝独秀,golang就完整实现了协程化,不仅仅socket,文件访问和cpu密集型任务都可以融入到协程里面来做,所以golang具有更完整意义的cosocket。

openresty受欢迎,我觉得很大程度得益于它站在了巨人的肩膀上,那就是nginx和luajit。但是更好的事物都有时代的局限性。我这里展开来说一些它们的缺点。

先说nginx吧,nginx是多进程架构,每个worker进程(单线程)公平地去抢夺进来的tcp连接,独立处理每个tcp连接上的http请求。socket读写非阻塞,每个worker进程都有一个selector来select所有socket。处理一个http请求没有进程间切换意味着更好的性能。但多进程也有弊端:

  1. 在接受连接后就只能固定在一个进程里面,如果恰好该进程所处理的连接里面的http请求很多很繁忙,那么它也无法委托给其他进程来代劳,即便其他进程是空闲的,对于http2而言,我觉得这一缺点尤为突出。
  2. 多进程之间无法安全地共享资源。nginx的方案是放数据在共享内存里面,例如openresty的queue就是放里面的,并且通过放在共享内存里面的pthread mutex来同步。但是弊端很明显,对共享内存的操作不是原子的,例如上锁后,要对共享内存里面的红黑树做remove操作,那么对应的C代码就不少,对应到共享内存上,就有很多步操作,那么如果进行操作的进程异常退出,那么就会留下一个无法收拾的局面。例如,上锁后退出,资源就一直处于加锁状态,其他进程无法获取继续访问,这个还比较容易观察和调试出来。一般多进程系统都需要一个父进程来清理残局,但nginx没有这样做。
  3. worker进程是单线程,无法用它来做CPU密集型任务或者磁盘IO任务,nginx为了解决这个问题,引入worker thread pool,但openresty很难利用这个新特性,因为受限于lua虚拟机只能支持单线程的事实,如果利用,线程间交互以及数据拷贝是很大问题。
  4. nginx本身只是一个平台,一个特定的平台,起来一个http server给你让你处理http请求,并且能做的实现依赖于nginx导出了什么api给你,所以有时候你很难施展拳脚去适配自己的项目,例如我访问kafka,要作为它的consumer,那么就没法做了,因为没法作为server给kafka调用。

而且nginx最著名的特点:性能,也并非一枝独秀,目前rust就完全可以追上它,我后面会提到。

好了,再来说一下luajit,作者确实是一个天才,我那段时间看了很多他写的文章,他的各种理论都是如此高深莫测,他的dynasm可谓解放了汇编开发的生产力,而luajit更是让人佩服。用lua来写业务逻辑,很自然会担心性能,相比官方原生的lua的解释器性能和C不是一个等级,luajit的jit弥补了这一点,使得你既可以用lua很高兴很轻松写代码,又不必过分担心性能代价。但是,有如下问题:

  1. 最大的问题是lua版本的分裂,自lua5.2后,很多地方不再和官方lua兼容,并且长期停留在5.1上,作者没有意愿去改变这个局面。
  2. 源码实现太复杂,几乎只有Mike Pall自己才能维护它,但作者近几年来的开发活跃度很低,几年来都没发布2.1的正式版本,长期停留在beta,不知道他在忙什么。Mike Pall似乎早就说过要找接班人,但好像一直找不到。
  3. 你写的lua代码要极力去适配luajit的脾胃,才能让luajit给你实现编译,才能真的达到高性能,先不说如何调试适配是多么痛苦的事情,就说你适配了,你的代码有时候也变得很丑陋很怪异,例如要用tail call去替代循环。我写luajit.io的时候就深有体会。没错,如果jit得好,那么甚至有时候会比C更快,之所以更快,你可以认为是经过了profile适配(PGO)的C比普通的C快。但是你要极力去优化,使得有很高的编译通过率才行,这一点就不是每个人都能做到,是一个明显的心智负担。尤其对于大型项目而言,留心费神去优化每一行代码是不现实的。说白了吧,普通的C写出来有80%的好性能,但普通的lua写出来不调优,就只能有50%甚至更低的性能(虽然luajit的解释器也很快,但再快比C还是差了一大截)。所以jit,很多时候只是镜花水月而已。

终于说到openresty了。作者章亦春也是一个大神,它的coscoket在当时来说还是很前卫的概念。我就冒昧来谈谈它的缺点:

  1. openresty的所有功能源自nginx,也就受限于nginx。而nginx只是一个特定平台,不是一门语言,所以可扩展性是有局限性的。再进一步说,nginx是用C写的,扩展模块也要用C写,openresty之后就要用lua来写的(openresty就是为了提高生产力出现的),但lua本身是一个极其简单的嵌入式语言,没有自己的生态链,其功能完全依赖于宿主系统,在这里宿主就是openresty,也就是说,你能通过lua来做的完全取决于openresty提供多少api给你,没有给你的,你做不了,举个例子,我想开一个线程来做CPU密集的加密任务,没办法,因为没API给你。但如果是一门语言,那么你想做什么就做什么。
  2. 你不能调用阻塞的lua api或者C函数,或者做一些CPU密集型的任务,或者大量读写文件,因为这样会阻塞nginx的worker进程的单线程,使得性能大幅度下降,而且很容易出现一些让开发者痛苦的事情,例如发现访问redis超时了,明明通过tcpdump看到redis的响应包及时到了,但就是超时,很矛盾很纠结,结果经过一番折腾后发现原来是因为做了一些阻塞的事情,使得nginx的selector在处理io事件之前先处理了timer事件,使得socket明明有数据也被openresty的api报告超时。
  3. lua和C之间的数据转换是一个overhead。由于lua的数据结构和C那么的不同,所以交换数据要互相拷贝。这一点对于http请求承载大量数据的应用来说很痛苦。例如我在K公司实现文件服务器的功能,这个文件服务器不能直接委托给nginx的file send,因为要对原始文件数据做处理,例如md5校验。这也是为什么openresty后面慢慢提供一些通过luajit ffi来实现的api接口,就是为了减少拷贝,提高性能。
  4. 无法实现高性能的缓存,因为luajit的string interning很死板,对每个字符串,不管是常量还是动态生成的变量,都统一经过内部的哈希表来存放和去重,其目的就是为了使得用字符串作为table的key时,加快查找速度,因为比对是否同一个gcobject即可。但对缓存逻辑是一个噩梦,因为每生成一个字符串都需要哈希操作,而缓存恰好会生成很多字符串,luajit的interning哈希表在海量字符串的量级下性能很差。我在k公司做的项目对此有很深的体会。

在我看来,openresty相比rust,最大的好处就是lua代码能被动态更新和替换,对于静态编译语言来说是不可能的(dynamic load可以,但dynamic unload是不行的,因为符号之间的引用关联实在没法很轻易解耦)。

我2017年去K公司的时候,发现K公司很钟情openresty,很多项目都基于openresty来做,甚至公司还向openresty捐助过一笔小钱。但K公司的人是滥用openresty,在不知道其原理机制的前提下做了很多错事,很多项目其实不应该用openresty但也用。正如后来我去到E公司发现很钟情springboot一样,我觉得现在的公司很喜欢用一些品牌项目作为基础,或因其名气,或因其简单易入门,而不是具体问题具体分析,按项目实际需要来选型。

我曾一度觉得golang是openresty更好的选择,但golang的http性能确实不好。直到最近这半年我对rust的研究,觉得rust才是未来。

golang的语言设计很简陋,而相比之下,rust很美很优雅。这里不展开解释。我只说一点,那就是golang从无到有自己实现一门语言,包括编译器完全自己来做,甚至连C库都抛开,直接封装系统调用,这是我最不喜欢的,为什么呢?

  1. 无法充分利用这十几年来的社区成果,例如gcc和llvm,所以优化度很低,例如llvm的simd,它就无法享用。
  2. 和C互通代价太大,但很多时候C库是避不开的。
  3. 不兼容目前经典的调试器,例如gdb、valgrind、systemtap,而它自带的调试器功能相对简陋。

而rust呢?在语言特性上非常先进,例如通过ownership解决了C/C++的问题,还不需要付出gc的代价。并且充分利用社区成果,做好语言层面就好了,生成代码和链接代码就交给更专业的llvm,这样一来既专注在语言层面,提供更多更好的特性给用户(例如最近的await),和C互通又很低成本,因为它没有绕开C库。

golang的协程,在rust里面就是通过futrure/async/await来做,开发效率是一样的,运行效率更是胜于golang,因为rust的协程是在编译阶段解析生成的,所有栈数据是用heap上的struct/enum来包装,并且在所有suspend点做了drop,使得内存不需要像golang的协程栈那样在运行时增量分配,也不需要gc来干扰。

我这两三年一直做golang的开发,尤其在K公司。

golang的生态链比较丰富,上下游的各种库和框架都比较全,在国内各大公司都能见到go的踪影。例如这是我最近发布的开源项目,大家有空关注一下:

kingluo/pgcatgithub.com

这是postgresql的增强逻辑复制软件,当时候做的时候也考虑过是否用rust,但是rust没有postgresql的replication相关的库,如果要完全自己写,工作量比较大,而go的pgx库则完全满足要求,另有一个pgoutput的简单库用来解析pgoutput plugin的输出,所以我就选择了go。所以很多时候语言选型跟语言的生态链确实有很大关系。

但是我现在始终觉得rust才是未来,rust的生态链也会慢慢丰富起来,在我接下来的技术生涯里面我会phase out掉golang。

最后,我给一个小例子来验证一下rust的性能。

这个小例子就是http server和hello world。

golang的实现:

代码语言:javascript
复制
package main

import (
    "net/http"
)

func main() {
    http.HandleFunc("/", HelloServer)
    http.ListenAndServe(":8080", nil)
}

var str = []byte("hello")

func HelloServer(w http.ResponseWriter, r *http.Request) {
    w.Write(str)
}

openresty的实现:

代码语言:javascript
复制
worker_processes auto;
error_log logs/error.log;
events {
    worker_connections 1024;
}
http {
    access_log off;
    server {
        listen 8080;
        location / {
            default_type 'text/plain; charset=utf-8';
            content_by_lua_block {
                ngx.print("hello")
            }
        }
    }
}

rust hyper:

代码语言:javascript
复制
usehyper::service::{make_service_fn,service_fn};usehyper::{Body,Request,Response,Server};asyncfn hello(_: Request<Body>)-> Result<Response<Body>,Infallible>{letmutres=Response::new(Body::from("hello"));res.headers_mut().insert("Content-Type",HeaderValue::from_static("text/plain; charset=utf-8"),);Ok(res)}asyncfn run_server()-> Result<(),Box<dynstd::error::Error+Send+Sync>>{pretty_env_logger::init();letmake_svc=make_service_fn(|_conn|async{Ok::<_,Infallible>(service_fn(hello))});letaddr=([0,0,0,0],8080).into();letserver=Server::bind(&addr).serve(make_svc);println!("Listening on http://{}",addr);server.await?;Ok(())}fn main(){letrt=tokio::runtime::Builder::new().build().unwrap();rt.block_on(run_server()).unwrap();}

rust actix-web:

代码语言:javascript
复制
useactix_web::{web,App,HttpRequest,HttpServer,Responder};fn greet(_: HttpRequest)-> implResponder{"hello"}fn main(){HttpServer::new(||App::new().route("/",web::get().to(greet))).bind("0.0.0.0:8080").expect("Can not bind to port 8080").run().unwrap();}

服务端运行在一个双核的服务器上,在同一局域网段的另一个双核服务器上运行wrk作为客户端来压测:

代码语言:javascript
复制
wrk -c100 -d60s http://testserver:8080

结果如下,从好到坏排列:

  1. rust actix-web
代码语言:javascript
复制
  2 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   654.26us  226.01us  13.21ms   97.09%
    Req/Sec    73.74k     9.06k  123.48k    41.13%
  8810858 requests in 1.00m, 0.99GB read
Requests/sec: 146603.79
Transfer/sec:     16.92MB

2. rust hyper

代码语言:javascript
复制
  2 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   786.47us  273.89us  16.47ms   92.97%
    Req/Sec    63.19k     2.39k   70.41k    67.67%
  7544745 requests in 1.00m, 0.85GB read
Requests/sec: 125738.24
Transfer/sec:     14.51MB

3. openresty

代码语言:javascript
复制
  2 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   801.19us  353.80us  20.29ms   97.67%
    Req/Sec    62.05k     2.20k   67.38k    66.08%
  7409230 requests in 1.00m, 1.32GB read
Requests/sec: 123460.63
Transfer/sec:     22.60MB

4. golang

代码语言:javascript
复制
  2 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.33ms  652.90us  22.42ms   68.23%
    Req/Sec    37.89k   712.77    41.19k    76.00%
  4523628 requests in 1.00m, 522.00MB read
Requests/sec:  75392.66
Transfer/sec:      8.70MB

虽然这个小例子不算严谨,但性能结果之间的比例还是可以参考的。rust的actix-web最好,这个跟网上对actix-web的赞誉是一致的,但它唯一的缺点是在代码上还没过渡到async/await。而rust的hyper也不错,跟openresty的性能差不多,这已经让我觉得很舒服。golang性能最差,这符合我一直以来对它的性能预期。

补充一下fasthttp的benchmark:

代码语言:javascript
复制
package main

import (
    "flag"
    "fmt"
    "log"

    "github.com/valyala/fasthttp"
)

var (
    addr = flag.String("addr", ":8080", "TCP address to listen to")
)

func main() {
    flag.Parse()

    h := requestHandler

    if err := fasthttp.ListenAndServe(*addr, h); err != nil {
        log.Fatalf("Error in ListenAndServe: %s", err)
    }
}

func requestHandler(ctx *fasthttp.RequestCtx) {
    fmt.Fprint(ctx, "hello")

    ctx.SetContentType("text/plain; charset=utf8")
}
代码语言:javascript
复制
  2 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   620.40us  552.92us  42.16ms   97.75%
    Req/Sec    76.52k    16.12k   98.31k    51.25%
  9137712 requests in 1.00m, 1.17GB read
Requests/sec: 152256.88
Transfer/sec:     20.04MB

居然比actix-web还快,我在K公司也用过fasthttp,当时候的版本唯一的缺点我认为就是无法流读取request body(对于上传文件尤为重要),目前好像仍然是这样?

Streaming request body · Issue #622 · valyala/fasthttpgithub.com

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-10-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Rust语言学习交流 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档