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

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


  • 首页

  • 归档

  • 搜索

Java过滤XSS脚本攻击记录一下

发表于 2021-11-18

背景

  • 最近公司信息安全部门对公司项目进行网络安全升级时,发现项目里可能会出现XSS脚本攻击漏洞,所以就需要对其参数进行过滤拦截。

XSS

  • 百度百科:XSS攻击全称:cross site scripting(这里是为了和CSS区分,所以叫XSS),跨站脚本攻击(XSS),是最普遍的Web应用安全漏洞。这类漏洞能够使得攻击者嵌入恶意脚本代码到正常用户会访问到的页面中,当正常用户访问该页面时,则可导致嵌入的恶意脚本代码的执行,从而达到恶意攻击用户的目的。攻击者可以使用户在浏览器中执行其预定义的恶意脚本,其导致的危害可想而知,如劫持用户会话,插入恶意内容、重定向用户、使用恶意软件劫持用户浏览器、繁殖XSS蠕虫,甚至破坏网站、修改路由器配置信息等。
  • xss漏洞攻击分为三种:
    • 反射性XSS攻击:前端在发送请求时,在url参数里携带一些脚本命令,然后等服务端把脚本在反射给浏览器执行脚本代码,进行XSS漏洞攻击
    • 存储性XSS攻击:和反射性XSS漏洞攻击相似,但是服务器会进行持久化保存脚本命令,后续用户访问该数据时持久性进行XSS漏洞攻击
    • DOS性XSS攻击:和服务端没有交互,靠浏览器的DOM解析进行XSS攻击

Java过滤

  • 预防XSS漏洞攻击除了web端进行解析过滤外,也还需要服务端进行校验过滤
  • 本次使用springboot项目中过滤器进行对前端传来的参数进行过滤拦截
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
java复制代码/**
* springboot注册过滤器
*
* @author: zrh
* @date: 2021-11-17
*/

@Configuration
public class XssFilterConfig {

@Bean
public FilterRegistrationBean xssFilterRegistrationBean () {
FilterRegistrationBean initXssFilterBean = new FilterRegistrationBean();
// 设置自定义过滤器
initXssFilterBean.setFilter(new XssFilter());
// 设置优先级(值越低,优先级越高)
initXssFilterBean.setOrder(1);
// 设置过滤路径
initXssFilterBean.addUrlPatterns("/*");
// 设置过滤器名称
initXssFilterBean.setName("XSS_filter");
// 设置过滤器作用范围(可以配置多种,这里指定过滤请求资源)
initXssFilterBean.setDispatcherTypes(DispatcherType.REQUEST);
return initXssFilterBean;
}
}
  • spring项目中可以使用web.xml定义过滤器,springboot中没有web.xml配置文件,那么就可以使用FilterRegistrationBean类把过滤器实例注册到容器
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复制代码/**
* 自定义过滤器
*
* @author: zrh
* @date: 2021-11-17
*/
@Slf4j
public class XssFilter implements Filter {

@Override
public void init (FilterConfig filterConfig) {
// 初始化调用
}

@Override
public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
final XssHttpServletRequestWrapper requestWrapper = new XssHttpServletRequestWrapper((HttpServletRequest) servletRequest);
filterChain.doFilter(requestWrapper, servletResponse);
}

@Override
public void destroy () {
// 销毁调用
}
}
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
java复制代码/**
* 重写请求参数过滤
*
* @author: zrh
* @date: 2021-11-17
*/
@Slf4j
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {

public XssHttpServletRequestWrapper (HttpServletRequest request) {
super(request);
}

/**
* 对GET请求中参数进行过滤校验
*
* @param name
* @return
*/
@Override
public String[] getParameterValues (String name) {
String[] values = super.getParameterValues(name);
if (values == null) {
return null;
}
int count = values.length;
String[] cleanParams = new String[count];
for (int i = 0; i < count; i++) {
cleanParams[i] = String.valueOf(XssUtil.filterParam(values[i]));
log.info("getParameterValues -> name:{},过滤前参数:{},过滤后参数:{}", name, values[i], cleanParams[i]);
}
return cleanParams;
}

/**
* 对POST请求头进行参数过滤校验
*
* @param header
* @return
*/
@Override
public Enumeration getHeaders (String header) {
final String value = super.getHeader(header);
final LinkedList list = new LinkedList();
if (value != null) {
final Object param = XssUtil.filterParam(value);
list.addFirst(param);
log.info("getHeaders -> header:{},过滤前参数:{},过滤后参数:{}", header, value, param);
}
return Collections.enumeration(list);
}

/**
* 对POST请求中body参数进行校验
*
* @return
* @throws IOException
*/
@Override
public ServletInputStream getInputStream () throws IOException {
final ByteArrayInputStream stream = new ByteArrayInputStream(inputHandlers(super.getInputStream()).getBytes());
return new ServletInputStream() {
@Override
public int read () {
return stream.read();
}

@Override
public boolean isFinished () {
return false;
}

@Override
public boolean isReady () {
return false;
}

@Override
public void setReadListener (ReadListener readListener) {
}
};
}

/**
* 解析请求流参数
*
* @param servletInputStream
* @return
*/
public String inputHandlers (ServletInputStream servletInputStream) {
StringBuilder sb = new StringBuilder();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(servletInputStream, Charset.forName("UTF-8")));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
log.error("异常 e:", e);
} finally {
if (servletInputStream != null) {
try {
servletInputStream.close();
} catch (IOException e) {
log.error("servletInputStream 关闭异常 e:", e);
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
log.error("reader 关闭异常 e:", e);
}
}
}
final String param = XssUtil.filterBody(sb.toString());
log.info("getInputStream -> 过滤前参数:{},过滤后参数:{}", sb, param);
return param;
}
}
  • 重写HttpServletRequestWrapper类用于过滤改变请求参数值
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
scss复制代码/**
* 参数校验工具类
*
* @author: zrh
* @date: 2021-11-17
*/
@Slf4j
public final class XssUtil {

private XssUtil () {
}

/**
* 网上找的XSS匹配正则表达式
*/
private final static Pattern[] PATTERNS = new Pattern[]{
// Script fragments
Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE),
// src='...'
Pattern.compile("src[\r\n]*=[\r\n]*\'(.*?)\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("src[\r\n]*=[\r\n]*\"(.*?)\"", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
// lonely script tags
Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),
Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
// eval(...)
Pattern.compile("eval\((.*?)\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
// expression(...)
Pattern.compile("expression\((.*?)\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
// javascript:...
Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE),
// vbscript:...
Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE),
// 空格英文单双引号
Pattern.compile("[\s'"]+", Pattern.CASE_INSENSITIVE),
// onload(...)=...
Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
// alert
Pattern.compile("alert(.*?)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("<", Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile(">", Pattern.MULTILINE | Pattern.DOTALL),
//Checks any html tags i.e. <script, <embed, <object etc.
Pattern.compile("(<(script|iframe|embed|frame|frameset|object|img|applet|body|html|style|layer|link|ilayer|meta|bgsound))")
};

/**
* 对请求对象参数进行过滤校验
*
* @param params
* @return
*/
public static String filterBody (String params) {
try {
if (StringUtils.isBlank(params)) {
return params;
}
final Map<String, Object> map = JSONObject.parseObject(params, Map.class);
if (map.isEmpty()) {
return params;
}

// 参数过滤
final Iterator<Map.Entry<String, Object>> iterator = map.entrySet().stream().iterator();
while (iterator.hasNext()) {
final Map.Entry<String, Object> next = iterator.next();
next.setValue(filterParam(next.getValue()));
}
return JSON.toJSONString(map);
} catch (Exception e) {
log.error("XSS过滤异常:", e);
}
return params;
}

/**
* 对请求字符串参数进行过滤校验
*
* @param param
* @param <T>
* @return
*/
public static <T> Object filterParam (T param) {
if (param instanceof String) {
try {
String value = String.valueOf(param);
for (Pattern pattern : PATTERNS) {
value = pattern.matcher(value).replaceAll("");
}
return value;
} catch (Exception e) {
log.error("XSS参数过滤异常:", e);
}
}
return param;
}
}
  • XSS过滤参数的正则工具类
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
less复制代码/**
*
* @Author: ZRH
* @Date: 2021/11/17
*/
@RestController
public class XssTest {

@PostMapping("/xss/test")
public String test (@RequestBody JSONObject jsonObject) {
System.out.println(jsonObject.toJSONString());
return "OK";
}

@GetMapping("/xss/test")
public String test (@RequestParam Integer data, @RequestParam String result) {
System.out.println(data);
return "OK";
}
}

模拟请求响应结果:
17:07:06.597 - [http-nio-8888-exec-1] - getHeaders -> header:content-type,过滤前参数:application/json,过滤后参数:application/json
17:07:06.598 - [http-nio-8888-exec-1] - getHeaders -> header:token,过滤前参数:123,过滤后参数:123
17:07:06.598 - [http-nio-8888-exec-1] - getHeaders -> header:a,过滤前参数:123,过滤后参数:123
17:07:06.598 - [http-nio-8888-exec-1] - getHeaders -> header:b,过滤前参数:<script>alert("XSS");</script>,过滤后参数:
17:07:06.599 - [http-nio-8888-exec-1] - getHeaders -> header:content-length,过滤前参数:67,过滤后参数:67
17:07:06.599 - [http-nio-8888-exec-1] - getHeaders -> header:host,过滤前参数:localhost:8888,过滤后参数:localhost:8888
17:07:06.599 - [http-nio-8888-exec-1] - getHeaders -> header:connection,过滤前参数:Keep-Alive,过滤后参数:Keep-Alive
17:07:06.599 - [http-nio-8888-exec-1] - getHeaders -> header:user-agent,过滤前参数:Apache-HttpClient/4.5.12 (Java/11.0.8),过滤后参数:Apache-HttpClient/4.5.12(Java/11.0.8)
17:07:06.599 - [http-nio-8888-exec-1] - getHeaders -> header:accept-encoding,过滤前参数:gzip,deflate,过滤后参数:gzip,deflate
17:07:06.648 - [http-nio-8888-exec-1] - getInputStream -> 过滤前参数:{ "a": "1", "b": 2, "c": "<script>alert(\"XSS\");</script>"},过滤后参数:{"a":"1","b":2,"c":""}
{"a":"1","b":2,"c":""}

