逍遥-《Go实现的高性能http缓存服务器Jaguar》


逍遥 / 美丽联合集团技术专家

开发维护过 Winzip 等大型软件。2014年加入美丽联合集团,从无到有构建基础平台商品体系。2015年开始在蘑菇街落地基于 ATS 的静态化方案。2017年开始和小伙伴用 go 语言实现 ATS 的替代方案 Jaguar,目前已在集团内完成对 ATS的替换。热爱 Golang 和 Python ,目前正专注于 Jaguar 优化和流式任务平台 Hulk 的开发工作。



前言

       我是来自美丽联合集团电商基础的逍遥,2014年开始承担优化详情页链路的工作。2015年开始落地基于 ATS 的页面静态化方案,到2017年,详情页、店铺页、会场页等展示性系统链路都已经被此套方案覆盖。然而,在使用的过程中我们也发现了很多 ATS 的不便之处,于是从去年开始,我们成立了一个虚拟团队,用 golang 重写实现了一个 http 缓存服务器 Jaguar,今天的演讲就来向大家介绍一下这个服务器。


       今天的分享分为四个模块,第一是介绍一下业界的缓存方案,第二是 Jaguar 相关介绍,第三是分享一些实现过程中性能优化的经验,第四是介绍下具体的业务使用场景。

逍遥-《Go实现的高性能http缓存服务器Jaguar》

缓存方案

   问题描述

       首先,我相信在座的各位所在的公司应该都用过 http 缓存服务器,在整个用户访问链路中很多节点都有缓存,客户端会有客户端缓存、服务端会有集中式缓存类似 redis,也会有本机的缓存库例如 guava。今天要讲的 http 缓存服务器则是相对比较独立的一个缓存节点,虽然主要实现的一些优化效果跟其它缓存类似,但是它的优势在于不仅可以降低整个访问链路的 RT,更会对它的下游服务器起到一定的保护作用,也会节省一些带宽。

逍遥-《Go实现的高性能http缓存服务器Jaguar》

   业界方案

       在业界,http 缓存服务器有一些前辈产品,有些公司内部也会自己实现 http 缓存服务器。我们在2015年落地详情页静态化方案的时候,主要参考了三个业界比较著名的 http 缓存服务器:VARNISH CACHE,SQUID 和 apache traffic server,但由于varnish 和 squid 对动静态数据合并支持比较弱,且当时 ATS 在业界有不少成功的业界场景并且性能卓越,所以最后选择了 ATS 来实现。而 NGIMX+LUA 的方式由于可能会需要用 lua 实现一些比较复杂的功能以及性能上不及 ATS,最后没有被采用。

逍遥-《Go实现的高性能http缓存服务器Jaguar》

   ATS存在的问题

       一、ATS 本身部署起来比较麻烦,各个公司自己的运维体系集成不太方便,我们之前在应用过程中曾把它 nginx 搭配起来用,结果就导致日常 PE 维护起来比较费劲

       二、我们会存在一些需要全量失效的场景,比如大促的时候,可能过了零点之后有些数据需要失效,但是 ATS 的缓存类似一个大的 hashmap, 根据某个 pattern 来失效需要扫全表,这样的话效率非常低,例如我们站内有几千万个 K,失效功能约等于不可用

       三、功能扩展、定制化方面存在一定的制约,ATS 支持用 c plugin 和用 lua 写扩展脚本,前者实现起来难度比较大,后者提供的接口不是很全面的。

       四、ATS 本身实现起来比较复杂,不方便 debug 等定位问题,有很多代码逻辑藏得比较深导致排查问题很困难。

Jaguar介绍

   Jaguar的优势

       一、我们在业务层面支持更多数据合并的标准,比如以详情页为例,有很多数据是跟当前登陆的用户有关,比如有没有喜欢/收藏某个商品,这是动态的数据,而有些数据是不经常变动的,比如商品标题等,这些是静态的数据。前端拿到的数据,应该保证每个数据都是正确实时的。我们为了支持动静态数据合并提供了很多数据合并标准的支持。

       二、Jaguar 提供了更灵活的 lua 扩展功能, 支持更多的扩展

       三、metrics 等数据方便上报,我们开放了很多接口,可以直接嵌入进去。

      四、在一些特点的业务场景下,Jaguar 的性能好于 ATS。

       五、ATS 如果要升级协议不太方便,Jaguar 比较方便网络升级协议, 目前支持 h1/h2

   系统架构

      接下来说一下 Jaguar 的架构。上层有做请求转发的一个 Router,Jaguar 主体有一个 pipeline, cachekey (缓存 key 生成)、static (缓存)、amerger (动静态合并)valve

