Sam's Dev Life

探索技术,记录生活

使用Spring ai和Ollama完全私有化使用大模型

Tags: functions, ollama, 大模型

你已经安装ollama,假如有什么问题,搜索一下,非常方便,支持window,linux,macos。

执行 ollama pull llama3.1 ,下载llama3.1,登上一点时间,毕竟有好几个G需要下载。

示例说明:本例子中,我会通过创建一个加法,乘法工具,通过llama3.1模型分析文本,自动调用需要工具,在ollama中叫function,openai中的叫工具。

下面,直接上代码

  1. 代码
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
package springai;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Description;

import java.util.function.Function;

@SpringBootApplication
public class SpringOllama implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(SpringOllama.class);
}

@Autowired
ChatModel chatModel;

public static class MockCalculate {
public record Request(Double n1, Double n2) {
}

public record Response(Double r) {
}
}

public static class MockCalculateAdd implements Function<MockCalculate.Request, MockCalculate.Response> {

public MockCalculate.Response apply(MockCalculate.Request request) {
System.out.println("add request: " + request);
return new MockCalculate.Response(request.n1 + request.n2);
}
}

public static class MockCalculateMultiply implements Function<MockCalculate.Request, MockCalculate.Response> {

public MockCalculate.Response apply(MockCalculate.Request request) {
System.out.println("mul request: " + request);
return new MockCalculate.Response(request.n1 * request.n2);
}
}

public static class MockCalculateDivide implements Function<MockCalculate.Request, MockCalculate.Response> {

public MockCalculate.Response apply(MockCalculate.Request request) {
System.out.println("div request: " + request);
return new MockCalculate.Response(request.n1 / request.n2);
}
}

@Bean
@Description("calculate sum")
public Function<MockCalculate.Request, MockCalculate.Response> add() {
return new MockCalculateAdd();
}

@Bean
@Description("calculate multiplication")
public Function<MockCalculate.Request, MockCalculate.Response> multiply() {
return new MockCalculateMultiply();
}

@Bean
@Description("calculate divide")
public Function<MockCalculate.Request, MockCalculate.Response> divide() {
return new MockCalculateDivide();
}

@Override
public void run(String... args) {
ChatClient.Builder builder = ChatClient.builder(chatModel);
ChatClient chatClient = builder.build();
ChatClient.CallResponseSpec responseSpec = chatClient.prompt()
.system("you are a calculate, you need analyze the user input, and solve it step by step, use the tools to calculate the value.")
.user("hello, what is 30 increase 47%")
.functions("add", "multiply", "divide")
.call();
System.out.println(responseSpec.content());
System.exit(0);
}
}

  1. 项目文件
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
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.4'
id 'io.spring.dependency-management' version '1.1.4'
}

group = 'org.bk.langchain4j'
version = '0.0.1-SNAPSHOT'

java {
sourceCompatibility = '21'
}

ext {
langchain4jVersion = "0.32.0"
}

repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.session:spring-session-core'
implementation 'org.springframework.ai:spring-ai-ollama'
implementation 'org.springframework.ai:spring-ai-ollama-spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation platform("org.springframework.ai:spring-ai-bom:1.0.0-SNAPSHOT")
}

tasks.named('test') {
useJUnitPlatform()
}
  1. 配置文件
1
2
3
4
5
6
spring:
ai:
ollama:
chat:
options:
model: "llama3.1"
  1. 执行结果
1
2
3
4
5
6
7
8
9
10

add request: Request[n1=0.47, n2=30.0]
add request: Request[n1=14.1, n2=30.0]
To calculate the increase of 47% on 30, I will use the following steps:

1. First, I need to convert the percentage into a decimal by dividing it by 100: 47 / 100 = 0.47
2. Then, I will multiply the original value (30) by this decimal: 30 * 0.47 = 14.1
3. Finally, I will add this result to the original value: 30 + 14.1 = 44.1

Therefore, 30 increased by 47% is equal to 44.10.