最后

  • 除了可以使用过滤器以外,还可以使用拦截器进行拦截校验。大致流程也差不多,先获取参数,然后进行校验,最后重新赋值。
  • 上述代码例子只是简单粗化版,在实际项目中要根据需求进行代码调整和性能优化后才可在线上使用。
  • 虚心学习,共同进步 -_-

本文转载自: 掘金

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

Docker 核心架构及入门实例 1 Docker 核心架

发表于 2021-11-18

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

  1. Docker 核心架构

  • 镜像: 一个镜像代表一个应用环境,他是一个只读的文件,如 mysql镜像,redis镜像,nginx镜像等
  • 容器: 镜像每次运行之后就是产生一个容器,就是正在运行的镜像,特点就是可读可写
  • 仓库:用来存放镜像的位置,是镜像下载和上传的位置
  • dockerFile:docker生成镜像配置文件,用来书写自定义镜像的一些配置
  • tar:一个对镜像打包的文件,日后可以还原成镜像
    在这里插入图片描述
  1. Docker 换源

我们知道docker默认不是国内的镜像源的,所以我们要换一个国内源头。

  • ubuntu
1
shell复制代码sudo vim /etc/docker/daemon.json(如果不存在则创建)

填入一下内容

1
2
3
4
5
6
7
8
9
10
11
shell复制代码{
"registry-mirrors":[
"http://docker.mirrors.ustc.edu.cn",
"http://hub-mirror.c.163.com",
"http://registry.docker-cn.com"
] ,
"insecure-registries":[
"docker.mirrors.ustc.edu.cn",
"registry.docker-cn.com"
]
}

重启服务

1
shell复制代码service restart docker
  • window

在这里插入图片描述

在这里换源

在这里插入图片描述

然后重启服务
在这里插入图片描述

  1. 简单例子

1
shell复制代码docker run hello-world

在这里插入图片描述
当我们运行这条命令的时候,这里会显示本地没有这个镜像,就会去拉取这个镜像

在这里插入图片描述
然后就成功运行了

  • 查看本地的镜像
1
shell复制代码docker images

在这里插入图片描述

  • 查看容器运行情况
1
shell复制代码docker ps -a

在这里插入图片描述

本文转载自: 掘金

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

更新草稿功能、发布自动部署工具

发表于 2021-11-18

那天写随笔,好不容易写完,结果点发布时候session过期了,直接重定向到登陆,好家伙,这才体会到自动保存草稿的重要性。

因为我一般不直接在博客中写,因为很多文章都涉及到图片,我的服务器带宽小,加载图片太费时,所以经常在掘金写完,吧markdown复制过来,所以没有增加草稿功能,这也就是为啥大家经常看到的图片水印中有掘金社区几个字。

但是自己的随笔还是在我的博客中写,前几天才发现这个问题,所以抽时间增加了定期保存草稿功能。

image.png

github: github.com/houxinlin/O…

第二个准备开源的东西是一个自动部署工具,因为在个人的项目中,发布一个应用,通常最老的办法是手动上传,或者是宝塔之类的工具。

但是问题在于,上传速度,还有如果发布后发现一个小错误,就得重新上传,很费时间,所以我基于Github的WebHook功能,做了一个自动部署工具。

功能如下:

  1. 拉去Github项目,根据设置的命令、脚本编译/部署
  2. 单独执行Gradle的Task,因为可以检测出Gradle中所有的Task,包括自己写的。
  3. Github发出push推送时候自动执行命令/脚本。
  4. WebSocket推送所有日志。

image.png

录屏_选择区域_20211117221318.gif

其中对Gradle的项目支持最好。

因为要拉去Github项目的私有仓库时候,需要认证,系统是通过SSH,就需要先生成密钥,当然也不能让你手动去生成,这些系统都已经集成。

我们部署应用一般分类两步,第一步是根据你项目的构建工具进行打包,然后执行特定脚本,这个脚本具体任务由你决定。所以在这里提供了这两个定义方式。

image.png

比如系统在拉取VUE项目后,首先通过npm run build打包,接着要把他dist下的内容复制到nginx下,所以这部分要定义在脚本中。

之后就是系统调用执行了,整体系统比较简单,但是会提升以往的发布速度。

整体的运行流程如下:

  1. 本地执行git push
  2. github向我们的系统发送push消息
  3. 系统收到后重新pull项目
  4. 执行打包命令
  5. 执行脚本任务
  6. WebSocket通知用户。

但是这个应用这两个还在测试,基本可以使用了,在周六会在github开源。

但你也可以使用jenkins,但我喜欢造轮子。

本文转载自: 掘金

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

Python爬虫实战,pymysql模块,Python实现抓

发表于 2021-11-18

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

前言

利用Python爬取QQ音乐评论。废话不多说。

让我们愉快地开始吧~

开发工具

Python版本: 3.6.4

相关模块:

requests模块;

re模块;

pymysql模块;

以及一些Python自带的模块。

环境搭建

安装Python并添加到环境变量,pip安装需要的相关模块即可。

通过这次爬取,学习了数据库MySQL,因为之前都是在windows上操作,而这回需要在Mac上操作,所以就在Mac上安装了MySQL以及MySQL的管理工具Sequel Pro,最后也是安装成功,数据库连接也没有问题。

终端

设置

接下来创建数据库,表格及主键信息。

1
2
3
4
5
6
python复制代码import pymysql
# 创建数据库
db = pymysql.connect(host='127.0.0.1', user='root', password='774110919', port=3306)
cursor = db.cursor()
cursor.execute("CREATE DATABASE QQ_Music DEFAULT CHARACTER SET utf8mb4")
db.close()

针对QQ音乐中去年夏天的网页进行分析,查看了所有评论的尾页,发现时间缩水了,因为热评中有一条评论的时间7月12号,而所有评论最后一页的时间却是7月16号。很明显,所有评论并不是货真价实的所有评论,不知这算不算QQ音乐的BUG。

评论1

评论2

还有一个就是直接点击最后一页的时候,并不能直接返回真正的信息,需要从最后一页往前翻,到了真正的信息页时,然后再往后翻,才能得到最后一页的真正信息。

评论3

评论4

同样是Ajax请求,确认网址后,分析一下请求头,发现主要是三个参数发生变化:jsoncallback

pagenum

lasthotcommentid

pagenum不难理解,就是页数。jsoncallback经过实验后,发现并不会影响请求,所以设置时无需改动,lasthotcommentid的值对应的是上一页最后一个评论者的ID,所以需要随时改动。

即改变pagenum,lasthotcommentid的值,就可成功实现请求。

图片

图片

图片

部分代码

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
python复制代码import re
import json
import time
import pymysql
import requests

URL = 'https://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg?'

HEADERS = {
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36'
}

PARAMS = {
'g_tk': '5381',
'jsonpCallback': 'jsoncallback4823183319594757',
'loginUin': '0',
'hostUin': '0',
'format': 'jsonp',
'inCharset': 'utf8',
'outCharset': 'GB2312',
'notice': '0',
'platform': 'yqq',
'needNewCode': '0',
'cid': '205360772',
'reqtype': '2',
'biztype': '1',
'topid': '213910991',
'cmd': '8',
'needmusiccrit': '0',
'pagenum': '0',
'pagesize': '25',
'lasthotcommentid': '',
'callback': 'jsoncallback4823183319594757',
'domain': 'qq.com',
'ct': '24',
'cv': '101010',
}

LAST_COMMENT_ID = ''

db = pymysql.connect(host='127.0.0.1', user='root', password='774110919', port=3306, db='QQ_Music', charset='utf8mb4')
cursor = db.cursor()


def get_html(url, headers, params=None, tries=3):
try:
response = requests.get(url=url, headers=headers, params=params)
response.raise_for_status()
response.encoding = 'utf-8'
except requests.HTTPError:
print("connect failed")
if tries > 0:
print("reconnect...")
last_url = url
get_html(last_url, headers, tries-1)
else:
print("3 times failure")
return None
return response

if __name__ == '__main__':
main()

最后成功获取评论信息

打印数据

获取评论信息

本文转载自: 掘金

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

Redis6源码系列(二)- 自动碎片整理defrag 1、

发表于 2021-11-18

1、内存碎片

运行在用户空间(user space)的进程无法直接执行内核代码或者访问内核函数来分配内存资源,需要通过 系统调用接口brk/sbrk(),请求系统内核来操作。但是系统调用会使得CPU从用户态(user mode)切换到内核态(kernel mode),这在需要频繁申请、释放内存的使用场景下会带来较大的性能开销。

为了尽量减少系统调用brk/sbrk()的调用次数,内存管理函数malloc/free()在实现上做了一定的优化。

空闲内存列表.png

一般情况下,在使用free()函数释放内存时不降低 programe break 的位置,而是将需要释放的内存添加到 空闲内存列表 ,供malloc()函数后续循环使用。

也就是说,malloc()函数在申请内存时,会优先在空闲内存列表查找大于或等于申请大小的内存块。如果找到满足需求的内存块,直接返回给调用者;如果内存块较大,可能会对其进行分割,在将一块大小满足需求的内存返回给调用者的同时,把多余的内存块保留着空闲内存列表中。

malloc分配机制.png

Redis自身没有实现底层内存的管理机制,而是依赖于jemalloc/tcmalloc等内存分配器(allocator)的malloc/free()函数族;在删除key或者清除过期keys的时候,调用free()函数来释放内存。实际上这部分内存可能并没有及时返还给操作系统,而是由内存分配器继续持有。

