View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.hadoop.hbase.http;
19  
20  import com.google.common.base.Joiner;
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.lang.reflect.Method;
25  import java.util.ArrayList;
26  import java.util.List;
27  import java.util.concurrent.TimeUnit;
28  import java.util.concurrent.atomic.AtomicInteger;
29  import java.util.concurrent.locks.Lock;
30  import java.util.concurrent.locks.ReentrantLock;
31  
32  import javax.servlet.http.HttpServlet;
33  import javax.servlet.http.HttpServletRequest;
34  import javax.servlet.http.HttpServletResponse;
35  
36  import org.apache.commons.logging.Log;
37  import org.apache.commons.logging.LogFactory;
38  import org.apache.hadoop.hbase.util.ProcessUtils;
39  
40  /**
41   * Servlet that runs async-profiler as web-endpoint.
42   * Following options from async-profiler can be specified as query paramater.
43   * //  -e event          profiling event: cpu|alloc|lock|cache-misses etc.
44   * //  -d duration       run profiling for duration seconds (integer)
45   * //  -i interval       sampling interval in nanoseconds (long)
46   * //  -j jstackdepth    maximum Java stack depth (integer)
47   * //  -b bufsize        frame buffer size (long)
48   * //  -t                profile different threads separately
49   * //  -s                simple class names instead of FQN
50   * //  -o fmt[,fmt...]   output format: summary|traces|flat|collapsed|svg|tree|jfr
51   * //  --width px        SVG width pixels (integer)
52   * //  --height px       SVG frame height pixels (integer)
53   * //  --minwidth px     skip frames smaller than px (double)
54   * //  --reverse         generate stack-reversed FlameGraph / Call tree
55   * Example:
56   * - To collect 30 second CPU profile of current process (returns FlameGraph svg)
57   * curl "http://localhost:10002/prof"
58   * - To collect 1 minute CPU profile of current process and output in tree format (html)
59   * curl "http://localhost:10002/prof?output=tree&duration=60"
60   * - To collect 30 second heap allocation profile of current process (returns FlameGraph svg)
61   * curl "http://localhost:10002/prof?event=alloc"
62   * - To collect lock contention profile of current process (returns FlameGraph svg)
63   * curl "http://localhost:10002/prof?event=lock"
64   * Following event types are supported (default is 'cpu') (NOTE: not all OS'es support all events)
65   * // Perf events:
66   * //    cpu
67   * //    page-faults
68   * //    context-switches
69   * //    cycles
70   * //    instructions
71   * //    cache-references
72   * //    cache-misses
73   * //    branches
74   * //    branch-misses
75   * //    bus-cycles
76   * //    L1-dcache-load-misses
77   * //    LLC-load-misses
78   * //    dTLB-load-misses
79   * //    mem:breakpoint
80   * //    trace:tracepoint
81   * // Java events:
82   * //    alloc
83   * //    lock
84   */
85  public class ProfileServlet extends HttpServlet {
86  
87    private static final long serialVersionUID = 1L;
88    private static final Log LOG = LogFactory.getLog(ProfileServlet.class);
89  
90    private static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
91    private static final String ALLOWED_METHODS = "GET";
92    private static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
93    private static final String CONTENT_TYPE_TEXT = "text/plain; charset=utf-8";
94    private static final String ASYNC_PROFILER_HOME_ENV = "ASYNC_PROFILER_HOME";
95    private static final String ASYNC_PROFILER_HOME_SYSTEM_PROPERTY = "async.profiler.home";
96    private static final String PROFILER_SCRIPT = "/profiler.sh";
97    private static final int DEFAULT_DURATION_SECONDS = 10;
98    private static final AtomicInteger ID_GEN = new AtomicInteger(0);
99    static final String OUTPUT_DIR = System.getProperty("java.io.tmpdir") + "/prof-output";
100 
101   enum Event {
102     CPU("cpu"),
103     ALLOC("alloc"),
104     LOCK("lock"),
105     PAGE_FAULTS("page-faults"),
106     CONTEXT_SWITCHES("context-switches"),
107     CYCLES("cycles"),
108     INSTRUCTIONS("instructions"),
109     CACHE_REFERENCES("cache-references"),
110     CACHE_MISSES("cache-misses"),
111     BRANCHES("branches"),
112     BRANCH_MISSES("branch-misses"),
113     BUS_CYCLES("bus-cycles"),
114     L1_DCACHE_LOAD_MISSES("L1-dcache-load-misses"),
115     LLC_LOAD_MISSES("LLC-load-misses"),
116     DTLB_LOAD_MISSES("dTLB-load-misses"),
117     MEM_BREAKPOINT("mem:breakpoint"),
118     TRACE_TRACEPOINT("trace:tracepoint"),;
119 
120     private final String internalName;
121 
122     Event(final String internalName) {
123       this.internalName = internalName;
124     }
125 
126     public String getInternalName() {
127       return internalName;
128     }
129 
130     public static Event fromInternalName(final String name) {
131       for (Event event : values()) {
132         if (event.getInternalName().equalsIgnoreCase(name)) {
133           return event;
134         }
135       }
136 
137       return null;
138     }
139   }
140 
141   enum Output {
142     SUMMARY,
143     TRACES,
144     FLAT,
145     COLLAPSED,
146     SVG,
147     TREE,
148     JFR
149   }
150 
151   @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "SE_TRANSIENT_FIELD_NOT_RESTORED",
152     justification = "This class is never serialized nor restored.")
153   private transient Lock profilerLock = new ReentrantLock();
154   private transient volatile Process process;
155   private String asyncProfilerHome;
156   private Integer pid;
157 
158   public ProfileServlet() {
159     this.asyncProfilerHome = getAsyncProfilerHome();
160     this.pid = ProcessUtils.getPid();
161     LOG.info("Servlet process PID: " + pid + " asyncProfilerHome: " + asyncProfilerHome);
162   }
163 
164   @Override
165   protected void doGet(final HttpServletRequest req, final HttpServletResponse resp)
166       throws IOException {
167     if (!HttpServer.isInstrumentationAccessAllowed(getServletContext(), req, resp)) {
168       resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
169       setResponseHeader(resp);
170       resp.getWriter().write("Unauthorized: Instrumentation access is not allowed!");
171       return;
172     }
173 
174     // make sure async profiler home is set
175     if (asyncProfilerHome == null || asyncProfilerHome.trim().isEmpty()) {
176       resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
177       setResponseHeader(resp);
178       resp.getWriter().write("ASYNC_PROFILER_HOME env is not set.\n\n" +
179         "Please ensure the prerequsites for the Profiler Servlet have been installed and the\n" +
180         "environment is properly configured. For more information please see\n" +
181         "http://hbase.apache.org/book.html#profiler\n");
182       return;
183     }
184 
185     // if pid is explicitly specified, use it else default to current process
186     pid = getInteger(req, "pid", pid);
187 
188     // if pid is not specified in query param and if current process pid cannot be determined
189     if (pid == null) {
190       resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
191       setResponseHeader(resp);
192       resp.getWriter().write(
193         "'pid' query parameter unspecified or unable to determine PID of current process.");
194       return;
195     }
196 
197     final int duration = getInteger(req, "duration", DEFAULT_DURATION_SECONDS);
198     final Output output = getOutput(req);
199     final Event event = getEvent(req);
200     final Long interval = getLong(req, "interval");
201     final Integer jstackDepth = getInteger(req, "jstackdepth", null);
202     final Long bufsize = getLong(req, "bufsize");
203     final boolean thread = req.getParameterMap().containsKey("thread");
204     final boolean simple = req.getParameterMap().containsKey("simple");
205     final Integer width = getInteger(req, "width", null);
206     final Integer height = getInteger(req, "height", null);
207     final Double minwidth = getMinWidth(req);
208     final boolean reverse = req.getParameterMap().containsKey("reverse");
209 
210     if (process == null || !isAlive(process)) {
211       try {
212         int lockTimeoutSecs = 3;
213         if (profilerLock.tryLock(lockTimeoutSecs, TimeUnit.SECONDS)) {
214           try {
215             File outputFile = new File(OUTPUT_DIR, "async-prof-pid-" + pid + "-" +
216               event.name().toLowerCase() + "-" + ID_GEN.incrementAndGet() + "." +
217               output.name().toLowerCase());
218             List<String> cmd = new ArrayList<>();
219             cmd.add(asyncProfilerHome + PROFILER_SCRIPT);
220             cmd.add("-e");
221             cmd.add(event.getInternalName());
222             cmd.add("-d");
223             cmd.add("" + duration);
224             cmd.add("-o");
225             cmd.add(output.name().toLowerCase());
226             cmd.add("-f");
227             cmd.add(outputFile.getAbsolutePath());
228             if (interval != null) {
229               cmd.add("-i");
230               cmd.add(interval.toString());
231             }
232             if (jstackDepth != null) {
233               cmd.add("-j");
234               cmd.add(jstackDepth.toString());
235             }
236             if (bufsize != null) {
237               cmd.add("-b");
238               cmd.add(bufsize.toString());
239             }
240             if (thread) {
241               cmd.add("-t");
242             }
243             if (simple) {
244               cmd.add("-s");
245             }
246             if (width != null) {
247               cmd.add("--width");
248               cmd.add(width.toString());
249             }
250             if (height != null) {
251               cmd.add("--height");
252               cmd.add(height.toString());
253             }
254             if (minwidth != null) {
255               cmd.add("--minwidth");
256               cmd.add(minwidth.toString());
257             }
258             if (reverse) {
259               cmd.add("--reverse");
260             }
261             cmd.add(pid.toString());
262             process = ProcessUtils.runCmdAsync(cmd);
263 
264             // set response and set refresh header to output location
265             setResponseHeader(resp);
266             resp.setStatus(HttpServletResponse.SC_ACCEPTED);
267             String relativeUrl = "/prof-output/" + outputFile.getName();
268             resp.getWriter().write(
269               "Started [" + event.getInternalName() +
270               "] profiling. This page will automatically redirect to " +
271               relativeUrl + " after " + duration + " seconds.\n\ncommand:\n" +
272               Joiner.on(" ").join(cmd));
273 
274             // to avoid auto-refresh by ProfileOutputServlet, refreshDelay can be specified via
275             // url param
276             int refreshDelay = getInteger(req, "refreshDelay", 0);
277 
278             // instead of sending redirect, set auto-refresh so that browsers will refresh with
279             // redirected url
280             resp.setHeader("Refresh", (duration + refreshDelay) + ";" + relativeUrl);
281             resp.getWriter().flush();
282           } finally {
283             profilerLock.unlock();
284           }
285         } else {
286           setResponseHeader(resp);
287           resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
288           resp.getWriter().write(
289             "Unable to acquire lock. Another instance of profiler might be running.");
290           LOG.warn("Unable to acquire lock in " + lockTimeoutSecs +
291             " seconds. Another instance of profiler might be running.");
292         }
293       } catch (InterruptedException e) {
294         LOG.warn("Interrupted while acquiring profile lock.", e);
295         resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
296       }
297     } else {
298       setResponseHeader(resp);
299       resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
300       resp.getWriter().write("Another instance of profiler is already running.");
301     }
302   }
303 
304   // Java 8+ has Process#isAlive, earlier versions do not
305   private static Method isAliveMethod;
306   static {
307     try {
308       isAliveMethod = Process.class.getDeclaredMethod("isAlive");
309     } catch (Exception e) {
310       isAliveMethod = null;
311     }
312   }
313 
314   private static boolean isAlive(Process process) {
315     // Invoke Process#isAlive if we can
316     if (isAliveMethod != null) {
317       try {
318         return (boolean) isAliveMethod.invoke(process);
319       } catch (Exception e) {
320         if (LOG.isTraceEnabled()) {
321           LOG.trace("Failed to invoke Process#isAlive on " + process, e);
322         }
323         // fall through to alternative
324       }
325     }
326     // We can still determine if the process is alive or not by calling Process#exitValue,
327     // which will throw an exception if the process has not exited.
328     try {
329       int exitValue = process.exitValue();
330       if (LOG.isTraceEnabled()) {
331         LOG.trace("Process " + process + " is dead with exitValue " + exitValue);
332       }
333     } catch (IllegalThreadStateException e) {
334       // The process is still alive.
335       return true;
336     }
337     // As far as we can determine, the process is dead.
338     return false;
339   }
340 
341   private Integer getInteger(final HttpServletRequest req, final String param,
342       final Integer defaultValue) {
343     final String value = req.getParameter(param);
344     if (value != null) {
345       try {
346         return Integer.valueOf(value);
347       } catch (NumberFormatException e) {
348         return defaultValue;
349       }
350     }
351     return defaultValue;
352   }
353 
354   private Long getLong(final HttpServletRequest req, final String param) {
355     final String value = req.getParameter(param);
356     if (value != null) {
357       try {
358         return Long.valueOf(value);
359       } catch (NumberFormatException e) {
360         return null;
361       }
362     }
363     return null;
364   }
365 
366   private Double getMinWidth(final HttpServletRequest req) {
367     final String value = req.getParameter("minwidth");
368     if (value != null) {
369       try {
370         return Double.valueOf(value);
371       } catch (NumberFormatException e) {
372         return null;
373       }
374     }
375     return null;
376   }
377 
378   private Event getEvent(final HttpServletRequest req) {
379     final String eventArg = req.getParameter("event");
380     if (eventArg != null) {
381       Event event = Event.fromInternalName(eventArg);
382       return event == null ? Event.CPU : event;
383     }
384     return Event.CPU;
385   }
386 
387   private Output getOutput(final HttpServletRequest req) {
388     final String outputArg = req.getParameter("output");
389     if (req.getParameter("output") != null) {
390       try {
391         return Output.valueOf(outputArg.trim().toUpperCase());
392       } catch (IllegalArgumentException e) {
393         return Output.SVG;
394       }
395     }
396     return Output.SVG;
397   }
398 
399   private static void setResponseHeader(final HttpServletResponse response) {
400     response.setHeader(ACCESS_CONTROL_ALLOW_METHODS, ALLOWED_METHODS);
401     response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
402     response.setContentType(CONTENT_TYPE_TEXT);
403   }
404 
405   static String getAsyncProfilerHome() {
406     String asyncProfilerHome = System.getenv(ASYNC_PROFILER_HOME_ENV);
407     // if ENV is not set, see if -Dasync.profiler.home=/path/to/async/profiler/home is set
408     if (asyncProfilerHome == null || asyncProfilerHome.trim().isEmpty()) {
409       asyncProfilerHome = System.getProperty(ASYNC_PROFILER_HOME_SYSTEM_PROPERTY);
410     }
411 
412     return asyncProfilerHome;
413   }
414 
415   public static class DisabledServlet extends HttpServlet {
416 
417     private static final long serialVersionUID = 1L;
418 
419     @Override
420     protected void doGet(final HttpServletRequest req, final HttpServletResponse resp)
421         throws IOException {
422       resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
423       setResponseHeader(resp);
424       resp.getWriter().write("The profiler servlet was disabled at startup.\n\n" +
425         "Please ensure the prerequsites for the Profiler Servlet have been installed and the\n" +
426         "environment is properly configured. For more information please see\n" +
427         "http://hbase.apache.org/book.html#profiler\n");
428       return;
429     }
430 
431   }
432 
433 }