串联,完成正常处理流程。最下游会有一个 adapter,会有两个作用,一个是会跟底层网络框架连在一起,还有一个是方便调试。

逍遥-《Go实现的高性能http缓存服务器Jaguar》

   执行流程

       这是整个请求进来的流程,对请求处理的大流程处理和底层网络一块出于实现成本考虑我们借鉴了业界著名的 caddy 框架,之后的开源版本中我们会将这块自己实现掉 caddy 本身所有一个大的 pipeline,我们会在其中插入一个 jaguar 自己的 pipeline,来做缓存相关的操作

逍遥-《Go实现的高性能http缓存服务器Jaguar》

   部署架构

       这是我们用 Jaguar 之前基于 ATS 的架构,看起来比较复杂的圆圈里是 ATS+Nginx 的架构,需要部署相关的东西,比较复杂。

逍遥-《Go实现的高性能http缓存服务器Jaguar》

       现在替换成 Jaguar 之后链路比较清晰,服务发现框架 SLB 替代了内网 DNS,缓存未命中情况下回源也是通过 SLB ,然后 Amerger+ATS+Nginx 三个部件的进程间通信变成了Jaguar 内部的函数调用

逍遥-《Go实现的高性能http缓存服务器Jaguar》

   性能比较

       这是 ATS Jaguar 性能和功能的比较。首先100%场景下 ATS 的确会比 Jaguar 出色一点,因为它是由C++实现的,相比 golang gc 在内存等处理上会更好一些,但是在80%命中率的 esi 场景下,我们的性能是 ATS 的 1.5 倍,在80%命中率 json 场景下我们的性能是 ATS 的2倍。而网络协议升级,jesi 标准支持,缓存全量失效等功能支持也是 Jaguar 优于 ATS 的点。ESI merge、JSON merge JESI merge 大家可以理解成为这是一个动静态数据合并的标准。

逍遥-《Go实现的高性能http缓存服务器Jaguar》

   业务功能

       Jaguar 业务功能有:

       一、esi/jesi/json 多种数据合并规范。

       二、Lua 扩展可定制化。

       三、多域名多 pattern 支持,配置按 nginx 配置风格。

       四、预留服务发现&metric 上报接口,方便扩展

       

        esi 合并标准页面里面出现多个分散动态数据场景,就会发多个请求,jesi 会把多个请求合并,然后在后端进行重组。

逍遥-《Go实现的高性能http缓存服务器Jaguar》

       下面说明下 lua 扩展是如何插入到处理链路中去请求进来的时候,通过 remap 节点,可以通过 lua 扩展去修改请求的 header cookie 等数据,在 cachekey 生成可以通过 lua 扩展修改缓存 key,在获取到缓存之后和回写 response 等节点也可以通过 lua 扩展修改上下文数据。

逍遥-《Go实现的高性能http缓存服务器Jaguar》

   为什么要用 golang 来实现?

       一、之后可能会调整站内网络协议栈,比如升级到 h2,QUIC 等,golang 写相关代码比 c java 来得方便

       二、站内网关选择了 caddy 做二次开发,插件化的架构更加合理,而且本身实现了 h2https 相关功能

      三、简单,语法上单直白,没有 java 废话多,也有 C++ 过分复杂指针操作

     四、能卓 nodejs, java, lua, golang 实现json merge 核心代码 golang 性更突出

      五、test, profile 等工具简单够用,部署非常简单就一个可执行文件

       六、golang 程序多核系统下有一定优化


性能优

   Json Merge 优化

      接下来说一下之前一直提到的动静态数据合并。这是一个 Esi Merge 场景,页面静态数据中有一个 esi 标签,当请求进来时,会从缓存中要取这部分数据,Esi 引擎会解析 esi 标签中带有的动态数据回源的 url,然后到后端取到对应的数据,这个数据取到之后会进行简单的纯文本替换,最后出来这样一个数据返回给前端。这个标准在早期 html 页面用起来还是比较方便的,页面比较简。但在当今很多 SPA 等复杂页面场景出现的情况下就显得比较死板,性能上也不甚理想

