码迷,mamicode.com
首页 > 其他好文 > 详细

Varnish缓存机制详细介绍及简单配置

时间:2016-12-30 22:21:32      阅读:1652      评论:0      收藏:0      [点我收藏+]

标签:varnish缓存机制详细介绍及简单配置


Varnish是一款高性能的开源HTTP加速器,其主要用来做为反向代理中的缓存服务器使用,但其实Varnish本身也是具有反向代理功能的,但在创建连接和维持连接上,与Nginx相比差距很大,现在有一个很流行的架构就是前端用Nginx作为反向代理,后面加Varnish缓存服务器为Web服务加速


在将Varnish前先谈谈我们的浏览器缓存机制,现在的浏览器基本都具有缓存功能,它能将我们以前访问过的静态内容和可进行缓存的动态内容缓存再本地,而后在下次访问相同资源时,如果可以确认Server端的资源未发生修改,则可以直接使用本地缓存的数据,而不再需要再从server端加载数据


但是需要注意的是有的资源就不能缓存,如不能静态化的动态资源,及一些私人数据,如账号密码登录时产生的cookie等


而缓存空间又分为私有缓存空间和公有缓存空间,私有缓存空间主要缓存那些涉及到用户私密的数据,如用户经常浏览的网页cookie,就不能随意让他人获取(负责会暴露你的嗜好);而公有缓存一般对大多数人都可以一起使用如Nginx,Varnish,Squid等可以缓存不涉及隐私的数据


一般缓存都具有有效期限,一旦过期,缓存就会失效,对于缓存资源是否失效由以下几种判断策略:

1:Expire 在http响应的首部明确告诉client这个数据在社么时候过期:如

  Expire: 2016-12-29 10.54.59 则一旦到达这个时间缓存就会失效,但这种绝对时间有个缺陷就是,

client和server上的时间可能不同步,甚至可能不在一个时区,这种方法主要在http1.0中才用


2:max-age:相对时间计时,在http响应的首部直接告诉client这个数据可以缓存多久,则在这段时间之前缓存都可以直接使用



3:Cahe-Control:用于定义所有缓存机制都必须遵循的缓存指示,这些指示有以下指令

public 说明这个资源可以放在公共缓存上,不涉及隐私内容,所有人都可以使用

private 说明这个资源部可以放在公共缓存上如Nginx,Varnish,Squid等

no-cache 说明此资源可以缓存,但你不能直接使用,而是每次使用时都要向server端进行验证,此资源是否发生了修改,如果没有修改则可以使用,如果修改了则重新从server端加载资源(no-cache一般用在资源随时都有可能发生变化的场景)

no-store:可以缓存但不能进行存储

must-revalidate;与no-cache类似,可以缓存,但使用前要验证是否修改

注:Cache-Control中的设定时间会覆盖Expire中定义的时间


但是有的网站的页面可能有动态脚本生成所以它的页面内容可能会在一秒内发生好几次改变,所以时间戳计时法就有点粗糙,可能造成已近失效的资源仍然被使用

4:E-tag:响应报文的首部,用于在响应的web资源中定义版本标示,E-tag会为每次改变的页面生成一个随机的标签(网页该变频率低于秒也无影响),而下次client请求相同的资源时只需要对比一下E-tag,如果相同则资源未修改,直接使用本地缓存,否则重新加载


在http1.1中还引入了条件判断机制 如:

If-modify-since:用在请求报文的首部,client访问同一个资源时,会询问server端,自从我上次获取资源到现在,此资源是否发生了修改,如果修改了,server会发送修改后的资源给client,否则会返回304(not modify)状态码,直接使用本地缓存


If-none-match: 条件式判断请求报文首部,client请求相同资源时,会询问server,E-tag是否不匹配,如果回答yes,则内容发生了修改,no则未修改直接使用本地缓存


Vary:响应首部,原始server会根据不同的请求来源,发送不同的响应报文首部,最常用的vary如:accept-encording(client是否支持压缩),如果client请求时报文首部说支持,则会压缩响应client,否则以源码响应


Age:缓存的server可发送一个额外的响应首部,用于指定响应的有效期限,浏览器通常根据此首部决定缓存的有效时长,如果响应报文首部还有max-age,那么缓存有效时长为max-age 减 Age



如下是一个资源请求和响应的报文的首部

Request URL:https://eclick.baidu.com/fp.htm?br=2&fp=7319E

Request Method:GET

Status Code:200 OK #如果有缓存server且缓存未修改则返回304(no modify)

Remote Address:123.125.115.164:443

Response Headers

view source

Accept-Ranges:bytes

Cache-Control:max-age=0

Connection:keep-alive

Content-Length:114

Content-Type:text/html

Date:Thu, 29 Dec 2016 03:29:56 GMT

ETag:"58638da3-72"

Expires:Thu, 29 Dec 2016 03:29:56 GMT

Last-Modified:Wed, 28 Dec 2016 10:02:11 GMT

Server:nginx

Request Headers

view source

Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

Accept-Encoding:gzip, deflate, sdch, br

Accept-Language:zh-CN,zh;q=0.8

Connection:keep-alive

Cookie:BDUSS=nJaZDdIVy1HYn5MRnpUTzdFRnhRQkN5TnF5eWF1UUZ0WXpHdTB0b1NGSllhZlpYQVFBQUFBJCQAAAA AAAAAAAEAAAB~NOBPvNnD5mk5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFjczldY3M5XQ

Host:eclick.baidu.com

Referer:https://pos.baidu.com/wh/o.htm?ltr=

Upgrade-Insecure-Requests:1

User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko)            Chrome/51.0.2704.106 Safari/537.36

Query String Parameters

view source

view URL encoded



一个拥有优秀缓存策略的缓存服务器,可以大大提高缓存的命中率,从而减轻后端原始服务器的压力,也可以为用户带来更优秀的用户体验


在实际部署中,可以将缓存server结合CDN(内容分发网络)来使用,在智能DNS的帮助下,始终将用户的请求分发到距离用户最近的一台缓存服务器上,如果没有在当前缓存server上找到缓存资源,当前的缓存server会请求其上一级(一般也是缓存服务器,与当前缓存server为父子关系),如果还没有就找原始服务器,从而可以极大的降低高并发下带给原始服务器的压力,同时可以加速用户请求的响应时间,为用户带来更加优秀的体验,


