`
catastiger
  • 浏览: 138554 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

使用ETags减少Web应用带宽和负载

 
阅读更多
介绍
最近,大众对于REST风格应用架构表现出强烈兴趣,这表明Web的优雅设计开始受到人们的注意。现在,我们逐渐理解了“3W架构(Architecture of the World Wide Web)”内在所蕴含的可伸缩性和弹性,并进一步探索运用其范式的方法。本文中,我们将探究一个可被Web开发者利用的、鲜为人知的工具,不引人注意的“ETag响应头(ETag Response Header)”,以及如何将它集成进基于Spring和Hibernate的动态Web应用,以提升应用程序性能和可伸缩性。

我们将要使用的Spring框架应用是基于“宠物诊所(petclinic)”的。下载文件中包含了关于如何增加必要的配置及源码的说明,你可以自己尝试。

什么是“ETag”?
HTTP协议规格说明定义ETag为“被请求变量的实体值” (参见 http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html —— 章节 14.19)。 另一种说法是,ETag是一个可以与Web资源关联的记号(token)。典型的Web资源可以一个Web页,但也可能是JSON或XML文档。服务器单独负责判断记号是什么及其含义,并在HTTP响应头中将其传送到客户端。

ETag如何帮助提升性能?
聪明的服务器开发者会把ETags和GET请求的“If-None-Match”头一起使用,这样可利用客户端(例如浏览器)的缓存。因为服务器首先产生ETag,服务器可在稍后使用它来判断页面是否已经被修改。本质上,客户端通过将该记号传回服务器要求服务器验证其(客户端)缓存。

其过程如下:

客户端请求一个页面(A)。
服务器返回页面A,并在给A加上一个ETag。
客户端展现该页面,并将页面连同ETag一起缓存。
客户再次请求页面A,并将上次请求时服务器返回的ETag一起传递给服务器。
服务器检查该ETag,并判断出该页面自上次客户端请求之后还未被修改,直接返回响应304(未修改——Not Modified)和一个空的响应体。
本文的其余部分将展示在基于Spring框架的Web应用中利用ETag的两种方法,该应用使用Spring MVC。首先我们将使用Servlet 2.3 Filter,利用展现视图(rendered view)的MD5校验和(checksum)以实现生成ETag的方法(一个“浅显的”ETag实现)。 第二种方法使用更为复杂的方法追踪view中所使用的model,以确定ETag有效性(一个“深入的”ETag实现)。尽管我们使用的是Spring MVC,但该技术可以应用于任何MVC风格的Web框架。

在我们继续之前,强调一下这里所展现的是提升动态产生页面性能的技术。已有的优化技术也应作为整体优化和应用性能特性调整分析的一部分来考虑。(见下)。

自顶向下的Web缓存

本文主要涉及对动态生成页面使用HTTP缓存技术。当考虑提升Web应用的性能的时候,应采取一个整体的、自顶向下的方法。为了这一目的,理解HTTP请求经过的各层是很重要的,应用哪些适当的技术取决于你所关注的热点。例如:

将Apache作为Servlet容器的前端,来处理如图片和javascript脚本这样的静态文件,而且还可以使用FileETag指令创建ETag响应头。
使用针对javascript文件的优化技术,如将多个文件合并到一个文件中以及压缩空格。
利用GZip和缓存控制头(Cache-Control headers)。
为确定你的Spring框架应用的痛处所在,可以考虑使用 JamonPerformanceMonitorInterceptor。
确信你充分利用ORM工具的缓存机制,因此对象不需要从数据库中频繁的再生。花时间确定如何让查询缓存为你工作是值得的。
确保你最小化数据库中获取的数据量,尤其是大的列表。如果每个页面只请求大列表的一个小子集,那么大列表的数据应由其中某个页面一次获得。
使放入到HTTP session中的数据量最小。这样内存得到释放,而且当将应用集群的时候也会有所帮助。
使用数据库明细(database profiling)工具来查看在查询的时候使用了什么索引,在更新的时候整个表没有被上锁。
当然,应用性能优化的至理名言是:两次测量,一次剪裁(measure twice, cut once)。哦,等等,这是对木工而言的!没错,但是它在这里也很适用!

ETag Filter内容体
我们要考虑的第一种方法是创建一个Servlet Filter,它将基于页面(MVC中的“View”)的内容产生其ETag 记号。乍一看,使用这种方法所获得的任何性能提升看起来都是违反直觉的。我们仍然不得不产生页面,而且还增加了产生记号的计算时间。然而,这里的想法是减少带宽使用。在大的响应时间情形下,如你的主机和客户端分布在这个星球的两端,这很大程度上是有益的。我曾见过东京办公室使用纽约服务器上托管的应用,其响应时间达到了 350 ms。随着并发用户数的增长,这将变成巨大的瓶颈。

代码
我们用来产生记号的技术是基于从页面内容计算MD5哈希值。这通过在响应之上创建一个包装器来实现。该包装器使用字节数组来保存所产生的内容,在filter链处理完成之后我们利用数组的MD5哈希值计算记号。

doFilter方法的实现如下所示。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
ServletException {
HttpServletRequest servletRequest = (HttpServletRequest) req;
HttpServletResponse servletResponse = (HttpServletResponse) res;

  ByteArrayOutputStream baos = new ByteArrayOutputStream();
ETagResponseWrapper wrappedResponse = new ETagResponseWrapper(servletResponse, baos);
chain.doFilter(servletRequest, wrappedResponse);

  byte[] bytes = baos.toByteArray();

  String token = """ + ETagComputeUtils.getMd5Digest(bytes) + """;
servletResponse.setHeader("ETag", token); // always store the ETag in the header

  String previousToken = servletRequest.getHeader("If-None-Match");
if (previousToken != null && previousToken.equals(token)) { // compare previous token with current one
logger.debug("ETag match: returning 304 Not Modified");
servletResponse.sendError(HttpServletResponse.SC_NOT_MODIFIED);
// use the same date we sent when we created the ETag the first time through
servletResponse.setHeader("Last-Modified", servletRequest.getHeader("If-Modified-Since"));
} else  { // first time through - set last modified time to now
Calendar cal = Calendar.getInstance();
cal.set(Calendar.MILLISECOND, 0);
Date lastModified = cal.getTime();
servletResponse.setDateHeader("Last-Modified", lastModified.getTime());

  logger.debug("Writing body content");
servletResponse.setContentLength(bytes.length);
ServletOutputStream sos = servletResponse.getOutputStream();
sos.write(bytes);
sos.flush();
sos.close();
}
}
清单 1:ETagContentFilter.doFilter

你需注意到,我们还设置了Last-Modified头。这被认为是为服务器产生内容的正确形式,因为其迎合了不认识ETag头的客户端。

下面的例子使用了一个工具类EtagComputeUtils来产生对象所对应的字节数组,并处理MD5摘要逻辑。我使用了javax.security MessageDigest来计算MD5哈希码。

public static byte[] serialize(Object obj) throws IOException {
byte[] byteArray = null;
ByteArrayOutputStream baos = null;
ObjectOutputStream out = null;
try {
// These objects are closed in the finally.
baos = new ByteArrayOutputStream();
out = new ObjectOutputStream(baos);
out.writeObject(obj);
byteArray = baos.toByteArray();
} finally {
if (out != null) {
out.close();
}
}
return byteArray;
}

  public static String getMd5Digest(byte[] bytes) {
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 cryptographic algorithm is not available.", e);
}
byte[] messageDigest = md.digest(bytes);
BigInteger number = new BigInteger(1, messageDigest);
// prepend a zero to get a "proper" MD5 hash value
StringBuffer sb = new StringBuffer("0");
sb.append(number.toString(16));
return sb.toString();
}
清单 2:ETagComputeUtils

直接在web.xml中配置filter。

    <filter>
       <filter-name>ETag Content Filter</filter-name>
       <filter-class>org.springframework.samples.petclinic.web.ETagContentFilter</filter-class>
     </filter>

      <filter-mapping>
       <filter-name>ETag Content Filter</filter-name>
       <url-pattern>/*.htm</url-pattern>
     </filter-mapping>
清单 3:web.xml中配置filter。

每个.htm文件将被EtagContentFilter过滤,如果页面自上次客户端请求后没有改变,它将返回一个空内容体的HTTP响应。

我们在这里展示的方法对特定类型的页面是有用的。但是,该方法有两个缺点:

我们是在页面已经被展现在服务器之后计算ETag的,但是在返回客户端之前。如果有Etag匹配,实际上并不需要再为model装进数据,因为要展现的页面不需要发送回客户端。
对于类似于在页脚显示日期时间这样的页面,即使内容实际上并没有改变,每个页面也将是不同的。
下一节,我们将着眼于另一种方法,其通过理解更多关于构造页面的底层数据来克服这些问题的某些限制。

ETag拦截器(Interceptor)
Spring MVC HTTP 请求处理途径中包括了在一个controller前插接拦截器(Interceptor)的能力,因而有机会处理请求。这儿是应用我们ETag比较逻辑的理想场所,因此如果我们发现构建一个页面的数据没有发生变化,我们可以避免进一步处理。

这儿的诀窍是你怎么知道构成页面的数据已经改变了?为了达到本文的目的,我创建了一个简单的ModifiedObjectTracker,它通过Hibernate事件侦听器清楚地知道插入、更新和删除操作。该追踪器为应用程序的每个view维护一个唯一的号码,以及一个关于哪些Hibernate实体影响每个view的映射。每当一个POJO被改变了,使用了该实体的view的计数器就加1。我们使用该计数值作为ETag,这样当客户端将ETag送回时我们就知道页面背后的一个或多个对象是否被修改了。

代码
我们就从ModifiedObjectTracker开始吧:

public interface ModifiedObjectTracker {
void notifyModified(> String entity);
}
够简单吧?这个实现还有一点更有趣的。任何时候一个实体改变了,我们就更新每个受其影响的view的计数器:

public void notifyModified(String entity) {
// entityViewMap is a map of entity -> list of view names
List views = getEntityViewMap().get(entity);

  if (views == null) {
return; // no views are configured for this entity
}

  synchronized (counts) {
for (String view : views) {
Integer count = counts.get(view);
counts.put(view, ++count);
}
}
}
一个“改变”就是插入、更新或者删除。这里给出的是侦听删除操作的处理器(配置为Hibernate 3 LocalSessionFactoryBean上的事件侦听器):

public class DeleteHandler extends DefaultDeleteEventListener {
private ModifiedObjectTracker tracker;

  public void onDelete(DeleteEvent event) throws HibernateException {
getModifiedObjectTracker().notifyModified(event.getEntityName());
}

public ModifiedObjectTracker getModifiedObjectTracker() {
return tracker;
}
  public void setModifiedObjectTracker(ModifiedObjectTracker tracker) {
this.tracker = tracker;
}
}
ModifiedObjectTracker通过Spring配置被注入到DeleteHandler中。还有一个SaveOrUpdateHandler来处理新建或更新POJO。

如果客户端发送回当前有效的ETag(意味着自上次请求之后我们的内容没有改变),我们将阻止更多的处理,以实现我们的性能提升。在Spring MVC里,我们可以使用HandlerInterceptorAdaptor并覆盖preHandle方法:

public final boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws
ServletException, IOException {
String method = request.getMethod();
if (!"GET".equals(method))
return true;

  String previousToken = request.getHeader("If-None-Match");
String token = getTokenFactory().getToken(request);

  // compare previous token with current one
if ((token != null) && (previousToken != null && previousToken.equals(""" + token + """))) {
response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
// re-use original last modified timestamp
response.setHeader("Last-Modified", request.getHeader("If-Modified-Since"))
return false; // no further processing required
}

  // set header for the next time the client calls
if (token != null) {
response.setHeader("ETag", """ + token + """);

  // first time through - set last modified time to now
Calendar cal = Calendar.getInstance();
cal.set(Calendar.MILLISECOND, 0);
Date lastModified = cal.getTime();
response.setDateHeader("Last-Modified", lastModified.getTime());
}

  return true;
}
我们首先确信我们正在处理GET请求(与PUT一起的ETag可以用来检测不一致的更新,但其超出了本文的范围。)。如果该记号与上次我们发送的记号相匹配,我们返回一个“304未修改”响应并“短路”请求处理链的其余部分。否则,我们设置ETag响应头以便为下一次客户端请求做好准备。

你需注意到我们将产生记号逻辑抽出到一个接口中,这样可以插接不同的实现。该接口有一个方法:

public interface ETagTokenFactory {
String getToken(HttpServletRequest request);
}
为了把代码清单减至最小,SampleTokenFactory的简单实现还担当了ETagTokenFactory的角色。本例中,我们通过简单返回请求URI的更改计数值来产生记号:

public String getToken(HttpServletRequest request) {
String view = request.getRequestURI();
Integer count = counts.get(view);
if (count == null) {
return null;
}

  return count.toString();
}
大功告成!

会话
这里,如果什么也没改变,我们的拦截器将阻止任何搜集数据或展现view的开销。现在,让我们看看HTTP头(借助于LiveHTTPHeaders),看看到底发生了什么。下载文件中包含了配置该拦截器的说明,因此owner.htm“能够使用ETag”:

我们发起的第一个请求说明该用户已经看过了这个页面:

---------------------------------------------------------- 
http://localhost:8080/petclinic/owner.htm?ownerId=10 

GET /petclinic/owner.htm?ownerId=10 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8
X-lori-time-1: 1182364348062
If-Modified-Since: Wed, 20 Jun 2007 18:29:03 GMT
If-None-Match: "-1"

HTTP/1.x 304 Not Modified
Server: Apache-Coyote/1.1
Date: Wed, 20 Jun 2007 18:32:30 GMT
我们现在应该做点修改,看看ETag是否改变了。我们给这个物主增加一个宠物:

----------------------------------------------------------
http://localhost:8080/petclinic/addPet.htm?ownerId=10

GET /petclinic/addPet.htm?ownerId=10 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://localhost:8080/petclinic/owner.htm?ownerId=10
Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8
X-lori-time-1: 1182364356265

HTTP/1.x 200 OK
Server: Apache-Coyote/1.1
Pragma: No-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Cache-Control: no-cache, no-store
Content-Type: text/html;charset=ISO-8859-1
Content-Language: en-US
Content-Length: 2174
Date: Wed, 20 Jun 2007 18:32:57 GMT
----------------------------------------------------------
http://localhost:8080/petclinic/addPet.htm?ownerId=10 

POST /petclinic/addPet.htm?ownerId=10 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://localhost:8080/petclinic/addPet.htm?ownerId=10
Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8
X-lori-time-1: 1182364402968
Content-Type: application/x-www-form-urlencoded
Content-Length: 40
name=Noddy&birthDate=1000-11-11&typeId=5
HTTP/1.x 302 Moved Temporarily
Server: Apache-Coyote/1.1
Pragma: No-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Cache-Control: no-cache, no-store
Location: http://localhost:8080/petclinic/owner.htm?ownerId=10
Content-Language: en-US
Content-Length: 0
Date: Wed, 20 Jun 2007 18:33:23 GMT
因为对addPet.htm我们没有配置任何已知ETag,也没有设置头信息。现在,我们再一次查看id为10的物主。注意ETag这时是1:

----------------------------------------------------------
http://localhost:8080/petclinic/owner.htm?ownerId=10

GET /petclinic/owner.htm?ownerId=10 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://localhost:8080/petclinic/addPet.htm?ownerId=10
Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8
X-lori-time-1: 1182364403109
If-Modified-Since: Wed, 20 Jun 2007 18:29:03 GMT
If-None-Match: "-1"

HTTP/1.x 200 OK
Server: Apache-Coyote/1.1
Etag: "1"
Last-Modified: Wed, 20 Jun 2007 18:33:36 GMT
Content-Type: text/html;charset=ISO-8859-1
Content-Language: en-US
Content-Length: 4317
Date: Wed, 20 Jun 2007 18:33:45 GMT
最后,我们再次查看id为10的物主。这次我们的ETag命中了,我们得到一个“304未修改”响应:

----------------------------------------------------------
http://localhost:8080/petclinic/owner.htm?ownerId=10

GET /petclinic/owner.htm?ownerId=10 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8
X-lori-time-1: 1182364493500
If-Modified-Since: Wed, 20 Jun 2007 18:33:36 GMT
If-None-Match: "1"

HTTP/1.x 304 Not Modified
Server: Apache-Coyote/1.1
Date: Wed, 20 Jun 2007 18:34:55 GMT
我们已经利用HTTP缓存节约了带宽和计算时间!

细粒度印记(The Fine Print):实践中,我们可以通过以更细粒度的跟踪对象变化来获得更大的功效,例如使用对象id。然而,这种使修改对象关联到view上的想法高度依赖应用程序的整体数据模型设计。这里的实现(ModifiedObjectTracker)是说明性的,有意为更多的探索提供想法。它并不是旨在生产环境中使用(比如它在簇中使用还不稳定)。一个可选的更深的考虑是使用数据库触发器来跟踪变化,让拦截器访问触发器所写入的表。

结论
我们已经看了两种使用ETag减少带宽和计算的方法。我希望本文已为你当下或将来基于Web的项目提供了精神食粮,并正确评价在底层利用ETag响应头的做法。

正如牛顿(Isaac Newton)的名言所说:“如果说我看得更远,那是因为我站在巨人的肩膀上。”REST风格应用的核心是简单、好的软件设计、不要重新发明轮子。我相信随着使用量和知名度的增长,针对基于Web应用的REST风格架构有益于主流应用开发的迁移,我期盼着它在我将来的项目中发挥更大的作用。

关于作者
我要感谢我的同事Patrick Bourke和Erick Dorvale的帮助,他们对这篇文章提供的反馈意见。

代码和说明可以从这里下载。


查看英文原文:Using ETags to Reduce Bandwith & Workload with Spring & Hibernate
来自:http://www.infoq.com/cn/articles/etags
分享到:
评论

相关推荐

    使用ETags减少Web应用带宽和负载第1/2页

    ### 使用ETags减少Web应用带宽和负载 #### 引言 随着RESTful架构的兴起,Web开发领域正经历一场革新。人们越来越意识到万维网架构(Architecture of the World Wide Web)的重要性和潜力,特别是在可扩展性和弹性...

    Web重点难点学习资料

    本文将详细介绍Web前端优化机制和JavaScript闭包的概念及其使用方法。 首先,Web前端优化机制是提升网页性能、改善用户体验的关键技术之一。其优化策略可大致分为内容优化、服务器端优化和Cookie优化三部分。 内容...

    lein-codeindex:使用etags,ctags或gtags的索引代码

    **lein-codeindex:使用etags,ctags或gtags的索引代码** 在软件开发过程中,快速定位代码是提高效率的关键。`lein-codeindex` 是一个Leiningen插件,它帮助Clojure开发者通过集成`etags`, `ctags`, 或者 `gtags` ...

    ctags-etags-5.8-2.el6.i686.rpm

    ctags-etags-5.8-2.el6.i686.rpm

    tomacat及插件

    3. Tomcat Manager:允许通过Web界面管理应用的部署、启动、停止和更新。 4. JConsole:官方提供的JVM监视工具,可以监控Tomcat的内存、线程、类加载等状态。 5. CATALINA_OPTS和JAVA_OPTS:环境变量用于传递JVM参数...

    有关web效率,有著名的14条规则

    每次请求都需要消耗时间和带宽,因此减少请求次数可以显著提高页面加载速度。 - **实现方法**: - 合并文件:将多个脚本或样式文件合并成一个文件。 - CSS Sprites:将多个小图像合并到一张大图像中,通过CSS背景...

    etag-lastmod-example:使用带有 Spring Data REST 的 ETags 进行乐观并发控制和条件 GET

    本文将详细讲解如何利用Spring Data REST框架和ETags、Last-Modified头来实现这些功能,主要针对Java开发者。 首先,ETag(实体标签)是一种HTTP响应头,用于标识资源的版本或状态。当客户端发送一个GET请求时,...

    tomcat-5.5.9

    4. **缓存策略**:使用Etags或Last-Modified来缓存静态资源,减轻服务器负载。 5. **日志优化**:合理设置日志级别,避免过多的日志输出消耗系统资源。 六、安全策略 1. **SSL/TLS配置**:启用HTTPS,保护数据...

    web前端性能分析工具导引

    总结来说,前端性能分析是提高Web应用质量的关键,涉及到众多策略和工具的运用。遵循Yahoo的14+20条高性能网页最佳实践,结合各种性能分析工具,可以帮助开发者有效地减少加载时间,节省数据传输量,减轻服务器负担...

    Building a RESTful Web Service with Spring

    Spring框架是一个广泛使用的Java平台,用于构建企业级应用,特别是Web应用。我们的RESTful Web服务将会遵循以下架构设计: - **架构**:我们将采用分层架构,确保各个组件之间职责清晰。 - **数据模型**:定义服务...

    ctags-etags-5.8-22.el8.ppc64le.rpm

    官方离线安装包,亲测可用

    Tomcat 5.5 中文文档 (html)

    3. **缓存策略**:使用缓存机制,如Etags和Last-Modified头,减少服务器负载。 4. **JSP缓存**:开启JSP编译缓存,减少不必要的JSP编译。 **五、Tomcat故障排查** 1. **日志系统**:Tomcat生成多种日志文件,如...

    ctags-etags-5.8-22.el8.x86_64.rpm

    官方离线安装包,亲测可用

Global site tag (gtag.js) - Google Analytics