Fork me on GitHub
夸克的博客


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

库存设计的场景原则

发表于 2020-11-22 | 分类于 系统设计 | 热度: ℃
字数统计: 1,025 | 阅读时长 ≈ 3

基本原则

对于秒杀商品的库存设计应该遵守的几个原则:

  • 渠道隔离:秒杀活动中使用的库存应当按照渠道进行隔离,这样既能保证不对正常售卖渠道产生影响,又有利于精细化运作库存管理。比如,根据用户群体或平台支持的力度不同,我们可能需要在不同渠道透出不同的库存数量,并且它们与正常售卖渠道分离,这种场景下就需要渠道隔离;
  • 防止超卖:业务来说,超卖可能意味着资损;对技术来说,超卖意味着架构的失败。试想,原价999元商品的秒杀价为599,库存100件却卖出了10000件,那么我们就会面临严重的客诉或资损;
  • 防止重复扣减:与超卖相对的是没有卖出去,其同样不可小觑。比如,10000件的库存仅有10人成单,库存明明还在却显示已经售罄,活动未到达预期,前期准备和推广的资金投入都打了水漂,而由系统设计缺陷造成的重复扣减 就会导致这种糟糕的情况发生;也就是同时也防止少卖。
  • 高性能:在前面的文章我们谈到了如何通过缓存提高秒杀架构中的”读“性能,殊不知”写“性能也是秒杀架构的重要指标之一。举例来说,10000比订单,每秒写入300单和每秒写入3000单在用户体验上有着显著的差异。

常用库存扣减方案

基于数据库的库存扣减方案

顾名思义,基于数据库的库存扣减方案指的是利用数据库的特性在数据库层面完成库存扣减。这种方式实现起来比较简单,对于并发量低或库存低的场景,推荐使用这种方案。所谓库存低,指的是当库存很少时,我们只要把大流量拦截在应用之外或数据库之外即可,确保数据库的并发量处于低位。否则,在高并发写入的场景下,这种方案是不合适的

基于缓存的库存扣减方案

既然数据库方案无法满足高并发写入场景,那么缓存是否可以呢?答案是看情况。对于Redis这种缓存,虽然速度较快,但是存在数据丢失的可能,这是我们无法接受的。所以,当我们决定使用基于缓存的扣减方案时,我们就必须考虑如何保证缓存不丢失的稳定,比如使用LevelDB等。另外,相较于数据库方案,缓存方案需要更高的实现复杂度。

分库分表库存扣减方案

解决数据库高并发的写入问题,除了使用缓存方案外,还可以采用分库分表的库存扣减方案,将库存分散到不同的库中。比如,单台数据库每秒能支撑300的库存更新,那么10台数据库即可支撑3000的并发写入。

当然,相较于前两种方案,虽然分库分表的优势明显,但具有更高的复杂性和实现成本。

常用库存扣减方式

  • 下单扣库存:优势在于简单,链路短,性能好,缺点在于容易被恶意下单。活动刚开始,可能即被恶意下单清空库存;
  • 支付扣库存:优势在于可以控制恶意下单,最后得到库存的都是有效订单。当然,其缺点也较为明显,无法控制下单人数,用户需要在支付时再次确认库存;
  • 下单预扣库存,超时取消:相较于前两种方式,这种方式较为折中且有效,对于正常下单的用户来说抢单即是得到,对于恶意下单的来说,占据的库存会超时自动释放。

RR隔离级别下的gap和插入意向锁死锁分析

发表于 2020-09-18 | 分类于 mysql | 热度: ℃
字数统计: 1,116 | 阅读时长 ≈ 5

死锁日志的查看

https://segmentfault.com/a/1190000018730103

sql

1
show engine innodb status;

记录锁,间隙锁,Next-key 锁和插入意向锁。这四种锁对应的死锁如下:

记录锁(LOCK_REC_NOT_GAP): lock_mode X locks rec but not gap
间隙锁(LOCK_GAP): lock_mode X locks gap before rec
Next-key 锁(LOCK_ORNIDARY): lock_mode X
插入意向锁(LOCK_INSERT_INTENTION): lock_mode X locks gap before rec insert intention

死锁案例

https://my.oschina.net/hebaodan/blog/1835966
https://my.oschina.net/hebaodan/blog/3033276

gap锁 + 并发insert死锁

表及Sql

image-20220701102753626

分析

image-20220701102803796

锁冲突矩阵

image-20220701102814013

并发delete + insert唯一键冲突死锁

表及场景sql:

image-20220701102823672

分析

image-20220701102834314

死锁日志

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
*** (1) TRANSACTION:
TRANSACTION 11074, ACTIVE 11 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 10, OS thread handle 123145442168832, query id 160 localhost 127.0.0.1 root updating
// T2的SQL
delete from t4 where a = 1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED: // 等待的锁
RECORD LOCKS space id 68 page no 3 n bits 72 index PRIMARY of table
// 可以看到这里是阻塞等待X行锁
`aliyun`.`t4` trx id 11074 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32
0: len 4; hex 80000001; asc ;;
1: len 6; hex 000000002b41; asc +A;;
2: len 7; hex 2e000001dc13c4; asc . ;;

*** (2) TRANSACTION:
TRANSACTION 11073, ACTIVE 40 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 9, OS thread handle 123145442725888, query id 161 localhost 127.0.0.1 root update
// 事务T1的SQL
insert into t4 values(1)
*** (2) HOLDS THE LOCK(S):// 持有的锁
RECORD LOCKS space id 68 page no 3 n bits 72 index PRIMARY of table
`aliyun`.`t4` trx id 11073 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32
0: len 4; hex 80000001; asc ;;
1: len 6; hex 000000002b41; asc +A;;
2: len 7; hex 2e000001dc13c4; asc . ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED: // 等待的锁
RECORD LOCKS space id 68 page no 3 n bits 72 index PRIMARY of table
// 这里看到事务T1 S-Next锁 等待行锁
`aliyun`.`t4` trx id 11073 lock mode S waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32
0: len 4; hex 80000001; asc ;;
1: len 6; hex 000000002b41; asc +A;;
2: len 7; hex 2e000001dc13c4; asc . ;;

*** WE ROLL BACK TRANSACTION (1)

sql加锁分析

https://www.aneasystone.com/archives/2017/12/solving-dead-locks-three.html (这是一个系列)

基本加锁规则

  • 常见语句的加锁
    • select 快照读,在RC和RR下不加锁
    • select … lock in share mode为当前读,加S锁
    • select … for update为当前读,加X锁
    • dml语句,当前读,加X锁
    • ddl语句,加标记所,隐式提交,不能回滚。
  • 表锁
    • 表锁(S和X锁)
    • 意向锁(IS锁和IX锁)
    • 自增锁(一般看不见 只有在innodb_autoinc_lock_mode=0或者bulk inserts时才可能有)
  • 行锁
    • 记录锁(S和X锁)
    • 间隙锁(S和X锁)
    • Next-Key锁(S和X锁)
    • 插入意向锁
  • 行锁分析
    • 行锁是加在索引上的,最终都会落在聚簇索引上。
    • 加行锁是一行行加的
  • 锁冲突
    image-20220701102847454

  • 不同隔离级别下的锁

    • select快照读在Serializable隔离级别下为当前读,加S锁。
    • RC下没有间隙锁和Next-Key锁(唯一索引情况下有特殊,purge线程+记录有锁唯一索引,会加S Next-Key锁)

