美团CAT源码分析 - 服务端启动初始化

美团点评CAT源码解析 - 服务端启动初始化

1.概述

CAT通过Tomcat容器运行,通过 web.xml 配置servlet和filter等处理请求
核心的servlet包括

  • CatServlet:处理Catclient的请求
  • mvc-servlet:处理Cat-home页面的用户请求

MVC是美团封装的MVC框架入口,与CAT核心逻辑关系不大,重点在于CatServlet
![servlet关系](https://img-blog.csdnimg.cn/img_convert/4818b93dd03470b2011aab981d8a88b8.png#align=left&display=inline&height=260&margin=[object Object]&name=image.png&originHeight=520&originWidth=854&size=63452&status=done&style=none&width=427)
类关系如下
在这里插入图片描述

AbstractContainerServlet重写了init方法,在启动时,执行初始化逻辑,主要包含四部分

  1. 初始化httpServlet
  2. Plexus IOC容器初始化
  3. 生成Plexus Logger日志类
  4. 调用子类实现,初始化Components
@Override
public void init(ServletConfig config) throws ServletException {
	// 初始化Servlet
    super.init(config);

	try {
        // 获取Plexus容器
		if (m_container == null) {
			m_container = ContainerLoader.getDefaultContainer();
		}

        // 获取logger
		m_logger = ((DefaultPlexusContainer) m_container).getLoggerManager().getLoggerForComponent(
		      getClass().getName());

        // 初始化CAT的Module Components组件
		initComponents(config);
	} catch (Exception e) {
		if (m_logger != null) {
			m_logger.error("Servlet initializing failed. " + e, e);
		} else {
			System.out.println("Servlet initializing failed. " + e);
			e.printStackTrace(System.out);
		}

		throw new ServletException("Servlet initializing failed. " + e, e);
	}
}

关键CAT服务端逻辑在于第4步,初始化Components


2. Plexus容器初始化

CAT服务端使用了Plexus作为对象实例的生命周期管理工具。

2.1 Plexus使用简介

Plexus是Maven提供的一种依赖注入管理容器,与Spring框架的作用类似。简单教程
Plexus通过 Role 标记一个类的实例进行管理,通过XML配置文件(默认所有项目根目录下/META-INF/plexus/components.xml)对依赖关系进行声明,然后使用相关注解完成注入。例如:
Plexus配置文件

<plexus>
	<components>
		<!-- 定义一个组件 -->
		<component>
			<!-- 指定role -->
			<role>com.dianping.cat.report.server.ServersUpdater</role>
			<!-- 指定对应实现类 -->
			<implementation>com.dianping.cat.report.task.DefaultRemoteServersUpdater</implementation>
			<!-- 有依赖的类时 -->
			<requirements>
				<requirement>
					<role>com.dianping.cat.report.service.ModelService</role>
					<!-- 多种实现时,通过hint区分 -->
					<role-hint>state</role-hint>
				</requirement>
			</requirements>
		</component>
		<component>
			<role>org.unidal.initialization.ModuleManager</role>
			<implementation>org.unidal.initialization.DefaultModuleManager</implementation>
			<configuration>
				<!-- configuration指定基本类型参数 -->
				<topLevelModules>cat-home</topLevelModules>
			</configuration>
		</component>
	</components>
</plexus>

对应类

@Named(type = ServersUpdater.class)
public class DefaultRemoteServersUpdater implements ServersUpdater {
	// 这里会注入ModelService的state实现类
	@Inject(type = ModelService.class, value = StateAnalyzer.ID)
	private ModelService<StateReport> m_service;
    // ..
}

@Named(type = ModuleManager.class)
public class DefaultModuleManager extends ContainerHolder implements ModuleManager {
    // 此处自动注入“cat-home”
    @InjectAttribute
    private String m_topLevelModules;
   //....
}

此处用到注入方式与教程中略有出入,需要调用该类中的方法,手动注入

com.dianping.cat.build.report.ReportComponentConfigurator#defineComponents
all.add(C(ModelService.class, StateAnalyzer.ID, CompositeStateService.class)

配置后,通过调用 PlexusContainer.lookup() 方法,进行实例化,可选参数为role的name或者class

2.2 Plexus的初始化

通过ContainerLoader获取Plexus的Container时载入自定义的Plexus配置文件 /META-INF/plexus/ plexus.xml 对Plexus默认的logger进行替换(替换为Log4j)
代码如下:org.unidal.lookup.ContainerLoader#getDefaultContainer()

  public static PlexusContainer getDefaultContainer() {
     DefaultContainerConfiguration configuration = new DefaultContainerConfiguration();
     // 载入自定义配置件
     configuration.setContainerConfiguration("/META-INF/plexus/plexus.xml");
     return getDefaultContainer(configuration);
  }
  public static PlexusContainer getDefaultContainer(ContainerConfiguration configuration) {
     if (s_container == null) {
        // Two ContainerLoaders should share the same PlexusContainer
        Class<?> loaderClass = findLoaderClass();
        synchronized (ContainerLoader.class) {
           if (loaderClass != null) {
              s_container = getContainerFromLookupLibrary(loaderClass);
           }
           if (s_container == null) {
              try {
                 preConstruction(configuration);
                 // 创建DefaultContainer,管理所有的实例对象
                 s_container = new DefaultPlexusContainer(configuration);
                 postConstruction(s_container);
                 if (loaderClass != null) {
                    setContainerToLookupLibrary(loaderClass, s_container);
                 }
              } catch (Exception e) {
                 throw new RuntimeException("Unable to create Plexus container.", e);
              }
           }
        }
     }
     return s_container;
  }

Plexus.xml 配置文件中对Logger实现类进行覆盖

<plexus>
	<components>
		<component>
			<role>org.codehaus.plexus.logging.LoggerManager</role>
			<implementation>org.unidal.lookup.logger.Log4jLoggerManager</implementation>
			<configuration>
				<configurationFile>log4j.properties</configurationFile>
			</configuration>
		</component>
	</components>
</plexus>

创建Plexus默认的 DefaultContainer ,该类中聚合的 ComponentRegistry 负责管理所有的实例对象。


3. 获取并设置Logger

完成Plexus容器初始化后,初始化逻辑执行第三步,对 m_logger 进行设置。通过上述配置已经可以知道,logger实现选择为Log4j方式,实现类部分代码如下:org.unidal.lookup.logger.Log4jLoggerManager

public class Log4jLoggerManager extends BaseLoggerManager {
    private String m_configurationFile = "log4j.xml";// 配置被修改为log4j.properties

    private String m_baseDirRef;

    @Override
    protected org.codehaus.plexus.logging.Logger createLogger(String name) {
        Logger logger = LogManager.getLogger(name);
        LevelAdapter level = new LevelAdapter(logger.getLevel());

        return new LoggerAdapter(logger, level.getThreshold());
    }
    // 其他逻辑
}

4. CAT组件初始化

4.1 整体逻辑

com.dianping.cat.servlet.CatServlet#initComponents
执行初始化时,创建 DefaultModuleContext ,并将Plexus容器的添加到其中,完成设置。
DefaultModuleContext 还管理了Cat的配置文件, cat-server.xmlcat-client.xml 由于cat的服务单端应用程序本身即接收SDK的数据,又需要监控自身的状态,因此需要读取server和client两种配置文件。

@Override
protected void initComponents(ServletConfig servletConfig) throws ServletException {
	try {
	    // 1.获取container容器,设置logger
		ModuleContext ctx = new DefaultModuleContext(getContainer());
		// 3.实例化initializer
		ModuleInitializer initializer = ctx.lookup(ModuleInitializer.class);
		// 2.设置配置文件
		File clientXmlFile = getConfigFile(servletConfig, "cat-client-xml", "client.xml");
		File serverXmlFile = getConfigFile(servletConfig, "cat-server-xml", "server.xml");

		ctx.setAttribute("cat-client-config-file", clientXmlFile);
		ctx.setAttribute("cat-server-config-file", serverXmlFile);
        // 执行初始化
		initializer.execute(ctx);
	} catch (Exception e) {
		m_exception = e;
		System.err.println(e);
		throw new ServletException(e);
	}
}

之后,通过容器实例化ModuleInitializer,该实现类的xml配置文件在依赖的Jar包中,实现类为 DefaultModuleInitializer 。后者注入了DefaultModuleManager管理,该类具体管理了CAT应当具有哪些Module,并且在配置文件中指定了最高级的Module为“cat-home”

@Named(type = ModuleInitializer.class)
public class DefaultModuleInitializer implements ModuleInitializer {
   @Inject
   private ModuleManager m_manager; // 引用ModuleManager

   @InjectAttribute
   private boolean m_verbose;

   private int m_index = 1;

    // 其他属性和方法 ...
}

@Named(type = ModuleManager.class)
public class DefaultModuleManager extends ContainerHolder implements ModuleManager {
   @InjectAttribute
   private String m_topLevelModules; // 指定最高级的module
   
   // 其他属性和方法 ...
}
<component>
	<role>org.unidal.initialization.ModuleManager</role>
	<implementation>org.unidal.initialization.DefaultModuleManager</implementation>
	<configuration>
    <!-- 配置文件指定topLevelModules 为cat-home -->
		<topLevelModules>cat-home</topLevelModules>
	</configuration>
</component>

完成以上配置后,通过initialzer执行初始化逻辑

4.2 Module的载入

Initializer的execute方法首先获取顶级Module,执行execute方法,对module执行递归的载入。
每个module都实现了getDependency方法,该方法返回当前module依赖的其他module,递归深度优先,最底层依赖的Module最先加入集合。
全部载入后,再执行各个Module的initialize方法进行初始化。
关键代码如下:

@Named(type = ModuleInitializer.class)
public class DefaultModuleInitializer implements ModuleInitializer {
    @Inject
    private ModuleManager m_manager;

 	// 其他代码
    
    // 载入module
    @Override
    public void execute(ModuleContext ctx) {
       // 传入顶级module,此处是catHomeModule
       Module[] modules = m_manager.getTopLevelModules();
       execute(ctx, modules);
    }
 
    @Override
    public void execute(ModuleContext ctx, Module... modules) {
       Set<Module> all = new LinkedHashSet<Module>();
 
       info(ctx, "Initializing top level modules:");
 
       for (Module module : modules) {
          // 记录初始化了哪些module
          info(ctx, "   " + module.getClass().getName());
       }
 
       try {
          // 递归读取所有依赖的module,存入all集合中
          expandAll(ctx, modules, all);
          for (Module module : all) {
             if (!module.isInitialized()) {
                // 逐一执行初始化
                executeModule(ctx, module, m_index++);
             }
          }
       } catch (Exception e) {
          throw new RuntimeException("Error when initializing modules! Exception: " + e, e);
       }
    }
    
    // 具体执行初始化
    private synchronized void executeModule(ModuleContext ctx, Module module, int index) throws Exception {
      long start = System.currentTimeMillis();

      // 设置标记位避免重复初始化
      module.setInitialized(true);

      info(ctx, index + " ------ " + module.getClass().getName());

      // 执行自身的初始化逻辑
      module.initialize(ctx);

      long end = System.currentTimeMillis();
      info(ctx, index + " ------ " + module.getClass().getName() + " DONE in " + (end - start) + " ms.");
   }
   
   //递归载入
   private void expandAll(ModuleContext ctx, Module[] modules, Set<Module> all) throws Exception {
      if (modules != null) {
         for (Module module : modules) {
            // 获取依赖的module,指定递归
            expandAll(ctx, module.getDependencies(ctx), all);

            if (!all.contains(module)) {
               if (module instanceof AbstractModule) {
                   // 执行module的setup方法
                  ((AbstractModule) module).setup(ctx);
               }
               // 添加module到集合中 
			   all.add(module);
            }
         }
      }
   }   
}

catHomeModule获取依赖Module的方法如下
![image.png](https://img-blog.csdnimg.cn/img_convert/7456368092d8d16e2cdfadb7aabb648a.png#align=left&display=inline&height=93&margin=[object Object]&name=image.png&originHeight=93&originWidth=500&size=9795&status=done&style=none&width=500)
通过其他getDependencies方法可以了解Module的依赖关系如下

4.3 各Module的初始化

Module的载入和初始化过程,使用了模版方法设计模式,通过Module接口,AbstractModule抽象类调用方法,不同的Module执行重写的excute方法完成。每个module的逻辑如下

4.3.1 CatCoreModule

通过context容器实例化ServerUpdateManager,后者注入了DefaultRemoteServersUpdater并且实现了Plexus的Initializable接口,实例化时自动调用initialize方法。该方法的主要目的是,读取并保存当前(CURRENT)和前一小时(LAST)的CAT主机IP。
代码如下com.dianping.cat.report.server.ServersUpdaterManager#initialize

@Named
public class ServersUpdaterManager implements Initializable {

	@Inject
	private ServersUpdater m_remoteServerUpdater;

	@Inject
	private RemoteServersManager m_remoteServersManager;

	@Override
	public void initialize() throws InitializationException {
		TimerSyncTask.getInstance().register(new SyncHandler() {

			@Override
			public String getName() {
				return "remote-server-updater";
			}

			@Override
			public void handle() throws Exception {
				try {
					long currentHour = TimeHelper.getCurrentHour().getTime();
                    // 调用DefaultRemoteServer的builderServers方法,读取当前CAT的机器IP
					Map<String, Set<String>> currentServers = m_remoteServerUpdater.buildServers(new Date(currentHour));
					// 记录在RemoteServersManager中
					m_remoteServersManager.setCurrentServers(currentServers);
					// 读取LAST一小时的CAT机器IP
					long lastHour = currentHour - TimeHelper.ONE_HOUR;
					Map<String, Set<String>> lastServers = m_remoteServerUpdater.buildServers(new Date(lastHour));
					// 记录
					m_remoteServersManager.setLastServers(lastServers);
				} catch (Exception e) {
					Cat.logError(e);
				}
			}
		});
	}
}

com.dianping.cat.report.task.DefaultRemoteServersUpdater#buildServers实现如下
其中运用了CAT中的Service请求机制,向集群内其他机器读取应用名(domain)为“cat”的机器IP集合。Service机制在后续介绍。
获取到返回的Report后,CAT使用了访问者模式处理数据。Report在CAT中是数据结构非常固定的的类型,因此使用访问模式可以灵活的处理不同的report内容。

@Named(type = ServersUpdater.class)
public class DefaultRemoteServersUpdater implements ServersUpdater {

	@Inject(type = ModelService.class, value = StateAnalyzer.ID)
	private ModelService<StateReport> m_service;

	@Override
	public Map<String, Set<String>> buildServers(Date hour) {
        // 查询CAT指定时段的StateReport
        // Report是CAT中服务端之间的消息、报表实体
		StateReport currentReport = queryStateReport(Constants.CAT, hour.getTime());
		StateReportVisitor visitor = new StateReportVisitor();
		// 访问者模式,处理数据
		visitor.visitStateReport(currentReport);
		return visitor.getServers();
	}

	public StateReport queryStateReport(String domain, long time) {
        // 时间周期的枚举,CURRENT,LAST,HISTORY
		ModelPeriod period = ModelPeriod.getByTime(time);
		if (period == ModelPeriod.CURRENT || period == ModelPeriod.LAST) {
            // 创建请求参数
			ModelRequest request = new ModelRequest(domain, time);
			if (m_service.isEligable(request)) {
                // m_service实现是com.dianping.cat.report.page.state.service.CompositeStateService
                // Service在Cat服务端用于服务器的相互请求,获取数据后并聚合展示
				ModelResponse<StateReport> response = m_service.invoke(request);
                // StateReport包含机器IP和机器信息集合
				StateReport report = response.getModel();
				return report;
			} else {
				throw new RuntimeException("Internal error: no eligable state report service registered for " + request	+ "!");
			}
		} else {
			throw new RuntimeException("Domain server update period is not right: " + period + ", time is: "	+ new Date(time));
		}
	}

	public static class StateReportVisitor extends BaseVisitor {

		private Map<String, Set<String>> m_servers = new ConcurrentHashMap<String, Set<String>>();

		private String m_ip;

		public Map<String, Set<String>> getServers() {
			return m_servers;
		}

		@Override
		public void visitMachine(Machine machine) {
            // 2.记录机器的IP
			m_ip = machine.getIp();
            // 3.回调父类方法
			super.visitMachine(machine);
		}

		@Override
		public void visitProcessDomain(ProcessDomain processDomain) {
			if (processDomain.getTotal() > 0) {
                // 4.获取/创建指定domain的ip Map集合
				String domain = processDomain.getName();
				Set<String> servers = m_servers.get(domain);
				if (servers == null) {
					servers = new HashSet<String>();
					m_servers.put(domain, servers);
				}
                // 把当前ip添加到其中
				servers.add(m_ip);
			}
		}
	}
}

这里的StateReportVisitor继承了BaseVisitor

public abstract class BaseVisitor implements IVisitor {
   @Override
   public void visitDetail(Detail detail) {
   }

   @Override
   public void visitMachine(Machine machine) {
      for (ProcessDomain processDomain : machine.getProcessDomains().values()) {
         // 4.处理机器所有的Domain(cat)信息,被StateReportVisitor覆盖
         visitProcessDomain(processDomain);
      }

      for (Message message : machine.getMessages().values()) {
         // 5. StateReportVisitor没有实现该方法,所以没有意义
         visitMessage(message);
      }
   }

   @Override
   public void visitMessage(Message message) {
   }

   @Override
   public void visitProcessDomain(ProcessDomain processDomain) {
      for (Detail detail : processDomain.getDetails().values()) {
         visitDetail(detail);
      }
   }

   // 1.方法入口
   @Override
   public void visitStateReport(StateReport stateReport) {
      // 遍历所有的机器信息
      for (Machine machine : stateReport.getMachines().values()) {
         // 2.该方法被覆盖,见上一段代码
         visitMachine(machine);
      }
   }
}

综上,服务端读取保存了“cat”的所有机器列表

4.3.2 CatHadoopModule

CatHadoopModule初始化时,实例化 LogviewProcessor 并启动线程执行其定时逻辑。
Logview是Cat存储的基本日志实体,支持本地存储和HDFS存储。该processor的处理逻辑为
org.unidal.cat.message.storage.clean.LogviewProcessor
初始化时,通过CAT的Config机制读取hdfs配置的本地存储目录LocalBaseDir。Config和DB的存储机制后续解释
run方法定时每小时第10分钟执行,判断hdfs上传是否开启,开启时读bucket地址上传hdfs,删除本地文件;未开启时,判断并删除本地文件。Logview的存储方式后续解释

@Override
public void initialize() throws InitializationException {
    // 获取本地存储目录
	m_baseDir = new File(m_configManager.getHdfsLocalBaseDir("dump"));
}

@Override
public void run() {
	boolean active = true;
	while (active) {
		long start = System.currentTimeMillis();
		long current = start / 1000 / 60;
		int min = (int) (current % (60));
		Calendar nextStart = Calendar.getInstance();
		// 计算下次执行时间
		nextStart.set(Calendar.MINUTE, 10);
		nextStart.add(Calendar.HOUR, 1);
		try {
			if (m_configManager.isHdfsOn()) {
				// make system 0-10 min is not busy
				if (min >= 9) {
                    // 判断读取bucket路径
					List<String> paths = findOldBuckets();
					// 上传hdfs删除本地文件
					processLogviewFiles(paths, true);
				}
			} else {
				// 删除本地文件
				deleteOldMessages();
			}
		} catch (Throwable e) {
			Cat.logError(e);
		}
		try {
            // sleep避免CPU空跑
			long end = System.currentTimeMillis();
			long sleepTime = nextStart.getTimeInMillis() - end;
			if (sleepTime > 0) {
				Thread.sleep(sleepTime);
			}
		} catch (InterruptedException e) {
			active = false;
		}
	}
}

4.3.3 CatConsumerModule

execute方法为空,未执行任何操作

4.4.4 CatHomeModule

首先,需要注意,在expandAll方法递归载入Module时,会执行每个Module的setup方法。之后才会执行所有Module的execute。而该方法只在CatHomeModule中进行了重写实现。
该方法实例化了TcpSocketReceiver,并完成初始化启动,注册销毁监听。CAT的客户端/服务端通信使用了Netty框架,具体的通信机制后续介绍

@Override
protected void setup(ModuleContext ctx) throws Exception {
	final TcpSocketReceiver messageReceiver = ctx.lookup(TcpSocketReceiver.class);
	messageReceiver.init();
	Runtime.getRuntime().addShutdownHook(new Thread() {
		@Override
		public void run() {
			messageReceiver.destory();
		}
	});
}

com.dianping.cat.analysis.TcpSocketReceiver#startServer代码可以看出,设置Netty的tcp属性,自定义编解码器,默认指定2280作为通信端口。也可以看到,服务端没有设置心跳检查handler,因此,服务端无法主动感知客户端下线。

private int m_port = 2280; // default port number from phone, C:2, A:2, T:8
// 其他代码
public void init() {
	try {
        // 启动server
		startServer(m_port);
	} catch (Exception e) {
		m_logger.error(e.getMessage(), e);
	}
}

public synchronized void startServer(int port) throws InterruptedException {
	boolean linux = getOSMatches("Linux") || getOSMatches("LINUX");
	int threads = 24;
	ServerBootstrap bootstrap = new ServerBootstrap();
	m_bossGroup = linux ? new EpollEventLoopGroup(threads) : new NioEventLoopGroup(threads);
	m_workerGroup = linux ? new EpollEventLoopGroup(threads) : new NioEventLoopGroup(threads);
	bootstrap.group(m_bossGroup, m_workerGroup);
	bootstrap.channel(linux ? EpollServerSocketChannel.class : NioServerSocketChannel.class);
	bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
		@Override
		protected void initChannel(SocketChannel ch) throws Exception {
			ChannelPipeline pipeline = ch.pipeline();
			// 设置编辑吗器
			pipeline.addLast("decode", new MessageDecoder());
			pipeline.addLast("encode", new ClientMessageEncoder());
		}
	});
	// netty常规配置
	bootstrap.childOption(ChannelOption.SO_REUSEADDR, true);
	bootstrap.childOption(ChannelOption.TCP_NODELAY, true);
	bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
	bootstrap.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
	try {
        // 绑定端口,启动
		m_future = bootstrap.bind(port).sync();
		m_logger.info("start netty server!");
	} catch (Exception e) {
		m_logger.error("Started Netty Server Failed:" + port, e);
	}
}

CatHomeModule的execute方法中,会初始化ServerConfigManager载入系统配置。
初始化并启动ReportReloadTask,启动线程读取本地存储的Report报表。报表的具体细节后续介绍。
实例化MessageConsumer,执行后续消息处理,并且添加shutdown的写入检查点逻辑。消息处理逻辑后续介绍。
实例化任务消费者DefaultTaskConsumer,判断并执行Job机任务。Job机功能后续介绍。
实例化告警管理器AlarmManager,判断并执行告警机任务。告警功能后续介绍。

@Named(type = Module.class, value = CatHomeModule.ID)
public class CatHomeModule extends AbstractModule {
	public static final String ID = "cat-home";

	@Override
	protected void execute(ModuleContext ctx) throws Exception {
		ServerConfigManager serverConfigManager = ctx.lookup(ServerConfigManager.class);
		ReportReloadTask reportReloadTask = ctx.lookup(ReportReloadTask.class);

		Threads.forGroup("cat").start(reportReloadTask);

		ctx.lookup(MessageConsumer.class);

		if (serverConfigManager.isJobMachine()) {
			DefaultTaskConsumer taskConsumer = ctx.lookup(DefaultTaskConsumer.class);

			Threads.forGroup("cat").start(taskConsumer);
		}

		AlarmManager alarmManager = ctx.lookup(AlarmManager.class);

		if (serverConfigManager.isAlertMachine()) {
			alarmManager.startAlarm();
		}

		final MessageConsumer consumer = ctx.lookup(MessageConsumer.class);
		Runtime.getRuntime().addShutdownHook(new Thread() {

			@Override
			public void run() {
				consumer.doCheckpoint();
			}
		});
	}
    // 其他代码
}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值