首页 文章 精选 留言 我的

精选列表

搜索[入门],共9384篇文章
优秀的个人博客,低调大师

JVM的入门知识

前言:巴拉巴拉,今天给大家分享一点java三剑客(jre,jvm,jdk)中的jvm,纯理论教科书篇。 非原创,里面摘取了多个博客里面的内容 1JDK、 JRE、JVM 的关系是什么? 我们学习JVM的之前,简单科普一下他们三者有啥关系 JVM JAVA 虚拟机(Java Virtual Machine)。它只识别 .class 类型文件,它能够将 class 文件中的字节码指令进行识别并调用操作系统向上的 API 完成动作 JRE Java 运行时环境(Java Runtime Environment)。它主要包含两个部分:JVM 的标准实现和 Java 的一些基本类库。相对于JVM 来说,JRE多出了一部分 Java 类库 JDK Java 开发工具包(Java Development Kit)。JDK 是整个 Java 开发的核心,它集成了 JRE 和一些好用的小工具 常用工具 jar.exe jar文件管理工具,打包压缩解压jar文件 java.exe java运行工具,运行.class字节码文件或 .jar文件 javac.exe java编译工具,用来编译.java源代码文件 javap.exe java反编译工具,根据java字节码文件反汇编成java源代码文件 jvisualvm.exe jvm监控,分析工具 这三者的关系:JDK > JRE > JVM 由于Oracle jdk 从jdk 8u211以后商业用途需要收费,提供一下免费JDK 阿里dragonwell8 https://github.com/alibaba/dragonwell8/releases 亚马逊Corretto https://docs.aws.amazon.com/corretto/latest/corretto-8-ug/downloads-list.html adoptopenjdk https://adoptopenjdk.net openjdk http://openjdk.java.net/install 2 JVM的核心 JVM(Java Virtual Machine)是用来运行Java字节码的虚拟机,包括字节码指令集、程序寄存器、栈、堆、方法区和垃圾回收器。 JVM运行在操作系统之上,不与硬件设备直接交互。 Java源文件在通过编译器之后被编译成相应的.Class文件(字节码文件),.Class文件又被JVM中的解释器编译成机器码在不同的操作系统(Windows、Linux、Mac)上运行。 每种操作系统的解释器都是不同的,但基于解释器实现的虚拟机是相同的,这也是Java能够跨平台的原因。 在一个Java进程开始运行后,虚拟机就开始实例化了,有多个进程启动就会实例化多个虚拟机实例。进程退出或者关闭,则虚拟机实例消亡,在多个虚拟机实例之间不能共享数据。 Java程序的具体运行过程如下。 (1)Java源文件被编译器编译成字节码文件。 (2)JVM将字节码文件编译成相应操作系统的机器码。 (3)机器码调用相应操作系统的本地方法库执行相应的方法。 Java虚拟机包括一个类加载器子系统(Class Loader SubSystem)、运行时数据区(Runtime Data Area)、执行引擎和本地接口库(Native InterfaceLibrary)。 本地接口库通过调用本地方法库(Native Method Library)与操作系统交互。 JVM核心图 ◎ 类加载器子系统用于将编译好的.Class文件加载到JVM中; ◎ 运行时数据区用于存储在JVM运行过程中产生的数据,包括程序计数器、方法区、本地方法区、虚拟机栈和虚拟机堆; ◎ 执行引擎包括即时编译器和垃圾回收器,即时编译器用于将Java字节码编译成具体的机器码,垃圾回收器用于回收在运行过程中不再使用的对象; ◎ 本地接口库用于调用操作系统的本地方法库完成具体的指令操作。 3 JVM的内存区域 JVM的内存区域分为线程私有区域(程序计数器、栈、本地方法区)、线程共享区域(堆、方法区)和直接内存。 3.1 线程私有区域 生命周期与线程相同,随线程的启动而创建,随线程的结束而销毁。在JVM内,每个线程都与操作系统的本地线程直接映射,因此这部分内存区域的存在与否和本地线程的启动和销毁对应。 3.1.1程序计数器 程序计数器是一块很小的内存空间,用于存储当前运行的线程所执行的字节码的行号指示器。每个运行中的线程都有一个独立的程序计数器,在方法正在执行时,该方法的程序计数器记录的是实时虚拟机字节码指令的地址;如果该方法执行的是Native方法,则程序计数器的值为空(Undefined)。程序计数器属于“线程私有”的内存区域,它是唯一没有Out Of Memory(内存溢出)的区域。 3.1.2虚拟机栈 虚拟机栈是描述Java方法的执行过程的内存模型,它在当前栈帧(Stack Frame)中存储了局部变量表、操作数栈、动态链接、方法出口等信息。同时,栈帧用来存储部分运行时数据及其数据结构,处理动态链接(Dynamic Linking)方法的返回值和异常分派(Dispatch Exception)。栈帧用来记录方法的执行过程,在方法被执行时虚拟机会为其创建一个与之对应的栈帧,方法的执行和返回对应栈帧在虚拟机栈中的入栈和出栈。无论方法是正常运行完成还是异常完成(抛出了在方法内未被捕获的异常),都视为方法运行结束。 上图展示了线程运行图。 线程1在CPU1上运行,线程2在CPU2上运行,在CPU资源不够时其他线程将处于等待状态,等待获取CPU时间片。 而在线程内部,每个方法的执行和返回都对应一个栈帧的入栈和出栈,每个运行中的线程当前只有一个栈帧处于活动状态。 jvm参数: -Xss128k:每个线程栈的大小,合理的减少可以使剩余的系统内存支持更多的线程。 3.1.3 本地方法区 本地方法区和虚拟机栈的作用类似,区别是虚拟机栈为执行Java方法服务,本地方法栈为Native方法服务。 3.2线程共享区域 随虚拟机的启动而创建,随虚拟机的关闭而销毁。 3.2.1 堆 也叫作运行时数据区,在JVM运行过程中创建的对象和产生的数据都被存储在堆中,堆是被线程共享的内存区域,也是垃圾收集器进行垃圾回收的最主要的内存区域。由于现代JVM采用分代收集算法,因此Java堆从GC(Garbage Collection,垃圾回收)的角度还可以细分为:新生代、老年代和永久代。 jvm参数: -Xms4G : JVM启动时整个堆(包括年轻代,年老代)的初始化大小 (一般将和最大保持一致,可以避免堆内存频繁震荡,导致系统性能下降,jvm会尽可能维持在最小空间运行,这样很有可能发生频繁GC)。 -Xmx4G : JVM启动时整个堆的最大值。 -Xmn2G:年轻代的空间大小,剩下的是年老代的空间。 3.2.2 方法区 方法区也被称为永久代,用于存储常量、静态变量、类信息、即时编译器编译后的机器码、运行时常量池等数据 JVM把GC分代收集扩展到了方法区,这样JVM的垃圾收集器就可以像管理Java堆一样管理这部分内存。 永久代的内存回收主要针对常量池的回收和类的卸载,可回收的对象很少。 3.3 直接内存 也叫堆外内存,就是把内存对象分配在Java虚拟机的堆以外的内存 ,它并不是JVM运行时数据区的一部分,直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。 JDK的NIO模块提供的基于Channel与Buffer的I/O操作方式就是基于堆外内存实现的,NIO模块通过调用Native函数库直接在操作系统上分配堆外内存,然后使用java.nio.DirectByteBuffer对象作为这块内存的引用 对内存进行操作。 这样可以加快复制速度,因为堆内数据刷新到远程时,会先复制到直接内存,然后再发送,可以减少堆内存和直接内存的来回复制影响性能,因此堆外内存在高并发应用场景下被广泛使用( Ehcache,Netty、Flink、HBase、Hadoop都有用到堆外内存)。 4 JVM的运行时内存(堆) JVM的运行时内存也叫作JVM堆,从GC的角度可以将JVM堆分为新生代、老年代和永久代。其中新生代默认占1/3堆空间,老年代默认占2/3堆空间,永久代占非常少的堆空间。新生代又分为Eden区、ServivorFrom区和ServivorTo区,Eden区默认占8/10新生代空间,ServivorFrom区和ServivorTo区默认分别占1/10新生代空间。 4.1 新生代 JVM新创建的对象(除了大对象外)会被存放在新生代,默认占1/3堆内存空间。由于JVM会频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。 新生代又分为Eden区、ServivorTo区和ServivorFrom区 ◎Eden区:Java新创建的对象首先会被存放在Eden区,如果新创建的对象属于大对象,则直接将其分配到老年代。大对象的定义和具体的JVM版本、堆大小和垃圾回收策略有关,一般为2KB~128KB,可通过XX:PretenureSizeThreshold设置其大小。在Eden区的内存空间不足时会触发GC。 ◎ServivorTo区:保留上一次GC时的幸存者。 ◎ServivorFrom区: 上一次GC的幸存者,作为这一次GC的被扫描者。 新生代的GC过程叫作MinorGC,采用复制算法实现,具体过程如下。 (1)Eden区内存空间不足会触发GC (2)扫描Eden区和ServivorFrom区进行GC回收 (3)将存活的对象复制到ServivorTo区(如果某对象的年龄达到老年代的标准(对象晋升老年代的标准由XX:MaxTenuringThreshold设置,默认为15),则将其复制到老年代。如果ServivorTo区的内存空间不够,则也直接将其复制到老年代;如果对象属于大对象(大小为2KB~128KB的对象属于大对象,例如通过XX:PretenureSizeThreshold=2097152设置大对象为2MB,1024×1024×2),则也直接将其复制到老年代) (4)将现有ServivorTo区的存活的对象年龄加1 (5)清空Eden区和ServivorFrom区中的对象 (6)将ServivorTo区和ServivorFrom区互换(原来的ServivorTo区成为下一次GC时的ServivorFrom区) 4.2 老年代 老年代主要存放有长生命周期的对象和大对象。老年代的GC过程叫作MajorGC。在老年代,对象比较稳定,MajorGC不会被频繁触发。在进行MajorGC前,JVM会进行一次MinorGC,在MinorGC过后仍然出现老年代空间不足或无法找到足够大的连续空间分配给新创建的大对象时,会触发MajorGC进行垃圾回收,释放JVM的内存空间。MajorGC采用标记清除算法,该算法首先会扫描所有对象并标记存活的对象,然后回收未被标记的对象,并释放内存空间。因为要先扫描老年代的所有对象再回收,所以MajorGC的耗时较长。MajorGC的标记清除算法容易产生内存碎片。在老年代没有内存空间可分配时,会抛出Out Of Memory异常。 报错误的原因是因为执行垃圾收集的时间比例太大, 有效的运算量太小。默认情况下, 如果GC花费的时间超过 98%, 并且GC回收的内存少于 2%, JVM就会抛出这个错误。 4.3 永久代 永久代指内存的永久保存区域,主要存放Class和Meta(元数据)的信息。Class在类加载时被放入永久代。永久代和老年代、新生代不同,GC不会在程序运行期间对永久代的内存进行清理,这也导致了永久代的内存会随着加载的Class文件的增加而增加,在加载的Class文件过多时会抛出Out OfMemory异常,比如Tomcat引用Jar文件过多导致JVM内存不足而无法启动。需要注意的是,在Java 8中永久代已经被元数据区(也叫作元空间)取代。元数据区的作用和永久代类似,二者最大的区别在于:元数据区并没有使用虚拟机的内存,而是直接使用操作系统的本地内存。因此,元空间的大小不受JVM内存的限制,只和操作系统的内存有关。在Java 8中,JVM将类的元数据放入本地内存(Native Memory)中,将常量池和类的静态变量放入Java堆中,这样JVM能够加载多少元数据信息就不再由JVM的最大可用内存(MaxPermSize)空间决定,而由操作系统的实际可用内存空间决定。 5.垃圾回收与算法 5.1 如何确定是垃圾? Java采用引用计数法和可达性分析来确定对象是否应该被回收,其中,引用计数法容易产生循环引用的问题,可达性分析通过根搜索算法(GC RootsTracing)来实现。根搜索算法以一系列GC Roots的点作为起点向下搜索,在一个对象到任何GCRoots都没有引用链相连时,说明其已经死亡。根搜索算法主要针对栈中的引用、方法区中的静态引用和JNI中的引用展开分析,如图1-6所示。 5.1.1 引用计数法 在Java中如果要操作对象,就必须先获取该对象的引用,因此可以通过引用计数法来判断一个对象是否可以被回收。在为对象添加一个引用时,引用计数加1;在为对象删除一个引用时,引进计数减1;如果一个对象的引用计数为0,则表示此刻该对象没有被引用,可以被回收。引用计数法容易产生循环引用问题。循环引用指两个对象相互引用,导致它们的引用一直存在,而不能被回收。 Object1与Object2互为引用,如果采用引用计数法,则Object1和Object2由于互为引用,其引用计数一直为1,因而无法被回收。 5.1.2 可达性分析 为了解决引用计数法的循环引用问题,Java还采用了可达性分析来判断对象是否可以被回收。具体做法是首先定义一些GC Roots对象,然后以这些GCRoots对象作为起点向下搜索,如果在GC roots和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象要经过至少两次标记才能判定其是否可以被回收,如果在两次标记后该对象仍然是不可达的,则将被垃圾收集器回收。 5.2 常用的垃圾回收算法 Java中常用的垃圾回收算法有标记清除(Mark-Sweep)、复制(Copying)、标记整理(Mark-Compact)和分代收集(GenerationalCollecting)这4种垃圾回收算法。 5.2.1标记清除算法、 标记清除算法是基础的垃圾回收算法,其过程分为标记和清除两个阶段。在标记阶段标记所有需要回收的对象,在清除阶段清除可回收的对象并释放其所占用的内存空间 由于标记清除算法在清理对象所占用的内存空间后并没有重新整理可用的内存空间,因此如果内存中可被回收的小对象居多,则会引起内存碎片化的问题,继而引起大对象无法获得连续可用空间的问题。 5.2.2 复制算法 复制算法是为了解决标记清除算法内存碎片化的问题而设计的。复制算法首先将内存划分为两块大小相等的内存区域,即区域1和区域2,新生成的对象都被存放在区域1中,在区域1内的对象存储满后会对区域1进行一次标记,并将标记后仍然存活的对象全部复制到区域2中,这时区域1将不存在任何存活的对象,直接清理整个区域1的内存即可。 复制算法的内存清理效率高且易于实现,但由于同一时刻只有一个内存区域可用,即可用的内存空间被压缩到原来的一半,因此存在大量的内存浪费。同时,在系统中有大量长时间存活的对象时,这些对象将在内存区域1和内存区域2之间来回复制而影响系统的运行效率。因此,该算法只在对象为“朝生夕死”状态时运行效率较高。 5.2.3标记整理算法 标记整理算法结合了标记清除算法和复制算法的优点,其标记阶段和标记清除算法的标记阶段相同,在标记完成后将存活的对象移到内存的另一端,然后清除该端的对象并释放内存。 5.2.4分代收集算法 无论是标记清除算法、复制算法还是标记整理算法,都无法对所有类型(长生命周期、短生命周期、大对象、小对象)的对象都进行垃圾回收。因此,针对不同的对象类型,JVM采用了不同的垃圾回收算法,该算法被称为分代收集算法。分代收集算法根据对象的不同类型将内存划分为不同的区域,JVM将堆划分为新生代和老年代。新生代主要存放新生成的对象,其特点是对象数量多但是生命周期短,在每次进行垃圾回收时都有大量的对象被回收;老年代主要存放大对象和生命周期长的对象,因此可回收的对象相对较少。因此,JVM根据不同的区域对象的特点选择了不同的算法。目前,大部分JVM在新生代都采用了复制算法,因为在新生代中每次进行垃圾回收时都有大量的对象被回收,需要复制的对象(存活的对象)较少,不存在大量的对象在内存中被来回复制的问题,因此采用复制算法能安全、高效地回收新生代大量的短生命周期的对象并释放内存。JVM将新生代进一步划分为一块较大的Eden区和两块较小的Servivor区,Servivor区又分为ServivorFrom区和ServivorTo区。JVM在运行过程中主要使用Eden区和ServivorFrom区,进行垃圾回收时会将在Eden区和ServivorFrom区中存活的对象复制到ServivorTo区,然后清理Eden区和ServivorFrom区的内存空间。 老年代主要存放生命周期较长的对象和大对象,因而每次只有少量非存活的对象被回收,因而在老年代采用标记清除算法。在JVM中还有一个区域,即方法区的永久代,永久代用来存储Class类、常量、方法描述等。在永久代主要回收废弃的常量和无用的类。JVM内存中的对象主要被分配到新生代的Eden区和ServivorFrom区,在少数情况下会被直接分配到老年代。在新生代的Eden区和ServivorFrom区的内存空间不足时会触发一次GC,该过程被称为MinorGC。在MinorGC后,在Eden区和ServivorFrom区中存活的对象会被复制到ServivorTo区,然后Eden区和ServivorFrom区被清理。如果此时在ServivorTo区无法找到连续的内存空间存储某个对象,则将这个对象直接存储到老年代。若Servivor区的对象经过一次GC后仍然存活,则其年龄加1。在默认情况下,对象在年龄达到15时,将被移到老年代。 5.2.5分区收集算法 分区算法将整个堆空间划分为连续的大小不同的小区域,对每个小区域都单独进行内存使用和垃圾回收,这样做的好处是可以根据每个小区域内存的大小灵活使用和释放内存。分区收集算法可以根据系统可接受的停顿时间,每次都快速回收若干个小区域的内存,以缩短垃圾回收时系统停顿的时间,最后以多次并行累加的方式逐步完成整个内存区域的垃圾回收。如果垃圾回收机制一次回收整个堆内存,则需要更长的系统停顿时间,长时间的系统停顿将影响系统运行的稳定性。 5.3 java中垃圾收集器 Java堆内存分为新生代和老年代:新生代主要存储短生命周期的对象,适合使用复制算法进行垃圾回收;老年代主要存储长生命周期的对象,适合使用标记整理算法进行垃圾回收。因此,JVM针对新生代和老年代分别提供了多种不同的垃圾收集器,针对新生代提供的垃圾收集器有Serial、ParNew、Parallel Scavenge,针对老年代提供的垃圾收集器有Serial Old、Parallel Old、CMS,还有针对不同区域的G1,ZGC分区收集算法。 1. Serial (新生代单线程复制算法) 针对新生代的垃圾回收器,它是单线程执行的,是一款串行的垃圾回收器,采用的是复制算法。它的单线程并不仅仅指它在进行垃圾回收时是单线程或者单处理器执行,更深的含义是它在垃圾回收时,需要暂停其他所有的线程,造成 STW。 当 JVM 处于客户端模式下时,Serial 是默认的垃圾回收器,它的优点是简单高效。在内存资源受限的环境下,Serial 垃圾回收器相比其他垃圾回收器,它所占用的内存更小。对于单处理器的场景,Serial 处理器由于是单线程的,它省去了线程之间的资源竞争,因此会更加高效。 当使用参数 「-XX:+UseSerialGC」时,在开启使用Serial垃圾回收器同时,老年代的垃圾回收器为Serial Old。 2.Serial Old(老年代单线程标记整理算法) 和 Serial 一样,Serial Old 也是单线程执行的,是一款串行的垃圾回收器,不同的是 Serail Old 回收的是老年代区域,采用的算法是标记-压缩(整理)算法。在进行垃圾回收时,同样也会造成 STW 的现象。 3.ParNew(新生代多线程复制算法) 针对新生代区域的垃圾回收器,它是 Serial 垃圾收集器的多线程版本,即它是一款并行的垃圾回收器,支持多个垃圾回收线程同时并行回收垃圾,使用的也是复制算法。ParNew 的大部分参数配置和 Serial 收集器一样,但额外多了部分参数,如:可以通过参数 「-XX:ParallelGCThreads」 来指定并行的垃圾回收的线程个数,默认情况下,垃圾回收线程的个数与处理器的个数相等。在单处理器的系统中,ParNew 的性能并不一定比 Serial 好,因为线程的切换需要额外耗费 CPU 资源。 可以使用参数 「-XX:+UseParNewGC」 来开启使用 ParNew 进行垃圾回收。 ParNew 可以和 Serial Old 或者 CMS 搭配使用,然而从 JDK9 开始,官方已经移除了 ParNew 和 Serial Old 的组合使用方式,同时 JDK9 中将 CMS 标记为 Deprecated 状态,在 JDK14 中彻底移除 CMS,这就导致了 ParNew 将处于一个十分尴尬的地位,在高版本中既不能和 Serial Old 搭配使用,也将在未来无法和 CMS 搭配使用,这就导致了 ParNew 这款垃圾回收器必然消失在历史的舞台。 4.Parallel Scavenge (新生代多线程复制算法) 针对新生代的并行的垃圾回收器,它和 ParNew 虽然都是并行、针对新生代,但是它们的区别很大,Parallel Scavenge 是一款「吞吐量优先」的垃圾回收器。适用于那些期望尽可能的利用 CPU 资源、尽快完成程序的运算任务以及不太注重用户交互行为的场景。 Parallel Scavenge 提供了两个参数来精准地控制吞吐量,分别是 「MaxGCPauseMillis」 和 「GCTimeRatio」。 MaxGCPauseMillis 表示的是每次进行 GC 时,系统的最大停顿时间,如果配置了该参数,那么 JVM 在每次进行垃圾回收时,它会尽可能的将停顿时间控制在 MaxGCPauseMillis 之内。该参数并不是配置的越小越好,如果配置得很小,那么 JVM 可能会为了达到停顿时间控制在 MaxGCPauseMillis 之内的目的,选择以减小新生代区域的大小为代价,毕竟每次回收 300M 的空间所花的时间肯定比 500M 的短。「而 JVM 将新生代的内存区域调小后,带来的后果就是垃圾回收进行得更加频繁了,最后会导致系统的吞吐量下降」。通常情况下,我们无法精准地把控每次垃圾回收需要停顿的时间,所以该参数需要慎用,一不小心,配置的不合理,可能适得其反。 GCTimeRatio 表示的是每次 GC 的时间占用的比率是多少(具体计算方是:GCTimeRatio = 用户线程运行时间/ GC 线程运行时间),例如:如果 GCTimeRatio 参数的值配置的 19,那么 GC 运行的时间占总时间的 5%(1/(1+19))。JVM 通过这个参数来达到控制系统吞吐量的目的。 另外 JVM 还提供了一个参数,叫做「UseAdpativeSizePolicy」,它表示的是让 JVM「根据系统的运行情况来动态调整」新生代(Eden、S0、S1)、老年代的大小,我们只需要设置好最基本的内存参数以及 MaxGCPauseMillis(最大停顿时间)或者 GCTimeRatio(目标吞吐量)即可,不需要设置-XX:Xmn(新生代的内存大小)、-XX:SurvivorRatio (Surivivior区域的比例)等参数了,JVM 会根据系统运行时监控到相关信息,来动态进行调整。Parallel Scavenge 支持动态调整策略,这也算是它和 ParNew 收集器的另一大不同之处了。 5.Parallel Old (老年代多线程标记整理算法) 收集器的老年代版本,也是支持多线程的并行执行,它底层是基于标记-压缩(整理)算法来实现的。在 JDK6 中才开始提供,在 Parallel Old 出现之前,Parallel Scavenge 收集器只能配合着 Serial Old 使用,无法与 CMS 垃圾回收器配合使用,这是因为 Parallel Scavenge 与 CMS、Serial、ParNew 这些收集器的底层框架不一样,无法兼容导致的。而 Serial Old 又是单线程的垃圾收集器,在多处理器的场景下,性能不高,白白浪费了 Parallel Scavenge 并行的优点,好车配劣马,所以在 Parallel Old 出现之前,Parallel Scavenge 一直处于比较鸡肋的地位。目前,Parallel Scavenge 和 Parallel Old 的组合,其垃圾回收效果不错,是 JDK8 中默认的垃圾回收组合方式。 6.CMS(老年代多线程标记清除算法) CMS 的全称是 Concurrent-Mark-Sweep 的缩写,翻译过来就是并发标记清除,它是一款「以低停顿时间为目标」的垃圾回收器,特点是低延时。 CMS 的工作原理大致分为四个步骤:初始标记、并发标记、重新标记、并发清除。 使用参数:「-XX:+UseConcMarkSweepGC」 即可开启使用 CMS 垃圾回收器。 「初始标记」指的是仅仅只标记出和 GC Roots 直接关联的对象,这个过程需要暂停所有的用户线程,因此会产生 STW。由于这一步仅仅标记和 GC Roots 直接关联的对象,因此这一步耗费的时间会很短,造成的停顿时间会很短。 「并发标记」。这一步是从和 GC Roots 直接关联的对象出发,开始遍历整个对象图引用链,这个过程是 GC 线程和用户线程并发执行的,因此不会造成 STW。这一步因为需要遍历所有对象的引用链,所以耗费时间较长,由于不会造成 STW,即使耗时较长,也没有关系。 「重新标记」。在并发标记阶段,用户线程仍然在运行,因此会改变对象之间的引用关系,那么在重新标记阶段,就是对并发标记的结果进行修正。把那些怀疑是垃圾,而实际不是垃圾的对象重新标记为存活对象。这一步需要暂停所有的用户线程,因此会造成 STW 的现象,这一步的耗时会比初始标记阶段长一些,但是远小于并发标记阶段的耗时。 「并发清除」。这一阶段是垃圾回收线程和用户线程一起并发执行,垃圾回收线程进行垃圾对象的清除,这一步耗时较长,但不会造成 STW。 整体上来看,CMS 垃圾回收器只有在初始标记阶段和重新标记阶段会造成用户线程的停顿,但是这两步都耗时较短,因此整体上,CMS 进行垃圾回收时,是低延时的。 7.G1(新生代和老年代多线程分区标记整理算法) G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。使用不同的 Region来表示Eden、S0区,S1区,Old区等。 独立使用这些区域的内存资源并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,在垃圾回收过程中根据系统允许的最长垃圾收集时间,优先回收垃圾最多的区域。 在JDK1.7正式启用,是JDK9以后默认的垃圾回收器,被Oracle官方称为“全功能的垃圾收集器” 优点: 1. 并行与并发 并行性:G1在回收期间,可以有多个GC线程同时工作(不再是一个GC线程),有效利用多核计算能力,此时用户线程处于STW 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况 2. 分区收集,支持新老代 同时兼顾年轻代和老年代。将堆空间分为若干个小区域(Region),这些区域中包含了逻辑上的年轻代和老年代。 3.可预测的停顿时间模型 回收时间可预测性,每次根据允许的时间优先回收价值最大的Region,尽可能提高收集效率 配置参数 -XX:+UseG1GC:手动指定使用G1收集器执行内存回收任务。 -XX:G1HeapRegionSize:设置每个Region的大小,大小区间只能是1M、2M、4M、8M、16M和32M , 如果G1HeapRegionSize为默认值,则在堆初始化时计算Region的实践大小 。 -XX:MaxGCPauseMillis:设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值是200ms -XX:ParallelGCThread:设置STW工作线程数的值,最多设置为8 -XX:ConcGCThreads:设置并发标记的线程数。 -XX:InitiatingHeapoccupancyPercent:设置触发并发GC周期的Java堆占用率阙值。超过此值,就触发GC。默认值是45。 文章:https://mp.weixin.qq.com/s/7CWbARimO5rFBHq4NtAt5Q 8.最前沿的低延时垃圾回收技术——ZGC,Shenandoah ZGC 全称为 Z Garbage Collector,一款在保证吞吐量的情况下,追求低延时的垃圾回收器。 ZGC 是目前垃圾回收器中最前沿的技术,可惜的是目前 ZGC 还没有被正式使用,一直处于实验状态(Experiment)。从 JDK11 开始,被加入到了 OpenJDK 中,到目前 2020 年 4 月份发布的最新 Oracle JDK14 中,ZGC 依旧处于实验状态。 可以通过添加 JVM 参数:-XX:+UnlockExperimentalVMOptions 进行解锁实验状态。 Shenandoah 的目标是将垃圾回收的停顿时间控制在 10ms 以内,这意味着 Shenandoah 不仅需要在并发标记阶段实现并发,还需要在标记清除阶段实现并发。 Shenandoah 垃圾回收器是 RedHat 公司发明的,非 Oracle 公司官方实现,不是 Oracle 的亲儿子,因此在一定程度上遭到了“排挤”,只在开源的 OpenJDK12 中开始出现,而在商业版的 Oracle JDK12 中则没有。 ZGC文章:https://mp.weixin.qq.com/s/FkG0iweym0q8gGDx2b8iMg Shenandoah文章:https://mp.weixin.qq.com/s/J9lOoihkfUKvJpt-7GSxXw 9.总结 随着JDK的不断更新,垃圾回收器的效率也越来越高,每一次JDK大版本的更新,必然会对垃圾回收器更新,截止到目前,JDK14可以使用的最新的垃圾回收器ZGC,在 JDK9 中,取消了 ParNew 与 Serial Old、Serial 与 CMS 的搭配组合,并且 CMS 被标记 Deprecated,在 JDK14 中被彻底移除。 评估GC性能的重要指标: 吞吐量:运行用户代码的时间占总运行时间的比例 暂停时间[STW]:执行GC线程时,用户线程被暂停的时间 内存占用:Java堆区所占的内存大小 吞吐量 吞吐量就是CPU运行用户代码的时间与CPU总消耗时间的比值,即: 吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间) 吞吐量高是降低了内存回收的执行频率 比如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞量就是99% 暂停时间 暂停时间是指一个时间段内应用程序线程暂停,让GC线程执行的状态,例如: GC期间100毫秒的暂停时间,意味着在这100毫秒期间内没有应用程序线程是活动的 暂停时间短,但是频繁的执行内存回收 这两个指标本质上是互斥的,我们只能在最大吞吐量优先的情况下,降低停顿时间。 想知道自己的GC算法,可以使用 java -XX:+PrintCommandLineFlags -version 查看 我的14默认的是G1 6 JVM的类加载机制 6.1 JVM的类加载阶段 JVM的类加载分为5个阶段:加载、验证、准备、解析、初始化。在类初始化完成后就可以使用该类的信息,在一个类不再被需要时可以从JVM中卸载。 1.加载 指JVM读取Class文件,并且根据Class文件描述创建java.lang.Class对象的过程。类加载过程主要包含将Class文件读取到运行时区域的方法区内,在堆中创建java.lang.Class对象,并封装类在方法区的数据结构的过程,在读取Class文件时既可以通过文件的形式读取,也可以通过jar包、war包读取,还可以通过代理自动生成Class或其他方式读取。 2.验证 主要用于确保Class文件符合当前虚拟机的要求,保障虚拟机自身的安全,只有通过验证的Class文件才能被JVM加载。 3.准备 主要工作是在方法区中为类的变量分配内存空间并设置不同数据类型的静态变量的默认值。 4.解析 JVM会将常量池中的符号引用替换为直接引用。 5.初始化 初始化阶段,执行类构造器<clinit>()方法的过程 <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。 JVM规定,只有在父类的<client>方法都执行成功后,子类中的<client>方法才可以被执行。 在一个类中既没有静态变量赋值操作也没有静态语句块时,编译器不会为该类生成<client>方法。 哪些情况会进行类的初始化?主动引用 1.创建类的实例 2.访问类的静态变量 3.访问类的静态方法 4.反射(Class.forName) 5.子类初始化会先对父类初始化 6.虚拟机启动时,定义了main()方法的会先初始化 哪些情况不会进行类初始化?被动引用 1. 子类调用父类的静态变量,子类不会被初始化。只有父类被初始化。对于静态字段,只有直接定义这个字段的类才会被初始化. 2. 通过数组定义来引用类,不会触发类的初始化 3. 访问类的常量,不会初始化类 对于类的初始化我们搞点小demo瞧瞧,上东西~~ 6.2 类加载器 JVM提供了3种类加载器,分别是启动类加载器、扩展类加载器和应用程序类加载器。 (1)启动类加载器:负责加载JAVA_HOME/lib目录中的类库,或通过-Xbootclasspath参数指定路径中被虚拟机认可的类库。 (2)扩展类加载器:负责加载JAVA_HOME/lib/ext目录中的类库,或通过java.ext.dirs系统变量加载指定路径中的类库。 (3)应用程序类加载器:负责加载用户路径(classpath)上的类库。 除了上述3种类加载器,我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。 6.3 双亲委派机制 JVM通过双亲委派机制对类进行加载。双亲委派机制指一个类在收到类加载请求后不会尝试自己加载这个类,而是把该类加载请求向上委派给其父类去完成,其父类在接收到该类加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中。若父类加载器在接收到类加载请求后发现自己也无法加载该类(通常原因是该类的Class文件在父类的类加载路径中不存在),则父类会将该信息反馈给子类并向下委派子类加载器加载该类,直到该类被成功加载,若找不到该类,则JVM会抛出ClassNotFoud异常。双亲委派类加载机制的类加载流程如下。 (1)将自定义加载器挂载到应用程序类加载器。 (2)应用程序类加载器将类加载请求委托给扩展类加载器。 (3)扩展类加载器将类加载请求委托给启动类加载器。 (4)启动类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由扩展类加载器加载。 (5)扩展类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由应用程序类加载器加载。 (6)应用程序类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由自定义加载器加载。 (7)在自定义加载器下查找并加载用户指定目录下的Class文件,如果在自定义加载路径下未找到目标Class文件,则抛出ClassNotFoud异常。双亲委派机制的核心是保障类的唯一性和安全性。例如在加载rt.jar包中的java.lang.Object类时,无论是哪个类加载器加载这个类,最终都将类加载请求委托给启动类加载器加载,这样就保证了类加载的唯一性。如果在JVM中存在包名和类名相同的两个类,则该类将无法被加载,JVM也无法完成类加载流程。