对于insert操作

如果没有这两个问题
(1)为了防止幻读,如果记录之间加有 GAP 锁,此时不能 INSERT;(插入意向锁解决)
(2)如果 INSERT 的记录和已有记录造成唯一键冲突,此时不能 INSERT;(对已存在唯一记录加S Next锁)

  • insert一开始只有隐式锁,除非隐式锁转换为显式锁: InnoDb 在插入记录时,是不加锁的。如果事务 A 插入记录且未提交。
    如果这时事务 B 尝试对这条记录加锁,事务 B 会先去判断记录上保存的事务 id 是否活跃,如果活跃的话,那么就帮助事务 A 去建立一个锁对象,然后自身进入等待事务 A 状态,这就是所谓的隐式锁转换为显式锁。还比如对于辅助索引也是隐式锁。
  • insert 对唯一索引的加锁逻辑:
    • 1.先做UK冲突检测,如果存在目标行,先对目标行加S Next Key Lock(该记录在等待期间被其他事务删除,此锁被同时删除)
    • 2.如果1成功,对对应行加插入意向锁
    • 3.如果2成功,插入记录,并对记录加X + 行锁(有可能是隐式锁)

image-20220701102856707

dubbo集群容错Cluster

发表于 2020-07-16 | 分类于 dubbo | 热度: ℃
字数统计: 1,751 | 阅读时长 ≈ 8

Cluster接口

Cluster接口提供了集群容错的功能。在 Dubbo 中,通过 Cluster 这个接口把一组可供调用的 Provider 信息组合成为一个统一的 Invoker 供调用方进行调用。经过 Router 过滤、LoadBalance 选址之后,选中一个具体 Provider 进行调用,如果调用失败,则会按照集群的容错策略进行容错处理。

Cluster接口的工作流程

Cluster接口工作流程分为两步:

  • 在Consumer进行服务引用的时候,会创建对应Cluster实现类的集群容错策略对应的ClusterInvoker。也就是说在Cluster接口实现中,都会创建对应的Invoker对象,这些都继承自AbstractClusterInvoker抽象类。
    image-20220716000438172
  • 调用时使用ClusterInvoker实例,内部会实现集群容错的逻辑,且会依赖Directory、Router、LoadBalance等组件得到最终要调用的Invoker对象。

也就是说,因为Cluster在服务引用的过程中将多个Invoker伪装为带有集群容错的ClusterInvoker实现,所以在调用的时候可以在对应集群容错逻辑下,再对Invokers进行服务目录、服务路由过滤、负载均衡选址选出真正调用的Invoker发起远程调用逻辑。

image-20220716000449753

常见的几种集群容错的方式

image-20220716000501970

Dubbo中的AbstractClusterInvoker

了解了 Cluster Invoker 的继承关系之后,我们首先来看 AbstractClusterInvoker,它有两点核心功能:一个是实现的 Invoker 接口,对 Invoker.invoke() 方法进行通用的抽象实现;另一个是实现通用的负载均衡算法。

在 AbstractClusterInvoker.invoke() 方法中,会通过 Directory 获取 Invoker 列表,然后通过 SPI 初始化 LoadBalance,最后调用 doInvoke() 方法执行子类的逻辑。在 Directory.list() 方法返回 Invoker 集合之前,已经使用 Router 进行了一次筛选。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public Result invoke(final Invocation invocation) throws RpcException {
// 检查当前Invoker是否已销毁
checkWhetherDestroyed();

// binding attachments into invocation.
// 将RpcContext中的attachment添加到Invocation中
Map<String, Object> contextAttachments = RpcContext.getContext().getObjectAttachments();
if (contextAttachments != null && contextAttachments.size() != 0) {
((RpcInvocation) invocation).addObjectAttachments(contextAttachments);
}

// 通过Directory获取Invoker对象列表 RegistryDirectory内部会调用Router完成服务路由的功能
List<Invoker<T>> invokers = list(invocation); // 先去服务目录选择
// 再通过SPI加载对应的负载均衡实现
LoadBalance loadbalance = initLoadBalance(invokers, invocation);// 再负载均衡
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);

// 委托给具体的集群容错子类 实现调用
return doInvoke(invocation, invokers, loadbalance);
}

在子类实现的doInvoke方法中,调用了在抽象类中提供的select()方法,来完成负载均衡。这里没有去直接委托给LoadBalance去做负载均衡,而是在 select() 方法中会根据配置决定是否开启粘滞连接特性,如果开启了,则需要将上次使用的 Invoker 缓存起来,只要 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
protected Invoker<T> select(LoadBalance loadbalance, Invocation invocation,
List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {

if (CollectionUtils.isEmpty(invokers)) {
return null;
}
// 方法名称
String methodName = invocation == null ? StringUtils.EMPTY_STRING : invocation.getMethodName();

// 获取sticky配置,sticky表示粘滞连接,所谓粘滞连接是指Consumer会尽可能地调用同一个Provider节点,除非这个Provider无法提供服务
boolean sticky = invokers.get(0).getUrl()
.getMethodParameter(methodName, CLUSTER_STICKY_KEY, DEFAULT_CLUSTER_STICKY);

//ignore overloaded method
if (stickyInvoker != null && !invokers.contains(stickyInvoker)) {
stickyInvoker = null;
}
//ignore concurrency problem
if (sticky && stickyInvoker != null && (selected == null || !selected.contains(stickyInvoker))) {
if (availablecheck && stickyInvoker.isAvailable()) {
return stickyInvoker;
}
}

// doSelect选择新的Invoker
Invoker<T> invoker = doSelect(loadbalance, invocation, invokers, selected);

if (sticky) {
stickyInvoker = invoker;
}
return invoker;
}

Dubbo中的AbstractCluster

常用的 ClusterInvoker 实现都继承了 AbstractClusterInvoker 类型,对应的 Cluster 扩展实现都继承了 AbstractCluster 抽象类。AbstractCluster 抽象类的核心逻辑是在 ClusterInvoker 外层包装一层 ClusterInterceptor,从而实现类似切面的效果。

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
@Override
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
// 2. 外层对ClusterInvoker进行切面包装 返回InterceptorInvokerNode (继承自AbstractClusterInvoker接口)
return buildClusterInterceptors(
// 1. 先doJoin获取最终要调用的Invoker对象 比如直接返回一个FailOverClusterInvoker
doJoin(directory),
directory.getUrl().getParameter(REFERENCE_INTERCEPTOR_KEY)
);
}

// buildClusterInterceptors方法
private <T> Invoker<T> buildClusterInterceptors(AbstractClusterInvoker<T> clusterInvoker, String key) {
AbstractClusterInvoker<T> last = clusterInvoker;
// ClusterInterceptor是SPI加载自动激活的扩展实现
List<ClusterInterceptor> interceptors = ExtensionLoader.getExtensionLoader(ClusterInterceptor.class).getActivateExtension(clusterInvoker.getUrl(), key);

if (!interceptors.isEmpty()) {
for (int i = interceptors.size() - 1; i >= 0; i--) {
// 将InterceptorInvokerNode收尾连接到一起,形成调用链
final ClusterInterceptor interceptor = interceptors.get(i);
final AbstractClusterInvoker<T> next = last;
last = new InterceptorInvokerNode<>(clusterInvoker, interceptor, next);
}
}
return last;
}

