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

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


  • 首页

  • 归档

  • 搜索

jvm的GC原理及调优 GC 基础原理 CMS 原理及调优

发表于 2021-11-26

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

GC 基础原理

本文介绍 GC 基础原理和理论,GC 调优方法思路和方法,基于 Hotspot jdk1.8,学习之后你将了解如何对生产系统出现的 GC 问题进行排查解决。

GC 调优目标

大多数情况下对 Java 程序进行 GC 调优,主要关注两个目标:

  1. 响应速度(Responsiveness):响应速度指程序或系统对一个请求的响应有多迅速。

比如,用户订单查询响应时间,对响应速度要求很高的系统,较大的停顿时间是不可接受的。调优的重点是在短的时间内快速响应。

  1. 吞吐量(Throughput):吞吐量关注在一个特定时间段内应用系统的最大工作量。

例如每小时批处理系统能完成的任务数量,在吞吐量方面优化的系统,较长的 GC 停顿时间也是可以接受的,因为高吞吐量应用更关心的是如何尽可能快地完成整个任务,不考虑快速响应用户请求。

GC 调优中,GC 导致的应用暂停时间影响系统响应速度,GC 处理线程的 CPU 使用率影响系统吞吐量。

GC 分代收集算法

现代的垃圾收集器基本都是采用分代收集算法,其主要思想: 将 Java 的堆内存逻辑上分成两块:新生代、老年代,针对不同存活周期、不同大小的对象采取不同的垃圾回收策略。
在这里插入图片描述

新生代(Young Generation)

新生代又叫年轻代,大多数对象在新生代中被创建,很多对象的生命周期很短。

每次新生代的垃圾回收(又称 Young GC、Minor GC、YGC)后只有少量对象存活,所以使用复制算法,只需少量的复制操作成本就可以完成回收。

新生代内又分三个区:一个 Eden 区,两个 Survivor 区(S0、S1,又称From Survivor、To Survivor),大部分对象在 Eden 区中生成。

当 Eden 区满时,还存活的对象将被复制到两个 Survivor 区(中的一个);当这个 Survivor 区满时,此区的存活且不满足晋升到老年代条件的对象将被复制到另外一个 Survivor 区。

对象每经历一次复制,年龄加 1,达到晋升年龄阈值后,转移到老年代。

老年代(Old Generation)

在新生代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到老年代,该区域中对象存活率高。老年代的垃圾回收通常使用“标记-整理”算法。

GC 事件分类

根据垃圾收集回收的区域不同,垃圾收集主要分为:

  • Young GC
  • Old GC
  • Full GC
  • Mixed GC

①Young GC

新生代内存的垃圾收集事件称为 Young GC(又称 Minor GC),当 JVM 无法为新对象分配在新生代内存空间时总会触发 Young GC。

比如 Eden 区占满时,新对象分配频率越高,Young GC 的频率就越高。

Young GC 每次都会引起全线停顿(Stop-The-World),暂停所有的应用线程,停顿时间相对老年代 GC 造成的停顿,几乎可以忽略不计。

②Old GC 、Full GC、Mixed GC

Old GC: 只清理老年代空间的 GC 事件,只有 CMS 的并发收集是这个模式。

Full GC: 清理整个堆的 GC 事件,包括新生代、老年代、元空间等 。

Mixed GC: 清理整个新生代以及部分老年代的 GC,只有 G1 有这个模式。

GC 日志分析

GC 日志是一个很重要的工具,它准确记录了每一次的 GC 的执行时间和执行结果,通过分析 GC 日志可以调优堆设置和 GC 设置,或者改进应用程序的对象分配模式。

开启的 JVM 启动参数如下:

1
java复制代码-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps  -XX:+PrintGCTimeStamps

常见的 Young GC、Full GC 日志含义如下:

在这里插入图片描述
在这里插入图片描述

免费的 GC 日志图形分析工具推荐下面 2 个:

  • GCViewer,下载 jar 包直接运行 。
  • gceasy,Web 工具,上传 GC 日志在线使用。

内存分配策略

Java 提供的自动内存管理,可以归结为解决了对象的内存分配和回收的问题。

前面已经介绍了内存回收,下面介绍几条最普遍的内存分配策略:

①对象优先在 Eden 区分配: 大多数情况下,对象在先新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Young GC。

②大对象之间进入老年代: JVM 提供了一个对象大小阈值参数(-XX:PretenureSizeThreshold,默认值为 0,代表不管多大都是先在 Eden 中分配内存)。

大于参数设置的阈值值的对象直接在老年代分配,这样可以避免对象在 Eden 及两个 Survivor 直接发生大内存复制。

③长期存活的对象将进入老年代:对象每经历一次垃圾回收,且没被回收掉,它的年龄就增加 1,大于年龄阈值参数(-XX:MaxTenuringThreshold,默认 15)的对象,将晋升到老年代中。

④空间分配担保:当进行 Young GC 之前,JVM 需要预估:老年代是否能够容纳 Young GC 后新生代晋升到老年代的存活对象,以确定是否需要提前触发 GC 回收老年代空间,基于空间分配担保策略来计算。

continueSize,老年代最大可用连续空间:
在这里插入图片描述

Young GC 之后如果成功(Young GC 后晋升对象能放入老年代),则代表担保成功,不用再进行 Full GC,提高性能。

如果失败,则会出现“promotion failed”错误,代表担保失败,需要进行 Full GC。

⑤动态年龄判定:新生代对象的年龄可能没达到阈值(MaxTenuringThreshold 参数指定)就晋升老年代。

如果 Young GC 之后,新生代存活对象达到相同年龄所有对象大小的总和大于任意 Survivor 空间(S0+S1空间)的一半,此时 S0 或者 S1 区即将容纳不了存活的新生代对象。

年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。

另外,如果 Young GC 后 S0 或 S1 区不足以容纳:未达到晋升老年代条件的新生代存活对象,会导致这些存活对象直接进入老年代,需要尽量避免。

CMS 原理及调优

名词解释

可达性分析算法:用于判断对象是否存活,基本思想是通过一系列称为“GC Root”的对象作为起点(常见的 GC Root 有系统类加载器、栈中的对象、处于激活状态的线程等),基于对象引用关系,从 GC Roots 开始向下搜索,所走过的路径称为引用链,当一个对象到 GC Root 没有任何引用链相连,证明对象不再存活。

Stop The World:GC 过程中分析对象引用关系,为了保证分析结果的准确性,需要通过停顿所有 Java 执行线程,保证引用关系不再动态变化,该停顿事件称为 Stop The World(STW)。

Safepoint:代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的,如果有需要 GC,线程可以在这个位置暂停。

HotSpot 采用主动中断的方式,让执行线程在运行期轮询是否需要暂停的标志,若需要则中断挂起。

CMS 简介

CMS(Concurrent Mark and Sweep 并发-标记-清除),是一款基于并发、使用标记清除算法的垃圾回收算法,只针对老年代进行垃圾回收。

CMS 收集器工作时,尽可能让 GC 线程和用户线程并发执行,以达到降低 STW 时间的目的。

通过以下命令行参数,启用 CMS 垃圾收集器:

1
java复制代码-XX:+UseConcMarkSweepGC

值得补充的是,下面介绍到的 CMS GC 是指老年代的 GC,而 Full GC 指的是整个堆的 GC 事件,包括新生代、老年代、元空间等,两者有所区分。

新生代垃圾回收

能与 CMS 搭配使用的新生代垃圾收集器有 Serial 收集器和 ParNew 收集器。

这 2 个收集器都采用标记复制算法,都会触发 STW 事件,停止所有的应用线程。不同之处在于,Serial 是单线程执行,ParNew 是多线程执行。
在这里插入图片描述

老年代垃圾回收

在这里插入图片描述

CMS GC 以获取最小停顿时间为目的,尽可能减少 STW 时间,可以分为 7 个阶段:

阶段 1:初始标记(Initial Mark)

此阶段的目标是标记老年代中所有存活的对象, 包括 GC Root 的直接引用, 以及由新生代中存活对象所引用的对象,触发第一次 STW 事件。

这个过程是支持多线程的(JDK7 之前单线程,JDK8 之后并行,可通过参数 CMSParallelInitialMarkEnabled 调整)。

阶段 2:并发标记(Concurrent Mark)

此阶段 GC 线程和应用线程并发执行,遍历阶段 1 初始标记出来的存活对象,然后继续递归标记这些对象可达的对象。

阶段 3:并发预清理(Concurrent Preclean)

此阶段 GC 线程和应用线程也是并发执行,因为阶段 2 是与应用线程并发执行,可能有些引用关系已经发生改变。

通过卡片标记(Card Marking),提前把老年代空间逻辑划分为相等大小的区域(Card)。

如果引用关系发生改变,JVM 会将发生改变的区域标记为“脏区”(Dirty Card),然后在本阶段,这些脏区会被找出来,刷新引用关系,清除“脏区”标记。

阶段 4:并发可取消的预清理(Concurrent Abortable Preclean)

此阶段也不停止应用线程。本阶段尝试在 STW 的最终标记阶段(Final Remark)之前尽可能地多做一些工作,以减少应用暂停时间。

在该阶段不断循环处理:标记老年代的可达对象、扫描处理 Dirty Card 区域中的对象,循环的终止条件有:

达到循环次数

达到循环执行时间阈值

新生代内存使用率达到阈值

阶段 5:最终标记(Final Remark)

这是 GC 事件中第二次(也是最后一次)STW 阶段,目标是完成老年代中所有存活对象的标记。

在此阶段执行:

遍历新生代对象,重新标记

根据 GC Roots,重新标记

遍历老年代的 Dirty Card,重新标记

阶段 6:并发清除(Concurrent Sweep)

此阶段与应用程序并发执行,不需要 STW 停顿,根据标记结果清除垃圾对象。

阶段 7:并发重置(Concurrent Reset)

