简

人生短暂,学海无边,而大道至简。


  • 首页

  • 归档

  • 分类

  • 标签

虚拟机性能监控与故障处理工具

发表于 2019-08-24 | 分类于 java

JDK的命令行工具

JDK开发团队选择采用Java代码来实现这些监控工具是有特别用意的:当应用程序部署到生产环境后,无论是直接接触物理服务器还是远程Telnet到服务器上都可能会受到限制。借助tools.jar类库里面的接口,我们可以直接在应用程序中实现功能强大的监控分析功能。

JDK监控和故障处理工具

** jps:虚拟机进程状况工具 **

jps命令格式:

jps [ options ] [ hostid ]

jps执行样例:

D:\Develop\Java\jdk1.6.0_21\bin>jps -l 2388 D:\Develop\glassfish\bin..\modules\admin-cli.jar 2764 com.sun.enterprise.glassfish.bootstrap.ASMain 3788 sun.tools.jps.Jps

jps可以通过RMI协议查询开启了RMI服务的远程虚拟机进程状态,hostid为RMI注册表中注册的主机名。

** jstat:虚拟机统计信息监视工具**

jstat(JVM Statistics Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。

jstat命令格式为:

jstat [ option vmid [interval[s|ms] [count]] ]

对于命令格式中的VMID与LVMID需要特别说明一下:如果是本地虚拟机进程, VMID与LVMID是一致的,如果是远程虚拟机进程,那VMID的格式应当是:

[protocol:][//]lvmid[@hostname[:port]/servername]

参数interval和count代表查询间隔和次数,如果省略这两个参数,说明只查询一次。假设需要每250毫秒查询一次进程2764垃圾收集的状况,一共查询20次,那命令应当是:

jstat -gc 2764 250 20

选项option代表着用户希望查询的虚拟机信息,主要分为3类:类装载、垃圾收集和运行期编译状况

jstat执行样例

1
2
3
D:\Develop\Java\jdk1.6.0_21\bin>jstat -gcutil 2764 
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.00 6.20 41.42 47.20 16 0.105 3 0.472 0.577

查询结果表明:这台服务器的新生代Eden区(E,表示Eden)使用了6.2%的空间,两个Survivor区(S0、S1,表示Survivor0、Survivor1)里面都是空的,老年代(O,表示Old)和永久代(P,表示Permanent)则分别使用了41.42%和47.20%的空间。程序运行以来共发生Minor GC(YGC,表示Young GC)16次,总耗时0.105秒,发生Full GC(FGC,表示Full GC)3次,Full GC总耗时(FGCT,表示Full GC Time)为0.472秒,所有GC总耗时(GCT,表示GC Time)为0.577秒。

使用jstat工具在纯文本状态下监视虚拟机状态的变化,确实不如后面将会提到的VisualVM等可视化的监视工具直接以图表展现的那样直观。但许多服务器管理员都习惯了在文本控制台中工作,直接在控制台中使用jstat命令依然是一种常用的监控方式。

** jinfo:Java配置信息工具**

jinfo(Configuration Info for Java)的作用是实时地查看和调整虚拟机的各项参数。JDK 1.6之后,jinfo在Windows和Linux平台都有提供,并且加入了运行期修改参数的能力,可以使用-flag [+|-]name或-flag name=value修改一部分运行期可写的虚拟机参数值。

jinfo命令格式:

jinfo [ option ] pid

执行样例:查询CMSInitiatingOccupancyFraction参数值。

C:>jinfo -flag CMSInitiatingOccupancyFraction 1444 -XX:CMSInitiatingOccupancyFraction=85

** jmap:Java内存映像工具**

jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文件)。如果不使用jmap命令,要想获取Java堆转储快照还有一些比较“暴力”的手段:譬如在第2章中用过的-XX:HeapDumpOnOutOfMemoryError参数,可以让虚拟机在OOM异常出现之后自动生成dump文件,通过-XX:HeapDumpOnCtrlBreak参数则可以使用[Ctrl]+[Break]键让虚拟机生成dump文件,又或者在Linux系统下通过Kill-3命令发送进程退出信号“恐吓”一下虚拟机,也能拿到dump文件。

jmap的作用并不仅仅是为了获取dump文件,它还可以查询finalize执行队列,Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。

jmap命令格式:

jmap [ option ] vmid

jhat:虚拟机堆转储快照分析工具

Sun JDK提供jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,可以在浏览器中查看。不过实事求是地说,在实际工作中,除非笔者手上真的没有别的工具可用,否则一般都不会去直接使用jhat命令来分析dump文件,主要原因有二:一是一般不会在部署应用程序的服务器上直接分析dump文件,即使可以这样做,也会尽量将dump文件拷贝到其他机器上进行分析,因为分析工作是一个耗时而且消耗硬件资源的过程,既然都要在其他机器上进行,就没必要受到命令行工具的限制了。另外一个原因是jhat的分析功能相对来说比较简陋,后文将会介绍到的VisualVM,以及专业用于分析dump文件的Eclipse Memory Analyzer、IBM HeapAnalyzer等工具,都能实现比jhat更强大更专业的分析功能。代码清单4-3演示了使用jhat分析上一节采用jmap生成的Eclipse IDE的内存快照文件。

** jstack:Java堆栈跟踪工具**

jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些什么事情,或者等待着什么资源。

jstack命令格式:

jstack [ option ] vmid

JDK的可视化工具

JDK中除了提供大量的命令行工具外,还有两个功能强大的可视化工具:JConsole和VisualVM,这两个工具是JDK的正式成员,没有被贴上“unsupported and experimental”的标签。

其中JConsole是在JDK 1.5时期就已经提供的虚拟机监控工具,而VisualVM在JDK 1.6 Update7中才首次发布,现在已经成为Sun(Oracle)主力推动的多合一故障处理工具,并且已经从JDK中分离出来成为可以独立发展的开源项目。

** JConsole:Java监视与管理控制台**

JConsole(Java Monitoring and Management Console)是一款基于JMX的可视化监视和管理的工具。它管理部分的功能是针对JMX MBean进行管理,由于MBean可以使用代码、中间件服务器的管理控制台或者所有符合JMX规范的软件进行访问,所以本节中将会着重介绍JConsole监视部分的功能。
** VisualVM:多合一故障处理工具**

VisualVM(All-in-One Java Troubleshooting Tool)是到目前为止,随JDK发布的功能最强大的运行监视和故障处理程序,除了运行监视、故障处理外,还提供了很多其他方面的功能。如性能分析(Profiling),VisualVM的性能分析功能甚至比起JProfiler、YourKit等专业且收费的Profiling工具都不会逊色多少,而且VisualVM的还有一个很大优点:不需要被监视的程序基于特殊Agent运行,因此它对应用程序的实际性能的影响很小,使得它可以直接应用在生产环境中。这个优点是JProfiler、YourKit等工具无法与之媲美的。

除了JDK自带的工具之外,常用的故障处理工具还有很多,如果读者使用的是非Sun系列的JDK,非HotSpot的虚拟机,就需要使用对应的工具进行分析,如:

□ IBM的Support Assistant、Heap Analyzer、Javacore Analyzer、Garbage Collector Analyzer适用于IBM J9VM。

□ HP的HPjmeter、HPjtune适用于HP-UX、SAP、HotSpot VM。

□ Eclipse的Memory Analyzer Tool(MAT)适用于HP-UX、SAP、HotSpot VM,安装IBM DTFJ插件后可支持IBM J9VM。

□ BEA的JRockit Mission Control,适用于JRockit VM。

缓存穿透、缓存雪崩,缓存击穿解决方案(转)

发表于 2019-08-23 | 分类于 分布式

什么样的数据适合缓存

数据访问频率 频率高 适合缓存 效果好
频率低 不适合缓存 效果不好
数据读写比例 读多写少 适合缓存 效果好
读少写多 不适合缓存 效果不好
数据一致性 一致性要求低 适合缓存 效果好
一致性要求高 不适合缓存 效果不好

缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

解决方案:

1
2
1)有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层数据库的查询压力。
2)另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

缓存雪崩

缓存雪崩是指在设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,导致所有的查询都落在数据库上,造成了缓存雪崩。

解决方案:

1
2
3
4
1)在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
2)可以通过缓存reload机制,预先去更新缓存,在即将发生大并发访问前手动触发加载缓存。
3)不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
4)做二级缓存,或者双缓存策略。A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。

缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
### 1)后台刷新
后台定义一个job(定时任务)专门主动更新缓存数据.比如,一个缓存中的数据过期时间是30分钟,那么job每隔29分钟定时刷新数据(将从数据库中查到的数据更新到缓存中).
注:这种方案比较容易理解,但会增加系统复杂度。比较适合那些 key 相对固定,cache 粒度较大的业务,key 比较分散的则不太适合,实现起来也比较复杂。

### 2)检查更新
将缓存key的过期时间(绝对时间)一起保存到缓存中(可以拼接,可以添加新字段,可以采用单独的key保存..不管用什么方式,只要两者建立好关联关系就行).在每次执行get操作后,都将get出来的缓存过期时间与当前系统时间做一个对比,如果缓存过期时间-当前系统时间<=1分钟(自定义的一个值),则主动更新缓存.这样就能保证缓存中的数据始终是最新的(和方案一一样,让数据不过期.)

注:这种方案在特殊情况下也会有问题。假设缓存过期时间是12:00,而 11:59 到 12:00这 1 分钟时间里恰好没有 get 请求过来,又恰好请求都在 11:30 分的时 候高并发过来,那就悲剧了。这种情况比较极端,但并不是没有可能。因为“高 并发”也可能是阶段性在某个时间点爆发。

### 3)分级缓存
采用 L1 (一级缓存)和 L2(二级缓存) 缓存方式,L1 缓存失效时间短,L2 缓存失效时间长。 请求优先从 L1 缓存获取数据,如果 L1缓存未命中则加锁,只有 1 个线程获取到锁,这个线程再从数据库中读取数据并将数据再更新到到 L1 缓存和 L2 缓存中,而其他线程依旧从 L2 缓存获取数据并返回。

注:这种方式,主要是通过避免缓存同时失效并结合锁机制实现。所以,当数据更 新时,只能淘汰 L1 缓存,不能同时将 L1 和 L2 中的缓存同时淘汰。L2 缓存中 可能会存在脏数据,需要业务能够容忍这种短时间的不一致。而且,这种方案 可能会造成额外的缓存空间浪费。