逍遥-《Go实现的高性能http缓存服务器Jaguar》

       所以我们现在提出 Json Merge 标准,因为现在很多 h5 或者客户端页面都是通过后端的 json 数据来进行页面渲染的如果 json 格式的动静态数据合并由前端和客户端同学来做,会涉及到不同语言实现的细节上有差异,而且这个逻辑交给前端同学做逻辑上也不太合适,对他们来说这个步骤应该是透明的。

逍遥-《Go实现的高性能http缓存服务器Jaguar》

       这边对算法做一下描述:

一、有静态数据 StaticData,动态数据 DynData 两块数据,都为 string 类型

二、反序列化 StaticData DynData

三、广度优先遍历 StaticData 节点,获取某个节点对应的 DynData 中的数据,如果存在并且为 array 或者基础数据类型(int, string...)则新值覆盖

四、对于节点为 Object 的对象采用递归的方式重复3步骤

五、序列化 merge 后的数据结构为字符串返回


最开始用 golang 的一些 json 库写了算法实现,压测下来性能都不太理想。跑 profile 发现主要的损耗在反射这一块。

逍遥-《Go实现的高性能http缓存服务器Jaguar》

然后我们就开始转变问题的解决思路,其实我们并不需要把它当做一个普通的 Json 问题来处理,这其实就是一个有限状态机处理的问题,可以转化为字符串处理的问题,这样算法复杂度页降低到了 O(n)。经过试验之后,我们发现每次合并处理的时间会降到原来的四分之一。吞吐量都上去了。

逍遥-《Go实现的高性能http缓存服务器Jaguar》

逍遥-《Go实现的高性能http缓存服务器Jaguar》

       简单的总结一下这块的优化经验

      一、 基本去除了反射,在解析 dynData 时还是用了 gjson 来读,因为需要把它转化为一个类似 hashmap 的东西,但其中也几乎没有反射代码

        二、 尽量减少 value 的展开,比如某个 value 预判下来是 object 的情况,如果 dynData 是中该位置的 value 为空,则可以直接写入 buf,反之如果 dynData 中的 key staticData 中不存在,则可直接将 dynData 中的 value 写入 buf

      三、 字符串尽量用 slice 的方式提取,可以在遍历的时候记录下索引,因为用 buf WriteByte 有很大的性能开销

      四、用 bufio bytes.Buffer 包一下,后者的 Write 方法中都会有一个试图增长 buf 的函数,开销比较大,而 bufio 包装过后,最终 flush 一把性能提升比较明显


逍遥-《Go实现的高性能http缓存服务器Jaguar》

   Lua优化

       Lua 优化是在我们开发过程中第二个比较重要的优化点,在请求处理的各个节点 Jaguar 都会预埋一些 Hook 点,lua 脚本会在 hook 点执行一些操作,第一个版本上去之后发现性能差不多下降一半。后来发现,GC 在某个节点会变得特别频繁,最终导致到某个点之后系统会崩掉。经过分析之后,找到主要原因是因为最开始每次请求进来会构造一个 LState 对象,那个对象是比较大的一个对象,但是它长时间都不会被回收掉慢慢就会导致 GC 频繁。最后我们想到了一个办法,利用池化技术,在整个程序启动的时候会构建一个 LState Pool,每次请求进来我们会从这里面去取,用完之后归还给 pool采用这种方式后,性能上面的影响就比较少了

逍遥-《Go实现的高性能http缓存服务器Jaguar》

逍遥-《Go实现的高性能http缓存服务器Jaguar》

应用场景

   详情页

       接下来说一下我们的使用场景。这是一个很典型的蘑菇街详情页,页面中会包含一些动态的数据,比如像 SKU 数据,商品的库存、商品的喜欢状态,这些数据要实时的。而另一些比如标题和图文详情描述这些数据变化是不太频繁的,我们会定义为静态数据。这个场景就会用到 json 动静态数据合并。