此阶段与应用程序并发执行,重置 CMS 算法相关的内部数据, 为下一次 GC 循环做准备。

CMS 常见问题

①最终标记阶段停顿时间过长问题

CMS 的 GC 停顿时间约 80% 都在最终标记阶段(Final Remark),若该阶段停顿时间过长,常见原因是新生代对老年代的无效引用,在上一阶段的并发可取消预清理阶段中,执行阈值时间内未完成循环,来不及触发 Young GC,清理这些无效引用。

通过添加参数:-XX:+CMSScavengeBeforeRemark。

在执行最终操作之前先触发 Young GC,从而减少新生代对老年代的无效引用,降低最终标记阶段的停顿。

但如果在上个阶段(并发可取消的预清理)已触发 Young GC,也会重复触发 Young GC。

②并发模式失败(concurrent mode failure)&晋升失败(promotion failed)问题。

在这里插入图片描述

并发模式失败: 当 CMS 在执行回收时,新生代发生垃圾回收,同时老年代又没有足够的空间容纳晋升的对象时,CMS 垃圾回收就会退化成单线程的 Full GC。所有的应用线程都会被暂停,老年代中所有的无效对象都被回收。
在这里插入图片描述
晋升失败:当新生代发生垃圾回收,老年代有足够的空间可以容纳晋升的对象,但是由于空闲空间的碎片化,导致晋升失败,此时会触发单线程且带压缩动作的 Full GC。

并发模式失败和晋升失败都会导致长时间的停顿,常见解决思路如下:

  • 降低触发 CMS GC 的阈值。

即参数 -XX:CMSInitiatingOccupancyFraction 的值,让 CMS GC 尽早执行,以保证有足够的空间。

  • 增加 CMS 线程数,即参数 -XX:ConcGCThreads。
  • 增大老年代空间。
  • 让对象尽量在新生代回收,避免进入老年代。

③内存碎片问题

通常 CMS 的 GC 过程基于标记清除算法,不带压缩动作,导致越来越多的内存碎片需要压缩。

常见以下场景会触发内存碎片压缩:

  • 新生代 Young GC 出现新生代晋升担保失败(promotion failed))
  • 程序主动执行System.gc()

可通过参数 CMSFullGCsBeforeCompaction 的值,设置多少次 Full GC 触发一次压缩。

默认值为 0,代表每次进入 Full GC 都会触发压缩,带压缩动作的算法为上面提到的单线程 Serial Old 算法,暂停时间(STW)时间非常长,需要尽可能减少压缩时间。

G1 原理及调优

G1 简介

G1(Garbage-First)是一款面向服务器的垃圾收集器,支持新生代和老年代空间的垃圾收集,主要针对配备多核处理器及大容量内存的机器。

G1 最主要的设计目标是:实现可预期及可配置的 STW 停顿时间。

G1 堆空间划分
在这里插入图片描述

①Region

为实现大内存空间的低停顿时间的回收,将划分为多个大小相等的 Region。每个小堆区都可能是 Eden 区,Survivor 区或者 Old 区,但是在同一时刻只能属于某个代。

在逻辑上, 所有的 Eden 区和 Survivor 区合起来就是新生代,所有的 Old 区合起来就是老年代,且新生代和老年代各自的内存 Region 区域由 G1 自动控制,不断变动。

②巨型对象

当对象大小超过 Region 的一半,则认为是巨型对象(Humongous Object),直接被分配到老年代的巨型对象区(Humongous Regions)。

这些巨型区域是一个连续的区域集,每一个 Region 中最多有一个巨型对象,巨型对象可以占多个 Region。

==G1 把堆内存划分成一个个 Region 的意义在于==:

  • 每次 GC 不必都去处理整个堆空间,而是每次只处理一部分 Region,实现大容量内存的 GC。
  • 通过计算每个 Region 的回收价值,包括回收所需时间、可回收空间,在有限时间内尽可能回收更多的垃圾对象,把垃圾回收造成的停顿时间控制在预期配置的时间范围内,这也是 G1 名称的由来:Garbage-First。

G1工作模式

针对新生代和老年代,G1 提供 2 种 GC 模式,Young GC 和 Mixed GC,两种会导致 Stop The World。

Young GC:当新生代的空间不足时,G1 触发 Young GC 回收新生代空间。

Young GC 主要是对 Eden 区进行 GC,它在 Eden 空间耗尽时触发,基于分代回收思想和复制算法,每次 Young GC 都会选定所有新生代的 Region。

同时计算下次 Young GC 所需的 Eden 区和 Survivor 区的空间,动态调整新生代所占 Region 个数来控制 Young GC 开销。

Mixed GC:当老年代空间达到阈值会触发 Mixed GC,选定所有新生代里的 Region,根据全局并发标记阶段(下面介绍到)统计得出收集收益高的若干老年代 Region。

在用户指定的开销目标范围内,尽可能选择收益高的老年代 Region 进行 GC,通过选择哪些老年代 Region 和选择多少 Region 来控制 Mixed GC 开销。

全局并发标记

在这里插入图片描述

全局并发标记主要是为 Mixed GC 计算找出回收收益较高的 Region 区域,具体分为 5 个阶段:

阶段 1:初始标记(Initial Mark)

暂停所有应用线程(STW),并发地进行标记从 GC Root 开始直接可达的对象(原生栈对象、全局对象、JNI 对象)。

当达到触发条件时,G1 并不会立即发起并发标记周期,而是等待下一次新生代收集,利用新生代收集的 STW 时间段,完成初始标记,这种方式称为借道(Piggybacking)。

阶段 2:根区域扫描(Root Region Scan)

在初始标记暂停结束后,新生代收集也完成的对象复制到 Survivor 的工作,应用线程开始活跃起来。

此时为了保证标记算法的正确性,所有新复制到 Survivor 分区的对象,需要找出哪些对象存在对老年代对象的引用,把这些对象标记成根(Root)。

这个过程称为根分区扫描(Root Region Scanning),同时扫描的 Suvivor 分区也被称为根分区(Root Region)。

根分区扫描必须在下一次新生代垃圾收集启动前完成(接下来并发标记的过程中,可能会被若干次新生代垃圾收集打断),因为每次 GC 会产生新的存活对象集合。

阶段 3:并发标记(Concurrent Marking)

标记线程与应用程序线程并行执行,标记各个堆中 Region 的存活对象信息,这个步骤可能被新的 Young GC 打断。

所有的标记任务必须在堆满前就完成扫描,如果并发标记耗时很长,那么有可能在并发标记过程中,又经历了几次新生代收集。

阶段 4:再次标记(Remark)

和 CMS 类似暂停所有应用线程(STW),以完成标记过程短暂地停止应用线程, 标记在并发标记阶段发生变化的对象,和所有未被标记的存活对象,同时完成存活数据计算。

阶段 5:清理(Cleanup)

为即将到来的转移阶段做准备, 此阶段也为下一次标记执行所有必需的整理计算工作:

整理更新每个 Region 各自的 RSet(Remember Set,HashMap 结构,记录有哪些老年代对象指向本 Region,key 为指向本 Region 的对象的引用,value 为指向本 Region 的具体 Card 区域,通过 RSet 可以确定 Region 中对象存活信息,避免全堆扫描)。

回收不包含存活对象的 Region。

统计计算回收收益高(基于释放空间和暂停目标)的老年代分区集合。

G1调优注意点

①Full GC 问题

G1 的正常处理流程中没有 Full GC,只有在垃圾回收处理不过来(或者主动触发)时才会出现,G1 的 Full GC 就是单线程执行的 Serial old gc,会导致非常长的 STW,是调优的重点,需要尽量避免 Full GC。

常见原因如下:

  • 程序主动执行 System.gc()
  • 全局并发标记期间老年代空间被填满(并发模式失败)
  • Mixed GC 期间老年代空间被填满(晋升失败)
  • Young GC 时 Survivor 空间和老年代没有足够空间容纳存活对象

类似 CMS,常见的解决是:

增大 -XX:ConcGCThreads=n 选项增加并发标记线程的数量,或者 STW 期间并行线程的数量:-XX:ParallelGCThreads=n。

减小 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。

增大预留内存 -XX:G1ReservePercent=n,默认值是 10,代表使用 10% 的堆内存为预留内存,当 Survivor 区域没有足够空间容纳新晋升对象时会尝试使用预留内存。

②巨型对象分配

巨型对象区中的每个 Region 中包含一个巨型对象,剩余空间不再利用,导致空间碎片化,当 G1 没有合适空间分配巨型对象时,G1 会启动串行 Full GC 来释放空间。

可以通过增加 -XX:G1HeapRegionSize 来增大 Region 大小,这样一来,相当一部分的巨型对象就不再是巨型对象了,而是采用普通的分配方式。

③不要设置 Young 区的大小

原因是为了尽量满足目标停顿时间,逻辑上的 Young 区会进行动态调整。如果设置了大小,则会覆盖掉并且会禁用掉对停顿时间的控制。

④平均响应时间设置

使用应用的平均响应时间作为参考来设置 MaxGCPauseMillis,JVM 会尽量去满足该条件,可能是 90% 的请求或者更多的响应时间在这之内, 但是并不代表是所有的请求都能满足,平均响应时间设置过小会导致频繁 GC。

调优方法与思路

如何分析系统 JVM GC 运行状况及合理优化?

GC 优化的核心思路在于:尽可能让对象在新生代中分配和回收,尽量避免过多对象进入老年代,导致对老年代频繁进行垃圾回收,同时给系统足够的内存减少新生代垃圾回收次数,进行系统分析和优化也是围绕着这个思路展开。

分析系统的运行状况

分析系统的运行状况:

  • 系统每秒请求数、每个请求创建多少对象,占用多少内存。
  • Young GC 触发频率、对象进入老年代的速率。
  • 老年代占用内存、Full GC 触发频率、Full GC 触发的原因、长时间 Full GC 的原因。

