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

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


  • 首页

  • 归档

  • 搜索

nginx配置详解

发表于 2021-11-21

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

nginx是一种服务端的负载均衡代理,Http代理,反向代理:作为web服务器最常用的功能之一,尤其是反向代理。Nginx在做反向代理时,提供性能稳定,并且能够提供配置灵活的转发功能。Nginx可以根据不同的正则匹配,采取不同的转发策略。本篇博主将收集到的配置选项总结出来,供小伙伴们参考。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
ini复制代码#user  nobody;   #配置用户或者组,默认为nobody nobody。
worker_processes 1; #允许生成的进程数,默认为1

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info; #制定日志路径,级别

#pid logs/nginx.pid; #指定nginx进程运行文件存放地址

events {
accept_mutex on; #设置网路连接序列化,防止惊群现象发生,默认为on
worker_connections 65535; #最大连接数,默认为512
multi_accept on; #设置一个进程是否同时接受多个网络连接,默认为off
}

http {
include mime.types; #文件扩展名与文件类型映射表
default_type application/octet-stream; #默认文件类型,默认为text/plain

#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"'; #自定义格式

#access_log logs/acc.log main; #combined为日志格式的默认值

sendfile on; #允许sendfile方式传输文件,默认为off,可以在http块,server块,location块
tcp_nopush on;
tcp_nodelay on;

#keepalive_timeout 0;
keepalive_timeout 65; #连接超时时间,默认为75s,可以在http,server,location块

upstream fzjh {
server 127.0.0.1:7878;
server 192.168.10.121:3333 backup; #热备 若无则是负载均衡
}
server {
listen 8000; #监听端口
server_name localhost; #监听地址

charset utf-8;

#access_log logs/host.access.log main; #combined为日志格式的默认值

location / { #请求的url过滤,正则匹配,~为区分大小写,~*为不区分大小写。
#root path; #根目录
#index vv.txt; #设置默认页
proxy_pass http://127.0.0.1:3000; #指向地址
# proxy_pass fzjh ; 请求转向mysvr 负载均衡
# deny 127.0.0.1; #拒绝的ip
# allow 172.18.5.54; #允许的ip
}

location /check_token { #请求的url过滤,正则匹配,~为区分大小写,~*为不区分大小写。
proxy_pass http://127.0.0.1:8088; #指向地址
# proxy_redirect on;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

location /logout { #请求的url过滤,正则匹配,~为区分大小写,~*为不区分大小写。

proxy_pass http://127.0.0.1:8088; #指向地址
# proxy_redirect on;
proxy_set_header Host $host ;
proxy_set_header X-Real-IP $remote_addr ;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ;
}

location ~ \.aaaa$ { #请求的url过滤,正则匹配,~为区分大小写,~*为不区分大小写。

rewrite ^/([\w]+)/(.*)\.air$ /$2 break ;
proxy_pass http://127.0.0.1:8088;
# proxy_redirect on;
proxy_set_header Host $host ;
proxy_set_header X-Real-IP $remote_addr ;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html { #错误页
root html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}


# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;

# location / {
# root html;
# index index.html index.htm;
# }
#}


# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;

# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;

# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;

# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;

# location / {
# root html;
# index index.html index.htm;
# }
#}

}

2.其他配置

  1. include mime.types; #文件扩展名与文件类型映射表
  2. default_type application/octet-stream; #默认文件类型,默认为text/plain
  3. #access_log off; #取消服务日志
  4. log_format myFormat ‘ remoteaddr–remote_addr–remotea​ddr–remote_user [timelocal]time_local] timel​ocal]request statusstatus statusbody_bytes_sent httprefererhttp_referer httpr​efererhttp_user_agent $http_x_forwarded_for’; #自定义格式
  5. access_log log/access.log myFormat; #combined为日志格式的默认值
  6. sendfile on; #允许sendfile方式传输文件,默认为off,可以在http块,server块,location块。
  7. sendfile_max_chunk 100k; #每个进程每次调用传输数量不能大于设定的值,默认为0,即不设上限。
  8. keepalive_timeout 65; #连接超时时间,默认为75s,可以在http,server,location块。
  9. proxy_connect_timeout 1; #nginx服务器与被代理的服务器建立连接的超时时间,默认60秒
  10. proxy_read_timeout 1; #nginx服务器想被代理服务器组发出read请求后,等待响应的超时间,默认为60秒。
  11. proxy_send_timeout 1; #nginx服务器想被代理服务器组发出write请求后,等待响应的超时间,默认为60秒。
  12. proxy_http_version 1.0 ; #Nginx服务器提供代理服务的http协议版本1.0,1.1,默认设置为1.0版本。
  13. #proxy_method get; #支持客户端的请求方法。post/get;
  14. proxy_ignore_client_abort on; #客户端断网时,nginx服务器是否终端对被代理服务器的请求。默认为off。
  15. proxy_ignore_headers “Expires” “Set-Cookie”; #Nginx服务器不处理设置的http相应投中的头域,这里空格隔开可以设置多个。
  16. proxy_intercept_errors on; #如果被代理服务器返回的状态码为400或者大于400,设置的error_page配置起作用。默认为off。
  17. proxy_headers_hash_max_size 1024; #存放http报文头的哈希表容量上限,默认为512个字符。
  18. proxy_headers_hash_bucket_size 128; #nginx服务器申请存放http报文头的哈希表容量大小。默认为64个字符。
  19. proxy_next_upstream timeout; #反向代理upstream中设置的服务器组,出现故障时,被代理服务器返回的状态值。error|timeout|invalid_header|http_500|http_502|http_503|http_504|http_404|off
  20. #proxy_ssl_session_reuse on; 默认为on,如果我们在错误日志中发现“SSL3_GET_FINSHED:digest check failed”的情况时,可以将该指令设置为off。

本文转载自: 掘金

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

都什么年代了你还在用 Date

发表于 2021-11-21

前言

上篇文章搞清楚了时区,这篇文章就主要来谈一谈 Java 中处理日期时间用什么 API 比较好。我本来不准备写这篇文章的,因为我觉得 Java17 都特么出来了,大家对 Java8 提供的时间日期 API 都很熟悉了。但是经过我调研,很多中小公司还在用老版本的 Date 来处理时间日期,视 Java8 提供的时间日期 API 于无物,所以还是想来推荐一下新一代的时间日期 API,希望对大家有帮助。

传统的 Date

老版本的 Date 相信大家都很熟悉了,这里就简单介绍几个点

可观不可触的时区

对于老版本的 Date、SimpleDateFormat 相信大家都很熟悉,值得注意的是,你是无法直接设置 Date 的时区信息的,但与之矛盾的是我们在代码中从数据库读取一个带时区的时间,例如:2021-11-01 13:50:47.138494+00 ,它却能够自动解析成当前服务器所在时区的时间封装在 Date 对象中。其实这是它的一个成员变量 private transient BaseCalendar.Date cdate 去做的事情,由于 Unix 时间戳是和时区无关的,所以在从数据库读取时它会将数据库带有时区时间转换为 Unix 时间戳,然后在用这个时间戳转换为当前服务器所在时区的时间,并且携带着时区信息。

其实我们可以看一下 Date 构造方法源码,他就是获取的当前 Unix 时间戳。

1
2
3
csharp复制代码public Date() {
this(System.currentTimeMillis());
}

复杂的时区转换计算

如果我们需要显示一个 Date 对象在不同时区的时间,那么我们需要通过 SimpleDateFormat 来实现

1
2
3
4
5
6
7
csharp复制代码    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = new Date();//默认北京时区的时间
System.out.println("北京时区:" + sdf.getTimeZone());
System.out.println("北京时区时间:" + sdf.format(date));
sdf.setTimeZone(TimeZone.getTimeZone(ZoneId.of("Asia/Jakarta")));
System.out.println("雅加达时区:" + sdf.getTimeZone());
System.out.println("雅加达时区时间:" + sdf.format(date));

如果你想得到的不是字符串而是转换为另一个时区的 Date 对象,那么你还需要再从字符串反转到 Date ……这无疑是非常麻烦的

复杂时间间隔计算

要说 Date 最难受的地方之一就是两个日期的时间差计算,之前 JDK 并没有提供直接的 API 来计算两个 Date 的间隔。通常我们是把 Date 转成时间戳之后进行操作

1
2
3
4
5
6
7
8
ini复制代码    Date date1 = new Date();
TimeUnit.SECONDS.sleep(10);
Date date2 = new Date();
long time1 = date1.getTime();
long time2 = date2.getTime();
System.out.println("间隔秒数:" + (time2 - time1) / 1000);
System.out.println("间隔小时数:" + (time2 - time1) / (1000 * 3600));
//...

是不是很麻烦?如果你并不觉得麻烦,那么你只是没有见过更好的方式。我们来看看新版 API 怎么来做(后面会详细介绍)

1
2
3
4
ini复制代码    Duration duration = Duration.between(time2, time1);
long days = duration.toDays();//获取天数间隔
long hours = duration.toHours();//获取小时间隔
//...

这样是不是很简单!

数据库到底要不要存储时区信息

在谈新版的时间 API 之前,我们先要搞清楚一个问题,那就是你真的有必要把时区信息存储到数据库吗?其实我觉得对于绝大多数的公司应该都是不需要的,下我以我们公司印度尼西亚业务为例来分析这个问题。

存储时区

我们目前是先在代码中获取当前服务器(我们服务器在亚马逊 UTC-3 )所在时区的 Date 对象,在存入数据库时,在数据库层面将其转换为 UTC+8 时区的时间存储到数据库中,例如 2021-10-24 15:47:47.138494-03 存到数据库中是 2021-10-25 02:47:47.138494+08,然后在前端页面可以直接调用 API 根据带时区的时间计算出前端设备当地时区的时间。

1
arduino复制代码    xxx//假如设备在印度尼西亚,那么前端 API 获得的时间就是 2021-10-25 01:47:47.138494

不过不是很明白,我们既然存时区,那么应该也要存印度尼西亚的时区啊……毕竟我们的业务在印尼,也不在北京……

不存储时区

正常来说服务器所在地一般是唯一的,即使有 100 个服务实例来做负载均衡,总不可能出现一半服务器在美国,一半在中国吧?那么代码中的时间日期操作都是默认使用服务器所在地时区(UTC-3),既然如此那么我们数据库就可以不存储时区信息,只存储一个时间描述例如 2021-10-24 13:50:47.138494,它本身没有时区,但是我们都知道它的默认时区就是 UTC-3 。

这样在前端代码中只需要调用 API 的时候带上带上该时间的所属时区 UTC-3 即可算出前端设备当地时区的时间。

1
arduino复制代码xxx//假如设备在印度尼西亚,那么前端 API 获得的时间就是 2021-10-25 01:47:47.138494

而且这个不带时区的时间不会受数据库时区的影响,无论你把数据库设置成哪个时区,它都不会变化。这种方式就是整个业务里我们把所有时区都干掉,只留一个服务器时区,当有任何涉及时区的业务功能时,只需要把源时间换算成服务器所在时区的时间即可。

总结

不存储时区还有一种情况就是有些程序员喜欢存时间戳,不得不吐槽一下我觉得这是最 low 的方式之一(也许你能说出它仅有的个别优点,但我不接受反驳)。虽然时间戳是和时区无关的,但是它的可读性真的太差了……而且代码中也不好进行操作

其实对比一下就能发现数据库存不存储时区信息,对于前后端的操作区别不大的。而且我觉得绝大多数业务场景,不存储时区相对更简单!

Java8 的时间日期 API

LocalDate、LocalTime、LocalDateTime

看源码是一个好习惯,看源码注释更是一个好习惯。这三个类的注释说的很清楚,首先这些类是线程安全的,对于它的任何操作都会产生一个新的实例,这和 String 类是一样的。其次它不存储或表示时区。 相反,它是对日期时间的描述。

值得注意的是,它不存储时区,但不代表它没有时区,细品这句话!通过一张图来理解三者的关系

下面以 LocalDateTime 为例简单介绍下用法

1
2
3
4
5
6
7
8
9
scss复制代码LocalDateTime time1 = LocalDateTime.now();
LocalDateTime time2 = LocalDateTime.now();

time1.isAfter(time2); time1.isBefore(time2);//比较时间
time1.plusDays(1L); time1.minusHours(1L);//加减时间日期
LocalDateTime.parse("2021-11-19T15:16:17");//解析时间
LocalDateTime.of(2019, 11, 30, 15, 16, 17);//指定日期时间
LocalDateTime.now(ZoneId.of("Asia/Jakarta"));//其他时区相对此服务器时区的时间
//...

如果你可以不在数据库中存储时区信息的话,那么请使用这个类。如果你一定要存储,那么也请使用下面的 OffsetDateTime 而不要使用 Date 。

OffsetDateTime

带有时区偏移量的日期时间类,相当于 OffsetDateTime = LocalDateTime + ZoneOffset

大多数情况下我们使用带有偏移量的日期时间已经能够满足需求。

ZonedDateTime

真正带有完整时区信息的日期时间类

Duration、Period

这两个类是代表一段时间或者说是两个时间的间隔,以 Duration 为例,试想在 Java8 之前你有一个业务要表示一个令牌的有效期为 7 天,那么通常的做法可能是存储令牌的创建时间,然后在代码中用系统当前时间减去令牌创建时间和 7 天做比较,例如

1
2
scss复制代码long duration = System.currentTimeMillis() - token.getCreateTime().getTime();
if (duration < 7 * 24 * 60 * 60 * 1000) { //令牌合法}

但是在使用 Duration 之后,

1
2
ini复制代码Duration duration = Duration.between(token.getCreateTime(), LocalDateTime.now());
boolean negative = duration.minus(effective).isNegative();//是否过期

其次 Duration 在 SpringBoot 项目中,配置也很方便

1
2
yaml复制代码token:
effective-time: 7d # d:天 , h:小时 , m:分钟 , s:秒

在实体类中,可以使用 @ConfigurationProperties 或者 @Value 将它直接映射成 Duration 对象,当然这依赖于 SpringBoot 中提供的丰富的类型转换器。下一篇文章会介绍

TemporalAdjuster 和 TemporalAdjusters

第一眼看到这两个类是不是想起了熟悉的 Collection 和 Collections ,与之类似这两个类是时间矫正器接口和时间矫正器的工具类。新版日期时间类几乎都实现了 TemporalAdjuster ,以便于针对所有日期时间都可以对其进行计算得到另外一个时间,例如

1
2
3
4
5
scss复制代码LocalDateTime.now().with(TemporalAdjusters.firstDayOfMonth());//当月第一天
LocalDateTime.now().with(TemporalAdjusters.firstDayOfNextMonth());//下个月第一天
LocalDateTime.now().with(TemporalAdjusters.dayOfWeekInMonth(2,DayOfWeek.MONDAY));//第N个星期几
LocalDateTime.now().with(TemporalAdjusters.next(DayOfWeek.MONDAY));//下个星期几
//...

有这么丰富的 API ,你还需要写一堆日期时间的工具类吗?

Date 和 LocalDateTime 互转

不可否认存在一种现象就是你的项目一直用的都是 Date,而 leader 又不愿意花费时间精力去升级,或者老的业务限制的情况,那么某些场景下你可以使用 Java8 提供的 Instant 将两者互转来简化一些业务代码操作

1
2
3
less复制代码LocalDateTime.ofInstant(new Date().toInstant(), ZoneId.systemDefault());//Date 转 LocalDateTime

Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant());// LocalDateTime 转 Date

总结

Java8 的时间日期工具还有很多用法,这里就不一一介绍了,总之 Date 有的功能 LocalDateTime 都有,Date 没有的功能,LocalDateTime 还有很多。使用新版的日期时间,几乎是不存在原来的 DateUtil 的。所以还需要我告诉你选谁吗~~

结语

人们总是对于自己熟悉的东西持有倾向,对于不熟悉的新事物往往会抵触,曾经我也不止一次的抵触我亲爱的架构师让我们更换新的技术组件,但后来我都爱上了这些新的技术。

抵触新技术不是一个优秀程序员该有,这会阻碍你的成长。新技术的出现往往是弥补老技术的缺陷,没有哪个组织会花费人力物力出一个废物组件……所以每当有新技术组件出现时,请尝试它,也许会有意想不到的收获!

如果这篇文章对你有帮助,记得点赞加关注,你的支持就是我继续创作的动力!

本文转载自: 掘金

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

重写finalize方法的锅:一次full gc耗时且频繁的

发表于 2021-11-21

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

背景

事情最初是博主这边用jersey提供的客户端API封装了一个rest客户端集成到业务系统A中,结果某次系统A在线上运行时崩了,分析线程栈的dump文件时,发现是因为大量线程阻塞拖跨了应用。

线程阻塞的原因是因为jersey的方法内部存在同步操作,遇到流量陡增并且机器资源也比较紧张时对CPU调度产生了影响,导致线程阻塞耗时久,请求处理慢。恶性循环下,导致线程爆了。

于是对封装的rest客户端进行了减少同步操作的优化。

优化之后的rest客户端,自然是要做一些压测来看下性能,然后,问题来了。

新的问题

首先,对未优化的老版本做一些压测作为基准数据,老版本的tps虽然不高,但是持续压测一段时间后,内存、CPU、GC情况看起来还是比较稳定。

优化后的新版本,在相同并发情况下,在压测的最初时间段,可以很明显的看到吞吐量有一个质的提升。在持续压测一段时间后,出现了应用进程hung死的现象。此时CPU占用基本是100%的状态。

即使停止压测,这种现象还是存在,通过jvisualvm的界面查看内存情况,内存居高不下,像是内存溢出了。

停止压测,查看GC信息,但是依然频繁full gc,且full gc耗时很久。在压测过程中dump线程栈信息,优化前的同步操作阻塞线程的情况很少,几乎忽略不计。

定位

p.s. 堆内存配置:最大堆内存3G,初始内存3G,老年代与新生代比例设置的是2:1,用的Parallel垃圾收集器

我重新配置JVM参数,打印GC日志,重新压测,然后实时查看GC日志,发现最初young gc虽然频繁,但是full gc还是隔一段时间才会触发,不过full gc还是比较耗时,可能几秒。但是看起来至少程序还可以正常运行。压测一段时间后,full gc开始频繁且耗时,最长的一次可能达到20多秒,要知道这个最大堆内存只有3g。

即使一直在full gc,但是老年代内存,依然无法回收掉,如下:

本来猜测,可能是每次请求临时大对象过多,所以老版本的时候还可以正常的gc掉,新版本由于tps高于老版本,说明,相同时间段内产生的临时大对象更多导致,但是这里显示老年代迟迟回收不掉,所以应该不是临时变量导致的。

然后使用jmc查看热点类的时候,发现HashMap占比过多,达到20%,其实这里判断错了,所以后续定位不到原因。

这个时候,虽然没有办法,但是分析堆dump是最好的选择。但是进程一直在full gc,所以使用jvisualvm来dump堆信息失败,使用命令也没有响应,然后使用-F强制执行,经过几个小时后,终于dump下来一个大小几个G的堆dump文件。

接下来使用mat查看dominator_tree的时候,发现java.lang.ref.Finalizer对象及引用对象的内存占比达到了98%。查看发现了问题原来出在Jersey的ClientRuntime类,如下:

可以看到Finalizer类引用了很Finalizer实例,每个实例引用ClientRuntime实例。这时候,猜测应该是ClientRuntime重载finalize方法的原因,查看源码,果然是:

它重载这个方法进行垃圾回收时的资源释放。

这里简单说明下Finalizer与重载finalize方法关系。如果某个类实现了finalize方法,在构造实例后,JVM会创建一个Finalizer实例引用它,看上面那个mat分析的截图,可以知道Finalizer这个类系统类是可以做为GC根的,所以最终引用的实现了finalize方法的类对象是可达的,所以GC的时候不会被回收掉,多次后就放入了老年代,并且实现了finalize方法会被放入一个引用队列中。

这个引用队列会由一个Finalizer线程不停的弹出一个对象并调用finalize方法,之后取消Finalizer对象的引用关系,下次GC才会回收掉。

所以上面这里说明了它能被GC掉的原因,之前认为这个Finalizer线程优先级特别低,执行的机会也要低于工作线程,这样导致了这类对象被GC的更慢,其实这是因为这个框架的实现有同步操作的原因,我看了JDK的源码,Finalizer线程优先级的值为8(JDK7中),当然了实际优先级是和操作系统有关。

然后,我查看dump的线程信息:

发现,竟然还存在同步操作,根据这个ID查下,发现对象初始化调用的时候,也必须持有这个锁,这个锁是全局唯一的,所有初始化操作包括执行finalize方法都必须持有这把锁(200个工程线程加上1条Finalizer线程此时有100多个线程阻塞在这个锁上)!!!这便是为什么 GC特别慢的原因了。

这下就全部清楚了。

原因

  1. 未优化版本为什么没有问题:

因为未优化版本使用相同的并发数,每个请求同步操作过多,整体吞吐量上不来,虽然每个请求都要创建一定数量 的ClientRuntime对象,但是tps在这摆这,某个时间段也只创建这么多个对象,虽然GC比较慢,但是创建与回收勉强能达到一个平衡,所以没有问题。

  1. 优化后的版本为什么出现问题:

优化后的版本,减少每个请求的同步操作次数,请求处理速度更快,整体tps,上来了,能处理的请求更多了,虽然每个请求创建 的ClientRuntim对象还是这么多,但是某个时间段创建的总数更多了。

同时 ,每条线程阻塞的机会变少,又导致Finalizer这个守护线程执行的机会更少,引用队列的要回收的对象更多,但是处理速度更慢。恶性循环,所以导致了后来频繁full gc,且full gc非常耗时。

  1. 为什么停止请求后还一直在full gc,内存下不来:

因为这些要回收的对象都是在引用队列中,Finalizer线程一直执行的才能给垃圾回收器机会回收这些已经处理过的对象,但是这个时full gc频繁且耗时的原因,导致Finalizer线程偶尔才有机会处理几个对象,让垃圾回收器回收下,所以需要很长时间,一点一点的最终把这些对象回收完。

本文转载自: 掘金

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

微服务架构中,二次浅封装实践 一、背景简介 二、框架浅封装

发表于 2021-11-21

一、背景简介

分布式系统中存在很多拆分的服务,在不断迭代升级的过程中,会出现如下常见的棘手情况:

某个技术组件版本升级,依赖包升级导致部分语法或者API过期,或者组件修复紧急的漏洞,从而会导致分布式系统下各个服务被动的升级迭代,很容易引发意外的问题;不同的服务中对组件的依赖和版本各不相同,从而导致不兼容问题的出现,很难对版本做统一的管理和维护,一旦出现问题很容易手忙脚乱,引发蝴蝶效应;

所以在复杂的系统中,对于依赖的框架和组件进行统一管理和二次浅封装,可以较大程度降低上述问题的处理成本与风险,同时可以更好的管理和控制技术栈。

二、框架浅封装

1、浅封装作用

为什么浅封装,核心目的在于统一管理和协调组件的依赖与升级,并对常用方法做一层包装,实际上很多组件使用到的功能点并不多,只是在业务中的使用点很多,这样给组件本身的迭代升级带来了一定的难度:

例如某个组件常用的API中存在巨大风险漏洞,或者替换掉过期的用法,需要对整个系统中涉及的地方做升级,这种操作的成本是非常高的;

如果是对这种常用的组件方法进行二次包装,作为处理业务的工具方法,那么解决上面的问题就相对轻松许多,只要对封装的工具方法升级,服务的依赖升级即可,降低时间成本和风险。

通过浅封装的手段,可以实现两个方面的解耦:

业务与技术

技术栈中常用的方法进行二次浅封装,这样可以较大程度的降低业务与技术的耦合,如此可以独立的升级技术栈,扩展功能而不影响业务服务的迭代。

框架与组件

不同的框架与组件都需要一定程度的自定义配置,同时分模块管理,在不同的服务中引入特定的依赖,也可以在基础包中做统一依赖,以此实现技术栈的快速组合搭配。

这里说的浅封装,是指包装常规常用的语法,组件本身就是技术层面的深度封装,所以也不可能完全隔开技术栈原生用法。

2、统一版本控制

例如微服务架构下,不同的研发组负责不同的业务模块,然而受到开发人员的经验和能力影响,很容易出现不同的服务组件选型不一致,或者相同的组件依赖版本不同,这样很难对系统架构做标准的统一管理。

对于二次封装的方式,可以严格的控制技术栈的迭代扩展,以及版本冲突的问题,通过对二次封装层的统一升级,可以快速实现业务服务的升级,解决不同服务的依赖差异问题。

三、实践案例

1、案例简介

Java分布式系统中,微服务基础组件(Nacos、Feign、Gateway、Seata)等,系统中间件(Quartz、Redis、Kafka、ElasticSearch,Logstash)等,对常用功能、配置、API等,进行二次浅封装并统一集成管理,以满足日常开发中基础环境搭建与临时工具的快速实现。

  • butte-flyer 组件封装的应用案例;
  • butte-frame 常用技术组件二次封装;

2、分层架构

整体划分五层:网关层、应用层、业务层、中间件层、基础层,组合成一套分布式系统。

服务总览

服务名 分层 端口 缓存库 数据库 描述
flyer-gateway 网关层 8010 db1 nacos 路由控制
flyer-facade 应用层 8082 db2 facade 门面服务
flyer-admin 应用层 8083 db3 admin 后端管理
flyer-account 业务层 8084 db4 account 账户管理
flyer-quartz 业务层 8085 db5 quartz 定时任务
kafka 中间件 9092 — —— 消息队列
elasticsearch 中间件 9200 — —— 搜索引擎
redis 中间件 6379 — —— 缓存中心
logstash 中间件 5044 — es6.8.6 日志采集
nacos 基础层 8848 — nacos 注册配置
seata 基础层 8091 — seata 分布事务
mysql 基础层 3306 — —— 数据存储

3、目录结构

在butte-frame中对各个技术栈进行二次封装管理,在butte-flyer中进行依赖引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
but复制代码butte-frame
├── frame-base 基础代码块
├── frame-jdbc 数据库组件
├── frame-core 服务基础依赖
├── frame-gateway 路由网关
├── frame-nacos 注册与配置中心
├── frame-seata 分布式事务
├── frame-feign 服务间调用
├── frame-security 安全管理
├── frame-search 搜索引擎
├── frame-redis 缓存管理
├── frame-kafka 消息中间件
├── frame-quartz 定时任务
├── frame-swagger 接口文档
└── frame-sleuth 链路日志

butte-flyer
├── flyer-gateway 网关服务:路由控制
├── flyer-facade 门面服务:功能协作接口
├── flyer-account 账户服务:用户账户
├── flyer-quartz 任务服务:定时任务
└── flyer-admin 管理服务:后端管理

4、技术栈组件

系统常用的技术栈:基础框架、微服务组件、缓存、安全管理、数据库、定时任务、工具依赖等。

名称 版本 说明
spring-cloud 2.2.5.RELEASE 微服务框架基础
spring-boot 2.2.5.RELEASE 服务基础依赖
gateway 2.2.5.RELEASE 路由网关
nacos 2.2.5.RELEASE 注册中心与配置管理
seata 2.2.5.RELEASE 分布式事务管理
feign 2.2.5.RELEASE 微服务间请求调用
security 2.2.5.RELEASE 安全管理
sleuth 2.2.5.RELEASE 请求轨迹链路
security-jwt 1.0.10.RELEASE JWT加密组件
hikari 3.4.2 数据库连接池,默认
mybatis-plus 3.4.2 ORM持久层框架
kafka 2.0.1 MQ消息队列
elasticsearch 6.8.6 搜索引擎
logstash 5.2 日志采集
redis 2.2.5.RELEASE 缓存管理与加锁控制
quartz 2.3.2 定时任务管理
swagger 2.6.1 接口文档
apache-common 2.7.0 基础依赖包
hutool 5.3.1 基础工具包

四、微服务组件

1、Nacos

Nacos在整个组件体系中,提供两个核心能力,注册发现:适配微服务注册与发现标准,快速实现动态服务注册发现、元数据管理等,提供微服务组件中最基础的能力;配置中心:统一管理各个服务配置,集中在Nacos中存储管理,隔离多环境的不同配置,并且可以规避线上配置放开的风险;

连接管理

1
2
3
4
5
6
7
8
9
10
11
12
yml复制代码spring:
cloud:
nacos:
# 配置读取
config:
prefix: application
server-addr: 127.0.0.1:8848
file-extension: yml
refresh-enabled: true
# 注册中心
discovery:
server-addr: 127.0.0.1:8848

配置管理

  • bootstrap.yml :服务中文件,连接和读取Nacos中配置信息;
  • application.yml :公共基础配置,这里配置mybatis组件;
  • application-dev.yml :中间件连接配置,用作环境标识隔离;
  • application-def.yml :各个服务的自定义配置,参数加载;

2、Gateway

Gateway网关核心能力,提供统一的API路由管理,作为微服务架构体系下请求唯一入口,还可以在网关层处理所有的非业务功能,例如:安全控制,流量监控限流,等等。

路由控制:各个服务的发现和路由;

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

@Resource
private RouteService routeService ;

/**
* 加载全部路由
* @since 2021-11-14 18:08
*/
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return Flux.fromIterable(routeService.getRouteDefinitions());
}