### 4)加锁
// 方法1:
public synchronized List<String> getData01() {
List<String> result = new ArrayList<String>();
// 从缓存读取数据
result = getDataFromCache();
if (result.isEmpty()) {
// 从数据库查询数据
result = getDataFromDB();
// 将查询到的数据写入缓存
setDataToCache(result);
}
return result;
}  
注:这种方式确实能够防止缓存失效时高并发到数据库,但是缓存没有失效的时候,在从缓存中拿数据时需要排队取锁,这必然会大大的降低了系统的吞吐量.

方法2:
// 方法2:
static Object lock = new Object();

public List<String> getData02() {
List<String> result = new ArrayList<String>();
// 从缓存读取数据
result = getDataFromCache();
if (result.isEmpty()) {
synchronized (lock) {
// 从数据库查询数据
result = getDataFromDB();
// 将查询到的数据写入缓存
setDataToCache(result);
}
}
return result;
}  
注:这个方法在缓存命中的时候,系统的吞吐量不会受影响,但是当缓存失效时,请求还是会打到数据库,只不过不是高并发而是阻塞而已.但是,这样会造成用户体验不佳,并且还给数据库带来额外压力.

方法3:
//方法3
public List<String> getData03() {
List<String> result = new ArrayList<String>();
// 从缓存读取数据
result = getDataFromCache();
if (result.isEmpty()) {
synchronized (lock) {
//双重判断,第二个以及之后的请求不必去找数据库,直接命中缓存
// 查询缓存
result = getDataFromCache();
if (result.isEmpty()) {
// 从数据库查询数据
result = getDataFromDB();
// 将查询到的数据写入缓存
setDataToCache(result);
}
}
}
return result;
}
注:双重判断虽然能够阻止高并发请求打到数据库,但是第二个以及之后的请求在命中缓存时,还是排队进行的.比如,当30个请求一起并发过来,在双重判断时,第一个请求去数据库查询并更新缓存数据,剩下的29个请求则是依次排队取缓存中取数据.请求排在后面的用户的体验会不爽.

//方法4:
static Lock reenLock = new ReentrantLock();

public List<String> getData04() throws InterruptedException {
List<String> result = new ArrayList<String>();
// 从缓存读取数据
result = getDataFromCache();
if (result.isEmpty()) {
if (reenLock.tryLock()) {
try {
System.out.println("我拿到锁了,从DB获取数据库后写入缓存");
// 从数据库查询数据
result = getDataFromDB();
// 将查询到的数据写入缓存
setDataToCache(result);
} finally {
reenLock.unlock();// 释放锁
}

} else {
result = getDataFromCache();// 先查一下缓存
if (result.isEmpty()) {
System.out.println("我没拿到锁,缓存也没数据,先小憩一下");
Thread.sleep(100);// 小憩一会儿
return getData04();// 重试
}
}
}
return result;
}
注:最后使用互斥锁的方式来实现,可以有效避免前面几种问题.

在实际分布式场景中,我们还可以使用 redis、tair、zookeeper 等提供的分布式锁来实现,如果我们的并发量如果只有几千的话,这些都是很好的方式。

jvm深入-自动内存管理机制

发表于 2019-08-23 | 分类于 java

内存模型

每个JVM都有两种机制:类装载子系统和执行引擎

每个JVM都包含:方法区、Java堆、Java栈、本地方法栈、程序计数器

20200423160634

20200423160643

程序计数器(Program Counter Register)

  线程私有,它的生命周期与线程相同。它的作用可以看做是当前线程所执行的字节码的行号指示器,是一块较小的内存空间。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

  由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

  如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈(JVM Stacks)

  线程私有的,它的生命周期与线程相同。

  虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型),它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

该区域可能抛出以下异常:

  • 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
  • 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。

本地方法栈(Native Method Stacks)

  与虚拟机栈非常相似,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。

Java堆(Heap)

  被所有线程共享,在虚拟机启动时创建,用来存放对象实例,几乎所有的对象实例都在这里分配内存。

  对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做”GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;新生代又有Eden空间、From Survivor空间、To Survivor空间三部分。

Java 堆不需要连续内存,并且可以通过动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。

方法区(Method Area)

  • 用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
  • 对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现,HotSpot 虚拟机把它当成永久代(Permanent Generation)来进行垃圾回收。

** 运行时常量池(Runtime Constant Pool)**

1
2
3
运行时常量池是方法区的一部分。
Class 文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。这部分常量也会被放入运行时常量池。

在 JDK1.7之前,HotSpot 使用永久代实现方法区;从 JDK1.7 开始HotSpot 开始移除永久代。其中符号引用(Symbols)被移动到 Native Heap中,字符串常量和类引用被移动到 Java Heap中。

在 JDK1.8 中,永久代已完全被元空间(Meatspace)所取代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

** 直接内存(Direct Memory)**
  直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现。
在 JDK 1.4 中新加入了 NIO 类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和 Native 堆中来回复制数据。

20200423160656

(1)如果线程请求的栈深度太深,超出了虚拟机所允许的深度,就会出现StackOverFlowError(比如无限递归。因为每一层栈帧都占用一定空间,而 Xss 规定了栈的最大空间,超出这个值就会报错)

(2)虚拟机栈可以动态扩展,如果扩展到无法申请足够的内存空间,会出现OOM

OutOfMemoryError异常

除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(下文称OOM)异常的可能。

Java堆溢出

Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量到达最大堆的容量限制后产生内存溢出异常。

Java堆内存溢出异常测试

限制Java堆的大小为20MB,不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDump OnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析。

1
2
3
4
5
6
7
8
9
10
/** VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError */
static class OOMObject {
}

public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}

运行结果:

1
2
3
4
5
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid14104.hprof ...
Heap dump file created [29232817 bytes in 0.273 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
...

Java堆内存的OOM异常是实际应用中最常见的内存溢出异常情况。出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。

内存分析:

上示例所示,生成文件 java_pid14104.hprof,打开文件信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
D:\java-workspaces\idea-work\huajin>jhat java_pid14104.hprof
Reading from java_pid14104.hprof...
Dump file created Tue Oct 08 15:23:06 CST 2019
Snapshot read, resolving...
Resolving 830511 objects...
Chasing references, expect 166 dots......................................................................................................................................................................
Eliminating duplicate references......................................................................................................................................................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.

```
若有时dump文件很大,需要设置jhat参数:jhat -J-Xmx2048m <heap dump file>,但是默认情况下是1024m。

就可以找到文件:

![20200423160713](https://img.huyunshun.com/img/20200423160713.png)

这种是通过jhat命令打开访问dump文件。

要解决这个区域的异常,一般通过内存映像分析工具(eclipse里面有 Eclipse Memory Analyzer,在idea中是JProfilerl或Java自带的JVisualVM)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。

* 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置。
* 如果不存在泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

以上是处理Java堆内存问题的简略思路,处理这些问题所需要的知识、工具与经验是后面三章的主题。

### 虚拟机栈和本地方法栈溢出

由于在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:

* 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
* 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

这里把异常分成两种情况看似更加严谨,但却存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。

* 使用-Xss参数减少栈内存容量。结果:抛出StackOverflowError异常,异常出现时输出的栈深度相应缩小。
* 定义了大量的本地变量,增加此方法帧中本地变量表的长度。结果:抛出StackOverflowError异常时输出的栈深度相应缩小。

虚拟机栈和本地方法栈OOM测试(仅作为第1点测试程序)

```java
private int stackLength = 1;

/** -Xss128k */
public void stackLeak() {
stackLength++;
stackLeak();
}

public static void main(String[] args) throws Throwable {
Demo oom = new Demo();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}

运行结果:

1
2
stack length:997
Exception in thread "main" java.lang.StackOverflowError

实验结果表明:在单个线程下,无论是由于栈帧太大,还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。

如果测试时不限于单线程,通过不断地建立线程的方式倒是可以产生内存溢出异常,这样产生的内存溢出异常与栈空间是否足够大并不存在任何联系,或者准确地说,在这种情况下,给每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。

原因其实不难理解,操作系统分配给每个进程的内存是有限制的,譬如32位的Windows限制为2GB。虚拟机提供了参数来控制Java堆和方法区的这两部分内存的最大值。剩余的内存为2GB(操作系统限制)减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”了。每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。

这一点读者需要在开发多线程应用的时候特别注意,出现StackOverflowError异常时有错误堆栈可以阅读,相对来说,比较容易找到问题的所在。而且,如果使用虚拟机默认参数,栈深度在大多数情况下(因为每个方法压入栈的帧大小并不是一样的,所以只能说大多数情况下)达到1000~2000完全没有问题,对于正常的方法调用(包括递归),这个深度应该完全够用了。但是,如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。如果没有这方面的经验,这种通过“减少内存”的手段来解决内存溢出的方式会比较难以想到。

运行时常量池溢出

如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量,如:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* VM Args:-XX:PermSize=1M -XX:MaxPermSize=1M
*/
public static void main(String[] args) {
// 使用List保持着常量池引用,避免Full GC回收常量池行为
List<String> list = new ArrayList<>();
// 10MB的PermSize在integer范围内足够产生OOM了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}

运行结果:

1
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at java.lang.String.intern(Native Method) at

从运行结果中可以看到,运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是“PermGen space”,说明运行时常量池属于方法区(HotSpot虚拟机中的永久代)的一部分。

方法区溢出

方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这个区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。虽然直接使用Java SE API也可以动态产生类(如反射时的GeneratedConstructorAccessor和动态代理等),借助CGLib直接操作字节码运行时,生成了大量的动态类。在实际应用中:当前的很多主流框架,如Spring和Hibernate对类进行增强时,都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
*/
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}

static class OOMObject {

}

运行结果:

Caused by: java.lang.OutOfMemoryError: PermGen space at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632) at java.lang.ClassLoader.defineClass(ClassLoader.java:616) … 8 more

方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收掉,判定条件是非常苛刻的。在经常动态生成大量Class的应用中,需要特别注意类的回收状况。这类场景除了上面提到的程序使用了GCLib字节码增强外,常见的还有:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。

直接内存溢出

DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆的最大值(-Xmx指定)一样。上面的示例越过了DirectByteBuffer类,直接通过反射获取Unsafe实例并进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回实例,也就是设计者希望只有rt.jar中的类才能使用Unsafe的功能)。因为,虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe. allocateMemory()。

使用unsafe分配本机内存

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
*/
private static final int _1MB = 1024 * 1024;

public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}

运行结果:

1
2
3
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.huajin.Demo.main(Demo.java:28)

参考:《深入理解Java虚拟机》

微信公众号开发准备

发表于 2019-07-23 | 分类于 微信开发

** 通讯机制 **

20200423163057

** 接入步骤 **
1、填写服务器配置

2、验证服务器地址的有效性

3、依据接口文档实现业务逻辑