常用工具如下:

jstat:JVM 自带命令行工具,可用于统计内存分配速率、GC 次数,GC 耗时。

常用命令格式:

1
java复制代码jstat -gc <pid> <统计间隔时间>  <统计次数>

输出返回值代表含义如下:
在这里插入图片描述
例如:jstat -gc 32683 1000 10,统计 pid=32683 的进程,每秒统计 1 次,统计 10 次。

jmap:JVM 自带命令行工具,可用于了解系统运行时的对象分布。

常用命令格式如下:

1
2
3
4
5
6
7
java复制代码// 命令行输出类名、类数量数量,类占用内存大小,
// 按照类占用内存大小降序排列
jmap -histo <pid>

// 生成堆内存转储快照,在当前目录下导出dump.hrpof的二进制文件,
// 可以用eclipse的MAT图形化工具分析
jmap -dump:live,format=b,file=dump.hprof <pid>

jinfo,命令格式:

1
java复制代码jinfo <pid>

用来查看正在运行的 Java 应用程序的扩展参数,包括 Java System 属性和 JVM 命令行参数。

其他 GC 工具:

  • 监控告警系统:Zabbix、Prometheus、Open-Falcon
  • jdk 自动实时内存监控工具:VisualVM
  • 堆外内存监控:Java VisualVM 安装 Buffer Pools 插件、google perf工具、Java NMT(Native Memory Tracking)工具
  • GC 日志分析:GCViewer、gceasy
  • GC 参数检查和优化:xxfox.perfma.com/

GC 优化案例

①数据分析平台系统频繁 Full GC

平台主要对用户在 App 中行为进行定时分析统计,并支持报表导出,使用 CMS GC 算法。

数据分析师在使用中发现系统页面打开经常卡顿,通过 jstat 命令发现系统每次 Young GC 后大约有 10% 的存活对象进入老年代。

原来是因为 Survivor 区空间设置过小,每次 Young GC 后存活对象在 Survivor 区域放不下,提前进入老年代。

通过调大 Survivor 区,使得 Survivor 区可以容纳 Young GC 后存活对象,对象在 Survivor 区经历多次 Young GC 达到年龄阈值才进入老年代。

调整之后每次 Young GC 后进入老年代的存活对象稳定运行时仅几百 Kb,Full GC 频率大大降低。

②业务对接网关 OOM

网关主要消费 Kafka 数据,进行数据处理计算然后转发到另外的 Kafka 队列,系统运行几个小时候出现 OOM,重启系统几个小时之后又 OOM。

通过 jmap 导出堆内存,在 eclipse MAT 工具分析才找出原因:代码中将某个业务 Kafka 的 topic 数据进行日志异步打印,该业务数据量较大,大量对象堆积在内存中等待被打印,导致 OOM。

③账号权限管理系统频繁长时间 Full GC

系统对外提供各种账号鉴权服务,使用时发现系统经常服务不可用,通过 Zabbix 的监控平台监控发现系统频繁发生长时间 Full GC,且触发时老年代的堆内存通常并没有占满,发现原来是业务代码中调用了 System.gc()。

总结

GC 问题可以说没有捷径,排查线上的性能问题本身就并不简单,除了将本文介绍到的原理和工具融会贯通,还需要我们不断去积累经验,真正做到性能最优。
篇幅所限,不再展开介绍常见 GC 参数的使用,我发布在 GitHub:
github.com/caison/cais…

本文转载自: 掘金

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

大话数据结构--初始图 七、图 71图的定义

发表于 2021-11-26

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

七、图

7.1图的定义

图( Graph)是由顶点的有穷非空集合和顶点之间边的集合组,通常表示为: G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。

image-20211117094029909

  • 线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元素,我们则称之为顶点(Vertex)
  • 线性表中可以没有数据元素,称为空表。树中可以没有结点,叫做空树。那么对于图呢?在图结构中,不允许没有顶点。在定义中,若V是顶点的集合,则强调了顶点集合V有穷非空。
  • 线性表中,相邻的数据元素之间具有线性关系,树结构中,相邻两层的结点具有层次关系,而图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。

7.1.1 各种图的定义

无序对(unordered pair)一种特殊的集合.即仅含两个元素的集合.


有向图

每条边都是有方向的

image-20211117095526493

无向图

每条边都是无方向的

image-20211117095534527

完全图

任意两个点都有一条边相连

image-20211117095746697

稀疏图

有很少边或弧的图(e <nlogn)

稠密图

有较多边或弧的图

网

边/弧带权的图

邻接

有边/弧相连的两个顶点之间的关系

​ 存在(Vi, Vj),则称v和v:互为邻接点;

​ 存在<Vi, Vj>,则称Vi邻接到Vj, Vj邻接于Vi;

关联(依附)

边/弧与顶点之间的关系

​ 存在(Vi, Vj)/<Vi, Vj>,则称该边/弧关联于Vi和Vj;

顶点的度

与该顶点相关联的边的数目,记为TD(v)

在有向图中,顶点的度等于该顶点的入度与出度之和。

​ 顶点V的入度是以v为终点的有向边的条数,记作ID(v)

​ 顶点v的出度是以V为始点的有向边的条数,记作OD(v)

看实例:

image-20211117101211186

当有向图中仅1个顶点的入度为0,其余顶点的入度均为1,此时是何形状?
是树!而且是一棵有向树!

image-20211117101538804

路径

若干条边构造的顶点序列

image-20211117102927932

路径长度

路径上边或弧的数目/权值之和

如果没有权值,上图这个路径的长度就是2

如果边有权值,那么边上的权值相加就是路径长度

回路(环)

第一个顶点和最后一个顶点相同的路径

image-20211117103322575

简单路径

除路径起点和终点可以相同外,其余顶点均不相同的路径

image-20211117103300841

简单回路(简单环)

除路径起点和终点相同外,其余顶点均不相同的路径。

连通图(强连通图)

在无(有)向图G=(V, {E} )中,若对任何两个顶点v、u都存在从V到u的路径,则称G是连通图(强连通图)

image-20211117151301404

从图中能很明白的看出各个概念之间的差异

这里不多解释

子图

image-20211117152043639

image-20211117152058995

如上,可以轻松看出图b和图c都是图a的子图

连通分量

无向图G的极大连通子图称为G的连通分量。

极大连通子图是指顶点的个数已经是最大的了,在添加顶点的话子图不能形成连通了

image-20211117152634004

强连通分量

有向图G的极大强连通子图称为G的强连通分量。

极大强连通子图意思是:该子图是G的强连通子图,将D的任何不在该
子图中的顶点加入,子图不再是强连通的。

image-20211117153030009

极小连通子图

该子图是G的连通子图,在该子图中删除任何一条边子图不在连通

极小连通子图可以包含所有顶点,也可以不包含所有顶点

生成树

包含无向图G所有顶点的极小连通子图

生成森林

对非连通图,由各个连通分量的生成树的集合

image-20211117153655978

7.1.2图的定义与术语总结

图按照有无方向分为无向图和有向图。无向图由顶点和边构成,有向图由顶点和弧构成。弧有弧尾和弧头之分。

图按照边或弧的多少分稀疏图和稠密图。如果任意两个顶点之间都存在边叫完全图,有向的叫有向完全图。若无重复的边或顶点到自身的边则叫简单图

图中顶点之间有邻接点、依附的概念。无向图顶点的边数叫做度,有向图顶点分为入度和出度。

图上的边或弧上带权则称为网。

图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始点则称为环,当中不重复叫简单路径。若任意两顶点都是连通的,则图就是连通图,有向则称强连通图。图中有子图,若子图极大连通则就是连通分量,有向的则称强连通分量。

无向图中连通且n个顶点n-1条边叫生成树。有向图中一顶点入度为0其余顶点入度为1的叫有向树。一个有向图由若千棵有向树构成生成森林。

本文转载自: 掘金

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

向 MySQL 数据库插入 100w 条数据的优化方案

发表于 2021-11-26

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

多线程插入(单表)

问:为何对同一个表的插入多线程会比单线程快?同一时间对一个表的写操作不应该是独占的吗?

答:在数据里做插入操作的时候,整体时间的分配是这样的:

  • 链接耗时 (30%)
  • 发送query到服务器 (20%)
  • 解析query (20%)
  • 插入操作 (10% * 词条数目)
  • 插入index (10% * Index的数目)
  • 关闭链接 (10%)

从这里可以看出来,真正耗时的不是操作,而是链接,解析的过程。

MySQL插入数据在写阶段是独占的,但是插入一条数据仍然需要解析、计算、最后才进行写处理,比如要给每一条记录分配自增id,校验主键唯一键属性,或者其他一些逻辑处理,都是需要计算的,所以说多线程能够提高效率。

多线程插入(多表)

分区分表后使用多线程插入。

预处理SQL

  • 普通SQL,即使用Statement接口执行SQL
  • 预处理SQL,即使用PreparedStatement接口执行SQL

使用PreparedStatement接口允许数据库预编译SQL语句,以后只需传入参数,避免了数据库每次都编译SQL语句,因此性能更好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码String sql = "insert into testdb.tuser (name, remark, createtime, updatetime) values (?, ?, ?, ?)";
for (int i = 0; i < m; i++) {
//从池中获取连接
Connection conn = myBroker.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
for (int k = 0; k < n; k++) {
pstmt.setString(1, RandomToolkit.generateString(12));
pstmt.setString(2, RandomToolkit.generateString(24));
pstmt.setDate(3, new Date(System.currentTimeMillis()));
pstmt.setDate(4, new Date(System.currentTimeMillis()));
//加入批处理
pstmt.addBatch();
}
pstmt.executeBatch(); //执行批处理
pstmt.close();
myBroker.freeConnection(conn); //连接归池
}

