Java正则匹配失效

背景

最近生产上出现一个正则匹配失效的情况,排查后发现是因为包含终止符NEL,需要用DOTALL模式匹配。原本是以为客户错误输入导致,再深入排查发现是由于utf8字符错误解码成iso8859导致。

Java: jdk8

排查流程

问题复现

省略掉排查日志,本地复现的步骤,这里直接用抽象的核心问题代码复现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static void bugRecur(){
// 模拟实际情况中的utf8字节数组
byte[] bugBytes=new String("\r\n\r\n公司123\r\n").getBytes(StandardCharsets.UTF_8);
byte[] normalBytes=new String("\r\n\r\n普通字符123\r\n").getBytes(StandardCharsets.UTF_8);

// 模拟业务需要,以iso8859-1编码将字节数组转回字符串(实际上上面和下面在实际业务的不同系统做的)
String bugStr=new String(bugBytes,StandardCharsets.ISO_8859_1);
String normalStr=new String(normalBytes,StandardCharsets.ISO_8859_1);

// 进行正则匹配
String patternStr = "\\r\\n\\r\\n(.*?)\\r\\n";
Pattern pattern = Pattern.compile(patternStr);
Matcher bugMatcher = pattern.matcher(bugStr);
Matcher normalMatcher = pattern.matcher(normalStr);

System.out.println("问题字符匹配结果: " + bugMatcher.find());
System.out.println("普通字符匹配结果: " + normalMatcher.find());
}

结果:

1
2
问题字符匹配结果: false
普通字符匹配结果: true

通过上面代码很容易发现两段字符串一个能匹配一个不能匹配明显有问题,且应该就是字符集不一致导致的问题,而且此问题不是必现。实际上相关的业务代码之前仅处理英文不处理中文所以一直没有问题,直到最近业务改造有了中文且通过了联调测试阶段一直到生产才发现问题。

问题根源

既然发现了是字符集问题,尝试去找到问题根源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private static void deepReason(){
// 模拟实际情况中的utf8字节数组
byte[] bugBytes=new String("\r\n\r\n好公司123\r\n").getBytes(StandardCharsets.UTF_8);
byte[] normalBytes=new String("\r\n\r\n普通字符123\r\n").getBytes(StandardCharsets.UTF_8);

// 模拟业务需要,以iso8859-1编码将字节数组转回字符串(实际上上面和下面在实际业务的不同系统做的)
String bugStr=new String(bugBytes,StandardCharsets.ISO_8859_1);
String normalStr=new String(normalBytes,StandardCharsets.ISO_8859_1);


System.out.println("错误编码后的问题字符串字节数组: "+ Arrays.toString(bugStr.getBytes(StandardCharsets.ISO_8859_1)));
System.out.println("错误编码后的问题字符串字节数组: "+ Arrays.toString(normalStr.getBytes(StandardCharsets.ISO_8859_1)));
// 这里是往控制台输出的结果,一般流会通过utf8编码传输,对分析函数内部行为没什么意义
// System.out.println("错误编码后的问题字符串: "+bugStr);

// 更关键的是看字符串在java内存中的utf16编码后的结果
// System.out.println("错误编码后的问题字符串在java内存中的utf16编码字符数组: "+ Arrays.toString(bugStr.toCharArray())); // 这一行会输出失败
System.out.println("错误编码后的问题字符串在java内存中的utf16编码字节数组: "+ Arrays.toString(bugStr.getBytes(StandardCharsets.UTF_16)));
// System.out.println("错误编码后的普通字符串在java内存中的utf16编码字符数组: "+ Arrays.toString(normalStr.toCharArray())); // 这一行会输出失败
System.out.println("错误编码后的普通字符串在java内存中的utf16编码字节数组: "+ Arrays.toString(normalStr.getBytes(StandardCharsets.UTF_16)));

String patternStr = "\\r\\n\\r\\n(.*?)\\r\\n";
Pattern pattern = Pattern.compile(patternStr);
Matcher bugMatcher = pattern.matcher(bugStr);
Matcher normalMatcher = pattern.matcher(normalStr);

System.out.println("问题字符匹配结果: " + bugMatcher.find());
System.out.println("普通字符匹配结果: " + normalMatcher.find());

}

结果输出:

1
2
3
4
5
6
7
错误编码后的问题字符串字节数组: [13, 10, 13, 10, -27, -91, -67, -27, -123, -84, -27, -113, -72, 49, 50, 51, 13, 10]
错误编码后的问题字符串字节数组: [13, 10, 13, 10, -26, -103, -82, -23, -128, -102, -27, -83, -105, -25, -84, -90, 49, 50, 51, 13, 10]

错误编码后的问题字符串在java内存中的utf16编码字节数组: [-2, -1, 0, 13, 0, 10, 0, 13, 0, 10, 0, -27, 0, -91, 0, -67, 0, -27, 0, -123, 0, -84, 0, -27, 0, -113, 0, -72, 0, 49, 0, 50, 0, 51, 0, 13, 0, 10]
错误编码后的普通字符串在java内存中的utf16编码字节数组: [-2, -1, 0, 13, 0, 10, 0, 13, 0, 10, 0, -26, 0, -103, 0, -82, 0, -23, 0, -128, 0, -102, 0, -27, 0, -83, 0, -105, 0, -25, 0, -84, 0, -90, 0, 49, 0, 50, 0, 51, 0, 13, 0, 10]
问题字符匹配结果: false
普通字符匹配结果: true

分析上面的结果前需要知道一个前提,字符串在Java内部使用的utf16进行编码保存,pattern.matcher(bugStr)正则去匹配时,matcher是按String中的Unicode单元操作,也就是字符char(暂不考虑代理对)

回到字节数组的分析上,开头的[-2, -1]是0xFE 0xFF表示UTF-16 BOM(大端)说明后续每两个字节代表一个char,可以发现原来的单字节iso8859编码内容在Java的String里面是由utf16两字节表示的,且第一个字节都是用0补上。