CDN和缓存服务器的使用主要目的是加速响应,和减轻原始服务器压力,让请求尽量少找原始server,但有些动态资源就必须通过原始服务器获取(许多动态资源无法进行静态化),即时是静态资源你也不可能全部进行整站缓存


其中CDN网络一般都是按流量进行收费的,一般的大公司可能会选择自建CDN网络,而只能DNS有免费的,如DNSPod,也有收费的(性能好)



Varnish作为缓存服务器与Squid相比


Varnish与Squid都是一个反向代理服务器,都可用作高性能的代理缓存服务器,并且都是开源软件


Varnish稳定性很高,两者在完成相同负荷的工作时,Squid服务器发生故障的几率要高于Varnish,因为Squid需要经常重启


Varnish访问速度更快,Varnish采用了“Visual PageCache”技术,所有缓存数据都直接从内存读取,而Squid是从硬盘读取缓存数据,因此Varnish在访问速度方面会更快


Varnish支持更多的并发连接,因为Varnish的TCP连接释放要比Squid快,所以在高并发连接情况下可以支持更多的TCP连接


Varnish可以通过管理端口,使用正则表达式批量清除部分缓存,而Squid做不到


Varnish进程一旦挂起、崩溃或者重启,缓存数据都会从内存中完全释放,此时所有请求都会被发送到后端服务器,在高并发情况下,这会给后端服务器造成很大压力


Varnish配置相比Squid简单,监控接口丰富,性能好,但Squid资料多,功能丰富,支持对ACL的访问控制


Varnish主要运行两个进程:Management进程和Child进程(也叫Cache进程)。

 

Management进程主要实现应用新的配置、编译VCL、监控varnish、初始化varnish以及提供一个命令行接口等。此外Management进程会每隔几秒钟探测一下Child进程以判断其是否正常运行,如果在指定的时长内未得到Child进程的回应,Management将会重启此Child进程。

 

Child进程包含多种类型的线程,常见的如:

Acceptor线程:接收新的连接请求并响应;

Worker线程:child进程会为每个会话启动一个worker线程,因此,在高并发的场景中可能会出现数百个worker线程甚至更多;

Expiry线程:从缓存中清理过期内容;

 

Varnish依赖“工作区(workspace)”以降低线程在申请或修改内存时出现竞争的可能性。在varnish内部有多种不同的工作区,其中最关键的当属用于管理会话数据的session工作区。

 

如下图:


技术分享


Varnish将数据缓存在内存中,并且将日志也保存在内存中(不是在磁盘上,这样就避免了I/O的产生),Varnish已启动,就会预先分配一定的内存为记录日志使用(包括记录有多少client请求进来了,有多少client请求命中了,有多少没命中等都会进行记录)

Child进程使用了可以通过文件系统接口进行访问的共享内存日志(shared memory log),因此,如果某线程需要记录信息,其仅需要持有一个锁,而后向共享内存中的某内存区域写入数据,再释放持有的锁即可。而为了减少竞争,每个worker线程都使用了日志数据缓存。

 

共享内存日志大小一般为90M,其分为两部分,前一部分为计数器,后半部分为客户端请求的数据。varnish提供了多个不同的工具如varnishlog、varnishncsa或varnishstat等来分析共享内存日志中的信息并能够以指定的方式进行显示。




VCL(varnish Configuration Language)varnish的配置语言

Varnish的配置是给你一个接口,让你用VCL这种语言去编一个程序(指明那些数据缓存,那些不缓存等等);VCL策略在启用前,会由management进程将其转换为C代码,再由gcc再次编译成为二进制格式才能执行^_^,所以Cache/Child进程的工作是由编译完的share object(共享对象)决定的。


Varnish修改配置的开销非常小,其可以同时保有几份尚在引用的旧版本配置,也能够让新的配置即刻生效。编译后的旧版本配置通常在varnish重启时才会被丢弃,如果需要手动清理,则可以使用varnishadm的vcl.discard命令完成。


一般来说Varnish的并发能力超过5000性能就会出现下降,所以不能让其支持太多的并发连接,如果并发很高,可以再Varnish前段加调度器或反向代理服务器进行多台Varnish的负载均衡



Varnish的后端存储


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

(1)file:使用特定的文件存储全部的缓存数据,并通过操作系统的mmap()系统调用将整个缓存文件映射至内存区域(如果条件允许);

(2)malloc:使用malloc()库调用在varnish启动时向操作系统申请指定大小的内存空间以存储缓存对象;

(3)persistent(experimental):与file的功能相同,但可以持久存储数据(即重启varnish数据时不会被清除);仍处于测试期;


varnish无法追踪某缓存对象是否存入了缓存文件,从而也就无从得知磁盘上的缓存文件是否可用,因此,file存储方法在varnish停止或重启时会清除数据。而persistent方法的出现对此有了一个弥补,但persistent仍处于测试阶段,例如目前尚无法有效处理要缓存对象总体大小超出缓存空间的情况,所以,其仅适用于有着巨大缓存空间的场景。


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


为varnishd指定使用的缓存类型时,-s选项可接受的参数格式如下:

malloc[,size] 或

file[,path[,size[,granularity]]] 或

persistent,path,size {experimental}

file中的granularity用于设定缓存空间分配单位,默认单位是字节,所有其它的大小都会被圆整。




A.Varnish状态引擎(state engine)


VCL用于让管理员定义缓存策略,而定义好的策略将由varnish的management进程分析、转换成C代码、编译成二进制程序并连接至child进程(即实例)。varnish内部有几个所谓的状态(state),在这些状态上可以附加通过VCL定义的策略以完成相应的缓存处理机制,因此VCL也经常被称作“域专用”语言或状态引擎,“域专用”指的是有些数据仅出现于特定的状态中。

1、VCL状态引擎


在VCL状态引擎中,状态之间具有相关性,但彼此间互相隔离,每个引擎使用return(x)来退出当前状态并指示varnish进入下一个状态。