多值插入SQL

  • 普通插入SQL:INSERT INTO TBL_TEST (id) VALUES(1)
  • 多值插入SQL:INSERT INTO TBL_TEST (id) VALUES (1), (2), (3)

使用多值插入SQL,SQL语句的总长度减少,即减少了网络IO,同时也降低了连接次数,数据库一次SQL解析,能够插入多条数据。

事务(N条提交一次)

在一个事务中提交大量INSERT语句可以提高性能。

  1. 将表的存储引擎修改为myisam
  2. 将 sql 拼接成字符串,每 1000 条左右提交事务。
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
java复制代码/// <summary>
/// 执行多条SQL语句,实现数据库事务。
/// </summary>mysql数据库
/// <param name="SQLStringList">多条SQL语句</param>
public void ExecuteSqlTran(List<string> SQLStringList)
{
using (MySqlConnection conn = new MySqlConnection(connectionString))
{
if (DBVariable.flag)
{
conn.Open();
MySqlCommand cmd = new MySqlCommand();
cmd.Connection = conn;
MySqlTransaction tx = conn.BeginTransaction();
cmd.Transaction = tx;
try
{
for (int n = 0; n < SQLStringList.Count; n++)
{
string strsql = SQLStringList[n].ToString();
if (strsql.Trim().Length > 1)
{
cmd.CommandText = strsql;
cmd.ExecuteNonQuery();
}
//后来加上的
if (n > 0 && (n % 1000 == 0 || n == SQLStringList.Count - 1))
{
tx.Commit();
tx = conn.BeginTransaction();
}
}
//tx.Commit();//原来一次性提交
}
catch (System.Data.SqlClient.SqlException E)
{
tx.Rollback();
throw new Exception(E.Message);
}
}
}
}

10w条数据大概用时10s!

本文转载自: 掘金

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

【LeetCode】二叉搜索树中的搜索Java题解 题目描述

发表于 2021-11-26

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

题目描述

给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
makefile复制代码例如

给定二叉搜索树:

4
/ \
2 7
/ \
1 3

和值: 2
你应该返回如下子树:

2
/ \
1 3
在上述示例中,如果要找的值是 5,但因为没有节点值为 5,我们应该返回 NULL。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-in-a-binary-search-tree
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路分析

  • 今天的算法每日一题是二叉搜索树查找问题。什么是二叉搜索树呢?
  • 二叉搜索树(Binary Search Tree 或者是一棵空树,或者是具有下列性质的二叉树:
  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉排序树。
  • 利用二叉搜索树以上的性质,我们就可以快速查找题目节点子树。实现代码如下:

通过代码

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
java复制代码/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
if (root == null) {
return null;
}
if (root.val == val) {
return root;
} else if (root.val > val) {
return searchBST(root.left, val);
} else {
return searchBST(root.right, val);
}
}
}

image.png

总结

  • 递归算法的时间复杂度是O(log n),空间复杂度是O(log n)
  • 二叉搜索树是一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。我们要掌握好这种数据结构。
  • 坚持算法每日一题,加油!

本文转载自: 掘金

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

一文带你掌握Mybatis延迟加载原理简单了解 Mybati

发表于 2021-11-26

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

Mybatis延迟加载

什么是延迟加载

延迟加载是加载策略的一种,加载策略分为延迟加载和直接加载两种策略。延迟加载主要体现在关联查询中。

比如在开发过程中很多时候我们并不需要总是在加载⽤户信息时就⼀定要加载他的订单信息。此时就是我们所说的延迟加载。

延迟加载就是在需要⽤到数据时才进⾏加载,不需要⽤到数据时就不加载数据。延迟加载也称懒加载。

延迟加载的优点

Mybatis先从单表查询,需要时再从关联表去关联查询,⼤⼤提⾼数据库性能,因为查询单表要⽐关联查询多张表速度要快。

延迟加载的缺点

因为只有当需要⽤到数据时,才会进⾏数据库查询,这样在⼤批量数据查询时,因为查询⼯作也要消耗时间,所以可能造成⽤户等待时间变⻓,造成⽤户体验下降。

注意事项

  • 在多表中:
+ ⼀对多,多对多:通常情况下采⽤延迟加载⼀对⼀
+ (多对⼀):通常情况下采⽤⽴即加载
  • 延迟加载是基于嵌套查询来实现的

延迟加载的实现

局部延迟加载实现

在association和collection标签中都有⼀个fetchType属性,通过修改它的值,可以修改局部的加载策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码 <!--开启⼀对多延迟加载-->
<resultMap id="userMap" type="user">
<id column="id" property="id"></id>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
<!--fetchType="lazy" 懒加载策略 fetchType="eager" ⽴即加载策略-->
<collection property="orderList" ofType="order" column="id" select="com.mybatis.dao.OrderMapper.findByUid" fetchType="lazy">
</collection>
</resultMap>

<select id="findAll" resultMap="userMap">
SELECT * FROM `user`
</select>

全局延迟加载实现

在Mybatis的核⼼配置⽂件中可以使⽤setting标签修改全局的加载策略。

1
2
3
4
xml复制代码<settings>
<!--开启全局延迟加载功能-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>

注意事项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<!--关闭⼀对⼀延迟加载-->
<resultMap id="orderMap" type="order">
<id column="id" property="id"></id>
<result column="ordertime" property="ordertime"></result>
<result column="total" property="total"></result>
<!-- fetchType="lazy" 懒加载策略 fetchType="eager" ⽴即加载策略-->
<association
property="user"
column="uid"
javaType="user"
select="com.mybatis.dao.UserMapper.findById"
fetchType="eager">
</association>
</resultMap>
<select id="findAll" resultMap="orderMap">
SELECT * from orders
</select>

延迟加载原理实现

它的原理是,使⽤ CGLIB 或 Javassist( 默认 ) 创建⽬标对象的代理对象。当调⽤代理对象的延迟加载属性的getting ⽅法时,进⼊拦截器⽅法。⽐如调⽤ ⽅法,进⼊拦截器的 ⽅法,发现 需要延迟加载时,那么就会单独发送事先保存好的查询关联 B 对象的 SQL ,把 B 查询上来,然后调⽤a.setB(b) ⽅法,于是成 a.getB().getName() ⽅法的调⽤。这就是延迟加载的基本原理 对象b属性就有值了,接着往下走

总结

延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定⽅法,执⾏数据加载。

本文转载自: 掘金

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

700 二叉搜索树中的搜索 二叉树的搜索

发表于 2021-11-26

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

题目描述

这是 LeetCode 上的 700. 二叉搜索树中的搜索 ,难度为 简单。

Tag : 「树的搜索」、「迭代」、「递归」

给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。

例如,

1
2
3
4
5
6
7
8
9
makefile复制代码给定二叉搜索树:

4
/ \
2 7
/ \
1 3

和值: 2

你应该返回如下子树:

1
2
3
markdown复制代码      2     
/ \
1 3

在上述示例中,如果要找的值是 5,但因为没有节点值为 5,我们应该返回 NULL。

递归

根据题意,进行「递归」搜索即可。

代码:

1
2
3
4
5
6
Java复制代码class Solution {
public TreeNode searchBST(TreeNode root, int val) {
if (root == null || root.val == val) return root;
return root.val < val ? searchBST(root.right, val) : searchBST(root.left, val);
}
}
  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:忽略递归带来的额外空间开销,复杂度为 O(n)O(n)O(n)

迭代

同理,可以使用「迭代」进行搜索。

代码:

1
2
3
4
5
6
7
8
Java复制代码class Solution {
public TreeNode searchBST(TreeNode root, int val) {
while (root != null && root.val != val) {
root = root.val < val ? root.right : root.left;
}
return root;
}
}
  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(1)O(1)O(1)

最后

这是我们「刷穿 LeetCode」系列文章的第 No.700 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour… 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

本文转载自: 掘金

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

AEJoy —— AE 插件开发中的 命令选择器(四) 正文

发表于 2021-11-26

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

参加该活动的第 41 篇文章

正文

信息收发

After Effects 和插件之间的通信通道。

PF_Cmd_EVENT

这个选择器使用了额外的参数; 要处理的事件类型由 e_type 字段表示,它是由 extra 指向的结构的成员。参见Effect UI & Events。

PF_Cmd_USER_CHANGED_PARAM

用户修改了一个参数值。只有设置了 PF_ParamFlag_SUPERVISE 标志,你才会收到这个命令。您可以修改参数以控制值,或使一个参数的值影响其他参数。参数可以通过不同的操作来修改。 data.current_time 被设置为用户在 UI 中查看的帧的时间(内部地,合成的当前时间转换为层时间),同时它们正在改变触发 PF_Cmd_USER_CHANGED_PARAM 的参数。它也是一个自动添加的关键帧的时间(如果还没有一个,则秒表会被启用的)。这通常与紧随其后的 PF_Cmd_RENDER 传递的值相同(除非 caps 锁定关闭),但不一定 —— 可能有其他合成窗口打开,导致在不同的时间的渲染以响应改变的参数。

PF_Cmd_UPDATE_PARAMS_UI

效果控制面板(ECP)需要更新。这可能发生在打开 ECP 或移动到合成中的新时间之后。您可以通过调用 PF_UpdateParamUI() 来修改参数特征(例如启用或禁用它们)。响应此命令只能进行装饰性的更改。不要改变参数值,当响应 PF_Cmd_UPDATE_PARAMS_UI 的时候; 而应该是在 PF_Cmd_USER_CHANGED_PARAM 期间这样做。如果在 PiPL 中设置了 PF_OutFlag_SEND_UPDATE_PARAMS_UI ,并且在 PF_Cmd_GLOBAL_SETUP 期间,此命令才会定期发送。注意: 在此选择器期间,永远不要检查参数。几乎可以保证会产生递归的不良结果。

PF_Cmd_DO_DIALOG