所以原输入”公“的unicode为U+516C -> utf8编码传输到java内的字节流数据是”E5 85 AC“,转为对应的字节数组是[-27, -123, -84],这个字节数组如果按照utf8解码则会将其作为整体解析为字符“公”,但如果按iso8859解码则是一个字节一个字节解析,结果没意义,但也没有变更整个字节流结果。但是当把这个解析结果作为String对象存在内存里面时,“公”变成的三个无意义字符,会用utf16将其一个个编码。最终结果可以看到除了最前面两字节添加的标识,是在原字节基础上补一个字节,内容为0。utf16将iso8859解码的字符又编码保存到内存,此时新的字符的字节数组最终长度是原长度*2+2

在内存中调用matcher方法时,匹配的便是以[0, 13],[0, 10]这样两字节为一单位char的字符,而恰巧[0,-123]以utf16解码结果是0x85是换行终止符,也就导致输入的正常中文在一系列编码解码后的字符串,经过正则匹配时匹配失败,因为默认情况“.”匹配除了终止符的任意字符

1
2
3
4
5
6
7
8
Java byte: -123
无符号值: -123 & 0xFF = 133
十六进制: 133 = 0x85

字节数组: [0, -123]
十六进制: [0x00, 0x85]
UTF-16解码: (0x00 << 8) | 0x85 = 0x0085
结果: Unicode码点 U+0085 代表:NEL (Next Line) 终止符

结论:由于中文字符“公”的utf8编码的三个字节的某个字节用byte表示是-123,经过iso8859单字节解码为一个无意义字符完整保留,而该无意义字符char在java内存里面是以utf16编码保存(用byte表示则是[0,-123],转为unicode则是U+0085)恰好是终止符NEL,导致最后在匹配正则时匹配失败

解决办法

  1. 字符集一致:这个是最好的。但实际上之前这边采用iso8859解码就是因为采用utf8有问题才换的。具体原因是在业务中,字符串是作为表单的一部分传入且表单还有二进制的文件,且传入的字节数组是完整的表单数据,如果用utf8解码会损坏文件。为什么不用Spring提供的注解或方法提取表单?因为业务代码实际在非mvc的reactor网关,用的ModifyRequestBodyGatewayFilterFactory类直接拿的二进制数组进行处理

  2. 采用正则匹配DOTALL模式

    1
    2
    3
    Pattern pattern = Pattern.compile(patternStr);
    // 改为使用DOTALL模式
    Pattern compatiblePattern = Pattern.compile(patternStr, Pattern.DOTALL);

此种方式需要重新测试是否有异常情况

找到基本中文字符中存在这样问题的中文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public static void wordWith0x75(){
int count=0;
// CJK统一汉字基本区:U+4E00 ~ U+9FFF
for (int codePoint = 0x4E00; codePoint <= 0x9FFF; codePoint++) {
char ch = (char) codePoint;
String s = String.valueOf(ch);

byte[] bytes = s.getBytes(StandardCharsets.UTF_8);

// UTF-8 编码的汉字应该是 3 字节
if (bytes.length == 3) {
boolean has85 = false;
for (byte b : bytes) {
// Java byte 范围是 -128~127,需要转成无符号
int unsigned = b & 0xFF;
if (unsigned == 0x85) {
has85 = true;
break;
}
}
if (has85) {
// 打印字符 + UTF-8字节的十六进制表示
System.out.printf("U+%04X %s -> %s%n", codePoint, ch, bytesToHex(bytes));
count++;
}
}
}
System.out.printf("基本中文共有%d个符合要求",count);
}

private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < bytes.length; i++) {
if (i > 0) sb.append(" ");
sb.append(String.format("0x%02X", bytes[i] & 0xFF));
}
sb.append("]");
return sb.toString();
}

基础中文共有643个符合要求

安装NVIDIA驱动和CUDA

背景

Linux环境跑模型项目需要安装CUDA依赖

环境

  • Debain12
  • 3070

安装cuda和驱动

官方链接:https://developer.nvidia.com/cuda-downloads

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
wget https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo add-apt-repository contrib
sudo apt-get update

# cuda工具包
sudo apt-get -y install cuda-toolkit-12-6
# 驱动
sudo apt-get install -y cuda-drivers

# 改下环境变量
nano ~/.bashrc
# 加入下面内容
export PATH=/usr/local/cuda/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH

# 改完应用一下
source ~/.bashrc

# 查看是否安装成功
nvidia-smi

# 查看驱动编译器是否安装成功
nvcc --version

安装cuDNN