GPT4o写的俄罗斯方块

几乎一次成功,Tetrominoes内部类位置放错了,导致引用的地方报错,把他挪到第一层之后就可以工作了。

游戏界面

游戏结束

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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Random;

public class Tetris extends JFrame {

private static final int BOARD_WIDTH = 10;
private static final int BOARD_HEIGHT = 22;

private Timer timer;
private boolean isFallingFinished = false;
private boolean isStarted = false;
private boolean isPaused = false;
private int numLinesRemoved = 0;
private int curX = 0;
private int curY = 0;
private JLabel statusbar;
private Shape curPiece;
private Tetrominoes[] board;

public Tetris() {
initUI();
}

private void initUI() {
statusbar = new JLabel(" 0");
add(statusbar, BorderLayout.SOUTH);
Board board = new Board(this);
add(board);
board.start();

setTitle("Tetris");
setSize(200, 400);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocationRelativeTo(null);
}

public static void main(String[] args) {
EventQueue.invokeLater(() -> {
Tetris game = new Tetris();
game.setVisible(true);
});
}

JLabel getStatusBar() {
return statusbar;
}

class Board extends JPanel implements ActionListener {

private final int BOARD_WIDTH = 10;
private final int BOARD_HEIGHT = 22;
private Timer timer;
private boolean isFallingFinished = false;
private boolean isStarted = false;
private boolean isPaused = false;
private int numLinesRemoved = 0;
private int curX = 0;
private int curY = 0;
private JLabel statusbar;
private Shape curPiece;
private Tetrominoes[] board;

public Board(Tetris parent) {
setFocusable(true);
curPiece = new Shape();
timer = new Timer(400, this);
timer.start();

statusbar = parent.getStatusBar();
board = new Tetrominoes[BOARD_WIDTH * BOARD_HEIGHT];
addKeyListener(new TAdapter());
clearBoard();
}

public void start() {
if (isPaused)
return;

isStarted = true;
isFallingFinished = false;
numLinesRemoved = 0;
clearBoard();

newPiece();
timer.start();
}

private void pause() {
if (!isStarted)
return;

isPaused = !isPaused;

if (isPaused) {
timer.stop();
statusbar.setText("paused");
} else {
timer.start();
statusbar.setText(String.valueOf(numLinesRemoved));
}

repaint();
}

@Override
public void actionPerformed(ActionEvent e) {
if (isFallingFinished) {
isFallingFinished = false;
newPiece();
} else {
oneLineDown();
}
}

private int squareWidth() {
return (int) getSize().getWidth() / BOARD_WIDTH;
}

private int squareHeight() {
return (int) getSize().getHeight() / BOARD_HEIGHT;
}

private Tetrominoes shapeAt(int x, int y) {
return board[(y * BOARD_WIDTH) + x];
}

private void clearBoard() {
for (int i = 0; i < BOARD_HEIGHT * BOARD_WIDTH; i++) {
board[i] = Tetrominoes.NoShape;
}
}

private void pieceDropped() {
for (int i = 0; i < 4; i++) {
int x = curX + curPiece.x(i);
int y = curY - curPiece.y(i);
board[(y * BOARD_WIDTH) + x] = curPiece.getShape();
}

removeFullLines();

if (!isFallingFinished) {
newPiece();
}
}

private void newPiece() {
curPiece.setRandomShape();
curX = BOARD_WIDTH / 2 + 1;
curY = BOARD_HEIGHT - 1 + curPiece.minY();

if (!tryMove(curPiece, curX, curY)) {
curPiece.setShape(Tetrominoes.NoShape);
timer.stop();
isStarted = false;
statusbar.setText("game over");
}
}

private void oneLineDown() {
if (!tryMove(curPiece, curX, curY - 1)) {
pieceDropped();
}
}

private boolean tryMove(Shape newPiece, int newX, int newY) {
for (int i = 0; i < 4; i++) {
int x = newX + newPiece.x(i);
int y = newY - newPiece.y(i);

if (x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT)
return false;

if (shapeAt(x, y) != Tetrominoes.NoShape)
return false;
}

curPiece = newPiece;
curX = newX;
curY = newY;
repaint();

return true;
}

private void drawSquare(Graphics g, int x, int y, Tetrominoes shape) {
Color colors[] = {
new Color(0, 0, 0), new Color(204, 102, 102),
new Color(102, 204, 102), new Color(102, 102, 204),
new Color(204, 204, 102), new Color(204, 102, 204),
new Color(102, 204, 204), new Color(218, 170, 0)
};

Color color = colors[shape.ordinal()];

g.setColor(color);
g.fillRect(x + 1, y + 1, squareWidth() - 2, squareHeight() - 2);

g.setColor(color.brighter());
g.drawLine(x, y + squareHeight() - 1, x, y);
g.drawLine(x, y, x + squareWidth() - 1, y);

g.setColor(color.darker());
g.drawLine(x + 1, y + squareHeight() - 1,
x + squareWidth() - 1, y + squareHeight() - 1);
g.drawLine(x + squareWidth() - 1, y + squareHeight() - 1,
x + squareWidth() - 1, y + 1);
}

private void removeFullLines() {
int numFullLines = 0;

for (int i = BOARD_HEIGHT - 1; i >= 0; i--) {
boolean lineIsFull = true;

for (int j = 0; j < BOARD_WIDTH; j++) {
if (shapeAt(j, i) == Tetrominoes.NoShape) {
lineIsFull = false;
break;
}
}

if (lineIsFull) {
numFullLines++;
for (int k = i; k < BOARD_HEIGHT - 1; k++) {
for (int j = 0; j < BOARD_WIDTH; j++) {
board[(k * BOARD_WIDTH) + j] = shapeAt(j, k + 1);
}
}
}
}

if (numFullLines > 0) {
numLinesRemoved += numFullLines;
statusbar.setText(String.valueOf(numLinesRemoved));
isFallingFinished = true;
curPiece.setShape(Tetrominoes.NoShape);
repaint();
}
}

@Override
public void paint(Graphics g) {
super.paint(g);

Dimension size = getSize();
int boardTop = (int) size.getHeight() - BOARD_HEIGHT * squareHeight();

for (int i = 0; i < BOARD_HEIGHT; i++) {
for (int j = 0; j < BOARD_WIDTH; j++) {
Tetrominoes shape = shapeAt(j, BOARD_HEIGHT - i - 1);

if (shape != Tetrominoes.NoShape) {
drawSquare(g, j * squareWidth(),
boardTop + i * squareHeight(), shape);
}
}
}

if (curPiece.getShape() != Tetrominoes.NoShape) {
for (int i = 0; i < 4; i++) {
int x = curX + curPiece.x(i);
int y = curY - curPiece.y(i);
drawSquare(g, x * squareWidth(),
boardTop + (BOARD_HEIGHT - y - 1) * squareHeight(),
curPiece.getShape());
}
}
}

private class TAdapter extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
if (!isStarted || curPiece.getShape() == Tetrominoes.NoShape) {
return;
}

int keycode = e.getKeyCode();

if (keycode == 'p' || keycode == 'P') {
pause();
return;
}

if (isPaused) {
return;
}

switch (keycode) {
case KeyEvent.VK_LEFT:
tryMove(curPiece, curX - 1, curY);
break;
case KeyEvent.VK_RIGHT:
tryMove(curPiece, curX + 1, curY);
break;
case KeyEvent.VK_DOWN:
tryMove(curPiece.rotateRight(), curX, curY);
break;
case KeyEvent.VK_UP:
tryMove(curPiece.rotateLeft(), curX, curY);
break;
case KeyEvent.VK_SPACE:
dropDown();
break;
case 'd':
oneLineDown();
break;
}
}
}

