[临窗旋墨]javaMelody初始化以及销毁时的处理逻辑及监控日志丢失问题排查

文章来源原创   作者:许秋冬   发布时间:2020-11-03   阅读:1107   标签:源码

[toc]

[临窗旋墨]javaMelody初始化以及销毁时的处理逻辑及监控日志丢失问题排查

一 javaMelody销毁时的处理逻辑

在MonitoringFilter调用destroy销毁的方法中,主要调用的方法:FilterContext#destroy

在过滤器上下文销毁的时候,调用了collector.stop();即停止计数器,

在停止计数器的时候, 会把计数器信息序列化到对应的.ser.gz文件中;

MonitoringFilter#destroy

  1. public void destroy() {
  2. if (monitoringDisabled || !instanceEnabled) {
  3. return;
  4. }
  5. final long start = System.currentTimeMillis();
  6. try {
  7. if (filterContext != null) {
  8. //过滤器上下文销毁 ,参见下文代码0
  9. filterContext.destroy();
  10. }
  11. } finally {
  12. final String contextPath = Parameters.getContextPath(filterConfig.getServletContext());
  13. CONTEXT_PATHS.remove(contextPath);
  14. // 把一些变量置为空 代码略
  15. }
  16. final long duration = System.currentTimeMillis() - start;
  17. LOG.debug("JavaMelody filter destroy done in " + duration + " ms");

FilterContext#destroy

  1. void destroy() {
  2. try {
  3. try {
  4. if (collector != null) {
  5. // 写入相关信息到监控目录下的last_shutdown.html文件里
  6. new MonitoringController(collector, null).writeHtmlToLastShutdownFile();
  7. }
  8. } finally {
  9. //停止并注销jdbc wrapper
  10. JdbcWrapper.SINGLETON.stop();
  11. //销毁jdbcDriver
  12. deregisterJdbcDriver();
  13. // 移除logging
  14. deregisterLogs();
  15. // 销毁定时器
  16. if (JobInformations.QUARTZ_AVAILABLE) {
  17. JobGlobalListener.destroyJobGlobalListener();
  18. }
  19. //注销CounterRequestMXBean bean。
  20. unregisterJmxExpose();
  21. }
  22. } finally {
  23. MonitoringInitialContextFactory.stop();
  24. // 定时器清除
  25. if (timer != null) {
  26. timer.cancel();
  27. }
  28. // 应该是停止采集cpu的一些东西
  29. if (samplingProfiler != null) {
  30. samplingProfiler.clear();
  31. }
  32. // 停止计数器, 但是在停止之前会把计数器写到文件 参见下文代码
  33. if (collector != null) {
  34. collector.stop();
  35. }
  36. // 调用的是JRobin.stop(); Robin注释:RRD存储和统计图表.....
  37. Collector.stopJRobin();
  38. //JVM的一些监控统计
  39. Collector.detachVirtualMachine();
  40. }
  41. }

Collector#stop

  1. public void stop() {
  2. try {
  3. try {
  4. for (final Counter counter : counters) {
  5. // 写入到这样的一个文件里:
  6. //new File(storageDirectory, counter.getStorageName() + ".ser.gz");
  7. //写入的时候用的时候 ObjectOutputStream 也即直接把对象序列化到文件中
  8. counter.writeToFile();
  9. }
  10. } finally {
  11. storageLock.release();
  12. }
  13. } catch (final IOException e) {
  14. // persistance échouée, tant pis
  15. LOG.warn("exception while writing counters data to files", e);
  16. } finally {
  17. try {
  18. for (final Counter counter : counters) {
  19. counter.clear();
  20. }
  21. } finally {
  22. if (metricsPublishers != null) {
  23. for (final MetricsPublisher metricsPublisher : metricsPublishers) {
  24. metricsPublisher.stop();
  25. }
  26. }
  27. }
  28. stopped = true;
  29. // 一串看不懂的注释...................
  30. // ici on ne fait pas de nettoyage de la liste counters car cette méthode
  31. // est appelée sur la webapp monitorée quand il y a un serveur de collecte
  32. // et que cette liste est envoyée au serveur de collecte,
  33. // et on ne fait pas de nettoyage des maps qui servent dans le cas
  34. // où le monitoring de la webapp monitorée est appelée par un navigateur
  35. // directement même si il y a par ailleurs un serveur de collecte
  36. // (dans ce dernier cas les données sont bien sûr partielles)
  37. }
  38. }

二 javaMelody初始化处理逻辑

代码太长,只粗略的浏览部分代码链

  1. 在MonitoringFilter#init中初始化过滤器上下文FilterContext,非常重要的一段代码
  2. FilterContext构造代码中,几个重要的逻辑:
    1. initCounters(); 初始化各种计数器
    2. 构造Collector对象,从文件中读取各个计数器的信息
    3. CollectTimerTask 新建收集器的定时任务, 这个定时器 为把收集到的信息写到文件★★★★★★★

MonitoringFilter#init

  1. public void init(FilterConfig config) throws ServletException {
  2. final long start = System.currentTimeMillis(); // NOPMD
  3. final String contextPath = Parameters.getContextPath(config.getServletContext());
  4. if (!instanceEnabled) {
  5. if (!CONTEXT_PATHS.contains(contextPath)) {
  6. instanceEnabled = true;
  7. } else {
  8. return;
  9. }
  10. }
  11. CONTEXT_PATHS.add(contextPath);
  12. this.filterConfig = config;
  13. this.servletApi2 = config.getServletContext().getMajorVersion() < 3;
  14. Parameters.initialize(config);
  15. monitoringDisabled = Parameter.DISABLED.getValueAsBoolean();
  16. if (monitoringDisabled) {
  17. return;
  18. }
  19. LOG.debug("JavaMelody filter init started");
  20. // 初始化过滤器上下文,非常重要的一段代码 ★★★★★ 见下文代码
  21. this.filterContext = new FilterContext(getApplicationType());
  22. this.httpAuth = new HttpAuth();
  23. config.getServletContext().setAttribute(ReportServlet.FILTER_CONTEXT_KEY, filterContext);
  24. final Collector collector = filterContext.getCollector();
  25. this.httpCounter = collector.getCounterByName(Counter.HTTP_COUNTER_NAME);
  26. this.errorCounter = collector.getCounterByName(Counter.ERROR_COUNTER_NAME);
  27. logEnabled = Parameter.LOG.getValueAsBoolean();
  28. rumEnabled = Parameter.RUM_ENABLED.getValueAsBoolean();
  29. if (Parameter.URL_EXCLUDE_PATTERN.getValue() != null) {
  30. urlExcludePattern = Pattern.compile(Parameter.URL_EXCLUDE_PATTERN.getValue());
  31. }
  32. }

FilterContext 构造器

  1. FilterContext(final String applicationType) {
  2. super();
  3. assert applicationType != null;
  4. this.applicationType = applicationType;
  5. boolean initOk = false;
  6. // 新建守护进程的定时器
  7. this.timer = new Timer("javamelody"
  8. + Parameters.getContextPath(Parameters.getServletContext()).replace('/', ' '),
  9. true);
  10. try {
  11. //打印一些系统信息和参数
  12. logSystemInformationsAndParameters();
  13. //初始化log相关, 判断使用的是哪种log,然后注册
  14. initLogs();
  15. //初始化MonitoringInitialContextFactory, 就是一个System.setProperty动作
  16. if (Parameter.CONTEXT_FACTORY_ENABLED.getValueAsBoolean()) {
  17. MonitoringInitialContextFactory.init();
  18. }
  19. //大概就是包裹jdbc,使之能够监控到sql
  20. JdbcWrapper.SINGLETON.initServletContext(Parameters.getServletContext());
  21. if (!Parameters.isNoDatabase()) {
  22. JdbcWrapper.SINGLETON.rebindDataSources();
  23. } else {
  24. JdbcWrapper.SINGLETON.stop();
  25. }
  26. if (JobInformations.QUARTZ_AVAILABLE) {
  27. JobGlobalListener.initJobGlobalListener();
  28. }
  29. if (MOJARRA_AVAILABLE) {
  30. JsfActionHelper.initJsfActionListener();
  31. }
  32. if (JPA2_AVAILABLE) {
  33. JpaPersistence.initPersistenceProviderResolver();
  34. }
  35. // 初始化 samplingProfiler:通过定期采样线程的堆栈跟踪来检测CPU热点CPU。
  36. //里面是一个定时器每10s执行一次sampler.update();
  37. this.samplingProfiler = initSamplingProfiler();
  38. //初始化各种计数器 非常重要★★★★★ 参见下文代码
  39. final List<Counter> counters = initCounters();
  40. final String application = Parameters.getCurrentApplication();
  41. // ★★★★★★这个构造函数代码比较长, 其中有个重要的逻辑就是
  42. // 从文件中反序列化出每个计数器, 以及初始化 当前日期的计数器
  43. this.collector = new Collector(application, counters, this.samplingProfiler);
  44. // 新建收集器的定时任务, 这个定时器 为把收集到的信息写到文件★★★★★★★(代码太长 略)
  45. this.collectTimerTask = new CollectTimerTask(collector);
  46. //初始化 收集器,开启collectTimerTask
  47. // 使用自己的文件同步Timer 操作相关Rrd
  48. initCollect();
  49. if (Parameter.JMX_EXPOSE_ENABLED.getValueAsBoolean()) {
  50. initJmxExpose();
  51. }
  52. //检测melody版本? 定时器 10分钟后执行 每24小时执行一次
  53. UpdateChecker.init(timer, collector, applicationType);
  54. if (Parameters.getServletContext().getServerInfo().contains("Google App Engine")) {
  55. // https://issuetracker.google.com/issues/72216727
  56. final String fontConfig = System.getProperty("java.home")
  57. + "/lib/fontconfig.Prodimage.properties";
  58. if (new File(fontConfig).exists()) {
  59. System.setProperty("sun.awt.fontconfig", fontConfig);
  60. }
  61. }
  62. initOk = true;
  63. } finally {
  64. if (!initOk) {
  65. // si exception dans initialisation, on annule la création du timer
  66. // (sinon tomcat ne serait pas content)
  67. timer.cancel();
  68. LOG.debug("JavaMelody init failed");
  69. }
  70. }
  71. }

FilterContext#initCounters

  1. private static List<Counter> initCounters() {
  2. final Counter sqlCounter = JdbcWrapper.SINGLETON.getSqlCounter();
  3. final Counter httpCounter = new Counter(Counter.HTTP_COUNTER_NAME, "dbweb.png", sqlCounter);
  4. final Counter errorCounter = new Counter(Counter.ERROR_COUNTER_NAME, "error.png");
  5. errorCounter.setMaxRequestsCount(250);
  6. final Counter jpaCounter = MonitoringProxy.getJpaCounter();
  7. final Counter ejbCounter = MonitoringProxy.getEjbCounter();
  8. final Counter springCounter = MonitoringProxy.getSpringCounter();
  9. final Counter guiceCounter = MonitoringProxy.getGuiceCounter();
  10. final Counter servicesCounter = MonitoringProxy.getServicesCounter();
  11. final Counter strutsCounter = MonitoringProxy.getStrutsCounter();
  12. final Counter jsfCounter = MonitoringProxy.getJsfCounter();
  13. final Counter logCounter = LoggingHandler.getLogCounter();
  14. final Counter jspCounter = JspWrapper.getJspCounter();
  15. final List<Counter> counters;
  16. if (JobInformations.QUARTZ_AVAILABLE) {
  17. final Counter jobCounter = JobGlobalListener.getJobCounter();
  18. counters = Arrays.asList(httpCounter, sqlCounter, jpaCounter, ejbCounter, springCounter,
  19. guiceCounter, servicesCounter, strutsCounter, jsfCounter, jspCounter,
  20. errorCounter, logCounter, jobCounter);
  21. } else {
  22. counters = Arrays.asList(httpCounter, sqlCounter, jpaCounter, ejbCounter, springCounter,
  23. guiceCounter, servicesCounter, strutsCounter, jsfCounter, jspCounter,
  24. errorCounter, logCounter);
  25. }
  26. //设置每个计数器对应的查询正则表达式
  27. setRequestTransformPatterns(counters);
  28. final String displayedCounters = Parameter.DISPLAYED_COUNTERS.getValue();
  29. if (displayedCounters == null) {
  30. // 默认情况下,将显示HTTP、SQL、Error和Log((和使用的)计数器。
  31. httpCounter.setDisplayed(true);
  32. sqlCounter.setDisplayed(!Parameters.isNoDatabase());
  33. errorCounter.setDisplayed(true);
  34. logCounter.setDisplayed(true);
  35. jpaCounter.setDisplayed(jpaCounter.isUsed());
  36. ejbCounter.setDisplayed(ejbCounter.isUsed());
  37. springCounter.setDisplayed(springCounter.isUsed());
  38. guiceCounter.setDisplayed(guiceCounter.isUsed());
  39. servicesCounter.setDisplayed(servicesCounter.isUsed());
  40. strutsCounter.setDisplayed(strutsCounter.isUsed());
  41. jsfCounter.setDisplayed(jsfCounter.isUsed());
  42. jspCounter.setDisplayed(jspCounter.isUsed());
  43. } else {
  44. setDisplayedCounters(counters, displayedCounters);
  45. }
  46. LOG.debug("counters initialized");
  47. return counters;
  48. }

Collector构造函数

去掉

  1. public Collector(String application, List<Counter> counters,
  2. SamplingProfiler samplingProfiler) {
  3. super();
  4. assert application != null;
  5. assert counters != null;
  6. this.application = application;
  7. this.counters = Collections.unmodifiableList(new ArrayList<Counter>(counters));
  8. this.samplingProfiler = samplingProfiler;
  9. for (final Counter counter : counters) {
  10. for (final Counter otherCounter : counters) {
  11. assert counter == otherCounter || !counter.getName().equals(otherCounter.getName());
  12. }
  13. counter.setApplication(application);
  14. final Counter dayCounter = new PeriodCounterFactory(counter)
  15. .createDayCounterAtDate(new Date());
  16. dayCountersByCounter.put(counter, dayCounter);
  17. }
  18. periodMillis = Parameters.getResolutionSeconds() * 1000;
  19. try {
  20. //反序列化计数器
  21. for (final Counter counter : counters) {
  22. counter.readFromFile();
  23. }
  24. // 反序列化当前日期的计数器
  25. for (final Counter counter : counters) {
  26. dayCountersByCounter.get(counter).readFromFile();
  27. }
  28. LOG.debug("counters data read from files in "
  29. + Parameters.getStorageDirectory(application));
  30. } catch (final IOException e) {
  31. LOG.warn("exception while reading counters data from files in "
  32. + Parameters.getStorageDirectory(application), e);
  33. }
  34. this.storageLock = new StorageLock(application);
  35. this.webappVersions = new WebappVersions(application);
  36. }

曾遇到的部分监控数据丢失的问题:

  1. 一个Counter 对应一个计数器,对应一个磁盘上的.ser.gz格式的压缩文件(是一个序列化的javaBean:Counter)
  2. 一个Counter中包含一个查询数列表requests;
  3. 通过代码可知, 当一个计数器中的查询列表超过一万条以后,再次往计数机中插入查询信息的时候,就会删除点击数少于10的查询记录;

Counter#addRequestsAndErrors部分代码摘录

  1. void addRequestsAndErrors(Counter newCounter) {
  2. assert getName().equals(newCounter.getName());
  3. for (final CounterRequest newRequest : newCounter.getRequests()) {
  4. if (newRequest.getHits() > 0) {
  5. final CounterRequest request = getCounterRequestInternal(newRequest.getName());
  6. synchronized (request) {
  7. request.addHits(newRequest);
  8. }
  9. }
  10. }
  11. int size = requests.size();
  12. // 默认最大10000
  13. final int maxRequests = getMaxRequestsCount();
  14. if (size > maxRequests) {
  15. //如果查询次数超过10000次(例如,SQL没有被复制)。
  16. //我们在这里尝试避免使记忆饱和()和硬盘饱和
  17. //在所有这些不同的申请中,删除不到10个HITS的申请。
  18. //((例如,对工厂内每年的聚合有用)
  19. //根据一个新的查询,因为这将由集合类完成
  20. for (final CounterRequest request : requests.values()) {
  21. if (request.getHits() < 10) {
  22. removeRequest(request.getName());
  23. size--;
  24. if (size <= maxRequests) {
  25. break;
  26. }
  27. }
  28. }
  29. }
  30. if (isErrorCounter()) {
  31. addErrors(newCounter.getErrors());
  32. }
  33. }

基于我对javamelody的了解非常有限,我觉得作者设计的大于一万条之后的清理逻辑应该是出于对内存的考虑吧, 因为,曾在测试环境,由于长时间未清理监控日志文件,在javamelody启动时候大概占用200M内存,若不限制的追加,还是有造成内存泄漏的可能的。

​ 所以,我个人建议在一段时间之后可以尝试清理javamelody的监控日志文件(在关闭服务后清理)。


发表评论

目录