在说varnish前,先说说http缓存

http缓存

缓存为何能加速

根据Memory Hierarchy理论,按照离CPU由近到远的顺序依次是CPU寄存器、Cache、内存、硬盘,越靠近CPU的存储器容量越小但访问速度越快,以下摘自维基百科.
存储层次可分如下四层:

  1. 寄存器–可能是最快的访问。在32位处理器,每个寄存器就是32位。x86处理器共有16个寄存器。
  2. 高速缓存(L1-L3: SRAM)
    第一级高速缓存(L1)–通常访问只需要几个周期,通常是几十个KB。最快访问速度到 700 GiB/second

第二级高速缓存(L2)–比L1约有2到10倍较高延迟性,通常是几百个KB或更多。 最快访问速度到 200 GiB/second
第三级高速缓存(L3)(不一定有)–比L2更高的延迟性,通常有数MB之大。最快访问速度到 100 GB/second
第四级高速缓存(L4)(不普遍)–CPU外部的DRAM,但速度较主存高。最快访问速度到 40 GB/second

  1. 主存(DRAM)–访问需要几百个周期,可以大到数十、百GB。最快访问速度到 10 GB/second
  2. 磁盘存储–需要成千上百个周期,容量非常大。固态硬盘最快访问速度到 2000 MB/second
    11.png

程序是由指令和数据组成,假如需要处理的数据存储在上述越离cpu近的地方,理论上程序运行的速度会越快,由于资源的有限,通常静态的可缓存数据缓存在内存或者ssd高速硬盘中,因此缓存可以提升程序的运行时间。

缓存注意事项

  1. 缓存生命期:根据实际的业务情况调整缓存的存活期,避免缓存数据失效
  2. 缓存空间耗尽:当有效期的缓存占慢空间是,一般是采用LRU(Least Recently Used)即最近最少使用算法,算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。所以当LRU运行频繁时,说明需要扩充缓存空间了。
  3. 缓存命中率:缓存命中率是体现缓存有效性的一个重要指标,hit/(hit+miss)*100%

http缓存周期控制

http的响应报文首部定义

通过过期日期来控制:

  1. HTTP/1.0响应报文首部中的过期日期机制:
    Expires:Mon, 25 Sep 2017 01 :00 :31 GMT 指明了具体过期绝对时间
  2. HTTP/1.1响应报文首部中的过期日志机制:
    Cache-Control:max-age=100 max-age指明了一个相对时长,可缓存多少秒
  3. 通过文件的内容变化判断
    ETag:内容的扩展标签,提供给前端缓存,用于验证缓存的内容与实际服务器上的内容是否一致

通过Cache-Control控制相关
Cache-Control可以在请求报文控制,也可以在配置在响应报文

  • public:可以放在公共缓存上,公共缓存是指除了客户端的自身浏览器的缓存,其他任何位置都是公共缓存
  • private:可以放在私有缓存上进行缓存
  • no-cache:表示能缓存,但是当客户端下次请求同样的资源时,不能直接用缓存的内容响应给客户端,而是要到原始服务器验证缓存的有效性
  • no-store:不可缓存,表示响应的内容不允许缓存
  • must-revalidate:必须重新校验,表示内容缓存下来后,当用户请求同样的内容时,必须要原始服务器上进行校验缓存有效性
  • max-age:可缓存的时长,相对时长,也就是从此刻开始,可以缓存多少秒
  • s-max-age:公共缓存服务器的缓存,可缓存的时长。控制公共缓存最大有效期,如果没有公共缓存的定义,也能使用私有缓存中的数据(也就是s-max-age可以不用指明,但是如果没有指明,如果公共缓存中的数据需要缓存,缓存的有效时长就取决于max-age)

http请求报文首部

基于时间的条件式请求:mtime: if-modified-since|if-Unmodified-Since

  • 相当于客户端发送请求,缓存服务器收到请求后,如果缓存服务器缓存有请求的资源,不会直接使用该资源响应用户请求,而是缓存服务器去向原始服务器询问,是否有该资源,如果没有资源,则证明缓存的已经没用了,就返回给客户端没有对应的资源;如果实际服务器上有对应的资源,缓存服务就将自己缓存的资源的时间戳提供给原始服务器,询问原始服务器上资源在此时间戳之后有没有发生过修改,如果没有发生过修改,原始服务器则返回304,Not Modified给缓存服务器,证明缓存服务器上的资源与原始服务器的资源是一致的,如果在时间戳之后发生过修改,则原始服务器将修改后的资源重新发送给缓存服务器,缓存服务器重新缓存,然后发送给客户端。