优秀的个人博客,低调大师

Kubernetes Ingress简单入门

本文转载自Rancher Labs 不知道你是否注意到一个奇怪的现象,尽管Kubernetes Ingress API仍然处于beta状态,但是已经有许多公司使用它来暴露Kubernetes服务。从事相关项目的工程师表示,Kubernetes Ingress API越来越有可能摘下其beta标签。实际上,Kubernetes Ingress API处于beta状态已经持续了几年的时间,准确来说,是在2015年秋季开始进入该阶段的。但是,漫长的beta阶段可以让Kubernetes贡献者有时间来完善规范并使其与已经搭建好的实施软件(HAProxy、NGINX、Traefik等)保持一致,从而使API标准化以反映最常见并且有需求的功能。 随着该功能GA的临近,那么现在应该是一个合适的时机可以帮助新手快速了解Ingress的工作方式。简而言之,Ingress是一个规则,可以绘制出在集群内部的服务如何弥合鸿沟,暴露到客户可以使用它的外部世界。同时,称为Ingress controller的代理在集群网络的边缘进行侦听(监视要添加的规则),并将每个服务映射到特定的URL路径或域名以供公众使用。在Kubernetes维护者开发API的同时,其他开源项目也实现了Ingress Controller并为其代理添加了自己的独特功能。 在本文中,我将介绍这些概念,并帮助你了解Ingress模式背后的驱动力。 路由问题 在Kubernetes中创建Pod时,需要为其分配selector标签,如Deployment manifest的以下片段所示: 该Deployment创建了运行Docker镜像my-app的三个副本,并为其分配app=foo标签。除了直接访问Pod,通常将它们分组在Service下,这使它们可以在单个集群IP地址上使用(但是只能在同一集群中使用)。Service充当抽象层,隐藏了pod的周期短暂特性,可以随时增加或减少或替换它们。它还可以执行基本的循环负载均衡。 例如,以下Service定义收集所有带有selector标签app = foo的Pod,并在其中平均路由流量。 但是,只能从集群内部以及运行在附近的其他Pod访问此服务。Kubernetes Operator正在努力解决如何为集群外部的客户端提供访问权限。该问题在早期就已经出现,并且将两种机制直接集成到Service规范中进行处理。编写service manifest时,包括一个名为type的字段,该字段的值为NodePort或LoadBalancer。这是一个将类型设置为NodePort的示例: NodePort类型的服务使用起来很简单。本质上,这些服务希望Kubernetes API为他们分配一个随机的TCP端口,并将其暴露到集群之外。这样做的方便之处在于,客户端可以使用该端口将集群中的任何节点作为目标,并且他们的消息将被中转到正确的位置。这就类似于你可以拨打美国境内的任何电话,而接听电话的人都会确保为你转接到合适的人。 缺点在于,该端口的值必须介于30000到32767之间,虽然这个范围安全地避开了常用端口地范围,但是与常见的HTTP端口80和HTTPS 443相比,该端口显然不是很标准。此外,随机性本身也是一个障碍,因为它意味着你事先不知道值是什么,这使得配置NAT、防火墙规则更具挑战性——尤其是需要为每项服务设置不同的随机端口。 另一个选项是将类型设置为LoadBalancer。但是,这有一些前提条件——仅当你在GKE或EKS之类的云托管环境中运行并且可以使用该云供应商的负载均衡器技术时,它才有效,因为它是自动选择并配置的。其缺点是比较昂贵,因为使用这种类型的服务会为每个服务启动一个托管的负载均衡器以及一个新的公共IP地址,这会产生额外的费用。 Ingress路由 分配一个随机端口或外部负载均衡器是很容易操作的,但也带来了独特的挑战。定义许多NodePort服务会造成随机端口混乱,而定义许多负载均衡器服务会导致需要支付比实际所需更多的云资源费用。这些情况不可能完全避免,但也许可以减少它的使用范围,甚至你只需要分配1个随机端口或1个负载均衡器就能够暴露许多内部服务。因此,这一平台需要一个新的抽象层,该层可以在入口点(entrypoint)后面整合许多服务。 那时,Kubernetes API引入了一种称为Ingress的新型manifest,它为路由问题提供了新的思路。它的工作方式是这样的:你编写一个Ingress manifest,声明你希望客户端如何路由到服务。manifest实际上并不自行执行任何操作,你必须将Ingress Controller部署到你的集群中,以监视这些声明并对其执行操作。 与其他任何应用程序一样,Ingress controller是Pod,因此它们是集群的一部分并且可以看到其他Pod。它们是使用在市场上已经发展了多年的反向代理搭建的,因此,你可以选择HAProxy Ingress Controller、NGINX Ingress Controller等。底层代理为其提供了第7层路由和负载均衡功能。不同的代理将自己的功能集放到表中。例如,HAProxy Ingress Controller不需要像NGINX Ingress Controller那样频繁地重新加载,因为它为服务器分配了slot,并使用Runtime API在运行时填充slot。这使得该Ingress Controller拥有更好的性能。 Ingress Controller本身位于集群内部,与其他Kubernetes Pod一样,也容易受到同一“监狱”的“监禁”。你需要通过NodePort或LoadBalancer类型的服务将它们暴露到外部。但是,现在你只有一个入口点,所有流量都将通过此处:一个服务连接到一个Ingress Controller,Ingress Controller依次连接到许多内部Pod。Controller具有检查HTTP请求的功能,可以根据其发现的特征(例如URL路径或域名)将客户端定向到正确的Pod。 参考这个Ingress的示例,该示例定义了URL路径/foo应该如何连接到名为foo-service的后端服务,而URL路径/bar被定向到名称为bar-service的服务。 如上文所示,你依旧需要为你的Pod设置服务,但是你不需要在Pod上设置类型字段,因为路由和负载均衡将由Ingress层处理。服务的作用被简化为以通用名称对Pod进行分组。最终,两个路径,/foo和/bar,将由一个公共IP地址和域名提供服务,例如example.com/foo 和 example.com/bar 。本质上,这是API网关模式,在API网关中,单个地址将请求路由到多个后端应用程序。 添加Ingress Controller Ingress manifest的声明式方法是你可以指定所需的内容,而无需知道如何实现。Ingress Controller的工作之一是执行,它需要监控新的ingress规则并配置其底层代理以制定相应的路由。 你可以使用Kubernetes包管理工具Helm安装HAProxy Ingress Controller。首先,通过下载Helm二进制文件并将其复制到PATH环境变量中包含的文件夹(例如/usr/local/bin/)中来安装Helm。接下来,添加HAProxy Technologies Helm库,并使用helm install命令部署Ingress Controller。 通过运行命令kubectl get service列出所有正在运行的服务来验证是否已创建Ingress Controller。 HAProxy Ingress Controller在集群的pod中运行,并使用NodePort类型的Service资源发布对外部客户端的访问。在上面显示的输出中,你可以看到为HTTP选择了端口31704,为HTTPS选择了端口32255。你还可以在端口30347上查看HAProxy信息统计页面。HAProxy Ingress Controller会提供有关流经它的流量的详细指标,因此你可以更好地观察到进入集群的流量。 在controller创建类型为NodePort的服务时,这意味着需要分配一个随机的端口并且端口编号往往很高,但是现在你只需管理几个此类端口,也就是只需管理连接到Ingress Controller的端口,无需再为每个服务创建一个端口。你也可以将其配置为使用LoadBalancer类型,只要在云端进行操作即可。它看起来如下: 总体而言,不需要管理太多Ingress Controller。安装后,它基本上会在后台执行其工作。你只需要定义Ingress manifest,controller就会立即将它们连接起来。Ingress manifest的定义与引用的服务有所区别,因此你可以控制何时暴露服务。 结 论 Ingress资源通过允许API网关样式的流量路由,整合了外部客户端如何访问Kubernetes集群中的服务。代理服务通过公共入口点(entrypoint)进行中转,你可以使用intent-driven、YAML声明来控制何时以及如何公开服务。 当Ingress API这一功能GA之后,你一定会看到这种模式变得越来越流行。当然,可能产生一些细微的变化,主要是为了使API与现有controller中已经实现的功能保持一致。其他改进可能会指导controller如何继续发展以符合Kubernetes维护者的愿景。总而言之,现在是开始使用此功能的好时机!

