开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Python开发基础总结(五)模块+日志+自省 一、模块的使

发表于 2021-11-14

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」

一、模块的使用

1、 如果不想将模块的某些函数和变量被别的模块使用,可以以单下划线开头。这样import 是没有的,但是使用import mode,然后mode.fun仍然可以调用。在class中是以双下划线开头的。
2、 使用from。。。import导入的符号,应该是本地符号,更改的话,无法更改模块中的值。可以通过mode.name=来修改。

3、 init.py的作用:可以这样理解:包也是一个对象,这个py就是这个包的构造函数。导入这个包,就会自动的执行__init_.py。如果在这个py中导入其他符号,import 这个包并且加也会导入这个符号。

4、 import 无法导入模块中以_开头的符号。但是,不用是可以的。

5、 import的本质也是创建一个符号,指向一个对象的引用。这个符号和被import的模块的符号是没有关系的。和c的extern不一样。extern可以更改变量的值,但是,这在Python中是不可以的。

1
2
3
4
5
6
7
python复制代码from srctest import itest, outitest, setitest
import srctest
# itest = 9#这个地方其实改变的是本模块中符号的引用,无法更改srctest中对应符号。
#srctest.itest = 9#这个可以更改srctest中的itest
setitest(9)#这个可以更改srctest中的itest,但是改变不了当前模块的itest,也就是,这种设置是无法同步的。
print(itest)#打印当前模块的itest
    printitest()#打印srctest中的itest

Python的设计哲学:看似不方便的背后,其实有Python的设计哲学。便捷性很多时候都是模块性的大敌。在软件开发中,模块间的最短路径未必是最合理路径,而且往往是最不合理路径。它会破坏软件原有的交互原则。

Python这样设计的理由应该是,尽量将数据和对数据的操作放在一起。如果数据会扩散,那么,就将数据设计为只读的。这样有助于提高程序模块的内聚性(全局变量是内聚性的大敌),降低耦合性。降低程序的复杂性(数据只读,调试根据方便)。

srctest.itest是可以改变itest的值的,说明我们可以通过改变这个对象的属性来改变对象(模块也是对象)。

可能有一点小题大做。

6、 两个模块不可以双向import。那万一两个模块都要互相调用对方怎么办?Python的设计哲学告诉你,这不是一个好的实践,所以这样不行。应该怎么弄?一个模块调用另外一个模块,如果被调用模块想调用调用模块的方法,通过回调的形式。这样可以保证,模块间的连接都是单向的。

二、日志的使用

1、 日志的标准模块logging基本可以满足我的工作。

2、 设置log的初始化工作:

1
2
3
4
5
ini复制代码logging.basicConfig(
    filename = "test.log",
    format = "[%(asctime)s-%(levelname)s] %(message)s [%(filename)s,%(lineno)d]",
    level = logging.INFO,
    datefmt = "%F %T")

3、 除此之外,一个比较强大的功能就是过滤功能:可以针对级别,文件,行号等等很多的东西进行过滤。

三、自省的使用

1、 type()可以查看对象的类型。这就是自省。也就是可以看看自己是什么类型。这个功能在动态语言中非常有用。

2、 getattr函数:这是个非常有用的函数,它可以根据字符串,从模块,类,对象实例中获取属性和方法的应用并且调用。这个功能非常类似于c语言的函数指针,以及c++中的成员函数的指针。

1)从模块中获取函数和成员

1
2
3
ini复制代码import testfun
tf = getattr(testfun, 'test')
tstr = getattr(testfun, 'str')

2)从类中获取属性和方法

class test():

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码tst = 2
        def __init__(self):
        self.abc = 1
      def method(self):
        print('in test.method', self)
      def __test(self):
        print('in test')
tm = getattr(test, 'method’)#获取类方法method函数指针。因为没有实例,所以调用必须用下面的方法:
t = test()
tm(t)#申请一个实例,并且作为第一个参数传进去。
tm = getattr(test, '__test’)#这里会报错,也就是无法获取私有方法。
tabc = getattr(test, 'abc’)#这是错误的。无法获取。
ttst = getattr(test, 'tst’)#这是可以的。。

3)从对象实例中获取属性和方法

1
2
3
4
scss复制代码t = test()
tm = getattr(t, 'method')
 tm()#可以这样调用,而不用传入t实例。
tabc = getattr(test, 'abc’)#可以获取实例的属性。

3、 callable:函数表示某个对象是否可以调用。它和getattr结合起来,可以获取一个对象中的所有的method列表:

1
python复制代码methods = [method for method in dir[object] if callable(getattr(object, method))]

4、 自省也叫放射。

5、 exec(‘print “test”‘):可以执行字符串代码。这个特性有助于动态执行代码,可以用于机器学习,自动生成代码。

exec的参数可以使一个打开的文件对象,string,code object。

code object可以通过函数

类似的方法:execfile(filename[, globals[, locals]])。

6、 可以更改类的方法,将它指向一个新的方法。如下:

1
2
3
4
5
6
7
8
scss复制代码class ctest():
def test(self):
print('c test test')
def testfun():
print('test fun !')
c = ctest()
c.test = testfun
c.test()

对象c的方法test被替换为新的方法:testfun。这个特性有助于根据动态的代码实现,但是往往会增加代码的透明性。

类似的,setattr也可以实现这样的功能。delattr可以删除属性。

1
2
python复制代码setattr(c, 'test', testfun)
delattr(c, 'test')

c.test()#这里调用的其实就是ctest的test方法。也就是说,delattr会首先删除setattr设置的属性,如果在调用一次delattr,才会删除c的test方法。但是如果多调用几次setattr,也只要调用一次delattr即可删除。所以,要删除一个方法,最多调用两次delattr。

这个特性可以用于动态更改代码。也可用于补丁。

setattr无法对Python的c扩展模块进行操作。

7、 如何判断一个变量是否存在:

1
2
scss复制代码‘v’ in dir()
‘v’ in locals.key()

\

​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Golang实践录:使用gin框架实现转发功能:上传文件并转

发表于 2021-11-14

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

近段时间需要实现一个转发 post 请求到指定后端服务的小工具,由于一直想学习 gin 框架,所以就使用这个框架进行尝试,预计会产生几篇文章。本文先研究如何在 gin 框架中实现上传和转发功能。

问题提出

一后台 web 服务,有众多历史版本,本身运行无问题,但后来需求变更,需将不同的历史版本单独运行,并指定不同端口。对外相当于有众多的服务。在请求 post 中带有日期时间,需要根据该日期时间将请求转发到不同的端口的后端服务。注意,post 请求是直接使用文件的形式,对文件名称有特定要求。

思路

nginx可以根据端口来转发,但本文是根据请求的内容转发的,因此需要实现一个转发工具。即先读取外部请求的文件内容,解析得到时间,再根据时间,转发到不同的端口服务中。要解决的问题:

如何做到既解析 post 请求,又要将该请求原封不动地发到后端服务?后端服务返回的数据,如何原封不动地返回请求者?

如何管理后端服务?如果使用额外的脚本,则添加了运维部署的步骤,略有麻烦。故考虑在转发工具中实现。

实现

  • 使用工具进行 post 请求,并且指定文件名。可用 postman 或 curl,本文使用后者。

转发函数:

  • 利用 ctx.Request.FormFile 得到文件名称、文件内容,此时,可以使用 gin 提供的 SaveUploadedFile 函数保存文件,也可以调用 io.Copy 保存。前者省事。
  • 调用再次转发函数。
  • 将再次转发函数返回值转换成 json 形式,返回 post 请求工具。

再次转发函数:

  • 利用 multipart 包创建文件,将上一步得到的文件拷贝进去。
  • 再用 http 库发送请求。注意需要设置格式。
  • 最后读取请求的返回值,再返回,注意,内容为字节形式。

代码

主要接口代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
scss复制代码func RunWebServer(args []string) {
runWebOnlyPost()
}

func runWebOnlyPost() {
router := gin.New()
router.Use(gin.Logger())
router.Use(gin.Recovery())

testRouter(router)

klog.Println("Server started at ", conf.Port)
router.Run(":" + conf.Port)
}


func testRouter(r *gin.Engine) {
fmt.Println("test post...")

r.POST("/foobar/test", foobar_test)
r.POST("/foobar/test_back", foobar_test_back)
}

实现代码

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
go复制代码/*
curl http://127.0.0.1:84/foobar/test -X POST -F "file=@sample.json"

临时:
file读取了一次,再读就没有内容了,字节数为0
*/
func foobar_test(ctx *gin.Context) {

// 2种方式都可,但 ctx.Request.FormFile 可以得到文件句柄,可直接拷贝
//file, err := ctx.FormFile("file")
file, header, err := ctx.Request.FormFile("file")
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{
"error": err,
})
return
}

//fmt.Printf("Request: %+v\n", ctx.Request);
//fmt.Printf("Formfile: %+v | %+v ||| %v %v\n", file, header, err, reflect.TypeOf(file));

// 拿到文件和长度,后面使用到
var jsonfilename string = header.Filename
mysize := header.Size
fmt.Printf("filename: %s size: %d\n", jsonfilename, mysize);

if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{
"error": err,
})
return
}

// 处理json文件
// 注:如果读取了文件,再转发,就没有内容了,所以这里不读取

// 指向后端的服务URL
url := "http://127.0.0.1:85/foobar/test_back"
resp := post_data_gin(url, jsonfilename, file);

// 解析返回字符切片,得到map,当成json,赋值给gin
var data1 map[string]interface{}
err = json.Unmarshal(resp, &data1)
//fmt.Println("muti unmarshal: ", err, data1)

ctx.JSON(http.StatusOK, data1)

return
}

/*
模拟后台的仅获取file字段的json,不作其它处理
curl http://127.0.0.1:84/foobar/test_back -X POST -F "file=@sample.json"
*/
func foobar_test_back(ctx *gin.Context) {

// 2种方式都可,但 ctx.Request.FormFile 可以得到文件句柄,可直接拷贝
//file, err := ctx.FormFile("file")
_, header, err := ctx.Request.FormFile("file")
if err != nil {
ctx.JSON(
http.StatusBadRequest,
gin.H{
"code": -1,
"msg": "failed",
"data": gin.H{
"result": "failed in back end server",
},
},
)

return
}

// 拿到文件和长度,后面使用到
var myfile string = header.Filename
mysize := header.Size
fmt.Printf("filename: %s size: %d\n", myfile, mysize);

if mysize <= 0 {
ctx.JSON(
http.StatusBadRequest,
gin.H{
"code": -1,
"msg": "failed",
"data": gin.H{
"result": "failed in back end server, json size 0",
},
},
)

return
}

// 此处可保存文件

//保存成功返回正确的Json数据
ctx.JSON(
http.StatusOK,
gin.H{
"code": 0,
"msg": "ok",
"data": gin.H{
"result": "ok in back end server",
},
},
)

return
}