在经过一段时间的使用后,Redis可能会持有大量分配了却没有使用的内存空间,这部分空间被称为 内存碎片。Redis的内存碎片情况可以通过 INFO MEMORY 命令查看:

1
2
3
4
5
6
7
8
9
10
11
csharp复制代码[root@localhost redis-6.2.6]# redis-cli info memory
# Memory
// 进程申请内存
used_memory:934384
// 实际分配内存
used_memory_rss:2830336
// 碎片率
mem_fragmentation_ratio:3.20
// 碎片大小(字节)
mem_fragmentation_bytes:1946360
...

Redis作为一款内存数据库(in-memory database),需要频繁的分配、释放内存,持有适量的空闲内存能有效减少系统性能开销、提升内存分配速度。

但是根据malloc()函数的内存分配机制可以知道,维护在空闲内存列表的 内存块 在经过malloc()函数多次地查找、分割之后,会变得越来越小。直至最后,空闲内存列表中包含大量的小块内存,然而这部分内存的任意一块都无法满足malloc()函数的内存分配需求。

例如,此时堆空间中有总数40k的空闲内存块,但是无法满足一个20k大小的数据的的内存分配需求:

空间不足.png

在物理内存资源紧张的情况下,大量的内存碎片会导致Redis出现 swap交换 甚至是 内存溢出(oom)的情况,影响Redis服务的性能和稳定性。

注:更多内存分配相关的内容,可以查看 Redis6源码系列(一)- 内存管理zmalloc

2、Memory compaction

内存碎片的问题不仅是体现在用户进程上,还体现在操作系统内核上。

在现代操作系统体系中,往往使用大页面(huge pages)来提升处理器的性能;但是huge pages要求系统能够找到连续的物理内存区域,这些区域不仅要求足够大,而且还要求能正确进行对齐。由于大量内存碎片的存在,系统很可能无法找到满足需求的连续内存空间。

为了解决碎片的问题,内核开发人员采用了各种方法来进行尝试,其中就包含 内存压缩(Memory compaction,也称为内存紧缩)技术。

内存压缩1.png

假定一块内存区域如上图所示:白色为空闲内存页,着色的部分为已被分配使用的内存页。

我们可以简单的认为,内存压缩由2个步骤组成:

标识内存页

可移动内存页列表

从内存区域的地步开始,标识已分配使用的内存页,并构造成一个已分配内存页表,称为可移动内存页列表(Movanle pages)

空闲内存页列表

同时,从内存区域的顶部开始,标识未被分配使用的空闲内存页,并构造成空闲内存页列表(Free pages)

内存压缩2.png

页面迁移

两个标识并创建内存页列表的动作 在内存区域靠近中间的部分相遇,此时将 已分配使用的页面 移动到 内存区域顶部的 空闲空间。

内存压缩3.png

已分配内存页移动后,就得到了一块较为规整的内存区域。当然,这里是一个简化的逻辑,实际上内存压缩(Memory compaction)的实现相当复杂,比如可移动内存页的识别、内存页的移动、压缩动作的触发等等一系列“细节”都是不容易实现的。

3、Redis的碎片整理

在查看Redis内存使用情况时,除了使用 info 命令之外,还可以考虑 memory 命令

内存统计

使用 memory stats 命令可以查看Redis服务的内存统计信息:

1
2
3
4
5
6
7
8
php复制代码[root@localhost redis-6.2.6]# ./src/redis-cli memory stats
// Redis使用内存的峰值
1) "peak.allocated"
2) (integer) 931888
// Redis 使用其分配器分配的总字节数
3) "total.allocated"
4) (integer) 872024
...

memory stats 命令返回的结果几乎都能在 info memory 命令的结果中找到对应的数据项。

内存分配状态

在使用jemalloc作为分配器时,可以查看内存分配状态的分析报告:

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码[root@localhost redis-6.2.6]# ./src/redis-cli memory malloc-stats
___ Begin jemalloc statistics ___
Version: "5.1.0-0-g0"
Build-time option settings
config.cache_oblivious: true
...
Arenas: 16
Quantum size: 8
Page size: 4096
Maximum thread-cached size class: 32768
...  
--- End jemalloc statistics ---

内存清理:purge

内存清理 memory purge 同样是jemalloc分配器特有的命令,在使用其他分配器时并不支持。

在进程终止的时候,其所占用的所有内存都会返还给操作系统,所以很多程序的实现中都会依赖这种内存的“自动释放”机制。

但是Redis作为一个数据库服务进程,停机会是一个影响比较大的操作,在常规的生产环境下不应该也不允许经常性的停机重启服务。所以就需要有可以在不停机的情况下清理内存碎片的方法,这就是 memory purge 命令:

1
2
csharp复制代码[root@localhost redis-6.2.6]# ./src/redis-cli memory purge
OK

自动整理:defrag

Redis提供了内存碎片自动整理功能(Active Defragmentation),允许服务实例在不停机、无需人工干预的情况下主动整理内存碎片。通过参数设置 config set activedefrag yes 即可启用:

1
2
3
4
5
perl复制代码[root@localhost redis-6.2.6]# ./src/redis-cli config get activedefrag
1) "activedefrag"
2) "no"
[root@localhost redis-6.2.6]# ./src/redis-cli config set activedefrag yes
OK

内存碎片自动整理功能最早是在 Redis 4.0 版本引入的,不过在当时这只是一个实验性质的特性。现如今的Redis已经发展到了 6.x 版本,实验性质(experimental)的警告标识也早就已经从配置文件中移除了。

来看下Redis对Active Defragmentation的介绍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bash复制代码########################### ACTIVE DEFRAGMENTATION #######################
#
# What is active defragmentation?
# -------------------------------
#
# Active (online) defragmentation allows a Redis server to compact the
# spaces left between small allocations and deallocations of data in memory,
# thus allowing to reclaim back memory.
#
# Fragmentation is a natural process that happens with every allocator (but
# less so with Jemalloc, fortunately) and certain workloads. Normally a server
# restart is needed in order to lower the fragmentation, or at least to flush
# away all the data and create it again. However thanks to this feature
# implemented by Oran Agra for Redis 4.0 this process can happen at runtime
# in a "hot" way, while the server is running.
#
# Basically when the fragmentation is over a certain level (see the
# configuration options below) Redis will start to create new copies of the
# values in contiguous memory regions by exploiting certain specific Jemalloc
# features (in order to understand if an allocation is causing fragmentation
# and to allocate it in a better place), and at the same time, will release the
# old copies of the data. This process, repeated incrementally for all the keys
# will cause the fragmentation to drop back to normal values.

简单地理解,在内存碎片达到一定阈值时,Redis会利用某些特定的Jemalloc特性对碎片空间进行整理。换言之,Redis的Active Defragmentation特性只在使用Jemalloc作为底层的分配器时有效。

这一点在配置文件中也有声明:

1
2
3
4
bash复制代码# Important things to understand:
# 1. This feature is disabled by default, and only works if you compiled Redis
#   to use the copy of Jemalloc we ship with the source code of Redis.
#   This is the default with Linux builds.

启用defrag

默认情况下,内存碎片自动管理功能(defrag)是禁用的,可以通过 CONFIG SET activedefrag yes 命令启用。

相关的配置项有以下几个,在清楚地了解每项配置的含义之后可以根据需求进行调整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码# Enabled active defragmentation
activedefrag no
​
# Minimum amount of fragmentation waste to start active defrag
active-defrag-ignore-bytes 100mb
​
# Minimum percentage of fragmentation to start active defrag
active-defrag-threshold-lower 10
​
# Maximum percentage of fragmentation at which we use maximum effort
active-defrag-threshold-upper 100
​
# Minimal effort for defrag in CPU percentage, to be used when the lower
# threshold is reached
active-defrag-cycle-min 1
​
# Maximal effort for defrag in CPU percentage, to be used when the upper
# threshold is reached
active-defrag-cycle-max 25
​
# Maximum number of set/hash/zset/list fields that will be processed from
# the main dictionary scan
active-defrag-max-scan-fields 1000

根据作用可以将这些配置项归类为三类,分别是 功能开关、碎片的整理力度、资源的使用情况:

功能开关

  • activedefrag:内存碎片整理总开关,默认为禁用状态 no
  • active-defrag-ignore-bytes:可容忍的内存碎片量(字节),内存碎片达到该阈值时允许整理;默认允许最大持有100mb的内存碎片
  • active-defrag-threshold-lower:可容忍的内存碎片率,内存碎片率达到该阈值时允许整理;默认允许存在10%的内存碎片

在 同时 满足上面三项配置时,内存碎片自动整理功能才会启用

整理力度

  • active-defrag-threshold-upper:内存碎片空间占操作系统分配给 Redis 的总空间比例达到此阀值(默认100%)时,则尽最大努力整理
  • active-defrag-max-scan-fields:碎片整理 扫描set/hash/zset/list时,仅当 set/hash/zset/list 的长度小于此阀值时,才会将此key加入碎片整理

资源占用

  • active-defrag-cycle-min:清理内存碎片占用 CPU 时间的比例不低于此阀值(默认1%),保证清理能正常开展
  • active-defrag-cycle-max:一旦超过则停止清理,从而避免在清理时,大量的内存拷贝阻塞 Redis,导致其他请求延迟

在实际使用中,建议是在Redis服务出现较多的内存碎片时启用(内存碎片率大于1.5),正常情况下尽量保持禁用状态。

4、defrag 实现

内存碎片自动整理功能(Active Defragmentation)是一项比较有意思的特性,来看看它是怎么实现的。

HAVE_DEFRAG

在分析Redis内存分配管理模块 zmalloc 的时候,发现头文件中根据宏变量 HAVE_DEFRAG 定义了2个函数:

1
2
3
4
5
6
7
8
9
10
11
12
arduino复制代码// 1、定义变量
#if defined(USE_JEMALLOC) && defined(JEMALLOC_FRAG_HINT)
#define HAVE_DEFRAG
#endif
​
// 2、如果存在变量HAVE_DEFRAG,则编译以下函数
#ifdef HAVE_DEFRAG
// 释放内存
void zfree_no_tcache(void *ptr);
// 分配内存
void *zmalloc_no_tcache(size_t size);
#endif

这2个函数分别用于内存的分配和释放,在实现上区别于常规的分配和释放函数zmalloc/zfree()。以 zmalloc_no_tcache() 为例,内部通过调用je_mallocx()函数来分配内存;je_mallocx()会绕过线程缓存,直接分配内存块,这是在自动内存碎片整理时所要使用到的函数。

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
scss复制代码#elif defined(USE_JEMALLOC)
...
// 重命名je_mallocx函数为mallocx
#define mallocx(size,flags) je_mallocx(size,flags)
// 重命名je_dallocx函数为dallocx
#define dallocx(ptr,flags) je_dallocx(ptr,flags)
#endif

// 更新已使用内存大小函数
#define update_zmalloc_stat_alloc(__n) atomicIncr(used_memory,(__n))
#define update_zmalloc_stat_free(__n) atomicDecr(used_memory,(__n))

// 已使用内存大小计时器
static redisAtomic size_t used_memory = 0;

/* Allocation and free functions that bypass the thread cache
* and go straight to the allocator arena bins.
* Currently implemented only for jemalloc. Used for online defragmentation. */
// 如果存在变量HAVE_DEFRAG,则编译以下函数
#ifdef HAVE_DEFRAG
void *zmalloc_no_tcache(size_t size) {
ASSERT_NO_SIZE_OVERFLOW(size);
// 分配内存
void *ptr = mallocx(size+PREFIX_SIZE, MALLOCX_TCACHE_NONE);
// 检查分配情况
if (!ptr) zmalloc_oom_handler(size);
// 更新内存使用统计信息
update_zmalloc_stat_alloc(zmalloc_size(ptr));
return ptr;
}

void zfree_no_tcache(void *ptr) {
if (ptr == NULL) return;
// 更新内存使用统计信息
update_zmalloc_stat_free(zmalloc_size(ptr));
// 释放内存
dallocx(ptr, MALLOCX_TCACHE_NONE);
}
#endif

zmalloc_no_tcache()和zfree_no_tcache()函数的定义依赖于宏变量 HAVE_DEFRAG ;从上面源码中的 使用宏定义对je_mallocx()函数重命名 的逻辑不难看出来,HAVE_DEFRAGE 变量的定义需要满足当前使用Jemalloc作为底层内存分配器这一条件(存在 USE_JEMALLOC 变量)。

1
2
3
4
5
6
arduino复制代码/* We can enable the Redis defrag capabilities only if we are using Jemalloc
* and the version used is our special version modified for Redis having
* the ability to return per-allocation fragmentation hints. */
#if defined(USE_JEMALLOC) && defined(JEMALLOC_FRAG_HINT)
#define HAVE_DEFRAG
#endif

这里需要留意的是 defined(JEMALLOC_FRAG_HINT),判断是否有定义 JEMALLOC_FRAG_HIT 变量。

JEMALLOC_FRAG_HIT 变量的定义在Jemalloc的依赖文件 jemalloc_macros.h.in 中,用于标识当前版本Jemalloc支持碎片整理。标准的Jemalloc内存分配器中是不包含这个变量的,Redis使用的是经过修改的Jemalloc版本。

1
2
arduino复制代码/* This version of Jemalloc, modified for Redis, has the je_get_defrag_hint() function. */
#define JEMALLOC_FRAG_HINT

注释上面的 je_get_defrag_hint() 在Redis 4(使用jemalloc4)中能找到,是 jemalloc.c 提供的一个函数;但是在后续版本中,碎片整理功能的实现有较大的调整,已经不再提供该函数的实现了。

初始化

工具有了,但是怎么去使用又是一个问题。Redis源码中包含了一个叫 defrag.c 的文件,从命名上可以猜测到,自动内存整理功能(Active Memory Defragmentation)的实现应该就在这里。

Defrag由配置项 activedefrag 、active-defrag-ignore-bytes 、active-defrag-threshold-lower 联合决定是否启用,那么在服务启动读取配置文件 redis.conf 之后,就应该会有判断是否启用的逻辑。

Redis程序入口是 server.c 文件的 main()函数,在加载和解析配置文件后调用 initServer() 函数执行初始化服务逻辑,初始化服务的逻辑里面包含一个创建时间事件(aeTimeEvent)的动作。

初始化创建的这个时间事件里面包含了大部分需要异步完成操作,其中就包含自动内存碎片整理:

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
scss复制代码int main(int argc, char **argv) {
   // 加载、解析配置信息等操作
  ...
   // 初始化服务
   initServer();
   // 其他操作
  ...
}
​
// 初始化服务
void initServer(void) {
  ...
   // 创建定时器,包含异步的增量操作如客户端超时、key过期等
   if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
       serverPanic("Can't create event loop timers.");
       exit(1);
  }
  ...
}
​
/* This is our timer interrupt, called server.hz times per second.
* Here is where we do a number of things that need to be done asynchronously.
* For instance:
*
* - Active expired keys collection (it is also performed in a lazy way on
*   lookup).
* - Software watchdog.
* - Update some statistic.
* - Incremental rehashing of the DBs hash tables.
* - Triggering BGSAVE / AOF rewrite, and handling of terminated children.
* - Clients timeout of different kinds.
* - Replication reconnection.
* - Many more...
*
* Everything directly called here will be called server.hz times per second,
* so in order to throttle execution of things we want to do less frequently
* a macro is used: run_with_period(milliseconds) { .... }
*/
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
  ...
   /* Handle background operations on Redis databases. */
   databasesCron();
  ...
}
​
// 后台执行的增量操作,例如key过期、rehashing
void databasesCron(void) {
   // key过期失效处理
   if (server.active_expire_enabled) {
       if (iAmMaster()) {
           activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
      } else {
           expireSlaveKeys();
      }
  }
​
   /* Defrag keys gradually. */
   // 渐进式碎片整理
   activeDefragCycle();
  ...
}

服务初始化涉及较多的代码逻辑,去除掉不关联的部分后将函数调用进行简化,可以得到调用链如下:

activedefrag.png

Redis的ae事件模型我们先不去深究,可以简单认为这里的 aeCreateTimeEvent() 函数创建了一个每秒执行一次的定时器。

defrag.c

从Redis服务初始化的执行逻辑可以知道,内存碎片整理的实现在 activeDefragCycle() 函数里面。再来看看 defrag.c 文件,它的内部实现主要就是由 activeDefragCycle()、activeDefragAlloc()、activeDefragStringOb() 这三个函数组成的。

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
arduino复制代码#include "server.h"
#include <time.h>
#include <assert.h>
#include <stddef.h>
​
#ifdef HAVE_DEFRAG
​
// 内存自动管理逻辑实现
......
​
#else /* HAVE_DEFRAG */
​
// 空实现,什么也不做
void activeDefragCycle(void) {
   /* Not implemented yet. */
}
​
void *activeDefragAlloc(void *ptr) {
   UNUSED(ptr);
   return NULL;
}
​
robj *activeDefragStringOb(robj *ob, long *defragged) {
   UNUSED(ob);
   UNUSED(defragged);
   return NULL;
}
​
#endif

未完待续…

本文转载自: 掘金

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

SpringBoot整合微信公众号开发(二) 扫码关注登录

发表于 2021-11-18

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

前言

上一篇 我们已经打通了微信服务端和我们本地项目之间的通道,接下来,我们来实现扫码登录的功能。

登录逻辑

  • 用户扫码
  • 带参传到服务端,服务端根据信息获取用户的具体信息
  • 本地服务端根据获取到的微信信息去做逻辑处理,用户已存在则登录,返回登录认证信息,不存在则先注册,之后返回登录认证信息。

具体实现

目前微信服务端已经可以和本地实现通信,接下来我们要去调取微信服务端的接口获取用户的信息,以及公众号二维码等信息。

获取这些信息的前提是获取用户微信服务端授权,基于Oauth2协议,我们已经有了appID,appsecret,接下来获取accessToken,就可以了。

因为频繁的使用到accessToken,所有这里我存在了redis里。

具体实习可以看后面的工具类

1. 获取微信公众号二维码

获取一个带参数的二维码,用户扫码时会将用户一些信息以及参数传给后端,接下我们就可以围绕着这些参数做文章了

2. 扫码触发事件

这里有个小坑,之前看文档一直不明白,微信扫码触发事件,参数是通过xml包的形式发送给我们的,所以正常拿是拿不到的,这里需要整合一下xml解析的工具类,拿到参数。

主要是我们可以拿到用户的openid,之后就是获取用户信息,然后具体的登录逻辑就可以根据实际情况去写代码了。

添加依赖

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.thoughtworks.xstream/xstream -->
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.18</version>
</dependency>

实现逻辑(伪代码)

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
java复制代码@ApiOperation(value = "用户关注/或者取消关注")
@PostMapping("/wechat")
public void follow(HttpServletRequest request) throws Exception {
try {
// 接受扫描二维码回调参数
Map<String, String> map = new HashMap<>();
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(request.getInputStream());
// 得到xml根元素
Element root = document.getRootElement();
XmlUtil.parserXml(root, map);
log.info("【map】= {}", map);
String userInfo = weChatUtil.getUserInfo(request.getParameter("openid"));
JSONObject userInfoJson = JSONUtil.parseObj(userInfo);
String openid = userInfoJson.getStr("openid");
String eventKey = map.get("EventKey").replace("qrscene_", "");
log.info(eventKey);
// 监听扫码回调事件
String event = map.get("Event");
switch (event) {
// 扫码触发
case "SCAN":
//查询用户存在就直接返回登录信息,不存在先注册再返回
break;
// 关注
case "subscribe":
//查询用户存在就直接返回登录信息,不存在先注册再返回
break;
// 取关
case "unsubscribe":
System.out.println("取关了");
break;
default:
log.info("【map】= {}", map);
break;
}
} catch (Exception e) {
throw new ServiceException("获取微信用户信息异常");
}
}