基于扩展标签的条件式请求 :ETag : if-none-match|if-Match

  • 想象此类情况:当请求的资源的时间戳发生了改变,但是文件内容却没有发生改变(如touch现有的文件),但是其内容是有效的缓存内容,此种情况下,我们给每个文件一个版本号(专业称呼为:扩展标记ETag),一旦文件内容发生改变,扩展标记就会发生改变,内容不变,扩展标记也不变,当缓存服务器与后端服务器进行比对时,就是比对扩展标记,如果扩展标记不一致,就重新缓存,这样就实现了基于文件内容的判断机制。

通过Cache-Control控制相关

  • public:可以放在公共缓存上,公共缓存是指除了客户端的自身浏览器的缓存,其他任何位置都是公共缓存
  • private:可以放在私有缓存上进行缓存
  • no-cache:表示能缓存,但是当客户端下次请求同样的资源时,不能直接用缓存的内容响应给客户端,而是要到原始服务器验证缓存的有效性
  • no-store:不可缓存,表示响应的内容不允许缓存
  • must-revalidate:必须重新校验,表示内容缓存下来后,当用户请求同样的内容时,必须要原始服务器上进行校验缓存有效性
  • max-age:可缓存的时长,相对时长,也就是从此刻开始,可以缓存多少秒
  • s-max-age:公共缓存服务器的缓存,可缓存的时长。控制公共缓存最大有效期,如果没有公共缓存的定义,也能使用私有缓存中的数据(也就是s-max-age可以不用指明,但是如果没有指明,如果公共缓存中的数据需要缓存,缓存的有效时长就取决于max-age)

缓存头部对比

55.png

http缓存工作流程

http缓存处理过程的一个大致图示过程:
22.png

理论上来讲,当一个资源被缓存存储后,该资源应该可以被永久存储在缓存中。由于缓存只有有限的空间用于存储资源副本,所以缓存会定期地将一些副本删除,这个过程叫做缓存驱逐。另一方面,当服务器上面的资源进行了更新,那么缓存中的对应资源也应该被更新,由于HTTP是C/S模式的协议,服务器更新一个资源时,不可能直接通知客户端及其缓存,所以双方必须为该资源约定一个过期时间,在该过期时间之前,该资源(缓存副本)就是新鲜的,当过了过期时间后,该资源(缓存副本)则变为陈旧的。驱逐算法用于将陈旧的资源(缓存副本)替换为新鲜的,注意,一个陈旧的资源(缓存副本)是不会直接被清除或忽略的,当客户端发起一个请求时,缓存检索到已有一个对应的陈旧资源(缓存副本),则缓存会先将此请求附加一个If-None-Match头,然后发给目标服务器,以此来检查该资源副本是否是依然还是算新鲜的,若服务器返回了 304 (Not Modified)(该响应不会有带有实体信息),则表示此资源副本是新鲜的,客户端继续使用本地缓存,这样一来,可以节省一些带宽。若服务器通过 If-None-Match 或 If-Modified-Since判断后发现已过期,那么会带有该资源的实体内容返回。

定义最佳 Cache-Control 策略

摘自Google Developers
66.png
按照以上决策树为应用使用的特定资源或一组资源确定最佳缓存策略。在理想的情况下,目标内容应该是在客户端上缓存尽可能多的响应,缓存尽可能长的时间,并且为每个响应提供验证令牌,以实现高效的重新验证。

varnish

varnish简介

网上有很多varnish的介绍资料,简单来说,varnish是一个专门的轻量级http缓存加速器,其官方组织比较活跃,约6个月更新一次版本,目前最新版是5.2.1,官方维护的稳定版本是4.1.9.

varnish架构以及工作过程

varnish架构:

33.png

  • 由于vcl配置文件是要经过c编译后子进程才能读取,因此安装varnish是需要依赖安装c的编译环境。