Varnish开始处理一个请求时,首先需要分析HTTP请求本身,比如从首部获取请求方法、验正其是否为一个合法的HTT请求等。当这些基本分析结束后就需要做出第一个决策,即varnish是否从缓存中查找请求的资源。这个决定的实现则需要由VCL来完成,简单来说,要由vcl_recv方法来完成。如果管理员没有自定义vcl_recv函数,varnish将会执行默认的vcl_recv函数。然而,即便管理员自定义了vcl_recv,但如果没有为自定义的vcl_recv函数指定其终止操作(terminating),其仍将执行默认的vcl_recv函数。事实上,varnish官方强烈建议让varnish执行默认的vcl_recv以便处理自定义vcl_recv函数中的可能出现的漏洞。


2、VCL语法

VCL的设计参考了C和Perl语言,因此,对有着C或Perl编程经验者来说,其非常易于理解。其基本语法说明如下:

(1)//、#或/* comment */用于注释

(2)sub $name 定义函数

(3)不支持循环,有内置变量

(4)使用终止语句,没有返回值

(5)域专用

(6)操作符:=(赋值)、==(等值比较)、~(模式匹配)、!(取反)、&&(逻辑与)、||(逻辑或)

VCL的函数不接受参数并且没有返回值,因此,其并非真正意义上的函数,这也限定了VCL内部的数据传递只能隐藏在HTTP首部内部进行。VCL的return语句用于将控制权从VCL状态引擎返回给Varnish,而非默认函数,这就是为什么VCL只有终止语句而没有返回值的原因。同时,对于每个“域”来说,可以定义一个或多个终止语句,以告诉Varnish下一步采取何种操作,如查询缓存或不查询缓存等。


3、VCL的内置函数


VCL提供了几个函数来实现字符串的修改,添加bans,重启VCL状态引擎以及将控制权转回Varnish等。


regsub(str,regex,sub)

regsuball(str,regex,sub):这两个用于基于正则表达式搜索指定的字符串并将其替换为指定的字符串;但regsuball()可以将str中能够被regex匹配到的字符串统统替换为sub,regsub()只替换一次;

ban(expression):

ban_url(regex):Bans所有其URL能够由regex匹配的缓存对象;

purge:从缓存中挑选出某对象以及其相关变种一并删除,这可以通过HTTP协议的PURGE方法完成;

hash_data(str):

return():当某VCL域运行结束时将控制权返回给Varnish,并指示Varnish如何进行后续的动作;其可以返回的指令包括:lookup、pass、pipe、hit_for_pass、fetch、deliver和hash等;但某特定域可能仅能返回某些特定的指令,而非前面列出的全部指令;

return(restart):重新运行整个VCL,即重新从vcl_recv开始进行处理;每一次重启都会增加req.restarts变量中的值,而max_restarts参数则用于限定最大重启次数。



如图为Varnish工作的流程图

技术分享



先由vcl_recv接受请求,如果vcl_recv上的函数return()值为pass,则直接到vcl_pass引擎处理,再判断要不要转向后端server,再由vcl_fetch判断是否需要进行缓存,无论需不需要都要最后交给vcl_deliver引擎处理


如果vcl_recv引擎return()的值为lookup,则交给vcl_hash引擎处理,再判断缓存中是否有需要的对象,如果有交给vcl_hit引擎,在交给vcl_pass引擎再判断是否转向后端server交给vcl_fetch,或者直接到vcl_deliver;如果没有交给vcl_miss,则交给vcl_pass处理或者判断完是否向后端转发后,交给vcl_fetch引擎处理,vcl_fetch判断是否需要缓存,无论缓存与否都要交给vcl_deliver处理;


其中的vcl_pass甚至可以重新将请求转到vcl_recv让重新判断,说不定第二次判断就命中了,取决于你的策略


如果vcl_recvreturn()返回值为pipe则直接交给vcl_pipe引擎,知道pipe utile close(管道关闭)



VCL内置函数应用如下所示:

[1]vcl_recv:用于接收和处理请求。当请求到达并被成功接收调用,通过判断请求的数据来决定如果处理请求。例如:应该如何响应、通过什么方式响应、调度使用哪个后端服务器。

作用应用如下:

vcl_recv是在Varnish完成对请求报文的解码为基本数据结构后第一个要执行的子例程,它通常有四个主要用途:


(1)修改客户端数据以减少缓存对象差异性;比如删除URL中的www.等字符;

(2)基于客户端数据选用缓存策略;比如仅缓存特定的URL请求、不缓存POST请求等;

(3)为某web应用程序执行URL重写规则;

(4)挑选合适的后端Web服务器;


可以使用下面的终止语句,即通过return()向Varnish返回的指示操作:


1. pass:绕过缓存,即不从缓存中查询内容或不将内容存储至缓存中;

 

2. pipe:不对客户端进行检查或做出任何操作,而是在客户端与后端服务器之间建立专用“管道”,

并直接将数据在二者之间进行传送;此时,keep-alive连接中后续传送的数据也都将通过此管道进行

直接传送,并不会出现在任何日志中;

 

3. lookup:在缓存中查找用户请求的对象,如果缓存中没有其请求的对象,后续操作很可能会将其请求的

对象进行缓存;

 

4. error:由Varnish自己合成一个响应报文,一般是响应一个错误类信息、重定向类信息或负载均衡器

返回的后端web服务器健康状态检查类信息;


vcl_recv也可以通过精巧的策略完成一定意义上的安全功能,以将某些特定的攻击扼杀于摇篮中。同时,它也可以检查出一些拼写类的错误并将其进行修正等。


Varnish默认的vcl_recv专门设计用来实现安全的缓存策略,它主要完成两种功能:

(1)仅处理可以识别的HTTP方法,并且只缓存GET和HEAD方法;

(2)不缓存任何用户特有的数据;

安全起见,一般在自定义的vcl_recv中不要使用return()终止语句,而是再由默认vcl_recv进行处理,并由其做出相应的处理决策。


下面是一个自定义的使用示例:


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";

}

}


此例中的VCL创建一个X-Device请求首部,其值可能为mobile或desktop,于是web服务器可以基于此完成不同类型的响应,以提高用户体验。


[2]vcl_fetch:相对于vcl_recv是根据客户端的请求作出缓存决策来说,vcl_fetch则是根据服务器端的响应作出缓存决策。在任何VCL状态引擎中返回的pass操作都将由vcl_fetch进行后续处理。vcl_fetch中有许多可用的内置变量,比如最常用的用于定义某对象缓存时长的beresp.ttl变量。通过return()返回给Varnish的操作指示有:


(1)deliver:缓存此对象,并将其发送给客户端(经由vcl_deliver);

(2)hit_for_pass:不缓存此对象,但可以导致后续对此对象的请求直接送达到vcl_pass进行处理;

(3)restart:重启整个VCL,并增加重启计数;超出max_restarts限定的最大重启次数后将会返回错误信息;

(4)error code [reason]:返回指定的错误代码给客户端并丢弃此请求;

默认的vcl_fetch放弃了缓存任何使用了Set-Cookie首部的响应。


[3]vcl_pipe:此函数在进入pipe模式时被调用,用于将请求直接传递至后端主机,在请求和返回内容没有改变的情况下,将不变的内容返回给客户端,直到整个连接被关闭。

此函数一般以如下几个关键字结束:


** error code[reason]

** pipe


[4]vcl_pass:此函数在进入pass模式时被调用,用于将请求直接传递至后端主机,后端主机在应答数据后将应答数据发送给客户端,但不进行任何缓存,在当前连接下每次都返回最新的内容。

此函数一般以如下几个关键字结束:


** error code[reason]

** pass


[5]lookup:此模式表示在缓存中查找被请求的对象,并且根据查找的结果交给vcl_hash进行计算,最后把控制权交给函数vcl_hist或函数vcl_miss。


(6)vcl_hash:此函数在进入lookup模式时被调用,在vcl_recv调用后为请求创建一个hash值时,调用此函数;此hash值将作为varnish中搜索缓存对象的key;

此函数一般以如下几个关键字结束:


** error code[reason]

** hash


[7]vcl_hit:在执行lookup指令后,在缓存中找到请求的内容后将自动调用该函数。

此函数一般以如下几个关键字结束:



** deliver: 表示将找到的内容发送给客户端,并把控制权交给函数vcl_deliver.

** error code[reason]

** pass


[8]vcl_miss:在执行lookup指令后,在缓存中没有找到请求的内容时将自动调用该函数方法,此函数可用于判断是否需要从后端服务器获取内容。

此函数一般以如下几个关键字结束:



** fetch: 表示从后端获取请求的内容,并把控制权交给vcl_fetch函数

** error code[reason]

** pass

[9]vcl_deliver:将在缓存中找到请求的内容发送给客户端前调用此函数方法。

此函数一般以如下几个关键字结束:


** error code[reason]

** deliver


Varnish处理HTTP请求的过程大致分为以下几个步骤:

(1).Receive状态。也就是请求处理的入口状态,根据VCL规则判断该请求应该Pass或Pipe,还是进入Lookup(本地查询)

(2).Lookup状态。进入此状态后,会在hash表中查找数据,若找到,则进入Hit状态,否则进入Miss状态

(3)Pass状态。在此状态下,会进入后端请求,即进入Fetch状态。

(4)Fetch状态。在Fetch状态下,对请求进行后端获取,发送请求,获得数据,并进行本地存储。

(5)Deliver状态。将获取到的数据发送给客户端,然后完成本次请求。


B.修剪缓存对象

 

1、缓存内容修剪

 

提高缓存命中率的最有效途径之一是增加缓存对象的生存时间(TTL),但是这也可能会带来副作用,比如缓存的内容在到达为其指定的有效期之间已经失效。因此,手动检验缓存对象的有效性或者刷新缓存是缓存很有可能成为服务器管理员的日常工作之一,相应地,Varnish为完成这类的任务提供了三种途径:HTTP 修剪(HTTP purging)、禁用某类缓存对象(banning)和强制缓存未命令(forced cache misses)。

 

这里需要特殊说明的是,Varnish 2中的purge()操作在Varnish 3中被替换为了ban()操作,而Varnish 3也使用了purge操作,但为其赋予了新的功能,且只能用于vcl_hit或vcl_miss中替换Varnish 2中常用的set obj.ttl=0s。

 

 

 

在具体执行某清理工作时,需要事先确定如下问题:

(1)仅需要检验一个特定的缓存对象,还是多个?

(2)目的是释放内存空间,还是仅替换缓存的内容?

(3)是不是需要很长时间才能完成内容替换?

(4)这类操作是个日常工作,还是仅此一次的特殊需求?

 

2、移除单个缓存对象

 

purge用于清理缓存中的某特定对象及其变种(variants),因此,在有着明确要修剪的缓存对象时可以使用此种方式。HTTP协议的PURGE方法可以实现purge功能,不过,其仅能用于vcl_hit和vcl_miss中,它会释放内存工作并移除指定缓存对象的所有Vary:-变种,并等待下一个针对此内容的客户端请求到达时刷新此内容。另外,其一般要与return(restart)一起使用。下面是个在VCL中配置的示例。

 

acl purgers {

"127.0.0.1";

"192.168.139.0"/24;

}

sub vcl_recv {

if (req.request == "PURGE") {

if (!client.ip ~ purgers) {

error 405 "Method not allowed";

}

return (lookup);

}

}

sub vcl_hit {

if (req.request == "PURGE") {

purge;

error 200 "Purged";

}

}

sub vcl_miss {

if (req.request == "PURGE") {

purge;

error 404 "Not in cache";

}

}

sub vcl_pass {

if (req.request == "PURGE") {

error 502 "PURGE on a passed object";

}

}

 

客户端在发起HTTP请求时,只需要为所请求的URL使用PURGE方法即可,其命令使用方式如下:

# curl -I PURGE http://varniship/path/to/someurl

 

3、强制缓存未命中

 

vcl_recv中使用return(pass)能够强制到上游服务器取得请求的内容,但这也会导致无法将其缓存。使用purge会移除旧的缓存对象,但如果上游服务器宕机而无法取得新版本的内容时,此内容将无法再响应给客户端。使用req.has_always_miss=ture,可以让Varnish在缓存中搜寻相应的内容但却总是回应“未命中”,于是vcl_miss将后续地负责启动vcl_fetch从上游服务器取得新内容,并以新内容缓存覆盖旧内容。此时,如果上游服务器宕机或未响应,旧的内容将保持原状,并能够继续服务于那些未使用req.has_always_miss=true的客户端,直到其过期失效或由其它方法移除。

 

