学习中间件最好的方法是读源码,读源码最好的方式是调试一遍。调试第一步就是找中间件的入口。下面带着这几个问题找源码入口类及分析。后续的文章都是基于Canal 1.1.4版本做源码分析,咨询过Canal作者其核心代码几个版本都没有变化过。
本文是笔者基于问题的启发式源码阅读技巧的展示,建议带着如下问题开始本文的阅读:
- 如何找到Canal入口类?
- 入口类CanalLauncher.class做了什么?
- Canal使用什么技术?
一、如何找到Canal入口类
在Canal的bin文件夹startup.sh下我们发现有一个类 com.alibaba.otter.canal.deployer.CanalLauncher好像是Canal的入口类。打开源码的确CanalLauncher是Canal的入口类。下面我们分析CanalLauncher到底做了什么?
if [ -e $canal_conf -a -e $logback_configurationFile ]
then
for i in $base/lib/*;
do CLASSPATH=$i:"$CLASSPATH";
done
CLASSPATH="$base/conf:$CLASSPATH";
echo "cd to $bin_abs_path for workaround relative path"
cd $bin_abs_path
echo LOG CONFIGURATION : $logback_configurationFile
echo canal conf : $canal_conf
echo CLASSPATH :$CLASSPATH
$JAVA $JAVA_OPTS $JAVA_DEBUG_OPT $CANAL_OPTS -classpath .:$CLASSPATH com.alibaba.otter.canal.deployer.CanalLauncher 1>>$base/logs/canal/canal_stdout.log 2>&1 &
echo $! > $base/bin/canal.pid
echo "cd to $current_path for continue"
cd $current_path
else
echo "canal conf("$canal_conf") OR log configration file($logback_configurationFile) is not exist,please create then first!"
fi
二、入口类CanalLauncher.class做了什么?
2.1、分析Canal使用哪些技术?
- netty4.1.6
- zookeeper
- spring 3.2.18
- protobuf
- druid 1.1.9
- kafka
- rocketMq
- h2
- fastjson
通过列举Canal背后使用什么技术,我们会对Canal工作原理有进一步了解,如何知道Canal使用什么技术?canal.deployer工程下pom.xml有列举。
2.2、入口类CanalLauncher做了什么
2.2.1、加载配置
静态加载:Canal通过main方法启动项目,然后读取canal.conf配置文件,然后初始化整个框架。
public static void main(String[] args) {
try {
logger.info("## set default uncaught exception handler");
setGlobalUncaughtExceptionHandler();
logger.info("## load canal configurations");
String conf = System.getProperty("canal.conf", "classpath:canal.properties");
Properties properties = new Properties();
if (conf.startsWith(CLASSPATH_URL_PREFIX)) {
conf = StringUtils.substringAfter(conf, CLASSPATH_URL_PREFIX);
properties.load(CanalLauncher.class.getClassLoader().getResourceAsStream(conf));
} else {
properties.load(new FileInputStream(conf));
}
动态加载:canal支持修改canal.conf配置后,canal会自动监听文件的变化完成实例的启动,也就是新配置自动生效。
executor.scheduleWithFixedDelay(new Runnable() {
private PlainCanal lastCanalConfig;
public void run() {
try {
if (lastCanalConfig == null) {
lastCanalConfig = configClient.findServer(null);
} else {
PlainCanal newCanalConfig = configClient.findServer(lastCanalConfig.getMd5());
if (newCanalConfig != null) {
// 远程配置canal.properties修改重新加载整个应用
canalStater.stop();
Properties managerProperties = newCanalConfig.getProperties();
// merge local
managerProperties.putAll(properties);
canalStater.setProperties(managerProperties);
canalStater.start();
lastCanalConfig = newCanalConfig;
}
}
} catch (Throwable e) {
logger.error("scan failed", e);
}
}
}, 0, scanIntervalInSecond, TimeUnit.SECONDS);
2.2.2、通过SPI加载Canal所有依赖的框架
Canal启动接口根据canal.conf配置参数,做了如下工作。
- 通过SPI动态加载kafka、rocketmq中间件
- 初始化生产者 是通过netty直接推送日志给canal客户端,还是通过消息中间件做缓冲然后给客户端,如果通过消息中间件binlog不做二次解析提高cpu性能。
- 初始化shutdownThread线程,并且对其下钩子,当关机的时候实现优雅关闭CanalRuntime.getRuntime().addShutdownHook(shutdownThread); 如果对JVM钩子不明白的人可以看这篇文章有详细介绍。
/**
* 启动方法
*
* @throws Throwable
*/
public synchronized void start() throws Throwable {
String serverMode = CanalController.getProperty(properties, CanalConstants.CANAL_SERVER_MODE);
if (serverMode.equalsIgnoreCase("kafka")) {
canalMQProducer = new CanalKafkaProducer();
} else if (serverMode.equalsIgnoreCase("rocketmq")) {
canalMQProducer = new CanalRocketMQProducer();
}
if (canalMQProducer != null) {
// disable netty
System.setProperty(CanalConstants.CANAL_WITHOUT_NETTY, "true");
// 设置为raw避免ByteString->Entry的二次解析
System.setProperty("canal.instance.memory.rawEntry", "false");
}
logger.info("## start the canal server.");
controller = new CanalController(properties);
controller.start();
logger.info("## the canal server is running now ......");
shutdownThread = new Thread() {
public void run() {
try {
logger.info("## stop the canal server");
controller.stop();
CanalLauncher.runningLatch.countDown();
} catch (Throwable e) {
logger.warn("##something goes wrong when stopping canal Server:", e);
} finally {
logger.info("## canal server is down.");
}
}
};
Runtime.getRuntime().addShutdownHook(shutdownThread);
if (canalMQProducer != null) {
canalMQStarter = new CanalMQStarter(canalMQProducer);
MQProperties mqProperties = buildMQProperties(properties);
String destinations = CanalController.getProperty(properties, CanalConstants.CANAL_DESTINATIONS);
canalMQStarter.start(mqProperties, destinations);
controller.setCanalMQStarter(canalMQStarter);
}
// start canalAdmin
String port = properties.getProperty(CanalConstants.CANAL_ADMIN_PORT);
if (canalAdmin == null && StringUtils.isNotEmpty(port)) {
String user = properties.getProperty(CanalConstants.CANAL_ADMIN_USER);
String passwd = properties.getProperty(CanalConstants.CANAL_ADMIN_PASSWD);
CanalAdminController canalAdmin = new CanalAdminController(this);
canalAdmin.setUser(user);
canalAdmin.setPasswd(passwd);
String ip = properties.getProperty(CanalConstants.CANAL_IP);
CanalAdminWithNetty canalAdminWithNetty = CanalAdminWithNetty.instance();
canalAdminWithNetty.setCanalAdmin(canalAdmin);
canalAdminWithNetty.setPort(Integer.valueOf(port));
canalAdminWithNetty.setIp(ip);
canalAdminWithNetty.start();
this.canalAdmin = canalAdminWithNetty;
}
running = true;
}
2.2.3、初始化通讯模块让Canal各个节点可以通讯
2.3、感受CanalLauncher设计技巧