显示一个选项对话框。当点击选项按钮(或选择菜单命令)时发送。这个选择器只会在 effect 先前表明它有一个对话框时被发送(通过设置全局 PF_OutFlag_I_DO_DIALOG 标志来响应 PF_Cmd_GLOBAL_SETUP )。在版本 3.x,用 PF_Cmd_DO_DIALOG 传递的参数无效。现在情况不再是这样了; 插件可以访问非层参数,在其他时间检出(check out)参数,并在 PF_Cmd_DO_DIALOG 期间执行 UI 更新。但是它们仍然不能改变参数的值。

PF_Cmd_ARBITRARY_CALLBACK

管理您的任意数据类型。只有在注册了自定义数据类型参数后,才会收到此消息。额外的参数指示正在调用哪个处理程序函数。自定义数据类型将在Implementing Arbitrary Data中进一步讨论。

Messaging

The communication channel between After Effects and your plug-in.

PF_Cmd_EVENT

This selector makes use of the extra parameter; the type of event to be handled is indicated by the e_type field, a member of the structure pointed to by extra.See Effect UI & Events.

PF_Cmd_USER_CHANGED_PARAM

The user changed a parameter value. You will receive this command only if you’ve set the PF_ParamFlag_SUPERVISE flag.You modify the parameter to control values, or make one parameter’s value affect others. A parameter can be modified by different actions.in_data.current_time is set to the time of the frame that the user is looking at in the UI (internally, the current time of the comp converted into layer time) while they are changing the param that triggered the PF_Cmd_USER_CHANGED_PARAM.It’s also the time of a keyframe that is added automatically (if there isn’t one already, and the stopwatch is enabled).This is usually the same as the value passed for the PF_Cmd_RENDER that follows immediately after (unless caps lock is down), but not necessarily – there could be other comp windows open that cause a render at a different time in response to the changed param.

PF_Cmd_UPDATE_PARAMS_UI

The effect controls palette (ECP) needs to be updated. This might occur after opening the ECP or moving to a new time within the composition.You can modify parameter characteristics (enabling or disabling them, for example) by calling PF_UpdateParamUI().Only cosmetic changes may be made in response to this command. Don’t change parameter values while responding to PF_Cmd_UPDATE_PARAMS_UI; do so during PF_Cmd_USER_CHANGED_PARAM instead.This command will only be sent regularly if PF_OutFlag_SEND_UPDATE_PARAMS_UI was set in the PiPL, and during PF_Cmd_GLOBAL_SETUP.NOTE: Never check out parameters during this selector. Recursive badness is almost guaranteed to result.

PF_Cmd_DO_DIALOG

Display an options dialog. this is sent when the Options button is clicked (or a menu command has been selected).This selector will only be sent if the effect has previously indicated that it has a dialog(by setting the global PF_OutFlag_I_DO_DIALOG flag in response to PF_Cmd_GLOBAL_SETUP).In version 3.x, the params passed with PF_Cmd_DO_DIALOG were invalid.This is no longer the case; plug-ins can access non-layer parameters, check out parameters at other times, and perform UI updates during PF_Cmd_DO_DIALOG.They still may not change the parameter’s values.

PF_Cmd_ARBITRARY_CALLBACK

Manage your arbitrary data type. You’ll only receive this if you’ve registered a custom data type parameter.The extra parameter indicates which handler function is being called.Custom data types are discussed further in Implementing Arbitrary Data.

(下接)

本文转载自: 掘金

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

3 Kubernetes集群安装 前言 采坑 目录 一

发表于 2021-11-26

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

前言: 采坑

整了两天, 虚拟机才在我的mac上顺利跑起来.

采坑备注: . Mac版本的幸好是 MacOS Mojave 10.14.6, 这个版本下, 安装了3款虚拟机, 均不成功. 别人的虚拟机一按就好, 就我的哪款都不行. 后来感觉可能是mac版本的事.

于是升级了, 阿欧…..三款虚拟机都能用了. 最后选择使用最熟悉的VMWare.

重点: MacOS 升级到MacOS Catalina , 版本是10.15.4. 虚拟就可以正常安装


目录:

  1. 环境准备
  2. 集群安装
  3. 安装镜像

一. 环境准备

软件准备: 我使用的是VMware Fusion专业版11.5.3

官网下载地址: my.vmware.com/cn/web/vmwa…

网盘链接: pan.baidu.com/s/1D7eD4B5z… 密码:1727

激活码: XKZYV-PK9CC-A1Y0X-K5HZL-Y65ZV

二. 集群安装

2.1 .创建master01.

第一步: 创建自定虚拟机

第二步: 选择操作系统–linux, centos7

第三步: 选择固件类型BIOS

第四步: 新建虚拟磁盘

第五步: 自定义设置参数

参数设置:

1. 配置处理器和内存: 处理器4核, 内存2G

2. 设置网络为仅主机模式NAT

3. 硬盘设置为100G, 取消拆分为多个文件存储

2.2. 创建node01 和 node02

node和master01配置差不多, 只是内存4G, 硬盘100G稍微设置的大一些, 实际工作者内存大一些, 运行的快

2.3. 创建harbor

harbor和前面的master, node都差不多, 内存2G就可以了, 硬盘100G就可以了.

2.4. 创建koolshare

第一步: koolshare要选择windows系统, window10 64位版本

第二步: 固件类型选择BOIS

第三步, 新建虚拟磁盘并自定义参数

设置处理器和内存: 2核cpu, 4G内存

磁盘: 20G, 总线类型: IDE(重点), 存储为单个文件

网络选择仅主机模式ANT

三. 安装镜像

  1. 安装koolshare

选择CD/DVD, 从磁盘启动. 选择win10系统镜像

这里注意有坑: 安装的win10需要是标准版的. 不能使PE版本的镜像

镜像下载地址: www.microsoft.com/zh-cn/softw…

第一步: 安装windows

看到了久违的windows界面

第二步: 打开CD/DVD, 选择20190419_184043.iso镜像文件, 然后关闭

第三步: 打开我的电脑, 找到DVD驱动器, 点击, 找到IMG写盘工具

第四步: 右键IMG写盘工具, 以管理员运行, 下一步, 选择驱动器

点击开始–> 确定

第五步: 设置koolshare虚拟机, 将CD/DVD光盘拿出来, 取消勾选

第六步: 关机

第七步: 设置koolshare

设置为1G内存, 1核cpu

再添加一块网卡

现在有两块网卡了

为什么要在添加一块网卡呢?来看下面的图

原因如下:

1
2
3
4
markdown复制代码1. 我本地主机的网络是hostnet
2. koolshare上的第二块网卡是NAT模式, 那么他就可以直接和本地网络连通, 那么他就可以上网了
3. 第一块网卡是仅主机模式, 他的作用是可以和k8s集群中的其他节点通讯
4. 在koolshare中, 有一个SSR插件, SSR插件是通过本地网络, 进行科学>上网, 让我们的k8s服务器拥有访问谷歌,云镜像服务器的能力

第八步: 设置安装节点的网络信息

比如是: 192.168.66.0/244网段 的.

那么, master是10, node1是20, node2是21

koolshare也需要时192.168.66的网段. koolshare的默认网段是192.168.1.1, 如果我想要访问koolshare的仪表盘, 我们需要打开本地host的网络, 在仅主机模式的网卡上配置

设置mac的仅主机模式的的方式, 参考文章:

  1. blog.csdn.net/Thomas0713/…
  1. blog.51cto.com/gladiator/1…
  1. 安装master

右击设置,选择CD/DVD

选择从光盘安装

关闭设置, 右击启动

设置网络和主机名

点击安装位置–> 默认安装

点击安装源–> 填写一个阿里云的安装源:mirrors.aliyun.com/centos/7/os/x86_64

软件选择–>最小安装

接下来设置密码: admin

一路默认, 安装成功!

本文转载自: 掘金

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

Java 项目中使用 Resilience4j 框架实现故障

发表于 2021-11-26

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

Java 项目中使用 Resilience4j 框架实现故障隔离

到目前为止,在本系列中,我们已经了解了 Resilience4j 及其 Retry, RateLimiter 和 TimeLimiter 模块。在本文中,我们将探讨 Bulkhead 模块。我们将了解它解决了什么问题,何时以及如何使用它,并查看一些示例。

代码示例

本文附有 GitHub 上的工作代码示例。

什么是 Resilience4j?

请参阅上一篇文章中的描述,快速了解 Resilience4j 的一般工作原理。

什么是故障隔离?

几年前,我们遇到了一个生产问题,其中一台服务器停止响应健康检查,负载均衡器将服务器从池中取出。

就在我们开始调查这个问题的时候,还有第二个警报——另一台服务器已经停止响应健康检查,也被从池中取出。

几分钟后,每台服务器都停止响应健康探测,我们的服务完全关闭。

我们使用 Redis 为应用程序支持的几个功能缓存一些数据。正如我们后来发现的那样,Redis 集群同时出现了一些问题,它已停止接受新连接。我们使用 Jedis 库连接到 Redis,该库的默认行为是无限期地阻塞调用线程,直到建立连接。

我们的服务托管在 Tomcat 上,它的默认请求处理线程池大小为 200 个线程。因此,通过连接到 Redis 的代码路径的每个请求最终都会无限期地阻塞线程。

几分钟之内,集群中的所有 2000 个线程都无限期地阻塞了——甚至没有空闲线程来响应负载均衡器的健康检查。

该服务本身支持多项功能,并非所有功能都需要访问 Redis 缓存。但是当这一方面出现问题时,它最终影响了整个服务。

这正是故障隔离要解决的问题——它可以防止某个服务区域的问题影响整个服务。

虽然我们的服务发生的事情是一个极端的例子,但我们可以看到缓慢的上游依赖如何影响调用服务的不相关区域。

如果我们在每个服务器实例上对 Redis 设置了 20 个并发请求的限制,那么当 Redis 连接问题发生时,只有这些线程会受到影响。剩余的请求处理线程可以继续为其他请求提供服务。