优秀的个人博客,低调大师

MaxCompute 入门必知

一、什么是MaxCompute? 大数据计算服务(MaxCompute,原名ODPS)是一种快速、完全托管的EB级数据仓库解决方案。 随着数据收集手段不断丰富,行业数据大量积累,数据规模已增长到了传统软件行业无法承载的海量数据(百TB、PB、EB)级别。MaxCompute致力于批量结构化数据的存储和计算,提供海量数据仓库的解决方案及分析建模服务。 由于单台服务器的处理能力有限,海量数据的分析需要分布式的计算模型。分布式的计算模型对数据分析人员要求较高且不易维护。数据分析人员不仅需要了解业务需求,同时还需要熟悉底层分布式计算模型。MaxCompute为您提供完善的数据导入方案以及多种经典的分布式计算模型,您可以不必关心分布式计算和维护细节,便可轻松完成大数据分析。 二、概述与产品特点 1、采用分布式架构,规模可以根据需要平行扩展2、自动存储容错机制,保障数据高可靠性3、所有计算在沙箱中运行,保障数据高安全性4、以 RESTful API 的方式提供服务5、支持高并发、高吞吐量的数据上传下载6、提供离线计算、机器学习两类模型及计算服务7、支持基于 SQL、MapReduce、Graph、MPI 等多种编程模型的数据处理方式8、支持多租户,多个用户可以协同分析数据9、支持基于 ACL 和 Policy 的用户权限管理,可以配置灵活的数据访问控制策略,防止数据越权访问 三、主要应用场景 日志分析、机器学习、数据仓库、数据挖掘、用户画像、数据化运营、商业智能等,例如: 四、MaxCompute 系统架构 五、MaxCompute 作业流程