第1步中服务器配置包含服务器地址(URL)、令牌(Token) 和 消息加解密密钥(EncodingAESKey)。 ​ 可在开发–>基本配置–>服务器配置中配置,测试号没有EncodingAESKey

​ 服务器地址即公众号后台提供业务逻辑的入口地址,目前只支持80端口,之后包括接入验证以及任何其它的操作的请求(例如消息的发送、菜单管理、素材管理等)都要从这个地址进入。接入验证和其它请求的区别就是,接入验证时是get请求,其它时候是post请求;

Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性);

EncodingAESKey由开发者手动填写或随机生成,将用作消息体加解密密钥。本例中全部以未加密的明文消息方式,不涉及此配置项。

20200423163107

第2步 开发者提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数如下表所示:

1
2
3
4
5
参数	描述
signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
timestamp 时间戳
nonce 随机数
echostr 随机字符串\

开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:

1)将token、timestamp、nonce三个参数进行字典序排序

2)将三个参数字符串拼接成一个字符串进行sha1加密

3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信

Spring Boot实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@WebServlet("weixin.do")
public class WeixinServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String signature = req.getParameter("signature");
String timestamp = req.getParameter("timestamp");
String nonce = req.getParameter("nonce");
String echostr = req.getParameter("echostr");

PrintWriter out = resp.getWriter();
if (CheckUtil.checkSignature(signature, timestamp, nonce)) {
out.print(echostr);
}
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.hu.utils;

import java.security.MessageDigest;
import java.util.Arrays;

/**
* @Description TODO
* @Author huyunshun 2019/9/11
*/
public class CheckUtil {
/*
* 自定义token, 用作生成签名,从而验证安全性
* */
private static final String token = "demodemo";//token是在验证的时候进行配置的

public static boolean checkSignature(String signature,String timestamp,String nonce){

/**
* 接收微信服务器发送请求时传递过来的参数
*/
String[] arr = new String[]{token,timestamp,nonce};
/**
* 将token、timestamp、nonce三个参数进行字典序排序
* 并拼接为一个字符串
*/
Arrays.sort(arr);
//生成字符串
StringBuffer content = new StringBuffer();
for(int i=0;i<arr.length;i++){
content.append(arr[i]);
}
//sha1加密
String temp = getSha1(content.toString());
//校验微信服务器传递过来的签名 和 加密后的字符串是否一致, 若一致则签名通过
return temp.equals(signature);
}
public static String getSha1(String str){
if (null == str || 0 == str.length()){
return null;
}
char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f'};
try {
MessageDigest mdTemp = MessageDigest.getInstance("SHA1");
mdTemp.update(str.getBytes("UTF-8"));

byte[] md = mdTemp.digest();
int j = md.length;
char[] buf = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
buf[k++] = hexDigits[byte0 >>> 4 & 0xf];
buf[k++] = hexDigits[byte0 & 0xf];
}
return new String(buf);
} catch (Exception e) {
return null;
}
}
}

启动后保证在本地访问没问题,之后使用内网映射工具(如Ngrok)到公网。然后,在测试号中配置:

20200423163124

配置成功后说明,公众号应用已经能够和微信服务器正常通信,公众号已经接入到微信公众平台了。

** access_token **

access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。

接口调用上限每天2000次,所以不能调用太频繁。

公众号可以使用AppID和AppSecret调用本接口来获取access_token。AppID和AppSecret可在“微信公众平台-开发-基本配置”页中获得(需要已经成为开发者,且帐号没有异常状态)。

调用接口时,请登录“微信公众平台-开发-基本配置”提前将服务器IP地址添加到IP白名单中,点击查看设置方法,否则将无法调用成功。

接口调用请求说明

https请求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

1
2
3
4
参数	是否必须	说明
grant_type 是 获取access_token填写client_credential
appid 是 第三方用户唯一凭证
secret 是 第三方用户唯一凭证密钥,即appsecret

返回说明

正常情况下,微信会返回下述JSON数据包给公众号:

1
{"access_token":"ACCESS_TOKEN","expires_in":7200}

参数说明

1
2
3
4
参数	说明
access_token 获取到的凭证
expires_in 凭证有效时间,单位:秒
错误时微信会返回错误码等信息,JSON数据包示例如下(该示例为AppID无效错误):
1
{"errcode":40013,"errmsg":"invalid appid"}

返回码说明

1
2
3
4
5
6
返回码	说明
-1 系统繁忙,此时请开发者稍候再试
0 请求成功
40001 AppSecret错误或者AppSecret不属于这个公众号,请开发者确认AppSecret的正确性
40002 请确保grant_type字段值为client_credential
40164 调用接口的IP地址不在白名单中,请在接口IP白名单中进行设置。(小程序及小游戏调用不要求IP地址在白名单内。)

代码实现获取access_token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@Data
public class AccessToken {
private String tokenName; //获取到的凭证
private int expireSecond; //凭证有效时间 单位:秒

}

public class AccessTokenInfo {
public static AccessToken accessToken = null;
}

RestController
@RequestMapping("/weixin")

@Slf4j
public class AccessTokenController {

@Autowired
private RestTemplate restTemplate;

@RequestMapping("getweixintoken")
public void doWeixin() throws Exception{

System.out.println("-----启动AccessTokenController-----");

final String appId = "wxe1fe5190eeb82893";//request.getParameter("appId");
final String appSecret = "22150a2fd88b02b9ae23dad993dd6f7c";//request.getParameter("appSecret");

new Thread(()->{
while (true) {
try {
//获取accessToken
AccessTokenInfo.accessToken = getAccessToken(appId, appSecret);
//获取成功
if (AccessTokenInfo.accessToken != null) {
//获取到access_token 休眠7000秒,大约2个小时左右
Thread.sleep(7000 * 1000);
} else {
//获取失败
Thread.sleep(1000 * 3); //获取的access_token为空 休眠3秒
}
} catch (Exception e) {
log.error("发生异常:" + e.getMessage());
e.printStackTrace();
try {
Thread.sleep(1000 * 10); //发生异常休眠1秒
} catch (Exception e1) {
e.printStackTrace();
}
}
}
}).start();
}

private AccessToken getAccessToken(String appId, String appSecret) {
/**
* 接口地址为https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
*
* grant_type固定写为client_credential即可。
*/
String url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", appId, appSecret);
//此请求为https的get请求,返回的数据格式为{"access_token":"ACCESS_TOKEN","expires_in":7200}
JSONObject jsonObject = restTemplate.getForObject(url,JSONObject.class);
log.info("获取到的access_token="+jsonObject.getString("access_token"));

AccessToken token = new AccessToken();
token.setTokenName(jsonObject.getString("access_token"));
token.setExpireSecond(jsonObject.getInteger("expires_in"));
return token;
}
}

启动项目,浏览器访问,控制台:

1
2
----启动AccessTokenController-----
2019-09-12 10:25:59.899 INFO 12136 --- [ Thread-10] com.hu.config.AccessTokenController : 获取到的access_token=25_yKsFws5iSmFPTQiHBv9r97cDzEXsjY37skMbzdFzSh73q3mkzaSdCP4WUSICQ1DYMPOcxhk8mk4Ynow3koQ-1C-A53jzuY8VsNfNnNccomvBfDZryZzd5RmyR2OSWQph3uroE258k-n9WWPNHJJgAFARTT

OK!

docker部署spring boot项目(两种构建Docker镜像方式)

发表于 2019-05-25 | 分类于 docker

一、通过插件方式

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @Description TODO
* @Author huyunshun 2019/8/14
*/
@SpringBootApplication
@RestController
public class Application {

@RequestMapping("/")
public String demo() {
return "Docker";
}

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

}
1
2
server:
port: 8000

Pom:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?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>

<groupId>com.hu</groupId>
<artifactId>docker-demo</artifactId>
<version>1.0-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<properties>
<docker.image.prefix>springboot</docker.image.prefix>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.hu.Application</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>

<!-- Docker maven plugin -->
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>1.0.0</version>
<configuration>
<!-- <imageName>${docker.image.prefix}/${project.artifactId}</imageName>-->
<imageName>hu/dockerdemo</imageName>
<dockerDirectory>src/main/docker</dockerDirectory>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
</plugin>
<!-- Docker maven plugin -->
</plugins>
</build>
</project>

Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ADD docker-demo-1.0-SNAPSHOT.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

#构建 Jdk 基础环境,添加 Spring Boot Jar 到镜像中,简单解释一下:
#
#FROM ,表示使用 Jdk8 环境 为基础镜像,如果镜像不是本地的会从 DockerHub 进行下载
#VOLUME ,VOLUME 指向了一个/tmp的目录,由于 Spring Boot 使用内置的Tomcat容器,Tomcat 默认使用/tmp作为工作目录。这个命令的效果是:在宿主机的/var/lib/docker目录下创建一个临时文件并把它链接到容器中的/tmp目录
#ADD ,拷贝文件并且重命名
#ENTRYPOINT ,为了缩短 Tomcat 的启动时间,添加java.security.egd的系统属性指向/dev/urandom作为 ENTRYPOINT

首先,保证 通过mvn package 打包运行,没有问题。