故障隔离背后的想法是对我们对远程服务进行的并发调用数量设置限制。我们将对不同远程服务的调用视为不同的、隔离的池,并对可以同时进行的调用数量设置限制。

术语舱壁本身来自它在船舶中的使用,其中船舶的底部被分成彼此分开的部分。如果有裂缝,并且水开始流入,则只有该部分会充满水。这可以防止整艘船沉没。

Resilience4j 隔板概念

resilience4j-bulkhead 的工作原理类似于其他 Resilience4j 模块。我们为它提供了我们想要作为函数构造执行的代码——一个进行远程调用的 lambda 表达式或一个从远程服务中检索到的某个值的 Supplier,等等——并且隔板用代码装饰它以控制并发调用数。

Resilience4j 提供两种类型的隔板 - SemaphoreBulkhead 和 ThreadPoolBulkhead。

SemaphoreBulkhead 内部使用
java.util.concurrent.Semaphore 来控制并发调用的数量并在当前线程上执行我们的代码。

ThreadPoolBulkhead 使用线程池中的一个线程来执行我们的代码。它内部使用
java.util.concurrent.ArrayBlockingQueue 和
java.util.concurrent.ThreadPoolExecutor 来控制并发调用的数量。

SemaphoreBulkhead

让我们看看与信号量隔板相关的配置及其含义。

maxConcurrentCalls 确定我们可以对远程服务进行的最大并发调用数。我们可以将此值视为初始化信号量的许可数。

任何尝试超过此限制调用远程服务的线程都可以立即获得 BulkheadFullException 或等待一段时间以等待另一个线程释放许可。这由 maxWaitDuration 值决定。

当有多个线程在等待许可时,fairCallHandlingEnabled 配置确定等待的线程是否以先进先出的顺序获取许可。

最后, writableStackTraceEnabled 配置让我们可以在 BulkheadFullException 发生时减少堆栈跟踪中的信息量。这很有用,因为如果没有它,当异常多次发生时,我们的日志可能会充满许多类似的信息。通常在读取日志时,只知道发生了 BulkheadFullException 就足够了。

ThreadPoolBulkhead

coreThreadPoolSize 、 maxThreadPoolSize 、 keepAliveDuration 和 queueCapacity 是与 ThreadPoolBulkhead 相关的主要配置。ThreadPoolBulkhead 内部使用这些配置来构造一个 ThreadPoolExecutor。

internalThreadPoolExecutor 使用可用的空闲线程之一执行传入的任务。 如果没有线程可以自由执行传入的任务,则该任务将排队等待线程可用时稍后执行。如果已达到 queueCapacity,则远程调用将被拒绝并返回 BulkheadFullException。

ThreadPoolBulkhead 也有 writableStackTraceEnabled 配置来控制 BulkheadFullException 的堆栈跟踪中的信息量。

使用 Resilience4j 隔板模块

让我们看看如何使用 resilience4j-bulkhead 模块中可用的各种功能。

我们将使用与本系列前几篇文章相同的示例。假设我们正在为一家航空公司建立一个网站,以允许其客户搜索和预订航班。我们的服务与 FlightSearchService 类封装的远程服务对话。

SemaphoreBulkhead

使用基于信号量的隔板时,BulkheadRegistry、BulkheadConfig 和 Bulkhead 是我们使用的主要抽象。

BulkheadRegistry 是一个用于创建和管理 Bulkhead 对象的工厂。

BulkheadConfig 封装了 maxConcurrentCalls、maxWaitDuration、writableStackTraceEnabled 和 fairCallHandlingEnabled 配置。每个 Bulkhead 对象都与一个 BulkheadConfig 相关联。

第一步是创建一个 BulkheadConfig:

1
java复制代码BulkheadConfig config = BulkheadConfig.ofDefaults();

这将创建一个 BulkheadConfig,其默认值为 maxConcurrentCalls(25)、maxWaitDuration(0s)、writableStackTraceEnabled(true) 和 fairCallHandlingEnabled(true)。

假设我们希望将并发调用的数量限制为 2,并且我们愿意等待 2 秒让线程获得许可:

1
2
3
4
java复制代码BulkheadConfig config = BulkheadConfig.custom()
.maxConcurrentCalls(2)
.maxWaitDuration(Duration.ofSeconds(2))
.build();

然后我们创建一个 Bulkhead:

1
2
3
java复制代码BulkheadRegistry registry = BulkheadRegistry.of(config);

Bulkhead bulkhead = registry.bulkhead("flightSearchService");

现在让我们表达我们的代码以作为 Supplier 运行航班搜索并使用 bulkhead 装饰它:

1
2
java复制代码BulkheadRegistry registry = BulkheadRegistry.of(config);
Bulkhead bulkhead = registry.bulkhead("flightSearchService");

最后,让我们调用几次装饰操作来了解隔板的工作原理。我们可以使用 CompletableFuture 来模拟来自用户的并发航班搜索请求:

1
2
3
4
5
java复制代码for (int i=0; i<4; i++) {
CompletableFuture
.supplyAsync(decoratedFlightsSupplier)
.thenAccept(flights -> System.out.println("Received results"));
}

输出中的时间戳和线程名称显示,在 4 个并发请求中,前两个请求立即通过:

1
2
3
4
5
6
7
8
9
10
11
12
shell复制代码Searching for flights; current time = 11:42:13 187; current thread = ForkJoinPool.commonPool-worker-3
Searching for flights; current time = 11:42:13 187; current thread = ForkJoinPool.commonPool-worker-5
Flight search successful at 11:42:13 226
Flight search successful at 11:42:13 226
Received results
Received results
Searching for flights; current time = 11:42:14 239; current thread = ForkJoinPool.commonPool-worker-9
Searching for flights; current time = 11:42:14 239; current thread = ForkJoinPool.commonPool-worker-7
Flight search successful at 11:42:14 239
Flight search successful at 11:42:14 239
Received results
Received results

第三个和第四个请求仅在 1 秒后就能够获得许可,在之前的请求完成之后。

如果线程无法在我们指定的 2s maxWaitDuration 内获得许可,则会抛出 BulkheadFullException:

1
2
3
4
5
6
shell复制代码Caused by: io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'flightSearchService' is full and does not permit further calls
at io.github.resilience4j.bulkhead.BulkheadFullException.createBulkheadFullException(BulkheadFullException.java:49)
at io.github.resilience4j.bulkhead.internal.SemaphoreBulkhead.acquirePermission(SemaphoreBulkhead.java:164)
at io.github.resilience4j.bulkhead.Bulkhead.lambda$decorateSupplier$5(Bulkhead.java:194)
at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1700)
... 6 more

除了第一行,堆栈跟踪中的其他行没有增加太多价值。如果 BulkheadFullException 发生多次,这些堆栈跟踪行将在我们的日志文件中重复。

我们可以通过将 writableStackTraceEnabled 配置设置为 false 来减少堆栈跟踪中生成的信息量:

1
2
3
4
5
java复制代码BulkheadConfig config = BulkheadConfig.custom()
.maxConcurrentCalls(2)
.maxWaitDuration(Duration.ofSeconds(1))
.writableStackTraceEnabled(false)
.build();

现在,当 BulkheadFullException 发生时,堆栈跟踪中只存在一行:

1
2
3
4
5
6
7
shell复制代码Searching for flights; current time = 12:27:58 658; current thread = ForkJoinPool.commonPool-worker-3
Searching for flights; current time = 12:27:58 658; current thread = ForkJoinPool.commonPool-worker-5
io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'flightSearchService' is full and does not permit further calls
Flight search successful at 12:27:58 699
Flight search successful at 12:27:58 699
Received results
Received results

与我们见过的其他 Resilience4j 模块类似,Bulkhead 还提供了额外的方法,如 decorateCheckedSupplier()、decorateCompletionStage()、decorateRunnable()、decorateConsumer() 等,因此我们可以在 Supplier 供应商之外的其他结构中提供我们的代码。

ThreadPoolBulkhead

当使用基于线程池的隔板时,
ThreadPoolBulkheadRegistry、ThreadPoolBulkheadConfig 和 ThreadPoolBulkhead 是我们使用的主要抽象。

ThreadPoolBulkheadRegistry 是用于创建和管理 ThreadPoolBulkhead 对象的工厂。

ThreadPoolBulkheadConfig 封装了 coreThreadPoolSize 、 maxThreadPoolSize、 keepAliveDuration 和 queueCapacity 配置。每个 ThreadPoolBulkhead 对象都与一个 ThreadPoolBulkheadConfig 相关联。

第一步是创建一个 ThreadPoolBulkheadConfig:

1
2
java复制代码ThreadPoolBulkheadConfig config =
ThreadPoolBulkheadConfig.ofDefaults();

这将创建一个 ThreadPoolBulkheadConfig,其默认值为 coreThreadPoolSize(可用处理器数量 -1)、maxThreadPoolSize(可用处理器最大数量)、keepAliveDuration(20ms)和 queueCapacity(100)。

假设我们要将并发调用的数量限制为 2:

1
2
3
4
5
java复制代码ThreadPoolBulkheadConfig config = ThreadPoolBulkheadConfig.custom()
.maxThreadPoolSize(2)
.coreThreadPoolSize(1)
.queueCapacity(1)
.build();

然后我们创建一个 ThreadPoolBulkhead:

1
2
java复制代码ThreadPoolBulkheadRegistry registry = ThreadPoolBulkheadRegistry.of(config);
ThreadPoolBulkhead bulkhead = registry.bulkhead("flightSearchService");

现在让我们表达我们的代码以作为 Supplier 运行航班搜索并使用 bulkhead 装饰它:

