Varnish 进阶与优化(二) weir 2015-09-05 16:43:30.0 java,分布式 2525 GRACE模式 当几个客户端请求同一个页面的时候,varnish只发送一个请求到后端服务器,然后让那个其他几个请 求挂起等待返回结果,返回结果后,复制请求的结果发送给客户端。 如果您的服务每秒有数千万的点击率,那么这个队列是庞大的,没有用户喜欢等待服务器响应。为了解 决这个问题,可以指示varnish去保持缓存的对象超过他们的TTL(就是该过期的,先别删除),并且去提供旧 的内容给正在等待的请求。 为了提供旧的内容,首先我们必须有内容去提供。使用以下VCL,以使varnish保持所有对象超出了他们 的TTL30分钟。 sub vcl_fetch { set beresp.grace = 30m;} 这样,varnish还不会提供旧对象。为了启用varnish去提供旧对象,我们必须在请求上开启它。下面表 示,我们接收15s的旧对象: sub vcl_recv { set req.grace = 15s;} 你可能想知道,为什么,如果我们无法提供这些对象,我们在缓存中保持这些对象30分钟?如果你开启 健康检查,你可以检查后端是否出问题。如果出问题了,我们可以提供长点时间的旧内容。 if (! req.backend.healthy) { set req.grace = 5m; } else { set req.grace = 15s;} 所以,总结下,Grace模式解决了两个问题: 1:通过提供旧的内容,避免请求扎堆。 2:如果后端坏了,提供旧的内容。 Saint模式 神圣模式可以让你抛弃一个后端服务器的某个页面,并尝试从其他服务器获取,或 提供缓存中的旧内容。让我们看看如何在VCL中开启: sub vcl_fetch { if (beresp.status == 500) { set beresp.saintmode = 10s; return(restart); } set beresp.grace = 5m; } 设置beresp.saintmode为10秒时,varnish会不请求该服务器10秒。或多或少可以算 是一个黑名单。restart被执行时,如果我们有其他后端可以提供该内容,varnish会请求 它们。当没有其他后端可用,varnish就会提供缓存中的旧内容。 常见VCL应用片断 为不同的设备设置不同的header参数: sub vcl_recv { if (req.http.User-Agent ~ "iPad" || req.http.User-Agent ~ "iPhone" || req.http.User-Agent ~ "Android") { set req.http.X-Device = "mobile"; } else { set req.http.X-Device = "desktop"; } } 想要取消访问/images的request的cookie: sub vcl_recv { if (req.url ~ "^/images") { unset req.http.cookie; } } 通过ACL来控制能访问的ip地址 acl local { "localhost"; "192.168.1.0"/24; /* and everyone on the local network */ ! "192.168.1.23"; /* except for the dialin router */ } sub vcl_recv { if (req.request == "PURGE") { if (client.ip ~ local) { return(lookup); } } } sub vcl_hit { if (req.request == "PURGE") { set obj.ttl = 0s; error 200 "Purged."; } } sub vcl_miss { if (req.request == "PURGE") { error 404 "Not in cache."; } } 修改从后台服务器返回的对象的ttl: sub vcl_fetch { if (req.url ~ "\.(png|gif|jpg)$") { unset beresp.http.set-cookie; set beresp.ttl = 1h; } } 设置客户端发送的accept-encoding头只有gzip和default两种编码,gzip优先 if (req.http.Accept-Encoding) { if (req.url ~ "\.(jpg|png|gif|gz|tgz|bz2|tbz|mp3|ogg)$") { # No point in compressing these remove req.http.Accept-Encoding; } elsif (req.http.Accept-Encoding ~ "gzip") { set req.http.Accept-Encoding = "gzip"; } elsif (req.http.Accept-Encoding ~ "deflate") { set req.http.Accept-Encoding = "deflate"; } else { # unknown algorithm remove req.http.Accept-Encoding; } } 简单的图片防盗链: if (req.http.referer ~ "http://.*") { if ( !(req.http.referer ~ "http://.*sishuok\.com" || req.http.referer ~ "http://.*google\.com" || req.http.referer ~ "http://.*google\.cn" )) { set req.http.host = "www.sishuok.com"; set req.url = "/static/images/logo.gif"; } return (lookup); } VCL常用的函数 在VCL里面,可以使用如下这些内置函数: hash_data(str): 增加一个散列值,默认hash_data() 是调用request的host和url regsub(str,regex,sub): 用sub来替换指定的目标 regsuball(str,regex,sub): 用sub替换所有发现的目标 ban_url(regex): 禁用缓存中url匹配regex的所有对象, ban_url(regex)预计在4.0会去 掉,建议使用ban(expression) ban(expression): 禁用缓存中匹配表达式的所有对象,这是一种清空缓存中某些无效内容的 方法 Vanish常用的HTTP头 Cache-Control:指定了缓存如何处理内容。varnish关心max-age参数,并用它来计算对象的TTL。 “Cache-Control:no-cache”是被忽略的。 Age:varnish添加了一个Age头信息,以指示在Varnish中该对象被保持了多久。你可以通过varnishlog 像下面那样抓出Age:varnishlog -i TxHeader -I ^Age Pragma:一个HTTP 1.0服务器可能会发送”Pragma:no-cache”。Varnish忽略这种头信息。在VCL中你 可以很方便的增加对这种头信息的支持,在vcl_fetch中: if (beresp.http.Pragma ~ "nocache") { pass;} Authorization:varnish看到授权头信息时,它会pass该请求。你也可以unset这个头信息 Cookies:varnish不会缓存来自后端的具有Set-Cookie头信息的对象。同样,如果客户端发送了一个 Cookie头信息,varnish将绕过缓存,直接发给后端。 Vary:Vary头信息是web服务器发送的,代表什么引起了HTTP对象的变化。可以通过Accept-Encoding这 样的头信息弄明白。当服务器发出”Vary:Accept-Encoding”,它等于告诉varnish,需要对每个来自 客户端的不同的Accept-Encoding缓存不同的版本。所以,如果客户端只接收gzip编码。varnish就不会 提供deflate编码的页面版本。 如果Accept-Encoding字段含有很多不同的编码,比如浏览器这样发送: Accept-Encodign: gzip,deflate 另一个这样发送: Accept-Encoding: deflate,gzip 因为Accept-Encoding头信息不同,varnish将保存两种不同 的请求页面。规范Accept-Encoding头信息将确保你的不同请求的缓存尽可能的少,后面有个例子。 VCL的子程序 一个子程序就是一串可读和可用的代码,子程序在VCL中没有参数,也没有返回值。示例如 下: sub pipe_if_local { if (client.ip ~ local) { pipe; } } 调用一个子程序,使用子程序的关键字名字,如下所示:call pipe_if_local; 有很多默认子程序和varnish的工作流程相关,这些子程序会检查和操作http头文件和各种 各样的请求,决定哪个、哪些请求被使用,如果这些子程序没有被定义,或者没有完成预 定的处理而被终止,控制权将被转交给系统默认的子程序。它们是: 1:vcl_init 当VCL加载时调用,之后加载客户请求。一般用于初始化VMOD模块。 返回值有:ok 表示正常返回值,返回OK后VCL加载。 VCL的变量 由于子程序没有参数,子进程必须的信息通过全局变量来处理。 以下是到处都可用的变量: now:当前时间 下面的变量在backend申明中有效: .host:一个backend的主机名或者IP地址 .port:一个backend的服务名字或者端口号 下面的变量在处理请求时有效: client.ip:客户端IP client.identity:客户的id,用在负载均衡的时候的client director server.hostname:server的主机名 server.identity:server的身份,使用-i 参数设置,如果–i参数没有传递给varnishd, server.identity将给varnishd实例设置名字。 server.ip:客户端连接上socket,接收到的IP地址 server.port:客户端连接上socket,接收到的端口号 req.request:请求类型,例如”GET”,”HEAD” req.proto:客户端的Http协议 req.url:请求的URL req.backend:使用哪个后端服务器为这个请求提供服务 req.backend.healthy:后端服务器是否健康 req.http.header:对应的HTTP头 req.hash_always_miss:强制本请求的缓存失效 req.hash_ignore_busy:当lookup缓存的时候,忽略busy的对象 req.can_gzip:设置能使用gzip req.restarts:设置最大的重启次数 req.esi:设置是否支持ESI,今后会改变,建议不要使用 req.esi_level:设置ESI的level req.grace:设置对象被保持的时间 req.xid:请求的唯一id 下面这些变量在访问backend的时候用 bereq.request:请求的类型(如"GET", "HEAD") bereq.url:请求的url bereq.proto:请求的协议 bereq.http.header:请求的HTTP header bereq.connect_timeout:等待后端服务器响应的时间 bereq.first_byte_timeout:等待接收第一个字节的等待时间,pipe模式中无效。 bereq.between_bytes_timeout:两次从后端服务器接收到字节的间隔,pipe模式无效。 下面这些变量在从backend取回,但还没有进入缓存的时候使用,也就是vcl_fetch变量 beresp.do_stream:对象会直接返回给客户端,不会在varnish中缓存。在Varnish3里面, 这些对象将会被标记为busy beresp.do_esi:是否进行ESI处理 beresp.do_gzip:是否在存储前Gzip压缩 beresp.do_gunzip:是否在存储前解压缩 beresp.http.header:HTTP header beresp.proto:HTTP的协议 beresp.status:HTTP的状态码 beresp.response:服务端返回的状态消息 beresp.ttl:对象保存的时间 beresp.grace:对象grace保存的时间 beresp.saintmode:saint模式持续的时间 beresp.backend.name:response的backend的名字 beresp.backend.ip:response的backend的ip beresp.backend.port:response的backend的端口 beresp.storage:强制Varnish保存这个对象 下面这些变量在请求目标被成功的从后端服务器或者缓存中获得后有效 obj.proto:返回请求目标的HTTP版本 obj.status :服务器返回的HTTP状态码 obj.response :服务器返回的HTTP状态信息 obj.ttl:目标的剩余生存时间,以秒为单位。 obj.lastuse:最后一个请求后,过去的时间,以秒为单位。 obj.hits:大概的delivered的次数,如果为0,表明缓存出错。 obj.grace:对象grace的存活时间 obj.http.header:Http header 下面这些变量在目标hash key以后有效 req.hash:hashkey 和缓存中的目标相关,在读出和写入缓存时使用。 下面这些变量在准备回应客户端时使用 resp.proto:准备响应的HTTP协议版本 resp.status:返回客户端的HTTP状态码 resp.response:返回客户端的HTTP状态信息 resp.http.header:通信的HTTP头 使用SET关键字,把值分配给变量: sub vcl_recv { # Normalize the Host: header if (req.http.host ~ "^(www.)?example.com$") { set req.http.host = "www.example.com"; } } 可以使用remove关键字把HTTP头彻底的删除: sub vcl_fetch { remove obj.http.Set-Cookie; Varnish的性能调优 Varnish的性能调优分成两个部分,一个是硬件、操作系统和网络部分的优化;另外一个, 也是最重要的一个,就是VCL的调优。 要进行硬件、操作系统和网络部分的优化,了解Varnish的进程和线程架构是有必要的,他 们能帮助你更好的去调整优化,以及整合应用系统。 管理进程(The management process) Varnish主要有两个进程,管理进程和子进程,管理进程负责:管理配置的变更(包括VCL和参 数)、编译VCL、监控Varnish运行、初始化Varnish,以及提供命令行接口等。 管理进程会每隔几秒钟检查一下子进程,如果发现子进程挂了,会kill掉然后重新启动一 个。这些操作都会记录在日志里面,以利于你检查错误。 子进程(The child process) 子进程包括几个不同类型的线程,包括但不限于: 1:Acceptor线程:接受新的连接并代表它们 2:Worker线程:一个会话一个线程,通常会使用数百个Worker线程 3:Expiry线程:负责从缓存中清除旧的内容 工作区(Workspace) Varnish使用Workspace来减少多个线程间对于内存的竞争。Varnish有多个工作区,最重 要的是session workspace,它通常用来操作会话数据。比如:修改www.weir.com到weir.com,以减少重复缓存。 就算你的Session工作区有5G,使用了1000个线程,但实际使用的内存容量不会这么多, 虚拟内存会是5个G,但是实际的内存是根据你实际的使用来的,内存控制器和操作系统会帮你管 理内存,这个不用担心 和系统的通讯,子进程会使用共享内存日志访问文件系统,这意味着,如果一个线程需要 记录日志,它只需要获得锁,然后写入内存,然后释放锁就可以了。另外每个线程都有一 份日志数据的缓存,以减少锁的竞争 日志数据通常为80M,分成两个部分,第一个部分是计数器,第二个部分是请求数据。你可 以使用日志工具来查看共享内存日志,因为日志记录并不会是原始的格式。 对于储存类型,如果不需要永久保持缓存的话,建议使用malloc,如果要永久保存,或者 是要缓存的内容超过了物理内存的大小,那么使用file。 另外一个要注意:缓存对象会有额外的开销,以保持对缓存的追踪,大概一个对象 需要1k的额外开销,这也意味着,最终使用的内存会比你通过-s指定的内存大小要大一 些。 通常对硬件、操作系统和网络部分的优化,主要就是相关参数的优化。参数调优的方法: 通常是在CLI界面去调整,然后测试,如果ok的话,就把它们配置到配置文件里面去 线程模式 子进程是Varnish真正产生奇迹的地方,它包含一系列的线程来执行不同的任务,下面罗列几个 常见的: cache-worker:每个连接一个,负责处理请求 cache-main:一个,负责启动 ban lurker:一个,负责禁用的处理 acceptor:一个,负责接受新的连接 epoll/kqueue:可配置,缺省是2个,管理线程池 expire:一个,负责删除过期内容 backend poll:一个backend poll一个线程,用于健康检查 通常我们只需要配置cache-worker线程数,其他的线程可以不用管。 调整Varnish的时候,需要考虑预期的流量,线程模型允许你使用多个线程池,但时间和 经验表明,只要你有2个线程池就够了,加入更多也不会提高性能。一些旧的资料会建议你一个 cpu核运行一个线程池,这个已经过时了,现在只要2个就够了。 线程池相关的参数,前面已经都讲过了,最重要的是thread_pool_min和 thread_pool_max。通常保持最少在500-1000个线程都是合适的,具体的可以根据varnishstat来 查看n_wrk_queued,根据具体情况来配置 线程growth时间 Varnish能支持数千个线程同时运行,但并不是所有的操作系统内核都能支撑,因此 使用thread_pool_add_delay来保证每个线程间有一点延迟,现在已经不重要了,操作系统 都比较成熟了,一般从20ms到2s 系统级参数 随着Varnish越来越成熟,越来越少的参数需要调整了。sess_workspace是一个要调 整的重要参数,它设置的是从客户端传入的Http 头的workspace大小: 1:通常这个值从缺省的65536字节到10M 2:要注意,它是虚拟的内存,不是实际使用的内存 另外一些可调优的参数就是各种时间,如: connect_timeout、first_byte_timeout、between_bytes_timeout、send_timeout、 sess_timeout、cli_timeout 通常情况下,默认的数值能满足大多数应用的需要,但你还是需要结合实际的应用 进行调整。比如connect_timeout,默认是0.7秒,如果Varnish和backend是通过远程来连 接和访问,这个时间可能就需要延长。 优化linx内核参数 内核参数是用户和系统内核之间交互的一个接口,通过这个接口,用户可以在系统运行的同时动 态的更新内核配置,而这些内核参数是通过Linux Proc文件系统存在的,因此,可以通过对Proc文件系 统进行调整,达到性能优化的目的。 以下参数是官方给出的一个配置,内容如下: net.ipv4.ip_local_port_range = 1024 65536 net.core.rmem_max=16777216 net.core.wmem_max=16777216 net.ipv4.tcp_rmem=4096 87380 16777216 net.ipv4.tcp_wmem=4096 65536 16777216 net.ipv4.tcp_fin_timeout = 30 net.core.netdev_max_backlog = 30000。 net.ipv4.tcp_no_metrics_save=1 net.core.somaxconn = 262144 net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_max_orphans = 262144 net.ipv4.tcp_max_syn_backlog = 262144 net.ipv4.tcp_synack_retries = 2 net.ipv4.tcp_syn_retries = 2 将以上内容添加到/etc/sysctl.conf文件中,然后执行如下命令,让设置生效: sysctl –p 对上面的每个选项含义解释如下: net.ipv4.ip_local_port_range:指定外部连接的端口范围,默认32768到61000 net.core.rmem_max:该文件指定了接收套接字缓冲区大小的最大值,单位是字节 net.core.wmem_max:该文件指定了发送套接字缓冲区大小的最大值,单位是字节 net.ipv4.tcp_rmem:此参数与net.ipv4.tcp_wmem都是用来优化TCP接收/发送缓冲区,包含三 个整数值:min,default,max tcp_rmem:min表示为TCP socket预留用于接收缓冲的最小内存数量,default为TCP socket预 留用于接收缓冲的缺省内存数量,max用于TCP socket接收缓冲的内存最大值 tcp_wmem:min表示为TCP socket预留用于发送缓冲的内存最小值,default为TCP socket预留 用于发送缓冲的缺省内存值,max用于TCP socket发送缓冲的内存最大值 net.ipv4.tcp_fin_timeout:用来减少处于FIN-WAIT-2连接状态的时间,使系统可以处理更多 的连接。值为整数,单位为秒 举一个例子:在一个tcp会话过程中,在会话结束时,A首先向B发送一个fin包,在 获得B的ack确认包后,A就进入FIN WAIT2状态等待B的fin包,然后给B发ack确认包。此参 数就是用来设置A进入FIN WAIT2状态等待对方fin包的超时时间。如果时间到了仍未收到对 方的fin包就主动释放该会话 net.core.netdev_max_backlog:该参数表示在每个网络接口接收数据包的速率比内核处理这 些包的速率快时,允许送到队列的数据包的最大数量 net.ipv4.tcp_syncookie:该文件表示是否打开SYN Cookie功能,tcp_syncookies是一个开 关,该功能有助于保护服务器免受SyncFlood攻击。默认为0,这里设置为1 net.ipv4.tcp_max_orphans:表示系统中最多有多少TCP套接字不被关联到任何一个用户文件 句柄上。如果超过这个数字,孤儿连接就会复位并打输出警告信息。这个限制仅仅是为了 防止简单的DoS攻击。此值不能太小。这里设置为262144 net.ipv4.tcp_max_syn_backlog:表示SYN队列的长度,预设为1024,这里设置队列长度为 262144,以容纳更多等待连接 net.ipv4.tcp_synack_retries:这个参数用于设置内核放弃连接之前发送SYN+ACK包的数量。 net.ipv4.tcp_syn_retries:此参数表示在内核放弃建立连接之前发送SYN包的数量。 最后别忘了我们的初衷,varnish的作用是使用内存来加快我们读取静态资源和部分动态的页面,从而提高整个网站的运行速度。