/* * Copyright 2014 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.netflix.eureka; import javax.inject.Inject; import javax.inject.Singleton; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.netflix.appinfo.AbstractEurekaIdentity; import com.netflix.appinfo.EurekaClientIdentity; import com.netflix.eureka.util.EurekaMonitors; import com.netflix.discovery.util.RateLimiter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Rate limiting filter, with configurable threshold above which non-privileged clients * will be dropped. This feature enables cutting off non-standard and potentially harmful clients * in case of system overload. Since it is critical to always allow client registrations and heartbeats into * the system, which at the same time are relatively cheap operations, the rate limiting is applied only to * full and delta registry fetches. Furthermore, since delta fetches are much smaller than full fetches, * and if not served my result in following full registry fetch from the client, they have relatively * higher priority. This is implemented by two parallel rate limiters, one for overall number of * full/delta fetches (higher threshold) and one for full fetches only (low threshold). * <p> * The client is identified by {@link AbstractEurekaIdentity#AUTH_NAME_HEADER_KEY} HTTP header * value. The privileged group by default contains: * <ul> * <li> * {@link EurekaClientIdentity#DEFAULT_CLIENT_NAME} - standard Java eureka-client. Applications using * this client automatically belong to the privileged group. * </li> * <li> * {@link com.netflix.eureka.EurekaServerIdentity#DEFAULT_SERVER_NAME} - connections from peer Eureka servers * (internal only, traffic replication) * </li> * </ul> * It is possible to turn off privileged client filtering via * {@link EurekaServerConfig#isRateLimiterThrottleStandardClients()} property. * <p> * Rate limiting is not enabled by default, but can be turned on via configuration. Even when disabled, * the throttling statistics are still counted, although on a separate counter, so it is possible to * measure the impact of this feature before activation. * * <p> * Rate limiter implementation is based on token bucket algorithm. There are two configurable * parameters: * <ul> * <li> * burst size - maximum number of requests allowed into the system as a burst * </li> * <li> * average rate - expected number of requests per second * </li> * </ul> * * @author Tomasz Bak */ @Singleton public class RateLimitingFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(RateLimitingFilter.class); private static final Set<String> DEFAULT_PRIVILEGED_CLIENTS = new HashSet<String>( Arrays.asList(EurekaClientIdentity.DEFAULT_CLIENT_NAME, EurekaServerIdentity.DEFAULT_SERVER_NAME) ); private static final Pattern TARGET_RE = Pattern.compile("^.*/apps(/[^/]*)?$"); enum Target {FullFetch, DeltaFetch, Application, Other} /** * Includes both full and delta fetches. */ private static final RateLimiter registryFetchRateLimiter = new RateLimiter(TimeUnit.SECONDS); /** * Only full registry fetches. */ private static final RateLimiter registryFullFetchRateLimiter = new RateLimiter(TimeUnit.SECONDS); private EurekaServerConfig serverConfig; @Inject public RateLimitingFilter(EurekaServerContext server) { this.serverConfig = server.getServerConfig(); } // for non-DI use public RateLimitingFilter() { } @Override public void init(FilterConfig filterConfig) throws ServletException { if (serverConfig == null) { EurekaServerContext serverContext = (EurekaServerContext) filterConfig.getServletContext() .getAttribute(EurekaServerContext.class.getName()); serverConfig = serverContext.getServerConfig(); } } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { Target target = getTarget(request); if (target == Target.Other) { chain.doFilter(request, response); return; } HttpServletRequest httpRequest = (HttpServletRequest) request; if (isRateLimited(httpRequest, target)) { incrementStats(target); if (serverConfig.isRateLimiterEnabled()) { ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); return; } } chain.doFilter(request, response); } private static Target getTarget(ServletRequest request) { Target target = Target.Other; if (request instanceof HttpServletRequest) { HttpServletRequest httpRequest = (HttpServletRequest) request; String pathInfo = httpRequest.getRequestURI(); if ("GET".equals(httpRequest.getMethod()) && pathInfo != null) { Matcher matcher = TARGET_RE.matcher(pathInfo); if (matcher.matches()) { if (matcher.groupCount() == 0 || matcher.group(1) == null || "/".equals(matcher.group(1))) { target = Target.FullFetch; } else if ("/delta".equals(matcher.group(1))) { target = Target.DeltaFetch; } else { target = Target.Application; } } } if (target == Target.Other) { logger.debug("URL path {} not matched by rate limiting filter", pathInfo); } } return target; } private boolean isRateLimited(HttpServletRequest request, Target target) { if (isPrivileged(request)) { logger.debug("Privileged {} request", target); return false; } if (isOverloaded(target)) { logger.debug("Overloaded {} request; discarding it", target); return true; } logger.debug("{} request admitted", target); return false; } private boolean isPrivileged(HttpServletRequest request) { if (serverConfig.isRateLimiterThrottleStandardClients()) { return false; } Set<String> privilegedClients = serverConfig.getRateLimiterPrivilegedClients(); String clientName = request.getHeader(AbstractEurekaIdentity.AUTH_NAME_HEADER_KEY); return privilegedClients.contains(clientName) || DEFAULT_PRIVILEGED_CLIENTS.contains(clientName); } private boolean isOverloaded(Target target) { int maxInWindow = serverConfig.getRateLimiterBurstSize(); int fetchWindowSize = serverConfig.getRateLimiterRegistryFetchAverageRate(); boolean overloaded = !registryFetchRateLimiter.acquire(maxInWindow, fetchWindowSize); if (target == Target.FullFetch) { int fullFetchWindowSize = serverConfig.getRateLimiterFullFetchAverageRate(); overloaded |= !registryFullFetchRateLimiter.acquire(maxInWindow, fullFetchWindowSize); } return overloaded; } private void incrementStats(Target target) { if (serverConfig.isRateLimiterEnabled()) { EurekaMonitors.RATE_LIMITED.increment(); if (target == Target.FullFetch) { EurekaMonitors.RATE_LIMITED_FULL_FETCH.increment(); } } else { EurekaMonitors.RATE_LIMITED_CANDIDATES.increment(); if (target == Target.FullFetch) { EurekaMonitors.RATE_LIMITED_FULL_FETCH_CANDIDATES.increment(); } } } @Override public void destroy() { } // For testing purposes static void reset() { registryFetchRateLimiter.reset(); registryFullFetchRateLimiter.reset(); } }