安装完上面的驱动后,cuDNN需要单独安装,官方链接(https://developer.nvidia.com/cudnn-downloads)

1
2
# 安装cuda12
sudo apt-get -y install cudnn-cuda-12

安装实时语音转文字WhisperStream

背景

想要实现实时语音转文字,并需要通过web共享出去,WhisperStreamWeb项目非常合适

环境

  • Debain12
  • 3070
  • CUDA:12.7
  • cuDNN:9.6.0

步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 安装ffmpeg依赖
sudo apt install ffmpeg


# 建好conda虚拟环境,需要安装miniconda
conda create --name tts python=3.10.6

# 激活虚拟环境
conda activate tts

# 拉取目标仓库
git clone https://github.com/QuentinFuxa/whisper_streaming_web && cd whisper_streaming_web

# 安装依赖
pip install librosa soundfile

# Whisper streaming web required dependencies
pip install fastapi ffmpeg-python

# 安装后端依赖,这里我用的本地GPU所以选择faster-whisper, 需求有前置要求要安装好cuda,参考[官方链接](https://developer.nvidia.com/cuda-downloads) 或[安装cuda](luyublog.com/InstallNvidiaDriverAndCUDA)

# 安装后端
pip install faster-whisper

# 安装音频控制器用来检测是否有声音在说话
pip install torch torchaudio

# 安装uvicorn作为服务器
pip install uvicorn
pip install websockets

# 检测下是否正常输出
python whisper_fastapi_online_server.py -h

# 启动:
python whisper_fastapi_online_server.py --host 0.0.0.0 --port 8000 --model large-v3-turbo --backend faster-whisper --vac --vad

# 带上 --log-level DEBUG参数查看debug信息
--log-level DEBUG

报错

缺失ffmpeg

描述:FileNotFoundError: [Errno 2] No such file or directory: ‘ffmpeg’

原因:系统中缺少实际的 ffmpeg 可执行文件。ffmpeg-python 是 Python 的一个接口,用于与 ffmpeg 交互,但它需要依赖操作系统上安装的 ffmpeg 二进制程序。

解决:

1
2
# 安装ffmpeg
sudo apt install ffmpeg

libcudnn_ops.so无法加载

描述:Unable to load any of {libcudnn_ops.so.9.1.0, libcudnn_ops.so.9.1, libcudnn_ops.so.9, libcudnn_ops.so}
Invalid handle. Cannot load symbol cudnnCreateTensorDescriptor

原因:找不到libcudnn_ops.so系列库。缺乏正确版本的 cuDNN,CUDA Toolkit和cuDNN不是一个东西,CUDA Toolkit默认不会包含cuDNN库

解决:安装cuDNN

电脑浏览器访问本地端口识别失败

原因:

  • 本地ip是http,浏览器默认禁止了麦克风访问权限
  • 如果不是本机启动,需要将ws地址改为目标ip地址

解决:

  • 浏览器访问”edge://flags/“ 找到Insecure origins treated as secure特性,把地址加入白名单

  • 把ws地址改为实际地址

手机浏览器识别失败

原因:

  • 手机浏览器获取输入设备失败,可能是因为http的原因,尝试换成https

解决: 使用nginx开启https反代处理成功

Git多用户环境隔离提交

多用户仓库提交

背景

想要将开源项目和日常使用的两个账号区分开来,各自推送代码。

具体原理就是通过token结合远程仓库地址和本地用户信息配置完成git隔离。

步骤

  1. github个人页面选择setting->developer settings->personal access tokens创建两个账号各自的token,token记得赋予读写权限

  2. 修改工程的远程地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 查看一下当前仓库配置信息,主要是看下源仓库远程链接
    git config --local -l

    # 删除当前关联关系
    git remote rm origin

    # 关联新格式的远程仓库
    # 举个例子,假如我的仓库为https://github.com/luyublog/myrepo.git
    # 那么替换后的格式即为https://luyublog:token@github.com/luyublog/myrepo.git
    git remote add origin https://your_user_name:your_token@github.com/your_user_name/your_repo_name.git

    # 拉取仓库信息
    git fetch

    # 重设一下跟踪分支,这里我直接关联的main分支,创建的早的可能是master
    git branch --set-upstream-to=origin/main

遇到的问题

pull时报错:当前分支没有跟踪信息

这个错误信息表示当前分支没有和任何远程分支建立跟踪关系,所以Git不知道你想要从哪个远程分支pull更新,关联一个远程分支即可,这里我直接关联main分支

1
git branch --set-upstream-to=origin/main

git设置的本地提交名称失效

具体现象就是即使通过git config user.name name设置了名称,最后提交人员也是github的用户名。不想解决。

一些常用命令

1
2
3
4
5
6
7
8
9
# 查看仓库的本地配置
git config --local -l

# 全局配置
git config --global -l

# 系统级配置
git config --system -l

linux上启用comfast无线网卡

背景

用u盘给电脑装上debian12后发现之前的comfast无线网卡无法正常工作,之后查看商品页面发现确实没写支持linux设备,所以只能试试自己收集驱动启动

环境

  • 系统: debian12
  • 无线网卡型号:comfast 926AC

步骤

1. 收集固件

这里建议先跟随这篇文章查询一下自己的网卡信息,如果有具体信息则可以不用往下看,直接按文章操作即可。

但我的ID是0e8d:2870,在网站上只有一行描述:”This is the emulated CD-ROM driver storage device that comes with some WiFi adapters.”

CD-ROM一般自动挂载在/media下,我检查后发现里面是.exe后缀的驱动安装程序,那就只能自行寻找驱动了。

通过谷歌获取到0e8d:2870对应的芯片是Mediatek MT7612U,通常应该加载的是mt76系列模块

随后通过命令安装对应模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装通用模块,这里如果找不到包需要额外添加non-free软件源
sudo apt install firmware-misc-nonfree

# 加载我需要的mt76模块 MT7612U 属于 MT76x2 系列设备。
sudo modprobe mt76x2u

# 查看模块是否成功加载
lsmod | grep mt76

# 查看设备识别信息
sudo dmesg | grep mt76

# 最关键的查看是否出现无线网卡
ip link show

我运行ip link show后并没有看到对应网卡出现,通过lsusb发现网卡还是被识别为”Bus 001 Device 011: ID 0e8d:2870 MediaTek Inc. Љ” 和之前看到的一样,还是只是一个CD-ROM设备,也就是说可能光有驱动还不行,还要从软件层面开启

2. 转换网卡形态

目前虽然有了对应驱动,但是网卡还是被识别为CD-ROM,通过CHATGPT了解到应该是因为作为免驱无线网卡,默认以CD-ROM模式启动,我们需要将其转换为网卡形态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装对应工具
sudo apt install usb-modeswitch

# 切换形态,以我为例,我的设备ID为0e8d:2870,那么我的切换命令为
sudo usb_modeswitch -v 0e8d -p 2870 -R

# 查看网卡信息,这时应该有一个新网卡出现
ip link show

# 再运行lsusb应该就会看到设备名称变更了 我的从"MediaTek Inc. Љ"变为"MediaTek Inc. MT7612U 802.11a/b/g/n/ac Wireless Adapter"
lsusb

# 也可以查看下设备识别日志里的相关信息
sudo dmesg | tail -n 50

3. 激活并连接wifi

网卡转换后可以插拔后运行”ip link show”来检查识别是否正常,正常的话就可以激活并连接wifi了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 激活对应网卡,这里的wln0是你运行ip link show对应的无线网卡名称
sudo ip link set wln0 up

# 安装网络管理工具,方便连接wifi
sudo apt install network-manager

# 查看可连接wifi列表
sudo nmcli dev wifi list

# 连接
sudo nmcli dev wifi connect "wifi名称" password "密码"

# 查看连接状态和网络接口信息:
nmcli connection show
nmcli dev status

4. 开不了机

有一段时间没运行debian,今天开机发现显卡在工作但无输出,显示器不显示,怀疑根本没开机,把无线网卡拔掉重启下就正常了,暂时没时间就不追究根因了

win11安装双系统

背景

记录一下win11装双系统流程

参考

根据Windows10+Ubuntu18.04双系统简单安装指北跟随操作即可

步骤

  1. windows系统下找到目标盘压缩卷
  2. 使用rufus制作启动盘
  3. 进入bois选择u盘启动
  4. 安装系统,选择graphic install,分盘方式选新手推荐
  5. bois按需调整启动顺序

问题

无线网卡没网

一般的windows平台免驱无线网卡都不支持linux,改为网线连接后执行”sudo dhclient enp4s0”通过dhcp自动获取网络

尝试激活无线网卡

参考:linux启用comfast无线网卡

apt更新失败

默认源来自于iso镜像,换为网络源后再update

创建news2tg的docker镜像

创建镜像

1.创建dockerfile文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 使用多阶段构建
FROM rust:latest AS rust-builder
WORKDIR /usr/src/news2tg

# 安装git,protobuf-compiler等工具
RUN apt-get update && apt-get install -y git && apt-get install -y protobuf-compiler
RUN protoc --version

# 从GitHub克隆Rust仓库
RUN git clone https://github.com/cheedonghu/news2tg.git .
RUN cargo build --release

FROM python:3.11
WORKDIR /app

RUN apt-get update && apt-get install -y git

# 从GitHub克隆Python仓库
RUN git clone https://github.com/cheedonghu/hacker-news-digest.git .

# 安装Python依赖
RUN pip install --no-cache-dir -r ./page_content_extractor/requirements.txt

# 从Rust构建阶段复制编译好的二进制文件
COPY --from=rust-builder /usr/src/news2tg/target/release/news2tg /app/news2tg

# 创建配置文件目录
RUN mkdir /config

# 创建日志目录
RUN mkdir /logs

# 设置环境变量指向配置文件位置
ENV RUST_CONFIG_PATH=/config/config.toml

# 暴露端口(其实没必要暴露)
EXPOSE 50051

# 下载 wait-for-it.sh 脚本
RUN curl -o /usr/local/bin/wait-for-it.sh https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh
RUN chmod +x /usr/local/bin/wait-for-it.sh

# 启动命令
CMD ["sh", "-c", "python -m page_content_extractor.main & wait-for-it.sh localhost:50051 -- ./news2tg -c $RUST_CONFIG_PATH >> /logs/news2tg.log 2>&1"]

2.构建镜像

1
2
# 最好在网络通畅环境下运行,避免网络问题
docker build -t news2tg .

3.(可选)推送到dockerhub

这里我build机器和运行的机器不一致所以需要推送后在另一台机器拉取镜像运行。

1
2
3
4
5
6
7
8
# 先登录
docker login

# 再给镜像加上标签
docker tag news2tg cheedonghu/news2tg:latest

# 推送镜像
docker push cheedonghu/news2tg:latest

其他方式:github workflow

  1. 在 GitHub 仓库的 Settings -> Secrets 页面中添加以下 Secrets
  • DOCKER_USERNAME:您的 Docker Hub 用户名
  • DOCKER_PASSWORD:您的 Docker Hub 密码或访问令牌
  • 确保 Dockerfile 位于仓库根目录中
  1. 选择项目下Actions->Docker image->Configure

  2. 创建yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    name: News2tg Docker Image CI

    on:
    push:
    branches: [ "main" ]

    jobs:

    build:

    runs-on: ubuntu-latest

    steps:
    - name: Checkout repository
    uses: actions/checkout@v2

    # 登录到 Docker Hub(需要在仓库的 Secrets 中配置 DOCKER_USERNAME 和 DOCKER_PASSWORD)
    - name: Login to Docker Registry
    uses: docker/login-action@v2
    with:
    username: ${{ secrets.DOCKER_USERNAME }}
    password: ${{ secrets.DOCKER_PASSWORD }}

    # 使用 docker/build-push-action 构建并推送镜像
    - name: Build and push Docker image
    uses: docker/build-push-action@v3
    with:
    context: .
    file: ./Dockerfile
    platforms: linux/amd64
    push: true
    tags: ${{ secrets.DOCKER_USERNAME }}/news2tg:latest

使用镜像

步骤

1
2
3
4
5
6
7
8
9
10
11
# 1. 创建docker-compose.yml 按需复制下面的yml
nano docker-compose.yml

# 2. 创建挂载的文件夹
mkdir ./logs && mkdir ./config

# 3. 把配置文件放在config文件夹下
nano ./config/config.toml

# 4. 运行
docker compose up -d

使用本地构建的镜像

1
2
3
4
5
6
7
services:
news2tg:
image: news2tg
volumes:
- ./config:/config
- ./logs:/logs
restart: unless-stopped

使用dockerhub上的镜像

1
2
3
4
5
6
7
services:
news2tg:
image: cheedonghu/news2tg:latest
volumes:
- ./config:/config
- ./logs:/logs
restart: unless-stopped

更新镜像

1
2
3
4
5
# 更新镜像至最新版本
1. sudo docker compose pull news2tg

# 在docker-compose.yml同级目录执行重建并启动
2. sudo docker compose up -d

其他方式

如果是本机直接跑则构建运行即可。

1
2
3
4
5
6
7
8
9
10
services:
news2tg:
build:
context: .
dockerfile: path/to/your/Dockerfile
volumes:
- ./config:/config
- ./logs:/logs
restart: unless-stopped

问题

docker启动失败

原因:1.目录权限问题导致配置映射失败 2.老配置文件导致启动失败

SpringAI简单使用

SpringAI

介绍

SpringAI

了解原因

  1. AI趋势
  2. Spring生态

特性

  1. 支持大量模型OpenAI, Microsoft, Amazon, Google, and Huggingface.
  2. 支持大量模型输出:问答,做图,语音处理等
  3. 方便使用的已封装API接口:用户交互api,模型交互api
  4. 支持大量的向量数据库及相关交互
  5. 支持模型函数调用
  6. SpringBoot自动配置支持
  7. ETL(Extract, Transform,and Load) framework for Data Engineering

使用案例

  1. 基于大模型的问答
  2. 基于大模型的文生图

基于大模型的文档问答

实现方式

  1. Fine Tuning

  2. Prompt Stuffing

  3. Function Calling

Fine Tuning

将目标数据与模型一起训练

优点:

  • 与模型一体,不用额外处理

缺点:

  • 重新训练非常耗费资源

Prompt Stuffing

Prompt填充,代表技术:Retrieval Augmented Generation (RAG),先从向量数据库中获取相关信息然后填充到prompt中一起发送给大模型,从而达到大模型引用外部知识目的

将文档存入向量数据库这一步骤很重要,大致分为两步

  1. 将文档按照语义边界进行分割。避免将同一语义信息从中间进行分割(例如不要把一个function从中间分割了)
  2. 将文档进一步分割使其大小符合模型token限制

优点:

  • 方便灵活

缺点:

  • 受prompt限制,受相似度查询准确率限制,受大模型稳定性限制

Function Calling

大模型在训练完成后知识库便固定了正常无法获取外部数据,函数调用技术允许提供外部数据给大模型使用。

优点:

  • 灵活

缺点:

  • 不是所有大模型都有函数接口

SpringAI实现基于大模型的文档问答示例

环境信息

  • 框架:SpringAI 0.8.1
  • 大模型:LLAMA3-7B(ollama本地部署)
  • 检索方式:RAG - Prompt Stuffing
  • 向量数据库:Neo4j

核心代码

没啥特别的基本都有starter,按文档往pom和配置文件加下参数就能跑起来

这里更多的难点是在如何更好与大模型交互,在于prompt的编写

出现的问题记录

由于是SpringAI早期开发版本所以会有各种各样问题

1.Neo4j的向量维度限制

当前Neo4j的配置类Neo4jVectorStoreConfig源码将向量维度限制在了2048,而llama3模型为4096

1
2
3
4
5
public Builder withEmbeddingDimension(int newEmbeddingDimension) {
Assert.isTrue(newEmbeddingDimension >= 1 && newEmbeddingDimension <= 2048, "Dimension has to be withing the boundaries 1 and 2048 (inclusively)");
this.embeddingDimension = newEmbeddingDimension;
return this;
}

临时性的解决措施:通过反射修改bean的属性

2.Pinecone数据库相似度查询

当使用Pinecone作为向量数据库时,进行相似度查询时,相似度度量方式有特定限制,采用cosine度量可能会导致查询不到目标信息。这是PineconeVectorStorec.class源码,注意其中score部分限制

1
2
3
4
5
6
7
8
9
10
11
12
return queryResponse.getMatchesList()
.stream()
.filter(scoredVector -> scoredVector.getScore() >= request.getSimilarityThreshold()) // here my socres are negative numbers
.map(scoredVector -> {
var id = scoredVector.getId();
Struct metadataStruct = scoredVector.getMetadata();
var content = metadataStruct.getFieldsOrThrow(CONTENT_FIELD_NAME).getStringValue();
Map<String, Object> metadata = extractMetadata(metadataStruct);
metadata.put(DISTANCE_METADATA_FIELD_NAME, 1 - scoredVector.getScore());
return new Document(id, content, metadata);
})
.toList();

解决办法: 社区反馈会在1.0.0-M1版本支持更多度量方式,或者自己重写(然后去PR)

完整Demo

demo

Java模板,泛型,通配符使用

背景

模板,泛型,通配符的使用记录

下单示例

从一个下单的例子来记录下模板与通配符的使用

定义接口

下单一般流程为报文定义,报文核对,落表数据生成,落表,落表后操作等等这些,具体视业务情况而定,这里主要记录模板泛型通配符,后续就简化成只操作一个落表接口

报文接口

这里定义一个报文接口的目的是将不同渠道格式的报文定义一个统一的内容(比如直接返回部分初始化的数据表对象),这样实现类就不用依赖于具体的某个请求报文而是下单报文接口(依赖倒置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

/**
* 下单报文接口
* @author: east
* @date: 2023/11/23
*/
public interface OrderRequest<T extends OrderInfo> {
/**
* 将不同渠道报文转为T的统一格式数据(比如转为落表所需数据)
* 通过T extends OrderInfo限制模板类型为OrderInfo及其子类
*
* @return T 统一数据
*/
public T generateOrderInfo();
}

/**
* 账单通用数据
*/
public class OrderInfo {
protected BigDecimal amt;
protected String serial;
protected String rcvNo;

public OrderInfo(BigDecimal amt, String serial, String rcvNo) {
this.amt = amt;
this.serial = serial;
this.rcvNo = rcvNo;
}

public OrderInfo() {
}

@Override
public String toString() {
return "OrderInfo{" +
"amt=" + amt +
", serial='" + serial + '\'' +
", rcvNo='" + rcvNo + '\'' +
'}';
}
}

下单接口

这里简化一下,只定义落表接口。落表接口接收不同类型的要落表信息,但要落表信息都是基础落表信息(BaseSavedInfo.java)的子类,所以使用”extends SavedInfo”关键字去限制一下,具体的实现类则必须传入SavedInfo或者其子类。因此这里可以使用模板接口或者模板方法,定义方法,这两种方法各有其特点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 下单要落表的最基本信息,如果除了账单信息,其他渠道需要落特别的数据表,则基础本类并重写Save接口即可,满足开闭原则
* 账单信息
* 其他信息自行继承本类实现
*
* @author: east
* @date: 2023/11/25
*/
public class BaseSavedInfo {
protected OrderInfo orderInfo;

public BaseSavedInfo(OrderInfo orderInfo) {
this.orderInfo = orderInfo;
}

@Override
public String toString() {
return "SavedInfo{" +
"orderInfo=" + orderInfo +
'}';
}
}
模板方法接口Save

这里Save接口有一个模板方法save,他的方法模板类型限制在BaseSavedInfo及其子类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 保存下单相关信息, 使用模板方法
*
* @author: east
* @date: 2023/11/23
*/
public interface Save {
/**
* 保存下单数据
*
* @param savedInfo 相关信息:账单表,统计表
*/
public <T extends BaseSavedInfo> void save(T savedInfo);
}
模板接口SaveT

这里定义的为一个模板接口SaveT,模板范围限制在BaseSavedInfo及其子类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 保存下单相关信息, 模板类
*
* @author: east
* @date: 2023/11/23
*/
public interface SaveT<T extends BaseSavedInfo> {
/**
* 保存下单数据
*
* @param savedInfo 相关信息:账单表,统计表
*/
public void save(T savedInfo);
}

下单行为抽象类

这里设置一个下单行为的抽象类目的是为了将下单流程组装起来,因为涉及到Save接口作为依赖,所以接口不适用需要换为抽象类。这里抽象类的定义可以分为三种

使用Save接口的抽象下单类:方式一
1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class AbstractOrder<T extends OrderRequest<?>> {
// 使用Save接口作为其依赖
protected Save save;

public AbstractOrder(Save save) {
this.save = save;
}

/**
* 下单接口;T为实现了OrderRequest接口的请求报文
*/
public abstract void order(T orderRequest);
}
  • 这里的”<T extends OrderRequest<?>” 表示AbstractOrder是一个模板类,且其实现类的模板类型限制在OrderRequest接口,这样做的好处是所有下单虚函数的实现类都实现了OrderRequest接口,从而统一下单行为,满足同一个标准(当然也可以不指定),后面的’?’表示任意通配符,也就是OrderRequest接口的泛型这里不关心

  • AbstractOrder抽象下单类使用Save接口作为其依赖因此直接定义Save类型的成员变量即可。如果是SaveT类型则需要特别处理

  • “public abstract void order(T orderRequest);” 表示这是一个抽象下单接口,接收一个模板类型参数,也就是实现了OrderRequest接口的请求报文体。该报文体根据下单渠道自行定义

使用SaveT接口的抽象下单类: 方式二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class AbstractOrderTR<T extends OrderRequest<?>, R extends BaseSavedInfo> {
// 使用SaveT接口作为其依赖
// 用模板指定了参数的类型为R
protected SaveT<R> save;

public AbstractOrderTR(SaveT<R> save) {
this.save = save;
}

/**
* 下单
*/
public abstract void order(T orderRequest);
}
  • 这里AbstractOrderTR抽象下单类使用SaveT接口作为其依赖,而且在类上通过模板”R extends BaseSavedInfo”指定SaveT模板类型为BaseSavedInfo及其子类
使用SaveT接口的抽象下单类: 方式三
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class AbstractOrderT<T extends OrderRequest<?>> {
// 使用SaveT接口作为其依赖
// 不通过了类模板指定,仅用通配符参数限定,
protected SaveT<? extends BaseSavedInfo> save; // note 如果实现类中不指定SaveT的类型,则save接口无法使用
// 原因: 编译器没那么聪明根据子类传入的saveT子对象推断出SaveT模板具体对应哪一个类,只能在这里明确指定或者子类重写

// 也可以为SaveT<?> 仅通配符不做限制
// protected SaveT<?> save;

// 也可以不限制,仅使用SaveT
// protected SaveT save;

public AbstractOrderT(SaveT<? extends BaseSavedInfo> save) {
this.save = save;
}

/**
* 下单
*/
public abstract void order(T orderRequest);
}
  • 这里AbstractOrderTR抽象下单类使用SaveT接口作为其依赖,但不像方式二通过模板来限制SaveT模板类型,而是使用SaveT<? extends BaseSavedInfo>通配符,来限制该模板类型仅为BaseSavedInfo及其子类,当然也可以不限制仅用SaveT<?>和SaveT。但是SaveT和(SaveT<?>,SaveT<? extends BaseSavedInfo>)不一样,SaveT并没有使用通配符,它更类似于方式一的用法,只是一个基类。而SaveT<?>和SaveT<? extends BaseSavedInfo>因为都作为通配符使用,所以在AbstractOrderTR的实现类AbstractOrderTRImp中不能直接使用,因为编译器并不能确定此处SaveT的具体类型,详见

定义实现

这里用外部渠道下单和内部渠道下单来模拟实现。一些实体类定义不具体展示,详细可在这里查看完整包

内部渠道下单实现类

InnerOrder类继承了抽象下单类AbstractOrder(方式一),并指定了模板类型为InnerOrderRequest(内部请求报文)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* 内部下单实现类
*
* @author: east
* @date: 2023/11/24
*/
public class InnerOrder extends AbstractOrder<InnerOrderRequest> {

// InnerSaveOrder为Save接口实现类
public InnerOrder(InnerSaveOrder save) {
super(save);
}

//InnerOrderRequest为OrderRequest实现类
@Override
public void order(InnerOrderRequest innerOrderRequest) {
try {

// 校验等接口调用....

// InnerBaseSavedInfo为内部渠道需要落表的数据,是BaseSavedInfo的子类
// 落表 这里save为模板方法
save.save(new InnerBaseSavedInfo(innerOrderRequest.generateOrderInfo(), "this is inner special message"));

} catch (Exception e) {
throw new RuntimeException(e);
}
}

// public static void main(String[] args) {
// InnerSaveOrder innerSaveOrder = new InnerSaveOrder();
// InnerOrder innerOrder = new InnerOrder(innerSaveOrder);
// innerOrder.order(new InnerOrderRequest(BigDecimal.ONE, "innerSerial", "rcvNo"));
// }

}

/**
* 内部渠道保存账单
* Save接口的实现类
*
* @author: east
* @date: 2023/11/24
*/
public class InnerSaveOrder implements Save {

/**
* 保存下单数据
*
* @param savedInfo 相关信息:账单表,统计表
*/
@Override
public <T extends BaseSavedInfo> void save(T savedInfo) {

// 落表
System.out.println("内部渠道下单信息开始落表");

// todo 这里有个很大的问题就是如果不通过强制类型转换无法拿到特定种类的数据
// InnerSavedInfo innerSavedInfo=(InnerSavedInfo) savedInfo;
// System.out.println("内部渠道特有数据获取: "+innerSavedInfo.getSpecialMsg());

System.out.println(savedInfo.toString());
}
}
  • 从代码里面可以看到Save接口采用模板方法这种方式有个麻烦的地方在于实现类中因为是重写save方法所以也必须是模板方法,虽然这时已经明确知道参数类型为BaseSavedInfo的实现类InnerSavedInfo,但是在方法中无法直接获取其特定属性(比如获取InnerSavedInfo子类独有的字段),暂时没明白除了强制类型转换外该怎么解决

外部渠道下单实现类(AbstractOrderTR)

OuterOrderTR继承了下单虚函数AbstractOrderTR(方式二),并指定了AbstractOrderTR的两个模板为OuterOrderRequest和OuterBaseSavedInfo,从而明确指定了抽象类的SaveT这个成员函数的模板类型为OuterBaseSavedInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class OuterOrderTR extends AbstractOrderTR<OuterOrderRequest, OuterBaseSavedInfo> {

public OuterOrderTR(OuterSaveOrderImp save) {
super(save);
}

@Override
public void order(OuterOrderRequest outerOrderRequest) {
try {

// 校验等逻辑....

// 落表
// note 这里通过模板类全量指定
save.save(new OuterBaseSavedInfo(outerOrderRequest.generateOrderInfo()));

} catch (Exception e) {
throw new RuntimeException(e);
}
}

// public static void main(String[] args) {
// OuterSaveOrderImp saveOrderAction = new OuterSaveOrderImp();
// OuterOrderTR orderAction = new OuterOrderTR(saveOrderAction);
// orderAction.order(new OuterOrderRequest(BigDecimal.ONE, "outSerialTR", "rcvNo"));
// }

}

外部渠道下单实现类(AbstractOrderT)

这里OuterOrderT继承了AbstractOrderT虚函数方式三,成员函数SaveT<? extends BaseSavedInfo> save只通过上界通配符限定,我一开始觉得编译器能通过传入的OuterBaseSavedInfo类对象来初始化SaveT对象时推断出SaveT的模板类型为OuterBaseSavedInfo而不是InnerBaseSavedInfo(BaseSavedInfo的另一个实现类),但实际上编译器好像无法推测出save的类型导致SaveT对象的public void save(T savedInfo)方法无法接收任何参数(除了null),这里和List<? extends Number> list无法list.add(1)问题很像,调用save方法时会导致类型安全问题,无法编译通过。

相关解释

解决办法为明确指定SaveT模板类型

1
protected SaveT<OuterBaseSavedInfo> save;

类定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class OuterOrderT extends AbstractOrderT<OuterOrderRequest> {

protected SaveT<OuterBaseSavedInfo> save; // note 通配符如果不在模板指定,则只有在这里明确指定,save的方法才能正常调用, 可以将这行去掉试试

public OuterOrderT(OuterSaveOrderImp save) {
super(save);
this.save = save;
}

@Override
public void order(OuterOrderRequest outerOrderRequest) {
try {

// 校验等逻辑....

// 落表
// 通过父类调用 note: 这里没有通过模板指定(如果也不在成员函数指定的话则会报错,? extends通配只是占位,不指定类型则无法操作
save.save(new OuterBaseSavedInfo(outerOrderRequest.generateOrderInfo()));

} catch (Exception e) {
throw new RuntimeException(e);
}
}

// public static void main(String[] args) {
// OuterSaveOrderImp saveOrderAction = new OuterSaveOrderImp();
// OuterOrderT orderAction = new OuterOrderT(saveOrderAction);
// orderAction.order(new OuterOrderRequest(BigDecimal.ONE, "outSerialT", "rcvNo"));
// }

}

总结

  • 上面的三个实现类大概展示了通配符与模板使用的示例,以及一些设计原则(依赖倒置,接口隔离,单一,开闭)的使用,以及其中的一些坑,建议大家结合具体的示例或实际的业务来理解。

  • 对于上界和下界通配符,个人感觉<? extends ClassA>这种用得多一点,这种相当于指定了一个规范ClassA,后续传入的参数都要实现这个规范ClassA或者继承ClassA。至于上下界的定义为什么那么叫,个人感觉没那么重要

完整代码见:

简略版

详细版

docker安装kafka

背景

最近学习到Kafka相关知识,所以用docker在vps上安装单节点版和集群版方便后续学习

环境

  • Debian10
  • Kafka:

单节点版

使用docker compose安装。注意在yml目录下创建一个data目录放上两个文件message.json和update_run.sh,做好权限修改后up一下就行

docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
version: '2'
services:

kafka1:
image: confluentinc/cp-kafka:7.2.1
hostname: kafka1
container_name: kafka1
ports:
- "9092:9092"
- "9997:9997"
environment:
KAFKA_BROKER_ID: 1
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'
KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka1:29092,PLAINTEXT_HOST://154.40.46.38:9092'
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_JMX_PORT: 9997
KAFKA_JMX_HOSTNAME: localhost
KAFKA_PROCESS_ROLES: 'broker,controller'
KAFKA_NODE_ID: 1
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka1:29093'
KAFKA_LISTENERS: 'PLAINTEXT://kafka1:29092,CONTROLLER://kafka1:29093,PLAINTEXT_HOST://0.0.0.0:9092'
KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
volumes:
- ./data/update_run.sh:/tmp/update_run.sh
command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'"

schemaregistry1:
image: confluentinc/cp-schema-registry:7.2.1
ports:
- 18085:8085
depends_on:
- kafka1
volumes:
- ./data/jaas:/conf
environment:
SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka1:29092
SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT
SCHEMA_REGISTRY_HOST_NAME: schemaregistry1
SCHEMA_REGISTRY_LISTENERS: http://schemaregistry1:8085

# ui那边SCHEMA模块默认账户密码: admin/letmein
SCHEMA_REGISTRY_AUTHENTICATION_METHOD: BASIC
SCHEMA_REGISTRY_AUTHENTICATION_REALM: SchemaRegistryProps
SCHEMA_REGISTRY_AUTHENTICATION_ROLES: admin
SCHEMA_REGISTRY_OPTS: -Djava.security.auth.login.config=/conf/schema_registry.jaas

SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: "http"
SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO
SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas

kafka-init-topics:
image: confluentinc/cp-kafka:7.2.1
volumes:
- ./data/message.json:/data/message.json
depends_on:
- kafka1
command: "bash -c 'echo Waiting for Kafka to be ready... && \
cub kafka-ready -b kafka1:29092 1 30 && \
kafka-topics --create --topic users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:29092 && \
kafka-topics --create --topic messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:29092 && \
kafka-console-producer --bootstrap-server kafka1:29092 --topic users < /data/message.json'"

# ui控制
kafka-ui:
container_name: kafka-ui
image: provectuslabs/kafka-ui:latest
ports:
- 18080:8080
depends_on:
- kafka1
- schemaregistry1
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka1:29092
KAFKA_CLUSTERS_0_METRICS_PORT: 9997
KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schemaregistry1:8085
KAFKA_CLUSTERS_0_SCHEMAREGISTRYAUTH_USERNAME: admin
KAFKA_CLUSTERS_0_SCHEMAREGISTRYAUTH_PASSWORD: letmein
AUTH_TYPE: "LOGIN_FORM"
SPRING_SECURITY_USER_NAME: username
SPRING_SECURITY_USER_PASSWORD: passwd

ref: https://blog.csdn.net/dghkgjlh/article/details/133418837

https://flxdu.cn/2023/01/23/Kafka-in-Docker-%E5%8D%95%E8%8A%82%E7%82%B9-%E5%A4%9A%E8%8A%82%E7%82%B9/

update_run.sh

1
2
3
4
5
6
7
8
9
10
11
# This script is required to run kafka cluster (without zookeeper)
#!/bin/sh

# Docker workaround: Remove check for KAFKA_ZOOKEEPER_CONNECT parameter
sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure

# Docker workaround: Ignore cub zk-ready
sed -i 's/cub zk-ready/echo ignore zk-ready/' /etc/confluent/docker/ensure

# KRaft required step: Format the storage directory with a new cluster ID
echo "kafka-storage format --ignore-formatted -t $(kafka-storage random-uuid) -c /etc/kafka/kafka.properties" >> /etc/confluent/docker/ensure

message.json

放个空的就行

1
{}

Kafka集群版

这里用多个docker模拟集群的多节点, 直接复制yml然后up就行

参考

docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# Copyright VMware, Inc.
# SPDX-License-Identifier: APACHE-2.0

version: "2"

services:
kafka-0:
image: docker.io/bitnami/kafka:3.6
ports:
- "9092:9092"
restart: unless-stopped
environment:
# KRaft settings
# 结点序号1
- KAFKA_CFG_NODE_ID=0
# 结点角色:controller,broker(同一时间只会有一个controller)
- KAFKA_CFG_PROCESS_ROLES=controller,broker
# controller选举投票权结点
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka-0:9093,1@kafka-1:9093,2@kafka-2:9093
# 禁用自动创建topic
- KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=false
# 日志保留时间
- KAFKA_CFG_LOG_RETENTION_HOURS=168
# 单个分区大小
- KAFKA_CFG_LOG_RETENTION_BYTES=1073741824
# 启用日志压缩
- KAFKA_CFG_LOG_CLEANER_ENABLE=true
# 消息体最大为1M
- KAFKA_CFG_MESSAGE_MAX_BYTES=1000012
# ISR中最小个数,小于这个生产者可能发送失败,数据丢失风险增加,可靠性降低
- KAFKA_CFG_MIN_IN_SYNC_REPLICAS=2
# 集群ID
- KAFKA_KRAFT_CLUSTER_ID=abcdefghijklmnopqrstuv
# Listeners
# 服务监听部分,监听器PLAINTEXT(没错这只是个监听器名称)监听9092,监听器CONTROLLER监听9093端口
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
# 连接建议监听器为PLAINTEXT,端口为9092,这里为了外部访问需要指定外部ip(如果外部有代理则这里就用集群内部格式就行)
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://103.171.26.248:9092
# 映射监听器名称和使用的协议,PLAINTEXT使用的协议是PLAINTEXT,CONTROLLER也用的PLAINTEXT(比较方便)
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
# 指定用于控制器通信的监听器
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
# broker间通信使用的监听器
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT
# Clustering
# 主题消费记录偏移量的副本个数(记录消费组消费主题的偏移量的副本)
- KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=3
# Kafka 会在集群中的三个不同节点上保持事务状态日志的副本
- KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=3
# 至少有两个副本(包括领导副本)需要是同步的,才能继续事务操作
- KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2
volumes:
- kafka_0_data:/bitnami/kafka
kafka-1:
image: docker.io/bitnami/kafka:3.6
ports:
- "9093:9092"
restart: unless-stopped
environment:
# KRaft settings
- KAFKA_CFG_NODE_ID=1
- KAFKA_CFG_PROCESS_ROLES=controller,broker
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka-0:9093,1@kafka-1:9093,2@kafka-2:9093
- KAFKA_KRAFT_CLUSTER_ID=abcdefghijklmnopqrstuv
- KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=false
- KAFKA_CFG_LOG_RETENTION_HOURS=168
- KAFKA_CFG_LOG_RETENTION_BYTES=1073741824
- KAFKA_CFG_LOG_CLEANER_ENABLE=true
- KAFKA_CFG_MESSAGE_MAX_BYTES=1000012
- KAFKA_CFG_MIN_IN_SYNC_REPLICAS=2
# Listeners
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://103.171.26.248:9093
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT
# Clustering
- KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2
volumes:
- kafka_1_data:/bitnami/kafka
kafka-2:
image: docker.io/bitnami/kafka:3.6
ports:
- "9094:9092"
restart: unless-stopped
environment:
# KRaft settings
- KAFKA_CFG_NODE_ID=2
- KAFKA_CFG_PROCESS_ROLES=controller,broker
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka-0:9093,1@kafka-1:9093,2@kafka-2:9093
- KAFKA_KRAFT_CLUSTER_ID=abcdefghijklmnopqrstuv
- KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=false
- KAFKA_CFG_LOG_RETENTION_HOURS=168
- KAFKA_CFG_LOG_RETENTION_BYTES=1073741824
- KAFKA_CFG_LOG_CLEANER_ENABLE=true
- KAFKA_CFG_MESSAGE_MAX_BYTES=1000012
- KAFKA_CFG_MIN_IN_SYNC_REPLICAS=2
# Listeners
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://103.171.26.248:9094
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT
# Clustering
- KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2
volumes:
- kafka_2_data:/bitnami/kafka

volumes:
kafka_0_data:
driver: local
kafka_1_data:
driver: local
kafka_2_data:
driver: local

查看集群内的topic

1
2
3
4
5
# 进入容器
docker exec -it [容器名称或ID] /bin/bash

# 调用kafka脚本
kafka-topics.sh --bootstrap-server localhost:9092 --list

集群内新建一个topic

1
2
# 用脚本创建
kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 3 --partitions 3 --topic TOPIC_LY_TEST

查看某个topic信息

1
kafka-topics.sh --describe --topic TOPIC_LY_TEST --bootstrap-server localhost:9092