1
2
3
4
java复制代码Supplier<List<Flight>> flightsSupplier =
() -> service.searchFlightsTakingOneSecond(request);
Supplier<CompletionStage<List<Flight>>> decoratedFlightsSupplier =
ThreadPoolBulkhead.decorateSupplier(bulkhead, flightsSupplier);

与返回一个 Supplier<List<Flight>> 的
SemaphoreBulkhead.decorateSupplier() 不同,
ThreadPoolBulkhead.decorateSupplier() 返回一个 Supplier<CompletionStage<List<Flight>>。这是因为 ThreadPoolBulkHead 不会在当前线程上同步执行代码。

最后,让我们调用几次装饰操作来了解隔板的工作原理:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码for (int i=0; i<3; i++) {
decoratedFlightsSupplier
.get()
.whenComplete((r,t) -> {
if (r != null) {
System.out.println("Received results");
}
if (t != null) {
t.printStackTrace();
}
});
}

输出中的时间戳和线程名称显示,虽然前两个请求立即执行,但第三个请求已排队,稍后由释放的线程之一执行:

1
2
3
4
5
6
7
8
9
shell复制代码Searching for flights; current time = 16:15:00 097; current thread = bulkhead-flightSearchService-1
Searching for flights; current time = 16:15:00 097; current thread = bulkhead-flightSearchService-2
Flight search successful at 16:15:00 136
Flight search successful at 16:15:00 135
Received results
Received results
Searching for flights; current time = 16:15:01 151; current thread = bulkhead-flightSearchService-2
Flight search successful at 16:15:01 151
Received results

如果队列中没有空闲线程和容量,则抛出 BulkheadFullException:

1
2
3
4
shell复制代码Exception in thread "main" io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'flightSearchService' is full and does not permit further calls
at io.github.resilience4j.bulkhead.BulkheadFullException.createBulkheadFullException(BulkheadFullException.java:64)
at io.github.resilience4j.bulkhead.internal.FixedThreadPoolBulkhead.submit(FixedThreadPoolBulkhead.java:157)
... other lines omitted ...

我们可以使用 writableStackTraceEnabled 配置来减少堆栈跟踪中生成的信息量:

1
2
3
4
5
6
java复制代码ThreadPoolBulkheadConfig config = ThreadPoolBulkheadConfig.custom()
.maxThreadPoolSize(2)
.coreThreadPoolSize(1)
.queueCapacity(1)
.writableStackTraceEnabled(false)
.build();

现在,当 BulkheadFullException 发生时,堆栈跟踪中只存在一行:

1
2
3
4
5
6
7
shell复制代码Searching for flights; current time = 12:27:58 658; current thread = ForkJoinPool.commonPool-worker-3
Searching for flights; current time = 12:27:58 658; current thread = ForkJoinPool.commonPool-worker-5
io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'flightSearchService' is full and does not permit further calls
Flight search successful at 12:27:58 699
Flight search successful at 12:27:58 699
Received results
Received results

上下文传播

有时我们将数据存储在 ThreadLocal 变量中并在代码的不同区域中读取它。我们这样做是为了避免在方法链之间显式地将数据作为参数传递,尤其是当该值与我们正在实现的核心业务逻辑没有直接关系时。

例如,我们可能希望将当前用户 ID 或事务 ID 或某个请求跟踪 ID 记录到每个日志语句中,以便更轻松地搜索日志。对于此类场景,使用 ThreadLocal 是一种有用的技术。

使用 ThreadPoolBulkhead 时,由于我们的代码不在当前线程上执行,因此我们存储在 ThreadLocal 变量中的数据在其他线程中将不可用。

让我们看一个例子来理解这个问题。首先我们定义一个 RequestTrackingIdHolder 类,一个围绕 ThreadLocal 的包装类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码class RequestTrackingIdHolder {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();


static String getRequestTrackingId() {
return threadLocal.get();
}


static void setRequestTrackingId(String id) {
if (threadLocal.get() != null) {
threadLocal.set(null);
threadLocal.remove();
}
threadLocal.set(id);
}


static void clear() {
threadLocal.set(null);
threadLocal.remove();
}
}

静态方法可以轻松设置和获取存储在 ThreadLocal 上的值。我们接下来在调用隔板装饰的航班搜索操作之前设置一个请求跟踪 ID:

1
2
3
4
5
6
7
8
9
10
java复制代码for (int i=0; i<2; i++) {
String trackingId = UUID.randomUUID().toString();
System.out.println("Setting trackingId " + trackingId + " on parent, main thread before calling flight search");
RequestTrackingIdHolder.setRequestTrackingId(trackingId);
decoratedFlightsSupplier
.get()
.whenComplete((r,t) -> {
// other lines omitted
});
}

示例输出显示此值在隔板管理的线程中不可用:

1
2
3
4
5
6
7
8
shell复制代码Setting trackingId 98ff99df-466a-47f7-88f7-5e31fc8fcb6b on parent, main thread before calling flight search
Setting trackingId 6b98d73c-a590-4a20-b19d-c85fea783caf on parent, main thread before calling flight search
Searching for flights; current time = 19:53:53 799; current thread = bulkhead-flightSearchService-1; Request Tracking Id = null
Flight search successful at 19:53:53 824
Received results
Searching for flights; current time = 19:53:54 836; current thread = bulkhead-flightSearchService-1; Request Tracking Id = null
Flight search successful at 19:53:54 836
Received results

为了解决这个问题,ThreadPoolBulkhead 提供了一个 ContextPropagator。ContextPropagator 是一种用于跨线程边界检索、复制和清理值的抽象。它定义了一个接口,其中包含从当前线程 (retrieve()) 获取值、将其复制到新的执行线程 (copy()) 并最终在执行线程 (clear()) 上进行清理的方法。

让我们实现一个
RequestTrackingIdPropagator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码class RequestTrackingIdPropagator implements ContextPropagator {
@Override
public Supplier<Optional> retrieve() {
System.out.println("Getting request tracking id from thread: " + Thread.currentThread().getName());
return () -> Optional.of(RequestTrackingIdHolder.getRequestTrackingId());
}


@Override
Consumer<Optional> copy() {
return optional -> {
System.out.println("Setting request tracking id " + optional.get() + " on thread: " + Thread.currentThread().getName());
optional.ifPresent(s -> RequestTrackingIdHolder.setRequestTrackingId(s.toString()));
};
}


@Override
Consumer<Optional> clear() {
return optional -> {
System.out.println("Clearing request tracking id on thread: " + Thread.currentThread().getName());
optional.ifPresent(s -> RequestTrackingIdHolder.clear());
};
}
}

我们通过在 ThreadPoolBulkheadConfig 上的设置来为 ThreadPoolBulkhead 提供 ContextPropagator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码class RequestTrackingIdPropagator implements ContextPropagator {
@Override
public Supplier<Optional> retrieve() {
System.out.println("Getting request tracking id from thread: " + Thread.currentThread().getName());
return () -> Optional.of(RequestTrackingIdHolder.getRequestTrackingId());
}


@Override
Consumer<Optional> copy() {
return optional -> {
System.out.println("Setting request tracking id " + optional.get() + " on thread: " + Thread.currentThread().getName());
optional.ifPresent(s -> RequestTrackingIdHolder.setRequestTrackingId(s.toString()));
};
}


@Override
Consumer<Optional> clear() {
return optional -> {
System.out.println("Clearing request tracking id on thread: " + Thread.currentThread().getName());
optional.ifPresent(s -> RequestTrackingIdHolder.clear());
};
}
}

现在,示例输出显示请求跟踪 ID 在隔板管理的线程中可用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
shell复制代码Setting trackingId 71d44cb8-dab6-4222-8945-e7fd023528ba on parent, main thread before calling flight search
Getting request tracking id from thread: main
Setting trackingId 5f9dd084-f2cb-4a20-804b-038828abc161 on parent, main thread before calling flight search
Getting request tracking id from thread: main
Setting request tracking id 71d44cb8-dab6-4222-8945-e7fd023528ba on thread: bulkhead-flightSearchService-1
Searching for flights; current time = 20:07:56 508; current thread = bulkhead-flightSearchService-1; Request Tracking Id = 71d44cb8-dab6-4222-8945-e7fd023528ba
Flight search successful at 20:07:56 538
Clearing request tracking id on thread: bulkhead-flightSearchService-1
Received results
Setting request tracking id 5f9dd084-f2cb-4a20-804b-038828abc161 on thread: bulkhead-flightSearchService-1
Searching for flights; current time = 20:07:57 542; current thread = bulkhead-flightSearchService-1; Request Tracking Id = 5f9dd084-f2cb-4a20-804b-038828abc161
Flight search successful at 20:07:57 542
Clearing request tracking id on thread: bulkhead-flightSearchService-1
Received results

Bulkhead事件

Bulkhead 和 ThreadPoolBulkhead 都有一个 EventPublisher 来生成以下类型的事件:

  • BulkheadOnCallPermittedEvent
  • BulkheadOnCallRejectedEvent 和
  • BulkheadOnCallFinishedEvent

我们可以监听这些事件并记录它们,例如:

1
2
3
4
java复制代码Bulkhead bulkhead = registry.bulkhead("flightSearchService");
bulkhead.getEventPublisher().onCallPermitted(e -> System.out.println(e.toString()));
bulkhead.getEventPublisher().onCallFinished(e -> System.out.println(e.toString()));
bulkhead.getEventPublisher().onCallRejected(e -> System.out.println(e.toString()));

示例输出显示了记录的内容:

1
2
3
4
5
shell复制代码2020-08-26T12:27:39.790435: Bulkhead 'flightSearch' permitted a call.
... other lines omitted ...
2020-08-26T12:27:40.290987: Bulkhead 'flightSearch' rejected a call.
... other lines omitted ...
2020-08-26T12:27:41.094866: Bulkhead 'flightSearch' has finished a call.

Bulkhead 指标

SemaphoreBulkhead

