大家都知道如果做 Web,Web 只须要水平伸缩、扩展就行,那么为什么还要高性能呢?事实上有些做事是不适宜水平伸缩的,比如有状态的做事。我盘点了过去几年利用过的做事,创造有状态的数据库做事的确很多:
统一集中式的缓存 Redis高性能的行列步队 Kafka传统的关系型数据库 MySQL、Postgres新兴的非关系型数据库 MongoDB、ElasticsearchEmbedded 数据库,属于内嵌数据库,跟做事配置在一起的 SQLite3、H2、Boltdb最新的时序数据库 InfluxDB、Prometheus 等上面这些做事实际上都是有状态的,它的扩容、伸缩不是那么大略,一样平常因此 Sharding 的办法通过人工操作手动做伸缩。
而根本做事和平台做事是全体公司的做事所依赖的,一家公司的做事可能有成千上百台机器,但是根本做事该当只占很小的比例,以是我们对根本的通用做事有高性能需求,比如 Gateway 网关、Logging、Tracing等监控系统,以及公司级别的用户的 API、Session/Token校验,这类做事对性能有一定哀求。

此外,水平扩展是有限度的,随着机器的增多,机器供应的容量、QPS 不是一个线性增长的过程。
高性能的好处,我认为有以下几个方面:
本钱低,运维大略:伸缩容不敏感,一台机器可以扛多台机器的量;便于流量分发:机器少可以便于流量分发,支配快意味着回滚快;面对完备不兼容,或者对极度的流量平滑迁移有需求的情形会用到红绿支配;简化设计:运用内的缓存会更高效,按机器纬度可以做一些大略的 ABTest;“程序员的自我教化”。如何做高性能做事绝大部分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 密集的问题。
目前市情上主流的异步措辞和框架包含:
C 和 C++,一样平常不会拿来写 Web 运用;PHP(swoole),这个生态相对来说没有那么好;Java,近些年借助 Spring Cloud、Gateway 又火了一把,但还是一个比较低级的状态,还是在那种 then/onError 结合子的状况下面去编程;NodeJS,谈异步肯定离不开 JS 和 NodeJS,JS 生态是异步的生态,它没有同步的壅塞。从开始设计到现在,经由这么多年景长,从最开始回调,逐渐蜕变成为 Promise 类库的加持,再到 NodeJS 的 async/await、IO 这类方便用 generate 写异步代码的状态,async/await 的模型也是当前全体业界比较认可和推崇的写异步的办法;Python,Python 经历了从最开始的 yield 做 generate,到 yield+send 做成协程的状态,再到 yield+from 加 generate 做流式的处理,到现在 3.x 版本的 asyncio 纳入官方库的过程;Rust,Rust 是种新兴措辞,它基于内存模型的分外,选择了 pull 模型做异步的实行。它是在 1.39.0 的时候即今年 9 月份,async/await 这种模式才是一个 stable 的状态,一个async/await 的 MVP 最小可用的产品,而 tokio 作为官方 de-facto 的 runtime 是在 3-6 个月稳定,以是 2020 年上半年可能可以看到一波用 Rust 去写 Web 程序的浪潮;Golang,是用 Goroutine 去做切换,不同于其他,它看起来是一个同步代码,实际上是在后面的 runtime 做异步调度的形式。那为什么还要学 OpenResty 呢?这里首先是限定在 2015 年,本日要讲的实践大约是在 2015-2016 年做的,可能韶光上没有这么新了。但在 2015 年,上述这些异步除了 JS,其他的成熟度没有那么高,以是 在当时 OpenResty 的确是很好的做异步的框架。
什么是 OpenRestyNginx
Nginx 是一流的反向代理做事器,它有完善的异步开源生态,且在一线互联网公司已经成为标配,是已经引入的技能栈。因此在 OpenResty 根本上再引入一些东西是风险极低的事情,只是引进来一个 Module,就可以复用已有的学习本钱。
此外,Nginx有天然的架构设计:
Event DrivingMaster-Worker ModelURL Router,不用再选路由库,本身在 config 里就很高效Processing Phases,关于对要求的处理流程这一点只有在利用 OpenResty 后才能感想熏染到Lua
Lua 是一个小巧灵巧的编程措辞,天生和 C 亲和,支持 Coroutine。除此之外它还有一个很高效的 LuaJIT 实现,供应了一个性能很好、很高的 FFI 接口,根据 Benchmark 这种办法比手写C 代码调 C 函数还要快。
OpenResty
OpenResty 是Nginx+Lua(JIT),它是两者完美的有机结合,把异步的Nginx 生态用 Lua 去驱动。Lua 本身不是一个异步生态,这里提一下春哥为什么要去做 OPM 和包管理,实在是由于 luarocks 是同步的生态,以是 OpenResty 很多包都是 lua-resty 开头的,表明它是异步的,能在 OpenResty 上利用。
OpenResty 运用实践:WebBeaconWebBeacon 概述
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 里初始化都会重复这个过程。在这个项目中,每次打开和关闭只是为了写一条日志进去,这就会使性能变得很差。
面临的寻衅
主要程度很高,很长一段韶光,全体链家网的大数据部门唯一生产资料便是源于此做事,如果没有这个做事,就没有后面所有数据统计打算和报表的输出,对付下贱的依赖方,这个做事主要度相称高。高吞吐状态,所有的 PC 站、M 站、安卓、iOS、小程序乃至内网做事都可以往这个做事上打,以是对吞吐有一个非常高的哀求。这个做事有业务,它要去到不同的地方取信息,再做一些转换,末了落地,肯定须要有一个灵巧的编程的东西可以搞定它。资源隔离性差,当时链家网的全体做事是一个稠浊支配的状态,只靠操作系统的隔离性在坚持,以是会涌现多个做事跑在同一台机器上的情形,对 CPU、内存、磁盘等有争用的状态。坚持的原则
性能 Max,性能越高越好;避免对磁盘的(硬)依赖,由于稠浊支配的缘故原由,希望它能够在关键路径上乃至从头到尾能够避免对磁盘的依赖;即刻响运用户,一个 WebBeacon 的做事,实际上核心是落数据,用户可以直接相应,不须要等,落数据可以放在后台做,不须要卡着用户的要求。我们希望有一个机制,能够拿到要求,直接返回一个要求,后面再做业务重的部分;重构成本尽可能低,这个项目已经有初版了,虽然是重构,但相称于重写了一版。我们希望它能在这个过程中耗时越短越好,在原有的根本上能够继续下来所有的功能点,同时有自己新的特点。主体逻辑
上图是OpenResty 的阶段图,写过 OpenResty 程序的人都会知道它是非常主要的东西,是 OpenResty 的核心流程。常日大家在 Content generatedby?阶段会用 upstream 去做 balancer,但实际在做 Web 运用时,直接写 content_by_lua 就可以,由于是你直接来输出,同时 access 和 header 两个阶段做一些数据的搜集,通过 log_by_lua 这个阶段把数据落地,达到我们即刻响运用户的需求。content_by_luacontent_by_lua实际上很大略,便是无脑地吐固定的内容:
如上图,声明 Content-Length=43,如果不声明则默认是Chunked 模式在我们的场景里是没故意义的;z 是 Lua5.2 Multiline String 的一种写法,它会把当前的换行和后面的空缺字符全部截取掉,然后拼回去。
access_by_lua
我们在 access_by_lua 的过程中做了几件事情:
首先是解析 cookie,要去记录和下发一些 cookie,我们利用 cloudflare/lua-resty-cookie 的包去解析 cookie。
然后天生uuid 去标识设备 ID 或者要求 ID 等一系列这种随机的状态,我们用了 openssl 的 C.RAND_bytes,随机天生了 16 个 bytes、128 bit,然后用 C.ngx_hex_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 条不一样的埋点日志再落地。为此我们做了以下的事情:
限定 Max body,由于不肯望落磁盘,以是设置了 64K 的大小。考虑到我们有 GZip,可能之前有 400k-500k 的状态,汇总搜集后该当是足够利用了;客户端编码,做事端自然要去解码。通过 zlib 去解 Gzip,然后 decode URL,再做 json_decode,终于把一个要求拆成了 N 个,一个 list 下面包含 N 个要求的状况,把每一个 list item 重新编码成埋点日志再落回去;用 table new,table.new(#list,0)->table.new(0,30)/table.clear,然后做 json_encode、ngx.escape_url 等操作,末了形成单条的日志;假设在这个过程中涌现任何问题就会做降级,用 log_escape 把一个 raw body 落到了日志里面,这样后期还是有能力去做找回。header(body)_filter_by_lua
还有一个逻辑是搜集、汇总字段的过程。
为什么不在 access_by_lua 的时候一起做了?缘故原由是我们在压测时创造在高并发压力的情形下,某个操作在 access_by_lua 阶段会发生 coredump。也有可能是我们用的办法不对,本身就不应该用在 access_by_lua 段,这块儿没有穷究。
以是我们把一部分过滤阶段与当前要求没有太大关系的业务挪到了header_filter_by_lua 的过程中,我们有解X-Forwarded-For 去落 IP,有解lianjia_token 去落 ucid,链家里的 ucid是一个长串的数字。Lua 里面的数字有 51 位的精度,这意味着这块数字没办法落下来,于是我们利用 FFI 去 new 一个 64 位的 URL 的 number,并做一系列的变换,再通过打印截取的办法落出来想要的 20 多位的 ucid 的长度。
log_by_lua 备选方案
下面先容最核心的一环即 log_by_lua 落日志的过程。
access_log 是 Nginx 原生内置的办法,这种办法不适用我们的场景,由于我们有 TB 级别的日志量,并且是稠浊支配,以是磁盘是争用的状态;最核心的一点是 Read 和 Write 是壅塞的操作系统调用,这会让它的性能急剧低落;张德江写的 doujiang24/lua-resty-kafka 是另一个办法,这个实在我们并没有做调研,由于 kafka 的协议和特性太多太繁杂,这个方案当前特性和后续发展可能没法知足我们的需求。我们末了选定了 cloudflare/lua-resty-looger-socket 的库,可以远程非壅塞地落日志。rsyslog
落日志的工具我们选择 rsyslog,实际上并没有做太多的技能选型,直接在原来的技能选型上做了定制和优化:
单条日志最大长度是 8k,超长的单条日志它会截断,这是 rsyslog 里面的规范,但是在它里面也可以做定制;rsyslog 内部通过 Queue 通报,内部 Queue 用得非常重。我们选取了 Disk-Assisted Memory,它能在 Memory 爆掉时落地到磁盘做降级,担保它的可靠程度;Parser 没有用新版本的 RFC,而是选用老版本的 RFC3164,是很大略的一个协议,如上图, local msg=\"大众<142>\"大众..timestamp..\公众\"大众..topic..\"大众:\"大众..msg 便是 RFC3164 的规范日志;我们有一条日志落多条的需求,日志和日志之间要做切割,这里实在开启了 SupportOctetCountedFraming 的参数做切割,实际上便是在当前的 message 的头上再加上 message 的长度,通过一个根本的 RFC,加上额外的功能特性完玉成部日志的落地;用 imptcp 去建立 unix socket 的文件句柄,没有用 TCP、UDP,由于 unix socket 的损耗会更低。值得把稳的是,cloudflare/lua-resty-looger-socket 的库不支持 dgram unix socket,目前 cloudflare 已经停滞掩护这个库了;输出用了 omkafka 的 output module,开启了两个分外的操作 :开启 dynaTopic 可以定制录入到制订 topic;开启 snappy 的压缩可以减轻传输流量;额外开启两个 module:一个是 omfile,它接到 imptcp 传过来要求,在推 kafka 的同时落地到本地,这么做的缘故原由是之前下贱推送程序宕机导致 kafka 里的没有及时被拿走而造成数据的丢失,因此须要做主要数据的备份,再加上磁盘的依赖;另一个是 impstats,它是做监控,输出 Queue 容量、当前的消费状况等一系列数据。支配方案前面提到我们是稠浊支配的状况,线上会有准入的标准: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。
总结总的来说,架构是第一位的,我们利用 OpenResty+rsyslog+Kafka 三款久经磨练的组件构建出一个高性能的做事;要保持边界,守住底线,比如不落磁盘,就要想尽统统办法不落磁盘,担保有极致的性能;有时候你看起来很高性能,但是代码写起来还是会有很多坑,你可以通过火焰图等很多办法来避免。主要的是,你须要故意识地去把握性能的问题,从一点一滴的小事情做起。比如 NYI 的 not yet implement 方法要避免去利用;比如 table.new/table clear一次性预分配已知大小 table array 的操作,都要避免重复的、多次的动态扩充;ngx.now 的实现性能非常高,它是有缓存的,基本知足对韶光精度的哀求;uuid 的天生,从 libuuid 转换成一次性天生 16 字节数据;通过 shared dict 之类的减少 CPU 的操作等等,末了我们可以构建出一个高性能的 Web 做事。分享自:https://segmentfault.com/a/1190000020728562