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


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

    文章来源:临窗旋墨的博客,转载望注明出处。

    一 javaMelody销毁时的处理逻辑

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

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

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

    MonitoringFilter#destroy

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

    FilterContext#destroy

    void destroy() {
        try {
            try {
                if (collector != null) {
                    // 写入相关信息到监控目录下的last_shutdown.html文件里
                    new MonitoringController(collector, null).writeHtmlToLastShutdownFile();
                }
            } finally {
                //停止并注销jdbc wrapper
                JdbcWrapper.SINGLETON.stop();
    			//销毁jdbcDriver
                deregisterJdbcDriver();
    
                // 移除logging
                deregisterLogs();
    
                // 销毁定时器
                if (JobInformations.QUARTZ_AVAILABLE) {
                    JobGlobalListener.destroyJobGlobalListener();
                }
    			//注销CounterRequestMXBean bean。
                unregisterJmxExpose();
            }
        } finally {
            MonitoringInitialContextFactory.stop();
    
            // 定时器清除
            if (timer != null) {
                timer.cancel();
            }
            // 应该是停止采集cpu的一些东西
            if (samplingProfiler != null) {
                samplingProfiler.clear();
            }
            // 停止计数器, 但是在停止之前会把计数器写到文件 参见下文代码
            if (collector != null) {
                collector.stop();
            }
            // 调用的是JRobin.stop(); Robin注释:RRD存储和统计图表.....
            Collector.stopJRobin();
            //JVM的一些监控统计
            Collector.detachVirtualMachine();
        }
    }
    

    Collector#stop

    	public void stop() {
    		try {
    			try {
    				for (final Counter counter : counters) {
                        // 写入到这样的一个文件里:
                        //new File(storageDirectory, counter.getStorageName() + ".ser.gz");
                        //写入的时候用的时候 ObjectOutputStream  也即直接把对象序列化到文件中
    					counter.writeToFile();
    				}
    			} finally {
    				storageLock.release();
    			}
    		} catch (final IOException e) {
    			// persistance échouée, tant pis
    			LOG.warn("exception while writing counters data to files", e);
    		} finally {
    			try {
    				for (final Counter counter : counters) {
    					counter.clear();
    				}
    			} finally {
    				if (metricsPublishers != null) {
    					for (final MetricsPublisher metricsPublisher : metricsPublishers) {
    						metricsPublisher.stop();
    					}
    				}
    			}
    			stopped = true;
                // 一串看不懂的注释...................
    			// ici on ne fait pas de nettoyage de la liste counters car cette méthode
    			// est appelée sur la webapp monitorée quand il y a un serveur de collecte
    			// et que cette liste est envoyée au serveur de collecte,
    			// et on ne fait pas de nettoyage des maps qui servent dans le cas
    			// où le monitoring de la webapp monitorée est appelée par un navigateur
    			// directement même si il y a par ailleurs un serveur de collecte
    			// (dans ce dernier cas les données sont bien sûr partielles)
    		}
    	}
    

    二 javaMelody初始化处理逻辑

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

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

    MonitoringFilter#init

    public void init(FilterConfig config) throws ServletException {
    		final long start = System.currentTimeMillis(); // NOPMD
    		final String contextPath = Parameters.getContextPath(config.getServletContext());
    		if (!instanceEnabled) {
    			if (!CONTEXT_PATHS.contains(contextPath)) {
    				instanceEnabled = true;
    			} else {
    				return;
    			}
    		}
    		CONTEXT_PATHS.add(contextPath);
    		this.filterConfig = config;
    		this.servletApi2 = config.getServletContext().getMajorVersion() < 3;
    		Parameters.initialize(config);
    		monitoringDisabled = Parameter.DISABLED.getValueAsBoolean();
    		if (monitoringDisabled) {
    			return;
    		}
    
    		LOG.debug("JavaMelody filter init started");
    		// 初始化过滤器上下文,非常重要的一段代码 ★★★★★ 见下文代码
    		this.filterContext = new FilterContext(getApplicationType());
    		this.httpAuth = new HttpAuth();
    		config.getServletContext().setAttribute(ReportServlet.FILTER_CONTEXT_KEY, filterContext);
    		final Collector collector = filterContext.getCollector();
    		this.httpCounter = collector.getCounterByName(Counter.HTTP_COUNTER_NAME);
    		this.errorCounter = collector.getCounterByName(Counter.ERROR_COUNTER_NAME);
    
    		logEnabled = Parameter.LOG.getValueAsBoolean();
    		rumEnabled = Parameter.RUM_ENABLED.getValueAsBoolean();
    		if (Parameter.URL_EXCLUDE_PATTERN.getValue() != null) {
    	
    		urlExcludePattern = Pattern.compile(Parameter.URL_EXCLUDE_PATTERN.getValue());
    		}	
    	}
    

    FilterContext 构造器

    	FilterContext(final String applicationType) {
    		super();
    		assert applicationType != null;
    		this.applicationType = applicationType;
    
    		boolean initOk = false;
            // 新建守护进程的定时器
    		this.timer = new Timer("javamelody"
    				+ Parameters.getContextPath(Parameters.getServletContext()).replace('/', ' '),
    				true);
    		try {
                //打印一些系统信息和参数
    			logSystemInformationsAndParameters();
    			//初始化log相关, 判断使用的是哪种log,然后注册
    			initLogs();
    
                //初始化MonitoringInitialContextFactory, 就是一个System.setProperty动作
    			if (Parameter.CONTEXT_FACTORY_ENABLED.getValueAsBoolean()) {
    				MonitoringInitialContextFactory.init();
    			}
    
    			//大概就是包裹jdbc,使之能够监控到sql
    			JdbcWrapper.SINGLETON.initServletContext(Parameters.getServletContext());
    			if (!Parameters.isNoDatabase()) {
    				JdbcWrapper.SINGLETON.rebindDataSources();
    			} else {
    				
    				JdbcWrapper.SINGLETON.stop();
    			}
    
    			if (JobInformations.QUARTZ_AVAILABLE) {
    				JobGlobalListener.initJobGlobalListener();
    			}
    
    			if (MOJARRA_AVAILABLE) {
    				JsfActionHelper.initJsfActionListener();
    			}
    			if (JPA2_AVAILABLE) {
    				JpaPersistence.initPersistenceProviderResolver();
    			}
    			// 初始化 samplingProfiler:通过定期采样线程的堆栈跟踪来检测CPU热点CPU。
                //里面是一个定时器每10s执行一次sampler.update();
    			this.samplingProfiler = initSamplingProfiler();
    			//初始化各种计数器 非常重要★★★★★ 参见下文代码
    			final List<Counter> counters = initCounters();
    			final String application = Parameters.getCurrentApplication();
                // ★★★★★★这个构造函数代码比较长, 其中有个重要的逻辑就是
                // 从文件中反序列化出每个计数器, 以及初始化 当前日期的计数器
    			this.collector = new Collector(application, counters, this.samplingProfiler);
                // 新建收集器的定时任务, 这个定时器 为把收集到的信息写到文件★★★★★★★(代码太长 略)
    			this.collectTimerTask = new CollectTimerTask(collector);
    			//初始化 收集器,开启collectTimerTask  
                // 使用自己的文件同步Timer 操作相关Rrd
    			initCollect();
    
    			if (Parameter.JMX_EXPOSE_ENABLED.getValueAsBoolean()) {
    				initJmxExpose();
    			}
    	       //检测melody版本? 定时器 10分钟后执行 每24小时执行一次
    			UpdateChecker.init(timer, collector, applicationType);
    
    			if (Parameters.getServletContext().getServerInfo().contains("Google App Engine")) {
    				// https://issuetracker.google.com/issues/72216727
    				final String fontConfig = System.getProperty("java.home")
    						+ "/lib/fontconfig.Prodimage.properties";
    				if (new File(fontConfig).exists()) {
    					System.setProperty("sun.awt.fontconfig", fontConfig);
    				}
    			}
    
    			initOk = true;
    		} finally {
    			if (!initOk) {
    				// si exception dans initialisation, on annule la création du timer
    				// (sinon tomcat ne serait pas content)
    				timer.cancel();
    				LOG.debug("JavaMelody init failed");
    			}
    		}
    	}
    

    FilterContext#initCounters

    private static List<Counter> initCounters() {
    		final Counter sqlCounter = JdbcWrapper.SINGLETON.getSqlCounter();
    		final Counter httpCounter = new Counter(Counter.HTTP_COUNTER_NAME, "dbweb.png", sqlCounter);
    		final Counter errorCounter = new Counter(Counter.ERROR_COUNTER_NAME, "error.png");
    		errorCounter.setMaxRequestsCount(250);
    
    		final Counter jpaCounter = MonitoringProxy.getJpaCounter();
    		final Counter ejbCounter = MonitoringProxy.getEjbCounter();
    		final Counter springCounter = MonitoringProxy.getSpringCounter();
    		final Counter guiceCounter = MonitoringProxy.getGuiceCounter();
    		final Counter servicesCounter = MonitoringProxy.getServicesCounter();
    		final Counter strutsCounter = MonitoringProxy.getStrutsCounter();
    		final Counter jsfCounter = MonitoringProxy.getJsfCounter();
    		final Counter logCounter = LoggingHandler.getLogCounter();
    		final Counter jspCounter = JspWrapper.getJspCounter();
    		final List<Counter> counters;
    		if (JobInformations.QUARTZ_AVAILABLE) {
    			final Counter jobCounter = JobGlobalListener.getJobCounter();
    			counters = Arrays.asList(httpCounter, sqlCounter, jpaCounter, ejbCounter, springCounter,
    					guiceCounter, servicesCounter, strutsCounter, jsfCounter, jspCounter,
    					errorCounter, logCounter, jobCounter);
    		} else {
    			counters = Arrays.asList(httpCounter, sqlCounter, jpaCounter, ejbCounter, springCounter,
    					guiceCounter, servicesCounter, strutsCounter, jsfCounter, jspCounter,
    					errorCounter, logCounter);
    		}
    		//设置每个计数器对应的查询正则表达式	
    		setRequestTransformPatterns(counters);
    		final String displayedCounters = Parameter.DISPLAYED_COUNTERS.getValue();
    		if (displayedCounters == null) {
    			// 默认情况下,将显示HTTP、SQL、Error和Log((和使用的)计数器。
    			httpCounter.setDisplayed(true);
    			sqlCounter.setDisplayed(!Parameters.isNoDatabase());
    			errorCounter.setDisplayed(true);
    			logCounter.setDisplayed(true);
    			jpaCounter.setDisplayed(jpaCounter.isUsed());
    			ejbCounter.setDisplayed(ejbCounter.isUsed());
    			springCounter.setDisplayed(springCounter.isUsed());
    			guiceCounter.setDisplayed(guiceCounter.isUsed());
    			servicesCounter.setDisplayed(servicesCounter.isUsed());
    			strutsCounter.setDisplayed(strutsCounter.isUsed());
    			jsfCounter.setDisplayed(jsfCounter.isUsed());
    			jspCounter.setDisplayed(jspCounter.isUsed());
    		} else {
    			setDisplayedCounters(counters, displayedCounters);
    		}
    		LOG.debug("counters initialized");
    		return counters;
    	}
    
    

    Collector构造函数

    去掉

    public Collector(String application, List<Counter> counters,
    			SamplingProfiler samplingProfiler) {
    		super();
    		assert application != null;
    		assert counters != null;
    		this.application = application;
    		this.counters = Collections.unmodifiableList(new ArrayList<Counter>(counters));
    		this.samplingProfiler = samplingProfiler;
    		for (final Counter counter : counters) {
    			for (final Counter otherCounter : counters) {
    				assert counter == otherCounter || !counter.getName().equals(otherCounter.getName());
    			}
    			counter.setApplication(application);
    			final Counter dayCounter = new PeriodCounterFactory(counter)
    					.createDayCounterAtDate(new Date());
    			dayCountersByCounter.put(counter, dayCounter);
    		}
    		periodMillis = Parameters.getResolutionSeconds() * 1000;
    
    		try {
    			//反序列化计数器
    			for (final Counter counter : counters) {
    				counter.readFromFile();
    			}
    			// 反序列化当前日期的计数器
    			for (final Counter counter : counters) {
    				dayCountersByCounter.get(counter).readFromFile();
    			}
    			LOG.debug("counters data read from files in "
    					+ Parameters.getStorageDirectory(application));
    		} catch (final IOException e) {
    			
    			LOG.warn("exception while reading counters data from files in "
    					+ Parameters.getStorageDirectory(application), e);
    		}
    		this.storageLock = new StorageLock(application);
    		this.webappVersions = new WebappVersions(application);
    	}
    

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

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

    Counter#addRequestsAndErrors部分代码摘录

    void addRequestsAndErrors(Counter newCounter) {
    		assert getName().equals(newCounter.getName());
    
    		for (final CounterRequest newRequest : newCounter.getRequests()) {
    			if (newRequest.getHits() > 0) {
    				final CounterRequest request = getCounterRequestInternal(newRequest.getName());
    				synchronized (request) {
    					request.addHits(newRequest);
    				}
    			}
    		}
    
    		int size = requests.size();
           // 默认最大10000
    		final int maxRequests = getMaxRequestsCount();
    		if (size > maxRequests) {
    			//如果查询次数超过10000次(例如,SQL没有被复制)。
    			//我们在这里尝试避免使记忆饱和()和硬盘饱和
    			//在所有这些不同的申请中,删除不到10个HITS的申请。
    			//((例如,对工厂内每年的聚合有用)
    			//根据一个新的查询,因为这将由集合类完成
    			for (final CounterRequest request : requests.values()) {
    				if (request.getHits() < 10) {
    					removeRequest(request.getName());
    					size--;
    					if (size <= maxRequests) {
    						break;
    					}
    				}
    			}
    		}
    
    		if (isErrorCounter()) {
    			addErrors(newCounter.getErrors());
    		}
    	}
    

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

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

  • 相关阅读:
    PHP脚本 校验/补全身份证号码
    常用解压缩软件打包速度比拼
    用ffmpeg对视频转码,视频格式转换
    PHP使用readability提取任意网页正文内容
    PHP获取/判断HTML/网页的charset/编码
    缺少HTTPOnly和Secure属性解决方案
    7Zip/7z命令行中英文对照说明
    RSA加解密、签名验签算法,附代码
    WPF 笔记 十 项目资源收集
    WPF 笔记 九 通过UserControl实例说明依赖属性和绑定
  • 原文地址:https://www.cnblogs.com/xuqiudong/p/13921329.html
Copyright © 2020-2023  润新知