/**
* 添加路由
* @since 2021-11-14 18:08
*/
@Override
public Mono<Void> save(Mono<RouteDefinition> routeMono) {
return routeMono.flatMap(routeDefinition -> {
routeService.saveRouter(routeDefinition);
return Mono.empty();
});
}
}

全局过滤:作为网关的基础能力;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Component
public class GatewayFilter implements GlobalFilter {

private static final Logger logger = LoggerFactory.getLogger(GatewayFilter.class);

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String uri = request.getURI().getPath() ;
String host = String.valueOf(request.getHeaders().getHost()) ;
logger.info("request host : {} , uri : {}",host,uri);
return chain.filter(exchange);
}
}

3、Feign

Feign组件是声明式的WebService客户端,使微服务之间的调用变得更简单,Feign通过注解手段,将请求进行模板化和接口化管理,可以更加标准的管理各个服务间的通信交互。

响应解码:定义Feign接口响应时解码逻辑,校验和控制统一的接口风格;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class FeignDecode extends ResponseEntityDecoder {

public FeignDecode(Decoder decoder) {
super(decoder);
}

@Override
public Object decode(Response response, Type type) {
if (!type.getTypeName().startsWith(Rep.class.getName())) {
throw new RuntimeException("响应格式异常");
}
try {
return super.decode(response, type);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
}

4、Seata

Seata组件是开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务,实现AT、TCC、SAGA、XA事务模式,支持一站式的分布式解决方案。

事务配置:基于nacos管理Seata组件的参数定义;

服务注册:在需要管理分布式事务的服务中连接和使用Seata服务;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yml复制代码seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: butte-seata-group
config:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.config.server-addr}
group: DEFAULT_GROUP
registry:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.config.server-addr}
application: seata-server
group: DEFAULT_GROUP

