• Varnish的VCL


    Varnish的子进程

    VCL

    	Varnish配置语言VCL是一种“域”专有类型的配置语言,用于描述Varnish Cache的请求处理和文档高速缓存策略。
    	当加载新配置时,Manager进程会创建VCC进程,然后将VCL代码转换为C代码,C代码被gcc编译成共享对象,然后共享对象被加载到cacher进程中。
    	VCL有多个状态引擎(state engine),状态之间存在相关性,但状态引擎彼此间互相隔离。
    	每个状态引擎可使用return(x)指明关联至哪个下一级引擎,每个状态引擎对应于vcl文件中的一个配置段,即为subroutine。
    		vcl_hash --> return(hit) --> vcl_hit
    

    varnish的有限状态机

    	实际上		
    		vcl_recv ——>vcl_purge/vcl_pipe/vcl_hash		
    		vcl_hash——>vcl_miss/vcl_hit		
    		其他的如图所示		
    		vcl_hash --> return(X) --> vcl_X		
    				
    	VCL工作流看作是一个有限状态机 。
    	每个请求被分开处理,每个请求在任何给定的时间都是独立于其他人的,状态是相关的但也是孤立的。
    	内置的VCL代码始终存在,并附加在您自己的VCL下面。
    	当Varnish处理请求时,它首先解析请求本身,稍后Varnish将请求方法从头文件中分离出来,验证它是否是有效的HTTP请求等等。
    	当基本解析完成后,首先检查策略以作出决定,策略是VCL代码用于做出决定的一组规则。
    	每个状态都在VCL编码中有对应的可用参数,在VCL上的状态被概念化为子进程,在VCL中采用的等待状态描述中的等待状态即不是参数也不是返回值。
    	每个内置的子程序以前缀vcl_开始,被return(action)终止,退出当前状态并指示varnish进入下一个状态,其中action是一个关键词用于指定期望的输出。
    	子进程可以检查和操控http报文头部区域和各种其他方面的请求,并指示如何处理请求。
    	varnish创建的子进程被挂在varnish的工作中,这些子进程被以vcl_为前缀来命名的,而我们自己的进程就不能以其为前缀命名。
    	我们称这些有关状态的子进程为状态引擎(state engine),VCL有多个状态引擎,状态之间存在相关性,但状态引擎彼此间互相隔离,每个状态引擎可使用return(x)指明关联至哪个下一级引擎,每个状态引擎对应于vcl文件中的一个配置段,即为subroutine。
    	vcl_recv的默认配置:
    		sub vcl_recv {
    			if (req.method == "PRI") {
    				/* We do not support SPDY or HTTP/2.0 */
    				return (synth(405));
    			}
    			if (req.method != "GET" &&
    			req.method != "HEAD" &&
    			req.method != "PUT" &&
    			req.method != "POST" &&
    			req.method != "TRACE" &&
    			req.method != "OPTIONS" &&
    			req.method != "DELETE") {
    				/* Non-RFC2616 or CONNECT which is weird. */
    				return (pipe);
    			}
    	
    			if (req.method != "GET" && req.method != "HEAD") {
    				/* We only deal with GET and HEAD by default */
    				return (pass);
    			}
    			if (req.http.Authorization || req.http.Cookie) {
    				/* Not cacheable by default */
    				return (pass);
    			}
    				return (hash);
    			}
    		}
    

    客户端和后端工作线程的详细的varnish请求流程

    	Client Side:
    		vcl_recv, vcl_pass, vcl_hit, vcl_miss, vcl_pipe, vcl_purge, vcl_synth, vcl_deliver
    	Backend Side:
    		vcl_backend_fetch, vcl_backend_response, vcl_backend_error
    	vcl_recv:
    		hash:vcl_hash
    		pass: vcl_pass 
    		pipe: vcl_pipe
    		synth: vcl_synth
    		purge: vcl_hash --> vcl_purge
    	vcl_hash:
    		lookup:
    		hit: vcl_hit
    		miss: vcl_miss
    		pass, hit_for_pass: vcl_pass
    		purge: vcl_purge
    	两个特殊的引擎:
    		vcl_init:在处理任何请求之前要执行的vcl代码:主要用于初始化VMODs;
    		vcl_fini:所有的请求都已经结束,在vcl配置被丢弃时调用;主要用于清理VMODs;
    

    VCL语法

    		VCL文件以vcl 4.0开始;
    		//,#和/ *  * /是注释
    		子进程用sub关键字声明
    		没有循环,状态有限的变量
    		用下一个关键字作为return()函数的参数来终止语句,即:return(action)
    		特定领域
    	从Varnish 4.0开始,每个VCL文件必须首先在文件顶部用一个特殊的标记声明它的版本。
    	块由花括号分隔,以分号结尾。
    	VCL中的子程序既不带参数,也不返回值。
    	VCL中的子程序只能通过HTTP头交换数据。
    	VCL有终止语句,而不是传统的返回值。
    	子程序在执行return(*action*)语句时结束执行。
    	该行action告诉varnish下一步该做什么。
    	VCL有两个指令来使用来自另一个文件的内容,这些指令是include和import,并用于不同的目的。
    		include用于从另一个文件插入VCL代码,Varnish查找被varnishd的vcl_dir参数指定目录中的文件,请注意include语法中的引号。
    		import用于加载VMOD并将其功能提供给VCL代码,Varnish将查找VMOD以加载到由varnishd的vmod_dir参数指定的目录。
    		请注意import语法中缺少引号,你可以使用varnishtest中的include和import。
    	三类主要语法:
    		sub subroutine {
    			...
    		}
    		
    		if CONDITION {
    			...
    		} else {	
    			...
    		}
    		
    		return(), hash_data()
    

    VCL函数、关键字和变量

    	函数:
    		regsub(str, regex, sub)
    		regsuball(str, regex, sub)
    		ban(boolean expression)
    		hash_data(input)
    		synthetic(str)
    	Keywords:
    		call subroutine
    		return(action)
    		new
    		set
    		unset 
    	操作符:
    		==, !=, ~, >, >=, <, <=
    		逻辑操作符:&&, ||, !
    		变量赋值:=
    	举例:
    		obj.hits是内建变量,用于保存某缓存项的从缓存中命中的次数;
    		if (obj.hits>0) {
    			set resp.http.X-Cache = "HIT via " + server.ip;
    				} else {
    					set resp.http.X-Cache = "MISS from " + server.ip;
    				}
    	
    	变量类型:
    		内建变量:
    			req.*:request,表示由客户端发来的请求报文相关;
    				req.http.*
    					req.http.User-Agent, req.http.Referer, ...
    			bereq.*:由varnish发往BE主机的httpd请求相关;
    				bereq.http.*
    			beresp.*:由BE主机响应给varnish的响应报文相关;
    				beresp.http.*
    			resp.*:由varnish响应给client相关;
    			obj.*:存储在缓存空间中的缓存对象的属性;只读;
    			
    		常用变量:
    			bereq.*, req.*:
    				bereq.http.HEADERS
    				bereq.request:请求方法;
    				bereq.url:请求的url;
    				bereq.proto:请求的协议版本;
    				bereq.backend:指明要调用的后端主机;
    				req.http.Cookie:客户端的请求报文中Cookie首部的值; 
    				req.http.User-Agent ~ "chrome"
    			beresp.*, resp.*:
    				beresp.http.HEADERS
    				beresp.status:响应的状态码;
    				reresp.proto:协议版本;
    				beresp.backend.name:BE主机的主机名;
    				beresp.ttl:BE主机响应的内容的余下的可缓存时长;
    			obj.*
    				obj.hits:此对象从缓存中命中的次数;
    				obj.ttl:对象的ttl值
    			server.*
    				server.ip:varnish主机的IP;
    				server.hostname:varnish主机的Hostname;
    			client.*
    				client.ip:发请求至varnish主机的客户端IP;
    			
    		用户自定义:
    			set 
    			unset 
    
    	vcl_backend_response
    			覆盖某些URL的缓存时间
    			剥离Set-Cookie不需要的头部字段
    			剥离Vary头部字段
    			将helper-headers添加到对象以用于禁止
    			清理服务器响应
    			应用其他缓存策略
    		vcl_backend_response采用以下其中之一会被终止:deliver,retry,abandon。
    		deliver终止动作可以或者可以不依赖于后端的响应插入对象到缓存中。
    		retry操作使Varnish通过调用vcl_backend_fetch子程序再次将请求传输到后端。
    		abandon操作会放弃来自后端的任何响应。
    		后端可能会响应一个304HTTP头部,当有时间戳if-modified-since在http头部,且请求对象没能被修改时304响应会发生。
    		如果请求触及一个非新鲜的对象,Varnish将If-Modified-Since头的值添加t_origin到请求中,并将其发送到后端。
    		304响应不包含消息正文,因此Varnish使用缓存中的实体构建响应,304响应更新缓存对象的属性。
    		内建vcl_backend_response
    			sub vcl_backend_response {
    				if (beresp.ttl <= 0s ||
    					beresp.http.Set-Cookie ||
    					beresp.http.Surrogate-control ~ "no-store" ||
    					(!beresp.http.Surrogate-Control &&
    					beresp.http.Cache-Control ~ "no-cache|no-store|private") ||
    					beresp.http.Vary == "*") {
    					/*
    					 * Mark as "Hit-For-Pass" for the next 2 minutes
    					 */
    					set beresp.ttl = 120s;
    					set beresp.uncacheable = true;
    				}
    				return (deliver);
    			}
    			vcl_backend_response内建子进程被设计于避免缓存那些不希望的情况。
    			例如,避免缓存cookies响应,带有set-cookie http头域的响应,这个内建子进程也避免请求serialization,这个在waiting state选项中有描述。
    			为避免请求serialization,beresp.uncacheable被设定为true,并轮流创建以hit-fot-pass对象。
    			hti-fot-pass详细解释了这个对象的类型。
    			如果你仍然决定通过采用自己设定的来跳过内建vcl_backend_response子进程,请确保绝不要设定beresp.ttl为0。
    			如果你跳过内建子进程并设置TLL值为0,可以有效地从缓存中删除最终有可能用于避免请求serialization的对象。
    			berep.ttl的初始值
    				在varnish运行vcl_backend_response前,beresp.ttl变量就已经被设定了。
    				beresp.ttl用它在下面找到的第一个值进行初始化:
    					Cache-Control响应头字段中的s-maxage变量
    					Cache-Control响应头字段中的max-age变量
    					Expires响应报头字段
    					default_ttl参数
    				默认情况,下面的状态码会被缓存:
    					200:ok
    					203:非权威信息
    					300:多种选择
    					301:永久移动
    					302:暂时移动
    					304:没有修改
    					307:临时重定向
    					410:gone
    					404: Not Found
    				你可以不采用上面列出的而缓存其他状态码,但你需要在vcl_backend_response中给beresp.ttl设置一个正值。
    				因为beresp.ttl的设置是在vcl_backend_response执行之前,你可以修改cache-control头域的导引而不英雄beresp.ttl,反之亦然。
    				后端响应可能包括共享缓存s-maxage的最大响应头字段,通过所有varnish服务该字段覆盖了所有max-age值。
    				例如,如果后端发送cache-control:max-age=300,s-maxage=3600,所有varnish installations将缓存带有一个age值大于等于3600秒的缓存对象,这就意味着在age为301到3600s间的响应将不会被客户端web浏览器缓存,这是因为age的值超过了max-age。
    				一个明智的方法是使用s-maxage指令来指示varnish缓存响应。然后,在递送响应前使用vcl_backend_response上的regsub()来删除s-maxage指定。采用这个方法,你可以为varnish servers安全地使用s-maxage,并为客户端设置max-age为持续缓存。
    				警告 :
    					请记住,删除或更改Age响应标题字段可能会影响响应在下游的处理方式。删除Age字段的影响取决于下游中间件或客户端的HTTP实施。例如,假设您有三个varnish服务器串行设置。如果您删除Age第一个Varnish服务器中的字段,则第二个Varnish服务器将假定Age=0。在这种情况下,您可能会无意中将陈旧的对象传递给客户端。
    			示例:
    				1.设置.jpg urls的TTL设置为60秒
    					sub vcl_backend_response {
    						if (bereq.url ~ ".jpg$") {
    							set beresp.ttl = 60s;
    						}
    					}
    					上面的例子将以.jpg结尾的所有URL缓存60秒。请记住,内置的VCL仍然被执行。这意味着带有Set-Cookie字段的图像不会被缓存。
    				2.缓存.JPG 60秒仅当s-maxage不存在
    					sub vcl_backend_response {
    						if (beresp.http.cache-control !~ "s-maxage" && bereq.url ~ ".jpg$") {
    							set beresp.ttl = 60s;
    						}
    					}
    

    VCL子程序

    	VCL子进程,在其中定制Varnish的行为。
    	VCL子例程可用于:
    		添加自定义标头,更改Varnish错误消息的外观,在Varnish中添加HTTP重定向功能,清除内容以及定义缓存对象的哪些部分是唯一的。
    	注意 
    		强烈建议尽可能让默认的内置子程序,内置子程序的设计考虑到安全性,这通常意味着它们可以合理的方式处理VCL代码中的任何缺陷。
    	vcl_recv
    			规范化客户端输入
    			选择一个后端Web服务器
    			重新编写Web应用程序的客户端数据
    			根据客户端输入决定缓存策略
    			访问控制列表(ACL)
    			安全屏障,例如针对SQL注入攻击
    			修复错误,例如index.htlm- >index.html
    		vcl_recv是Varnish第一个VCL子进程,将客户端请求解析为其基本数据结构之后执行。 
    		vcl_recv有四个主要用途:
    			修改客户端数据以减少缓存的多样性。
    			决定使用哪个Web服务器。
    			根据客户端数据决定缓存策略。
    			执行特定Web应用程序所需的重写规则。
    		在vcl_recv你可以执行以下终止操作:
    			pass:它通过缓存查找,但它执行Varnish请求流的其余部分。 pass不会将来自后端的响应存储在缓存中。
    			pipe:此操作创建一个全双工管道,将客户端请求转发到后端,且不查看其内容。后端回复被转发回客户端且不缓存其内容。由于Varnish不再尝试将内容映射到请求上,因此任何子进程的请求发送给活动连接将被通过pipe转发。pipe请求不会出现在任何日志中。
    			hash:它在缓存中查找请求。
    			purge:它在缓存中查找请求以便删除它。
    			synth -从Varnish生成合成响应。这种合成响应通常是一个带有错误信息的网页。 synth也可以用来重定向客户端请求。
    		同样可以使用vcl_recv来设置以下安全措施。varnish不是入侵检测系统的替代品,但仍可以用来提前阻止一些典型的攻击。简单访问控制列表(ACL)也可以应用到vcl_recv上。
    		内建的vcl_recv子进程不会缓存所有你想要的,同时也最好不要缓存错误内容而是把它们发送给错误的用户。
    		重新访问内置的vcl_recv:
    			sub vcl_recv {
    				if (req.method == "PRI") {
    					/* We do not support SPDY or HTTP/2.0 */
    					return (synth(405));
    				}
    				if (req.method != "GET" &&
    				  req.method != "HEAD" &&
    				  req.method != "PUT" &&
    				  req.method != "POST" &&
    				  req.method != "TRACE" &&
    				  req.method != "OPTIONS" &&
    				  req.method != "DELETE") {
    					/* Non-RFC2616 or CONNECT which is weird. */
    					return (pipe);
    				}
    				if (req.method != "GET" && req.method != "HEAD") {
    					/* We only deal with GET and HEAD by default */
    					return (pass);
    				}
    				if (req.http.Authorization || req.http.Cookie) {
    					/* Not cacheable by default */
    					return (pass);
    				}
    				return (hash);
    			}
    		例子:
    			基本设备检测
    				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_pass
    		进入pass模式是调用
    			sub vcl_pass {
    				return (fetch);
    			}
    		当上一层子进程返回pass动作后才会调用vcl_pass子进程,这动作的请求是在pass模式中设置的,vcl_pass通常作为一个重要的catch-all,服务于vcl_hit和vcl_miss执行结果。
    		vcl_pass可能会返回是三个动作:fetch、synth、或者是restart。
    		当返回的的是fetch时,正在进行的请求就采用pass模式。
    		采用pass模式从请求中抓取的对象不被缓存,但会传递到客户端。
    		synth和restart返回的动作会调用相关的子进程。
    		hit-for-pass
    				当一个对象不应该被缓存是使用
    				hit-for-pass对象取代抓取的对象
    				存在TTL
    			一些请求就不应该被缓存,一个典型的例子就是当一个请求页中含有set-cookie响应头部时,且必须并只能把它递送给所需的客户端。
    			因此你可以告诉varnish创建个hit-for-pass的对象并存储这个对象到缓存,而不是存储抓取的这个对象,分布式的请求被采用pass模式处理。
    			当一个对象不需要被缓存是,beresp.uncacheable变量会设置为true。
    			结果,cacher进程会保持对hit-for-pass对象的hash散列应用,这种情况下,对请求的查找操作会传递给hash来找个hit-for-pass对象,如此类的请求会被vclpass子进程中的pass模式给处理。
    			如同其他缓存对象一样,hit-for-pass对象也有一个TTL(生命周期)。一旦生命周期过了,这个对象就会从缓存上删除。
    
    	vcl_backend_fetch
    		sub vcl_backend_fetch {
    			return (fetch);
    		}
    		vcl_backend_fetch 可以从vcl_miss或vcl_pass中调用。当vcl_backend_fetch从vcl_miss中调用时,抓取的对象会被缓存。如果vcl_backend_fetch被从vcl_pass中调用时,抓取的对象也不会被缓存的,即使是obj.ttl或obj.keep变量的值比0大。
    		一个相关的变量是bereq.uncacheable,这个变量指示出从后端来的对象请求是否被缓存。当然从pass请求中来的对象是绝不被缓存的。
    		vcl_backend_fetch有俩个可能的终端操作,fetch或abandon。fetch动作发送请求给后动,abandon动作调用vcl_synth子进程。内建vcl_bakend_fetch子进程只返回fetch动作。
    		后端响应被vcl_backend-response还是vcl_backend_error处理取决于响应来之于那个服务。如果Varnish收到语法正确的HTTP响应,则Varnish将控制权交给vcl_backend_response。语法正确的HTTP响应包括HTTP 5xx错误代码。如果Varnish没有收到HTTP响应,则将控制权交给vcl_backend_error。
    	vcl_hash
    			定义什么是唯一的请求
    			vcl_hash终是在vcl_recv后,或者另个子进程范围hash动作关键词。
    			sub vcl_hash {
    				hash_data(req.url);
    				if (req.http.host) {
    					hash_data(req.http.host);
    				} else {
    					hash_data(server.ip);
    				}
    				return (lookup);
    			}
    		vcl_hash定义要用于缓存对象的hash key。
    		Hash key将一个缓存对象与另一个缓存对象区分开来,默认的VCL为vcl_hash添加主机名或ip地址,同时添加请求的url给cache hash。
    		vcl_hash的一个用法是在cache hash上添加用户名来识别用户指定的数据,当然缓存用户数据时应该谨慎进行,一个更好的选择可能是hash每个会话缓存对象。
    		vcl_hash子进程返回lookup操作关键字,不像其他动作关键词,lookup是一个操作,而不是子进程,在vcl_hash后的下个状态取决于在缓存中lookup的查找。
    		当lookup操作没能匹配到任何hash时,它会创建一个带有busy标志的对象并存储在缓存中,然后,请求会被发送到vcl_miss子进程中,一旦请求被处理busy标志会被删除,并从后端的响应中更新对象。
    		随后遇到busy标记的对象请求将被发送到等待列表中,这个等待名单旨在提高响应性能,这个在waiting state 选项中有解释。
    		注意:一个高速缓存散列可以指代一个或多个对象变量。对象变量是基于Vary头域的创建的。在一个缓存散列下保留多个变量是比较好的做法,而不是每个变量创建一个散列。
    	vcl_hit
    			在lookup操作之后执行,调用vcl_hash,找到(hits)在缓存上的对象。
    			sub vcl_hit {
    				if (obj.ttl >= 0s) {
    					// A pure unadultered hit, deliver it
    					return (deliver);
    				}
    				if (obj.ttl + obj.grace > 0s) {
    					// Object is in grace, deliver it
    					// Automatically triggers a background fetch
    					return (deliver);
    				}
    				// fetch & deliver once we get the result
    				return (fetch);
    			}
    		vcl_hit子进程通常通过调用含有deliver,restart或者synth的return()来进行终止。
    		如果对象的TTL+grace time没有过时的话,返回的deliver会控制vcl_deliver。
    		如果过时时间超过了TTL,但没有超过TTL+grace time,deliver会调用与vcl_deliver同步的background fetch。
    		background fetch是一种异步调用,用来插入一个新的请求对象到缓存中。grace time会在grace模式选项中有解释。
    		restart重启传输,并增加重启计数器设定值。如果重启的次数比max_restarts设定的值要大,varnish会发出一个guru mediation的错误。
    		synth(status code,reason)返回指定状态码给客户端并丢弃请求。
    
    	vcl_miss
    			如果一个请求对象没有被lookup操作找到时子进程会被调用。
    			包含有是否尝试从后端检索文档以及使用那个后端的策略。
    			sub vcl_miss {
    				return (fetch);
    			}
    		子进程vcl_hit和vcl_miss是相关的。你很少调用他们,因为HTTP请求投吧的修改通常是在vcl_recv中进行。但是,如果你不希望发送X-Varnish头部给后端服务,你可以把它移动动vcl_miss或vcl_pass中。基于这种情况,你可以使用unset bereq ,http,x-varnish。
    
    	vcl_deliver
    			所有请求流程的公共最后退出点,除了通过vcl_pipe的请求。
    			经常用于添加和移除debug-headers。
    			sub vcl_deliver {
    				return (deliver);
    			}
    		vcl_deliver子进程是简单的,同样也是对修改varnish的输出很有用的。如果你需要删除一个头部,或添加一个不应该存储在cache中的头部,vcl_deliver可以胜任这个工作。
    		在vcl_deliver中常用的且被可被修改的变量是:
    			resp.http.*:发送个客户端的头部,它们可以被set和unset。
    			resp.status:状态码为200,404,503等
    			resp.reason:被返回给客户端的http状态信息
    			obj.hit:在对象上的cache-hits的数。因此,0代表miss,可以评估这个变量来轻松地显示响应是来自缓存命中还是未命中。
    			req.restarts:在VCL中发出的重启次数 - 如果没有发生,则返回0。
    		
    	vcl_synth
    			用于在Varnish中生成内容
    			错误消息可以在这里创建
    			其他用例:重定向用户(301/302重定向)
    			vcl/default-vcl_synth.vcl:
    				sub vcl_synth {
    					set resp.http.Content-Type = "text/html; charset=utf-8";
    					set resp.http.Retry-After = "5";
    					synthetic( {"<!DOCTYPE html>
    				<html>
    				  <head>
    					<title>"} + resp.status + " " + resp.reason + {"</title>
    				  </head>
    				  <body>
    					<h1>Error "} + resp.status + " " + resp.reason + {"</h1>
    					<p>"} + resp.reason + {"</p>
    					<h3>Guru Meditation:</h3>
    					<p>XID: "} + req.xid + {"</p>
    					<hr>
    					<p>Varnish cache server</p>
    				  </body>
    				</html>
    				"} );
    					return (deliver);
    				}
    		你可以创建合成响应,例如,在vcl_synth上的个性化错误信息。调用这个子进程你可以做:
    			return (synth(status_code, "reason"));
    		注意synth不是一个关键字,而是个带有参数的函数。
    		你必须为vcl_synth明确地返回status code和reason参数。在resp.http上设置合成响应的头部。
    		注意:
    			从 vcl/default-vcl_synth.vcl注意到 {" and "}可以用于创建多行的字段。这个不仅限于synthetic()函数,在其他地址也可以使用。
    			vcl_synth定义的对象绝不在缓存上存储,对立与vcl_backend_error定义的对象。
    		例子:
    			使用vcl_synth重定向请求
    				sub vcl_recv {
    					if (req.http.host == "www.example.com") {
    						set req.http.location = "http://example.com" + req.url;
    						return (synth(750, "Permanently moved"));
    					}
    				}
    				sub vcl_synth {
    					if (resp.status == 750) {
    						set resp.http.location = req.http.location;
    						set resp.status = 301;
    						return (deliver);
    					}
    				}
    
  • 相关阅读:
    扩展AuthorizeAttribute
    扩展ValidationAttribute 1
    动态linq to list排序
    CSS属性书写顺序及命名规则
    增强网站可访问性的25种方法
    -webkit-filter是神马?
    docker安装
    docker的体系结构
    docker和虚拟化
    初识docker——docker基本概述
  • 原文地址:https://www.cnblogs.com/shenxm/p/8465135.html
Copyright © 2020-2023  润新知