티스토리 뷰
쇼핑몰 프로젝트를 디벨롭하다가 처음 보는 경고 문구랑 마주쳤다
Unable to add the resource at [/img/product22.jpg] to the cache for web application [] because there was insufficient free space available after evicting expired cache entries - consider increasing the maximum size of the cache
이 경고문이 해당 페이지에 필요한 정적 이미지 리소스의 수만큼 적혀있어서 원인을 찾아보았는데, 10MB로 설정된 톰캣 기본 캐시 사이즈가 부족할 때 나오는 경고문이라고 한다. xml 파일 설정으로 해결할 수 있다고 나오지만, 보다 정확한 이유가 궁금하여 구글링을 해보던 중, 아래 stackoverflow 게시글에서 친절하고 상세한 설명을 찾을 수 있었다.
Tomcat 8 throwing - org.apache.catalina.webresources.Cache.getResource Unable to add the resource
I have just upgraded Tomcat from version 7.0.52 to 8.0.14. I am getting this for lots of static image files: org.apache.catalina.webresources.Cache.getResource Unable to add the resource at [...
stackoverflow.com
이하 내용은 해당 글을 토대로 오류 발생 이유와 해결법을 번역, 정리한 내용이다.
해결법만 필요하다면 글을 쭉~ 내려서 해결법 / 내가 해결한 방법 을 확인하면 된다.
배경 지식
WebSource는 웹 애플리케이션 내의 파일이나 디렉토리를 의미한다. 톰캣은 WebSources를 성능 향상을 위해 캐시에 저장할 수 있다. 이 모든 정적 리소스 캐시 데이터 총합량의 최댓값 디폴트는 10240 kbyte, 즉 10MB 로 지정되어 있다. WebResource는 요청 될 시 (예를 들어 정적 이미지가 로딩될 때) 캐시에 로딩되고, 캐시 내부 데이터(cache entry)로 불린다. 모든 캐시 내부 데이터는 지정된 TTL(time to live) 시간 동안 캐시에 머무를 수 있다. TTL 시간이 초과된 후에는 캐시에서 지워질 수 있다. TTL의 디폴트 설정값은 5000 millisecounds(=5초) 이다.
원인
캐시 클래스의 이하 코드는 캐시 정책의 세부사항을 보여준다.
152 // Content will not be cached but we still need metadata size
153 long delta = cacheEntry.getSize();
154 size.addAndGet(delta);
156 if (size.get() > maxSize) {
157 // Process resources unordered for speed. Trades cache
158 // efficiency (younger entries may be evicted before older
159 // ones) for speed since this is on the critical path for
160 // request processing
161 long targetSize =
162 maxSize * (100 - TARGET_FREE_PERCENT_GET) / 100;
163 long newSize = evict(
164 targetSize, resourceCache.values().iterator());
165 if (newSize > maxSize) {
166 // Unable to create sufficient space for this resource
167 // Remove it from the cache
168 removeCacheEntry(path);
169 log.warn(sm.getString("cache.addFail", path));
170 }
171 }
아래는 주석의 번역본이다.
152 // 캐시에 저장되지 않더라도 메타데이터 사이즈가 필요함
153 long delta = cacheEntry.getSize();
154 size.addAndGet(delta);
156 if (size.get() > maxSize) {
157 // 속도가 캐시의 효율성에 비해 우선하는데,
158 // 요청 처리에 있어 지금이 중요한 절차에 해당되기에,
159 // 리소스 처리를 별도 요청하지 않는다
160 // (더 최신의 데이터가 예전 데이터보다 일찍 지워질 수 있는 문제가 있다)
161 long targetSize =
162 maxSize * (100 - TARGET_FREE_PERCENT_GET) / 100;
163 long newSize = evict(
164 targetSize, resourceCache.values().iterator());
165 if (newSize > maxSize) {
166 // 이 리소스에 충분한 캐시 공간을 확보할 수 없을 때
167 // 이 리소스를 캐시에서 제거
168 removeCacheEntry(path);
169 log.warn(sm.getString("cache.addFail", path));
170 }
171 }
WebResource를 불러올 때, 코드에서 캐시 사이즈를 새로 계산한다. 계산된 사이즈가 설정 최대값 사이즈보다 클 때, 하나 또는 그 이상의 캐시 내부 데이터가 제거되어야 한다. 그래서 코드는 "targetSize"를 계산하는데, targetSize란 캐시가 (최적화를 위해) 목표로 하는 사이즈를 뜻한다. 디폴트값은 최대값의 95%로 설정되어 있다. targetSize에 맞추기 위해, 내부 데이터들은 캐시에서 삭제되어야 한다. 이 과정은 아래 코드로 이루어진다.
215 private long evict(long targetSize, Iterator<CachedResource> iter) {
217 long now = System.currentTimeMillis();
219 long newSize = size.get();
221 while (newSize > targetSize && iter.hasNext()) {
222 CachedResource resource = iter.next();
224 // Don't expire anything that has been checked within the TTL
225 if (resource.getNextCheck() > now) {
226 continue;
227 }
229 // Remove the entry from the cache
230 removeCacheEntry(resource.getWebappPath());
232 newSize = size.get();
233 }
235 return newSize;
236 }
아래는 주석 번역본이다.
215 private long evict(long targetSize, Iterator<CachedResource> iter) {
217 long now = System.currentTimeMillis();
219 long newSize = size.get();
221 while (newSize > targetSize && iter.hasNext()) {
222 CachedResource resource = iter.next();
224 // TTL 시간이 초과되지 않는 건 삭제시키지 않는다
225 if (resource.getNextCheck() > now) {
226 continue;
227 }
229 // 캐시에서 데이터 삭제
230 removeCacheEntry(resource.getWebappPath());
232 newSize = size.get();
233 }
235 return newSize;
236 }
이렇게 TTL 시간이 초과된 캐시 내부 데이터는 삭제되고, 그럼에도 targetSize를 초과할 때가 있다.
캐시 내부 데이터를 지움으로써 캐시 공간을 확보하려는 시도 후에, 아래 코드가 실행된다.
165 if (newSize > maxSize) {
166 // Unable to create sufficient space for this resource
167 // Remove it from the cache
168 removeCacheEntry(path);
169 log.warn(sm.getString("cache.addFail", path));
170 }
아래는 주석 번역본이다.
165 if (newSize > maxSize) {
166 // 이 리소스를 위해 충분한 공간을 확보하지 못했을 시,
167 // 캐시에서 이 리소스를 제거
168 removeCacheEntry(path);
169 log.warn(sm.getString("cache.addFail", path));
170 }
공간 확보 시도 후에서도 최대 사이즈를 여전히 초과한다면, 해당 메시지가 출력될 것이다.
cache.addFail=Unable to add the resource at [{0}] to the cache for web application [{1}] because there was insufficient free space available after evicting expired cache entries - consider increasing the maximum size of the cache
문제점
경고 메세지가 보여주듯이 문제는 이하와 같다.
insufficient free space available after evicting expired cache entries - consider increasing the maximum size of the cache
만료된 캐시 내부 데이터 삭제 후에도 여유공간의 확보에 실패함 - 캐시의 최대 사이즈를 늘리는 것을 고려해라
웹 애플리케이션이 짧은 시간 (5초) 내에 아직 캐시에 올라가지않은 다량의 webResources를 (총합 사이즈가 디폴트값 10MB 사이즈가 넘을 때) 로드한다면 경고메시지가 출력될 것이다.
톰캣 7 버전에서는 이런 경고가 나오지 않는 것이 다소 이상할 수 있다.
(이하 톰캣 7버전에 대한 내용은 생략합니다.)
해결법
이하의 해결방안이 있다.
- 캐시 사이즈 늘이기 (추천)
- TTL 시간을 줄이기 (비추천)
- 경고문 끄기 (비추천)
- 캐시 사용 비활성화
1. 캐시 사이즈 늘이기 (추천)
tomcat.apache.org/tomcat-8.0-doc/config/resources.html
Apache Tomcat 8 Configuration Reference (8.0.53) - The Resources Component
The Resources element represents all the resources available to the web application. This includes classes, JAR files, HTML, JSPs and any other files that contribute to the web application. Implementations are provided to use directories, JAR files and WAR
tomcat.apache.org
이 링크에서 설명된 방법이다. Context 요소의 $CATALINA_BASE/conf/context.xml 에 <Resources cacheMaxSize="XXXXX" />를 추가한다. "XXXXX"에는 kbytes단위로 증가시킬 캐시 사이즈를 넣는다.
디폴트 값이 10240(=10MB) 이기에, 이보다 큰 사이즈를 입력해라.
최적화 값을 찾아야 할 수도 있다. 트래픽/리소스 요청이 급증했을 때, 같은 문제가 다시 발생할 수 있음의 유의하라. 새로 캐시 사이즈를 설정할 때 마다 서버를 재실행하지 않으려면, JMX을 이용해서 설정하면 된다. JMX를 사용하려면 $CATALINA_BASE/conf/server.xml 의 Server 요소에
<Listener className="org.apache.catalina.mbeans.JmxRemoteLifecycleListener" rmiRegistryPortPlatform="6767" rmiServerPortPlatform="6768" />
를 추가하고, catalina-jmx-remote.jar 를 tomcat.apache.org/download-80.cgi 에서 다운받은 후, $CATALINA_HOME/lib 에 추가하라.
그리고 jConsole (Java JDK에서 기본 제공)로 JMK를 서버에 연결한 후, 서버가 실행되는 동안 캐시 사이즈 설정을 찾아내서 변경하면 된다. 이 변경사항은 즉시 적용될 것이다.
2. TTL 시간 줄이기 (비추천)
TTL 값을 5000 밀리초 이하로 줄이는 것이다. 예시 : <Resources cacheTtl="2000" />
효과적으로 램에서 캐시량을 줄일 것이다.
3. 경고문 끄기 (비추천)
(내용 생략)
4. 캐시 사용 비활성화
Apache Tomcat 8 Configuration Reference (8.0.53) - The Resources Component
The Resources element represents all the resources available to the web application. This includes classes, JAR files, HTML, JSPs and any other files that contribute to the web application. Implementations are provided to use directories, JAR files and WAR
tomcat.apache.org
위 항목을 false. <Resources cachingAllowed="false" /> 로 설정함으로써 캐시 사용을 비활성화 할 수 있다.
내가 해결한 방법
나는 스프링 부트 내장 톰캣을 사용하고 있었기에, 다음 글의 답변을 참고하여 해결했다.
stackoverflow.com/questions/39146476/how-does-spring-boot-control-tomcat-cache
How does Spring Boot control Tomcat cache?
I'm porting 5 years old Spring MVC application with JSPs to Spring Boot. Therefore, according to sample in http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-feature...
stackoverflow.com
src/main/webapp/META-INF 경로에 context.xml 파일을 추가하고 이하 코드를 넣었다.
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<Resources cachingAllowed="true" cacheMaxSize="204800" />
</Context>
그리고 실행해보니, 경고문이 출력되지 않고 정상적으로 작동한다.
같은 문제가 또 생긴다면 사이즈를 증가시키면 될 것 같다.
아무래도 전공생이 아니다 보니, 이런 문제를 맞닥뜨리면 처음 보는 용어와 생경한 설명에 오랜 시간 붙잡고 읽게 되는 것 같다. 그래도 이런 오류들 덕분에 매번 몰랐던 사실을 많이 알아가니, 가벼운 오류들은 고맙다는 생각도 든다 😉