4、Banning

 

ban()是一种从已缓存对象中过滤(filter)出某此特定的对象并将其移除的缓存内容刷新机制,不过,它并不阻止新的内容进入缓存或响应于请求。在Varnish中,ban的实现是指将一个ban添加至ban列表(ban-list)中,这可以通过命令行接口或VCL实现,它们的使用语法是相同的。ban本身就是一个或多个VCL风格的语句,它会在Varnish从缓存哈希(cache hash)中查找某缓存对象时对搜寻的对象进行比较测试,因此,一个ban语句就是类似匹配所有“以/downloads开头的URL”,或“响应首部中包含nginx的对象”。例如:

ban req.http.host == "magedu.com" && req.url ~ "\.gif$"


定义好的所有ban语句会生成一个ban列表(ban-list),新添加的ban语句会被放置在列表的首部。缓存中的所有对象在响应给客户端之前都会被ban列表检查至少一次,检查完成后将会为每个缓存创建一个指向与其匹配的ban语句的指针。Varnish在从缓存中获取对象时,总是会检查此缓存对象的指针是否指向了ban列表的首部。如果没有指向ban列表的首部,其将对使用所有的新添加的ban语句对此缓存对象进行测试,如果没有任何ban语句能够匹配,则更新ban列表。

 

ban这种实现方式持反对意见有有之,持赞成意见者亦有之。反对意见主要有两种,一是ban不会释放内存,缓存对象仅在有客户端访问时被测试一次;二是如果缓存对象曾经被访问到,但却很少被再次访问时ban列表将会变得非常大。赞成的意见则主要集中在ban可以让Varnish在恒定的时间内完成向ban列表添加ban的操作,例如在有着数百万个缓存对象的场景中,添加一个ban也只需要在恒定的时间内即可完成。其实现方法本处不再详细说明。

 

C.Varnish检测后端主机的健康状态

 

Varnish可以检测后端主机的健康状态,在判定后端主机失效时能自动将其从可用后端主机列表中移除,而一旦其重新变得可用还可以自动将其设定为可用。为了避免误判,Varnish在探测后端主机的健康状态发生转变时(比如某次探测时某后端主机突然成为不可用状态),通常需要连续执行几次探测均为新状态才将其标记为转换后的状态。

 

每个后端服务器当前探测的健康状态探测方法通过.probe进行设定,其结果可由req.backend.healthy变量获取,也可通过varnishlog中的Backend_health查看或varnishadm的debug.health查看。

 

backend web1 {

.host = "www..com";

.probe = {

.url = "/.healthtest.html";

.interval = 1s;

.window = 5;

.threshold = 2;

}

}

 

.probe中的探测指令常用的有:

(1) .url:探测后端主机健康状态时请求的URL,默认为“/”;

(2) .request: 探测后端主机健康状态时所请求内容的详细格式,定义后,它会替换.url指定的探测方式;比如:

.request =

"GET /.healthtest.html HTTP/1.1"

"Host: www..com"

"Connection: close";

(3) .window:设定在判定后端主机健康状态时基于最近多少次的探测进行,默认是8;

(4) .threshold:在.window中指定的次数中,至少有多少次是成功的才判定后端主机正健康运行;默认是3;

(5) .initial:Varnish启动时对后端主机至少需要多少次的成功探测,默认同.threshold;

(6) .expected_response:期望后端主机响应的状态码,默认为200;

(7) .interval:探测请求的发送周期,默认为5秒;

(8) .timeout:每次探测请求的过期时长,默认为2秒;

 

因此,如上示例中表示每隔1秒对此后端主机www.RS.com探测一次,请求的URL为http://www.RS.com/.healthtest.html,在最近5次的探测请求中至少有2次是成功的(响应码为200)就判定此后端主机为正常工作状态。

 

如果Varnish在某时刻没有任何可用的后端主机,它将尝试使用缓存对象的“宽容副本”(graced copy),当然,此时VCL中的各种规则依然有效。因此,更好的办法是在VCL规则中判断req.backend.healthy变量显示某后端主机不可用时,为此后端主机增大req.grace变量的值以设定适用的宽容期限长度。

 

D.Varnish使用多台后端主机

 

Varnish中可以使用director指令将一个或多个近似的后端主机定义为一个逻辑组,并可以指定的调度方式(也叫挑选方法)来轮流将请求发送至这些主机上。不同的director可以使用同一个后端主机,而某director也可以使用“匿名”后端主机(在director中直接进行定义)。每个director都必须有其专用名,且在定义后必须在VCL中进行调用,VCL中任何可以指定后端主机的位置均可以按需将其替换为调用某已定义的director。

 

backend web1 {

.host = "backweb1.RS.com";

.port = "80";

}

 

director webservers random {

  .retries = 5;

  {

    .backend = web1;

    .weight  = 2;

  }

  {

    .backend  = {

      .host = "backweb2.RS.com";

  .port = "80";

    }

  .weight         = 3;

  }

}

 

如上示例中,web1为显式定义的后端主机,而webservers这个directors还包含了一个“匿名”后端主机(backweb2.RS.com)。webservers从这两个后端主机中挑选一个主机的方法为random,即以随机方式挑选。

 

Varnish的director支持的挑选方法中比较简单的有round-robin和random两种。其中,round-robin类型没有任何参数,只需要为其指定各后端主机即可,挑选方式为“轮叫”,并在某后端主机故障时不再将其视作挑选对象;random方法随机从可用后端主机中进行挑选,每一个后端主机都需要一个.weight参数以指定其权重,同时还可以director级别使用.retires参数来设定查找一个健康后端主机时的尝试次数。

 

Varnish 2.1.0后,random挑选方法又多了两种变化形式clienthashclient类型的director使用client.identity作为挑选因子,这意味着client.identity相同的请求都将被发送至同一个后端主机。client.identity默认为cliet.ip,但也可以在VCL中将其修改为

所需要的标识符。类似地,hash类型的director使用hash数据作为挑选因子,这意味着对同一个URL的请求将被发往同一个后端主机,其常用于多级缓存的场景中。然而,无论是client还hash,当其倾向于使用后端主机不可用时将会重新挑选新的后端其机。