再进行构建镜像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
PS D:\java-workspaces\idea-work\spring-boot-sample-demo\docker-demo> mvn package docker:build
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------------< com.hu:docker-demo >-------------------------
[INFO] Building docker-demo 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:3.1.0:resources (default-resources) @ docker-demo ---
[WARNING] Using platform encoding (GBK actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 1 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.8.0:compile (default-compile) @ docker-demo ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-resources-plugin:3.1.0:testResources (default-testResources) @ docker-demo ---
[WARNING] Using platform encoding (GBK actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory D:\java-workspaces\idea-work\spring-boot-sample-demo\docker-demo\src\test\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.0:testCompile (default-testCompile) @ docker-demo ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:2.22.1:test (default-test) @ docker-demo ---
[INFO] No tests to run.
[INFO]
[INFO] --- maven-jar-plugin:3.1.1:jar (default-jar) @ docker-demo ---
[INFO]
[INFO] --- spring-boot-maven-plugin:2.1.3.RELEASE:repackage (default) @ docker-demo ---
[INFO] Replacing main artifact with repackaged archive
[INFO]
[INFO] --- docker-maven-plugin:1.0.0:build (default-cli) @ docker-demo ---
[INFO] Using authentication suppliers: [ConfigFileRegistryAuthSupplier]
[INFO] Copying D:\java-workspaces\idea-work\spring-boot-sample-demo\docker-demo\target\docker-demo-1.0-SNAPSHOT.jar -> D:\java-workspaces\idea-work\spring-boot-sample-demo\docker-demo\target\docker\docker-demo-1.0-SNAPSHOT.jar
[INFO] Copying src\main\docker\Dockerfile -> D:\java-workspaces\idea-work\spring-boot-sample-demo\docker-demo\target\docker\Dockerfile
[INFO] Building image hu/dockerdemo
Step 1/4 : FROM openjdk:8-jdk-alpine

Pulling from library/openjdk
e7c96db7181b: Pull complete
f910a506b6cb: Pull complete
c2274a1a0e27: Pull complete
Digest: sha256:94792824df2df33402f201713f932b58cb9de94a0cd524164a0f2283343547b3
Status: Downloaded newer image for openjdk:8-jdk-alpine
---> a3562aa0b991
Step 2/4 : VOLUME /tmp

---> Running in 85aea02ce334
Removing intermediate container 85aea02ce334
---> b32bc2fbb3f0
Step 3/4 : ADD docker-demo-1.0-SNAPSHOT.jar app.jar

---> 909569c00d0d
Step 4/4 : ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

---> Running in 88c174756528
Removing intermediate container 88c174756528
---> b3ca1f38330a
ProgressMessage{id=null, status=null, stream=null, error=null, progress=null, progressDetail=null}
Successfully built b3ca1f38330a
Successfully tagged hu/dockerdemo:latest
[INFO] Built hu/dockerdemo
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 52.666 s
[INFO] Finished at: 2019-08-15T11:47:14+08:00
[INFO] ------------------------------------------------------------------------

查看镜像:

1
2
3
PS D:\java-workspaces\idea-work\spring-boot-sample-demo\docker-demo> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hu/dockerdemo latest b3ca1f38330a About a minute ago 122MB

运行:

1
2
3
4
5
6
7
8
9
PS D:\java-workspaces\idea-work\spring-boot-sample-demo\docker-demo> docker run --name docker-demo-springboot -p 8000:8000 b3ca1f38330a

. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.3.RELEASE)

启动完成后;就可以访问。

遇到的问题:

1
[ERROR] Failed to execute goal com.spotify:docker-maven-plugin:1.0.0:build (default-cli) on project docker-demo: Exception caught: java.util.concurrent.ExecutionException: com.spotify.docker.client.shaded.javax.ws.rs.ProcessingException: org.apache.http.conn.HttpHostConnectException: Connect to localhost:2375 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused: connect -> [Help 1]

设置下docker

二、普通方式

上面的Dockerfile 和 springBoot 打包的项目(可以在pom中去掉docker插件)docker-demo-1.0-SNAPSHOT.jar放到一个目录中

执行指令:docker build -t hu-docker-demo .

执行docker build命令,docker就会根据Dockerfile里你定义好的命令进行构建新的镜像。

-t :指定要创建的目标镜像名 名字:镜像的tag
.代表当前目录,也就是Dockerfile所在的目录,可以指定Dockerfile 的绝对路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
PS D:\java-workspaces\idea-work\spring-boot-sample-demo\docker-demo\1111> docker build -t hu-docker-demo .
Sending build context to Docker daemon 16.72MB
Step 1/4 : FROM openjdk:8-jdk-alpine
---> a3562aa0b991
Step 2/4 : VOLUME /tmp
---> Using cache
---> b32bc2fbb3f0
Step 3/4 : ADD docker-demo-1.0-SNAPSHOT.jar app.jar
---> Using cache
---> 48f1eb33fed5
Step 4/4 : ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
---> Using cache
---> 5e7b25db2cbf
Successfully built 5e7b25db2cbf
Successfully tagged hu-docker-demo:latest
SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.

#增加了镜像
PS D:\java-workspaces\idea-work\spring-boot-sample-demo\docker-demo\1111> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
docker latest 5e7b25db2cbf 9 minutes ago 122MB
hu-docker-demo latest 5e7b25db2cbf 9 minutes ago 122MB

#运行
PS D:\java-workspaces\idea-work\spring-boot-sample-demo\docker-demo\1111> docker run --name docker-demo-hu -p 8000:8000 hu-docker-demo

. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.3.RELEASE)

2019-08-15 05:50:27.972 INFO 1 --- [ main] com.hu.Application : Starting Application on 842d73789909 with PID 1 (/app.jar started by root in /)
2019-08-15 05:50:27.982 INFO 1 --- [ main] com.hu.Application : No active profile set, falling back to default profiles: default
2019-08-15 05:50:29.357 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8000 (http)
2019-08-15 05:50:29.390 INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2019-08-15 05:50:29.391 INFO 1 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.16]
2019-08-15 05:50:29.403 INFO 1 --- [ main] o.a.catalina.core.AprLifecycleListener : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [/usr/lib/jvm/java-1.8-openjdk/jre/lib/amd64/server:/usr/lib/jvm/java-1.8-openjdk/jre/lib/amd64:/usr/lib/jvm/java-1.8-openjdk/jre/../lib/amd64:/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib]
2019-08-15 05:50:29.470 INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2019-08-15 05:50:29.470 INFO 1 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1392 ms
2019-08-15 05:50:29.701 INFO 1 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-08-15 05:50:30.044 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8000 (http) with context path ''
2019-08-15 05:50:30.048 INFO 1 --- [ main] com.hu.Application : Started Application in 2.773 seconds (JVM running for 3.202)

完成!

提交镜像到hub.docker.com网站;
执行docker login登录,期间会要求输入用户名和密码;

执行命令docker push hu-docker-demo,即可将本地镜像push到hub.docker.com; 注意镜像名称的前缀,例如我这里的前缀是bolingcavalry,要和账号保持一致;

提交成功后,在hub.docker.com网站即可看到此镜像,如下图,此时任何人都可以pull来下使用了

Elastic-Job分布式定时任务

发表于 2019-05-22 | 分类于 分布式

Elastic-Job是当当网大牛基于Zookepper,Quartz开发并且开源的Java分布式定时任务,解决Quartz不支持分布式的弊端。它由两个相互独立的子项目Elastic-Job-Lite和Elastic-Job-Cloud组成。

基本概念

  • 分片概念:任务分布式的执行,需要将一个任务拆分成多个独立的任务项,然后由分布式的服务器分别执行某一个或几个分片项
  • 个性化参数:shardingItemParameter,可以和分片项匹配对应关系。比如:将商品的状态分成上架,下架。那么配置0=上架,1=下架,代码中直接使用上架下架的枚举值即可完成分片项与业务逻辑的对应关系
  • 作用高可用:将分片总数设置成1,多台服务器执行作业将采用1主n从的方式执行
  • 弹性扩容:将任务拆分为n个任务项后,各个服务器分别执行各自分配到的任务项。一旦有新的服器加入集群或有服务器宕机。Elastic-Job将保留本次任务不变,下次任务开始前重新分片。
  • 并行调度:采用任务分片方式实现。将一个任务拆分为n个独立的任务项,由分布式的服务器并行执行各自分配到的分片项。
  • 集中管理:采用基于zookepper的注册中心,集中管理和协调分布式作业的状态,分配和监听。外部系统可直接根据Zookeeper的数据管理和监控elastic-job。
  • 定制化流程任务:作业可分为简单和数据流处理两种模式,数据流又分为高吞吐处理模式和顺序性处理模式,其中高吞吐处理模式可以开启足够多的线程快速的处理数据,而顺序性处理模式将每个分片项分配到一个独立线程,用于保证同一分片的顺序性,这点类似于kafka的分区顺序性。

20200423145846

Elastic-Job的具体模块的底层及如何实现

Elastic-Job采用去中心化设计,主要分为注册中心、数据分片、分布式协调、定时任务处理和定制化流程型任务等模块。

去中心化:指Elastic-Job没有调度中心这一概念。每个运行在集群中的作业服务器都是对等的,节点之间通过注册中心进行分布式协调。
但elastic-job有主节点的概念,主节点用于处理一些集中式任务,如分片,清理运行时信息等,并无调度功能,定时调度都是由作业服务器自行触发。

| 中心化 | 去中心化
-|-|-
实现难度 | 难 | 易
部署难度 | 难 | 易
触发时间统一控制 | 可以 | 不可以
触发延迟 | 有 | 无
异构语言支持 | 容易 | 困难

** 注册中心** :注册中心模块目前直接使用zookeeper,用于记录作业的配置,服务器信息以及作业运行状态。Zookeeper虽然很成熟,但原理复杂,使用较难,在海量数据支持的情况下也会有性能和网络问题。
** 数据分片** :数据分片是elastic-job中实现分布式的重要概念,将真实数据和逻辑分片对应,用于解耦作业框架和数据的关系。作业框架只负责将分片合理的分配给相关的作业服务器,而作业服务器需要根据所分配的分片匹配数据进行处理。服务器分片目前都存储在注册中心中,各个服务器根据自己的IP地址拉取分片。
** 分布式协调** :分布式协调模块用于处理作业服务器的动态扩容缩容。一旦集群中有服务器发生变化,分布式协调将自动监测并将变化结果通知仍存活的作业服务器。协调时将会涉及主节点选举,重分片等操作。目前使用的Zookeeper的临时节点和监听器实现主动检查和通知功能。
** 定时任务处理** :定时任务处理根据cron表达式定时触发任务,目前有防止任务同时触发,错过任务重出发等功能。主要还是使用Quartz本身的定时调度功能,为了便于控制,每个任务都使用独立的线程池。
** 定制化流程型任务** :定制化流程型任务将定时任务分为多种流程,有不经任何修饰的简单任务;有用于处理数据的fetchData/processData的数据流任务;以后还将增加消息流任务,文件任务,工作流任务等。用户能以插件的形式扩展并贡献代码。

作业开发

Elastic-Job提供Simple、Dataflow和Script 3种作业类型。方法参数shardingContext包含作业配置、片和运行时信息。可通过getShardingTotalCount(), getShardingItem()等方法分别获取分片总数,运行在本作业服务器的分片序列号等。
Simple类型的作业:该类型意为简单实现,只需实现SimpleJob接口,重写它的execute方法即可
Dataflow类型作业:用于处理数据流,实现DataflowJob接口,并重写两个方法——用于抓取(fetchData方法)和处理(processData方法)数据。比如在fetchData方法里面查询没有上架的商品,在processData方法修改该商品的状态。
注意:可通过DataflowJobConfiguration配置是否流式处理。当配置成流式处理,fetchData方法返回值(返回值是集合)是null或长度是0,作业才停止抓取,否则将一直运行。非流式的则每次作业只执行一次这两个方法就结束该作业。
Script类型作业:意为脚本类型作业,支持shell、python、perl等类型脚本。只需通过控制台或代码配置scriptCommandLine即可,无需编码。

