2019 年 8 月 31 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙·成都站,原贝壳找房基础架构部工程师尹吉峰在活动上做了《使用 OpenResty 搭建高性能 Web 应用 》的分享。
OpenResty x Open Talk 全国巡回沙龙是由 OpenResty 社区、又拍云发起,邀请业内资深的 OpenResty 技术专家,分享 OpenResty 实战经验,增进 OpenResty 使用者的交流与学习,推动 OpenResty 开源项目的发展。
尹吉峰,原贝壳找房基础架构部工程师,多语言爱好者,偏向异步和函数式编程,酷爱原型搭建,先后在贝壳使用 OpenResty 搭建了 WebBeacon、图片处理等服务。
以下是分享全文:
今天和大家介绍一个 OpenResty 比较小众的使用场景,使用 OpenResty 做 Web 框架写服务,希望能给大家带来一些新的东西。
首先给高性能 Web 服务一个简单定义:QPS 过万的服务是高性能 Web 服务。我认为一个好服务绝对不是优化出来的,架构决定一个服务的基准,过早优化是万恶之源。
大家都知道如果做 Web,Web 只需要水平伸缩、扩展就行,那么为什么还要高性能呢?事实上有些服务是不适合水平伸缩的,比如有状态的服务。我盘点了过去几年使用过的服务,发现有状态的数据库服务的确很多:
上面这些服务实际上都是有状态的,它的扩容、伸缩不是那么简单,一般是以 Sharding 的方式通过人工操作手动做伸缩。
而基础服务和平台服务是整个公司的服务所依赖的,一家公司的服务可能有成千上百台机器,但是基础服务应该只占很小的比例,所以我们对基础的通用服务有高性能需求,比如 Gateway 网关、Logging、Tracing等监控系统,以及公司级别的用户的 API、Session/Token校验,这类服务对性能有一定要求。
此外,水平扩展是有限度的,随着机器的增多,机器提供的容量、QPS 不是一个线性增长的过程。
高性能的好处,我认为有以下几个方面:
绝大部分Web 应用实际上都是 IO 密集型服务,而非 CPU 的密集型服务。对 CPU 的性能,大家可能没有一个直观感受,这里给大家举个例子:π 秒约等于一个纳世纪,说的是人在观察世界的时候是以秒级的尺度。而 CPU 的是纳秒级别的尺度。对于 CPU 来说,3.14 秒相当于人类的一个世纪这么漫长,1 秒就相当于 CPU 的“33 年”。如果是 1-2个毫秒,我们觉得很快,但对于 CPU 实际上是它的“20 天左右”。
而这仅仅是单核,实际上还有多核的加持。面对这么强的 CPU 性能,怎么充分利用呢?这就有了 2000 年以来的新型编程模式:异步模型,也叫事件驱动模型。异步编程、事件驱动,是把阻塞的、慢的 IO 操作转化成了快的 CPU 操作,用通过 CPU 完全无阻塞的事件通知机制来驱动整个应用。
我自2012 年在手机搜狐网接触 Python Tornado 异步编程以来,用过多个语言和框架做异步编程。我认为同步模型就是“线程池 频繁的上下文切换 线程之间数据同步的大锁”,这意味着如果是同步模型,需要根据当前的 loading 去做系统的调优,根据目前的各个状况,一点一滴去做,实际上调优难度是非常大的。
而异步模型相当于一个高并发的状态,因为它是EventLoop,是在不停循环的。同时有两个请求过来,它可能同时会触发,如果其中一个请求操作 CPU 没有及时让出,就会影响另一个请求。它用潜在的时延换来更高的并发。高性能就相当于“异步 缓存”,即异步解决 IO 密集、缓存解决 CPU 密集的问题。
目前市面上主流的异步语言和框架包含:
那为什么还要学 OpenResty 呢?这里首先是限定在 2015 年,今天要讲的实践大约是在 2015-2016 年做的,可能时间上没有这么新了。但在 2015 年,上述这些异步除了 JS,其他的成熟度没有那么高,所以 在当时 OpenResty 的确是很好的做异步的框架。
Nginx
Nginx 是一流的反向代理服务器,它有完善的异步开源生态,且在一线互联网公司已经成为标配,是已经引入的技术栈。因此在 OpenResty 基础上再引入一些东西是风险极低的事情,只是引进来一个 Module,就可以复用已有的学习成本。
此外,Nginx有天然的架构设计:
Lua
Lua 是一个小巧灵活的编程语言,天生和 C 亲和,支持 Coroutine。除此之外它还有一个很高效的 LuaJIT 实现,提供了一个性能很好、很高的 FFI 接口,根据 Benchmark 这种方式比手写C 代码调 C 函数还要快。
OpenResty
OpenResty 是Nginx Lua(JIT),它是两者完美的有机结合,把异步的Nginx 生态用 Lua 去驱动。Lua 本身不是一个异步生态,这里提一下春哥为什么要去做 OPM 和包管理,其实是因为 luarocks 是同步的生态,所以 OpenResty 很多包都是 lua-resty 开头的,表明它是异步的,能在 OpenResty 上使用。
WebBeacon 概述
Beacon 是一个埋点的服务,记录 http 请求,做后续的数据分析,从而计算出会话数量、访问时长、跳出率等一系列的数据。
dig.lianjia.com 是WebBeacon 服务的前端,它负责支持数据的搜集,不负责数据的处理和计算。它还负责接收 http 请求,返回 1×1 的 gif 图片;接收到了 http 请求后,会有一个内部的格式称作为 UAL(Universal Access Logging),这是统一全局的访问日志格式,把http 请求相关的信息落地推到 Kafka,就可以做实时或者离线的数据统计,这就是整个服务的概况。
我们接手时已经有一个版本,第一个版本是用 PHP 实现的,性能不是很好。它是 FastCGI PHP Logging to file 直接接收到请求,PHP 写到文件里,再由 rsyslog imfile,file 的 input module 去读取日志文件,通过 Kafka 的 output module 去推到 Kafka 的一个状况。
举个例子,为什么 PHP 性能不高呢?根据 PHP 的请求模型,为了防止内存泄漏,你是不需要关注资源释放的。每个请求开始时打开日志文件,然后写入日志,请求结束时自动关闭,只要不是在 Extension 里初始化都会重复这个过程。在这个项目中,每次打开和关闭只是为了写一条日志进去,这就会使性能变得很差。
面临的挑战
坚持的原则
主体逻辑
上图是OpenResty 的阶段图,写过 OpenResty 程序的人都会知道它是非常重要的东西,是 OpenResty 的核心流程。通常大家在 Content generatedby?阶段会用 upstream 去做 balancer,但实际在做 Web 应用时,直接写 contentbylua 就可以,因为是你直接来输出,同时 access 和 header 两个阶段做一些数据的搜集,通过 logbylua 这个阶段把数据落地,达到我们即刻响应用户的需求。contentbylua*content_by_lua实际上很简单,就是无脑地吐固定的内容:
如上图,声明 Content-Length=43,如果不声明则默认是Chunked 模式在我们的场景里是没有意义的;z 是 Lua5.2 Multiline String 的一种写法,它会把当前的换行和后面的空白字符全部截取掉,然后拼回去。
accessbylua
我们在 accessbylua 的过程中做了几件事情:
首先是解析 cookie,要去记录和下发一些 cookie,我们使用 cloudflare/lua-resty-cookie 的包去解析 cookie。
然后生成uuid 去标识设备 ID 或者请求 ID 等一系列这种随机的状态,我们用了 openssl 的 C.RANDbytes,随机生成了 16 个 bytes、128 bit,然后用 C.ngxhex_dump 转化,再一点一点切成 uuid 的状态。因为我们内部大量使用 uuid 的生成,所以这块希望它的性能越高越好。
除了uuid 还有 ssid,ssid 是 session ID,session 是记录会话的数量。在 WebBeacon 里,会话是指用户在 30 分钟内连续地访问。如果一个用户在 30 分钟内持续访问我们的服务,则认为它是一个会话的状态;超 30 分钟 cookie 过期了,会重新生成一个新的 cookie,即是一个新的会话。此外,这里的统计是不跨自然天的,比如晚上 11:40 分,一个用户进来,会只给他20 分钟的 cookie,从而保证不跨自然天。
除了以上这些,我们还有一个最重的业务逻辑。
手机端浏览器每次发请求都会有成本,我们做了设定,将手机端搜集的日志打包一起上传,把多条的埋点日志汇总,当用户按 home 键退出或放到后台时触发上报流程,以 POST 的形式去上报,POST 时同时做编码,加上 GZIP,它的流量损耗会很低。这意味着我们要去解析 body,去把一条上报上来的 POST 请求拆成 N 条不一样的埋点日志再落地。为此我们做了以下的事情:
header(body)filterby_lua
还有一个逻辑是搜集、汇总字段的过程。
为什么不在 accessbylua 的时候一起做了?原因是我们在压测时发现在高并发压力的情况下,某个操作在 accessbylua 阶段会发生 coredump。也有可能是我们用的方式不对,本身就不应该用在 accessbylua 段,这块儿没有深究。
所以我们把一部分过滤阶段与当前请求没有太大关系的业务挪到了headerfilterbylua 的过程中,我们有解X-Forwarded-For 去落 IP,有解lianjiatoken 去落 ucid,链家里的 ucid是一个长串的数字。Lua 里面的数字有 51 位的精度,这意味着这块数字没办法落下来,于是我们使用 FFI 去 new 一个 64 位的 URL 的 number,并做一系列的变换,再通过打印截取的方式落出来想要的 20 多位的 ucid 的长度。
logbylua 备选方案
下面介绍最核心的一环即 logbylua 落日志的过程。
rsyslog
落日志的工具我们选择 rsyslog,实际上并没有做太多的技术选型,直接在原来的技术选型上做了定制和优化:
部署方案
前面提到我们是混合部署的状况,线上会有准入的标准:tar 包 run.sh
。我们预先编译好 OpenResty,把所有依赖静态地编译进去,在发布时把 OpenResty 的二进制文件拉到本地和 Lua 脚本混在一起,rsync 到固定的位置上再跑起来,使用 supervisord 或者 systemctl 做线上的daemon管理。
测试环境是自我维护的,这个项目可以分成两块,一块是 OpenResty,另一块是 rsyslog。可能你会把 OpenResty 的所有代码落到代码仓库,而忽略了实际上 rsyslog 的配置也是相当重要,所以其实应该把所有的项目相关的东西都落到代码仓库里。我们通过 Ansible 去做剩下的所有事情来管理测试环境。
性能数据
最终的性能数据如下图:
2018 年初我做了统计,QPS 峰值大概是 26000QPS,单机压测的峰值在 30000QPS,所以其实一台 OpenResty 的机器可以抗住整个埋点的流量。日志传输压缩之后大概有 30M,每天的日志落起来大概在 10 亿条左右。为了保证服务的可靠性,当时线上的服务器是三台 EC2 C3.2xlarg。
以上是我今天的全部分享,谢谢大家!
分享PPT下载及现场视频:
使用 OpenResty 搭建高性能 Web 应用
本文由博客一文多发平台 OpenWrite 发布!