Dubbo中具体的集群容错实现

FailOverCluster

默认是FailOverCluster,可以看到其实现的doJoin方法就是创建一个对应的FailOverClusterInvoker对象并返回。

1
2
3
4
5
@Override
public <T> AbstractClusterInvoker<T> doJoin(Directory<T> directory) throws RpcException {
// 直接委托给FailoverClusterInvoker
return new FailoverClusterInvoker<>(directory);
}

而在FailOverClutserInvoker中实现的doList方法中,有对失败重试容错的实现:

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
@Override
@SuppressWarnings({"unchecked", "rawtypes"})
public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
List<Invoker<T>> copyInvokers = invokers;
checkInvokers(copyInvokers, invocation);
String methodName = RpcUtils.getMethodName(invocation);
// 获取重试次数
int len = getUrl().getMethodParameter(methodName, RETRIES_KEY, DEFAULT_RETRIES) + 1;
if (len <= 0) {
// 默认重试次数是1
len = 1;
}
// retry loop. 循环来实现重试

// 最近的一次异常
RpcException le = null; // last exception.
// 记录已经尝试调用过的Invoker
List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyInvokers.size()); // invoked invokers.

// 记录执行过的provider地址
Set<String> providers = new HashSet<String>(len);
for (int i = 0; i < len; i++) {
//Reselect before retry to avoid a change of candidate `invokers`.
//NOTE: if `invokers` changed, then `invoked` also lose accuracy.
if (i > 0) {
// 第二次进来就是重试的逻辑 需要再次走一遍 服务目录list(内部会走route服务路由)拉取最新的Invoker列表并检查
checkWhetherDestroyed();
// list重新从服务目录中获取集合
copyInvokers = list(invocation);
// check again
checkInvokers(copyInvokers, invocation);
}
// AbstractClusterInvoker.select() 有自己的粘连逻辑(优先调用一个provider) 也会根据负载均衡算法来选出provider
Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
invoked.add(invoker);
RpcContext.getContext().setInvokers((List) invoked);
try {
// 进行调用
Result result = invoker.invoke(invocation);
if (le != null && logger.isWarnEnabled()) {
logger.warn("Although retry the method " + methodName
+ " in the service " + getInterface().getName()
+ " was successful by the provider " + invoker.getUrl().getAddress()
+ ", but there have been failed providers " + providers
+ " (" + providers.size() + "/" + copyInvokers.size()
+ ") from the registry " + directory.getUrl().getAddress()
+ " on the consumer " + NetUtils.getLocalHost()
+ " using the dubbo version " + Version.getVersion() + ". Last error is: "
+ le.getMessage(), le);
}
return result;
} catch (RpcException e) {
if (e.isBiz()) { // biz exception. dubbo内部的biz异常
throw e;
}
// 记录异常
le = e;
} catch (Throwable e) {
// 封装为rpc异常 然后记录下次重试
le = new RpcException(e.getMessage(), e);
} finally {
// 记录已经尝试过的provider地址
providers.add(invoker.getUrl().getAddress());
}
}
// 超过重试次数 抛出异常
throw new RpcException(le.getCode(), "Failed to invoke the method "
+ methodName + " in the service " + getInterface().getName()
+ ". Tried " + len + " times of the providers " + providers
+ " (" + providers.size() + "/" + copyInvokers.size()
+ ") from the registry " + directory.getUrl().getAddress()
+ " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version "
+ Version.getVersion() + ". Last error is: "
+ le.getMessage(), le.getCause() != null ? le.getCause() : le);
}

duubo调用经历源码路径

发表于 2020-06-21 | 分类于 dubbo | 热度: ℃
字数统计: 0 | 阅读时长 ≈ 1

image-20220702043817608

duubo的proxy代理

发表于 2020-06-21 | 分类于 dubbo | 热度: ℃
字数统计: 1,482 | 阅读时长 ≈ 7

Proxy层

image-20220701064957928

proxy层是dubbo中的动态代理层,主要是为了屏蔽dubbo内部Cluster、Protocol概念设计,能让业务层像调用本地方法一样去发起RPC调用。

阅读全文 »

sql慢查的原因总结

发表于 2020-06-17 | 分类于 mysql | 热度: ℃
字数统计: 1,356 | 阅读时长 ≈ 5
  1. SQL的where没有对应的索引,导致走了全表查询。
  2. 索引失效

    • 索引列的隐式转换。类型不匹配。
    • 索引列直接使用内置函数。
    • 多个列排序时顺序不一致。order by a asc,b desc会导致索引失效。
    • 查询条件包含or可能会导致索引失效。
      比如sql,优化器成本分析走name索引之后还要去全表扫描过滤age条件,不如全表扫描,所以最后没有走索引。

      1
      2
      -- name有索引 age没有
      select * from user where name = 'xxx' or age =10;
    • like通配符可能导致索引失效。比如查询条件是 like ‘%aaa’。这里可以优化的一个点是优化为最左前缀原则和select的字段尽量只有索引值,让sql可以走索引覆盖。

    • 联合索引没有走最左前缀原则。这里可以注意下二级联合索引的icp索引下推减少回表次数的特性。
    • 索引值进行运算。走不了索引的。
    • in、not in、is null、is not null使用如果扫描过多行记录,优化器可能选择全表扫描。
    • 左右连接,用于关联的字段编码格式不一样,这个比较隐蔽。
    • Mysql优化器的选择不一定是最佳的,如果想让走一个索引可以使用force inedx。
  3. limit深度分页问题

    • limit的过程:

      1
      select * from table where created_time > '2020-01-01' limit 100000, 10;
      1. 通过二级索引created_time,过滤条件,找到符合条件的id。
      2. 主键id回表查询。
      3. 依次扫描符合条件的100010行,取最后10行返回。
    • 深度分页慢的原因
      1. limit深度分页会扫描前100000行,然后再取到对应步长的数据。
      2. 扫描这么多行,意味着需要回表这么多次,回表查询是一个随机IO的过程。
    • 如何优化?

      1. 标签记录。

        1
        select  id,name,balance FROM account where id > 100000 limit 10;

        这个方法有一定的局限性,比如要求查询条件能定位到id这种标签,走聚簇索引去直接拿到对应的记录去捞取10条记录。

      2. 延迟关联
        这个方法的思路就是拆分成join查询,因为之前一次查询只能走二级索引之后回表去查询对应深度的分页数据,这里思路是先在二级索引上查找符合条件的主键id,再与原来的表join通过主键id关联,这里的连接查询是主键关联,不需要二级索引每条查出来主键id去回表,速度也是不慢的。

        1
        2
        select id, name, balance from account b inner join 
        (select a.id from account a where a.created_time > '2020-01-01' limit 100000, 10) on a.id = b.id
  4. 单表中的数据太大,三层B+树大概能承载2000w的记录,所以如果单表数据量太大,那么B+树的高度会变高,加多了加载数据的磁盘IO次数,即索引的效率会变慢。

  5. 过多的表连接查询。一般不建议超过3个表进行连接查询,连接查询也要用索引列关联,否则会占用内存创建join_buffer,来用块的嵌套循环查询算法,加大对内存的压力。可以尝试在应用代码里做关联。
  6. 数据库在刷脏页时可能会阻塞sql的执行。

    • redo log文件写满了,要将循环写的redo log文件中记录的数据脏页刷入磁盘,已能为redo log提供空间。这时会阻塞写sql的性能。
    • Buffer Pool没有额外的空闲缓存页,这时候要根据lru链表将一些脏页、不常用的冷数据刷入到磁盘。因为脏页刷入磁盘是随机IO的过程。
  7. order by 文件排序

