成也容器重启,败也容器重启,说好的重启治百病,在容器这里,是重启出百病啊!
之前说过,我们使用statefuset类型使canal-server域名固定之后又挂载pv使server即使重启也不会丢失数据,本以为万事大吉,没想到在用adapter同步数据时发现还是出了bug…
使用域名注册server之后,马上启了个adapter去检测一下域名方式是否可用,将canalServerHost的值设置成域名之后启动,数据成功同步,内心狂喜,感觉自己已经彻底成功了,但还是本着严谨的态度,手动delete掉canal-server的pod打算做一下测试,sever停止,adapter马上抛出异常,然后尝试重连,嗯,是我预想的现象,但是…五分钟过去了,server的pod早就重启了,为什么adapter还在尝试重连?
哪里的问题呢?第一次使用域名连接同步正常,表示域名方式的连接是没有问题的,但是重启之后就连接不上了,心里觉得和adapter与server的连接机制有关系,没办法,去读代码吧。
注:本人在使用canal-adapter-v1.1.4版本的时候遇到过数据丢失的情况,没有查出原因,最终选择了1.1.5-alpha-1版本
/**
* 初始化canal-client
*/
public void init() {
loader = ExtensionLoader.getExtensionLoader(OuterAdapter.class);
String canalServerHost = this.canalClientConfig.getCanalServerHost();
SocketAddress sa = null;
if (canalServerHost != null) {
String[] ipPort = canalServerHost.split(":");
sa = new InetSocketAddress(ipPort[0], Integer.parseInt(ipPort[1]));
}
String zkHosts = this.canalClientConfig.getZookeeperHosts();
if ("tcp".equalsIgnoreCase(canalClientConfig.getMode())) {
// 初始化canal-client的适配器
for (CanalClientConfig.CanalAdapter canalAdapter : canalClientConfig.getCanalAdapters()) {
List> canalOuterAdapterGroups = new CopyOnWriteArrayList<>();
for (CanalClientConfig.Group connectorGroup : canalAdapter.getGroups()) {
List canalOutConnectors = new CopyOnWriteArrayList<>();
for (OuterAdapterConfig c : connectorGroup.getOuterAdapters()) {
loadAdapter(c, canalOutConnectors);
}
canalOuterAdapterGroups.add(canalOutConnectors);
}
CanalAdapterWorker worker;
if (sa != null) {
worker = new CanalAdapterWorker(canalClientConfig,
canalAdapter.getInstance(),
sa,
canalOuterAdapterGroups);
} else if (zkHosts != null) {
worker = new CanalAdapterWorker(canalClientConfig,
canalAdapter.getInstance(),
zkHosts,
canalOuterAdapterGroups);
} else {
throw new RuntimeException("No canal server connector found");
}
canalWorkers.put(canalAdapter.getInstance(), worker);
worker.start();
logger.info("Start adapter for canal instance: {} succeed", canalAdapter.getInstance());
}
}
从client的初始化过程中,我们发现,CanalAdapterWorker对象传入的是SocketAddress对象,这个对象就是构建的我们要连接的canal-server地址
public InetSocketAddress(String hostname, int port) {
checkHost(hostname);
InetAddress addr = null;
String host = null;
try {
addr = InetAddress.getByName(hostname);
} catch(UnknownHostException e) {
host = hostname;
}
holder = new InetSocketAddressHolder(host, addr, checkPort(port));
}
public static InetAddress getByName(String host)
throws UnknownHostException {
return InetAddress.getAllByName(host)[0];
}
而从InetSocketAddress的构建过程来看,getByName方法获取的是我们域名后面对应的ip地址,看到这里,终于明白为什么在容器重启之后,重连一直失败了,传入CanalAdapterWorker对象的sa对象绑定的一直是初次建立连接时server的ip地址,容器重启之后,ip地址变化,canaladapterWorker还是在重连之前已经错误的ip,这当然有问题了!
知道了问题所在,一切就好解决了,只需要将sa对象构建地址的位置放在连接时就可以了,所以我们直接给CanalAdapterWorker创建一个新的构造方法,传入携带sa对象连接信息的canalServerHost参数即可。
/**
* worker针对域名类server重连机制修改构造方法
* @param canalClientConfig
* @param canalDestination
* @param address
* @param hoststr ip:port
* @param canalOuterAdapters
*/
public CanalAdapterWorker(CanalClientConfig canalClientConfig, String canalDestination, SocketAddress address,String hoststr,
List> canalOuterAdapters){
super(canalOuterAdapters);
this.canalClientConfig = canalClientConfig;
this.canalDestination = canalDestination;
connector = CanalConnectors.newSingleConnector(address,hoststr, canalDestination, "", "");
}
其他需要添加相应的方法的地方不再赘述,编辑器会告诉你的,哪里报错加哪里!
private InetSocketAddress doConnect() throws CanalClientException {
try {
channel = SocketChannel.open();
channel.socket().setSoTimeout(soTimeout);
SocketAddress address = getAddress();
if (address == null) {
address = getNextAddress();
}
channel.connect(address);
readableChannel = Channels.newChannel(channel.socket().getInputStream());
writableChannel = Channels.newChannel(channel.socket().getOutputStream());
通过源码,我们发现真正连接server是在SimpleCanalConnector的doConnect方法内,将其改造成如下
private InetSocketAddress doConnect() throws CanalClientException {
try {
channel = SocketChannel.open();
channel.socket().setSoTimeout(soTimeout);
if (null != hostPort && !"".equals(hostPort)) {
String[] ipPort = hostPort.split(":");
address = new InetSocketAddress(ipPort[0], Integer.parseInt(ipPort[1]));
channel.connect(address);
}else {
SocketAddress address = getAddress();
if (address == null) {
address = getNextAddress();
}
channel.connect(address);
}
readableChannel = Channels.newChannel(channel.socket().getInputStream());
writableChannel = Channels.newChannel(channel.socket().getOutputStream());
if (sa != null) {
worker = new CanalAdapterWorker(canalClientConfig,
canalAdapter.getInstance(),
sa,
canalServerHost,
canalOuterAdapterGroups);
}
最后别忘了将init方法内的CanalAdapterWorker替换成我们新增的构造方法
到这里,canal-adapter在k8s环境下使用域名方式连接canal-server的问题就顺利解决了,只需要将工程重新打包,然后按照之前构建canal-adapter镜像的方式重新构建一下镜像就大功告成了!
欢迎关注我的个人微信公众号,一个菜鸟程序猿的技术分享和奔溃日常