优秀的个人博客,低调大师

Dart 入门教程

[TOC] 一、开篇 dart 语言具有如下特性 一切变量皆是对象,每个对象都是类的实例。int、double、函数、 null 等都是对象,所有对象都继承自 Object 类 dart 是强类型语言,但由于具备类型推导功能所以类型声明是可选的 dart 支持顶级函数、静态函数、实例函数,也允许在函数中嵌套函数,即局部函数。类似的,dart 也支持顶级变量、静态变量和实例变量 dart 没有关于 public、protected、private 的关键字。通过为变量标识符添加下划线前缀,表明该标识符对其库是私有的 .... 先来看个小例子 /** * 多行注释 */ void printString(String msg) { print("msg value: $msg"); } void main() { var msg = "H

优秀的个人博客,低调大师

Kubernetes - 1 入门介绍

什么是Kubernetes Kubernetes这个名字起源于希腊语,意思是舵手,由于k到s之间有8个字符又简称k8s。Google在2014年开源了Kubernetes项目,基于容器技术的分布式管理系统,在Docker技术的基础上,为容器化的应用提供部署运行、资源调度、服务发现和动态伸缩等一系列完整功能,提高了大规模容器集群管理的便捷性。 为什么要使用Kubernetes 轻量:与使用VM虚拟机相比,容器镜像创建的简便性和效率更高。 敏捷:持续集成和部署,通过快速的升级或回滚,提供可靠且频繁的容器映像构建和部署。 分离:在构建时创建镜像,不依赖发布环境,将运行环境及基础架构分离。 一致:开发环境、测试环境、生产环境的运行环境一致性。 移植:不依赖基础架构,可在不同的平台上快速应用。 隔离:通过NameSpace实现不同环境中、不同租户的资源隔离。 解耦:应用程序被分解成较小的独立部分,而不是整体在一台单机上运行。 伸缩:可将应用基于资源利用率等进行快速的动态伸缩。 效率:高密度及高效率的资源利用率。 Kubernetes架构 Kubernetes集群中有2种角色,一个是Master作为集群的管理节点,负责管理集群,提供集群的资源数据访问入口,另外一个是Node用来承载被分配Pod的运行,是Pod运行的宿主机。 Kubernetes Master apiserver 提供了资源操作的唯一入口,并提供认证、授权、访问控制、API 注册和发现等机制controller manager 负责维护集群的状态,比如故障检测、自动扩展、滚动更新等scheduler 负责资源的调度,按照预定的调度策略将 Pod 调度到相应的机器上etcd 保存了整个集群的状态 Kubernetes Node kubelet 负责维护容器的生命周期,同时也负责 Volume和 Network 的管理Container runtime 负责镜像管理以及 Pod 和容器的真正运行时kube-proxy 负责为 Service 提供 cluster 内部的服务发现和负载均衡 除了核心组件之外的其他组件 kube-dns 负责为整个集群提供 DNS 服务Ingress Controller 为服务提供外网入口Heapster 提供资源监控Dashboard 提供 GUIFederation 提供跨可用区的集群Fluentd-elasticsearch 提供集群日志采集、存储与查询 Kubernetes资源和对象 Kubernetes把所有内容都抽象为资源,把资源实例化出来后称为对象。 workload: 工作负载相关Pod, ReplicaSet, Deployment, StatefulSet, DaemonSet, Job, Cronjob, ReplicationController service discover and balance: 服务发现及均衡相关Service, Ingress config and storage: 配置及存储相关ConfigMap, Secret, Volumn, CSI cluster resouce: 集群级资源Namespace, Node, Role, ClusterRole, RoleBinding, ClusterRoleBinding metadata resource: 元数据级资源HPA, PodTemplate, LimitRange

优秀的个人博客,低调大师

SBT无痛入门指南

SBT是 Scala 的构建工具,全称是 Simple Build Tool, 类似 Maven 或 Gradle。 SBT 的野心很大,采用Scala编程语言本身编写配置文件,这使得它稍显另类,虽然增强了灵活性,但是对于初学者来说同时也增加了上手难度。另外由于SBT默认从国外下载依赖,导致第一次构建非常缓慢,使用体验非常糟糕! 如果你是一名Scala初学者,本文希望帮你减轻一些第一次使用的痛苦。 本文的主要内容是帮助初学者从头到尾构建并运行一个Scala项目,重点在于讲解国内镜像仓库的配置。对于每一个操作步骤,会分别针对Windows、Mac和Linux三个主流操作系统进行讲解, 最终帮助你快速构建一个可运行的Scala开发环境。 第一步:安装SBT 单击这里下载SBT 1.3.0,下载完成后解压到指定目录,例如D:\Software\sbt-1.3.0,然后将D:\Software\sbt-1.3.0\bin添加至环境变量PATH。SBT 1.3.0 采用Coursier以无锁的方式并行下载依赖,极大地提升了使用体验! 请确认本机已安装Java运行环境。 第二步:设置国内仓库,加快构建过程 1) 设置全局仓库 首先创建.sbt目录。 如果是Windows系统,则进入CMD执行如下命令: cd C:\Users\USER_NAME mkdir .sbt cd .sbt 如果是Mac或Linux系统,则进入Bash执行如下命令: cd ~ mkdir .sbt cd .sbt 然后创建repositories文件内容如下,并将文件拷贝到 .sbt 目录下, [repositories] local huaweicloud-maven: https://repo.huaweicloud.com/repository/maven/ maven-central: https://repo1.maven.org/maven2/ sbt-plugin-repo: https://repo.scala-sbt.org/scalasbt/sbt-plugin-releases, [organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[revision]/[type]s/[artifact](-[classifier]).[ext] 2) 设置所有项目均使用全局仓库配置,忽略项目自身仓库配置 该参数可以通过Java System Property进行设置。在 SBT 中,有三种方法可以设置Java System Property,可以根据需要自行选择。 方法一:修改SBT配置文件(推荐) 提醒一下, sbt-1.3.0/conf/目录下有两个配置文件, sbtconfig.txt仅适用于 Windows平台,而 sbtopts仅适用于 Mac/Linux平台。 针对Windows平台,打开sbt-1.3.0/conf/sbtconfig.txt 文件,在末尾新增一行,内容如下: -Dsbt.override.build.repos=true 针对Mac/Linux平台,打开sbt-1.3.0/conf/sbtopts 文件,在末尾新增一行,内容如下: -Dsbt.override.build.repos=true 方法二: 设置环境变量 在Windows上通过set命令进行设置, set SBT_OPTS="-Dsbt.override.build.repos=true" 在Mac/Linux上使用export命令进行设置, export SBT_OPTS="-Dsbt.override.build.repos=true" 方法三:传入命令行参数 执行 sbt 命令时, 直接在命令后面加上配置参数, sbt -Dsbt.override.build.repos=true 注意,如果由于某种原因, repositories文件并不在默认的 .sbt目录下,则需要通过 -Dsbt.repository.config指定 repositories文件的具体位置,该参数的三种设置方法同 -Dsbt.override.build.repos。例如采用修改SBT配置文件方式 (推荐),则打开 sbt-1.3.0\conf\sbtconfig.txt 文件,在末尾新增如下内容: -Dsbt.override.build.repos=true -Dsbt.repository.config=path_to_repositories_file 第三步:构建并运行第一个Scala项目 1)修改项目SBT构建版本 单击hello-scala下载一个最简单的Scala项目,并解压到指定目录,如D:\idea-projects。由于SBT 1.3.0包含了多项性能提升,如果是已有的本地项目,请手动将项目的SBT构建版本改成1.3.0 。具体方法为:打开project/build.properties文件,将内容修改如下: sbt.version = 1.3.0 在命令行中切换至hello-scala目录,执行sbt命令进入sbt shell, 第一次进入 sbt shell时,由于需要下载相关依赖,大概需要几十秒时间,第二次及以后进入 sbt shell会很快。 检查当前项目的SBT构建版本是否为1.3.0, sbt:hello-scala> sbtVersion [info] 1.3.0 sbt:hello-scala> 2)确认全局仓库是否已经覆盖项目自身仓库 sbt:hello-scala> show overrideBuildResolvers [info] true 确认仓库列表是否与~/.sbt/repositories文件一致: sbt:hello-scala> show fullResolvers 3)编译并运行 确认无误后执行编译命令, sbt:hello-scala> compile [info] Compiling 1 Scala source to D:\idea-projects\hello-scala\target\scala-2.12\classes ... [success] Total time: 4 s, completed 2019-9-16 11:39:41 查看SBT本地缓存,确认一下是否从国内仓库下载依赖。针对不同的操作系统,对应的缓存路径如下: Windows缓存路径是%LOCALAPPDATA%\Coursier\Cache\v1,即如果用户名是joymufeng,则完整路径是C:\Users\joymufeng\AppData\Local\Coursier\Cache\v1。 Linux缓存路径为~/.cache/coursier/v1。 Mac缓存路径为~/Library/Caches/Coursier/v1 。 下面是Windows缓存截图: 最后执行项目,