private void dropDown() {
int newY = curY;

while (newY > 0) {
if (!tryMove(curPiece, curX, newY - 1)) {
break;
}
--newY;
}

pieceDropped();
}
}

}
enum Tetrominoes {NoShape, ZShape, SShape, LineShape, TShape, SquareShape, LShape, MirroredLShape}

class Shape {

private Tetrominoes pieceShape;
private int coords[][];
private int[][][] coordsTable;

public Shape() {
coords = new int[4][2];
setShape(Tetrominoes.NoShape);
}

public void setShape(Tetrominoes shape) {
coordsTable = new int[][][]{
{{0, 0}, {0, 0}, {0, 0}, {0, 0}},
{{0, -1}, {0, 0}, {-1, 0}, {-1, 1}},
{{0, -1}, {0, 0}, {1, 0}, {1, 1}},
{{0, -1}, {0, 0}, {0, 1}, {0, 2}},
{{-1, 0}, {0, 0}, {1, 0}, {0, 1}},
{{0, 0}, {1, 0}, {0, 1}, {1, 1}},
{{-1, -1}, {0, -1}, {0, 0}, {0, 1}},
{{1, -1}, {0, -1}, {0, 0}, {0, 1}}
};

for (int i = 0; i < 4; i++) {
for (int j = 0; j < 2; ++j) {
coords[i][j] = coordsTable[shape.ordinal()][i][j];
}
}

pieceShape = shape;
}

private void setX(int index, int x) {
coords[index][0] = x;
}

private void setY(int index, int y) {
coords[index][1] = y;
}

public int x(int index) {
return coords[index][0];
}

public int y(int index) {
return coords[index][1];
}

public Tetrominoes getShape() {
return pieceShape;
}

public void setRandomShape() {
Random r = new Random();
int x = Math.abs(r.nextInt()) % 7 + 1;
Tetrominoes[] values = Tetrominoes.values();
setShape(values[x]);
}

public int minX() {
int m = coords[0][0];
for (int i = 0; i < 4; i++) {
m = Math.min(m, coords[i][0]);
}
return m;
}

public int minY() {
int m = coords[0][1];
for (int i = 0; i < 4; i++) {
m = Math.min(m, coords[i][1]);
}
return m;
}

public Shape rotateLeft() {
if (pieceShape == Tetrominoes.SquareShape)
return this;

Shape result = new Shape();
result.pieceShape = pieceShape;

for (int i = 0; i < 4; ++i) {
result.setX(i, y(i));
result.setY(i, -x(i));
}
return result;
}

public Shape rotateRight() {
if (pieceShape == Tetrominoes.SquareShape)
return this;

Shape result = new Shape();
result.pieceShape = pieceShape;

for (int i = 0; i < 4; ++i) {
result.setX(i, -y(i));
result.setY(i, x(i));
}
return result;
}
}