逍遥-《Go实现的高性能http缓存服务器Jaguar》

       这是整个详情页从用户访问的链路图,我们曾经尝试过一个二级 CDN 的方案,比如在华南、华北区域部署一些 CDN,来加速用户的访问,但因为最终机房还是在江浙沪这一带,就会导致动态数据回源成为一个瓶颈点,导致二级 CDN 效果并不好,所以我们现在暂时把二级 CDN 去掉了。但是站内像大促会场场景,我们还是在用二级 CDN 方案。还有,缓存会对应一个失效逻辑,现在失效模式有两种,一是主动发 purge 请求到 jaguar 上,也可以配一个缓存失效时间。我们有一个失效中心,它会监听一些关键事件的消息,比如商品表、店铺表的变更 binlog 消息,会触发静态化服务器某些缓存失效(精确的 url or pattern)

逍遥-《Go实现的高性能http缓存服务器Jaguar》

       这是一典型的详情页的 Jaguar 缓存配置,我们可以配置缓存的时间,提取哪些参数作为缓存 key 的一部分等

逍遥-《Go实现的高性能http缓存服务器Jaguar》

   魔方系统

       第二个要介绍的场景是和魔方系统的配合,大多数电商网站每次大促或者日常活动期间都会有一些活动页。我们有一个魔方系统可以用它来把页面和数据绑定起来,渲染完之后会生成一个活动页面。这个生成的页面会最终 push jaguar 上缓存起来,这个场景就不存在动态数据了。

逍遥-《Go实现的高性能http缓存服务器Jaguar》

逍遥-《Go实现的高性能http缓存服务器Jaguar》

       魔方系统用到了 lua 扩展的功能,主要要实现的功能是根据请求的 User-Agent 对客户端浏览器进行归类,以便返回不通的页面数据。因为我们的浏览器很多,有些浏览器其实是 android ios 中的容器等需要进行归类

逍遥-《Go实现的高性能http缓存服务器Jaguar》

手头事

       一、针对小公司并没有特别好的前后端分离的场景,所以之后我们会支持简单的模板,和前端模板引擎相结合,把简单的页面渲染,渲染完之后直接放 jaguar 去。

       二、进行 H2 协议 QUIC 协议的内测。

       三、是集中式缓存 (eg. redis) 等对接

       四、开源版本会有两个,一个是作为 caddy 插件,一个是一个 all-in-one 版本。

       五、控制台相关功能优化。

       

       这是我们站内在做的接入层相关的规划,会 waf 等插件做一些结合,再做接入层相关的一些改造。

逍遥-《Go实现的高性能http缓存服务器Jaguar》

今年规划

       这是今年的一些规划,从去年年底开始,我们预计今年3月份主体代码完成,7月份站内替换完成,9月份会出一个社区版本。

逍遥-《Go实现的高性能http缓存服务器Jaguar》

Q&A


提问:我之前做移动开发,有一个问题是想问关于动态数据和静态数据,为什么会有这个需求?你说的静态数据是文件形式的,还是说这个数据是在两个不同的库里面?

   

逍遥:在规模比较小的情况下可以完全不区分,也可以完全不接入,请求直接打到后端,每次去调内部的缓存或者直接 db 去取数据,这是在请求量比较小的时候。但是在请求量比较大的时候,尤其是有些热点数据,如果每次都要穿透到后端服务器,会导致后端服务器或 DB 等资源的浪费,http 缓存服务器完全可以把一些数据直接缓存起来直接返回。

   

提问:静态数据存在哪里

   

逍遥:目前默认的数据内存中的,我们根据 boltdb 这个 lib 做了一些改造,增加了一些容量控制和 lru 等算法的实现。

   

主持人:你现在在做的这个东西,ATS 里面都是支持的?

   

逍遥:有相当一部分功能 ATS 是不支持的,比如 json merge, jesi 等,ATS 更像是一个通用的框架,业务上的定制会少很多

   

主持人:另外一个问题,你说你们现在这个也支持 lualua 是通过什么方式调用的?

   

逍遥:其实是用了 golua 这个库,但是我们现在对这个库进行了改造,因为原生来讲这个库的性能并不理想

   

主持人:因为我了解下来的 go lua 都是基于 cgo 的模式去调用,但我特别讨厌 cgo

   

逍遥:对,所以如果是一些通用的业务场景,我们会把它沉淀在核心代码里面 golang 来实现,但是我们也在考虑用 golang 原生去写 lua 的解释器,来提升性能。