优秀的个人博客,低调大师

Dubbo实战(一) - 入门

1 背景 随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进。 1.1 单一应用架构 当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。 此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。 1.2 垂直应用架构 当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。 1.3 分布式服务架构 当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。 此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。 1.4 流动计算架构 当

优秀的个人博客,低调大师

Docker入门-数据挂载

Docker数据管理 在容器中管理数据主要有两种方式: 数据卷(Volumes) 挂载主机目录(Bind mounts) 数据卷 数据卷是一个可供一个或多个容器使用的特殊目录,它绕过UFS,可以提供很多有用的特性: 数据卷可以在容器之间共享和重用 对数据卷的修改会立马生效 对数据卷的更新,不会影响镜像 数据卷默认会一直存在,即使容器被删除 注意: 数据卷的使用,类似于Linux下对目录或文件进行mount,镜像中的被指定为挂载点的目录中的文件会隐藏掉,能显示看的是挂载的数据卷。 Docker中提供了两种挂载方式,-v和-mount Docker新用户应该选择 --mount参数 经验丰富的Docker使用者对-v或者--volume已经很熟悉了,但是推荐使用-mount参数。 创建一个数据卷 docker volume create my-volume 查看指定数据卷的信息 docker volume inspect my-volume 启动一个挂载数据卷的容器: 在用docker run命令的时候,使用--mount标记来将数据卷挂载到容器里。 创建一个名为session-web的容器,并加载一个数据卷到容器中的/webapp目录。 # 方法一 docker run --name session-web -d -p 8888:8080 --mount source=my-volume,target=/webapp session-web:latest # 方法二 docker run --name session-web -d -p 8888:8080 -v my-volume:/webapp session-web:latest 删除数据卷 docker volume rm my-volume 数据卷是被设计用来持久化数据的,它的生命周期独立于容器,Docker不会在容器被删除后自动删除数据卷,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的数据卷。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用docker rm -v这个命令。 无主的数据卷可能会占据很多空间,要清理请使用以下命令 docker volume prune 挂载主机目录 使用--mount标记可以指定挂载一个本地主机的目录到容器中去 # 方法一 docker run --name session-web -d -p 8888:8080 \ -v my-volume:/webapp \ session-web:latest # 方法二 docker run --name session-web -d -p 8888:8080 \ --mount type=bind,source=/src/webapp,target=/opt/webapp session-web:latest 上面的命令加载主机的/src/webapp目录到容器的/opt/webapp目录。这个功能在进行测试的时候十分方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。 本地目录的路径必须是绝对路径 以前,使用-v参数时如果本地目录不存在Docker会自动为你创建一个文件夹。 现在,使用--mount参数时如果本地目录不存在,Docker会报错。Docker挂载主机目录的默认权限是读写,用户也可以通过增加readonly指定为只读。 挂载一个本地主机文件作为数据卷 --mount标记也可以从主机挂载单个文件到容器中 # 方法一 docker run --rm -it \ --mount type=bind,source=#HOME/.bash_history,target=/root/.bash_history \ ubuntu:17.10 bash # 方法二 docker run --rm -it \ -v $HOME/.bash_history:/root/.bash_history \ ubuntu:17.10 bash

优秀的个人博客,低调大师

大数据入门干货

首先,提及大数据一词的概念。大数据本质还在于数据,但是它有着新的特征亮点。包括:数据来源广,数据格式多样化(结构化数据、非结构化数据、Excel文件、文本文件等)、数据量大(最少也是TB级别的、甚至可能是PB级别)、数据增长速度快等等。 扩展讲大数据的4个基本特征,我们将进行下面额思考: 1.数据来源广? 广泛的数据来源从何而来,通过何种方式进行采集与汇总?相对应的我们出现Sqoop, Cammel,Datax等工具。 2.数据采集之后,该如何存储? 采集之后,为了方便存储,我们对应的出现了GFS,HDFS,TFS等分布式文件存储系统。 并且,介于数据的增长速度非常之快,这也就要求我们,数据存储必须可以进行水平扩展。 3.数据存储之后,该如何通过运算快速转化成一致的格式,又该如何快速运算出自己想要的结果? 对此,MapReduce的分布式运算框架提出了解决方案。但是由于MapReduce需要java的编程代码较多,由此,又出现了Hive,Pig等将SQL转化成MapReduce的解析引擎;再有,因普通的MapReduce只能一批一批地批量处理数据,时间消耗过多,最终目的又要求我们输入的一条数据就能得到结果,于是又出现了Storm/JStorm这样的低时延的流式计算框架;但是如果同时需要批处理和流处理,按照如上就得搭两个集群,Hadoop集群(包括HDFS+MapReduce+Yarn)和Storm集群,不易于管理,所以出现了Spark这样的一站式的计算框架,既可以进行批处理,又可以进行流处理(实质上是微批处理)。而后Lambda架构,Kappa架构的出现,又提供了一种业务处理的通用架构。 4.此外,为了提高工作效率,加快运速度,出现了一些辅助工具: Ozzie,azkaban:定时任务调度的工具。 Hue,Zepplin:图形化任务执行管理,结果查看工具。 Scala语言:编写Spark程序的最佳语言,当然也可以选择用Python。 Python语言:编写一些脚本时会用到。 Allluxio,Kylin等:通过对存储的数据进行预处理,加快运算速度的工具。

优秀的个人博客,低调大师

Spring Cloud Gateway 入门