为测试方便,文中实现的 gin 框架程序在运行时可指定端口。因此,代码中实现了2个 url 的响应函数。

测试

本文使用 sample.json 文件测试,内容如下:

1
2
3
4
5
6
7
8
json复制代码{
"enID": "ID250",
"exID": "ID251",
"exTime": "2020-10-17T20:00:27",
"type": 1,
"money": 250.44,
"distance": 274050
}

先运行 84 端口服务(称为 84 服务),此为对外的服务。再运行 85 端口服务(称为 85 服务),此为模拟后端的服务。

启动一终端,执行测试命令:

1
perl复制代码curl http://127.0.0.1:84/foobar/ -X POST -F  "file=@sample.json"

84 服务打印:

1
2
3
4
5
csharp复制代码[2021-11-13 00:04:10.424 busy.go:79] Server started at  84
[GIN-debug] Listening and serving HTTP on :84
filename: sample.json size: 95
io copy: 95 <nil>
[GIN] 2021/11/13 - 00:05:33 | 200 | 3.0002ms | 127.0.0.1 | POST "/foobar/test"

85 服务打印:

1
2
3
csharp复制代码[GIN-debug] Listening and serving HTTP on :85
filename: sample.json size: 95
[GIN] 2021/11/13 - 00:05:33 | 200 | 0s | 127.0.0.1 | POST "/foobar/test_back"

测试命令返回:

1
2
3
ruby复制代码  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
Dload Upload Total Spent Left Speed
100 361 100 63 100 298 31500 145k --:--:-- --:--:-- --:--:-- 352k{"code":0,"data":{"result":"ok in back end server"},"msg":"ok"}

也可直接向后端服务请求:

1
2
3
4
css复制代码$ curl http://127.0.0.1:85/fee/test_back -X POST -F  "file=@sample.json"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 361 100 63 100 298 63000 291k --:--:-- --:--:-- --:--:-- 352k{"code":0,"data":{"result":"ok in back end server"},"msg":"ok"}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Linux命令拾遗-动态追踪工具

发表于 2021-11-14

原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处。

简介

这是Linux命令拾遗系列的第六篇,本篇主要介绍工作中常用的动态追踪工具strace、arthas、bpftrace等。

本系列文章索引

Linux命令拾遗-入门篇

Linux命令拾遗-文本处理篇

Linux命令拾遗-软件资源观测

Linux命令拾遗-硬件资源观测

Linux命令拾遗-剖析工具

之前介绍了一些线程与内存剖析工具,如jstack、pstack能很容易观测到线程被卡在了什么位置,而jmap、gcore则容易排查内存泄露、oom等问题。

但线程与内存剖析,只能观测到进程的整体情况,有些时候我们需要观测到某一方法级别,比如调用方法test()时,传入的参数是什么,返回值是多少,花费了多少时间?这种情况下,我们就需要使用一些动态追踪工具了,如strace、arthas、bpftrace、systemtap等。

strace与ltrace

strace是Linux中用来观测系统调用的工具,学过操作系统原理都知道,操作系统向应用程序暴露了一批系统调用接口,应用程序只能通过这些系统调用接口来访问操作系统,比如申请内存、文件或网络io操作等。

用法如下:

1
2
3
4
5
6
7
bash复制代码# -T 打印系统调用花费的时间
# -tt 打印系统调用的时间点
# -s 输出的最大长度,默认32,对于调用参数较长的场景,建议加大
# -f 是否追踪fork出来子进程的系统调用,由于服务端服务普通使用线程池,建议加上
# -p 指定追踪的进程pid
# -o 指定追踪日志输出到哪个文件,不指定则直接输出到终端
$ strace -T -tt -f -s 10000 -p 87 -o strace.log

实例:抓取实际发送的SQL

有些时候,我们会发现代码中完全没问题的SQL,却查不到数据,这极有可能是由于项目中一些底层框架改写了SQL,导致真实发送的SQL与代码中的SQL不一样。

遇到这种情况,先别急着扒底层框架代码,那样会比较花时间,毕竟程序员的时间很宝贵,不然要加更多班的,怎么快速确认是不是这个原因呢?

有两种方法,第一种是使用wireshark抓包,第二种就是本篇介绍的strace了,由于程序必然要通过网络io相关的系统调用,将SQL命令发送到数据库,因此我们只需要用strace追踪所有系统调用,然后grep出发送SQL的系统调用即可,如下:

1
shell复制代码$ strace -T -tt -f -s 10000 -p 87 |& tee strace.log

strace

从图中可以清晰看到,mysql的jdbc驱动是通过sendto系统调用来发送SQL,通过recvfrom来获取返回结果,可以发现,由于SQL是字符串,strace自动帮我们识别出来了,而返回结果因为是二进制的,就不容易识别了,需要非常熟悉mysql协议才行。

另外,从上面其实也很容易看出SQL执行耗时,计算相同线程号的sendto与recvfrom之间的时间差即可。

ltrace

由于大多数进程基本都会使用基础c库,而不是系统调用,如Linux上的glibc,Windows上的msvc,所以还有一个工具ltrace,可以用来追踪库调用,如下:

1
bash复制代码$ ltrace -T -tt -f -s 10000 -p 87 -o ltrace.log

基本用法和strace一样,一般来说,使用strace就够了。

arthas

arthas是java下的一款动态追踪工具,可以观测到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
bash复制代码# 下载arthas
$ wget https://arthas.aliyun.com/download/3.4.6?mirror=aliyun -O arthas-packaging-3.4.6-bin.zip

# 解压
$ unzip arthas-packaging-3.4.6-bin.zip -d arthas && cd arthas/

# 进入arthas命令交互界面
$ java -jar arthas-boot.jar `pgrep -n java`
[INFO] arthas-boot version: 3.4.6
[INFO] arthas home: /home/work/arthas
[INFO] Try to attach process 3368243
[INFO] Attach process 3368243 success.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'


wiki https://arthas.aliyun.com/doc
tutorials https://arthas.aliyun.com/doc/arthas-tutorials.html
version 3.4.6
pid 3368243
time 2021-11-13 13:35:49

# help可查看arthas提供了哪些命令
[arthas@3368243]$ help
# help watch可查看watch命令具体用法
[arthas@3368243]$ help watch

watch、trace与stack

在arthas中,使用watch、trace、stack命令可以观测方法调用情况,如下:

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
bash复制代码# watch观测执行的查询SQL,-x 3指定对象展开层级
[arthas@3368243]$ watch org.apache.ibatis.executor.statement.PreparedStatementHandler parameterize '{target.boundSql.sql,target.boundSql.parameterObject}' -x 3
method=org.apache.ibatis.executor.statement.PreparedStatementHandler.parameterize location=AtExit
ts=2021-11-13 14:50:34; [cost=0.071342ms] result=@ArrayList[
@String[select id,log_info,create_time,update_time,add_time from app_log where id=?],
@ParamMap[
@String[id]:@Long[41115],
@String[param1]:@Long[41115],
],
]

# watch观测耗时超过200ms的SQL
[arthas@3368243]$ watch com.mysql.jdbc.PreparedStatement execute '{target.toString()}' 'target.originalSql.contains("select") && #cost > 200' -x 2
Press Q or Ctrl+C to abort.
Affect(class count: 3 , method count: 1) cost in 123 ms, listenerId: 25
method=com.mysql.jdbc.PreparedStatement.execute location=AtExit
ts=2021-11-13 14:58:42; [cost=1001.558851ms] result=@ArrayList[
@String[com.mysql.jdbc.PreparedStatement@6283cfe6: select count(*) from app_log],
]

# trace追踪方法耗时,层层追踪,就可找到耗时根因,--skipJDKMethod false显示jdk方法耗时,默认不显示
[arthas@3368243]$ trace com.mysql.jdbc.PreparedStatement execute 'target.originalSql.contains("select") && #cost > 200' --skipJDKMethod false
Press Q or Ctrl+C to abort.
Affect(class count: 3 , method count: 1) cost in 191 ms, listenerId: 26
---ts=2021-11-13 15:00:40;thread_name=http-nio-8080-exec-47;id=76;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@5a2d131d
---[1001.465544ms] com.mysql.jdbc.PreparedStatement:execute()
+---[0.022119ms] com.mysql.jdbc.PreparedStatement:checkClosed() #1274
+---[0.016294ms] com.mysql.jdbc.MySQLConnection:getConnectionMutex() #57
+---[0.017862ms] com.mysql.jdbc.PreparedStatement:checkReadOnlySafeStatement() #1278
+---[0.008996ms] com.mysql.jdbc.PreparedStatement:createStreamingResultSet() #1294
+---[0.010783ms] com.mysql.jdbc.PreparedStatement:clearWarnings() #1296
+---[0.017843ms] com.mysql.jdbc.PreparedStatement:fillSendPacket() #1316
+---[0.008543ms] com.mysql.jdbc.MySQLConnection:getCatalog() #1320
+---[0.009293ms] java.lang.String:equals() #57
+---[0.008824ms] com.mysql.jdbc.MySQLConnection:getCacheResultSetMetadata() #1328
+---[0.009892ms] com.mysql.jdbc.MySQLConnection:useMaxRows() #1354
+---[1001.055229ms] com.mysql.jdbc.PreparedStatement:executeInternal() #1379
+---[0.02076ms] com.mysql.jdbc.ResultSetInternalMethods:reallyResult() #1388
+---[0.011517ms] com.mysql.jdbc.MySQLConnection:getCacheResultSetMetadata() #57
+---[0.00842ms] com.mysql.jdbc.ResultSetInternalMethods:getUpdateID() #1404
---[0.008112ms] com.mysql.jdbc.ResultSetInternalMethods:reallyResult() #1409