E.varnish管理进阶

 

1、可调参数

 

Varnish有许多参数,虽然大多数场景中这些参数的默认值都可以工作得很好,然而特定的工作场景中要想有着更好的性能的表现,则需要调整某些参数。可以在管理接口中使用param.show命令查看这些参数,而使用param.set则能修改这些参数的值。然而,在命令行接口中进行的修改不会保存至任何位置,因此,重启varnish后这些设定会消失。此时,可以通过启动脚本使用-p选项在varnishd启动时为其设定参数的值。然而,除非特别需要对其进行修改,保持这些参数为默认值可以有效降低管理复杂度。

 

2、共享内存日志

 

共享内存日志(shared memory log)通常被简称为shm-log,它用于记录日志相关的数据,大小为80M。varnish以轮转(round-robin)的方式使用其存储空间。一般不需要对shm-log做出更多的设定,但应该避免其产生I/O,这可以使用tmpfs实现,其方法为在/etc/fstab中设定一个挂载至/var/lib/varnish目录(或其它自定义的位置)临时文件系统即可。

 

3、线程模型(Trheading model)

 

varnish的child进程由多种不同的线程组成,分别用于完成不同的工作。例如:

cache-worker线程: 每连接一个,用于处理请求;

cache-main线程:  全局只有一个,用于启动cache;

ban lurker线程:  一个,用于清理bans;

acceptor线程:    一个,用于接收新的连接请求;

epoll/kqueue线程: 数量可配置,默认为2,用于管理线程池;

expire线程 一个, 用于移除老化的内容;

backend poll线程: 每个后端服务器一个,用于检测后端服务器的健康状况;

 

在配置varnish时,一般只需为关注cache-worker线程,而且也只能配置其线程池的数量,而除此之外的其它均非可配置参数。与此同时,线程池的数量也只能在流量较大的场景下才需要增加,而且经验表明其多于2个对提升性能并无益处。

 

4、线程相关的参数(Threading parameters)

 

varnish为每个连接使用一个线程,因此,其worker线程的最大数决定了varnish的并发响应能力。下面是线程池相关的各参数及其配置:

 

thread_pool_add_delay      2 [milliseconds]

thread_pool_add_threshold     2 [requests]

thread_pool_fail_delay       200 [milliseconds]

thread_pool_max            500 [threads]

thread_pool_min            5 [threads]

thread_pool_purge_delay      1000 [milliseconds]

thread_pool_stack          65536 [bytes]

thread_pool_timeout         120 [seconds]

thread_pool_workspace      16384 [bytes]

thread_pools              2 [pools]

thread_stats_rate          10 [requests]

 

其中最关键的当属thread_pool_max和thread_pool_min,它们分别用于定义每个线程池中的最大线程数和最少线程数。因此,在某个时刻,至少有thread_pool_min*thread_pools个worker线程在运行,但至多不能超出thread_pool_max*thread_pools个。根据需要,这两个参数的数量可以进行调整,varnishstat命令的n_wrk_queued可以显示当前varnish的线程数量是否足够,如果队列中始终有不少的线程等待运行,则可以适当调大thread_pool_max参数的值。但一般建议每台varnish服务器上最多运行的worker线程数不要超出5000个。

 

 

当某连接请求到达时,varnish选择一个线程池负责处理此请求。而如果此线程池中的线程数量已经达到最大值,新的请求将会被放置于队列中或被直接丢弃。默认线程池的数量为2,这对最繁忙的varnish服务器来说也已经足够。

 


 

F.Varnish的命令行工具

 

1、varnishadm命令

 

命令语法:varnishadm [-t timeout] [-S secret_file] [-T address:port] [-n name] [command [...]]

 

通过命令行的方式连接至varnishd进行管理操作的工具,指定要连接的varnish实例的方法有两种:

-n name —— 连接至名称为“name”的实例;

-T address:port —— 连接至指定套接字上的实例;

 

其运行模式有两种,当不在命令行中给出要执行的"command"时,其将进入交互式模式;否则,varnishadm将执行指定的"command"并退出。要查看本地启用的缓存,可使用如下命令进行。

# varnishadm -S /etc/varnish/secret -T 127.0.0.1:6082 storage.list




vcl配置常见变量

   

1、在任何引擎中均可使用:


now, .host, .port


2、用于处理请求阶段:


client.ip, server.hostname, server.ip, server.port


req.request:请求方法


req.url: 请求的URL


req.proto: HTTP协议版本


req.backend: 用于服务此次请求的后端主机;


req.backend.healthy: 后端主机健康状态;


req.http.HEADER: 引用请求报文中指定的首部;


req.can_gzip:客户端是否能够接受gzip压缩格式的响应内容;


req.restarts: 此请求被重启的次数;




3、varnish向backend主机发起请求前可用的变量


bereq.request: 请求方法


bereq.url:请求url


bereq.proto:请求协议


bereq.http.HEADER:请求首部


bereq.connect_timeout: 等待与be建立连接的超时时长



4、backend主机的响应报文到达本主机(varnish)后,将其放置于cache中之前可用的变量


beresp.do_stream: 流式响应;


beresp.do_gzip:是否压缩之后再存入缓存;


beresp.do_gunzip:是否解压缩之后存入缓存


beresp.http.HEADER:报文首部;


beresp.proto: 协议


beresp.status:响应状态码


beresp.response:响应时的原因短语


beresp.ttl:响应对象剩余的生存时长,单位为second;


beresp.backend.name: 此响应报文来源backend名称;


beresp.backend.ip:后端主机ip


beresp.backend.port:后端主机的端口


beresp.storage:



5、缓存对象存入cache之后可用的变量


obj.proto:协议


obj.status:状态


obj.response:响应报文


obj.ttl:生存周期


obj.hits:命中


obj.http.HEADER:http首部



6、在决定对请求键做hash计算时可用的变量


req.hash:将请求交给hash



7、在为客户端准备响应报文时可用的变量


resp.proto:协议


resp.status:状态


resp.response:响应


resp.http.HEADER:http首部


各变量可用的状态引擎

技术分享



实验环境

 node2 192.168.139.4

 node4 192.168.139.8

 node5 192.168.139.9


 node1安装Varnish,gcc作为缓存服务器

 node4|node5安装httpd,作为web服务器