Bulkhead 暴露了两个指标:

  • 可用权限的最大数量(resilience4j.bulkhead.max.allowed.concurrent.calls),和
  • 允许的并发调用数(resilience4j.bulkhead.available.concurrent.calls)。

bulkhead.available 指标与我们在 BulkheadConfig 上配置的 maxConcurrentCalls 相同。

首先,我们像前面一样创建 BulkheadConfig、BulkheadRegistry 和 Bulkhead。然后,我们创建一个 MeterRegistry 并将 BulkheadRegistry 绑定到它:

1
2
3
java复制代码MeterRegistry meterRegistry = new SimpleMeterRegistry();
TaggedBulkheadMetrics.ofBulkheadRegistry(registry)
.bindTo(meterRegistry);

运行几次隔板装饰操作后,我们显示捕获的指标:

1
2
3
4
5
6
7
8
9
java复制代码Consumer<Meter> meterConsumer = meter -> {
String desc = meter.getId().getDescription();
String metricName = meter.getId().getName();
Double metricValue = StreamSupport.stream(meter.measure().spliterator(), false)
.filter(m -> m.getStatistic().name().equals("VALUE"))
.findFirst()
.map(m -> m.getValue())
.orElse(0.0);
System.out.println(desc + " - " + metricName + ": " + metricValue);};meterRegistry.forEachMeter(meterConsumer);

这是一些示例输出:

1
2
shell复制代码The maximum number of available permissions - resilience4j.bulkhead.max.allowed.concurrent.calls: 8.0
The number of available permissions - resilience4j.bulkhead.available.concurrent.calls: 3.0

ThreadPoolBulkhead

ThreadPoolBulkhead 暴露五个指标:

  • 队列的当前长度(resilience4j.bulkhead.queue.depth),
  • 当前线程池的大小(resilience4j.bulkhead.thread.pool.size),
  • 线程池的核心和最大容量(resilience4j.bulkhead.core.thread.pool.size 和 resilience4j.bulkhead.max.thread.pool.size),以及
  • 队列的容量(resilience4j.bulkhead.queue.capacity)。

首先,我们像前面一样创建 ThreadPoolBulkheadConfig、
ThreadPoolBulkheadRegistry 和 ThreadPoolBulkhead。然后,我们创建一个 MeterRegistry 并将
ThreadPoolBulkheadRegistry 绑定到它:

1
2
java复制代码MeterRegistry meterRegistry = new SimpleMeterRegistry();
TaggedThreadPoolBulkheadMetrics.ofThreadPoolBulkheadRegistry(registry).bindTo(meterRegistry);

运行几次隔板装饰操作后,我们将显示捕获的指标:

1
2
3
4
5
shell复制代码The queue capacity - resilience4j.bulkhead.queue.capacity: 5.0
The queue depth - resilience4j.bulkhead.queue.depth: 1.0
The thread pool size - resilience4j.bulkhead.thread.pool.size: 5.0
The maximum thread pool size - resilience4j.bulkhead.max.thread.pool.size: 5.0
The core thread pool size - resilience4j.bulkhead.core.thread.pool.size: 3.0

在实际应用中,我们会定期将数据导出到监控系统并在仪表板上进行分析。

实施隔板时的陷阱和良好实践

使隔板成为单例

对给定远程服务的所有调用都应通过同一个 Bulkhead 实例。对于给定的远程服务,Bulkhead 必须是单例。

如果我们不强制执行此操作,我们代码库的某些区域可能会绕过 Bulkhead 直接调用远程服务。为了防止这种情况,远程服务的实际调用应该在一个核心、内部层和其他区域应该使用内部层暴露的隔板装饰器。

我们如何确保未来的新开发人员理解这一意图? 查看 Tom 的文章,该文章展示了解决此类问题的一种方法,即通过组织包结构来明确此类意图。此外,它还展示了如何通过在 ArchUnit 测试中编码意图来强制执行此操作。

与其他 Resilience4j 模块结合

将隔板与一个或多个其他 Resilience4j 模块(如重试和速率限制器)结合使用会更有效。例如,如果有 BulkheadFullException,我们可能希望在一些延迟后重试。

结论

在本文中,我们学习了如何使用 Resilience4j 的 Bulkhead 模块对我们对远程服务进行的并发调用设置限制。我们了解了为什么这很重要,还看到了一些有关如何配置它的实际示例。

您可以使用 GitHub 上的代码演示一个完整的应用程序。


本文译自: Implementing Bulkhead with Resilience4j - Reflectoring

本文转载自: 掘金

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

轻松入门正则表达式

发表于 2021-11-26

​文章背景

  • 对于正则表达式,相信很多开发者在进行一些特殊的文本处理或者日常排查日志中会使用到。
  • 今天就通过一个实战带大家轻松入门正则表达式。
  • 让我们直接开始练习吧
  • 首先准备练习文本,这份文档是从网上改编而来,我们可将其命名为test.txt,
1
2
3
4
5
6
7
erlang复制代码Football game is not use feet only.
this dress doesn't fit me.
GNU is free air not free beer.
Her hair is very beauty.
I can't finish the test.
Oh! The soup taste good.
Test tool

通过grep来使用正则表达式

  • 本篇文章将介绍
    • grep 命令
      • 行首行尾符
      • 任意和重复字符
      • 限定连续字符范围
      • 字符组匹配
      • 正则表达式特殊符号
      • 正则中贪婪模式和非贪婪模式的区别
  • grep命令
  • 常用参数说明
    • -c :计算符合规则的字符串个数
      • -i :忽略大小写的差别
      • -n :带行号输出
      • -v :反向查找,显示不符合规则内容的行
1
2
3
perl复制代码grep -c 'test' test.txt
grep -n 'test' test.txt
grep -in 'test' test.txt

1
perl复制代码grep -vn 'test' test.txt

  • 行首行尾符
    • 行首符 ^ 与行尾符 $
      • 当查找含有GNU开头的字符串时,则可以使用以下命令
1
perl复制代码grep -n '^GNU' test.txt
![](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/d0e0f0032d75ba35171d2d71f431a582cff59530d6450ee101b0fd4e1ca68ff6)
  • 任意和重复字符
    • 可以用任意字符.与重复字符*来查找s?u?类型的字符
1
perl复制代码grep -n 's.u.' test.txt
![](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/1b646875d0baea61f26375131e8a29e6df98a1298330d90c6e53e7027945fd3d)
1
2
3
4
5
6
markdown复制代码小数点.表示任意一个字符
*(星号):代表重复前面 0 个或者多个字符。
s*:表示具有空字符或者一个以上 s 字符。
ss*,表示前面的第一个 s 字符必须存在。第二个 s 则可以是 0 个或者多个 s 字符。
sss*,表示前面两个 s 字符必须存在。第三个 s 则可以是 0 个或者多个 s 字符。
ss*s :表示前面的第一个与第三个 s 字符必须存在。第二个 s 则可以是 0 个或者多个 s 字符。
1
2
perl复制代码grep -n 's*' test.txt
grep -n 'oo*' test.txt

  • 限定连续字符范围
    • 此处需要注意的是用到了转义字符``
![](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/e3dcdca2443af002be3b71ed52429b1012bd182400247e0ed511d593ded85d09)
+ 限定连续字符范围{}
+ `{ }` 可限制一个范围区间内的重复字符数。如果现在要求找出存在连续的两个 e 字符的字符串,根据前面所学的知识,我们可以使用:



1
perl复制代码grep -n 'ooo*' test.txt
+ 第2种方法便是使用`{}` `grep -n 'o{2}' test.txt`
  • 字符组匹配
    • []可以用来匹配字符组
      • []里无论是使用多少个字符,它只代表一个,比如我们想查找use和me这两个?e格式
1
perl复制代码grep -n '[sm]e' test.txt

[^] 为反向选择字符组,用于排除后面的字符,使用方式为 [^...]。它和参数-v区别在于-v只会输出不含有反向选择的字符的行。

1
2
perl复制代码grep -nv 'eet' text.txt
grep -n '[^eet]' test.txt

其他使用例子

1
2
3
4
less复制代码[abc]           :表示 “a” 或 “b” 或 “c”
[0-9]           :表示 0~9 中任意一个数字,等价于 [0123456789],-代表着是一个范围
[^abc]         :表示除 “a”、“b”、“c” 外的其它任意一个字符
[^A-Z]         :表示除大写字母外的任意一个字符
  • 正则表达式特殊符号
1
2
3
4
5
6
7
8
9
10
11
css复制代码[:xdigit:]代表 16 进位的数字类型
[:alpha:]代表英文大小写字母
[:alnum:]代表英文大小写字母及数字
[:digit:]代表数字
[:lower:]小写字母
[:upper:]代表大写字母
[:blank:]代表空格和 tab 键
[:cntrl:]键盘上的控制按键,如 CR,LF,TAB,DEL
[:print:]可以被打印出来的任何字符
[:punct:]代表标点符号
[:space:]任何会产生空白的字符如空格,tab 等
  • 正则中贪婪模式和非贪婪模式的区别
1
2
3
4
5
6
7
8
9
10
11
12
javascript复制代码贪婪与非贪婪模式影响的是被量词修饰的子表达式的匹配行为,通俗说法就是匹配范围的大小。
贪婪模式在整个表达式匹配成功的前提下,尽可能多的匹配。
非贪婪模式(懒惰模式)在整个表达式匹配成功的前提下,尽可能少的匹配。
{m,n}、{m,}、?、* 和 +。属于贪婪模式,但是在后面加上?就变成了非贪婪模式。如,“{m,n}?”这个就属于非贪婪模式

贪婪模式
"123456abc";
/\d{1,3}/;

非贪婪模式?
"123456abc";
/\d{1,3}?/;

今天就和大家介绍到这了,欢迎关注我的微信公众号【那未明】来聊聊!

本文转载自: 掘金

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

1…182183184…956

开发者博客

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