Demo

创建一个项目 配置:

1
2
3
4
5
6
7
8
spring:
application:
name: job1

#配置zookeeper
regCenter:
serverList: localhost:2181
namespace: job1

注册中心配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 配置zookeeper
*/
@Configuration
@ConditionalOnExpression("'${regCenter.serverList}'.length() > 0") //ConditionalOnExpression决定是否生效
public class JobRegistryCenterConfig {

@Bean(initMethod = "init")
public ZookeeperRegistryCenter regCenter(@Value("${regCenter.serverList}") final String serverList,
@Value("${regCenter.namespace}") final String namespace) {
return new ZookeeperRegistryCenter(new ZookeeperConfiguration(serverList, namespace));
}

}

定义一个任务:

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
/**
* 一个任务
*/
public class MySimpleJob implements SimpleJob {
Logger logger = LoggerFactory.getLogger(MySimpleJob.class);

private String name;

public MySimpleJob() {
super();
}
public MySimpleJob(String name) {
this.name = name;
}
@Override
public void execute(ShardingContext shardingContext) {
logger.info(String.format("任务:%s "+
"Thread ID: %s 作业分片总数: %s " +
"当前分片项: %s 当前参数: %s " +
"作业名称: %s 作业自定义参数: %s "
,
this.name,
Thread.currentThread().getId(),
shardingContext.getShardingTotalCount(),
shardingContext.getShardingItem(),
shardingContext.getShardingParameter(),
shardingContext.getJobName(),
shardingContext.getJobParameter()
));
}
}

配置任务作业:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 配置任务
*/
@Configuration
public class MyJobConfig {
/**
* 配置任务的时候,这里定义了四个参数,分别是:
*
* cron:cron表达式,用于控制作业触发时间。
* shardingTotalCount:作业分片总数
* shardingItemParameters:分片序列号和参数用等号分隔,多个键值对用逗号分隔
* 分片序列号从0开始,不可大于或等于作业分片总数
*
*/
private final String cron = "0/10 * * * * ?"; //每十秒
private final int shardingTotalCount = 2;//作业分片总数
private final String shardingItemParameters = "0=A,1=B";//分片序列号和参数用等号分隔,多个键值对用逗号分隔
private final String jobParameters = "hello";//作业自定义参数,可通过传递该参数为作业调度的业务方法传参,用于实现带参数的作业

@Autowired
private ZookeeperRegistryCenter regCenter;

@Bean
public SimpleJob stockJob() {
// 任务
return new MySimpleJob("One");
}

//初始化作业
@Bean(initMethod = "init")
public JobScheduler simpleJobScheduler(final SimpleJob simpleJob) {
return new SpringJobScheduler(simpleJob, regCenter, getLiteJobConfiguration(simpleJob.getClass(),
cron, shardingTotalCount, shardingItemParameters, jobParameters));
}

private LiteJobConfiguration getLiteJobConfiguration(final Class<? extends SimpleJob> jobClass,
final String cron,
final int shardingTotalCount,
final String shardingItemParameters,
final String jobParameters) {
// 定义作业核心配置
JobCoreConfiguration simpleCoreConfig = JobCoreConfiguration.newBuilder(jobClass.getName(), cron, shardingTotalCount).
shardingItemParameters(shardingItemParameters).jobParameter(jobParameters).build();
// 定义SIMPLE类型配置
SimpleJobConfiguration simpleJobConfig = new SimpleJobConfiguration(simpleCoreConfig, jobClass.getCanonicalName());
// 定义Lite作业根配置
LiteJobConfiguration simpleJobRootConfig = LiteJobConfiguration.newBuilder(simpleJobConfig).overwrite(true).build();
return simpleJobRootConfig;

}
}

启动后,两个分片交替执行,并且每次都是新启动一个线程执行作业。

如果,修改端口,启动两个实例,查看日志如下:

1
2
3
4
5
6
7
8
2019-07-30 16:20:50.227  INFO 14088 --- [b.MySimpleJob-2] com.hu.job.MySimpleJob                   : 任务:One  Thread ID: 57   作业分片总数: 2   当前分片项: 1   当前参数: B  作业名称: com.hu.job.MySimpleJob   作业自定义参数: hello  
2019-07-30 16:20:50.226 INFO 14088 --- [b.MySimpleJob-1] com.hu.job.MySimpleJob : 任务:One Thread ID: 56 作业分片总数: 2 当前分片项: 0 当前参数: A 作业名称: com.hu.job.MySimpleJob 作业自定义参数: hello
2019-07-30 16:21:00.112 INFO 14088 --- [pleJob_Worker-1] com.hu.job.MySimpleJob : 任务:One Thread ID: 26 作业分片总数: 2 当前分片项: 1 当前参数: B 作业名称: com.hu.job.MySimpleJob 作业自定义参数: hello
2019-07-30 16:21:10.052 INFO 14088 --- [pleJob_Worker-1] com.hu.job.MySimpleJob : 任务:One Thread ID: 26 作业分片总数: 2 当前分片项: 1 当前参数: B 作业名称: com.hu.job.MySimpleJob 作业自定义参数: hello
2019-07-30 16:21:20.036 INFO 14088 --- [pleJob_Worker-1] com.hu.job.MySimpleJob : 任务:One Thread ID: 26 作业分片总数: 2 当前分片项: 1 当前参数: B 作业名称: com.hu.job.MySimpleJob 作业自定义参数: hello
2019-07-30 16:21:30.039 INFO 14088 --- [pleJob_Worker-1] com.hu.job.MySimpleJob : 任务:One Thread ID: 26 作业分片总数: 2 当前分片项: 1 当前参数: B 作业名称: com.hu.job.MySimpleJob 作业自定义参数: hello
2019-07-30 16:21:40.048 INFO 14088 --- [pleJob_Worker-1] com.hu.job.MySimpleJob : 任务:One Thread ID: 26 作业分片总数: 2 当前分片项: 1 当前参数: B 作业名称: com.hu.job.MySimpleJob 作业自定义参数: hello
2019-07-30 16:21:50.043 INFO 14088 --- [pleJob_Worker-1] com.hu.job.MySimpleJob : 任务:One Thread ID: 26 作业分片总数: 2 当前分片项: 1 当前参数: B 作业名称: com.hu.job.MySimpleJob 作业自定义参数: hello
1
2
3
4
2019-07-30 16:21:00.181  INFO 3348 --- [pleJob_Worker-1] com.hu.job.MySimpleJob                   : ▒▒▒▒One  Thread ID: 20   ▒▒ҵ▒▒Ƭ▒▒▒▒: 2   ▒▒ǰ▒▒Ƭ▒▒: 0   ▒▒ǰ▒▒▒▒: A  ▒▒ҵ▒▒▒▒: com.hu.job.MySimpleJob   ▒▒ҵ▒Զ▒▒▒▒▒▒: hello
2019-07-30 16:21:10.065 INFO 3348 --- [pleJob_Worker-1] com.hu.job.MySimpleJob : ▒▒▒▒One Thread ID: 20 ▒▒ҵ▒▒Ƭ▒▒▒▒: 2 ▒▒ǰ▒▒Ƭ▒▒: 0 ▒▒ǰ▒▒▒▒: A ▒▒ҵ▒▒▒▒: com.hu.job.MySimpleJob ▒▒ҵ▒Զ▒▒▒▒▒▒: hello
2019-07-30 16:21:20.048 INFO 3348 --- [pleJob_Worker-1] com.hu.job.MySimpleJob : ▒▒▒▒One Thread ID: 20 ▒▒ҵ▒▒Ƭ▒▒▒▒: 2 ▒▒ǰ▒▒Ƭ▒▒: 0 ▒▒ǰ▒▒▒▒: A ▒▒ҵ▒▒▒▒: com.hu.job.MySimpleJob ▒▒ҵ▒Զ▒▒▒▒▒▒: hello
2019-07-30 16:21:30.052 INFO 3348 --- [pleJob_Worker-1] com.hu.job.MySimpleJob : ▒▒▒▒One Thread ID: 20 ▒▒ҵ▒▒Ƭ▒▒▒▒: 2 ▒▒ǰ▒▒Ƭ▒▒: 0 ▒▒ǰ▒▒▒▒: A ▒▒ҵ▒▒▒▒: com.hu.job.MySimpleJob ▒▒ҵ▒Զ▒▒▒▒▒▒: hello

可以看出,刚开始没有启动第二个实例的时候,分片交替作业,,当第二个实例启动以后,由于分成两片,就分别执行。

注册中心数据结构

20200423145907

作业名称节点下又包含4个数据子节点,分别是config, instances, sharding, servers和leader。

config节点

作业配置信息,以JSON格式存储

instances节点

作业运行实例信息,子节点是当前作业运行实例的主键。作业运行实例主键由作业运行服务器的IP地址和PID构成。作业运行实例主键均为临时节点,当作业实例上线时注册,下线时自动清理。注册中心监控这些节点的变化来协调分布式作业的分片以及高可用。 可在作业运行实例节点写入TRIGGER表示该实例立即执行一次。

sharding节点

作业分片信息,子节点是分片项序号,从零开始,至分片总数减一。分片项序号的子节点存储详细信息。每个分片项下的子节点用于控制和记录分片运行状态。节点详细信息说明:

子节点名 临时节点 描述
instance 否 执行该分片项的作业运行实例主键
running 是 分片项正在运行的状态 仅配置monitorExecution时有效
failover 是 如果该分片项被失效转移分配给其他作业服务器,则此节点值记录执行此分片的作业服务器IP
misfire 否 是否开启错过任务重新执行
disabled 否 是否禁用此分片项

servers节点

作业服务器信息,子节点是作业服务器的IP地址。可在IP地址节点写入DISABLED表示该服务器禁用。 在新的cloud native架构下,servers节点大幅弱化,仅包含控制服务器是否可以禁用这一功能。为了更加纯粹的实现job核心,servers功能未来可能删除,控制服务器是否禁用的能力应该下放至自动化部署系统。

leader节点

作业服务器主节点信息,分为election,sharding和failover三个子节点。分别用于主节点选举,分片和失效转移处理。

leader节点是内部使用的节点,如果对作业框架原理不感兴趣,可不关注此节点。