[root@node2 ~]# yum -y install varnish #本机gcc已近安装

[root@node4 ~]# service httpd start

[root@node5 ~]# service httpd start

编辑varnish的启动配置文件之前先保存一份

[root@node2 varnish]# cp  /etc/sysconfig/varnish /etc/sysconfig/varnish.bak1

[root@node2 ~]# vim /etc/sysconfig/varnish 

# Configuration file for varnish

#

# /etc/init.d/varnish expects the variable $DAEMON_OPTS to be set from this

# shell script fragment.

#


NFILES=131072 #定义可以打开的最大文件数量

MEMLOCK=82000 #定义log信息使用多大的内存空间


RELOAD_VCL=1 #

VARNISH_VCL_CONF=/etc/varnish/default.vcl #定义VCL的主配置文件

VARNISH_LISTEN_PORT=80 #定义监听的端口默认为80,默认为6082


VARNISH_ADMIN_LISTEN_ADDRESS=127.0.0.1 #管理varnish的监听地址

VARNISH_ADMIN_LISTEN_PORT=6082 #管理varnish的监听端口


VARNISH_SECRET_FILE=/etc/varnish/secret #varnish的密钥配置文件


VARNISH_MIN_THREADS=1 #varnish的最小进程数



VARNISH_MAX_THREADS=1000 #varnish的最大进程数


VARNISH_THREAD_TIMEOUT=120 #定义varnish的工作进程的超时时长


#VARNISH_STORAGE_FILE=/var/lib/varnish/varnish_storage.bin #缓存文件的存储类型和缓存路径,可以定                                              义为使用内存存储


#VARNISH_STORAGE_SIZE=1G #定义varnish的缓冲空间的大小

#VARNISH_STORAGE="maloc,256M" #定义以内存的方式缓存,缓存空间大小为256M

VARNISH_STORAGE="file,/var/lib/varnish/varnish_storage.bin,1G"#定义缓存以单个文件存储,不支持持久机制


VARNISH_TTL=120  #定义缓存时长


DAEMON_OPTS="-a ${VARNISH_LISTEN_ADDRESS}:${VARNISH_LISTEN_PORT} \ #启动命令

             -f ${VARNISH_VCL_CONF} \

             -T ${VARNISH_ADMIN_LISTEN_ADDRESS}:${VARNISH_ADMIN_LISTEN_PORT} \

             -t ${VARNISH_TTL} \

             -w ${VARNISH_MIN_THREADS},${VARNISH_MAX_THREADS},${VARNISH_THREAD_TIMEOUT} \

VARNISH_USER=varnish    #主进程所使用的用户

VARNISH_GROUP=varnish    #主进程所使用的组

#DAEMON_OPTS="-p thread_pool_min=5 -p thread_pool_max=500 -p thread_pool_timeout=300"

加这条配置后varnish启动不了

[root@node1 varnish]# service varnish start

Starting varnish HTTP accelerator:                         [  OK  ]



VCL配置文件的说明

vcl_recv    接收请求 

cacheable    判断是否为可缓存对象

incache     判断hash后的结果是否存在

vcl_hash    可缓存对象hash计算

vcl_hit     缓存中命中

vcl_miss     缓存中未命中

vcl_fetch    获取后端内容

vcl_deliver   构建缓存发送

vcl_pipe     客户端请求的方法不是常见方法时,直接交给后端服务器处理

vcl_pass     不检查缓存直接从后端服务器取

vcl_error    varnish直接返回错误响应


[root@node2 ~]# cd /etc/varnish/

[root@node2 varnish]# vim default.vcl

backend default {

  .host = "192.168.139.9";

  .port = "80";

}



技术分享

刷新一次看响应报文Status Code: 304 Not Modified



    1. Request URL:

      http://192.168.139.4/

    2. Request Method:

      GET

    3. Status Code:


      304 Not Modified

    4. Remote Address:

      192.168.139.4:80

  1. Response Headersview source

    1. Connection:

      keep-alive

    2. Date:

      Thu, 29 Dec 2016 11:17:25 GMT

    3. ETag:

      "dff01-23-544ca2bd6b91f"

    4. Last-Modified:

      Thu, 29 Dec 2016 11:13:13 GMT

    5. Via:

      1.1 varnish

    6. X-Varnish:

      1600444971


配置Varnish VCL策略配置文件,实现添加响应报文首部

[root@node2 varnish]# vim test1.vcl 


backend web1 {  --定义后端主机

  .host = "192.168.139.8";

  .port = "80"; --定义后端主机监听端口

}


sub vcl_deliver { --在vcl_deliver状态引擎中定义

     if (obj.hits > 0) { 

       set resp.http.X-Cache = "HIT"; --如果缓存命中次数大于0,则添加响应首部X-Cache;设置值为HIT,我实验时"HIT " + server.ip;会报错,已醉

     } else {

       set resp.http.X-Cache = "MISS"; --否则,设置值为MISS

     }

       return (deliver); --定义返回状态

}


[root@node2 varnish]# varnishadm -S /etc/varnish/secret -T 127.0.0.1:6082

200 199     

-----------------------------

Varnish Cache CLI 1.0

-----------------------------

Linux,2.6.32-573.el6.x86_64,x86_64,-sfile,-hcritbit


Type ‘help‘ for command list.

Type ‘quit‘ to close CLI session.


vcl.load test1 /etc/varnish/test1.vcl  #载入配置文件进行编译为二进制格式

200 13      

VCL compiled.


vcl.list                   #列出所有的vcl配置文件

200 47      

active         1 boot     #这个是default.vcl文件中的配置,active表示正在被使用

available       0 test1  #刚刚用vcl.load编译成二进制的vcl配置,available表示可使用的


vcl.use test1     #应用新的配置test1

200 0        #显示200 0则应用成功



技术分享

技术分享

第一次显示为MISS(未命中),再刷新

技术分享

技术分享

显示HIT(命中)

但是过一会后再刷新,有是MISS,说明缓存已经过期

技术分享



  1. [root@node2 varnish]# vim test1.vcl 

新加入以下内容

 