五、中间件集成

1、Kafka

Kafka是由Apache开源,具有分布式、分区的、多副本的、多订阅者,基于Zookeeper协调的分布式消息处理平台,由Scala和Java语言编写。还常用于搜集用户在应用服务中产生的日志数据。

消息发送:封装消息发送的基础能力;

1
2
3
4
5
6
7
8
9
10
java复制代码@Component
public class KafkaSendOperate {

@Resource
private KafkaTemplate<String, String> kafkaTemplate ;

public void send (SendMsgVO entry) {
kafkaTemplate.send(entry.getTopic(),entry.getKey(),entry.getMsgBody()) ;
}
}

消息消费:消费监听时有两种策略;

  • 消息生产方自己消费,通过Feign接口去执行具体消费服务的逻辑,这样有利于流程跟踪排查;
  • 消息消费方直接监听,减少消息处理的流程节点,当然也可以打造统一的MQ总线服务(文尾);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class KafkaListen {
private static final Logger logger = LoggerFactory.getLogger(KafkaListen.class);
/**
* Kafka消息监听
* @since 2021-11-06 16:47
*/
@KafkaListener(topics = KafkaTopic.USER_TOPIC)
public void listenUser (ConsumerRecord<?,String> record, Acknowledgment acknowledgment) {
try {
String key = String.valueOf(record.key());
String body = record.value();
switch (key){ }
} catch (Exception e){
e.printStackTrace();
} finally {
acknowledgment.acknowledge();
}
}
}