子节点名 临时节点 描述
election\instance 是 主节点服务器IP地址
一旦该节点被删除将会触发重新选举
重新选举的过程中一切主节点相关的操作都将阻塞
election\latch 否 主节点选举的分布式锁
为curator的分布式锁使用
sharding\necessary 否 是否需要重新分片的标记
如果分片总数变化,或作业服务器节点上下线或启用/禁用,以及主节点选举,会触发设置重分片标记
作业在下次执行时使用主节点重新分片,且中间不会被打断
作业执行时不会触发分片
sharding\processing 是 主节点在分片时持有的节点
如果有此节点,所有的作业执行都将阻塞,直至分片结束
主节点分片结束或主节点崩溃会删除此临时节点
failover\items\分片项 否 一旦有作业崩溃,则会向此节点记录
当有空闲作业服务器时,会从此节点抓取需失效转移的作业项
failover\items\latch 否 分配失效转移分片项时占用的分布式锁
为curator的分布式锁使用

https://blog.csdn.net/qq924862077/article/details/82956790

代码见:https://github.com/huingsn/tech-point-record

Docker Kubernetes项目

发表于 2019-05-21 | 分类于 docker

介绍

Kubernetes 是 Google 团队发起并维护的基于 Docker 的开源容器集群管理系统,它不仅支持常见的云平台,而且支持内部数据中心。

建于 Docker 之上的 Kubernetes 可以构建一个容器的调度服务,其目的是让用户透过 Kubernetes 集群来进行云端容器集群的管理,而无需用户进行复杂的设置工作。系统会自动选取合适的工作节点来执行具体的容器集群调度处理工作。其核心概念是 Container Pod。一个 Pod 由一组工作于同一物理工作节点的容器构成。这些组容器拥有相同的网络命名空间、IP以及存储配额,也可以根据实际情况对每一个 Pod 进行端口映射。此外,Kubernetes 工作节点会由主系统进行管理,节点包含了能够运行 Docker 容器所用到的服务。

它的目标是管理跨多个主机的容器,提供基本的部署,维护以及运用伸缩,主要实现语言为 Go 语言。Kubernetes 是:

  • 易学:轻量级,简单,容易理解
  • 便携:支持公有云,私有云,混合云,以及多种云平台
  • 可拓展:模块化,可插拔,支持钩子,可任意组合
  • 自修复:自动重调度,自动重启,自动复制

Kubernetes 构建于 Google 数十年经验,一大半来源于 Google 生产环境规模的经验。结合了社区最佳的想法和实践。

在分布式系统中,部署,调度,伸缩一直是最为重要的也最为基础的功能。Kubernetes 就是希望解决这一序列问题的。

基本说明

具体见 Kubernetes 学习笔记

快速部署

Kubernetes 依赖 Etcd 服务来维护所有主节点的状态。Etcd 参见etcd项目介绍。

启动 Etcd 服务

docker run –net=host -d gcr.io/google_containers/etcd:2.0.9 /usr/local/bin/etcd –addr=127.0.0.1:4001 –bind-addr=0.0.0.0:4001 –data-dir=/var/etcd/data

启动主节点(启动 kubelet)

docker run –net=host -d -v /var/run/docker.sock:/var/run/docker.sock gcr.io/google_containers/hyperkube:v0.17.0 /hyperkube kubelet –api_servers=http://localhost:8080 –v=2 –address=0.0.0.0 –enable_server –hostname_override=127.0.0.1 –config=/etc/kubernetes/manifests

启动服务代理

docker run -d –net=host –privileged gcr.io/google_containers/hyperkube:v0.17.0 /hyperkube proxy –master=http://127.0.0.1:8080 –v=2

测试:在本地访问 8080 端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ curl 127.0.0.1:8080
{
"paths": [
"/api",
"/api/v1beta1",
"/api/v1beta2",
"/api/v1beta3",
"/healthz",
"/healthz/ping",
"/logs/",
"/metrics",
"/static/",
"/swagger-ui/",
"/swaggerapi/",
"/validate",
"/version"
]
}

查看服务

所有服务启动后,查看本地实际运行的 Docker 容器,有如下几个。

1
2
3
4
5
6
7
8
CONTAINER ID        IMAGE                                        COMMAND                CREATED             STATUS              PORTS               NAMES
ee054db2516c gcr.io/google_containers/hyperkube:v0.17.0 "/hyperkube schedule 2 days ago Up 1 days k8s_scheduler.509f29c9_k8s-master-127.0.0.1_default_9941e5170b4365bd4aa91f122ba0c061_e97037f5
3b0f28de07a2 gcr.io/google_containers/hyperkube:v0.17.0 "/hyperkube apiserve 2 days ago Up 1 days k8s_apiserver.245e44fa_k8s-master-127.0.0.1_default_9941e5170b4365bd4aa91f122ba0c061_6ab5c23d
2eaa44ecdd8e gcr.io/google_containers/hyperkube:v0.17.0 "/hyperkube controll 2 days ago Up 1 days k8s_controller-manager.33f83d43_k8s-master-127.0.0.1_default_9941e5170b4365bd4aa91f122ba0c061_1a60106f
30aa7163cbef gcr.io/google_containers/hyperkube:v0.17.0 "/hyperkube proxy -- 2 days ago Up 1 days jolly_davinci
a2f282976d91 gcr.io/google_containers/pause:0.8.0 "/pause" 2 days ago Up 2 days k8s_POD.e4cc795_k8s-master-127.0.0.1_default_9941e5170b4365bd4aa91f122ba0c061_e8085b1f
c060c52acc36 gcr.io/google_containers/hyperkube:v0.17.0 "/hyperkube kubelet 2 days ago Up 1 days serene_nobel
cc3cd263c581 gcr.io/google_containers/etcd:2.0.9 "/usr/local/bin/etcd 2 days ago Up 1 days happy_turing

这些服务大概分为三类:主节点服务、工作节点服务和其它服务。

主节点服务

apiserver 是整个系统的对外接口,提供 RESTful 方式供客户端和其它组件调用;

scheduler 负责对资源进行调度,分配某个 pod 到某个节点上;

controller-manager 负责管理控制器,包括 endpoint-controller(刷新服务和 pod 的关联信息)和 replication-controller(维护某个 pod 的复制为配置的数值)。

工作节点服务

kubelet 是工作节点执行操作的 agent,负责具体的容器生命周期管理,根据从数据库中获取的信息来管理容器,并上报 pod 运行状态等;

proxy 为 pod 上的服务提供访问的代理。

其它服务

Etcd 是所有状态的存储数据库;

Docker Etcd项目

发表于 2019-05-20 | 分类于 docker

Etcd 是 CoreOS 团队发起的一个管理配置信息和服务发现(Service Discovery)的项目。

它的目标是构建一个高可用的分布式键值(key-value)数据库,基于 Go 语言实现。在分布式系统中,各种服务的配置信息的管理分享,服务的发现是一个很基本同时也是很重要的问题。CoreOS 项目就希望基于 etcd 来解决这一问题。etcd 目前在 github.com/etcd-io/etcd 进行维护。

受到 Apache ZooKeeper 项目和 doozer 项目的启发,etcd 在设计的时候重点考虑了下面四个要素:

  • 简单:具有定义良好、面向用户的 API (gRPC)

  • 安全:支持 HTTPS 方式的访问

  • 快速:支持并发 10 k/s 的写操作

  • 可靠:支持分布式结构,基于 Raft 的一致性算法

  • Apache ZooKeeper 是一套知名的分布式系统中进行同步和一致性管理的工具。

  • doozer 是一个一致性分布式数据库。

  • Raft 是一套通过选举主节点来实现分布式系统一致性的算法,相比于大名鼎鼎的 Paxos 算法,它的过程更容易被人理解,由 Stanford 大学的 Diego Ongaro 和 John Ousterhout 提出。更多细节可以参考 raftconsensus.github.io。

一般情况下,用户使用 etcd 可以在多个节点上启动多个实例,并添加它们为一个集群。同一个集群中的 etcd 实例将会保持彼此信息的一致性。

安装

二进制文件方式下载

编译好的二进制文件都在 github.com/etcd-io/etcd/releases 页面,用户可以选择需要的版本:

下载解压:

1
2
3
4
5
6
7
8
9
[root@VM_0_3_centos ~]#curl -L https://github.com/etcd-io/etcd/releases/download/v3.4.0/etcd-v3.4.0-linux-amd64.tar.gz -o etcd-v3.4.0-linux-amd64.tar.gz
[root@VM_0_3_centos ~]#tar xzf etcd-v3.4.0-linux-amd64.tar.gz
[root@VM_0_3_centos ~]# ls
dockerfile etcd-v3.4.0-linux-amd64 etcd-v3.4.0-linux-amd64.tar.gz
[root@VM_0_3_centos ~]# mv etcd-v3.4.0-linux-amd64 /usr/local/etcd3.4/

[root@VM_0_3_centos ~]# ls /usr/local/etcd3.4/
Documentation etcd etcdctl README-etcdctl.md README.md READMEv2-etcdctl.md
[root@VM_0_3_centos ~]# cd /usr/local/etcd3.4/

复制到目录,并且复制到bin,etcd 是服务主文件,etcdctl 是提供给用户的命令客户端。

1
2
3
[root@VM_0_3_centos etcd3.4]# cp etcd* /usr/local/bin/
[root@VM_0_3_centos etcd3.4]# ls /usr/local/bin/
etcd etcdctl

启动

默认 2379 端口处理客户端的请求,2380 端口用于集群各成员间的通信。启动 etcd 显示类似如下的信息:

1
2
3
4
5
6
[root@VM_0_3_centos etcd3.4]# etcd
[WARNING] Deprecated '--logger=capnslog' flag is set; use '--logger=zap' flag instead
2019-11-05 15:19:01.909992 I | etcdmain: etcd Version: 3.4.0
.......
2019-11-05 15:19:02.634795 I | embed: ready to serve client requests
2019-11-05 15:19:02.635348 N | embed: serving insecure client requests on 127.0.0.1:2379, this is strongly discouraged!

测试:

1
2
3
4
5
6
7
8
#使用 etcdctl 命令进行测试,设置和获取键值 testkey: "hello world",检查 etcd 服务是否启动成功:
[root@VM_0_3_centos ~]# ETCDCTL_API=3 etcdctl member list
8e9e05c52164694d, started, default, http://localhost:2380, http://localhost:2379, false
[root@VM_0_3_centos ~]# ETCDCTL_API=3 etcdctl put testkey "hello world"
OK
[root@VM_0_3_centos ~]# etcdctl get testkey
testkey
hello world

也可以通过 HTTP 访问本地 2379 或 4001 端口的方式来进行操作,例如查看 testkey 的值:curl -L http://localhost:4001/v2/keys/testkey

Docker 镜像方式运行