根据官方提供的架构图,varnish和nignx相似,主要运行两个进程:Management进程和Child/cache进程。

  1. Management进程主要实现应用新的配置、编译VCL、监控varnish、初始化varnish以及提供一个命令行接口等。Management进程会每隔几秒钟探测一下Child进程以判断其是否正常运行,如果在指定的时长内未得到Child进程的回应,Management将会重启此Child进程。
  2. Child进程包含多种类型的线程,其中最重要的是工作线程,Worker threads:child进程会为每个会话启动一个worker线程,此worker线程真正来管理缓存,构建响应报文,因此,在高并发的场景中可能会出现数百个worker线程甚至更多,worker线程读入uri后,将会查找已有的object,命中直接返回,没有命中,则会从后端服务器中取出来,放到缓存中。如果缓存已满,会根据LRU算法,释放旧的object。对于释放缓存,有一个超时线程会检测缓存中所有object的生命周期,如果缓存过期(ttl),则删除,释放相应的存储内存。;

varnish安装

直接配置官方提供的yum源安装或者源码安装即可。

varnish使用

varnish支持的存储类型

varnish支持多种不同类型的缓存对象存储方案,这可以在varnishd启动时使用-s选项指定。存储的类型包括:

  1. file:自管理的文件系统,使用特定的一个文件存储全部的缓存数据,并通过操作系统的mmap()系统调用将整个缓存文件映射至内存区域(如果内存大小条件允许);varnish重启时,所有缓存对象都将被清除;
    配置格式:file[,path[,size[,granularity]]]
  • file中的granularity用于设定缓存空间分配单位,也就是当我们size假设设置为20G,这20G的空间不是一次性分配的,而是初始时比方说分配1G,用完后,逐步增加,一次增加的多大(此即为granularity),默认单位是字节,一次次的增加,直到达到size大小
  1. malloc:使用malloc()库调用在varnish启动时向操作系统申请指定大小的内存空间以存储缓存对象;varnish重启时,所有缓存对象都将被清除;
    配置格式:malloc[,size]
  2. persistent:与file的功能相同,但可以持久存储数据(即重启varnish数据时不会被清除);仍处于测试期;
    配置格式:persistent,path,size

选择何种存储方式

选择使用合适的存储方式有助于提升系统性,根据实际的业务量评估,在内存空间足以存储所有的缓存对象时使用malloc的方法,反之,file存储将有着更好的性能的表现。然而,需要注意的是,varnishd实际上使用的空间比使用-s选项指定的缓存空间更大,一般说来,其需要为每个缓存对象多使用差不多1K左右的存储空间,这意味着,对于100万个缓存对象的场景来说,其使用的缓存空间将超出指定大小1G左右。另外,为了保存数据结构等,varnish自身也会占去不小的内存空间。

varnish管理

varnish提供丰富的CLI工具来进行管理和查看varnish的运行状态。

  • 命令行管理工具程序:
    varnishadm
  • Shared Memory Log交互工具:
    varnishhist

varnishlog
varnishncsa
varnishstat
varnishtop

  • 测试工具程序:
    varnishtest
  • VCL配置文件重载程序:
    varnish_reload_vcl

varnish vcl

vcl的状态引擎以及工作流

VCL,Varnish Configuration Language 是varnish配置缓存策略的工具,它是一种基于“域”(domain)的简单编程语言,它支持有限的算术运算和逻辑运算操作、允许使用正则表达式进行字符串匹配、允许用户使用set自定义变量、支持if判断语句,也有内置的函数和变量等。

官方提供的vcl状态引擎流:

44.png

  • vcl_recv:接受用户请求进varnish的入口的引擎,接受到结果之后,利用return(lookup),将请求转交给vcl_hash引擎进行处理
  • vcl_hash:接受到用户请求后,对用户请求的URL进行hash计算,根据请求的首部信息,以及hash结果进行下一步处理的引擎
  • vcl_hit:经过vcl_hash引擎处理后,发现用户请求的资源本地有缓存,则vcl_hash引擎通过return(hit)将请求交给vcl_hit引擎进行处理,vcl_hit引擎处理后将请求交给vcl_deliver引擎,vcl_deliver引擎构建响应报文,响应给用户
  • vcl_miss:经过vcl_hash引擎处理后,发现用户请求的资源本地没有缓存,则vcl_hash引擎通过return(miss)将请求交给vcl_miss引擎进行处理
  • vcl_purge:经过vcl_hash引擎处理后,发现请求是对缓存的内容进行修剪时,则通过return(purge)交给vcl_purge引擎进行处理,vcl_purge引擎处理后,利用vcl_synth引擎将处理的结果告知给用户
  • vcl_pipe:经过vcl_hash引擎处理后,发现用户请求的报文varnish无法理解,则通过return(pipe),将请求交给vcl_pipe引擎,pipe引擎直接将请求交给后端真实服务器
  • vcl_pass:当请求经过vcl_hash处理后,发现请求报文不让从缓存中进行响应或其他原因没办法查询缓存,则由return(pass)或return(hit-for-pass)交由vcl_pass引擎进行处理
  • vcl_backend_fetch:当发现缓存未命中或由vcl_pass传递过来的某些不能查询缓存的请求,交由vcl_backend_fetch引擎处理,vcl_backend_fetch引擎会向后端真实web服务器发送请求报文,请求对应的资源
  • vcl_backend_response:当后端发送响应报文到varnish后,会由vcl_backend_resonse引擎进行处理,如:判断响应的内容是否可缓存,如果能缓存,则缓存下来后,交给vcl_deliver引擎,如果不能缓存,则直接交给vcl_deliver引擎,vcl_deliver引擎构建响应报文给客户端

因此,常见的状态引擎之间的处理流程为:

  1. 如果缓存命中:
  • client request–>vcl_recv–>vcl_hash–>vcl_hit–>vcl_deliver–>client
  1. 如果缓存未命中:
  • client request–>vcl_recv–>vcl_hash–>vcl_miss–>vcl_pass–>vcl_backend_fetch–>vcl_backend_response–>vcl_deliver–>client
  1. 如果不能从缓存中进行响应
  • client request–>vcl_recv–>vcl_hash–>vcl_pass–>vcl_backend_fetch–>vcl_backend_response–>vcl_deliver–>client
  1. 如果进行缓存修剪
  • client request–>vcl_recv–>vcl_hash–>vcl_purge–>vcl_synth–>client
  1. 如果请求报文无法理解
  • client request–>vcl_recv–>vcl_hash–>vcl_pipe–>交给后端服务器

varnish的优化和最佳实践

  1. Varnish Cache使用jemalloc作为其默认内存分配器。Jemalloc速快,效率高,而且非常稳定,与原生内存系统调用函数是malloc(),free()相比,主要体现在避免内存碎片与并发扩展上。
    varnish给出了示例,建议调整两个jemalloc参数
  • lg_dirty_mult。这告诉jemalloc它应该以什么程度寻找并将碎片耗费交还给内核。默认比率(或乘数)为3.将此值设置为8,对于减少碎片而言没有太多的CPU开销会有非常积极的影响。
    第二个调节参数是
  • lg_chunk。这设置了jemalloc分配不同对象类的内部块大小。如果您将此值仔细设置为比您的平均对象大小更大的几个标准偏差(或者对于80%的对象足够大),则此参数也可以非常有效地减少总体碎片和控制虚拟内存的增长。在平均对象大小为112KB的情况下,值为18(256KB)被认为是最佳的。
    为了放置这些参数,运行以下命令:

sudo ln -s "lg_dirty_mult:8,lg_chunk:18" /etc/malloc.conf

  1. varnish的启动参数优化,根据实际业务场景调试
  2. 明确哪些内容适合被缓存,如css样式文件、js文件、logo、图标、html文件、可以下载的内容
  3. worker进程池数量thread_pools小于等于cpu数量
  4. 存储空间的合理规划,由于存储的每个对象也会带来超出实际存储区域的开销。所以,即使你指定了Varnish的缓存大小,实际上也可能使用double。varnish每个对象的开销约为1KB。所以,如果缓存中有很多小对象,开销可能会很大,因此实际缓存内容的空间保守为计划空间的75%左右。
  5. 根据业务的实际情况合理配置缓存的有效时长和可压缩对象。