如果order by不能使用索引排序,则会使用file sort文件排序,这里超过sort_buffer之后会到磁盘中排序,性能很差。索引天生有序这个特点要在order by这个语句中使用,尽量让排序走索引。

  1. 锁等待。

如果出现同一行数据被多个事务竞争锁,那么会造成事务锁等待,会加大sql的执行时间。

  1. group by 默认排序和使用临时表

group by默认是排序的,而且为了实现分组可能使用临时表进行统计。这两点可能造成慢查询。优化点可以有:

  • 让group by 的字段不排序,则少了一个排序的过程。这个取决于业务上只要分组。
  • 注意内存临时表参数的设置,避免使用磁盘临时表。
  • group by使用索引,因为索引天生有序,所以统计可以直接扫描索引树。
  1. 本身数据库的机器问题和一些参数设置。
    本身机器比如IOPS(机器随机读写的性能)、CPU、内存、网络带宽都会影响到数据库的效率。

比如测试环境的机器打开了索引下推ICP或者index merge,而线上机器没有打开,则sql查询使用索引情况肯定不一样,这里要注意下。

还有Buffer Pool设置的大小,如果设置的太小,频繁造成刷页到内存中,会造成数据库慢查询。

dubbo分层架构

发表于 2020-06-12 | 分类于 dubbo | 热度: ℃
字数统计: 732 | 阅读时长 ≈ 3

dubbo整体分层

image-20220702043515731

  • 左边淡蓝色背景的是服务消费者使用的接口,右边淡绿色背景的是服务提供者使用的接口,位于中轴线是双方都使用到的接口。
  • 由上到下分为十层,各层为单向依赖,右边黑色箭头为依赖关系。每一层都可以剥离上层被复用,其中Service层和config层是api,其他层是SPI。
  • 绿色小块是扩展接口,蓝色小块为实现类。
  • 蓝色虚线是初始化部分,启动时候的export和refer流程;红色实线是调用过程,即发起一次dubbo调用。紫色箭头为继承。

各层的说明

  • config配置层:对外配置接口,以ServiceConfig、ReferenceConfig为中心,初始化配置类,也可以通过Spring容器生成配置类。
  • Proxy层:服务接口透明代理,生成服务的客户端Stub和服务器端Skeleton,可以理解为在客户端生成代理来发起远程调用,服务端生成统一的可调用代理,扩展接口是ProxyFactory,主要实现是Javassist动态代理。
  • registry注册中心层:封装服务注册和发现,以服务URL为中心,实现注册、订阅、监听回调等功能,扩展接口为RegistryFactory、RegistryService、Registry,代码层面分为api和具体实现,有多种实现比如etcd、redis、zk等。
  • cluster路由层:封装多个服务提供者的服务目录、路由、负载均衡,以Invoker为中心,并桥接注册中心,扩展接口为Cluster、Directory、Router、LoadBalance。
  • monitor层:RPC调用次数和调用时间监控。
  • protocol远程调用层:封装RPC调用,以Invocation、Result为中心,扩展接口为Protocol、Invoker、Exporter。代码层面有dubbo-rpc-api和具体的协议实现。
  • exchange信息交换层:封装请求响应模式,同步转异步,以Request、Response为中心,扩展接口为Exchanger、ExchangeChannel、ExchangeClient、ExchangeServer。
  • transport网络传输层:抽闲mina和netty等为统一接口,以Message为中心,扩展接口是Channel、Transporter、Client、Server、Codec
  • serialize 数据序列化层:提供一些可复用的序列化算法和工具,扩展接口为 Serialization、ObjectInput、ObjectOutput、ThreadPool

各层之间的关系

  • 在dubbo中,Protocol是核心层,也就是只要有Protocol+Invoker+Exporter就可以完成非透明的RPC调用。
  • Cluster不是必须的,只是服务提供者都是集群,Cluster目的是伪装为一个Invoker,外层只需要关注Protocol层Invoker即可,只有一个服务提供者是不需要Cluster的。
  • Proxy层是和用户(服务端、客户端)交互时屏蔽RPC的细节使用的。让PRC调用变得更透明,调用者就像调用本地的接口。
  • Remoting包含了Exchang信息交换层和Transport信息传输层,Transport只是封装了单向的信息传输,比如Netty接收发送消息,而在Exchange层封装了Request-Reponse语义。

调用时经历的模块

image-20220702043536485

duubo注解整合spring原理

发表于 2020-06-01 | 分类于 dubbo | 热度: ℃
字数统计: 2,613 | 阅读时长 ≈ 12

dubbo整合Spring使用的注解

对于一个Provider的启动类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Application {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ProviderConfiguration.class);
context.start();
System.in.read();
}

@Configuration
@EnableDubbo(scanBasePackages = "org.apache.dubbo.demo.provider")
@PropertySource("classpath:/spring/dubbo-provider.properties")
static class ProviderConfiguration {
@Bean
public RegistryConfig registryConfig() {
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://127.0.0.1:2181");
return registryConfig;
}
}
}

可以看到在配置类中加入了@EnableDubbo注解,还利用Spring的@PropertySource(path)导入了provider的配置,后者其实就是把配置文件的properties属性导入到Spring的Environment中,方便使用。

@EnableDubbo肯定也是采用了@Enable + @Import模式导入了BeanDefinition来实现Dubbo和Spring的整合。

暴露一个Dubbo接口使用@Service注解,引用一个Dubbo服务使用
Dubbo@Reference注解。

Dubbo和Spring整合要做的事情

总结一下Dubbo和Spring整合要做的一些事情:

  • 把配置properties文件实例化为一个个bean来管理,方便注入到ServiceBean(dubbo暴露的服务bean)和ReferenceBean(@Reference注解引用的bean)。这里可以看下ProviderConfig、ApplicationConfig等配置类,内部的变量属性就是配置文件中的key。
  • 扫描应用中使用@Service注解的服务,生成其对应的ServiceBean,表示其为一个dubbo服务。然后在一定的时机(比如收到上下文刷新的事件之后)触发此接口的export导出逻辑。export是dubbo内部的逻辑,在Spring整合Dubbo这块不纠结。
  • 扫描@Reference注解的属性或者方法,注入引用的Dubbo服务代理对象。这里要完成对dubbo对象的属性注入,且完成其需要的代理逻辑,然后调用dubbo内部的refer()方法。

整体流程和原理

image-20220701063232019

  • @Service的处理在Spring扩展点:BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry。
  • @Reference的处理在Spring扩展点:InstantiationAwareBeanPostProcessorAdapter.postProcessPropertyValues。
  • @Service注解会先注册Spring bean(和dubbo无关),比如DemoServiceImpl,这个就是最普通的bean。还会生成Dubbo标识暴露服务的ServiceBean。
  • 在Spring启动完成之后,会通过事件或者回调函数来完成export即dubbo接口导出的工作。
  • @Reference的解析会在Spring容器中存放ReferenceBean,如果是本地的Dubbo服务,会直接将ServiceBean作为ReferenceBean,且代理逻辑会直接调用类的方法;而如果是远程的Dubbo服务,会调用get()方法内部走代理逻辑,其中也会走refer()方法。