文章首发于公众号《程序员果果》地址:https://mp.weixin.qq.com/s/wRwq99fNEW4gqgHvR9a-gQ 简介 Spring Cloud Gateway ,相比之前我们使用的 Zuul(1.x) 它有哪些优势呢?Zuul(1.x) 基于 Servlet,使用阻塞 API,它不支持任何长连接,如 WebSockets。Spring Cloud Gateway 使用非阻塞 API,支持 WebSockets,支持限流等新特性。本文首先用官方的案例带领大家来体验下Spring Cloud的一些简单的功能。 创建工程 创建工程springcloud工程,名为springcloud-gateway-hello pom.xml 内容如下: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.gf</groupId> <artifactId>springcloud-gateway-hello</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springcloud-gateway-hello</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>Greenwich.RELEASE</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories> </project> 创建一个简单的路由 Spring Cloud Gateway 使用路由来处理对下游服务的请求。创建RouteLocator的Bean,在本案例将把所有请求路由到 http://httpbin.org。路由可以通过多种方式配置: @SpringBootApplication public class SpringcloudGatewayHelloApplication { public static void main(String[] args) { SpringApplication.run( SpringcloudGatewayHelloApplication.class, args ); } @Bean public RouteLocator myRoutes(RouteLocatorBuilder builder) { return builder.routes() .route(p -> p .path("/get") .filters(f -> f.addRequestHeader("Hello", "World")) .uri("http://httpbin.org:80")) .build(); } } 上述myRoutes方法RouteLocatorBuilder可以很容易地用于创建路由。除了创建路由之外,RouteLocatorBuilder还允许你在路由中添加各种 predicates(断言) 和 filters,以便根据特定条件更改请求和响应。 上面创建的route可以让请求“/get”请求都转发到“http://httpbin.org/get”。在route配置上,我们添加了一个filter,该filter会将请求添加一个header,key为hello,value为world。 启动项目,访问http://127.0.0.1:8080/get,显示如下: { "args": { }, "headers": { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "zh-CN,zh;q=0.9,zh-TW;q=0.8", "Cache-Control": "max-age=0", "Connection": "close", "Forwarded": "proto=http;host=\"127.0.0.1:8080\";for=\"127.0.0.1:55607\"", "Hello": "World", "Host": "httpbin.org", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", "X-Forwarded-Host": "127.0.0.1:8080" }, "origin": "127.0.0.1, 124.74.78.150", "url": "http://127.0.0.1:8080/get" } 可见当向gateway工程请求“/get”,gateway会将工程的请求转发到“http://httpbin.org/get”,并且在转发之前,加上一个filter,该filter会将请求添加一个header,key为hello,value为world。 使用Hystrix 在spring cloud gateway中可以使用Hystrix。Hystrix是 spring cloud中一个服务熔断降级的组件,在微服务系统有着十分重要的作用。Hystrix 在 spring cloud gateway中是以filter的形式使用的,代码如下: @Bean public RouteLocator myRoutes(RouteLocatorBuilder builder) { return builder.routes() .route(p -> p .path("/get") .filters(f -> f.addRequestHeader("Hello", "World")) .uri("http://httpbin.org:80")) .route(p -> p .host("*.hystrix.com") .filters(f -> f.hystrix(config -> config .setName("mycmd") .setFallbackUri("forward:/fallback"))) .uri("http://httpbin.org:80")) .build(); } 在上面的代码中,我们使用了另外一个router,该router使用host去断言请求是否进入该路由,当请求的host为 “*.hystrix.com”,都会进入该router,该router中有一个hystrix的filter,该filter可以配置名称、和指向性fallback的逻辑的地址,比如本案例中重定向到了“/fallback”。 现在写的一个“/fallback”的l逻辑: @RequestMapping("/fallback") public String fallback() { return "fallback"; } 使用curl执行以下命令: curl --dump-header - --header 'Host: www.hystrix.com' http://localhost:8080/delay/3 返回的响应为: HTTP/1.1 200 OK Content-Type: text/plain;charset=UTF-8 Content-Length: 8 fallback 可见,带host www.hystrix.com 的请求执行了hystrix的fallback的逻辑。 源码 https://github.com/gf-huanchupk/SpringCloudLearning/tree/master/chapter13/springcloud-gateway-hello

优秀的个人博客,低调大师

Prometheus入门+grafana集成

开始使用Prometheus $ systemctl start prometheus $ netstat -lntp tcp6 0 0 :::9090 :::* LISTEN 19824/./prometheus 在浏览器访问:http://ip:9090/graph 。Prometheus会把自身作为一个项目进行自监控,查看收集到监控项:http://172.16.180.129:9090/metrics (如果是首次启动,需要等待30s左右的时间) 使用内置表达式查看数据 地址:http://ip:9090/graph Prometheus内置监控项 prometheus_target_interval_length_seconds ,将该监控项直接输入console查询,可获取数据: ![image-20190322132500188](/Users/adai/Library/Application Support/typora-user-images/image-20190322132500188.png) 使用prometheus监控服务器 上面用Prometheus本身的数据简单演示了监控数据的查询,这里我们用一个监控服务器状态的例子来更加直观说明。 为监控服务器CPU、内存、磁盘、I/O等信息,首先需要安装node_exporter。node_exporter的作用是用于机器系统数据收集。 安装node_exporter node_exporter也是用Golang实现,直接使用预编译的二进制文件部署,开箱即用。 $ cd /home/prometheus && wget https://github.com/prometheus/node_exporter/releases/download/v0.17.0/node_exporter-0.17.0.linux-amd64.tar.gz $ tar zxvf node_exporter-0.17.0.linux-amd64.tar.gz $ mv node_exporter-0.17.0.linux-amd64 /usr/local/prometheus/node_exporter 创建systemd服务 $ vim /usr/lib/systemd/system/node_exporter.service [Unit] Description=node_exporter After=network.target [Service] Type=simple User=prometheus ExecStart=/usr/local/prometheus/node_exporter/node_exporter Restart=on-failure [Install] WantedBy=multi-user.target 启动node_exporter $ systemctl start node_exporter $netstat -lntp Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp6 0 0 :::9100 :::* LISTEN 24126/node_exporter 修改prometheus.yml,加入下面的监控目标(node_exporter默认的抓取地址为http://ip:9100) - job_name: 'linux' static_configs: - targets: ['localhost:9100'] labels: instance: node1 说明:prometheus.yml中一共定义了两个监控,一个是监控prometheus自身服务,另一个是监控Linux服务器。 这里给一个完整示例: scrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090'] - job_name: 'linux' static_configs: - targets: ['localhost:9100'] labels: instance: node1 重启prometheus服务 $ systemctl restart prometheus 在Prometheus Web查看监控的目标:访问Prometheus Web,在Status->Targets页面下,我们可以看到我们配置的两个Target(linux和prometheus),它们的State为UP。 查看memory使用情况 ![image-20190322134529082](/Users/adai/Library/Application Support/typora-user-images/image-20190322134529082.png) Prometheus Web界面自带的图表是非常基础的,比较适合用来做测试。如果要构建强大的Dashboard,还是需要更加专业的工具才行。接下来我们将使用Grafana来对Prometheus采集到的数据进行可视化展示。 prometheus集成grafana 在Grafana中添加Prometheus数据源:Configuration——DataSource——"add new DataSource"——Prometheus http URL:http://localhost:9090 access:server 上述配置完成后需要导入node-exporter-server-metrics 的数据模板到grafana,两种导入方法: 通过url导入:grafana——菜单栏加号➕——import—— 输入URL https://grafana.com/dashboards/405 —— 导入——Options (Name、Folder、Prometheus) —— import 下载后导入:grafana——菜单栏加号➕——import—— 输入 下载 好的json文件 ——import 完成上述操作后即可看到node-exporter采集的数据。 在Dashboards上选Node Exporter Server Metrics模板,就可以看到被监控服务器的CPU, 内存, 磁盘等统计信息。 在Dashboards上选Prometheus Status模板,查看Prometheus各项指标数据。

优秀的个人博客,低调大师

RocketMQ入门