2、Redis

Redis是一款开源组件,基于内存的高性能的key-value数据结构存储系统,它可以用作数据库、缓存和消息中间件,支持多种类型的数据结构,如字符串、集合等。在实际应用中,通常用来做变动频率低的热点数据缓存和加锁机制。

KV数据缓存:作为Redis最常用的功能,即缓存一个指定有效期的键和值,在使用时直接获取;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Component
public class RedisKvOperate {

@Resource
private StringRedisTemplate stringRedisTemplate ;

/**
* 创建缓存,必须带缓存时长
* @param key 缓存Key
* @param value 缓存Value
* @param expire 单位秒
* @return boolean
* @since 2021-08-07 21:12
*/
public boolean set (String key, String value, long expire) {
try {
stringRedisTemplate.opsForValue().set(key,value,expire, TimeUnit.SECONDS);
} catch (Exception e){
e.printStackTrace();
return Boolean.FALSE ;
}
return Boolean.TRUE ;
}
}

Lock加锁机制:基于spring-integration-redis中RedisLockRegistry,实现分布式锁;

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

@Resource
protected RedisLockRegistry redisLockRegistry;

/**
* 尝试一次加锁,采用默认时间
* @param lockKey 加锁Key
* @return java.lang.Boolean
* @since 2021-09-12 13:14
*/
@SneakyThrows
public <T> Boolean tryLock(T lockKey) {
return redisLockRegistry.obtain(lockKey).tryLock(time, TimeUnit.MILLISECONDS);
}