无题

某年某月某日他们是有梦想的
某年某月某日他们没了梦想

About

About Me

AI帮我理清了什么是可观测性

近年来又有各种新名词,比如突然间可观测性,作为多年监控系统开发人员,突然键有点迷惑了。以下是我跟AI的一段对话,突然豁然开朗了。

我:

请描述可观测性和监控系统的区别

AI:

可观测性和监控系统之间的主要区别如下:
可观测性(Observability)是指从系统中获取遥测数据(指标、日志、跟踪等)的能力,以洞察系统内部状态和行为。可观测性关注的是收集遥测数据以理解系统,而不仅仅是监控它。
监控(Monitoring)是基于可观测性系统持续收集遥测数据,并根据预定义的阈值和规则主动发出告警通知的系统。监控系统的目的是发现系统问题和异常情况并通知运维人员。
总结一下:

  • 可观测性是观察系统内部状态和行为的基础能力。监控系统建立在可观测性之上。
  • 可观测性更关注主动收集数据理解系统。监控更关注根据规则检测问题并告警。
  • 可观测性提供了原始遥测数据。监控系统在此基础上做分析和告警。
  • 可观测性是一种广泛的实践。监控是可观测性在运维领域的具体应用。
  • 好的可观测性能力是构建监控系统的基础。监控系统依赖可观测性提供的数据。
    所以简单来说,可观测性是基础,监控是在此基础上进行问题检测和告警的具体应用。两者互为依赖和补充。