工具类

XmlUtil

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
java复制代码public class XmlUtil {
/**
* 扩展 xstream,获取CDATA内容
*/
public static XStream xstream = new XStream(new XppDriver() {
@Override
public HierarchicalStreamWriter createWriter(Writer out) {
return new PrettyPrintWriter(out) {
// 对所有xml节点的转换都增加CDATA标记
boolean cdata = true;

@Override
public void startNode(String name, @SuppressWarnings("rawtypes") Class clazz) {
super.startNode(name, clazz);
}

@Override
protected void writeText(QuickWriter writer, String text) {
if (cdata) {
writer.write("<![CDATA[");
writer.write(text);
writer.write("]]>");
} else {
writer.write(text);
}
}
};
}
});

/**
* xml解析为map
*
* @param root 根节点
* @param map 返回的map
*/
@SuppressWarnings("unchecked")
public static void parserXml(Element root, Map<String, String> map) {
// 得到根元素的所有子节点
List<Element> elementList = root.elements();
// 判断有没有子元素列表
if (elementList.size() == 0) {
map.put(root.getName(), root.getText());
} else {
// 遍历
for (Element e : elementList) {
parserXml(e, map);
}
}
}
}

WeChatUtil

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

@Value("${wechat.appID}")
private String appID;

@Value("${wechat.appsecret}")
private String appsecret;

@Autowired
private RedisCache redisCache;

private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
private static final String TICKET_URL = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s";
private static final String USER_INFO_URL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN";


/**
* 获取 access_token
*/
public String getAccessToken() {
String accessTokenUrl = String.format(ACCESS_TOKEN_URL, appID, appsecret);
String s = HttpUtil.get(accessTokenUrl);
JSONObject result = JSONUtil.parseObj(s);
String access_token = result.getStr("access_token");
Integer expires_in = result.getInt("expires_in");
redisCache.setCacheObject(Constants.ACCESS_TOKEN, access_token, expires_in, TimeUnit.SECONDS);
return access_token;
}

/**
* 获取 创建二维码的ticket
* @param scene_str
*/
public JSONObject getTicket(String scene_str) throws Exception {
String ticketUrl = String.format(TICKET_URL, getReidAccessToken());
String body = "{" +
""expire_seconds": 1200," +
""action_name": "QR_STR_SCENE"," +
""action_info": {" +
""scene": {" +
""scene_str": "" + scene_str + """ +
"}" +
"}" +
"}";
String ticketJson = HttpUtil.post(ticketUrl, body);
return JSONUtil.parseObj(ticketJson);
}

/**
* 根据 openid 和 access_token 获取用户信息
*/
public String getUserInfo(String openid) throws Exception {
String userInfoUrl = String.format(USER_INFO_URL, getReidAccessToken(), openid);
return HttpUtil.get(userInfoUrl);
}

private String getReidAccessToken() throws Exception {
String access_token = redisCache.getCacheObject(Constants.ACCESS_TOKEN);
if (StringUtils.isBlank(access_token)) {
// access_token 过期了
// 刷新 access_token
access_token = getAccessToken();
}
return access_token;
}

}

本文转载自: 掘金

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

gin框架实践【Go-Gin_Api】20 工具篇

发表于 2021-11-18

Hello,我是Rocket

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

引言

  • 新增了cmd工具,能支持生成读取mysql表生成Model、以及初始化表和表数据,源码就在项目里
  • github传送门
  • 喜欢的铁子们给点个star

1、make:model 支持读取mysql生成Model

代码在项目cmd/make-model.go

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
erlang复制代码package main

import (
"go-api/core"
"go-api/global"
"go-api/initialize"
"go-api/tool"

cmd "github.com/18211167516/go-cmd"
)

// versionCmd represents the version command
var structCmd = &cmd.Command{
Use: "make:model",
Short: "mysql转struct",
Long: `读取mysql的表结构转成Model文件`,
Run: func(Command *cmd.Command, args []string) {

table, _ := Command.Flags().GetString("table")
prefix, _ := Command.Flags().GetString("prefix")
file, _ := Command.Flags().GetString("file")
global.VP = core.Viper("../static/config/app.toml") //初始化配置
dsn := initialize.GetMasterDsn()

vip := core.Viper("./config/cmd.toml")
config := &tool.T2tConfig{
StructNameRtrims: vip.GetBool("StructNameRtrims"),//默认去除表名尾部s字符
UcFirstOnly: vip.GetBool("UcFirstOnly"),//
SavePath: vip.GetString("SavePath"),保存目录名
}
grom := tool.NewTable2Struct(config)
grom.
Table(table).
Prefix(prefix).
SavePath(file).
Dsn(dsn).
Run()
},
}

func init() {
cmd.RootCmd.AddCommand(structCmd)
structCmd.Flags().StringP("table", "t", "", "指定的表名,如果未指定则全部导出")
structCmd.Flags().StringP("prefix", "p", "", "表前缀")
structCmd.Flags().StringP("file", "f", "", "生成的目录")//如果不传默认会走配置文件SavePath
}

主要调用tool下table2struct.go

1
2
3
4
arduino复制代码//1、校验mysql链接
//2、检测文件夹是否存在,不存在就创建目录
//3、获取表信息
//4、循环执行生成model

我们做了一个比较有意思的处理;