/**
* 释放锁
* @param lockKey 解锁Key
* @since 2021-09-12 13:32
*/
public <T> void unlock(T lockKey) {
redisLockRegistry.obtain(lockKey).unlock();
}

}

3、ElasticSearch

ElasticSearch是一个基于Lucene的搜索服务器,它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口,Elasticsearch是用Java开发的,是当前流行的企业级搜索引擎。

索引管理:索引的创建和删除,结构添加和查询;

基于ElasticsearchRestTemplate的模板方法操作;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@Component
public class TemplateOperate {

@Resource
private ElasticsearchRestTemplate template ;

/**
* 创建索引和结构
* @param clazz 基于注解类实体
* @return java.lang.Boolean
* @since 2021-08-15 19:25
*/
public <T> Boolean createPut (Class<T> clazz){
boolean createIf = template.createIndex(clazz) ;
if (createIf){
return template.putMapping(clazz) ;
}
return Boolean.FALSE ;
}
}

基于RestHighLevelClient原生API操作;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@Component
public class IndexOperate {

@Resource
private RestHighLevelClient client ;

/**
* 判断索引是否存在
* @return boolean
* @since 2021-08-07 18:57
*/
public boolean exists (IndexVO entry) {
GetIndexRequest getReq = new GetIndexRequest (entry.getIndexName()) ;
try {
return client.indices().exists(getReq, entry.getOptions());
} catch (Exception e) {
e.printStackTrace();
}
return Boolean.FALSE ;
}
}

数据管理:数据新增、主键查询、修改、批量操作,业务性质的搜索封装复杂度很高;

数据的增删改方法;

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

@Resource
private RestHighLevelClient client ;

/**
* 批量更新数据
* @param entry 对象主体
* @since 2021-08-07 18:16
*/
public void bulkUpdate (DataVO entry){
if (CollUtil.isEmpty(entry.getDataList())){
return ;
}
// 请求条件
BulkRequest bulkUpdate = new BulkRequest(entry.getIndexName(),entry.getType()) ;
bulkUpdate.setRefreshPolicy(entry.getRefresh()) ;
entry.getDataList().forEach(dataMap -> {
UpdateRequest updateReq = new UpdateRequest() ;
updateReq.id(String.valueOf(dataMap.get("id"))) ;
updateReq.doc(dataMap) ;
bulkUpdate.add(updateReq) ;
});
try {
// 执行请求
client.bulk(bulkUpdate, entry.getOptions());
} catch (IOException e) {
e.printStackTrace();
}
}
}

索引主键查询,分组查询方法;

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

@Resource
private RestHighLevelClient client ;

/**
* 指定字段分组查询
* @since 2021-10-07 19:00
*/
public Map<String,Object> groupByField (QueryVO entry){
Map<String,Object> groupMap = new HashMap<>() ;
// 分组API
String groupName = entry.getGroupField()+"_group" ;
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.size(0) ;
TermsAggregationBuilder termAgg = AggregationBuilders.terms(groupName)
.field(entry.getGroupField()) ;
sourceBuilder.aggregation(termAgg);
// 查询API
SearchRequest searchRequest = new SearchRequest(entry.getIndexName());
searchRequest.source(sourceBuilder) ;
try {
// 执行API
SearchResponse response = client.search(searchRequest, entry.getOptions());
// 响应结果
Terms groupTerm = response.getAggregations().get(groupName) ;
if (CollUtil.isNotEmpty(groupTerm.getBuckets())){
for (Terms.Bucket bucket:groupTerm.getBuckets()){
groupMap.put(bucket.getKeyAsString(),bucket.getDocCount()) ;
}
}
} catch (IOException e) {
e.printStackTrace();
}
return groupMap ;
}
}

4、Logstash

Logstash是一款开源的数据采集组件,具有实时管道功能。Logstash能够动态的从多个来源采集数据,进行标准化转换数据,并将数据传输到所选择的存储容器。

  • Sleuth:管理服务链路,提供核心TraceId和SpanId生成;
  • ElasticSearch:基于ES引擎做日志聚合存储和查询;
  • Logstash:提供日志采集服务,和数据发送ES的能力;

logback.xml:服务连接Logstash地址,并加载核心配置;

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml" />

<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="butte_app" />
<springProperty scope="context" name="DES_URI" source="logstash.destination.uri" />
<springProperty scope="context" name="DES_PORT" source="logstash.destination.port" />

<!-- 输出到LogStash配置,需要启动LogStash服务 -->
<appender name="LogStash"
class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${DES_URI:- }:${DES_PORT:- }</destination>
<encoder
class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>UTC</timeZone>
</timestamp>
<pattern>
<pattern>
{
"severity": "%level",
"service": "${APP_NAME:-}",
"trace": "%X{X-B3-TraceId:-}",
"span": "%X{X-B3-SpanId:-}",
"exportable": "%X{X-Span-Export:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger{40}",
"rest": "%message"
}
</pattern>
</pattern>
</providers>
</encoder>
</appender>
</configuration>

5、Quartz

Quartz是一个完全由java编写的开源作业调度框架,用来执行各个服务中的定时调度任务,在微服务体系架构下,通常开发一个独立的Quartz服务,通过Feign接口去触发各个服务的任务执行。

配置参数:定时任务基础信息,数据库表,线程池;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yml复制代码spring:
quartz:
job-store-type: jdbc
properties:
org:
quartz:
scheduler:
instanceName: ButteScheduler
instanceId: AUTO
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: qrtz_
isClustered: true
clusterCheckinInterval: 15000
useProperties: false
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadPriority: 5
threadCount: 10
threadsInheritContextClassLoaderOfInitializingThread: true

6、Swagger

Swagger是常用的接口文档管理组件,通过对API接口和对象的简单注释,快速生成接口描述信息,并且提供可视化界面可以快速对接口发送请求和调试,该组件在前后端联调中,极大的提高效率。

配置基本的包扫描能力即可;

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Configuration
public class SwaggerConfig {

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.butte"))
.paths(PathSelectors.any())
.build();
}
}

访问:服务:端口/swagger-ui.html即可打开接口文档;

六、数据库配置

1、MySQL

微服务架构下,不同的服务对应不同的MySQL库,基于业务模块做库的划分是当前常用的方式,可以对各自业务下的服务做迭代升级,同时可以避免单点故障导致雪崩效应。

2、HikariCP

HikariCP作为SpringBoot2版本推荐和默认采用的数据库连接池,具有速度极快、轻量简单的特点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yml复制代码spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/${data.name.mysql}?${spring.datasource.db-param}
username: root
password: 123456
db-param: useUnicode=true&characterEncoding=UTF8&zeroDateTimeBehavior=convertToNull&useSSL=false
hikari:
minimumIdle: 5
maximumPoolSize: 10
idleTimeout: 300000
maxLifetime: 500000
connectionTimeout: 30000

连接池的配置根据业务的并发需求量,做适当的调优即可。

3、Mybatis

Mybatis持久层的框架组件,支持定制化SQL、存储过程以及高级映射,MyBatis-Plus是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,可以简化开发、提高效率。

1
2
3
4
yml复制代码mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

七、源代码地址

1
2
3
4
5
ruby复制代码应用仓库:
https://gitee.com/cicadasmile/butte-flyer-parent

组件封装:
https://gitee.com/cicadasmile/butte-frame-parent

本文转载自: 掘金

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

码农吸猫必备,【几行代码】就能采集万张猫咪图

发表于 2021-11-21

一起用代码吸猫!本文正在参与【喵星人征文活动】。