我:

能不能这么理解,可观测性是更为基础的能力,而监控是利用可观测性获取数据,而形成更具实际运用的场景

AI:

您的理解是正确的。
可观测性是更基础的能力,它提供了从系统中获取遥测数据的机制。这些遥测数据可以包括指标、日志、跟踪等,用于洞察系统内部状态。
监控系统则是在可观测性提供的数据基础上,构建了具体的场景应用。它利用可观测性获取的数据,定义告警规则和阈值,然后持续分析数据并在满足规则时发出告警。
所以可观测性是构建监控的基础,而监控是可观测性在实际运维场景中的应用。它将可观测性的数据转化为监控系统的输入,并产生监控告警作为输出。
这样理解是正确的,可观测性和监控是不同层次的能力,可观测性提供基础数据,监控进行分析告警。两者互为依赖和补充,共同提高系统的运维管理能力。

我:

那为什么在运维监控领域现在大谈特谈可观测性,这是不是一种实践的倒退

AI:

目前将可观测性作为重点讨论的原因不是实践的倒退,而是由于以下几个因素:

  1. 复杂分布式系统的兴起,传统监控方式受到挑战。这类系统动态性强,组件复杂,需要从根本上提高可观测性能力。
  2. 监控本身存在一些痛点,如告警疲劳、无法根因分析等。这需要从源头提升可观测性,获得更多更好的遥测数据。
  3. 云原生和微服务架构推崇可观测性设计理念。需要各组件提供统一的遥测数据接口。
  4. 新技术如Prometheus和OpenTelemetry可以大幅提高遥测数据的收集能力。
  5. AI运维、精益运维等新理念需要可观测性提供大量可解释的遥测数据。
  6. Kubernetes等新技术的出现,需要重新思考监控策略。

综上,重视可观测性是根据实践发展的需要,而不是倒退。它为构建更智能、更精细的监控系统和运维模式奠定基础。可观测性和监控将持续协同发展。
所以说,当前实践的重心在可观测性上,是对整个监控体系发展的积极推进,而不是倒退。它是适应新技术和新需求的必然选择,也是未来的发展方向。

总而言之,可观测性强调的是跟广泛地应用数据透出能力;而更多地指标、日志、跟踪能更好地促进监控的落地和实施。

Windows 10的netstat命令

Windows里面的 netstat 是一个很有用的命令,可以用来来监视和解决许多网络问题,通过本文,你将了解如何在Windows 10上开始使用该工具。

使用

windows开始菜单→输入cmd→搜索cmd控制台,选择用管理员执行,因为有些命令需要管理员权限才能运行。

打开了cmd命令行窗口之后,输入 netstat,就能看到当前的网络连接情况。

显示网络连接

-n

-n 第一个选项,现实ip或者端口,而不用根据常用端口转成对应的协议名。注意上图中的https/http,事实上不加这个选项,这个命令通常会很慢。

-a

-a参数指的是现实活动的和不活动连接,以及所有协议的。

-b

-b 显示连接相关的应用。这个选项在有时候端口不知道被谁占用的时候很有用,或者你也可以用 -o参数一起使用,-o参数则会显示连接相关的进程号。

-o

显示连接占用的进程号。

-f

显示远端的FQDN名称,这个选项是-n的一个反向处理,会用完整的远端机器名来替换ip地址。

-p

过滤指显示指定协议的连接,比如 -p tcp只显示TCP协议的连接,可用的选项有:IP, IPv6, ICMP, ICMPv6, TCP, TCPv6, UDP, 或者UDPv6

-q

显示所有的连接,包括所有监听的和绑定的连接。

-x