镜像名称为 quay.io/coreos/etcd,可以通过下面的命令启动 etcd 服务监听到 2379 和 2380 端口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ docker run \
-p 2379:2379 \
-p 2380:2380 \
--mount type=bind,source=/tmp/etcd-data.tmp,destination=/etcd-data \
--name etcd-gcr-v3.4.0 \
quay.io/coreos/etcd:v3.4.0 \
/usr/local/bin/etcd \
--name s1 \
--data-dir /etcd-data \
--listen-client-urls http://0.0.0.0:2379 \
--advertise-client-urls http://0.0.0.0:2379 \
--listen-peer-urls http://0.0.0.0:2380 \
--initial-advertise-peer-urls http://0.0.0.0:2380 \
--initial-cluster s1=http://0.0.0.0:2380 \
--initial-cluster-token tkn \
--initial-cluster-state new \
--log-level info \
--logger zap \
--log-outputs stderr

打开新的终端按照上一步的方法测试 etcd 是否成功启动。

macOS 中运行

$ brew install etcd

$ etcd

$ etcdctl member list

Etcd 集群

使用 Docker Compose 模拟启动一个 3 节点的 etcd 集群。

docker-compose.yml 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
version: "3.6"
services:

node1:
image: quay.io/coreos/etcd:v3.4.0
volumes:
- node1-data:/etcd-data
expose:
- 2379
- 2380
networks:
cluster_net:
ipv4_address: 172.16.238.100
environment:
- ETCDCTL_API=3
command:
- /usr/local/bin/etcd
- --data-dir=/etcd-data
- --name
- node1
- --initial-advertise-peer-urls
- http://172.16.238.100:2380
- --listen-peer-urls
- http://0.0.0.0:2380
- --advertise-client-urls
- http://172.16.238.100:2379
- --listen-client-urls
- http://0.0.0.0:2379
- --initial-cluster
- node1=http://172.16.238.100:2380,node2=http://172.16.238.101:2380,node3=http://172.16.238.102:2380
- --initial-cluster-state
- new
- --initial-cluster-token
- docker-etcd

node2:
image: quay.io/coreos/etcd:v3.4.0
volumes:
- node2-data:/etcd-data
networks:
cluster_net:
ipv4_address: 172.16.238.101
environment:
- ETCDCTL_API=3
expose:
- 2379
- 2380
command:
- /usr/local/bin/etcd
- --data-dir=/etcd-data
- --name
- node2
- --initial-advertise-peer-urls
- http://172.16.238.101:2380
- --listen-peer-urls
- http://0.0.0.0:2380
- --advertise-client-urls
- http://172.16.238.101:2379
- --listen-client-urls
- http://0.0.0.0:2379
- --initial-cluster
- node1=http://172.16.238.100:2380,node2=http://172.16.238.101:2380,node3=http://172.16.238.102:2380
- --initial-cluster-state
- new
- --initial-cluster-token
- docker-etcd

node3:
image: quay.io/coreos/etcd:v3.4.0
volumes:
- node3-data:/etcd-data
networks:
cluster_net:
ipv4_address: 172.16.238.102
environment:
- ETCDCTL_API=3
expose:
- 2379
- 2380
command:
- /usr/local/bin/etcd
- --data-dir=/etcd-data
- --name
- node3
- --initial-advertise-peer-urls
- http://172.16.238.102:2380
- --listen-peer-urls
- http://0.0.0.0:2380
- --advertise-client-urls
- http://172.16.238.102:2379
- --listen-client-urls
- http://0.0.0.0:2379
- --initial-cluster
- node1=http://172.16.238.100:2380,node2=http://172.16.238.101:2380,node3=http://172.16.238.102:2380
- --initial-cluster-state
- new
- --initial-cluster-token
- docker-etcd

volumes:
node1-data:
node2-data:
node3-data:

networks:
cluster_net:
driver: bridge
ipam:
driver: default
config:
-
subnet: 172.16.238.0/24

使用 docker-compose up 启动集群之后使用 docker exec 命令登录到任一节点测试 etcd 集群。

1
2
3
4
5
6
7
8
9
10
[root@VM_0_3_centos tmp]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
45c34fa7d4d5 quay.io/coreos/etcd:v3.4.0 "/usr/local/bin/etcd…" 25 minutes ago Up 20 minutes 2379-2380/tcp etcd_node1_1
b1d9ecc03d64 quay.io/coreos/etcd:v3.4.0 "/usr/local/bin/etcd…" 25 minutes ago Up 25 minutes 2379-2380/tcp etcd_node3_1
dae9d24f3197 quay.io/coreos/etcd:v3.4.0 "/usr/local/bin/etcd…" 25 minutes ago Up 25 minutes 2379-2380/tcp etcd_node2_1
[root@VM_0_3_centos tmp]# docker exec -it 45 /bin/sh
# etcdctl member list
daf3fd52e3583ff, started, node3, http://172.16.238.102:2380, http://172.16.238.102:2379, false
422a74f03b622fef, started, node1, http://172.16.238.100:2380, http://172.16.238.100:2379, false
ed635d2a2dbef43d, started, node2, http://172.16.238.101:2380, http://172.16.238.101:2379, false

使用 etcdctl

etcdctl 支持如下的命令,大体上分为数据库操作和非数据库操作两类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
NAME:
etcdctl - A simple command line client for etcd3.

USAGE:
etcdctl

VERSION:
3.4.0

API VERSION:
3.4


COMMANDS:
get Gets the key or a range of keys
put Puts the given key into the store
del Removes the specified key or range of keys [key, range_end)
txn Txn processes all the requests in one transaction
compaction Compacts the event history in etcd
alarm disarm Disarms all alarms
alarm list Lists all alarms
defrag Defragments the storage of the etcd members with given endpoints
endpoint health Checks the healthiness of endpoints specified in `--endpoints` flag
endpoint status Prints out the status of endpoints specified in `--endpoints` flag
watch Watches events stream on keys or prefixes
version Prints the version of etcdctl
lease grant Creates leases
lease revoke Revokes leases
lease timetolive Get lease information
lease keep-alive Keeps leases alive (renew)
member add Adds a member into the cluster
member remove Removes a member from the cluster
member update Updates a member in the cluster
member list Lists all members in the cluster
snapshot save Stores an etcd node backend snapshot to a given file
snapshot restore Restores an etcd member snapshot to an etcd directory
snapshot status Gets backend snapshot status of a given file
make-mirror Makes a mirror at the destination etcd cluster
migrate Migrates keys in a v2 store to a mvcc store
lock Acquires a named lock
elect Observes and participates in leader election
auth enable Enables authentication
auth disable Disables authentication
user add Adds a new user
user delete Deletes a user
user get Gets detailed information of a user
user list Lists all users
user passwd Changes password of user
user grant-role Grants a role to a user
user revoke-role Revokes a role from a user
role add Adds a new role
role delete Deletes a role
role get Gets detailed information of a role
role list Lists all roles
role grant-permission Grants a key to a role
role revoke-permission Revokes a key from a role
check perf Check the performance of the etcd cluster
help Help about any command

OPTIONS:
--cacert="" verify certificates of TLS-enabled secure servers using this CA bundle
--cert="" identify secure client using this TLS certificate file
--command-timeout=5s timeout for short running command (excluding dial timeout)
--debug[=false] enable client-side debug logging
--dial-timeout=2s dial timeout for client connections
--endpoints=[127.0.0.1:2379] gRPC endpoints
--hex[=false] print byte strings as hex encoded strings
--insecure-skip-tls-verify[=false] skip server certificate verification
--insecure-transport[=true] disable transport security for client connections
--key="" identify secure client using this TLS key file
--user="" username[:password] for authentication (prompt if password is not supplied)
-w, --write-out="simple" set the output format (fields, json, protobuf, simple, table)

数据库操作

数据库操作围绕对键值和目录的 CRUD (符合 REST 风格的一套操作:Create)完整生命周期的管理。

etcd 在键的组织上采用了层次化的空间结构(类似于文件系统中目录的概念),用户指定的键可以为单独的名字,如 testkey,此时实际上放在根目录 / 下面,也可以为指定目录结构,如 cluster1/node2/testkey,则将创建相应的目录结构。

注:CRUD 即 Create, Read, Update, Delete,是符合 REST 风格的一套 API 操作。

put/set

1
2
$ etcdctl put /testdir/testkey "Hello world"
OK

get

获取指定键的值。例如

1
2
3
4
5
$ etcdctl put testkey hello
OK
$ etcdctl get testkey
testkey
hello

支持的选项为

–sort 对结果进行排序

–consistent 将请求发给主节点,保证获取内容的一致性

del

删除某个键值。例如

1
2
$ etcdctl del testkey
1

update

当键存在时,更新值内容。例如

1
2
3
4
$ etcdctl set testkey hello
hello
$ etcdctl update testkey world
world

当键不存在时,则会报错。例如

1
2
$ etcdctl update testkey2 world
Error: 100: Key not found (/testkey2) [1]

非数据库操作

watch

监测一个键值的变化,一旦键值发生更新,就会输出最新的值。

例如,用户更新 testkey 键值为 Hello world。

$ etcdctl watch testkey

PUT

testkey
2

member

通过 list、add、update、remove 命令列出、添加、更新、删除 etcd 实例到 etcd 集群中。

例如本地启动一个 etcd 服务实例后,可以用如下命令进行查看。

1
2
$ etcdctl member list
422a74f03b622fef, started, node1, http://172.16.238.100:2380, http://172.16.238.100:23

Docker Swarm 项目

发表于 2019-05-19 | 分类于 docker

Docker Swarm 是 Docker 官方三剑客项目之一,提供 Docker 容器集群服务,是 Docker 官方对容器云生态进行支持的核心方案。

使用它,用户可以将多个 Docker 主机封装为单个大型的虚拟 Docker 主机,快速打造一套容器云平台。

注意:Docker 1.12.0+ Swarm mode 已经内嵌入 Docker 引擎,成为了 docker 子命令 docker swarm,绝大多数用户已经开始使用 Swarm mode,Docker 引擎 API 已经删除 Docker Swarm。Swarm mode 内置 kv 存储功能,提供了众多的新特性,比如:具有容错能力的去中心化设计、内置服务发现、负载均衡、路由网格、动态伸缩、滚动更新、安全传输等。使得 Docker 原生的 Swarm 集群具备与 Mesos、Kubernetes 竞争的实力。

基本概念

节点

运行 Docker 的主机可以主动初始化一个 Swarm 集群或者加入一个已存在的 Swarm 集群,这样这个运行 Docker 的主机就成为一个 Swarm 集群的节点 (node) 。

节点分为管理 (manager) 节点和工作 (worker) 节点。