RocketMQ整体结构 如上图所示,整体可以分成4个角色,分别是:Producer,Consumer,Broker以及NameServer; 1.NameServer 可以理解为是消息队列的协调者,Broker向它注册路由信息,同时Client向其获取路由信息,如果使用过Zookeeper,就比较容易理解了,但是功能比Zookeeper弱; NameServer本身是没有状态的,并且多个NameServer直接并没有通信,可以横向扩展多台,Broker会和每一台NameServer建立长连接; 2.Broker Broker是RocketMQ的核心,提供了消息的接收,存储,拉取等功能,一般都需要保证Broker的高可用,所以会配置Broker Slave,当Master挂掉之后,Consumer然后可以消费Slave; Broker分为Master和Slave,一个Master可以对应多个Slave,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave; 3.Producer 消息队列的生产者,需要与NameServer建立连接,从NameServer获取Topic路由信息,并向提供Topic服务的Broker Master建立连接;Producer无状态,看集群部署; 4.Consumer 消息队列的消费者,同样与NameServer建立连接,从NameServer获取Topic路由信息,并向提供Topic服务的Broker Master,Slave建立连接; 5.Topic和Message Queue 在介绍完以上4个角色以后,还需要重点介绍一下上面提到的Topic和Message Queue;字面意思就是主题,用来区分不同类型的消息,发送和接收消息前都需要先创建Topic,针对Topic来发送和接收消息,为了提高性能和吞吐量,引入了Message Queue,一个Topic可以设置一个或多个Message Queue,有点类似kafka的分区(Partition),这样消息就可以并行往各个Message Queue发送消息,消费者也可以并行的从多个Message Queue读取消息; 单机配置和部署 以下部署在centos7,jdk1.8,rocketmq4.3.2;启动RocketMQ的顺序是先启动NameServer,然后再启动Broker; 1.NameServer启动 [root@localhost bin]# ./mqnamesrv Java HotSpot(TM) 64-Bit Server VM warning: Using the DefNew young collector with the CMS collector is deprecated and will likely be removed in a future release Java HotSpot(TM) 64-Bit Server VM warning: UseCMSCompactAtFullCollection is deprecated and will likely be removed in a future release. The Name Server boot success. serializeType=JSON 如上日志表示启动成功,默认端口为9876; 2.Broker启动 [root@localhost bin]# ./mqbroker Java HotSpot(TM) 64-Bit Server VM warning: INFO: os::commit_memory(0x00000005c0000000, 8589934592, 0) failed; error='Cannot allocate memory' (errno=12) # # There is insufficient memory for the Java Runtime Environment to continue. # Native memory allocation (mmap) failed to map 8589934592 bytes for committing reserved memory. # An error report file with more information is saved as: # /root/rocketmq-all-4.3.2-bin-release/bin/hs_err_pid3977.log 启动失败,报内存不足,主要是rocketmq默认配置的启动参数值比较大,修改runbroker.sh即可 [root@localhost bin]# vi runbroker.sh JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g -Xmn4g" 默认配置的可用内存为8g,虚拟机内存不够,修改为如下即可 JAVA_OPT="${JAVA_OPT} -server -Xms128m -Xmx128m -Xmn128m" 再次启动,日志如下,表示启动成功,默认端口为10911; [root@localhost bin]# ./mqbroker The broker[localhost.localdomain, 192.168.237.128:10911] boot success. serializeType=JSON 3.简单测试 3.1生产者 public class SyncProducer { public static void main(String[] args) throws Exception { // 构造Producer DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName"); producer.setNamesrvAddr("192.168.237.128:9876"); // 初始化Producer,整个应用生命周期内,只需要初始化1次 producer.start(); for (int i = 0; i < 100; i++) { Message msg = new Message("TopicTest", "TagA", ("Hello RocketMQ" + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); SendResult sendResult = producer.send(msg); System.out.println(sendResult); } producer.shutdown(); } } 创建了一个DefaultMQProducer对象,同时设置了GroupName和NameServer地址,然后创建Message消息通过DefaultMQProducer将消息发送出去,返回一个SendResult对象; 3.2消费者 public class PushConsumer { public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please rename to unique group name"); consumer.setNamesrvAddr("192.168.237.128:9876"); consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); consumer.subscribe("TopicTest", "*"); consumer.registerMessageListener(new MessageListenerConcurrently() { public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { System.out.printf(Thread.currentThread().getName() + "Receive New Messages :" + msgs + "%n"); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); consumer.start(); } } 同样指定了GroupName和NameServer地址,订阅了Topic; 3.3运行测试 直接运行生产者报如下错误: Exception in thread "main" org.apache.rocketmq.client.exception.MQClientException: No route info of this topic, TopicTest See http://rocketmq.apache.org/docs/faq/ for further details. at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendDefaultImpl(DefaultMQProducerImpl.java:634) at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(DefaultMQProducerImpl.java:1253) at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(DefaultMQProducerImpl.java:1203) at org.apache.rocketmq.client.producer.DefaultMQProducer.send(DefaultMQProducer.java:214) at com.rocketmq.SyncProducer.main(SyncProducer.java:26) 错误显示"没有此Topic的路由信息",也就是生产者在发送消息的时候没有获取到路由信息,找不到指定的Broker,可能的原因: 1.Broker没有正确连接NameServer 2.Producer没有连接NameServer 3.Topic没有被正确创建 SyncProducer中指定了NameServer的地址,同时RocketMQ默认情况下会自动创建Topic,所以原因是Broker没有注册到NameServer,重新指定NameServer再启动: [root@localhost bin]# ./mqbroker -n localhost:9876 The broker[localhost.localdomain, 192.168.237.128:10911] boot success. serializeType=JSON and name server is localhost:9876 再次运行SyncProducer,日志如下: SendResult [sendStatus=SEND_OK, msgId=0A0D53073B6073D16E933086C4C60000, offsetMsgId=C0A8ED8000002A9F000000000000229C, messageQueue=MessageQueue[topic=TopicTest, brokerName=localhost.localdomain, queueId=1], queueOffset=11] SendResult [sendStatus=SEND_OK, msgId=0A0D53073B6073D16E933086C4CD0001, offsetMsgId=C0A8ED8000002A9F000000000000234D, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=2], queueOffset=9] SendResult [sendStatus=SEND_OK, msgId=0A0D53073B6073D16E933086C4D90002, offsetMsgId=C0A8ED8000002A9F00000000000023FE, messageQueue=MessageQueue[topic=TopicTest, brokerName=localhost.localdomain, queueId=3], queueOffset=9] SendResult [sendStatus=SEND_OK, msgId=0A0D53073B6073D16E933086C4E80003, offsetMsgId=C0A8ED8000002A9F00000000000024AF, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=0], queueOffset=11] SendResult [sendStatus=SEND_OK, msgId=0A0D53073B6073D16E933086C4F40004, offsetMsgId=C0A8ED8000002A9F0000000000002560, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=1], queueOffset=12] SendResult [sendStatus=SEND_OK, msgId=0A0D53073B6073D16E933086C4F70005, offsetMsgId=C0A8ED8000002A9F0000000000002611, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=2], queueOffset=10] SendResult [sendStatus=SEND_OK, msgId=0A0D53073B6073D16E933086C5030006, offsetMsgId=C0A8ED8000002A9F00000000000026C2, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=3], queueOffset=10] SendResult [sendStatus=SEND_OK, msgId=0A0D53073B6073D16E933086C5070007, offsetMsgId=C0A8ED8000002A9F0000000000002773, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=0], queueOffset=12] SendResult [sendStatus=SEND_OK, msgId=0A0D53073B6073D16E933086C50A0008, offsetMsgId=C0A8ED8000002A9F0000000000002824, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=1], queueOffset=13] SendResult [sendStatus=SEND_OK, msgId=0A0D53073B6073D16E933086C50D0009, offsetMsgId=C0A8ED8000002A9F00000000000028D5, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=2], queueOffset=11] 消费者使用的是push模式,可以实时接受消息: ConsumeMessageThread_13Receive New Messages :[MessageExt [queueId=1, storeSize=177,queueOffset=11, sysFlag=0, bornTimestamp=1547086138566, bornHost=/192.168.237.1:53524, storeTimestamp=1547139430770, storeHost=/192.168.237.128:10911, msgId=C0A8ED8000002A9F000000000000229C, commitLogOffset=8860, bodyCRC=705268097, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='TopicTest', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=12, CONSUME_START_TIME=1547086138573, UNIQ_KEY=0A0D53073B6073D16E933086C4C60000, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 82, 111, 99, 107, 101, 116, 77, 81, 48], transactionId='null'}]] ConsumeMessageThread_3Receive New Messages :[MessageExt [queueId=2, storeSize=177, queueOffset=9, sysFlag=0, bornTimestamp=1547086138573, bornHost=/192.168.237.1:53524, storeTimestamp=1547139430783, storeHost=/192.168.237.128:10911, msgId=C0A8ED8000002A9F000000000000234D, commitLogOffset=9037, bodyCRC=1561245975, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='TopicTest', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=10, CONSUME_START_TIME=1547086138598, UNIQ_KEY=0A0D53073B6073D16E933086C4CD0001, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 82, 111, 99, 107, 101, 116, 77, 81, 49], transactionId='null'}]] ConsumeMessageThread_17Receive New Messages :[MessageExt [queueId=3, storeSize=177, queueOffset=9, sysFlag=0, bornTimestamp=1547086138585, bornHost=/192.168.237.1:53524, storeTimestamp=1547139430794, storeHost=/192.168.237.128:10911, msgId=C0A8ED8000002A9F00000000000023FE, commitLogOffset=9214, bodyCRC=1141369005, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='TopicTest', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=10, CONSUME_START_TIME=1547086138601, UNIQ_KEY=0A0D53073B6073D16E933086C4D90002, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 82, 111, 99, 107, 101, 116, 77, 81, 50], transactionId='null'}]] ConsumeMessageThread_9Receive New Messages :[MessageExt [queueId=0, storeSize=177, queueOffset=11, sysFlag=0, bornTimestamp=1547086138600, bornHost=/192.168.237.1:53524, storeTimestamp=1547139430807, storeHost=/192.168.237.128:10911, msgId=C0A8ED8000002A9F00000000000024AF, commitLogOffset=9391, bodyCRC=855693371, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='TopicTest', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=12, CONSUME_START_TIME=1547086138612, UNIQ_KEY=0A0D53073B6073D16E933086C4E80003, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 82, 111, 99, 107, 101, 116, 77, 81, 51], transactionId='null'}]] ConsumeMessageThread_15Receive New Messages :[MessageExt [queueId=1, storeSize=177, queueOffset=12, sysFlag=0, bornTimestamp=1547086138612, bornHost=/192.168.237.1:53524, storeTimestamp=1547139430809, storeHost=/192.168.237.128:10911, msgId=C0A8ED8000002A9F0000000000002560, commitLogOffset=9568, bodyCRC=761548184, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='TopicTest', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=13, CONSUME_START_TIME=1547086138626, UNIQ_KEY=0A0D53073B6073D16E933086C4F40004, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 82, 111, 99, 107, 101, 116, 77, 81, 52], transactionId='null'}]] ConsumeMessageThread_11Receive New Messages :[MessageExt [queueId=2, storeSize=177, queueOffset=10, sysFlag=0, bornTimestamp=1547086138615, bornHost=/192.168.237.1:53524, storeTimestamp=1547139430820, storeHost=/192.168.237.128:10911, msgId=C0A8ED8000002A9F0000000000002611, commitLogOffset=9745, bodyCRC=1516469518, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='TopicTest', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=11, CONSUME_START_TIME=1547086138628, UNIQ_KEY=0A0D53073B6073D16E933086C4F70005, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 82, 111, 99, 107, 101, 116, 77, 81, 53], transactionId='null'}]] ConsumeMessageThread_4Receive New Messages :[MessageExt [queueId=3, storeSize=177,queueOffset=10, sysFlag=0, bornTimestamp=1547086138627, bornHost=/192.168.237.1:53524, storeTimestamp=1547139430824, storeHost=/192.168.237.128:10911, msgId=C0A8ED8000002A9F00000000000026C2,commitLogOffset=9922, bodyCRC=1131031732,reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='TopicTest', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=11, CONSUME_START_TIME=1547086138633, UNIQ_KEY=0A0D53073B6073D16E933086C5030006, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 82, 111, 99, 107, 101, 116, 77, 81, 54], transactionId='null'}]] ConsumeMessageThread_14Receive New Messages :[MessageExt [queueId=0, storeSize=177, queueOffset=12, sysFlag=0, bornTimestamp=1547086138631, bornHost=/192.168.237.1:53524, storeTimestamp=1547139430827, storeHost=/192.168.237.128:10911, msgId=C0A8ED8000002A9F0000000000002773, commitLogOffset=10099, bodyCRC=879565858, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='TopicTest', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=13, CONSUME_START_TIME=1547086138635, UNIQ_KEY=0A0D53073B6073D16E933086C5070007, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 82, 111, 99, 107, 101, 116, 77, 81, 55], transactionId='null'}]] ConsumeMessageThread_10Receive New Messages :[MessageExt [queueId=1, storeSize=177, queueOffset=13, sysFlag=0, bornTimestamp=1547086138634, bornHost=/192.168.237.1:53524, storeTimestamp=1547139430830, storeHost=/192.168.237.128:10911, msgId=C0A8ED8000002A9F0000000000002824, commitLogOffset=10276, bodyCRC=617742771, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='TopicTest', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=14, CONSUME_START_TIME=1547086138638, UNIQ_KEY=0A0D53073B6073D16E933086C50A0008, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 82, 111, 99, 107, 101, 116, 77, 81, 56], transactionId='null'}]] ConsumeMessageThread_7Receive New Messages :[MessageExt [queueId=2, storeSize=177, queueOffset=11, sysFlag=0, bornTimestamp=1547086138637, bornHost=/192.168.237.1:53524, storeTimestamp=1547139430833, storeHost=/192.168.237.128:10911, msgId=C0A8ED8000002A9F00000000000028D5, commitLogOffset=10453, bodyCRC=1406480677, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='TopicTest', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=12, CONSUME_START_TIME=1547086138641, UNIQ_KEY=0A0D53073B6073D16E933086C50D0009, WAIT=true, TAGS=TagA}, body=[72, 101, 108, 108, 111, 32, 82, 111, 99, 107, 101, 116, 77, 81, 57], transactionId='null'}]] 多机集群配置和部署 分别部署两台NameServer,两台Broker并且分别提供Slave,准备两台电脑分别是本机的windows以及虚拟机centos; 1.启动NameServer 分别启动2台NameServer,端口号都使用默认的9876,地址端口如下: 192.168.237.128:9876 10.13.83.7:9876 2.启动Broker 每台机器上分别启动一个Master和一个Slave,互为主备,在主目录下的conf文件夹下提供了多种broker配置模式,分别有:2m-2s-async,2m-2s-sync,2m-noslave,可以以此为模版做如下配置: 2.1配置10.13.83.7Master和Slave Master配置如下: namesrvAddr=192.168.237.128:9876;10.13.83.7:9876 brokerClusterName=DefaultCluster brokerName=broker-a brokerId=0 deleteWhen=04 fileReservedTime=48 brokerRole=SYNC_MASTER flushDiskType=ASYNC_FLUSH listenPort=10911 storePathRootDir=E:/rocketmq-all-4.3.2-bin-release/store-a-m Slave配置如下: namesrvAddr=192.168.237.128:9876;10.13.83.7:9876 brokerClusterName=DefaultCluster brokerName=broker-a brokerId=1 deleteWhen=04 fileReservedTime=48 brokerRole=SLAVE flushDiskType=ASYNC_FLUSH listenPort=10811 storePathRootDir=E:/rocketmq-all-4.3.2-bin-release/store-a-s 分别启动结果如下: E:\rocketmq-all-4.3.2-bin-release\bin>mqbroker -c E:\rocketmq-all-4.3.2-bin-rele ase\conf\broker-m.conf The broker[broker-a, 10.13.83.7:10911] boot success. serializeType=JSON and name server is 192.168.237.128:9876;10.13.83.7:9876 以上是Master启动日志,Slave日志如下: E:\rocketmq-all-4.3.2-bin-release\bin>mqbroker -c E:\rocketmq-all-4.3.2-bin-rele ase\conf\broker-s.conf The broker[broker-a, 10.13.83.7:10811] boot success. serializeType=JSON and name server is 192.168.237.128:9876;10.13.83.7:9876 2.2配置10.13.83.7Slave Master配置如下: namesrvAddr=192.168.237.128:9876;10.13.83.7:9876 brokerClusterName=DefaultCluster brokerName=broker-b brokerId=0 deleteWhen=04 fileReservedTime=48 brokerRole=SYNC_MASTER flushDiskType=ASYNC_FLUSH listenPort=10911 storePathRootDir=/root/rocketmq-all-4.3.2-bin-release/store-b-m Slave配置如下: namesrvAddr=192.168.237.128:9876;10.13.83.7:9876 brokerClusterName=DefaultCluster brokerName=broker-b brokerId=1 deleteWhen=04 fileReservedTime=48 brokerRole=SLAVE flushDiskType=ASYNC_FLUSH listenPort=10811 storePathRootDir=/root/rocketmq-all-4.3.2-bin-release/store-b-s 启动日志分别如下: [root@localhost bin]# ./mqbroker -c ../conf/broker-m.conf The broker[broker-b, 192.168.237.128:10911] boot success. serializeType=JSON and name server is 192.168.237.128:9876;10.13.83.7:9876 [root@localhost bin]# ./mqbroker -c ../conf/broker-s.conf The broker[broker-b, 192.168.237.128:10811] boot success. serializeType=JSON and name server is 192.168.237.128:9876;10.13.83.7:9876 3.配置说明 1.namesrvAddr NameServer地址,可以配置多个,用逗号分隔; 2.brokerClusterName 所属集群名称,如果节点较多可以配置多个 3.brokerName broker名称,master和slave使用相同的名称,表明他们的主从关系 4.brokerId 0表示Master,大于0表示不同的slave 5.deleteWhen 表示几点做消息删除动作,默认是凌晨4点 6.fileReservedTime 在磁盘上保留消息的时长,单位是小时 7.brokerRole 有三个值:SYNC_MASTER,ASYNC_MASTER,SLAVE;同步和异步表示Master和Slave之间同步数据的机制; 8.flushDiskType 刷盘策略,取值为:ASYNC_FLUSH,SYNC_FLUSH表示同步刷盘和异步刷盘;SYNC_FLUSH消息写入磁盘后才返回成功状态,ASYNC_FLUSH不需要; 9.listenPort 启动监听的端口号 10.storePathRootDir 存储消息的根目录 管理工具 1.命令行管理工具 mqadmin是RocketMQ自带的命令行管理工具,可以创建、修改Topic,查询消息,更新配置信息等操作,具体可以通过如下命令查看: E:\rocketmq-all-4.3.2-bin-release\bin>mqadmin The most commonly used mqadmin commands are: updateTopic Update or create topic deleteTopic Delete topic from broker and NameServer. updateSubGroup Update or create subscription group deleteSubGroup Delete subscription group from broker. updateBrokerConfig Update broker's config updateTopicPerm Update topic perm topicRoute Examine topic route info topicStatus Examine topic Status info topicClusterList get cluster info for topic brokerStatus Fetch broker runtime status data queryMsgById Query Message by Id queryMsgByKey Query Message by Key queryMsgByUniqueKey Query Message by Unique key queryMsgByOffset Query Message by offset printMsg Print Message Detail printMsgByQueue Print Message Detail sendMsgStatus send msg to broker. brokerConsumeStats Fetch broker consume stats data producerConnection Query producer's socket connection and client vers consumerConnection Query consumer's socket connection, client version ubscription consumerProgress Query consumers's progress, speed consumerStatus Query consumer's internal data structure cloneGroupOffset clone offset from other group. clusterList List all of clusters topicList Fetch all topic list from name server updateKvConfig Create or update KV config. deleteKvConfig Delete KV config. wipeWritePerm Wipe write perm of broker in all name server resetOffsetByTime Reset consumer offset by timestamp(without client t). updateOrderConf Create or update or delete order conf cleanExpiredCQ Clean expired ConsumeQueue on broker. cleanUnusedTopic Clean unused topic on broker. startMonitoring Start Monitoring statsAll Topic and Consumer tps stats allocateMQ Allocate MQ checkMsgSendRT check message send response time clusterRT List All clusters Message Send RT getNamesrvConfig Get configs of name server. updateNamesrvConfig Update configs of name server. getBrokerConfig Get broker config by cluster or special broker! queryCq Query cq command. sendMessage Send a message consumeMessage Consume message See 'mqadmin help <command>' for more information on a specific command. 列出了所有支持的命令以及简单的介绍,如果想看详细的可以如下命令: E:\rocketmq-all-4.3.2-bin-release\bin>mqadmin help statsAll usage: mqadmin statsAll [-a] [-h] [-n <arg>] [-t <arg>] -a,--activeTopic print active topic only -h,--help Print help -n,--namesrvAddr <arg> Name server address list, eg: 192.168.0.1:9876;192.168 .0.2:9876 -t,--topic <arg> print select topic only 2.图形界面管理工具 除了命令,还提供了图形界面管理工具,在RocketMQ的扩展包里面,具体地址如下: https://github.com/apache/rocketmq-externals/tree/release-rocketmq-console-1.0.0/rocketmq-console 目前的稳定版本是1.0.0,可以下载下来在本地运行,对application.properties做简单配置: rocketmq.config.namesrvAddr=10.13.83.7:9876 需要指定NameServer的地址,然后就可以打包运行了,运作之后会启动8080端口,直接访问地址: http://localhost:8080 总结 本文从最简单的安装部署入手,并对常用的配置参数做了简单介绍;然后了解了RocketMQ的部署的整体结构,分别对其中的角色做了简单介绍;最后介绍了两种RocketMQ的管理工具,方便对RocketMQ的监控和管理。

