mirror of
https://github.com/alibaba/canal.git
synced 2026-11-04 00:30:15 +08:00
Compare commits
104 Commits
canal-1.1.
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87be50e876 | ||
|
|
e20e442446 | ||
|
|
08d736b33f | ||
|
|
cf628769d6 | ||
|
|
cf97b2ae31 | ||
|
|
ac6242bae9 | ||
|
|
c24307c893 | ||
|
|
a97d841737 | ||
|
|
8a5dc38a58 | ||
|
|
5110cfd49d | ||
|
|
da7ab64d55 | ||
|
|
09382c6b91 | ||
|
|
69a06dba09 | ||
|
|
1a265c8960 | ||
|
|
24877fdf03 | ||
|
|
c0de979595 | ||
|
|
fcadf564ba | ||
|
|
54c84b1535 | ||
|
|
4ded2869c9 | ||
|
|
f07a075dce | ||
|
|
dcea8afc93 | ||
|
|
c0619c4217 | ||
|
|
8a4199a7b0 | ||
|
|
79338be02d | ||
|
|
d18bde71c6 | ||
|
|
5a5e60d371 | ||
|
|
af9375440d | ||
|
|
9f2fc7409f | ||
|
|
556d998db1 | ||
|
|
25501db3ac | ||
|
|
8523a51623 | ||
|
|
16831825b8 | ||
|
|
1e37fe2dee | ||
|
|
5607444bad | ||
|
|
3a7223e460 | ||
|
|
553438e227 | ||
|
|
a3ea3d32d9 | ||
|
|
50a570290a | ||
|
|
6b7ddbbe92 | ||
|
|
2b53164253 | ||
|
|
e101287c25 | ||
|
|
b153e61f0d | ||
|
|
9bd4357f37 | ||
|
|
dad209feb8 | ||
|
|
e21e98aa83 | ||
|
|
c998b3f100 | ||
|
|
c15129b0ac | ||
|
|
0741ccda1e | ||
|
|
02fffc9e74 | ||
|
|
5ca4c39313 | ||
|
|
3162d7093d | ||
|
|
f350fbdc0a | ||
|
|
2d0dcf0e5d | ||
|
|
515c05c873 | ||
|
|
7ee551cbf7 | ||
|
|
923675aa49 | ||
|
|
ebb541001c | ||
|
|
44ed76ebb8 | ||
|
|
fe836f7515 | ||
|
|
b30544c750 | ||
|
|
66483ef83f | ||
|
|
b391188baf | ||
|
|
2a7a507987 | ||
|
|
f53749bfc5 | ||
|
|
80ca4366db | ||
|
|
be0d945d7d | ||
|
|
5a968b3715 | ||
|
|
1e5b8a2021 | ||
|
|
ff82fd6532 | ||
|
|
b060f46ccf | ||
|
|
7e31b40531 | ||
|
|
b2b3223d1d | ||
|
|
fea4d8210a | ||
|
|
fb3d2c6811 | ||
|
|
3439c48948 | ||
|
|
2b8a309ec7 | ||
|
|
9373429015 | ||
|
|
9493ff733d | ||
|
|
31530dc9af | ||
|
|
b0f3ff8dcc | ||
|
|
5c61f2fb56 | ||
|
|
7bb619cf12 | ||
|
|
d66a15fe35 | ||
|
|
aa05d8e2a0 | ||
|
|
5ff6449620 | ||
|
|
bbea802914 | ||
|
|
e2219c88dd | ||
|
|
a92e61f5a9 | ||
|
|
eaacc01b77 | ||
|
|
caa46f3672 | ||
|
|
c0ff058fe1 | ||
|
|
059f21208d | ||
|
|
8ca900377a | ||
|
|
6dffcc8573 | ||
|
|
711971599f | ||
|
|
87c7a95523 | ||
|
|
e255baff11 | ||
|
|
d457cd835e | ||
|
|
cf9c64629e | ||
|
|
307547746c | ||
|
|
c7d5ed4ba1 | ||
|
|
b9ed5caf05 | ||
|
|
348d218120 | ||
|
|
e2a64daad8 |
46
.github/workflows/maven.yml
vendored
Normal file
46
.github/workflows/maven.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
name: Java CI with Maven
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
java: [ 8, 11, 17, 21 ]
|
||||
fail-fast: false
|
||||
max-parallel: 16
|
||||
name: Test JDK ${{ matrix.java }}, ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: ${{ matrix.java }}
|
||||
cache: 'maven'
|
||||
- name: Build with Maven if test jdk8
|
||||
if: ${{ matrix.java == '8' || matrix.java == '11'}}
|
||||
run: ./mvnw -Pgen-javadoc clean package -B
|
||||
- name: Build with Maven if test jdk17
|
||||
if: ${{ matrix.java == '17' || matrix.java == '21' }}
|
||||
run: ./mvnw -Penable-for-jdk17+,gen-code-cov clean package -B
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,6 +7,7 @@ test-output/
|
||||
.settings/
|
||||
tmp
|
||||
temp
|
||||
logs/
|
||||
*.log
|
||||
antx.properties
|
||||
otter.properties
|
||||
@@ -18,6 +19,8 @@ jtester.properties
|
||||
*.rpm
|
||||
client-adapter/example/
|
||||
*.dat
|
||||
.qwen/
|
||||
.vscode/
|
||||
conf/
|
||||
canal-admin/canal-admin-ui/dist/
|
||||
canal-admin/canal-admin-ui/node/
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
- [Introduction](https://github.com/alibaba/canal/wiki/Introduction)
|
||||
- [QuickStart](https://github.com/alibaba/canal/wiki/QuickStart)
|
||||
- [Docker QuickStart](https://github.com/alibaba/canal/wiki/Docker-QuickStart)
|
||||
- [Canal Kafka/RocketMQ QuickStart](https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart")
|
||||
- [Canal Kafka/RocketMQ QuickStart](https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart)
|
||||
- [Aliyun RDS for MySQL QuickStart](https://github.com/alibaba/canal/wiki/aliyun-RDS-QuickStart)
|
||||
- [Prometheus QuickStart](https://github.com/alibaba/canal/wiki/Prometheus-QuickStart)
|
||||
- Canal Admin
|
||||
|
||||
118
SECURITY.md
Normal file
118
SECURITY.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# 漏洞奖励计划
|
||||
## 报告
|
||||
如果您认为自己在本程序中发现了任何安全(技术)漏洞,欢迎您通过 https://security.alibaba.com 向我们提交漏洞报告。
|
||||
如果您报告任何安全漏洞,请注意您可能包含以下信息(合格报告):
|
||||
* git程序URL地址,运行的环境
|
||||
* 包含必要屏幕截图的详细说明
|
||||
* 重现漏洞的步骤以及修复漏洞的建议。
|
||||
* 其他有用信息
|
||||
|
||||
## 处理
|
||||
ASRC(Alibaba Security Response Center阿里安全响应中心)将尽快审核并回复您的提交内容,并在我们努力修复您提交的漏洞时随时通知您。如有必要,我们可能会与您联系以获取更多信息。
|
||||
|
||||
|
||||
## 条款和条件
|
||||
1. 仅接受技术漏洞并对其进行评级
|
||||
2. 出于安全原因,上报者同意与ASRC合作完成他/她提交的漏洞,不向任何第三方透露任何漏洞信息
|
||||
3. 如果不止一个人报告相同的安全漏洞,奖励将给予完成合格报告的第一个人
|
||||
4. 为了保护程序的用户,请在修复之前不要直接提交git的issue,也不要在社区讨论任何漏洞信息
|
||||
5. 所有奖励和声誉积分将提供给仅向ASRC提交其安全漏洞的上报者
|
||||
6. 安全漏洞奖励的解释权利归ASRC所有
|
||||
|
||||
## 收集范围
|
||||
我们的主要收集漏洞类别是:
|
||||
* 服务器端请求伪造(SSRF)
|
||||
* SQL注入
|
||||
* 拒绝服务攻击
|
||||
* 远程执行代码(RCE)
|
||||
* XML外部实体攻击(XXE)
|
||||
* 访问控制问题(不安全的直接对象参考问题等)
|
||||
* 敏感目录遍历问题
|
||||
* 本地文件读取(LFD)
|
||||
* 敏感信息泄露(密钥,Cookie,Session等)
|
||||
|
||||
## 奖励
|
||||
* 可直接导致严重问题的每个漏洞奖励7000元人民币
|
||||
* 存在限制及需要一定特殊环境下才能利用的问题将给予700-5600元人民币不等的奖励,比如需要用户主动点击才会触发的问题或需要admin权限
|
||||
* 只有在指定环境下才可以运行的利用将有可能被收纳但不给予奖励,或直接被忽略,比如只在fastjson+linux特定版本才会出现的问题
|
||||
|
||||
## 不在收集范围的报告
|
||||
* 影响过时浏览器或平台用户的漏洞
|
||||
* Self-XSS
|
||||
* 会话固定
|
||||
* 内容欺骗
|
||||
* 缺少cookie标记
|
||||
* 混合内容警告
|
||||
* SSL / TLS问题
|
||||
* Clickjacking
|
||||
* 基于Flash的漏洞
|
||||
* 反射文件下载攻击(RFD)
|
||||
* 物理或社会工程攻击
|
||||
* 未验证自动化工具或扫描仪的结果
|
||||
* 登录/注销/未认证/低影响CSRF
|
||||
* 需要MITM或物理访问用户设备的攻击
|
||||
* 与网络协议或行业标准相关的问题
|
||||
* 不能用于直接攻击的错误信息泄露
|
||||
* 缺少与安全相关的HTTP标头等
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Vulnerability Reward Program
|
||||
## Reporting
|
||||
If you believe you have found any security (technical) vulnerability in the Program, you are welcomed to submit a vulnerability report to us at https://security.alibaba.com
|
||||
In case of reporting any security vulnerability, please be noted that you may including following information (Qualified Reporting):
|
||||
* The git program URL and running version
|
||||
* A detailed description with necessary screenshots
|
||||
* Steps to reappearance the vulnerability and your advice to fix it
|
||||
* Other useful information
|
||||
|
||||
|
||||
## Processing
|
||||
ASRC (Alibaba Security Response Center) will review and respond as quickly as possible to your submission, and keep you informed as we work to fix the vulnerability you submitted. We may contact you for further information if necessary.
|
||||
|
||||
|
||||
## Terms and Conditions
|
||||
1. ONLY technical vulnerabilities will be accepted and rated.
|
||||
2. With regarding to security reasons, reporters agree to cooperate with ASRC exclusively on the vulnerability he/she submitted and not disclose any information of vulnerability to any third-parties.
|
||||
3. In the case that more than one person report the same security vulnerability, the reward will be given to the first person who accomplish a Qualified Reporting.
|
||||
4. To protect users of the program, please do not directly submit issue on github or discuss anything with the community
|
||||
5. All Rewards and Reputation Credits are given to the reporters who submit his/her security vulnerabilities ONLY to ASRC.
|
||||
6. All rights for the security vulnerability rewards are reserved by ASRC.
|
||||
|
||||
## Scope of Collecting
|
||||
The main categories of vulnerabilities that we are sincerely looking for are:
|
||||
* Server-Side Request Forgery (SSRF)
|
||||
* SQL Injection
|
||||
* Denial of Service Attack
|
||||
* Remote Code Execution (RCE)
|
||||
* XML External Entity Attacks (XXE)
|
||||
* Access Control Issues (Insecure Direct Object Reference issues, etc.)
|
||||
* Directory Traversal Issues
|
||||
* Local File Disclosure (LFD)
|
||||
* Sensitive Information Leakage (Key, Cookie, Session etc.)
|
||||
|
||||
## Reward
|
||||
* $1,000 for one valid report
|
||||
* $100-$800 for Vuls which is limited. For example, Vuls that need user interactions or administrator authority
|
||||
* Vuls which only work on the special version will be accepted but no reward, or directly rejected. For example, Vul runs only on a special linux version
|
||||
|
||||
## Ineligible Reports
|
||||
* Vulnerabilities affecting users of outdated browsers or platforms
|
||||
* "Self" XSS
|
||||
* Session fixation
|
||||
* Content Spoofing
|
||||
* Missing cookie flags
|
||||
* Mixed content warnings
|
||||
* SSL/TLS best practices
|
||||
* Clickjacking/UI redressing
|
||||
* Flash-based vulnerabilities
|
||||
* Reflected file download attacks (RFD)
|
||||
* Physical or social engineering attacks
|
||||
* Unverified Results of automated tools or scanners
|
||||
* Login/logout/unauthenticated/low-impact CSRF
|
||||
* Attacks requiring MITM or physical access to a user's device
|
||||
* Issues related to networking protocols or industry standards
|
||||
* Error information disclosure that cannot be used to make a direct attack
|
||||
* Missing security-related HTTP headers which do not lead directly to a vulnerability
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.admin</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.admin</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>canal.admin-web</artifactId>
|
||||
|
||||
@@ -11,7 +11,7 @@ set CLASSPATH=%conf_dir%
|
||||
set CLASSPATH=%conf_dir%\..\lib\*;%CLASSPATH%
|
||||
|
||||
set JAVA_MEM_OPTS= -Xms128m -Xmx512m
|
||||
set JAVA_OPTS_EXT= -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -Dapplication.codeset=UTF-8 -Dfile.encoding=UTF-8
|
||||
set JAVA_OPTS_EXT= -Djava.awt.headless=true -Djava.net.preferIPv4Stack=false -Dapplication.codeset=UTF-8 -Dfile.encoding=UTF-8
|
||||
set ADAPTER_OPTS= -DappName=canal-admin
|
||||
|
||||
set JAVA_OPTS= %JAVA_MEM_OPTS% %JAVA_OPTS_EXT% %ADAPTER_OPTS%
|
||||
@@ -19,4 +19,4 @@ set JAVA_OPTS= %JAVA_MEM_OPTS% %JAVA_OPTS_EXT% %ADAPTER_OPTS%
|
||||
set CMD_STR= java %JAVA_OPTS% -classpath "%CLASSPATH%" com.alibaba.otter.canal.admin.CanalAdminApplication
|
||||
echo start cmd : %CMD_STR%
|
||||
|
||||
java %JAVA_OPTS% -classpath "%CLASSPATH%" com.alibaba.otter.canal.admin.CanalAdminApplication
|
||||
java %JAVA_OPTS% -classpath "%CLASSPATH%" com.alibaba.otter.canal.admin.CanalAdminApplication
|
||||
|
||||
@@ -79,7 +79,7 @@ else
|
||||
JAVA_OPTS="-server -Xms1024m -Xmx1024m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:MaxPermSize=128m $JAVA_OPTS"
|
||||
fi
|
||||
|
||||
JAVA_OPTS=" $JAVA_OPTS -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -Dfile.encoding=UTF-8"
|
||||
JAVA_OPTS=" $JAVA_OPTS -Djava.awt.headless=true -Djava.net.preferIPv4Stack=false -Dfile.encoding=UTF-8"
|
||||
CANAL_OPTS="-DappName=canal-admin"
|
||||
|
||||
for i in $base/lib/*;
|
||||
|
||||
@@ -98,6 +98,26 @@ public class WebConfig implements WebMvcConfigurer {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否使用默认密码,如果是则仅允许改密和登出操作
|
||||
User validUser = (User) httpServletRequest.getAttribute("user");
|
||||
if (validUser != null && validUser.isDefaultPassword()) {
|
||||
String uri = httpServletRequest.getRequestURI();
|
||||
String method = httpServletRequest.getMethod();
|
||||
boolean isPasswordChange = "PUT".equals(method) && uri.matches("/api/[^/]+/user");
|
||||
boolean isLogout = uri.contains("/user/logout");
|
||||
if (!isPasswordChange && !isLogout) {
|
||||
BaseModel baseModel = BaseModel.getInstance(null);
|
||||
baseModel.setCode(50015);
|
||||
baseModel.setMessage("Default password must be changed before proceeding");
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
String json = mapper.writeValueAsString(baseModel);
|
||||
httpServletResponse.setContentType("application/json;charset=UTF-8");
|
||||
PrintWriter out = httpServletResponse.getWriter();
|
||||
out.print(json);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
})
|
||||
|
||||
@@ -56,6 +56,9 @@ public class UserController {
|
||||
String token = UUID.randomUUID().toString();
|
||||
loginUsers.put(token, loginUser);
|
||||
tokenResp.put("token", token);
|
||||
if (loginUser.isDefaultPassword()) {
|
||||
tokenResp.put("needPasswordChange", "true");
|
||||
}
|
||||
return BaseModel.getInstance(tokenResp);
|
||||
} else {
|
||||
BaseModel<Map<String, String>> model = BaseModel.getInstance(null);
|
||||
@@ -98,6 +101,7 @@ public class UserController {
|
||||
HttpServletRequest httpServletRequest) {
|
||||
userService.update(user);
|
||||
String token = (String) httpServletRequest.getAttribute("token");
|
||||
user.setDefaultPassword(false);
|
||||
loginUsers.put(token, user);
|
||||
return BaseModel.getInstance("success");
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ public class User extends Model {
|
||||
@Transient
|
||||
private String oldPassword;
|
||||
|
||||
@Transient
|
||||
private boolean defaultPassword;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -115,4 +118,12 @@ public class User extends Model {
|
||||
public void setOldPassword(String oldPassword) {
|
||||
this.oldPassword = oldPassword;
|
||||
}
|
||||
|
||||
public boolean isDefaultPassword() {
|
||||
return defaultPassword;
|
||||
}
|
||||
|
||||
public void setDefaultPassword(boolean defaultPassword) {
|
||||
this.defaultPassword = defaultPassword;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.alibaba.otter.canal.admin.service.impl;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.alibaba.otter.canal.admin.common.exception.ServiceException;
|
||||
@@ -19,7 +21,11 @@ import com.alibaba.otter.canal.protocol.SecurityUtil;
|
||||
@Service
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
private static byte[] seeds = "canal is best!".getBytes();
|
||||
private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
|
||||
|
||||
private static final String DEFAULT_PASSWORD_HASH = "6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9";
|
||||
|
||||
private static byte[] seeds = "canal is best!".getBytes();
|
||||
|
||||
private static final Integer PASSWORD_LENGTH = 5;
|
||||
|
||||
@@ -40,6 +46,11 @@ public class UserServiceImpl implements UserService {
|
||||
throw new ServiceException("user:" + user.getName() + " auth failed!");
|
||||
}
|
||||
|
||||
if (DEFAULT_PASSWORD_HASH.equalsIgnoreCase(user.getPassword())) {
|
||||
user.setDefaultPassword(true);
|
||||
logger.warn("Default admin password detected for user: {}. Please change immediately.", username);
|
||||
}
|
||||
|
||||
user.setPassword("");
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ SET FOREIGN_KEY_CHECKS = 0;
|
||||
DROP TABLE IF EXISTS `canal_adapter_config`;
|
||||
CREATE TABLE `canal_adapter_config` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`category` varchar(45) NOT NULL,
|
||||
`name` varchar(45) NOT NULL,
|
||||
`category` varchar(255) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`status` varchar(45) DEFAULT NULL,
|
||||
`content` text NOT NULL,
|
||||
`modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
@@ -25,7 +25,7 @@ CREATE TABLE `canal_adapter_config` (
|
||||
DROP TABLE IF EXISTS `canal_cluster`;
|
||||
CREATE TABLE `canal_cluster` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(63) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`zk_hosts` varchar(255) NOT NULL,
|
||||
`modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
@@ -39,7 +39,7 @@ CREATE TABLE `canal_config` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`cluster_id` bigint(20) DEFAULT NULL,
|
||||
`server_id` bigint(20) DEFAULT NULL,
|
||||
`name` varchar(45) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`status` varchar(45) DEFAULT NULL,
|
||||
`content` text NOT NULL,
|
||||
`content_md5` varchar(128) NOT NULL,
|
||||
@@ -56,7 +56,7 @@ CREATE TABLE `canal_instance_config` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`cluster_id` bigint(20) DEFAULT NULL,
|
||||
`server_id` bigint(20) DEFAULT NULL,
|
||||
`name` varchar(45) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`status` varchar(45) DEFAULT NULL,
|
||||
`content` text NOT NULL,
|
||||
`content_md5` varchar(128) DEFAULT NULL,
|
||||
@@ -72,8 +72,8 @@ DROP TABLE IF EXISTS `canal_node_server`;
|
||||
CREATE TABLE `canal_node_server` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`cluster_id` bigint(20) DEFAULT NULL,
|
||||
`name` varchar(63) NOT NULL,
|
||||
`ip` varchar(63) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`ip` varchar(255) NOT NULL,
|
||||
`admin_port` int(11) DEFAULT NULL,
|
||||
`tcp_port` int(11) DEFAULT NULL,
|
||||
`metric_port` int(11) DEFAULT NULL,
|
||||
@@ -88,10 +88,10 @@ CREATE TABLE `canal_node_server` (
|
||||
DROP TABLE IF EXISTS `canal_user`;
|
||||
CREATE TABLE `canal_user` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(31) NOT NULL,
|
||||
`password` varchar(128) NOT NULL,
|
||||
`name` varchar(31) NOT NULL,
|
||||
`roles` varchar(31) NOT NULL,
|
||||
`username` varchar(255) NOT NULL,
|
||||
`password` varchar(255) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`roles` varchar(255) NOT NULL,
|
||||
`introduction` varchar(255) DEFAULT NULL,
|
||||
`avatar` varchar(255) DEFAULT NULL,
|
||||
`creation_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>canal</artifactId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>canal.admin</artifactId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>canal admin module for otter ${project.version}</name>
|
||||
<modules>
|
||||
@@ -36,7 +36,7 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>2.5.4</version>
|
||||
<version>2.5.15</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -48,7 +48,12 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean</artifactId>
|
||||
<version>11.41.1</version>
|
||||
<version>11.45.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<version>2.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
160
charts/README.md
Normal file
160
charts/README.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# helm charts 使用说明
|
||||
|
||||
包含`canal-admin`和`canal-server`两个chart,基本配置保存在k8s的`secret`里,`canal.properties`和`instance.properties`保存在mysql数据库中,使用zookeeper注册中心实现高可用。
|
||||
|
||||
- [helm charts 使用说明](#helm-charts-使用说明)
|
||||
- [创建zk集群](#创建zk集群)
|
||||
- [初始化数据库](#初始化数据库)
|
||||
- [部署canal-admin chart](#部署canal-admin-chart)
|
||||
- [配置values.yaml](#配置valuesyaml)
|
||||
- [配置集群信息](#配置集群信息)
|
||||
- [部署canal-server chart](#部署canal-server-chart)
|
||||
- [配置values.yaml](#配置valuesyaml-1)
|
||||
- [instance 实例创建](#instance-实例创建)
|
||||
|
||||
## 创建zk集群
|
||||
|
||||
```sh
|
||||
helm install canal-zookeeper oci://registry-1.docker.io/bitnamicharts/zookeeper
|
||||
```
|
||||
|
||||
如需挂载磁盘参考 [bitnami/zookeeper](`https://github.com/bitnami/charts/tree/main/bitnami/zookeeper`)
|
||||
|
||||
|
||||
## 初始化数据库
|
||||
|
||||
[/admin/admin-web/src/main/resources/canal_manager.sql](/admin/admin-web/src/main/resources/canal_manager.sql)
|
||||
|
||||
[/deployer/src/main/resources/spring/tsdb/sql/create_table.sql](/deployer/src/main/resources/spring/tsdb/sql/create_table.sql)
|
||||
|
||||
## 部署canal-admin chart
|
||||
|
||||
### 配置values.yaml
|
||||
|
||||
```yaml
|
||||
# 主要配置
|
||||
admin:
|
||||
config: |
|
||||
server:
|
||||
port: 8089
|
||||
spring:
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
time-zone: GMT+8
|
||||
spring.datasource:
|
||||
address: your_db_host:3306
|
||||
database: canal_manager
|
||||
username: ****
|
||||
password: ****
|
||||
driver-class-name: com.mysql.jdbc.Driver
|
||||
url: jdbc:mysql://${spring.datasource.address}/${spring.datasource.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true
|
||||
hikari:
|
||||
maximum-pool-size: 30
|
||||
minimum-idle: 1
|
||||
canal:
|
||||
adminUser: admin
|
||||
adminPasswd: admin
|
||||
# debug: true
|
||||
```
|
||||
|
||||
全量参数见:[canal-admin/values.yaml](./canal-admin/values.yaml),测试用例见:[tests/admin-values.yaml](./tests/admin-values.yaml)
|
||||
|
||||
```sh
|
||||
helm install canal-admin -f ./admin-values.yaml ./canal-admin
|
||||
```
|
||||
|
||||
### 配置集群信息
|
||||
|
||||

|
||||
|
||||
等待pod启动后,使用默认账户`admin`密码`123456`登录
|
||||
|
||||
创建集群`default`并配置`zk地址`为`canal-zookeeper.default.svc.cluster.local:2181`
|
||||
|
||||
配置完成后,点击`配置`-`主配置`,修改以下配置项,不是覆盖整个文件
|
||||
|
||||
```yaml
|
||||
canal.user = admin
|
||||
canal.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
|
||||
|
||||
canal.zkServers = canal-zookeeper.default.svc.cluster.local:2181
|
||||
# tcp, kafka, rocketMQ, rabbitMQ, pulsarMQ 看实际需求,此处改为了rabbitMQ
|
||||
canal.serverMode = rabbitMQ
|
||||
|
||||
#canal.instance.tsdb.dir = ${canal.file.data.dir:../conf}/${canal.instance.destination:}
|
||||
canal.instance.tsdb.url = jdbc:mysql://you_db_host:3306/canal_tsdb?useUnicode=true&characterEncoding=UTF-8&useSSL=false
|
||||
canal.instance.tsdb.dbUsername = ****
|
||||
canal.instance.tsdb.dbPassword = ****
|
||||
|
||||
#canal.instance.tsdb.spring.xml = classpath:spring/tsdb/h2-tsdb.xml
|
||||
canal.instance.tsdb.spring.xml = classpath:spring/tsdb/mysql-tsdb.xml
|
||||
|
||||
#canal.instance.global.spring.xml = classpath:spring/file-instance.xml
|
||||
canal.instance.global.spring.xml = classpath:spring/default-instance.xml
|
||||
|
||||
# 如果用的是阿里云polardb需要配置
|
||||
canal.aliyun.accessKey = ****
|
||||
canal.aliyun.secretKey = ****
|
||||
canal.aliyun.uid = 主账号id
|
||||
|
||||
# 我们是推送到rabbitmq所以需要配置
|
||||
rabbitmq.host = ****:5672
|
||||
rabbitmq.virtual.host = ****
|
||||
rabbitmq.exchange = ****
|
||||
# canal.aliyun.accessKey
|
||||
rabbitmq.username = ****
|
||||
# canal.aliyun.secretKey
|
||||
rabbitmq.password = ****
|
||||
rabbitmq.deliveryMode = direct
|
||||
```
|
||||
|
||||
## 部署canal-server chart
|
||||
|
||||
### 配置values.yaml
|
||||
|
||||
```yaml
|
||||
# 主要配置
|
||||
server:
|
||||
config: |
|
||||
canal.port = 11111
|
||||
canal.metrics.pull.port = 11112
|
||||
|
||||
# register ip
|
||||
canal.register.ip =
|
||||
|
||||
# canal admin config
|
||||
canal.admin.manager = canal-admin.default:8089
|
||||
canal.admin.port = 11110
|
||||
canal.admin.user = admin
|
||||
canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
|
||||
# admin auto register
|
||||
canal.admin.register.auto = true
|
||||
canal.admin.register.cluster = default
|
||||
```
|
||||
|
||||
```sh
|
||||
helm install canal-server -f ./server-values.yaml ./canal-server
|
||||
```
|
||||
|
||||
全量参数见:[canal-server/values.yaml](./canal-server/values.yaml),测试用例见:[tests/server-values.yaml](./tests/server-values.yaml)
|
||||
|
||||

|
||||
|
||||
至此,canal-server已自动创建,并自动注册。
|
||||
|
||||
## instance 实例创建
|
||||
|
||||
主要配置如下
|
||||
|
||||
```yaml
|
||||
# 配置你要监听的数据库
|
||||
canal.instance.master.address=127.0.0.1:3306
|
||||
canal.instance.dbUsername=****
|
||||
canal.instance.dbPassword=****
|
||||
|
||||
# 过滤监听哪些表
|
||||
canal.instance.filter.regex=.*\\..*
|
||||
|
||||
# 如果是推送到rabbitmq,需要配置 Routing Key
|
||||
canal.mq.topic=你的Routing Key
|
||||
```
|
||||
23
charts/canal-admin/.helmignore
Normal file
23
charts/canal-admin/.helmignore
Normal file
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
24
charts/canal-admin/Chart.yaml
Normal file
24
charts/canal-admin/Chart.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
apiVersion: v2
|
||||
name: canal-admin
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 1.1.8-alpha-3
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
||||
22
charts/canal-admin/templates/NOTES.txt
Normal file
22
charts/canal-admin/templates/NOTES.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "canal-admin.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "canal-admin.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "canal-admin.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "canal-admin.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
62
charts/canal-admin/templates/_helpers.tpl
Normal file
62
charts/canal-admin/templates/_helpers.tpl
Normal file
@@ -0,0 +1,62 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "canal-admin.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "canal-admin.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "canal-admin.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "canal-admin.labels" -}}
|
||||
helm.sh/chart: {{ include "canal-admin.chart" . }}
|
||||
{{ include "canal-admin.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "canal-admin.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "canal-admin.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "canal-admin.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "canal-admin.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
75
charts/canal-admin/templates/deployment.yaml
Normal file
75
charts/canal-admin/templates/deployment.yaml
Normal file
@@ -0,0 +1,75 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "canal-admin.fullname" . }}
|
||||
labels:
|
||||
{{- include "canal-admin.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "canal-admin.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "canal-admin.labels" . | nindent 8 }}
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "canal-admin.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.port }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.readinessProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- mountPath: /home/admin/canal-admin/conf/application.yml
|
||||
name: secret
|
||||
subPath: application.yml
|
||||
{{- with .Values.volumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: secret
|
||||
secret:
|
||||
defaultMode: 420
|
||||
secretName: {{ include "canal-admin.fullname" . }}
|
||||
{{- with .Values.volumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
32
charts/canal-admin/templates/hpa.yaml
Normal file
32
charts/canal-admin/templates/hpa.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "canal-admin.fullname" . }}
|
||||
labels:
|
||||
{{- include "canal-admin.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "canal-admin.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
43
charts/canal-admin/templates/ingress.yaml
Normal file
43
charts/canal-admin/templates/ingress.yaml
Normal file
@@ -0,0 +1,43 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "canal-admin.fullname" . }}
|
||||
labels:
|
||||
{{- include "canal-admin.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.ingress.className }}
|
||||
ingressClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- with .pathType }}
|
||||
pathType: {{ . }}
|
||||
{{- end }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "canal-admin.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
9
charts/canal-admin/templates/secret.yaml
Normal file
9
charts/canal-admin/templates/secret.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "canal-admin.fullname" . }}
|
||||
labels:
|
||||
{{- include "canal-admin.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
application.yml: {{tpl .Values.admin.config $ | b64enc | quote }}
|
||||
15
charts/canal-admin/templates/service.yaml
Normal file
15
charts/canal-admin/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "canal-admin.fullname" . }}
|
||||
labels:
|
||||
{{- include "canal-admin.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "canal-admin.selectorLabels" . | nindent 4 }}
|
||||
13
charts/canal-admin/templates/serviceaccount.yaml
Normal file
13
charts/canal-admin/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "canal-admin.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "canal-admin.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||
{{- end }}
|
||||
15
charts/canal-admin/templates/tests/test-connection.yaml
Normal file
15
charts/canal-admin/templates/tests/test-connection.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "canal-admin.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "canal-admin.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "canal-admin.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
||||
146
charts/canal-admin/values.yaml
Normal file
146
charts/canal-admin/values.yaml
Normal file
@@ -0,0 +1,146 @@
|
||||
# Default values for canal-admin.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
|
||||
replicaCount: 1
|
||||
|
||||
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
|
||||
image:
|
||||
repository: canal/canal-admin
|
||||
# This sets the pull policy for images.
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: "latest"
|
||||
|
||||
# This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||
imagePullSecrets: []
|
||||
# This is to override the chart name.
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: false
|
||||
# Automatically mount a ServiceAccount's API credentials?
|
||||
automount: false
|
||||
# Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
# This is for setting Kubernetes Annotations to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
|
||||
podAnnotations: {}
|
||||
# This is for setting Kubernetes Labels to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/
|
||||
service:
|
||||
# This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
|
||||
type: ClusterIP
|
||||
# This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
|
||||
port: 8089
|
||||
|
||||
# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: chart-example.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
|
||||
# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# Additional volumes on the output Deployment definition.
|
||||
volumes: []
|
||||
# - name: foo
|
||||
# secret:
|
||||
# secretName: mysecret
|
||||
# optional: false
|
||||
|
||||
# Additional volumeMounts on the output Deployment definition.
|
||||
volumeMounts: []
|
||||
# - name: foo
|
||||
# mountPath: "/etc/foo"
|
||||
# readOnly: true
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
admin:
|
||||
config: |
|
||||
server:
|
||||
port: 8089
|
||||
spring:
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
time-zone: GMT+8
|
||||
spring.datasource:
|
||||
address: you_db_host:3306
|
||||
database: canal_manager
|
||||
username: ****
|
||||
password: ****
|
||||
driver-class-name: com.mysql.jdbc.Driver
|
||||
url: jdbc:mysql://${spring.datasource.address}/${spring.datasource.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true
|
||||
hikari:
|
||||
maximum-pool-size: 30
|
||||
minimum-idle: 1
|
||||
canal:
|
||||
adminUser: admin
|
||||
adminPasswd: admin
|
||||
# debug: true
|
||||
23
charts/canal-server/.helmignore
Normal file
23
charts/canal-server/.helmignore
Normal file
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
24
charts/canal-server/Chart.yaml
Normal file
24
charts/canal-server/Chart.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
apiVersion: v2
|
||||
name: canal-server
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 1.1.8-alpha-3
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
||||
22
charts/canal-server/templates/NOTES.txt
Normal file
22
charts/canal-server/templates/NOTES.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "canal-server.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "canal-server.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "canal-server.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "canal-server.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
62
charts/canal-server/templates/_helpers.tpl
Normal file
62
charts/canal-server/templates/_helpers.tpl
Normal file
@@ -0,0 +1,62 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "canal-server.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "canal-server.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "canal-server.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "canal-server.labels" -}}
|
||||
helm.sh/chart: {{ include "canal-server.chart" . }}
|
||||
{{ include "canal-server.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "canal-server.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "canal-server.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "canal-server.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "canal-server.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
32
charts/canal-server/templates/hpa.yaml
Normal file
32
charts/canal-server/templates/hpa.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "canal-server.fullname" . }}
|
||||
labels:
|
||||
{{- include "canal-server.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "canal-server.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
9
charts/canal-server/templates/secret.yaml
Normal file
9
charts/canal-server/templates/secret.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "canal-server.fullname" . }}
|
||||
labels:
|
||||
{{- include "canal-server.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
canal.properties: {{tpl .Values.server.config $ | b64enc | quote }}
|
||||
23
charts/canal-server/templates/service.yaml
Normal file
23
charts/canal-server/templates/service.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ printf "%s-headless" (include "canal-server.fullname" .) | trunc 63 | trimSuffix "-" }}
|
||||
labels:
|
||||
{{- include "canal-server.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.ports.admin }}
|
||||
targetPort: admin
|
||||
protocol: TCP
|
||||
name: tcp-admin
|
||||
- port: {{ .Values.service.ports.tcp }}
|
||||
targetPort: tcp
|
||||
protocol: TCP
|
||||
name: tcp-tcp
|
||||
- port: {{ .Values.service.ports.metric }}
|
||||
targetPort: metric
|
||||
protocol: TCP
|
||||
name: tcp-metric
|
||||
selector:
|
||||
{{- include "canal-server.selectorLabels" . | nindent 4 }}
|
||||
13
charts/canal-server/templates/serviceaccount.yaml
Normal file
13
charts/canal-server/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "canal-server.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "canal-server.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||
{{- end }}
|
||||
80
charts/canal-server/templates/statefulsets.yaml
Normal file
80
charts/canal-server/templates/statefulsets.yaml
Normal file
@@ -0,0 +1,80 @@
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ include "canal-server.fullname" . }}
|
||||
labels:
|
||||
{{- include "canal-server.labels" . | nindent 4 }}
|
||||
spec:
|
||||
serviceName: {{ printf "%s-headless" (include "canal-server.fullname" .) | trunc 63 | trimSuffix "-" }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "canal-server.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "canal-server.labels" . | nindent 8 }}
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "canal-server.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: admin
|
||||
containerPort: {{ .Values.service.ports.admin }}
|
||||
protocol: TCP
|
||||
- name: tcp
|
||||
containerPort: {{ .Values.service.ports.tcp }}
|
||||
protocol: TCP
|
||||
- name: metric
|
||||
containerPort: {{ .Values.service.ports.metric }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.readinessProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- mountPath: /home/admin/canal-server/conf/canal.properties
|
||||
name: secret
|
||||
subPath: canal.properties
|
||||
{{- with .Values.volumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: secret
|
||||
secret:
|
||||
defaultMode: 420
|
||||
secretName: {{ include "canal-server.fullname" . }}
|
||||
{{- with .Values.volumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
15
charts/canal-server/templates/tests/test-connection.yaml
Normal file
15
charts/canal-server/templates/tests/test-connection.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "canal-server.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "canal-server.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "canal-server.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
||||
144
charts/canal-server/values.yaml
Normal file
144
charts/canal-server/values.yaml
Normal file
@@ -0,0 +1,144 @@
|
||||
# Default values for canal-server.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
|
||||
replicaCount: 1
|
||||
|
||||
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
|
||||
image:
|
||||
repository: canal/canal-server
|
||||
# This sets the pull policy for images.
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: "1.1.8-alpha-3"
|
||||
|
||||
# This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||
imagePullSecrets: []
|
||||
# This is to override the chart name.
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: false
|
||||
# Automatically mount a ServiceAccount's API credentials?
|
||||
automount: false
|
||||
# Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
# This is for setting Kubernetes Annotations to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
|
||||
podAnnotations: {}
|
||||
# This is for setting Kubernetes Labels to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/
|
||||
service:
|
||||
# This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
|
||||
type: ClusterIP
|
||||
# This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
|
||||
# port: 80
|
||||
ports:
|
||||
admin: 11110
|
||||
tcp: 11111
|
||||
metric: 11112
|
||||
|
||||
# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: chart-example.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: metric
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: metric
|
||||
|
||||
# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# Additional volumes on the output Deployment definition.
|
||||
volumes: []
|
||||
# - name: foo
|
||||
# secret:
|
||||
# secretName: mysecret
|
||||
# optional: false
|
||||
|
||||
# Additional volumeMounts on the output Deployment definition.
|
||||
volumeMounts: []
|
||||
# - name: foo
|
||||
# mountPath: "/etc/foo"
|
||||
# readOnly: true
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
server:
|
||||
config: |
|
||||
canal.port = 11111
|
||||
canal.metrics.pull.port = 11112
|
||||
|
||||
# register ip
|
||||
canal.register.ip =
|
||||
|
||||
# canal admin config
|
||||
canal.admin.manager = canal-admin.default:8089
|
||||
canal.admin.port = 11110
|
||||
canal.admin.user = admin
|
||||
canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
|
||||
# admin auto register
|
||||
canal.admin.register.auto = true
|
||||
canal.admin.register.cluster = default
|
||||
BIN
charts/images/cluster.png
Normal file
BIN
charts/images/cluster.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
BIN
charts/images/server.png
Normal file
BIN
charts/images/server.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
34
charts/tests/admin-values.yaml
Normal file
34
charts/tests/admin-values.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
ingress:
|
||||
enabled: true
|
||||
hosts:
|
||||
- host: admin.canal.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
|
||||
admin:
|
||||
config: |
|
||||
server:
|
||||
port: 8089
|
||||
spring:
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
time-zone: GMT+8
|
||||
spring.datasource:
|
||||
address: you_db_host:3306
|
||||
database: canal_manager
|
||||
username: ****
|
||||
password: ****
|
||||
driver-class-name: com.mysql.jdbc.Driver
|
||||
url: jdbc:mysql://${spring.datasource.address}/${spring.datasource.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true
|
||||
hikari:
|
||||
maximum-pool-size: 30
|
||||
minimum-idle: 1
|
||||
canal:
|
||||
adminUser: admin
|
||||
adminPasswd: admin
|
||||
debug: true
|
||||
|
||||
image:
|
||||
repository: registry.us-east-1.aliyuncs.com/sll0/canal-admin
|
||||
tag: "latest"
|
||||
20
charts/tests/server-values.yaml
Normal file
20
charts/tests/server-values.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
server:
|
||||
config: |
|
||||
canal.port = 11111
|
||||
canal.metrics.pull.port = 11112
|
||||
|
||||
# register ip
|
||||
canal.register.ip =
|
||||
|
||||
# canal admin config
|
||||
canal.admin.manager = canal-admin.default:8089
|
||||
canal.admin.port = 11110
|
||||
canal.admin.user = admin
|
||||
canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
|
||||
# admin auto register
|
||||
canal.admin.register.auto = true
|
||||
canal.admin.register.cluster = default
|
||||
|
||||
image:
|
||||
repository: registry.us-east-1.aliyuncs.com/sll0/canal-server
|
||||
tag: "v1.1.8-alpha-3"
|
||||
5
charts/tests/test.sh
Executable file
5
charts/tests/test.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#! /bin/bash
|
||||
|
||||
helm install canal-admin -f ./admin-values.yaml ../canal-admin --dry-run
|
||||
|
||||
helm install canal-server -f ./server-values.yaml ../canal-server --dry-run
|
||||
@@ -1,11 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -60,7 +58,9 @@
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.8</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
@@ -81,4 +81,4 @@
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -13,10 +13,7 @@ import com.alibaba.druid.pool.DruidDataSource;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.config.MappingConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.config.MappingConfig.DbMapping;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.support.SyncUtil;
|
||||
import com.alibaba.otter.canal.client.adapter.support.AbstractEtlService;
|
||||
import com.alibaba.otter.canal.client.adapter.support.AdapterConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.EtlResult;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
import com.alibaba.otter.canal.client.adapter.support.*;
|
||||
|
||||
/**
|
||||
* ClickHouse ETL 操作业务类
|
||||
@@ -42,7 +39,8 @@ public class ClickHouseEtlService extends AbstractEtlService {
|
||||
*/
|
||||
public EtlResult importData(List<String> params) {
|
||||
DbMapping dbMapping = config.getDbMapping();
|
||||
String sql = "SELECT * FROM " + dbMapping.getDatabase() + "." + dbMapping.getTable();
|
||||
DruidDataSource dataSource = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());
|
||||
String sql = "SELECT * FROM " + SyncUtil.getSourceDbTableName(dbMapping, dataSource.getDbType());
|
||||
return importData(sql, params);
|
||||
}
|
||||
|
||||
@@ -128,13 +126,13 @@ public class ClickHouseEtlService extends AbstractEtlService {
|
||||
|
||||
int i = 1;
|
||||
for (Map.Entry<String, String> entry : columnsMap.entrySet()) {
|
||||
String targetClolumnName = entry.getKey();
|
||||
String targetColumnName = entry.getKey();
|
||||
String srcColumnName = entry.getValue();
|
||||
if (srcColumnName == null) {
|
||||
srcColumnName = targetClolumnName;
|
||||
srcColumnName = targetColumnName;
|
||||
}
|
||||
|
||||
Integer type = columnType.get(targetClolumnName.toLowerCase());
|
||||
Integer type = columnType.get(targetColumnName.toLowerCase());
|
||||
Object value = rs.getObject(srcColumnName);
|
||||
if (value != null) {
|
||||
SyncUtil.setPStmt(type, pstmt, value, i);
|
||||
|
||||
@@ -268,6 +268,17 @@ public class SyncUtil {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String getSourceDbTableName(MappingConfig.DbMapping dbMapping, String dbType) {
|
||||
String result = "";
|
||||
String backtick = getBacktickByDbType(dbType);
|
||||
if (StringUtils.isNotEmpty(dbMapping.getDatabase())) {
|
||||
result += (backtick + dbMapping.getDatabase() + backtick + ".");
|
||||
}
|
||||
|
||||
result += (backtick + dbMapping.getTable() + backtick);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据DbType返回反引号或空字符串
|
||||
*
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.util.function.Function;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import com.alibaba.druid.pool.DruidDataSource;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
@@ -28,9 +29,9 @@ import com.google.common.cache.LoadingCache;
|
||||
|
||||
public class Util {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(Util.class);
|
||||
private static final Logger logger = LoggerFactory.getLogger(Util.class);
|
||||
|
||||
public static final String AUTO_GENERATED_PREFIX = "AUTO_GENERATED_";
|
||||
public static final String AUTO_GENERATED_PREFIX = "AUTO_GENERATED_";
|
||||
|
||||
/**
|
||||
* 通过DS执行sql
|
||||
@@ -38,7 +39,17 @@ public class Util {
|
||||
public static Object sqlRS(DataSource ds, String sql, Function<ResultSet, Object> fun) {
|
||||
try (Connection conn = ds.getConnection();
|
||||
Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) {
|
||||
stmt.setFetchSize(Integer.MIN_VALUE);
|
||||
|
||||
DruidDataSource druidDataSource = (DruidDataSource) ds;
|
||||
if ("postgresql".equals(druidDataSource.getDbType())) {
|
||||
conn.setAutoCommit(false);
|
||||
stmt.setFetchSize(1000);
|
||||
} else if ("sqlserver".equals(druidDataSource.getDbType())) {
|
||||
stmt.setFetchSize(100);
|
||||
} else {
|
||||
stmt.setFetchSize(Integer.MIN_VALUE);
|
||||
}
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery(sql)) {
|
||||
return fun.apply(rs);
|
||||
}
|
||||
@@ -52,7 +63,13 @@ public class Util {
|
||||
try (Connection conn = ds.getConnection()) {
|
||||
try (PreparedStatement pstmt = conn
|
||||
.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) {
|
||||
pstmt.setFetchSize(Integer.MIN_VALUE);
|
||||
DruidDataSource druidDataSource = (DruidDataSource) ds;
|
||||
if ("postgresql".equals(druidDataSource.getDbType())) {
|
||||
conn.setAutoCommit(false);
|
||||
pstmt.setFetchSize(1000);
|
||||
} else {
|
||||
pstmt.setFetchSize(Integer.MIN_VALUE);
|
||||
}
|
||||
if (values != null) {
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
pstmt.setObject(i + 1, values.get(i));
|
||||
@@ -165,39 +182,38 @@ public class Util {
|
||||
|
||||
public static ThreadPoolExecutor newFixedDaemonThreadPool(int nThreads, long keepAliveTime) {
|
||||
return new ThreadPoolExecutor(nThreads,
|
||||
nThreads,
|
||||
keepAliveTime,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new SynchronousQueue<>(),
|
||||
DaemonThreadFactory.daemonThreadFactory,
|
||||
(r, exe) -> {
|
||||
if (!exe.isShutdown()) {
|
||||
try {
|
||||
exe.getQueue().put(r);
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
nThreads,
|
||||
keepAliveTime,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new SynchronousQueue<>(),
|
||||
DaemonThreadFactory.daemonThreadFactory,
|
||||
(r, exe) -> {
|
||||
if (!exe.isShutdown()) {
|
||||
try {
|
||||
exe.getQueue().put(r);
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public static ThreadPoolExecutor newSingleDaemonThreadExecutor(long keepAliveTime) {
|
||||
return new ThreadPoolExecutor(1,
|
||||
1,
|
||||
keepAliveTime,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new SynchronousQueue<>(),
|
||||
DaemonThreadFactory.daemonThreadFactory,
|
||||
(r, exe) -> {
|
||||
if (!exe.isShutdown()) {
|
||||
try {
|
||||
exe.getQueue().put(r);
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
1,
|
||||
keepAliveTime,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new SynchronousQueue<>(),
|
||||
DaemonThreadFactory.daemonThreadFactory,
|
||||
(r, exe) -> {
|
||||
if (!exe.isShutdown()) {
|
||||
try {
|
||||
exe.getQueue().put(r);
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public final static String timeZone; // 当前时区
|
||||
@@ -327,4 +343,56 @@ public class Util {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the time string has millisecond or microsecond units
|
||||
*
|
||||
* @param timeStr time string
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isAccuracyOverSecond(String timeStr) {
|
||||
if (StringUtils.isEmpty(timeStr)) {
|
||||
return false;
|
||||
}
|
||||
String[] times = StringUtils.split(timeStr, ".");
|
||||
return times.length > 1 && !times[times.length - 1].isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the datetime string has microsecond or nanosecond units
|
||||
*
|
||||
* @param datetimeStr datetime string
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isAccuracyOverMillisecond(String datetimeStr) {
|
||||
if (StringUtils.isEmpty(datetimeStr)) {
|
||||
return false;
|
||||
}
|
||||
String[] times = StringUtils.split(datetimeStr, ".");
|
||||
return times.length > 1 && times[times.length - 1].length() > 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* MySQL has fractional seconds support for TIME, DATETIME, and TIMESTAMP
|
||||
* values, with up to microseconds (6 digits) precision. ISO-8601 standard
|
||||
* format is with up to nanoseconds (9 digits) precision, which is sufficient
|
||||
* for storing MySQL time-related data.
|
||||
*
|
||||
* @param datetimeStr datetime string
|
||||
* @return LocalDateTime
|
||||
*/
|
||||
public static LocalDateTime parseISOLocalDateTime(String datetimeStr) {
|
||||
if (StringUtils.isEmpty(datetimeStr)) {
|
||||
return null;
|
||||
}
|
||||
datetimeStr = datetimeStr.trim();
|
||||
try {
|
||||
// Replace SPACE in middle with `T` to comply with ISO-8601 standard format
|
||||
return LocalDateTime.parse(datetimeStr.replace(" ", "T"));
|
||||
} catch (Exception e) {
|
||||
logger.error("Convert datetime string to ISOLocalDateTime fail:{}", datetimeStr, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
@@ -18,10 +20,14 @@ import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.PropertyPlaceholderHelper;
|
||||
|
||||
public class YamlUtils {
|
||||
|
||||
public static <T> T resourceYmlToObj(String resource, String prefix, Class<T> clazz) {
|
||||
if (!StringUtils.startsWithIgnoreCase(resource, "classpath:")) {
|
||||
resource = "classpath:" + resource;
|
||||
}
|
||||
ClassPathResource classPathResource = new ClassPathResource(resource);
|
||||
|
||||
String content;
|
||||
@@ -85,9 +91,15 @@ public class YamlUtils {
|
||||
entry.setValue(((OriginTrackedValue) value).getValue());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ConfigurationPropertySource sources = new MapConfigurationPropertySource(properties);
|
||||
Binder binder = new Binder(sources);
|
||||
PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("${", "}");
|
||||
Binder binder = new Binder(Arrays.asList(sources), value -> {
|
||||
if (value instanceof String) {
|
||||
return propertyPlaceholderHelper.replacePlaceholders((String) value, baseProperties);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
return binder.bind(prefix, Bindable.of(clazz)).get();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -74,7 +74,9 @@
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.8</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -74,7 +74,9 @@
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.8</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
|
||||
@@ -2,11 +2,7 @@ package com.alibaba.otter.canal.client.adapter.es7x.support;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
@@ -43,13 +39,10 @@ public class ES7xTemplate implements ESTemplate {
|
||||
.getLogger(ESTemplate.class);
|
||||
|
||||
private static final int MAX_BATCH_SIZE = 1000;
|
||||
|
||||
private ESConnection esConnection;
|
||||
|
||||
private ESBulkRequest esBulkRequest;
|
||||
|
||||
// es 字段类型本地缓存
|
||||
private static ConcurrentMap<String, Map<String, String>> esFieldTypes = new ConcurrentHashMap<>();
|
||||
private ESConnection esConnection;
|
||||
private ESBulkRequest esBulkRequest;
|
||||
|
||||
public ES7xTemplate(ESConnection esConnection){
|
||||
this.esConnection = esConnection;
|
||||
@@ -69,8 +62,9 @@ public class ES7xTemplate implements ESTemplate {
|
||||
if (mapping.getId() != null) {
|
||||
String parentVal = (String) esFieldData.remove("$parent_routing");
|
||||
if (mapping.isUpsert()) {
|
||||
ESUpdateRequest updateRequest = esConnection.new ES7xUpdateRequest(mapping.getIndex(),
|
||||
pkVal.toString()).setDoc(esFieldData).setDocAsUpsert(true);
|
||||
ESUpdateRequest updateRequest = esConnection.new ES7xUpdateRequest(mapping.getIndex(), pkVal.toString())
|
||||
.setDoc(esFieldData)
|
||||
.setDocAsUpsert(true);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
updateRequest.setRouting(parentVal);
|
||||
}
|
||||
@@ -287,7 +281,8 @@ public class ES7xTemplate implements ESTemplate {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getESDataFromDmlData(ESMapping mapping, Map<String, Object> dmlData, Map<String, Object> esFieldData) {
|
||||
public Object getESDataFromDmlData(ESMapping mapping, Map<String, Object> dmlData,
|
||||
Map<String, Object> esFieldData) {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
Object resultIdVal = null;
|
||||
@@ -311,25 +306,29 @@ public class ES7xTemplate implements ESTemplate {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getESDataFromDmlData(ESMapping mapping,String owner, Map<String, Object> dmlData, Map<String, Object> dmlOld,
|
||||
Map<String, Object> esFieldData) {
|
||||
public Object getESDataFromDmlData(ESMapping mapping, String owner, Map<String, Object> dmlData,
|
||||
Map<String, Object> dmlOld, Map<String, Object> esFieldData) {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
ColumnItem columnItem = fieldItem.getColumnItems().iterator().next();
|
||||
if (columnItem.getOwner() == null || columnItem.getColumnName() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!columnItem.getOwner().equals(owner)) {
|
||||
continue;
|
||||
}
|
||||
String columnName = columnItem.getColumnName();
|
||||
|
||||
String columnName = columnItem.getColumnName();
|
||||
if (fieldItem.getFieldName().equals(idFieldName)) {
|
||||
resultIdVal = getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName);
|
||||
}
|
||||
|
||||
if (dmlOld.containsKey(columnName) && !mapping.getSkips().contains(fieldItem.getFieldName())) {
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()),
|
||||
getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName));
|
||||
getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,16 +55,9 @@ import com.alibaba.otter.canal.client.adapter.es.core.support.ESBulkRequest;
|
||||
public class ESConnection {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ESConnection.class);
|
||||
|
||||
public enum ESClientMode {
|
||||
TRANSPORT, REST
|
||||
}
|
||||
|
||||
private ESClientMode mode;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private TransportClient transportClient;
|
||||
|
||||
private RestHighLevelClient restHighLevelClient;
|
||||
|
||||
public ESConnection(String[] hosts, Map<String, String> properties, ESClientMode mode) throws UnknownHostException{
|
||||
@@ -86,9 +79,10 @@ public class ESConnection {
|
||||
if (StringUtils.isNotEmpty(nameAndPwd) && nameAndPwd.contains(":")) {
|
||||
String[] nameAndPwdArr = nameAndPwd.split(":");
|
||||
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
|
||||
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(nameAndPwdArr[0],
|
||||
nameAndPwdArr[1]));
|
||||
restClientBuilder.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider));
|
||||
credentialsProvider.setCredentials(AuthScope.ANY,
|
||||
new UsernamePasswordCredentials(nameAndPwdArr[0], nameAndPwdArr[1]));
|
||||
restClientBuilder.setHttpClientConfigCallback(
|
||||
httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider));
|
||||
}
|
||||
restHighLevelClient = new RestHighLevelClient(restClientBuilder);
|
||||
}
|
||||
@@ -138,11 +132,94 @@ public class ESConnection {
|
||||
logger.error(e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 通过别名查询mapping返回的是真实索引名称,mappings.get(index)返回null,为兼容别名情况修改如下:
|
||||
mappingMetaData = mappings.get(index);
|
||||
if (mappingMetaData == null && !mappings.isEmpty()) {
|
||||
// 如果通过索引名找不到,可能是别名,取第一个映射
|
||||
mappingMetaData = mappings.values().iterator().next();
|
||||
}
|
||||
|
||||
// 如果还是null,说明索引不存在或没有mapping
|
||||
if (mappingMetaData == null) {
|
||||
throw new IllegalArgumentException("Not found the mapping info of index: " + index);
|
||||
}
|
||||
}
|
||||
return mappingMetaData;
|
||||
}
|
||||
|
||||
// ------ get/set ------
|
||||
public ESClientMode getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public void setMode(ESClientMode mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public TransportClient getTransportClient() {
|
||||
return transportClient;
|
||||
}
|
||||
|
||||
public void setTransportClient(TransportClient transportClient) {
|
||||
this.transportClient = transportClient;
|
||||
}
|
||||
|
||||
public RestHighLevelClient getRestHighLevelClient() {
|
||||
return restHighLevelClient;
|
||||
}
|
||||
|
||||
public void setRestHighLevelClient(RestHighLevelClient restHighLevelClient) {
|
||||
this.restHighLevelClient = restHighLevelClient;
|
||||
}
|
||||
|
||||
private HttpHost createHttpHost(String uriStr) {
|
||||
URI uri = URI.create(uriStr);
|
||||
if (!org.springframework.util.StringUtils.hasLength(uri.getUserInfo())) {
|
||||
return HttpHost.create(uri.toString());
|
||||
}
|
||||
try {
|
||||
return HttpHost.create(new URI(uri
|
||||
.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment())
|
||||
.toString());
|
||||
} catch (URISyntaxException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public enum ESClientMode {
|
||||
TRANSPORT, REST
|
||||
}
|
||||
|
||||
public static class ES7xBulkResponse implements ESBulkRequest.ESBulkResponse {
|
||||
|
||||
private BulkResponse bulkResponse;
|
||||
|
||||
public ES7xBulkResponse(BulkResponse bulkResponse){
|
||||
this.bulkResponse = bulkResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFailures() {
|
||||
return bulkResponse.hasFailures();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processFailBulkResponse(String errorMsg) {
|
||||
for (BulkItemResponse itemResponse : bulkResponse.getItems()) {
|
||||
if (!itemResponse.isFailed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (itemResponse.getFailure().getStatus() == RestStatus.NOT_FOUND) {
|
||||
logger.error(itemResponse.getFailureMessage());
|
||||
} else {
|
||||
throw new RuntimeException(errorMsg + itemResponse.getFailureMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ES7xIndexRequest implements ESBulkRequest.ESIndexRequest {
|
||||
|
||||
private IndexRequestBuilder indexRequestBuilder;
|
||||
@@ -443,71 +520,4 @@ public class ESConnection {
|
||||
this.bulkRequest = bulkRequest;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ES7xBulkResponse implements ESBulkRequest.ESBulkResponse {
|
||||
|
||||
private BulkResponse bulkResponse;
|
||||
|
||||
public ES7xBulkResponse(BulkResponse bulkResponse){
|
||||
this.bulkResponse = bulkResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFailures() {
|
||||
return bulkResponse.hasFailures();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processFailBulkResponse(String errorMsg) {
|
||||
for (BulkItemResponse itemResponse : bulkResponse.getItems()) {
|
||||
if (!itemResponse.isFailed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (itemResponse.getFailure().getStatus() == RestStatus.NOT_FOUND) {
|
||||
logger.error(itemResponse.getFailureMessage());
|
||||
} else {
|
||||
throw new RuntimeException(errorMsg + itemResponse.getFailureMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------ get/set ------
|
||||
public ESClientMode getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public void setMode(ESClientMode mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public TransportClient getTransportClient() {
|
||||
return transportClient;
|
||||
}
|
||||
|
||||
public void setTransportClient(TransportClient transportClient) {
|
||||
this.transportClient = transportClient;
|
||||
}
|
||||
|
||||
public RestHighLevelClient getRestHighLevelClient() {
|
||||
return restHighLevelClient;
|
||||
}
|
||||
|
||||
public void setRestHighLevelClient(RestHighLevelClient restHighLevelClient) {
|
||||
this.restHighLevelClient = restHighLevelClient;
|
||||
}
|
||||
|
||||
private HttpHost createHttpHost(String uriStr) {
|
||||
URI uri = URI.create(uriStr);
|
||||
if (!org.springframework.util.StringUtils.hasLength(uri.getUserInfo())) {
|
||||
return HttpHost.create(uri.toString());
|
||||
}
|
||||
try {
|
||||
return HttpHost.create(new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(),
|
||||
uri.getQuery(), uri.getFragment()).toString());
|
||||
} catch (URISyntaxException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -69,7 +69,9 @@
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.8</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es8x;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.elasticsearch.action.search.SearchResponse;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.ESAdapter;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.config.ESSyncConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.es8x.etl.ESEtlService;
|
||||
@@ -9,13 +18,6 @@ import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.EtlResult;
|
||||
import com.alibaba.otter.canal.client.adapter.support.OuterAdapterConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.SPI;
|
||||
import org.elasticsearch.action.search.SearchResponse;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* ES 8.x 外部适配器
|
||||
|
||||
@@ -39,13 +39,10 @@ public class ES8xTemplate implements ESTemplate {
|
||||
.getLogger(ESTemplate.class);
|
||||
|
||||
private static final int MAX_BATCH_SIZE = 1000;
|
||||
|
||||
private ESConnection esConnection;
|
||||
|
||||
private ESBulkRequest esBulkRequest;
|
||||
|
||||
// es 字段类型本地缓存
|
||||
private static ConcurrentMap<String, Map<String, String>> esFieldTypes = new ConcurrentHashMap<>();
|
||||
private ESConnection esConnection;
|
||||
private ESBulkRequest esBulkRequest;
|
||||
|
||||
public ES8xTemplate(ESConnection esConnection){
|
||||
this.esConnection = esConnection;
|
||||
@@ -65,8 +62,9 @@ public class ES8xTemplate implements ESTemplate {
|
||||
if (mapping.getId() != null) {
|
||||
String parentVal = (String) esFieldData.remove("$parent_routing");
|
||||
if (mapping.isUpsert()) {
|
||||
ESUpdateRequest updateRequest = esConnection.new ES8xUpdateRequest(mapping.getIndex(),
|
||||
pkVal.toString()).setDoc(esFieldData).setDocAsUpsert(true);
|
||||
ESUpdateRequest updateRequest = esConnection.new ES8xUpdateRequest(mapping.getIndex(), pkVal.toString())
|
||||
.setDoc(esFieldData)
|
||||
.setDocAsUpsert(true);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
updateRequest.setRouting(parentVal);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es8x.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyStore;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.auth.AuthScope;
|
||||
import org.apache.http.auth.UsernamePasswordCredentials;
|
||||
import org.apache.http.client.CredentialsProvider;
|
||||
import org.apache.http.impl.client.BasicCredentialsProvider;
|
||||
import org.apache.http.ssl.SSLContextBuilder;
|
||||
import org.apache.http.ssl.SSLContexts;
|
||||
import org.elasticsearch.action.bulk.BulkItemResponse;
|
||||
import org.elasticsearch.action.bulk.BulkRequest;
|
||||
import org.elasticsearch.action.bulk.BulkRequestBuilder;
|
||||
@@ -51,6 +62,47 @@ public class ESConnection {
|
||||
private RestHighLevelClient restHighLevelClient;
|
||||
|
||||
public ESConnection(String[] hosts, Map<String, String> properties) throws UnknownHostException{
|
||||
String caPath = properties.get("security.ca.path");
|
||||
if (StringUtils.isNotEmpty(caPath)) {
|
||||
connectEsWithCa(hosts, properties, caPath);
|
||||
} else {
|
||||
connectEsWithoutCa(hosts, properties);
|
||||
}
|
||||
}
|
||||
|
||||
private void connectEsWithCa(String[] hosts, Map<String, String> properties, String caPath) {
|
||||
Path caCertificatePath = Paths.get(caPath);
|
||||
try (InputStream is = Files.newInputStream(caCertificatePath)) {
|
||||
CertificateFactory factory = CertificateFactory.getInstance("X.509");
|
||||
Certificate trustedCa = factory.generateCertificate(is);
|
||||
KeyStore trustStore = KeyStore.getInstance("pkcs12");
|
||||
trustStore.load(null, null);
|
||||
trustStore.setCertificateEntry("ca", trustedCa);
|
||||
SSLContextBuilder sslContextBuilder = SSLContexts.custom().loadTrustMaterial(trustStore, null);
|
||||
final SSLContext sslContext = sslContextBuilder.build();
|
||||
|
||||
HttpHost[] httpHosts = Arrays.stream(hosts).map(this::createHttpHost).toArray(HttpHost[]::new);
|
||||
RestClientBuilder restClientBuilder = RestClient.builder(httpHosts);
|
||||
String nameAndPwd = properties.get("security.auth");
|
||||
if (StringUtils.isNotEmpty(nameAndPwd) && nameAndPwd.contains(":")) {
|
||||
String[] nameAndPwdArr = nameAndPwd.split(":");
|
||||
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
|
||||
credentialsProvider.setCredentials(AuthScope.ANY,
|
||||
new UsernamePasswordCredentials(nameAndPwdArr[0], nameAndPwdArr[1]));
|
||||
restClientBuilder.setHttpClientConfigCallback(httpClientBuilder -> {
|
||||
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
|
||||
return httpClientBuilder.setSSLContext(sslContext);
|
||||
});
|
||||
}
|
||||
restHighLevelClient = new RestHighLevelClientBuilder(restClientBuilder.build())
|
||||
.setApiCompatibilityMode(true)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void connectEsWithoutCa(String[] hosts, Map<String, String> properties) {
|
||||
HttpHost[] httpHosts = Arrays.stream(hosts).map(this::createHttpHost).toArray(HttpHost[]::new);
|
||||
RestClientBuilder restClientBuilder = RestClient.builder(httpHosts);
|
||||
String nameAndPwd = properties.get("security.auth");
|
||||
@@ -90,11 +142,73 @@ public class ESConnection {
|
||||
logger.error(e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 通过别名查询mapping返回的是真实索引名称,mappings.get(index)返回null,为兼容别名情况修改如下:
|
||||
mappingMetaData = mappings.get(index);
|
||||
if (mappingMetaData == null && !mappings.isEmpty()) {
|
||||
// 如果通过索引名找不到,可能是别名,取第一个映射
|
||||
mappingMetaData = mappings.values().iterator().next();
|
||||
}
|
||||
|
||||
// 如果还是null,说明索引不存在或没有mapping
|
||||
if (mappingMetaData == null) {
|
||||
throw new IllegalArgumentException("Not found the mapping info of index: " + index);
|
||||
}
|
||||
|
||||
return mappingMetaData;
|
||||
}
|
||||
|
||||
public RestHighLevelClient getRestHighLevelClient() {
|
||||
return restHighLevelClient;
|
||||
}
|
||||
|
||||
public void setRestHighLevelClient(RestHighLevelClient restHighLevelClient) {
|
||||
this.restHighLevelClient = restHighLevelClient;
|
||||
}
|
||||
|
||||
private HttpHost createHttpHost(String uriStr) {
|
||||
URI uri = URI.create(uriStr);
|
||||
if (!org.springframework.util.StringUtils.hasLength(uri.getUserInfo())) {
|
||||
return HttpHost.create(uri.toString());
|
||||
}
|
||||
try {
|
||||
return HttpHost.create(new URI(uri
|
||||
.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment())
|
||||
.toString());
|
||||
} catch (URISyntaxException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ES8xBulkResponse implements ESBulkRequest.ESBulkResponse {
|
||||
|
||||
private BulkResponse bulkResponse;
|
||||
|
||||
public ES8xBulkResponse(BulkResponse bulkResponse){
|
||||
this.bulkResponse = bulkResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFailures() {
|
||||
return bulkResponse.hasFailures();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processFailBulkResponse(String errorMsg) {
|
||||
for (BulkItemResponse itemResponse : bulkResponse.getItems()) {
|
||||
if (!itemResponse.isFailed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (itemResponse.getFailure().getStatus() == RestStatus.NOT_FOUND) {
|
||||
logger.error(itemResponse.getFailureMessage());
|
||||
} else {
|
||||
throw new RuntimeException(errorMsg + itemResponse.getFailureMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ES8xIndexRequest implements ESBulkRequest.ESIndexRequest {
|
||||
|
||||
private IndexRequestBuilder indexRequestBuilder;
|
||||
@@ -280,9 +394,7 @@ public class ESConnection {
|
||||
private BulkRequest bulkRequest;
|
||||
|
||||
public ES8xBulkRequest(){
|
||||
|
||||
bulkRequest = new BulkRequest();
|
||||
|
||||
}
|
||||
|
||||
public void resetBulk() {
|
||||
@@ -345,55 +457,4 @@ public class ESConnection {
|
||||
this.bulkRequest = bulkRequest;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ES8xBulkResponse implements ESBulkRequest.ESBulkResponse {
|
||||
|
||||
private BulkResponse bulkResponse;
|
||||
|
||||
public ES8xBulkResponse(BulkResponse bulkResponse){
|
||||
this.bulkResponse = bulkResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFailures() {
|
||||
return bulkResponse.hasFailures();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processFailBulkResponse(String errorMsg) {
|
||||
for (BulkItemResponse itemResponse : bulkResponse.getItems()) {
|
||||
if (!itemResponse.isFailed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (itemResponse.getFailure().getStatus() == RestStatus.NOT_FOUND) {
|
||||
logger.error(itemResponse.getFailureMessage());
|
||||
} else {
|
||||
throw new RuntimeException(errorMsg + itemResponse.getFailureMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public RestHighLevelClient getRestHighLevelClient() {
|
||||
return restHighLevelClient;
|
||||
}
|
||||
|
||||
public void setRestHighLevelClient(RestHighLevelClient restHighLevelClient) {
|
||||
this.restHighLevelClient = restHighLevelClient;
|
||||
}
|
||||
|
||||
private HttpHost createHttpHost(String uriStr) {
|
||||
URI uri = URI.create(uriStr);
|
||||
if (!org.springframework.util.StringUtils.hasLength(uri.getUserInfo())) {
|
||||
return HttpHost.create(uri.toString());
|
||||
}
|
||||
try {
|
||||
return HttpHost.create(new URI(uri
|
||||
.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment())
|
||||
.toString());
|
||||
} catch (URISyntaxException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.alibaba.otter.canal.client.adapter.es.core.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Blob;
|
||||
import java.sql.SQLException;
|
||||
@@ -101,13 +102,22 @@ public class ESSyncUtil {
|
||||
break;
|
||||
case "float":
|
||||
case "half_float":
|
||||
case "scaled_float":
|
||||
if (val instanceof Number) {
|
||||
res = ((Number) val).floatValue();
|
||||
} else {
|
||||
res = Float.parseFloat(val.toString());
|
||||
}
|
||||
break;
|
||||
case "scaled_float":
|
||||
// scaled_float 需要保持高精度,使用 BigDecimal 进行处理
|
||||
if (val instanceof BigDecimal) {
|
||||
res = val; // 保持 BigDecimal 精度
|
||||
} else if (val instanceof Number) {
|
||||
res = new BigDecimal(val.toString());
|
||||
} else {
|
||||
res = new BigDecimal(val.toString());
|
||||
}
|
||||
break;
|
||||
case "boolean":
|
||||
if (val instanceof Boolean) {
|
||||
res = val;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es.core.support;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* ESSyncUtil 测试类
|
||||
*
|
||||
* @author jianghang
|
||||
*/
|
||||
public class ESSyncUtilTest {
|
||||
|
||||
@Test
|
||||
public void testScaledFloatPrecision() {
|
||||
// 测试高精度 decimal 转换为 scaled_float
|
||||
BigDecimal originalValue = new BigDecimal("1234567890.12345678901234567890");
|
||||
|
||||
// 转换为 scaled_float
|
||||
Object result = ESSyncUtil.typeConvert(originalValue, "scaled_float");
|
||||
|
||||
// 验证结果仍然是 BigDecimal,保持精度
|
||||
Assert.assertTrue("scaled_float 转换结果应该是 BigDecimal 类型", result instanceof BigDecimal);
|
||||
Assert.assertEquals("scaled_float 转换应该保持原始精度", originalValue, result);
|
||||
|
||||
// 验证 toString 表示
|
||||
Assert.assertEquals("scaled_float 精度应该保持完整", "1234567890.12345678901234567890", result.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScaledFloatFromString() {
|
||||
// 测试字符串转换为 scaled_float
|
||||
String originalString = "9876543210.98765432109876543210";
|
||||
|
||||
// 转换为 scaled_float
|
||||
Object result = ESSyncUtil.typeConvert(originalString, "scaled_float");
|
||||
|
||||
// 验证结果是 BigDecimal
|
||||
Assert.assertTrue("scaled_float 字符串转换结果应该是 BigDecimal 类型", result instanceof BigDecimal);
|
||||
|
||||
// 验证值正确
|
||||
BigDecimal expected = new BigDecimal(originalString);
|
||||
Assert.assertEquals("scaled_float 字符串转换应该正确解析", expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScaledFloatFromDouble() {
|
||||
// 测试 double 转换为 scaled_float(注意:double 本身有精度限制)
|
||||
Double originalDouble = 1234567890.1234567890;
|
||||
|
||||
// 转换为 scaled_float
|
||||
Object result = ESSyncUtil.typeConvert(originalDouble, "scaled_float");
|
||||
|
||||
// 验证结果是 BigDecimal
|
||||
Assert.assertTrue("scaled_float double 转换结果应该是 BigDecimal 类型", result instanceof BigDecimal);
|
||||
|
||||
// 验证值正确(由于 double 精度限制,这里验证转换过程正确)
|
||||
BigDecimal expected = new BigDecimal(originalDouble.toString());
|
||||
Assert.assertEquals("scaled_float double 转换应该正确处理", expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOtherFloatTypes() {
|
||||
// 测试其他浮点类型仍然正常工作
|
||||
BigDecimal value = new BigDecimal("123.45");
|
||||
|
||||
// 测试 float 类型
|
||||
Object floatResult = ESSyncUtil.typeConvert(value, "float");
|
||||
Assert.assertTrue("float 转换结果应该是 Float 类型", floatResult instanceof Float);
|
||||
|
||||
// 测试 half_float 类型
|
||||
Object halfFloatResult = ESSyncUtil.typeConvert(value, "half_float");
|
||||
Assert.assertTrue("half_float 转换结果应该是 Float 类型", halfFloatResult instanceof Float);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -60,7 +60,9 @@
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.8</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -57,7 +57,9 @@
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.8</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
|
||||
@@ -2,11 +2,7 @@ package com.alibaba.otter.canal.client.adapter.kudu.service;
|
||||
|
||||
import java.sql.ResultSetMetaData;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
@@ -45,13 +41,13 @@ public class KuduEtlService extends AbstractEtlService {
|
||||
boolean flag = kuduTemplate.tableExists(kuduMapping.getTargetTable());
|
||||
// 表不存在,停止导入
|
||||
if (!flag) {
|
||||
logger.info("{} is don't hava,please check your kudu table !", kuduMapping.getTargetTable());
|
||||
logger.info("{} is don't exist , please check your kudu table !", kuduMapping.getTargetTable());
|
||||
errMsg.add(kuduMapping.getTargetTable() + " is don't hava,please check your kudu table !");
|
||||
etlResult.setErrorMessage(Joiner.on("\n").join(errMsg));
|
||||
return etlResult;
|
||||
}
|
||||
logger.info("{} etl is starting!", kuduMapping.getTargetTable());
|
||||
String sql = "SELECT * FROM " + kuduMapping.getDatabase() + "." + kuduMapping.getTable();
|
||||
String sql = "SELECT * FROM " + SyncUtil.getDbTableName(kuduMapping);
|
||||
return importData(sql, params);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -17,7 +17,7 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>2.5.4</version>
|
||||
<version>2.5.15</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -68,7 +68,6 @@
|
||||
<groupId>com.microsoft.sqlserver</groupId>
|
||||
<artifactId>mssql-jdbc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.hbase</groupId>
|
||||
<artifactId>hbase-shaded-client</artifactId>
|
||||
@@ -79,6 +78,18 @@
|
||||
<artifactId>clickhouse-jdbc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- prometheus hotspot exporter -->
|
||||
<dependency>
|
||||
<groupId>io.prometheus</groupId>
|
||||
<artifactId>simpleclient_hotspot</artifactId>
|
||||
<version>0.4.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.prometheus</groupId>
|
||||
<artifactId>simpleclient_httpserver</artifactId>
|
||||
<version>0.4.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- outer adapter jar with dependencies-->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
args=$@
|
||||
|
||||
case $(uname) in
|
||||
Linux)
|
||||
bin_abs_path=$(readlink -f $(dirname $0))
|
||||
@@ -9,5 +11,5 @@ Linux)
|
||||
;;
|
||||
esac
|
||||
|
||||
sh "$bin_abs_path"/stop.sh
|
||||
sh "$bin_abs_path"/startup.sh
|
||||
sh "$bin_abs_path"/stop.sh $args
|
||||
sh "$bin_abs_path"/startup.sh $args
|
||||
|
||||
@@ -11,7 +11,7 @@ set CLASSPATH=%conf_dir%
|
||||
set CLASSPATH=%conf_dir%\..\lib\*;%CLASSPATH%
|
||||
|
||||
set JAVA_MEM_OPTS= -Xms128m -Xmx512m -XX:PermSize=128m
|
||||
set JAVA_OPTS_EXT= -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -Dapplication.codeset=UTF-8 -Dfile.encoding=UTF-8
|
||||
set JAVA_OPTS_EXT= -Djava.awt.headless=true -Djava.net.preferIPv4Stack=false -Dapplication.codeset=UTF-8 -Dfile.encoding=UTF-8
|
||||
set ADAPTER_OPTS= -DappName=canal-adapter
|
||||
|
||||
set JAVA_OPTS= %JAVA_MEM_OPTS% %JAVA_OPTS_EXT% %ADAPTER_OPTS%
|
||||
@@ -19,4 +19,4 @@ set JAVA_OPTS= %JAVA_MEM_OPTS% %JAVA_OPTS_EXT% %ADAPTER_OPTS%
|
||||
set CMD_STR= java %JAVA_OPTS% -classpath "%CLASSPATH%" com.alibaba.otter.canal.adapter.launcher.CanalAdapterApplication
|
||||
echo start cmd : %CMD_STR%
|
||||
|
||||
java %JAVA_OPTS% -classpath "%CLASSPATH%" com.alibaba.otter.canal.adapter.launcher.CanalAdapterApplication
|
||||
java %JAVA_OPTS% -classpath "%CLASSPATH%" com.alibaba.otter.canal.adapter.launcher.CanalAdapterApplication
|
||||
|
||||
@@ -9,7 +9,10 @@ case "`uname`" in
|
||||
bin_abs_path=`cd $(dirname $0); pwd`
|
||||
;;
|
||||
esac
|
||||
|
||||
base=${bin_abs_path}/..
|
||||
jaas_conf=$base/conf/jaas.conf
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export BASE=$base
|
||||
|
||||
@@ -56,31 +59,44 @@ in
|
||||
exit;;
|
||||
esac
|
||||
|
||||
JavaVersion=`$JAVA -version 2>&1 |awk 'NR==1{ gsub(/"/,""); print $3 }' | awk -F '.' '{print $1}'`
|
||||
str=`file -L $JAVA | grep 64-bit`
|
||||
JAVA_VERSION=`$JAVA -version 2>&1 |awk 'NR==1{ gsub(/"/,""); print $3 }' | awk -F '.' '{print $1}'`
|
||||
|
||||
JAVA_OPTS="$JAVA_OPTS -Xss1m -XX:+AggressiveOpts -XX:-UseBiasedLocking -XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$base/logs"
|
||||
if [ $JavaVersion -ge 11 ] ; then
|
||||
JAVA_OPTS="$JAVA_OPTS -Xss1m -XX:+AggressiveOpts -XX:-UseBiasedLocking -XX:-OmitStackTraceInFastThrow"
|
||||
JAVA_OPTS="$JAVA_OPTS -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$base/logs"
|
||||
|
||||
if [ $JAVA_VERSION -ge 11 ] ; then
|
||||
#JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:$base_log/gc.log:time "
|
||||
JAVA_OPTS="$JAVA_OPTS"
|
||||
JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:file=$base/logs/adapter/gc.log::filecount=5,filesize=32M"
|
||||
else
|
||||
#JAVA_OPTS="$JAVA_OPTS -Xloggc:$base/logs/canal/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime"
|
||||
JAVA_OPTS="$JAVA_OPTS -XX:+UseFastAccessorMethods -XX:+PrintAdaptiveSizePolicy -XX:+PrintTenuringDistribution"
|
||||
JAVA_OPTS="$JAVA_OPTS -Xloggc:$base/logs/canal/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=32M"
|
||||
JAVA_OPTS="$JAVA_OPTS -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:-PrintHeapAtGC -XX:+PrintGCApplicationStoppedTime"
|
||||
JAVA_OPTS="$JAVA_OPTS -XX:+UseFastAccessorMethods -XX:-PrintReferenceGC -XX:+PrintAdaptiveSizePolicy -XX:+PrintTenuringDistribution"
|
||||
JAVA_OPTS="$JAVA_OPTS -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 -XX:+SafepointTimeout -XX:SafepointTimeoutDelay=2000"
|
||||
JAVA_OPTS="$JAVA_OPTS -XX:+UseCountedLoopSafepoints -XX:+DisableExplicitGC -XX:+ExplicitGCInvokesConcurrent -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses"
|
||||
fi
|
||||
|
||||
if [ -n "$str" ]; then
|
||||
if [ $JavaVersion -ge 11 ] ; then
|
||||
OS_ARCH=`file -L $JAVA | grep 64-bit`
|
||||
if [ -n "$OS_ARCH" ]; then
|
||||
if [ $JAVA_VERSION -ge 11 ] ; then
|
||||
# For G1
|
||||
JAVA_OPTS="-server -Xms2g -Xmx3g -XX:+UseG1GC -XX:MaxGCPauseMillis=250 -XX:+UseGCOverheadLimit -XX:+ExplicitGCInvokesConcurrent $JAVA_OPTS"
|
||||
else
|
||||
JAVA_OPTS="-server -Xms2g -Xmx3g -Xmn1g -XX:SurvivorRatio=2 -XX:PermSize=96m -XX:MaxPermSize=256m -XX:MaxTenuringThreshold=15 -XX:+DisableExplicitGC $JAVA_OPTS"
|
||||
## JDK 1.8 2c4g [-XX:ConcGCThreads=1 -XX:ParallelGCThreads=2], 4c8g [-XX:ConcGCThreads=2 -XX:ParallelGCThreads=4]
|
||||
#JAVA_OPTS="$JAVA_OPTS -XX:InitialRAMPercentage=45.0 -XX:MinRAMPercentage=45.0 -XX:MaxRAMPercentage=45.0"
|
||||
#JAVA_OPTS="$JAVA_OPTS -XX:-UseParallelGC -XX:+UseConcMarkSweepGC -XX:ConcGCThreads=2 -XX:ParallelGCThreads=4"
|
||||
#JAVA_OPTS="$JAVA_OPTS -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70"
|
||||
#JAVA_OPTS="-server -XX:NewRatio=1 -XX:SurvivorRatio=2 -XX:MetaspaceSize=96m -XX:MaxMetaspaceSize=128m "
|
||||
fi
|
||||
else
|
||||
JAVA_OPTS="-server -Xms1024m -Xmx1024m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:MaxPermSize=128m $JAVA_OPTS"
|
||||
fi
|
||||
|
||||
JAVA_OPTS=" $JAVA_OPTS -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -Dfile.encoding=UTF-8"
|
||||
JAVA_OPTS=" $JAVA_OPTS -Djava.awt.headless=true -Djava.net.preferIPv4Stack=false -Dfile.encoding=UTF-8"
|
||||
ADAPTER_OPTS="-DappName=canal-adapter"
|
||||
if [ -f "$jaas_conf" ]; then
|
||||
ADAPTER_OPTS="$ADAPTER_OPTS -Djava.security.auth.login.config=$jaas_conf"
|
||||
fi
|
||||
|
||||
for i in $base/lib/*;
|
||||
do CLASSPATH=$i:"$CLASSPATH";
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
*/
|
||||
@SpringBootApplication(exclude= { DataSourceAutoConfiguration.class})
|
||||
public class CanalAdapterApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 支持rocketmq client 配置日志路径
|
||||
System.setProperty("rocketmq.client.logUseSlf4j","true");
|
||||
|
||||
@@ -8,6 +8,8 @@ import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import com.alibaba.otter.canal.protocol.exception.CanalClientException;
|
||||
import org.apache.commons.lang.exception.ExceptionUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -32,27 +34,27 @@ import com.alibaba.otter.canal.connector.core.spi.ProxyCanalMsgConsumer;
|
||||
*/
|
||||
public class AdapterProcessor {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AdapterProcessor.class);
|
||||
private static final Logger logger = LoggerFactory.getLogger(AdapterProcessor.class);
|
||||
|
||||
private static final String CONNECTOR_SPI_DIR = "/plugin";
|
||||
private static final String CONNECTOR_STANDBY_SPI_DIR = "/canal-adapter/plugin";
|
||||
private static final String CONNECTOR_SPI_DIR = "/plugin";
|
||||
private static final String CONNECTOR_STANDBY_SPI_DIR = "/canal-adapter/plugin";
|
||||
|
||||
private CanalMsgConsumer canalMsgConsumer;
|
||||
private CanalMsgConsumer canalMsgConsumer;
|
||||
|
||||
private String canalDestination; // canal实例
|
||||
private String groupId = null; // groupId
|
||||
private List<List<OuterAdapter>> canalOuterAdapters; // 外部适配器
|
||||
private CanalClientConfig canalClientConfig; // 配置
|
||||
private ExecutorService groupInnerExecutorService; // 组内工作线程池
|
||||
private volatile boolean running = false; // 是否运行中
|
||||
private Thread thread = null;
|
||||
private Thread.UncaughtExceptionHandler handler = (t, e) -> logger
|
||||
private String canalDestination; // canal实例
|
||||
private String groupId = null; // groupId
|
||||
private List<List<OuterAdapter>> canalOuterAdapters; // 外部适配器
|
||||
private CanalClientConfig canalClientConfig; // 配置
|
||||
private ExecutorService groupInnerExecutorService; // 组内工作线程池
|
||||
private volatile boolean running = false; // 是否运行中
|
||||
private Thread thread = null;
|
||||
private Thread.UncaughtExceptionHandler handler = (t, e) -> logger
|
||||
.error("parse events has an error", e);
|
||||
|
||||
private SyncSwitch syncSwitch;
|
||||
private SyncSwitch syncSwitch;
|
||||
|
||||
public AdapterProcessor(CanalClientConfig canalClientConfig, String destination, String groupId,
|
||||
List<List<OuterAdapter>> canalOuterAdapters) {
|
||||
List<List<OuterAdapter>> canalOuterAdapters){
|
||||
this.canalClientConfig = canalClientConfig;
|
||||
this.canalDestination = destination;
|
||||
this.groupId = groupId;
|
||||
@@ -63,9 +65,12 @@ public class AdapterProcessor {
|
||||
|
||||
// load connector consumer
|
||||
ExtensionLoader<CanalMsgConsumer> loader = new ExtensionLoader<>(CanalMsgConsumer.class);
|
||||
canalMsgConsumer = new ProxyCanalMsgConsumer(loader
|
||||
.getExtension(canalClientConfig.getMode().toLowerCase(), destination, CONNECTOR_SPI_DIR,
|
||||
CONNECTOR_STANDBY_SPI_DIR));
|
||||
// see https://github.com/alibaba/canal/pull/5175
|
||||
String key = destination + "_" + groupId;
|
||||
canalMsgConsumer = new ProxyCanalMsgConsumer(loader.getExtension(canalClientConfig.getMode().toLowerCase(),
|
||||
key,
|
||||
CONNECTOR_SPI_DIR,
|
||||
CONNECTOR_STANDBY_SPI_DIR));
|
||||
|
||||
Properties properties = canalClientConfig.getConsumerProperties();
|
||||
properties.put(CanalConstants.CANAL_MQ_FLAT_MESSAGE, canalClientConfig.getFlatMessage());
|
||||
@@ -170,7 +175,7 @@ public class AdapterProcessor {
|
||||
}
|
||||
|
||||
int retry = canalClientConfig.getRetries() == null
|
||||
|| canalClientConfig.getRetries() == 0 ? 1 : canalClientConfig.getRetries();
|
||||
|| canalClientConfig.getRetries() == 0 ? 1 : canalClientConfig.getRetries();
|
||||
if (retry == -1) {
|
||||
// 重试次数-1代表异常时一直阻塞重试
|
||||
retry = Integer.MAX_VALUE;
|
||||
@@ -183,7 +188,7 @@ public class AdapterProcessor {
|
||||
logger.info("=============> Start to connect destination: {} <=============", this.canalDestination);
|
||||
canalMsgConsumer.connect();
|
||||
logger.info("=============> Subscribe destination: {} succeed <=============", this.canalDestination);
|
||||
while (running) {
|
||||
out: while (running) {
|
||||
try {
|
||||
syncSwitch.get(canalDestination, 1L, TimeUnit.MINUTES);
|
||||
} catch (TimeoutException e) {
|
||||
@@ -213,6 +218,17 @@ public class AdapterProcessor {
|
||||
}
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
Throwable th = e.getCause();
|
||||
// Handle source error when getting message
|
||||
if (th instanceof CanalClientException) {
|
||||
String message = ExceptionUtils.getRootCauseMessage(th);
|
||||
if (message.contains("end of stream when reading header")
|
||||
|| message.contains("Connection reset by peer") || message.contains("Broken pipe")) {
|
||||
logger.error("Sync failed, reconnect to canal instance. Error: {}", message);
|
||||
break out;
|
||||
}
|
||||
}
|
||||
// Handle sink error
|
||||
if (i != retry - 1) {
|
||||
canalMsgConsumer.rollback(); // 处理失败, 回滚数据
|
||||
logger.error(e.getMessage() + " Error sync and rollback, execute times: " + (i + 1));
|
||||
@@ -224,7 +240,7 @@ public class AdapterProcessor {
|
||||
logger.error("finish turn off switch of destination:" + canalDestination);
|
||||
} else {
|
||||
canalMsgConsumer.ack();
|
||||
logger.error(e.getMessage() + " Error sync but ACK!");
|
||||
logger.error(e.getMessage() + " Error sync but ACK!", e);
|
||||
}
|
||||
}
|
||||
Thread.sleep(500);
|
||||
|
||||
@@ -18,7 +18,9 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cloud.context.refresh.ContextRefresher;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.yaml.snakeyaml.LoaderOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||
|
||||
import com.alibaba.otter.canal.adapter.launcher.loader.CanalAdapterService;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
@@ -70,7 +72,7 @@ public class ApplicationConfigMonitor {
|
||||
super.onFileChange(file);
|
||||
try {
|
||||
// 检查yml格式
|
||||
new Yaml().loadAs(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8), Map.class);
|
||||
new Yaml(new SafeConstructor(new LoaderOptions())).loadAs(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8), Map.class);
|
||||
|
||||
canalAdapterService.destroy();
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.alibaba.otter.canal.adapter.launcher.prometheus;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* @author sunxien
|
||||
* @date 2025/5/19
|
||||
* @since 1.0.0-SNAPSHOT
|
||||
*/
|
||||
public class CanalAdapterExports {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CanalAdapterExports.class);
|
||||
|
||||
private CanalAdapterExports() {
|
||||
}
|
||||
|
||||
private static class SingletonHolder {
|
||||
private static final CanalAdapterExports SINGLETON = new CanalAdapterExports();
|
||||
}
|
||||
|
||||
public static CanalAdapterExports instance() {
|
||||
return SingletonHolder.SINGLETON;
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
public void terminate() {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.alibaba.otter.canal.adapter.launcher.prometheus;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import io.prometheus.client.exporter.HTTPServer;
|
||||
import io.prometheus.client.hotspot.DefaultExports;
|
||||
|
||||
/**
|
||||
* @author sunxien
|
||||
* @date 2025/5/19
|
||||
* @since 1.0.0-SNAPSHOT
|
||||
*/
|
||||
@Component
|
||||
public class PrometheusService implements InitializingBean, DisposableBean {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PrometheusService.class);
|
||||
|
||||
private final CanalAdapterExports adapterExports;
|
||||
private volatile boolean running = false;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* Canal Server admin port: 11110
|
||||
* Canal Server tcp port: 11111
|
||||
* Canal Server metric port: 11112
|
||||
* Canal Adapter metric port: 11113
|
||||
* </pre>
|
||||
*/
|
||||
private int port = 11113;
|
||||
private HTTPServer httpServer;
|
||||
|
||||
private PrometheusService() {
|
||||
this.adapterExports = CanalAdapterExports.instance();
|
||||
}
|
||||
|
||||
private static class SingletonHolder {
|
||||
private static final PrometheusService SINGLETON = new PrometheusService();
|
||||
}
|
||||
|
||||
public static PrometheusService getInstance() {
|
||||
return SingletonHolder.SINGLETON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
PrometheusService.getInstance().initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
PrometheusService.getInstance().terminate();
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
try {
|
||||
logger.info("Starting prometheus HTTPServer on port {}....", port);
|
||||
// TODO 2.Https?
|
||||
httpServer = new HTTPServer(port);
|
||||
logger.info("Start prometheus HTTPServer on port {} success", port);
|
||||
} catch (IOException e) {
|
||||
logger.error("Unable to start prometheus HTTPServer on port {}.", port, e);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// JVM exports
|
||||
DefaultExports.initialize();
|
||||
// adapterExports.initialize();
|
||||
this.running = true;
|
||||
} catch (Throwable t) {
|
||||
logger.error("Unable to initialize adapter exports. (Register the default Hotspot collectors)", t);
|
||||
}
|
||||
}
|
||||
|
||||
public void terminate() {
|
||||
try {
|
||||
this.running = false;
|
||||
adapterExports.terminate();
|
||||
if (httpServer != null) {
|
||||
httpServer.stop();
|
||||
logger.info("Stop prometheus HTTPServer on port {} success", port);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.error("Something happened while terminating prometheus HTTPServer.", t);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public void setServerPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
}
|
||||
@@ -217,7 +217,7 @@ public class CommonRest {
|
||||
resStatus = "off";
|
||||
}
|
||||
Map<String, String> res = new LinkedHashMap<>();
|
||||
res.put("stauts", resStatus);
|
||||
res.put("status", resStatus);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ canal.conf:
|
||||
# hosts: 127.0.0.1:9300 # 127.0.0.1:9200 for rest mode
|
||||
# properties:
|
||||
# mode: transport # or rest
|
||||
# # security.ca.path: /etc/es8/ca.crt
|
||||
# # security.auth: test:123456 # only used for rest mode
|
||||
# cluster.name: elasticsearch
|
||||
# - name: kudu
|
||||
@@ -113,4 +114,4 @@ canal.conf:
|
||||
# jdbc.password: 123456
|
||||
# batchSize: 3000
|
||||
# scheduleTime: 600 # second unit
|
||||
# threads: 3 # parallel threads
|
||||
# threads: 3 # parallel threads
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -100,7 +100,9 @@
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.8</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
|
||||
@@ -172,11 +172,12 @@ public class PhoenixEtlService {
|
||||
"SELECT * FROM " + dbMapping.getDatabase() + "." + dbMapping.getTable());
|
||||
|
||||
// 拼接条件
|
||||
appendCondition(params, dbMapping, srcDS, sql);
|
||||
List<Object> values = new ArrayList<>();
|
||||
appendCondition(params, dbMapping, srcDS, sql, values);
|
||||
|
||||
// 获取总数
|
||||
String countSql = "SELECT COUNT(1) FROM ( " + sql + ") _CNT ";
|
||||
long cnt = (Long) Util.sqlRS(srcDS, countSql, rs -> {
|
||||
long cnt = (Long) Util.sqlRS(srcDS, countSql, values, rs -> {
|
||||
Long count = null;
|
||||
try {
|
||||
if (rs.next()) {
|
||||
@@ -206,14 +207,14 @@ public class PhoenixEtlService {
|
||||
sqlFinal = sql + " LIMIT " + offset + "," + cnt;
|
||||
}
|
||||
executor
|
||||
.execute(() -> executeSqlImport(srcDS, targetDSConnection, sqlFinal, dbMapping, successCount, errMsg, debug));
|
||||
.execute(() -> executeSqlImport(srcDS, targetDSConnection, sqlFinal, values, dbMapping, successCount, errMsg, debug));
|
||||
}
|
||||
|
||||
executor.shutdown();
|
||||
//noinspection StatementWithEmptyBody
|
||||
while (!executor.awaitTermination(3, TimeUnit.SECONDS)) ;
|
||||
} else {
|
||||
executeSqlImport(srcDS, targetDSConnection, sql.toString(), dbMapping, successCount, errMsg, debug);
|
||||
executeSqlImport(srcDS, targetDSConnection, sql.toString(), values, dbMapping, successCount, errMsg, debug);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
@@ -235,7 +236,7 @@ public class PhoenixEtlService {
|
||||
}
|
||||
|
||||
private static void appendCondition(List<String> params, DbMapping dbMapping, DataSource ds,
|
||||
StringBuilder sql) {
|
||||
StringBuilder sql, List<Object> values) {
|
||||
if (params != null && params.size() == 1 && dbMapping.getEtlCondition() == null) {
|
||||
AtomicBoolean stExists = new AtomicBoolean(false);
|
||||
// 验证是否有SYS_TIME字段
|
||||
@@ -256,13 +257,27 @@ public class PhoenixEtlService {
|
||||
return null;
|
||||
});
|
||||
if (stExists.get()) {
|
||||
sql.append(" WHERE SYS_TIME >= '").append(params.get(0)).append("' ");
|
||||
sql.append(" WHERE SYS_TIME >= ? ");
|
||||
values.add(params.get(0));
|
||||
}
|
||||
} else if (dbMapping.getEtlCondition() != null && params != null && params.size() > 0) {
|
||||
String etlCondition = dbMapping.getEtlCondition();
|
||||
int size = params.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
etlCondition = etlCondition.replace("{" + i + "}", params.get(i));
|
||||
String placeholder = "{" + i + "}";
|
||||
int idx = etlCondition.indexOf(placeholder);
|
||||
if (idx >= 0) {
|
||||
int start = idx;
|
||||
int end = idx + placeholder.length();
|
||||
// 兼容配置中 '{i}' 这种被单引号包裹的写法,避免生成 '?' 语法错误
|
||||
if (start > 0 && etlCondition.charAt(start - 1) == '\''
|
||||
&& end < etlCondition.length() && etlCondition.charAt(end) == '\'') {
|
||||
start--;
|
||||
end++;
|
||||
}
|
||||
etlCondition = etlCondition.substring(0, start) + "?" + etlCondition.substring(end);
|
||||
values.add(params.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
sql.append(" ").append(etlCondition);
|
||||
@@ -272,8 +287,8 @@ public class PhoenixEtlService {
|
||||
/**
|
||||
* 执行导入
|
||||
*/
|
||||
private static boolean executeSqlImport(DataSource srcDS, Connection targetDSConnection, String sql, DbMapping dbMapping,
|
||||
AtomicLong successCount, List<String> errMsg, boolean debug) {
|
||||
private static boolean executeSqlImport(DataSource srcDS, Connection targetDSConnection, String sql, List<Object> values,
|
||||
DbMapping dbMapping, AtomicLong successCount, List<String> errMsg, boolean debug) {
|
||||
try {
|
||||
Map<String, String> columnsMap = new LinkedHashMap<>();
|
||||
Map<String, Integer> columnType = new LinkedHashMap<>();
|
||||
@@ -300,7 +315,7 @@ public class PhoenixEtlService {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
Util.sqlRS(srcDS, sql, rs -> {
|
||||
Util.sqlRS(srcDS, sql, values, rs -> {
|
||||
int idx = 1;
|
||||
|
||||
try {
|
||||
@@ -337,13 +352,13 @@ public class PhoenixEtlService {
|
||||
pstmt.clearParameters();
|
||||
|
||||
// 删除数据
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
Map<String, Object> deleteValues = new LinkedHashMap<>();
|
||||
StringBuilder deleteSql = new StringBuilder(
|
||||
"DELETE FROM " + SyncUtil.getDbTableName(dbMapping) + " WHERE ");
|
||||
appendCondition(dbMapping, deleteSql, values, rs);
|
||||
appendCondition(dbMapping, deleteSql, deleteValues, rs);
|
||||
try (PreparedStatement pstmt2 = connTarget.prepareStatement(deleteSql.toString())) {
|
||||
int k = 1;
|
||||
for (Object val : values.values()) {
|
||||
for (Object val : deleteValues.values()) {
|
||||
pstmt2.setObject(k++, val);
|
||||
}
|
||||
pstmt2.execute();
|
||||
@@ -352,13 +367,13 @@ public class PhoenixEtlService {
|
||||
Map<String, Object> insertValues = new HashMap<>();
|
||||
int i = 1;
|
||||
for (Map.Entry<String, String> entry : columnsMap.entrySet()) {
|
||||
String targetClolumnName = entry.getKey();
|
||||
String targetColumnName = entry.getKey();
|
||||
String srcColumnName = entry.getValue();
|
||||
if (srcColumnName == null) {
|
||||
srcColumnName = targetClolumnName;
|
||||
srcColumnName = targetColumnName;
|
||||
}
|
||||
|
||||
Integer type = columnType.get(targetClolumnName.toLowerCase());
|
||||
Integer type = columnType.get(targetColumnName.toLowerCase());
|
||||
|
||||
try {
|
||||
Object value = rs.getObject(srcColumnName);
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>canal</artifactId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>canal client adapter module for otter ${project.version}</name>
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot</artifactId>
|
||||
<version>2.5.4</version>
|
||||
<version>2.5.15</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
@@ -136,7 +136,7 @@
|
||||
<artifactId>kafka-clients</artifactId>
|
||||
<version>${kafka_version}</version>
|
||||
</dependency>
|
||||
<!-- rabbotmq -->
|
||||
<!-- rabbitmq -->
|
||||
<dependency>
|
||||
<groupId>com.rabbitmq</groupId>
|
||||
<artifactId>amqp-client</artifactId>
|
||||
@@ -238,7 +238,7 @@
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<version>1.29</version>
|
||||
<version>2.0</version>
|
||||
</dependency>
|
||||
<!-- test -->
|
||||
<dependency>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -68,7 +68,9 @@
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.8</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package com.alibaba.otter.canal.client.adapter.rdb.service;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.ResultSetMetaData;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -17,10 +13,7 @@ import com.alibaba.druid.pool.DruidDataSource;
|
||||
import com.alibaba.otter.canal.client.adapter.rdb.config.MappingConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.rdb.config.MappingConfig.DbMapping;
|
||||
import com.alibaba.otter.canal.client.adapter.rdb.support.SyncUtil;
|
||||
import com.alibaba.otter.canal.client.adapter.support.AbstractEtlService;
|
||||
import com.alibaba.otter.canal.client.adapter.support.AdapterConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.EtlResult;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
import com.alibaba.otter.canal.client.adapter.support.*;
|
||||
|
||||
/**
|
||||
* RDB ETL 操作业务类
|
||||
@@ -44,7 +37,9 @@ public class RdbEtlService extends AbstractEtlService {
|
||||
*/
|
||||
public EtlResult importData(List<String> params) {
|
||||
DbMapping dbMapping = config.getDbMapping();
|
||||
String sql = "SELECT * FROM " + dbMapping.getDatabase() + "." + dbMapping.getTable();
|
||||
// 获取源数据源,根据数据库类型拼装数据库名和表名
|
||||
DruidDataSource dataSource = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());
|
||||
String sql = "SELECT * FROM " + SyncUtil.getSourceDbTableName(dbMapping, dataSource.getDbType());
|
||||
return importData(sql, params);
|
||||
}
|
||||
|
||||
@@ -127,13 +122,13 @@ public class RdbEtlService extends AbstractEtlService {
|
||||
|
||||
int i = 1;
|
||||
for (Map.Entry<String, String> entry : columnsMap.entrySet()) {
|
||||
String targetClolumnName = entry.getKey();
|
||||
String targetColumnName = entry.getKey();
|
||||
String srcColumnName = entry.getValue();
|
||||
if (srcColumnName == null) {
|
||||
srcColumnName = targetClolumnName;
|
||||
srcColumnName = targetColumnName;
|
||||
}
|
||||
|
||||
Integer type = columnType.get(targetClolumnName.toLowerCase());
|
||||
Integer type = columnType.get(targetColumnName.toLowerCase());
|
||||
Object value = rs.getObject(srcColumnName);
|
||||
if (value != null) {
|
||||
SyncUtil.setPStmt(type, pstmt, value, i);
|
||||
|
||||
@@ -294,7 +294,7 @@ public class RdbSyncService {
|
||||
batchExecutor.execute(insertSql.toString(), values);
|
||||
} catch (SQLException e) {
|
||||
if (skipDupException
|
||||
&& (e.getMessage().contains("Duplicate entry") || e.getMessage().startsWith("ORA-00001:"))) {
|
||||
&& (e.getMessage().contains("Duplicate entry") || e.getMessage().contains("duplicate key") || e.getMessage().startsWith("ORA-00001:"))) {
|
||||
// ignore
|
||||
// TODO 增加更多关系数据库的主键冲突的错误码
|
||||
} else {
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
package com.alibaba.otter.canal.client.adapter.rdb.support;
|
||||
|
||||
import com.alibaba.druid.DbType;
|
||||
import com.alibaba.otter.canal.client.adapter.rdb.config.MappingConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.druid.DbType;
|
||||
import com.alibaba.otter.canal.client.adapter.rdb.config.MappingConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
|
||||
public class SyncUtil {
|
||||
private static final Logger logger = LoggerFactory.getLogger(SyncUtil.class);
|
||||
|
||||
@@ -220,11 +222,16 @@ public class SyncUtil {
|
||||
pstmt.setTime(i, new java.sql.Time(((java.util.Date) value).getTime()));
|
||||
} else if (value instanceof String) {
|
||||
String v = (String) value;
|
||||
java.util.Date date = Util.parseDate(v);
|
||||
if (date != null) {
|
||||
pstmt.setTime(i, new Time(date.getTime()));
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
if(Util.isAccuracyOverSecond(v)) {
|
||||
//the java.sql.time doesn't support for even millisecond, only setObject works here.
|
||||
pstmt.setObject(i, v);
|
||||
}else {
|
||||
java.util.Date date = Util.parseDate(v);
|
||||
if (date != null) {
|
||||
pstmt.setTime(i, new Time(date.getTime()));
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
@@ -238,11 +245,22 @@ public class SyncUtil {
|
||||
} else if (value instanceof String) {
|
||||
String v = (String) value;
|
||||
if (!v.startsWith("0000-00-00")) {
|
||||
java.util.Date date = Util.parseDate(v);
|
||||
if (date != null) {
|
||||
pstmt.setTimestamp(i, new Timestamp(date.getTime()));
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
if(Util.isAccuracyOverMillisecond(v)){
|
||||
//convert to ISO-8601 standard format, with up to nanoseconds (9 digits) precision
|
||||
LocalDateTime isoDatetime = Util.parseISOLocalDateTime(v);
|
||||
if (isoDatetime != null) {
|
||||
pstmt.setTimestamp(i, Timestamp.valueOf(isoDatetime));
|
||||
} else {
|
||||
//if can't convert, set to null
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
}else {
|
||||
java.util.Date date = Util.parseDate(v);
|
||||
if (date != null) {
|
||||
pstmt.setTimestamp(i, new Timestamp(date.getTime()));
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pstmt.setObject(i, value);
|
||||
@@ -266,6 +284,17 @@ public class SyncUtil {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String getSourceDbTableName(MappingConfig.DbMapping dbMapping, String dbType) {
|
||||
String result = "";
|
||||
String backtick = getBacktickByDbType(dbType);
|
||||
if (StringUtils.isNotEmpty(dbMapping.getDatabase())) {
|
||||
result += (backtick + dbMapping.getDatabase() + backtick + ".");
|
||||
}
|
||||
|
||||
result += (backtick + dbMapping.getTable() + backtick);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据DbType返回反引号或空字符串
|
||||
*
|
||||
@@ -284,6 +313,9 @@ public class SyncUtil {
|
||||
case mariadb:
|
||||
case oceanbase:
|
||||
return "`";
|
||||
// 当dbType 为 postgresql时,返回双引号(避免表名为postgresql的关键字时,sql查询报错)
|
||||
case postgresql:
|
||||
return "\"";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -66,7 +66,9 @@
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.8</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>canal</artifactId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
|
||||
@@ -51,7 +51,7 @@ public class CanalMessageDeserializer {
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new CanalClientException("deserializer failed", e);
|
||||
throw new CanalClientException("deserializer failed by " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
package com.alibaba.otter.canal.client.impl;
|
||||
|
||||
import java.net.SocketAddress;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.otter.canal.client.CanalConnector;
|
||||
import com.alibaba.otter.canal.client.CanalNodeAccessStrategy;
|
||||
import com.alibaba.otter.canal.protocol.Message;
|
||||
import com.alibaba.otter.canal.protocol.exception.CanalClientException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.SocketAddress;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 集群版本connector实现,自带了failover功能<br/>
|
||||
*
|
||||
*
|
||||
* @author jianghang 2012-10-29 下午08:04:06
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@@ -65,7 +64,7 @@ public class ClusterCanalConnector implements CanalConnector {
|
||||
currentConnector.connect();
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
logger.warn("failed to connect to:{} after retry {} times", accessStrategy.currentNode(), times);
|
||||
logger.warn("failed to connect to canal server after retry {} times", times);
|
||||
currentConnector.disconnect();
|
||||
currentConnector = null;
|
||||
// retry for #retryTimes for each node when trying to
|
||||
|
||||
@@ -78,14 +78,15 @@ public class KafkaCanalConnector implements CanalMQConnector {
|
||||
return;
|
||||
}
|
||||
|
||||
connected = true;
|
||||
if (kafkaConsumer == null && !flatMessage) {
|
||||
kafkaConsumer = new KafkaConsumer<>(properties);
|
||||
|
||||
}
|
||||
|
||||
if (kafkaConsumer2 == null && flatMessage) {
|
||||
kafkaConsumer2 = new KafkaConsumer<>(properties);
|
||||
}
|
||||
|
||||
connected = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
package com.alibaba.otter.canal.client.pulsarmq;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.pulsar.client.api.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.otter.canal.client.CanalMQConnector;
|
||||
import com.alibaba.otter.canal.client.CanalMessageDeserializer;
|
||||
@@ -9,13 +17,6 @@ import com.alibaba.otter.canal.protocol.FlatMessage;
|
||||
import com.alibaba.otter.canal.protocol.Message;
|
||||
import com.alibaba.otter.canal.protocol.exception.CanalClientException;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.pulsar.client.api.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* PulsarMQ的连接
|
||||
@@ -24,91 +25,98 @@ import java.util.concurrent.TimeUnit;
|
||||
* 注意点:
|
||||
* 1. 相比于canal {@linkplain SimpleCanalConnector}, 这里get和ack操作不能有并发, 必须是一个线程执行get后,内存里执行完毕ack后再取下一个get
|
||||
* </pre>
|
||||
*
|
||||
* todo 重复消费的概率相当高。一次批处理中,只要有一个消息处理失败,则该批次全部重试
|
||||
*
|
||||
* @since 1.1.1
|
||||
*/
|
||||
public class PulsarMQCanalConnector implements CanalMQConnector {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PulsarMQCanalConnector.class);
|
||||
private static final Logger logger = LoggerFactory
|
||||
.getLogger(PulsarMQCanalConnector.class);
|
||||
|
||||
private volatile Messages<byte[]> lastGetBatchMessage;
|
||||
|
||||
/**
|
||||
* 连接pulsar客户端
|
||||
*/
|
||||
private PulsarClient pulsarClient;
|
||||
private PulsarClient pulsarClient;
|
||||
/**
|
||||
* 消费者
|
||||
*/
|
||||
private Consumer<byte[]> consumer;
|
||||
private Consumer<byte[]> consumer;
|
||||
/**
|
||||
* 是否扁平化Canal消息内容
|
||||
*/
|
||||
private boolean isFlatMessage = false;
|
||||
private boolean isFlatMessage = false;
|
||||
/**
|
||||
* 主题名称
|
||||
*/
|
||||
private String topic;
|
||||
private String topic;
|
||||
/**
|
||||
* 环境连接URL
|
||||
*/
|
||||
private String serviceUrl;
|
||||
private String serviceUrl;
|
||||
/**
|
||||
* 角色认证token
|
||||
*/
|
||||
private String roleToken;
|
||||
private String roleToken;
|
||||
|
||||
/**
|
||||
* listener name
|
||||
*/
|
||||
private String listenerName;
|
||||
|
||||
/**
|
||||
* 订阅客户端名称
|
||||
*/
|
||||
private String subscriptName;
|
||||
private String subscriptName;
|
||||
/**
|
||||
* 每次批量获取数据的最大条目数,默认30
|
||||
*/
|
||||
private int batchSize = 30;
|
||||
private int batchSize = 30;
|
||||
/**
|
||||
* 与{@code batchSize}一起决定批量获取的数据大小
|
||||
* 当:
|
||||
* 与{@code batchSize}一起决定批量获取的数据大小 当:
|
||||
* <p>
|
||||
* 1. {@code batchSize} 条消息未消费时<br/>
|
||||
* 2. 距上一次批量消费时间达到{@code batchTimeoutSeconds}秒时
|
||||
* </p>
|
||||
* 任一条件满足,即执行批量消费
|
||||
*/
|
||||
private int getBatchTimeoutSeconds = 30;
|
||||
private int getBatchTimeoutSeconds = 30;
|
||||
/**
|
||||
* 批量处理消息时,一次批量处理的超时时间秒数
|
||||
* <p>
|
||||
* 该时间应该根据{@code batchSize}和{@code batchTimeoutSeconds}合理设置
|
||||
* </p>
|
||||
*/
|
||||
private int batchProcessTimeoutSeconds = 60;
|
||||
private int batchProcessTimeoutSeconds = 60;
|
||||
/**
|
||||
* 消费失败后的重试秒数,默认60秒
|
||||
*/
|
||||
private int redeliveryDelaySeconds = 60;
|
||||
private int redeliveryDelaySeconds = 60;
|
||||
/**
|
||||
* 当客户端接收到消息,30秒还没有返回ack给服务端时,ack超时,会重新消费该消息
|
||||
*/
|
||||
private int ackTimeoutSeconds = 30;
|
||||
private int ackTimeoutSeconds = 30;
|
||||
/**
|
||||
* 是否开启消息失败重试功能,默认开启
|
||||
*/
|
||||
private boolean isRetry = true;
|
||||
private boolean isRetry = true;
|
||||
/**
|
||||
* <p>
|
||||
* true重试(-RETRY)和死信队列(-DLQ)后缀为大写,有些地方创建的为小写,需确保正确
|
||||
* </p>
|
||||
*/
|
||||
private boolean isRetryDLQUpperCase = false;
|
||||
private boolean isRetryDLQUpperCase = false;
|
||||
/**
|
||||
* 最大重试次数
|
||||
*/
|
||||
private int maxRedeliveryCount = 128;
|
||||
private int maxRedeliveryCount = 128;
|
||||
/**
|
||||
* 连接标识位,在连接或关闭连接后改变值
|
||||
*/
|
||||
private boolean connected = false;
|
||||
private boolean connected = false;
|
||||
|
||||
/**
|
||||
* 除必要参数外,其他参数使用默认值
|
||||
@@ -118,16 +126,21 @@ public class PulsarMQCanalConnector implements CanalMQConnector {
|
||||
* </p>
|
||||
*
|
||||
* @param isFlatMessage true使用扁平消息
|
||||
* @param serviceUrl pulsar服务连接地址,通常为:pulsar:host:ip或http://host:ip
|
||||
* @param roleToken 有对应topic的消费者权限的角色token
|
||||
* @param topic 订阅主题
|
||||
* @param serviceUrl pulsar服务连接地址,通常为:pulsar:host:ip或http://host:ip
|
||||
* @param roleToken 有对应topic的消费者权限的角色token
|
||||
* @param topic 订阅主题
|
||||
* @param subscriptName 订阅和客户端名称,同一个订阅名视为同一个消费实例
|
||||
* @date 2021/9/18 08:54
|
||||
* @author chad
|
||||
* @since 1 by chad at 2021/9/18 完善
|
||||
*/
|
||||
public PulsarMQCanalConnector(boolean isFlatMessage, String serviceUrl, String roleToken, String topic
|
||||
, String subscriptName) {
|
||||
public PulsarMQCanalConnector(boolean isFlatMessage, String serviceUrl, String roleToken, String topic,
|
||||
String subscriptName){
|
||||
this(isFlatMessage, serviceUrl, roleToken, topic, subscriptName, null);
|
||||
}
|
||||
|
||||
public PulsarMQCanalConnector(boolean isFlatMessage, String serviceUrl, String roleToken, String topic,
|
||||
String subscriptName, String listenerName){
|
||||
this.isFlatMessage = isFlatMessage;
|
||||
this.serviceUrl = serviceUrl;
|
||||
this.roleToken = roleToken;
|
||||
@@ -136,6 +149,7 @@ public class PulsarMQCanalConnector implements CanalMQConnector {
|
||||
if (StringUtils.isEmpty(this.subscriptName)) {
|
||||
throw new RuntimeException("Pulsar Consumer subscriptName required");
|
||||
}
|
||||
this.listenerName = listenerName;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,10 +159,31 @@ public class PulsarMQCanalConnector implements CanalMQConnector {
|
||||
* @author chad
|
||||
* @since 1 by chad at 2021/9/18 完善
|
||||
*/
|
||||
public PulsarMQCanalConnector(boolean isFlatMessage, String serviceUrl, String roleToken, String topic
|
||||
, String subscriptName, int batchSize, int getBatchTimeoutSeconds, int batchProcessTimeoutSeconds
|
||||
, int redeliveryDelaySeconds, int ackTimeoutSeconds, boolean isRetry, boolean isRetryDLQUpperCase
|
||||
, int maxRedeliveryCount) {
|
||||
public PulsarMQCanalConnector(boolean isFlatMessage, String serviceUrl, String roleToken, String topic,
|
||||
String subscriptName, int batchSize, int getBatchTimeoutSeconds,
|
||||
int batchProcessTimeoutSeconds, int redeliveryDelaySeconds, int ackTimeoutSeconds,
|
||||
boolean isRetry, boolean isRetryDLQUpperCase, int maxRedeliveryCount){
|
||||
this(isFlatMessage,
|
||||
serviceUrl,
|
||||
roleToken,
|
||||
topic,
|
||||
subscriptName,
|
||||
batchSize,
|
||||
getBatchTimeoutSeconds,
|
||||
batchProcessTimeoutSeconds,
|
||||
redeliveryDelaySeconds,
|
||||
ackTimeoutSeconds,
|
||||
isRetry,
|
||||
isRetryDLQUpperCase,
|
||||
maxRedeliveryCount,
|
||||
null);
|
||||
}
|
||||
|
||||
public PulsarMQCanalConnector(boolean isFlatMessage, String serviceUrl, String roleToken, String topic,
|
||||
String subscriptName, int batchSize, int getBatchTimeoutSeconds,
|
||||
int batchProcessTimeoutSeconds, int redeliveryDelaySeconds, int ackTimeoutSeconds,
|
||||
boolean isRetry, boolean isRetryDLQUpperCase, int maxRedeliveryCount,
|
||||
String listenerName){
|
||||
this.isFlatMessage = isFlatMessage;
|
||||
this.serviceUrl = serviceUrl;
|
||||
this.roleToken = roleToken;
|
||||
@@ -165,16 +200,20 @@ public class PulsarMQCanalConnector implements CanalMQConnector {
|
||||
this.isRetry = isRetry;
|
||||
this.isRetryDLQUpperCase = isRetryDLQUpperCase;
|
||||
this.maxRedeliveryCount = maxRedeliveryCount;
|
||||
this.listenerName = listenerName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() throws CanalClientException {
|
||||
// 连接创建客户端
|
||||
try {
|
||||
pulsarClient = PulsarClient.builder()
|
||||
.serviceUrl(serviceUrl)
|
||||
.authentication(AuthenticationFactory.token(roleToken))
|
||||
.build();
|
||||
ClientBuilder builder = PulsarClient.builder()
|
||||
.serviceUrl(serviceUrl)
|
||||
.authentication(AuthenticationFactory.token(roleToken));
|
||||
if (StringUtils.isNotEmpty(listenerName)) {
|
||||
builder.listenerName(listenerName);
|
||||
}
|
||||
pulsarClient = builder.build();
|
||||
} catch (PulsarClientException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -223,15 +262,14 @@ public class PulsarMQCanalConnector implements CanalMQConnector {
|
||||
builder.subscriptionType(SubscriptionType.Failover);
|
||||
|
||||
builder
|
||||
// 调用consumer.negativeAcknowledge(message) (即nack)来表示消费失败的消息
|
||||
// 在指定的时间进行重新消费,默认是1分钟。
|
||||
.negativeAckRedeliveryDelay(this.redeliveryDelaySeconds, TimeUnit.SECONDS)
|
||||
.subscriptionName(this.subscriptName)
|
||||
;
|
||||
// 调用consumer.negativeAcknowledge(message) (即nack)来表示消费失败的消息
|
||||
// 在指定的时间进行重新消费,默认是1分钟。
|
||||
.negativeAckRedeliveryDelay(this.redeliveryDelaySeconds, TimeUnit.SECONDS)
|
||||
.subscriptionName(this.subscriptName);
|
||||
if (this.isRetry) {
|
||||
DeadLetterPolicy.DeadLetterPolicyBuilder dlqBuilder = DeadLetterPolicy.builder()
|
||||
// 最大重试次数
|
||||
.maxRedeliverCount(this.maxRedeliveryCount);
|
||||
// 最大重试次数
|
||||
.maxRedeliverCount(this.maxRedeliveryCount);
|
||||
// 指定重试队列,不是多个或通配符topic才能判断重试队列
|
||||
if (!MQUtil.isPatternTag(this.topic)) {
|
||||
String retryTopic = this.topic + (this.isRetryDLQUpperCase ? "-RETRY" : "-retry");
|
||||
@@ -240,29 +278,26 @@ public class PulsarMQCanalConnector implements CanalMQConnector {
|
||||
dlqBuilder.deadLetterTopic(dlqTopic);
|
||||
}
|
||||
|
||||
//默认关闭,如果需要重试则开启
|
||||
builder.enableRetry(true)
|
||||
.deadLetterPolicy(dlqBuilder.build());
|
||||
// 默认关闭,如果需要重试则开启
|
||||
builder.enableRetry(true).deadLetterPolicy(dlqBuilder.build());
|
||||
}
|
||||
|
||||
// ack超时
|
||||
builder.ackTimeout(this.ackTimeoutSeconds, TimeUnit.SECONDS);
|
||||
|
||||
// pulsar批量获取消息设置
|
||||
builder.batchReceivePolicy(new BatchReceivePolicy.Builder()
|
||||
.maxNumMessages(this.batchSize)
|
||||
.timeout(this.getBatchTimeoutSeconds, TimeUnit.SECONDS)
|
||||
.build());
|
||||
builder.batchReceivePolicy(new BatchReceivePolicy.Builder().maxNumMessages(this.batchSize)
|
||||
.timeout(this.getBatchTimeoutSeconds, TimeUnit.SECONDS)
|
||||
.build());
|
||||
|
||||
try {
|
||||
this.consumer = builder.subscribe();
|
||||
connected = true;
|
||||
} catch (PulsarClientException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
connected = true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void subscribe() throws CanalClientException {
|
||||
this.subscribe(null);
|
||||
@@ -283,7 +318,7 @@ public class PulsarMQCanalConnector implements CanalMQConnector {
|
||||
* 不关注业务执行结果,只要收到消息即认识消费成功,自动ack
|
||||
*
|
||||
* @param timeout 阻塞获取消息的超时时间
|
||||
* @param unit 时间单位
|
||||
* @param unit 时间单位
|
||||
* @return java.util.List<com.alibaba.otter.canal.protocol.Message>
|
||||
* @date 2021/9/13 22:24
|
||||
* @author chad
|
||||
@@ -299,13 +334,14 @@ public class PulsarMQCanalConnector implements CanalMQConnector {
|
||||
}
|
||||
|
||||
/**
|
||||
* 关心业务执行结果,业务侧根据执行结果调用 {@link PulsarMQCanalConnector#ack()}或{@link PulsarMQCanalConnector#rollback()}
|
||||
* 关心业务执行结果,业务侧根据执行结果调用
|
||||
* {@link PulsarMQCanalConnector#ack()}或{@link PulsarMQCanalConnector#rollback()}
|
||||
* <p>
|
||||
* 本方法示支持多线程,在MQ保障顺序的前提下,也无法提供单Topic多线程
|
||||
* </p>
|
||||
*
|
||||
* @param timeout 阻塞获取消息的超时时间
|
||||
* @param unit 时间单位
|
||||
* @param unit 时间单位
|
||||
* @return java.util.List<com.alibaba.otter.canal.protocol.Message>
|
||||
* @date 2021/9/13 22:26
|
||||
* @author chad
|
||||
@@ -339,7 +375,8 @@ public class PulsarMQCanalConnector implements CanalMQConnector {
|
||||
* @return java.util.List<T>
|
||||
* @date 2021/9/14 15:20
|
||||
* @author chad
|
||||
* @since 1 by chad at 2021/9/14 供{@link PulsarMQCanalConnector#getListWithoutAck(Long, TimeUnit)}
|
||||
* @since 1 by chad at 2021/9/14
|
||||
* 供{@link PulsarMQCanalConnector#getListWithoutAck(Long, TimeUnit)}
|
||||
* 和{@link PulsarMQCanalConnector#getFlatListWithoutAck(Long, TimeUnit)}调用
|
||||
*/
|
||||
private <T> List<T> getListWithoutAck() {
|
||||
|
||||
@@ -6,7 +6,6 @@ import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.alibaba.otter.canal.client.ConsumerBatchMessage;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.rocketmq.acl.common.AclClientRPCHook;
|
||||
import org.apache.rocketmq.acl.common.SessionCredentials;
|
||||
@@ -25,6 +24,7 @@ import org.slf4j.LoggerFactory;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.otter.canal.client.CanalMQConnector;
|
||||
import com.alibaba.otter.canal.client.CanalMessageDeserializer;
|
||||
import com.alibaba.otter.canal.client.ConsumerBatchMessage;
|
||||
import com.alibaba.otter.canal.client.impl.SimpleCanalConnector;
|
||||
import com.alibaba.otter.canal.protocol.FlatMessage;
|
||||
import com.alibaba.otter.canal.protocol.Message;
|
||||
@@ -155,11 +155,10 @@ public class RocketMQCanalConnector implements CanalMQConnector {
|
||||
}
|
||||
});
|
||||
rocketMQConsumer.start();
|
||||
connected = true;
|
||||
} catch (MQClientException ex) {
|
||||
connected = false;
|
||||
logger.error("Start RocketMQ consumer error", ex);
|
||||
throw new RuntimeException("Start RocketMQ consumer error", ex);
|
||||
}
|
||||
connected = true;
|
||||
}
|
||||
|
||||
private boolean process(List<MessageExt> messageExts) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>canal</artifactId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<artifactId>canal.common</artifactId>
|
||||
@@ -87,6 +87,10 @@
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.net.ServerSocket;
|
||||
import java.util.Enumeration;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang.ArrayUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -75,4 +76,27 @@ public class AddressUtils {
|
||||
logger.error("Could not get local host ip address, will use 127.0.0.1 instead.");
|
||||
return localAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拆分IP地址和端口号
|
||||
*
|
||||
* @param text ip地址和端口号,ip和端口号以英文冒号(:)分隔;
|
||||
*
|
||||
* <pre>
|
||||
* ipv4 127.0.0.1:3306
|
||||
* ipv6 [::1]:3306
|
||||
* </pre>
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String[] splitIPAndPort(String text) {
|
||||
text = text.replace("[", "").replace("]", "");
|
||||
int idx = text.lastIndexOf(':');
|
||||
if (idx > 0) {
|
||||
String ip = text.substring(0, idx);
|
||||
String port = text.substring(idx + 1);
|
||||
return new String[] { ip, port };
|
||||
}
|
||||
return ArrayUtils.EMPTY_STRING_ARRAY;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.alibaba.otter.canal.common.utils;
|
||||
|
||||
import com.alibaba.druid.filter.config.ConfigTools;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
@@ -50,4 +52,8 @@ public class CommonUtils {
|
||||
|
||||
return dirFile.delete();
|
||||
}
|
||||
|
||||
public static String decryptDruidPassword(String publicKeyText, String cipherText) throws Exception {
|
||||
return ConfigTools.decrypt(publicKeyText, cipherText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public class ExecutorTemplate {
|
||||
check(future);
|
||||
}
|
||||
|
||||
public void submit(Callable<Exception> task) {
|
||||
public void submit(Callable task) {
|
||||
Future future = executor.submit(task);
|
||||
futures.add(future);
|
||||
check(future);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.alibaba.otter.canal.common.utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -88,8 +89,43 @@ public class FileUtils {
|
||||
return res.toString();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
String res = readFileFromOffset("test2.txt", 2, "UTF-8");
|
||||
System.out.println(res);
|
||||
/**
|
||||
* 校验自定义的文件名,是否在允许的基目录范围内,如何合法就返回全路径,否则就直接报错
|
||||
*
|
||||
* @param baseDir
|
||||
* @param destination
|
||||
* @return
|
||||
*/
|
||||
public static String validateFileName(String baseDir, String destination) {
|
||||
try {
|
||||
// 验证 destination 是否在允许的基目录范围内
|
||||
String basePath = new File(baseDir).getCanonicalPath();
|
||||
String fullPath = new File(basePath, destination).getCanonicalPath();
|
||||
|
||||
// 检查 fullPath 是否以 basePath 开头
|
||||
if (!fullPath.startsWith(basePath + File.separator)) {
|
||||
throw new IllegalArgumentException("Invalid destination path");
|
||||
}
|
||||
|
||||
return fullPath;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to read file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
String fullPath = validateFileName("/tmp/", "1.txt");
|
||||
System.out.println(fullPath);
|
||||
System.out.println(org.apache.commons.io.FileUtils.readLines(new File(fullPath)));
|
||||
|
||||
fullPath = validateFileName("/tmp/", "test");
|
||||
fullPath = validateFileName(fullPath,"1.txt");
|
||||
System.out.println(fullPath);
|
||||
System.out.println(org.apache.commons.io.FileUtils.readLines(new File(fullPath)));
|
||||
|
||||
|
||||
fullPath = validateFileName("/tmp/", "../etc/hosts");
|
||||
System.out.println(fullPath);
|
||||
System.out.println(org.apache.commons.io.FileUtils.readLines(new File(fullPath)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.connector</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.alibaba.otter.canal.connector.core.filter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -19,7 +18,6 @@ import com.googlecode.aviator.Expression;
|
||||
*/
|
||||
public class AviaterRegexFilter {
|
||||
|
||||
private static final String SPLIT = ",";
|
||||
private static final String PATTERN_SPLIT = "|";
|
||||
private static final String FILTER_EXPRESSION = "regex(pattern,target)";
|
||||
private static final RegexFunction regexFunction = new RegexFunction();
|
||||
@@ -43,8 +41,9 @@ public class AviaterRegexFilter {
|
||||
if (StringUtils.isEmpty(pattern)) {
|
||||
list = new ArrayList<>();
|
||||
} else {
|
||||
String[] ss = StringUtils.split(pattern, SPLIT);
|
||||
list = Arrays.asList(ss);
|
||||
// fix issue #5442: only split on commas outside regex grouping constructs,
|
||||
// so quantifiers like \d{1,2} are kept intact.
|
||||
list = splitPattern(pattern);
|
||||
}
|
||||
|
||||
// 对pattern按照从长到短的排序
|
||||
@@ -56,6 +55,60 @@ public class AviaterRegexFilter {
|
||||
this.pattern = StringUtils.join(list, PATTERN_SPLIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按顶层逗号拆分多个正则表达式,跳过位于 {}, [], () 内的逗号,避免破坏类似 \d{1,2} 的量词写法。
|
||||
*/
|
||||
private static List<String> splitPattern(String pattern) {
|
||||
List<String> result = new ArrayList<>();
|
||||
int braceDepth = 0;
|
||||
int bracketDepth = 0;
|
||||
int parenDepth = 0;
|
||||
int start = 0;
|
||||
int len = pattern.length();
|
||||
for (int i = 0; i < len; i++) {
|
||||
char c = pattern.charAt(i);
|
||||
// 反斜杠转义:跳过下一个字符,避免把 \{ \[ \( 等误判为分组开始
|
||||
if (c == '\\' && i + 1 < len) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
switch (c) {
|
||||
case '{':
|
||||
braceDepth++;
|
||||
break;
|
||||
case '}':
|
||||
if (braceDepth > 0) braceDepth--;
|
||||
break;
|
||||
case '[':
|
||||
bracketDepth++;
|
||||
break;
|
||||
case ']':
|
||||
if (bracketDepth > 0) bracketDepth--;
|
||||
break;
|
||||
case '(':
|
||||
parenDepth++;
|
||||
break;
|
||||
case ')':
|
||||
if (parenDepth > 0) parenDepth--;
|
||||
break;
|
||||
case ',':
|
||||
if (braceDepth == 0 && bracketDepth == 0 && parenDepth == 0) {
|
||||
if (i > start) {
|
||||
result.add(pattern.substring(start, i));
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (start < len) {
|
||||
result.add(pattern.substring(start));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean filter(String filtered) {
|
||||
if (StringUtils.isEmpty(pattern)) {
|
||||
return defaultEmptyValue;
|
||||
|
||||
@@ -79,7 +79,7 @@ public class CanalMessageSerializerUtil {
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error when serializing message to byte[] ");
|
||||
throw new RuntimeException("Error when serializing message to byte[] by " + e.getMessage() , e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -125,7 +125,7 @@ public class CanalMessageSerializerUtil {
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new CanalClientException("deserializer failed", e);
|
||||
throw new CanalClientException("deserializer failed by " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.connector</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
<parent>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>canal</artifactId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>canal.connector</artifactId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>canal connector module for otter ${project.version}</name>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.connector</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8-SNAPSHOT</version>
|
||||
<version>1.1.9-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user