@EnableDubbo注解

1
2
3
4
5
6
@EnableDubboConfig
@DubboComponentScan
// @EnableDubbo注解来标注在spring boot应用上 来开启dubbo接口暴露、引用(dubbo的@Service、@Reference注解的扫描)。还有dubbo config配置初始化为bean的过程
// 主要功能在@EnableDubboConfig、@EnableComponentScan
public @interface EnableDubbo {
}

主要是由内部两个注解完成。

@EnableDubboConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Import(DubboConfigConfigurationRegistrar.class)
// 解析Dubbo 配置 (properties文件)的支持 导入了DubboConfigConfigurationRegistrar
public @interface EnableDubboConfig {

/**
* It indicates whether binding to multiple Spring Beans.
*
* @return the default value is <code>false</code>
* @revised 2.5.9
*/
boolean multiple() default true;

}

可以看到了Import了DubboConfigConfigurationRegistrar这个bean定义的注册器,这个注册器其中主要注册了两个DubboConfigConfiguration内部类的Bean定义,这两个Bean上的又注解了@EnableConfigurationBeanBindings。

@EnableConfigurationBeanBindings注解import了ConfigurationBeanBindingPostProcessor,这个bean定义注册器中:

  • 开启import各个具体的配置bean 解析内部的每一个BeanBinding 生成对应的configBean。
  • 注册一个后置处理器(ConfigurationBeanBindingPostProcessor),去利用Spring的DataBinder去将properties中的配置值映射到对应的configBean中。 比如处理ApplicationConfig对象中的所有属性字段,值从properties文件中获取。
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
public class DubboConfigConfigurationRegistrar implements ImportBeanDefinitionRegistrar {

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

// 获取EnableDubboConfig注解信息
AnnotationAttributes attributes = AnnotationAttributes.fromMap(
importingClassMetadata.getAnnotationAttributes(EnableDubboConfig.class.getName()));

// multiple的配置值 multiple就是 dubbo.applications(复数)这种多个配置
boolean multiple = attributes.getBoolean("multiple");

// Single Config Bindings
// 注册Single配置Bean定义 内部主要是@EnableConfigurationBeanBindings注解再次import了具体配置bean定义的注册(ConfigurationBeanBindingsRegister)—
// ,并且注册了对应的后置处理器去解析具体配置值
registerBeans(registry, DubboConfigConfiguration.Single.class);

if (multiple) { // Since 2.6.6 https://github.com/apache/dubbo/issues/3193
registerBeans(registry, DubboConfigConfiguration.Multiple.class);
}

// Since 2.7.6
registerCommonBeans(registry);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ConfigurationBeanBindingsRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {

private ConfigurableEnvironment environment;

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

AnnotationAttributes attributes = AnnotationAttributes.fromMap(
importingClassMetadata.getAnnotationAttributes(EnableConfigurationBeanBindings.class.getName()));

AnnotationAttributes[] annotationAttributes = attributes.getAnnotationArray("value");

ConfigurationBeanBindingRegistrar registrar = new ConfigurationBeanBindingRegistrar();

registrar.setEnvironment(environment);

for (AnnotationAttributes element : annotationAttributes) {
// 对定义好的每个binding(其实就是properties文件中的属性)去注册对应的Bean定义 且注册一个bean后置处理器(ConfigurationBeanBindingPostProcessor)去绑定配置中的值(值来源于properties文件解析之后放入的environment中)
registrar.registerConfigurationBeanDefinitions(element, registry);
}
}

@DubboComponentScan

可以看到这个注解就是处理@Service和@Reference注解的扫描生成对应的Bean。
注解import了DubboComponentScanRegistrar这个注册器(ImportBeanDefinitionRegistrar的实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// DubboComponentScanRegistrar dubbo扫描@Service和@Reference注解 解析
public class DubboComponentScanRegistrar implements ImportBeanDefinitionRegistrar {

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

// 获取要扫描的包路径
Set<String> packagesToScan = getPackagesToScan(importingClassMetadata);

// 注册路径下处理@Service注解的Bean后置处理器
registerServiceAnnotationBeanPostProcessor(packagesToScan, registry);

// @since 2.7.6 Register the common beans
registerCommonBeans(registry);
}

可以看到注册了ServiceAnnotationBeanPostProcessor和ReferenceServiceAnnotationBeanPostProcessor两个bean的后置处理器。

ServiceAnnotationBeanPostProcessor

这个后置处理器是实现的Spring的扩展点:BeanDefinitionRegistryPostProcessor 。 会调用其postProcessBeanDefinitionRegistry方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//此方法中处理 dubbo @Service注解
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

// @since 2.7.5
// 注册DubboBootstrapApplicationListener 监听事件之后启动Dubbo
registerBeans(registry, DubboBootstrapApplicationListener.class);

// 注解上解析出需要扫描的路径
Set<String> resolvedPackagesToScan = resolvePackagesToScan(packagesToScan);

if (!CollectionUtils.isEmpty(resolvedPackagesToScan)) {
// 去注册ServiceBean
registerServiceBeans(resolvedPackagesToScan, registry);
} else {
if (logger.isWarnEnabled()) {
logger.warn("packagesToScan is empty , ServiceBean registry will be ignored!");
}
}

}

注册ServiceBean的方法registerServiceBeans如下:

    1. 扫描DubboClass路径,将实现类本身的Bean注册到容器中。
    1. 为@Service注解标注的Dubbo服务实现类去往容器中注册一个ServiceBean。
    1. Bean生成之后,监听上下文刷新事件来触发ServiceBean去真正的导出服务。
      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
      /**
      * Registers Beans whose classes was annotated {@link Service}
      *
      * @param packagesToScan The base packages to scan
      * @param registry {@link BeanDefinitionRegistry}
      */
      private void registerServiceBeans(Set<String> packagesToScan, BeanDefinitionRegistry registry) {

      // 创建一个dubbo类路径扫描器 去扫描
      DubboClassPathBeanDefinitionScanner scanner =
      new DubboClassPathBeanDefinitionScanner(registry, environment, resourceLoader);

      BeanNameGenerator beanNameGenerator = resolveBeanNameGenerator(registry);

      scanner.setBeanNameGenerator(beanNameGenerator);

      // refactor @since 2.7.7
      serviceAnnotationTypes.forEach(annotationType -> {
      // 要扫描的@Service注解 兼容版本
      scanner.addIncludeFilter(new AnnotationTypeFilter(annotationType));
      });

      for (String packageToScan : packagesToScan) {

      // Registers @Service Bean first
      // scan去扫描路径下的 @Service注解Bean
      // 1. 先注册类本身的bean到Spring容器中 scanner内部扫描到会注册bean定义
      scanner.scan(packageToScan);

      // Finds all BeanDefinitionHolders of @Service whether @ComponentScan scans or not.
      Set<BeanDefinitionHolder> beanDefinitionHolders =
      findServiceBeanDefinitionHolders(scanner, packageToScan, registry, beanNameGenerator);

      if (!CollectionUtils.isEmpty(beanDefinitionHolders)) {

      for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
      // 2. 再去去注册每个Dubbo服务接口的ServiceBean 内部会构建ServiceBean定义并注册在Spring容器中
      registerServiceBean(beanDefinitionHolder, registry, scanner);
      }

      if (logger.isInfoEnabled()) {
      logger.info(beanDefinitionHolders.size() + " annotated Dubbo's @Service Components { " +
      beanDefinitionHolders +
      " } were scanned under package[" + packageToScan + "]");
      }

      } else {

      if (logger.isWarnEnabled()) {
      logger.warn("No Spring Bean annotating Dubbo's @Service was found under package["
      + packageToScan + "]");
      }

      }

      }

      }