作为一个 Python 爬虫爱好者,当看到涉及猫咪活动的时候,首先想到的就是采集猫咪图,那我们就实现一款猫咪图采集器吧。

目标站点说明

本次要采集的站点为:《素材公社》,该网站提供了丰富的图片资源,这些内容都可以分类采集,本文仅采集与 “猫咪” 相关的素材。

image.png

本案例用到的技术模块

  • requests:负责请求发送;
  • lxml:负责数据提取。

目标站点分析

列表页分页规则如下所示:

1
2
3
txt复制代码https://www.tooopen.com/img/89_869_1_1.aspx
https://www.tooopen.com/img/89_869_1_2.aspx
https://www.tooopen.com/img/89_869_1_{页码}.aspx

通过开发者工具查阅页面元素时,发现页面有 JS 动态加载而成,在 DOM 结构中存在如下代码(下图红框区域),这部分数据为 JSON 格式,在后续的编码过程中,可以直接进行序列化操作。

image.png

获取猫咪大图

由于列表页展示的是猫咪缩略图,所以需要进入详情页提取分辨率更高的图片,这里采用两步编码,第一步提取详情页地址,第二步从详情页提取大图地址。

拿 详情页 举例,该部分标签中存 id=Detail-MaterialID,既然标签存在 ID 值,那后续的提取就变的简单了许多。

image.png

编码时间

首先封装一个通用的请求函数,原因是存在 3 次请求的发送:

  1. 请求列表页,获取详情页地址;
  2. 请求详情页,获取猫咪大图地址;
  3. 请求猫咪大图,获取图片数据(不过最后一步,没有写到该函数中进行判断)
1
2
3
4
5
6
7
8
9
10
python复制代码# 获取网页响应内容
def common_requests(url):
try:
res = requests.get(url=url, headers=headers, timeout=3)
html_str = res.text
# format_html(html_str)
return html_str
except Exception as e:
print(e)
return None

公用请求函数编写完毕之后,就可以编写核心逻辑部分,这部分代码由 2 部分构成:

  1. 获取网页响应源码;
  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
python复制代码def get_html(url):
# 列表页响应的源码
ret_html = common_requests(url)
if ret_html is not None:
format_html(html=ret_html)


def format_html(html):
element = etree.HTML(html)
# 得到详情页地址
img_name = element.xpath('//a[@class="pic"]/@title')
detail_links = element.xpath('//a[@class="pic"]/@href')
name_links = list(zip(img_name, detail_links))

data_more_str = element.xpath('//div[@id="data-more"]/text()')[0]
# 获取更多图片
data_more = json.loads(data_more_str)

if len(data_more) > 0:
name_links.extend([(_['title'], _["url"]) for _ in data_more])

if len(name_links) > 0:
for name, link in name_links:

# 详情页响应源码
ret_detail_html = common_requests(link)
if ret_detail_html is not None:
get_big_img(name, ret_detail_html)

else:
pass

上述代码需要关注的是,提取本文开篇提及目标数据标签位置的代码,局部代码如下:

1
python复制代码element.xpath('//div[@id="data-more"]/text()')[0]

上述代码还调用了一个函数:get_big_img(),该函数目的是获取猫咪大图,函数体如下所示,可同步编写 save_img() 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码def get_big_img(name, ret_detail_html):
element = etree.HTML(ret_detail_html)
img_url = element.xpath('//img[@id="imgMainView"]/@src')
if len(img_url) > 0:
save_img(name, img_url[0])
else:
pass


# 保存图片
def save_img(name, img_url):
img_headers = {
"Host": "img08.tooopen.com",
"Referer": "https://www.tooopen.com/view/2298460.html"
}
res = requests.get(url=img_url, headers=img_headers, timeout=10)
data = res.content
if data is not None:
with open(f'./imgs/{name}.jpg', 'wb+') as f:
f.write(data)

在请求图片地址时,由于其存在外链限制,所以需要在请求头中增加 HOST 与 Referer 参数。

最后在增加 main() 函数,实现对上述代码的调用,即可完成本案例。

1
2
3
4
5
6
7
8
python复制代码if __name__ == '__main__':
headers = {
"user-agent": 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0'
}
base_url = 'https://www.tooopen.com/img/89_869_1_{}.aspx'
urls = [base_url.format(_) for _ in range(1, 100)]
for url in urls:
get_html(url)

运行代码之后,就会得到高清猫咪图,(由于目标站点不属于自己,可能存在版权问题,顾采集之后的图片及时删除)

image.png

本案例顺利完成,欢迎在评论区一起交流+吸猫

本文转载自: 掘金

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

力扣第102题-二叉树的层序遍历 前言 一、思路 二、实现

发表于 2021-11-21

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

前言

力扣第102题 二叉树的层序遍历 如下所示:

给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。

示例:

二叉树:[3,9,20,null,null,15,7],

1
2
3
4
5
markdown复制代码    3
/ \
9 20
/ \
15 7

返回其层序遍历结果:

1
2
3
4
5
csharp复制代码[
[3],
[9,20],
[15,7]
]

一、思路

题目中就一个要求:返回层序遍历结果

既然要按树的深度(层数)从左至右获取结果,所以我们至少要知道一个信息:当前层数有多少个节点。而当前层数的节点,是由前面的节点决定的。

所以我们需要在遍历当前的节点时,将不为空 null 的左孩子和右孩子加入下一层节点 nextLevel,提供给下一次遍历。

综上所述,实现的步骤只需要两步:

  1. 遍历当前层的节点,将节点值加入到当前层结果集,并依次将各节点不为空的 左孩子 和 右孩子 加入到下一层的节点
  2. 只要下一层节点不为空,则重复第一步的过程

举个例子

在这里我们可以使用 递归 或者 遍历 来实现,这两种实现代码都会在实现代码中给出示例。在这里我们使用 递归 来举个例子:

一些变量说明:

List<TreeNode> currentLevel:当前层的不为空的节点
List<TreeNode> nextLevel:下一层不为空的节点
List<Integer> temp:当前层的结果集

以示例中的 [3,9,20,null,null,15,7] 作为例子

  1. 根节点不为空,遍历第一层节点 currentLevel = [3],可获得结果集 temp = [3],再将左孩子右孩子加入下一层节点 nextLevel = [9, 20]
  2. 遍历第二层 currentLevel = [9, 20],可获得结果集 temp = [9, 20],再将他们的左孩子和右孩子加入下一层节点 nextLevel = [15, 7]
  3. 遍历第三层 currentLevel = [15, 7],可获得节点集 temp = [15, 7]。当前层节点中都没有左右孩子了,故结束遍历
  4. 返回结果 ret = [[3], [9, 20], [15, 7]] 即可

二、实现

实现代码

实现代码与思路中保持一致,下方为递归和遍历两种实现方式

递归

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
java复制代码List<List<Integer>> ret = new ArrayList<>();

public List<List<Integer>> levelOrder(TreeNode root) {
if (root!=null) {
dfs(Collections.singletonList(root));
}
return ret;
}

public void dfs(List<TreeNode> currentLevel) {
if (currentLevel.size() < 1)
return;
List<Integer> temp = new ArrayList<>();
List<TreeNode> nextLevel = new ArrayList<>();
// 遍历当前层级
for (TreeNode treeNode : currentLevel){
temp.add(treeNode.val);
if (treeNode.left != null){
nextLevel.add(treeNode.left);
}
if (treeNode.right != null){
nextLevel.add(treeNode.right);
}
}
ret.add(temp);
dfs(nextLevel);
}

遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public List<List<Integer>> levelOrder1(TreeNode root) {
List<List<Integer>> ret = new ArrayList<>();
if (root == null) {
return ret;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
List<Integer> temp = new ArrayList<>();
int currentLevelSize = queue.size();
for (int i = 1; i <= currentLevelSize; ++i) {
TreeNode node = queue.poll();
temp.add(node.val);
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
ret.add(temp);
}
return ret;
}

测试代码

1
2
3
4
5
6
java复制代码public static void main(String[] args) {
TreeNode treeNode = new TreeNode(3,
new TreeNode(9),
new TreeNode(20, new TreeNode(15), new TreeNode(7)));
new Number102().levelOrder(treeNode);
}

结果

image.png

三、总结

感谢看到最后,非常荣幸能够帮助到你~♥

如果你觉得我写的还不错的话,不妨给我点个赞吧!如有疑问,也可评论区见~

本文转载自: 掘金

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

Nacos入门 1 简介 2 安装 3 实践

发表于 2021-11-21

1 简介

Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。——《nacos官网》

前提环境:

  • 64 bit JDK 1.8+
  • Maven 3.2.x+

2 安装

下载地址:github.com/alibaba/nac…

首先在GitHub上下载nacos源码,然后解压。

启动命令:

Windows:startup.cmd -m standalone

Linux:sh startup.sh -m standalone

默认以集群模式启动,可在启动命令中添加-m standalone指定单机模式启动,也可以通过修改启动启动文件中的MODE配置为standalone
在这里插入图片描述

启动成功页:
在这里插入图片描述

浏览器输入http://127.0.0.1:8848/nacos/进入Nacos控制台,使用默认账号密码nacos登录
在这里插入图片描述

3 实践

3.1新建SpringBoot项目nacos-provide

pom.xml:

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
xml复制代码<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.1.18.RELEASE</spring-boot.version>
<spring-cloud-alibaba.version>2.1.2.RELEASE</spring-cloud-alibaba.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

在配置文件application.properties或者application.yml中指定nacos的配置:

1
2
3
4
5
6
7
bash复制代码# 应用名称
spring.application.name=nacos-server
# 应用服务 WEB 访问端口
server.port=8080

# Nacos 服务发现与注册配置,指定nacos地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

3.2 启动应用

启动新建好的SpringBoot项目,就能在服务列表里面可以看到注册进来的服务,至此,就实现了将服务注册进nacos中。
在这里插入图片描述

3.3 接口调用

前面只是实现了如何将一个服务注册进nacos中,下面继续实现如何实现两个服务之间通过nacos进行接口调用。

  1. 在nacos-provide中编写测试接口:
1
2
3
4
5
6
7
java复制代码@RestController
public class ProvideController {
@GetMapping("/provide_test")
public String provideTest(){
return "test";
}
}
  1. 新建nacos-consumer服务,pom和配置同nacos-provide一样。
  2. 然后启动nacos-consumer服务,即可在nacos服务列表中看到两个服务都已被注册到nacos中。

3.3.1方法一:使用RestTemplate

在nacos-consumer中编写测试接口,然后使用浏览器或者postman调用nacos-consumer的/consumer_test接口,就会通过RestTemplate远程调用到nacos-provide服务的provide_test接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@RestController
public class ConsumerController {

@Autowired
private RestTemplate restTemplate;

@GetMapping("/consumer_test")
public String consumerTest(){
String res = restTemplate.getForObject("http://nacos-provide/provide_test", String.class);
return res;
}

@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
}

3.3.2 方法二:使用OpenFeign

  1. pom.xml
1
2
3
4
5
xml复制代码<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>
  1. 启动类加上@EnableFeignClients注解
1
2
3
4
5
6
7
8
9
java复制代码@SpringBootApplication
@EnableFeignClients
public class NacosConsumerApplication {

public static void main(String[] args) {
SpringApplication.run(NacosConsumerApplication.class, args);
}

}
  1. 新建接口TestFeign
1
2
3
4
5
java复制代码@FeignClient("nacos-provide")
public interface TestFeign {
@GetMapping("/provide_test")
public String test();
}
  1. 编写测试接口
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@RestController
public class ConsumerController {

@Autowired
private TestFeign testFeign;

@GetMapping("/consumer_test")
public String consumerTest(){
return testFeign.test();
}

}

同样的使用浏览器或者postman调用nacos-consumer的/consumer_test接口,就会通过OpenFeign远程调用到nacos-provide服务的provide_test接口。

本文转载自: 掘金

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

性能工具之JMeter Dubbo 脚本开发

发表于 2021-11-21

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


前言

Apache Dubbo 是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。

IDEA 环境项目部署

工程整个目录结果

image.png

主要包括:

  • API 接口与实体类
  • Provider数据提供者
  • Consumer消费者

API 关键接口代码

UserInfoService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码package com.dunshan.api.service;

import com.dunshan.api.pojo.UserInfo;

import java.util.HashMap;
import java.util.List;

/**
* @author 7DGroup
* @version 1.0
* @Date: 2021-05-04 11:51
* @Description: rpc接口调用
*/
public interface UserInfoService {

List<UserInfo> queryList();

HashMap<String, Object> queryMap(String name);
}

Provider 关键代码

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
java复制代码package com.dunshan.provider.Impl;

import com.dunshan.api.pojo.UserInfo;
import com.dunshan.api.service.UserInfoService;
import org.apache.dubbo.config.annotation.Service;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

/**
* @author 7DGroup
* @version 1.0
* @Date: 2021-05-04 12:03
* @Description: 提供者实现类
*/
@Service
public class UserInfoServiceImpl implements UserInfoService {
@Override
public List<UserInfo> queryList() {
// 初始化数据
UserInfo testDTO1 = new UserInfo();
testDTO1.setId(1);
testDTO1.setName("学生");
testDTO1.setNumber(100);
testDTO1.setCreateTime(new Date());
UserInfo testDTO2 = new UserInfo();
testDTO2.setId(2);
testDTO2.setName("7D-RESAR-性能测试");
testDTO2.setNumber(101);
testDTO2.setCreateTime(new Date());
// 组装数据
List<UserInfo> list = new ArrayList<>();
list.add(testDTO1);
list.add(testDTO2);
return list;
}

@Override
public HashMap<String, Object> queryMap(String name) {
HashMap<String, Object> map = new HashMap<>(2);
map.put(name, "7D-RESAR-初级工具班");
map.put("nacos", "注册中心,配置管理中心");
map.put("date", System.currentTimeMillis());
return map;
}
}

配置文件 application.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
yml复制代码server:
port: 8861
dubbo:
# 配置服务信息
application:
name: dubbo-provider
# 禁用QOS同一台机器可能会有端口冲突现象
qos-enable: false
qos-accept-foreign-ip: false
# 配置注册中心
registry:
address: nacos://127.0.0.1:8848
# 设置协议-协议由提供方指定消费方被动接受
protocol:
name: dubbo
port: 20880
spring:
main:
# 解决Bean重复定义问题
allow-bean-definition-overriding: true

Consumer 关键代码

ConsumerController:

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
java复制代码package com.dunshan.consumer.controller;

import com.dunshan.api.pojo.ResultVO;
import com.dunshan.api.pojo.UserInfo;
import com.dunshan.api.service.UserInfoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.List;

/**
* @author 7DGroup
* @version 1.0
* @Date: 2021-05-04 11:54
* @Description: 消费测试接口
*/
@RestController
@RequestMapping("/api")
public class ConsumerController {

/**
* Dubbo远程调用注解
*/
@Reference
private UserInfoService userInfoService;

@RequestMapping(value = "/list", method = RequestMethod.GET)
public ResultVO getList() {
List<UserInfo> providerTestDTOList = userInfoService.queryList();
return new ResultVO.Builder<>().code(200).message("success").data(providerTestDTOList).build();
}

/**
* 查询查询
* @param name
* @return
*/
@GetMapping("/api/query")
public ResultVO query(String name) {
HashMap<String, Object> map = userInfoService.queryMap(name);
return new ResultVO.Builder<>().code(200).message("success").data(map).build();
}

}

application.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
yml复制代码server:
port: 8862
dubbo:
# 配置服务信息
application:
name: dubbo-consumer
# 禁用QOS同一台机器可能会有端口冲突现象
qos-enable: false
qos-accept-foreign-ip: false
# 配置注册中心
registry:
address: nacos://127.0.0.1:8848
# 设置超时时间
consumer:
timeout: 4000
spring:
main:
# 解决Bean重复定义问题
allow-bean-definition-overriding: true

验证结果

image.png

Nacos 环境部署

下载 nacos

下载链接:nacos.io/zh-cn/docs/…

启动:

1
bash复制代码sh startup.sh -m standalone

image.png

页面显示如下:
image.png

项目启动后,nacos显示如下:
image.png

dubbo 插件部署

1、下载: gitee.com/liselotte/s…

2、IDEA 中编译 Jar 包
image.png

3、打成 Jar 包放入 ${JMETER_HOME}\lib\ext 路径下,重启即可

image.png

image.png

4、重启 JMeter 查看插件

image.png

添加成功如:
image.png

测试 dubbo 接口

有上面的环境,并且 JMeter 中也有 Dubbo 插件,那么剩下的就是通过插件完成今天的接口开发,接下来看下需要测试的接口有哪些,这些只模拟无参接口与有参数接口。

下面是这次做测试 Dubbo 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package com.dunshan.api.service;

import com.dunshan.api.pojo.UserInfo;
import java.util.HashMap;
import java.util.List;

/**
* @author 7DGroup
* @version 1.0
* @Date: 2021-05-04 11:51
* @Description: rpc接口调用
*/
public interface UserInfoService {

List<UserInfo> queryList();
HashMap<String, Object> queryMap(String name);

不带参数请求

添加 Dubbo Sample 请求,并且根据上面需要测试的接口,在 Get Provider LIst 中的 Interface 中输入接口名字,与方法名称点击获取即可把需要测试的全部方法名称用列表显示出来。

image.png

添加结果查看树进行结果验证:
image.png

带参参数请求

添加 Dubbo Sample 请求

image.png

验证结果:
image.png

总结

有上面认识后自己搭建环境学习也好,办理事情也好,都会慢慢按步骤一步一步去实现。

源码地址:

  • github.com/zuozewei/bl…

本文转载自: 掘金

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

性能监控之 Java Metrics 度量包

发表于 2021-11-21

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


一、前言

前几天因为需要,折腾了一下 Java Metrics。发现之前的文章中并没有写过这个。

本着要把所有看到的性能相关的话题都要涉及的目的,在这里也要写一下。

二、简介

Metrics的官网首页简单的很,里面只有这么段描述:

Metrics is a Java library which gives you unparalleled insight into what your code does in production.
Metrics provides a powerful toolkit of ways to measure the behavior of critical components in your production environment.With modules for common libraries like Jetty, Logback, Log4j, Apache HttpClient, Ehcache, JDBI, Jersey and reporting backends like Graphite, Metrics provides you with full-stack visibility.

也就是说这个工具包可以让你在生产环境中产生度量的一些数据,并且支持不同的输出方式。

它可以度量代码中关键组件,响应时间、计数器等都可以采集,也可以取操作系统信息。

它的基本类型有如下几种:

在这里插入图片描述

看下官网的文档结构,就知道它能干多少事了,英文不错的,可以看下
metrics.dropwizard.io/4.0.0/。

三、举例说明

来看一个 Meter 的例子:

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
java复制代码/**
* Meter
* 作用:度量速率(例如,tps)
* Meters会统计最近1分钟(m1),5分钟(m5),15分钟(m15),还有全部时间的速率(速率就是平均值)。
*/

public class TestMeter {
public static void main(String[] args) throws InterruptedException {

/**
* 实例化一个MetricRegistry,是一个metrics的容器,维护一个MAP
*/

final MetricRegistry registry = new MetricRegistry(); //因为该类的一个属性final ConcurrentMap<String, Metric> metrics,在实际使用中做成单例就好

/**
* 实例化ConsoleReporter,输出
*/

ConsoleReporter reporter = ConsoleReporter.forRegistry(registry)
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
.build();
reporter.start(3, TimeUnit.SECONDS); //从启动后的3s后开始(所以通常第一个计数都是不准的,从第二个开始会越来越准),每隔3秒从MetricRegistry钟poll一次数据

//实例化一个Meter并注册到容器中去
Meter meterTps = registry.meter(MetricRegistry.name(TestMeter.class, "request", "tps")); //将该Meter类型的指定name的metric加入到MetricsRegistry中去
System.out.println("执行与业务逻辑");

//模拟数据
while (true) {
meterTps.mark(); //总数以及m1,m5,m15的数据都+1
Thread.sleep(500);
}
}
}

上面完整的示例代码都有注释,就不一一解释了。输出如下:

1
2
3
4
5
6
7
bash复制代码-- Meters ----------------------------------------------------------------------
com.zee.metrics.zeemetrics.TestMeter.request.tps
count = 12
mean rate = 2.00 events/second
1-minute rate = 2.00 events/second
5-minute rate = 2.00 events/second
15-minute rate = 2.00 events/second

这就可以统计这个请求在这段时间内访问了多少次,也就是 TPS。但是也不能老在 console 里看呀。

还是放到图形化的界面中看比较好,这里用的是 influxDB+Grafana,在 influxDB 中创建一个 database。

1
2
3
4
5
6
7
8
bash复制代码[gaolou@7dgroup2 ~]$ influx
Connected to http://localhost:8086 version 1.6.0
InfluxDB shell version: 1.6.0
> create database mydb;
> use mydb;
Using database mydb
> show MEASUREMENTS
>

其中的 MEASUREMENTS 是空的。

把数据输出进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Bean
public Meter requestMeter(MetricRegistry metrics) {
return metrics.meter("request");
}

@Bean(name = "influxdbReporter")
public ScheduledReporter influxdbReporter(MetricRegistry metrics) throws Exception {
return InfluxdbReporter.forRegistry(metrics)
.protocol(InfluxdbProtocols.http("1.1.1.1", 8086, "admin", "admin", "mydb"))
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
.filter(MetricFilter.ALL)
.skipIdleMetrics(false)
.build();
}

select * from request 就看到产生了如下内容。

在这里插入图片描述

再配置下 grafana 中的 data source

在这里插入图片描述

在 dashboard 中加下 panel,选择 influxdb 数据源,看到列表,选择想要看的数据表,再在 field 中选择想要看的列,保存。

在这里插入图片描述

然后在 dashboard 里就可以看到数据了。

在这里插入图片描述

四、总结

这个逻辑,在操作中并不困难。但是,从我自己的行业经验上来看。在研发时就让他们把要看到的数据做好监控的规划,最后运维时能如此清晰地展示出来,并最终对判断生产问题有用,这个过程的沟通成功会非常的高。

主要是看公司的组织结构是什么样,如果是全栈的结构会比较容易

如果是研发运维是分开的团队,并且相互配合的不好,会相当的痛苦。

在之前的项目中,有运维问我怎么才能监控到业务性能,我想了下那个公司的背景之后,沉吟良久说了声:难。

性能可做的东西并不少,前提是看格局有多大。

本文转载自: 掘金

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

JVM之虚拟机类加载时机

发表于 2021-11-21

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

概述

虚拟机类加载机制就是Java虚拟机把描述类从Class文件加载到内存并对数据进行校验、转换、解析与初始化最终形成可以被虚拟机直接使用的Java类型的过程。

Java语言中,类型的加载、连接和初始化都是在程序运行期间完成的,这会增加一些额外的性能开销,不过这也给Java带来了极大的扩展性和灵活性。

类的加载时机

一个类的生命周期主要为:加载->验证->准备->解析->初始化->使用->卸载 这七个阶段,其中验证、准备、解析这三个阶段有被称为连接阶段。发生的顺序如下图:

image.png

其中解析阶段有可能在初始化阶段之后开始(参考java的运行时绑定这一性质)。

主动引用

类加载的时机只有以下六种情况:

  1. 遇见new、getstatic、putstatic以及invokestatic这四条字节码指令时候,如果类型没有初始化则需要先进行初始化。
  2. 对类型进行反射调用的时候如果类型没有被初始化,则需要先进行初始化。
  3. 初始化类的时候,其父类没有被初始化,则先初始化其父类。
  4. 虚拟机启动的时候,虚拟机会先初始化主类(包含main()方法的那个类)。
  5. 使用jdk7的动态语言支持时,一个java.lang.invoke.MethodHandle实例最后解析的结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,如果这个句柄的类没有被初始化则先触发初始化。
  6. 一个接口定义了JDK8新加入的默认方法(被default关键字修饰的接口方法),在初始化该接口的实现类的时候,需要先初始化该接口。

被动引用

刚刚介绍的六种情况被称为主动引用,而除此之外所有引用类型的方式都不会触发初始化,而这些则被称为被动引用。

例如:

  • 子类引用父类静态字段不会导致子类初始化。
  • 通过数组来定义引用类不会触发此类的初始化。
  • 常量在编译阶段会存入调用类的常量池,本质上没有直接引用到定义常量的类,所以不会触发初始化。

在这插入一点小知识:常量和类以及静态变量是存储在方法区中,而实例对象则是被存储在Java堆中。栈的话存放了基本变量类型和引用对象的变量。

关于接口初始化,其在初始化的时候和类的初始化不同,其并不需要父接口的初始化,只有真正用到了父接口的时候才会初始化。

本文转载自: 掘金

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

1…252253254…956

开发者博客

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