显示直接链接,(NetworkDirect is a specification for Remote Direct Memory Access (RDMA))

-y

显示连接模板

https://www.notion.so

统计信息

-e

显示网络统计信息,发送、接收字节,组播、单播包数,错误、丢弃包数。

-s

根据协议,IP版本显示统计信息。

配置信息

-r

显示路由信息,命令等同于 route print

-t

显示连接的offload状态(offload是一种网络技术)

[How to use netstat command on Windows 10 | Windows Central](https://www.windowscentral.com/how-use-netstat-command-windows-10#:~:text=The “InHost” value indicates that,present on supported network adapters.)

https://www.notion.so

Debian 10/11安装Mysql 5.7

跟Java8一样,Mysql 5.7依然是大家的最爱。本文介绍一下如何在Debian10/11下安装Mysql5.7。

1. 添加Mysql仓库

1
2
wget https://dev.mysql.com/get/mysql-apt-config_0.8.18-1_all.deb
sudo dpkg -i mysql-apt-config_0.8.18-1_all.deb

Debian11(Blueseye),还没有,依旧选择Debian 10(Buster)

第二步有点迷惑,默认是选择mysql-8.0,第一次我以为这只是mysql 8.0。别怕,选中,回车。

然后就会出现mysql 5.7的选项

最后选择OK

2. 更新源

1
sudo apt update

但是这里会出现一个签名错误。*signatures couldn’t be verified* ,那就添加签名吧。

1
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29

添加完再重新更新

1
sudo apt update

3. 安装

1
sudo apt install -y mysql-community-server

设置root密码

4. 服务配置

1
2
sudo systemctl restart mysql
sudo systemctl enable mysql

参考

Json转CSV

https://images.unsplash.com/photo-1517694712202-14dd9538aa97?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb

一个最简单的Json文件转csv脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import pandas as pd
from pathlib import Path
import json

# set path to file
p = Path(r'c:\some_path_to_file\test.json')

# read json
with p.open('r', encoding='utf-8') as f:
data = json.loads(f.read())

# create dataframe
df = pd.json_normalize(data)

# dataframe view
pk model fields.codename fields.name fields.content_type
22 auth.permission add_logentry Can add log entry 8
23 auth.permission change_logentry Can change log entry 8
24 auth.permission delete_logentry Can delete log entry 8
4 auth.permission add_group Can add group 2
10 auth.permission add_message Can add message 4

# save to csv
df.to_csv('test.csv', index=False, encoding='utf-8')

JVM DNS缓存问题

JVM DNS缓存问题

在某一项目中,一个跨机房调用,机房之间日常有专线连接,万一在专线故障的情况下,降级通过公网通讯。

  • 方案一: 为服务配置两个域名,一个域名其中一个域名解析到公网地址,另一个解析到专线地址。客户端会根据一个故障指令,从专线域名切换的公网域名。
  • 方案二: 只用一个域名,通过dns服务来决定走公网还是走专线。这就涉及到两个问题,dns更新,和客户端的dns缓存。

DNS是一个各层都喜欢缓存的东西,比如浏览器,操作系统,DNS服务器等会缓存,每一层缓存的时间和规则都有些不一致。

这里重点讨论JVM的缓存,也顺道提供一下如何查看操作系统的dns TTL的命令。

盲测

直接写测试代码,打印解析结果

1
2
3
4
5
6
7
8
9
10
11
public class DnsLookupTest {
public static void main(String[] args) throws UnknownHostException, InterruptedException {
while (true) {
InetAddress[] inetAddresses = InetAddress.getAllByName("www.baidu.com");
for (InetAddress inetAddress : inetAddresses) {
System.out.println(new Date() + ":" + inetAddress.getHostName() + "-->" + inetAddress.getHostAddress());
}
Thread.sleep(1000);
}
}
}

同时修改windows的hosts文件,模拟dns变更。

1
2
# The current time is: 16:55:29.67
127.0.0.4 www.baidu.com

输出

1
2
3
4
5
6
7
8
Wed Feb 01 16:55:51 CST 2023:www.baidu.com-->127.0.0.3
Wed Feb 01 16:55:52 CST 2023:www.baidu.com-->127.0.0.3
Wed Feb 01 16:55:53 CST 2023:www.baidu.com-->127.0.0.3
Wed Feb 01 16:55:54 CST 2023:www.baidu.com-->127.0.0.3
Wed Feb 01 16:55:55 CST 2023:www.baidu.com-->127.0.0.4
Wed Feb 01 16:55:56 CST 2023:www.baidu.com-->127.0.0.4
Wed Feb 01 16:55:58 CST 2023:www.baidu.com-->127.0.0.4
Wed Feb 01 16:55:59 CST 2023:www.baidu.com-->127.0.0.4

从测试结果看,16:55:29修改hosts,Java代码中16:55:55更新,大概30秒不到。

刨根问底

先看下java.net.InetAddress.java的代码,其中获取地址的一段如下。Jvm中有一个缓存和一个缓存策略。支持三种策略:不缓存,永远缓存,TTL方式。 缓存分两种,一种是成功解析的,一种是没有解析的。对应的两个配置,InetAddressCachePolicy.getNegative()InetAddressCachePolicy.get()

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
if (addresses == this) {
// lookup name services
InetAddress[] inetAddresses;
UnknownHostException ex;
int cachePolicy;
try {
inetAddresses = getAddressesFromNameService(host, reqAddr);
ex = null;
cachePolicy = InetAddressCachePolicy.get();
} catch (UnknownHostException uhe) {
inetAddresses = null;
ex = uhe;
cachePolicy = InetAddressCachePolicy.getNegative();
}
// remove or replace us with cached addresses according to cachePolicy
if (cachePolicy == InetAddressCachePolicy.NEVER) {
cache.remove(host, this);
} else {
CachedAddresses cachedAddresses = new CachedAddresses(
host,
inetAddresses,
cachePolicy == InetAddressCachePolicy.FOREVER
? 0L
// cachePolicy is in [s] - we need [ns]
: System.nanoTime() + 1000_000_000L * cachePolicy
);
if (cache.replace(host, this, cachedAddresses) &&
cachePolicy != InetAddressCachePolicy.FOREVER) {
// schedule expiry
expirySet.add(cachedAddresses);
}
}
if (inetAddresses == null) {
throw ex == null ? new UnknownHostException(host) : ex;
} return inetAddresses;
}

再来看下sun.net.InetAddressCachePolicy.java的代码,成功的和失败的,又对应两组配置,命名看,一个配置加上一个兜底的。 默认的,成功解析的TTL是30秒,失败的不解析。

1
2
3
4
5
6
// Controls the cache policy for successful lookups only
private static final String cachePolicyProp = "networkaddress.cache.ttl";
private static final String cachePolicyPropFallback = "sun.net.inetaddr.ttl";
// Controls the cache policy for negative lookups only
private static final String negativeCachePolicyProp = "networkaddress.cache.negative.ttl";
private static final String negativeCachePolicyPropFallback = "sun.net.inetaddr.negative.ttl";

要修改JVM DNS TTL

有以下两种方式,修改全局,Java8 对应的配置文件$JAVA_HOME/jre/lib/security/java.security,Java 11及以上 $JAVA_HOME/conf/security/java.security

1
networkaddress.cache.ttl=60

程序内修改

1
java.security.Security.setProperty("networkaddress.cache.ttl" , "60");

参考

  1. windows 下看 dns ttl
1
nslookup -debug www.www.baidu.com

windows nslokup

  1. linux下看dns ttl
1
dig A www.cyberciti.biz

linux dig

  1. java dns cache ttl
  2. how-to see time to live ttl for a dns record

Mysql加密连接

什么是加密连接

Mysql加密连接的支持,为数据库的公网开放提供了可能性。X.509能够有效的标识连接客户端的身份。具体的算法可以不用太纠结,主要就是利用不对称加密,数据传输双向加密,来实现数据传输的保密性。

在mysql 5.7.28以前可以用yassl,在5.7.28之后,全部采用OpenSSL了。本文针对5.7.28以后版本。

前置工作

检查mysql版本和配置

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> SHOW VARIABLES like '%version%';
+-------------------------+------------------------------+
| Variable_name | Value |
+-------------------------+------------------------------+
| innodb_version | 5.7.37 |
| protocol_version | 10 |
| slave_type_conversions | |
| tls_version | TLSv1,TLSv1.1,TLSv1.2 |
| version | 5.7.37 |
| version_comment | MySQL Community Server (GPL) |
| version_compile_machine | x86_64 |
| version_compile_os | Linux |
+-------------------------+------------------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
mysql> SHOW VARIABLES like '%ssl%';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| have_openssl | YES |
| have_ssl | YES |
| ssl_ca | ca.pem |
| ssl_capath | |
| ssl_cert | server-cert.pem |
| ssl_cipher | |
| ssl_crl | |
| ssl_crlpath | |
| ssl_key | server-key.pem |
+---------------+-----------------+

准备证书

  1. 生成一个私钥和请求文件
1
openssl req -newkey rsa:2048 -days 365 -nodes -keyout user1-key.pem -out user1-req.pem

请求文件中会包含申请的主题信息和签名信息。注意参数中有两个输出 keyout 和 out

  1. 用mysql的私钥签名提交的请求,生成x509证书
1
2
3
4
5
$ sudo openssl x509 -req -in user1-req.pem -days 365 -CA /var/lib/mysql/ca.pem \
-CAkey /var/lib/mysql/ca-key.pem -set_serial 01 -out user1-cert.pem
Signature ok
subject=C = AU, ST = Some-State, O = Code123, OU = abc
Getting CA Private Key

用请求文件和mysql的ca.pem来生成客户端证书。通俗来说,就是在请求证书上签个字。下次连接的时候用这个证书来,mysql服务器就会放行。这里的输出主要注意下,一些证书信息,后面会用到

创建用户

1
CREATE USER 'code123'@'%' IDENTIFIED BY 'xxxxxx' REQUIRE SUBJECT '/O=Code123/OU=abc';

创建一个用户,需要主题信息是/O=Code123/OU=abc意思就是,组织和部门名称,需要验证其他更多的信息,可以用/来添加就行。

客户端工具连接

举例dbeaver的客户端配置

JKS文件和JDBC客户端连接

首先把前文的客户端证书转成pkcs12

1
2
openssl pkcs12 -export -in client-cert.pem -inkey client-key.pem \
-name "mysqlclient" -passout pass:mypassword -out client-keystore.p12

用keytool把pkcs12证书写入jks文件,jks是java key store的文件格式

1
2
keytool -importkeystore -srckeystore client-keystore.p12 -srcstoretype pkcs12 \
-srcstorepass mypassword -destkeystore code123.jks -deststoretype JKS -deststorepass mypassword

这里需要提供deststorepass是jks的加密密码

jdbc连接配置

1
2
3
4
url=jdbc:mysql://host:port/dbname?useSSL=true&requireSSL=true&verifyServerCertificate=false&clientCertificateKeyStoreUrl=file:conf/code123.jks&clientCertificateKeyStorePassword=jkspassword
driver=com.mysql.jdbc.Driver
user=xxxx
pass=dbpass
  • useSSL:true&requireSSL=true 开启ssl
  • verifyServerCertificate=false 客户端不验证服务器证书
  • clientCertificateKeyStoreUrl=file:conf/code123.jks 这里指向生成的jks文件
  • clientCertificateKeyStorePassword=jkspassword jks文件的密码

参考

https://blogs.oracle.com/mysql/post/howto-connect-to-mysql-using-a-client-certificate https://dev.mysql.com/doc/refman/5.7/en/encrypted-connections.html

0%