1
2
3
4
5
6
7
8
9
c复制代码type SysRule struct {
ID int `gorm:"primary_key" json:"id" uri:"id"`
CreatedAt XTime `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt XTime `json:"updated_at" gorm:"autoUpdateTime" `
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
Role_name string `desc:"角色名称" json:"role_name" form:"role_name" gorm:"comment:角色名称;not null"`
Role_desc string `desc:"角色描述" json:"role_desc" form:"role_desc" gorm:"comment:角色描述;not null"`
Status *int `desc:"角色状态" json:"status" form:"status" gorm:"type:tinyint;size:1;default:1;comment:角色状态;"`
}

如果包含id,created_at,updated_at,deleted_at会合并成下边的model

1
2
3
4
5
6
c复制代码type SysRule struct {
Model
Role_name string `desc:"角色名称" json:"role_name" form:"role_name" gorm:"comment:角色名称;not null"`
Role_desc string `desc:"角色描述" json:"role_desc" form:"role_desc" gorm:"comment:角色描述;not null"`
Status *int `desc:"角色状态" json:"status" form:"status" gorm:"type:tinyint;size:1;default:1;comment:角色状态;"`
}

2、initdb 初始化表和表数据

代码在项目cmd/initdb.go

1
2
3
arduino复制代码//初始化表结构
//初始化默认数据
//具体代码请看源码

3、make:mysql 通过识别model生成mysql数据表

代码在项目cmd/make-mysql.go

1、在app/models/mysql.go 新增map用户存储实例化model的方法

1
go复制代码var AutoMigratFunc = make(map[string]func() interface{})

2、参考app/models/test.go 主要靠init方法注册

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码
type Test struct {
Model
Name string `desc:"菜单name" json:"name" form:"name" gorm:"comment:名称;not null"`
Sort int `desc:"菜单排序值" json:"sort" form:"sort" gorm:"type:int;size:10;default:1;comment:排序标记;not null"`
}

func init() {
AutoMigratFunc["test"] = func() interface{} {
return Test{}
}
}

3、具体生成方法 cmd/initdata/init.go AutoMigrate方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
lua复制代码func AutoMigrate(db *gorm.DB, table string) {

if db.Migrator().HasTable(table) {
log.Printf("[make:mysql]-->数据表【%s】已存在\n", table)
os.Exit(0)
}

if value, ok := models.AutoMigratFunc[table]; !ok {
log.Printf("make:mysql-->数据表【%s】没有定义model层init方法初始化struct\n", table)
os.Exit(0)
} else {
if err := db.AutoMigrate(value()); err != nil {
log.Printf("[make:mysql]-->生成数据表【%s】失败,err: %v\n", table, err)
os.Exit(0)
}
}

log.Println("[make:mysql]-->生成数据表【%s】成功", table)

}

4、后续计划

计划支持支持生成controller、service、view

  1. 系列文章

  • 连载一 golang环境搭建
  • 连载二 安装Gin
  • 连载三 定义目录结构
  • 连载四 搭建案例API1
  • 连载五 搭建案例API2
  • 连载六 接入swagger接口文档
  • 连载七 日志组件
  • 连载八 优雅重启和停止
  • 连载番外 Makefile构建
  • 连载番外 Cron定时任务
  • 连载番外 打造命令行工具
  • 连载番外 3天打造专属Cache(First day)
  • 连载番外 3天打造专属Cache(Second day)
  • 连载番外 3天打造专属Cache(Third day)
  • gin框架实践[Go-Gin_Api]2.0

本文转载自: 掘金

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

如何使用 Spring Boot Actuator 组件实现

发表于 2021-11-18

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

通过前面的学习,我们知道如何使用 Spring Boot 开发我们 Web 应用。这一讲我们将介绍 Spring Boot 中一个非常有特色的主题——系统监控。

系统监控是 Spring Boot 中引入的一项全新功能,它能够有效的对应用程序运行状态进行管理。它为我们的应用提供了强大的监控能力。随着互联网发展,现在的应用程序越来越复杂了,线上常常需要借助一些监控工具去帮助我们快速的定位问题。

Spring Boot Actuator 组件主要通过一系列 HTTP 端点提供的系统监控功能来实现系统监控。因此,接下来我们将引入 Spring Boot Actuator 组件,介绍如何使用它进行系统监控,以及如何对 Actuator 端点进行扩展。

引入 Actuator 组件

如何整合 Actuator,第一件事需要引用依赖。首先我们需要引入 Spring Boot Actuator 组件,具体操作为在 pom 中添加如下所示的 Maven 依赖:

1
2
3
4
xml复制代码 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

请注意,引入 Spring Boot Actuator 组件后,并不是所有的端点都对外暴露。这样的应用就整合好了, 我们把应用启动起来。我们就可以在启动日志中发现如下所示内容:

1
csharp复制代码Exposing 1 endpoint(s) beneath base path '/actuator'

通过浏览器访问 http://localhost:8080/actuator 端点后,会得到如下所示结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
json复制代码{
"_links": {
"self": {
"href": "http://localhost:8080/actuator",
"templated": false
},
"health": {
"href": "http://localhost:8080/actuator/health",
"templated": false
},
"health-path": {
"href": "http://localhost:8080/actuator/health/{*path}",
"templated": true
}
}
}

这种结果就是 HATEOAS 风格的 HTTP 响应。

health 端点

Actuator 端点是干嘛用的呢?这是 Spring Boot Actuator 的一个导航端点。它可以展示出 Spring Boot Actuator 所有端点。

通过浏览器访问 http://localhost:8080/actuator/health 端点后,会得到如下所示结果。

1
json复制代码{"status":"UP"}

health 端点是一个非常重要的端点。在后面的课程中我们会不断的用到。

它的作用是什么呢?是健康检查。什么是健康检查呢?检查的又是什么呢?检查的是应用的资源。什么是应用的资源呢?要解释清楚这一点,我们需要加一个配置。

1
ini复制代码management.endpoint.health.show-details=always

这个配置是做什么的呢?上述配置项指定了针对 health 端点需要显示它的详细信息。这时,如果我们重启 Spring Boot 应用程序,并重新访问 http://localhost:8082/actuator/health 端点,就可以获取如下所示的详细信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
json复制代码{
"status": "UP",
"components": {
"diskSpace": {
"status": "UP",
"details": {
"total": 222801424384,
"free": 16696889344,
"threshold": 10485760,
"exists": true
}
},
"ping": {
"status": "UP"
}
}
}

Spring Boot Actuator 支持检查哪些资源呢?这个就非常多了。如下图所示:

image.png

status 取值有四种取值。第一种 up 表示正常。第二种 DOWN 表示遇到了问题,不正常。第三种 OUT_OF_SERVICE 这表达的是什么意思呢,资源未在使用或者不该使用。第四种 UNKNOWN 不知道,Actuator 不知道状态是什么。

info 端点

Info 端点用于暴露 Spring Boot 应用的自身信息。在 Spring Boot 内部,它把这部分工作委托给了一系列 InfoContributor 对象,而 Info 端点会暴露所有 InfoContributor 对象所收集的各种信息。

访问 http://localhost:8080/actuator/info 端点后,返回了一个空。它是做什么用的呢?info 端点是 Actuator 里面的一个例外,它并不是一个监控端点,而是一个描述性的端点。它是用来描述应用的使用方式非常的简单。它的格式 key-value 形式,主要遵守这个格式,随便怎么写。

1
2
3
ini复制代码info.app.encoding=UTF-8
info.app.java.source=1.8.0_291
info.app.java.target=1.8.0_291

现在访问 http://localhost:8080/actuator/info 端点,我们就能得到如下的 Environment 信息。

1
2
3
4
5
6
7
8
9
json复制代码{
"app": {
"encoding": "UTF-8",
"java": {
"source": "1.8.0_291",
"target": "1.8.0_291"
}
}
}

同时我们还可以这么配置 info 端点,而不是硬编码这些值。我们就可以按照如下所示的配置,重写前面的示例,重启我们应用,最后得到同样的效果。

1
2
3
ini复制代码info.app.encoding=@project.build.sourceEncoding@
info.app.java.source=@java.version@
info.app.java.target=@java.version@

info 一般的使用是建议描述应用。什么意思呢?比如我可以写上应用的名称。项目的开发者是谁,开发者的邮箱,一旦我们的应用出现问题,我们就可以知道是谁开发的,找到相应的人解决问题。

暴露端点

前面我们说过 Actuator 有点提供了强大的监控能力。但是我们现在这个应用才有两个端点,那么如何暴露所有的端点?如果我们想看到默认情况下看不到的所有端点,则需要在配置文件中添加如下所示的配置信息。

1
ini复制代码management.endpoints.web.exposure.include=*

重启应用后,我们就能获取到 Spring Boot Actuator 暴露的所有端点,如下代码所示:

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
json复制代码{
"_links": {
"self": {
"href": "http://localhost:8080/actuator",
"templated": false
},
"beans": {
"href": "http://localhost:8080/actuator/beans",
"templated": false
},
"caches-cache": {
"href": "http://localhost:8080/actuator/caches/{cache}",
"templated": true
},
"caches": {
"href": "http://localhost:8080/actuator/caches",
"templated": false
},
"health": {
"href": "http://localhost:8080/actuator/health",
"templated": false
},
"health-path": {
"href": "http://localhost:8080/actuator/health/{*path}",
"templated": true
},
"info": {
"href": "http://localhost:8080/actuator/info",
"templated": false
},
"conditions": {
"href": "http://localhost:8080/actuator/conditions",
"templated": false
},
"configprops": {
"href": "http://localhost:8080/actuator/configprops",
"templated": false
},
"configprops-prefix": {
"href": "http://localhost:8080/actuator/configprops/{prefix}",
"templated": true
},
"env": {
"href": "http://localhost:8080/actuator/env",
"templated": false
},
"env-toMatch": {
"href": "http://localhost:8080/actuator/env/{toMatch}",
"templated": true
},
"loggers": {
"href": "http://localhost:8080/actuator/loggers",
"templated": false
},
"loggers-name": {
"href": "http://localhost:8080/actuator/loggers/{name}",
"templated": true
},
"heapdump": {
"href": "http://localhost:8080/actuator/heapdump",
"templated": false
},
"threaddump": {
"href": "http://localhost:8080/actuator/threaddump",
"templated": false
},
"metrics-requiredMetricName": {
"href": "http://localhost:8080/actuator/metrics/{requiredMetricName}",
"templated": true
},
"metrics": {
"href": "http://localhost:8080/actuator/metrics",
"templated": false
},
"scheduledtasks": {
"href": "http://localhost:8080/actuator/scheduledtasks",
"templated": false
},
"mappings": {
"href": "http://localhost:8080/actuator/mappings",
"templated": false
}
}
}

这些端点具体是什么,你可以到 Spring Boot 官网阅读文档,这里不做详细介绍。

Actuator 端点那么多,有时候有的端点并不想激活,那怎么办呢?比如只向激活 metrics。

只要配置 metrics 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
json复制代码{
"_links": {
"self": {
"href": "http://localhost:8080/actuator",
"templated": false
},
"metrics-requiredMetricName": {
"href": "http://localhost:8080/actuator/metrics/{requiredMetricName}",
"templated": true
},
"metrics": {
"href": "http://localhost:8080/actuator/metrics",
"templated": false
}
}
}

那如果我想激活多个端点,比如暴露 health 端点和 metrics 端点,端点与端点使用逗号分隔。具体配置如下:

1
ini复制代码management.endpoints.web.exposure.include=metrics,health

我们把应用重启下,访问 http://localhost:8080/actuator/ 端点,可以看到现在应用只会暴露 metrics 端点以及 health 端点,其他的都被隐藏了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
json复制代码{
"_links": {
"self": {
"href": "http://localhost:8080/actuator",
"templated": false
},
"health": {
"href": "http://localhost:8080/actuator/health",
"templated": false
},
"health-path": {
"href": "http://localhost:8080/actuator/health/{*path}",
"templated": true
},
"metrics-requiredMetricName": {
"href": "http://localhost:8080/actuator/metrics/{requiredMetricName}",
"templated": true
},
"metrics": {
"href": "http://localhost:8080/actuator/metrics",
"templated": false
}
}
}

但是 Actuator 其实有很多的配置。全部都是以 management 开头。这说明了 Actuator 提供了非常灵活的配置,帮助你自定义 Actuator 的各种行为。

到目前为止,我们用 Spring Boot Actuator 端点监控应用的时候,都是以 json 文本的形式在使用。而实际项目中,我们是不是往往需要一个可视化的监控工具。有没有这样的工具呢?肯定是有的。

总结

Spring Boot 内置的 Actuator 组件使得开发人员在管理应用程序运行的状态有了更加直接且高效的手段。我们引入了 Actuator 组件并介绍了该组件提供的一系列核心端点,同时重点分析了 Info 和 Health 这两个基础端点。

本文转载自: 掘金

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

数据结构之动态数组 数组(Array) 动态数组(Dynam

发表于 2021-11-18

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

数组(Array)

线性表

image.png

数组的特点

  • 数组是一种顺序存储的线性表,数组内所有元素的内存地址都是连续的,
  • 数组的长度一旦确定则不可更改
  • 数组只能存储同一类型的数据
  • 数组提供角标的方式访问元素
1
java复制代码int[] array=new int[]{11,22,33,44,55}

image.png

1
2
c复制代码- array存放在栈空间中
- array数组中的元素放在堆空间中,每个数组元素占用4个字节
  • 数组的缺点
+ 数组一般都是都无法动态修改容量,只有在初始化数组的时候固定好数组长度。
+ 实际应用中,数据的容量都是动态改变的。

动态数组(Dynamic Array)

概念

动态数组是指在声明时没有确定数组大小的数组,可以根据用户需求可以自动增加或者减少数组空间,有效的利用空间。

接口设计

  • 创建ArrayList类,创建size属性来管理数组中元素的个数, 创建elements属性来管理存取的数据。
  • 可以对动态数组进行增删改查操作。

image.png

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
java复制代码package array;

/**
* @program: data-structure
* @author:
* @created: 2021/11/16
* 动态数组源码解析
*/
public class ArrayList<E> {
//元素的数量
private int size;

//所有的元素
private E[] elements;

//初始容量
private static final int DEFAULT_CAPACITY = 10;

//查找不到元素就-1
private static final int ELEMENT_NOT_FOUND = -1;

// 元素的数量
int size();

// 是否为空
boolean isEmpty();

// 是否包含某个元素
boolean contains(E element);

// 添加元素到最后面
void add(E element);

// 返回index位置对应的元素
E get(int index);

// 设置index位置的元素
E set(int index, E element);

// 往index位置添加元素
void add(int index, E element);

// 删除index位置对应的元素
E remove(int index);

// 查看元素的位置
int indexOf(E element);

// 清除所有元素
void clear();
}

动态数组实现

构造方法

  • 如果构造的数组长度小于默认长度,则会以默认长度构建数组。
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 class ArrayList<E> {
//元素的数量
private int size;

//所有的元素
private E[] elements;

//初始容量
private static final int DEFAULT_CAPACITY = 10;

//查找不到元素就-1
private static final int ELEMENT_NOT_FOUND = -1;

/**
* 有参构造
*
* @param capacity
*/
public ArrayList(int capacity) {
//容量小于10一律扩充为10,三元表达式
capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
elements = (E[]) new Object[capacity];
}

/**
* 无参构造,默认创建长度为10的数组
*/
public ArrayList() {
this(DEFAULT_CAPACITY);
}
}

查看数组元素数量

  • size的值,即为元素的数量。
1
2
3
java复制代码public int size() {
return size;
}

数组是否为空

  • size为空,则表示数组为空
1
2
3
java复制代码public boolean isEmpty() {
return size == 0;
}

数组是否包含某个元素

  • 通过判断索引(indexOf)是否等于ELEMENT_ON_FOUND即可。
1
2
3
java复制代码public boolean contains(E element) {
return indexOf(element) != ELEMENT_NOT_FOUND;
}

根据索引获取对应位置的元素

  • 通过数组下标进行查询
1
2
3
java复制代码public E get(int index) {
return elements[index];
}

根据索引和元素替换对应的老元素

  • 先获取原来的元素,然后把新增的元素替换到原来的元素位置,并且返回原来index位置的元素
1
2
3
4
5
Java复制代码public E set(int index, E element) {
E old = elements[index];
elements[index] = element;
return old;
}

添加元素

  • 添加元素就是将指定位置之后的元素统一后移一位,并将指定元素插入到指定位置

数组越界

  • 添加元素的时候索引的大小有限制,不能小于0, 也不能大于size。
1
2
3
4
5
java复制代码private void rangeCheckForAdd(int index) {
if (index < 0 || index > size) {
outOfBounds(index);
}
}

数组动态扩容(核心所在)

  • 由于数组长度是默认长度为10,那么当数组存满元素,就需要对该数组进行扩容操作。
  • 因为数组是无法动态增加的,就需要创建一个新的数组,并且数组容量一般都是原数组容量的1.5呗,然后将原数组的元素循环放入新数组中,这就是动态扩容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码private void ensureCapacity(int capacity) {
// 获取当前数组的容量
int oldCapacity = elements.length;
// 当前存储的元素个数 < 当前数组容量, 直接返回
if (oldCapacity >= capacity) {
return;
}
// 创建新的新容量为旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//根据新的容量创建新数组
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
// 拷贝原数组元素到新数组
newElements[i] = elements[i];
}
// 引用新数组
elements = newElements;
System.out.println("size=" + oldCapacity + ", 扩容到了" + newCapacity);
}

image.png

数组添加实现

  • 数组添加之前需要先验证索引是否越界
  • 然后判断当前数组是否需要扩容操作
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public void add(int index, E element) {
//检查下标是否越界
rangeCheckForAdd(index);
// 判断数组是否需要越界
ensureCapacity(size + 1);
// 先从后往前开始, 将每个元素往后移一位, 然后再赋值
for (int i = size; i > index; i--) {
elements[i] = elements[i - 1];
}
// 复制
elements[index] = element;
size++;
}

删除元素

  • 删除数组元素就是删除指定位置的元素,并将指定位置之后的元素统一前移一位

数组越界

  • 移除元素的时候索引的大小有限制,不能小于0, 也不能大于等于size。
1
2
3
4
5
6
7
8
9
java复制代码private void outOfBounds(int index) {
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
}

private void rangeCheck(int index) {
if (index < 0 || index >= size) {
outOfBounds(index);
}
}

动态缩容

  • 当数组中的元素删除后,可能会导致数组的剩余空间很大,会造成内存的浪费
  • 当删除数组中的元素,需要先判断是否打到缩容的条件,如果达不到,就不进行缩容处理
  • 动态缩容实现方法类似于扩容,当数组中容量小于某个值时,创建新的数组,然后将原有数组中的元素存入新数组即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码private void trimToSize() {
// 获取当前数组的容量
int oldCapacity = elements.length;
//设置新数组的容量为原数组容量的0.5倍
int newCapacity = oldCapacity >> 1;
// 当size大于等于容量的一半, 或则容量已经小于默认容量(10)时, 直接返回
if (size >= (newCapacity) || oldCapacity <= DEFAULT_CAPACITY) {
return;
}
//根据新的容量创建新数组
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
// 拷贝原数组元素到新数组
newElements[i] = elements[i];
}
elements = newElements;
System.out.println("size=" + oldCapacity + ", 缩容到" + newCapacity);

}

image.png

数组的删除实现

  • 数组删除之前需要先验证索引是否越界
  • 然后判断当前数组是否需要缩容操作
1
2
3
4
5
6
7
8
9
10
java复制代码public E remove(int index) {
rangeCheck(index);
trimToSize();
E old = elements[index];
for (int i = index; i < size; i++) {
elements[i] = elements[i + 1];
}
elements[--size] = null;
return old;
}

查询元素的对应位置

  • 通过循环查找元素的对应位置
  • 注意:假如数组中可以存储null,而null是不能调用equals方法的,所以需要对传入的元素进行判断,如果查找的元素是null,需要单独处理。
  • 当元素存在时返回索引,否则返回变量ELEMENT_ON_FOUND的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public int indexOf(E element) {
if (null == element) {
for (int i = 0; i < size; i++) {
if (elements[i] == null) {
return i;
}
}
} else {
for (int i = 0; i < size; i++) {
if (element.equals(elements[i])) {
return i;
}
}
}
return ELEMENT_NOT_FOUND;
}

本文转载自: 掘金

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

vearch源码阅读——http重要接口一览 基本名词解释

发表于 2021-11-18

基本名词解释

Vearch 是对大规模深度学习向量进行高性能相似搜索的弹性分布式系统。可以做一个和mysql类比的抽象理解,vearch就是一个分布式数据库,只不过存的数据的某些属性可能是向量。下面对vearch里数据相关一些名词做解释:

  1. db :一个库,类似mysql的一个数据库
  2. space :一个表空间,对应mysql的一个table
  3. partition :一个space上的数据分布在多个partition上,每个partition只能存在一台机器上,一个机器可以有多个partition
  4. doc :一个doc可以理解成mysql存的一条数据
  5. field :doc的属性,一个field可以理解成mysql table中的一列。
    • field有自己的type,如果type是vector,表明这个field是一个向量

在verach运行过程中,机器分为三种角色:

  1. router :接收对数据(doc)增删改查的请求,然后把请求发往ps
  2. master :接收对集群的操作,比如对db、space、partition的增删,还有对集群的维护操作
  3. ps :ps上存着一个或多个partition,ps的工作就是接收router的请求并调用c++ faiss库负责具体的向量运算了,是真正的worker

image.png

主要接口与问题

向量召回的步骤简单理解为这样:

  1. 模型同学训练好自己的模型
  2. 把所有物料经过计算算出向量,存在某个地方
  3. 线上服务机器把上一步算好的所有物料向量都拉到本地存着
  4. 一条请求过来,用户特征经过模型计算出一个用户向量
  5. 用户向量在步骤[3]中存的物料向量里搜索,取出最相近的topn个物料向量
  6. 召回完成

如果有了vearch,上述步骤[3]和步骤[5]都可以变成一条rpc请求

  • 对于步骤[3], 离线任务可以将自己训练的物料向量通过vearch的插入&批量插入接口存在vearch里
  • 对于步骤[5], 线上服务可以将本地搜索过程也变成一个rpc或http请求,通过vearch的查询(search)接口找到topn相近的向量
    这么做的好处是:
  • 避免线上服务定时拉取更新物料向量
  • 线上服务机器不需要存储物料向量,省内存
    但也存在一些担忧:
  • 通过rpc请求进行向量搜索时间大概率是没有本地搜索快
  • 正常召回只需要返回topN物料的ID以及用户向量与物料向量的内积即可,但是如果有的业务需要返回物料的向量,比如我要top3000物料的向量,那体积过大,明显不现实,只有把物料向量存在本地才能这么玩

在上述过程中,业务方主要需要使用的这几个接口:

  • 单条插入&批量插入
  • 查询
    下面对vearch接口工作流程进行一个梳理

vearch工作步骤

Vearch对用户http请求调用步骤大致都是:

  1. router收到指令,开始构建RouterRequest
  2. 解析请求,填充RouterRequest物料相关的信息
  3. 根据缓存和ETCD,找到对应的partition的具体机器信息
  4. 发送RouterRequest给partition所在的ps机器
  5. partition所在的ps机器接收到请求,调用gamma引擎执行c++代码进行向量运算

这里提出几个问题,最后回答

  1. 创建新的doc,怎么选择对应存储的partition
  2. router的缓存都有什么
  3. 怎么根据doc的id确认对应的partition

单条插入

单条插入的请求如下

1
2
3
4
5
6
7
8
9
vbnet复制代码curl -XPOST -H "content-type: application/json"  -d'
{
"field1": "value1",
"field2": "value2",
"field3": {
"feature": [0.1, 0.2]
}
}
' http://router_server/$db_name/$space_name

请求中的field与space的field一一对应,如果field类型是向量,通过feature:[xxx, xxx]写入,并且维数要与space中该field对应

对应调用handleUpdateDoc()方法,这个方法通过传入的http请求初始化一个UpdateRequest,这里需要注意的是为UpdateRequest设置PKey这里

1
2
ini复制代码URLParamID        = "_id"
args.Doc.PKey = params.ByName(URLParamID)

如果传入参数有 _id,那么PKey就等于传入的_id,否则为””(空)。我们这里是插入,不需要传入_id,所以_id为空。而当调用查询之类接口时,会传入_id。插入新物料时,后续的SetDocsField(docs)方法中会为插入的物料自动生成一个id,方法大概就是自增,这里不深究,只要知道router会为新插入的物料生成唯一id就行了。

完成后,调用updateDoc()函数处理初始化的UpdateRequest。

updateDoc()

updateDoc 函数通过传入的 pb UpdateRequest构建一个RouterRequest并发送给partition,步骤分为:

  1. 装填RouterRequest
  2. 发送请求给partition

RouterRequest的结构

RouterRequest结构如下

image.png

  • head里是请求的基本信息:包括用户名,密码,目标dbname以及spacename
  • md是一个map,记录了请求的方法和id
    • key为HandlerType时,value表示该请求对应的方法(增删改查)
    • key为MessageID时,value表示本条请求的唯一id
  • docs是本条请求的物料信息
  • space是本条请求对应的space的信息
  • sendMap的key是partitionID,value是要发给这个partition的信息,其中items包含了doc信息,其他别的借口时候再补充

updateDoc 关键的步骤代码如下:

1
2
3
4
5
6
7
8
9
css复制代码request := client.NewRouterRequest(ctx, docService.client)
request.SetMsgID()
.SetMethod(client.ReplaceDocHandler)
.SetHead(args.Head)
.SetSpace()
.SetDocs(docs)
.SetDocsField()
.PartitionDocs()
items := request.Execute()

装填

首先通过一串函数装填RouterRequest

  • SetMsgID()为本条request生成唯一id,填入md[MessageID]。
  • SetMethod()填写md[HandlerType],表明本request是一条更新请求
  • SetSpace()填写request的space字段,获取方式是先从router本地缓存找,找不到就去etcd里拿
  • SetDocs()填写request的docs数组
  • SetDocsField()为docs数组里每一个doc填写PKey和Fields字段,
    • 创建时,PKey为空,generateUUID为doc自动生成一个id
    • 这个函数在Fields里加了一个PKey
  • PartitionDocs()填充sendMap字段,就是把docs字段里的doc都加进sendmap[id]的items数组里
    • id是根据doc的PKey做哈希算出来的,这决定了该doc存在哪个partition上
      RouterRequest装填完毕,下一步就是发送了

router发送给partition

发送是通过RouterRequest的Execute()函数

所有要给partition发送请求的接口最后都会落在这个函数上,这个函数分为两步

  1. 构建rpcClient
  2. 正式发送请求

先来看构建rpcClient

上一步中,我们已经确认了要发往每个partition的数据,存在RouterRequest的sendMap成员里

首先通过partitionID获取对应partition的信息,包括机器地址等信息。获取的方式就是先从本地缓存中取,如果没有就从ETCD里拿,这里介绍一下router的本地缓存,router本地缓存如下图所示:

image.png

缓存相关都在router.client.masterclient.cliCache下面

  • router.client.masterclient.cliCache本身继承了sync.map,存储了nodeID对应的rpcClient,避免多次创建
  • partitionCache里存放了partitionID对应的partition相关信息,包括机器节点ID(没有地址)
  • serverCache里存放了NodeID对应的机器信息,包括IP、端口等
  • 如果在缓存里没有找到,router会访问etcd获取相关数据,router.client.masterclient.store就是etcd相关

继续回到构建rpcClient,构建它的关键就是填写ip和端口。从缓存和etcd拿到nodeID后,调用GetOrCreateRPCClient(ctx, nodeID),同样从缓存和etcd拿到nodeID对应的具体机器信息(地址、端口),并构造一个rpcClient,完毕

构建好以后,发送就完事了,远程调用的方法是UnaryHandler

partition处理请求

方法入口是UnaryHandler.Execute,根据请求是插入,调用update(ctx, store, req.Items)
这里贴一段代码,后面进入gamma引擎了,这里不做研究

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码func update(ctx context.Context, store PartitionStore, items []*vearchpb.Item) {
item := items[0]
docGamma := &gamma.Doc{Fields: item.Doc.Fields}
docBytes := docGamma.Serialize()
docCmd := &vearchpb.DocCmd{Type: vearchpb.OpType_REPLACE, Doc: docBytes}
if err := store.Write(ctx, docCmd, nil, nil); err != nil {
log.Error("Add doc failed, err: [%s]", err.Error())
item.Err = vearchpb.NewError(vearchpb.ErrorEnum_INTERNAL_ERROR, err).GetError()
} else {
item.Doc.Fields = nil
}
}

批量插入

批量插入的请求如下,每一个插入的物料要两行

  • 第一行固定 {“index”:{“_id”:”xxx”}} \n,_id可以为空,router会自动生成唯一id
  • 第二行里是物料每一列的值
  • 每一行用结尾需要‘\n’分开
1
2
3
4
5
6
vbnet复制代码curl -H "content-type: application/json" -XPOST -d'
{"index": {"_id": "v1"}}\n
{"field1": "value", "field2": {"feature": []}}\n
{"index": {"_id": "v2"}}\n
{"field1": "value", "field2": {"feature": []}}\n
' http://router_server/$db_name/$space_name/_bulk

router中对应的处理方法是handleBulk(),该方法通过http请求初始化一个BulkRequest,主要就是解析请求中每一个doc,把他们填入BulkRequest.docs里,BulkRequest结构如下:

1
2
3
4
ini复制代码message BulkRequest{
RequestHead head = 1;
repeated Document docs = 4;
}

填充完后,调用bulk()方法填充一个RouterRequest并发送,步骤和单条插入里的updateDoc()方法类似。

1
css复制代码reply := handler.docService.bulk(ctx, args)

与单条插入不同的是,批量插入的rpc请求中call的方法是BatchHandler,ps接到router批量插入请求,调用对应的处理方法是bulk()

查询

查询接口示例如下

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
vbnet复制代码curl -H "content-type: application/json" -XPOST -d'
{
"query": {
"sum": [{
"field": "field_name",
"feature": [0.1, 0.2, 0.3, 0.4, 0.5],
"min_score": 0.9,
"boost": 0.5
}],
"filter": [{
"range": {
"field_name": {
"gte": 160,
"lte": 180
}
}
},
{
"term": {
"field_name": ["100", "200", "300"],
"operator": "or"
}
}]
},
"retrieval_param": {
"nprobe": 20
},
"fields": ["field1", "field2"],
"is_brute_search": 0,
"online_log_level": "debug",
"quick": false,
"vector_value": false,
"client_type": "leader",
"l2_sqrt": false,
"sort": [{"field1":{"order": "asc"}}],
"size": 10
}
' http://router_server/$db_name/$space_name/_search

工作的方法与前面大同小异,无非是构造请求然后发送,这里mark一下重要的参数

  • sum:跟需要查询的特征,下面又有几个参数:
    1. field 指定创建表时特征字段的名称。类似 select col from table中的 col
    2. feature 传递特征,维数和定义表结构时维数必须相同。
    3. min_score 指定返回结果中分值必须大于等于0.9,两个向量计算结果相似度在0-1之间,min_score可以指定返回结果分值最小值,max_score可以指定最大值。如设置: “min_score”: 0.8,“max_score”: 0.95 代表过滤0.8<= 分值<= 0.95 的结果。同时另外一种分值过滤的方式是使用: “symbol”:”>=”,”value”:0.9 这种组合方式,symbol支持的值类型包含: > 、 >= 、 <、 <= 4种,value及min_score、max_score值在0到1之间。
    4. boost指定相似度的权重,比如两个向量相似度分值是0.7,boost设置成0.5之后,返回的结果中会将分值0.7乘以0.5即0.35。
  • size指定最多返回的结果数量,通过这个参数指定topN。若请求url中设置了size值http://router\_server/$db\_name/$space\_name/\_search?size=20优先使用url中指定的size值。
  • quick搜索结果默认将PQ召回向量进行计算和精排,为了加快服务端处理速度设置成true可以指定只召回,不做计算和精排。(这个不是很理解)

问题回答

  1. 创建新的doc,怎么选择对应存储的partition
  • A: router为doc生成新的唯一id,然后通过hash函数计算partition
  1. router的缓存都有什么
  • A: 缓存相关都在router.client.masterclient.cliCache下面
    • router.client.masterclient.cliCache本身继承了sync.map,存储了nodeID对应的rpcClient,避免多次创建
    • partitionCache里存放了partitionID对应的partition相关信息,包括机器节点ID(没有地址)
    • serverCache里存放了NodeID对应的机器信息,包括IP、端口等
    • 如果在缓存里没有找到,router会访问etcd获取相关数据,router.client.masterclient.store就是etcd相关
  1. 怎么根据doc的id确认对应的partition
  • A:hash函数,代码对应这句:
1
css复制代码partitionID := r.space.PartitionId(murmur3.Sum32WithSeed(cbbytes.StringToByte(doc.PKey), 0))

本文转载自: 掘金

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

1…291292293…956

开发者博客

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