文章目录
美团点评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方法,在启动时,执行初始化逻辑,主要包含四部分
- 初始化httpServlet
- Plexus IOC容器初始化
- 生成Plexus Logger日志类
- 调用子类实现,初始化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.xml
、 cat-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();
}
});
}
// 其他代码
}