sub vcl_fetch {                      --在vcl.fetch状态引擎定义

  if (bereq.http.Set-Cookie) {       --如果varnish请求有cookies信息

     unset bereq.http.Set-Cookie;    --则将cookie信息清除(对cookie信息不进行缓存)

     set beresp.ttl = 120s;          --设置缓存时长为120s

  }

  return (deliver);                  --定义返回状态

}

[root@node2 varnish]# varnishadm -S /etc/varnish/secret -T 127.0.0.1:6082

200 199     

-----------------------------

Varnish Cache CLI 1.0

-----------------------------

Linux,2.6.32-573.el6.x86_64,x86_64,-sfile,-hcritbit


Type ‘help‘ for command list.

Type ‘quit‘ to close CLI session.


vcl.load test2 /etc/varnish/test1.vcl 

200 13      

VCL compiled.

vcl.list

200 71      

available       0 boot

active         3 test1

available       0 test2


vcl.show test2     #可以直接查看显示配置的vcl

200 436     

backend web1 {    

  .host = "192.168.139.8";    

  .port = "80";             

}

 

sub vcl_deliver {       

     if (obj.hits > 0) {   

       set resp.http.X-Cache = "HIT";

     } else {

       set resp.http.X-Cache = "MISS"; 

     }

       return (deliver);   

}

 

sub vcl_fetch {             

  if (bereq.http.Set-Cookie) { 

     unset bereq.http.Set-Cookie; 

     set beresp.ttl = 120s;      

  }

  return (deliver);             

}

vcl.use test2 #使用test2的配置

200 0   


测试

[root@node2 varnish]# curl -I http://192.168.139.4

HTTP/1.1 200 OK

Server: Apache/2.2.15 (CentOS)

Last-Modified: Thu, 29 Dec 2016 11:12:44 GMT

ETag: "dff0b-22-544ca2a1ed862"

Content-Type: text/html; charset=UTF-8

Content-Length: 34

Date: Fri, 30 Dec 2016 05:50:39 GMT

X-Varnish: 1531569619

Age: 0

Via: 1.1 varnish

Connection: keep-alive

X-Cache: MISS   #缓存未命中且Age;0


[root@node2 varnish]# curl -I http://192.168.139.4

HTTP/1.1 200 OK

Server: Apache/2.2.15 (CentOS)

Last-Modified: Thu, 29 Dec 2016 11:12:44 GMT

ETag: "dff0b-22-544ca2a1ed862"

Content-Type: text/html; charset=UTF-8

Content-Length: 34

Date: Fri, 30 Dec 2016 05:50:49 GMT

X-Varnish: 1531569620 1531569619

Age: 10

Via: 1.1 varnish 

Connection: keep-alive

X-Cache: HIT   #缓存命中,且缓存开始计时Age:10



配置Varnish VCL策略配置文件,利用varnish自带着函数(purge)清空缓存

[root@node2 varnish]# vim test1.vcl 

新加入以下内容


acl purge {            --定义ACL访问名称
  "localhost";         --授权本地主机允许访问
  "127.0.0.1";         --授权127.0.0.1允许访问
  "192.168.139.0"/24;     --授权192.168.139.0/24网段内主机访问
}
 
--以下设定,当发送PURGE请求的客户端不是在acl中设定的地址时,将返回405状态代码,提示Not allowed.
--当请求的URL是以.php和.cgi结尾时,则交给后端服务器处理响应
sub vcl_recv {
  if (req.request == "PURGE") {
      if (!client.ip ~ purge) {
      error 405 "Not allowed.";
   }
      elseif(req.url ~ "\.(php|cgi($|\?))"){
      return (pass);
   }
      else {
      return (lookup);
   }
  }
}
 
sub vcl_hit {                    --在vcl_hit状态引擎中定义策略
  if (req.request == "PURGE") {  --如果缓存命中且请求方法为"PURGE"
   purge;                        --执行内置函数purge  
   error 200 "Purged";           --返回信息及状态码
 
}
 
sub vcl_miss {                    --在vcl_miss中定义策略
  if (req.request == "PURGE") {   --如果请求方法"PURGE"没有命中
   purge;                         --执行内置函数purge
   error 200 "no cache.";         --返回信息及状态码
  }
}
 
sub vcl_pass {                            --在vcl_pass中定义策略
  if (req.request == "PURGE") {
   error 502 "Purged on a passed object.";
 }
}


在做实验时vcl.load加载vcl配置时在hit和miss段老是提醒错误,错误如下

Expected ‘(‘ got ‘;‘(program line 324), at(input Line 44 Pos 9) purge;     



只有把 purge; 去掉才可以,反正我已醉 看不出来错       


vcl.load -> vcl.use 后测试

[root@node2 varnish]# curl -I http://192.168.139.4

HTTP/1.1 200 OK

Server: Apache/2.2.15 (CentOS) 

Last-Modified: Thu, 29 Dec 2016 11:12:44 GMT

ETag: "dff0b-22-544ca2a1ed862"

Content-Type: text/html; charset=UTF-8

Content-Length: 34

Date: Fri, 30 Dec 2016 07:40:26 GMT

X-Varnish: 1531569643 1531569642 

Age: 31

Via: 1.1 varnish

Connection: keep-alive

X-Cache: HIT   #可以命中,且Age:31


[root@node2 varnish]# curl -I -X http://192.168.139.4

curl: no URL specified!

curl: try ‘curl --help‘ or ‘curl --manual‘ for more information

[root@node2 varnish]# curl -I -X PURGE  http://192.168.139.4

HTTP/1.1 200 Purged #协议/版本号 状态码(200) 请求方式

Server: Varnish  

Retry-After: 0

Content-Type: text/html; charset=utf-8

Content-Length: 380

Date: Fri, 30 Dec 2016 07:41:51 GMT

X-Varnish: 1531569644

Age: 0      #Age: 0

Via: 1.1 varnish

Connection: close

X-Cache: MISS #不能命中(此时缓存本应该还没过期,应该命中),curl请求时用PURGE时vcl的配置将缓存清理了






















本文出自 “11097124” 博客,请务必保留此出处http://11107124.blog.51cto.com/11097124/1887710

Varnish缓存机制详细介绍及简单配置

标签:varnish缓存机制详细介绍及简单配置

原文地址:http://11107124.blog.51cto.com/11097124/1887710

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!