管理节点用于 Swarm 集群的管理,docker swarm 命令基本只能在管理节点执行(节点退出集群命令 docker swarm leave 可以在工作节点执行)。一个 Swarm 集群可以有多个管理节点,但只有一个管理节点可以成为 leader,leader 通过 raft 协议实现。

工作节点是任务执行节点,管理节点将服务 (service) 下发至工作节点执行。管理节点默认也作为工作节点。你也可以通过配置让服务只运行在管理节点。

服务和任务

任务 (Task)是 Swarm 中的最小的调度单位,目前来说就是一个单一的容器。

服务 (Services) 是指一组任务的集合,服务定义了任务的属性。服务有两种模式:

1
2
3
replicated services 按照一定规则在各个工作节点上运行指定个数的任务。
global services 每个工作节点上运行一个任务
两种模式通过 docker service create 的 --mode 参数指定。

创建 Swarm 集群

创建好docker主机后,在管理主机上初始化集群。

1
2
3
4
5
6
7
8
9
10
11
12
$ docker-machine ssh manager #进入该docker主机

docker@manager:~$ docker swarm init --advertise-addr 192.168.99.100
Swarm initialized: current node (dxn1zf6l61qsb1josjja83ngz) is now a manager.

To add a worker to this swarm, run the following command:

docker swarm join \
--token SWMTKN-1-49nj1cmql0jkz5s954yi3oex3nedyz0fb0xx14ie39trti4wxv-8vxv8rssmk743ojnwacrr2e7c \
192.168.99.100:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

增加工作节点

创建主机

$ docker-machine create -d virtualbox worker1

进入虚拟主机 worker1

$ docker-machine ssh worker1

1
2
3
4
5
6
7
$ docker-machine ssh worker1

docker@worker1:~$ docker swarm join \
--token SWMTKN-1-49nj1cmql0jkz5s954yi3oex3nedyz0fb0xx14ie39trti4wxv-8vxv8rssmk743ojnwacrr2e7c \
192.168.99.100:2377

This node joined a swarm as a worker.

查看集群

经过上边的两步,我们已经拥有了一个最小的 Swarm 集群,包含一个管理节点和两个工作节点。

在管理节点使用 docker node ls 查看集群。

$ docker node ls

命令 docker info 可以查看 swarm 集群状态

部署服务

新建服务

进入集群管理节点:

docker-machine ssh manager1

使用 docker 中国镜像:docker pull registry.docker-cn.com/….

在Swarm 集群中运行一个名为 nginx 服务。

$ docker service create –replicas 3 -p 80:80 –name nginx nginx:1.13.7-alpine ping baidu.com

现在我们使用浏览器,输入任意节点 IP ,即可看到 nginx 默认页面。

命令解释

1
2
3
4
docker service create 命令创建一个服务
--name 服务名称命名为 nginx
--replicas 设置启动的示例数
alpine指的是使用的镜像名称,ping 指的是容器运行的bash

查看服务

使用 docker service ls 来查看当前 Swarm 集群运行的服务。

1
2
3
$ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
kc57xffvhul5 nginx replicated 3/3 nginx:1.13.7-alpine *:80->80/tcp

使用 docker service ps 来查看某个服务的详情。

1
2
3
4
5
$ docker service ps nginx
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
pjfzd39buzlt nginx.1 nginx:1.13.7-alpine swarm2 Running Running about a minute ago
hy9eeivdxlaa nginx.2 nginx:1.13.7-alpine swarm1 Running Running about a minute ago
36wmpiv7gmfo nginx.3 nginx:1.13.7-alpine swarm3 Running Running about a minute ago

使用 docker service logs 来查看某个服务的日志。

1
2
3
4
5
6
7
$ docker service logs nginx
nginx.3.36wmpiv7gmfo@swarm3 | 10.255.0.4 - - [25/Nov/2017:02:10:30 +0000] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:58.0) Gecko/20100101 Firefox/58.0" "-"
nginx.3.36wmpiv7gmfo@swarm3 | 10.255.0.4 - - [25/Nov/2017:02:10:30 +0000] "GET /favicon.ico HTTP/1.1" 404 169 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:58.0) Gecko/20100101 Firefox/58.0" "-"
nginx.3.36wmpiv7gmfo@swarm3 | 2017/11/25 02:10:30 [error] 5#5: *1 open() "/usr/share/nginx/html/favicon.ico" failed (2: No such file or directory), client: 10.255.0.4, server: localhost, request: "GET /favicon.ico HTTP/1.1", host: "192.168.99.102"
nginx.1.pjfzd39buzlt@swarm2 | 10.255.0.2 - - [25/Nov/2017:02:10:26 +0000] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:58.0) Gecko/20100101 Firefox/58.0" "-"
nginx.1.pjfzd39buzlt@swarm2 | 10.255.0.2 - - [25/Nov/2017:02:10:27 +0000] "GET /favicon.ico HTTP/1.1" 404 169 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:58.0) Gecko/20100101 Firefox/58.0" "-"
nginx.1.pjfzd39buzlt@swarm2 | 2017/11/25 02:10:27 [error] 5#5: *1 open() "/usr/share/nginx/html/favicon.ico" failed (2: No such file or directory), client: 10.255.0.2, server: localhost, request: "GET /favicon.ico HTTP/1.1", host: "192.168.99.101"

删除服务

使用 docker service rm 来从 Swarm 集群移除某个服务。

$ docker service rm nginx

查看加入集群manager管理节点的命令

1
2
3
4
5
6
7
8
9
10
11
#docker swarm join-token manager
To add a manager to this swarm, run the following command:
docker swarm join --token SWMTKN-1-3sp9uxzokgr252u1jauoowv74930s7f8f5tsmm5mlk5oim359e-7tdlpdnkyfl1bnq34ftik9wxw 192.168.139.175:2377

查看加入集群worker节点的命令

# docker swarm join-token worker
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-3sp9uxzokgr252u1jauoowv74930s7f8f5tsmm5mlk5oim359e-dk52k5uul50w49gbq4j1y7zzb 192.168.139.175:2377
```
### 查docker swarm的管理网络

#docker network ls
NETWORK ID NAME DRIVER SCOPE
05efca714d2f bridge bridge local
c9cd9c37edd7 docker_gwbridge bridge local
10ac9e48d81b host host local
n60tdenc5jy7 ingress overlay swarm
a9284277dc18 none null local

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
## 服务伸缩
我们可以使用 docker service scale 对一个服务运行的容器数量进行伸缩。当业务处于高峰期时,我们需要扩展服务运行的容器数量。

$ docker service scale nginx=5

当业务平稳时,我们需要减少服务运行的容器数量。

$ docker service scale nginx=2

调整实例个数 调整 nginx 的服务实例数为2个

docker service update --replicas 2 nginx

## 监控集群状态

登录管理节点 manager1:docker-machine ssh manager1

运行 docker service inspect --pretty <SERVICE-ID> 查询服务概要状态,以 nginx 服务为例:

docker@manager1:~$ docker service inspect –pretty nginx

1
2
3
4
### 查询服务详细信息
docker service inspect nginx
### 查看那个节点正在运行服务
运行docker service ps <SERVICE-ID>

docker@manager1:~$ docker service ps helloworld

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
### 在工作节点查看任务的执行情况
首先进入docker-machine ssh worker1

再在节点执行docker ps 查看容器的运行状态。

### 查看容器的运行状态
docker@worker1:~$ docker ps


这样的话,我们在 Swarm 集群中成功的运行了一个 helloworld 服务,根据命令可以看出在 worker1 节点上运行。


### 退出 Swarm 集群

如果 Manager 想要退出 Swarm 集群, 在 Manager Node 上执行如下命令:

docker swarm leave

就可以退出集群,如果集群中还存在其它的 Worker Node,还希望 Manager 退出集群,则加上一个强制选项,命令行如下所示:

docker swarm leave --force

在 Worker2 上进行退出测试,登录 worker2 节点

docker-machine ssh worker2

执行退出命令

docker swarm leave

重新搭建命令,使用 VirtualBox 做测试的时候,如果想重复实验可以将实验节点删掉再重来。

#停止虚拟机
docker-machine stop [arg…] #一个或多个虚拟机名称

docker-machine stop manager1 worker1 worker2
#移除虚拟机
docker-machine rm [OPTIONS] [arg…]

docker-machine rm manager1 worker1 worker2
停止、删除虚拟主机后,再重新创建即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

排空节点上的集群容器 。
docker node update --availability drain g36lvv23ypjd8v7ovlst2n3yt

主动离开集群,让节点处于down状态,才能删除
docker swarm leave

删除指定节点 (管理节点上操作)
docker node rm g36lvv23ypjd8v7ovlst2n3yt

管理节点,解散集群
docker swarm leave --force

解散集群步骤:

[root@server2 ~]# docker swarm leave #server2对应的node节点离开集群
[root@server3 ~]# docker swarm leave #server3对应的node节点离开集群
[root@server1 ~]# docker swarm leave –force #必须使用参数–force,强制离开集群,否则会报错
[root@server1 ~]# docker node ls #此时再次查看集群的节点,会报错。这是因为i集群已经散了,该节点不再是manager节点,而只有manager节点才能查看集群的节点。

1
2
3

# Portainer
使用Portainer管理集群

#docker service create
–name portainer
–publish 9000:9000
–constraint ‘node.role == manager’
–mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock
portainer/portainer
-H unix:///var/run/docker.sock
#docker images |grep portainer
portainer/portainer latest 07cde96d4789 2 weeks ago 10.4MB

#docker service ls ###查看集群列表
ID NAME MODE REPLICAS IMAGE PORTS
p5bo3n0fmqgz portainer replicated 1/1 portainer/portainer:latest *:9000->9000/tcp

这就部署好了  浏览器输入http://localhost:9000

Docker容器里的centos、unbuntu无法使用 systemctl 命令

发表于 2019-05-19 | 分类于 docker

据说在 Linux Docker中无法使用 systemd(systemctl) 相关命令的原因是 1号进程不是 init ,而是其他例如 /bin/bash ,所以导致缺少相关文件无法运行。(System has not been booted with systemd as init system (PID 1). Can’t operat)

解决方案:/sbin/init

docker run -dit –privileged 07d413ca6e34 /usr/sbin/init

docker exec -it test /bin/bash

–privilaged=true一定要加上的。

上一页123…25下一页
初晨

初晨

永远不要说你知道本质,更别说真相了。

249 日志
46 分类
109 标签
近期文章
  • WebSocket、Socket、TCP、HTTP区别
  • Springboot项目的接口防刷
  • 深入理解Volatile关键字及其实现原理
  • 使用vscode搭建个人笔记环境
  • HBase介绍安装与操作
© 2018 — 2020 Copyright
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4