# stack追踪方法调用栈,找到耗时SQL来源
[arthas@3368243]$ stack com.mysql.jdbc.PreparedStatement execute 'target.originalSql.contains("select") && #cost > 200'
Press Q or Ctrl+C to abort.
Affect(class count: 3 , method count: 1) cost in 138 ms, listenerId: 27
ts=2021-11-13 15:01:55;thread_name=http-nio-8080-exec-5;id=2d;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@5a2d131d
@com.mysql.jdbc.PreparedStatement.execute()
at com.alibaba.druid.pool.DruidPooledPreparedStatement.execute(DruidPooledPreparedStatement.java:493)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:63)
at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)
at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:63)
at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:326)
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:136)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:77)
at sun.reflect.GeneratedMethodAccessor75.invoke(null:-1)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
at com.sun.proxy.$Proxy113.selectOne(null:-1)
at org.mybatis.spring.SqlSessionTemplate.selectOne(SqlSessionTemplate.java:166)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:83)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
at com.sun.proxy.$Proxy119.selectCost(null:-1)
at com.demo.example.web.controller.TestController.select(TestController.java:57)

可以看到watch、trace、stack命令中都可以指定条件表达式,只要满足ognl表达式语法即可,ognl完整语法很复杂,如下是一些经常使用的:

分类 示例
内置变量 watch/trace/stack命令中内置了params,target,returnObj,throwExp,#cost,分别表示参数,调用对象自身,返回值,异常,执行耗时。
属性获取 通过.user获取对象user属性值,.user.userId获取对象中user属性中的userId属性。
数组、集合或Map元素获取 通过.orders[0]获取数组或集合中第一个元素,通过.userMap["lisi"]获取Map中对应key值
对象方法调用 通过.getUser()可直接调用对象的方法
静态变量与方法访问 静态变量访问@class@member, 静态方法调用@class@method(args)
条件判断 数值可使用> >= ==等比较,字符串可直接通过== !=比较,如.name=="zhangsan"
逻辑运算 通过`&&
数组包含 对于数组/List/Set,可以使用in,not in判断元素是否存在,如"zhangsan" in .names
变量赋值 当前时间赋值到变量obj,#obj=new java.util.Date(), #obj.toString()
List、Map构造 List构造{"green", "red", "blue"}, Map构造#{"id" : 1, "name" : "lisi", "birthday" : new java.util.Date()}
字段提取 提取userList中的birthday,.userList.{birthday.getYear()}
列表过滤 过滤出userList中的id<2的元素,.userList.{? id<2 }

ognl

通过ognl命令,可直接查看静态变量的值,如下:

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
bash复制代码# 调用System.getProperty静态函数,查看jvm默认字符编码
[arthas@3368243]$ ognl '@System@getProperty("file.encoding")'
@String[UTF-8]

# 找到springboot类加载器
[arthas@3368243]$ classloader -t
+-BootstrapClassLoader
+-sun.misc.Launcher$ExtClassLoader@492691d7
+-sun.misc.Launcher$AppClassLoader@764c12b6
+-org.springframework.boot.loader.LaunchedURLClassLoader@4361bd48

# 获取springboot中所有的beanName,-c指定springboot的classloader的hash值
# 一般Spring项目,都会定义一个SpringUtil的,用于获取bean容器ApplicationContext
[arthas@3368243]$ ognl -c 4361bd48 '#context=@com.demo.example.web.SpringUtil@applicationContext, #context.beanFactory.beanDefinitionNames'
@String[][
@String[org.springframework.context.annotation.internalConfigurationAnnotationProcessor],
@String[org.springframework.context.annotation.internalAutowiredAnnotationProcessor],
@String[org.springframework.context.annotation.internalCommonAnnotationProcessor],
@String[testController],
@String[apiController],
@String[loginService],
...
]

# 获取springboot配置,如server.port是配置http服务端口的
[arthas@3368243]$ ognl -c 4361bd48 '#context=@com.demo.example.web.SpringUtil@applicationContext, #context.getEnvironment().getProperty("server.port")'
@String[8080]

# 查看server.port定义在哪个配置文件中
# 可以很容易看到,server.port定义在application-web.yml
[arthas@3368243]$ ognl -c 4361bd48 '#context=@com.demo.example.web.SpringUtil@applicationContext, #context.environment.propertySources.propertySourceList.{? containsProperty("server.port")}'
@ArrayList[
@ConfigurationPropertySourcesPropertySource[ConfigurationPropertySourcesPropertySource {name='configurationProperties'}],
@OriginTrackedMapPropertySource[OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/application-web.yml]'}],
]

# 调用springboot中bean的方法,获取返回值
[arthas@3368243]$ ognl -c 4361bd48 '#context=@com.demo.example.web.SpringUtil@applicationContext, #context.getBean("demoMapper").queryOne(12)' -x 2
@ArrayList[
@HashMap[
@String[update_time]:@Timestamp[2021-11-09 18:38:13,000],
@String[create_time]:@Timestamp[2021-04-17 15:52:55,000],
@String[log_info]:@String[TbTRNsh2SixuFrkYLTeb25a6zklEZj0uWANKRMe],
@String[id]:@Long[12],
@String[add_time]:@Integer[61],
],
]

# 查看springboot自带tomcat的线程池的情况
[arthas@3368243]$ ognl -c 4361bd48 '#context=@com.demo.example.web.SpringUtil@applicationContext, #context.webServer.tomcat.server.services[0].connectors[0].protocolHandler.endpoint.executor'
@ThreadPoolExecutor[
sm=@StringManager[org.apache.tomcat.util.res.StringManager@16886f49],
submittedCount=@AtomicInteger[1],
threadRenewalDelay=@Long[1000],
workQueue=@TaskQueue[isEmpty=true;size=0],
mainLock=@ReentrantLock[java.util.concurrent.locks.ReentrantLock@69e9cf90[Unlocked]],
workers=@HashSet[isEmpty=false;size=10],
largestPoolSize=@Integer[49],
completedTaskCount=@Long[10176],
threadFactory=@TaskThreadFactory[org.apache.tomcat.util.threads.TaskThreadFactory@63c03c4f],
handler=@RejectHandler[org.apache.tomcat.util.threads.ThreadPoolExecutor$RejectHandler@3667e559],
keepAliveTime=@Long[60000000000],
allowCoreThreadTimeOut=@Boolean[false],
corePoolSize=@Integer[10],
maximumPoolSize=@Integer[8000],
]

其它命令

arthas还提供了jvm大盘、线程剖析、堆转储、反编译、火焰图等功能,如下:

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
bash复制代码# 显示耗cpu较多的前4个线程
[arthas@3368243]$ thread -n 4
"C2 CompilerThread0" [Internal] cpuUsage=8.13% deltaTime=16ms time=46159ms


"C2 CompilerThread1" [Internal] cpuUsage=4.2% deltaTime=8ms time=47311ms


"C1 CompilerThread2" [Internal] cpuUsage=3.06% deltaTime=6ms time=17402ms


"http-nio-8080-exec-40" Id=111 cpuUsage=1.29% deltaTime=2ms time=624ms RUNNABLE (in native)
at java.net.SocketInputStream.socketRead0(Native Method)
...
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4113)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2570)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2731)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2818)
...
at com.demo.example.web.controller.TestController.select(TestController.java:57)

# 堆转储
[arthas@3368243]$ heapdump
Dumping heap to /tmp/heapdump2021-11-13-15-117226383240040009563.hprof ...
Heap dump file created

# cpu火焰图,容器环境下profiler start可能用不了,可用profiler start -e itimer替代
[arthas@3368243]$ profiler start
Started [cpu] profiling
[arthas@3368243]$ profiler stop
OK
profiler output file: /home/work/app/arthas-output/20211113-151208.svg

# dashboard就类似Linux下的top一样,可看jvm线程、堆内存的整体情况
[arthas@3368243]$ dashboard

# jvm就类似Linux下的ps一样,可以看jvm进程的一些基本信息,如:jvm参数、类加载、线程数、打开文件描述符数等
[arthas@3368243]$ jvm

# 反编译
[arthas@3368243]$ jad com.demo.example.web.controller.TestController

可见,arthas已经不是一个单纯的动态追踪工具了,它把jvm下常用的诊断功能几乎全囊括了。

相关项目地址:

arthas.aliyun.com/doc/index.h…

github.com/jvm-profili…

bpftrace

arthas只能追踪java程序,对于原生程序(如MySQL)就无能为力了,好在Linux生态提供了大量的机制以及配套工具,可用于追踪原生程序的调用,如perf、bpftrace、systemtap等,由于bpftrace使用难度较小,本篇主要介绍它的用法。

bpftrace是基于ebpf技术实现的动态追踪工具,它对ebpf技术进行封装,实现了一种脚本语言,就像上面介绍的arthas基于ognl一样,它实现的脚本语言类似于awk,封装了常见语句块,并提供内置变量与内置函数,如下:

1
2
3
bash复制代码$ sudo bpftrace -e 'BEGIN { printf("Hello, World!\n"); } '
Attaching 1 probe...
Hello, World!

实例:在调用端追踪慢SQL

前面我们用strace追踪过mysql的jdbc驱动,它使用sendto与recvfrom系统调用来与mysql服务器通信,因此,我们在sendto调用时,计下时间点,然后在recvfrom结束时,计算时间之差,就可以得到相应SQL的耗时了,如下:

  1. 先找到sendto与recvfrom系统调用在bpftrace中的追踪点,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码# 查找sendto|recvfrom系统调用的追踪点,可以看到sys_enter_开头的追踪点应该是进入时触发,sys_exit_开头的退出时触发
$ sudo bpftrace -l '*tracepoint:syscalls*' |grep -E 'sendto|recvfrom'
tracepoint:syscalls:sys_enter_sendto
tracepoint:syscalls:sys_exit_sendto
tracepoint:syscalls:sys_enter_recvfrom
tracepoint:syscalls:sys_exit_recvfrom

# 查看系统调用参数,方便我们编写脚本
$ sudo bpftrace -lv tracepoint:syscalls:sys_enter_sendto
tracepoint:syscalls:sys_enter_sendto
int __syscall_nr;
int fd;
void * buff;
size_t len;
unsigned int flags;
struct sockaddr * addr;
int addr_len;
  1. 编写追踪脚本trace_slowsql_from_syscall.bt,脚本代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bash复制代码#!/usr/local/bin/bpftrace

BEGIN {
printf("Tracing jdbc SQL slower than %d ms by sendto/recvfrom syscall\n", $1);
printf("%-10s %-6s %6s %s\n", "TIME(ms)", "PID", "MS", "QUERY");
}

tracepoint:syscalls:sys_enter_sendto /comm == "java"/ {
// mysql协议中,包开始的第5字节指示命令类型,3代表SQL查询
$com = *(((uint8 *) args->buff)+4);
if($com == (uint8)3){
@query[tid]=str(((uint8 *) args->buff)+5, (args->len)-5);
@start[tid]=nsecs;
}
}

tracepoint:syscalls:sys_exit_recvfrom /comm == "java" && @start[tid]/ {
$dur = (nsecs - @start[tid]) / 1000000;
if ($dur > $1) {
printf("%-10u %-6d %6d %s\n", elapsed / 1000000, pid, $dur, @query[tid]);
}
delete(@query[tid]);
delete(@start[tid]);
}

其中,comm表示进程名称,tid表示线程号,@query[tid]与@start[tid]类似map,以tid为key的话,这个变量就像一个线程本地变量了。

  1. 调用上面的脚本,可以看到各SQL执行耗时,如下:
1
2
3
4
5
6
7
8
bash复制代码$ sudo BPFTRACE_STRLEN=80 bpftrace trace_slowsql_from_syscall.bt
Attaching 3 probes...
Tracing jdbc SQL slower than 0 ms by sendto/recvfrom syscall
TIME(ms) PID MS QUERY
6398 3368243 125 select sleep(0.1)
16427 3368243 22 select id from app_log al order by id desc limit 1
16431 3368243 20 select id,log_info,create_time,update_time,add_time from app_log where id=11692
17492 3368243 21 select id,log_info,create_time,update_time,add_time from app_log where id=29214

实例:在服务端追踪慢SQL

从调用端来追踪SQL耗时,会包含网络往返时间,为了得到更精确的SQL耗时,我们可以写一个追踪服务端mysql的脚本,来观测SQL耗时,如下:

  1. 确定mysqld服务进程的可执行文件与入口函数
1
2
3
4
5
6
7
8
bash复制代码$ which mysqld
/usr/local/mysql/bin/mysqld

# objdump可导出可执行文件的动态符号表,做几张mysqld的火焰图就可发现,dispatch_command是SQL处理的入口函数
# 另外,由于mysql是c++写的,方法名是编译器改写过的,这也是为啥下面脚本中要用*dispatch_command*模糊匹配
$ objdump -tT /usr/local/mysql/bin/mysqld | grep dispatch_command
00000000014efdf3 g F .text 000000000000252e _Z16dispatch_commandP3THDPK8COM_DATA19enum_server_command
00000000014efdf3 g DF .text 000000000000252e Base _Z16dispatch_commandP3THDPK8COM_DATA19enum_server_command
  1. 使用uprobe追踪dispatch_command的调用,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bash复制代码#!/usr/bin/bpftrace
BEGIN{
printf("Tracing mysqld SQL slower than %d ms. Ctrl-C to end.\n", $1);
printf("%-10s %-6s %6s %s\n", "TIME(ms)", "PID", "MS", "QUERY");
}
uprobe:/usr/local/mysql/bin/mysqld:*dispatch_command*{
if (arg2 == (uint8)3) {
@query[tid] = str(*arg1);
@start[tid] = nsecs;
}
}
uretprobe:/usr/local/mysql/bin/mysqld:*dispatch_command* /@start[tid]/{
$dur = (nsecs - @start[tid]) / 1000000;
if ($dur > $1) {
printf("%-10u %-6d %6d %s\n", elapsed / 1000000, pid, $dur, @query[tid]);
}
delete(@query[tid]);
delete(@start[tid]);
}

追踪脚本整体上与之前系统调用版本的类似,不过追踪点不一样而已。

实例:找出扫描大量行的SQL

众所周知,SQL执行时需要扫描数据,并且扫描的数据越多,SQL性能就会越差。

但对于一些中间情况,SQL扫描行数不多也不少,如2w条。且这2w条数据都在缓存中的话,SQL执行时间不会很长,导致没有记录在慢查询日志中,但如果这样的SQL并发量大起来的话,会非常耗费CPU。

对于mysql的话,扫描行的函数是row_search_mvcc(如果你经常抓取mysql栈的话,很容易发现这个函数),每扫一行调用一次,如果在追踪脚本中追踪此函数,记录下调用次数,就可以观测SQL的扫描行数了,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bash复制代码#!/usr/bin/bpftrace
BEGIN{
printf("Tracing mysqld SQL scan row than %d. Ctrl-C to end.\n", $1);
printf("%-10s %-6s %6s %10s %s\n", "TIME(ms)", "PID", "MS", "SCAN_NUM", "QUERY");
}
uprobe:/usr/local/mysql/bin/mysqld:*dispatch_command*{
$COM_QUERY = (uint8)3;
if (arg2 == $COM_QUERY) {
@query[tid] = str(*arg1);
@start[tid] = nsecs;
}
}
uprobe:/usr/local/mysql/bin/mysqld:*row_search_mvcc*{
@scan_num[tid]++;
}
uretprobe:/usr/local/mysql/bin/mysqld:*dispatch_command* /@start[tid]/{
$dur = (nsecs - @start[tid]) / 1000000;
if (@scan_num[tid] > $1) {
printf("%-10u %-6d %6d %10d %s\n", elapsed / 1000000, pid, $dur, @scan_num[tid], @query[tid]);
}
delete(@query[tid]);
delete(@start[tid]);
delete(@scan_num[tid]);
}

脚本运行效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码$ sudo BPFTRACE_STRLEN=80 bpftrace trace_mysql_scan.bt 200
Attaching 4 probes...
Tracing mysqld SQL scan row than 200. Ctrl-C to end.
TIME(ms) PID MS SCAN_NUM QUERY
150614 1892 4 300 select * from app_log limit 300
# 全表扫描,慢!
17489 1892 424 43717 select count(*) from app_log
# 大范围索引扫描,慢!
193013 1892 253 20000 select count(*) from app_log where id < 20000
# 深分页,会查询前20300条,取最后300条,慢!
213395 1892 209 20300 select * from app_log limit 20000,300
# 索引效果不佳,虽然只会查到一条数据,但扫描数据量不会少,慢!
430374 1892 186 15000 select * from app_log where id < 20000 and seq = 15000 limit 1

如上所示,app_log是我建的一张测试表,共43716条数据,其中id字段是自增主键,seq值与id值一样,但没有索引。

可以看到上面的几个场景,不管什么场景,只要扫描行数变大,耗时就会变长,但也都没有超过500毫秒的,原因是这个表很小,数据可以全部缓存在内存中。

可见,像bpftrace这样的动态追踪工具是非常强大的,而且比arthas更加灵活,arthas只能追踪单个函数,而bpftrace可以跨函数追踪。

bpftrace使用手册:github.com/iovisor/bpf…

总结

已经介绍了不少诊断工具了,这里简单概括一下它们的应用场景:

  1. 软件资源观测,如ps、lsof、netstat,用来检查进程基本情况,如内存占用、打开哪些文件、创建了哪些连接等。
  2. 硬件资源观测,如top、iostat、sar等,类似Windows上任务管理器一样,让你对硬件资源占用以及在进程上情况有个大概了解,最多观测到线程级别,这些工具一般只能指示进一步调查的方向,很少能直接确认原因。
  3. 线程与内存剖析,如jstack、jmap等,比较容易分析线程卡住或内存oom问题,而oncpu火焰图容易找出热代码路径,分析高cpu瓶颈,offcpu火焰图则容易找出经常阻塞的代码路径,分析高耗时问题。
  4. 动态追踪工具,如arthas、bpftrace等,能追踪到方法调用级,常用于问题需要深入调查的场景,如问题不在代码实现上,而在调用数据上,就像上面row_search_mvcc函数一样,抓火焰图会发现它出现频繁,但由于它是核心函数,这个频繁可能是正常的,也可能是不正常的,需要将调用次数分散到SQL上才能确认。

往期内容

Linux命令拾遗-剖析工具

原来awk真是神器啊

Linux文本命令技巧(上)

Linux文本命令技巧(下)

字符编码解惑

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

k8s series 25 calico中级(架构)

发表于 2021-11-14

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战」。

架构图

以下架构图来源于官方文档
image.png

下图为我们自已搭建的集群架构图

image.png

分析

从官方架构图中,显示了calico官方提供了一些可选组件,像Typha组件已经安装,但没有配置生效,Dikastes组件服务于Istio网格Envoy的没有启用,大型网络BGP路由反射器也没有配置。

calico-kube-controllers组件安装在kubernetes集群,主要基于集群状态监视kubernetes API要施行的动作,该组件包括策略,命令空间,账号服务,pod,节点等相关控制器

1
js复制代码kubectl get pods -A  | grep calico-kube-controllers

image.png

calico datastore是一个数据存储插件,它可以和kubenetes共用集群etcd,或单独再使用一套etcd服务,目前我们的架构是和kubernetes共用

其主要是为了calico网络组件之间的通信提供数据存储,在共用etcd之后,可以使用k8s rbac控制calico资源的访问,可以使用k8s审计日志生成calico资源更改的审计日志

如果单独立再部署一套的优势是,k8s和calico资源隔离,以及calico可以包含多个单集,对于中小型架构或不需要复杂架构的来说,共用是最好的选择

Felix 运行在每个节点的agent之一,主要负责路由和acl,主机pod连接等任务。

felix监听datastore的变化,管理着主机上的网络接口,处理来自pod的流量请求和转发

felix不定期上传状态报告到网络健康数据到存储datastore,将calico的相关出站入站等规则写入到iptables中,其中关于pod路由部分写入到linux 内核fib中,这些fib信息后续有客户端处理(bird)

1
js复制代码ps aux | grep felix | grep -v grep

可以在每个calico节点上看到felix的守护进程
image.png

BIRD是一个客户端,也是运行在每个节点的agent之一,它在ipip模式下是个普通的bgp客户端,负载从felix获取路由信息然后分发给网络上的bgp对等点

当Felix将路由写入到linux 内核的fib时,bgp客户端将它们分配给其它节点

只有启用BGP模式,使用Typha组件,bird才路由反射器功能,BGP的路由反射器是BGP客户端的中心,具体变化在路由中只有一条中心路由,取代其它相互建立的多条路由

1
js复制代码ps aux | grep bird | grep -v grep

可以在每个calico节点上看到bird的守护进程
image.png

confd是个状成管理工个,也是运行在每个节点的agent之一,它主机监视calico数据存储,以查看bgp配置和全局默认值(as号,日志级别,ipam信息)的变更

1
js复制代码ps aux | grep confd | grep -v grep

image.png

CNI是实现kubenetes网络模型的一组工具,主要为kubenetes提供calico网络,它安装在kubenetes的每个节点,将充当网络协调器,提供工具命令和配置

在kubelet watch来自kubernetes api server的变更,调用calico 提供的配置和工具来配置容器的网络和规则

1
2
js复制代码#网络配置
ll /etc/cni/net.d/

image.png

1
2
js复制代码#工具命令
ll /opt/cni/bin/ | grep cali

image.png

小结

除了calico-kube-controllers部署在k8s中,在各节点上其实还部署了不少守护进程

除了felix,confd,bird。还有分配tunnel IP的,监控token和IP的一些进程,这些都是calici内部的自动运维工具,定时维护着组件的状态

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

RabbitMQ 使用和集群

发表于 2021-11-14

MQ 介绍

  1. MQ:Message Queue,消息队列。生产者生产消息存放到队列里,消费者监听队列内容,伺机消费消息

image-20211110212039640
2. 优点:MQ 中消息的生产和消费是异步的,生产者与消费者无侵入、低耦合
3. 常见的 MQ 框架

1
2
3
4
5
6
7
8
markdown复制代码# ActiveMQ
Apache 出品的,老牌的消息总线,遵从 JMS 规范,提供丰富的 API。
# Kafka
Apache 顶级项目,开源的发布订阅消息系统,不支持事务,对错误、丢失没有严格可控制,吞吐量高,使用适用于大数据量的数据收集业务。
# RocketMQ
阿里开源的消息中间件,纯 Java 开发,高吞吐量、高可用性,适合大规模分布式系统应用。思路起源于 Kafka。
# RabbitMQ
Erlang 语言编写的开源消息队列系统,基于 AMQP 协议,面向消息、队列、路由,具有可靠性、安全性。对数据一致性、稳定性要求高的场景适用

RabbitMQ 介绍

  1. RabbitMQ 官网:www.rabbitmq.com/

image-20211110222608877
2. RabbitMQ 的优点

1
2
3
4
markdown复制代码- 使用 Erlang 语言开发,Erlang 是性能强劲的 Socket 编程语言
- 基于 AMQP 协议,具有跨平台性
- 轻松集成 SpringBoot
- 对数据一致性非常友好
  1. RabbitMQ 相关概念
1
2
3
4
5
6
7
8
9
10
markdown复制代码# AMQP 协议
2003 年被提出,是一种高级的消息协议,不限定 API 层,直接定义网络交换的数据格式,有天然跨平台性。
# 虚拟主机
一个虚拟主机持有一组交换机、队列和绑定关系,用于划分 RabbiMQ 服务,一般不同的服务配置不同的虚拟主机,用户 在虚拟主机粒度进行权限控制,每个 RabbitMQ 服务器在默认配置时具有默认的虚拟主机 '/'
# 交换机
用于转发消息到队列,若没有队列与之绑定,会丢弃生产者生产的消息
# 队列
存储消息的数据容器
# connection 和 channel
通过 connection 对象创建 channel 对象,使用 channel 对象传输数据。channel 可看作虚拟的连接,避免频繁创建真实连接(connection)开销较大
  1. 获取 RabbitMQ
* Windows 通过官网下载 RabbitMQ 安装包,配合 Erlang 环境启动
* Linux 通过包管理器安装 RabbitMQ
* Linux 通过 [Dcoker](https://hub.docker.com/_/rabbitmq) 拉取 RabbitMQ 镜像并启动
  1. 管理 RabbitMQ

RabbitMQ 提供了一个 Web 管理页面,默认在 15672 端口,登录后可以管理 RabbitMQ 的服务的相关配置

如添加一个虚拟主机

image-20211110224645528

如添加一个用户,可以设置用户对虚拟主机的访问权限

image-20211110223853339

RabbitMQ 使用

下列代码的 RabbitMQ 版本为 3.8.23

创建通道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码 // 获取 MQ 连接对象的工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
// 设置连接 IP
connectionFactory.setHost("127.0.0.1");
// 设置端口号
connectionFactory.setPort(5672);
// 设置虚拟主机
connectionFactory.setVirtualHost("demoMQ");
// 设置用户名和密码
connectionFactory.setUsername("demoUser");
connectionFactory.setPassword("xxxxxxx");
// 获取连接对象
Connection connection = connectionFactory.newConnection();
// 创建通道对象
Channel channel = connection.createChannel();

创建队列

image-20211113163953778

1
2
3
4
5
6
7
8
java复制代码channel.queueDeclare("demoQueue", false, false, false, null);
/**
* 参数 1 queue:消息队列名称
* 参数 2 durable:队列是否持久化(不包括队列中的消息)
* 参数 3 exclusive:当前连接是否独占队列
* 参数 4 autoDelete:消息消费完成且断开连接后是否自动删除队列
* 参数 5 argument:额外的参数
*/

工作队列模型

  1. 工作队列模型

使用一个消息队列,有一个或多个消费者,各消费者获取不同的消息进行消费

img
2. 发布消息

image-20211113170015290

1
2
3
4
5
6
7
java复制代码channel.basicPublish("", "demoQueue", null, "demo queue".getBytes());
/**
* 参数 1 exchange:交换机名
* 参数 2 routingKey:路由
* 参数 3 props;额外参数
* 参数 4 body:消息的 byte 数组
*/

交换机名称为空字符时,使用默认交换机

每一个队列会自动将队列同名的 Routing Key 绑定到默认交换机上

channel 和 connection 使用完成后需要关闭
3. 消费消息

image-20211113172941017

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码// 绑定和生产者相同的队列
channel.queueDeclare("demoQueue", false, false, false, null);
// 启动消息监听
/**
* 参数 1 queue:队列名
* 参数 2 autoAck:是否自动确认
* 参数 3 callback:回调接口对象
*/
channel.basicConsume("demoQueue", true, new DefaultConsumer(channel) {
// body 是消息内容
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
BasicProperties properties,
byte[] body) throws IOException {
System.out.println(new String(body));
}
});
  1. 消息分配

在工作队列模型中,多个消费者默认平均分配消息

如果使用手动确认,且设置每个通道同时只消费一个消息,只有确认后才能消费下一个消息

这种方式消息分配数量和处理速度有关,即能者多劳

手动确认消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码// 设置通道同时只能消费一个消息
// 通道确认消息后,才会获取新消息的分配
channel.basicQos(1);
// autoAck false
channel.basicConsume("demoQueue", false, new DefaultConsumer(channel) {
@SneakyThrows
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
BasicProperties properties,
byte[] body) {
System.out.println(new String(body));
Thread.sleep(3000);
// 参数 1:消息的标志,通过 envelope.getDeliveryTag() 获取
// 参数 2:是否开启多消息确认
channel.basicAck(envelope.getDeliveryTag(), false);
}
});

发布订阅模型

  1. 广播模型

每个消费者都有自己的队列,每个队列都要绑定到交换机,生产者将消息发送到交换机,由交换机进行分配

img
2. 发布消息

1
2
3
4
5
6
java复制代码// 声明交换机
// 参数 1:交换机名称
// 参数 2:交换机类型,fanout 广播
channel.exchangeDeclare("demoExchange", "fanout");
// 发送消息
channel.basicPublish("demoExchange", "", null, "demo exchange".getBytes());
  1. 消费消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码// 声明交换机
channel.exchangeDeclare("demoExchange", "fanout");
// 获取临时队列名称
String tempQueueName = channel.queueDeclare().getQueue();
// 绑定队列到交换机
channel.queueBind(tempQueueName, "demoExchange", "");
// 启动消息监听
channel.basicConsume(tempQueueName, false, new DefaultConsumer(channel) {
@SneakyThrows
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
BasicProperties properties,
byte[] body) {
System.out.println(new String(body));
channel.basicAck(envelope.getDeliveryTag(), false);
}
});

发布订阅模型中生产者只要声明交换机即可,消费者声明临时队列并绑定到交换机,Routing Key 全部使用空字符串,一旦交换机接收到消息,就转发到每一个绑定的队列

路由主题模型

  1. 路由主题模型

在发布订阅模型中,全部消费者都可以获取相同的消息

在路由主题模型中,交换机不再将消息转发到每一个队列,而且根据路由和主题进行匹配

img
2. 发布消息

1
2
3
4
5
6
7
java复制代码// 声明交换机
channel.exchangeDeclare("routeExchange", "direct");
// 发送消息
// 发布一条消息,Routing Key 为 info
channel.basicPublish("routeExchange", "info", null, "demo route info".getBytes());
// 发布一条消息,Routing Key 为 error
channel.basicPublish("routeExchange", "error", null, "demo route error".getBytes());
  1. 消费消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码 // 声明交换机,direct 类型
channel.exchangeDeclare("routeExchange", "direct");
// 获取临时队列名称
String tempQueueName = channel.queueDeclare().getQueue();
// 绑定交换机、队列和 Routing Key
channel.queueBind(tempQueueName, "routeExchange", "info");
// 启动消息监听
channel.basicConsume(tempQueueName, false, new DefaultConsumer(channel) {
@SneakyThrows
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
BasicProperties properties,
byte[] body) {
System.out.println(new String(body));
channel.basicAck(envelope.getDeliveryTag(), false);
}
});

channel.queueBind 可以多次调用,以绑定多个 Routing Key
4. 动态路由

动态路由可以在 Routing Key 中使用通配符,#代表任意字符串,*代表一个单词

生产者发布消息

1
2
3
4
5
6
7
java复制代码// 声明交换机
channel.exchangeDeclare("topicExchange", "topic");
// 发送几个不同路由的消息
channel.basicPublish("topicExchange", "log.error", null, "log.error".getBytes());
channel.basicPublish("topicExchange", "log.error.file", null, "log.error.file".getBytes());
channel.basicPublish("topicExchange", "log.info", null, "log.info".getBytes());
channel.basicPublish("topicExchange", "log.info", null, "user.info".getBytes());

消费者绑定队列

1
2
3
4
5
6
7
8
9
10
11
java复制代码// 声明交换机,topic 类型
channel.exchangeDeclare("topicExchange", "topic");
// 获取临时队列名称
String tempQueueName = channel.queueDeclare().getQueue();
// 绑定队列和路由
channel.queueBind(tempQueueName, "topicExchange", "log.*");
/**
* log.* 可以匹配 log.error、log.info
* log.# 可以匹配 log.error、log.info、log.error.file
* *.info 可以匹配 log.info、user.info
*/

三大模型总结:

工作队列模型:无交换机单队列,争消息

发布订阅模型:有交换机空路由,同消息

路由主题模型:有交换机有路由,按路由

集成 SpringBoot

  1. 引入依赖
1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  1. 配置 application.yaml
1
2
3
4
5
6
7
java复制代码spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: demoUser
password: xxxxx
virtual-host: demoMQ

配置完成后,RabbitTemplate 就可用了。RabbitTemplate 封装了一系列 RabbitMQ 的操作
3. 工作队列模型

发布消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@SpringBootTest
public class RabbitProviderTest {
@Autowired
RabbitTemplate rabbit;
// 模拟 RabbitConsumer 对象
// 防止启动测试时实例化出的 RabbitConsumer 消费消息
@MockBean
public RabbitConsumer consumer;

@Test
public void queueProvide() {
// 发送消息到队列
rabbit.convertAndSend("demoQueue", "hello demo queue");
}
}

消费消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Component
// @Queue 可以指定 durable、exclusive 等参数
// 默认等同于 channel.queueDeclare("demoQueue", true, false, false, null);
@RabbitListener(queuesToDeclare = @Queue(name = "demoQueue"))
public class RabbitConsumer {
// 消息处理器,可直接获取消息体
@RabbitHandler
public void queueConsume(String message) {
System.out.println(message);
}
}
/**
* @RabbitListener 也可以用在方法上,表示方法为消息处理器
*/

在 Spring AMQP 中,工作队列模型是公平消费的,可以通过配置预处理个数和手动确认来实现能者多劳
4. 发布订阅模型

发布消息

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@SpringBootTest
public class RabbitProviderTest {
@Autowired
RabbitTemplate rabbit;
@MockBean
public RabbitConsumer consumer;

@Test
public void subscribeProvide() {
rabbit.convertAndSend("demoExchange","", "hello demo exchange" + i);
}
}

消费消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Component
public class RabbitConsumer {
// 第一个消费者
// @Queue 不指定参数则创建临时队列
@RabbitListener(bindings = @QueueBinding(value = @Queue, key = "",
exchange = @Exchange(name = "demoExchange", type = "fanout")))
public void subscribeConsumer1(String message) {
System.out.println("subscribeConsume1 接收:" + message);
}
// 第二个消费者
@RabbitListener(bindings = @QueueBinding(value = @Queue, key = "",
exchange = @Exchange(name = "demoExchange", type = "fanout")))
public void subscribeConsumer2(String message) {
System.out.println("subscribeConsume2 接收:" + message);
}
}
  1. 路由主题模型

发布消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@SpringBootTest
public class RabbitProviderTest {
@Autowired
RabbitTemplate rabbit;
@MockBean
public RabbitConsumer consumer;

@Test
public void routeProvide() {
rabbit.convertAndSend("routeExchange", "log.error", "log.error");
rabbit.convertAndSend("routeExchange", "log.info", "log.info");
rabbit.convertAndSend("routeExchange", "log.error.file", "log.error.file");
rabbit.convertAndSend("routeExchange", "user.info", "user.info");
}
}

消费消息

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
java复制代码@Component
public class RabbitConsumer {

@RabbitListener(bindings = @QueueBinding(value = @Queue, key = "log.*",
exchange = @Exchange(name = "routeExchange", type = "topic")))
public void routeConsumer1(String message) {
System.out.println("routeConsumer1 接收:" + message);
}

@RabbitListener(bindings = @QueueBinding(value = @Queue, key = "log.#",
exchange = @Exchange(name = "routeExchange", type = "topic")))
public void routeConsumer2(String message) {
System.out.println("routeConsumer2 接收:" + message);
}

@RabbitListener(bindings = @QueueBinding(value = @Queue, key = "*.info",
exchange = @Exchange(name = "routeExchange", type = "topic")))
public void routeConsumer3(String message) {
System.out.println("routeConsumer3 接收:" + message);
}
}
/**
* 若要使用通配符实现动态路由,则需指定 type 为 topic
* 不使用通配符,指定为 direct 即可
*/

RabbitMQ 集群

  1. 基础步骤

准备 RabbitMQ 服务,部署两台或以上 RabbitMQ 服务

配置.erlang.cookie文件,.erlang.cookie是加入服务集群的密钥,在集群服务中保持一致

在从节点上执行加入集群命令

1
2
shell复制代码rabbitmqctl join_cluster --ram rabbit@<主机名称>
# --ram 表示设置为内存节点,不加则默认为磁盘节点

在任意节点上设置镜像队列

1
2
3
4
5
6
shell复制代码rabbitmqctl set_policy <策略名称> "<队列名称>" '{"ha-mode":"<镜像模式>"}'
# 参数 1:策略名称
# 参数 2:队列名称的匹配规则,可使用正则表达式
# 参数 3:镜像队列的主体规则,json 字符串,有三个属性:ha-mode/ha-params/ha-sync-mode
# ha-mode:镜像模式,all/exactly/nodes,all 存储在所有节点
# --vhost 设置虚拟主机
  1. Docker RabbitMQ 集群

拉取 RabbitMQ 镜像

1
shell复制代码docker pull rabbitmq:3.8.23-management

准备rabbitmq.conf配置文件,配置默认账户、默认虚拟主机等

1
2
3
4
5
6
shell复制代码loopback_users.guest = false
listeners.tcp.default = 5672
management.tcp.port = 15672
default_user = cluster
default_pass = xxxxx
default_vhost = clusterMQ

启动三个 RabbitMQ 容器,挂载相同的.erlang.cookie文件

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
shell复制代码# 1
docker run \
--name rabbitmq1 \
-h rabbitmq1 \
-p 15673:15672 \
-p 5673:5672 \
-v /var/docker/rabbitmq_cluster/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v /var/docker/rabbitmq_cluster/data1:/var/lib/rabbitmq \
-v /var/docker/rabbitmq_cluster/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-d rabbitmq:3.8.23-management
# 2
docker run \
--name rabbitmq2 \
-h rabbitmq2 \
-p 15674:15672 \
-p 5674:5672 \
-v /var/docker/rabbitmq_cluster/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v /var/docker/rabbitmq_cluster/data2:/var/lib/rabbitmq \
-v /var/docker/rabbitmq_cluster/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
--link rabbitmq1:rabbitmq1 \
-d rabbitmq:3.8.23-management
# 3
docker run \
--name rabbitmq3 \
-h rabbitmq3 \
-p 15675:15672 \
-p 5675:5672 \
-v /var/docker/rabbitmq_cluster/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v /var/docker/rabbitmq_cluster/data3:/var/lib/rabbitmq \
-v /var/docker/rabbitmq_cluster/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
--link rabbitmq1:rabbitmq1 --link rabbitmq2:rabbitmq2 \
-d rabbitmq:3.8.23-management

加入集群

1
2
3
4
5
6
7
shell复制代码# 进入容器
docker exec -it rabbitmq2 bash
# 停止服务
rabbitmqctl stop_app
# 加入节点 1,rabbit@ 后需要使用主机名,使用 ip 不行
# 主机名即 docker run -h 后的参数
rabbitmqctl join_cluster --ram rabbit@rabbitmq1

image-20211114151829556

配置策略,指定虚拟主机为 clusterMQ

1
2
shell复制代码rabbitmqctl set_policy --vhost clusterMQ demoPolicy "^" '{"ha-mode":"all"}'
# 使用 "^" 表示对所有队列进行镜像

image-20211114154952146

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【算法攻坚】实现简易正则匹配

发表于 2021-11-13

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

今日题目

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*‘ 的正则表达式匹配。

‘.’ 匹配任意单个字符
‘*‘ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

示例 1:

输入:s = “aa” p = “a”
输出:false
解释:”a” 无法匹配 “aa” 整个字符串。

示例 2:

输入:s = “aa” p = “a*“
输出:true
解释:因为 ‘*‘ 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 ‘a’。因此,字符串 “aa” 可被视为 ‘a’ 重复了一次。

示例 3:

输入:s = “ab” p = “.*”
输出:true
解释:”.*” 表示可匹配零个或多个(’*‘)任意字符(’.’)。

示例 4:

输入:s = “aab” p = “c*a*b”
输出:true
解释:因为 ‘*‘ 表示零个或多个,这里 ‘c’ 为 0 个, ‘a’ 被重复一次。因此可以匹配字符串 “aab”。

示例 5:

输入:s = “mississippi” p = “mis*is*p*.”
输出:false

提示:

1 <= s.length <= 20
1 <= p.length <= 30
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。
保证每次出现字符 * 时,前面都匹配到有效的字符

思路

如果在实际开发过程中遇到类似的需求可千万别想着自己实现正则匹配

重复造轮子是对前辈的最大不尊敬,各个编程语言都有着现成的正则api,

一行代码就可以搞定:

1
2
3
java复制代码public boolean isMatch(String s, String p) {
return Pattern.compile(p).matcher(s).matches();
}

执行用时:39 ms, 在所有 Java 提交中击败了10.52%的用户

内存消耗:38.4 MB, 在所有 Java 提交中击败了14.85%的用户

解法

如果遇到算法题,还是要从基本的字符串解析入手,一般也不会让用现成的api

这道题采用前面提到过的动态规划思想去解题

难点无非就是找出状态转移方程和确定始值

首选,定义状态dp[i][j]的含义:

对于字符串 s 和字符规律 p,dp[i][j]: 表示s的前i个字符与p的前j个字符是否匹配,结果用boolean类型表示。

然后,确定状态转移方程(公式)

对于字符串s只会是a-z的字符组成

对于字符规律p会分为三种情况

  1. 全是a-z的字符
  2. 除了字符外包含特殊符号“.”
  3. 除了字符外包含特殊符号“*”

对于第一种情况全是字符的情况最简单只要字符相等即可

1
2
3
4
5
bash复制代码if(s[i] == p[j]){
dp[i][j] = dp[i - 1][j - 1]
}else{
dp[i][j] = false;
}

对于第二种情况包含特殊字符.时,此时的.代表可以匹配任意一个字符,也就说此处无论s的字符是什么都匹配,然后取决于进一步的判断

1
bash复制代码dp[i][j] = dp[i - 1][j - 1];

对于第三种情况包含特殊字符*时,此时的* 匹配前面字符的零次或多次

也就是说当出现*时,前面的字符出现或不出现都可以,

*实例化一下,对于s[i]p[j],当p[j]=时**, 此时必然存在p[j-1],且匹配与否取决于s[i]与p[j-1]

当s[i]与p[j-1]不匹配,此时由于*的存在,相当于p[j-1]p[j]没匹配,直接跳过这两个就可以

而且一定要注意p[j-1]一定不能为.不然就不是不匹配了

1
2
3
bash复制代码if(s[i] != p[j-1] && p[j-1] != '.'){
dp[i][j] = dp[i][j - 2];
}

当s[i]与p[j-1]匹配时,此时要么是s[i]与p[j-1]字符相等(有可能匹配多个s[i-x]= s[i] = p[]j-1),要么是p[j-1]= “.”匹配了任意字符(只会是两个)

1
bash复制代码dp[i][j] = dp[i-1][j] || dp[i][j - 2];

根据思路就可以完成代码实现

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
java复制代码public boolean isMatch(String s, String p) {
boolean[][] dp = new boolean[s.length() + 1][p.length() + 1];
dp[0][0] = true;
for (int j = 1; j <= p.length(); j++) {
if (p.charAt(j - 1) == '*') {
dp[0][j] = dp[0][j - 2];
}
}
for (int i = 1; i <= s.length(); i++) {
for (int j = 1; j <= p.length(); j++) {
char si = s.charAt(i - 1), pj = p.charAt(j - 1);
//全字符场景
if (Character.isLowerCase(pj)) {
if (si == pj)
dp[i][j] = dp[i - 1][j - 1];
//包含*
} else if (pj == '.') {
dp[i][j] = dp[i - 1][j - 1];
//包含.
} else {
char pre_pj = p.charAt(j - 2);
if (si != pre_pj && pre_pj != '.') {
dp[i][j] = dp[i][j - 2];
} else {
dp[i][j] = dp[i - 1][j] || dp[i][j - 2];
}
}
}
}
return dp[s.length()][p.length()];
}

小结

动态规划的题目还是挺有规律的,难点就是在找到状态转移方程,然后考虑边界,细心调试就能解出题目,加油!

今天多学一点,明天就少说一句求人的话,加油!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Helm 快速入门

发表于 2021-11-13

这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

首发于kubernetes 实践手册

介绍如何快速上手使用 Helm。

1、前提条件

在使用 Helm 前,需具备以下前提条件:

  • 一个 Kubernetes 环境

2、Helm 版本选择

Helm 是基于 Kubernetes 之上实现的,在选择 Helm 版本时需要考虑对应的 Kubernetes 版本。

当一个 Helm 的新版本发布时,它会针对 Kubernetes 的一个特定的次版本进行编译。比如,Helm 3.0.0 与 Kubernetes 的 1.16.2 的客户端版本交互,一次可以兼容 Kubernetes 1.16。

对于最新 Helm 版本,建议使用当前 Kubernetes 的最新稳定版本。

如果您选择了一个 Kubernetes 版本不支持的 Helm 版本,在使用过程中将面临未知的风险。

建议参考下表,以满足与 Kubernetes 的兼容性:

Helm 版本 支持的 Kubernetes 版本
3.7.x 1.22.x - 1.19.x
3.6.x 1.21.x - 1.18.x
3.5.x 1.20.x - 1.17.x
3.4.x 1.19.x - 1.16.x
3.3.x 1.18.x - 1.15.x
3.2.x 1.18.x - 1.15.x

更多参考 Helm 版本支持策略

2、安装

可以通过 homebrew (macOS 下包管理工具)下载二进制 Helm 安装包,也可以通过 Github 下载。

2.1 macOS

1
sh复制代码$ brew install helm

2.2 二进制安装

每个 Helm 版本都提供了各种操作系统对应的二进制版本包,这些版本可以手动下载和安装。

下载所需的版本。

解压(如:tar -zxvf helm-v3.7.1-linux-amd64.tar.gz)。

在解压目录中找到 helm 程序,移动到需要的目录中(如:mv linux-amd64/helm /usr/local/bin/helm)。

更多安装方式参考:安装 Helm

3、初始化

当已经安装好 Helm 之后,可以添加一个 chart 仓库。可从 Artifact Hub 中查找有效的 Helm chart 仓库。

image.png

TIP:通过 Artifact Hub 几乎可以找到任何想要的 chart 包,并能找到对应的源码(github 地址),然后就可以参考、修改为定制化的 chart 啦!

1
sh复制代码$ helm repo add bitnami https://charts.bitnami.com/bitnami

当添加完成,您将可以看到可以被您安装的 charts 列表:

1
2
3
4
5
6
7
sh复制代码$ helm search repo bitnami
NAME CHART VERSION APP VERSION DESCRIPTION
bitnami/bitnami-common 0.0.9 0.0.9 DEPRECATED Chart with custom templates used in ...
bitnami/airflow 8.0.2 2.0.0 Apache Airflow is a platform to programmaticall...
bitnami/apache 8.2.3 2.4.46 Chart for Apache HTTP Server
bitnami/aspnet-core 1.2.3 3.1.9 ASP.NET Core is an open-source framework create...
# ... and many more

4、安装 Chart 示例

通过 helm install 命令安装 chart。 Helm 可以通过多种途径查找和安装 chart,但最简单的是安装官方的 bitnami charts。

1
2
3
4
5
6
7
8
9
sh复制代码$ helm repo update # 确定我们可以拿到最新的 charts 列表
$ helm install bitnami/mysql --generate-name
NAME: mysql-1612624192
LAST DEPLOYED: Sat Feb 6 16:09:56 2021
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES: ...

在上面的例子中,bitnami/mysql 这个 chart 被发布,名字是 mysql-1612624192

您可以通过执行 helm show chart bitnami/mysql 命令简单的了解到这个 chart 的基本信息。 或者您可以执行 helm show all bitnami/mysql 获取关于该 chart 的所有信息。

每当您执行 helm install 的时候,都会创建一个新的发布版本。 所以一个 chart 在同一个集群里面可以被安装多次,每一个都可以被独立的管理和升级。

5、卸载

可以使用 helm uninstall 命令卸载你的版本。

1
2
sh复制代码$ helm uninstall mysql-1612624192
release "mysql-1612624192" uninstalled

该命令会从 Kubernetes 卸载 mysql-1612624192, 它将删除和该版本相关的所有相关资源(service、deployment、 pod 等)甚至版本历史。

如果您在执行 helm uninstall 的时候提供 –keep-history 选项, Helm 将会保存版本历史。 您可以通过命令查看该版本的信息

1
2
3
sh复制代码$ helm status mysql-1612624192
Status: UNINSTALLED
...

因为 –keep-history 选项会让 helm 跟踪你的版本(即使你卸载了他们),所以你可以审计集群历史甚至使用 helm rollback 回滚版本。

6、Helm 命令

如果您想通过 Helm 命令查看更多的有用的信息,请使用 helm -h 命令,或者在任意命令后添加 -h 选项:

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
sh复制代码$ helm -h
The Kubernetes package manager

Common actions for Helm:

- helm search: search for charts
- helm pull: download a chart to your local directory to view
- helm install: upload the chart to Kubernetes
- helm list: list releases of charts

Environment variables:

| Name | Description |
|------------------------------------|-----------------------------------------------------------------------------------|
| $HELM_CACHE_HOME | set an alternative location for storing cached files. |
| $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. |
| $HELM_DATA_HOME | set an alternative location for storing Helm data. |
| $HELM_DEBUG | indicate whether or not Helm is running in Debug mode |
| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, sql. |
| $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. |
| $HELM_MAX_HISTORY | set the maximum number of helm release history. |
| $HELM_NAMESPACE | set the namespace used for the helm operations. |
| $HELM_NO_PLUGINS | disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. |
| $HELM_PLUGINS | set the path to the plugins directory |
| $HELM_REGISTRY_CONFIG | set the path to the registry config file. |
| $HELM_REPOSITORY_CACHE | set the path to the repository cache directory |
| $HELM_REPOSITORY_CONFIG | set the path to the repositories file. |
| $KUBECONFIG | set an alternative Kubernetes configuration file (default "~/.kube/config") |
| $HELM_KUBEAPISERVER | set the Kubernetes API Server Endpoint for authentication |
| $HELM_KUBECAFILE | set the Kubernetes certificate authority file. |
| $HELM_KUBEASGROUPS | set the Groups to use for impersonation using a comma-separated list. |
| $HELM_KUBEASUSER | set the Username to impersonate for the operation. |
| $HELM_KUBECONTEXT | set the name of the kubeconfig context. |
| $HELM_KUBETOKEN | set the Bearer KubeToken used for authentication. |

Helm stores cache, configuration, and data based on the following configuration order:

- If a HELM_*_HOME environment variable is set, it will be used
- Otherwise, on systems supporting the XDG base directory specification, the XDG variables will be used
- When no other location is set a default location will be used based on the operating system

By default, the default directories depend on the Operating System. The defaults are listed below:

| Operating System | Cache Path | Configuration Path | Data Path |
|------------------|---------------------------|--------------------------------|-------------------------|
| Linux | $HOME/.cache/helm | $HOME/.config/helm | $HOME/.local/share/helm |
| macOS | $HOME/Library/Caches/helm | $HOME/Library/Preferences/helm | $HOME/Library/helm |
| Windows | %TEMP%\helm | %APPDATA%\helm | %APPDATA%\helm |

Usage:
helm [command]

Available Commands:
completion generate autocompletion scripts for the specified shell
create create a new chart with the given name
dependency manage a chart's dependencies
env helm client environment information
get download extended information of a named release
help Help about any command
history fetch release history
install install a chart
lint examine a chart for possible issues
list list releases
package package a chart directory into a chart archive
plugin install, list, or uninstall Helm plugins
pull download a chart from a repository and (optionally) unpack it in local directory
repo add, list, remove, update, and index chart repositories
rollback roll back a release to a previous revision
search search for a keyword in charts
show show information of a chart
status display the status of the named release
template locally render templates
test run tests for a release
uninstall uninstall a release
upgrade upgrade a release
verify verify that a chart at the given path has been signed and is valid
version print the client version information

Flags:
--debug enable verbose output
-h, --help help for helm
--kube-apiserver string the address and the port for the Kubernetes API server
--kube-as-group stringArray group to impersonate for the operation, this flag can be repeated to specify multiple groups.
--kube-as-user string username to impersonate for the operation
--kube-ca-file string the certificate authority file for the Kubernetes API server connection
--kube-context string name of the kubeconfig context to use
--kube-token string bearer token used for authentication
--kubeconfig string path to the kubeconfig file
-n, --namespace string namespace scope for this request
--registry-config string path to the registry config file (default "/Users/xcbeyond/Library/Preferences/helm/registry.json")
--repository-cache string path to the file containing cached repository indexes (default "/Users/xcbeyond/Library/Caches/helm/repository")
--repository-config string path to the file containing repository names and URLs (default "/Users/xcbeyond/Library/Preferences/helm/repositories.yaml")

Use "helm [command] --help" for more information about a command.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

新来的同事问我where 1=1 是什么意思

发表于 2021-11-13

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

写在前面

新的同事来之后问我where 1=1 是什么有意思,这样没意义啊,我笑了。今天来说明下。

where 1=1

先来看一段代码

1
2
3
4
5
6
7
8
9
xml复制代码<select id="queryBookInfo" parameterType="com.ths.platform.entity.BookInfo" resultType="java.lang.Integer">
select count(id) from t_book t where 1=1
<if test="title !=null and title !='' ">
AND title = #{title}
</if>
<if test="author !=null and author !='' ">
AND author = #{author}
</if>
</select>

上面的代码很熟悉,就是查询符合条件的总条数。在mybatis中常用到if标签判断where子句后的条件,为防止首字段为空导致sql报错。没错 ,当遇到多个查询条件,使用where 1=1 可以很方便的解决我们条件为空的问题,那么这么写 有什么问题吗 ?

网上有很多人说,这样会引发性能问题,可能会让索引失效,那么我们今天来实测一下,会不会不走索引

实测

title字段已经加上索引,我们通过EXPLAIN看下

EXPLAIN SELECT * FROM t_book WHERE title = ‘且在人间’;

image.png

EXPLAIN SELECT * FROM t_book WHERE 1=1 AND title = ‘且在人间’;

image.png

对比上面两种我们会发现 可以看到possible_keys(可能使用的索引) 和 key(实际使用的索引)都使用到了索引进行检索。

结论

where 1=1 也会走索引,不影响查询效率,我们写的sql指令会被mysql 进行解析优化成自己的处理指令,在这个过程中1 = 1这类无意义的条件将会被优化。使用explain EXTENDED sql 进行校对,发现确实where1=1这类条件会被mysql的优化器所优化掉。

那么我们在mybatis当中可以改变一下写法,因为毕竟mysql优化器也是需要时间的,虽然是走了索引,但是当数据量很大时,还是会有影响的,所以我们建议代码这样写:

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<select id="queryBookInfo" parameterType="com.ths.platform.entity.BookInfo" resultType="java.lang.Integer">
select count(*) from t_book t
<where>
<if test="title !=null and title !='' ">
title = #{title}
</if>
<if test="author !=null and author !='' ">
AND author = #{author}
</if>
</where>
</select>

我们用where标签代替。

弦外之音

感谢你的阅读,如果你感觉学到了东西,您可以点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

copy对象,这个操作有点骚!

干货!SpringBoot利用监听事件,实现异步操作

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Laravel 日志、调试、输出、授权等技巧总结 Last

发表于 2021-11-13

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战」

本篇对Laravel常用技巧进行汇总,如果大家想了解某一个分支下的使用技巧,比如集合、Eloquent等可以查看我之前的文章。

日志记录参数

我们可以使用 Log::info(),或使用更短的 info() 额外参数信息,来了解更多发生的事情

1
bash复制代码Log::info('User failed to login.', ['id' => $user->id]);

更方便的 DD

我们可以在 Eloquent 句子或者任何集合结尾添加 ->dd(),而不是使用 dd($result)。

1
2
3
4
5
6
shell复制代码// 以前
$users = User::where('name', '小明')->get();
dd($users);

// 现在
$users = User::where('name', '小明')->get()->dd();

使用 context 日志

在最新的 Laravel 8.49 中:Log::withContext() 将帮助我们区分不同请求之间的日志消息。

如果我们创建了中间件并且设置了 context,所有的长消息将包含在 context 中,我们搜索会更容易。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
php复制代码public function handle(Request $request, Closure $next)
{
$requestId = (string) Str::uuid();

Log::withContext(['request-id' => $requestId]);

$response = $next($request);

$response->header('request-id', $requestId);

return $response;
}

API 资源:带不带 “data”?

如果我们使用 Eloquent API 去返回数据,它们将自动封装到 data 中。

如果要将其删除,需要在
app/Providers/AppServiceProvider.php 中添加 JsonResource::withoutWrapping();

1
2
3
4
5
6
7
scala复制代码class AppServiceProvider extends ServiceProvider
{
public function boot()
{
JsonResource::withoutWrapping();
}
}

API 返回一切正常

如果我们有 API 端口执行某些操作但是没有响应,我们只想返回 “一切正常”:
我们可以返回 204 状态代码 “No content”。

在 Laravel 中,这就很简单: return response()->noContent();.

1
2
3
4
5
6
7
8
php复制代码public function reorder(Request $request)
{
foreach ($request->input('rows', []) as $row) {
Country::find($row['id'])->update(['position' => $row['position']]);
}

return response()->noContent();
}

一次检查多个权限

除了 @can Blade 指令外,还可以用 @canany 指令一次检查多个权限:

1
2
3
4
5
less复制代码@canany(['update', 'view', 'delete'], $articles)
// 当前用户可以修改,查看,或者删除文章
@elsecanany(['create'], \App\Article::class)
// 当前用户可以创建文章
@endcanany

更多关于用户注册的事件

希望在新用户注册后执行一些操作怎么优雅的实现呢?
我们可以转到 app/Providers/EventServiceProvider.php 和 添加更多的监听类,
然后在 $event->user 对象中实现 handle() 方法。

1
2
3
4
5
6
7
8
9
10
scala复制代码class EventServiceProvider extends ServiceProvider
{
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,

// 我们可以在这里添加任何Listener类
// 在该类中使用handle()方法
],
];

Auth::once () 的使用

使用方法 Auth::once(),可以用用户登录一个请求。
Auth::once () 不会使用任何会话或 cookie,这意味着该方法在构建无状态 API 时可能很有帮助。

1
2
3
less复制代码if (Auth::once($credentials)) {
//
}

更改用户密码更新的 API 令牌

当用户的密码更改时,可以方便地更改用户的 API 令牌。
这个实现非常重要

模型:

1
2
3
4
5
php复制代码public function setPasswordAttribute($value)
{
$this->attributes['password'] = $value;
$this->attributes['api_token'] = Str::random(100);
}

覆盖超级管理员的权限

如果你已经定义了网关(Gates)但是又想要覆盖超级管理员的所有权限。

给超级管理员所有权限,我们可以在 AuthServiceProvider.php 文件中用 Gate::before() 语句拦截网关(Gates)。

1
2
3
4
5
6
7
8
9
10
11
12
13
php复制代码// 拦截任何一个网关,检查它是否是超级管理员
Gate::before(function($user, $ability) {
if ($user->is_super_admin == 1) {
return true;
}
});

// 或者你使用一些权限包
Gate::before(function($user, $ability) {
if ($user->hasPermission('root')) {
return true;
}
});

Last but not least

技术交流群请到 这里来。 或者添加我的微信 wangzhongyang0601 ,一起学习。

感谢大家的点赞、评论、关注,谢谢大佬们的支持,感谢。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

为什么在大多数编程语言中 01 + 02 不等于 03

发表于 2021-11-13

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战」。

前言

在文章开始之前先看下面“诡异”的一幕。

1
2
3
4
5
6
7
python复制代码a, b = 0.1, 0.2
print(a + b == 0.3)
print(a + b)

out:
False
0.30000000000000004

0.1 + 0.2 == 0.3 结果竟然为 False ?不知道大家第一次见到这个场景作何感想,反正我是有点怀疑人生,为什么会产生这样的结果呢,下面详细看一下。

浮点数的限制

浮点数在计算机硬件中表示为一个以 2 为基数(二进制)的小数。我们先看看如果用十进制和二进制来表示0.125(10)。

十进制

0.125(10)

等于

1\times10^{-1} + 2\times10^{-2} + 5\times10^{-3} = \cfrac{1}{8}

**二进制**

0.001(2)

$$
0\times2^{-1} + 0\times2^{-2} + 1\times2^{-3} = \cfrac{1}{8}
这两个小数均表示 0.125(10),唯一真正的区别是第一个是以 10 为基数的小数表示法,第二个则是 2 为基数。

不幸的是,大多数的十进制小数都不能精确地表示为二进制小数,但有些浮点数也能够用二进制精确的表述,条件是位数有限且分母能表示成 2^n 的小数。如 0.5, 0.125。这将导致在大多数情况下,你输入的十进制浮点数都只能近似地以二进制浮点数形式储存在计算机中。

正如上文中的 0.1 ,我们手动计算一下它的二进制结果。

注:十进制整数转二进制方法:除2取余;十进制小数转二进制方法:乘2除整

计算过程:

1
2
3
4
5
6
7
8
python复制代码0.1 * 2 = 0.2 # 0
0.2 * 2 = 0.4 # 0
0.4 * 2 = 0.8 # 0
0.8 * 2 = 1.6 # 1
0.6 * 2 = 1.2 # 1
0.2 * 2 = 0.4 # 0
0.4 * 2 = 0.8 # 0
.....

从上面结果可以看出,0.1 的二进制为:

1
python复制代码0.0001100110011001100110011001100110011001100110011...

这是一个二进制无限循环小数,但计算机内存有限,我们不能储存所有的小数位数。那如何解决呢?

答案就是从末尾某个位置截断,直接取近似值,因此,在目前大部分编程语言(支持处理器浮点运算)中,浮点数都只能近似地使用二进制小数表示。

很多人使用 Python 的时候都不会意识到这个差异的存在,因为 Python 只会输出计算机中存储的二进制值的十进制近似值。但我们要牢记,即使输出的结果看起来好像就是 0.1 的精确值,实际储存的值只是最接近 0.1 的计算机可表示的二进制值。

解决方式

1.decimal

decimal 模块可以进行十进制数学计算,我们将浮点数转成字符串进行运算。

1
2
3
4
5
6
python复制代码from decimal import Decimal

a, b = Decimal('0.1'), Decimal('0.2')
a + b == Decimal('0.3')

out:True

2.numpy.float32

用 numpy 模块中的32为浮点型保存数据。

1
2
3
4
python复制代码import numpy as np

temp = np.array([0.1, 0.2, 0.3], dtype=np.float32)
temp[0] + temp[1] == temp[2]

当然体高精度的同时,性能可能会降低,在实际应用中这些近似值造成的细微偏差可能不会造成什么影响。如果碰到了留个心眼就好!

说了这么多,总结出一句话就是:浮点数转二进制时丢失了精度,计算完再转回十进制时和理论结果不同。不知道大家get到了吗?


这就是今天要分享的内容,微信搜 Python新视野,每天带你了解更多有用的知识。更有整理的近千套简历模板,几百册电子书等你来领取哦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…350351352…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%