ReferenceServiceAnnotationBeanPostProcessor

ReferenceServiceAnnotationBeanPostProcessor是继承的Spring的扩展点后置处理器InstantiationAwareBeanPostProcessorAdapter,实现了其postProcessPropertyValues方法来为@Reference注解标注的字段来注入对应的ReferenceBean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public PropertyValues postProcessPropertyValues(
PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException {

InjectionMetadata metadata = findInjectionMetadata(beanName, bean.getClass(), pvs);
try {
// 也是借助metadata.inject来实现的注入字段
metadata.inject(bean, beanName, pvs);
} catch (BeanCreationException ex) {
throw ex;
} catch (Throwable ex) {
throw new BeanCreationException(beanName, "Injection of @" + getAnnotationType().getSimpleName()
+ " dependencies is failed", ex);
}
return pvs;
}

这里inject会走到AnnotatedMethodElement.inject方法,内部会调用子类实现的doGetInjectedBean方法。Dubbo在ReferenceAnnotationBeanPostProcessor中实现了这个方法doGetInjectedBean。

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
@Override
protected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable {

Class<?> injectedType = pd.getPropertyType();

Object injectedObject = getInjectedObject(attributes, bean, beanName, injectedType, this);

ReflectionUtils.makeAccessible(method);

method.invoke(bean, injectedObject);

}

// getInjectedObject方法
protected Object getInjectedObject(AnnotationAttributes attributes, Object bean, String beanName, Class<?> injectedType,
InjectionMetadata.InjectedElement injectedElement) throws Exception {

String cacheKey = buildInjectedObjectCacheKey(attributes, bean, beanName, injectedType, injectedElement);

Object injectedObject = injectedObjectsCache.get(cacheKey);

if (injectedObject == null) {
// doGetInjectedBean 方法 dubbo实现了ReferenceBean注入的关键
injectedObject = doGetInjectedBean(attributes, bean, beanName, injectedType, injectedElement);
// Customized inject-object if necessary
injectedObjectsCache.putIfAbsent(cacheKey, injectedObject);
}

return injectedObject;

}

doGetInjectedBean方法:

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
/**
* 实现的doGetInjectedBean方法来解析@DubboReference字段 来注入dubbo服务(最终是ReferenceBean的代理对象)
* @param attributes
* @param bean
* @param beanName
* @param injectedType
* @param injectedElement
* @return
* @throws Exception
*/
@Override
protected Object doGetInjectedBean(AnnotationAttributes attributes, Object bean, String beanName, Class<?> injectedType,
InjectionMetadata.InjectedElement injectedElement) throws Exception {
/**
* The name of bean that annotated Dubbo's {@link Service @Service} in local Spring {@link ApplicationContext}
* 先在本地的Spring上下文中查找ServiceBean是否存在
* ServiceBean: com.xxx.DemoService:group:version
*/
String referencedBeanName = buildReferencedBeanName(attributes, injectedType);

/**
* The name of bean that is declared by {@link Reference @Reference} annotation injection
* 根据@Reference注解的属性和注入的类属性 来生成referenceBean的名称
*/
String referenceBeanName = getReferenceBeanName(attributes, injectedType);

// 从缓存中取ReferenceBean 如果不存在 会去创建(内部的配置也会注入)
ReferenceBean referenceBean = buildReferenceBeanIfAbsent(referenceBeanName, attributes, injectedType);

// 是否是本地的bean 上面的ServiceBeanName来在容器中寻找
boolean localServiceBean = isLocalServiceBean(referencedBeanName, referenceBean, attributes);

// 容器中注册referenceBean
registerReferenceBean(referencedBeanName, referenceBean, attributes, localServiceBean, injectedType);

cacheInjectedReferenceBean(referenceBean, injectedElement);

// 最终返回注入给@Reference变量是 ReferenceBean的代理对象 是个FactoryBean 代理逻辑在其get()方法中(远程bean)
return getOrCreateProxy(referencedBeanName, referenceBean, localServiceBean, injectedType);
}

因为客户端去引用Dubbo远程服务bean时要屏蔽一些细节,让引用的dubbo bean具有调用远程接口的能力,所以这里为ReferenceBean去生成代理,走的是getOrCreateProxy方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
private Object getOrCreateProxy(String referencedBeanName, ReferenceBean referenceBean, boolean localServiceBean,
Class<?> serviceInterfaceType) {
if (localServiceBean) { // If the local @Service Bean exists, build a proxy of Service
// 本地的dubbo服务 创建jdk动态代理 内部直接调用ServiceBean的ref的方法
return newProxyInstance(getClassLoader(), new Class[]{serviceInterfaceType},
newReferencedBeanInvocationHandler(referencedBeanName));
} else {
// 如果需要export 帮助依赖的ServiceBean 去 export
exportServiceBeanIfNecessary(referencedBeanName); // If the referenced ServiceBean exits, export it immediately
// get方法创建代理对象
return referenceBean.get();
}
}

这里是直接调用referenceBean.get()方法去生成的代理对象。

在Spring容器中存放的是ReferenceBean对象,但本身这个Bean也是实现FactoryBean接口的,所以会调用其getObject()方法来取出真正的代理之后的bean。其实getObejct方法也是调用自身的get()方法来走代理逻辑的:

1
2
3
4
5
6
7
getObject()方法:

@Override
public Object getObject() {
// 调用get生成代理对象
return get();
}

orika线程死循环记录

发表于 2020-05-24 | 分类于 bug记录 | 热度: ℃
字数统计: 411 | 阅读时长 ≈ 2

问题现象

在测试环境看到机器cpu报警,且cpu是突然升起来并且一直稳定跑满在百分之90左右。观察流量和接口的qps,并没有突然增加或者有突刺。

问题排查

上机器top -H -p pid + jstack观察之后发现很多http线程卡在orika的一个weakHashMap的get方法中:

image-20200524232028707

image-20200524232121915

很明显,这里是触发了经常看到HashMap一类分析文章中的map链表成环并且死循环的问题,然后就去查看了orika中的这个类,代码维护了一个全局的weakHashMap:

1
2
// 定义了一个WeakHashMap
private static volatile WeakHashMap<java.lang.reflect.Type, Integer> knownTypes = new WeakHashMap<java.lang.reflect.Type, Integer>();

这里有点不明白的是使用的是java1.8,记得代码中是更改了扩容时候链表的迁移方式,避免了成环操作,然后就去看了WeakHashMap的操作,发现原来WeakHashMap并没有去树化和改变迁移链表的方式,还是可能出现成环,然后在get的时候死循环导致cpu异常。

其实在WeakHashMap的注释中也看到了对应不同步的说法:

1
2
3
4
* <p> Like most collection classes, this class is not synchronized.
* A synchronized <tt>WeakHashMap</tt> may be constructed using the
* {@link Collections#synchronizedMap Collections.synchronizedMap}
* method.

问题解决

这里尝试去找了网上的文章和orika的issue,发现有遇到同样问题的文章和issue:

参考blog)

官方issue

这里都提示了在高版本解决了这个问题。我这里使用的是orika1.5.1版本的包,升级为最新的1.5.4之后,再看这个weakHashMap加上了同步方法:

GC日志和G1垃圾回收器

发表于 2020-05-24 | 分类于 Java基础 , JVM | 热度: ℃
字数统计: 3,355 | 阅读时长 ≈ 13

GC日志

输出的GC日志是分析线上问题的很关键的点,要能看懂不同收集器下对应的垃圾回收日志,比如CMS回收器的每次GC的每个阶段都在GC日志里详细标出。

对于应用程序可以配置jvm参数:

1
2
-Xloggc:./gc-%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M

其中:

  • -Xloggc:./gc-%t.log:日志文件存放的位置,./是当前位置
  • -XX:PrintGCDetails:打印GC日志详细信息
  • -XX:PrintGCDateStamps:打印GC发生日期戳
  • -XX:PrintGCTimeStamps:打印GC发生时间戳
  • -XX:PrintGCCause:打印GC原因
  • -XX:UseGCLogFileRotation:滚动日志记录
  • -XX:NumberOfGCLogFiles=10:分为10个日志文件记录
  • -XX:GCLogFileSize=100m:每个日志文件100兆大小

GC日志关于内存展示:
num1 -> num2(num3)。其中:

  • num1:GC之前此区域的使用大小
  • num2:GC之后此区域的使用大小
  • num3:此区域的总容量

Parallel收集器的GC日志

Parallel年轻代的收集器是Parallel Scavenge,老年代的垃圾收集器是Parallel Old。

MinorGC

1
2
3
4
5
6
2020-07-10T17:45:40.296+0800: 1.285:
[GC (Allocation Failure)
[PSYoungGen: 49152K->3892K(57344K)]
49152K->3900K(188416K),
0.0080059 secs]
[Times: user=0.03 sys=0.00, real=0.01 secs]
  • GC (Allocation Failure):代表一次MinorGC。Eden区分配内存失败引起的。
  • PSYoungGen:年轻代,收集器是Parallel Scavenge
  • 49152K->3892K(57344K):年轻代GC之前使用大小49152K,GC之后使用3892K,年轻代总内存57344K。
  • 49152K->3900K(188416K):堆内存GC之前使用大小49152K,GC之后3900K使用大小,总共188416K。
  • 0.0080059 secs:总共耗时
  • Times: user=0.03 sys=0.00, real=0.01 secs:分别代表用户态消耗的CPU、内核态消耗的CPU时间、real是操作从开始到结束经理的墙钟时间。墙钟时间和CPU时间区别是:墙钟时间包含各种非运行的等待耗时,比如等待IO、等待磁盘、等待线程阻塞等,而CPU时间不包含这些,现在是多CPU或者多核机器,user+sys > real是很正常的。

FullGC

1
2
3
4
5
6
7
8
2020-07-10T17:45:44.450+0800: 5.439: 
[Full GC (Metadata GC Threshold)
[PSYoungGen: 4286K->0K(201216K)]
[ParOldGen: 3289K->7277K(74752K)]
7575K->7277K(275968K),
[Metaspace: 20854K->20854K(1069056K)],
0.0542303 secs]
[Times: user=0.11 sys=0.00, real=0.06 secs]
  • Full GC (Metadata GC Threshold):因为元空间不足引起的FullGC
  • PSYoungGen: 4286K->0K(201216K。):年轻代GC前占用4286K,GC后占用0K,总共201216K。
  • ParOldGen: 3289K->7277K(74752K):老年代使用Parallel Old垃圾收集器,GC前占用3289K,GC后占用7277K,总共大小74752K。
  • MetaSpace: 20854K->20854K(1069056K):不赘述和上面一样。

CMS垃圾回收器的GC日志

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
# 1.CMS初始标记 标记GCRoots直接关联的对象 会STW 第一个内存是老年代 第二个内存数据是堆内存
0.245: [GC (CMS Initial Mark) [1 CMS-initial-mark: 32776K(53248K)] 41701K(99328K), 0.0061676 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]


# 2. CMS并发标记,进行GCRoots分析,标记存活对象,不会STW,用户线程和GC线程并发,且多个GC线程并行去标记
0.251: [CMS-concurrent-mark-start]
0.270: [CMS-concurrent-mark: 0.004/0.020 secs] [Times: user=0.08 sys=0.01, real=0.02 secs]


# 3. CMS在重新标记之前会预清理
# preClean:清理 card marking 标记的 dirty card,更新引用记录。方便后边并发扫描的时候去扫描跨代引用。
# abortable-preclean:调节final-remark阶段的时机,这个阶段不一定存在
0.270: [CMS-concurrent-preclean-start]
0.272: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.272: [CMS-concurrent-abortable-preclean-start]
0.291: [CMS-concurrent-abortable-preclean: 0.004/0.019 secs] [Times: user=0.09 sys=0.00, real=0.02 secs]

# 4. CMS的最终重新标记 会STW 可以多线程并行去标记 会STW,修正之前并发执行用户线程带来的浮动对象或者引用的错误。
0.291: [GC (CMS Final Remark) [YG occupancy: 17928 K (46080 K)]0.291: [Rescan (parallel) , 0.0082702 secs]0.299: [weak refs processing, 0.0000475 secs]0.299: [class unloading, 0.0002451 secs]0.299: [scrub symbol table, 0.0003183 secs]0.300: [scrub string table, 0.0001611 secs][1 CMS-remark: 49164K(53248K)] 67093K(99328K), 0.0091462 secs] [Times: user=0.04 sys=0.00, real=0.01 secs]

# 5. CMS的并发清理 进行GC回收垃圾 多线程执行
0.300: [CMS-concurrent-sweep-start]
0.300: [CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

# 6.CMS的并发重置 清除对象的标记(比如对象的三色标记),为下次GC做准备
0.300: [CMS-concurrent-reset-start]
0.300: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

CMS在资料里主要是四个阶段:初始标记、并发标记、最终重新标记、并发清理。在GC日志中会比较细和多了预清理、并发重置的过程。

CMS的concurrent mode failure

CMS在并发标记或者并发清理阶段(不会STW)中,可能因为之前浮动垃圾的产生,或者老年代担保机制失败,或者启动CMS垃圾回收的阈值太大导致剩余空间不够用等原因,可能又会触发了FullGC,那么会出现Concurrent mode failure,此时CMS会退化为单线程的Serial Old垃圾回收器。

1
2
3
4
5
6
7
8
# CMS并发标记开始
2022-05-23T16:34:29.502-0800: 0.687: [CMS-concurrent-mark-start]

# FullGC(可能是浮动垃圾+用户线程触发)
2022-05-23T16:34:29.503-0800: 0.688: [Full GC (Allocation Failure) 2022-05-23T16:34:29.503-0800: 0.688: [CMS2022-05-23T16:34:29.529-0800: 0.714: [CMS-concurrent-mark: 0.026/0.027 secs] [Times: user=0.04 sys=0.00, real=0.02 secs]

# concurrent mode failure 退化为SerialOld。
(concurrent mode failure): 194559K->194559K(194560K), 0.1069558 secs] 203774K->203771K(203776K), [Metaspace: 3306K->3306K(1056768K)], 0.1070098 secs] [Times: user=0.12 sys=0.00, real=0.11 secs]

可以看到在并发标记开始之后,触发了一次FullGC(用户线程没有STW),此时FullGC之后会有concurrent mode failure,CMS退化为Serial Old。

G1垃圾回收器

深入理解JVM中的知识点

G1(Garbage-First)收集器也是追求很低的停顿垃圾回收时间的收集器,在高版本的jdk中建议使用。

在G1之前垃圾收集器都是收集单独的年轻代或者老年代对象,而G1收集器将整个Java堆空间分为多个大小相等的Region,虽然还在概念上保留着新生代和老年代,但不再是物理隔离划分的Region,都是一部分Region(不需要连续)的集合。

G1垃圾回收器之所以能建立可预测的停顿时间模型,是因为其可有计划的在Java堆中进行全区域的垃圾收集。各个Region定义了一个垃垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要时间的经验值),G1在后台维护了一个优先列表, 每个根据允许的回收时间,来选择优先回收价值最大的Region(这解释了为什么叫做Garbage First)。这种使用了Region划分内存的方式和具有优先级回收的方式,保证了G1在有限时间内尽可能获取到最高的回收效率。

回想之前垃圾回收器的跨代引用,如果回收新生代时也要扫描老年代的引用,那么MinorGC的效率会很低。G1也存在这个问题,每个Region不可能是单独独立的,可能存在Region之间的相互引用,如果要扫描整个堆空间,那效率是不可接受的。和跨代引用一样,Region之间的对象引用扫描效率问题都是使用RememberdSet来避免扫描全部区域的。G1每个Region都有一个对应的RememberedSet,虚拟机在发现对Reference类型的对象赋值操作时,会通过内部写屏障来实现将跨Region的引用信息维护被引用对象的RememberedSet中,所以在GCRoots扫描过程中只扫描存在跨Region的区域即可,不需要扫描整个堆,提高了扫描效率。

G1垃圾回收的过程

  • 初始标记(Inital Marking):仅仅是标记一下GCRoots能直接关联的对象,这部分只扫描直接关联的对象,很快,同时此阶段需要STW。
  • 并发标记(Concurrent Marking):此阶段是从GCRoots开始对堆中对象进行可达性分析,标记连通还能存活的对象。和CMS一样,这个阶段是主要的耗时阶段,但不会STW,即可以和用户线程并发的执行。
  • 最终标记(Final Marking):修正在并发标记阶段因用户线程继续运行而导致标记有误的对象,这阶段会STW,可以并行标记,且时间也不会很长。
  • 筛选回收(Live Data Counting and Evacuation):筛选回收阶段首先对每个Region按照回收的价值和成本进行排序(G1自己维护的优先级列表),根据所期望的GC停顿时间计划来回收。注意这部分会STW,且GC线程可以并行执行。

image-20220701104805908

G1的优点

  • 并发和并行:G1能在多核环境下使用多个CPU来加快STW的时间(并行),而且在一些耗时阶段是可以也可以和用户线程一起运行,并STW用户线程(并发)。
  • 分代收集:G1的分代收集还是得以保留。G1可以不搭配其他垃圾收集器配合就能独立管理整个堆,但其保留了分代思想,对新创建的对象、已经存在一段时间、熬过很多次GC的旧对象都采用了不同的方式
  • 空间整合:G1从整体看是采用的标记-整理算法实现。但是G1从两个Region来看是基于复制算法实现的。空间整合代表G1在GC之后不会产生垃圾碎片,GC之后都是规整的内存。
  • 可预测的停顿:这是G1相比CMS垃圾收集器最大的优势。G1除了和CMS一样追求低停顿外,还建立了可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,停留在垃圾收集上的时间不超过N毫秒。

G1的垃圾回收分类

G1的mixedGC就是把老年代(逻辑分区)的一部分区域加载Eden和Surivor区域的后面,所有要回收的区域叫做Collection Set(CSet),最后用年轻代的算法进行回收。

这里也能看出逻辑分代的好处,没有真正的物理划分,而是通过划分更细粒度的Region来实现的mixedGC要回收的区域。

在执行mixedGC时,其实就是筛选回收的过程,会回收young region和部分old Region还有大对象Region,这其中对回收对象进行了价值排序,根据用户设置的期望停顿时间来优先回收价值高的对象,即G1的可预测的停顿模型。

相关问题

  1. G1的特点是什么
  • 并行和并发
  • 分代收集(逻辑分代),内存划分为Region
  • 垃圾回收分类为youngGC、mixedGC、fullGC
  • 整体来看采用标记-整理算法,不会有空间碎片产生,region之间采用复制算法清除(年轻代的younggc和mixedgc)
  • 最显著的特点:可预测的停顿模型。可以按照用户设置的在GC上停顿的时间来执行价值排序优先级的回收,实现追求最低停顿的GC时间。
  • 适用大内存服务器,来设置可预测的GC STW时间
  1. G1和CMS的区别
  • G1对整个内存区域回收、CMS是老年代的回收器,需要搭配ParNew等年轻代垃圾回收器一起使用
  • CMS是标记-清除算法,G1是标记-整理算法
  • 增量阶段的处理,CMS采用增量更新,G1采用的是satb(快照)
  • G1分代但是逻辑分代。
  • G1最大的特点:提供了用户可设置的GC停顿时间,提供了可预测的停顿模型,适用于大内存。
  1. G1如何控制暂停时间
    • 对young gc控制新生代Region的大小,会根据设置的-XX:MaxGCPauseMills值来调整,来减少触发YoungGC的次数。
    • 对mixed gc控制回收region的个数,根据参数-XX:InitiatingHeapOccupancyPercent值触犯,回收所有的Young区、部分Old区(根据期望的GC停顿时间对old区垃圾收集排序选出)、和大对象区,采用复制算法。
    • fullgc:会暂停应用程序,退化为单线程去标记、清除和压缩整理。

大内存系统为什么适合使用G1

类似于Kafka这种支撑高并发系统,会部署很大内存的机器,比如年轻代可能都会很大,这种情况下因为eden区的回收可能变得很慢(相对于常见4G的机器的eden区),那young gc带来的stw对于系统来说不能接收。那么在这种情况下G1可以设置期望的最大GC暂停时间,-XX:MaxGCPauseMills,整个系统在GC上的时间大幅降低,可以最大限度的一遍用户线程在跑,一遍去GC。G1天生适合大内存机器来对JVM进行垃圾回收,可以解决大内存机器垃圾回收时间过长造成停顿的问题。

1…345…12
夸克

夸克

愿赌服输

114 日志
32 分类
121 标签
GitHub E-Mail csdn
© 2022 夸克 | Site words total count: 168.9k
|
主题 — NexT.Muse v5.1.4
博客全站共168.9k字

载入天数...载入时分秒...