优秀的个人博客,低调大师

Maven入门教程

GitHub仓库:Fundebug/maven-tutorial Maven简介 Maven是Java项目构建工具,可以用于管理Java依赖,还可以用于编译、打包以及发布Java项目,类似于JavaScript生态系统中的NPM。 Maven的命令行工具为mvn,其常用命令如下表所示: | 命令 | 说明 | | ----------- | -----------------------| | mvn compile | 编译Java源代码 | | mvn package | 打包Java项目 || mvn deploy | 将Java项目发布到Maven仓库 || mvn clean | 删除构建目录 | Maven的配置文件为pom.xml,这个文件有个很吓人的学术名字Project Object Model,但是怎么看它都只是个普通的配置文件,与NPM中的package.json没啥本质区别。 Maven的中央仓库为Maven Repository,这里可以找到各种Java依赖,例如我们Fundebug的异常监控插件fundebug-java与fundebug-spring。 安装Maven 在MacBook上使用brew安装很方便 brew install maven 我安装的是maven版本3.5.4 mvn -version Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-18T02:33:14+08:00) Maven home: /usr/local/Cellar/maven/3.5.4/libexec Java version: 1.8.0_192, vendor: Oracle Corporation, runtime: /Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/jre Default locale: zh_CN, platform encoding: UTF-8 OS name: "mac os x", version: "10.14.2", arch: "x86_64", family: "mac" 示例代码 本文示例代码都在GitHub仓库Fundebug/maven-tutorial。 使用tree命令可以查看项目的目录结构: tree -v . ├── pom.xml └── src └── main └── java └── com └── fundebug └── Hello.java pom.xml为Maven配置文件,位于项目的根目录。 Hello.java为Java源代码,位于src/main/java/com/fundebug目录中。根据Maven对目录结构的要求,Java源代码必须位于src/main/java目录。 Hello.java package com.fundebug; import org.json.JSONObject; public class Hello { public static void main(String[] args) { JSONObject tomJsonObj = new JSONObject(); tomJsonObj.put("name", "Fundebug"); tomJsonObj.put("url", "https://www.fundebug.com"); System.out.println(tomJsonObj.toString(4)); } } Hello.java非常简单,定义了一个JSON对象,然后把它打印出来了。 package定义的包名为com.fundebug,需要与所在的目录结构相吻合,因此Hello.java位于src/main/java/com/fundebug目录中,而不是src/main/java/目录中。 代码依赖于第三方模块json,因此需要在pom.xml配置dependency: <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20180813</version> </dependency> pom.xml <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <!-- 坐标 --> <groupId>com.fundebug</groupId> <artifactId>hello</artifactId> <version>1.0</version> <!-- 依赖 --> <dependencies> <!-- https://mvnrepository.com/artifact/org.json/json --> <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20180813</version> </dependency> </dependencies> </project> pom.xml中,<project></project>为最外层的标签;<modelVersion>4.0.0</modelVersion>定义了所使用的POM版本。这2个标签基本上是不变的。 groupId、artifactId与version一起则定义了模块的坐标(Coordinates),每个公共模块的坐标应该是唯一的: groupId:组织名称,通常是把域名反过来,例如com.fundebug artifactId:模块名称,例如fundebug-java version:模块版本,例如0.2.0 <dependencies></dependencies>定义了当前项目所依赖的模块: <dependencies> <!-- https://mvnrepository.com/artifact/org.json/json --> <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20180813</version> </dependency> </dependencies> Maven可以根据<dependency></dependency>中定义的坐标,自动下载所依赖的模块。在MacBook上,Maven将下载的模块缓存在$HOME/.m2/目录。 使用mvn打包 执行mvn package命令,即可将源码打包为.jar文件: mvn package [INFO] Scanning for projects... [INFO] [INFO] -------------------------< com.fundebug:hello >------------------------- [INFO] Building hello 1.0 [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory /Users/fundebug/Desktop/maven-tutorial/src/main/resources [INFO] [INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ hello --- [INFO] Changes detected - recompiling the module! [WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent! [INFO] Compiling 1 source file to /Users/fundebug/Desktop/maven-tutorial/target/classes [INFO] [INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ hello --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory /Users/fundebug/Desktop/maven-tutorial/src/test/resources [INFO] [INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ hello --- [INFO] No sources to compile [INFO] [INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ hello --- [INFO] No tests to run. [INFO] [INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ hello --- [INFO] Building jar: /Users/fundebug/Desktop/maven-tutorial/target/hello-1.0.jar [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.789 s [INFO] Finished at: 2019-01-05T15:23:02+08:00 [INFO] ------------------------------------------------------------------------ mvn package执行之后,项目中会新增一个tartget目录,编译的字节码文件位于target/classes目录,而jar包位于target/hello-1.0.jar: tree -v . ├── pom.xml ├── src │ └── main │ └── java │ └── com │ └── fundebug │ └── Hello.java └── target ├── classes │ └── com │ └── fundebug │ └── Hello.class ├── hello-1.0.jar 使用mvn运行 打包好的jar包,可以直接使用java命令运行时,注意需要指定所依赖的jar包。对于所依赖的jar包,Maven则会自动下载依赖,放在本地仓库中。在MacBook上,Maven本地仓库位于$HOME/.m2/目录。 java -cp target/hello-1.0.jar:$HOME/.m2/repository/org/json/json/20180813/json-20180813.jar com.fundebug.Hello { "name": "Fundebug", "url": "https://www.fundebug.com" } 也可以使用mvn exec:java命令执行,不需要指定依赖的jar包,更加方便: mvn exec:java -Dexec.mainClass="com.fundebug.Hello" [INFO] Scanning for projects... [INFO] [INFO] -------------------------< com.fundebug:hello >------------------------- [INFO] Building hello 1.0 [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- exec-maven-plugin:1.6.0:java (default-cli) @ hello --- { "name": "Fundebug", "url": "https://www.fundebug.com" } [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 0.837 s [INFO] Finished at: 2019-01-05T15:33:57+08:00 [INFO] ------------------------------------------------------------------------ 参考 Simple Maven Project Maven Dependency Management Java JSON Tutorial and Example: JSON-Java (org.json)

优秀的个人博客,低调大师

Kubernetes Helm入门指南

什么是Helm?这可不是暗黑破坏神里装备的名称:头盔,而是Kubernetes的一个包管理工具,用来简化Kubernetes应用的部署和管理。我们Helm和Kubernetes的关系,我们可以理解成yum和CentOS,apt-get和Ubuntu的关系。 Helm由两部分组成,客户端helm和服务端tiller。 其中tiller运行在Kubernetes集群上,管理chart,而客户端helm就是一个命令行工具,可在本地运行,一般运行在持续集成/持续交付的服务器上 。 下图是helm的架构图。 我们现在就来试用下helm。 首先安装helm客户端。 下载helm执行文件的压缩包: wget -O helm.tar.gz https://storage.googleapis.com/kubernetes-helm/helm-v2.11.0-l

资源下载

更多资源
Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册