一只病猫

静坐常思己过,闲谈莫论人非

  • 首页
  • 归档
  • 标签
  • 分类
  • 搜索

面试分享(一).JAVA知识

发表于 2019-04-18 | 分类于 Java |

JAVA知识

ArrayList和LinkList有什么区别

  • ArrayList是数组实现的集合操作,而LinkedList是链表实现的集合操作,(LinkedList是双向链表,有next也有previous)。
  • 只是用List集合中的get()方法根据索引取数据的时候,ArrayList的时间复杂度为“O(1)”,而LinkedList的时间复杂度为“O(n)”(n为集合的长度),因为LinkedList要移动指针。
  • ArrayList在使用的时候默认的初始化数组的长度为10,如果空间不足则会采用2倍的形式进行容量的扩充,如果保存大数据的时候有可能造成垃圾的产生以及性能的下降,这个时候就可以用LinkedList子类保存。对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。

JDK动态代理与CGLib动态代理的区别

  • 区别
    • java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
    • 而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
    • 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
    • 如果目标对象实现了接口,可以强制使用CGLIB实现AOP
    • 如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换
  • 如何强制使用CGLIB实现AOP?
    • 添加CGLIB库,SPRING_HOME/cglib/*.jar
    • 在spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true"/>
  • JDK动态代理和CGLIB字节码生成的区别

    • JDK动态代理只能对实现了接口的类生成代理,而不能针对类
    • CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法
      因为是继承,所以该类或方法最好不要声明成final
  • 代码实现

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
package com.lf.shejimoshi.proxy.entity;
//用户管理接口
public interface UserManager {
//新增用户抽象方法
void addUser(String userName,String password);
//删除用户抽象方法
void delUser(String userName);

}

package com.lf.shejimoshi.proxy.entity;
//用户管理实现类,实现用户管理接口
public class UserManagerImpl implements UserManager{
//重写新增用户方法
@Override
public void addUser(String userName, String password) {
System.out.println("调用了新增的方法!");
System.out.println("传入参数为 userName: "+userName+" password: "+password);
}
//重写删除用户方法
@Override
public void delUser(String userName) {
System.out.println("调用了删除的方法!");
System.out.println("传入参数为 userName: "+userName);
}

}

//JDK动态代理
package com.lf.shejimoshi.proxy.jdk;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import com.lf.shejimoshi.proxy.entity.UserManager;
import com.lf.shejimoshi.proxy.entity.UserManagerImpl;
//JDK动态代理实现InvocationHandler接口
public class JdkProxy implements InvocationHandler {
private Object target ;//需要代理的目标对象

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("JDK动态代理,监听开始!");
Object result = method.invoke(target, args);
System.out.println("JDK动态代理,监听结束!");
return result;
}
//定义获取代理对象方法
private Object getJDKProxy(Object targetObject){
//为目标对象target赋值
this.target = targetObject;
//JDK动态代理只能针对实现了接口的类进行代理,newProxyInstance 函数所需参数就可看出
return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(), targetObject.getClass().getInterfaces(), this);
}

public static void main(String[] args) {
JdkProxy jdkProxy = new JdkProxy();//实例化JDKProxy对象
UserManager user = (UserManager) jdkProxy.getJDKProxy(new UserManagerImpl());//获取代理对象
user.addUser("admin", "123123");//执行新增方法
}

}

//Cglib动态代理(需要导入两个jar包,asm-5.2.jar,cglib-3.2.5.jar。版本自行选择)
package com.lf.shejimoshi.proxy.cglib;

import java.lang.reflect.Method;

import com.lf.shejimoshi.proxy.entity.UserManager;
import com.lf.shejimoshi.proxy.entity.UserManagerImpl;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

//Cglib动态代理,实现MethodInterceptor接口
public class CglibProxy implements MethodInterceptor {
private Object target;//需要代理的目标对象

//重写拦截方法
@Override
public Object intercept(Object obj, Method method, Object[] arr, MethodProxy proxy) throws Throwable {
System.out.println("Cglib动态代理,监听开始!");
Object invoke = method.invoke(target, arr);//方法执行,参数:target 目标对象 arr参数数组
System.out.println("Cglib动态代理,监听结束!");
return invoke;
}
//定义获取代理对象方法
public Object getCglibProxy(Object objectTarget){
//为目标对象target赋值
this.target = objectTarget;
Enhancer enhancer = new Enhancer();
//设置父类,因为Cglib是针对指定的类生成一个子类,所以需要指定父类
enhancer.setSuperclass(objectTarget.getClass());
enhancer.setCallback(this);// 设置回调
Object result = enhancer.create();//创建并返回代理对象
return result;
}

public static void main(String[] args) {
CglibProxy cglib = new CglibProxy();//实例化CglibProxy对象
UserManager user = (UserManager) cglib.getCglibProxy(new UserManagerImpl());//获取代理对象
user.delUser("admin");//执行删除方法
}

}

Java中序列化有哪些方式

Java原生序列化

Java原生序列化方法即通过Java原生流(InputStream和OutputStream之间的转化)的方式进行转化。需要注意的是JavaBean实体类必须实现Serializable接口,否则无法序列化。Java原生序列化代码示例如下所示:

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
package serialize;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author liqqc
*
*/
public class JavaSerialize {
public static void main(String[] args) throws ClassNotFoundException, IOException {
new JavaSerialize().start();
}

public void start() throws IOException, ClassNotFoundException {
User u = new User();
List<User> friends = new ArrayList<>();
u.setUserName("张三");
u.setPassWord("123456");
u.setUserInfo("张三是一个很牛逼的人");
u.setFriends(friends);

User f1 = new User();
f1.setUserName("李四");
f1.setPassWord("123456");
f1.setUserInfo("李四是一个很牛逼的人");

User f2 = new User();
f2.setUserName("王五");
f2.setPassWord("123456");
f2.setUserInfo("王五是一个很牛逼的人");

friends.add(f1);
friends.add(f2);

Long t1 = System.currentTimeMillis();
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream obj = new ObjectOutputStream(out);
for(int i = 0; i<10; i++) {
obj.writeObject(u);
}
System.out.println("java serialize: " +(System.currentTimeMillis() - t1) + "ms; 总大小:" + out.toByteArray().length );

Long t2 = System.currentTimeMillis();
ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new java.io.ByteArrayInputStream(out.toByteArray())));
User user = (User) ois.readObject();
System.out.println("java deserialize: " + (System.currentTimeMillis() - t2) + "ms; User: " + user);
}

}

Json序列化

Json序列化一般会使用jackson包,通过ObjectMapper类来进行一些操作,比如将对象转化为byte数组或者将json串转化为对象。现在的大多数公司都将json作为服务器端返回的数据格式。比如调用一个服务器接口,通常的请求为xxx.json?a=xxx&b=xxx的形式。Json序列化示例代码如下所示:

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
package serialize;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import com.fasterxml.jackson.databind.ObjectMapper;
/**
*
* @author liqqc
*
*/
public class JsonSerialize {
public static void main(String[] args) throws IOException {
new JsonSerialize().start();
}

public void start() throws IOException {
User u = new User();
List<User> friends = new ArrayList<>();
u.setUserName("张三");
u.setPassWord("123456");
u.setUserInfo("张三是一个很牛逼的人");
u.setFriends(friends);

User f1 = new User();
f1.setUserName("李四");
f1.setPassWord("123456");
f1.setUserInfo("李四是一个很牛逼的人");

User f2 = new User();
f2.setUserName("王五");
f2.setPassWord("123456");
f2.setUserInfo("王五是一个很牛逼的人");

friends.add(f1);
friends.add(f2);

ObjectMapper mapper = new ObjectMapper();
Long t1 = System.currentTimeMillis();
byte[] writeValueAsBytes = null;
for (int i = 0; i < 10; i++) {
writeValueAsBytes = mapper.writeValueAsBytes(u);
}
System.out.println("json serialize: " + (System.currentTimeMillis() - t1) + "ms; 总大小:" + writeValueAsBytes.length);
Long t2 = System.currentTimeMillis();
User user = mapper.readValue(writeValueAsBytes, User.class);
System.out.println("json deserialize: " + (System.currentTimeMillis() - t2) + "ms; User: " + user);

}
}

FastJson序列化

fastjson 是由阿里巴巴开发的一个性能很好的Java 语言实现的 Json解析器和生成器。特点:速度快,测试表明fastjson具有极快的性能,超越任其他的java json parser。功能强大,完全支持java bean、集合、Map、日期、Enum,支持范型和自省。无依赖,能够直接运行在Java SE 5.0以上版本

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
package serialize;

import java.util.ArrayList;
import java.util.List;

import com.alibaba.fastjson.JSON;
/**
*
* @author liqqc
*
*/
public class FastJsonSerialize {

public static void main(String[] args) {
new FastJsonSerialize().start();
}

public void start(){
User u = new User();
List<User> friends = new ArrayList<>();
u.setUserName("张三");
u.setPassWord("123456");
u.setUserInfo("张三是一个很牛逼的人");
u.setFriends(friends);

User f1 = new User();
f1.setUserName("李四");
f1.setPassWord("123456");
f1.setUserInfo("李四是一个很牛逼的人");

User f2 = new User();
f2.setUserName("王五");
f2.setPassWord("123456");
f2.setUserInfo("王五是一个很牛逼的人");

friends.add(f1);
friends.add(f2);

//序列化
Long t1 = System.currentTimeMillis();
String text = null;
for(int i = 0; i<10; i++) {
text = JSON.toJSONString(u);
}
System.out.println("fastJson serialize: " +(System.currentTimeMillis() - t1) + "ms; 总大小:" + text.getBytes().length);
//反序列化
Long t2 = System.currentTimeMillis();
User user = JSON.parseObject(text, User.class);
System.out.println("fastJson serialize: " + (System.currentTimeMillis() -t2) + "ms; User: " + user);
}
}

ProtoBuff序列化

ProtocolBuffer是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化。适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。

  • 优点:跨语言;序列化后数据占用空间比JSON小,JSON有一定- 的格式,在数据量上还有可以压缩的空间。

缺点:它以二进制的方式存储,无法直接读取编辑,除非你有 .proto 定义,否则无法直接读出 Protobuffer的任何内容。

其与thrift的对比:两者语法类似,都支持版本向后兼容和向前兼容,thrift侧重点是构建跨语言的可伸缩的服务,支持的语言多,同时提供了全套RPC解决方案,可以很方便的直接构建服务,不需要做太多其他的工作。 Protobuffer主要是一种序列化机制,在数据序列化上进行性能比较,Protobuffer相对较好。

ProtoBuff序列化对象可以很大程度上将其压缩,可以大大减少数据传输大小,提高系统性能。对于大量数据的缓存,也可以提高缓存中数据存储量。原始的ProtoBuff需要自己写.proto文件,通过编译器将其转换为java文件,显得比较繁琐。百度研发的jprotobuf框架将Google原始的protobuf进行了封装,对其进行简化,仅提供序列化和反序列化方法。其实用上也比较简洁,通过对JavaBean中的字段进行注解就行,不需要撰写.proto文件和实用编译器将其生成.java文件,百度的jprotobuf都替我们做了这些事情了。

一个带有jprotobuf注解的JavaBean如下所示

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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package serialize;

import java.io.Serializable;
import java.util.List;
import com.baidu.bjf.remoting.protobuf.FieldType;
import com.baidu.bjf.remoting.protobuf.annotation.Protobuf;

public class User implements Serializable {
private static final long serialVersionUID = -7890663945232864573L;

@Protobuf(fieldType = FieldType.INT32, required = false, order = 1)
private Integer userId;

@Protobuf(fieldType = FieldType.STRING, required = false, order = 2)
private String userName;

@Protobuf(fieldType = FieldType.STRING, required = false, order = 3)
private String passWord;

@Protobuf(fieldType = FieldType.STRING, required = false, order = 4)
private String userInfo;

@Protobuf(fieldType = FieldType.OBJECT, required = false, order = 5)
private List<User> friends;

public Integer getUserId() {
return userId;
}

public void setUserId(Integer userId) {
this.userId = userId;
}

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

public String getPassWord() {
return passWord;
}

public void setPassWord(String passWord) {
this.passWord = passWord;
}

public String getUserInfo() {
return userInfo;
}

public void setUserInfo(String userInfo) {
this.userInfo = userInfo;
}

public List<User> getFriends() {
return friends;
}

public void setFriends(List<User> friends) {
this.friends = friends;
}

@Override
public String toString() {
return "User [userId=" + userId + ", userName=" + userName + ", passWord=" + passWord + ", userInfo=" + userInfo
+ ", friends=" + friends + "]";
}

}

package serialize;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import com.baidu.bjf.remoting.protobuf.Codec;
import com.baidu.bjf.remoting.protobuf.ProtobufProxy;
/**
*
* @author liqqc
*
*/
public class ProtoBuffSerialize {

public static void main(String[] args) throws IOException {
new ProtoBuffSerialize().start();
}

public void start() throws IOException {
Codec<User> studentClassCodec = ProtobufProxy.create(User.class, false);

User u2 = new User();
List<User> friends = new ArrayList<>();
u2.setUserName("张三");
u2.setPassWord("123456");
u2.setUserInfo("张三是一个很牛逼的人");
u2.setFriends(friends);

User f1 = new User();
f1.setUserName("李四");
f1.setPassWord("123456");
f1.setUserInfo("李四是一个很牛逼的人");

User f2 = new User();
f2.setUserName("王五");
f2.setPassWord("123456");
f2.setUserInfo("王五是一个很牛逼的人");
friends.add(f1);
friends.add(f2);

Long stime_jpb_encode = System.currentTimeMillis();
byte[] bytes = null;
for(int i = 0; i<10; i++) {
bytes = studentClassCodec.encode(u2);
}
System.out.println("jprotobuf序列化耗时:" + (System.currentTimeMillis() - stime_jpb_encode) + "ms; 总大小:" + bytes.length);

Long stime_jpb_decode = System.currentTimeMillis();
User user = studentClassCodec.decode(bytes);
Long etime_jpb_decode = System.currentTimeMillis();
System.out.println("jprotobuf反序列化耗时:"+ (etime_jpb_decode-stime_jpb_decode) + "ms; User: " + user);
}

}

序列化底层实现

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
public class SerialDemo {

public static void main(String[] args) throws IOException, ClassNotFoundException {
//序列化
FileOutputStream fos = new FileOutputStream("object.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
User user1 = new User("xuliugen", "123456", "male");
oos.writeObject(user1);
oos.flush();
oos.close();
//反序列化
FileInputStream fis = new FileInputStream("object.out");
ObjectInputStream ois = new ObjectInputStream(fis);
User user2 = (User) ois.readObject();
System.out.println(user2.getUserName()+ " " +
user2.getPassword() + " " + user2.getSex());
//反序列化的输出结果为:xuliugen 123456 male
}
}

public class User implements Serializable {
private String userName;
private String password;
private String sex;
//全参构造方法、get和set方法省略
}

object.out文件如下
image
注:上图中0000000h-000000c0h表示行号;0-f表示列;行后面的文字表示对这行16进制的解释;对上述字节码所表述的内容感兴趣的可以对照相关的资料,查阅一下每一个字符代表的含义,这里不在探讨!

类似于我们Java代码编译之后的.class文件,每一个字符都代表一定的含义。序列化和反序列化的过程就是生成和解析上述字符的过程!

序列化图示:
image
反序列化图示:
image

相关注意事项

1、序列化时,只对对象的状态进行保存,而不管对象的方法;

2、当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;

3、当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;

4、并非所有的对象都可以序列化,至于为什么不可以,有很多原因了,比如:

安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行RMI传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的;

资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现;

5、声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态,transient代表对象的临时数据。

6、序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义serialVersionUID有两种用途:

在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;

在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

7、Java有很多基础类已经实现了serializable接口,比如String,Vector等。但是也有一些没有实现serializable接口的;

8、如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存!这是能用序列化解决深拷贝的重要原因;

并发编程的包,AQS和普通锁相比有什么好处

队列同步器AbstractQueuedSynchronizer,是用来构建锁或者其他同步组件的基础框架,它使用一个int成员表示同步状态,通过内部的FIFO队列来完成资源获取线程的排序工作。

AQS的设计

AQS的设计是基于模板方法模式的,也就是说,使用者需要继承AQS并重写指定的方法,随后将AQS组合在自定义同步组件的实现中,并调用AQS提供的模板方法,而这些模板方法将会调用使用者重写的方法。

1
private volatile int state;

AQS使用一个int的成员变量来表示同步状态。

1
2
3
protected final void setState(int newState) {
state = newState;
}

setState方法用来设置同步状态

1
2
3
protected final int getState() {
return state;
}

getState方法用来获取同步状态

1
2
3
4
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

compareAndSetState方法使用CAS操作来讲同步状态设置为给定的值

AQS中的同步队列

最开始就提到过AQS内部维护着一个FIFO的队列。而AQS就是依赖这个同步队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个节点Node,并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

同步队列中节点属性

1
2
//共享锁对应的节点
static final Node SHARED = new Node();

因为如果是共享锁,线程可以被多个线程获得。所以将这个属性定义为一个常量。

1
2
 //独占锁对应的节点
static final Node EXCLUSIVE = null;

独占锁因为只能对一个线程获得,所以设置为null,当某个线程获得锁时,将该线程对应的赋予这个属性

1
2
//节点的等待状态
volatile int waitStatus;

节点的等待状态有4个

  • CANCELLED:值为1,由于在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消等待,节点进入该状态将不会变化
  • SINGAL:值为-1,后继节点的线程处于等待状态,当前节点如果释放了同步状态,将会通知后继节点,使后继节点得以运行
  • CONDITION:值为-2,节点在等待队列中(这个在Condition的博客里会讲到),节点线程等待在Condition上,当其他线程对Condition调用了singal后,该节点会从等待队列转移到同步队列,加入到对同步状态的获取中去
  • PROPAGEATE:值为-3,表示下一次共享式同步状态获取将会无条件被传播下去
    1
    2
    //前驱节点,当节点加入同步队列时被设置(尾部添加)
    volatile Node prev;

同步队列中某个节点的前驱节点

1
2
//后继节点
volatile Node next;

同步队列中某个节点的后继节点

1
2
//等待队列的后继节点。如果当前节点是共享的,那么这个字段将是一个SHARED常量
Node nextWaiter;

这个是等待队列的后继节点(不是同步队列)

1
2
//获取同步状态的线程
volatile Thread thread;

当前获取到同步状态的线程

节点时构成同步队列的基础,AQS拥有首节点和尾节点,没有成功获取到同步状态的节点会加入到同步队列的尾部,同步队列的结构如下图所示
image
同步器AQS包含两个节点类型的引用,一个指向头结点,一个指向尾节点。

同步队列的操作

  • 将节点加入到同步队列:当一个线程成功获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全。AQS提供了一个基于CAS的设置尾节点的方法:compareAndSerTail,它需要传递当前线程认为的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
    image
  • 将节点设置为首节点:同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。
    image
    设置首节点是通过成功获取同步状态的线程完成的,由于只有一个线程能成功获取到同步状态,因此设置头节点并不需要使用CAS来保证,它只需要将首节点设置为原首节点的后继节点并断开原首节点的next引用即可。

synchronized底层实现,加在方法上和加在同步代码块中编译后的区别、类锁、对象锁。

原文链接

概念

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:

  • 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。

一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞

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
/**
* 同步线程
*/
class SyncThread implements Runnable {
private static int count;

public SyncThread() {
count = 0;
}

public void run() {
synchronized(this) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public int getCount() {
return count;
}
}
//SyncThread的调用
SyncThread syncThread = new SyncThread();
Thread thread1 = new Thread(syncThread, "SyncThread1");
Thread thread2 = new Thread(syncThread, "SyncThread2");
thread1.start();
thread2.start();
//结果如下
SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9*

当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。

我们再把SyncThread的调用稍微改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Thread thread1 = new Thread(new SyncThread(), "SyncThread1");
Thread thread2 = new Thread(new SyncThread(), "SyncThread2");
thread1.start();
thread2.start();
//结果如下:
SyncThread1:0
SyncThread2:1
SyncThread1:2
SyncThread2:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread1:7
SyncThread1:8
SyncThread2:9

不是说一个线程执行synchronized代码块时其它的线程受阻塞吗?为什么上面的例子中thread1和thread2同时在执行。这是因为synchronized只锁定对象,每个对象只有一个锁(lock)与之相关联,而上面的代码等同于下面这段代码:

1
2
3
4
5
6
SyncThread syncThread1 = new SyncThread();
SyncThread syncThread2 = new SyncThread();
Thread thread1 = new Thread(syncThread1, "SyncThread1");
Thread thread2 = new Thread(syncThread2, "SyncThread2");
thread1.start();
thread2.start();

这时创建了两个SyncThread的对象syncThread1和syncThread2,线程thread1执行的是syncThread1对象中的synchronized代码(run),而线程thread2执行的是syncThread2对象中的synchronized代码(run);我们知道synchronized锁定的是对象,这时会有两把锁分别锁定syncThread1对象和syncThread2对象,而这两把锁是互不干扰的,不形成互斥,所以两个线程可以同时执行。

当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。

多个线程访问synchronized和非synchronized代码块

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
class Counter implements Runnable{
private int count;

public Counter() {
count = 0;
}

public void countAdd() {
synchronized(this) {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

//非synchronized代码块,未对count进行读写操作,所以可以不用synchronized
public void printCount() {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + " count:" + count);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.equals("A")) {
countAdd();
} else if (threadName.equals("B")) {
printCount();
}
}
}
//调用代码
Counter counter = new Counter();
Thread thread1 = new Thread(counter, "A");
Thread thread2 = new Thread(counter, "B");
thread1.start();
thread2.start();
//结果如下
A:0
B count:1
A:1
B count:2
A:2
B count:3
A:3
B count:4
A:4
B count:5

上面代码中countAdd是一个synchronized的,printCount是非synchronized的。从上面的结果中可以看出一个线程访问一个对象的synchronized代码块时,别的线程可以访问该对象的非synchronized代码块而不受阻塞。

指定要给某个对象加锁

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
/**
* 银行账户类
*/
class Account {
String name;
float amount;

public Account(String name, float amount) {
this.name = name;
this.amount = amount;
}
//存钱
public void deposit(float amt) {
amount += amt;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//取钱
public void withdraw(float amt) {
amount -= amt;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public float getBalance() {
return amount;
}
}

/**
* 账户操作类
*/
class AccountOperator implements Runnable{
private Account account;
public AccountOperator(Account account) {
this.account = account;
}

public void run() {
synchronized (account) {
account.deposit(500);
account.withdraw(500);
System.out.println(Thread.currentThread().getName() + ":" + account.getBalance());
}
}
}

//调用代码
Account account = new Account("zhang san", 10000.0f);
AccountOperator accountOperator = new AccountOperator(account);

final int THREAD_NUM = 5;
Thread threads[] = new Thread[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i ++) {
threads[i] = new Thread(accountOperator, "Thread" + i);
threads[i].start();
}

//结果如下
Thread3:10000.0
Thread2:10000.0
Thread1:10000.0
Thread4:10000.0
Thread0:10000.0

在AccountOperator 类中的run方法里,我们用synchronized 给account对象加了锁。这时,当一个线程访问account对象时,其他试图访问account对象的线程将会阻塞,直到该线程访问account对象结束。也就是说谁拿到那个锁谁就可以运行它所控制的那段代码。
当有一个明确的对象作为锁时,就可以用类似下面这样的方式写程序。

1
2
3
4
5
6
7
8
public void method3(SomeObject obj)
{
//obj 锁定的对象
synchronized(obj)
{
// todo
}
}

当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Test implements Runnable
{
private byte[] lock = new byte[0]; // 特殊的instance变量
public void method()
{
synchronized(lock) {
// todo 同步代码块
}
}

public void run() {

}
}

说明:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

修饰一个方法

Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,public synchronized void method(){//todo}; synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。如将【Demo1】中的run方法改成如下的方式,实现的效果一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public synchronized void run() {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

//Synchronized作用于整个方法的写法。

//写法一
public synchronized void method()
{
// todo
}
//写法二
public void method()
{
synchronized(this) {
// todo
}
}

写法一修饰的是一个方法,写法二修饰的是一个代码块,但写法一与写法二是等价的,都是锁定了整个方法时的内容。

在用synchronized修饰方法时要注意以下几点:

  1. synchronized关键字不能继承。
    虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下:
    在子类方法中加上synchronized关键字
    1
    2
    3
    4
    5
    6
    class Parent {
    public synchronized void method() { }
    }
    class Child extends Parent {
    public synchronized void method() { }
    }

在子类方法中调用父类的同步方法

1
2
3
4
5
6
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public void method() { super.method(); }
}

  • 在定义接口方法时不能使用synchronized关键字。
  • 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。

修饰一个静态的方法

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
/**
* 同步线程
*/
class SyncThread implements Runnable {
private static int count;

public SyncThread() {
count = 0;
}

public synchronized static void method() {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public synchronized void run() {
method();
}
}


SyncThread syncThread1 = new SyncThread();
SyncThread syncThread2 = new SyncThread();
Thread thread1 = new Thread(syncThread1, "SyncThread1");
Thread thread2 = new Thread(syncThread2, "SyncThread2");
thread1.start();
thread2.start();


SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9

syncThread1和syncThread2是SyncThread的两个对象,但在thread1和thread2并发执行时却保持了线程同步。这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁。这与Demo1是不同的。

修饰一个类

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
/**
* 同步线程
*/
class SyncThread implements Runnable {
private static int count;

public SyncThread() {
count = 0;
}

public static void method() {
synchronized(SyncThread.class) {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public synchronized void run() {
method();
}
}

其效果和【Demo5】是一样的,synchronized作用于一个类T时,是给这个类T加锁,T的所有对象用的是同一把锁。

总结

  • 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
  • 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
    实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

锁升级的过程。

  • 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
  • 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象
  • 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象

Java中提供了两种实现同步的基础语义:synchronized方法和synchronized块

1
2
3
4
5
6
7
8
9
10
public class SyncTest {
public void syncBlock(){
synchronized (this){
System.out.println("hello block");
}
}
public synchronized void syncMethod(){
System.out.println("hello method");
}
}

当SyncTest.java被编译成class文件的时候,synchronized关键字和synchronized方法的字节码略有不同,我们可以用javap -v 命令查看class文件对应的JVM字节码信息,部分信息如下:

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
{
public void syncBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter // monitorenter指令进入同步块
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String hello block
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit // monitorexit指令退出同步块
14: goto 22
17: astore_2
18: aload_1
19: monitorexit // monitorexit指令退出同步块
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any


public synchronized void syncMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED //添加了ACC_SYNCHRONIZED标记
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String hello method
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

}

从上面的中文注释处可以看到,对于synchronized关键字而言,javac在编译时,会生成对应的monitorenter和monitorexit指令分别对应synchronized同步块的进入和退出,有两个monitorexit指令的原因是:为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。而对于synchronized方法而言,javac为其生成了一个ACC_SYNCHRONIZED关键字,在JVM进行方法调用时,发现调用的方法被ACC_SYNCHRONIZED修饰,则会先尝试获得锁。

在JVM底层,对于这两种synchronized语义的实现大致相同。

锁的几种形式

传统的锁(也就是下文要说的重量级锁)依赖于系统的同步函数,在linux上使用mutex互斥锁,最底层实现依赖于futex,关于futex可以看我之前的文章,这些同步函数都涉及到用户态和内核态的切换、进程的上下文切换,成本较高。对于加了synchronized关键字但运行时并没有多线程竞争,或两个线程接近于交替执行的情况,使用传统锁机制无疑效率是会比较低的。

在JDK 1.6之前,synchronized只有传统的锁机制,因此给开发者留下了synchronized关键字相比于其他同步机制性能不好的印象。

在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。

在看这几种锁机制的实现前,我们先来了解下对象头,它是实现多种锁机制的基础。

对象头

因为在Java中任意对象都可以用作锁,因此必定要有一个映射关系,存储该对象以及其对应的锁信息(比如当前哪个线程持有锁,哪些线程在等待)。一种很直观的方法是,用一个全局map,来存储这个映射关系,但这样会有一些问题:需要对map做线程安全保障,不同的synchronized之间会相互影响,性能差;另外当同步对象较多时,该map可能会占用比较多的内存。

所以最好的办法是将这个映射关系存储在对象头中,因为对象头本身也有一些hashcode、GC相关的数据,所以如果能将锁信息与这些信息共存在对象头中就好了。

在JVM中,对象在内存中除了本身的数据外还会有个对象头,对于普通对象而言,其对象头中有两类信息:mark word和类型指针。另外对于数组而言还会有一份记录数组长度的数据。

类型指针是指向该对象所属类对象的指针,mark word用于存储对象的HashCode、GC分代年龄、锁状态等信息。在32位系统上mark word长度为32字节,64位系统上长度为64字节。为了能在有限的空间里存储下更多的数据,其存储格式是不固定的,在32位系统上各状态的格式如下:
image

可以看到锁信息也是存在于对象的mark word中的。当对象状态为偏向锁(biasable)时,mark word存储的是偏向的线程ID;当状态为轻量级锁(lightweight locked)时,mark word存储的是指向线程栈中Lock Record的指针;当状态为重量级锁(inflated)时,为指向堆中的monitor对象的指针。

重量级锁

重量级锁是我们常说的传统意义上的锁,其利用操作系统底层的同步机制去实现Java中的线程同步。

重量级锁的状态下,对象的mark word为指向一个堆中monitor对象的指针。

一个monitor对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。

其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。
image

当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到cxq的队列尾部,然后暂停当前线程。当持有锁的线程释放锁前,会将cxq中的所有元素移动到EntryList中去,并唤醒EntryList的队首线程。

如果一个线程在同步块中调用了Object#wait方法,会将该线程对应的ObjectWaiter从EntryList移除并加入到WaitSet中,然后释放锁。当wait的线程被notify之后,会将对应的ObjectWaiter从WaitSet移动到EntryList中。

以上只是对重量级锁流程的一个简述,其中涉及到的很多细节,比如ObjectMonitor对象从哪来?释放锁时是将cxq中的元素移动到EntryList的尾部还是头部?notfiy时,是将ObjectWaiter移动到EntryList的尾部还是头部?

关于具体的细节,会在重量级锁的文章中分析。

轻量级锁

JVM的开发者发现在很多情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。

线程在执行同步块之前,JVM会先在当前的线程的栈帧中创建一个Lock Record,其包括一个用于存储对象头中的 mark word(官方称之为Displaced Mark Word)以及一个指向对象的指针。下图右边的部分就是一个Lock Record。
image

加锁过程

  1. 在线程栈中创建一个Lock Record,将其obj(即上图的Object reference)字段指向锁对象。

  2. 直接通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。如果失败,进入到步骤3。

  3. 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分(Displaced Mark Word)为null,起到了一个重入计数器的作用。然后结束。

  4. 走到这一步说明发生了竞争,需要膨胀为重量级锁。

解锁过程

  1. 遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。

  2. 如果Lock Record的Displaced Mark Word为null,代表这是一次重入,将obj设置为null后continue。

  3. 如果Lock Record的Displaced Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为Displaced Mark Word。如果成功,则continue,否则膨胀为重量级锁。

偏向锁

Java是支持多线程的语言,因此在很多二方包、基础库中为了保证代码在多线程的情况下也能正常运行,也就是我们常说的线程安全,都会加入如synchronized这样的同步语义。但是在应用在实际运行时,很可能只有一个线程会调用相关同步方法。比如下面这个demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.ArrayList;
import java.util.List;

public class SyncDemo1 {

public static void main(String[] args) {
SyncDemo1 syncDemo1 = new SyncDemo1();
for (int i = 0; i < 100; i++) {
syncDemo1.addString("test:" + i);
}
}

private List<String> list = new ArrayList<>();

public synchronized void addString(String s) {
list.add(s);
}

}

在这个demo中为了保证对list操纵时线程安全,对addString方法加了synchronized的修饰,但实际使用时却只有一个线程调用到该方法,对于轻量级锁而言,每次调用addString时,加锁解锁都有一个CAS操作;对于重量级锁而言,加锁也会有一个或多个CAS操作(这里的’一个‘、’多个‘数量词只是针对该demo,并不适用于所有场景)。

在JDK1.6中为了提高一个对象在一段很长的时间内都只被一个线程用做锁对象场景下的性能,引入了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只会执行几个简单的命令,而不是开销相对较大的CAS命令。我们来看看偏向锁是如何做的。

对象创建

当JVM启用了偏向锁模式(1.6以上默认开启),当新创建一个对象的时候,如果该对象所属的class没有关闭偏向锁模式(什么时候会关闭一个class的偏向模式下文会说,默认所有class的偏向模式都是是开启的),那新创建对象的mark word将是可偏向状态,此时mark word中的thread id(参见上文偏向状态下的mark word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。

加锁过程

case 1:当该对象第一次被线程获得锁的时候,发现是匿名偏向状态,则会用CAS指令,将mark word中的thread id由0改成当前线程Id。如果成功,则代表获得了偏向锁,继续执行同步块中的代码。否则,将偏向锁撤销,升级为轻量级锁。

case 2:当被偏向的线程再次进入同步块时,发现锁对象偏向的就是当前线程,在通过一些额外的检查后(细节见后面的文章),会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,然后继续执行同步块的代码,因为操纵的是线程私有的栈,因此不需要用到CAS指令;由此可见偏向锁模式下,当被偏向的线程再次尝试获得锁时,仅仅进行几个简单的操作就可以了,在这种情况下,synchronized关键字带来的性能开销基本可以忽略。

case 3.当其他线程进入同步块时,发现已经有偏向的线程了,则会进入到撤销偏向锁的逻辑里,一般来说,会在safepoint中去查看偏向的线程是否还存活,如果存活且还在同步块中则将锁升级为轻量级锁,原偏向的线程继续拥有锁,当前线程则走入到锁升级的逻辑里;如果偏向的线程已经不存活或者不在同步块中,则将对象头的mark word改为无锁状态(unlocked),之后再升级为轻量级锁。

由此可见,偏向锁升级的时机为:当锁已经发生偏向后,只要有另一个线程尝试获得偏向锁,则该偏向锁就会升级成轻量级锁。当然这个说法不绝对,因为还有批量重偏向这一机制。

解锁过程

当有其他线程尝试获得锁时,是根据遍历偏向线程的lock record来确定该线程是否还在执行同步块中的代码。因此偏向锁的解锁很简单,仅仅将栈中的最近一条lock record的obj字段设置为null。需要注意的是,偏向锁的解锁步骤中并不会修改对象头中的thread id。

下图展示了锁状态的转换流程:

image

另外,偏向锁默认不是立即就启动的,在程序启动后,通常有几秒的延迟,可以通过命令 -XX:BiasedLockingStartupDelay=0来关闭延迟。

批量重偏向与撤销

从上文偏向锁的加锁解锁过程中可以看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁。safe point这个词我们在GC中经常会提到,其代表了一个状态,在该状态下所有线程都是暂停的(大概这么个意思),详细可以看这篇文章。总之,偏向锁的撤销是有一定成本的,如果说运行时的场景本身存在多线程竞争的,那偏向锁的存在不仅不能提高性能,而且会导致性能下降。因此,JVM中增加了一种批量重偏向/撤销的机制。

存在如下两种情况:

  1. 一个线程创建了大量对象并执行了初始的同步操作,之后在另一个线程中将这些对象作为锁进行之后的操作。这种case下,会导致大量的偏向锁撤销操作。

  2. 存在明显多线程竞争的场景下使用偏向锁是不合适的,例如生产者/消费者队列。

批量重偏向(bulk rebias)机制是为了解决第一种场景。批量撤销(bulk revoke)则是为了解决第二种场景。

其做法是:以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id 改成当前线程Id。

当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

Java运行时区域及各个区域的作用、对GC的了解、Java内存模型及为什么要这么设计?

Java运行时区域及各个区域的作用

Java虚拟机所管理的内存将会包括一下几个运行时数据区域

image

程序计数器

程序计数器(Program Counter Register) 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条执行字节码指令。

每条线程都有一个独立的程序计数器。

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

Java虚拟机栈

同样是线程私有,描述Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个方法对应一个栈帧。

局部变量表存放了各种基本类型、对象引用和returnAddress类型(指向了一条字节码指令地址)。其中64位长度long 和 double占两个局部变量空间,其他只占一个。

规定的异常情况有两种:1.线程请求的栈的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;2.如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就抛出OutOfMemoryError异常。

本地方法栈

和Java虚拟机栈很类似,不同的是本地方法栈为Native方法服务。

Java堆

是Java虚拟机所管理的内存中最大的一块。由所有线程共享,在虚拟机启动时创建。堆区唯一目的就是存放对象实例。

堆中可细分为新生代和老年代,再细分可分为Eden空间、From Survivor空间、To Survivor空间。

堆无法扩展时,抛出OutOfMemoryError异常

方法区

所有线程共享,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

当方法区无法满足内存分配需求时,抛出OutOfMemoryError

运行时常量池

它是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池(Const Pool Table),用于存放编译期生成的各种字面量和符号引用。并非预置入Class文件中常量池的内容才进入方法运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。

当方法区无法满足内存分配需求时,抛出OutOfMemoryError

直接内存

并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。

JDK1.4加入了NIO,引入一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。因为避免了在Java堆和Native堆中来回复制数据,提高了性能。

当各个内存区域总和大于物理内存限制,抛出OutOfMemoryError异常。

GC相关

jvm 中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。

image

GC的对象

需要进行回收的对象就是已经没有存活的对象,判断一个对象是否存活常用的有两种办法:引用计数和可达分析。

  • 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
  • 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    在Java语言中,GC Roots包括:

    虚拟机栈中引用的对象。

    方法区中类静态属性实体引用的对象。

    方法区中常量引用的对象。

    本地方法栈中JNI引用的对象。

什么时候触发GC

  • 程序调用System.gc时可以触发
  • 系统自身来决定GC触发的时机(根据Eden区和From Space区的内存大小来决定。当内存大小不足时,则会启动GC线程并停止应用线程)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GC又分为 minor GC 和 Full GC (也称为 Major GC )

Minor GC触发条件:当Eden区满时,触发Minor GC。

Full GC触发条件:

  a.调用System.gc时,系统建议执行Full GC,但是不必然执行

  b.老年代空间不足

  c.方法区空间不足

  d.通过Minor GC后进入老年代的平均大小大于老年代的可用内存

  e.由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

GC常用算法

GC常用算法有:标记-清除算法,标记-压缩算法,复制算法,分代收集算法。
目前主流的JVM(HotSpot)采用的是分代收集算法。

标记-清除算法

为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。

  • 优点

    最大的优点是,标记—清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。此外,更重要的是,这个算法并不移动对象的位置。

  • 缺点
    它的缺点就是效率比较低(递归与全堆对象遍历)。每个活着的对象都要在标记阶段遍历一遍;所有对象都要在清除阶段扫描一遍,因此算法复杂度较高。没有移动对象,导致可能出现很多碎片空间无法利用的情况。

image

标记-压缩算法(标记-整理)

标记-压缩法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。

  • 优点
    该算法不会像标记-清除算法那样产生大量的碎片空间。
  • 缺点
    如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。
    image

左边是标记阶段,右边是整理之后的状态。可以看到,该算法不会产生大量碎片内存空间。

复制算法

该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。

注意:

这个算法与标记-整理算法的区别在于,该算法不是在同一个区域复制,而是将所有存活的对象复制到另一个区域内。

  • 优点
    实现简单;不产生内存碎片
  • 缺点
    每次运行,总有一半内存是空的,导致可使用的内存空间只有原来的一半。
    image
分代收集算法

现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。

具体过程:新生代(Young)分为Eden区,From区与To区

image

当系统创建一个对象的时候,总是在Eden区操作,当这个区满了,那么就会触发一次YoungGC,也就是年轻代的垃圾回收。一般来说这时候不是所有的对象都没用了,所以就会把还能用的对象复制到From区。

image

这样整个Eden区就被清理干净了,可以继续创建新的对象,当Eden区再次被用完,就再触发一次YoungGC,然后呢,注意,这个时候跟刚才稍稍有点区别。这次触发YoungGC后,会将Eden区与From区还在被使用的对象复制到To区,

image

再下一次YoungGC的时候,则是将Eden区与To区中的还在被使用的对象复制到From区。

image

经过若干次YoungGC后,有些对象在From与To之间来回游荡,这时候From区与To区亮出了底线(阈值),这些家伙要是到现在还没挂掉,对不起,一起滚到(复制)老年代吧。

image

老年代经过这么几次折腾,也就扛不住了(空间被用完),好,那就来次集体大扫除(Full GC),也就是全量回收。如果Full GC使用太频繁的话,无疑会对系统性能产生很大的影响。所以要合理设置年轻代与老年代的大小,尽量减少Full GC的操作。

垃圾收集器

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现

Serial收集器

串行收集器是最古老,最稳定以及效率高的收集器
可能会产生较长的停顿,只使用一个线程去回收
-XX:+UseSerialGC

  • 新生代、老年代使用串行回收
  • 新生代复制算法
  • 老年代标记-压缩

image

并行收集器
  • ParNew
    -XX:+UseParNewGC(new代表新生代,所以适用于新生代)

    • 新生代并行
    • 老年代串行
      Serial收集器新生代的并行版本
      在新生代回收时使用复制算法
      多线程,需要多核支持

-XX:ParallelGCThreads 限制线程数量
image

  • Parallel收集器
    类似ParNew
    新生代复制算法
    老年代标记-压缩
    更加关注吞吐量
    -XX:+UseParallelGC
    • 使用Parallel收集器+ 老年代串行
      -XX:+UseParallelOldGC
    • 使用Parallel收集器+ 老年代并行

image

其他GC参数

-XX:MaxGCPauseMills

- 最大停顿时间,单位毫秒
- GC尽力保证回收时间不超过设定值

-XX:GCTimeRatio

- 0-100的取值范围
- 垃圾收集时间占总时间的比
- 默认99,即最大允许1%时间做GC

这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优

CMS收集器
  • Concurrent Mark Sweep 并发标记清除(应用程序线程和GC线程交替执行)
  • 使用标记-清除算法
  • 并发阶段会降低吞吐量(停顿时间减少,吞吐量降低)
  • 老年代收集器(新生代使用ParNew)
  • -XX:+UseConcMarkSweepGC

CMS运行过程比较复杂,着重实现了标记的过程,可分为

  1. 初始标记(会产生全局停顿)
    • 根可以直接关联到的对象
    • 速度快
  2. 并发标记(和用户线程一起)
    • 主要标记过程,标记全部对象
  3. 重新标记 (会产生全局停顿)
    • 由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
  4. 并发清除(和用户线程一起)
    • 基于标记结果,直接清理对象

image

这里就能很明显的看出,为什么CMS要使用标记清除而不是标记压缩,如果使用标记压缩,需要多对象的内存位置进行改变,这样程序就很难继续执行。但是标记清除会产生大量内存碎片,不利于内存分配。

CMS收集器特点:

  • 尽可能降低停顿
  • 会影响系统整体吞吐量和性能
    • 比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半
  • 清理不彻底
    • 因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理
  • 因为和用户线程一起运行,不能在空间快满时再清理(因为也许在并发GC的期间,用户线程又申请了大量内存,导致内存不够)
    • -XX:CMSInitiatingOccupancyFraction设置触发GC的阈值
    • 如果不幸内存预留空间不够,就会引起concurrent mode failure
      一旦 concurrent mode failure产生,将使用串行收集器作为后备。

CMS也提供了整理碎片的参数:

- -XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次整理

整理过程是独占的,会引起停顿时间变长

-XX:+CMSFullGCsBeforeCompaction

- 设置进行几次Full GC后,进行一次碎片整理

-XX:ParallelCMSThreads

- 设定CMS的线程数量(一般情况约等于可用CPU数量)

CMS的提出是想改善GC的停顿时间,在GC过程中的确做到了减少GC时间,但是同样导致产生大量内存碎片,又需要消耗大量时间去整理碎片,从本质上并没有改善时间。

G1收集器

G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。

与CMS收集器相比G1收集器有以下特点:

(1) 空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。

(2)可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。

G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。

和CMS类似,G1收集器收集老年代对象会有短暂停顿。

步骤:

(1)标记阶段,首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event),并且会触发一次普通Mintor GC。对应GC log:GC pause (young) (inital-mark)

(2)Root Region Scanning,程序运行过程中会回收survivor区(存活到老年代),这一过程必须在young GC之前完成。

(3)Concurrent Marking,在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收(图中打X)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。

image

(4)Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。

(5)Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。

image

(6)复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。

finalize()方法详解

finalize的作用

(1)finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。

(2)finalize()与C++中的析构函数不是对应的。C++中的析构函数调用的时机是确定的(对象离开作用域或delete掉),但Java中的finalize的调用具有不确定性

(3)不建议用finalize方法完成“非内存资源”的清理工作,但建议用于:

① 清理本地对象(通过JNI创建的对象);

② 作为确保某些非内存资源(如Socket、文件等)释放的一个补充:在finalize方法中显式调用其他资源释放方法

finalize的问题

(1)一些与finalize相关的方法,由于一些致命的缺陷,已经被废弃了,如System.runFinalizersOnExit()方法、Runtime.runFinalizersOnExit()方法

(2)System.gc()与System.runFinalization()方法增加了finalize方法执行的机会,但不可盲目依赖它们

(3)Java语言规范并不保证finalize方法会被及时地执行、而且根本不会保证它们会被执行

(4)finalize方法可能会带来性能问题。因为JVM通常在单独的低优先级线程中完成finalize的执行

(5)对象再生问题:finalize方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的

(6)finalize方法至多由GC执行一次(用户当然可以手动调用对象的finalize方法,但并不影响GC对finalize的行为)

finalize的执行过程(生命周期)

(1) 首先,大致描述一下finalize流程:当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。

(2) 具体的finalize流程:
对象可由两种状态,涉及到两类状态空间,一是终结状态空间 F = {unfinalized, finalizable, finalized};二是可达状态空间 R = {reachable, finalizer-reachable, unreachable}。各状态含义如下:

  • unfinalized: 新建对象会先进入此状态,GC并未准备执行其finalize方法,因为该对象是可达的
  • finalizable: 表示GC可对该对象执行finalize方法,GC已检测到该对象不可达。正如前面所述,GC通过F-Queue队列和一专用线程完成finalize的执行
  • finalized: 表示GC已经对该对象执行过finalize方法
    reachable: 表示GC Roots引用可达
  • finalizer-reachable(f-reachable):表示不是reachable,但可通过某个finalizable对象可达
  • unreachable:对象不可通过上面两种途径可达

状态变迁图:

image

变迁说明:

(1)新建对象首先处于[reachable, unfinalized]状态(A)

(2)随着程序的运行,一些引用关系会消失,导致状态变迁,从reachable状态变迁到f-reachable(B, C, D)或unreachable(E, F)状态

(3)若JVM检测到处于unfinalized状态的对象变成f-reachable或unreachable,JVM会将其标记为finalizable状态(G,H)。若对象原处于[unreachable, unfinalized]状态,则同时将其标记为f-reachable(H)。

(4)在某个时刻,JVM取出某个finalizable对象,将其标记为finalized并在某个线程中执行其finalize方法。由于是在活动线程中引用了该对象,该对象将变迁到(reachable, finalized)状态(K或J)。该动作将影响某些其他对象从f-reachable状态重新回到reachable状态(L, M, N)

(5)处于finalizable状态的对象不能同时是unreahable的,由第4点可知,将对象finalizable对象标记为finalized时会由某个线程执行该对象的finalize方法,致使其变成reachable。这也是图中只有八个状态点的原因

(6)程序员手动调用finalize方法并不会影响到上述内部标记的变化,因此JVM只会至多调用finalize一次,即使该对象“复活”也是如此。程序员手动调用多少次不影响JVM的行为

(7)若JVM检测到finalized状态的对象变成unreachable,回收其内存(I)

(8)若对象并未覆盖finalize方法,JVM会进行优化,直接回收对象(O)

(9)注:System.runFinalizersOnExit()等方法可以使对象即使处于reachable状态,JVM仍对其执行finalize方法

总结

根据GC的工作原理,我们可以通过一些技巧和方式,让GC运行更加有效率,更加符合应用程序的要求。一些关于程序设计的几点建议:

1.最基本的建议就是尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后,自动设置为 null.我们在使用这种方式时候,必须特别注意一些复杂的对象图,例如数组,队列,树,图等,这些对象之间有相互引用关系较为复杂。对于这类对象,GC 回收它们一般效率较低。如果程序允许,尽早将不用的引用对象赋为null.这样可以加速GC的工作。

2.尽量少用finalize函数。finalize函数是Java提供给程序员一个释放对象或资源的机会。但是,它会加大GC的工作量,因此尽量少采用finalize方式回收资源。

3.如果需要使用经常使用的图片,可以使用soft应用类型。它可以尽可能将图片保存在内存中,供程序调用,而不引起OutOfMemory.

4.注意集合数据类型,包括数组,树,图,链表等数据结构,这些数据结构对GC来说,回收更为复杂。另外,注意一些全局的变量,以及一些静态变量。这些变量往往容易引起悬挂对象(dangling reference),造成内存浪费。

5.当程序有一定的等待时间,程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。使用增量式GC可以缩短Java程序的暂停时间。

countDownLatch用过没有,在项目中如何使用的,对AQS的了解。

介绍

  • countDownLatch是在java1.5被引入,跟它一起被引入的工具类还有CyclicBarrier、Semaphore、concurrentHashMap和BlockingQueue。
  • 存在于java.util.cucurrent包下。
  • countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
  • 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

类中有三个方法是最重要的

1
2
3
4
5
6
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
//将count值减1
public void countDown() { };

示例

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
public class CountDownLatchTest {

public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(2);
System.out.println("主线程开始执行…… ……");
//第一个子线程执行
ExecutorService es1 = Executors.newSingleThreadExecutor();
es1.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
System.out.println("子线程:"+Thread.currentThread().getName()+"执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
}
});
es1.shutdown();

//第二个子线程执行
ExecutorService es2 = Executors.newSingleThreadExecutor();
es2.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程:"+Thread.currentThread().getName()+"执行");
latch.countDown();
}
});
es2.shutdown();
System.out.println("等待两个线程执行完毕…… ……");
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("两个子线程都执行完毕,继续执行主线程");
}
}

//运行结果
主线程开始执行…… ……
等待两个线程执行完毕…… ……
子线程:pool-1-thread-1执行
子线程:pool-2-thread-1执行
两个子线程都执行完毕,继续执行主线程

//模拟并发示例
public class Parallellimit {
public static void main(String[] args) {
ExecutorService pool = Executors.newCachedThreadPool();
CountDownLatch cdl = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
CountRunnable runnable = new CountRunnable(cdl);
pool.execute(runnable);
}
}
}

class CountRunnable implements Runnable {
private CountDownLatch countDownLatch;
public CountRunnable(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
synchronized (countDownLatch) {
/*** 每次减少一个容量*/
countDownLatch.countDown();
System.out.println("thread counts = " + (countDownLatch.getCount()));
}
countDownLatch.await();
System.out.println("concurrency counts = " + (100 - countDownLatch.getCount()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

CountDownLatch和CyclicBarrier区别:

  • countDownLatch是一个计数器,线程完成一个记录一个,计数器递减,只能只用一次
  • CyclicBarrier的计数器更像一个阀门,需要所有线程都到达,然后继续执行,计数器递增,提供reset功能,可以多次使用

AQS

  • AQS的全称:AbstractQueuedSynchronizer,抽象队列同步器
  • java并发包下很多API都是基于AQS来实现的加锁和释放锁等功能的,AQS是java并发包的基础类。ReentrantLock、ReentrantReadWriteLock底层都是基于AQS来实现的。
  • 看一下ReentrantLock和AQS之间的关系
    ReentrantLock内部包含了一个AQS对象,也就是AbstractQueuedSynchronizer类型的对象。这个AQS对象就是ReentrantLock可以实现加锁和释放锁的关键性的核心组件。
    image

ReentrantLock加锁和释放锁的底层原理

  • 如果有一个线程过来尝试用ReentrantLock的lock()方法进行加锁,这个AQS对象内部有一个核心的变量叫做state,是int类型的,代表了加锁的状态。初始状态下,这个state的值是0。
  • 另外,这个AQS内部还有一个关键变量,用来记录当前加锁的是哪个线程,初始化状态下,这个变量是null。
  • 接着线程1跑过来调用ReentrantLock的lock()方法尝试进行加锁,这个加锁的过程,直接就是用CAS操作将state值从0变为1。如果之前没人加过锁,那么state的值肯定是0,此时线程1就可以加锁成功。一旦线程1加锁成功了之后,就可以设置当前加锁线程是自己。所以大家看下面的图,就是线程1跑过来加锁的一个过程。
    image
    看到这儿,大家应该对所谓的AQS有感觉了。说白了,就是并发包里的一个核心组件,里面有state变量、加锁线程变量等核心的东西,维护了加锁状态。ReentrantLock这种东西只是一个外层的API,内核中的锁机制实现都是依赖AQS组件的。

  • 这个ReentrantLock之所以用Reentrant打头,意思就是他是一个可重入锁。可重入锁的意思,就是你可以对一个ReentrantLock对象多次执行lock()加锁和unlock()释放锁,也就是可以对一个锁加多次,叫做可重入加锁。

  • 大家看明白了那个state变量之后,就知道了如何进行可重入加锁!
    其实每次线程1可重入加锁一次,会判断一下当前加锁线程就是自己,那么他自己就可以可重入多次加锁,每次加锁就是把state的值给累加1,别的没啥变化。
  • 接着,如果线程1加锁了之后,线程2跑过来加锁会怎么样呢?
    我们来看看锁的互斥是如何实现的?线程2跑过来一下看到,哎呀!state的值不是0啊?所以CAS操作将state从0变为1的过程会失败,因为state的值当前为1,说明已经有人加锁了!
  • 接着线程2会看一下,是不是自己之前加的锁啊?当然不是了,“加锁线程”这个变量明确记录了是线程1占用了这个锁,所以线程2此时就是加锁失败。
    image
  • 接着,线程2会将自己放入AQS中的一个等待队列,因为自己尝试加锁失败了,此时就要将自己放入队列中来等待,等待线程1释放锁之后,自己就可以重新尝试加锁了

image

  • 接着,线程1在执行完自己的业务逻辑代码之后,就会释放锁!他释放锁的过程非常的简单,就是将AQS内的state变量的值递减1,如果state值为0,则彻底释放锁,会将“加锁线程”变量也设置为null!
    整个过程,参见下图:
    image

  • 接下来,线程1会从等待队列的队头唤醒线程2重新尝试加锁。
    线程2现在就重新尝试加锁,这时还是用CAS操作将state从0变为1,- - 此时就会成功,成功之后代表加锁成功,就会将state设置为1;此外,还要把“加锁线程”设置为线程2自己,同时线程2自己就从等待队列中出队了。

image

总结

其实一句话总结AQS就是一个并发包的基础组件,用来实现各种锁,各种同步组件的。它包含了state变量、加锁线程、等待队列等并发中的核心组件。

写生产者消费者问题,考虑高并发的情况,可以使用Java 类库,白纸写代码。

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
class Info{ // 定义信息类  
private String name = "name";//定义name属性,为了与下面set的name属性区别开
private String content = "content" ;// 定义content属性,为了与下面set的content属性区别开
private boolean flag = true ; // 设置标志位,初始时先生产
public synchronized void set(String name,String content){
while(!flag){
try{
super.wait() ;
}catch(InterruptedException e){
e.printStackTrace() ;
}
}
this.setName(name) ; // 设置名称
try{
Thread.sleep(300) ;
}catch(InterruptedException e){
e.printStackTrace() ;
}
this.setContent(content) ; // 设置内容
flag = false ; // 改变标志位,表示可以取走
super.notify();
}
public synchronized void get(){
while(flag){
try{
super.wait() ;
}catch(InterruptedException e){
e.printStackTrace() ;
}
}
try{
Thread.sleep(300) ;
}catch(InterruptedException e){
e.printStackTrace() ;
}
System.out.println(this.getName() +
" --> " + this.getContent()) ;
flag = true ; // 改变标志位,表示可以生产
super.notify();
}
public void setName(String name){
this.name = name ;
}
public void setContent(String content){
this.content = content ;
}
public String getName(){
return this.name ;
}
public String getContent(){
return this.content ;
}
}
class Producer implements Runnable{ // 通过Runnable实现多线程
private Info info = null ; // 保存Info引用
public Producer(Info info){
this.info = info ;
}
public void run(){
boolean flag = true ; // 定义标记位
for(int i=0;i<10;i++){
if(flag){
this.info.set("姓名--1","内容--1") ; // 设置名称
flag = false ;
}else{
this.info.set("姓名--2","内容--2") ; // 设置名称
flag = true ;
}
}
}
}
class Consumer implements Runnable{
private Info info = null ;
public Consumer(Info info){
this.info = info ;
}
public void run(){
for(int i=0;i<10;i++){
this.info.get() ;
}
}
}
public class ThreadCaseDemo03{
public static void main(String args[]){
Info info = new Info(); // 实例化Info对象
Producer pro = new Producer(info) ; // 生产者
Consumer con = new Consumer(info) ; // 消费者
new Thread(pro).start() ;
//启动了生产者线程后,再启动消费者线程
try{
Thread.sleep(500) ;
}catch(InterruptedException e){
e.printStackTrace() ;
}

new Thread(con).start() ;
}
}

有没有排查过线上OOM的问题,如何排查的?

查看当前路径,oom.out文件已经生成了,该文件就是应用在发生OOM异常时自动导出的堆文件。那我们此时需要对该文件进行分析,因为其中记录了是什么对象导出了应用程OOM的发生。

分析OOM的工具推荐使用MAT,下载地址,在配置好Java环境的电脑中,直接打开即可,不需要安装,然后通过MAT打开已经生成的OOM文件oom.out,出现如下提示,选择“Leak Suspects Report”执行内存泄漏检查分析:
image

点击Finish按钮后,MAT会将可疑的内存泄漏的对象都展现出来:

image

可以看到线程java.lang.Thread @ 0xff617e80 的main方法中,有一个本地变量占用了96.43%的堆内存,实际内存占用的是char[]数组,因而被检测出来为OOM可疑的元凶。点击红色框中的“See stacktrace”,可以直接看到该对象所在线程的堆栈信息:

image

直接定位到了发生OOM的代码所在位置,至此该示例分析完成,MAT工具本身还有其它许多的功能,这里就不一一细说了。

下一篇会写服务器由于时间戳不一致,导致有些服务器可以访问,有些服务器却不能够访问的问题,如果感兴趣,请继续观注。

有没有使用过JVM自带的工具,如何使用的?

%JAVA_HOME/bin%下就是安装java时为我们自带的可运行程序的文件夹。

jps命令

jps(java process status):用于查看java进程。

option description
- 查看java进程
-l 显示全类名
-m 带参显示
-v JVM参数

jstat

  • jstat -gcutil pid

image

其中的pid是你关注的java进程号,可根据jps查询。
-gcutil是关心的指标,更多详尽信息请参看官方文档。

  • options
    image

  • description(后面也有详尽的字段说明)
    image

  • jstat -option pid peroid times(周期监控)
    image

jinfo

jinfo进行指定参数的查询。

jmap

jmap用于内存管理。

  • jmap -histo pid(类数量 / 实例数量)
  • jmap -dump:format=b,file=file导出运行信息以便于后续线下分析。

jhat(JVM Heap Analysis Tool)

  • jhat a.bin分析导出数据

jstack

options description
- 打印方法栈
-F 强制打印
-m 本地方法栈
-l 打印锁信息
  • jstack pid
  • jstack -l pid(锁信息,能看见线程状态)

jconsole

image
image

假设有下图所示的一个Full GC 的图,纵向是内存使用情况,横向是时间,你如何排查这个Full GC 的问题,怎么去解决你说出来的这些问题?

image

Full GC的原因

我们知道Full GC的触发条件大致情况有以下几种情况:

  1. 程序执行了System.gc() //建议jvm执行fullgc,并不一定会执行
  2. 执行了jmap -histo:live pid命令 //这个会立即触发fullgc
  3. 在执行minor gc的时候进行的一系列检查

    1
    2
    3
    4
    5
    执行Minor GC的时候,JVM会检查老年代中最大连续可用空间是否大于了当前新生代所有对象的总大小。
    如果大于,则直接执行Minor GC(这个时候执行是没有风险的)。
    如果小于了,JVM会检查是否开启了空间分配担保机制,如果没有开启则直接改为执行Full GC。
    如果开启了,则JVM会检查老年代中最大连续可用空间是否大于了历次晋升到老年代中的平均大小,如果小于则执行改为执行Full GC。
    如果大于则会执行Minor GC,如果Minor GC执行失败则会执行Full GC
  4. 使用了大对象 //大对象会直接进入老年代

  5. 在程序中长期持有了对象的引用 //对象年龄达到指定阈值也会进入老年代

对于我们的情况,可以初步排除1,2两种情况,最有可能是4和5这两种情况。为了进一步排查原因,我们在线上开启了 -XX:+HeapDumpBeforeFullGC。

1
2
3
注意:
JVM在执行dump操作的时候是会发生stop the word事件的,也就是说此时所有的用户线程都会暂停运行。
为了在此期间也能对外正常提供服务,建议采用分布式部署,并采用合适的负载均衡算法

JVM参数的设置:

线上这个dubbo服务是分布式部署,在其中一台机子上开启了 -XX:HeapDumpBeforeFullGC,总体JVM参数如下:

1
2
3
4
5
6
7
8
9
10
11
-Xmx2g 
-XX:+HeapDumpBeforeFullGC
-XX:HeapDumpPath=.
-Xloggc:gc.log
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=100m
-XX:HeapDumpOnOutOfMemoryError

Dump文件分析

dump下来的文件大约1.8g,用jvisualvm查看,发现用char[]类型的数据占用了41%内存,同时另外一个com.alibaba.druid.stat.JdbcSqlStat类型的数据占用了35%的内存,也就是说整个堆中几乎全是这两类数据。如下图:

image

查看char[]类型数据,发现几乎全是sql语句。
image

接下来查看char[]的引用情况:
image

找到了JdbcSqlStat类,在代码中查看这个类的代码,关键代码如下:

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
//构造函数只有这一个
public JdbcSqlStat(String sql){
this.sql = sql;
this.id = DruidDriver.createSqlStatId();
}

//查看这个函数的调用情况,找到com.alibaba.druid.stat.JdbcDataSourceStat#createSqlStat方法:

public JdbcSqlStat createSqlStat(String sql) {
lock.writeLock().lock();
try {
JdbcSqlStat sqlStat = sqlStatMap.get(sql);
if (sqlStat == null) {
sqlStat = new JdbcSqlStat(sql);
sqlStat.setDbType(this.dbType);
sqlStat.setName(this.name);
sqlStatMap.put(sql, sqlStat);
}

return sqlStat;
} finally {
lock.writeLock().unlock();
}
}

//这里用了一个map来存放所有的sql语句。

其实到这里也就知道什么原因造成了这个问题,因为我们使用的数据源是阿里巴巴的druid,这个druid提供了一个sql语句监控功能,同时我们也开启了这个功能。只需要在配置文件中把这个功能关掉应该就能消除这个问题,事实也的确如此,关掉这个功能后到目前为止线上没再触发FullGC

其他

如果用mat工具查看,建议把 “Keep unreachable objects” 勾上,否则mat会把堆中不可达的对象去除掉,这样我们的分析也许会变得没有意义。如下图:Window–>References 。另外jvisualvm对ool的支持不是很好,如果需要oql建议使用mat。
image

说说对Java中集合类的理解,项目中用过哪些,哪个地方用的,如何使用的?

从数据结构开始了解

数据结构

线性表

image

  • 顺序存储结构(也叫顺序表):一个线性表是n个具有相同特性的数据元素的有限序列。数据元素是一个抽象的符号,其具体含义在不同的情况下一般不同。
  • 链表:链表里面节点的地址不是连续的,是通过指针连起来的。

哈希表

解释一:
image
哈希表hashtable(key,value) 就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。

解释二:

数组的特点是:寻址容易,插入和删除困难;

而链表的特点是:寻址困难,插入和删除容易。

那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,我们可以理解为“链表的数组”,如图:

image

左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。

Hash 表的查询速度非常的快,几乎是O(1)的时间复杂度。

hash就是找到一种数据内容和数据存放地址之间的映射关系。

散列法:元素特征转变为数组下标的方法。

我想大家都在想一个很严重的问题:“如果两个字符串在哈希表中对应的位置相同怎么办?”,毕竟一个数组容量是有限的,这种可能性很大。解决该问题的方法很多,我首先想到的就是用“链表”。我遇到的很多算法都可以转化成链表来解决,只要在哈希表的每个入口挂一个链表,保存所有对应的字符串就OK了。

散列表的查找步骤

当存储记录时,通过散列函数计算出记录的散列地址

当查找记录时,我们通过同样的是散列函数计算记录的散列地址,并按此散列地址访问该记录

优缺点

  • 优点:不论哈希表中有多少数据,查找、插入、删除(有时包括删除)只需要接近常量的时间即0(1)的时间级。实际上,这只需要几条机器指令。

    哈希表运算得非常快,在计算机程序中,如果需要在一秒种内查找上千条记录通常使用哈希表(例如拼写检查器)哈希表的速度明显比树快,树的操作通常需要O(N)的时间级。哈希表不仅速度快,编程实现也相对容易。

    如果不需要有序遍历数据,并且可以提前预测数据量的大小。那么哈希表在速度和易用性方面是无与伦比的。

  • 缺点:它是基于数组的,数组创建后难于扩展,某些哈希表被基本填满时,性能下降得非常严重,所以程序员必须要清楚表中将要存储多少数据(或者准备好定期地把数据转移到更大的哈希表中,这是个费时的过程)。

哈希表的原理:

   1,对对象元素中的关键字(对象中的特有数据),进行哈希算法的运算,并得出一个具体的算法值,这个值 称为哈希值。

  2,哈希值就是这个元素的位置。

  3,如果哈希值出现冲突,再次判断这个关键字对应的对象是否相同。如果对象相同,就不存储,因为元素重复。如果对象不同,就存储,在原来对象的哈希值基础 +1顺延。

  4,存储哈希值的结构,我们称为哈希表。

  5,既然哈希表是根据哈希值存储的,为了提高效率,最好保证对象的关键字是唯一的。

  这样可以尽量少的判断关键字对应的对象是否相同,提高了哈希表的操作效率。

扩展:

相同的字符串如果存进去,哈希值相同并且equals方法为true,不会存入相同的

只要哈希值相同或者equals方法为true都成立才不会存入,只要其中一条不满足,都会储存

哈希表存储过程:

1.调用对象的哈希值(通过一个函数f()得到哈希值):存储位置 = f(关键字)

2.集合在容器内搜索有没有重复的哈希值,如果没有,存入新元素,记录哈希值

3.再次存储,重复上边的过程

4.如果有重复的哈希值,调用后来者的equals方法,参数为前来者,结果得到true,集合判断为重复元素,不存入

哈希冲突

然而万事无完美,如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?

哈希冲突的解决方案有多种:

开放定址法(发生冲突,继续寻找下一块未被占用的存储地址)

再散列函数法

链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式

关于hashcode和equals的一些问题,在面试中会问道:

1.两个对象哈希值相同,那么equals方法一定返回true吗?

不一定:取决于如何重写equals,如果重写固定了它返回false,结果就一定是false

2.equals方法返回true,那么哈希值一定相同吗?

一定:如果类中定义一个静态变量(static int a = 1),然后重写hashcode返回a+1,那么每一个对象的哈希值都不一样,不过java中规定:对象相等,必须具有相同的哈希码值,所以这里是一定的

数组

采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)

区别

1.数组

  • 优点:(1)随机访问效率高(根据下标查询),(2)搜索效率较高(可使用折半方法)。

  • 缺点:(1)内存连续且固定,存储效率低。(2)插入和删除效率低(可能会进行数组拷贝或扩容)。

2.链表

  • 优点:(1)不要求连续内存,内存利用率高,(2)插入和删除效率高(只需要改变指针指向)。

  • 缺点:(1)不支持随机访问,(2)搜索效率低(需要遍历)。

3.Hash表

  • 优点:(1)搜索效率高,(2)插入和删除效率较高,

  • 缺点:(1)内存利用率低(基于数组),(2)存在散列冲突。

集合类种重要概念词解释

泛型

java中很重要的概念, 集合里面应用很多.

集合的元素,可以是任意类型对象的引用,如果把某个对象放入集合,则会忽略它的类型,就会把它当做Object类型处理.

泛型则是规定了某个集合只可以存放特定类型的对象的引用,会在编译期间进行类型检查,可以直接指定类型来获取集合元素

在泛型集合中有能够存入泛型类型的对象实例还可以存入泛型的子类型的对象实例

注意:

1 泛型集合中的限定类型,不能使用基本数据类型

2 可以通过使用包装类限定允许存放基本数据类型

泛型的好处

1 提高了安全性(将运行期的错误转换到编译期)

2 省去强转的麻烦

哈希值

1 就是一个十进制的整数,有操作系统随机给出

2 可以使用Object类中的方法hashCode获取哈希值

3 Object中源码: int hashCode()返回该对象的哈希码值;

  源码:

1
2
//native:指调用了本地操作系统的方法实现
public native int hashCode();

平衡二叉树(称AVL树)

其特点是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。也就是说该二叉树的任何一个子节点,其左右子树的高度都相近。

注意:

关键点是左子树和右子树的深度的绝对值不超过1

那什么是左子树深度和右子树深度呢?

image
如上图中:

如果插入6元素, 则8的左子树深度就为2, 右子树深度就为0,绝对值就为2, 就不是一个平很二叉树

二叉排序树

1若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

2若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

3左、右子树也分别为二叉排序树

解释一:

现在有a[10] = {3, 2, 1, 4, 5, 6, 7, 10, 9, 8}需要构建二叉排序树。在没有学习平衡二叉树之前,根据二叉排序树的特性,通常会将它构建成如下左图。虽然完全符合二叉排序树的定义,但是对这样高度达到8的二叉树来说,查找是非常不利的。因此,更加期望构建出如下右图的样子,高度为4的二叉排序树,这样才可以提供高效的查找效率。
image
平衡二叉树是一种二叉排序树,是一种高度平衡的二叉树,其中每个结点的左子树和右子树的高度至多等于1.意味着:要么是一棵空树,要么左右都是平衡二叉树,且左子树和右子树深度之绝对值不超过1. 将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF,那么平衡二叉树上的所有结点的平衡因子只可能是-1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。

平衡二叉树的前提是它是一棵二叉排序树。

旋转

假设一颗 AVL 树的某个节点为 r,有四种操作会使 r 的左右子树高度差大于 1,从而破坏了原有 AVL 树的平衡性。使用旋转达到平衡性

1.对 r 的左儿子的左子树进行一次插入(左旋转 LL)
image
2.对 r 的左儿子的右子树进行一次插入(LR)
image
3.对 r 的右儿子的左子树进行一次插入(RL)
image
4.对 r 的右儿子的右子树进行一次插入(RR)
image

红黑树

红黑树(Red Black Tree) 是一种自平衡二叉查找树

(1) 检索效率O(log n)

(2) 红黑树的五点规定:

1.每个结点要么是红的要么是黑的

2.根结点是黑的

3.每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的

4.如果一个结点是红的,那么它的两个儿子都是黑的(反之不一定)

5.对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点

它的每个结点都额外有一个颜色的属性,颜色只有两种:红色和黑色。

示例:(这块难度比较大, 建议自行百度,查阅相关文档)

红黑树插入操作

如果是第一次插入,由于原树为空,所以只会违反红黑树的规则2,所以只要把根节点涂黑即可;

如果插入节点的父节点是黑色的,那不会违背红-黑树的规则,什么也不需要做;

但是遇到如下三种情况时,我们就要开始变色和旋转了:

1. 插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色的;

2. 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点;

3. 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点。

下面我们先挨个分析这三种情况都需要如何操作:

对于情况1:插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色的。此时,肯定存在祖父节点,但是不知道父节点是其左子节点还是右子节点,但是由于对称性,我们只要讨论出一边的情况,另一种情况自然也与之对应。

这里考虑父节点是祖父节点的左子节点的情况(即插入一个4节点,插入的节点一般为红色,不然可能违反规则5.),如下左图所示:

image

对于这种情况,我们要做的操作有:将当前节点(4)的父节点(5)和叔叔节点(8)涂黑,将祖父节点(7)涂红,变成上右图所示的情况。再将当前节点指向其祖父节点,再次从新的当前节点开始算法。这样上右图就变成了情况2了。

对于情况2:插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点。我们要做的操作有:将当前节点(7)的父节点(2)作为新的节点,以新的当前节点为支点做左旋操作。完成后如左下图所示,这样左下图就变成情况3了。

image

image

对于情况3:插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点。我们要做的操作有:将当前节点的父节点(7)涂黑,将祖父节点(11)涂红,在祖父节点为支点做右旋操作。最后把根节点涂黑,整个红-黑树重新恢复了平衡,如右上图所示。至此,插入操作完成!

我们可以看出,如果是从情况1开始发生的,必然会走完情况2和3,也就是说这是一整个流程,当然咯,实际中可能不一定会从情况1发生,如果从情况2开始发生,那再走个情况3即可完成调整,如果直接只要调整情况3,那么前两种情况均不需要调整了。故变色和旋转之间的先后关系可以表示为:变色->左旋->右旋。

红黑树删除操作

我们现在约定:后继节点的子节点称为“当前节点”.

删除节点有三种情况分析:

a. 叶子节点;(直接删除即可)
image
b. 仅有左或右子树的节点;(上移子树即可)
image 
c. 左右子树都有的节点。( 用删除节点的直接前驱或者直接后继来替换当前节点,调整直接前驱或者直接后继的位置)
image
删除操作后,如果当前节点是黑色的根节点,那么不用任何操作,因为并没有破坏树的平衡性,即没有违背红-黑树的规则,这很好理解。如果当前节点是红色的,说明刚刚移走的后继节点是黑色的,那么不管后继节点的父节点是啥颜色,我们只要将当前节点涂黑就可以了,红-黑树的平衡性就可以恢复。但是如果遇到以下四种情况,我们就需要通过变色或旋转来恢复红-黑树的平衡了。

  1. 当前节点是黑色的,且兄弟节点是红色的(那么父节点和兄弟节点的子节点肯定是黑色的);

  2. 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的两个子节点均为黑色的;

  3. 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的左子节点是红色,右子节点时黑色的;

  4. 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的右子节点是红色,左子节点任意颜色。

以上四种情况中,我们可以看出2,3,4其实是“当前节点是黑色的,且兄弟节点是黑色的”的三种子集,等会在程序中可以体现出来。现在我们假设当前节点是左子节点(当然也可能是右子节点,跟左子节点相反即可,我们讨论一边就可以了),分别解决上面四种情况:

对于情况1:当前节点是黑色的,且兄弟节点是红色的(那么父节点和兄弟节点的子节点肯定是黑色的)。如左下图所示:A节点表示当前节点。针对这种情况,我们要做的操作有:将父节点(B)涂红,将兄弟节点(D)涂黑,然后将当前节点(A)的父节点(B)作为支点左旋,然后当前节点的兄弟节点就变成黑色的情况了(自然就转换成情况2,3,4的公有特征了),如右下图所示:

image

对于情况2:当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的两个子节点均为黑色的。如左下图所示,A表示当前节点。针对这种情况,我们要做的操作有:将兄弟节点(D)涂红,将当前节点指向其父节点(B),将其父节点指向当前节点的祖父节点,继续新的算法(具体见下面的程序),不需要旋转。这样变成了右下图所示的情况:

image

对于情况3:当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的左子节点是红色,右子节点时黑色的。如左下图所示,A是当前节点。针对这种情况,我们要做的操作有:把当前节点的兄弟节点(D)涂红,把兄弟节点的左子节点(C)涂黑,然后以兄弟节点作为支点做右旋操作。然后兄弟节点就变成黑色的,且兄弟节点的右子节点变成红色的情况(情况4)了。如右下图:
image

对于情况4:当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的右子节点是红色,左子节点任意颜色。如左下图所示:A为当前节点,针对这种情况,我们要做的操作有:把兄弟节点(D)涂成父节点的颜色,再把父节点(B)涂黑,把兄弟节点的右子节点(E)涂黑,然后以当前节点的父节点为支点做左旋操作。至此,删除修复算法就结束了,最后将根节点涂黑即可。

image
我们可以看出,如果是从情况1开始发生的,可能情况2,3,4中的一种:如果是情况2,就不可能再出现3和4;如果是情况3,必然会导致情况4的出现;如果2和3都不是,那必然是4。当然咯,实际中可能不一定会从情况1发生,这要看具体情况了。

迭代器

迭代器模式

把访问逻辑从不同类型的集合类中抽取出来,从而避免向外部暴露集合的内部结构。在java中它是一个对象,其目的是遍历并选中其中的每个元素,而使用者(客户端)无需知道里面的具体细节。

Iterator

Collection集合元素的通用获取方式:在取出元素之前先判断集合中有没有元素。如果有,就把这个元素取出来,继续再判断,如果还有就再取出来,一直把集合中的所有元素全部取出来,这种取出元素的方式专业术语称为迭代。

java.util.Iterator:在Java中Iterator为一个接口,它只提供了迭代的基本规则。在JDK中它是这样定义的:对Collection进行迭代的迭代器。迭代器取代了Java Collection Framework中的Enumeration。

Collection中有一个抽象方法iterator方法,所有的Collection子类都实现了这个方法;返回一个Iterator对象

定义:

1
2
3
4
5
6
7
8
9
10
11
package java.util;

public interface Iterator<E> {

boolean hasNext();//判断是否存在下一个对象元素

E next();//获取下一个元素

void remove();//移除元素

}

在使用Iterator的时候禁止对所遍历的容器进行改变其大小结构的操作。例如: 在使用Iterator进行迭代时,如果对集合进行了add、remove操作就会出现ConcurrentModificationException异常。

在进行集合元素取出的时候,如果集合中没有元素了,还继续使用next()方法的话,将发生NoSuchElementException没有集合元素的错误

修改并发异常:在迭代集合中元素的过程中,集合的长度发生改变(进行了元素增加或者元素删除的操作), 增强for的底层原理也是迭代器,所以也需要避免这种操作;

解决以上异常的方法:使用ListIterator

任何集合都有迭代器。

任何集合类,都必须能以某种方式存取元素,否则这个集合容器就没有任何意义。

迭代器,也是一种模式(也叫迭代器模式)。迭代器要足够的“轻量”——创建迭代器的代价小。

Iterable(1.5)

Java中还提供了一个Iterable接口,Iterable接口实现后的功能是‘返回’一个迭代器,我们常用的实现了该接口的子接口有:Collection<E>、List<E>、Set<E>等。该接口的iterator()方法返回一个标准的Iterator实现。实现Iterable接口允许对象成为Foreach语句的目标。就可以通过foreach语句来遍历你的底层序列。

Iterable接口包含一个能产生Iterator对象的方法,并且Iterable被foreach用来在序列中移动。因此如果创建了实现Iterable接口的类,都可以将它用于foreach中。

定义:

1
2
3
4
5
Package java.lang; 
import java.util.Iterator;
public interface Iterable<T> {
Iterator<T> iterator();
}

Iterable是Java 1.5的新特性, 主要是为了支持forEach语法, 使用容器的时候, 如果不关心容器的类型, 那么就需要使用迭代器来编写代码. 使代码能够重用.

使用方法很简单:

1
2
3
4
List<String> strs = Arrays.asList("a", "b", "c"); 
for (String str: strs) {
out.println(str);
}
  • 好处:代码减少,方便遍历
  • 弊端:没有索引,不能操作容器里的元素

增强for循环底层也是使用了迭代器获取的,只不过获取迭代器由jvm完成,不需要我们获取迭代器而已,所以在使用增强for循环变量元素的过程中不准使用集合对象对集合的元素个数进行修改;

forEach()(1.8)

使用接收lambda表达式的forEach方法进行快速遍历.

1
2
3
List<String> strs = Arrays.asList("a", "b", "c"); 
// 使用Java 1.8的lambda表达式
strs.forEach(out::println);
Spliterator迭代器

Spliterator是1.8新增的迭代器,属于并行迭代器,可以将迭代任务分割交由多个线程来进行。

Spliterator可以理解为Iterator的Split版本(但用途要丰富很多)。使用Iterator的时候,我们可以顺序地遍历容器中的元素,使用Spliterator的时候,我们可以将元素分割成多份,分别交于不于的线程去遍历,以提高效率。使用 Spliterator 每次可以处理某个元素集合中的一个元素 — 不是从 Spliterator 中获取元素,而是使用 tryAdvance() 或 forEachRemaining() 方法对元素应用操作。但Spliterator 还可以用于估计其中保存的元素数量,而且还可以像细胞分裂一样变为一分为二。这些新增加的能力让流并行处理代码可以很方便地将工作分布到多个可用线程上完成

ListIterator

ListIterator是一个更强大的Iterator子类型,能用于各种List类访问,前面说过Iterator支持单向取数据,ListIterator可以双向移动,所以能指出迭代器当前位置的前一个和后一个索引,可以用set方法替换它访问过的最后一个元素。我们可以通过调用listIterator方法产生一个指向List开始处的ListIterator,并且还可以用过重载方法listIterator(n)来创建一个指定列表索引为n的元素的ListIterator。

ListIterator可以往前遍历,添加元素,设置元素

Iterator和ListIterator的区别:

两者都有next()和hasNext(),可以实现向后遍历,但是ListIterator有previous()和hasPrevious()方法,即可以实现向前遍历

ListIterator可以定位当前位置,nextIndex()和previous()可以实现

ListIterator有add()方法,可以向list集合中添加数据

都可以实现删除操作,但是ListIterator可以实现对对象的修改,set()可以实现,Iterator仅能遍历,不能修改

Fail-Fast

类中的iterator()方法和listIterator()方法返回的iterators迭代器是fail-fast的:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

迭代器与枚举有两点不同:

  1. 迭代器在迭代期间可以从集合中移除元素。

  2. 方法名得到了改进,Enumeration的方法名称都比较长。

迭代器的好处:屏蔽了集合之间的不同,可以使用相同的方式取出

集合类概念

集合类的作用

集合类也叫做容器类,和数组一样,用于存储数据,但数组类型单一,并且长度固定,限制性很大,而集合类可以动态增加长度。

集合存储的元素都是对象(引用地址),所以集合可以存储不同的数据类型,但如果是需要比较元素来排序的集合,则需要类型一致。

集合中提供了统一的增删改查方法,使用方便。

支持泛型,避免数据不一致和转换异常,还对常用的数据结构进行了封装。

所有的集合类的都在java.util包下。

集合框架体系的组成

集合框架体系是由Collection、Map(映射关系)和Iterator(迭代器)组成,各部分的作用如下所示。

Collection体系中有三种集合:Set、List、Queue

Set(集): 元素是无序的且不可重复。

List(列表):元素是有序的且可重复。

Queue(队列):封装了数据结构中的队列。

Map体系

Map用于保存具有映射关系的数据,即key-value(键值对)。Map集合的key是唯一的,不可重复,而value可以重复。所以一个value可以对应多个key。

Map体系除了常用类之外,还有Properties(属性类)也属于Map体系。

Iterator(迭代器)

请查看上面!

Collection的由来

由于数组中存放对象,对对象操作起来不方便。java中有一类容器,专门用来存储对象。

集合可以存储多个元素,但我们对多个元素也有不同的需求

多个元素,不能有相同的

多个元素,能够按照某个规则排序

针对不同的需求:java就提供了很多集合类,多个集合类的数据结构不同。但是,结构不重要,重要 的是能够存储东西,能够判断,获取.

把集合共性的内容不断往上提取,最终形成集合的继承体系—->Collection

并且所有的Collection实现类都重写了toString()方法.

集合和数组

集合与数组的区别:

  • 数组的长度固定的,而集合长度时可变的
    数组只能储存同一类型的元素,而且能存基本数据类型和引用数据类型。集合可以存储不同类型的元素,只能存储引用数据类型

集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用变量);而集合只能保存对象。

数组和集合的主要区别包括以下几个方面:

  • 数组声明了它容纳的元素的类型,而集合不声明。这是由于集合以object形式来存储它们的元素。
  • 一个数组实例具有固定的大小,不能伸缩。集合则可根据需要动态改变大小。
  • 数组是一种可读/可写数据结构没有办法创建一个只读数组。然而可以使用集合提供的ReadOnly方 只读方式来使用集合。该方法将返回一个集合的只读版本。

集合的作用:

如果一个类的内部有很多相同类型的属性,并且他们的作用与意义是一样的,比如说学生能选课学生类就有很多课程类型的属性,或者工厂类有很多机器类型的属性,我们用一个类似于容器的集合去盛装他们,这样在类的内部就变的井然有序———这就是:

  • 在类的内部,对数据进行组织的作用。
  • 简单而快速的搜索查找其中的某一条元素
  • 有的集合接口,提供了一系列排列有序的元素,并且可以在序列中间快速的插入或者删除有关元素。
  • 有的集合接口在其内部提供了映射关系的结构,可以通过关键字(key)去快速查找对应的唯一对象,而这个关键可以是任意类型的。

泛型与集合的区别

泛型听起来很高深的一个词,但实际上它的作用很简单,就是提高java程序的性能。

比如在计算机中经常用到一些数据结构,如队列,链表等,而其中的元素以前一般这么定义:object a=new object();

这样就带来一个严重的问题,用object来表示元素没有逻辑问题,但每次拆箱、封箱就占用了大量的计算机资源,导致程序性能低下,而这部分内容恰恰一般都是程序的核心部分,如果使用object,那么程序的表现就比较糟糕。

而使用泛型则很好的解决这个问题,本质就是在编译阶段就告诉编译器,数据结构中元素的种类,既然编译器知道了元素的种类,自然就避免了拆箱、封箱的操作,从而显著提高java程序的性能。

比如List<string>就直接使用string对象作为List的元素,而避免使用object对象带来的封箱、拆箱操作,从而提高程序性能。

集合接口与类

数组和集合一般就用到下面接口和集合

Array 数组

Arrays 数组工具

Collection 最基本的集合接口

Collections 集合工具类

List 接口

ArrayList 一种可以动态增长和缩减的索引序列

LinkedList 一种可以在任何位置进行高效地插入和删除操作的有序序列

Vector

Set

HashSet 一种没有重复元素的无序集合

TreeSet 一种有序集

LinkHashSet 一种可以记住元素插入次序的集合

map

HashMap 一种存储key:value关联的映射

HashTable

TreeMap 一种key有序的映射

LinkedHashMap 一种可以记住插入次序的映射

Deque

Stack

ArrayDeque 一种用循环数组实现的双端队列

Queue

PriorityQueue 一种可以高效删除最小元素的集合

Array

数组:是以一段连续内存保存数据的;随机访问是最快的,但不支持插入,删除,迭代等操作。

Array可以包含基本类型和对象类型

Array大小是固定的

指定数组引用为 null,则此类中的方法都会抛出 NullPointerException。

所创建的对象都放在堆中。

够对自身进行枚举(因为都实现了IEnumerable接口)。

具有索引(index),即可以通过index来直接获取和修改任意项。

Array类型的变量在声明的同时必须进行实例化(至少得初始化数组的大小),而ArrayList可以只是先声明。

Array只能存储同构的对象,而ArrayList可以存储异构的对象。

在CLR托管对中的存放方式

Array是始终是连续存放的,而ArrayList的存放不一定连续。

Array不能够随意添加和删除其中的项,而ArrayList可以在任意位置插入和删除项。

采用数组存在的一些缺陷:

1.数组长度固定不变,不能很好地适应元素数量动态变化的情况。

2.可通过数组名.length获取数组的长度,却无法直接获取数组中真实存储的个数。

3.在进行频繁插入、删除操作时同样效率低下。

Arrays

数组的工具类,里面都是操作数组的工具.

常用方法:

1、数组的排序:Arrays.sort(a);//实现了对数组从小到大的排序//注:此类中只有升序排序,而无降序排序。

2、数组元素的定位查找:Arrays.binarySearch(a,8);//二分查找法

3、数组的打印:Arrays.toString(a);//String 前的a和括号中的a均表示数组名称

4、 查看数组中是否有特定的值:Arrays.asList(a).contains(1);

Collection

Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements)。一些 Collection允许相同的元素而另一些不行。一些能排序而另一些不行。Java SDK不提供直接继承自Collection的类, Java SDK提供的类都是继承自Collection的“子接口”如List和Set。

所有实现Collection接口的类都必须提供两个标准的构造函数:无参数的构造函数用于创建一个空的Collection,有一个Collection参数的构造函数用于创建一个新的 Collection,这个新的Collection与传入的Collection有相同的元素。后一个构造函数允许用户复制一个Collection。

如何遍历Collection中的每一个元素?不论Collection的实际类型如何,它都支持一个iterator()的方法,该方法返回一个迭代子,使用该迭代子即可逐一访问Collection中每一个元素。典型的用法如下:

1
2
3
4
5
Iterator it = collection.iterator(); // 获得一个迭代子

while(it.hasNext()) {
Object obj = it.next(); // 得到下一个元素
}

由Collection接口派生的两个接口是List和Set。

Collection返回的是Iterator迭代器接口,而List中又有它自己对应的实现–>ListIterator接口 Collection。标识所含元素的序列,这里面又包含多种集合类,比如List,Set,Queue;它们都有各自的特点,比如List是按顺序插入元素,Set是不重复元素集合,Queue则是典型的FIFO结构

Collection接口描述:

  Collection接口常用的子接口有List 接口和Set接口

  List接口中常用的子类有:ArrayList类(数组列表)和LinkedList(链表)

  Set接口中常用的子类有:HashSet (哈希表)和LinkedHashSet(基于链表的哈希表)

Collection 层次结构 中的根接口。Collection 表示一组对象,这些对象也称为 collection 的元素。一些 collection 允许有重复的元素,而另一些则不允许。一些 collection 是有序的,而另一些则是无序的。JDK 不提供此接口的任何直接 实现:它提供更具体的子接口(如 Set和 List)实现。此接口通常用来传递 collection,并在需要最大普遍性的地方操作这些 collection。(面向接口的编程思想)

Collections

排序操作

Collections提供以下方法对List进行排序操作

void reverse(List list):反转

void shuffle(List list),随机排序

void sort(List list),按自然排序的升序排序

void sort(List list, Comparator c);定制排序,由Comparator控制排序逻辑

void swap(List list, int i , int j),交换两个索引位置的元素

void rotate(List list, int distance),旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面。

查找,替换操作

int binarySearch(List list, Object key), 对List进行二分查找,返回索引,注意List必须是有序的

int max(Collection coll),根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)

int max(Collection coll, Comparator c),根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c)

void fill(List list, Object obj),用元素obj填充list中所有元素

int frequency(Collection c, Object o),统计元素出现次数

int indexOfSubList(List list, List target), 统计targe在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target).

boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素。

同步控制

Collections中几乎对每个集合都定义了同步控制方法, 这些方法,来将集合包装成线程安全的集合

SynchronizedList(List);

SynchronizedSet(Set;

SynchronizedMap(Map);

SynchronizedMap和ConcurrentHashMap 区别

Collections.synchronizedMap()和Hashtable一样,实现上在调用map所有方法时,都对整个map进行同步,而ConcurrentHashMap的实现却更加精细,它对map中的所有桶加了锁。所以,只要要有一个线程访问map,其他线程就无法进入map,而如果一个线程在访问ConcurrentHashMap某个桶时,其他线程,仍然可以对map执行某些操作。这样,ConcurrentHashMap在性能以及安全性方面,明显比Collections.synchronizedMap()更加有优势。同时,同步操作精确控制到桶,所以,即使在遍历map时,其他线程试图对map进行数据修改,也不会抛出ConcurrentModificationException。

ConcurrentHashMap从类的命名就能看出,它必然是个HashMap。而Collections.synchronizedMap()可以接收任意Map实例,实现Map的同步

线程安全,并且锁分离。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。

List

List:有序(元素存入集合的顺序和取出的顺序一致),元素都有索引。元素可以重复。

List本身是Collection接口的子接口,具备了Collection的所有方法。

List的特有方法都有索引,这是该集合最大的特点。

List集合支持对元素的增、删、改、查。

List中存储的元素实现类排序,而且可以重复的存储相关元素。

次序是List最重要的特点:它保证维护元素特定的顺序。

List是有序的Collection,使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,这类似于Java的数组。

和下面要提到的Set不同,List允许有相同的元素。

除了具有Collection接口必备的iterator()方法外,List还提供一个listIterator()方法,返回一个 ListIterator接口,和标准的Iterator接口相比,ListIterator多了一些add()之类的方法,允许添加,删除,设定元素,还能向前或向后遍历。

优点:操作读取操作效率高,基于数组实现的,可以为null值,可以允许重复元素,有序,异步。

缺点:由于它是由动态数组实现的,不适合频繁的对元素的插入和删除操作,因为每次插入和删除都需要移动数组中的元素。

ArrayList

ArrayList 是基于数组实现,内存中分配连续的空间,需要维护容量大小。随机访问.

ArrayList就是动态数组,也是一个对象。

ArrayList不自定义位置添加元素和LinkedList性能没啥区别,ArrayList默认元素追加到数组后面,而LinkedList只需要移动指针,所以两者性能相差无几。

如果ArrayList自定义位置插入元素,越靠前,需要重写排序的元素越多,性能消耗越大,LinkedList无论插入任何位置都一样,只需要创建一个新的表项节点和移动一下指针,性能消耗很低。

ArrayList是基于数组,所以查看任意位置元素只需要获取当前位置的下标的数组就可以,效率很高,然而LinkedList获取元素需要从最前面或者最后面遍历到当前位置元素获取,如果集合中元素很多,就会效率很低,性能消耗大。

频繁遍历查看元素,使用 ArrayList 集合,ArrayList 查询快,增删慢

ArrayList线程不安全的

1、ArrayList是用数组实现的,该对象存放在堆内存中,这个数组的内存是连续的,不存在相邻元素之间还隔着其他内存。底层是一个可动态扩容的数组

2、索引ArrayList时,速度比原生数组慢是因为你要用get方法,这是一个函数调用,而数组直接用[ ]访问,相当于直接操作内存地址,速度当然比函数调用快。

3、新建ArrayList的时候,JVM为其分配一个默认或指定大小的连续内存区域(封装为数组)。

4、每次增加元素会检查容量,不足则创建新的连续内存区域(大小等于初始大小+步长),也用数组形式封装,并将原来的内存区域数据复制到新的内存区域,然后再用ArrayList中引用原来封装的数组对象的引用变量引用到新的数组对象:

1
elementData = Arrays.copyOf(elementData, newCapacity);

  ArrayList里面的removeIf方法就接受一个Predicate参数,采用如下Lambda表达式就能把,所有null元素删除:

1
list.removeIf(e -> e == null);

    

ArrayList:每次添加元素之前会检查是否需要扩容,是按照原数组的1.5倍延长。构造一个初始容量为 10 的空列表。

使用for适合循环ArrayLIst以及数组,当大批量的循环LinkedList时程序将会卡死,for适合循环数组结构,通过下标去遍历。

get访问List内部任意元素时,ArrayList的性能要比LinkedList性能好。LinkedList中的get方法是要按照顺序从列表的一端开始检查,直到另一端。

在ArrayList的 中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;

ArrayList的空 间浪费主要体现在在list列表的结尾预留一定的容量空间

ArrayList只能包含对象类型。

ArrayList的大小是动态变化的。

对于基本类型数据,集合使用自动装箱来减少编码工作量

够对自身进行枚举(因为实现了IEnumerable接口)。

具有索引(index),即可以通过index来直接获取和修改任意项。

ArrayList允许存放(不止一个)null元素

允许存放重复数据,存储时按照元素的添加顺序存储

ArrayList可以存放任何不同类型的数据(因为它里面存放的都是被装箱了的Object型对象,实际上ArrayList内部就是使用”object[] _items;”这样一个私有字段来封装对象的)

ArrayList不是一个线程安全的集合,如果集合的增删操作需要保证线程的安全性,可以考虑使用CopyOWriteArrayList或者使用collections.synchronizedList(Lise l)函数返回一个线程安全的ArrayList类。

实现了RandomAccess接口,底层又是数组,get读取元素性能很好

顺序添加很方便

删除和插入需要复制数组 性能很差(可以使用LinkindList)

为什么ArrayList的elementData是用transient修饰的?

transient修饰的属性意味着不会被序列化,也就是说在序列化ArrayList的时候,不序列化elementData。

为什么要这么做呢?

elementData不总是满的,每次都序列化,会浪费时间和空间

重写了writeObject 保证序列化的时候虽然不序列化全部 但是有的元素都序列化

所以说不是不序列化 而是不全部序列化。

elementData属性采用了transient来修饰,不使用Java默认的序列化机制来实例化,自己实现了序列化writeObject()和反序列化readObject()的方法。

每次对下标的操作都会进行安全性检查,如果出现数组越界就立即抛出异常。

如果提前知道数组元素较多,可以在添加元素前通过调用ensureCapacity()方法提前增加容量以减小后期容量自动增长的开销。

当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。

ArrayList基于数组方式实现,容量限制不大于Integer.MAX_VALUE的小大,每次扩容1.5倍。有序,可以为null,允许重复,非线程安全。

增加和删除会修改modCount,在迭代的时候需要保持单线程的唯一操作,如果期间进行了插入或者删除操作,就会被迭代器检查获知,从而出现运行时异常。

一般建议在单线程中使用ArrayList。

当在index处放置一个元素的时候,会将数组index处右边的元素全部右移

当在index处删除一个元素的时候,会将数组index处右边的元素全部左移

ArrayList底层是数组结构,因为数组有维护索引,所以查询效率高;而做插入、删除操作时,因为要判断扩容(复制一份新数组)且数组中的元素可能要大规模的后移或前移一个索引位置,所以效率差。

Arrays.asList()方法返回的List集合是一个固定长度的List集合,不是ArrayList实例,也不是Vector的实例

ArrayList也采用了快速失败(Fail-Fast机制)的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。具体介绍请参考HashMap的实现原理中的Fail-Fast机制。

linkedList

LinkedList 是基于循环双向链表数据结构,不需要维护容量大小。顺序访问。

频繁插入删除元素 使用 LinkedList 集合

LinkedList 线程不安全的

LinkedList在随机访问方面相对比较慢,但是它的特性集较ArrayList 更大。

LinkedList提供了大量的首尾操作

LinkedList:底层的数据结构是链表,线程不同步,增删元素的速度非常快。

LinkedList:底层基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还存储下一个元素的地址。链表增删快,查找慢

LinkedList由双链表实现,增删由于不需要移动底层数组数据,其底层是链表实现的,只需要修改链表节点指针,对元素的插入和删除效率较高。

LinkedList缺点是遍历效率较低。HashMap和双链表也有关系。

LinkedList是一个继承于AbstractSequentialList的双向链表,它可以被当做堆栈、队列或双端队列进行操作

LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。

使用foreach适合循环LinkedList,使用双链表结构实现的应当使用foreach循环。

LinkedList实现了List接口,允许null元素。

LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:

1
List list = Collections.synchronizedList(new LinkedList(…));

在LinkedList的中间插入或删除一个元素的开销是固定的。

LinkedList不支持高效的随机元素访问。

LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间

LinkedList是List和Deque接口的双向链表的实现。实现了所有可选列表操作,并允许包括null值。

Fail-Fast机制:LinkedList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。

LinkedList因为底层为链表结构,查询时需要从头节点(或尾节点)开始遍历所以查询效率差;但同时也因为是链表结构,做插入、删除操作时只要断开当前删除节点前驱、后驱引用,并将原来的前、后节点的引用链接起来,所以效率高。

千万不要使用普通for循环遍历LinkedList,这么做会让你崩溃!可以选择使用foreach或迭代器来进行遍历操作

LinedList适合用迭代遍历;

基于链表结构的集合 LinkedList。LinkedList 属于 java.util 包下面,也实现Iterable接口,说明可以使用迭代器遍历;LinkedList 还实现 Deque<E>,Queue<E>操作。Deque 和 Queue 是 LinkedList 的父接口,那么 LinkedList 也可以看成一种 Deque 或者 Queue;Queue表示一种队列,也是一种数据结构,它的特点是先进先出,因此在队列这个接口里面提供了一些操作队列的方法,同时LinkedList也具有这些方法;Deque(Double ended queues双端队列),支持在两端插入或者移除元素; 那也应该具有操作双端队列的一些方法;LinkedList 是他们的子类,说明都具有他们两者的方法;LinkedList也可以充当队列,双端队列,堆栈多个角色。

vector

Vector:底层的数据结构就是数组,线程同步的,Vector无论查询和增删都巨慢。

Vector:是按照原数组的2倍延长。

Vector是基于线程安全的,效率低 元素有放入顺序,元素可重复

Vector可以由我们自己来设置增长的大小,ArrayList没有提供相关的方法。

Vector相对ArrayList查询慢(线程安全的)

Vector相对LinkedList增删慢(数组结构)

以前还能见到Vector和Stack,但Vector太过古老,被ArrayList取代,所以这里不讲;而Stack已经被ArrayDeque取代。

对于想在迭代器迭代过程中针对集合进行增删改的,可以通过返回ListIterator来操作。

Vector:底层结构是数组,线程是安全的,添加删除慢,查找快,(同ArrayList)

ArrayList 和Vector是采用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,都允许直接序号索引元素,但是插入数据要涉及到数组元素移动等内存操作,所以索引数据快,插入数据慢,Vector由于使用了synchronized方法(线程安全)所以性能上比ArrayList要差,LinkedList使用双向链表实现存储,按序号索引数据需要进行向前或向后遍历,但是插入数据时只需要记录本项的前后项即可,所以插入数度较快。

Vector 是矢量队列,它是JDK1.0版本添加的类。继承于AbstractList,实现了List, RandomAccess, Cloneable这些接口。

Vector 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在Vector中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。

Vector 实现了Cloneable接口,即实现clone()函数。它能被克隆。

由Vector创建的Iterator,当一个Iterator被创建而且正在被使用,另一个线程改变了Vector的状态(例如,添加或删除了一些元素),这时调用Iterator的方法时将抛出ConcurrentModificationException,因此必须捕获该异常。

Set

无序(存入和取出顺序有可能不一致),不可以存储重复元素。必须保证元素唯一性。

元素无放入顺序,元素不可重复(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的)

Set具有与Collection完全一样的接口,因此没有任何额外的功能,只是行为不同。这是继承与多态思想的典型应用:表现不同的行为。

Set不保存重复的元素(至于如何判断元素相同则较为负责)

存入Set的每个元素都必须是唯一的,因为Set不保存重复元素,加入Set的元素必须定义equals()方法以确保对象的唯一性。

Set 是基于对象的值来确定归属性的。

Set本身有去重功能是因为String内部重写了hashCode()和equals()方法,在add里实现了去重, Set集合是不允许重复元素的,但是集合是不知道我们对象的重复的判断依据的,默认情况下判断依据是判断两者是否为同一元素(euqals方法,依据是元素==元素),如果要依据我们自己的判断来判断元素是否重复,需要重写元素的equals方法(元素比较相等时调用)hashCode()的返回值是元素的哈希码,如果两个元素的哈希码相同,那么需要进行equals判断。【所以可以自定义返回值作为哈希码】 equals()返回true代表两元素相同,返回false代表不同。

set集合没有索引,只能用迭代器或增强for循环遍历

set的底层是map集合

Set最多有一个null元素

必须小心操作可变对象(Mutable Object)。如果一个Set中的可变元素改变了自身状态导致Object.equals(Object)=true将导致一些问题。

Set具有与Collection完全一样的接口,没有额外的任何功能。所以把Set就是Collection,只是行为不同(这就是多态);Set是基于对象的值来判断归属的,由于查询速度非常快速,HashSet使用了散列,HashSet维护的顺序与TreeSet或LinkedHashSet都不同,因为它们的数据结构都不同,元素存储方式自然也不同。TreeSet的数据结构是“红-黑树”,HashSet是散列函数,LinkedHashSet也用了散列函数;如果想要对结果进行排序,那么选择TreeSet代替HashSet是个不错的选择

Hashset

HashSet : 为快速查找设计的Set。存入HashSet的对象必须定义hashCode()。

Hashset实现set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set的迭代顺序,别是它不保证该顺序恒久不变。此类允许使用Null元素

对于HashSet而言,它是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet的实现比较简单,相关HashSet的操作,基本上都说调用HashMap的相关方法来实现的

对于HashSet中保存的对象,请注意正确重写其equals和hashCode方法,以保证放入的对象的唯一性。

HashSet: 哈希表结构的集合 利用哈希表结果构成的集合查找速度会很快。

HashSet : 底层数据结构是哈希表,线程 是不同步的 。 无序,高效;HashSet 集合保证元素唯一性 :通过元素的 hashCode 方法,和 equals 方法完成的。当元素的 hashCode 值相同时,才继续判断元素的 equals 是否为 true。如果为 true,那么视为相同元素,不存。如果为 false,那么存储。如果 hashCode 值不同,那么不判断 equals,从而提高对象比较的速度。

HashSet类直接实现了Set接口, 其底层其实是包装了一个HashMap去实现的。HashSet采用HashCode算法来存取集合中的元素,因此具有比较好的读取和查找性能。

元素值可以为NULL,但只能放入一个null

  HashSet集合保证元素唯一性:通过元素的hashCode方法,和equals方法完成的。

当元素的hashCode值相同时,才继续判断元素的equals是否为true。

如果hashCode值不同,那么不判断equals,从而提高对象比较的速度。

对于HashSet集合,判断元素是否存在,或者删除元素,底层依据的是hashCode方法和equals方法。  

特点:存储取出都比较快

1、不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。

2、HashSet不是同步的,必须通过代码来保证其同步。

3、集合元素可以是null.

原理:简单说就是链表数组结合体

对象的哈希值:普通的一个整数,可以理解为身份证号,是hashset存储的依据

HashSet按Hash算法来存储集合中的元素。在存取和查找上有很好的性能。

当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该hashCode值决定该hashCode值决定该对象在HashSet中存储的位置。

如果有两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不相等,hashSet将会把它们存储在不同位置,依然可以添加成功。如果两个对象的hashCode()方法返回的hashCode值相同,当它们的equals()方法返回false时,会在hashCode所在位置采用链式结构保存多个对象。这样会降低hashSet的查询性能。

在使用HashSet中重写hashCode()方法的基本原则

1、在程序运行过过程中,同一个对象多次调用hashCode()方法应该返回相同的值。

2、当两个对象的equals()方法比较返回true时,这个两个对象的hashCode()方法返回相同的值。

3、对象中用作equals()方法比较标准的实例变量,都应该用于计算hashCode值。

把对象内的每个意义的实例变量(即每个参与equals()方法比较标准的实例变量)计算出一个int类型的hashCode值。用第1步计算出来的多个hashCode值组合计算出一个hashCode值返回

1
return f1.hashCode()+(int)f2;

为了避免直接相加产生的偶然相等(两个对象的f1、f2实例变量并不相等,但他们的hashCode的和恰好相等),可以通过为各个实例变量的hashCode值乘以一个质数后再相加

1
return f1.hashCode()*19+f2.hashCode()*37;

如果向HashSet中添加一个可变的对象后,后面的程序修改了该可变对想的实例变量,则可能导致它与集合中的其他元素的相同(即两个对象的equals()方法比较返回true,两个对象的hashCode值也相等),这就有可能导致HashSet中包含两个相同的对象。

Linkedhashset

LinkedHashSet : 具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素插入的次序

LinkedHashSet 综合了链表+哈希表,根据元素的hashCode值来决定元素的存储位置,它同时使用链表维护元素的次序。

当遍历该集合时候,LinkedHashSet 将会以元素的添加顺序访问集合的元素。

对于 LinkedHashSet 而言,它继承与 HashSet、又基于 LinkedHashMap 来实现的。

这个相对于HashSet来说有一个很大的不一样是LinkedHashSet是有序的。LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。

与HashSet相比,特点:

对集合迭代时,按增加顺序返回元素。

性能略低于HashSet,因为需要维护元素的插入顺序。但迭代访问元素时会有好性能,因为它采用链表维护内部顺序。

LinkedHashSet不允许元素的重复

存储的顺序是元素插入的顺序。

TreeSet

TreeSet : 保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列。

TreeSet 继承AbstractSet类,实现NavigableSet、Cloneable、Serializable接口。与HashSet是基于HashMap实现一样,TreeSet 同样是基于TreeMap 实现的。由于得到Tree 的支持,TreeSet 最大特点在于排序,它的作用是提供有序的Set集合。

用于对 Set 集合进行元素的指定顺序排序,排序需要依据元素自身具备的比较性。

如果元素不具备比较性,在运行时会抛出ClassCastException 异常。 所以元素需要实现Comparable 接口 ,让元素具备可比较性, 重写 compareTo 方法 。依据 compareTo 方法的返回值,确定元素在 TreeSet 数据结构中的位置。 或者用比较器方式,将Comparator对象传递给TreeSet构造器来告诉树集使用不同的比较方法

 

TreeSet底层的数据结构就是二叉树。

  • 不能写入空数据
  • 写入的数据是有序的。
  • 不写入重复数据

TreeSet方法保证元素唯一性的方式:就是参考比较方法的结果是否为0,如果return 0,视为两个对象重复,不存。

TreeSet集合排序有两种方式,Comparable和Comparator区别:

1:让元素自身具备比较性,需要元素对象实现Comparable接口,覆盖compareTo方法。

2:让集合自身具备比较性,需要定义一个实现了Comparator接口的比较器,并覆盖compare方法,并将该类对象作为实际参数传递给TreeSet集合的构造函数。

TreeSet类是SortedSet接口的实现类。因为需要排序,所以性能肯定差于HashSet。与HashSet相比,额外增加的方法有:

first():返回第一个元素

last():返回最后一个元素

lower(Object o):返回指定元素之前的元素

higher(Obect o):返回指定元素之后的元素

subSet(fromElement, toElement):返回子集合

可以定义比较器(Comparator)来实现自定义的排序。默认自然升序排序。

TreeSet两种排序方式:自然排序和定制排序,默认情况下,TreeSet采用自然排序

TreeSet会调用集合元素的compareTo(Object object)方法来比较元素之间的大小关系,然后将元素按升序排列

如果试图把一个元素添加到TreeSet中,则该对象必须实现Comparable接口实现Comparable接口必须实现compareTo(Object object),两个对象即通过这个方法进行比较Comparable的典型实现

BigDecimal、BigInteger以及所有的数值类型对应的包装类型,按对应的数值大小进行比较

Character:按字符的Unicode值进行比较

Boolean:true对应的包装类实例大于false包装类对应的实例

String:按字符对应的Unicode值进行比较

Date、Time:后面的时间、日期比前面的时间、日期大

向TreeSet中添加一个元素,只有第一个不需要使用compareTo()方法,后面的都要调用该方法

因为只有相同类的两个实例才会比较大小,所以向TreeSet中添加的应该是同一个类的对象

TreeSet采用红黑树的数据结构来存储集合元素

对于TreeSet集合而言,它判断两个对象的是否相等的唯一标准是:两个对象的通过compareTo(Object obj)方法比较是否返回0–如果通过compareTo(Object obj)方法比较返回0,TreeSet则会认为它们相等,否则认为它们不相等。对于语句,obj1.compareTo(obj2),如果该方法返回一个正整数,则表明obj1大于obj2;如果该方法返回一个负整数,则表明obj1小于obj2.

在默认的compareTo方法中,需要将的两个的类型的对象的转换同一个类型,因此需要将的保证的加入到TreeSet中的数据类型是同一个类型,但是如果自己覆盖compareTo方法时,没有要求两个对象强制转换成同一个对象,是可以成功的添加treeSet中

如果两个对象通过CompareTo(Object obj)方法比较返回0时,但它们通过equals()方法比较返回false时,TreeSet不会让第二个元素添加进去

Map

Map主要用于存储健值对,根据键得到值,因此不允许键重复,但允许值重复。

Map接口概述:Java.util.Map<k,v>接口:是一个双列集合

Map集合的特点: 是一个双列集合,有两个泛型key和value,使用的时候key和value的数据类型可以相同。也可以不同.

Key不允许重复的,value可以重复的;

一个key只能对应一个value

底层是一个哈希表(数组+单向链表):查询快,增删快, 是一个无序集合

Map接口中的常用方法:

1.get(key) 根据key值返回对应的value值,key值不存在则返回null

2.put(key , value); 往集合中添加元素(key和value)

  注意:添加的时候,如果key不存在,返回值null

  如果Key已经存在的话,就会新值替换旧值,返回旧值

  1. remove(key); 删除key值对应的键值对;如果key不存在 ,删除失败。返回值为 null,如果key存在则删除成功,返回值为删除的value

Map遍历方式

第一种方式:通过key找value的方式:

   Map中有一个方法:

      Set keySet(); 返回此映射包含的键的Set 集合

   操作步骤:

   1.调用Map集合的中方法keySet,把Map集合中所有的健取出来,存储到Set集合中

   2.遍历Set集合,获取Map集合中的每一个健

   3.通过Map集合中的方法get(key),获取value值

    可以使用迭代器跟增强for循环遍历

第二种方式:Map集合遍历键值方式

    Map集合中的一个方法:

    Set<Map.Entry<k,v>> entrySet(); 返回此映射中包含的映射关系的Set视图

 使用步骤

    * 1.使用Map集合中的方法entrySet,把键值对(键与值的映射关系),取出来存储到Set 集合中

    * 2.遍历Set集合,获取每一个Entry对象

    * 3.使用Entry对象中的方法getKey和getValue获取健和值

  可以使用迭代器跟增强for循环遍历

Collection中的集合元素是孤立的,可理解为单身,是一个一个存进去的,称为单列集合

Map中的集合元素是成对存在的,可理解为夫妻,是一对一对存进去的,称为双列集合

Map中存入的是:键值对,键不可以重复,值可以重复

Map主要用于存储带有映射关系的数据(比如学号与学生信息的映射关系)

Map没有继承Collection接口,Map提供key到value的映射。一个Map中不能包含相同的key,每个key只能映射一个 value。Map接口提供3种集合的视图,Map的内容可以被当作一组key集合,一组value集合,或者一组key-value映射。

Map具有将对象映射到其他对象的功能,是一个K-V形式存储容器,你可以通过containsKey()和containsValue()来判断集合是否包含某个减或某个值。Map可以很容以拓展到多维(值可以是其他容器甚至是其他Map):

Map<Object,List>

Map集合的数据结构仅仅针对键有效,与值无关。

HashMap

HashMap非线程安全,高效,支持null;

根据键的HashCode 来存储数据,根据键可以直接获取它的值,具有很快的访问速度。遍历时,取得数据的顺序是完全随机的。

HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null。(不允许键重复,但允许值重复)

HashMap不支持线程的同步(任一时刻可以有多个线程同时写HashMap,即线程非安全),可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap() 方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。

Hashtable与 HashMap类似。不同的是:它不允许记录的键或者值为空;它支持线程的同步(任一时刻只有一个线程能写Hashtable,即线程安全),因此也导致了 Hashtable 在写入时会比较慢。

HashMap里面存入的值在取出的时候是随机的,它根据键的HashCode来存储数据,根据键可以直接获取它的值,具有很快的访问速度。在Map 中插入、删除和定位元素,HashMap 是最好的选择。

HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。

Map map = Collections.synchronizedMap(new HashMap());

HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置。HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。学过数据结构的同学都知道,解决hash冲突的方法有很多,HashMap底层是通过链表来解决hash冲突的。

HashMap其实也是一个线性的数组实现的,所以可以理解为其存储数据的容器就是一个线性数组。这可能让我们很不解,一个线性的数组怎么实现按键值对来存取数据呢?这里HashMap有做一些处理。首先HashMap里面实现一个静态内部类Entry,其重要的属性有 key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。

HashMap是常用的Java集合之一,是基于哈希表的Map接口的实现。与HashTable主要区别为不支持同步和允许null作为key和value。由于HashMap不是线程安全的,如果想要线程安全,可以使用ConcurrentHashMap代替。

HashMap的底层是哈希数组,数组元素为Entry。HashMap通过key的hashCode来计算hash值,当hashCode相同时,通过“拉链法”解决冲突

相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。原本Map.Entry接口的实现类Entry改名为了Node。转化为红黑树时改用另一种实现TreeNode。

image

1.8中最大的变化就是在一个Bucket中,如果存储节点的数量超过了8个,就会将该Bucket中原来以链表形式存储节点转换为以树的形式存储节点;而如果少于6个,就会还原成链表形式存储。

为什么要这样做?前面已经说过LinkedList的遍历操作不太友好,如果在节点个数比较多的情况下性能会比较差,而树的遍历效率是比较好的,主要是优化遍历,提升性能。

HashMap:去掉了contains(),保留了containsKey(),containsValue()

HashMap:key,value可以为空.null作为key只能有一个,null作为value可以存在多个

HashMap:使用Iterator

HashMap:数组初始大小为16,扩容方式为2的指数幂形式

HashMap:重新计算hash值

HashMap是基于哈希表的Map接口的实现,HashMap是一个散列表,存储的内容是键值对(key-value)映射,键值对都可为null;

HashMap继承自 AbstractMap<K, V> 并实现Map<K, V>, Cloneable, Serializable接口;

HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。底层是个数组,数组上存储的数据是Entry<K,V>类型的链表结构对象。

HashMap是无序的,LinkedHashMap和treeMap是有序的;

HashMap基于哈希原理,可以通过put和get方法存储和获取对象。当我们将键值对传递给put方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到对应的bucket位置存储键对象和值对象作为Map.Entry;如果两个对象的hashcode相同,所以对应的bucket位置是相同的,HashMap采用链表解决冲突碰撞,这个Entry(包含有键值对的Map.Entry对象)会存储到链表的下一个节点中;如果对应的hashcode和key值都相同,则修改对应的value的值。HashMap在每个链表节点中存储键值对对象。当使用get()方法获取对象时,HashMap会根据键对象的hashcode去找到对应的bucket位置,找到对应的bucket位置后会调用keys.equals()方法去找到连表中对应的正确的节点找到对象。

HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。

HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆。

HashMap存数据的过程是:

HashMap内部维护了一个存储数据的Entry数组,HashMap采用链表解决冲突,每一个Entry本质上是一个单向链表。当准备添加一个key-value对时,首先通过hash(key)方法计算hash值,然后通过indexFor(hash,length)求该key-value对的存储位置,计算方法是先用hash&0x7FFFFFFF后,再对length取模,这就保证每一个key-value对都能存入HashMap中,当计算出的位置相同时,由于存入位置是一个链表,则把这个key-value对插入链表头。

HashMap中key和value都允许为null。key为null的键值对永远都放在以table[0]为头结点的链表中。

HashMap内存储数据的Entry数组默认是16,如果没有对Entry扩容机制的话,当存储的数据一多,Entry内部的链表会很长,这就失去了HashMap的存储意义了。所以HasnMap内部有自己的扩容机制。HashMap内部有:

变量size,它记录HashMap的底层数组中已用槽的数量;

变量threshold,它是HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子)    

变量DEFAULT_LOAD_FACTOR = 0.75f,默认加载因子为0.75

HashMap扩容的条件是:当size大于threshold时,对HashMap进行扩容  

扩容是是新建了一个HashMap的底层数组,而后调用transfer方法,将就HashMap的全部元素添加到新的HashMap中(要重新计算元素在新的数组中的索引位置)。 很明显,扩容是一个相当耗时的操作,因为它需要重新计算这些元素在新的数组中的位置并进行复制处理。因此,我们在用HashMap的时,最好能提前预估下HashMap中元素的个数,这样有助于提高HashMap的性能。

加载因子,如果加载因子越大,对空间的利用更充分,但是查找效率会降低(链表长度会越来越长);如果加载因子太小,那么表中的数据将过于稀疏(很多空间还没用,就开始扩容了),对空间造成严重浪费。如果我们在构造方法中不指定,则系统默认加载因子为0.75,这是一个比较理想的值,一般情况下我们是无需修改的。另外,无论我们指定的容量为多少,构造方法都会将实际容量设为不小于指定容量的2的次方的一个数,且最大值不能超过2的30次方。

HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。

HashMap扩容时是当前容量翻倍即:capacity2,Hashtable扩容时是容量翻倍+1即:capacity2+1。

HashMap和Hashtable的底层实现都是数组+链表结构实现。

HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸:

1
2
3
4
5
6
7
8
9
10
11
12
13
static int hash(int h) {     

h ^= (h >>> 20) ^ (h >>> 12);

return h ^ (h >>> 7) ^ (h >>> 4);

}

static int indexFor(int h, int length) {

return h & (length-1);

}

HashTable

HashTable线程安全,低效,不支持null ,Hashtable是同步的

HashTable这个类实现了哈希表从key映射到value的数据结构形式。任何非null的对象都可以作为key或者value。

要在hashtable中存储和检索对象,作为key的对象必须实现hashCode、equals方法。

一般来说,默认的加载因子(0.75)提供了一种对于空间、时间消耗比较好的权衡策略。太高的值(指加载因子loadFactor)虽然减少了空间开销但是增加了检索时间,这反应在对hashtable的很多操作中,比如get、put方法。

初始容量的控制也是在空间消耗和rehash操作耗时(该操作耗时较大)二者之间的权衡。 如果初始容量大于哈希表的当前最大的条目数除以加载因子,则不会发生rehash。但是,将初始容量设置过高会浪费空间。

如果有大量的数据需要放进hashtable,则选择设置较大的初始容量比它自动rehash更优。

如果不需要线程安全的实现,建议使用HashMap代替Hashtable

如果想要一个线程安全的高并发实现,那么建议使用java.util.concurrent.ConcurrentHashMap取代了Hashtable。

HashTable的父类是Dictionary

HashTable:线程安全,HashTable方法有synchronized修饰

HashTable:保留了contains(),containsKey(),containsValue()

HashTable:key,value都不能为空.原因是源码中方法里会遍历entry,然后用entry的key或者value调用equals(),所以要先判断key/value是否为空,如果为空就会抛出异常

HashTable:使用Enumeration,Iterator

HashTable:数组初始大小为11,扩容方式为2*old+1

HashTable: 直接使用hashcode()

Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。

Hashtable也是JDK1.0引入的类,是线程安全的,能用于多线程环境中。

Hashtable同样实现了Serializable接口,它支持序列化,实现了Cloneable接口,能被克隆。

Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模:

int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length;

底层数据结构是哈希表,特点和 hashMap 是一样的

    Hashtable 是线程安全的集合,是单线程的,运行速度慢

    HashMap 是线程不安全的集合,是多线程的,运行速度快

    Hashtable 命运和 Vector 是一样的,从 JDK1.2 开始,被更先进的 HashMap 取代

     HashMap 允许存储 null 值,null 健

    Hashtable 不允许存储 null 值,null 健

     Hashtable 他的孩子,子类 Properties 依然活跃在开发舞台

Properties

Java.util.Properties 集合extends Hashtable<k,v> 集合

Properties 集合特点:

Properties集合也是一个双列集合,key跟value都已经被内置为String类型

Properties集合是一个唯一和IO流相结合的集合

可以将集合中存储的临时数据,持久化到硬盘的文件中储存

可以把文件中储存对的键值对,读取到集合中使用

Properties集合的基本操作:添加数据,遍历集合,Key和value都已经被内置为String类型。里面包含了一些和String类的相关方法

Object setProperty(String key ,String value) 往集合中添加键值对,调用Hashtable的方法put添加

String getProperty(String key ) 通过key获取value的值,相当于Map集合中的get(key) 方法

Set<String > stringPropertyNames()返回此属性列表的键集。相当于Map集合中的keySet()方法;

Properties类的load方法:

可以把文件中存储的键值对,读取到集合中使用

void load(Reader reader)

void load(InputStream inStream)

参数:

Reader reader:字符输入流,可以使用FileReader

InputStream inStream:字节输入流,可以使用FileInputStream

操作步骤:

1.创建Properties集合对象

2.创建字符输入流FileReader对象,构造方法中绑定要读取的数据源

3.使用Properties集合中的方法load,把文件中存储的键值对,读取到集合中使 用

4.释放资源

5.遍历Properties集合

注意:

1.流使用Reader字符流,可以读取中文数据

2.流使用InputStream字节流,不能操作中文,会有乱码

3.Properties集合的配置文件中,可以使用注释单行数据,使用#

4.Properties集合的配置文件中,key和value默认都是字符串,不用添加””(画蛇 添足)

5.Properties集合的配置文件中,key和value的连接符号可以使用=,也可以使用 空格

Properties类的store方法使用:

可以把集合中存储的临时数据,持久化都硬盘的文件中存储

1
2
3
void store(Writer writer, String comments)  

void store(OutputStream out, String comments)

参数:

Writer writer:字符输出流,可以使用FileWriter

OutputStream out:字节输出流,可以使用FileOutputStream

String comments:注释,解释说明存储的文件,不能使用中文(乱码),默认编码格式为 Unicode编码

可以使用””空字符串

操作步骤:

1.创建Properties集合,往集合中添加数据

2.创建字符输出流FileWriter对象,构造方法中绑定要写入的目的地

3.调用Properties集合中的方法store,把集合中存储的临时数据,持久化都硬盘的文 件中存储

4.释放资源

注意:

1.流使用Writer字符流,可以写入中文数据的

2.流使用OutputStream字节流,不能操作中文,会有乱码

3.Propertie集合存储的文件,一般都以.properties结尾(程序员默认)

HashMap多线程put操作后,get操作导致死循环。为何出现死循环?

大家都知道,HashMap采用链表解决Hash冲突,具体的HashMap的分析因为是链表结构,那么就很容易形成闭合的链路,这样在循环的时候只要有线程对这个HashMap进行get操作就会产生死循环。但是,我好奇的是,这种闭合的链路是如何形成的呢。在单线程情况下,只有一个线程对HashMap的数据结构进行操作,是不可能产生闭合的回路的。那就只有在多线程并发的情况下才会出现这种情况,那就是在put操作的时候,如果size>initialCapacity*loadFactor,那么这时候HashMap就会进行rehash操作,随之HashMap的结构就会发生翻天覆地的变化。很有可能就是在两个线程在这个时候同时触发了rehash操作,产生了闭合的回路。

简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度依然为O(1),因为最新的Entry会插入链表头部,急需要简单改变引用链即可,而对于查找操作来讲,此时就需要遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

HashMap存储自定义类型:使用HashMap储存自定义类形式,因为要保证key的唯一性。需要 自定义类重写 hashCode()跟equals()方法;

HashMap的方法基本都是Map中声明的方法

实现原理:实现一个哈希表,存储元素(key/value)时,用key计算hash值,如果hash值没有碰撞,则只用数组存储元素;如果hash值碰撞了,则相同的hash值的元素用链表存储;如果相同hash值超过8个,则相同的hash值的元素用红黑树存储。获取元素时,用key计算hash值,用hash值计算元素在数组中的下标,取得元素如果命中,则返回;如果不是就在红黑树或链表中找。

PS:存储元素的数组是有冗余的。

采用了Fail-Fast机制,通过一个modCount值记录修改次数,在迭代过程中,判断modCount跟初始过程记录的expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map,马上抛出异常;另外扩容过程中还有可能产生环形链表。

synchronized是针对整张Hash表的,即每次锁住整张表让线程独占

LinkeHashMap

LinkedHashMap继承自HashMap,实现了Map<K,V>接口。其内部还维护了一个双向链表,在每次插入数据,或者访问、修改数据时,会增加节点、或调整链表的节点顺序。以决定迭代时输出的顺序。

默认情况,遍历时的顺序是按照插入节点的顺序。这也是其与HashMap最大的区别。

也可以在构造时传入accessOrder参数,使得其遍历顺序按照访问的顺序输出。

LinkedHashMap在实现时,就是重写override了几个方法。以满足其输出序列有序的需求。

LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的。

在遍历的时候会比HashMap慢,不过有种情况例外:当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢。因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和它的容量有关。

LinkedHashMap是HashMap的子类,保存了插入的顺序,需要输出的顺序和输入的顺序相同时可用LinkedHashMap;

LinkedHashMap 是HashMap的一个子类,如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现.

LinkedHashMap取键值对时,是按照你放入的顺序来取的。

LinkedHashMap由于它的插入有序特性,也是一种比较常用的Map集合。它继承了HashMap,很多方法都直接复用了父类HashMap的方法。本文将探讨LinkedHashMap的内部实现,以及它是如何保证插入元素是按插入顺序排序的。

在分析前可以先思考下,既然是按照插入顺序,并且以Linked-开头,就很有可能是链表实现。如果纯粹以链表实现,也不是不可以,LinkedHashMap内部维护一个链表,插入一个元素则把它封装成Entry节点,并把它插入到链表尾部。功能可以实现,但这带来的查找效率达到了O(n),显然远远大于HashMap在没有冲突的情况下O(1)的时间复杂度。这就丝毫不能体现出Map这种数据结构随机存取快的优点。

所以显然,LinkedHashMap不可能只有一个链表来维护Entry节点,它极有可能维护了两种数据结构:散列表+链表。

 

LinkedHashMap的LRU特性

先讲一下LRU的定义:LRU(Least Recently Used),即最近最少使用算法,最初是用于内存管理中将无效的内存块腾出而用于加载数据以提高内存使用效率而发明的算法。

目前已经普遍用于提高缓存的命中率,如Redis、Memcached中都有使用。

为啥说LinkedHashMap本身就实现了LRU算法?原因就在于它额外维护的双向链表中。

在上面已经提到过,在做get/put操作时,LinkedHashMap会将当前访问/插入的节点移动到链表尾部,所以此时链表头部的那个节点就是 “最近最少未被访问”的节点。

举个例子:

往一个空的LinkedHashMap中插入A、B、C三个结点,那么链表会经历以下三个状态:

  1. A 插入A节点,此时整个链表只有这一个节点,既是头节点也是尾节点

  2. A -> B 插入B节点后,此时A为头节点,B为尾节点,而最近最常访问的节点就是B节点(刚被插入),而最近最少使用的节点就是A节点(相对B节点来讲,A节点已经有一段时间没有被访问过)

  3. A -> B -> C 插入C节点后,此时A为头节点,C为尾节点,而最近最常访问的节点就是C节点(刚被插入),最近最少使用的节点就是A节点 (应该很好理解了吧 : ))

那么对于缓存来讲,A就是我最长时间没访问过的缓存,C就是最近才访问过的缓存,所以当缓存队列满时就会从头开始替换新的缓存值进去,从而保证缓存队列中的缓存尽可能是最近一段时间访问过的缓存,提高缓存命中率。

LinkedHashMap实现与HashMap的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。

TreeMap

TreeMap实现SortMap接口,能够把它保存的记录根据键排序。

默认是按键的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。

TreeMap取出来的是排序后的键值对。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。

TreeMap是基于红黑树结构实现的一种Map,要分析TreeMap的实现首先就要对红黑树有所了解。

要了解什么是红黑树,就要了解它的存在主要是为了解决什么问题,对比其他数据结构比如数组,链表,Hash表等树这种结构又有什么优点。

treeMap实现了sortMap接口,能够把保存的数据按照键的值排序,默认是按照自然数排序也可自定义排序方式。

TreeMap对键进行排序了。

当用Iterator遍历TreeMap时,得到的记录是排过序的。

如果使用排序的映射,建议使用TreeMap。

在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

二叉树插入元素是有顺序的,TreeSet的元素是有序的。

由于二叉树需要对结点排序(插入的结点位置),默认情况下没有排序方法,所以元素需要继承Comparator并重写compareTo方法来实现元素之间比较大小的功能。

对于TreeSet,compareTo方法来保证元素的唯一性。【这时候可以不重写equals】

二叉树需要结点排序,所以元素之间比较能够比较,所以对于自定义元素对象,需要继承Comparator并重写的compareTo方法。 两个元素相等时,compareTo返回0;左大于右时,返回正整数(一般返回1);小于时返回负整数(一般返回-1)

TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n)

TreeMap中默认的排序为升序

使用entrySet遍历方式要比keySet遍历方式快

entrySet遍历方式获取Value对象是直接从Entry对象中直接获得,时间复杂度T(n)=o(1);

keySet遍历获取Value对象则要从Map中重新获取,时间复杂度T(n)=o(n);keySet遍历Map方式比entrySet遍历Map方式多了一次循环,多遍历了一次table,当Map的size越大时,遍历的效率差别就越大。

HashMap通过hashcode对其内容进行快速查找,而TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。

在Map 中插入、删除和定位元素,HashMap是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。使用HashMap要求添加的键类明确定义了hashCode()和 equals()的实现。

TreeMap 底层数据结构是红黑树(一种自平衡的二叉树) ,其根据比较的返回值是否是0来保证元素唯一性, 元素的排序通过两种方式:第一种是自然排序(元素具备比较性) 即让元素所属的类实现Comparable接口,第二种是比较器排序(集合具备比较性) ,即让集合接收一个Comparator的实现类对象。

Comparable 和 Comparator 的区别:

  Comparable 是一个比较的标准,里面有比较的方法,对象要具有比较的标准,就必须实现 Comparable 接口;类实现这个接口,就有比较的方法;把元素放到 TreeSet 里面去,就会自动的调用 CompareTo 方法;但是这个 Comparable 并不是专为 TreeSet 设计的;只是说,TreeSet 顺便利用而已;就像 HashCode 和 equals 也一样,不是专门为 HashSet 设计一样;只是你顺便利用而已。

  Compartor 是个比较器,也不是专门为TreeSet设计. 就是一个第三方的比较器接口;如果对象没有比较性,自己就可以按照比较器的标准,设计一个比较器,创建一个类,实现这个接口,覆写方法。

Queue

Queue用于模拟队列这种数据结构,实现“FIFO”等数据结构。即第一个放进去就是第一个拿出来的元素(从一端进去,从另一端出来)。队列常作被当作一个可靠的将对象从程序的某个区域传输到另一个区域的途径。通常,队列不允许随机访问队列中的元素。

Queue 接口并未定义阻塞队列的方法,而这在并发编程中是很常见的。BlockingQueue 接口定义了那些等待元素出现或等待队列中有可用空间的方法,这些方法扩展了此接口。

Queue 实现通常不允许插入 null 元素,尽管某些实现(如 LinkedList)并不禁止插入 null。即使在允许 null 的实现中,也不应该将 null 插入到 Queue 中,因为 null 也用作 poll 方法的一个特殊返回值,表明队列不包含元素。

LinkedList提供了方法以支持队列的行为,并且实现了Queue接口。通过LinkedList向上转型(up cast)为Queue,看Queue的实现就知道相对于LinkedList,Queue添加了element、offer、peek、poll、remove方法

offer:在允许的情况下,将一个元素插入到队尾,或者返回false

peek,element:在不移除的情况下返回队头,peek在队列为空返回null,element抛异常NoSuchElementException

poll,remove:移除并返回队头,poll当队列为空是返回null,remove抛出NoSuchElementException异常

注意:queue.offer在自动包装机制会自动的把random.nextInt转化程Integer,把char转化成Character

Deque

Deque是Queue的子接口,我们知道Queue是一种队列形式,而Deque则是双向队列,它支持从两个端点方向检索和插入元素,因此Deque既可以支持LIFO形式也可以支持LIFO形式.Deque接口是一种比Stack和Vector更为丰富的抽象数据形式,因为它同时实现了以上两者.

添加功能

void push(E) 向队列头部插入一个元素,失败时抛出异常

void addFirst(E) 向队列头部插入一个元素,失败时抛出异常

void addLast(E) 向队列尾部插入一个元素,失败时抛出异常

boolean offerFirst(E) 向队列头部加入一个元素,失败时返回false

boolean offerLast(E) 向队列尾部加入一个元素,失败时返回false

获取功能

E getFirst() 获取队列头部元素,队列为空时抛出异常

E getLast() 获取队列尾部元素,队列为空时抛出异常

E peekFirst() 获取队列头部元素,队列为空时返回null

E peekLast() 获取队列尾部元素,队列为空时返回null

删除功能

boolean removeFirstOccurrence(Object) 删除第一次出现的指定元素,不存在时返回false

boolean removeLastOccurrence(Object) 删除最后一次出现的指定元素,不存在时返回false

弹出功能

E pop() 弹出队列头部元素,队列为空时抛出异常

E removeFirst() 弹出队列头部元素,队列为空时抛出异常

E removeLast() 弹出队列尾部元素,队列为空时抛出异常

E pollFirst() 弹出队列头部元素,队列为空时返回null

E pollLast() 弹出队列尾部元素,队列为空时返回null

迭代器

Iterator<E> descendingIterator() 返回队列反向迭代器

同Queue一样,Deque的实现也可以划分成通用实现和并发实现.

  通用实现主要有两个实现类ArrayDeque和LinkedList.

  ArrayDeque是个可变数组,它是在Java 6之后新添加的,而LinkedList是一种链表结构的list,LinkedList要比ArrayDeque更加灵活,因为它也实现了List接口的所有操作,并且可以插入null元素,这在ArrayDeque中是不允许的.

  从效率来看,ArrayDeque要比LinkedList在两端增删元素上更为高效,因为没有在节点创建删除上的开销.最适合使用LinkedList的情况是迭代队列时删除当前迭代的元素.此外LinkedList可能是在遍历元素时最差的数据结构,并且也LinkedList占用更多的内存,因为LinkedList是通过链表连接其整个队列,它的元素在内存中是随机分布的,需要通过每个节点包含的前后节点的内存地址去访问前后元素.

  总体ArrayDeque要比LinkedList更优越,在大队列的测试上有3倍与LinkedList的性能,最好的是给ArrayDeque一个较大的初始化大小,以避免底层数组扩容时数据拷贝的开销.

  LinkedBlockingDeque是Deque的并发实现,在队列为空的时候,它的takeFirst,takeLast会阻塞等待队列处于可用状态

Stack

Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得 Vector得以被当作堆栈使用。基本的push和pop方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。

栈,是指“LIFO”先进后出的集合容器,最后一个压入的元素是第一个出来的,就好比我们洗碗一样(或者叠罗汉)第一个摆放的碗放在最下面,自然是最后一个拿出来的。Stack是由LinkedList实现的,作为Stack的实现,下面是《java编程思想》给出基本的Stack实现:

peek和pop是返回T类型的对象。peek方法提供栈顶元素,但不删除栈顶,而pop是返回并删除栈顶元素;

ArrayDeque

ArrayDeque类是双端队列的实现类,类的继承结构如下面,继承自AbastractCollection(该类实习了部分集合通用的方法,其实现了Collection接口),其实现的接口Deque接口中定义了双端队列的主要的方法,比如从头删除,从尾部删除,获取头数据,获取尾部数据等等。

1
public class ArrayDeque<E> extends AbstractCollection<E>                           implements Deque<E>, Cloneable, Serializable

ArrayDeque基本特征

就其实现而言,ArrayDeque采用了循环数组的方式来完成双端队列的功能。

  1. 无限的扩展,自动扩展队列大小的。(当然在不会内存溢出的情况下。)

  2. 非线程安全的,不支持并发访问和修改。

  3. 支持fast-fail.

  4. 作为栈使用的话比比栈要快.

  5. 当队列使用比linklist要快。

  6. null元素被禁止使用。

最小初始化容量限制8(必须是2的幂次)

扩容:之所以说该ArrayDeque容量无限制,是因为只要检测到head==tail的时候,就直接调用doubleCapacity方法进行扩容。

删除元素:删除元素的基本思路为确定那一侧的数据少,少的一侧移动元素位置,这样效率相对于不比较更高些,然后,判断head是跨越最大值还是为跨越最大值,继而可以分两种不同的情况进行拷贝。但是该方法比较慢,因为存在数组的拷贝。

获取并删除元素:这里在举个简单点的例子,中间判断是不是null,可以看出该队列不支持null,通过把其值设为null就算是将其删除了。然后head向递增的方向退一位即可。

ArrayDeque和LinkedList是Deque的两个通用实现

ArrayDeque不是线程安全的。

ArrayDeque不可以存取null元素,因为系统根据某个位置是否为null来判断元素的存在。

当作为栈使用时,性能比Stack好;当作为队列使用时,性能比LinkedList好。

1.添加元素 addFirst(E e)在数组前面添加元素 addLast(E e)在数组后面添加元素 offerFirst(E e) 在数组前面添加元素,并返回是否添加成功 offerLast(E e) 在数组后天添加元素,并返回是否添加成功

2.删除元素 removeFirst()删除第一个元素,并返回删除元素的值,如果元素为null,将抛出异常 pollFirst()删除第一个元素,并返回删除元素的值,如果元素为null,将返回null removeLast()删除最后一个元素,并返回删除元素的值,如果为null,将抛出异常 pollLast()删除最后一个元素,并返回删除元素的值,如果为null,将返回null removeFirstOccurrence(Object o) 删除第一次出现的指定元素 removeLastOccurrence(Object o) 删除最后一次出现的指定元素

3.获取元素 getFirst() 获取第一个元素,如果没有将抛出异常 getLast() 获取最后一个元素,如果没有将抛出异常

4.队列操作 add(E e) 在队列尾部添加一个元素 offer(E e) 在队列尾部添加一个元素,并返回是否成功 remove() 删除队列中第一个元素,并返回该元素的值,如果元素为null,将抛出异常(其实底层调用的是removeFirst()) poll() 删除队列中第一个元素,并返回该元素的值,如果元素为null,将返回null(其实调用的是pollFirst()) element() 获取第一个元素,如果没有将抛出异常 peek() 获取第一个元素,如果返回null

5.栈操作 push(E e) 栈顶添加一个元素 pop(E e) 移除栈顶元素,如果栈顶没有元素将抛出异常

6.其他 size() 获取队列中元素个数 isEmpty() 判断队列是否为空 iterator() 迭代器,从前向后迭代 descendingIterator() 迭代器,从后向前迭代 contain(Object o) 判断队列中是否存在该元素 toArray() 转成数组 clear() 清空队列 clone() 克隆(复制)一个新的队列

PriorityQueue

我们知道队列是遵循先进先出(First-In-First-Out)模式的,但有些时候需要在队列中基于优先级处理对象。举个例子,比方说我们有一个每日交易时段生成股票报告的应用程序,需要处理大量数据并且花费很多处理时间。客户向这个应用程序发送请求时,实际上就进入了队列。我们需要首先处理优先客户再处理普通用户。在这种情况下,Java的PriorityQueue(优先队列)会很有帮助。

PriorityQueue类在Java1.5中引入并作为 Java Collections Framework 的一部分。PriorityQueue是基于优先堆的一个无界队列,这个优先队列中的元素可以默认自然排序或者通过提供的Comparator(比较器)在队列实例化的时排序。

优先队列不允许空值,而且不支持non-comparable(不可比较)的对象,比如用户自定义的类。优先队列要求使用Java Comparable和Comparator接口给对象排序,并且在排序时会按照优先级处理其中的元素。

优先队列的头是基于自然排序或者Comparator排序的最小元素。如果有多个对象拥有同样的排序,那么就可能随机地取其中任意一个。当我们获取队列时,返回队列的头对象。

优先队列的大小是不受限制的,但在创建时可以指定初始大小。当我们向优先队列增加元素的时候,队列大小会自动增加。

PriorityQueue是非线程安全的,所以Java提供了PriorityBlockingQueue(实现BlockingQueue接口)用于Java多线程环境。

由于知道PriorityQueue是基于Heap的,当新的元素存储时,会调用siftUpUsingComparator方法

PriorityQueue的逻辑结构是一棵完全二叉树,存储结构其实是一个数组。逻辑结构层次遍历的结果刚好是一个数组。

PriorityQueue优先队列,它逻辑上使用堆结构(完全二叉树)实现,物理上使用动态数组实现,并非像TreeMap一样完全有序,但是如果按照指定方式出队,结果可以是有序的。

这里的堆是一种数据结构而非计算机内存中的堆栈。堆结构在逻辑上是完全二叉树,物理存储上是数组。

完全二叉树并不是堆结构,堆结构是不完全有序的完全二叉树。

BlockingQueue

Java中Queue的最重要的应用大概就是其子类BlockingQueue了。

考虑到生产者消费者模型,我们有多个生产者和多个消费者,生产者不断提供资源给消费者,但如果它们的生产/消费速度不匹配或者不稳定,则会造成大量的生产者闲置/消费者闲置。此时,我们需要使用一个缓冲区来存储资源,即生产者将资源置于缓冲区,而消费者不断地从缓冲区中取用资源,从而减少了闲置和阻塞。

BlockingQueue,阻塞队列,即可视之为一个缓冲区应用于多线程编程之中。当队列为空时,它会阻塞所有消费者线程,而当队列为满时,它会阻塞所有生产者线程。

在queue的基础上,BlockingQueue又添加了以下方法:

put:队列末尾添加一个元素,若队列已满阻塞。

take:移除并返回队列头部元素,若队列已空阻塞。

drainTo:一次性获取所有可用对象,可以用参数指定获取的个数,该操作是原子操作,不需要针对每个元素的获取加锁。

ArrayBlockingQueue

由一个定长数组和两个标识首尾的整型index标识组成,生产者放入数据和消费者取出数据对于ArrayBlockingQueue而言使用了同一个锁(一个私有的ReentrantLock),因而无法实现真正的并行。可以在初始化时除长度参数以外,附加一个boolean类型的变量,用于给其私有的ReentrantLock进行初始化(初始化是否为公平锁,默认为false)。

LinkedBlockingQueue

LinkedBlockingQueue的最大特点是,若没有指定最大容量,其可以视为无界队列(有默认最大容量限制,往往系统资源耗尽也无法达到)。即,不对生产者的行为加以限制,只在队列为空的时候限制消费者的行为。LinkedBlockingQueue采用了读写分离的两个ReentrantLock去控制put和take,因而有了更好的性能(类似读写锁提供读写场景下更好的性能),如下:

1
2
3
4
5
6
7
8
/** Lock held by take, poll, etc */    
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();

ArrayBlockingQueue和LinkedBlockingQueue是最常用的两种阻塞队列。

PriorityBlockingQueue

PriorityBlockingQueue是对PriorityQueue的包装,因而也是一个优先队列。其优先级默认是直接比较,大者先出队,也可以从构造器传入自定义的Comparator。由于PriorityQueue从实现上是一个无界队列,PriorityBlockingQueue同样是一个无界队列,对生产者不做限制。

DelayQueue

DelayQueue是在PriorityBlockingQueue的基础上包装产生的,它用于存放Delayed对象,该队列的头部是延迟期满后保存时间最长的Delayed元素(即,以时间为优先级利用PriorityBlockingQueue),当没有元素延迟期满时,对其进行poll操作将会返回Null。take操作会阻塞。

SynchronousQueue

SynchronousQueue十分特殊,它没有容量——换言之,它是一个长度为0的BlockingQueue,生产者和消费者进行的是无中介的直接交易,当生产者/消费者没有找到合适的目标时,即会发生阻塞。但由于减少了环节,其整体性能在一些系统中可能更加适合。该方法同样支持在构造时确定为公平/默认的非公平模式,如果是非公平模式,有可能会导致某些生产者/消费者饥饿。

WeakHashMap

WeakHashMap是一种改进的HashMap,它对key实行“弱引用”,如果一个key不再被外部所引用,那么该key可以被GC回收。

EnumSet类

EnumSet是一个专门为枚举设计的集合类,EnumSet中的所有元素都必须是指定枚举类型的枚举值,该枚举类型的创建Enumset时显示会隐式的指定。Enumset的集合元素也是有序的,EnumSet以枚举值在Enum类内定义的顺序来决定集合元素的顺序。

使用Java8新增的Predicate操作集合

Java 8为Collection集合新增了removeIf(Predicate filter)方法,该方法将会批量删除符合条件的filter条件的所有元素

使用java 8 新增的Stream操作集合

Java8新增了Stream、IntStream、LongStream、DoubleStream等流式API,这些API代表了多个支持串行和并行聚集操作的元素,其中Stream是一个通用的流接口,而IntStream、LongStream、DoubleStream则代表了类型为int,long,double的流。

独立使用Stream的步骤如下:

1、使用Stream或XxxStream的builder()类方法创建该Stream对应的Builder。

2、重复调用Builder的add()方法向该流中的添加多个元素

3、调用Builder的build()方法获取对应的Stream

4、调用Stream的聚集方法。

在Stream中方法分为两类中间方法和末端方法

中间方法:中间操作允许流保持打开状态,并允许直接调用后续方法。上面程序中的map()方法就是中间方法。

末端方法:末端方法是对流的最终操作。当对某个Stream执行末端方法后,该流将会被”消耗”且不再可用。上面程序中的sum()、count()、average()等方法都是末端方法。

除此之外,关于流的方法还有如下特征:

有状态的方法:这种方法会给你流增加一些新的属性,比如元素的唯一性、元素的最大数量、保证元素的排序的方式被处理等。有状态的方法往往需要更大的性能开销

短路方法:短路方法可以尽早结束对流的操作,不必检查所有的元素。

对CAS的理解,CAS带来的问题,如何解决这些问题?

锁

回答这个问题,可以先介绍一下锁要解决的问题,以及锁机制的缺点。

引入锁就是为了解决多线程竞争同一个资源时,出现脏读、数据不一致问题。一般我们常用的是synchronized等排他锁,

这种锁存在的问题:

1、多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调 度延时,引起性能问题

2、一个线程持有锁会导致其它所有需要此锁的线程挂起直至该锁释放

CAS

cas是另一个无锁解决方案,更准确的是采用乐观锁技术,实现线程安全的问题。cas有三个操作数—-内存对象(V)、预期原值(A)、新值(B)。

CAS原理就是对v对象进行赋值时,先判断原来的值是否为A,如果为A,就把新值B赋值到V对象上面,如果原来的值不是A(代表V的值放生了变化),就不赋新值。

我们看一下AtomicInteger类,AtomicInteger是线程安全的,我们看一下源码

1
2
3
4
5
6
7
8
9
/**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @return the previous value
*/
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}

再看一下unsafe源码

1
2
3
4
5
6
7
8
public final long getAndAddLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

return var6;
}

我们看到do while自循环,这里为什么会有自循环,就是在 判断预期原值 如果与原来的值不符合,会再循环取原值,再走CAS流程,直到能够把新值B赋值成功。

CAS缺点

cas这个方式也存在一定的问题:

1、自循环时间长,开销大

2、只能保证一个共享变量的原子操作

3、ABA问题

什么是ABA问题?

考虑如下操作:

并发1(上):获取出数据的初始值是A,后续计划实施CAS乐观锁,期望数据仍是A的时候,修改才能成功

并发2:将数据修改成B

并发3:将数据修改回A

并发1(下):CAS乐观锁,检测发现初始值还是A,进行数据修改

上述并发环境下,并发1在修改数据时,虽然还是A,但已经不是初始条件的A了,中间发生了A变B,B又变A的变化,此A已经非彼A,数据却成功修改,可能导致错误,这就是CAS引发的所谓的ABA问题。

库存操作,出现ABA问题并不会对业务产生影响。
堆栈操作,会出现ABA的问题。

ABA问题的优化

ABA问题导致的原因,是CAS过程中只简单进行了“值”的校验,再有些情况下,“值”相同不会引入错误的业务逻辑(例如库存),有些情况下,“值”虽然相同,却已经不是原来的数据了。

优化方向:CAS不能只比对“值”,还必须确保的是原来的数据,才能修改成功。

常见实践:“版本号”的比对,一个数据一个版本,版本变化,即使值相同,也不应该修改成功。

一个共享变量的原子操作问题优化

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

volatile底层、synchronized底层、锁升级的过程、MESI

volatile底层

Java语言规范对于volatile定义如下:

Java编程语言允许线程访问共享变量,为了确保共享变量能够被准确和一致性地更新,线程应该确保通过排它锁单独获得这个变量。

首先我们从定义开始入手,官方定义比较拗口。通俗来说就是一个字段被volatile修饰,Java的内存模型确保所有的线程看到的这个变量值是一致的,但是它并不能保证多线程的原子操作。这就是所谓的线程可见性。我们要知道他是不能保证原子性的。

内存模型相关概念

Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的修改何时对另外一个线程可见。JMM定义了线程与主内存的抽象关系:线程之间的变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)保存着共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。
image

如果线程A与线程B通信:

  • 线程A要先把本地内存A中更新过的共享变量刷写到主内存中。
  • 线程B到主内存中读取线程A更新后的共享变量

计算机在运行程序时,每条指令都是在CPU中执行的,在执行过程中势必会涉及到数据的读写。我们知道程序运行的数据是存储在主存中,这时就会有一个问题,读写主存中的数据没有CPU中执行指令的速度快,如果任何的交互都需要与主存打交道则会大大影响效率,所以就有了CPU高速缓存。CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关。

有了CPU高速缓存虽然解决了效率问题,但是它会带来一个新的问题:数据一致性。在程序运行中,会将运行所需要的数据复制一份到CPU高速缓存中,在进行运算时CPU不再也主存打交道,而是直接从高速缓存中读写数据,只有当运行结束后才会将数据刷新到主存中。

举个例子:

i++;

当线程运行这行代码时,首先会从主内存中读取i,然后复制一份到CPU高速缓存中,接着CPU执行+1的操作,再将+1后的数据写在缓存中,最后一步才是刷新到主内存中。在单线程时没有问题,多线程就有问题了。

如下:假如有两个线程A、B都执行这个操作(i++),按照我们正常的逻辑思维主存中的i值应该=3,但事实是这样么?

分析如下:两个线程从主存中读取i的值(1)到各自的高速缓存中,然后线程A执行+1操作并将结果写入高速缓存中,最后写入主存中,此时主存i==2,线程B做同样的操作,主存中的i仍然=2。所以最终结果为2并不是3。这种现象就是缓存一致性问题。

解决缓存一致性方案有两种:

通过在总线加LOCK#锁的方式;

通过缓存一致性协议。

但是方案1存在一个问题,它是采用一种独占的方式来实现的,即总线加LOCK#锁的话,只能有一个CPU能够运行,其他CPU都得阻塞,效率较为低下。

第二种方案,缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。所以JMM就解决这个问题。

volatile实现原理

有volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令,该指令在多核处理器下会引发两件事情。

  • 将当前处理器缓存行数据刷写到系统主内存。
  • 这个刷写回主内存的操作会使其他CPU缓存的该共享变量内存地址的数据无效。

这样就保证了多个处理器的缓存是一致的,对应的处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器缓存行设置无效状态,当处理器对这个数据进行修改操作的时候会重新从主内存中把数据读取到缓存里。

使用场景

volatile经常用于两个场景:状态标记、double check

1、状态标记

1
2
3
4
5
6
7
8
//线程1
boolean stop = false;
while(!stop){
doSomething();
}

//线程2
stop = true;

这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。

下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

但是加上volatile就没问题了。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
volatile boolean flag = false;

while(!flag){
doSomething();
}

public void setFlag() {
flag = true;
}

volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;

//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);

2、double check

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton{
private volatile static Singleton instance = null;

private Singleton() {

}

public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}

synchronized底层

上面有写

锁升级的过程

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率,下文会详细分析。

markword

因为偏向锁,锁住对象时,会写入对象头相应的标识,我们先把对象头(官方叫法为:Mark Word)的图示如下(借用了网友的图片):

image

偏向锁

HotSpot [1] 的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

上文中黑体字部分,写得太简略,以致于很多初学者,对这个过程有点不明白,这个过程是怎么实现锁的升级、释放的?下面一一分析

  1. 线程2来竞争锁对象;
  2. 判断当前对象头是否是偏向锁;
  3. 判断拥有偏向锁的线程1是否还存在;
  4. 线程1不存在,直接设置偏向锁标识为0(线程1执行完毕后,不会主动去释放偏向锁);
  5. 使用cas替换偏向锁线程ID为线程2,锁不升级,仍为偏向锁;
  6. 线程1仍然存在,暂停线程1;
  7. 设置锁标志位为00(变为轻量级锁),偏向锁为0;
  8. 从线程1的空闲monitor record中读取一条,放至线程1的当前monitor record中;
  9. 更新mark word,将mark word指向线程1中monitor record的指针;
  10. 继续执行线程1的代码;
  11. 锁升级为轻量级锁;
  12. 线程2自旋来获取锁对象;

image

轻量级锁

(1)轻量级锁加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
(2)轻量级锁解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。下图是两个线程同时争夺锁,导致锁膨胀的流程图。
image
因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

缓存一致性和MESI

缓存一致性协议给缓存行(通常为64字节)定义了个状态:独占(exclusive)、共享(share)、修改(modified)、失效(invalid),用来描述该缓存行是否被多处理器共享、是否修改。所以缓存一致性协议也称MESI协议。

  • 独占(exclusive):仅当前处理器拥有该缓存行,并且没有修改过,是最新的值。
  • 共享(share):有多个处理器拥有该缓存行,每个处理器都没有修改过缓存,是最新的值。
  • 修改(modified):仅当前处理器拥有该缓存行,并且缓存行被修改过了,一定时间内会写回主存,会写成功状态会变为S。
  • 失效(invalid):缓存行被其他处理器修改过,该值不是最新的值,需要读取主存上最新的值。
    协议协作如下:

  • 一个处于M状态的缓存行,必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回CPU。

  • 一个处于S状态的缓存行,必须时刻监听使该缓存行无效或者独享该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I。
  • 一个处于E状态的缓存行,必须时刻监听其他试图读取该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S。
  • 当CPU需要读取数据时,如果其缓存行的状态是I的,则需要从内存中读取,并把自己状态变成S,如果不是I,则可以直接读取缓存中的值,但在此之前,必须要等待其他CPU的监听结果,如其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存之后,再读取。
  • 当CPU需要写数据时,只有在其缓存行是M或者E的时候才能执行,否则需要发出特殊的RFO指令(Read Or Ownership,这是一种总线事务),通知其他CPU置缓存无效(I),这种情况下会性能开销是相对较大的。在写入完成后,修改其缓存状态为M。

image

这个图的含义就是当一个core持有一个cacheline的状态为Y时,其它core对应的cacheline应该处于状态X, 比如地址 0x00010000 对应的cacheline在core0上为状态M, 则其它所有的core对应于0x00010000的cacheline都必须为I , 0x00010000 对应的cacheline在core0上为状态S, 则其它所有的core对应于0x00010000的cacheline 可以是S或者I ,

另外MESI协议为了提高性能,引入了Store Buffe和Invalidate Queues,还是有可能会引起缓存不一致,还会再引入内存屏障来确保一致性,可以参考[7]和[12]

存储缓存(Store Buffe)

也就是常说的写缓存,当处理器修改缓存时,把新值放到存储缓存中,处理器就可以去干别的事了,把剩下的事交给存储缓存。

失效队列(Invalidate Queues)

处理失效的缓存也不是简单的,需要读取主存。并且存储缓存也不是无限大的,那么当存储缓存满的时候,处理器还是要等待失效响应的。为了解决上面两个问题,引进了失效队列(invalidate queue)。处理失效的工作如下:

收到失效消息时,放到失效队列中去。
为了不让处理器久等失效响应,收到失效消息需要马上回复失效响应。
为了不频繁阻塞处理器,不会马上读主存以及设置缓存为invlid,合适的时候再一块处理失效队列。

MESI和CAS关系

在x86架构上,CAS被翻译为”lock cmpxchg…”,当两个core同时执行针对同一地址的CAS指令时,其实他们是在试图修改每个core自己持有的Cache line

假设两个core都持有相同地址对应cacheline,且各自cacheline 状态为S, 这时如果要想成功修改,就首先需要把S转为E或者M, 则需要向其它core invalidate 这个地址的cacheline,则两个core都会向ring bus发出 invalidate这个操作, 那么在ringbus上就会根据特定的设计协议仲裁是core0,还是core1能赢得这个invalidate, 胜者完成操作, 失败者需要接受结果, invalidate自己对应的cacheline,再读取胜者修改后的值, 回到起点.

对于我们的CAS操作来说, 其实锁并没有消失,只是转嫁到了ring bus的总线仲裁协议中. 而且大量的多核同时针对一个地址的CAS操作会引起反复的互相invalidate 同一cacheline, 造成pingpong效应, 同样会降低性能(参考[9])。当然如果真的有性能问题,我觉得这可能会在ns级别体现了,一般的应用程序中使用CAS应该不会引起性能问题

指令重排和内存屏障

指令重排

现代CPU的速度越来越快,为了充分的利用CPU,在编译器和CPU执行期,都可能对指令重排。举个例子:

1
2
3
LDR R1, [R0];//操作1
ADD R2, R1, R1;//操作2
ADD R3, R4, R4;//操作3

上面这段代码,如果操作1如果发生cache miss,则需要等待读取内存外存。看看有没有能优先执行的指令,操作2依赖于操作1,不能被优先执行,操作3不依赖1和2,所以能优先执行操作3。
JVM的JSR-133规范中定义了as-if-serial语义,即compiler, runtime, and hardware三者需要保证在单线程模型下程序不会感知到指令重排的影响。

在并发模型下,重排序还是可能会引发问题,比较经典的就是“单例模式失效”问题(DoubleCheckedLocking):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private static Singleton instance = null;

private Singleton() { }

public static Singleton getInstance() {
if(instance == null) {
synchronzied(Singleton.class) {
if(instance == null) {
instance = new Singleton(); //
}
}
}
return instance;
}
}

上面这段代码,初看没问题,但是在并发模型下,可能会出错,那是因为instance= new Singleton()并非一个原子操作,它实际上下面这三个操作:

1
2
3
memory =allocate();    //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址

上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:

1
2
3
memory =allocate();    //1:分配对象的内存空间
instance =memory; //3:instance指向刚分配的内存地址,此时对象还未初始化
ctorInstance(memory); //2:初始化对象

可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。在多线程场景下,可能A线程执行到了3,B线程发现已经不为空就返回继续执行,就会出错。

在java里面volatile可以防止重排,当然还有另外一个作用即内存可见性,这个知道的人还应该比较普遍,就不说了

内存屏障

硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。内存屏障有两个作用:

1.阻止屏障两侧的指令重排序;
2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

在JSR规范中定义了4种内存屏障:

  • LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • LoadStore屏障:(指令Load1; LoadStore; Store2),在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:(指令Store1; StoreStore; Store2),在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能
    对于volatile关键字,按照规范会有下面的操作:

  • 在每个volatile写入之前,插入一个StoreStore,写入之后,插入一个StoreLoad

  • 在每个volatile读取之前,插入LoadLoad,之后插入LoadStore
  • 具体到X86来看,其实没那么多指令,只有StoreLoad:

结合上面的【一】和【二】的内容,内存屏障首先阻止了指令的重排,另外也和MESI协议结合,确保了内存的可见性

怎么理解Java 中和 MySQL中的乐观锁、悲观锁?

java

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

乐观锁常见的两种实现方式

乐观锁一般会使用版本号机制或CAS(Compare-and-Swap,即比较并替换)算法实现。

版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

举一个简单的例子: 假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

  1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
  2. 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
  3. 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
  4. 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
    这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。
CAS算法

即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

需要读写的内存值 V
进行比较的值 A
拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

乐观锁的缺点

ABA 问题是乐观锁一个常见的问题。

  1. ABA 问题

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 “ABA”问题。

JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  1. 循环时间长开销大

自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

  1. 只能保证一个共享变量的原子操作

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

CAS与synchronized的使用情景

简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)

  • 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
  • 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
    补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

Mysql

悲观锁(Pessimistic Lock)

悲观锁的特点是先获取锁,再进行业务操作,即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。通常所说的“一锁二查三更新”即指的是使用悲观锁。通常来讲在数据库上的悲观锁需要数据库本身提供支持,即通过常用的select … for update操作来实现悲观锁。当数据库执行select for update时会获取被select中的数据行的行锁,因此其他并发执行的select for update如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。select for update获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。

这里需要注意的一点是不同的数据库对select for update的实现和支持都是有所区别的,例如oracle支持select for update no wait,表示如果拿不到锁立刻报错,而不是等待,mysql就没有no wait这个选项。另外mysql还有个问题是select for update语句执行中所有扫描过的行都会被锁上,这一点很容易造成问题。因此如果在mysql中用悲观锁务必要确定走了索引,而不是全表扫描。

乐观锁(Optimistic Lock)

乐观锁的特点先进行业务操作,不到万不得已不去拿锁。即“乐观”的认为拿锁多半是会成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好。

乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持。一般的做法是在需要锁的数据上增加一个版本号,或者时间戳,然后按照如下方式实现:

1
2
3
4
5
6
7
8
1. SELECT data AS old_data, version AS old_version FROM …;
2. 根据获取的数据进行业务操作,得到new_data和new_version
3. UPDATE SET data = new_data, version = new_version WHERE version = old_version
if (updated row > 0) {
// 乐观锁获取成功,操作完成
} else {
// 乐观锁获取失败,回滚并重试
}

乐观锁是否在事务中其实都是无所谓的,其底层机制是这样:在数据库内部update同一行的时候是不允许并发的,即数据库每次执行一条update语句时会获取被update行的写锁,直到这一行被成功更新后才释放。因此在业务操作进行前获取需要锁的数据的当前版本号,然后实际更新数据时再次对比版本号确认与之前获取的相同,并更新版本号,即可确认这之间没有发生并发的修改。如果更新失败即可认为老版本的数据已经被并发修改掉而不存在了,此时认为获取锁失败,需要回滚整个业务操作并可根据需要重试整个过程。

总结

  • 乐观锁在不发生取锁失败的情况下开销比悲观锁小,但是一旦发生失败回滚开销则比较大,因此适合用在取锁失败概率比较小的场景,可以提升系统并发性能
  • 乐观锁还适用于一些比较特殊的场景,例如在业务操作过程中无法和数据库保持连接等悲观锁无法适用的地方

对线程池的理解,项目中哪个地方使用了,如何使用的,用的Excutor框架中的哪个实现类,为什么用这个

线程池

线程池的作用

线程池作用就是限制系统中执行线程的数量。

根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。

为什么要用线程池?

减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

比较重要的几个类:

名称 作用
ExecutorService 真正的线程池接口。
ScheduledExecutorService 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
ThreadPoolExecutor ExecutorService的默认实现。
ScheduledThreadPoolExecutor 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。

newSingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

newFixedThreadPool

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

newCachedThreadPool

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,

那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

newScheduledThreadPool

创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

ThreadPoolExecutor详解

ThreadPoolExecutor的完整构造方法的签名是:ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) .

  • corePoolSize - 池中所保存的线程数,包括空闲线程。
  • maximumPoolSize-池中允许的最大线程数。
  • keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
  • unit - keepAliveTime 参数的时间单位。
  • workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute方法提交的 Runnable任务。
  • threadFactory - 执行程序创建新线程时使用的工厂。
  • handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
  • ThreadPoolExecutor是Executors类的底层实现。

下面介绍一下几个类的源码:

ExecutorService newFixedThreadPool (int nThreads):固定大小线程池。

可以看到,corePoolSize和maximumPoolSize的大小是一样的(实际上,后面会介绍,如果使用无界queue的话maximumPoolSize参数是没有意义的),keepAliveTime和unit的设值表名什么?-就是该实现不想keep alive!最后的BlockingQueue选择了LinkedBlockingQueue,该queue有一个特点,他是无界的。

1
2
3
4
5

public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

ExecutorService newSingleThreadExecutor():单线程

1
2
3
4
5
6

public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

ExecutorService newCachedThreadPool():无界线程池,可以进行自动线程回收

这个实现就有意思了。首先是无界的线程池,所以我们可以发现maximumPoolSize为big big。其次BlockingQueue的选择上使用SynchronousQueue。可能对于该BlockingQueue有些陌生,简单说:该QUEUE中,每个插入操作必须等待另一个线程的对应移除操作。

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {   
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

先从BlockingQueue<Runnable> workQueue这个入参开始说起。在JDK中,其实已经说得很清楚了,一共有三种类型的queue。

所有BlockingQueue 都可用于传输和保持提交的任务。可以使用此队列与池大小进行交互:

如果运行的线程少于 corePoolSize,则 Executor始终首选添加新的线程,而不进行排队。(如果当前运行的线程小于corePoolSize,则任务根本不会存放,添加到queue中,而是直接抄家伙(thread)开始运行)

如果运行的线程等于或多于 corePoolSize,则 Executor始终首选将请求加入队列,而不添加新的线程。

如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。

queue上的三种类型。

排队有三种通用策略:

直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

有界队列。当使用有限的 maximumPoolSizes时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

BlockingQueue的选择

例子一:使用直接提交策略,也即SynchronousQueue。

首先SynchronousQueue是无界的,也就是说他存数任务的能力是没有限制的,但是由于该Queue本身的特性,在某次添加元素后必须等待其他线程取走后才能继续添加。在这里不是核心线程便是新创建的线程,但是我们试想一样下,下面的场景。

我们使用一下参数构造ThreadPoolExecutor:

1
new ThreadPoolExecutor( 2, 3, 30, TimeUnit.SECONDS,new  SynchronousQueue<Runnable>(),new RecorderThreadFactory("CookieRecorderPool"),new ThreadPoolExecutor.CallerRunsPolicy());
1
new ThreadPoolExecutor(2, 3, 30, TimeUnit.SECONDS,new SynchronousQueue<Runnable>(),new RecorderThreadFactory("CookieRecorderPool"),new ThreadPoolExecutor.CallerRunsPolicy());

当核心线程已经有2个正在运行.

  1. 此时继续来了一个任务(A),根据前面介绍的“如果运行的线程等于或多于 corePoolSize,则 Executor始终首选将请求加入队列,而不添加新的线程。”,所以A被添加到queue中。
  2. 又来了一个任务(B),且核心2个线程还没有忙完,OK,接下来首先尝试1中描述,但是由于使用的SynchronousQueue,所以一定无法加入进去。
  3. 此时便满足了上面提到的“如果无法将请求加入队列,则创建新的线程,除非创建此线程超出maximumPoolSize,在这种情况下,任务将被拒绝。”,所以必然会新建一个线程来运行这个任务。
  4. 暂时还可以,但是如果这三个任务都还没完成,连续来了两个任务,第一个添加入queue中,后一个呢?queue中无法插入,而线程数达到了maximumPoolSize,所以只好执行异常策略了。

所以在使用SynchronousQueue通常要求maximumPoolSize是无界的,这样就可以避免上述情况发生(如果希望限制就直接使用有界队列)。对于使用SynchronousQueue的作用jdk中写的很清楚:此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。

什么意思?如果你的任务A1,A2有内部关联,A1需要先运行,那么先提交A1,再提交A2,当使用SynchronousQueue我们可以保证,A1必定先被执行,在A1么有被执行前,A2不可能添加入queue中。

例子二:使用无界队列策略,即LinkedBlockingQueue

这个就拿newFixedThreadPool来说,根据前文提到的规则:

如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。那么当任务继续增加,会发生什么呢?

如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。OK,此时任务变加入队列之中了,那什么时候才会添加新线程呢?

如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。这里就很有意思了,可能会出现无法加入队列吗?不像SynchronousQueue那样有其自身的特点,对于无界队列来说,总是可以加入的(资源耗尽,当然另当别论)。换句说,永远也不会触发产生新的线程!corePoolSize大小的线程数会一直运行,忙完当前的,就从队列中拿任务开始运行。所以要防止任务疯长,比如任务运行的实行比较长,而添加任务的速度远远超过处理任务的时间,而且还不断增加,不一会儿就爆了。

**例子三:有界队列,使用ArrayBlockingQueue。

这个是最为复杂的使用,所以JDK不推荐使用也有些道理。与上面的相比,最大的特点便是可以防止资源耗尽的情况发生。

举例来说,请看如下构造方法:

1
new ThreadPoolExecutor(2, 4, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2),new RecorderThreadFactory("CookieRecorderPool"), new ThreadPoolExecutor.CallerRunsPolicy());

1
new ThreadPoolExecutor(2, 4, 30, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(2),new RecorderThreadFactory("CookieRecorderPool"),new ThreadPoolExecutor.CallerRunsPolicy());

假设,所有的任务都永远无法执行完。

对于首先来的A,B来说直接运行,接下来,如果来了C,D,他们会被放到queue中,如果接下来再来E,F,则增加线程运行E,F。但是如果再来任务,队列无法再接受了,线程数也到达最大的限制了,所以就会使用拒绝策略来处理。

keepAliveTime

jdk中的解释是:当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。

有点拗口,其实这个不难理解,在使用了“池”的应用中,大多都有类似的参数需要配置。比如数据库连接池,DBCP中的maxIdle,minIdle参数。

什么意思?接着上面的解释,后来向老板派来的工人始终是“借来的”,俗话说“有借就有还”,但这里的问题就是什么时候还了,如果借来的工人刚完成一个任务就还回去,后来发现任务还有,那岂不是又要去借?这一来一往,老板肯定头也大死了。

合理的策略:既然借了,那就多借一会儿。直到“某一段”时间后,发现再也用不到这些工人时,便可以还回去了。这里的某一段时间便是keepAliveTime的含义,TimeUnit为keepAliveTime值的度量。

RejectedExecutionHandler

另一种情况便是,即使向老板借了工人,但是任务还是继续过来,还是忙不过来,这时整个队伍只好拒绝接受了。

RejectedExecutionHandler接口提供了对于拒绝任务的处理的自定方法的机会。在ThreadPoolExecutor中已经默认包含了4中策略,因为源码非常简单,这里直接贴出来。

CallerRunsPolicy:线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。

1
2
3
4
5
6

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}

这个策略显然不想放弃执行任务。但是由于池中已经没有任何资源了,那么就直接使用调用该execute的线程本身来执行。

AbortPolicy:处理程序遭到拒绝将抛出运行时RejectedExecutionException

1
2
3
4

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException();
}

这种策略直接抛出异常,丢弃任务。

DiscardPolicy:不能执行的任务将被删除

1
2
3
4

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {

}

这种策略和AbortPolicy几乎一样,也是丢弃任务,只不过他不抛出异常。

DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)

1
2
3
4
5
6
7

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}

该策略就稍微复杂一些,在pool没有关闭的前提下首先丢掉缓存在队列中的最早的任务,然后重新尝试运行该任务。这个策略需要适当小心。

设想:如果其他线程都还在运行,那么新来任务踢掉旧任务,缓存在queue中,再来一个任务又会踢掉queue中最老任务。

总结

keepAliveTime和maximumPoolSize及BlockingQueue的类型均有关系。如果BlockingQueue是无界的,那么永远不会触发maximumPoolSize,自然keepAliveTime也就没有了意义。

反之,如果核心数较小,有界BlockingQueue数值又较小,同时keepAliveTime又设的很小,如果任务频繁,那么系统就会频繁的申请回收线程。

怎么理解线程安全?

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。

线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据

安全性:
比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。
那好,我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。

一个final修饰的属性,定义的时候没有初始化,在无参构造函数中初始化,可以吗,为什么

static

  1. static修饰一个属性字段,那么这个属性字段将成为类本身的资源,public修饰为共有的,可以在类的外部通过test.a来访问此属性;在类内部任何地方可以使用。如果被修饰为private私有,那么只能在类内部使用。

  2. 如果属性被修饰为static静态类资源,那么这个字段永远只有一个,也就是说不管你new test()多少个类的对象,操作的永远都只是属于类的那一块内存资源。

final

final 用于声明属性、方法和类,分别表示属性一旦被分配内存空间就必须初始化并且以后不可变;方法一旦定义必须有实现代码并且子类里不可被覆盖;类一旦定义不能被定义为抽象类或是接口,因为不可被继承。

被final修饰而没有被static修饰的类的属性变量只能在两种情况下初始化:

  • 在它被定义的时候

    1
    2
    3
    4
    5
    6
    7

    public class Test{
    public final int a=0;
    private Test(){

    }
    }
  • 在构造函数里初始化

    1
    2
    3
    4
    5
    6
    7

    public class Test{
    public final int a;
       private Test(){
       a=0;
       }
    }

同时被final和static修饰的类的属性变量只能在两种情况下初始化

  • 在它被定义的时候

    1
    2
    3
    4
    5
    6
    7

    public class Test{
       public static final int a=0;
       private Test(){
      
       }
    }
  • 在类的静态块里初始化

    1
    2
    3
    4
    5
    6
    public class Test{
       public static final int a;
       static{
       a=0;
       }
    }

当类的属性被同时被修饰为static和final的时候,他属于类的资源,那么就是类在被加载进内存的时候(也就是应用程序启动的时候)就要为属性分配内存,所以此时属性已经存在,它又被final修饰,所以必须在属性定义了以后就给其初始化值。而构造函数是在当类被实例化的时候才会执行,所以不能用构造函数。而static块是类被加载的时候执行,且只执行这一次,所以在static块中可以执行初始化。

HashMap,concurrentHashMap底实现序列化层实现

实现了Serializable接口

Java中 Serializable 是 标示一个类是可以被 JDK 序列化和反序列化的,他只是一个接口并没有任何操作。

序列化,本质就是将内存里面的 Java 对象写入到流里面,还可以将流里面的Java序列化数据反序列化还原到实例对象。

当然,序列化的方法很多,常见的就是 JDK 序列化、JSON 序列化、还有 protobuffer 等等。

每一种框架,序列化和反序列化都是有一个统一的数据格式规范和算法。

什么是红黑树,什么是B-Tree,为什么HashMap中用红黑树不用其他树?

树的概念前面有。

那么很多人就有疑问为什么是使用红黑树而不是AVL树,AVL树是完全平衡二叉树阿?

最主要的一点是:

在CurrentHashMap中是加锁了的,实际上是读写锁,如果写冲突就会等待,
如果插入时间过长必然等待时间更长,而红黑树相对AVL树他的插入更快!

问题:为什么不使用AVL树而使用红黑树?
红黑树和AVL树都是最常用的平衡二叉搜索树,它们的查找、删除、修改都是O(lgn) time

AVL树和红黑树有几点比较和区别:
(1)AVL树是更加严格的平衡,因此可以提供更快的查找速度,一般读取查找密集型任务,适用AVL树。
(2)红黑树更适合于插入修改密集型任务。
(3)通常,AVL树的旋转比红黑树的旋转更加难以平衡和调试。

总结:

(1)AVL以及红黑树是高度平衡的树数据结构。它们非常相似,真正的区别在于在任何添加/删除操作时完成的旋转操作次数。

(2)两种实现都缩放为a O(lg N),其中N是叶子的数量,但实际上AVL树在查找密集型任务上更快:利用更好的平衡,树遍历平均更短。另一方面,插入和删除方面,AVL树速度较慢:需要更高的旋转次数才能在修改时正确地重新平衡数据结构。

(3)在AVL树中,从根到任何叶子的最短路径和最长路径之间的差异最多为1。在红黑树中,差异可以是2倍。

(4)两个都给O(log n)查找,但平衡AVL树可能需要O(log n)旋转,而红黑树将需要最多两次旋转使其达到平衡(尽管可能需要检查O(log n)节点以确定旋转的位置)。旋转本身是O(1)操作,因为你只是移动指针。

计算密集型/IO密集型任务分别如何设置线程池的核心线程数和最大线程数,为什么这么设置?

任务类型举例:

CPU密集型:

例如,一般我们系统的静态资源,比如js,css等,会存在一个版本号,如 main.js?v0,每当用户访问这个资源的时候,会发送一个比对请求到服务端,比对本地静态文件版本和服务端的文件版本是否一致,不一致则更新.这种任务一般不占用大量IO,所以后台服务器可以快速处理,压力落在CPU上.

I/O密集型:

比方说近期我们做的万科CRM系统,常有大数据量的查询和批量插入操作,此时的压力主要在I/O上.

线程数与任务类型的关系:

与CPU密集型的关系:

一般情况下,CPU核心数 == 最大同时执行线程数.在这种情况下(设CPU核心数为n),大量客户端会发送请求到服务器,但是服务器最多只能同时执行n个线程.

设线程池工作队列长度为m,且m>>n,则此时会导致CPU频繁切换线程来执行(如果CPU使用的是FCFS,则不会频繁切换,如使用的是其他CPU调度算法,如时间片轮转法,最短时间优先,则可能会导致频繁的线程切换).

所以这种情况下,无需设置过大的线程池工作队列,(工作队列长度 = CPU核心数 || CPU核心数+1) 即可.

与I/O密集型的关系:

1个线程对应1个方法栈,线程的生命周期与方法栈相同.

比如某个线程的方法栈对应的入站顺序为:controller()->service()->DAO(),由于DAO长时间的I/O操作,导致该线程一直处于工作队列,但它又不占用CPU,则此时有1个CPU是处于空闲状态的.

所以,这种情况下,应该加大线程池工作队列的长度(如果CPU调度算法使用的是FCFS,则无法切换),尽量不让CPU空闲下来,提高CPU利用率.

画一下Java 线程几个状态及状态之间互相转换的图?

image

在Java中线程的状态一共被分成6种:

初始态:NEW

创建一个Thread对象,但还未调用start()启动线程时,线程处于初始态。

运行态:RUNNABLE

在Java中,运行态包括就绪态 和 运行态。

  • 就绪态
    • 该状态下的线程已经获得执行所需的所有资源,只要CPU分配执行权就能运行。
    • 所有就绪态的线程存放在就绪队列中。
  • 运行态
    • 获得CPU执行权,正在执行的线程。
    • 由于一个CPU同一时刻只能执行一条线程,因此每个CPU每个时刻只有一条运行态的线程。

阻塞态

  • 当一条正在执行的线程请求某一资源失败时,就会进入阻塞态。
  • 而在Java中,阻塞态专指请求锁失败时进入的状态。
  • 由一个阻塞队列存放所有阻塞态的线程。
  • 处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。

PS:锁、IO、Socket等都资源。

等待态

  • 当前线程中调用wait、join、park函数时,当前线程就会进入等待态。
  • 也有一个等待队列存放所有等待态的线程。
  • 线程处于等待态表示它需要等待其他线程的指示才能继续运行。
  • 进入等待态的线程会释放CPU执行权,并释放资源(如:锁)

终止态

线程执行结束后的状态。

注意

  • wait()方法会释放CPU执行权 和 占有的锁。
  • sleep(long)方法仅释放CPU使用权,锁仍然占用;线程被放入超时等待队列,与yield相比,它会使线程较长时间得不到运行。
  • yield()方法仅释放CPU执行权,锁仍然占用,线程会被放入就绪队列,会在短时间内再次执行。
  • wait和notify必须配套使用,即必须使用同一把锁调用;
  • wait和notify必须放在一个同步块中
  • 调用wait和notify的对象必须是他们所处同步块的锁对象。

对线程池的理解,在项目中如何使用的,多个线程之间如何共享数据,多个进程之间如何共享数据?

首先想到的是将共享数据设置为全局变量,并且用static修饰,但是static修饰的变量是类变量,生命周期太长了,占用内存。

方法一:多个线程对共享数据的操作是相同的,那么创建
一个Runnable的子类对象,将这个对象作为参数传递给Thread的构造方法,此时因为多个线程操作的是同一个Runnable的子类对象,所以他们操作的是同一个共享数据。比如:买票系统,所以的线程的操作都是对票数减一的操作。

方法二:多个线程对共享数据的操作是不同的,将共享数据和操作共享数据的方法放在同一对象中,将这个对象作为参数传递给Runnable的子类,在子类中用该对象的方法对共享数据进行操作。如:生产者消费者。

方法三:多个线程对共享数据的操作是不同的, 用内部类的方式去实现,创建Runnable的子类作为内部类,将共享对象作为全局变量,在Runnable的子类中对共享数据进行操作。

方法四:ThreadLocal实现线程范围内数据的共享

设计模式

怎么理解命令模式和观察者模式,手写一个观察者模式或者命令模式的代码,策略模式也行

  • 命令模式(Command Pattern)是一种数据驱动的设计模式,它属于行为型模式。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。

    • 意图:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。
    • 主要解决:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。
    • 何时使用:在某些场合,比如要对行为进行”记录、撤销/重做、事务”等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将”行为请求者”与”行为实现者”解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。
    • 如何解决:通过调用者调用接受者执行命令,顺序:调用者→接受者→命令。
    • 应用实例:struts 1 中的 action 核心控制器 ActionServlet 只有一个,相当于 Invoker,而模型层的类会随着不同的应用有不同的模型类,相当于具体的 Command。
    • 优点: 1、降低了系统耦合度。 2、新的命令可以很容易添加到系统中去。
    • 缺点:使用命令模式可能会导致某些系统有过多的具体命令类。
    • 使用场景:认为是命令的地方都可以使用命令模式,比如: 1、GUI 中每一个按钮都是一条命令。 2、模拟 CMD。
    • 注意事项:系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作,也可以考虑使用命令模式,见命令模式的扩展。
    • 我们首先创建作为命令的接口 Order,然后创建作为请求的 Stock 类。实体命令类 BuyStock 和 SellStock,实现了 Order 接口,将执行实际的命令处理。创建作为调用对象的类 Broker,它接受订单并能下订单。Broker 对象使用命令模式,基于命令的类型确定哪个对象执行哪个命令。CommandPatternDemo,我们的演示类使用 Broker 类来演示命令模式。
      image
      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
      public interface Order {
      void execute();
      }

      public class Stock {

      private String name = "ABC";
      private int quantity = 10;

      public void buy(){
      System.out.println("Stock [ Name: "+name+",
      Quantity: " + quantity +" ] bought");
      }
      public void sell(){
      System.out.println("Stock [ Name: "+name+",
      Quantity: " + quantity +" ] sold");
      }
      }

      public class BuyStock implements Order {
      private Stock abcStock;

      public BuyStock(Stock abcStock){
      this.abcStock = abcStock;
      }

      public void execute() {
      abcStock.buy();
      }
      }

      public class SellStock implements Order {
      private Stock abcStock;

      public SellStock(Stock abcStock){
      this.abcStock = abcStock;
      }

      public void execute() {
      abcStock.sell();
      }
      }

      import java.util.ArrayList;
      import java.util.List;

      public class Broker {
      private List<Order> orderList = new ArrayList<Order>();

      public void takeOrder(Order order){
      orderList.add(order);
      }

      public void placeOrders(){
      for (Order order : orderList) {
      order.execute();
      }
      orderList.clear();
      }
      }

      public class CommandPatternDemo {
      public static void main(String[] args) {
      Stock abcStock = new Stock();

      BuyStock buyStockOrder = new BuyStock(abcStock);
      SellStock sellStockOrder = new SellStock(abcStock);

      Broker broker = new Broker();
      broker.takeOrder(buyStockOrder);
      broker.takeOrder(sellStockOrder);

      broker.placeOrders();
      }
      }

      Stock [ Name: ABC, Quantity: 10 ] bought
      Stock [ Name: ABC, Quantity: 10 ] sold
  • 观察者模式,当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知它的依赖对象。观察者模式属于行为型模式。

    • 意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
    • 主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。
    • 何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
    • 如何解决:使用面向对象技术,可以将这种依赖关系弱化。
    • 关键代码:在抽象类里有一个 ArrayList 存放观察者们。
    • 应用实例:1、拍卖的时候,拍卖师观察最高标价,然后通知给其他竞价者竞价。2、西游记里面悟空请求菩萨降服红孩儿,菩萨洒了一地水招来一个老乌龟,这个乌龟就是观察者,他观察菩萨洒水这个动作。
    • 优点: 1、观察者和被观察者是抽象耦合的。 2、建立一套触发机制。
    • 缺点:1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。 2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
    • 使用场景:1、一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。2、一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。3、一个对象必须通知其他对象,而并不知道这些对象是谁。4、需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。
    • 实现:观察者模式使用三个类 Subject、Observer 和 Client。Subject 对象带有绑定观察者到 Client 对象和从 Client 对象解绑观察者的方法。我们创建 Subject 类、Observer 抽象类和扩展了抽象类 Observer 的实体类。ObserverPatternDemo,我们的演示类使用 Subject 和实体类对象来演示观察者模式。
      image
      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
      import java.util.ArrayList;
      import java.util.List;

      public class Subject {

      private List<Observer> observers
      = new ArrayList<Observer>();
      private int state;

      public int getState() {
      return state;
      }

      public void setState(int state) {
      this.state = state;
      notifyAllObservers();
      }

      public void attach(Observer observer){
      observers.add(observer);
      }

      public void notifyAllObservers(){
      for (Observer observer : observers) {
      observer.update();
      }
      }
      }

      public abstract class Observer {
      protected Subject subject;
      public abstract void update();
      }

      public class BinaryObserver extends Observer{

      public BinaryObserver(Subject subject){
      this.subject = subject;
      this.subject.attach(this);
      }

      @Override
      public void update() {
      System.out.println( "Binary String: "
      + Integer.toBinaryString( subject.getState() ) );
      }
      }

      public class OctalObserver extends Observer{

      public OctalObserver(Subject subject){
      this.subject = subject;
      this.subject.attach(this);
      }

      @Override
      public void update() {
      System.out.println( "Octal String: "
      + Integer.toOctalString( subject.getState() ) );
      }
      }

      public class HexaObserver extends Observer{

      public HexaObserver(Subject subject){
      this.subject = subject;
      this.subject.attach(this);
      }

      @Override
      public void update() {
      System.out.println( "Hex String: "
      + Integer.toHexString( subject.getState() ).toUpperCase() );
      }
      }

      public class ObserverPatternDemo {
      public static void main(String[] args) {
      Subject subject = new Subject();

      new HexaObserver(subject);
      new OctalObserver(subject);
      new BinaryObserver(subject);

      System.out.println("First state change: 15");
      subject.setState(15);
      System.out.println("Second state change: 10");
      subject.setState(10);
      }
      }
  • 策略模式:在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。

    • 意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
    • 主要解决:在有多种算法相似的情况下,使用 if…else 所带来的复杂和难以维护。
    • 何时使用:一个系统有许多许多类,而区分它们的只是他们直接的行为。
    • 如何解决:将这些算法封装成一个一个的类,任意地替换。
    • 应用实例:1、诸葛亮的锦囊妙计,每一个锦囊就是一个策略。 2、旅行的出游方式,选择骑自行车、坐汽车,每一种旅行方式都是一个策略。 3、JAVA AWT中的LayoutManager。
    • 优点: 1、算法可以自由切换。2、避免使用多重条件判断。3、扩展性良好。
    • 缺点:1、策略类会增多。2、所有策略类都需要对外暴露。
    • 使用场景:1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。2、一个系统需要动态地在几种算法中选择一种。3、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
    • 注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。
    • 实现:我们将创建一个定义活动的 Strategy 接口和实现了 Strategy 接口的实体策略类。Context 是一个使用了某种策略的类。StrategyPatternDemo,我们的演示类使用 Context 和策略对象来演示 Context 在它所配置或使用的策略改变时的行为变化。
      image
      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
      public interface Strategy {
      public int doOperation(int num1, int num2);
      }

      public class OperationAdd implements Strategy{
      @Override
      public int doOperation(int num1, int num2) {
      return num1 + num2;
      }
      }

      public class OperationSubstract implements Strategy{
      @Override
      public int doOperation(int num1, int num2) {
      return num1 - num2;
      }
      }

      public class OperationMultiply implements Strategy{
      @Override
      public int doOperation(int num1, int num2) {
      return num1 * num2;
      }
      }

      public class Context {
      private Strategy strategy;

      public Context(Strategy strategy){
      this.strategy = strategy;
      }

      public int executeStrategy(int num1, int num2){
      return strategy.doOperation(num1, num2);
      }
      }

      public class StrategyPatternDemo {
      public static void main(String[] args) {
      Context context = new Context(new OperationAdd());
      System.out.println("10 + 5 = " + context.executeStrategy(10, 5));

      context = new Context(new OperationSubstract());
      System.out.println("10 - 5 = " + context.executeStrategy(10, 5));

      context = new Context(new OperationMultiply());
      System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
      }
      }

设计模式在项目中哪个地方用到了,怎么使用的,能不能画一个你熟悉的设计模式的UML图,手写单例模式,手写静态内部类实现的单例模式。

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {  

private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

private Singleton (){}

public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

掌握哪些设计模式,常用哪些,项目中如何使用的,为什么用这个,不用那个?手写一个线程安全的单例模式

  • 设计模式:工厂模式、抽象工厂模式、单例模式、建造者模式、原型模式、适配器模式、桥接模式、过滤器模式、组合模式、装饰器模式、外观模式、享元模式、代理模式、责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、空对象模式、策略模式、模板模式、访问者模式、MVC模式、业务代表模式、组合实体模式、数据访问对象模式、前端控制器模式、拦截过滤器模式、服务定位器模式、传输对象模式。
  • 线程安全的单例模式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class Singleton {  
    private volatile static Singleton singleton;

    private Singleton (){}

    public static Singleton getSingleton() {
    if (singleton == null) {
    synchronized (Singleton.class) {
    if (singleton == null) {
    singleton = new Singleton();
    }
    }
    }
    return singleton;
    }
    }

Spring Data Redis与Redisson对比

发表于 2019-03-26 | 分类于 Java |

Spring Data Redis与Redisson对比

Spring Data Redis

Spring Data Redis是更大的Spring Data系列的一部分,可以从Spring应用程序轻松配置和访问Redis。它提供了与商店交互的低级和高级抽象,使用户免于基础设施问题。Spring Boot 从 2.0版本开始,将默认的Redis客户端Jedis替换问Lettuce。

特性

  • 连接包作为多个Redis驱动程序/连接器的低级抽象(Jedis和Lettuce。不推荐支持JRedis和SRP。)
  • 异常转换到Spring的便携式数据访问异常层次结构Redis的驱动程序例外
  • RedisTemplate,提供高级抽象,用于执行各种Redis操作,异常转换和序列化支持
  • Pubsub支持(例如消息驱动的POJO的MessageListenerContainer)
  • Redis Sentinel和Redis Cluster支持
  • JDK,String,JSON和Spring Object / XML映射序列化程序
  • 在Redis之上的JDK Collection实现
  • 原子计数器支持classes
  • 排序和流水线功能
  • 专门支持SORT,SORT / GET模式和返回的批量值
  • Redis 实现了Spring 3.1缓存抽象
  • 自动实现Repository接口,包括支持自定义查找程序方法@EnableRedisRepositories
  • CDI对存储库的支持

使用

在pom.xml中加入

1
2
3
4
5
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.1.5.RELEASE</version>
</dependency>

在application.yml中加入

1
2
3
4
5
6
7
8
9
10
11
12
spring:
redis:
database: 6 #Redis索引0~15,默认为0
host: 127.0.0.1
port: 6379
password: #密码(默认为空)
pool:
max-active: 8 #连接池最大连接数(使用负值表示没有限制)
max-wait: -1ms #连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 5 #连接池中的最大空闲连接
min-idle: 0 #连接池中的最小空闲连接
timeout: 10000ms #连接超时时间(毫秒)

加入配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@EnableCaching
public class RedisConfiguration extends CachingConfigurerSupport {

/**
* RedisTemplate配置
*
* @param redisConnectionFactory redisConnectionFactory
* @return RedisTemplate
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 配置redisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//key序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());
//value序列化
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}

}

代码使用

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

@Autowired
private RedisTemplate redisTemplate;

@Autowired
private RedisConnectionFactory redisConnectionFactory;

@SuppressWarnings("unchecked")
public void test(){
//设置键值对
redisTemplate.opsForValue().set("test:set1", "testValue1");
//设置键值对数组
redisTemplate.opsForSet().add("test:set2", "asdf");
//数据hash存入
redisTemplate.opsForHash().put("hash1", "name1", "lms1");
redisTemplate.opsForHash().put("hash1", "name2", "lms2");
redisTemplate.opsForHash().put("hash1", "name3", "lms3");
//获取
System.out.println(redisTemplate.opsForValue().get("test:set"));
//hash获取
System.out.println(redisTemplate.opsForHash().get("hash1", "name1"));
//发布、订阅消息(更多请参考 https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/
String message = "dinghuang123@gmail.com";
byte[] msg = message.getBytes();
byte[] channel = message.getBytes();
redisConnectionFactory.getConnection().publish(msg, channel);
redisTemplate.convertAndSend("hello!", "world");
}

如图所示

Redisson

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。能够完美的在云计算环境里使用,并且支持AWS ElastiCache主备版,AWS ElastiCache集群版,Azure Redis Cache和阿里云(Aliyun)的云数据库Redis版。Redisson底层采用的是Netty 框架。支持Redis 2.8以上版本,支持Java1.6+以上版本。

Redisson作为独立节点 可以用于独立执行其他节点发布到分布式执行服务 和 分布式调度任务服务 里的远程任务。

特性

  • 复制的Redis服务器模式(还支持AWS ElastiCache和Azure Redis缓存):
    • 自动主服务器更改发现
  • 群集Redis服务器模式(还支持AWS ElastiCache和Azure Redis缓存:
    • 自动主从服务器发现
    • 自动状态和拓扑更新
    • 自动插槽更改发现
  • Sentinel Redis服务器模式:
    • 自动主,从和服务器发现
    • 自动状态和拓扑更新
  • 掌握Slave Redis服务器模式
  • 单Redis服务器模式
  • 线程安全的实现
  • Reactive Streams API
  • 异步 API
  • 异步连接池
  • Lua脚本
  • 分布式Java对象
    Object holder,Binary stream holder,Geospatial holder,BitSet,AtomicLong,AtomicDouble,PublishSubscribe,Bloom filter,HyperLogLog
  • 分布式Java集合
    Map,Multimap,Set,List,SortedSet,ScoredSortedSet,LexSortedSet,Queue,Deque,Blocking Queue,Bounded Blocking Queue,Blocking Deque,Delayed Queue,Priority Queue,Priority Deque
  • 分布式Java锁和同步器
    Lock,FairLock,MultiLock,RedLock,ReadWriteLock,Semaphore,PermitExpirableSemaphore,CountDownLatch
  • 分布式服务
    远程服务,Live Object服务,Executor服务,Scheduler服务,MapReduce服务
  • Spring框架
  • Spring Cache实现
    - Spring Transaction API实现
  • Spring Data Redis集成
  • Spring Boot Starter实现
  • Hibernate Cache实现
  • Transactions API
    - XA Transaction API实现
  • JCache API(JSR-107)实现
  • Tomcat会话管理器实现
  • Spring Session实现
  • Redis流水线(命令批处理)
  • 支持Android平台
  • 支持自动重新连接
  • 支持无法发送命令自动重试
  • 支持OSGi
  • 支持SSL
  • 支持许多流行的编解码器(Jackson JSON,Avro,Smile,CBOR,MsgPack,Kryo,Amazon Ion,FST,LZ4,Snappy和JDK Serialization)
  • 超过1800个单元测试

与spring-data-redis结合使用

pom.xml加入依赖

1
2
3
4
5
6
7
8
9
10
 <dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-21</artifactId>
<version>3.10.5</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.1.5.RELEASE</version>
</dependency>

在resources文件夹添加配置文件redisson.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
#Redisson配置
singleServerConfig:
address: "redis://127.0.0.1:6379"
password: null
clientName: null
database: 7 #选择使用哪个数据库0~15
idleConnectionTimeout: 10000
pingTimeout: 1000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
reconnectionTimeout: 3000
failedAttempts: 3
subscriptionsPerConnection: 5
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
connectionMinimumIdleSize: 32
connectionPoolSize: 64
dnsMonitoringInterval: 5000
#dnsMonitoring: false

threads: 0
nettyThreads: 0
codec:
class: "org.redisson.codec.JsonJacksonCodec"
transportMode: "NIO"

注册RedissonConnectionFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class RedissonSpringDataConfig {

@Bean
public RedissonConnectionFactory redissonConnectionFactory(RedissonClient redisson) {
return new RedissonConnectionFactory(redisson);
}

@Bean(destroyMethod = "shutdown")
public RedissonClient redisson(@Value("classpath:/redisson.yml") Resource configFile) throws IOException {
Config config = Config.fromYAML(configFile.getInputStream());
return Redisson.create(config);
}

}

代码使用

1
2
3
4
5
6
7
8
9
@Autowired
private RedissonClient redissonClient;

@SuppressWarnings("unchecked")
public void test(){
//设置键值对
RBucket<String> keyObj = redissonClient.getBucket("k1");
keyObj.set("v1236");
}

结论

spring-data-redis 支持的基本能够满足对redis的操作,提供了2种客户端连接,也支持redis集群的模式。如果涉及到利用redis做分布式锁的话,redisson封装了更多的工具和基础原子对象进行操作,redisson是优先选择,其次redisson兼容了很多的框架,那么多star不是没有道理的= =。同时也可以通过redisson与RxJava结合,实现线程安全的异步任务等等。

Servicecomb实践

发表于 2019-03-20 | 分类于 Java |

Servicecomb实践

Git地址

Apache ServiceComb Pack 是华为开源的一个微服务应用的数据最终一致性解决方案。

关键特性

  • 高可用:支持高可用的集群模式部署。
  • 高可靠:所有的关键事务事件都持久化存储在数据库中。
  • 高性能:事务事件是通过高性能gRPC来上报的,且事务的请求和响应消息都是通过Kyro进行序列化和反序列化。
  • 低侵入:仅需2-3个注解和编写对应的补偿方法即可引入分布式事务。
  • 部署简单:支持通过容器(Docker)进行快速部署和交付。
  • 补偿机制灵活:支持前向恢复(重试)及后向恢复(补偿)功能。
  • 扩展简单:基于Pack架构很容实现多种协调协议,目前支持TCC、Saga协议,未来还可以添加其他协议支持。

架构

ServiceComb Pack 架构是由 alpha 和 omega组成,其中:

  • alpha充当协调者的角色,主要负责对事务进行管理和协调。
  • omega是微服务中内嵌的一个agent,负责对调用请求进行拦截并向alpha上报事务事件。

下图展示了alpha, omega以及微服务三者的关系:

基础上我们除了实现saga协调协议以外,还实现了TCC协调协议。 详情可浏览ServiceComb Pack 设计文档。

Omega内部运行机制

omega是微服务中内嵌的一个agent。当服务收到请求时,omega会将其拦截并从中提取请求信息中的全局事务id作为其自身的全局事务id(即Saga事件id),并提取本地事务id作为其父事务id。在预处理阶段,alpha会记录事务开始的事件;在后处理阶段,alpha会记录事务结束的事件。因此,每个成功的子事务都有一一对应的开始及结束事件。

服务间通信流程

服务间通信的流程与Zipkin的类似。在服务生产方,omega会拦截请求中事务相关的id来提取事务的上下文。在服务消费方,omega会在请求中注入事务相关的id来传递事务的上下文。通过服务提供方和服务消费方的这种协作处理,子事务能连接起来形成一个完整的全局事务。

Saga 具体处理流程

Saga处理场景是要求相关的子事务提供事务处理函数同时也提供补偿函数。Saga协调器alpha会根据事务的执行情况向omega发送相关的指令,确定是否向前重试或者向后恢复。

成功场景

成功场景下,每个事务都会有开始和有对应的结束事件。

异常场景

异常场景下,omega会向alpha上报中断事件,然后alpha会向该全局事务的其它已完成的子事务发送补偿指令,确保最终所有的子事务要么都成功,要么都回滚。

超时场景 (需要调整)

超时场景下,已超时的事件会被alpha的定期扫描器检测出来,与此同时,该超时事务对应的全局事务也会被中断。

TCC 具体处理流程

TCC(try-confirm-cancel)与Saga事务处理方式相比多了一个Try方法。事务调用的发起方来根据事务的执行情况协调相关各方进行提交事务或者回滚事务。

成功场景

成功场景下, 每个事务都会有开始和对应的结束事件

异常场景

异常场景下,事务发起方会向alpha上报异常事件,然后alpha会向该全局事务的其它已完成的子事务发送补偿指令,确保最终所有的子事务要么都成功,要么都回滚。

omega、alpha的TSL双向证书

Saga 现在支持在omega和alpha服务之间采用 TLS 通信.同样客户端方面的认证(双向认证)。

准备证书 (Certificates)

你可以用下面的命令去生成一个用于测试的自签名的证书。 如果你想采用双向认证的方式,只需要客户端证书。

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
# Changes these CN's to match your hosts in your environment if needed.
SERVER_CN=localhost
CLIENT_CN=localhost # Used when doing mutual TLS

echo Generate CA key:
openssl genrsa -passout pass:1111 -des3 -out ca.key 4096
echo Generate CA certificate:
# Generates ca.crt which is the trustCertCollectionFile
openssl req -passin pass:1111 -new -x509 -days 365 -key ca.key -out ca.crt -subj "/CN=${SERVER_CN}"
echo Generate server key:
openssl genrsa -passout pass:1111 -des3 -out server.key 4096
echo Generate server signing request:
openssl req -passin pass:1111 -new -key server.key -out server.csr -subj "/CN=${SERVER_CN}"
echo Self-signed server certificate:
# Generates server.crt which is the certChainFile for the server
openssl x509 -req -passin pass:1111 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt
echo Remove passphrase from server key:
openssl rsa -passin pass:1111 -in server.key -out server.key
echo Generate client key
openssl genrsa -passout pass:1111 -des3 -out client.key 4096
echo Generate client signing request:
openssl req -passin pass:1111 -new -key client.key -out client.csr -subj "/CN=${CLIENT_CN}"
echo Self-signed client certificate:
# Generates client.crt which is the clientCertChainFile for the client (need for mutual TLS only)
openssl x509 -passin pass:1111 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt
echo Remove passphrase from client key:
openssl rsa -passin pass:1111 -in client.key -out client.key
echo Converting the private keys to X.509:
# Generates client.pem which is the clientPrivateKeyFile for the Client (needed for mutual TLS only)
openssl pkcs8 -topk8 -nocrypt -in client.key -out client.pem
# Generates server.pem which is the privateKeyFile for the Server
openssl pkcs8 -topk8 -nocrypt -in server.key -out server.pem

TLS为Alpha服务开启TLS

1.为alpha-server修改application.yaml文件,在alpha.server部门增加ssl配置。

1
2
3
4
5
6
7
8
alpha:
server:
ssl:
enable: true
cert: server.crt
key: server.pem
mutualAuth: true
clientCert: client.crt

  1. 将server.crt 和 server.pem 文件放到alpha-server的root 2目录。如果你想双向认证,合并所有client证书到一个client.crt文件,并把client.crt文件放到root目录.
  2. 重新启动alpha服务器.

为Omega启用TLS

  1. 获取CA证书串(chain), 如果你是将alpha服务运行在集群中,你可能需要去合并多个CA证书到一个文件中.
  2. 为客户端应用修改application.yaml文件, 在alpha.cluster 部分增加ssl配置.
1
2
3
4
5
6
7
8
9
alpha:
cluster:
address: alpha-server.servicecomb.io:8080
ssl:
enable: false
certChain: ca.crt
mutualAuth: false
cert: client.crt
key: client.pem
  1. 把ca.crt文件放到客户端应用程序的root目录 file under the client application root directory.如果你想用双向认证,仍需要把client.crt和client.pem放到root目录下.
  2. 重新启动客户端应用程序.

与Spring结合使用

Saga中的Event简介

1
2
3
4
5
6
7
8
public enum EventType {
SagaStartedEvent,
TxStartedEvent,
TxEndedEvent,
TxAbortedEvent,
TxCompensatedEvent,
SagaEndedEvent
}
  • SagaStartedEvent: 代表Saga事务的开始,Alpha接受到该事件会保存整个saga事务的执行上下文,其中包括多个本地事务/补偿请求
  • TxStartedEvent: 本地事务开始事件,其中包含了本地事务执行的上下文(调用方法名,以及相关调用参数)
  • TXEndedEvent: 本地事务结束事件
  • TxAbortedEvent: 本地事务执行失败事件,包含了事务执行失败的原因
  • TxCompensatedEvent: 本地事务补偿事件,Alpha会将本地事务执行的上下文传递给Omega,这样不需要Omega自己维护服务调用的状态。
  • SagaEndedEvent: 标志着saga事务请求的结束


成功场景下,全局事务事件SagaStartedEvent对应SagaEndedEvent ,每个子事务开始的事件TxStartedEvent都会有对应的结束事件TXEndedEvent。

异常场景下,Omega会向Alpha上报中断事件TxAbortedEvent,然后Alpha会根据全局事务的执行情况, 想其它已成功的子事务(以完成TXEndedEvent)的服务发送补偿指令,以确保最终所有的子事务要么都成功,要么都回滚。

超时场景下,已超时的事件会被alpha的定期扫描器检测出来,同时该超时事务对应的全局事务也会被中断。

  1. 用户发送Request请求调用业务方法(business logic)
  2. preIntercept向alpha发送TxStartedEvent
  3. 被AOP拦截的方法(business logic)被调用
  4. 当执行成功时postIntercept发送TxEndedEvent到alpha
  5. 最后业务方法向用户发送response

与Spring和Mysql结合使用

项目地址

通过源码编译,克隆代码

1
git clone https://github.com/apache/servicecomb-pack.git

在alpha/alpha-server/pom.xml文件中加入mysql依赖

1
2
3
4
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

构建docker镜像

1
2
cd ./servicecomb-pack
mvn clean install -DskipTests -Pdocker

成功后如图所示

1
2
3
4
[@dinghuangMacPro:~]$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
pack-web 0.3.0 77dedfe8e865 15 seconds ago 131MB
alpha-server 0.3.0 3a34b8cd4224 38 seconds ago 144MB

启动mysql镜像,如果本地有的话
创建库saga,用户saga,密码password,并执行数据库脚本schema-mysql.sql

启动alpha-server

1
docker run -d -p 8080:8080 -p 8090:8090 --link mysql:mysql.servicecomb.io -e JAVA_OPTS=-Dspring.profiles.active=mysql -e -Dspring.datasource.url=jdbc:mysql://127.0.0.1:3306/saga?useSSL=false alpha-server:0.3.0

启动对应的3个应用,分别说shop,order,hotel

访问api

1
curl -X POST http://127.0.0.1:8081/shop/userName/orderName/hotelName

事务解析

请求流程示意图:用户发起请求到shop,shop分别调用order和hotel。

使用TCC模式,TCC原理图如图所示:

情况一:正常事务结束




事务记录成功,订单酒店表都有数据。

情况二:父事件中调用订单成功后,出现异常

1
2
3
4
5
6
7
8
9
10
11
12
//父事务
@TccStart(timeout = 2)
@PostMapping("/shop_tcc/{name}/{order}/{hotel}")
public String shopTcc(@PathVariable String name, @PathVariable String order, @PathVariable String hotel) {
//调用订单服务的请求
template.postForEntity("http://127.0.0.1:8082/order_tcc/{name}/{order}",null, String.class, name, order);
//异常
postBooking();
//调用酒店服务的请求
template.postForEntity("http://127.0.0.1:8083/hotel_tcc/{name}/{hotel}",null, String.class, name, hotel);
return name + " order " + order + "hotel " + hotel + " cars OK";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//订单(酒店)中的代码逻辑
@Transactional(rollbackFor = Exception.class)
void cancel(OrderDO orderDO) {
orderRepository.deleteById(orderDO.getId());
}

@Transactional(rollbackFor = Exception.class)
void confirm(OrderDO orderDO) {
orderRepository.insert(orderDO);
}

@Participate(confirmMethod = "confirm", cancelMethod = "cancel")
@Transactional(rollbackFor = Exception.class)
public void orderTcc(OrderDO orderDO) {
}

数据库结果如图所示

订单和商店的表都没有生成数据。

情况三:父事件中调用订单和酒店成功后,出现异常

1
2
3
4
5
6
7
8
9
10
11
12
//父事务
@TccStart(timeout = 2)
@PostMapping("/shop_tcc/{name}/{order}/{hotel}")
public String shopTcc(@PathVariable String name, @PathVariable String order, @PathVariable String hotel) {
//调用订单服务的请求
template.postForEntity("http://127.0.0.1:8082/order_tcc/{name}/{order}",null, String.class, name, order);
//调用酒店服务的请求
template.postForEntity("http://127.0.0.1:8083/hotel_tcc/{name}/{hotel}",null, String.class, name, hotel);
//异常
postBooking();
return name + " order " + order + "hotel " + hotel + " cars OK";
}

数据如图所示:

订单表与酒店表都没有产生数据

情况四:父事件超时

1
2
3
4
5
6
7
8
9
10
11
12
//父事务
@TccStart(timeout = 2)
@PostMapping("/shop_tcc/{name}/{order}/{hotel}")
public String shopTcc(@PathVariable String name, @PathVariable String order, @PathVariable String hotel) throws InterruptedException {
//调用订单服务的请求
template.postForEntity("http://127.0.0.1:8082/order_tcc/{name}/{order}",null, String.class, name, order);
//调用酒店服务的请求
template.postForEntity("http://127.0.0.1:8083/hotel_tcc/{name}/{hotel}",null, String.class, name, hotel);
//超时
Thread.sleep(10000);
return name + " order " + order + "hotel " + hotel + " cars OK";
}

发现TCC的timeout选项好像没有作用。。。。看了下源码,的确没有用到,源码如下

ServiceComb在0.3.0加入了TCC的支持,所以有些功能还待完善把。

情况五:订单服务启动,酒店服务未启动

关闭hotel服务,执行后数据如下:

订单和酒店数据库都没有数据

情况六: 模拟运行过程中alpha服务挂起,订单、酒店、商店服务正常运行:

订单、酒店、商店服务后来日志显示心跳连接失效

请求数据返回错误信息,数据库表均未写入数据。

重新启动alpha服务,订单、酒店、商店服务重新连接到alpha,业务正常运行。

情况七: 模拟运行过程中alpha服务的mysql挂起,订单、酒店、商店服务正常运行:

请求未进入业务逻辑之前,alpha服务报错,请求未执行。
mysql重启成功后,alpha服务正常运行,请求数据正常执行。

总结

ServiceComb对于数据最终一致性的解决现阶段0.3.0是满足业务逻辑的,但是对于失败重试、超时等功能这一块还不支持,后期应该会扩展。ServiceComb功能比较简单,但是可以通过对omega的事物id结合调用链追踪实现业务流程与事务的追溯。

分布式应用监控

发表于 2019-02-18 | 分类于 java |

分布式应用监控

image
分布式系统已经诞生了很长时间,现代互联网公司规模都变得异常庞大,系统也变得越来越复杂,给监控工作带来了极大的难度:海量日志数据如何处理,服务如何追踪,如何高效定位故障缩短故障时常,常见的监控手段可以分为集中式日志系统(Logging),集中式度量系统(Metrics)和分布式追踪系统(Tracing)。

集中式日志系统

集中式日志系统,选取了最具代表性的ELK
image

Elasticsearch

Elasticsearch是个开源的分布式搜索引擎,提供搜索、分析、存储数据三大功能。它的特点有:分布式、自动发现、索引自动分片、索引副本机制、RESTful 风格接口、多数据源以及自动搜索负载等。
image

Logstash

Logstash 是一个开源的动态数据收集处理管道,它可以同时从多个源中提取数据,对其进行转换,并且拥有可扩展的插件生态系统,能够与 Elasticsearch 产生强大的协同作用。
image

Kibana

Kibana是一个开源的分析与可视化平台,设计出来用于和Elasticsearch一起使用的。你可以用kibana搜索、查看存放在Elasticsearch中的数据。Kibana与Elasticsearch的交互方式是各种不同的图表、表格、地图等,直观的展示数据,从而达到高级的数据分析与可视化的目的。
image

Beats

Beats 是 ELK Stack 技术栈中负责单一用途数据采集并推送给 Logstash 或 Elasticsearch 的轻量级产品。包括:

  • Filebeats:应用于日志收集场景的实现。
  • Metricbeat:轻量级的系统级性能指标监控工具。
  • Packetbeat:轻量级的网络数据包分析工具。
  • Winlogbeat:轻量级的 Windows 事件日志收集工具。
  • Heartbeat:心跳检测工具,主要监控服务的可用性。

image

安装

修改虚拟机的内存限制

1
vi /etc/sysctl.conf

加入

1
vm.max_map_count=262144

sysctl -p查看设置

docker安装ELK

1
2
docker pull sebp/elk
docker run -p 5601:5601 -p 9200:9200 -p 5044:5044 -e ES_MIN_MEM=128m -e ES_MAX_MEM=1024m -it --name elk sebp/elk

输入网址http://<your-host>:5601可以看到下面的界面,则说明安装成功
image

配置使用

1
docker exec -it <container-name> /bin/bash

进入容器,执行命令

1
/opt/logstash/bin/logstash -e 'input { stdin { } } output { elasticsearch { hosts => ["localhost"] } }'

如果有错误信息

1
service logstash stop

当命令成功被执行后,看到:Successfully started Logstash API endpoint {:port=>9600}信息后,输入:this is a dummy entry然后回车,模拟一条日志进行测试。

打开浏览器http://<your-host>:9200/_search?pretty,如图所示
image

打开浏览器,输入:http://<your-host>:5601 点击创建
image

看到如下界面,到此安装结束。
image

与java应用结合的日志分析系统可以通过Beats的Filebeats来实现,通过log4j将运行日志输出在文件中,通过Filebeats插件利用Logstash过滤并导入到Elasticsearch中,最后通过Kibana展示。

集中式度量系统

Prometheus

Prometheus是一个基于时间序列的数值数据的监控解决方案,这是一个开源项目,由前Google员工在SoundCloud启动,他们希望监控一个高度动态的容器环境,因为对传统的监控工具不甚满意,所以开发出Prometheus,并在上面进行工作。Prometheus解决了Devs如何监控高动态容器环境的问题。

例如我们想要获取所有的服务器上node_exporter暴露出来的数据,就必须有个程序去定时访问这些接口,如果想要增加或者修改这些接口,那么就需要有个配置文件来记录这些服务器的地址,如果想要访问历史的某个时间点的数据,那么就必须按照时间顺序存储获取到的指标和值。而如果想要将值绘制成图,也需要有代码去查询、计算和渲染。最后你可能还希望当服务器的某个指标超过一定的阈值时,向指定的接口发出告警信息。一切的一切其实都可以使用Prometheus来解决。

image

Prometheus检测mysql相关指标

前提:本地安装了mysql

安装node-exporter

1
2
docker pull node-exporter
docker run -d -p 9100:9100 --cap-add SYS_TIME --net="host" --pid="host" -v "/:/host:ro,rslave"quay.io/prometheus/node-exporter --cap-add=SYS_TIME --path.rootfs /host

安装mysqld-exporter

通过mysql命令界面创建相应角色并赋予权限

1
2
CREATE USER 'mysql_monitor'@'localhost' IDENTIFIED BY 'XXXXXXXX' WITH MAX_USER_CONNECTIONS 3;
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'mysql_monitor'@'localhost';

docker安装

1
2
docker pull mysqld-exporter
docker run -d -p 9104:9104 -e DATA_SOURCE_NAME="mysql_monitor:root@(127.0.0.1:3306)/" prom/mysqld-exporter

安装Prometheus

创建文件prometheus.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
global:
scrape_interval: 60s
evaluation_interval: 60s

scrape_configs:
- job_name: prometheus
static_configs:
- targets: ['106.15.226.184:9090']
labels:
instance: prometheus

- job_name: linux
static_configs:
- targets: ['106.15.226.184:9100']
labels:
instance: db1

- job_name: mysql
static_configs:
- targets: ['106.15.226.184:9104']
labels:
instance: db1

docker启动

1
2
docker pull prometheus
sudo docker run -d -p 9090:9090 -v /root/conf/prometheus.yml:/usr/local/src/file/prometheus.yml quay.io/prometheus/prometheus --config.file=/usr/local/src/file/prometheus.yml

安装Grafana

1
2
docker pull grafana/grafana
docker run -d --name=grafana -p 3000:3000 grafana/grafana

打开http:x.x.x.x:9090
image
如图所示,说明数据源管道agent启动成功

打开http://x.x.x.x:3000,配置Prometheus数据源
image

配置好数据源后,下载mysql监控模板,
解压后,找到mysql开头的模板,导入,最后如图所示:
image

image

Cat

image
CAT(Central Application Tracking)是一个实时和接近全量的监控系统,它侧重于对Java应用的监控,基本接入了美团上海侧所有核心应用。目前在中间件(MVC、RPC、数据库、缓存等)框架中得到广泛应用,为美团各业务线提供系统的性能指标、健康状况、监控告警等。

image

监控整体要求就是快速发现故障、快速定位故障以及辅助进行程序性能优化。为了做到这些,我们对监控系统的一些非功能做了如下的要求:

实时处理:信息的价值会随时间锐减,尤其是事故处理过程中。
全量数据:最开始的设计目标就是全量采集,全量的好处有很多。
高可用:所有应用都倒下了,需要监控还站着,并告诉工程师发生了什么,做到故障还原和问题定位。
故障容忍:CAT本身故障不应该影响业务正常运转,CAT挂了,应用不该受影响,只是监控能力暂时减弱。
高吞吐:要想还原真相,需要全方位地监控和度量,必须要有超强的处理吞吐能力。
可扩展:支持分布式、跨IDC部署,横向扩展的监控系统。
不保证可靠:允许消息丢失,这是一个很重要的trade-off,目前CAT服务端可以做到4个9的可靠性,可靠系统和不可靠性系统的设计差别非常大。
CAT从开发至今,一直秉承着简单的架构就是最好的架构原则,主要分为三个模块:CAT-client、CAT-consumer、CAT-home。

Cat-client 提供给业务以及中间层埋点的底层SDK。
Cat-consumer 用于实时分析从客户端提供的数据。
Cat-home 作为用户给用户提供展示的控制端。
在实际开发和部署中,Cat-consumer和Cat-home是部署在一个JVM内部,每个CAT服务端都可以作为consumer也可以作为home,这样既能减少整个层级结构,也可以增加系统稳定性。
上图是CAT目前多机房的整体结构图,图中可见:

路由中心是根据应用所在机房信息来决定客户端上报的CAT服务端地址,目前美团有广州、北京、上海三地机房。
每个机房内部都有独立的原始信息存储集群HDFS。
CAT-home可以部署在一个机房也可以部署在多个机房,在最后做展示的时候,home会从consumer中进行跨机房的调用,将所有的数据合并展示给用户。
实际过程中,consumer、home以及路由中心都是部署在一起的,每个服务端节点都可以充当任何一个角色。

安装使用Cat

本文演示单机集群安装部署

1
2
3
git clone https://github.com/dianping/cat.git
cd docker
docker-compose up

第一次运行以后,数据库中没有表结构,需要通过下面的命令创建表:

1
docker exec <container_id> bash -c "mysql -uroot -Dcat < /init.sql"

依赖配置说明

  • datasources.xml
    • CAT数据库配置,默认配置是mysql镜像,可以按需替换
  • docker-compose.yml
    • 通过docker-compose启动的编排文件,文件中包含cat和mysql。可以屏蔽掉mysql的部分,并且修改cat的环境变量,改为真实的mysql连接信息。
  • client.xml
    • CAT 初始化默认的路由列表,配置此文件可以将客户端数据上报指向到不同环境。
  • datasources.sh
    • 辅助脚本,脚本作用时修改datasources.xml,使用环境变量中制定的mysql连接信息。(通过sed命令替换)

Java 应用的集成

参考博客
需要指定 cat 专用的远程仓库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- %MAVEN_HOME%\conf\settings.xml -->

<profiles>
<profile>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<repositories>
<repository>
<id>central</id>
<layout>default</layout>
<url>http://repo1.maven.org/maven2</url>
</repository>
<repository>
<id>unidal.nexus</id>
<url>http://unidal.org/nexus/content/repositories/releases/</url>
</repository>
</repositories>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
</profile>
</profiles>

加入依赖(pom.xml)

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.dianping.cat</groupId>
<artifactId>cat-client</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>

添加过滤器 CatFilter

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class CatFilterConfigure {
@Bean
public FilterRegistrationBean catFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new CatFilter());
registration.addUrlPatterns("/*");
registration.setName("cat-filter");
registration.setOrder(1);
return registration;
}
}

添加注解

1
2
3
4
5
6
7
8
9
10
@CatCacheTransaction
public void test() {
}

@ResponseBody
@RequestMapping("/hello")
@CatHttpRequestTransaction(type = "URL", name = "/hello")
public String hello() {
return "hello!";
}

image

更多集成
image

管理平台的使用

控制台
http://192.168.126.101:8080/cat
帐号/密码: catadmin/catadmin

项目配置
http://192.168.126.101:8080/cat/s/config?op=projects

相关文档
部署文档: http://192.168.126.101:8080/cat/r/home?op=view&docName=deploy
用户文档:http://192.168.126.101:8080/cat/r/home?op=view&docName=user
告警文档:http://192.168.126.101:8080/cat/r/home?op=view&docName=alert
集成文档:http://192.168.126.101:8080/cat/r/home?op=view&docName=integration
开发文档:http://192.168.126.101:8080/cat/r/home?op=view&docName=develop
设计文档:http://192.168.126.101:8080/cat/r/home?op=view&docName=design
常见问题:http://192.168.126.101:8080/cat/r/home?op=view&docName=problem

实时查看
http://192.168.126.101:8080/cat/r/t
image

分布式追踪系统

Zipkin

Zipkin是一种分布式跟踪系统。它有助于收集解决微服务架构中的延迟问题所需的时序数据。它管理这些数据的收集和查找。Zipkin的设计基于Google Dapper论文。
应用程序用于向Zipkin报告时序数据。Zipkin UI还提供了一个依赖关系图,显示了每个应用程序通过的跟踪请求数。如果要解决延迟问题或错误,可以根据应用程序,跟踪长度,注释或时间戳对所有跟踪进行筛选或排序。选择跟踪后,您可以看到每个跨度所需的总跟踪时间百分比,从而可以识别问题应用程序。
image

共有四个组件构成了 Zipkin:

  • collector
  • storage
  • search
  • web UI

Zipkin Collector

一旦追踪数据抵达 Zipkin Collector 守护进程,Zipkin Collector 为了查询,会对其进行校验、存储和索引。

Storage

Zipkin 最初是构建在将数据存储在 Cassandra 中,因为 Cassandra 易跨站,支持灵活的 schema,并且在 Twitter 内部被大规模使用。然而,我们将这个组件做成了可插拔式的。在 Cassandra 之外,我们原生支持 ElasticSearch 和 MySQL。可作为第三方扩展提供给其它后端。

Zipkin 查询服务

一旦数据被存储索引,我们就需要一种方式提取它。查询守护进程提供了一个简单的 JSON API 查询和获取追踪数据。API 的主要消费者就是 Web UI。

Web UI

我们创建了一个用户图形界面为追踪数据提供了一个漂亮的视图。Web UI 提供了基于服务、时间和标记(annotation)查看追中数据的方法。注意:UI 没有内置的身份认证功能。

image

安装部署

参考
前提条件:已经安装好ElasticSearch

安装zookeeper和kafka

1
2
3
docker pull wurstmeister/zookeeper  

docker pull wurstmeister/kafka

启动镜像

1
2
3
4
5
docker run -d --name zookeeper --publish 2181:2181 --volume /etc/localtime:/etc/localtime zookeeper:latest
docker run -d --name kafka --publish 9092:9092 --link zookeeper --env KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
--env KAFKA_ADVERTISED_HOST_NAME=kafka所在宿主机的IP --env KAFKA_ADVERTISED_PORT=9092 --volume /etc/localtime:/etc/localtime
wurstmeister/kafka:latest
docker run -d --name zipkin-server -p 9411:9411 -e "KAFKA_BOOTSTRAP_SERVERS=your-kafka-address" -e "STORAGE_TYPE=elasticsearch" -e "ES_HOSTS=your-es-host" -e "ES_INDEX=zipkin" -e "ES_INDEX_SHARDS=1" -e "ES_INDEX_REPLICAS=1" zipkin:latest

使用

用maven新建springboot项目
引入依赖,包括Spring Cloud Sleuth和Kafka传输的支持依赖Spring Stream Kafak以及web依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency>
</dependencies>

配置

1
2
3
4
5
6
7
8
9
10
spring:
application:
name: service-producer # 配置应用名称
kafka:
bootstrap-servers: localhost:9092 # 缓冲kafka地址
sleuth:
sampler:
percentage: 1 # 设置采样频率,默认为0.1,设置为全采样,便于观测,实际项目中根据具体情况设置
server:
port: 8080

类似的方法再新建一个项目然后写一个接口进行2个服务之间的通讯,触发调用链,可以在http://localhost:9411查看如下效果图:
image
image
image
需要注意的是,我们使用的存储模块是ES,所以一段时间内的服务调用关系图是无法直接得到的(使用内存存储可以直接得到)。我们需要使用Zipkin官方提供的zipkin-dependencies来生成依赖关系图。

1
2
# ex to run the job to process yesterday's traces on OS/X
$ STORAGE_TYPE=elasticsearch ES_HOSTS=your-es-host ES_INDEX=zipkin ES_NODES_WAN_ONLY=true java -jar zipkin-dependencies.jar `date -uv-1d +%F`

Pinpoint

Pinpoint是一个开源的APM监控工具,我们可以通过pinpoint实时跟踪应用之间的调用、程序的响应时间以及服务器资源使用状态,可以在分布式环境中为没个调用生成代码级别的可视图并定位瓶颈点和失败点。Pinpoint的设计也是基于Google Dapper论文
image

安装部署

参考

1
2
3
git clone https://github.com/naver/pinpoint-docker.git
cd Pinpoint-Docker
docker-compose pull && docker-compose up -d

如有问题,请修改相对路径为绝对路径

1
2
3
4
5
...
volumes:
- /home/pinpoint/hbase
- /home/pinpoint/zookeeper
...

启动镜像,访问http://x.x.x.x:8079/
image
hbase页面 http://x.x.x.x:16010/
image

Skywalking

Skywalking是一款优秀的国产 APM 工具,包括了分布式追踪、性能指标分析、应用和服务依赖分析等。通过在应用程序中添加 SkyWalking Agent,就可以将接口、服务、数据库、MQ等进行追踪,将追踪结果通过 HTTP 或 gRPC 发送到 SkyWalking Collecter,SkyWalking Collecter 经过分析和聚合,将结果存储到 Elasticsearch 或 H2,SkyWalking 同时提供了一个 SkyWalking UI 的可视化界面,UI 以 GraphQL + HTTP 方式获取存储数据进行展示。
image

安装部署

参考博客

使用

拷贝apache-skywalking-apm-incubating目录下的agent目录到应用程序位置,探针包含整个目录,请不要改变目录结构

java程序启动时,增加JVM启动参数,-javaagent:/path/to/agent/skywalking-agent.jar。参数值为skywalking-agent.jar的绝对路径

agent探针配置,简单修改下agent.application_code即可

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
# 当前的应用编码,最终会显示在webui上。
# 建议一个应用的多个实例,使用有相同的application_code。请使用英文
agent.application_code=Your_ApplicationName

# 每三秒采样的Trace数量
# 默认为负数,代表在保证不超过内存Buffer区的前提下,采集所有的Trace
# agent.sample_n_per_3_secs=-1

# 设置需要忽略的请求地址
# 默认配置如下
# agent.ignore_suffix=.jpg,.jpeg,.js,.css,.png,.bmp,.gif,.ico,.mp3,.mp4,.html,.svg

# 探针调试开关,如果设置为true,探针会将所有操作字节码的类输出到/debugging目录下
# skywalking团队可能在调试,需要此文件
# agent.is_open_debugging_class = true

# 对应Collector的config/application.yml配置文件中 agent_server/jetty/port 配置内容
# 例如:
# 单节点配置:SERVERS="127.0.0.1:8080"
# 集群配置:SERVERS="10.2.45.126:8080,10.2.45.127:7600"
collector.servers=127.0.0.1:10800

# 日志文件名称前缀
logging.file_name=skywalking-agent.log

# 日志文件最大大小
# 如果超过此大小,则会生成新文件。
# 默认为300M
logging.max_file_size=314572800

# 日志级别,默认为DEBUG。
logging.level=DEBUG

一切正常的话,稍后就可以在skywalking ui看到了。
image

Jaeger

Uber开源的Jaeger用于监控和排除基于微服务的分布式系统,包括:

  • 分布式上下文传播
  • 分布式事务监控
  • 根本原因分析
  • 服务依赖性分析
  • 性能/延迟优化

image

安装部署

all-in-one 是Uber官方打包好的镜像,可以直接部署使用,但是只能用于测试环境,不能用于线上,因为它把数据放入了内存。

1
2
docker run -d -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 -p5775:5775/udp -p6831:6831/udp -p6832:6832/udp \
-p5778:5778 -p16686:16686 -p14268:14268 -p9411:9411 jaegertracing/all-in-one:latest

通过 http://localhost:16686 可以在浏览器查看 Jaeger的后台

正常安装参考

使用

参考

分布式链路追踪技术对比

来自博文

cat

由大众点评开源,基于Java开发的实时应用监控平台,包括实时应用监控,业务监控 。 集成方案是通过

代码埋点的方式来实现监控,比如: 拦截器,注解,过滤器等。 对代码的侵入性很大,集成成本较高。
支持技术栈:

  • dubbo
  • spring mvc ,spring aop ,springmvc-url
  • spring boot
  • mybatis
  • log4j , logback
  • playframework
  • http请求

风险较大。

zipkin

由Twitter团队开源, Zipkin是一个分布式的跟踪系统。它有助于收集数据需要解决潜在的问题在市微服架构的时机。它管理数据的收集和查找 .

该产品结合spring-cloud-sleuth使用较为简单, 集成很方便。 但是功能较简单。

支持技术栈:

  • spring cloud

以上是结合spring-cloud-sleuth支持的技术栈

pinpoint

由韩国团队naver团队开源,针对大规模分布式系统用链路监控,使用java写的工具。灵感来自短小精悍,帮助分析系统的总

体结构和内部组件如何被调用在分布式应用提供了一个很好的解决方案。

使用java探针字节码增加技术,实现对整个应用的监控 。 对应用零侵入

支持技术栈:

  • Tomcat 6+,Jetty 8/9,JBoss 6,Resin 4,Websphere 6+,Vertx 3.3+
  • Spring, Spring Boot (Embedded Tomcat, Jetty)
  • HTTP Client 3.x/4.x, HttpConnector, GoogleHttpClient, OkHttpClient, NingAsyncHttpClient
  • Thrift, Dubbo
  • mysql, oracle, mssql, cubrid,PostgreSQL, maria
  • arcus, memcached, redis, cassandra
  • MyBatis
  • DBCP, DBCP2, HIKARICP
  • gson, Jackson, Json Lib
  • log4j, Logback

skywalking

2015年由个人吴晟(华为开发者)开源 , 2017年加入Apache孵化器。

针对分布式系统的应用性能监控系统,特别针对微服务、cloud native和容器化(Docker, Kubernetes, Mesos)架构, 其核心是个分布式追踪系统。

使用java探针字节码增加技术,实现对整个应用的监控 。对应用零侵入

支持技术栈

  • Tomcat7+ , resin3+, jetty
  • spring boot ,spring mvc
  • strtuts2
  • spring RestTemplete ,spring-cloud-feign
  • okhttp , httpClient
  • msyql ,oracle , H2 , sharding-jdbc,PostgreSQL
  • dubbo,dubbox ,motan, gRpc ,
  • rocketMq , kafla
  • redis, mongoDB,memcached ,
  • elastic-job , Netflix Eureka , Hystric

总结

模拟了三种并发用户:500,750,1000。使用jmeter测试,每个线程发送30个请求,设置思考时间为10ms。使用的采样率为1,即100%,这边与生产可能有差别。

pinpoint默认的采样率为20,即50%,通过设置agent的配置文件改为100%。zipkin默认也是1。组合起来,一共有12种。

TestNG测试框架与rest-assured结合

发表于 2019-01-24 | 分类于 JAVA |

TestNG官网

TestNG简介

image

TestNG是一个受JUnit和NUnit启发的测试框架,但引入了一些新功能,使其功能更强大,相对于JUnit来说,xml的配置使的testNG对于不同测试之间的依赖程度有更好的把控性。

rest-assured简介

在Java中测试和验证REST服务比在Ruby和Groovy等动态语言中更难。REST Assured将使用这些语言的简单性带入了Java域。

TestNG测试框架与rest-assured结合

项目地址:
https://github.com/dinghuang/testNGExample

上面实现了模拟用户登录以及rest-assured的高级用法,同时可以通过命令行直接生成报告,报告中对http请求增加了过滤,会在报告中展示请求信息,可以通过xml解析直接获取,还实现了多个suit通过mvn命令直接运行。目前在Choerodon中已经增加了TestNG的支持,用户可以直接推到gitlab,gitlab中的runner会在ci中打包并运行测试jar包,把报告解析并提取请求信息生成测试用例。

项目的关键代码就不一一说了,项目中有注释,不懂的+我VX:742041978

Kubernetes学习(一)之认识Kubernetes

发表于 2019-01-12 | 分类于 Kubernetes |

Kubernetes学习(一)之认识Kubernetes

Kubernetes概念

简介

Kubernetes是一个跨主机集群的 开源的容器调度平台,它可以自动化应用容器的部署、扩展和操作 , 提供以容器为中心的基础架构。结合docker可以提供持续开发,持续部署的功能,我现在所从事开发的Choerodon就是基于这一套架构开发的企业级数字服务平台,具有敏捷化的应用交付和自动化的运营管理的特点。这里介绍的版本是v1.13

image

新的方式是通过部署容器方式实现,每个容器之间互相隔离,每个容器有自己的文件系统 ,容器之间进程不会相互影响,能区分计算资源。相对于虚拟机,容器能快速部署,由于容器与底层设施、机器文件系统解耦的,所以它能在不同云、不同版本操作系统间进行迁移。

容器占用资源少、部署快,每个应用可以被打包成一个容器镜像,每个应用与容器间成一对一关系也使容器有更大优势,使用容器可以在build或release 的阶段,为应用创建容器镜像,因为每个应用不需要与其余的应用堆栈组合,也不依赖于生产环境基础结构,这使得从研发到测试、生产能提供一致环境。类似地,容器比虚机轻量、更“透明”,这更便于监控和管理。

组件

image

Master组件

Kubernetes 主要由以下几个核心(Master)组件组成,Master组件提供集群的管理控制中心。Master组件可以在集群中任何节点上运行。但是为了简单起见,通常在一台VM/机器上启动所有Master组件,并且不会在此VM/机器上运行用户容器。请参考构建高可用群集以来构建multi-master-VM。

kube-apiserver

kube-apiserver。

etcd

etcd是Kubernetes提供默认的存储系统,保存所有集群数据,使用时需要为etcd数据提供备份计划。

kube-scheduler

主服务器上的组件,用于监视未创建节点的新创建的pod,并选择一个节点供其运行。 调度决策所考虑的因素包括个人和集体资源需求,硬件/软件/策略约束,亲和力和反亲和性规范,数据位置,工作负载间干扰和最后期限。

kube-controller-manager

kube-controller-manager运行管理控制器,它们是集群中处理常规任务的后台线程。逻辑上,每个控制器是一个单独的进程,但为了降低复杂性,它们都被编译成单个二进制文件,并在单个进程中运行。负责维护集群的状态,比如故障检测、自动扩展、滚动更新等。

这些控制器包括:

  • 节点(Node)控制器。
  • 副本(Replication)控制器:负责维护系统中每个副本中的pod。
  • 端点(Endpoints)控制器:填充Endpoints对象(即连接Services&Pods)。
    - Service Account和Token控制器:为新的Namespace 创建默认帐户访问API Token。
cloud-controller-manager

云控制器管理器负责与底层云提供商的平台交互。云控制器管理器是Kubernetes版本1.6中引入的,目前还是Alpha的功能。

云控制器管理器仅运行云提供商特定的(controller loops)控制器循环。可以通过将--cloud-provider flag设置为external启动kube-controller-manager ,来禁用控制器循环。

cloud-controller-manager 具体功能:

  • 节点(Node)控制器
  • 路由(Route)控制器
  • Service控制器
  • 卷(Volume)控制器

节点(Node)组件

节点组件运行在Node,提供Kubernetes运行时环境,以及维护Pod。

kubelet

kubelet是主要的节点代理,它会监视已分配给节点的pod,负责维护容器的生命周期,同时也负责 Volume(CVI)和网络(CNI)的管理,具体功能:

  • 安装Pod所需的volume。
  • 下载Pod的Secrets。
  • Pod中运行的 docker(或experimentally,rkt)容器。
  • 定期执行容器健康检查。
  • 通过在必要时创建镜像pod,将pod状态报告回系统的其余部分。
  • 将节点的状态返回到系统的其余部分。
kube-proxy

kube-proxy通过在主机上维护网络规则并执行连接转发来实现Kubernetes服务抽象。负责为 Service 提供 cluster 内部的服务发现和负载均衡。

Container Runtime

容器运行时是负责运行容器的软件。 Kubernetes支持多种运行时:Docker,rkt,runc和任何OCI运行时规范实现。

插件

插件(addon)是实现集群pod和Services功能的 。PodDeployments,ReplicationController等进行管理。Namespace 插件对象是在kube-system Namespace中创建。有关可用插件的扩展列表,请参阅插件。

DNS

虽然不严格要求使用插件,但Kubernetes集群都应该具有DNS集群。
群集 DNS是一个DNS服务器,能够为 Kubernetes services提供 DNS记录。
由Kubernetes启动的容器自动将这个DNS服务器包含在他们的DNS searches中。

用户界面

仪表板是Kubernetes集群的基于Web的通用UI。它允许用户管理和解决群集中运行的应用程序以及群集本身。

容器资源监测

容器资源监控提供一个UI浏览监控数据。

Cluster-level Logging

Cluster-level logging,负责保存容器日志,搜索/查看日志。

supervisord

supervisord是一个轻量级的监控系统,用于保障kubelet和docker运行。

fluentd

fluentd是一个守护进程,可提供cluster-level logging。

The Kubernetes API

API约定文档中描述了总体API约定

API参考中描述了API端点,资源类型和示例。

Controlling API Access文档中讨论了对API的远程访问。

Kubernetes API还可用作系统声明性配置架构的基础。 kubectl命令行工具可用于创建,更新,删除和获取API对象。

Kubernetes还根据API资源存储其序列化状态(当前在etcd中)。

Kubernetes本身被分解为多个组件,通过其API进行交互。

  • API更改
  • OpenAPI和Swagger定义
  • API版本控制
  • API组
  • 启用API组
  • 启用组中的资源

API更改

根据我们的经验,任何成功的系统都需要随着新用例的出现或现有用例的变化而增长和变化。因此,我们希望Kubernetes API能够不断变化和发展。但是,我们打算在很长一段时间内不破坏与现有客户端的兼容性。通常,可以预期频繁添加新的API资源和新的资源字段。消除资源或字段将需要遵循API弃用策略。

OpenAPI和Swagger定义

使用OpenAPI记录完整的API详细信息。 从Kubernetes 1.10开始,Kubernetes API服务器通过/openapi/ v2端点提供OpenAPI规范。通过设置HTTP标头指定请求的格式:

Header Possible Values
Accept application/json, application/com.github.proto-openapi.spec.v2@v1.0+protobuf (the default content-type is application/json for / or not passing this header)
Accept-Encoding gzip (not passing this header is acceptable)

在1.14之前,格式分离的端点(/swagger.json,/swagger-2.0.0.json,/swagger-2.0.0.pb-v1,/swagger-2.0.0.pb-v1.gz)为OpenAPI提供服务不同格式的规范。这些端点已弃用,将在Kubernetes 1.14中删除。

获取OpenAPI规范的示例:

Before 1.10 Starting with Kubernetes 1.10
GET /swagger.json GET /openapi/v2 Accept: application/json
GET /swagger-2.0.0.pb-v1 GET /openapi/v2 Accept: application/com.github.proto-openapi.spec.v2@v1.0+protobuf
GET /swagger-2.0.0.pb-v1.gz GET /openapi/v2 Accept: application/com.github.proto-openapi.spec.v2@v1.0+protobuf Accept-Encoding: gzip

Kubernetes为API实现了另一种基于Protobuf的序列化格式,主要用于集群内通信,在设计提案中有记录,每个模式的IDL文件都位于定义API对象的Go包中。

在1.14之前,Kubernetes apiserver还公开了一个API,可用于检索/ swaggerapi上的Swagger v1.2 Kubernetes API规范。该端点已弃用,将在Kubernetes 1.14中删除。

API版本控制

为了更容易消除字段或重构资源表示,Kubernetes支持多个API版本,每个API版本位于不同的API路径,例如/api/v1或/apis/extensions/v1beta1。

我们选择在API级别而不是在资源或字段级别进行版本化,以确保API提供清晰,一致的系统资源和行为视图,并允许控制对生命末端和/或实验API的访问。 JSON和Protobuf序列化模式遵循相同的模式更改指南 - 以下所有描述都涵盖两种格式。

请注意,API版本控制和软件版本控制仅间接相关。 API和发布版本控制提议描述了API版本控制和软件版本控制之间的关系。

不同的API版本意味着不同级别的稳定性和支持。 API更改文档中更详细地描述了每个级别的标准。他们总结在这里:

Alpha level:
  • 版本名称包含alpha(例如v1alpha1)。
  • 可能是马车。启用该功能可能会暴露错误。默认情况下禁用。
  • 可随时删除对功能的支持,恕不另行通知。
  • API可能会在以后的软件版本中以不兼容的方式更改,恕不另行通知。
  • 由于错误风险增加和缺乏长期支持,建议仅在短期测试集群中使用。
Beta level::
  • 版本名称包含beta(例如v2beta3)。
  • 代码经过了充分测试。启用该功能被认为是安全的。默认情况下启用。
  • 虽然细节可能会有所变化,但不会删除对整体功能的支持。
  • 在随后的beta版或稳定版中,对象的模式和/或语义可能以不兼容的方式发生变化。发生这种情况时,我们将提供迁移到下一版本的说明。这可能需要删除,编辑和重新创建API对象。编辑过程可能需要一些思考。对于依赖该功能的应用程序,这可能需要停机时间。
  • 建议仅用于非关键业务用途,因为后续版本中可能存在不兼容的更改。如果您有多个可以独立升级的群集,您可以放宽此限制。
  • 请尝试我们的测试版功能并提供反馈!一旦他们退出测试版,我们可能无法进行更多更改。
Stable level:
  • 该版本名称是vX这里X是一个整数。
  • 许多后续版本的已发布软件中将出现稳定版本的功能。

API组

为了更容易扩展Kubernetes API,我们实现了API组。API组在REST路径和apiVersion序列化对象的字段中指定。

目前有几个API组正在使用中:

  1. 核心组,常常被称为遗留组,是在REST路径/api/v1和用途apiVersion: v1。
  2. 命名组处于REST路径/apis/$GROUP_NAME/$VERSION,并使用apiVersion: $GROUP_NAME/$VERSION (例如apiVersion: batch/v1)。在Kubernetes API参考中可以看到支持的API组的完整列表。

使用自定义资源扩展API有两种受支持的路径:

  1. CustomResourceDefinition 适用于具有非常基本CRUD需求的用户。
  2. 需要完整Kubernetes API语义的用户可以实现自己的apiserver并使用聚合器 使其无缝地为客户端。

启用API组

默认情况下启用某些资源和API组。可以通过设置--runtime-config apiserver 来启用或禁用它们。--runtime-config接受逗号分隔值。例如:要禁用批处理/ v1,请设置 --runtime-config=batch/v1=false,以启用批处理/ v2alpha1,设置--runtime-config=batch/v2alpha1。该标志接受逗号分隔的一组key = value对,描述了apiserver的运行时配置。

重要信息:启用或禁用组或资源需要重新启动apiserver和controller-manager以获取--runtime-config更改。

启用组中的资源

默认情况下启用DaemonSet,Deployments,HorizontalPodAutoscalers,Ingresses,Jobs和ReplicaSet。可以通过设置--runtime-configapiserver 来启用其他扩展资源。--runtime-config接受逗号分隔值。例如:要禁用部署和入口,请设置 --runtime-config=extensions/v1beta1/deployments=false,extensions/v1beta1/ingresses=false

与Kubernetes对象一起工作

了解Kubernetes对象

了解Kubernetes对象

Kubernetes对象是Kubernetes系统中的持久实体。Kubernetes使用这些实体来表示集群的状态。具体来说,他们可以描述:

  • 容器化应用正在运行(以及在哪些节点上)
  • 这些应用可用的资源
  • 关于这些应用如何运行的策略,如重新策略,升级和容错
    Kubernetes对象是“record of intent”,一旦创建了对象,Kubernetes系统会确保对象存在。通过创建对象,可以有效地告诉Kubernetes系统你希望集群的工作负载是什么样的。

要使用Kubernetes对象(无论是创建,修改还是删除),都需要使用Kubernetes API。例如,当使用kubectl命令管理工具时,CLI会为提供Kubernetes API调用。你也可以直接在自己的程序中使用Kubernetes API,您还可以使用其中一个客户端库在您自己的程序中直接使用Kubernetes API。

对象(Object)规范和状态

每个Kubernetes对象都包含两个嵌套对象字段,用于管理Object的配置:Object Spec和Object Status。Spec描述了对象所需的状态 - 希望Object具有的特性,Status描述了对象的实际状态,并由Kubernetes系统提供和更新。

例如,通过Kubernetes Deployment 来表示在集群上运行的应用的对象。创建Deployment时,可以设置Deployment Spec,来指定要运行应用的三个副本。Kubernetes系统将读取Deployment Spec,并启动你想要的三个应用实例 - 来更新状态以符合之前设置的Spec。如果这些实例中有任何一个失败(状态更改),Kuberentes系统将响应Spec和当前状态之间差异来调整,这种情况下,将会开始替代实例。

有关object spec、status和metadata更多信息,请参考“Kubernetes API Conventions”。

描述Kubernetes对象

在Kubernetes中创建对象时,必须提供描述其所需Status的对象Spec,以及关于对象(如name)的一些基本信息。当使用Kubernetes API创建对象(直接或通过kubectl)时,该API请求必须将该信息作为JSON包含在请求body中。通常,可以将信息提供给kubectl .yaml文件,在进行API请求时,kubectl将信息转换为JSON。

以下示例是一个.yaml文件,显示Kubernetes Deployment所需的字段和对象Spec:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#application/deployment.yaml
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 2 # tells deployment to run 2 pods matching the template
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80

使用上述.yaml文件创建Deployment,是通过在kubectl中使用kubectl create命令来实现。将该.yaml文件作为参数传递。如下例子:

1
2
$ kubectl create -f https://k8s.io/examples/application/deployment.yaml --record
deployment.apps/nginx-deployment created

必填字段

对于要创建的Kubernetes对象的yaml文件,需要为以下字段设置值:

  • apiVersion - 创建对象的Kubernetes API 版本
  • kind - 要创建什么样的对象?
  • metadata- 具有唯一标示对象的数据,包括 name(字符串)、UID和Namespace(可选项)
    您还需要提供对象规范字段。对象规范的精确格式对于每个Kubernetes对象都是不同的,并且包含特定于该对象的嵌套字段。 Kubernetes API Reference可以帮助您找到可以使用Kubernetes创建的所有对象的规范格式。例如,可以在此处找到Pod对象的spec格式,可以在此处找到Deployment对象的spec格式。

name

Kubernetes REST API中的所有对象都由Name和UID明确标识。

对于非唯一的用户提供的属性,Kubernetes提供标签和注释。

有关名称和UID的精确语法规则,请参阅标识符设计文档。

  • Names
  • UIDs
Names

客户端提供的字符串,用于引用资源URL中的对象,例如/api/v1/pods/some-name。

只有给定类型的一个对象一次可以有一个给定的名称。但是,如果删除该对象,则可以创建具有相同名称的新对象。

按照惯例,Kubernetes资源的名称应最多为253个字符,并且由小写字母数字字符组成-,并且.,但某些资源具有更具体的限制。

UIDs

Kubernetes系统生成的字符串,用于唯一标识对象。

在Kubernetes集群的整个生命周期中创建的每个对象都具有不同的UID。它旨在区分类似实体的历史事件。

Namespaces

Kubernetes支持由同一物理集群支持的多个虚拟集群。这些虚拟集群称为名称空间。

  • 何时使用多个命名空间
  • 使用命名空间
  • 命名空间和DNS
  • 并非所有对象都在命名空间中
何时使用多个命名空间

命名空间旨在用于多个用户分布在多个团队或项目中的环境中。对于具有几个到几十个用户的集群,您根本不需要创建或考虑名称空间。当您需要它们提供的功能时,请开始使用命名空间。

命名空间提供名称范围。资源名称在名称空间中必须是唯一的,而不是跨名称空间。

命名空间是一种在多个用户之间划分群集资源的方法(通过资源配额)。

在Kubernetes的未来版本中,默认情况下,同一名称空间中的对象将具有相同的访问控制策略。

没有必要使用多个名称空间来分隔略有不同的资源,例如同一软件的不同版本:使用标签来区分同一名称空间中的资源。

使用命名空间

名称空间的管理指南文档中描述了名称空间的创建和删除。

查看名称空间

您可以使用以下命令列出集群中的当前名称空间:

1
2
3
4
5
$ kubectl get namespaces
NAME STATUS AGE
default Active 1d
kube-system Active 1d
kube-public Active 1d

Kubernetes以三个初始名称空间开头:

  • default 没有其他命名空间的对象的默认命名空间
  • kube-system Kubernetes系统创建的对象的命名空间
  • kube-public此命名空间是自动创建的,并且所有用户(包括未经过身份验证的用户)都可以读取。此命名空间主要用于群集使用,以防某些资源在整个群集中可见且可公开读取。此命名空间的公共方面只是一个约定,而不是一个要求。
设置请求的命名空间

要临时设置请求的命名空间,请使用该--namespace标志。

例如:

1
2
$ kubectl --namespace=<insert-namespace-name-here> run nginx --image=nginx
$ kubectl --namespace=<insert-namespace-name-here> get pods

设置命名空间首选项

您可以在该上下文中为所有后续kubectl命令永久保存命名空间。

1
2
3
$ kubectl config set-context $(kubectl config current-context) --namespace=<insert-namespace-name-here>
# Validate it
$ kubectl config view | grep namespace:
命名空间和DNS

创建服务时,它会创建相应的DNS条目。此条目是表单<service-name>.<namespace-name>.svc.cluster.local,这意味着如果容器只是使用<service-name>,它将解析为命名空间本地的服务。这对于在多个名称空间(如开发,分段和生产)中使用相同的配置非常有用。如果要跨命名空间访问,则需要使用完全限定的域名(FQDN)。

并非所有对象都在命名空间中

大多数Kubernetes资源(例如pod,服务,复制控制器等)都在某些名称空间中。但是,命名空间资源本身并不在命名空间中。并且低级资源(例如节点和persistentVolumes)不在任何名称空间中。

要查看哪些Kubernetes资源在命名空间中,哪些不在:

1
2
3
4
5
# In a namespace
$ kubectl api-resources --namespaced=true

# Not in a namespace
$ kubectl api-resources --namespaced=false

Labels and Selectors

标签是附加到对象(例如pod)的键/值对。标签旨在用于指定对用户有意义且相关的对象的标识属性,但不直接暗示核心系统的语义。标签可用于组织和选择对象的子集。标签可以在创建时附加到对象,随后可以随时添加和修改。每个对象都可以定义一组键/值标签。每个Key对于给定对象必须是唯一的。

1
2
3
4
5
6
"metadata": {
"labels": {
"key1" : "value1",
"key2" : "value2"
}
}

标签允许高效的查询和监视,非常适合在UI和CLI中使用。应使用注释记录非识别信息。

  • 动机
  • 语法和字符集
  • 标签选择器
  • API
动机

标签使用户能够以松散耦合的方式将他们自己的组织结构映射到系统对象,而无需客户端存储这些映射。

服务部署和批处理流水线通常是多维实体(例如,多个分区或部署,多个释放轨道,多个层,每层多个微服务)。管理通常需要交叉操作,这打破了严格的层次表示的封装,特别是由基础设施而不是用户确定的严格的层次结构。

示例标签:

  • "release" : "stable","release" : "canary"
  • "environment" : "dev","environment" : "qa","environment" : "production"
  • "tier" : "frontend","tier" : "backend","tier" : "cache"
  • "partition" : "customerA", "partition" : "customerB"
  • "track" : "daily", "track" : "weekly"
    这些只是常用标签的例子; 你可以自由地制定自己的约定。请记住,标签Key对于给定对象必须是唯一的。
语法和字符集

标签是键/值对。有效标签键有两个段:可选前缀和名称,用斜杠(/)分隔。名称段是必需的,必须是63个字符或更少,以字母数字字符([a-z0-9A-Z])开头和结尾,带有破折号(-),下划线(_),点(.)和字母数字之间。前缀是可选的。如果指定,前缀必须是DNS子域:由点(.)分隔的一系列DNS标签,总共不超过253个字符,后跟斜杠(/)。

如果省略前缀,则假定标签Key对用户是私有的。自动化系统组件(例如kube-scheduler,kube-controller-manager,kube-apiserver,kubectl,或其他第三方自动化),它添加标签终端用户对象都必须指定一个前缀。

在kubernetes.io/和k8s.io/前缀保留给Kubernetes核心组件。

有效标签值必须为63个字符或更少,并且必须为空或以字母数字字符([a-z0-9A-Z])开头和结尾,并带有短划线(-),下划线(_),点(.)和字母数字。

标签选择器

与名称和UID不同,标签不提供唯一性。通常,我们希望许多对象携带相同的标签。

通过标签选择器,客户端/用户可以识别一组对象。标签选择器是Kubernetes中的核心分组原语。

目前,API支持两种类型的选择:基于平等,和基于集的。标签选择器可以由逗号分隔的多个要求组成。在多个要求的情况下,必须满足所有要求,因此逗号分隔符充当逻辑AND(&&)运算符。

空或非指定选择器的语义取决于上下文,使用选择器的API类型应记录它们的有效性和含义。

注意:对于某些API类型(例如ReplicaSet),两个实例的标签选择器不得在命名空间内重叠,或者控制器可以将其视为冲突的指令,并且无法确定应存在多少副本。

基于平等的要求

基于平等或不平等的要求允许按标签键和值进行过滤。匹配对象必须满足所有指定的标签约束,尽管它们也可能有其他标签。三种操作都承认=,==,!=。前两个代表平等(简单地说是同义词),而后者代表不平等。例如:

1
2
environment = production
tier != frontend

前者选择密钥等于environment和值等于的所有资源production。后者选择密钥等于tier和值不同的frontend所有资源,以及没有带tier密钥标签的所有资源。可以过滤使用逗号运算符production排除的资源frontend:environment=production,tier!=frontend

基于等同的标签要求的一种使用场景是Pods指定节点选择标准。例如,下面的示例Pod选择标签为“ accelerator=nvidia-tesla-p100”的节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Pod
metadata:
name: cuda-test
spec:
containers:
- name: cuda-test
image: "k8s.gcr.io/cuda-vector-add:v0.1"
resources:
limits:
nvidia.com/gpu: 1
nodeSelector:
accelerator: nvidia-tesla-p100

基于集合的要求

基于集合的标签要求允许根据一组值过滤密钥。三种操作的支持:in,notin和exists(仅密钥标识符)。例如:

1
2
3
4
environment in (production, qa)
tier notin (frontend, backend)
partition
!partition

第一个示例选择键等于environment和值等于production或的所有资源qa。第二个示例选择密钥等于tier和除了frontend和之外的值的backend所有资源,以及没有带tier密钥标签的所有资源。第三个例子选择所有资源,包括带密钥的标签partition; 没有检查值。第四个示例选择没有带键的标签的所有资源partition; 没有检查值。类似地,逗号分隔符充当AND运算符。因此,使用partition密钥(无论值)和environment不同的 过滤资源qa都可以实现partition,environment notin (qa)。基于集合标签选择器是一种平等的一般形式,因为environment=production它等同于environment in (production); 同样的!=和notin。

基于集合的需求可以与基于相等的需求相结合。例如:partition in (customerA, customerB),environment!=qa。

API
LIST和WATCH过滤

LIST和WATCH操作可以指定标签选择器来过滤使用查询参数返回的对象集。这两个要求都是允许的(在此处显示为出现在URL查询字符串中):

  • 基于平等的要求:?labelSelector=environment%3Dproduction,tier%3Dfrontend
  • 基于集合的要求:?labelSelector=environment+in+%28production%2Cqa%29%2Ctier+in+%28frontend%29

两种标签选择器样式都可用于通过REST客户端列出或查看资源。例如,靶向apiserver与kubectl和使用基于平等-一个可写:

1
$ kubectl get pods -l environment=production,tier=frontend

或使用基于集合的要求:

1
$ kubectl get pods -l 'environment in (production),tier in (frontend)'

如前所述,基于集合的要求更具表现力。例如,他们可以在值上实现OR运算符:

1
$ kubectl get pods -l 'environment in (production, qa)'

或限制负匹配通过存在操作者:

1
$ kubectl get pods -l 'environment,environment notin (frontend)'

在API对象中设置引用

某些Kubernetes对象(例如services和replicationcontrollers)也使用标签选择器来指定其他资源集,例如pod。

服务和ReplicationController

service使用标签选择器定义目标的一组pod 。类似地,replicationcontroller应该管理的pod的数量也用标签选择器定义。

两个对象的标签选择器在使用映射定义json或yaml文件中定义,并且仅支持基于等同的需求选择器:

1
2
3
"selector": {
"component" : "redis",
}

要么

1
2
selector:
component: redis

这个选择器(分别以json或yaml格式)相当于component=redis或component in (redis)。

支持基于集合的需求的资源

较新的资源,如Job,Deployment,Replica Set,和Daemon Set,支持基于集合的要求也是如此。

1
2
3
4
5
6
selector:
matchLabels:
component: redis
matchExpressions:
- {key: tier, operator: In, values: [cache]}
- {key: environment, operator: NotIn, values: [dev]}

matchLabels是对的地图{key,value}。一个单一的{key,value}在matchLabels地图相当于一个元件matchExpressions,其key字段是“key”,则operator是“In”和values阵列仅包含“value”。matchExpressions是一个pod选择器要求列表。有效的运算符包括In,NotIn,Exists和DoesNotExist。在In和NotIn的情况下,设置的值必须是非空的。所有的要求,从两者matchLabels和matchExpressionsAND一起 - 他们必须满足,以匹配。

选择节点集

用于选择标签的一个用例是约束pod可以调度的节点集。有关更多信息,请参阅有关节点选择的文档。

Annotations

您可以使用Kubernetes注释将任意非标识元数据附加到对象。工具和库等客户端可以检索此元数据。

  • 将元数据附加到对象
  • 语法和字符集
将元数据附加到对象

您可以使用标签或注释将元数据附加到Kubernetes对象。标签可用于选择对象和查找满足特定条件的对象集合。相反,注释不用于识别和选择对象。注释中的元数据可以是小的或大的,结构化的或非结构化的,并且可以包括标签不允许的字符。

注释(如标签)是键/值映射

1
2
3
4
5
6
"metadata": {
"annotations": {
"key1" : "value1",
"key2" : "value2"
}
}

以下是可以在注释中记录的一些信息示例:

  • 由声明性配置层管理的字段。将这些字段作为注释附加,可以将它们与客户端或服务器设置的默认值以及自动生成的字段和自动调整大小或自动调整系统设置的字段区分开来。
  • 构建,发布或映像信息,如时间戳,版本ID,git分支,PR编号,镜像哈希和仓库地址。
  • 指向日志记录,监视,分析或审计存储库的指针。
  • 可用于调试目的的客户端库或工具信息:例如,名称,版本和构建信息。
  • 用户或工具/系统出处信息,例如来自其他生态系统组件的相关对象的URL。
  • 轻量推出工具元数据:例如,配置或检查点。
  • 负责人的电话或寻呼机号码,或指定可在何处找到该信息的目录条目,例如团队网站。
  • 从最终用户到实现的指令,用于修改行为或使用非标准功能。

您可以将此类信息存储在外部数据库或目录中,而不是使用注释,但这会使生成用于部署,管理,内省等的共享客户端库和工具变得更加困难。

语法和字符集

注释是键/值对。有效的注释键有两个段:可选的前缀和名称,用斜杠(/)分隔。名称段是必需的,必须是63个字符或更少,以字母数字字符([a-z0-9A-Z])开头和结尾,带有破折号(-),下划线(_),点(.)和字母数字之间。前缀是可选的。如果指定,前缀必须是DNS子域:由点(.)分隔的一系列DNS标签,总共不超过253个字符,后跟斜杠(/)。

如果省略前缀,则假定注释密钥对用户是私有的。自动化系统组件(例如kube-scheduler,kube-controller-manager,kube-apiserver,kubectl,或其他第三方自动化)的添加注释到最终用户的对象都必须指定一个前缀。

在kubernetes.io/和k8s.io/前缀保留给Kubernetes核心组件。

Field Selectors

  • 支持的字段
  • 支持操作
  • 链式选择器
  • 多种资源类型

字段选择器允许您根据一个或多个资源字段的值选择Kubernetes资源。以下是一些示例字段选择器查询:

  • metadata.name=my-service
  • metadata.namespace!=default
  • status.phase=Pending

此kubectl命令选择status.phase字段值为的所有Pod Running:

1
$ kubectl get pods --field-selector status.phase=Running

注意:
字段选择器本质上是资源过滤器。默认情况下,不应用选择器/过滤器,这意味着将选择指定类型的所有资源。这使以下kubectl查询等效:

1
2
$ kubectl get pods
$ kubectl get pods --field-selector ""

支持的字段

支持的字段选择器因Kubernetes资源类型而异。所有资源类型都支持metadata.name和metadata.namespace字段。使用不受支持的字段选择器会产生错误。例如:

1
2
$ kubectl get ingress --field-selector foo.bar=baz
Error from server (BadRequest): Unable to find "ingresses" that match label selector "", field selector "foo.bar=baz": "foo.bar" is not a known field selector: only "metadata.name", "metadata.namespace"

支持操作

您可以使用=,==以及!=与现场选择操作(=和==意思是一样的)。kubectl例如,此命令选择不在default命名空间中的所有Kubernetes服务:

1
$ kubectl get services --field-selector metadata.namespace!=default

链式选择器

与标签和其他选择器一样,字段选择器可以作为逗号分隔列表链接在一起。此kubectl命令选择status.phase不相等Running且spec.restartPolicy字段等于的所有Pod Always:

1
$ kubectl get pods --field-selector=status.phase!=Running,spec.restartPolicy=Always

多种资源类型

您可以跨多种资源类型使用字段选择器。此kubectl命令选择不在default命名空间中的所有Statefulsets和Services :

1
$ kubectl get statefulsets,services --field-selector metadata.namespace!=default

Recommended Labels

您可以使用比kubectl和仪表板更多的工具来可视化和管理Kubernetes对象。一组通用的标签允许工具以互操作的方式工作,以所有工具都能理解的通用方式描述对象。

除支持工具外,推荐标签还以可查询的方式描述应用程序。

  • 标签
  • 应用程序和应用程序实例
  • 例子
    元数据围绕应用程序的概念进行组织。Kubernetes不是一个服务平台(PaaS),也没有或强制执行正式的应用程序概念。相反,应用程序是非正式的,并使用元数据进 应用程序包含的内容的定义是松散的。

注意:这些是推荐标签。它们使管理应用程序变得更容易,但对于任何核心工具都不是必需的。

共享标签和注释共享一个共同的前缀:app.kubernetes.io。没有前缀的标签对用户是私有的。共享前缀可确保共享标签不会干扰自定义用户标签。

标签

为了充分利用这些标签,应将它们应用于每个资源对象。

键 描述 例 类型
app.kubernetes.io/name 应用程序的名称 string mysql
app.kubernetes.io/instance 标识应用程序实例的唯一名称 string wordpress-abcxzy
app.kubernetes.io/version 应用程序的当前版本(例如,语义版本,修订版哈希等) string 5.7.21
app.kubernetes.io/component 架构中的组件 string database
app.kubernetes.io/part-of 此级别的更高级别应用程序的名称 string wordpress
app.kubernetes.io/managed-by 该工具用于管理应用程序的操作 string helm

要说明这些标签的运行情况,请考虑以下StatefulSet对象:

1
2
3
4
5
6
7
8
9
10
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
app.kubernetes.io/name: mysql
app.kubernetes.io/instance: wordpress-abcxzy
app.kubernetes.io/version: "5.7.21"
app.kubernetes.io/component: database
app.kubernetes.io/part-of: wordpress
app.kubernetes.io/managed-by: helm

应用程序和应用程序实例

应用程序可以一次或多次安装到Kubernetes集群中,在某些情况下,可以安装在同一名称空间中。例如,wordpress可以不止一次安装,其中不同的网站是wordpress的不同安装。

应用程序的名称和实例名称分别记录。例如,在WordPress具有app.kubernetes.io/name的wordpress,同时它有一个实例名,被表示为app.kubernetes.io/instance具有值 wordpress-abcxzy。这使得应用程序的应用程序和实例可以识别。应用程序的每个实例都必须具有唯一的名称。

例子

为了说明使用这些标签的不同方式,以下示例具有不同的复杂性。

一种简单的无状态服务

考虑使用Deployment和Service对象部署的简单无状态服务的情况。以下两个代码段表示如何以最简单的形式使用标签。

本Deployment是用来监督运行应用程序本身的豆荚。

1
2
3
4
5
6
7
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/name: myservice
app.kubernetes.io/instance: myservice-abcxzy
...

将Service用于公开应用程序。

1
2
3
4
5
6
7
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/name: myservice
app.kubernetes.io/instance: myservice-abcxzy
...

使用数据库的Web应用程序

考虑一个稍微复杂的应用程序:使用Helm安装的使用数据库(MySQL)的Web应用程序(WordPress)。以下代码段说明了用于部署此应用程序的对象的开始。

以下Deployment内容用于WordPress:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/name: wordpress
app.kubernetes.io/instance: wordpress-abcxzy
app.kubernetes.io/version: "4.9.4"
app.kubernetes.io/managed-by: helm
app.kubernetes.io/component: server
app.kubernetes.io/part-of: wordpress
...

将Service用于公开WordPress的:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/name: wordpress
app.kubernetes.io/instance: wordpress-abcxzy
app.kubernetes.io/version: "4.9.4"
app.kubernetes.io/managed-by: helm
app.kubernetes.io/component: server
app.kubernetes.io/part-of: wordpress
...

MySQL作为一个StatefulSet包含它的元数据和它所属的更大的应用程序公开:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
app.kubernetes.io/name: mysql
app.kubernetes.io/instance: wordpress-abcxzy
app.kubernetes.io/managed-by: helm
app.kubernetes.io/component: database
app.kubernetes.io/part-of: wordpress
app.kubernetes.io/version: "5.7.21"
...

将Service用于公开MySQL作为WordPress的部分:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/name: mysql
app.kubernetes.io/instance: wordpress-abcxzy
app.kubernetes.io/managed-by: helm
app.kubernetes.io/component: database
app.kubernetes.io/part-of: wordpress
app.kubernetes.io/version: "5.7.21"
...

使用MySQL StatefulSet,Service您会注意到有关MySQL和Wordpress的信息,包括更广泛的应用程序。

对象管理使用kubectl

Kubernetes对象管理

该kubectl命令行工具支持多种不同的方法来创建和管理Kubernetes对象。本文档概述了不同的方法。

  • 管理技巧
  • 命令式命令
  • 势在必行的对象配置
  • 声明性对象配置
管理技巧

警告:应仅使用一种技术管理Kubernetes对象。对同一对象的混合和匹配技术会导致未定义的行为。

Management technique 操作 推荐环境 Supported writers Learning curve
Imperative commands Live objects Development projects 1+ Lowest
Imperative object configuration Individual files Production projects 1 Moderate
Declarative object configuration Directories of files Production projects 1+ Highest
命令式命令

使用命令性命令时,用户直接在群集中的活动对象上操作。用户将kubectl命令的操作作为参数或标志提供。

这是在集群中启动或运行一次性任务的最简单方法。由于此技术直接在活动对象上运行,因此它不提供先前配置的历史记录。

例子

通过创建Deployment对象来运行nginx容器的实例:

1
kubectl run nginx --image nginx

使用不同的语法执行相同的操作:

1
kubectl create deployment nginx --image nginx

权衡

与对象配置相比的优点:

  • 命令简单易学,易记。
  • 命令只需要一个步骤即可对集群进行更改。

与对象配置相比的缺点:

  • 命令不与更改审核过程集成。
  • 命令不提供与更改关联的审计跟踪。
  • 除了活动之外,命令不提供记录源。
  • 命令不提供用于创建新对象的模板。
势在必行的对象配置

在命令式对象配置中,kubectl命令指定操作(创建,替换等),可选标志和至少一个文件名。指定的文件必须包含YAML或JSON格式的对象的完整定义。

有关 对象定义的更多详细信息,请参阅API参考。

警告:replace命令式命令将现有规范替换为新提供的规范,删除对配置文件中缺少的对象的所有更改。此方法不应与其配置文件独立更新的资源类型一起使用。LoadBalancer例如,类型的服务使其externalIPs字段独立于群集的配置而更新。

例子

创建配置文件中定义的对象:

1
kubectl create -f nginx.yaml

删除两个配置文件中定义的对象:

1
kubectl delete -f nginx.yaml -f redis.yaml

通过覆盖实时配置来更新配置文件中定义的对象:

1
kubectl replace -f nginx.yaml

权衡

与命令式命令相比的优点:

  • 对象配置可以存储在诸如Git的源控制系统中。
  • 对象配置可以与进程集成,例如在推送和审计跟踪之前查看更改。
  • 对象配置提供了用于创建新对象的模板。

与命令式命令相比的缺点:

  • 对象配置需要对对象模式有基本的了解。
  • 对象配置需要编写YAML文件的附加步骤。

与声明对象配置相比的优点:

  • 命令式对象配置行为更简单,更易于理解。
  • 从Kubernetes 1.5版开始,命令式对象配置更加成熟。

与声明对象配置相比的缺点:

  • 命令对象配置最适合文件,而不是目录。
  • 活动对象的更新必须反映在配置文件中,否则在下次更换时会丢失。
声明性对象配置

使用声明性对象配置时,用户对本地存储的对象配置文件进行操作,但是用户不定义要对文件执行的操作。每个对象自动检测创建,更新和删除操作kubectl。这使得能够处理目录,其中可能需要不同对象的不同操作。

注意:声明性对象配置保留其他编写者所做的更改,即使更改未合并回对象配置文件也是如此。这可以通过使用patchAPI操作来仅写入观察到的差异,而不是使用replace API操作来替换整个对象配置。

例子

处理目录中的所有对象配置文件configs,并创建或修补活动对象。您可以先diff查看要进行的更改,然后应用:

1
2
kubectl diff -f configs/
kubectl apply -f configs/

递归处理目录:

1
2
kubectl diff -R -f configs/
kubectl apply -R -f configs/

权衡

与命令式对象配置相比的优点:

  • 即使它们未合并回配置文件,也会保留直接对活动对象所做的更改。
  • 声明性对象配置更好地支持对目录进行操作并自动检测每个对象的操作类型(创建,修补,删除)。

与命令式对象配置相比的缺点:

  • 声明性对象配置更难以调试,并在意外时理解结果。
  • 使用diff的部分更新会创建复杂的合并和修补操作。

使用命令式命令管理Kubernetes对象

可以使用命令kubectl行工具中内置的命令性命令直接创建,更新和删除Kubernetes对象。本文档说明了如何组织这些命令以及如何使用它们来管理实时对象。

  • 权衡
  • 如何创建对象
  • 如何更新对象
  • 如何删除对象
  • 如何查看对象
  • 使用set命令在创建之前修改对象
  • 使用–edit修改之前创建的对象
权衡

该kubectl工具支持三种对象管理:

  • 命令式命令
  • 势在必行的对象配置
  • 声明性对象配置

有关每种对象管理 的优缺点的讨论,请参阅Kubernetes对象管理。

如何创建对象

该kubectl工具支持动词驱动的命令,用于创建一些最常见的对象类型。这些命令被命名为不熟悉Kubernetes对象类型的用户可识别。

  • run:创建一个新的Deployment对象以在一个或多个Pod中运行Container。
  • expose:创建一个新的服务对象,以跨Pod调整流量负载。
  • autoscale:创建新的Autoscaler对象以自动水平扩展控制器,例如部署。

该kubectl工具还支持由对象类型驱动的创建命令。这些命令支持更多对象类型,并且更明确地表达了它们的意图,但要求用户知道他们打算创建的对象的类型。

  • create <objecttype> [<subtype>] <instancename>

某些对象类型具有您可以在create命令中指定的子类型。例如,Service对象有几个子类型,包括ClusterIP,LoadBalancer和NodePort。这是一个使用子类型NodePort创建服务的示例:

1
kubectl create service nodeport <myservicename>

在前面的示例中,该create service nodeport命令称为命令的子create service命令。

您可以使用该-h标志来查找子命令支持的参数和标志:

1
kubectl create service nodeport -h

如何更新对象

该kubectl命令支持一些常见更新操作的动词驱动命令。命名这些命令是为了使不熟悉Kubernetes对象的用户能够在不知道必须设置的特定字段的情况下执行更新:

  • scale:通过更新控制器的副本计数,水平缩放控制器以添加或删除Pod。
  • annotate:在对象中添加或删除注释。
  • label:在对象中添加或删除标签。

该kubectl命令还支持由对象的一个方面驱动的更新命令。设置此方面可以为不同的对象类型设置不同的字段:

  • set :设置对象的一个方面。

注意:在Kubernetes 1.5版中,并非每个动词驱动的命令都有一个关联的方面驱动命令。

该kubectl工具支持这些直接更新实时对象的其他方法,但是它们需要更好地理解Kubernetes对象模式。

  • edit:通过在编辑器中打开其配置,直接编辑活动对象的原始配置。
  • patch:使用补丁字符串直接修改活动对象的特定字段。有关修补程序字符串的更多详细信息,请参阅API约定中的修补程序部分 。
如何删除对象

您可以使用该delete命令从群集中删除对象:

  • delete <type>/<name>

注意:您可以使用kubectl delete命令式命令和命令式对象配置。不同之处在于传递给命令的参数。要 kubectl delete用作命令性命令,请将要删除的对象作为参数传递。这是一个传递名为nginx的Deployment对象的示例:

1
kubectl delete deployment/nginx
如何查看对象

有几个命令用于打印有关对象的信息:

  • get:打印有关匹配对象的基本信息。使用get -h查看选项列表。
  • describe:打印有关匹配对象的聚合详细信息。
  • logs:为在Pod中运行的容器打印stdout和stderr。
使用set命令在创建之前修改对象

有些对象字段没有可在create命令中使用的标志。在一些案件中,可以使用的组合 set并create指定对象创建前场的值。这是通过将create命令的输出传递给 set命令,然后返回到create命令来完成的。这是一个例子:

1
kubectl create service clusterip my-svc --clusterip="None" -o yaml --dry-run | kubectl set selector --local -f - 'environment=qa' -o yaml | kubectl create -f -
  1. 该kubectl create service -o yaml --dry-run命令为服务创建配置,但将其作为YAML打印到stdout,而不是将其发送到Kubernetes API服务器。
  2. 该kubectl set selector --local -f - -o yaml命令从stdin读取配置,并将更新的配置作为YAML写入stdout。
  3. 该kubectl create -f -命令使用stdin提供的配置创建对象。
使用–edit修改之前创建的对象

您可以kubectl create --edit在创建对象之前对其进行任意更改。这是一个例子:

1
2
kubectl create service clusterip my-svc --clusterip="None" -o yaml --dry-run > /tmp/srv.yaml
kubectl create --edit -f /tmp/srv.yaml

该kubectl create service命令为服务创建配置并将其保存到/tmp/srv.yaml。
该kubectl create --edit命令在创建对象之前打开配置文件以进行编辑。

使用配置文件管理Kubernetes对象

可以使用kubectl 命令行工具以及使用YAML或JSON编写的对象配置文件来创建,更新和删除Kubernetes对象。本文档介绍了如何使用配置文件定义和管理对象。

  • 权衡
  • 如何创建对象
  • 如何更新对象
  • 如何删除对象
  • 如何查看对象
  • 限制
  • 在不保存配置的情况下从URL创建和编辑对象
  • 从命令式命令迁移到命令式对象配置
  • 定义控制器选择器和PodTemplate标签
权衡

该kubectl工具支持三种对象管理:

  • 命令式命令
  • 势在必行的对象配置
  • 声明性对象配置

有关每种对象管理 的优缺点的讨论,请参阅Kubernetes对象管理。

如何创建对象

您可以使用kubectl create -f从配置文件创建对象。 有关详细信息,请参阅kubernetes API参考。

  • kubectl create -f <filename|url>
如何更新对象

警告:使用该replace命令更新对象会删除配置文件中未指定的规范的所有部分。这不应该与规范部分由集群管理的对象一起使用,例如类型服务LoadBalancer,其中externalIPs字段独立于配置文件进行管理。必须将独立管理的字段复制到配置文件中以防止replace丢弃它们。

您可以使用kubectl replace -f根据配置文件更新活动对象。

  • kubectl replace -f <filename|url>
如何删除对象

您可以使用kubectl delete -f删除配置文件中描述的对象。

  • kubectl delete -f <filename|url>
如何查看对象

您可以使用它kubectl get -f来查看有关配置文件中描述的对象的信息。

  • kubectl get -f <filename|url> -o yaml

该-o yaml标志指定打印完整对象配置。使用kubectl get -h查看选项列表。

限制

create,replace和delete命令工作得很好,当每个对象的配置完全确定并记录在它的配置文件。但是,当更新活动对象并且更新未合并到其配置文件中时,更新将在下次replace 执行时丢失。如果控制器(例如HorizontalPodAutoscaler)直接对活动对象进行更新,则会发生这种情况。这是一个例子:

  1. 您可以从配置文件创建对象。
  2. 另一个源通过更改某个字段来更新对象。
  3. 您从配置文件中替换该对象。步骤2中其他来源所做的更改将丢失。

如果需要支持同一对象的多个编写器,则可以使用它kubectl apply来管理对象。

在不保存配置的情况下从URL创建和编辑对象

假设您具有对象配置文件的URL。您可以 kubectl create --edit在创建对象之前用于更改配置。这对于指向可由读者修改的配置文件的教程和任务特别有用。

1
kubectl create -f <url> --edit
从命令式命令迁移到命令式对象配置
  1. 从命令式命令迁移到命令式对象配置涉及几个手动步骤。
    将活动对象导出到本地对象配置文件:
1
kubectl get <kind>/<name> -o yaml --export > <kind>_<name>.yaml
  1. 从对象配置文件中手动删除状态字段。

  2. 对于后续对象管理,请replace专门使用。

1
kubectl replace -f <kind>_<name>.yaml
定义控制器选择器和PodTemplate标签

警告:强烈建议不要更新控制器上的选择器。

推荐的方法是定义一个仅由控制器选择器使用的单个不可变PodTemplate标签,没有其他语义含义。

示例标签:

1
2
3
4
5
6
7
selector:
matchLabels:
controller-selector: "extensions/v1beta1/deployment/nginx"
template:
metadata:
labels:
controller-selector: "extensions/v1beta1/deployment/nginx"

使用配置文件声明管理Kubernetes对象

可以通过在目录中存储多个对象配置文件并使用kubectl apply根据需要递归创建和更新这些对象来创建,更新和删除Kubernetes对象。此方法保留对活动对象的写入,而不将更改合并回对象配置文件。kubectl diff还可以预览apply将要进行的更改。

  • 权衡
  • 在你开始之前
  • 如何创建对象
  • 如何更新对象
  • 如何删除对象
  • 如何查看对象
  • 如何应用计算差异并合并更改
  • 默认字段值
  • 如何更改配置文件和直接命令式编写器之间字段的所有权
  • 改变管理方法
  • 定义控制器选择器和PodTemplate标签
权衡

该kubectl工具支持三种对象管理:

  • 命令式命令
  • 势在必行的对象配置
  • 声明性对象配置

有关每种对象管理 的优缺点的讨论,请参阅Kubernetes对象管理。

在你开始之前

声明性对象配置需要牢固地理解Kubernetes对象定义和配置。如果您还没有阅读并填写以下文件:

  • 使用命令式命令管理Kubernetes对象
  • 使用配置文件管理Kubernetes对象

以下是本文档中使用的术语的定义:

  • 对象配置文件/配置文件:定义Kubernetes对象配置的文件。本主题说明如何将配置文件传递给kubectl apply。配置文件通常存储在源代码管理中,例如Git。
  • 实时对象配置/实时配置:Kubernetes集群观察到的对象的实时配置值。这些保存在Kubernetes集群存储中,通常是etcd。
  • 声明性配置writer / declarative writer:对活动对象进行更新的人员或软件组件。本主题中提到的实时编写器会更改对象配置文件并运行kubectl apply以编写更改。
如何创建对象

使用kubectl apply创建的所有对象,除了那些已经存在,通过配置文件在指定的目录中定义:

1
kubectl apply -f <directory>/

这将kubectl.kubernetes.io/last-applied-configuration: '{...}'在每个对象上设置注释。注释包含用于创建对象的对象配置文件的内容。

注意:添加-R标志以递归处理目录。

以下是对象配置文件的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#application/simple_deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
minReadySeconds: 5
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80

运行kubectl diff以打印将要创建的对象:

1
kubectl diff -f https://k8s.io/examples/application/simple_deployment.yaml

注意:diff使用服务器端干运行,需要启用kube-apiserver。

使用kubectl apply以下方法创建对象

1
kubectl apply -f https://k8s.io/examples/application/simple_deployment.yaml

使用kubectl get以下方式打印实时配置

1
kubectl get -f https://k8s.io/examples/application/simple_deployment.yaml -o yaml

输出显示kubectl.kubernetes.io/last-applied-configuration注释已写入实时配置,并且与配置文件匹配:

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
kind: Deployment
metadata:
annotations:
# ...
# This is the json representation of simple_deployment.yaml
# It was written by kubectl apply when the object was created
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"apps/v1","kind":"Deployment",
"metadata":{"annotations":{},"name":"nginx-deployment","namespace":"default"},
"spec":{"minReadySeconds":5,"selector":{"matchLabels":{"app":nginx}},"template":{"metadata":{"labels":{"app":"nginx"}},
"spec":{"containers":[{"image":"nginx:1.7.9","name":"nginx",
"ports":[{"containerPort":80}]}]}}}}
# ...
spec:
# ...
minReadySeconds: 5
selector:
matchLabels:
# ...
app: nginx
template:
metadata:
# ...
labels:
app: nginx
spec:
containers:
- image: nginx:1.7.9
# ...
name: nginx
ports:
- containerPort: 80
# ...
# ...
# ...
# ...

如何更新对象

您还可以使用kubectl apply更新目录中定义的所有对象,即使这些对象已存在。此方法可实现以下目标:

  1. 设置实时配置中配置文件中显示的字段。
  2. 清除实时配置中从配置文件中删除的字段。
1
2
kubectl diff -f <directory>/
kubectl apply -f <directory>/

注意:添加-R标志以递归处理目录。

这是一个示例配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#application/simple_deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
minReadySeconds: 5
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80

使用kubectl apply以下方法创建对象

1
kubectl apply -f https://k8s.io/examples/application/simple_deployment.yaml

注意:出于说明的目的,上述命令引用单个配置文件而不是目录。

使用kubectl get以下方式打印实时配置

1
kubectl get -f https://k8s.io/examples/application/simple_deployment.yaml -o yaml

输出显示kubectl.kubernetes.io/last-applied-configuration注释已写入实时配置,并且与配置文件匹配:

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
kind: Deployment
metadata:
annotations:
# ...
# This is the json representation of simple_deployment.yaml
# It was written by kubectl apply when the object was created
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"apps/v1","kind":"Deployment",
"metadata":{"annotations":{},"name":"nginx-deployment","namespace":"default"},
"spec":{"minReadySeconds":5,"selector":{"matchLabels":{"app":nginx}},"template":{"metadata":{"labels":{"app":"nginx"}},
"spec":{"containers":[{"image":"nginx:1.7.9","name":"nginx",
"ports":[{"containerPort":80}]}]}}}}
# ...
spec:
# ...
minReadySeconds: 5
selector:
matchLabels:
# ...
app: nginx
template:
metadata:
# ...
labels:
app: nginx
spec:
containers:
- image: nginx:1.7.9
# ...
name: nginx
ports:
- containerPort: 80
# ...
# ...
# ...
# ...

使用,直接更新replicas实时配置中的字段kubectl scale。这不使用kubectl apply:

1
kubectl scale deployment/nginx-deployment --replicas=2

使用kubectl get以下方式打印实时配置

1
kubectl get -f https://k8s.io/examples/application/simple_deployment.yaml -o yaml

输出显示该replicas字段已设置为2,并且last-applied-configuration注释不包含replicas字段:

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
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
# ...
# note that the annotation does not contain replicas
# because it was not updated through apply
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"apps/v1","kind":"Deployment",
"metadata":{"annotations":{},"name":"nginx-deployment","namespace":"default"},
"spec":{"minReadySeconds":5,"selector":{"matchLabels":{"app":nginx}},"template":{"metadata":{"labels":{"app":"nginx"}},
"spec":{"containers":[{"image":"nginx:1.7.9","name":"nginx",
"ports":[{"containerPort":80}]}]}}}}
# ...
spec:
replicas: 2 # written by scale
# ...
minReadySeconds: 5
selector:
matchLabels:
# ...
app: nginx
template:
metadata:
# ...
labels:
app: nginx
spec:
containers:
- image: nginx:1.7.9
# ...
name: nginx
ports:
- containerPort: 80
# ...

更新simple_deployment.yaml配置文件以将映像更改 nginx:1.7.9为nginx:1.11.9,并删除该minReadySeconds字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#application/update_deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.11.9 # update the image
ports:
- containerPort: 80

应用对配置文件所做的更改:

1
2
kubectl diff -f https://k8s.io/examples/application/update_deployment.yaml
kubectl apply -f https://k8s.io/examples/application/update_deployment.yaml

使用kubectl get以下方式打印实时配置

1
kubectl get -f https://k8s.io/examples/application/simple_deployment.yaml -o yaml

输出显示实时配置的以下更改:

该replicas字段保留2的值kubectl scale。这是可能的,因为它从配置文件中省略。
该image场已被更新,以nginx:1.11.9从nginx:1.7.9。
该last-applied-configuration批注已经更新了新的形象。
该minReadySeconds领域已被清除。
该last-applied-configuration注释不再包含minReadySeconds字段。

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
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
# ...
# The annotation contains the updated image to nginx 1.11.9,
# but does not contain the updated replicas to 2
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"apps/v1","kind":"Deployment",
"metadata":{"annotations":{},"name":"nginx-deployment","namespace":"default"},
"spec":{"selector":{"matchLabels":{"app":nginx}},"template":{"metadata":{"labels":{"app":"nginx"}},
"spec":{"containers":[{"image":"nginx:1.11.9","name":"nginx",
"ports":[{"containerPort":80}]}]}}}}
# ...
spec:
replicas: 2 # Set by `kubectl scale`. Ignored by `kubectl apply`.
# minReadySeconds cleared by `kubectl apply`
# ...
selector:
matchLabels:
# ...
app: nginx
template:
metadata:
# ...
labels:
app: nginx
spec:
containers:
- image: nginx:1.11.9 # Set by `kubectl apply`
# ...
name: nginx
ports:
- containerPort: 80
# ...
# ...
# ...
# ...

警告:混合kubectl apply与势在必行对象配置命令 create和replace不支持。这是因为create 并replace没有保留kubectl.kubernetes.io/last-applied-configuration 的是kubectl apply用来计算更新。

如何删除对象

删除管理对象有两种方法kubectl apply。

推荐的: kubectl delete -f <filename>

建议的方法是使用命令式命令手动删除对象,因为它更明确地删除了什么,并且不太可能导致用户无意中删除了某些内容

1
kubectl delete -f <filename>

替代方案: kubectl apply -f <directory/> --prune -l your=label

只有在你知道自己在做什么的情况下才能使用它。

警告: kubectl apply --prune处于alpha状态,后续版本中可能会引入向后不兼容的更改。

警告:使用此命令时必须小心,以免意外删除对象。

作为替代方法kubectl delete,您可以使用它kubectl apply来识别从目录中删除配置文件后要删除的对象。--prune 对API服务器应用查询以匹配一组标签的所有对象,并尝试将返回的活动对象配置与对象配置文件进行匹配。如果对象与查询匹配,并且目录中没有配置文件,并且它具有last-applied-configuration注释,则会将其删除。

1
kubectl apply -f <directory/> --prune -l <labels>

警告:只应对包含对象配置文件的根目录运行prune。如果对象被指定的标签选择器查询返回-l 并且未出现在子目录中,则对子目录运行会导致无意中删除对象。

如何查看对象

您可以使用kubectl getwith -o yaml来查看活动对象的配置:

1
kubectl get -f <filename|url> -o yaml
如何应用计算差异并合并更改

注意:补丁是一种更新操作,其范围限定为对象的特定字段而不是整个对象。这样可以仅更新对象上的特定字段集,而无需先读取对象。

当kubectl apply一个对象更新实时配置,它通过发送补丁请求API服务器这样做。该补丁定义了作用于活动对象配置的特定字段的更新。该kubectl apply命令使用配置文件,实时配置和实时配置中last-applied-configuration存储的注释来计算此修补程序请求 。

合并补丁计算

该kubectl apply命令将配置文件的内容写入 kubectl.kubernetes.io/last-applied-configuration注释。这用于标识已从配置文件中删除的字段,需要从实时配置中清除。以下是用于计算应删除或设置哪些字段的步骤:

  1. 计算要删除的字段。这些是last-applied-configuration配置文件中存在和丢失的字段。
  2. 计算要添加或设置的字段。这些是配置文件中存在的字段,其值与实时配置不匹配。

这是一个例子。假设这是Deployment对象的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#application/update_deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.11.9 # update the image
ports:
- containerPort: 80

另外,假设这是同一Deployment对象的实时配置:

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
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
# ...
# note that the annotation does not contain replicas
# because it was not updated through apply
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"apps/v1","kind":"Deployment",
"metadata":{"annotations":{},"name":"nginx-deployment","namespace":"default"},
"spec":{"minReadySeconds":5,"selector":{"matchLabels":{"app":nginx}},"template":{"metadata":{"labels":{"app":"nginx"}},
"spec":{"containers":[{"image":"nginx:1.7.9","name":"nginx",
"ports":[{"containerPort":80}]}]}}}}
# ...
spec:
replicas: 2 # written by scale
# ...
minReadySeconds: 5
selector:
matchLabels:
# ...
app: nginx
template:
metadata:
# ...
labels:
app: nginx
spec:
containers:
- image: nginx:1.7.9
# ...
name: nginx
ports:
- containerPort: 80
# ...

以下是将通过以下方式执行的合并计算kubectl apply:

  1. 通过读取值last-applied-configuration并将它们与配置文件中的值进行比较来计算要删除的字段 。清除字段在本地对象配置文件中显式设置为null,无论它们是否出现在last-applied-configuration。在此示例中,minReadySeconds出现在 last-applied-configuration注释中,但未出现在配置文件中。 Action:minReadySeconds`从实时配置中清除。
  2. 通过从配置文件中读取值并将它们与实时配置中的值进行比较来计算要设置的字段。在此示例中,image配置文件中的值与实时配置中的值不匹配。Action:设置image实时配置中的值。
  3. 设置last-applied-configuration注释以匹配配置文件的值。
  4. 将来自1,2,3的结果合并到API服务器的单个补丁请求中。

以下是合并的实时配置:

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
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
# ...
# The annotation contains the updated image to nginx 1.11.9,
# but does not contain the updated replicas to 2
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"apps/v1","kind":"Deployment",
"metadata":{"annotations":{},"name":"nginx-deployment","namespace":"default"},
"spec":{"selector":{"matchLabels":{"app":nginx}},"template":{"metadata":{"labels":{"app":"nginx"}},
"spec":{"containers":[{"image":"nginx:1.11.9","name":"nginx",
"ports":[{"containerPort":80}]}]}}}}
# ...
spec:
selector:
matchLabels:
# ...
app: nginx
replicas: 2 # Set by `kubectl scale`. Ignored by `kubectl apply`.
# minReadySeconds cleared by `kubectl apply`
# ...
template:
metadata:
# ...
labels:
app: nginx
spec:
containers:
- image: nginx:1.11.9 # Set by `kubectl apply`
# ...
name: nginx
ports:
- containerPort: 80
# ...
# ...
# ...
# ...

如何合并不同类型的字段

配置文件中的特定字段如何与实时配置合并取决于字段的类型。有几种类型的字段:

  • primitive:字符串,整数或布尔类型的字段。例如,image和replicas是原始字段。行动:替换。
  • map,也称为object:类型为map的字段或包含子字段的复杂类型。例如labels, annotations,spec并且metadata是所有map。Action:合并元素或子字段。
  • list:包含可以是基本类型或映射的项列表的字段。例如containers,ports和args是列表。行动:变化。

当kubectl apply更新map或列表字段,它通常不更换整个领域,而是更新各个子元素。例如,在合并spec部署时,spec不会替换整个部署。相反,比较和合并spec诸如的子字段replicas。

将更改合并到基本字段

将更改合并到基本字段

注意: -用于“不适用”,因为未使用该值。

对象配置文件中的字段 实时对象配置中的字段 最后应用配置中的字段 行动
是 是 - 设置为配置文件值。
是 没有 - 将实时设置为本地配置。
没有 - - 从实时配置中清除。
没有 - 没有 没做什么。保持实时价值。
合并对地图字段的更改

通过比较地图的每个子字段或元素来合并表示地图的字段:

注意: -用于“不适用”,因为未使用该值。

键入对象配置文件 键入实时对象配置 最后应用配置中的字段 行动
是 是 - 比较子字段值。
是 没有 - 将实时设置为本地配置。
没有 - 是 从实时配置中删除。
没有 - 没有 没做什么。保持实时价值。
合并类型列表字段的更改

将更改合并到列表使用以下三种策略之一:

  • 替换列表。
  • 合并复杂元素列表中的各个元素。
  • 合并原始元素列表。

战略的选择是基于每个领域。

替换列表

将列表视为与原始字段相同。替换或删除整个列表。这保留了订购。

例如:使用kubectl apply更新args一个pod里的一个Container的field。这会将args实时配置中的值设置为配置文件中的值。args之前已添加到实时配置的任何元素都将丢失。args配置文件中定义的元素的顺序将保留在实时配置中。

1
2
3
4
5
6
7
8
9
10
11
# last-applied-configuration value
args: ["a", "b"]

# configuration file value
args: ["a", "c"]

# live configuration
args: ["a", "b", "d"]

# result after merge
args: ["a", "c"]

说明:合并使用配置文件值作为新列表值。

合并复杂元素列表中的各个元素:

将列表视为映射,并将每个元素的特定字段视为键。添加,删除或更新单个元素。这不会保留排序。

此合并策略在每个字段上使用一个名为a的特殊标记patchMergeKey。patchMergeKey是在Kubernetes源代码中的每个字段中定义: types.go 当合并映射的列表,指定的字段作为patchMergeKey对于给定的元素被用于像该元素的映射键。

例如:使用kubectl apply更新containers一PodSpec的field。这将列表合并为好像是每个元素都被键入的映射name。

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
# last-applied-configuration value
containers:
- name: nginx
image: nginx:1.10
- name: nginx-helper-a # key: nginx-helper-a; will be deleted in result
image: helper:1.3
- name: nginx-helper-b # key: nginx-helper-b; will be retained
image: helper:1.3

# configuration file value
containers:
- name: nginx
image: nginx:1.10
- name: nginx-helper-b
image: helper:1.3
- name: nginx-helper-c # key: nginx-helper-c; will be added in result
image: helper:1.3

# live configuration
containers:
- name: nginx
image: nginx:1.10
- name: nginx-helper-a
image: helper:1.3
- name: nginx-helper-b
image: helper:1.3
args: ["run"] # Field will be retained
- name: nginx-helper-d # key: nginx-helper-d; will be retained
image: helper:1.3

# result after merge
containers:
- name: nginx
image: nginx:1.10
# Element nginx-helper-a was deleted
- name: nginx-helper-b
image: helper:1.3
args: ["run"] # Field was retained
- name: nginx-helper-c # Element was added
image: helper:1.3
- name: nginx-helper-d # Element was ignored
image: helper:1.3

说明:

  • 名为“nginx-helper-a”的容器已删除,因为配置文件中没有出现名为“nginx-helper-a”的容器。
  • 名为“nginx-helper-b”的容器保留args 了实时配置中的更改。kubectl apply能够识别实时配置中的“nginx-helper-b”与配置文件中的“nginx-helper-b”相同,即使它们的字段具有不同的值(args配置文件中没有)。这是因为patchMergeKey字段值(名称)在两者中都是相同的。
  • 添加了名为“nginx-helper-c”的容器,因为实时配置中没有出现具有该名称的容器,但配置文件中出现了具有该名称的容器。
  • 保留名为“nginx-helper-d”的容器,因为在最后应用的配置中没有出现具有该名称的元素。
合并原始元素列表

从Kubernetes 1.5开始,不支持合并原始元素列表。

注意:为给定字段选择的上述策略中的哪一个由types.go中的patchStrategy标记控制。如果没有为类型列表的字段指定patchStrategy,则替换列表。

默认字段值

如果在创建对象时未指定某些字段,则API服务器会将某些字段设置为实时配置中的默认值。

这是部署的配置文件。该文件未指定strategy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#application/simple_deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
minReadySeconds: 5
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80

使用kubectl apply以下方法创建对象

1
kubectl apply -f https://k8s.io/examples/application/simple_deployment.yaml

使用kubectl get以下方式打印实时配置

1
kubectl get -f https://k8s.io/examples/application/simple_deployment.yaml -o yaml

输出显示API服务器在实时配置中将多个字段设置为默认值。配置文件中未指定这些字段。

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
apiVersion: apps/v1
kind: Deployment
# ...
spec:
selector:
matchLabels:
app: nginx
minReadySeconds: 5
replicas: 1 # defaulted by apiserver
strategy:
rollingUpdate: # defaulted by apiserver - derived from strategy.type
maxSurge: 1
maxUnavailable: 1
type: RollingUpdate # defaulted apiserver
template:
metadata:
creationTimestamp: null
labels:
app: nginx
spec:
containers:
- image: nginx:1.7.9
imagePullPolicy: IfNotPresent # defaulted by apiserver
name: nginx
ports:
- containerPort: 80
protocol: TCP # defaulted by apiserver
resources: {} # defaulted by apiserver
terminationMessagePath: /dev/termination-log # defaulted by apiserver
dnsPolicy: ClusterFirst # defaulted by apiserver
restartPolicy: Always # defaulted by apiserver
securityContext: {} # defaulted by apiserver
terminationGracePeriodSeconds: 30 # defaulted by apiserver
# ...

在修补程序请求中,默认字段不会被重新默认,除非它们作为修补程序请求的一部分被明确清除。这可能会导致基于其他字段的值默认的字段出现意外行为。稍后更改其他字段时,除非明确清除,否则不会更新默认值。

因此,建议在配置文件中显式定义服务器默认的某些字段,即使所需的值与服务器默认值匹配也是如此。这样可以更轻松地识别不会被服务器重新默认的冲突值。

例:

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
# last-applied-configuration
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80

# configuration file
spec:
strategy:
type: Recreate # updated value
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80

# live configuration
spec:
strategy:
type: RollingUpdate # defaulted value
rollingUpdate: # defaulted value derived from type
maxSurge : 1
maxUnavailable: 1
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80

# result after merge - ERROR!
spec:
strategy:
type: Recreate # updated value: incompatible with rollingUpdate
rollingUpdate: # defaulted value: incompatible with "type: Recreate"
maxSurge : 1
maxUnavailable: 1
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80

说明:

  1. 用户无需定义即可创建部署strategy.type。
  2. 服务器默认strategy.type为RollingUpdate默认 strategy.rollingUpdate值。
  3. 用户更改strategy.type为Recreate。该strategy.rollingUpdate值保持在其默认的值,但服务器期望他们被清除。如果strategy.rollingUpdate最初在配置文件中定义了值,则更清楚的是它们需要被删除。
  4. 应用失败,因为strategy.rollingUpdate未清除。该strategy.rollingupdate 字段不能与被定义strategy.type的Recreate。

建议:应在对象配置文件中明确定义这些字段:

  • 工作负载上的选择器和PodTemplate标签,例如Deployment,StatefulSet,Job,DaemonSet,ReplicaSet和ReplicationController
  • 部署部署策略
如何清除其他编写者设置的服务器默认字段或字段

可以通过将其值设置为null然后应用配置文件来清除未出现在配置文件中的字段。对于服务器默认的字段,这会触发重新默认值。

如何更改配置文件和直接命令式编写器之间字段的所有权

这些是您应该用来更改单个对象字段的唯一方法:

  • 使用kubectl apply。
  • 直接写入实时配置而不修改配置文件:例如,使用kubectl scale。
将所有者从直接命令式编写器更改为配置文件

将该字段添加到配置文件中。对于现场,停止对未经过的实时配置的直接更新kubectl apply。

将所有者从配置文件更改为直接命令式编写器

从Kubernetes 1.5开始,将字段的所有权从配置文件更改为命令式编写器需要手动步骤:

  • 从配置文件中删除该字段。
  • 从kubectl.kubernetes.io/last-applied-configuration活动对象上的注释中删除该字段。
改变管理方法

应该一次只使用一种方法管理Kubernetes对象。可以从一种方法切换到另一种方法,但这是一种手动过程。

注意:使用命令式删除和声明式管理是可以的。

从命令式命令管理迁移到声明性对象配置

从命令式命令管理迁移到声明性对象配置涉及几个手动步骤:

  1. 将活动对象导出到本地配置文件:

    1
    kubectl get <kind>/<name> -o yaml --export > <kind>_<name>.yaml
  2. status从配置文件中手动删除该字段。

注意:此步骤是可选的,因为kubectl apply不会更新状态字段 即使它存在于配置文件中。

  1. kubectl.kubernetes.io/last-applied-configuration在对象上设置注释:

    1
    kubectl replace --save-config -f <kind>_<name>.yaml
  2. 更改kubectl apply用于专门管理对象的进程。

从命令式对象配置迁移到声明性对象配置
  1. kubectl.kubernetes.io/last-applied-configuration在对象上设置注释:

    1
    kubectl replace --save-config -f <kind>_<name>.yaml
  2. 更改kubectl apply用于专门管理对象的进程。

定义控制器选择器和PodTemplate标签

警告:强烈建议不要更新控制器上的选择器。

推荐的方法是定义一个仅由控制器选择器使用的单个不可变PodTemplate标签,没有其他语义含义。

例:

1
2
3
4
5
6
7
selector:
matchLabels:
controller-selector: "extensions/v1beta1/deployment/nginx"
template:
metadata:
labels:
controller-selector: "extensions/v1beta1/deployment/nginx"

Kubernetes Architecture

节点

节点是Kubernetes中的工作机器,以前称为一个 minion。节点可以是VM或物理机,具体取决于集群。每个节点都包含运行pods所需的服务,并由主组件管理。节点上的服务包括container runtime,kubelet和kube-proxy。有关更多详细信息,请参阅 体系结构设计文档中的Kubernetes节点部分。

  • 节点状态
  • 管理
  • API对象

节点状态

节点的状态包含以下信息:

  • 地址
  • 条件
  • 容量
  • 信息

下面详细描述每个部分。

地址

这些字段的使用取决于您的云提供商或裸机配置。

  • HostName:节点内核报告的主机名。可以通过kubelet--hostname-override参数覆盖。
  • ExternalIP:通常是可从外部路由的节点的IP地址(可从群集外部获得)。
  • InternalIP:通常仅在群集内可路由的节点的IP地址。
条件

该conditions字段描述了所有Running节点的状态。

节点条件 描述
OutOfDisk True 如果节点上的可用空间不足以添加新的pod,否则 False
Ready True如果节点是健康的并准备好接受pod,False如果节点不健康且不接受pod,并且Unknown节点控制器在最后一次没有从节点听到node-monitor-grace-period(默认为40秒)
MemoryPressure True如果节点存储器上存在压力 - 即节点存储器是否为低; 除此以外False
PIDPressure True如果进程存在压力 - 也就是说,如果节点上有太多进程; 除此以外False
DiskPressure True如果磁盘大小存在压力 - 即磁盘容量低; 除此以外False
NetworkUnavailable True 如果没有正确配置节点的网络,否则 False

节点条件表示为JSON对象。例如,以下响应描述了健康节点。

1
2
3
4
5
6
"conditions": [
{
"type": "Ready",
"status": "True"
}
]

如果就绪状态的状态保持Unknown或False超过pod-eviction-timeout,则会将参数传递给kube-controller-manager,并且节点控制器会调度节点上的所有Pod以进行删除。默认逐出超时持续时间为五分钟。在某些情况下,当节点无法访问时,apiserver无法与节点上的kubelet通信。在重新建立与apiserver的通信之前,不能将删除pod的决定传送到kubelet。同时,计划删除的pod可以继续在分区节点上运行。

在1.5之前的Kubernetes版本中,节点控制器会从apiserver中强制删除这些无法访问的pod。但是,在1.5及更高版本中,节点控制器不会强制删除容器,直到确认它们已停止在群集中运行。您可以看到可能在无法访问的节点上运行的Pod处于Terminating或Unknown状态。如果节点永久离开群集,如果Kubernetes无法从底层基础架构推断出,则群集管理员可能需要手动删除节点对象。从Kubernetes中删除节点对象会导致节点上运行的所有Pod对象从apiserver中删除,并释放它们的名称。

在版本1.12中,TaintNodesByCondition功能被提升为beta版,因此节点生命周期控制器会自动创建表示条件的 taints 。类似地,调度程序在考虑节点时忽略条件;相反,它会查看Node的污点和Pod的容忍度。 现在,用户可以在旧的调度模型和更灵活的新调度模型之间进行选择。根据旧型号,可以安排没有任何容忍度的Pod。但是可以在该节点上安排容忍特定节点的污点的Pod。

警告:启用此功能会在观察到条件和创建污点之间产生一个小延迟。此延迟通常小于一秒,但它可以增加成功安排但被kubelet拒绝的Pod的数量。

容量

描述节点上可用的资源:CPU,内存以及可以在节点上调度的最大pod数。

信息

有关节点的一般信息,例如内核版本,Kubernetes版本(kubelet和kube-proxy版本),Docker版本(如果使用),操作系统名称。信息由Kubelet从节点收集。

管理

与pod和服务不同,Kubernetes本身并不创建节点:它由Google Compute Engine等云提供商在外部创建,或者存在于物理或虚拟机池中。因此,当Kubernetes创建节点时,它会创建一个表示节点的对象。创建后,Kubernetes会检查节点是否有效。例如,如果您尝试从以下内容创建节点:

1
2
3
4
5
6
7
8
9
10
{
"kind": "Node",
"apiVersion": "v1",
"metadata": {
"name": "10.240.79.157",
"labels": {
"name": "my-first-k8s-node"
}
}
}

Kubernetes在内部创建节点对象(表示),并通过基于metadata.name字段的运行状况检查来验证节点。如果节点有效 - 即,如果所有必需的服务都在运行 - 它有资格运行pod。否则,对于任何群集活动,它将被忽略,直到它变为有效。

注意: Kubernetes保留无效节点的对象,并不断检查它是否有效。您必须显式删除Node对象才能停止此过程。

目前,有三个组件与Kubernetes节点接口交互:节点控制器,kubelet和kubectl。

节点控制器

节点控制器是Kubernetes主组件,它管理节点的各个方面。

节点控制器在节点的生命周期中具有多个角色。第一种是在注册时为节点分配CIDR块(如果打开了CIDR分配)。

第二个是使节点控制器的内部节点列表与云提供商的可用计算机列表保持同步。在云环境中运行时,只要节点不健康,节点控制器就会询问云提供商该节点的VM是否仍然可用。如果不是,则节点控制器从其节点列表中删除该节点。

第三是监测节点的健康状况。节点控制器负责在节点变得无法访问时将NodeStatus的NodeReady条件更新为ConditionUnknown(即节点控制器由于某种原因停止接收心跳,例如由于节点关闭),然后从节点中驱逐所有pod (如果节点仍然无法访问,则使用正常终止)。(默认超时为40 --node-monitor-period秒,开始报告ConditionUnknown,之后5米开始驱逐pod。)节点控制器每秒检查每个节点的状态。

在1.13之前的Kubernetes版本中,NodeStatus是节点的心跳。从Kubernetes 1.13开始,节点租用功能作为alpha功能引入(功能门NodeLease, KEP-0009)。启用节点租用功能时,每个节点都有一个关联的Lease对象 kube-node-lease由节点定期更新的命名空间,NodeStatus和节点租约都被视为来自节点的心跳。节点租约经常更新,而NodeStatus仅在有一些更改或经过足够时间时从节点报告为主节点(默认值为1分钟,这比不可达节点的默认超时40秒)。由于节点租约比NodeStatus轻得多,因此从可伸缩性和性能角度来看,此功能使节点心跳显着降低。

在Kubernetes 1.4中,我们更新了节点控制器的逻辑,以便在大量节点到达主站时遇到问题时更好地处理案例(例如,因为主站有网络问题)。从1.4开始,节点控制器在决定pod驱逐时查看集群中所有节点的状态。

在大多数情况下,节点控制器将驱逐率限制为每秒 --node-eviction-rate(默认值0.1),这意味着它不会每10秒从多个节点驱逐pod。

当给定可用区中的节点变得不健康时,节点逐出行为会发生变化。节点控制器同时检查区域中节点的百分比是否不健康(NodeReady条件是ConditionUnknown或ConditionFalse)。如果不健康节点的比例至少为 --unhealthy-zone-threshold(默认为0.55),则驱逐率降低:如果群集较小(即小于或等于--large-cluster-size-threshold节点 - 默认为50)则停止驱逐,否则驱逐率降低为 --secondary-node-eviction-rate(默认0.01)每秒。每个可用区域实施这些策略的原因是因为一个可用区域可能从主服务器分区而其他可用区域保持连接。如果您的群集未跨越多个云提供商可用区域,则只有一个可用区域(整个群集)。

在可用区域之间传播节点的一个关键原因是,当整个区域出现故障时,工作负载可以转移到健康区域。因此,如果区域中的所有节点都不健康,则节点控制器以正常速率驱逐--node-eviction-rate。角落情况是所有区域完全不健康(即群集中没有健康的节点)。在这种情况下,节点控制器假定主连接存在一些问题,并在某些连接恢复之前停止所有驱逐。

从Kubernetes 1.6开始,NodeController还负责驱逐在具有NoExecute污点的节点上运行的pod,当pod不能容忍taints时。此外,作为默认禁用的alpha功能,NodeController负责添加与节点无法访问或未就绪等节点问题相对应的污点。 有关污点和alpha功能的详细信息,请参阅此文档NoExecute。

从版本1.8开始,节点控制器可以负责创建表示节点条件的污点。这是1.8版的alpha功能。

节点自注册

当kubelet标志--register-node为true(默认值)时,kubelet将尝试向API服务器注册自己。这是大多数发行版使用的首选模式。

对于自行注册,可以使用以下选项启动kubelet:

  • --kubeconfig - 凭证路径,以向apiserver验证自身。
  • --cloud-provider - 如何与云提供商交谈以阅读有关自身的元数据。
  • --register-node - 自动注册API服务器。
  • --register-with-taints- 使用给定的taints列表注册节点(以逗号分隔<key>=<value>:<effect>)。No-op如果register-node是假的。
  • --node-ip - 节点的IP地址。
  • --node-labels- 在群集中注册节点时添加的标签(请参阅1.13+中NodeRestriction准入插件强制执行的标签限制)。
  • --node-status-update-frequency - 指定kubelet将节点状态发布到master的频率。

当节点授权模式和 NodeRestriction录取插件的启用,kubelets仅被授权创建/修改自己的节点资源。

手动节点管理

集群管理员可以创建和修改节点对象。

如果管理员希望手动创建节点对象,请设置kubelet标志 --register-node=false。

管理员可以修改节点资源(无论设置如何--register-node)。修改包括在节点上设置标签并将其标记为不可调度。

节点上的标签可以与pod上的节点选择器结合使用以控制调度,例如,将pod限制为仅有资格在节点的子集上运行。

将节点标记为不可调度可防止将新pod调度到该节点,但不会影响节点上的任何现有pod。这在节点重启等之前作为准备步骤很有用。例如,要标记节点不可调度,请运行以下命令:

1
kubectl cordon $NODENAME

注意:由DaemonSet控制器创建的Pod绕过Kubernetes调度程序,不遵守节点上的不可调度属性。这假设守护进程属于机器,即使它在准备重新启动时正在耗尽应用程序。

节点容量

节点的容量(cpus的数量和内存量)是节点对象的一部分。通常,节点在创建节点对象时注册自己并报告其容量。如果您正在进行手动节点管理,则需要在添加节点时设置节点容量。

Kubernetes调度程序确保节点上的所有pod都有足够的资源。它检查节点上容器请求的总和不大于节点容量。它包括由kubelet启动的所有容器,但不包括由容器运行时直接启动的容器,也不包括在容器外部运行的任何进程。

如果要为非Pod进程显式保留资源,请按照本教程 为系统守护程序保留资源。

API对象

Node是Kubernetes REST API中的顶级资源。有关API对象的更多详细信息,请参见: Node API对象。

主节点通信

本文档对master(实际上是apiserver)和Kubernetes集群之间的通信路径进行了编目。目的是允许用户自定义其安装以强化网络配置,以便群集可以在不受信任的网络(或云提供商上的完全公共IP)上运行。

  • 群集到Master
  • 掌握群集

群集到Master

从集群到主服务器的所有通信路径都在apiserver处终止(其他主服务器组件均未设计为公开远程服务)。在典型部署中,apiserver被配置为在安全HTTPS端口(443)上侦听远程连接,其中启用了一种或多种形式的客户端认证。 应启用一种或多种授权形式,尤其是 在允许匿名请求 或服务帐户令牌的情况下。

应为节点配置群集的公共根证书,以便它们可以安全地连接到apiserver以及有效的客户端凭据。例如,在默认GKE部署中,提供给kubelet的客户端凭证采用客户端证书的形式。请参阅 kubelet TLS bootstrapping 以自动配置kubelet客户端证书。

希望连接到apiserver的Pod可以通过利用服务帐户安全地执行此操作,以便Kubernetes在实例化时自动将公共根证书和有效的承载令牌注入到pod中。该kubernetes服务(在所有名称空间中)配置有虚拟IP地址,该地址被重定向(通过kube-proxy)到apiserver上的HTTPS端点。

主组件还通过安全端口与群集服务器通信。

因此,默认情况下,从群集(节点和节点上运行的节点)到主节点的连接的默认操作模式是安全的,可以在不受信任和/或公共网络上运行。

掌握群集

从主服务器(apiserver)到集群有两条主要通信路径。第一个是从apiserver到kubelet进程,它在集群中的每个节点上运行。第二种是通过apiserver的代理功能从apiserver到任何节点,pod或服务。

kubelet的保护者

从apiserver到kubelet的连接用于:

  • 获取pod的日志。
  • 附加(通过kubectl)到运行的pod。
  • 提供kubelet的端口转发功能。

这些连接终止于kubelet的HTTPS端点。默认情况下,apiserver不会验证kubelet的服务证书,这会使连接受到中间人攻击,并且 不安全地运行在不受信任的和/或公共网络上。

要验证此连接,请使用该--kubelet-certificate-authority标志为apiserver提供根证书包,以用于验证kubelet的服务证书。

如果无法做到这一点,请 在apiserver和kubelet之间使用SSH隧道,以避免连接不受信任或公共网络。

最后, 应启用Kubelet身份验证和/或授权以保护kubelet API。

节点,pod和服务的apiserver

从apiserver到节点,pod或服务的连接默认为纯HTTP连接,因此既未经过身份验证也未加密。它们可以通过前缀https:到API URL中的节点,pod或服务名称在安全HTTPS连接上运行,但它们不会验证HTTPS端点提供的证书,也不会提供客户端凭据,因此在连接将被加密时,它不会提供任何诚信保证。这些连接目前在不受信任和/或公共网络上运行是不安全的。

云控制器管理器的基础概念

最初创建云控制器管理器(CCM)概念(不要与二进制混淆),以允许特定于云的供应商代码和Kubernetes核心彼此独立地发展。云控制器管理器与其他主组件(如Kubernetes控制器管理器,API服务器和调度程序)一起运行。它也可以作为Kubernetes插件启动,在这种情况下它运行在Kubernetes之上。

云控制器管理器的设计基于一种插件机制,允许新的云提供商通过使用插件轻松地与Kubernetes集成。有计划在Kubernetes上加入新的云提供商,以及将云提供商从旧模型迁移到新的CCM模型。

本文档讨论了云控制器管理器背后的概念,并提供了有关其相关功能的详细信息。

这是没有云控制器管理器的Kubernetes集群的架构:

image

  • 设计
  • CCM的组成部分
  • CCM的功能
  • 插件机制
  • 授权
  • 供应商实施
  • 群集管理

设计

在上图中,Kubernetes和云提供商通过几个不同的组件集成:

  • Kubelet
  • Kubernetes控制器经理
  • Kubernetes API服务器

CCM整合了前三个组件中的所有依赖于云的逻辑,以创建与云的单一集成点。CCM的新架构如下所示:
image

CCM的组成部分

CCM打破了Kubernetes控制器管理器(KCM)的一些功能,并将其作为一个单独的进程运行。具体来说,它打破了KCM中依赖于云的控制器。KCM具有以下依赖于云的控制器循环:

  • 节点控制器
  • 音量控制器
  • 路线控制器
  • 服务控制器

在1.9版中,CCM运行前面列表中的以下控制器:

  • 节点控制器
  • 路线控制器
  • 服务控制器

此外,它还运行另一个名为PersistentVolumeLabels控制器的控制器。此控制器负责在GCP和AWS云中创建的PersistentVolumes上设置区域和区域标签。

注意:故意选择音量控制器不属于CCM。由于涉及复杂性并且由于现有的努力抽象出供应商特定的卷逻辑,因此决定不将卷控制器移动到CCM。

使用CCM支持卷的最初计划是使用Flex卷来支持可插拔卷。然而,正在计划一项名为CSI的竞争性工作来取代Flex。

考虑到这些动态,我们决定在CSI准备好之前进行中间止差测量。

CCM的功能

CCM从依赖于云提供商的Kubernetes组件继承其功能。本节基于这些组件构建。

1. Kubernetes控制器经理

CCM的大部分功能来自KCM。如上一节所述,CCM运行以下控制循环:

  • 节点控制器
  • 路线控制器
  • 服务控制器
  • PersistentVolumeLabels控制器
节点控制器

节点控制器负责通过从云提供商获取有关在集群中运行的节点的信息来初始化节点。节点控制器执行以下功能:

  1. 使用特定于云的区域/区域标签初始化节点。
  2. 使用特定于云的实例详细信息初始化节点,例如,类型和大小。
  3. 获取节点的网络地址和主机名。
  4. 如果节点无响应,请检查云以查看该节点是否已从云中删除。如果已从云中删除该节点,请删除Kubernetes Node对象。
路线控制器

Route控制器负责适当地配置云中的路由,以便Kubernetes集群中不同节点上的容器可以相互通信。路径控制器仅适用于Google Compute Engine群集。

服务控制器

服务控制器负责监听服务创建,更新和删除事件。根据Kubernetes中当前的服务状态,它配置云负载均衡器(如ELB或Google LB)以反映Kubernetes中的服务状态。此外,它还确保云负载平衡器的服务后端是最新的。

PersistentVolumeLabels控制器

PersistentVolumeLabels控制器在创建AWS EBS / GCE PD卷时应用标签。这消除了用户手动设置这些卷上的标签的需要。

这些标签对于pod的计划至关重要,因为这些卷仅限于在它们所在的区域/区域内工作。使用这些卷的任何Pod都需要在同一区域/区域中进行调度。

PersistentVolumeLabels控制器专门为CCM创建; 也就是说,在创建CCM之前它不存在。这样做是为了将Kubernetes API服务器(它是一个许可控制器)中的PV标记逻辑移动到CCM。它不在KCM上运行。

2. Kubelet

节点控制器包含kubelet的依赖于云的功能。在引入CCM之前,kubelet负责使用特定于云的详细信息(如IP地址,区域/区域标签和实例类型信息)初始化节点。CCM的引入已将此初始化操作从kubelet转移到CCM。

在这个新模型中,kubelet初始化一个没有特定于云的信息的节点。但是,它会为新创建的节点添加污点,使节点不可调度,直到CCM使用特定于云的信息初始化节点。然后它消除了这种污点。

3. Kubernetes API服务器

PersistentVolumeLabels控制器将Kubernetes API服务器的依赖于云的功能移动到CCM,如前面部分所述。

插件机制

云控制器管理器使用Go接口允许插入任何云的实现。具体来说,它使用此处定义的CloudProvider接口。

上面突出显示的四个共享控制器的实现,以及一些脚手架以及共享的cloudprovider接口,将保留在Kubernetes核心中。特定于云提供商的实现将在核心之外构建,并实现核心中定义的接口。

有关开发插件的更多信息,请参阅开发Cloud Controller Manager。

授权

本节分解了CCM执行其操作时各种API对象所需的访问权限。

节点控制器

Node控制器仅适用于Node对象。它需要完全访问get,list,create,update,patch,watch和delete Node对象。

V1 /节点:

  • Get
  • List
  • Create
  • Update
  • Patch
  • Watch
  • Delete
路线控制器

路由控制器侦听Node对象创建并适当地配置路由。它需要访问Node对象。

V1 /节点:

  • Get
服务控制器

服务控制器侦听Service对象创建,更新和删除事件,然后适当地为这些服务配置端点。

要访问服务,它需要列表和监视访问权限。要更新服务,它需要修补和更新访问权限。

要为服务设置端点,需要访问create,list,get,watch和update。

V1 /服务:

  • List
  • Get
  • Watch
  • Patch
  • Update
PersistentVolumeLabels控制器

PersistentVolumeLabels控制器侦听PersistentVolume(PV)创建事件,然后更新它们。该控制器需要访问以获取和更新PV。

V1 / PersistentVolume:

  • Get
  • List
  • Watch
  • Update
其他

CCM核心的实现需要访问以创建事件,并且为了确保安全操作,它需要访问以创建ServiceAccounts。

V1 /事件:

  • Create
  • Patch
  • Update

V1 / ServiceAccount:

  • Create

CCM的RBAC ClusterRole如下所示:

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
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cloud-controller-manager
rules:
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- update
- apiGroups:
- ""
resources:
- nodes
verbs:
- '*'
- apiGroups:
- ""
resources:
- nodes/status
verbs:
- patch
- apiGroups:
- ""
resources:
- services
verbs:
- list
- patch
- update
- watch
- apiGroups:
- ""
resources:
- serviceaccounts
verbs:
- create
- apiGroups:
- ""
resources:
- persistentvolumes
verbs:
- get
- list
- update
- watch
- apiGroups:
- ""
resources:
- endpoints
verbs:
- create
- get
- list
- watch
- update

供应商实施

以下云提供商已实施CCM:

  • Digital Ocean
  • Oracle
  • Azure
  • GCE
  • AWS

群集管理

此处提供了有关配置和运行CCM的完整说明

容器

镜像

您创建Docker镜像并将其推送到仓库,然后在Kubernetes的pod中引用它。

image容器的属性支持与docker命令相同的语法,包括私有仓库和标记。

  • 更新镜像
  • 用清单构建多架构镜像
  • 使用私人仓库

更新镜像

默认拉取策略IfNotPresent会导致Kubelet跳过拉动镜像(如果已存在)。如果您想总是强制Docker拉动,可以执行以下操作之一:

  • 将imagePullPolicy容器设置为Always。
  • 省略imagePullPolicy并使用它:latest作为要使用的镜像的标记。
  • 省略imagePullPolicy要使用的镜像和标记。
  • 启用AlwaysPullImages准入控制器。

请注意,您应该避免使用:latest标记,有关详细信息,请参阅配置的最佳实践。

用清单构建多架构图像

Docker CLI现在支持以下命令docker manifest中包含的子命令create,annotate并push。这些命令可用于构建和推送清单。您可以使用docker manifest inspect查看清单。

请在此处查看docker文档,
请参阅我们在构建工具中如何使用它的示例&i=nope&files=&repos=)。

这些命令完全依赖于Docker CLI,并且完全在Docker CLI上实现。您需要编辑$HOME/.docker/config.json和设置experimental密钥,enabled或者只需在调用CLI命令时将DOCKER_CLI_EXPERIMENTAL环境变量设置为enabled。

注意:请使用Docker 18.06或更高版本,以下版本有错误或不支持实验命令行选项。示例会在containerd下导致问题。

如果您在上传陈旧的清单时遇到问题,只需清理旧的清单$HOME/.docker/manifests即可重新开始。

对于Kubernetes,我们通常使用带后缀的镜像-$(ARCH)。为了向后兼容,请生成带有后缀的旧镜像。我们的想法是生成pause具有所有拱形清单的说明镜像,并说出pause-amd64哪些向后兼容旧配置或YAML文件,这些文件可能硬编码带有后缀的镜像。

使用私人仓库

私人仓库管理可能需要密钥才能从中读取图像。凭证可以通过多种方式提供:

  • 使用Google Container Registry
    • Per-cluster
    • 在Google Compute Engine或Google Kubernetes Engine上自动配置
    • 所有pod都可以读取项目的私有仓库
  • 使用AWS EC2容器仓库(ECR)
    • 使用IAM角色和策略来控制对ECR存储库的访问
    • 自动刷新ECR登录凭据
  • 使用Azure容器仓库(ACR)
  • 使用IBM Cloud Container Registry
  • 配置节点以验证私有仓库
    • 所有pod都可以读取任何已配置的私有仓库
    • 需要集群管理员进行节点配置
  • 预拉镜像
    • 所有pod都可以使用节点上缓存的任何镜像
    • 需要root权限才能设置所有节点
  • 在Pod上指定ImagePullSecrets
    • 只有提供自己密钥的pod才能访问私有仓库

下面更详细地描述每个选项。

使用Google Container Registry

在Google Compute Engine(GCE)上运行时,Kubernetes对Google Container Registry(GCR)提供原生支持。如果您在GCE或Google Kubernetes Engine上运行群集,只需使用完整的镜像名称(例如gcr.io/my_project/image:tag)。

群集中的所有pod都具有此仓库中镜像的读取权限。

kubelet将使用实例的Google服务帐户向GCR进行身份验证。实例上的服务帐户将具有一个 https://www.googleapis.com/auth/devstorage.read_only,因此它可以从项目的GCR中提取,但不能推送。

使用AWS EC2 Container Registry

当节点是AWS EC2实例时,Kubernetes对AWS EC2 Container Registry具有本机支持。

只需ACCOUNT.dkr.ecr.REGION.amazonaws.com/imagename:tag在Pod定义中使用完整的图像名称(例如)。

可以创建pod的群集的所有用户都可以运行使用ECR仓库中任何镜像的pod。

kubelet将获取并定期刷新ECR凭据。它需要以下权限才能执行此操作:

  • ecr:GetAuthorizationToken
  • ecr:BatchCheckLayerAvailability
  • ecr:GetDownloadUrlForLayer
  • ecr:GetRepositoryPolicy
  • ecr:DescribeRepositories
  • ecr:ListImages
  • ecr:BatchGetImage

要求:

  • 您必须使用kubelet版本v1.2.0或更新版本。(例如run /usr/bin/kubelet --version=true)。
  • 如果您的节点位于区域A中且您的注册表位于不同的区域B中,则需要v1.3.0更新版本或更新版本。
  • ECR必须在您所在的地区提供

故障排除:

  • 验证上述所有要求。
  • us-west-2在工作站上获取$ REGION(例如)凭据。SSH进入主机并使用这些信用卡手动运行Docker。它有用吗?
  • 验证kubelet是否正在运行--cloud-provider=aws。
  • 检查kubelet日志(例如journalctl -u kubelet)以获取日志行,例如:
    • plugins.go:56] Registering credential provider: aws-ecr-key
    • provider.go:91] Refreshing cache for provider: *aws_credentials.ecrProvider
使用Azure容器仓库(ACR)

使用Azure容器仓库时, 您可以使用管理员用户或服务主体进行身份验证。在任何一种情况下,身份验证都通过标准Docker身份验证完成 这些说明假定使用 azure-cli命令行工具。

您首先需要创建一个仓库并生成凭据,完整的文档可以在Azure容器仓库文档中找到。

创建容器仓库后,您将使用以下凭据登录:

  • DOCKER_USER :服务主体或管理员用户名
  • DOCKER_PASSWORD:服务主体密码或管理员用户密码
  • DOCKER_REGISTRY_SERVER: ${some-registry-name}.azurecr.io
  • DOCKER_EMAIL:${some-email-address}

填好这些变量后,您可以配置Kubernetes Secret并使用它来部署Pod。

使用IBM Cloud Container Registry

IBM Cloud Container Registry提供了一个多租户私有镜像仓库,您可以使用它来安全地存储和共享Docker镜像。默认情况下,集成的漏洞顾问会扫描私有仓库中的镜像,以检测安全问题和潜在漏洞。IBM Cloud帐户中的用户可以访问您的镜像,也可以创建令牌以授予对仓库命名空间的访问权限。

要安装IBM Cloud Container Registry CLI插件并为镜像创建命名空间,请参阅IBM Cloud Container Registry入门。

您可以使用IBM Cloud Container Registry将容器从IBM Cloud公共镜像和私有镜像部署到defaultIBM Cloud Kubernetes Service集群的命名空间中。要将容器部署到其他名称空间,或使用来自其他IBM Cloud Container Registry区域或IBM Cloud帐户的镜像,请创建Kubernetes imagePullSecret。有关更多信息,请参阅从镜像构建容器。

配置节点以验证私有仓库

注意:如果您在Google Kubernetes Engine上运行,则.dockercfg每个节点上都会有一个包含Google Container Registry凭据的节点。你不能使用这种方法。

注意:如果您在AWS EC2上运行并且正在使用EC2容器仓库(ECR),则每个节点上的kubelet将管理和更新ECR登录凭据。你不能使用这种方法

注意:如果您可以控制节点配置,则此方法是合适的。它不能可靠地在GCE和任何其他进行自动节点替换的云提供商上运行。

Docker将私有仓库的密钥存储在$HOME/.dockercfg或$HOME/.docker/config.json文件中。如果您将相同的文件放在下面的搜索路径列表中,则kubelet会在拉取镜像时将其用作凭据提供程序。

  • {--root-dir:-/var/lib/kubelet}/config.json
  • {cwd of kubelet}/config.json
  • ${HOME}/.docker/config.json
  • /.docker/config.json
  • {--root-dir:-/var/lib/kubelet}/.dockercfg
  • {cwd of kubelet}/.dockercfg
  • ${HOME}/.dockercfg
  • /.dockercfg

注意:您可能必须HOME=/root在环境文件中明确设置kubelet。

以下是配置节点以使用私有仓库的建议步骤。在此示例中,在桌面/笔记本电脑上运行这些:

  1. docker login [server]针对要使用的每组凭据运行。这更新$HOME/.docker/config.json。
  2. $HOME/.docker/config.json在编辑器中查看以确保它仅包含您要使用的凭据。
  3. 获取节点列表,例如:
  • 如果你想要这些名字: nodes=$(kubectl get nodes -o jsonpath='{range.items[*].metadata}{.name} {end}')
  • 如果你想获得IP: nodes=$(kubectl get nodes -o jsonpath='{range .items[*].status.addresses[?(@.type=="ExternalIP")]}{.address} {end}')
  1. 将本地复制.docker/config.json到上面的搜索路径列表之一。
  • 例如: for n in $nodes; do scp ~/.docker/config.json root@$n:/var/lib/kubelet/config.json; done

通过创建使用私有镜像的pod进行验证,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
kubectl create -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: private-image-test-1
spec:
containers:
- name: uses-private-image
image: $PRIVATE_IMAGE_NAME
imagePullPolicy: Always
command: [ "echo", "SUCCESS" ]
EOF
pod/private-image-test-1 created

如果一切正常,那么过了一会儿,你应该看到:

1
2
kubectl logs private-image-test-1
SUCCESS

如果失败了,那么你会看到:

1
2
kubectl describe pods/private-image-test-1 | grep "Failed"
Fri, 26 Jun 2015 15:36:13 -0700 Fri, 26 Jun 2015 15:39:13 -0700 19 {kubelet node-i2hq} spec.containers{uses-private-image} failed Failed to pull image "user/privaterepo:v1": Error: image user/privaterepo:v1 not found

您必须确保群集中的所有节点都具有相同的节点.docker/config.json。否则,pod将在某些节点上运行,而无法在其他节点上运行。例如,如果使用节点自动缩放,则每个实例模板都需要包含.docker/config.json或装载包含它的驱动器。

将私有仓库项添加到任何私有仓库中后,所有pod都将具有对镜像的读访问权限.docker/config.json。

预拉图像

注意:如果您在Google Kubernetes Engine上运行,则.dockercfg每个节点上都会有一个包含Google Container Registry凭据的节点。你不能使用这种方法。

注意:如果您可以控制节点配置,则此方法是合适的。它不能可靠地在GCE和任何其他进行自动节点替换的云提供商上运行。

默认情况下,kubelet将尝试从指定的仓库中提取每个镜像。但是,如果imagePullPolicy容器的属性设置为IfNotPresent或Never,则使用本地镜像(分别优先或排他)。

如果您希望依赖预先提取的镜像作为仓库身份验证的替代,则必须确保群集中的所有节点都具有相同的预拉镜像。

这可以用于预加载某些镜像以提高速度,或者作为对私有仓库进行身份验证的替代方法。

所有pod都可以读取任何预拉镜像。

在Pod上指定ImagePullSecrets

注意:此方法目前是Google Kubernetes Engine,GCE以及自动创建节点的任何云提供商的推荐方法。

Kubernetes支持在pod上指定仓库项。

使用Docker配置创建机密

运行以下命令,替换相应的大写值:

1
2
kubectl create secret docker-registry myregistrykey --docker-server=DOCKER_REGISTRY_SERVER --docker-username=DOCKER_USER --docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAIL
secret/myregistrykey created.

如果需要访问多个仓库,则可以为每个仓库创建一个秘密。 在为Pods提取镜像时,Kubelet会将任何内容合并imagePullSecrets为一个虚拟内容.docker/config.json。

Pod只能在自己的命名空间中引用镜像拉取秘密,因此每个命名空间需要执行一次此过程。

绕过kubectl会产生秘密

如果由于某种原因,您需要单个项目中的多个项目.docker/config.json或需要上述命令未给出的控制,那么您可以使用json或yaml创建一个秘密。

务必:

  • 设置数据项的名称 .dockerconfigjson
  • base64编码docker文件并粘贴该字符串,不间断作为字段的值 data[".dockerconfigjson"]
  • 设置type为kubernetes.io/dockerconfigjson

例:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Secret
metadata:
name: myregistrykey
namespace: awesomeapps
data:
.dockerconfigjson: UmVhbGx5IHJlYWxseSByZWVlZWVlZWVlZWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGx5eXl5eXl5eXl5eXl5eXl5eXl5eSBsbGxsbGxsbGxsbGxsbG9vb29vb29vb29vb29vb29vb29vb29vb29vb25ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubmdnZ2dnZ2dnZ2dnZ2dnZ2dnZ2cgYXV0aCBrZXlzCg==
type: kubernetes.io/dockerconfigjson

如果收到错误消息error: no objects passed to create,则可能表示base64编码的字符串无效。如果收到类似的错误消息Secret "myregistrykey" is invalid: data[.dockerconfigjson]: invalid value ...,则表示数据已成功取消base64编码,但无法解析为.docker/config.json文件。

参考Pod上的imagePullSecrets

现在,您可以通过向imagePullSecrets pod定义添加一个部分来创建引用该秘密的pod。

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: foo
namespace: awesomeapps
spec:
containers:
- name: foo
image: janedoe/awesomeapp:v1
imagePullSecrets:
- name: myregistrykey

需要对使用私有仓库的每个pod执行此操作。

但是,可以通过在serviceAccount资源中设置imagePullSecrets来自动设置此字段。检查将ImagePullSecrets添加到服务帐户以获取详细说明。

您可以将其与每个节点结合使用.docker/config.json。凭证将被合并。这种方法适用于Google Kubernetes Engine。

用例

有许多配置私有仓库的解决方案。以下是一些常见用例和建议的解决方案。

  1. 群集仅运行非专有(例如开源)镜像。无需隐藏镜像。
  • 在Docker hub上使用公共镜像。
    • 无需配置。
    • 在GCE / Google Kubernetes Engine上,自动使用本地镜像来提高速度和可用性。
  1. 群集运行一些专有镜像,这些镜像像应隐藏给公司外部的人员,但对所有群集用户可见。
  • 使用托管的私有Docker仓库。
    • 它可能托管在Docker Hub或其他地方。
    • 如上所述,在每个节点上手动配置.docker / config.json。
  • 或者,使用开放读取访问权限在防火墙后面运行内部私有仓库。
    • 不需要Kubernetes配置。
  • 或者,在使用GCE / Google Kubernetes Engine时,请使用该项目的Google Container Registry。
    • 与集群自动调节相比,它可以比手动节点配置更好地工作。
  • 或者,在更改节点配置不方便的群集上,请使用imagePullSecrets。
  1. 具有专有镜像的集群,其中一些需要更严格的访问控制。
  • 确保AlwaysPullImages准入控制器处于活动状态。否则,所有Pod都可能访问所有镜像。
  • 将敏感数据移动到“秘密”资源中,而不是将其打包在镜像中。
  1. 一个多租户群集,每个租户都需要拥有私有仓库。
  • 确保AlwaysPullImages准入控制器处于活动状态。否则,所有租户的所有Pod都可能访问所有图像。
  • 运行需要授权的私有仓库。
  • 为每个租户生成仓库凭据,保密,并为每个租户命名空间填充机密。
  • 租户将这个秘密添加到每个命名空间的imagePullSecrets。

容器环境变量

此页面描述Container环境中Container可用的资源。

容器环境

Kubernetes Container环境为容器提供了几个重要资源:

  • 文件系统,是镜像和一个或多个卷的组合。
  • 有关Container本身的信息。
  • 有关群集中其他对象的信息。
容器信息

Container 的主机名是运行Container的Pod的名称。它可以通过 libc中的hostname命令或 gethostname函数调用获得。

Pod名称和命名空间可通过向下API作为环境变量使用 。

Pod定义中的用户定义环境变量也可用于Container,Docker镜像中静态指定的任何环境变量也是如此。

群集信息

创建Container时运行的所有服务的列表可作为环境变量用于该Container。这些环境变量与Docker链接的语法相匹配。

对于名为foo的映射到名为bar的Container 的服务,定义了以下变量:

1
2
FOO_SERVICE_HOST=<the host the service is running on>
FOO_SERVICE_PORT=<the port the service is running on>

服务具有专用IP地址,如果启用了DNS插件,则可通过DNS使用Container 。

运行时类

特征状态: Kubernetes v1.12
该页面描述了RuntimeClass资源和运行时选择机制。

此功能目前处于alpha状态,意思是:
版本名称包含alpha(例如v1alpha1)。
可能是马车。启用该功能可能会暴露错误。默认情况下禁用。
可随时删除对功能的支持,恕不另行通知。
API可能会在以后的软件版本中以不兼容的方式更改,恕不另行通知。
由于错误风险增加和缺乏长期支持,建议仅在短期测试集群中使用。

运行时类

RuntimeClass是一个alpha功能,用于选择用于运行pod容器的容器运行时配置。

建立

作为早期的alpha功能,必须采取一些额外的设置步骤才能使用RuntimeClass功能:

  1. 启用RuntimeClass功能门(在apiservers&kubelets上,需要1.12+版本)
  2. 安装RuntimeClass CRD
  3. 在节点上配置CRI实现(取决于运行时)
  4. 创建相应的RuntimeClass资源
1.启用RuntimeClass feature gate

有关启用feature gates的说明,请参见feature gates。必须在apiservers和kubelet上启用RuntimeClass功能门。

2.安装RuntimeClass CRD

RuntimeClass CustomResourceDefinition(CRD)可以在Kubernetes git repo的addons目录中找到:kubernetes / cluster / addons / runtimeclass / runtimeclass_crd.yaml

安装CRD kubectl apply -f runtimeclass_crd.yaml。

3.在节点上配置CRI实现

使用RuntimeClass进行选择的配置取决于CRI实现。有关如何配置的信息,请参阅CRI实现的相应文档。由于这是一个alpha功能,并非所有CRI都支持多个RuntimeClasses。

注意: RuntimeClass当前假定整个集群中的同类节点配置(这意味着所有节点的配置方式与容器运行时相同)。任何异构性(变化的配置)必须通过调度功能独立于RuntimeClass进行管理(请参阅将Pod分配给节点)。

配置具有相应的RuntimeHandler名称,由RuntimeClass引用。RuntimeHandler必须是有效的DNS 1123子域(字母数字+ -和.字符)。

4.创建相应的RuntimeClass资源

步骤3中的配置设置应各自具有关联的RuntimeHandler名称,用于标识配置。对于每个RuntimeHandler(以及可选的空””处理程序),创建相应的RuntimeClass对象。

RuntimeClass资源当前只有2个重要字段:RuntimeClass name(metadata.name)和RuntimeHandler(spec.runtimeHandler)。对象定义如下所示:

1
2
3
4
5
6
7
apiVersion: node.k8s.io/v1alpha1  # RuntimeClass is defined in the node.k8s.io API group
kind: RuntimeClass
metadata:
name: myclass # The name the RuntimeClass will be referenced by
# RuntimeClass is a non-namespaced resource
spec:
runtimeHandler: myconfiguration # The name of the corresponding CRI configuration

注意:建议将RuntimeClass写入操作(create / update / patch / delete)限制为群集管理员。这通常是默认值。有关详细信息,请参阅授权概述。

用法

为集群配置RuntimeClasses后,使用它们非常简单。runtimeClassName在Pod规范中指定a 。例如:

1
2
3
4
5
6
7
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
runtimeClassName: myclass
# ...

这将指示Kubelet使用命名的RuntimeClass来运行此pod。如果命名的RuntimeClass不存在,或者CRI无法运行相应的处理程序,则pod将进入 Failed终端阶段。查找错误消息的相应事件。

如果未runtimeClassName指定,则将使用默认的RuntimeHandler,这相当于禁用RuntimeClass功能时的行为。

容器生命周期钩子

此页面描述了kubelet管理的容器如何使用Container生命周期钩子框架来运行在管理生命周期中由事件触发的代码。

  • 概观
  • 集装箱挂钩

概观

类似于许多具有组件生命周期钩子的编程语言框架,例如Angular,Kubernetes为Containers提供了生命周期钩子。钩子使Container能够了解其管理生命周期中的事件,并在执行相应的生命周期钩子时运行在处理程序中实现的代码。

容器钩子

有两个暴露给容器的钩子:

PostStart

在创建容器后立即执行此挂钩。但是,无法保证挂钩将在容器ENTRYPOINT之前执行。没有参数传递给处理程序。

PreStop

在容器终止之前立即调用此挂钩。它是阻塞的,意味着它是同步的,所以它必须在删除容器的调用之前完成。没有参数传递给处理程序。

终止行为的更详细描述可以在终端中找到 。

钩子处理程序实现

容器可以通过实现和注册该钩子的处理程序来访问钩子。可以为Container实现两种类型的钩子处理程序:

  • Exec - 执行特定命令,例如pre-stop.sh,在Container的cgroups和名称空间内。命令消耗的资源将根据Container计算。
  • HTTP - 对Container上的特定端点执行HTTP请求。
钩子处理程序执行

调用Container生命周期管理挂钩时,Kubernetes管理系统会在为该挂钩注册的Container中执行处理程序。

钩子处理程序调用在包含Container的Pod的上下文中是同步的。这意味着对于一个PostStart钩子,Container ENTRYPOINT和钩子异步发射。但是,如果挂钩运行或挂起太长时间,则Container无法达到某种running状态。

PreStop钩子的行为类似。如果钩子在执行期间挂起,则Pod阶段将保持Terminating状态并在terminationGracePeriodSecondspod结束后被杀死。如果一个 PostStart或PreStophook失败,则会终止Container。

用户应使其钩子处理程序尽可能轻量级。但是,有些情况下,长时间运行的命令是有意义的,例如在停止Container之前保存状态。

挂钩送货保证

钩子传递至少是一次,这意味着可以为任何给定事件多次调用钩子,例如for PostStart或PreStop。由钩子实现来正确处理这个问题。

通常,只进行单次交付。例如,如果HTTP挂钩接收器关闭且无法获取流量,则不会尝试重新发送。然而,在一些罕见的情况下,可能会发生双重递送。例如,如果一个kubelet在发送一个钩子的过程中重新启动,那么在该kubelet重新启动后可能会重新发送一个钩子。

调试Hook处理程序

在Pod事件中不公开Hook处理程序的日志。如果处理程序由于某种原因失败,它会广播一个事件。因为PostStart,这是FailedPostStartHook事件,因为PreStop这是FailedPreStopHook事件。您可以通过运行来查看这些事件kubectl describe pod <pod_name>。以下是运行此命令的一些事件输出示例:

1
2
3
4
5
6
7
8
9
10
11
12
Events:
FirstSeen LastSeen Count From SubobjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
1m 1m 1 {default-scheduler } Normal Scheduled Successfully assigned test-1730497541-cq1d2 to gke-test-cluster-default-pool-a07e5d30-siqd
1m 1m 1 {kubelet gke-test-cluster-default-pool-a07e5d30-siqd} spec.containers{main} Normal Pulling pulling image "test:1.0"
1m 1m 1 {kubelet gke-test-cluster-default-pool-a07e5d30-siqd} spec.containers{main} Normal Created Created container with docker id 5c6a256a2567; Security:[seccomp=unconfined]
1m 1m 1 {kubelet gke-test-cluster-default-pool-a07e5d30-siqd} spec.containers{main} Normal Pulled Successfully pulled image "test:1.0"
1m 1m 1 {kubelet gke-test-cluster-default-pool-a07e5d30-siqd} spec.containers{main} Normal Started Started container with docker id 5c6a256a2567
38s 38s 1 {kubelet gke-test-cluster-default-pool-a07e5d30-siqd} spec.containers{main} Normal Killing Killing container with docker id 5c6a256a2567: PostStart handler: Error executing in Docker Container: 1
37s 37s 1 {kubelet gke-test-cluster-default-pool-a07e5d30-siqd} spec.containers{main} Normal Killing Killing container with docker id 8df9fdfd7054: PostStart handler: Error executing in Docker Container: 1
38s 37s 2 {kubelet gke-test-cluster-default-pool-a07e5d30-siqd} Warning FailedSync Error syncing pod, skipping: failed to "StartContainer" for "main" with RunContainerError: "PostStart handler: Error executing in Docker Container: 1"
1m 22s 2 {kubelet gke-test-cluster-default-pool-a07e5d30-siqd} spec.containers{main} Warning FailedPostStartHook

Workloads

Pods

Pod概述

此页面概述Pod了Kubernetes对象模型中最小的可部署对象。

  • 了解Pods
  • 使用Pods
  • Pod模板
了解Pods

一个pod是在创建或部署Kubernetes对象模型Kubernetes-最小最简单的单元的基本构建块。Pod表示群集上正在运行的进程。

Pod封装了一个应用程序容器(或者,在某些情况下,多个容器),存储资源,一个唯一的网络IP以及控制容器应该如何运行的选项。Pod表示部署单元:Kubernetes中的单个应用程序实例,可能包含单个容器或少量紧密耦合且共享资源的容器。

Docker是Kubernetes Pod中最常用的容器运行时,但Pods也支持其他容器运行时。

Kubernetes集群中的Pod可以以两种主要方式使用:

  • 运行单个容器的Pod。“one-container-per-Pod”模型是最常见的Kubernetes用例; 在这种情况下,您可以将Pod视为单个容器的包装,而Kubernetes直接管理Pod而不是容器。
  • 运行多个需要协同工作的容器的Pod。Pod可以封装由多个共址容器组成的应用程序,这些容器紧密耦合并需要共享资源。这些共处一地的容器可能形成一个统一的服务单元 - 一个容器从共享卷向公众提供文件,而一个单独的“sidecar”容器刷新或更新这些文件。Pod将这些容器和存储资源作为单个可管理实体包装在一起。

该Kubernetes博客对pod用例一些额外的信息。有关更多信息,请参阅:

  • 分布式系统工具包:复合容器的模式
  • 容器设计模式

每个Pod都用于运行给定应用程序的单个实例。如果要水平扩展应用程序(例如,运行多个实例),则应使用多个Pod,每个实例一个。在Kubernetes中,这通常被称为复制。复制Pod通常由称为Controller的抽象创建和管理。有关更多信息,请参阅Pod和控制器。

Pod如何管理多个容器

Pod旨在支持多个协作流程(作为容器),形成一个有凝聚力的服务单元。Pod中的容器自动位于群集中的同一物理或虚拟机上,并共同调度。容器可以共享资源和依赖关系,彼此通信,并协调它们何时以及如何终止。

请注意,在单个Pod中对多个共存和共同管理的容器进行分组是一个相对高级的用例。您应该仅在容器紧密耦合的特定实例中使用此模式。例如,您可能有一个容器充当共享卷中文件的Web服务器,以及一个单独的“sidecar”容器,用于从远程源更新这些文件,如下图所示:
image

pod diagram

Pod为其组成容器提供两种共享资源:网络和存储。

联网

每个Pod都分配有唯一的IP地址。Pod中的每个容器都共享网络命名空间,包括IP地址和网络端口。Pod内的容器可以使用相互通信localhost。当Pod中的容器与Pod 外部的实体通信时,它们必须协调它们如何使用共享网络资源(例如端口)。

存储

Pod可以指定一组共享存储卷。Pod中的所有容器都可以访问共享卷,允许这些容器共享数据。如果需要重新启动其中一个容器,则卷还允许Pod中的持久数据存活。见卷上Kubernetes如何实现一个pod里的共享存储更多的信息。

使用Pods

你很少直接在Kubernetes - 甚至是单身Pod中创建单独的Pod。这是因为Pods被设计为相对短暂的一次性实体。当创建Pod(由您直接创建或由Controller间接创建)时,它将被安排在群集中的节点上运行。Pod保留在该节点上,直到进程终止,pod对象被删除,pod 因资源不足而被驱逐,或者Node失败。

注意:不应将重新启动Pod重新启动Pod中的容器。Pod本身不会运行,但是容器运行的环境会持续存在,直到删除为止。

pod本身不能自我修复。如果将Pod调度到失败的节点,或者调度操作本身失败,则删除Pod; 同样,由于缺乏资源或节点维护,Pod将无法在驱逐中存活。Kubernetes使用更高级别的抽象,称为Controller,它处理管理相对可处理的Pod实例的工作。因此,虽然可以直接使用Pod,但在Kubernetes中使用Controller管理pod更为常见。见pod和控制器上Kubernetes如何使用控制器来实现pod缩放和愈合的更多信息。

pod和控制器

Controller可以为您创建和管理多个Pod,处理复制和部署,并在集群范围内提供自我修复功能。例如,如果节点发生故障,Controller可能会通过在不同节点上安排相同的替换来自动替换Pod。

包含一个或多个pod的控制器的一些示例包括:

  • 部署
  • StatefulSet
  • DaemonSet

通常,控制器使用您提供的Pod模板来创建它负责的Pod。

Pod模板

Pod模板是pod规范,包含在其他对象中,例如 Replication Controllers,Jobs和 DaemonSets。控制器使用Pod模板制作实际的pod。下面的示例是Pod的简单清单,其中包含一个打印消息的容器。

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app: myapp
spec:
containers:
- name: myapp-container
image: busybox
command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']

pod模板不是指定所有副本的当前所需状态,而是像cookie 切割机。切割 cookie后,cookie与切割机无关。没有“量子纠缠”。对模板的后续更改甚至切换到新模板对已创建的pod没有直接影响。类似地,随后可以直接更新由复制控制器创建的pod。这与pod有意对比,pod确实指定了属于pod的所有容器的当前所需状态。这种方法从根本上简化了系统语义并增加了原语的灵活性。

Pods

pods是可以创建和管理Kubernetes计算的最小可部署单元。

  • 什么是Pod?
  • pod的动机
  • pod的使用
  • 考虑的替代方案
  • pod的耐久性(或缺乏)
  • 终止pod
  • pod容器的特权模式
  • API对象
什么是Pod?

一个pod(如在whales或pea pod中)是一组一个或多个容器(如Docker容器),具有共享存储/网络,以及如何运行容器的规范。pod的内容始终位于同一位置并共同调度,并在共享上下文中运行。pod模拟特定于应用程序的“逻辑主机” - 它包含一个或多个相对紧密耦合的应用程序容器 - 在预容器世界中,在同一物理或虚拟机上执行意味着在同一逻辑主机上执行。

虽然Kubernetes支持的容器运行时间多于Docker,但Docker是最常见的运行时,它有助于用Docker术语描述pod。

pod的共享上下文是一组Linux命名空间,cgroup,以及可能的隔离方面 - 与隔离Docker容器相同的东西。在pod的上下文中,各个应用程序可能会应用进一步的子隔离。

Pod中的容器共享IP地址和端口空间,并且可以通过它们找到彼此localhost。他们还可以使用标准的进程间通信(如SystemV信号量或POSIX共享内存)相互通信。不同pod中的容器具有不同的IP地址,并且在没有特殊配置的情况下无法通过IPC进行通信 。这些容器通常通过Pod IP地址相互通信。

pod中的应用程序还可以访问共享卷,共享卷被定义为pod的一部分,可以安装到每个应用程序的文件系统中。

就Docker构造而言,pod被建模为一组具有共享命名空间和共享卷的Docker容器 。

与单个应用程序容器一样,pod被认为是相对短暂的(而不是持久的)实体。正如在pod的生命周期中所讨论的,创建pod,分配唯一ID(UID),并调度到它们保留的节点,直到终止(根据重启策略)或删除。如果节点终止,则在超时期限之后,将调度计划到该节点的Pod进行删除。给定的pod(由UID定义)不会“重新安排”到新节点; 相反,它可以被相同的pod替换,如果需要,甚至可以使用相同的名称,但是使用新的UID(有关更多详细信息,请参阅复制控制器)。

当某些东西被认为具有与容量相同的生命周期时,例如卷,这意味着只要该容器(具有该UID)存在就存在。如果由于任何原因删除了该pod,即使创建了相同的替换,相关的东西(例如卷)也会被销毁并重新创建。
image

pod图

一个多容器窗格,包含文件提取程序和Web服务器,该服务器使用持久卷在容器之间共享存储。

pod的动机
管理

Pod是多个合作过程模式的模型,形成了一个有凝聚力的服务单元。它们通过提供比其组成应用程序集更高级别的抽象来简化应用程序部署和管理。Pod用作部署,水平扩展和复制的单元。对容器中的容器自动处理共置(共同调度),共享命运(例如终止),协调复制,资源共享和依赖关系管理。

资源共享和沟通

Pod可以实现其成员之间的数据共享和通信。

pod中的应用程序都使用相同的网络命名空间(相同的IP和端口空间),因此可以相互“找到”并使用它们进行通信localhost。因此,pod中的应用程序必须协调它们对端口的使用。每个pod在平面共享网络空间中具有IP地址,该网络空间与网络上的其他物理计算机和pod完全通信。

主机名设置为pod中应用程序容器的pod名称。关于网络的更多细节。

除了定义在pod中运行的应用程序容器之外,pod还指定了一组共享存储卷。卷使数据能够在容器重新启动后继续存在,并在容器内的应用程序之间共享。

pod的使用

Pod可用于托管垂直集成的应用程序堆栈(例如LAMP),但其主要动机是支持共址,共同管理的帮助程序,例如:

  • 内容管理系统,文件和数据加载器,本地缓存管理器等。
  • 日志和检查点备份,压缩,旋转,快照等
  • 数据变更观察者,日志零售商,日志和监控适配器,活动发布者等。
  • 代理,网桥和适配器
  • 控制器,管理器,配置器和更新器

通常,单个pod不用于运行同一应用程序的多个实例。

有关更长的说明,请参阅分布式系统工具包:复合容器的模式。

考虑的替代方案

为什么不在一个(Docker)容器中运行多个程序?

  1. 透明度。使基础架构内的容器对基础架构可见,使基础架构能够为这些容器提供服务,例如进程管理和资源监视。这为用户提供了许多便利。
  2. 解耦软件依赖关系。各个容器可以独立地进行版本化,重建和重新部署。Kubernetes甚至有一天可能会支持单个容器的实时更新。
  3. 便于使用。用户无需运行自己的流程管理器,担心信号和退出代码传播等。
  4. 效率。由于基础设施承担更多责任,因此集装箱的重量可以更轻。

为什么不支持基于亲和力的容器协同调度?

这种方法可以提供协同定位,但不会提供pod的大部分好处,例如资源共享,IPC,保证命运共享和简化管理。

pod的耐久性(或缺乏)

pod不应被视为耐用实体。它们将无法在调度故障,节点故障或其他驱逐(例如由于缺乏资源)或节点维护的情况下存活。

通常,用户不需要直接创建pod。他们应该几乎总是使用控制器,即使是singletons,例如, 部署。控制器提供集群范围的自我修复,以及复制和部署管理。像StatefulSet这样的控制器 也可以为有状态的pod提供支持。

使用集合API作为主要的面向用户的原语在集群调度系统中相对常见,包括Borg,Marathon,Aurora和Tupperware。

Pod作为基元公开,以便于:

  • 调度程序和控制器可插拔性
  • 支持pod级操作,无需通过控制器API“代理”它们
  • pod生命周期与控制器生命周期的解耦,例如引导
  • 控制器和服务的分离 - 端点控制器只是监视pod
  • 具有集群级功能的Kubelet级功能的清晰组合 - Kubelet实际上是“pod控制器”
  • 高可用性应用程序,它们将期望在终止之前更换pod,并且肯定在删除之前,例如在计划驱逐或图像预取的情况下。
终止pod

因为pod表示集群中节点上的正在运行的进程,所以允许这些进程在不再需要时优雅地终止(与使用KILL信号猛烈杀死并且没有机会清理)非常重要。用户应该能够请求删除并知道进程何时终止,但也能够确保删除最终完成。当用户请求删除pod时,系统会在允许pod强制终止之前记录预期的宽限期,并将TERM信号发送到每个容器中的主进程。宽限期到期后,KILL信号将发送到这些进程,然后从API服务器中删除该pod。如果在等待进程终止时重新启动Kubelet或容器管理器,

一个示例流程:

  1. 用户发送删除Pod的命令,默认宽限期(30秒)
  2. API服务器中的Pod随着时间的推移而更新,其中Pod被视为“死”以及宽限期。
  3. 在客户端命令中列出时,Pod显示为“终止”
  4. (与3同时)当Kubelet看到Pod已被标记为终止,因为已经设置了2中的时间,它开始了pod关闭过程。
    1. 如果其中一个Pod的容器定义了一个preStop挂钩,则会在容器内部调用它。如果在preStop宽限期到期后钩子仍在运行,则以小(2秒)延长的宽限期调用步骤2。
    2. 容器被发送TERM信号。请注意,并非Pod中的所有容器都会同时收到TERM信号,并且preStop如果它们关闭的顺序很重要,则每个容器都需要一个钩子。
  5. (与3同时)Pod从端点列表中删除以进行维护,并且不再被视为复制控制器的运行pod集的一部分。缓慢关闭的窗格无法继续提供流量,因为负载平衡器(如服务代理)会将其从旋转中移除。
  6. 当宽限期到期时,仍然在Pod中运行的任何进程都将被SIGKILL杀死。
  7. Kubelet将通过设置宽限期0(立即删除)完成删除API服务器上的Pod。Pod从API中消失,不再从客户端可见。

默认情况下,所有删除在30秒内都是正常的。该kubectl delete命令支持--grace-period=<seconds>允许用户覆盖默认值并指定其自己的值的选项。值0force删除 pod。在kubectl版本> = 1.5时,必须指定一个额外的标志--force一起--grace-period=0,以执行力的缺失。

强制删除pod

强制删除pod被定义为立即从群集状态和etcd删除pod。当执行强制删除时,许可证持有者不会等待来自kubelet的确认该pod已在其运行的节点上终止。它会立即删除API中的pod,以便可以使用相同的名称创建新的pod。在节点上,设置为立即终止的pod在被强制终止之前仍将被给予一个小的宽限期。

强制删除可能对某些pod有潜在危险,应谨慎执行。如果是StatefulSet pod,请参阅任务文档以从StatefulSet中删除 Pod 。

pod容器的特权模式

从Kubernetes v1.1开始,pod中的任何容器都可以使用容器规范中的privileged标志启用特权模式SecurityContext。这对于想要使用Linux功能(如操作网络堆栈和访问设备)的容器非常有用。容器内的进程获得与容器外部进程可用的几乎相同的权限。使用特权模式,将网络和卷插件编写为不需要编译到kubelet的独立窗格应该更容易。

如果主服务器正在运行Kubernetes v1.1或更高版本,并且节点运行的版本低于v1.1,那么api-server将接受新的特权pod,但不会启动。他们将处于待决状态。如果用户呼叫kubectl describe pod FooPodName,用户可以查看pod处于暂挂状态的原因。describe命令输出中的events表将说:

1
Error validating pod "FooPodName"."FooPodNamespace" from api, ignoring: spec.containers[0].securityContext.privileged: forbidden '<*>(0xc2089d3248)true'

如果主服务器运行的版本低于v1.1,则无法创建特权pod。如果用户尝试创建具有特权容器的pod,则用户将收到以下错误:

1
The Pod "FooPodName" is invalid. spec.containers[0].securityContext.privileged: forbidden '<*>(0xc20b222db0)true'

API对象

Pod是Kubernetes REST API中的顶级资源。有关API对象的更多详细信息,请参阅: Pod API对象。

Pod生命周期

该页面描述了Pod的生命周期。

  • Pod阶段
  • Pod条件
  • 容器探针
  • Pod和Container状态
  • Pod准备gate
  • 重启政策
  • Pod寿命
  • 例子
Pod阶段

Pod的status字段是 PodStatus 对象,它有一个phase字段。

Pod的阶段是Pod在其生命周期中的简单,高级摘要。该阶段不是对Container或Pod状态的全面观察汇总,也不是一个综合状态机。

Pod阶段值的数量和含义受到严密保护。除了这里记录的内容之外,没有任何关于具有给定phase值的Pod的假设。

以下是可能的值phase:

值 描述
Pending Pod已被Kubernetes系统接受,但尚未创建一个或多个Container图像。这包括计划之前的时间以及通过网络下载图像所花费的时间,这可能需要一段时间。
Running Pod已绑定到节点,并且已创建所有Container。至少有一个Container仍在运行,或者正在启动或重新启动。
Succeeded Pod中的所有容器都已成功终止,并且不会重新启动。
Failed Pod中的所有容器都已终止,并且至少有一个Container已终止失败。也就是说,Container要么退出非零状态,要么被系统终止。
Unknown 由于某种原因,无法获得Pod的状态,这通常是由于与Pod的主机通信时出错。
Pod条件

Pod有一个PodStatus,它有一个PodConditions数组, Pod已经或没有通过它。PodCondition数组的每个元素都有六个可能的字段:

  • 该lastProbeTime字段提供上次探测Pod条件的时间戳。
  • 该lastTransitionTime字段提供Pod最后从一个状态转换到另一个状态的时间戳。
  • 该message字段是人类可读的消息,指示有关转换的详细信息。
  • 该reason字段是该条件最后一次转换的唯一,单字,CamelCase原因。
  • 该status字段是一个字符串,可能的值为“ True”,“ False”和“ Unknown”。
  • 该type字段是一个包含以下可能值的字符串:
    • PodScheduled:Pod已被安排到一个节点;
    • Ready:Pod可以提供请求,应该添加到所有匹配服务的负载平衡池中;
    • Initialized:所有init容器 都已成功启动;
    • Unschedulable:调度程序现在无法调度Pod,例如由于缺少资源或其他限制;
    • ContainersReady:Pod中的所有容器都已准备就绪。
容器探针

一个探头是通过周期性地执行的诊断kubelet 上的容器。为了执行诊断,kubelet调用Container实现的 Handler。有三种类型的处理程序:

  • ExecAction:在Container内执行指定的命令。如果命令以状态代码0退出,则认为诊断成功。
  • TCPSocketAction:对指定端口上的Container的IP地址执行TCP检查。如果端口打开,则诊断被认为是成功的。
  • HTTPGetAction:对指定端口和路径上的Container的IP地址执行HTTP Get请求。如果响应的状态代码大于或等于200且小于400,则认为诊断成功。

每个探针都有三个结果之一:

  • 成功:Container通过了诊断。
  • 失败:容器未通过诊断。
  • 未知:诊断失败,因此不应采取任何措施。

在运行容器时,kubelet可以选择性地执行和响应两种探测器:

  • livenessProbe:指示Container是否正在运行。如果活动探测失败,则kubelet会杀死Container,并且Container将受其重启策略的约束。如果Container未提供活动探测,则默认状态为Success。
  • readinessProbe:指示Container是否已准备好为请求提供服务。如果准备就绪探测失败,则端点控制器会从与Pod匹配的所有服务的端点中删除Pod的IP地址。初始延迟之前的默认准备状态是Failure。如果Container未提供就绪探测,则默认状态为Success。
什么时候应该使用活力或准备探针?

如果您的Container中的进程在遇到问题或变得不健康时能够自行崩溃,则您不一定需要活动探测器; kubelet将根据Pod的内容自动执行正确的操作restartPolicy。

如果您希望在探测失败时杀死并重新启动Container,则指定活动探测,并指定restartPolicyAlways或OnFailure。

如果您只想在探测成功时开始向Pod发送流量,请指定准备探测。在这种情况下,准备情况探测可能与活动探测相同,但规范中存在准备探测意味着Pod将在不接收任何流量的情况下启动,并且仅在探测开始成功后才开始接收流量。

如果Container需要在启动期间处理大型数据,配置文件或迁移,请指定就绪性探针。

如果您希望Container能够自行维护,您可以指定一个就绪探针,用于检查特定于就绪状态的端点,该端点与活动探针不同。

请注意,如果您只想在删除Pod时排除请求,则不一定需要准备探测; 在删除时,无论准备情况探测是否存在,Pod都会自动将其置于未准备状态。Pod在等待Pod中的容器停止时仍处于未准备状态。

有关如何设置活动或准备情况探测的详细信息,请参阅 配置活动和准备探测。

Pod和Container状态

有关Pod容器状态的详细信息,请参阅 PodStatus 和 ContainerStatus。请注意,报告为Pod状态的信息取决于当前的 ContainerState。

Pod准备gate

特征状态: Kubernetes v1.12

此功能目前处于测试状态

为了通过注入额外的反馈或信号来增加Pod准备的可扩展性PodStatus,Kubernetes 1.11引入了一个名为Pod ready ++的功能。您可以使用新的字段ReadinessGate中PodSpec指定波德准备进行评估附加条件。如果Kubernetes在status.conditionsPod 的字段中找不到这样的条件,则条件的状态默认为“ False”。以下是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Kind: Pod
...
spec:
readinessGates:
- conditionType: "www.example.com/feature-1"
status:
conditions:
- type: Ready # this is a builtin PodCondition
status: "True"
lastProbeTime: null
lastTransitionTime: 2018-01-01T00:00:00Z
- type: "www.example.com/feature-1" # an extra PodCondition
status: "False"
lastProbeTime: null
lastTransitionTime: 2018-01-01T00:00:00Z
containerStatuses:
- containerID: docker://abcd...
ready: true
...

新Pod条件必须符合Kubernetes 标签密钥格式。由于该kubectl patch命令仍然不支持修补对象状态,因此必须PATCH使用其中一个KubeClient库通过操作注入新的Pod条件。

随着新Pod条件的引入,只有 当以下两个语句都成立时,才会评估Pod是否就绪:

  • Pod中的所有容器都已准备就绪。
  • 指定的所有条件ReadinessGates均为“ True”。

为了便于对Pod准备评估进行此更改,ContainersReady引入了一个新的Pod条件 来捕获旧的Pod Ready条件。

在K8s 1.11中,作为alpha功能,必须通过将PodReadinessGates 功能门设置 为true 来明确启用“Pod Ready ++”功能。

在K8s 1.12中,默认情况下启用该功能。

重启政策

PodSpec的restartPolicy字段可能包含Always,OnFailure和Never。默认值为Always。 restartPolicy适用于Pod中的所有容器。restartPolicy仅指由同一节点上的kubelet重新启动Container。由kubelet重新启动的已退出容器将以指数退避延迟(10秒,20秒,40秒……)重新启动,上限为五分钟,并在成功执行十分钟后重置。正如Pods文档中所讨论的 ,一旦绑定到节点,Pod将永远不会被反弹到另一个节点。

Pod寿命

一般来说,pod不会消失,直到有人摧毁它们。这可能是人或控制者。此规则的唯一例外是具有phase成功或失败超过一定持续时间(由terminated-pod-gc-thresholdmaster确定)的Pod将过期并自动销毁。

有三种控制器可供选择:

  • 使用Job for Pods预期终止,例如批量计算。作业仅适用于 restartPolicy等于OnFailure或Never的Pod。
  • 对不希望终止的Pod(例如,Web服务器)使用ReplicationController, ReplicaSet或 Deployment。ReplicationControllers仅适用于具有restartPolicyAlways的Pod。
  • 使用需要为每台计算机运行一个的Pod的DaemonSet,因为它们提供特定于计算机的系统服务。

所有三种类型的控制器都包含PodTemplate。建议创建适当的控制器并让它创建Pod,而不是自己直接创建Pod。这是因为Pods单独对机器故障没有弹性,但是控制器是。

如果节点死亡或与群集的其余部分断开连接,Kubernetes会应用策略phase将丢失节点上的所有Pod设置为Failed。

例子
高级活动探测示例

活动探测由kubelet执行,因此所有请求都在kubelet网络名称空间中进行。

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
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: liveness-http
spec:
containers:
- args:
- /server
image: k8s.gcr.io/liveness
livenessProbe:
httpGet:
# when "host" is not defined, "PodIP" will be used
# host: my-host
# when "scheme" is not defined, "HTTP" scheme will be used. Only "HTTP" and "HTTPS" are allowed
# scheme: HTTPS
path: /healthz
port: 8080
httpHeaders:
- name: X-Custom-Header
value: Awesome
initialDelaySeconds: 15
timeoutSeconds: 1
name: liveness

示例状态
  • Pod正在运行并有一个Container。集装箱出口成功。
    • 记录完成事件。
    • 如果restartPolicy是:
      • 始终:重启容器; Pod phase保持运行状态。
      • OnFailure:Pod phase成功。
      • 从不:Pod phase成功。
  • Pod正在运行并有一个Container。容器退出失败。
    • 记录失败事件。
    • 如果restartPolicy是:
      • 始终:重启容器; Pod phase保持运行状态。
      • OnFailure:重启容器; Pod phase保持运行状态。
      • 从不:Pod phase变得失败。
  • Pod正在运行并有两个容器。容器1出现故障。
    • 记录失败事件。
    • 如果restartPolicy是:
      • 始终:重启容器; Pod phase保持运行状态。
      • OnFailure:重启容器; Pod phase保持运行状态。
      • 从不:不要重启容器; Pod phase保持运行状态。
    • 如果Container 1未运行,并且Container 2退出:
      • 记录失败事件。
      • 如果restartPolicy是:
        • 始终:重启容器; Pod phase保持运行状态。
        • OnFailure:重启容器; Pod phase保持运行状态。
        • 从不:Pod phase变得失败。
  • Pod正在运行并有一个Container。容器耗尽内存。
    • 记录失败事件。
    • 记录OOM事件。
    • 如果restartPolicy是:
      • 始终:重启容器; Pod phase保持运行状态。
      • OnFailure:重启容器; Pod phase保持运行状态。
      • 从不:记录失败事件; Pod phase保持运行状态。
  • Pod正在运行,磁盘已经死亡。
    • 杀死所有容器。
    • 记录适当的事件。
    • Pod phase变得失败。
    • 如果在控制器下运行,Pod将在其他位置重新创建。
  • Pod正在运行,其节点已分段。
    • 杀死所有容器。
    • 记录适当的事件。
    • Pod phase变得失败。
    • 如果在控制器下运行,Pod将在其他位置重新创建。

初始容器

此页面提供了Init Containers的概述,它是在应用程序容器之前运行的专用容器,可以包含应用程序映像中不存在的实用程序或设置脚本。

  • 了解Init容器
  • Init容器可以用于什么?
  • 详细的行为
  • 支持和兼容性
了解Init容器

一个pod能够在其内运行的应用程序的多个容器,但它也可以有一个或多个初始化容器,该容器的应用容器启动之前运行。

Init容器与常规容器完全相同,除了:

  • 他们总是跑完成。
  • 每个人必须在下一个启动之前成功完成。

如果Init容器的Init容器失败,Kubernetes会重复重启Pod,直到Init容器成功。但是,如果Pod具有restartPolicyNever,则不会重新启动。

要将Container指定为Init容器,请将initContainersPodSpec上的字段添加 为应用程序数组旁边的Container类型的JSON containers数组。init容器的状态在.status.initContainerStatuses字段中作为容器状态的数组返回(类似于.status.containerStatuses字段)。

与常规容器的差异

Init Containers支持应用容器的所有字段和功能,包括资源限制,卷和安全设置。但是,Init容器的资源请求和限制的处理方式略有不同,这些内容在下面的参考资料中有说明。此外,Init Containers不支持就绪探针,因为它们必须在Pod准备好之前运行完成。

如果为Pod指定了多个Init容器,则按顺序一次运行一个Container。每一个都必须在下一次运行之前成功。当所有Init容器都运行完成后,Kubernetes会初始化Pod并像往常一样运行应用程序容器。

Init容器可以用于什么?

由于Init Containers具有来自应用容器的单独镜像,因此它们对于启动相关代码具有一些优势:

  • 出于安全原因,它们可以包含并运行不希望包含在应用容器镜像中的实用程序。
  • 它们可以包含应用程序镜像中不存在的用于设置的实用程序或自定义代码。例如,没有必要使镜像FROM的另一个镜像只使用像工具 sed,awk,python,或dig在安装过程中。
  • 应用程序映像构建器和部署者角色可以独立工作,而无需共同构建单个应用程序镜像。
  • 他们使用Linux命名空间,以便他们从应用程序容器中获得不同的文件系统视图。因此,他们可以访问应用容器无法访问的秘密。
  • 它们在任何应用程序容器启动之前运行完成,而应用程序容器并行运行,因此Init容器提供了一种简单的方法来阻止或延迟应用容器的启动,直到满足一组前置条件。
例子

以下是有关如何使用Init Containers的一些想法:

  • 等待使用shell命令创建服务,例如:
  • 我在{1..100}; 做睡觉1; 如果挖我的服务; 然后退出0; 网络连接; 完成; 退出1
  • 使用以下命令从向下API向远程服务器注册此Pod:

    curl -X POST http:// $ MANAGEMENT_SERVICE_HOST:$ MANAGEMENT_SERVICE_PORT / register -d’instal = $()IP = $()”

  • 等待一段时间,然后使用类似命令启动app Container sleep 60。
  • 将git存储库克隆到卷中。
  • 将值放入配置文件并运行模板工具以动态生成主应用程序Container的配置文件。例如,将POD_IP值放在配置中,并使用Jinja生成主应用程序配置文件。

可以在StatefulSets文档 和Production Pods指南中找到更详细的用法示例。

初始容器正在使用中

以下针对Kubernetes 1.5的yaml文件概述了一个具有两个Init容器的简单Pod。第一个等待,myservice第二个等待mydb。一旦两个容器完成,Pod就会开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app: myapp
annotations:
pod.beta.kubernetes.io/init-containers: '[
{
"name": "init-myservice",
"image": "busybox",
"command": ["sh", "-c", "until nslookup myservice; do echo waiting for myservice; sleep 2; done;"]
},
{
"name": "init-mydb",
"image": "busybox",
"command": ["sh", "-c", "until nslookup mydb; do echo waiting for mydb; sleep 2; done;"]
}
]'
spec:
containers:
- name: myapp-container
image: busybox
command: ['sh', '-c', 'echo The app is running! && sleep 3600']

Kubernetes 1.6中有一种新语法,尽管旧的注释语法仍适用于1.6和1.7。新语法必须用于1.8或更高版本。我们已将Init Containers的声明移至spec:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app: myapp
spec:
containers:
- name: myapp-container
image: busybox
command: ['sh', '-c', 'echo The app is running! && sleep 3600']
initContainers:
- name: init-myservice
image: busybox
command: ['sh', '-c', 'until nslookup myservice; do echo waiting for myservice; sleep 2; done;']
- name: init-mydb
image: busybox
command: ['sh', '-c', 'until nslookup mydb; do echo waiting for mydb; sleep 2; done;']

1.5语法仍适用于1.6,但我们建议使用1.6语法。在Kubernetes 1.6中,Init Containers在API中成为了一个领域。beta注释在1.6和1.7中仍然受到尊重,但在1.8或更高版本中不受支持。

下面YAML文件概述了mydb与myservice服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kind: Service
apiVersion: v1
metadata:
name: myservice
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9376
---
kind: Service
apiVersion: v1
metadata:
name: mydb
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9377

可以使用以下命令启动和调试此Pod:

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
$ kubectl create -f myapp.yaml
pod/myapp-pod created
$ kubectl get -f myapp.yaml
NAME READY STATUS RESTARTS AGE
myapp-pod 0/1 Init:0/2 0 6m
$ kubectl describe -f myapp.yaml
Name: myapp-pod
Namespace: default
[...]
Labels: app=myapp
Status: Pending
[...]
Init Containers:
init-myservice:
[...]
State: Running
[...]
init-mydb:
[...]
State: Waiting
Reason: PodInitializing
Ready: False
[...]
Containers:
myapp-container:
[...]
State: Waiting
Reason: PodInitializing
Ready: False
[...]
Events:
FirstSeen LastSeen Count From SubObjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
16s 16s 1 {default-scheduler } Normal Scheduled Successfully assigned myapp-pod to 172.17.4.201
16s 16s 1 {kubelet 172.17.4.201} spec.initContainers{init-myservice} Normal Pulling pulling image "busybox"
13s 13s 1 {kubelet 172.17.4.201} spec.initContainers{init-myservice} Normal Pulled Successfully pulled image "busybox"
13s 13s 1 {kubelet 172.17.4.201} spec.initContainers{init-myservice} Normal Created Created container with docker id 5ced34a04634; Security:[seccomp=unconfined]
13s 13s 1 {kubelet 172.17.4.201} spec.initContainers{init-myservice} Normal Started Started container with docker id 5ced34a04634
$ kubectl logs myapp-pod -c init-myservice # Inspect the first init container
$ kubectl logs myapp-pod -c init-mydb # Inspect the second init container

一旦我们启动mydb和myservice服务,我们就可以看到Init Containers完成并myapp-pod创建了:

1
2
3
4
5
6
$ kubectl create -f services.yaml
service/myservice created
service/mydb created
$ kubectl get -f myapp.yaml
NAME READY STATUS RESTARTS AGE
myapp-pod 1/1 Running 0 9m

这个例子非常简单,但应该为您创建自己的Init容器提供一些灵感。

详细的行为

在Pod启动期间,初始化网络和卷后,初始容器将按顺序启动。每个Container必须在下一个Container启动之前成功退出。如果Container由于运行时未能启动或因故障退出,则根据Pod重试restartPolicy。但是,如果将Pod restartPolicy设置为Always,则Init Containers将使用 RestartPolicyOnFailure。

在Ready所有Init容器都成功之前,Pod不能。Init容器上的端口不在服务下聚合。正在初始化的Pod处于Pending状态,但应将条件Initializing设置为true。

如果重新启动 Pod,则必须再次执行所有Init Containers。

Init容器规范的更改仅限于容器镜像字段。更改Init Container镜像字段相当于重新启动Pod。

由于Init Containers可以重新启动,重试或重新执行,因此Init Container代码应该是幂等的。特别是,EmptyDirs 应该为输出文件已经存在的可能性准备写入文件的代码。

Init Containers具有应用Container的所有字段。但是,Kubernetes禁止readinessProbe使用,因为Init Containers无法定义与完成不同的准备情况。这在验证期间强制执行。

使用activeDeadlineSeconds上podlivenessProbe的容器,以防止初始化容器从永远失败。活动截止日期包括Init Containers。

Pod中每个应用程序和Init容器的名称必须是唯一的; 任何与另一个名称共享名称的Container都会引发验证错误。

资源

给定Init Containers的排序和执行,适用以下资源使用规则:

  • 在所有Init Containers上定义的任何特定资源请求或限制的最高值是有效的init请求/限制
  • Pod 对资源的有效请求/限制是以下值中的较高者:
  • 资源的所有应用容器请求/限制的总和
  • 资源的有效init请求/限制
  • 调度是基于有效的请求/限制完成的,这意味着Init Containers可以预留在Pod生命周期内未使用的初始化资源。
  • Pod的有效QoS层的QoS层是Init Containers和app容器的QoS层。

根据有效的Pod请求和限制应用配额和限制。

Pod级别cgroup基于有效的Pod请求和限制,与调度程序相同。

Pod重启原因

Pod可以重新启动,导致重新执行Init Containers,原因如下:

  • 用户更新PodSpec,导致Init容器映像发生更改。App Container图像更改仅重新启动应用程序Container。
  • Pod基础架构容器重新启动。这种情况并不常见,必须由对节点具有root访问权限的人员来完成。
  • Pod中的所有容器都被终止,同时restartPolicy设置为Always,强制重新启动,并且Init Container完成记录由于垃圾回收而丢失。
支持和兼容性

具有Apiserver 1.6.0或更高版本的群集支持使用该.spec.initContainers字段的Init Containers 。以前的版本使用alpha或beta注释支持Init Containers。该.spec.initContainers字段还镜像为alpha和beta注释,以便Kubelet 1.3.0或更高版本可以执行Init Containers,因此版本1.6 apiserver可以安全地回滚到1.5.x版,而不会丢失现有创建的pod的Init Container功能。

在Apiserver和Kubelet 1.8.0或更高版本中,删除了对alpha和beta注释的支持,需要从不推荐的注释转换到 .spec.initContainers字段。

此功能已在1.6中退出测试版。可以在应用程序containers阵列旁边的PodSpec中指定Init容器。beta注释值仍将受到尊重并覆盖PodSpec字段值,但是,它们在1.6和1.7中已弃用。在1.8中,不再支持注释,必须将其转换为PodSpec字段。

Pod Preset

此页面提供PodPresets的概述,PodPresets是在创建时将特定信息注入pod的对象。信息可以包括秘密,卷,卷安装和环境变量。

  • 了解Pod预设
  • 这个怎么运作
  • 启用Pod预设
了解Pod预设

Pod Preset是一种API资源,用于在创建时将其他运行时需求注入Pod。您可以使用标签选择器 指定应用给定Pod预设的Pod。

使用Pod预设允许pod模板作者不必显式提供每个pod的所有信息。这样,使用特定服务的pod模板的作者不需要知道有关该服务的所有详细信息。

有关背景的更多信息,请参阅PodPreset的设计方案。

这个怎么运作

Kubernetes提供了一个准入控制器(PodPreset),当启用时,它将Pod Presets应用于传入的pod创建请求。发生pod创建请求时,系统会执行以下操作:

  1. 检索所有PodPresets可用的。
  2. 检查任何标签选择器是否PodPreset与正在创建的pod上的标签匹配。
  3. 尝试将所定义的各种资源合并PodPreset到正在创建的Pod中。
  4. 出错时,抛出一个记录pod上合并错误的事件,并创建pod 而不从中注入任何资源PodPreset。
  5. 注释生成的修改后的Pod规范,以指示它已被修改PodPreset。注释是形式的 podpreset.admission.kubernetes.io/podpreset-<pod-preset name>: "<resource version>"。

每个Pod可以匹配零个或多个Pod Presets; 并且每个PodPreset都可以应用于零个或多个pod。当 PodPreset应用于一个或多个Pod时,Kubernetes会修改Pod规范。对于更改Env,EnvFrom和 VolumeMounts,Kubernetes修改在波德所有容器容器规范; 对于更改Volume,Kubernetes修改Pod规范。

注意: Pod Preset能够.spec.containers在适当的时候修改Pod规范中的字段。没有从POD预置资源定义将被应用到initContainers外地。

禁用特定Pod的Pod预设

在某些情况下,您希望Pod不会被任何Pod Preset突变改变。在这些情况下,您可以在表单的Pod Spec中添加注释:podpreset.admission.kubernetes.io/exclude: "true"。

启用Pod预设

要在群集中使用Pod Presets,您必须确保以下内容:

  1. 您已启用API类型settings.k8s.io/v1alpha1/podpreset。例如,这可以通过包含settings.k8s.io/v1alpha1=true在--runtime-configAPI服务器的选项中来完成。在minikube中,--extra-config=apiserver.runtime-config=settings.k8s.io/v1alpha1=true在启动集群时添加此标志 。
  2. 您已启用准入控制器PodPreset。执行此操作的一种方法是包含PodPreset在--enable-admission-plugins为API服务器指定的选项值中。在minikube中,--extra-config=apiserver.enable-admission-plugins=Initializers,NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota,PodPreset 在启动集群时添加此标志 。
  3. 您已通过PodPreset在将使用的命名空间中创建对象来定义Pod预设。
中断

本指南适用于想要构建高可用性应用程序的应用程序所有者,因此需要了解Pod可能发生的中断类型。

它也适用于希望执行自动群集操作的群集管理员,例如升级和自动缩放群集。

  • 自愿和非自愿中断
  • 处理中断
  • 中断预算如何运作
  • PDB示例
  • 分离群集所有者和应用程序所有者角色
  • 如何在群集上执行破坏性操作
自愿和非自愿中断

在有人(一个人或一个控制器)摧毁它们,或者存在不可避免的硬件或系统软件错误之前,Pod不会消失。

我们将这些不可避免的案例称为对应用程序的非自愿中断。例如:

  • 支持节点的物理机的硬件故障
  • 集群管理员错误地删除了VM(实例)
  • 云提供商或虚拟机管理程序故障使虚拟机消失
  • 内核恐慌
  • 由于群集网络分区,节点从群集中消失
  • 由于节点资源不足而导致pod被驱逐。

除资源不足外,大多数用户都应熟悉所有这些条件; 它们不是Kubernetes特有的。

我们将其他案件称为自愿中断。其中包括应用程序所有者启动的操作和群集管理员启动的操作。典型应用程序所有者操作包

  • 删除管理pod的部署或其他控制器
  • 更新部署的pod模板导致重新启动
  • 直接删除pod(例如意外)

群集管理员操作包括:
- 耗尽节点进行修复或升级。

  • 从群集中排出节点以缩小群集(了解群集自动缩放 )。
  • 从节点中删除pod以允许其他内容适合该节点。

这些操作可以由集群管理员直接执行,也可以由集群管理员或集群主机提供商自动运行。

请咨询您的集群管理员或咨询您的云提供商或分发文档,以确定是否为您的集群启用了任何自愿中断源。如果未启用,则可以跳过创建Pod中断预算。

处理中断

以下是一些缓解非自愿中断的方法:

  • 确保您的pod 请求所需的资源。
  • 如果需要更高的可用性,请复制应用程序。(了解运行复制的 无状态 和有状态应用程序。)
  • 为了在运行复制的应用程序时获得更高的可用性,可以跨机架(使用反关联)或跨区域(如果使用 多区域群集)分布应用程序 。

自愿中断的频率各不相同。在基本的Kubernetes集群上,根本没有自愿中断。但是,您的群集管理员或托管服务提供商可能会运行一些导致自愿中断的其他服务。例如,推出节点软件更新可能会导致自愿中断。此外,群集(节点)自动缩放的某些实现可能会导致自动中断以进行碎片整理和压缩节点。您的集群管理员或托管服务提供商应记录预期的自愿中断级别(如果有)。

Kubernetes提供的功能可以帮助您在频繁的自愿中断的同时运行高可用性应用程序。我们将这组功能称为 中断预算。

中断预算如何运作

应用程序所有者可以PodDisruptionBudget为每个应用程序创建一个对象(PDB)。PDB限制复制应用程序的pod的数量,这些pod与自愿中断同时发生故障。例如,基于仲裁的应用程序希望确保运行的副本数量永远不会低于仲裁所需的数量。Web前端可能希望确保服务负载的副本数量永远不会低于总数的某个百分比。

集群管理器和托管提供商应使用通过调用Eviction API 而不是直接删除pod来遵守Pod Disruption Budgets的工具。示例是kubectl drain命令和Kubernetes-on-GCE集群升级脚本(cluster/gce/upgrade.sh)。

当集群管理员想要耗尽节点时,他们使用该kubectl drain命令。该工具试图驱逐机器上的所有pod。可以暂时拒绝逐出请求,并且该工具定期重试所有失败的请求,直到所有pod终止,或者直到达到可配置的超时。

PDB指定应用程序可以容忍的副本数量,相对于预期的副本数量。例如,具有部署.spec.replicas: 5应该在任何给定时间具有5个pod。如果其PDB允许一次有4个,则Eviction API将允许一次自动中断一个而不是两个pod。

组成应用程序的pod组使用标签选择器指定,与应用程序控制器使用的标签选择器相同(部署,有状态集等)。

“预期”数量的pod是根据.spec.replicaspods控制器计算出来的。使用.metadata.ownerReferences对象的pod从pod中发现控制器。

PDB不能防止非自愿中断发生,但它们确实违背了预算。

由于滚动升级到应用程序而被删除或不可用的Pod确实会计入中断预算,但是在执行滚动升级时,控制器(如部署和有状态集)不受PDB限制 - 在应用程序更新期间处理故障在控制器规范中。(了解有关更新部署的信息。)

当吊舱使用驱逐API逐出,它是正常终止(见 terminationGracePeriodSeconds在PodSpec。)

PDB示例
考虑具有3个节点的群集中,node-1通过node-3。群集正在运行多个应用程序。其中一人有3个副本最初称 pod-a,pod-b和pod-c。pod-x还示出了另一个没有PDB的不相关的pod。最初,pod的布局如下:

节点-1 节点-2- 节点-3-
pod-a 可用 pod-b 可用 pod-c 可用
pod-x 可用

所有3个pod都是部署的一部分,它们共同拥有一个PDB,要求所有3个pod中至少有2个可用。

例如,假设集群管理员想要重新启动到新的内核版本来修复内核中的错误。群集管理员首先尝试node-1使用该kubectl drain命令消耗。该工具试图驱逐pod-a和pod-x。这立即成功。两个pod同时进入该terminating。这使集群处于以下状态

节点-1 耗尽 节点-2- 节点-3-
pod-a 终止 pod-b 可用 pod-c 可用
pod-x 终止

部署注意到其中一个pod正在终止,因此它会创建一个名为的替换pod-d。由于node-1是封锁的,它落在另一个节点上。还创造pod-y了一些替代品pod-x。

(注意:对于一个StatefulSet,pod-a它将被称为类似的东西pod-1,需要在它被替换之前完全终止,也可以被调用,pod-1但是可以创建不同的UID。否则,该示例也适用于StatefulSet。)

现在集群处于以下状态:

节点-1 耗尽 节点-2- 节点-3-
pod-a 终止 pod-b 可用 pod-c 可用
pod-x 终止 pod-b 开始 POD-Y

在某些时候,pod终止,集群看起来像这样:

节点-1 耗尽 节点-2- 节点-3-
pod-b 可用 pod-c 可用
pod-b 开始 POD-Y

此时,如果一个不耐烦的集群管理员试图耗尽,node-2或者 node-3排除命令将阻塞,因为部署只有2个可用的pod,并且其PDB至少需要2.经过一段时间后,pod-d变为可用。

群集状态现在看起来像这样:

节点-1 耗尽 节点-2- 节点-3-
pod-b 可用 pod-c 可用
pod-b 可用 POD-Y

现在,集群管理员试图耗尽node-2。drain命令将尝试以某种顺序驱逐两个pod,pod-b先说然后再说 pod-d。它将成功驱逐pod-b。但是,当它试图逐出时pod-d,它将被拒绝,因为这将只留下一个可用于部署的pod。

部署创建了pod-b被叫的替代品pod-e。因为集群中没有足够的资源来安排 pod-e排水将再次阻塞。群集可能最终处于此状态:

节点-1 耗尽 节点-2- 节点-3- 没有节点
pod-b 可用 pod-c 可用 pod-e 待定
pod-b 可用 POD-Y

此时,集群管理员需要将节点添加回集群以继续升级。

你可以看到Kubernetes如何改变发生中断的速度,根据:

  • 应用程序需要多少个副本
  • 优雅地关闭实例需要多长时间
  • 启动新实例需要多长时间
  • 控制器的类型
  • 集群的资源容量
分离群集所有者和应用程序所有者角色

通常,将群集管理器和应用程序所有者视为彼此知之甚少的单独角色很有用。在这些情况下,这种职责分离可能有意义:

  • 当有许多应用程序团队共享Kubernetes集群时,角色有自然的专业化
  • 当第三方工具或服务用于自动化集群管理时

Pod Disruption Budgets通过在角色之间提供接口来支持这种角色分离。

如果您的组织中没有这样的责任分离,则可能不需要使用Pod Disruption Budgets。

如何在群集上执行破坏性操作

如果您是群集管理员,并且需要对群集中的所有节点执行中断操作,例如节点或系统软件升级,则可以使用以下选项:

  • 在升级期间接受停机时间。
  • 故障转移到另一个完整的副本群集。
    • 没有停机时间,但对于重复的节点以及人类协调切换的努力可能都是昂贵的。
  • 编写容错中断应用程序并使用PDB。
    • 没有停机时间。
    • 最小的资源重复。
    • 允许更多自动化群集管理。
    • 编写容忍破坏性的应用程序很棘手,但容忍自愿中断的工作很大程度上与支持自动缩放和容忍非自愿中断的工作重叠。

控制器

ReplicaSet

ReplicaSet是下一代复制控制器。现在ReplicaSet和 Replication Controller之间的唯一区别是选择器支持。ReplicaSet支持新的基于集合的选择器要求,如标签用户指南中所述, 而Replication Controller仅支持基于等同的选择器要求。

  • 如何使用ReplicaSet
  • 何时使用ReplicaSet
  • 例
  • 编写副本集规范
  • 使用ReplicaSet
  • ReplicaSet的替代品
如何使用ReplicaSet

大多数kubectl支持复制控制器的命令也支持ReplicaSet。rolling-update命令是一个例外 。如果您想要滚动更新功能,请考虑使用部署。此外, rolling-update命令是必需的,而Deployments是声明性的,因此我们建议通过rollout命令使用Deployments 。

虽然ReplicaSet可以独立使用,但今天它主要被 Deployments用作协调pod创建,删除和更新的机制。使用“部署”时,您不必担心管理它们创建的副本集。部署拥有并管理其ReplicaSet。

何时使用ReplicaSet

ReplicaSet确保在任何给定时间运行指定数量的pod副本。但是,Deployment是一个更高级别的概念,它管理ReplicaSet并为pod提供声明性更新以及许多其他有用的功能。因此,除非您需要自定义更新编排或根本不需要更新,否则我们建议使用部署而不是直接使用ReplicaSet。

这实际上意味着您可能永远不需要操作ReplicaSet对象:改为使用Deployment,并在spec部分中定义您的应用程序。

例

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
#controllers/frontend.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: frontend
labels:
app: guestbook
tier: frontend
spec:
# modify replicas according to your case
replicas: 3
selector:
matchLabels:
tier: frontend
matchExpressions:
- {key: tier, operator: In, values: [frontend]}
template:
metadata:
labels:
app: guestbook
tier: frontend
spec:
containers:
- name: php-redis
image: gcr.io/google_samples/gb-frontend:v3
resources:
requests:
cpu: 100m
memory: 100Mi
env:
- name: GET_HOSTS_FROM
value: dns
# If your cluster config does not include a dns service, then to
# instead access environment variables to find service host
# info, comment out the 'value: dns' line above, and uncomment the
# line below.
# value: env
ports:
- containerPort: 80

将此清单保存frontend.yaml到Kubernetes集群并将其提交到Kubernetes集群应该创建已定义的ReplicaSet及其管理的pod。

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
$ kubectl create -f http://k8s.io/examples/controllers/frontend.yaml
replicaset.apps/frontend created
$ kubectl describe rs/frontend
Name: frontend
Namespace: default
Selector: tier=frontend,tier in (frontend)
Labels: app=guestbook
tier=frontend
Annotations: <none>
Replicas: 3 current / 3 desired
Pods Status: 3 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
Labels: app=guestbook
tier=frontend
Containers:
php-redis:
Image: gcr.io/google_samples/gb-frontend:v3
Port: 80/TCP
Requests:
cpu: 100m
memory: 100Mi
Environment:
GET_HOSTS_FROM: dns
Mounts: <none>
Volumes: <none>
Events:
FirstSeen LastSeen Count From SubobjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
1m 1m 1 {replicaset-controller } Normal SuccessfulCreate Created pod: frontend-qhloh
1m 1m 1 {replicaset-controller } Normal SuccessfulCreate Created pod: frontend-dnjpy
1m 1m 1 {replicaset-controller } Normal SuccessfulCreate Created pod: frontend-9si5l
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
frontend-9si5l 1/1 Running 0 1m
frontend-dnjpy 1/1 Running 0 1m
frontend-qhloh 1/1 Running 0 1m

编写副本集规范

与所有其他Kubernetes API对象,一个ReplicaSet需要apiVersion,kind和metadata领域。有关使用清单的一般信息,请参阅使用kubectl进行对象管理。

ReplicaSet还需要一个.spec部分。

Pod模板

这.spec.template是唯一必需的领域.spec。这.spec.template是一个 pod模板。它与pod具有完全相同的架构 ,除了它是嵌套的并且没有apiVersion或kind。

除了pod的必填字段外,ReplicaSet中的pod模板还必须指定适当的标签和适当的重新启动策略。

对于标签,请确保不与其他控制器重叠。有关更多信息,请参阅pod选择器。

对于重新启动策略,唯一允许的值.spec.template.spec.restartPolicy是Always,这是默认值。

对于本地容器重新启动,ReplicaSet委托给节点上的代理程序,例如Kubelet或Docker。

Pod选择器

该.spec.selector字段是标签选择器。ReplicaSet使用与选择器匹配的标签管理所有pod。它不区分它创建或删除的pod以及另一个人或进程创建或删除的pod。这允许替换ReplicaSet而不影响正在运行的pod。

在.spec.template.metadata.labels必须匹配.spec.selector,否则会被API被拒绝。

在Kubernetes 1.9中,apps/v1ReplicaSet类型的API版本是当前版本,默认情况下已启用。apps/v1beta2不推荐使用API版本。

此外,您通常不应创建任何标签与此选择器匹配的pod,可以直接与另一个ReplicaSet一起创建,也可以与其他控制器(如Deployment)一起创建。如果这样做,ReplicaSet会认为它创建了其他pod。Kubernetes并没有阻止你这样做。

如果最终有多个具有重叠选择器的控制器,则必须自己管理删除。

副本集上的标签

ReplicaSet本身可以有标签(.metadata.labels)。通常,您可以将它们设置为相同.spec.template.metadata.labels。但是,允许它们不同,并且.metadata.labels不会影响ReplicaSet的行为。

副本

您可以通过设置指定应同时运行的pod数量.spec.replicas。在任何时间运行的数字可能更高或更低,例如,如果副本只是增加或减少,或者如果正常关闭吊舱,并且提前开始更换。

如果未指定.spec.replicas,则默认为1。

使用ReplicaSet
删除ReplicaSet及其Pod

要删除ReplicaSet及其所有Pod,请使用kubectl delete。该垃圾收集器在默认情况下会自动删除所有相关的荚。

使用REST API或client-go库时,必须设置propagationPolicy为Background或Foreground删除选项。例如:

1
2
3
4
kubectl proxy --port=8080
curl -X DELETE 'localhost:8080/apis/extensions/v1beta1/namespaces/default/replicasets/frontend' \
> -d '{"kind":"DeleteOptions","apiVersion":"v1","propagationPolicy":"Foreground"}' \
> -H "Content-Type: application/json"

仅删除副本集

您可以删除副本集,而不会影响使用kubectl delete该--cascade=false选项的任何pod 。使用REST API或client-go库时,必须设置propagationPolicy为Orphan,例如:

1
2
3
4
kubectl proxy --port=8080
curl -X DELETE 'localhost:8080/apis/extensions/v1beta1/namespaces/default/replicasets/frontend' \
> -d '{"kind":"DeleteOptions","apiVersion":"v1","propagationPolicy":"Orphan"}' \
> -H "Content-Type: application/json"

删除原始文件后,您可以创建一个新的ReplicaSet来替换它。只要旧的和新.spec.selector的相同,那么新的将采用旧的豆荚。但是,它不会做任何努力使现有的pod匹配一个新的,不同的pod模板。要以受控方式将pod更新为新规范,请使用滚动更新。

从副本集隔离pod

可以通过更改标签来从ReplicaSet的目标集中删除Pod。此技术可用于从服务中删除pod以进行调试,数据恢复等。以这种方式删除的Pod将自动替换(假设副本的数量也未更改)。

缩放副本集

只需更新.spec.replicas字段即可轻松扩展或缩小ReplicaSet 。ReplicaSet控制器确保具有匹配标签选择器的所需数量的pod可用且可操作。

ReplicaSet作为水平Pod自动缩放器目标

ReplicaSet也可以是 Horizontal Pod Autoscalers (HPA)的目标 。也就是说,HPA可以自动缩放ReplicaSet。以下是针对我们在上一个示例中创建的ReplicaSet的示例HPA。

1
2
3
4
5
6
7
8
9
10
11
12
#controllers/hpa-rs.yaml 
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: frontend-scaler
spec:
scaleTargetRef:
kind: ReplicaSet
name: frontend
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 50

将此清单保存hpa-rs.yaml到Kubernetes集群并将其提交到Kubernetes集群应该创建定义的HPA,该HPA根据复制的pod的CPU使用情况自动调整目标ReplicaSet。

1
kubectl create -f https://k8s.io/examples/controllers/hpa-rs.yaml

或者,您可以使用该kubectl autoscale命令来完成相同的操作(并且它更容易!)

1
kubectl autoscale rs frontend --max=10

ReplicaSet的替代品
部署(推荐)

Deployment是一个更高级别的API对象,它以类似的方式更新其底层ReplicaSet及其Pod kubectl rolling-update。如果您需要此滚动更新功能,则建议进行部署,因为kubectl rolling-update它们不同于声明式,服务器端,并具有其他功能。有关使用部署运行无状态应用程序的更多信息,请阅读使用部署运行无状态应用程序。

Bare Pods

与用户直接创建pod的情况不同,ReplicaSet会替换因任何原因而被删除或终止的pod,例如在节点故障或破坏性节点维护(例如内核升级)的情况下。因此,即使您的应用程序只需要一个pod,我们也建议您使用ReplicaSet。可以想象它与流程主管类似,只是它监控多个节点上的多个pod而不是单个节点上的单个进程。ReplicaSet将本地容器重新启动委派给节点上的某个代理程序(例如,Kubelet或Docker)。

Job

对于预期会自行终止的pod(即批处理作业),请使用Job而不是ReplicaSet。

DaemonSet

DaemonSet对于提供机器级功能的pod,例如机器监视或机器日志记录,请使用ReplicaSet而不是ReplicaSet。这些pod的生命周期与机器生命周期相关:pod需要在其他pod启动之前在机器上运行,并且当机器准备好重新启动/关闭时可以安全终止。

ReplicationController

注意:现在建议使用配置ReplicaSet的Deployment来设置复制。

ReplicationController确保pod副本的指定数量的在任何一个时间运行。换句话说,ReplicationController确保一个pod或一组同类pod总是可用。

  • ReplicationController的工作原理
  • 运行示例ReplicationController
  • 编写ReplicationController规范
  • 使用ReplicationControllers
  • 常见的使用模式
  • 编写复制程序
  • ReplicationController的职责
  • API对象
  • ReplicationController的替代品
  • 欲获得更多信息
ReplicationController的工作原理

如果存在太多pod,则ReplicationController将终止额外的pod。如果太少,ReplicationController将启动更多pod。与手动创建的pod不同,ReplicationController维护的pod在失败,删除或终止时会自动替换。例如,在内核升级等破坏性维护之后,会在节点上重新创建pod。因此,即使应用程序只需要一个pod,也应该使用ReplicationController。ReplicationController类似于进程管理程序,但是ReplicationController不是监视单个节点上的各个进程,而是监视多个节点上的多个pod。

在讨论中,ReplicationController通常缩写为“rc”或“rcs”,并且作为kubectl命令中的快捷方式。

一个简单的例子是创建一个ReplicationController对象,以无限期地可靠地运行Pod的一个实例。更复杂的用例是运行复制服务的几个相同副本,例如Web服务器。

运行示例ReplicationController

此示例ReplicationController配置运行nginx Web服务器的三个副本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#controllers/replication.yaml 
apiVersion: v1
kind: ReplicationController
metadata:
name: nginx
spec:
replicas: 3
selector:
app: nginx
template:
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80

通过下载示例文件然后运行此命令来运行示例作业:

1
2
$ kubectl create -f https://k8s.io/examples/controllers/replication.yaml
replicationcontroller/nginx created

使用以下命令检查ReplicationController的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ kubectl describe replicationcontrollers/nginx
Name: nginx
Namespace: default
Selector: app=nginx
Labels: app=nginx
Annotations: <none>
Replicas: 3 current / 3 desired
Pods Status: 0 Running / 3 Waiting / 0 Succeeded / 0 Failed
Pod Template:
Labels: app=nginx
Containers:
nginx:
Image: nginx
Port: 80/TCP
Environment: <none>
Mounts: <none>
Volumes: <none>
Events:
FirstSeen LastSeen Count From SubobjectPath Type Reason Message
--------- -------- ----- ---- ------------- ---- ------ -------
20s 20s 1 {replication-controller } Normal SuccessfulCreate Created pod: nginx-qrm3m
20s 20s 1 {replication-controller } Normal SuccessfulCreate Created pod: nginx-3ntk0
20s 20s 1 {replication-controller } Normal SuccessfulCreate Created pod: nginx-4ok8v

这里创建了三个pod,但没有一个正在运行,可能是因为正在拉动图像。稍后,相同的命令可能会显示:

1
Pods Status:    3 Running / 0 Waiting / 0 Succeeded / 0 Failed

要以机器可读的形式列出属于ReplicationController的所有pod,可以使用如下命令:

1
2
3
  $ pods=$(kubectl get pods --selector=app=nginx --output=jsonpath={.items..metadata.name})
echo $pods
nginx-3ntk0 nginx-4ok8v nginx-qrm3m

这里,选择器与ReplicationController的选择器相同(在kubectl describe输出中看到 ,并以不同的形式显示replication.yaml。该--output=jsonpath选项指定一个表达式,它只从返回列表中的每个pod获取名称。

编写ReplicationController规范

与所有其他Kubernetes配置,一个ReplicationController需要apiVersion,kind和metadata领域。有关使用配置文件的一般信息,请参阅对象管理。

ReplicationController还需要一个.spec部分。

Pod模板

这.spec.template是唯一必需的领域.spec。

这.spec.template是一个pod模板。它与pod具有完全相同的架构,除了它是嵌套的并且没有apiVersion或kind。

除了Pod的必需字段之外,ReplicationController中的pod模板还必须指定适当的标签和适当的重新启动策略。对于标签,请确保不要与其他控制器重叠。请参阅pod选择器。

只允许.spec.template.spec.restartPolicy等于Always,如果未指定,则为默认值。

对于本地容器重新启动,ReplicationControllers委托给节点上的代理程序,例如Kubelet或Docker。

ReplicationController上的标签

ReplicationController本身可以有labels(.metadata.labels)。通常,您可以将它们设置为相同.spec.template.metadata.labels; 如果.metadata.labels未指定,则默认为.spec.template.metadata.labels。但是,允许它们不同,并且.metadata.labels不会影响ReplicationController的行为。

Pod选择器

该.spec.selector字段是标签选择器。ReplicationController管理具有与选择器匹配的标签的所有pod。它不区分它创建或删除的pod以及另一个人或进程创建或删除的pod。这允许在不影响正在运行的pod的情况下替换ReplicationController。

如果指定,则.spec.template.metadata.labels必须等于.spec.selector,否则将被API拒绝。如果.spec.selector未指定,则默认为.spec.template.metadata.labels。

此外,您通常不应创建任何标签与此选择器匹配的pod,可以直接创建,与另一个ReplicationController或其他控制器(如Job)匹配。如果这样做,ReplicationController会认为它创建了其他pod。Kubernetes并没有阻止你这样做。

如果最终有多个具有重叠选择器的控制器,则必须自己管理删除(见下文)。

多个副本

您可以通过设置.spec.replicas要同时运行的窗格数来指定应同时运行的窗格数。在任何时间运行的数字可能更高或更低,例如,如果副本只是增加或减少,或者如果正常关闭吊舱,并且提前开始更换。

如果未指定.spec.replicas,则默认为1。

使用ReplicationControllers
删除ReplicationController及其Pod

要删除ReplicationController及其所有pod,请使用kubectl delete。在删除ReplicationController本身之前,Kubectl会将ReplicationController缩放为零并等待它删除每个pod。如果此kubectl命令被中断,则可以重新启动它。

使用REST API或转到客户端库时,需要显式执行这些步骤(将副本扩展为0,等待窗格删除,然后删除ReplicationController)。

仅删除ReplicationController

您可以删除ReplicationController而不影响其任何pod。

使用kubectl,指定--cascade=false选项kubectl delete。

使用REST API或转到客户端库时,只需删除ReplicationController对象即可。

删除原始文件后,您可以创建一个新的ReplicationController来替换它。只要旧的和新.spec.selector的相同,那么新的将采用旧的pod。但是,它不会做任何努力使现有的pod匹配一个新的,不同的pod模板。要以受控方式将pod更新为新规范,请使用滚动更新。

从ReplicationController中隔离pod

可以通过更改标签来从ReplicationController的目标集中删除Pod。此技术可用于从服务中删除pod以进行调试,数据恢复等。以这种方式删除的Pod将自动替换(假设副本的数量也未更改)。

常见的使用模式
重新安排

如上所述,无论您是要保持运行1个pod还是1000个,ReplicationController都将确保存在指定数量的pod,即使在节点发生故障或pod终止时(例如,由于另一个控制剂)。

缩放

通过简单地更新replicas字段,ReplicationController可以手动或通过自动缩放控制代理轻松扩展或缩小副本数量。

滚动更新

ReplicationController旨在通过逐个替换pod来促进对服务的滚动更新。

如#1353中所述,建议的方法是创建一个具有1个副本的新ReplicationController,逐个扩展新的(+1)和旧(-1)控制器,然后在它达到0个副本后删除旧控制器。无论意外故障如何,这都可以预测更新pod的集合。

理想情况下,滚动更新控制器会考虑应用程序准备情况,并确保在任何给定时间内有足够数量的pod可以高效地运行。

这两个ReplicationControllers需要创建具有至少一个区分标签的pod,例如pod的主容器的image标签,因为它通常是图像更新,可以激发滚动更新。

滚动更新在客户端工具中实现 kubectl rolling-update。访问kubectl rolling-update任务以获得更具体的示例。

多个发行tracks

除了在滚动更新正在进行时运行多个版本的应用程序之外,通常使用多个版本跟踪长时间运行多个版本,甚至连续运行多个版本。轨道将按标签区分。

例如,服务可能会定位所有pod tier in (frontend), environment in (prod)。现在说你有10个复制的pod组成这个层。但是你希望能够’canary’这个组件的新版本。您可以replicas为大部分副本设置一个设置为9 的ReplicationController ,带有标签tier=frontend, environment=prod, track=stable,另一个replicas设置为1的带有标签的ReplicationController 用于canarytier=frontend, environment=prod, track=canary。现在该服务涵盖了canary和non-canary pods。但是你可以分别搞乱ReplicationControllers来测试,监视结果等。

将ReplicationControllers与Services一起使用

多个ReplicationControllers可以位于单个服务之后,例如,某些流量转到旧版本,有些流量转到新版本。

ReplicationController永远不会自行终止,但预计它不会像服务一样长寿。服务可以由多个ReplicationControllers控制的pod组成,并且预计可以在服务的生命周期内创建和销毁许多ReplicationController(例如,执行运行服务的pod的更新)。服务本身及其客户端都应该忽略维护服务pod的ReplicationControllers。

编写复制程序

由ReplicationController创建的Pod旨在是可互换的和语义相同的,尽管它们的配置可能随着时间的推移变得异构。这显然适用于复制的无状态服务器,但ReplicationControllers也可用于维护主选,分片和工作池应用程序的可用性。此类应用程序应使用动态工作分配机制,例如RabbitMQ工作队列,而不是静态/一次性定制每个pod的配置,这被视为反模式。执行的任何pod自定义,例如资源的垂直自动调整(例如,cpu或内存),应由另一个在线控制器进程执行,与ReplicationController本身不同。

ReplicationController的职责

ReplicationController只是确保所需数量的pod与其标签选择器匹配并且可以运行。目前,只有已终止的广告连播从其计数中排除。将来,可以考虑系统提供的准备情况和其他信息,我们可以对替换策略添加更多控制,并且我们计划发出可以由外部客户使用的事件,以实现任意复杂的替换和/或扩展下行政策。

ReplicationController永远受限于这种狭隘的责任。它本身不会执行准备就绪或活力探测。它不是执行自动缩放,而是由外部自动缩放器控制(如#492中所述),这将改变其replicas字段。我们不会将调度策略(例如,传播)添加到ReplicationController。它也不应该验证控制的pod与当前指定的模板匹配,因为这会妨碍自动调整大小和其他自动化过程。同样,完成期限,排序依赖性,配置扩展和其他功能属于其他地方。我们甚至计划分析批量pod创建的机制(#170)。

ReplicationController旨在成为可组合的构建块原语。我们希望在它和其他补充原语之上构建更高级别的API和/或工具,以便将来用户使用。kubectl目前支持的“宏”操作(运行,缩放,滚动更新)是概念验证的例子。例如,我们可以想象像Asgard管理ReplicationControllers,自动缩放器,服务,调度策略,canary等。

API对象

复制控制器是Kubernetes REST API中的顶级资源。有关API对象的更多详细信息,请访问: ReplicationController API对象。

ReplicationController的替代品
ReplicaSet

ReplicaSet是支持新的基于集合的标签选择器的下一代ReplicationController 。它主要用作Deployment协调pod创建,删除和更新的机制。请注意,除非您需要自定义更新编排或根本不需要更新,否则我们建议您使用“部署”而不是直接使用“副本集”。

部署(推荐)

Deployment是一个更高级别的API对象,它以类似的方式更新其基础副本集及其Pod kubectl rolling-update。如果您需要此滚动更新功能,则建议进行部署,因为kubectl rolling-update它们不同于声明式,服务器端,并具有其他功能。

pod

与用户直接创建pod的情况不同,ReplicationController替换因任何原因而被删除或终止的pod,例如在节点故障或破坏性节点维护(例如内核升级)的情况下。因此,即使您的应用程序只需要一个pod,我们也建议您使用ReplicationController。可以想象它与流程主管类似,只是它监控多个节点上的多个pod而不是单个节点上的单个进程。ReplicationController将本地容器重新启动委派给节点上的某个代理(例如,Kubelet或Docker)。

job

Job对于预期会自行终止的pod(即批处理作业),请使用而不是ReplicationController。

DaemonSet

DaemonSet对于提供机器级功能的pod,例如机器监视或机器日志记录,请使用而不是ReplicationController。这些pod的生命周期与机器生命周期相关:pod需要在其他pod启动之前在机器上运行,并且当机器准备好重新启动/关闭时可以安全终止。

欲获得更多信息

读取运行无状态AP复制控制器。

部署

一个部署控制器提供声明更新pod和 ReplicaSets。

您在Deployment对象中描述了所需的状态,Deployment控制器以受控速率将实际状态更改为所需状态。您可以定义部署以创建新的ReplicaSet,或者删除现有的部署并使用新的部署采用所有资源。

注意:您不应管理部署所拥有的ReplicaSet。应通过操作Deployment对象来涵盖所有用例。如果您的用例未在下面介绍,请考虑在主Kubernetes存储库中打开一个问题。

  • 用例
  • 创建部署
  • 更新部署
  • 回滚部署
  • 扩展部署
  • 暂停和恢复部署
  • 部署状态
  • 清理政策
  • 用例
  • 编写部署规范
  • 部署的替代方案
用例

以下是部署的典型用例:

  • 创建部署以部署副本集。ReplicaSet在后台创建Pod。检查卷展栏的状态以查看它是否成功。
  • 通过更新Deployment的PodTemplateSpec来声明 Pod 的新状态。创建一个新的ReplicaSet,Deployment部署管理以受控速率将Pod从旧ReplicaSet移动到新ReplicaSet。每个新的ReplicaSet都会更新Deployment的修订版。
  • 如果部署的当前状态不稳定,则回滚到早期的部署修订版。每次回滚都会更新Deployment的修订版。
  • 扩展部署以促进更多负载。
  • 暂停部署以将多个修复程序应用于其PodTemplateSpec,然后恢复它以启动新的部署。
  • 使用部署的状态作为卷展栏卡住的指示符。
  • 清理不再需要的旧ReplicaSet。
创建部署

以下是部署的示例。它创建一个ReplicaSet来调出三个Pod nginx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#controllers/nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80

在这个例子中:

  • nginx-deployment创建名为的部署,由.metadata.name字段指示。
  • 部署创建三个复制的Pod,由replicas字段指示。
  • 该selector字段定义了Deployment如何找到要管理的Pod。在这种情况下,您只需选择Pod模板(app: nginx)中定义的标签。但是,只要Pod模板本身满足规则,就可以使用更复杂的选择规则。

注意: matchLabels是{key,value}对的映射。matchLabels映射中的单个{key,value} 等效matchExpressions于其元素,其键字段为“key”,运算符为“In”,值数组仅包含“value”。要求是AND。

  • 该template字段包含以下子字段:
    • app: nginx使用该labels字段标记Pod 。
    • Pod模板的规范或.template.spec字段表示Pod 运行一个容器nginx,该容器在版本1.7.9下运行nginx Docker Hub映像。
    • 创建一个容器并nginx使用该name字段命名。
    • nginx在版本运行图像1.7.9。
    • 打开端口,80以便容器可以发送和接受流量。

要创建此部署,请运行以下命令:

1
kubectl create -f https://k8s.io/examples/controllers/nginx-deployment.yaml

注意:您可以指定--record标志以写入在资源批注中执行的命令kubernetes.io/change-cause。它对于将来的内省非常有用,例如,可以查看每个Deployment修订版中执行的命令。

接下来,运行kubectl get deployments。输出类似于以下内容:

1
2
NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment 3 0 0 0 1s

检查群集中的“部署”时,将显示以下字段:

  • NAME 列出群集中的部署名称。
  • DESIRED显示应用程序的所需副本数,您在创建部署时定义这些副本。这是理想的状态。
  • CURRENT 显示当前正在运行的副本数量。
  • UP-TO-DATE 显示已更新以实现所需状态的副本数。
  • AVAILABLE 显示用户可以使用的应用程序副本数。
  • AGE 显示应用程序运行的时间。

请注意每个字段中的值如何与Deployment规范中的值相对应:

  • 根据.spec.replicas字段,所需副本的数量为3 。
  • 根据.status.replicas字段,当前副本的数量为0 。
  • 根据.status.updatedReplicas字段,最新副本的数量为0 。
  • 根据.status.availableReplicas字段,可用副本的数量为0 。

要查看“部署”卷展栏状态,请运行kubectl rollout status deployment.v1.apps/nginx-deployment。此命令返回以下输出:

1
2
Waiting for rollout to finish: 2 out of 3 new replicas have been updated...
deployment.apps/nginx-deployment successfully rolled out

kubectl get deployments几秒钟后再次运行:

1
2
NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment 3 3 3 3 18s

请注意,Deployment已创建所有三个副本,并且所有副本都是最新的(它们包含最新的Pod模板)并且可用(Pod状态至少为Deployment的.spec.minReadySeconds字段值准备就绪)。

要查看rs部署创建的ReplicaSet(),请运行kubectl get rs:

1
2
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-2035384211 3 3 3 18s

请注意,ReplicaSet的名称始终格式为[DEPLOYMENT-NAME]-[POD-TEMPLATE-HASH-VALUE]。创建部署时会自动生成哈希值。

要查看为每个pod自动生成的标签,请运行kubectl get pods --show-labels。返回以下输出:

1
2
3
4
NAME                                READY     STATUS    RESTARTS   AGE       LABELS
nginx-deployment-2035384211-7ci7o 1/1 Running 0 18s app=nginx,pod-template-hash=2035384211
nginx-deployment-2035384211-kzszj 1/1 Running 0 18s app=nginx,pod-template-hash=2035384211
nginx-deployment-2035384211-qqcnn 1/1 Running 0 18s app=nginx,pod-template-hash=2035384211

创建的ReplicaSet确保始终有三个Pod nginx在运行。

注意:您必须在部署中指定适当的选择器和Pod模板标签(在本例中 app: nginx)。不要将标签或选择器与其他控制器(包括其他部署和StatefulSet)重叠。Kubernetes不会阻止您重叠,如果多个控制器具有重叠的选择器,那么这些控制器可能会发生冲突并出现意外行为。

Pod-template-hash标签

注意:请勿更改此标签。

pod-template-hash部署控制器将标签添加到部署创建或采用的每个ReplicaSet。

此标签可确保部署的子ReplicaSet不重叠。它是通过散列PodTemplateReplicaSet并使用生成的散列作为添加到ReplicaSet选择器,Pod模板标签以及ReplicaSet可能具有的任何现有Pod中的标签值生成的。

更新部署

注意:当且仅当部署的pod模板(即.spec.template)更改时,才会触发Deployment的部署,例如,如果更新模板的标签或容器图像。其他更新(例如扩展部署)不会触发部署。

假设您现在想要更新nginx Pod以使用nginx:1.9.1镜像而不是nginx:1.7.9图像。

1
2
$ kubectl --record deployment.apps/nginx-deployment set image deployment.v1.apps/nginx-deployment
nginx=nginx:1.9.1 image updated

或者,您可以edit部署和改变.spec.template.spec.containers[0].image从nginx:1.7.9到nginx:1.9.1:

1
2
$ kubectl edit deployment.v1.apps/nginx-deployment
deployment.apps/nginx-deployment edited

要查看卷展栏状态,请运行:

1
2
3
$ kubectl rollout status deployment.v1.apps/nginx-deployment
Waiting for rollout to finish: 2 out of 3 new replicas have been updated...
deployment.apps/nginx-deployment successfully rolled out

部署成功后,您可能需要get部署:

1
2
3
$ kubectl get deployments
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
nginx-deployment 3 3 3 3 36s

最新副本的数量表示Deployment已将副本更新为最新配置。当前副本表示此部署管理的副本总数,可用副本表示可用的当前副本数。

您可以运行kubectl get rs以查看部署通过创建新的ReplicaSet并将其扩展到3个副本来更新Pod,以及将旧的ReplicaSet缩减为0个副本。

1
2
3
4
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-deployment-1564180365 3 3 3 6s
nginx-deployment-2035384211 0 0 0 36s

get pods现在运行应该只显示新的Pod:

1
2
3
4
5
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-deployment-1564180365-khku8 1/1 Running 0 14s
nginx-deployment-1564180365-nacti 1/1 Running 0 14s
nginx-deployment-1564180365-z9gth 1/1 Running 0 14s

下次要更新这些Pod时,只需再次更新Deployment的pod模板。

部署可以确保在更新时只有一定数量的Pod可能会关闭。默认情况下,它确保至少比所需的Pod数量少25%(最大不可用25%)。

部署还可以确保在所需数量的Pod之上只能创建一定数量的Pod。默认情况下,它确保最多比所需数量的Pod多25%(最大浪涌25%)。

例如,如果仔细查看上面的部署,您将看到它首先创建了一个新的Pod,然后删除了一些旧的Pod并创建了新的Pod。在有足够数量的新Pod出现之前,它不会杀死旧的Pod,并且在足够数量的旧Pod被杀之前不会创建新的Pod。它确保可用Pod的数量至少为2,并且Pod的总数最多为4。

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
$ kubectl describe deployments
Name: nginx-deployment
Namespace: default
CreationTimestamp: Thu, 30 Nov 2017 10:56:25 +0000
Labels: app=nginx
Annotations: deployment.kubernetes.io/revision=2
Selector: app=nginx
Replicas: 3 desired | 3 updated | 3 total | 3 available | 0 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: app=nginx
Containers:
nginx:
Image: nginx:1.9.1
Port: 80/TCP
Environment: <none>
Mounts: <none>
Volumes: <none>
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable
OldReplicaSets: <none>
NewReplicaSet: nginx-deployment-1564180365 (3/3 replicas created)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 2m deployment-controller Scaled up replica set nginx-deployment-2035384211 to 3
Normal ScalingReplicaSet 24s deployment-controller Scaled up replica set nginx-deployment-1564180365 to 1
Normal ScalingReplicaSet 22s deployment-controller Scaled down replica set nginx-deployment-2035384211 to 2
Normal ScalingReplicaSet 22s deployment-controller Scaled up replica set nginx-deployment-1564180365 to 2
Normal ScalingReplicaSet 19s deployment-controller Scaled down replica set nginx-deployment-2035384211 to 1
Normal ScalingReplicaSet 19s deployment-controller Scaled up replica set nginx-deployment-1564180365 to 3
Normal ScalingReplicaSet 14s deployment-controller Scaled down replica set nginx-deployment-2035384211 to 0

在这里,您可以看到,当您第一次创建部署时,它创建了一个ReplicaSet(nginx-deployment-2035384211)并直接将其扩展到3个副本。更新部署时,它创建了一个新的ReplicaSet(nginx-deployment-1564180365)并将其扩展为1,然后将旧的ReplicaSet缩小为2,这样至少有2个Pod可用,最多创建了4个Pod一直。然后,它继续使用相同的滚动更新策略向上和向下扩展新旧ReplicaSet。最后,您将在新的ReplicaSet中拥有3个可用副本,并将旧的ReplicaSet缩小为0。

Rollover (aka multiple updates in-flight)

每次部署控制器观察到新的部署对象时,如果没有现有的ReplicaSet,则会创建ReplicaSet以显示所需的Pod。现有的ReplicaSet控制其标签匹配.spec.selector但模板不匹配的Pod .spec.template按比例缩小。最终,新的ReplicaSet将缩放到,.spec.replicas并且所有旧的ReplicaSet将缩放为0。

如果在现有部署过程中更新部署,则部署将根据更新创建新的ReplicaSet并开始向上扩展,并将翻转之前正在扩展的ReplicaSet - 它会将其添加到其列表中旧的ReplicaSet和将开始缩小它。

例如,假设您创建了一个部署以创建5个副本nginx:1.7.9,但是nginx:1.9.1当仅创建了3个副本时,则更新部署以创建5个副本nginx:1.7.9。在这种情况下,部署将立即开始杀死nginx:1.7.9它创建的3个Pod,并将开始创建 nginx:1.9.1Pod。nginx:1.7.9在更改课程之前,它不会等待创建5个副本。

标签选择器更新

通常不鼓励进行标签选择器更新,建议您事先规划选择器。在任何情况下,如果您需要执行标签选择器更新,请务必小心谨慎,并确保您已掌握所有含义。

注意:在API版本中apps/v1,部署的标签选择器在创建后是不可变的。

  • 选择器添加要求使用新标签更新部署规范中的pod模板标签,否则将返回验证错误。此更改是非重叠的,这意味着新选择器不会选择使用旧选择器创建的ReplicaSet和Pod,从而导致孤立所有旧ReplicaSet并创建新的ReplicaSet。
  • 选择器更新 - 即更改选择器键中的现有值 - 导致与添加相同的行为。
  • 选择器删除 - 即从部署选择器中删除现有密钥 - 不需要对pod模板标签进行任何更改。没有现有的ReplicaSet是孤立的,并且未创建新的ReplicaSet,但请注意,已删除的标签仍存在于任何现有的Pod和ReplicaSet中。
回滚部署

有时您可能想要回滚部署; 例如,当部署不稳定时,例如崩溃循环。默认情况下,所有Deployment的卷展栏历史记录都保留在系统中,以便您可以随时回滚(可以通过修改修订历史记录限制来更改)。

注意:触发Deployment的部署时会创建Deployment的修订版。这意味着当且仅当部署的pod模板(.spec.template)发生更改时才会创建新修订,例如,如果更新模板的标签或容器图像。其他更新(例如扩展部署)不会创建部署版本,因此您可以方便地同时进行手动或自动扩展。这意味着当您回滚到早期版本时,仅回滚Deployment的pod模板部分。

假设您在更新部署时输入了拼写错误,方法是将图像名称nginx:1.91替换为nginx:1.9.1:

1
2
$ kubectl set image deployment.v1.apps/nginx-deployment nginx=nginx:1.91 --record=true
deployment.apps/nginx-deployment image updated

推出将被卡住。

1
2
$ kubectl rollout status deployment.v1.apps/nginx-deployment
Waiting for rollout to finish: 1 out of 3 new replicas have been updated...

按Ctrl-C可停止上面的卷展状态监视。有关卡片推出的更多信息, 请在此处阅读更多信息。

您将看到旧副本的数量(nginx-deployment-1564180365和nginx-deployment-2035384211)为2,新副本(nginx-deployment-3066724191)为1。

1
2
3
4
5
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-deployment-1564180365 3 3 3 25s
nginx-deployment-2035384211 0 0 0 36s
nginx-deployment-3066724191 1 1 0 6s

查看创建的Pod,您将看到由新ReplicaSet创建的1 Pod陷入图像拉环。

1
2
3
4
5
6
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-deployment-1564180365-70iae 1/1 Running 0 25s
nginx-deployment-1564180365-jbqqo 1/1 Running 0 25s
nginx-deployment-1564180365-hysrc 1/1 Running 0 25s
nginx-deployment-3066724191-08mng 0/1 ImagePullBackOff 0 6s

注意: Deployment控制器将自动停止错误的卷展栏,并将停止扩展新的ReplicaSet。这取决于maxUnavailable您指定的rollingUpdate参数(特别是)。默认情况下,Kubernetes将值设置为25%。

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
$ kubectl describe deployment
Name: nginx-deployment
Namespace: default
CreationTimestamp: Tue, 15 Mar 2016 14:48:04 -0700
Labels: app=nginx
Selector: app=nginx
Replicas: 3 desired | 1 updated | 4 total | 3 available | 1 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: app=nginx
Containers:
nginx:
Image: nginx:1.91
Port: 80/TCP
Host Port: 0/TCP
Environment: <none>
Mounts: <none>
Volumes: <none>
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True ReplicaSetUpdated
OldReplicaSets: nginx-deployment-1564180365 (3/3 replicas created)
NewReplicaSet: nginx-deployment-3066724191 (1/1 replicas created)
Events:
FirstSeen LastSeen Count From SubobjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
1m 1m 1 {deployment-controller } Normal ScalingReplicaSet Scaled up replica set nginx-deployment-2035384211 to 3
22s 22s 1 {deployment-controller } Normal ScalingReplicaSet Scaled up replica set nginx-deployment-1564180365 to 1
22s 22s 1 {deployment-controller } Normal ScalingReplicaSet Scaled down replica set nginx-deployment-2035384211 to 2
22s 22s 1 {deployment-controller } Normal ScalingReplicaSet Scaled up replica set nginx-deployment-1564180365 to 2
21s 21s 1 {deployment-controller } Normal ScalingReplicaSet Scaled down replica set nginx-deployment-2035384211 to 1
21s 21s 1 {deployment-controller } Normal ScalingReplicaSet Scaled up replica set nginx-deployment-1564180365 to 3
13s 13s 1 {deployment-controller } Normal ScalingReplicaSet Scaled down replica set nginx-deployment-2035384211 to 0
13s 13s 1 {deployment-controller } Normal ScalingReplicaSet Scaled up replica set nginx-deployment-3066724191 to 1

要解决此问题,您需要回滚到稳定的以前版本的Deployment。

检查部署的部署历史记录

首先,检查此部署的修订版:

1
2
3
4
5
6
$ kubectl rollout history deployment.v1.apps/nginx-deployment
deployments "nginx-deployment"
REVISION CHANGE-CAUSE
1 kubectl create --filename=https://k8s.io/examples/controllers/nginx-deployment.yaml --record=true
2 kubectl set image deployment.v1.apps/nginx-deployment nginx=nginx:1.9.1 --record=true
3 kube ctl set image deployment.v1.apps/nginx-deployment nginx=nginx:1.91 --record=true

CHANGE-CAUSEkubernetes.io/change-cause在创建时从部署批注复制到其修订版。您可以CHANGE-CAUSE通过以下方式指定消息:

  • 注释部署 kubectl annotate deployment.v1.apps/nginx-deployment kubernetes.io/change-cause="image updated to 1.9.1"
  • 附加--record标志以保存kubectl对资源进行更改的命令。
  • 手动编辑资源的清单。

要进一步查看每个修订的详细信息,请运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ kubectl rollout history deployment.v1.apps/nginx-deployment --revision=2
deployments "nginx-deployment" revision 2
Labels: app=nginx
pod-template-hash=1159050644
Annotations: kubernetes.io/change-cause=kubectl set image deployment.v1.apps/nginx-deployment nginx=nginx:1.9.1 --record=true
Containers:
nginx:
Image: nginx:1.9.1
Port: 80/TCP
QoS Tier:
cpu: BestEffort
memory: BestEffort
Environment Variables: <none>
No volumes.

回滚到以前的版本

现在,您已决定撤消当前的卷展栏并回滚到上一版本:

1
2
$ kubectl rollout undo deployment.v1.apps/nginx-deployment
deployment.apps/nginx-deployment

或者,您可以通过在--to-revision以下位置指定回滚到特定修订:

1
2
$ kubectl rollout undo deployment.v1.apps/nginx-deployment --to-revision=2
deployment.apps/nginx-deployment

有关与推出相关的命令的更多详细信息,请阅读kubectl rollout。

部署现在回滚到以前的稳定版本。如您所见,DeploymentRollback从Deployment控制器生成用于回滚到版本2 的事件。

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
$ kubectl get deployment nginx-deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
nginx-deployment 3 3 3 3 30m

$ kubectl describe deployment nginx-deployment
Name: nginx-deployment
Namespace: default
CreationTimestamp: Sun, 02 Sep 2018 18:17:55 -0500
Labels: app=nginx
Annotations: deployment.kubernetes.io/revision=4
kubernetes.io/change-cause=kubectl set image deployment.v1.apps/nginx-deployment nginx=nginx:1.9.1 --record=true
Selector: app=nginx
Replicas: 3 desired | 3 updated | 3 total | 3 available | 0 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: app=nginx
Containers:
nginx:
Image: nginx:1.9.1
Port: 80/TCP
Host Port: 0/TCP
Environment: <none>
Mounts: <none>
Volumes: <none>
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable
OldReplicaSets: <none>
NewReplicaSet: nginx-deployment-c4747d96c (3/3 replicas created)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 12m deployment-controller Scaled up replica set nginx-deployment-75675f5897 to 3
Normal ScalingReplicaSet 11m deployment-controller Scaled up replica set nginx-deployment-c4747d96c to 1
Normal ScalingReplicaSet 11m deployment-controller Scaled down replica set nginx-deployment-75675f5897 to 2
Normal ScalingReplicaSet 11m deployment-controller Scaled up replica set nginx-deployment-c4747d96c to 2
Normal ScalingReplicaSet 11m deployment-controller Scaled down replica set nginx-deployment-75675f5897 to 1
Normal ScalingReplicaSet 11m deployment-controller Scaled up replica set nginx-deployment-c4747d96c to 3
Normal ScalingReplicaSet 11m deployment-controller Scaled down replica set nginx-deployment-75675f5897 to 0
Normal ScalingReplicaSet 11m deployment-controller Scaled up replica set nginx-deployment-595696685f to 1
Normal DeploymentRollback 15s deployment-controller Rolled back deployment "nginx-deployment" to revision 2
Normal ScalingReplicaSet 15s deployment-controller Scaled down replica set nginx-deployment-595696685f to 0

扩展部署

您可以使用以下命令扩展部署:

1
2
$ kubectl scale deployment.v1.apps/nginx-deployment --replicas=10
deployment.apps/nginx-deployment scaled

假设在群集中启用了水平pod自动缩放,则可以为Deployment设置自动缩放器,并根据现有Pod的CPU利用率选择要运行的最小和最大Pod数。

1
2
$ kubectl autoscale deployment.v1.apps/nginx-deployment --min=10 --max=15 --cpu-percent=80
deployment.apps/nginx-deployment scaled
比例缩放

RollingUpdate Deployments支持同时运行多个版本的应用程序。当您或自动扩展器扩展正在部署(正在进行或暂停)的RollingUpdate部署时,部署控制器将平衡现有活动副本集(具有Pod的副本集)中的其他副本,以降低风险。这称为比例缩放。

例如,您正在运行具有10个副本的部署,maxSurge = 3和maxUnavailable = 2。

1
2
3
$ kubectl get deploy
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
nginx-deployment 10 10 10 10 50s

您更新到一个新的映像,该映像恰好在集群内部无法解析。

1
2
$ kubectl set image deployment.v1.apps/nginx-deployment nginx=nginx:sometag
deployment.apps/nginx-deployment image updated

图像更新使用ReplicaSet nginx-deployment-1989198191开始新的部署,但由于maxUnavailable您在上面提到的要求而被阻止 。

1
2
3
4
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-deployment-1989198191 5 5 0 9s
nginx-deployment-618515232 8 8 8 1m

然后出现一个新的部署扩展请求。自动缩放器将部署副本增加到15.部署控制器需要决定在哪里添加这些新的5个副本。如果您没有使用比例缩放,则所有5个都将添加到新的ReplicaSet中。通过比例缩放,您可以在所有ReplicaSet上传播其他副本。具有最多副本的ReplicaSets和较低比例的较大比例将转到具有较少副本的ReplicaSet。任何剩余物都会添加到具有最多副本的ReplicaSet中。零副本的ReplicaSet不会按比例放大。

在上面的示例中,3个副本将添加到旧的ReplicaSet中,2个副本将添加到新的ReplicaSet中。假设新副本变得健康,推出过程最终应将所有副本移动到新的ReplicaSet。

1
2
3
4
5
6
7
$ kubectl get deploy
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
nginx-deployment 15 18 7 8 7m
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-deployment-1989198191 7 7 0 7m
nginx-deployment-618515232 11 11 11 7m

暂停和恢复部署

您可以在触发一个或多个更新之前暂停部署,然后恢复它。这将允许您在暂停和恢复之间应用多个修复,而不会触发不必要的部署。

例如,使用刚刚创建的部署:

1
2
3
4
5
6
$ kubectl get deploy
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
nginx 3 3 3 3 1m
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-2142116321 3 3 3 1m

通过运行以下命令暂停:

1
2
$ kubectl rollout pause deployment.v1.apps/nginx-deployment
deployment.apps/nginx-deployment paused

然后更新部署的映像:

1
2
$ kubectl set image deployment.v1.apps/nginx-deployment nginx=nginx:1.9.1
deployment.apps/nginx-deployment image updated

请注意,没有新的卷展栏开始:

1
2
3
4
5
6
7
8
$ kubectl rollout history deployment.v1.apps/nginx-deployment
deployments "nginx"
REVISION CHANGE-CAUSE
1 <none>

$ kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-2142116321 3 3 3 2m

您可以根据需要进行更多更新,例如,更新将使用的资源:

1
2
$ kubectl set resources deployment.v1.apps/nginx-deployment -c=nginx --limits=cpu=200m,memory=512Mi
deployment.apps/nginx-deployment resource requirements updated

暂停之前部署的初始状态将继续其功能,但只要部署暂停,部署的新更新将不会产生任何影响。

最后,恢复部署并观察一个新的ReplicaSet,提供所有新的更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ kubectl rollout resume deployment.v1.apps/nginx-deployment
deployment.apps/nginx-deployment resumed
$ kubectl get rs -w
NAME DESIRED CURRENT READY AGE
nginx-2142116321 2 2 2 2m
nginx-3926361531 2 2 0 6s
nginx-3926361531 2 2 1 18s
nginx-2142116321 1 2 2 2m
nginx-2142116321 1 2 2 2m
nginx-3926361531 3 2 1 18s
nginx-3926361531 3 2 1 18s
nginx-2142116321 1 1 1 2m
nginx-3926361531 3 3 1 18s
nginx-3926361531 3 3 2 19s
nginx-2142116321 0 1 1 2m
nginx-2142116321 0 1 1 2m
nginx-2142116321 0 0 0 2m
nginx-3926361531 3 3 3 20s
^C
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-2142116321 0 0 0 2m
nginx-3926361531 3 3 3 28s

注意:在恢复暂停部署之前,无法回滚暂停部署。

部署状态

部署在其生命周期中进入各种状态。它可以前进,同时推出新ReplicaSet,也可以是完整的,也可以不取得进展。

进步部署

当执行以下任务之一时,Kubernetes将部署标记为进度:

  • 部署创建一个新的ReplicaSet。
  • 部署正在扩展其最新的ReplicaSet。
  • 部署正在缩减其旧的ReplicaSet。
  • 新Pod已准备就绪或可用(至少准备MinReadySeconds)。

您可以使用监视部署的进度kubectl rollout status。

完成部署

Kubernetes 在具有以下特征时将部署标记为完成:

  • 与部署关联的所有副本都已更新为您指定的最新版本,这意味着您已请求的任何更新已完成。
  • 可以使用与部署关联的所有副本。
  • 没有旧的部署副本正在运行。

您可以使用检查部署是否已完成kubectl rollout status。如果卷展栏成功完成,则kubectl rollout status返回零退出代码。

1
2
3
4
5
$ kubectl rollout status deployment.v1.apps/nginx-deployment
Waiting for rollout to finish: 2 of 3 updated replicas are available...
deployment.apps/nginx-deployment successfully rolled out
$ echo $?
0
部署失败

您的部署可能会在尝试部署其最新的ReplicaSet时遇到困难,而无需完成。这可能是由于以下一些因素造成的:

  • 配额不足
  • 准备探针失败
  • 图像拉错误
  • 权限不足
  • 限制范围
  • 应用程序运行时配置错误

检测此情况的一种方法是在部署规范中指定截止时间参数:(.spec.progressDeadlineSeconds)。.spec.progressDeadlineSeconds表示部署控制器在指示(在“部署”状态中)部署进度已停止之前等待的秒数。

以下kubectl命令设置规范progressDeadlineSeconds以使控制器报告在10分钟后缺少部署进度:

1
2
$ kubectl patch deployment.v1.apps/nginx-deployment -p '{"spec":{"progressDeadlineSeconds":600}}'
deployment.apps/nginx-deployment patched

超过截止日期后,Deployment控制器会向Deployment部署一个具有以下属性的DeploymentCondition .status.conditions:

  • 类型=进展
  • 状态=假
  • 原因= ProgressDeadlineExceeded

有关状态条件的更多信息,请参阅Kubernetes API约定。

注意:除了报告状态条件之外,Kubernetes不对停顿的部署采取任何操作 Reason=ProgressDeadlineExceeded。更高级别的协调器可以利用它并相应地采取相应措施,例如,将部署回滚到其先前版本。

注意:如果您暂停部署,Kubernetes不会根据您指定的截止日期检查进度。您可以安全地在部署和暂停期间暂停部署,而不会触发超出截止日期的条件。

由于您设置的超时时间较短或者由于任何其他可被视为瞬态的错误,您可能会遇到部署的暂时性错误。例如,假设您的配额不足。如果您描述部署,您将注意到以下部分:

1
2
3
4
5
6
7
8
9
$ kubectl describe deployment nginx-deployment
<...>
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True ReplicaSetUpdated
ReplicaFailure True FailedCreate
<...>

如果您运行kubectl get deployment nginx-deployment -o yaml,部署状态可能如下所示:

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
status:
availableReplicas: 2
conditions:
- lastTransitionTime: 2016-10-04T12:25:39Z
lastUpdateTime: 2016-10-04T12:25:39Z
message: Replica set "nginx-deployment-4262182780" is progressing.
reason: ReplicaSetUpdated
status: "True"
type: Progressing
- lastTransitionTime: 2016-10-04T12:25:42Z
lastUpdateTime: 2016-10-04T12:25:42Z
message: Deployment has minimum availability.
reason: MinimumReplicasAvailable
status: "True"
type: Available
- lastTransitionTime: 2016-10-04T12:25:39Z
lastUpdateTime: 2016-10-04T12:25:39Z
message: 'Error creating: pods "nginx-deployment-4262182780-" is forbidden: exceeded quota:
object-counts, requested: pods=1, used: pods=3, limited: pods=2'
reason: FailedCreate
status: "True"
type: ReplicaFailure
observedGeneration: 3
replicas: 2
unavailableReplicas: 2

最终,一旦超出部署进度截止日期,Kubernetes将更新状态和进度条件的原因:

1
2
3
4
5
6
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing False ProgressDeadlineExceeded
ReplicaFailure True FailedCreate

您可以通过缩小部署,缩小可能正在运行的其他控制器或增加命名空间中的配额来解决配额不足的问题。如果您满足配额条件,然后部署控制器完成“部署”卷展栏,您将看到部署状态更新成功条件(Status=True和Reason=NewReplicaSetAvailable)。

1
2
3
4
5
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable

Type=Available与Status=True您的部署具有最小可用性手段。最低可用性由部署策略中指定的参数决定。Type=Progressing和 Status=True表示您的部署正处于推出过程中并且正在进行中或已成功完成其进度并且所需的最小新副本可用(请参阅详细信息的条件原因 - 在我们的情况下 Reason=NewReplicaSetAvailable意味着部署完成)。

您可以使用检查部署是否未能进展kubectl rollout status。kubectl rollout status 如果部署已超过进度截止日期,则返回非零退出代码。

1
2
3
4
5
$ kubectl rollout status deployment.v1.apps/nginx-deployment
Waiting for rollout to finish: 2 out of 3 new replicas have been updated...
error: deployment "nginx" exceeded its progress deadline
$ echo $?
1
在失败的部署上运行

适用于完整部署的所有操作也适用于失败的部署。如果需要在“部署”窗格模板中应用多个调整,可以向上/向下缩放,回滚到以前的版本,甚至可以暂停它。

清理政策

您可以.spec.revisionHistoryLimit在部署中设置字段,以指定要保留此部署的旧ReplicaSet数。其余的将在后台进行垃圾收集。默认情况下,它是10。

注意:将此字段显式设置为0将导致清理部署的所有历史记录,从而部署将无法回滚。

用例
Canary部署

如果要使用部署将发布部署到用户或服务器的子集,则可以按照管理资源中描述的canary模式创建多个部署,每个版本一个 。

编写部署规范

与所有其他Kubernetes CONFIGS,部署需求apiVersion,kind以及metadata各个领域。有关使用配置文件的一般信息,请参阅部署应用程序,配置容器以及使用kubectl管理资源文档。

部署还需要一个.spec部分。

Pod模板

这.spec.template是唯一必需的领域.spec。

这.spec.template是一个pod模板。它与Pod具有完全相同的架构,除了它是嵌套的并且没有 apiVersion或kind。

除了Pod的必填字段外,部署中的pod模板还必须指定适当的标签和适当的重新启动策略。对于标签,请确保不要与其他控制器重叠。见选择器)。

只允许.spec.template.spec.restartPolicy等于Always,如果未指定,则为默认值。

副本

.spec.replicas是一个可选字段,指定所需Pod的数量。默认为1。

选择

.spec.selector是一个可选字段,用于指定 此部署所针对的Pod 的标签选择器。

.spec.selector必须匹配.spec.template.metadata.labels,否则它将被API拒绝。

在API版本apps/v1,.spec.selector并且.metadata.labels不默认.spec.template.metadata.labels,如果没有设置。所以必须明确设置它们。另请注意,.spec.selector在创建部署后,它是不可变的apps/v1。

部署可以终止其标签与选择器匹配的Pod,如果它们的模板不同.spec.template或者此类Pod的总数超过.spec.replicas。.spec.template如果Pod 的数量小于所需的数量,它会调出新的Pod 。

注意:您不应通过创建另一个部署或通过创建另一个控制器(如ReplicaSet或ReplicationController)来创建其标签与此选择器匹配的其他pod。如果您这样做,第一个部署认为它创建了这些其他pod。Kubernetes并没有阻止你这样做。

如果您有多个具有重叠选择器的控制器,控制器将相互争斗并且行为不正确。

战略

.spec.strategy指定用于替换旧Pod的策略。 .spec.strategy.type可以是“重新创建”或“RollingUpdate”。“RollingUpdate”是默认值。

重新创建部署

所有现有的Pod都会在创建新的Pod之前被杀死.spec.strategy.type==Recreate。

滚动更新部署

部署时会以滚动更新 方式更新Pod .spec.strategy.type==RollingUpdate。您可以指定maxUnavailable并maxSurge控制滚动更新过程。

maxUnavailable

.spec.strategy.rollingUpdate.maxUnavailable是一个可选字段,指定更新过程中可用的最大Pod数。该值可以是绝对数(例如,5)或所需Pod的百分比(例如,10%)。通过四舍五入计算绝对数字的百分比。如果.spec.strategy.rollingUpdate.maxSurge为0,则该值不能为0.默认值为25%。

例如,当此值设置为30%时,旧的ReplicaSet可以在滚动更新开始时立即按比例缩小到所需Pod的70%。准备好新的Pod后,可以进一步缩小旧的ReplicaSet,然后扩展新的ReplicaSet,确保在更新期间始终可用的Pod总数至少是所需Pod的70%。

Max Surge

.spec.strategy.rollingUpdate.maxSurge是一个可选字段,指定可以在所需数量的Pod上创建的最大Pod数。该值可以是绝对数(例如,5)或所需Pod的百分比(例如,10%)。如果MaxUnavailable为0,则该值不能为0.绝对数量是通过向上舍入的百分比计算的。默认值为25%。

例如,当此值设置为30%时,可以在滚动更新开始时立即按比例放大新的ReplicaSet,这样旧的和新的Pod的总数不会超过所需Pod的130%。一旦旧的Pod被杀死,新的ReplicaSet可以进一步扩展,确保在更新期间随时运行的Pod总数最多为所需Pod的130%。

进度截止日期

.spec.progressDeadlineSeconds是一个可选字段,指定在系统报告部署失败进度之前等待部署进度的秒数 - 表示为带有Type=Progressing,Status=False。的条件。以及Reason=ProgressDeadlineExceeded资源的状态。部署控制器将继续重试部署。将来,一旦实现自动回滚,部署控制器将在观察到这种情况后立即回滚部署。

如果指定,则此字段必须大于.spec.minReadySeconds。

Min Ready Seconds

.spec.minReadySeconds是一个可选字段,指定新创建的Pod应该在没有任何容器崩溃的情况下准备好的最小秒数,以使其可用。默认为0(Pod一旦准备好就会被视为可用)。要了解有关何时认为Pod已准备就绪的详细信息,请参阅容器探测器。

回滚

现场.spec.rollbackTo已被弃用的API版本extensions/v1beta1和apps/v1beta1,并在API版本不再支持开始apps/v1beta2。相反,应该使用回滚到先前版本中的kubectl rollout undo介绍。

修订历史限制

部署的修订历史记录存储在它控制的副本集中。

.spec.revisionHistoryLimit是一个可选字段,指定要保留以允许回滚的旧ReplicaSet的数量。其理想值取决于新部署的频率和稳定性。如果未设置此字段,则默认情况下将保留所有旧的ReplicaSet,消耗资源etcd并拥挤输出kubectl get rs。每个Deployment修订版的配置都存储在其ReplicaSet中; 因此,一旦删除旧的ReplicaSet,您将无法回滚到该部署版本。

更具体地说,将此字段设置为零意味着将清除所有具有0副本的旧ReplicaSet。在这种情况下,无法撤消新的“部署”卷展栏,因为它的修订历史记录已清除。

已暂停

.spec.paused是一个可选的布尔字段,用于暂停和恢复部署。暂停部署与未暂停部署之间的唯一区别是,暂停部署的PodTemplateSpec的任何更改都不会触发新的部署,只要它暂停即可。默认情况下,部署在创建时不会暂停。

部署的替代方案
kubectl滚动更新

kubectl rolling update以类似的方式更新Pod和ReplicationControllers。但建议使用部署,因为它们是声明性的,服务器端,并且具有其他功能,例如即使在滚动更新完成后回滚到任何先前的修订版。

CentOS上安装Shadowsocks客户端

发表于 2019-01-11 | 分类于 Linux |

CentOS上安装Shadowsocks客户端

Shadowsocks简介

Shadowsocks,是一种加密的传输方式(一种基于 Socks5 代理方式的网络数据加密传输包);SS 是目前主流的科学上网方式,是目前最稳定最好用的科学上网工具之一。

安装

安装pip

pip是Python的包管理工具,我们接下来是使用pip安装的Shadowsocks。

  1. 通过yum管理工具安装:

    1
    yum install -y pip
  2. 镜像库没有这个包,那么可以手动安装:

    1
    2
    curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
    python get-pip.py

安装Shadowsocks客户端

1
2
pip install --upgrade pip
pip install shadowsocks

新建配置文件vi /etc/shadowsocks.json:

1
2
3
4
5
6
7
8
9
10
{
"server":"x.x.x.x",
"server_port":25247,
"local_address": "127.0.0.1",
"local_port":25252,
"password":"123456",
"timeout":1000,
"method":"aes-256-cfb",
"workers": 10
}

编写启动服务vi /etc/systemd/system/shadowsocks.service:

1
2
3
4
5
6
7
8
9
[Unit]
Description=shadowsocks

[Service]
TimeoutStartSec=30
ExecStart=/usr/bin/sslocal -c /etc/shadowsocks.json

[Install]
WantedBy=multi-user.target

启动服务systemctl start shadowsocks,运行 curl --socks5 127.0.0.1:25252 http://httpbin.org/ip , 返回你ss服务器ip,则说明Shadowsocks客户端启动成功。

使用Privoxy把shadowsocks转换为Http代理

Privoxy简介

Privoxy是一个代理辅助工具,这里用Privoxy把Shadowsocks socks5代理转换为http代理。可以作为kubernetes的docker容器需要访问google的服务,也同时可以作为命令行的代理,本实例用作命令行代理。

安装

使用yum安装:

1
yum install privoxy -y

修改配置文件vi /etc/privoxy/config,加入一行代码

1
2
forward-socks5 / 127.0.0.1:25252 .
listen-address 127.0.0.1:8118 #这里的ip也可以是k8s的ip

启动服务systemctl start privoxy,执行命令curl -x localhost:8118 google.com,返回数据则表示服务启动成功

全局设置

编辑文件vi /etc/profile:

1
2
export http_proxy=http://127.0.0.1:8118
export https_proxy=http://127.0.0.1:8118

使配置生效

1
source /etc/profile

测试代码curl www.google.com,返回数据则成功设置全局命令行代理

基于Frp内网穿透反向代理的端口转发实现本地服务器

发表于 2019-01-07 | 分类于 Linux |

基于frp内网穿透反向代理的端口转发实现本地服务器

frp简介

frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp, udp, http, https 协议。

  • 利用处于内网或防火墙后的机器,对外网环境提供 http 或 https 服务。
  • 对于 http, https 服务支持基于域名的虚拟主机,支持自定义域名绑定,使多个域名可以共用一个80端口。
  • 利用处于内网或防火墙后的机器,对外网环境提供 tcp 和 udp 服务,例如在家里通过 ssh 访问处于公司内网环境内的主机。

使用

准备条件

公网服务器一台,内网服务器一台,公网服务器绑定域名1个。

开始搭建

公网服务器

ssh连接到公网服务器上,新建目录

1
mkdir -p /usr/local/frp

根据对应的操作系统及架构,从 Release 页面下载最新版本的程序。

1
wget https://github.com/fatedier/frp/releases/download/v0.22.0/frp_0.22.0_linux_arm64.tar.gz

解压

1
tar -zxvf  frp_0.22.0_linux_arm64.tar.gz

首先删掉frpc、frpc.ini两个文件,然后再进行配置
修改frps.ini文件,这里使用了最简化的配置:

1
2
3
4
5
6
7
8
9
10
11
# frps.ini
[common]
bind_port = 7000
vhost_http_port = 6081
max_pool_count = 20
allow_ports = 2000-3000,6081,4000-50000 #端口白名单
dashboard_port = 7500
dashboard_user = admin
dashboard_pwd = admin
token = 123456 #客户端也要配置一样的token
authentication_timeout = 90000 #超时时间,如果客户端遇到服务启动认证失败,大概率是时区问题,服务器设置一下就好了

保存然后启动服务

1
./frps -c ./frps.ini

这是前台启动,后台启动命令为

1
nohup ./frps -c ./frps.ini &

可以通过访问http://xx.xx.xx.xx:7500/static/#/proxies/tcp访问frp服务的监控界面,账号密码与上面配置的一致。

内网服务器

根据对应的操作系统及架构,从 Release 页面下载最新版本的程序。

1
wget https://github.com/fatedier/frp/releases/download/v0.22.0/frp_0.22.0_linux_arm64.tar.gz

解压

1
tar -zxvf  frp_0.22.0_linux_arm64.tar.gz

首先删掉frpc、frpc.ini两个文件,然后再进行配置
修改 frpc.ini 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# frpc.ini
[common]
server_addr = xx.xx.xx.xx #公网ip地址
server_port = 7000
token = 123456

#公网通过ssh访问内部服务器
[ssh]
type = tcp #连接协议
local_ip = 127.0.0.1
local_port = 22 #ssh默认端口号
remote_port = 6000 #自定义的访问内部ssh端口号

#公网访问内部web服务器以http方式
[web]
type = http #访问协议
local_port = 8081 #内网web服务的端口号
custom_domains = strongcat.top #所绑定的公网服务器域名,一级、二级域名都可以

保存然后执行启动

1
./frpc -c ./frpc.ini

这是前台启动,后台启动命令为

1
nohup ./frpc -c ./frpc.ini &

认证超时解决办法

一般认证超时的原因是由于2个服务器之间时间不同,可以通过命令tzselect修改时区,按照步骤设置时区

1
$ tzselect

同步服务器时间

1
2
3
sudo yum install ntp
timedatectl set-timezone Asia/Shanghai
timedatectl set-ntp yes

查看时间确保同步timedatectl

开机自启动

我用的是centOS7的操作系统,为了防止因为网络或者重启问题Frp失效,所以写了一个开机启动服务,公网服务器配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=frp

[Service]
TimeoutStartSec=30
Type=simple
ExecStart=/root/frp/frp_0.22.0_linux_386/frps -c /root/frp/frp_0.22.0_linux_386/frps.ini
ExecStop=/bin/kill $MAINPID
Restart=on-failure
RestartSec=60s

[Install]
WantedBy=multi-user.target

内网服务器配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Unit]
Description=frp
After=network.target
Wants=network.target

[Service]
TimeoutStartSec=30
Type=simple
ExecStart=/root/frp/frp_0.22.0_linux_386/frpc -c /root/frp/frp_0.22.0_linux_386/frpc.ini
ExecStop=/bin/kill $MAINPID
Restart=on-failure
RestartSec=60s

[Install]
WantedBy=multi-user.target

文件保存到/etc/systemd/system/frp.service中,并执行systemctl daemon-reload,systemctl start frp,开机启动systemctl enable frp

外网ssh访问内网服务器(直接使用配置里面数据演示)

1
ssh -oPort=6000 root@x.x.x.x

将 www.strongcat.top 的域名 A 记录解析到 IP x.x.x.x,如果服务器已经有对应的域名,也可以将 CNAME 记录解析到服务器原先的域名。

通过浏览器访问 http://www.yourdomain.com:8080 即可访问到处于内网机器上的 web 服务。

有些系统默认自带防火墙,需要开通端口

1
2
3
firewall-cmd --zone=public --add-port=6000/tcp --permanent  
systemctl stop firewalld.service
systemctl start firewalld.service

如果遇到authorization timeout错误的话,需要进行2个服务器之间的时间同步。2边服务器都执行下面的命令:

1
2
3
4
5
6
7
8
9
10
11
#下载ntpdate
yum install -y ntpdate
#调整时区为上海,也就是北京时间+8区
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
yes | cp -f /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
#使用NTP来同步时间
ntpdate us.pool.ntp.org
#定时同步时间(每隔10分钟同步时钟)
crontab -l >/tmp/crontab.bak
echo "*/10 * * * * /usr/sbin/ntpdate us.pool.ntp.org | logger -t NTP" >> /tmp/crontab.bak
crontab /tmp/crontab.bak

如果是像我这种笔记本的话,可以设置系统关闭盖子的动作

1
vim /etc/systemd/logind.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
HandlePowerKey         按下电源键后的行为,默认power off
HandleSleepKey 按下挂起键后的行为,默认suspend
HandleHibernateKey 按下休眠键后的行为,默认hibernate
HandleLidSwitch 合上笔记本盖后的行为,默认suspend

ignore 忽略,跳过
power off 关机
eboot 重启
halt 挂起
suspend shell内建指令,可暂停目前正在执行的shell。若要恢复,则必须使用SIGCONT信息。所有的进程都会暂停,但不是消失(halt是进程关闭)
hibernate 让笔记本进入休眠状态
hybrid-sleep 混合睡眠,主要是为台式机设计的,是睡眠和休眠的结合体,当你选择Hybird时,系统会像休眠一样把内存里的数据从头到尾复制到硬盘里 ,然后进入睡眠状态,即内存和CPU还是活动的,其他设置不活动,这样你想用电脑时就可以快速恢复到之前的状态了,笔记本一般不用这个功能。
lock 仅锁屏,计算机继续工作。

更多指令可以参考这篇博客

最后重新加载服务使配置生效

1
systemctl restart systemd-logind

进阶(配合nginx实现域名转发)

购买域名

购买域名,国内需要备案,然后再阿里云中添加域名,创建域名解析,如图所示
image

安装nginx

1
docker pull nginx

新建文件nginx.conf

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

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;


events {
worker_connections 1024;
}


http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;
#tcp_nopush on;

keepalive_timeout 65;

#gzip on;

include /etc/nginx/conf.d/*.conf;

# frp的接收http请求的反向代理
server {
listen 80;
server_name *.strongsickcat.com strongsickcat.com;

location / {
# 7071端口即为frp监听的http端口
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host:80;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;

}
# 防止爬虫抓取
if ($http_user_agent ~* "360Spider|JikeSpider|Spider|spider|bot|Bot|2345Explorer|curl|wget|webZIP|qihoobot|Baiduspider|Googlebot|Googlebot-Mobile|Googlebot-Image|Mediapartners-Google|Adsbot-Google|Feedfetcher-Google|Yahoo! Slurp|Yahoo! Slurp China|YoudaoBot|Sosospider|Sogou spider|Sogou web spider|MSNBot|ia_archiver|Tomato Bot|NSPlayer|bingbot")
{
return 403;
}
}
}

docker启动nginx

1
docker run -p 80:80 --name mynginx -v $PWD/www:/www -v $PWD/nginx.conf:/etc/nginx/nginx.conf -v $PWD/logs:/wwwlogs  -d nginx

附上frp客户端与服务端配置

1
2
3
4
5
6
7
8
9
10
11
12
#frps.ini
[common]
bind_port = 7000
max_pool_count = 20
allow_ports = 4000-50000
dashboard_port = 7500
dashboard_user = admin
dashboard_pwd = 742041978
token = 2524668868
authentication_timeout = 900
vhost_http_port = 8080
subdomain_host = strongsickcat.com

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
#客户端 frpc.ini
[common]
server_addr = 106.15.226.184
server_port = 7000
token = 2524668868
admin_addr = 127.0.0.1
admin_port = 7400

[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 22
remote_port = 6000

[test_static_file]
type = tcp
remote_port = 16001
plugin = static_file
plugin_local_path = /root/file
plugin_strip_prefix = static
plugin_http_user = admin
plugin_http_passwd = 742041978

[kibana]
type = http
# local_port代表你想要暴露给外网的本地web服务端口
local_port = 5601
# subdomain 在全局范围内要确保唯一,每个代理服务的subdomain不能重名,否则会影响正常使用。
# 客户端的subdomain需和服务端的subdomain_host配合使用
subdomain = kibana

[elasticsearch]
type = http
local_port = 9200
subdomain = elasticsearch

[mysql]
type = tcp
local_port = 3306
remote_port = 13306

[prometheus]
type = http
local_port = 9090
subdomain = prometheus

[prometheus-linux]
type = tcp
local_port = 9100
remote_port = 9100

[prometheus-mysql]
type = tcp
local_port = 9104
remote_port = 9104

[grafana]
type = http
local_port = 3000
subdomain = grafana

[prometheusa]
type = tcp
local_port = 9090
remote_port = 9090

按照我的配置文件,可以直接通过子域名访问kibana服务
http://kibana.strongsickcat.com:8080

Selenium测试框架

发表于 2019-01-04 | 分类于 JAVA |

Selenium官网

Selenium简介

Selenium可以对浏览器进行自动化测试。它主要用于自动化Web应用程序以进行测试,但当然不仅限于此。无聊的基于Web的管理任务也可以自动化。 Selenium得到了一些最大的浏览器供应商的支持,这些供应商已采取(或正在采取)将Selenium作为其浏览器本机部分的步骤。它也是无数其他浏览器自动化工具,API和框架的核心技术。支持多种语言,在java中可以作为自动化测试框架,在python中可以模拟页面用户点击对自动化爬虫进行补充。

支持的浏览器:
image

Selenium使用

Selenium提供了一种非常简单的开发方式,例如用Chrome开发的话,去开发者工具下载Katalon Selenium IDE,如图所示。
image

开始录制

录制过程中,IDE会自动帮我们把命令行插入到测试用例中,包括:

  • 单击链接
  • 输入值
  • 从下拉框选择数据
  • 单击按钮或者选择框
    点击开始录制,在最上方输入网站域名,后期可以通过更换域名来实现不同域名下的应用的测试。

使用上下文菜单添加验证和断言

使用Selenium IDE录制,转到显示测试应用程序的浏览器,然后右键单击页面上的任意位置。您将看到一个显示验证和/或断言命令的上下文菜单。
Selenium命令有三种“风格”:动作,访问器和断言。

  • 动作是通常操纵应用程序状态的命令。他们执行“点击此链接”和“选择该选项”之类的操作。如果操作失败或出错,则停止执行当前测试。
  • 访问者检查应用程序的状态并将结果存储在变量中,例如“storeTitle”。它们还用于自动生成断言。
  • 断言与访问器类似,但它们验证应用程序的状态是否符合预期。示例包括“确保页面标题为X”和“验证是否选中此复选框”。

脚本语法

命令很简单,由2个参数构成:

verifyText //div//a[2] Login

这些参数并不总是必需的;这取决于命令。在某些情况下,两者都是必需的,在其他情况下需要一个参数,而在另一些情况下,命令可能根本不需要参数。这里有几个例子:

chooseCancelOnNextPrompt
pause 500
type id=phone (555) 666-7066
type id=address1 ${myVariableAddress}

命令参考描述了每个命令的参数要求。 参数有所不同,但它们通常是:

  • Locators用于标识页面内UI元素的定位器。
  • text patterns用于验证或声明预期页面内容的文本模式。
  • text patterns or selenium variables文本模式或selenium变量,用于在输入字段中输入文本或从选项列表中选择选项。

    常用的Selenium命令

  • open 打开url页面
  • click 执行单击操作,并可选择等待加载新页面。
  • verifyTitle/assertTitle 验证预期的页面标题。
  • verifyTextPresent 验证预期文本是否在页面上的某个位置。
  • verifyText 验证预期文本及其相应的HTML标记出现在页面上。
  • verifyTable 验证表的预期内容。

验证页面元素

断言与验证的选择

  • assert 错误后会不继续执行并中断当前的测试用例
  • verify 错误后会继续执行
    的最佳用途是对测试命令进行逻辑分组,并使用“assert”后跟一个或多个“verify”测试命令启动每个组。一个例子如下:
verifyElementPresent
Command Target Value
verifyElementPresent //div/p/img

此命令验证页面上是否存在由<img> HTML标记的存在指定的图像,并且它遵循<div>标记和<p>标记。第一个(也是唯一的)参数是一个定位器,用于告诉Selenese命令如何查找元素。
verifyElementPresent可用于检查页面中是否存在任何HTML标记。您可以检查链接,段落,分区<div>等是否存在。以下是一些示例。

Command Target Value
verifyElementPresent //div/p
verifyElementPresent //div/a
verifyElementPresent id=Login
verifyElementPresent link=Go to Marketing Research
verifyElementPresent //a[2]
verifyElementPresent //head/title
verifyText

必须在测试文本及其UI元素时使用verifyText。 verifyText必须使用定位器。如果选择XPath或DOM定位器,则可以验证特定文本是否显示在页面上相对于页面上其他UI组件的特定位置。
Command | Target | Value
—|— |—
verifyText |//table/tr/td/div/p | This is my text and it occurs right after the div inside the table.

定位元素

对于许多Selenium命令,需要一个目标。此目标标识Web应用程序内容中的元素,并包含位置策略,后跟位置格式为locatorType = location。在许多情况下可以省略定位器类型。下面解释各种定位器类型,每个定位器类型都有示例。

按标识符定位

例如,页面源可以具有id和name属性,如下所示:

1
2
3
4
5
6
7
8
9
10
<html>
<body>
<form id="loginForm">
<input name="username" type="text" />
<input name="password" type="password" />
<input name="continue" type="submit" value="Login" />
<input name="continue" type="button" value="Clear" />
</form>
</body>
<html>

以下定位器策略将返回上面由行号指示的HTML片段中的元素:

  • identifier=loginForm (3)
  • identifier=password (5)
  • identifier=continue (6)
  • continue (6)
    由于定位器的标识符类型是默认值,因此上面的前三个示例中的标识符=不是必需的。

通过id定位

id=loginForm (3)

通过名称定位

  • name=username (4)
  • name=continue value=Clear (7)
  • name=continue Clear (7)
  • name=continue type=button (7)

与某些类型的XPath和DOM定位器不同,上面三种类型的定位器允许Selenium测试UI元素,而与其在页面上的位置无关。因此,如果页面结构和组织被更改,测试仍将通过。您可能想也可能不想测试页面结构是否发生变化。在Web设计者经常更改页面但其功能必须经过回归测试的情况下,通过id和name属性进行测试,或者通过任何HTML属性进行测试变得非常重要。

由于只有xpath定位符以“//”开头,因此在指定XPath定位符时不必包含xpath =标签。

  • xpath=/html/body/form[1] (3) - 绝对路径(如果HTML仅稍微更改,则会中断)
  • //form[1] (3) - HTML中的第一个表单元素
  • xpath=//form[@id=’loginForm’] (3) - 表单元素,其属性名为“id”,值为“loginForm”
  • xpath=//form[input/@name=’username’] (3) - 带有输入子元素的第一个表单元素,其属性名为“name”,值为“username”
  • //input[@name=’username’] (4) - 第一个输入元素,其属性名为“name”,值为“username”
  • //form[@id=’loginForm’]/input[1] (4) - 表单元素的第一个输入子元素,其属性名为“id”,值为“loginForm”
  • //input[@name=’continue’][@type=’button’] (7) -输入名为’name’的属性和值’continue’以及名为’type’的属性和值’button’
  • //form[@id=’loginForm’]/input[4] (7) - 表单元素的第四个输入子元素,其属性名为“id”,值为“loginForm”

主要的语法参考Xpath
可以使用浏览器的devtools复制XPath:
image

通过链接文本查找超链接

这是一种使用链接文本在网页中查找超链接的简单方法。如果存在具有相同文本的两个链接,则将使用第一个匹配。

1
2
3
4
5
6
7
<html>
<body>
<p>Are you sure you want to do this?</p>
<a href="continue.html">Continue</a>
<a href="cancel.html">Cancel</a>
</body>
<html>

  • link=Continue (4)
  • link=Cancel (5)

通过CSS定位

1
2
3
4
5
6
7
8
9
10
<html>
<body>
<form id="loginForm">
<input class="required" name="username" type="text" />
<input class="required passfield" name="password" type="password" />
<input name="continue" type="submit" value="Login" />
<input name="continue" type="button" value="Clear" />
</form>
</body>
<html>
  • css=form#loginForm (3)
  • css=input[name=”username”] (4)
  • css=input.required[type=”text”] (4)
  • css=input.passfield (5)
  • css=#loginForm input[type=”button”] (7)
  • css=#loginForm input:nth-child(2) (5)

可以参考 the W3C publication

没有明确的设定选择器的话,将会默认使用id选择器

存储命令和Selenium变量

可以使用Selenium变量在脚本开头存储常量。此外,当与数据驱动的测试设计(在后面的部分中讨论)结合使用时,Selenium变量可用于存储从命令行,从另一个程序或从文件传递到测试程序的值。

plain store命令是许多存储命令中最基本的命令,可用于在selenium变量中简单地存储常量值。它需要两个参数,即要存储的文本值和一个selenium变量。在为变量选择名称时,请使用仅包含字母数字字符的标准变量命名约定。

Command Target Value
store paul@mysite.org userName

稍后在脚本中,将需要使用变量的存储值。要访问变量的值,请将变量括在大括号({})中,并在其前面加上美元符号。

Command Target Value
verifyText //div/p ${userName}

变量的常见用途是存储输入字段的输入

Command Target Value
type id=login ${userName}

storeText

StoreText对应于verifyText。它使用定位器来标识特定的页面文本。如果找到该文本,则存储在变量中。 StoreText可用于从正在测试的页面中提取文本。

echo命令可以用来打印变量

Alerts, Popups, and Multiple Windows

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
<!DOCTYPE HTML>
<html>
<head>
<script type="text/javascript">
function output(resultText){
document.getElementById('output').childNodes[0].nodeValue=resultText;
}

function show_confirm(){
var confirmation=confirm("Chose an option.");
if (confirmation==true){
output("Confirmed.");
}
else{
output("Rejected!");
}
}

function show_alert(){
alert("I'm blocking!");
output("Alert is gone.");
}
function show_prompt(){
var response = prompt("What's the best web QA tool?","Selenium");
output(response);
}
function open_window(windowName){
window.open("newWindow.html",windowName);
}
</script>
</head>
<body>

<input type="button" id="btnConfirm" onclick="show_confirm()" value="Show confirm box" />
<input type="button" id="btnAlert" onclick="show_alert()" value="Show alert" />
<input type="button" id="btnPrompt" onclick="show_prompt()" value="Show prompt" />
<a href="newWindow.html" id="lnkNewWindow" target="_blank">New Window Link</a>
<input type="button" id="btnNewNamelessWindow" onclick="open_window()" value="Open Nameless Window" />
<input type="button" id="btnNewNamedWindow" onclick="open_window('Mike')" value="Open Named Window" />

<br />
<span id="output">
</span>
</body>
</html>
Command Description
assertFoo(pattern) 如果模式与弹出窗口的文本不匹配,则抛出错误
assertFooPresent 如果弹出窗口不可用则抛出错误
assertFooNotPresent 如果存在任何弹出窗口则抛出错误
storeFoo(variable) 将弹出文本存储在变量中
storeFooPresent(variable) 将弹出窗口的文本存储在变量中并返回true或false

在Selenium下运行时,不会显示JavaScript弹出窗口。这是因为函数调用实际上是由Selenium自己的JavaScript在运行时覆盖的。但是,仅仅因为你看不到弹出窗口并不意味着你不必处理它。要处理弹出窗口,必须调用其assertFoo(模式)函数。如果您未能断言是否存在弹出窗口,则您的下一个命令将被阻止,您将收到类似于以下错误的错误[错误]错误
error] Error: There was an unexpected Confirmation! [Chose an option.]

Alerts

让我们从Alerts开始,因为它们是最简单的弹出窗口。首先,在浏览器中打开上面的HTML示例,然后单击“Show alert”按钮。您会注意到,在您关闭警报后,页面上会显示“警报已消失。”文本。现在使用Selenium IDE录制完成相同的步骤,并在关闭警报后验证是否添加了文本。您的测试看起来像这样:

Command Target value
open /
click btnAlert
assertAlert I’m blocking!
verifyTextPresent Alert is gone.

您可能会想“这很奇怪,我从未试图断言该警报。”但这是Selenium-IDE处理并为您关闭警报。如果您删除该步骤并重播测试,您将获得以下内容 [error] Error: There was an unexpected Alert! [I'm blocking!].

如果您只想声明警报存在但是不知道或不关心它包含哪个文本,则可以使用assertAlertPresent。这将返回true或false,错误地停止测试。

Confirmations

确认的行为与警报的行为大致相同,其中assertConfirmation和assertConfirmationPresent提供与其警报对应物相同的特征。但是,默认情况下,Selenium会在弹出确认时选择“确定”。尝试单击示例页面中的“显示确认框”按钮,但单击弹出窗口中的“取消”按钮,然后断言输出文本。您的测试可能如下所示:

Command Target value
open /
click btnAlert
chooseCancelOnNextConfirmation
assertConfirmation Choose an option.
verifyTextPresent Rejected

chooseCancelOnNextConfirmation函数告诉Selenium所有后续确认都应该返回false。可以通过调用chooseOkOnNextConfirmation来重置它。

可能会注意到您无法重播此测试,因为Selenium抱怨存在未经处理的确认。这是因为Selenium-IDE记录的事件顺序导致click和chooseCancelOnNextConfirmation被置于错误的顺序(如果考虑它就有意义,Selenium在打开确认之前无法知道正在取消)切换这两个命令,你的测试运行正常。

Prompts

提示的行为与警报的行为大致相同,其中assertPrompt和assertPromptPresent提供与其警报对应项相同的特征。默认情况下,Selenium会在弹出提示时等待您输入数据。尝试单击示例页面中的“显示提示”按钮,然后在提示中输入“Selenium”。测试可能如下所示:

Command Target value
open /
answerOnNextPrompt Selenium!
click id=btnPrompt
assertPrompt What’s the best web QA tool?
verifyTextPresent Selenium!

如果在提示中选择取消,您可能会注意到answerOnNextPrompt只显示空白目标。 Selenium对取消和提示上的空白条目基本上是一样的。

调试

断点

要设置断点,请选择一个命令,单击鼠标右键,然后从上下文菜单中选择“切换断点”。然后单击“运行”按钮以从开始到断点运行测试用例。
从测试用例的中间位置到结束位置运行测试用例或者到达起始点之后的断点有时也很有用。例如,假设您的测试用例首先登录到网站,然后执行一系列测试,并且您正在尝试调试其中一个测试。但是,您只需要登录一次,但是在开发测试时需要不断重新运行测试。您可以登录一次,然后从测试用例的登录部分之后的起点运行测试用例。这将阻止您每次重新运行测试用例时都必须手动注销。

按步骤执行测试

要一次执行一个测试用例(“step through”),只需重复按此按钮。

image

Find Button

“查找”按钮用于查看当前显示的网页(在浏览器中)中当前选定的Selenium命令中使用的UI元素。在为命令的第一个参数构建定位器时,这非常有用(请参阅定位元素一节)。它可以与标识网页上UI元素的任何命令一起使用,例如,单击,单击和等待,键入,以及某些断言和验证命令等。 从表视图中,选择具有locator参数的任何命令。单击“查找”按钮。现在查看网页:应该有一个明亮的绿色矩形,包围locator参数指定的元素。

Java中应用Selenium

项目地址

我在本地录制了一个简单的脚本,选择导出到java+junit,类似下面:

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
public class SearchGoogle {
private WebDriver driver;
private boolean acceptNextAlert = true;
private StringBuffer verificationErrors = new StringBuffer();
private SeleniumConfigure seleniumConfigure = SeleniumConfigureParse.getSeleniumConfigure();

@Before
public void setUp() {
driver = DriverUtil.getDriver();
driver .manage().window().maximize();//全屏
driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
}

@Test
public void testSearchGoogle() {
driver.get(seleniumConfigure.getBaseUrl());
driver.findElement(By.name("q")).click();
driver.findElement(By.name("q")).clear();
driver.findElement(By.name("q")).sendKeys("google");
driver.findElement(By.name("q")).sendKeys(Keys.ENTER);
}

@After
public void tearDown() {
driver.quit();
String verificationErrorString = verificationErrors.toString();
if (!"".equals(verificationErrorString)) {
fail(verificationErrorString);
}
}

private boolean isElementPresent(By by) {
try {
driver.findElement(by);
return true;
} catch (NoSuchElementException e) {
return false;
}
}

private boolean isAlertPresent() {
try {
driver.switchTo().alert();
return true;
} catch (NoAlertPresentException e) {
return false;
}
}

private String closeAlertAndGetItsText() {
try {
Alert alert = driver.switchTo().alert();
String alertText = alert.getText();
if (acceptNextAlert) {
alert.accept();
} else {
alert.dismiss();
}
return alertText;
} finally {
acceptNextAlert = true;
}
}
}

新建项目,pom.xml如下

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
 <?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>choerodon</groupId>
<artifactId>selenium</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-server</artifactId>
<version>3.14.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.yaml/snakeyaml -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.23</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<!--<dependency>-->
<!--<groupId>org.projectlombok</groupId>-->
<!--<artifactId>lombok</artifactId>-->
<!--<version>1.18.4</version>-->
<!--<scope>provided</scope>-->
<!--</dependency>-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.5</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M3</version>
<dependencies>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-junit47</artifactId>
<version>3.0.0-M3</version>
</dependency>
</dependencies>
<configuration>
<outputDirectory>${basedir}/output</outputDirectory>
<outputName>测试报告</outputName>
</configuration>
</plugin>
<!--<plugin>-->
<!--<groupId>org.apache.maven.plugins</groupId>-->
<!--<artifactId>maven-site-plugin</artifactId>-->
<!--<version>2.1</version>-->
<!--<configuration>-->
<!--<outputDirectory>${basedir}/output</outputDirectory>-->
<!--<outputName>测试报告</outputName>-->
<!--</configuration>-->
<!--</plugin>-->
</plugins>
</build>

</project>

与Docker结合

使用远程的驱动服务来测试,目前只支持Chrome和FireFox,本地如果要起服务,请在docker中执行下面的命令启动服务

1
2
3
docker pull elgalu/selenium
docker run -d --name=grid -p 4444:24444 -p 5900:25900 -e TZ="Asia/Shanghai" -e MAX_INSTANCES=20 -e MAX_SESSIONS=20 -v /Users/dinghuang/Documents/Tool/selenium/shm:/dev/shm --privileged elgalu/selenium
docker exec grid wait_all_done 30s

可以在http://localhost:4444/grid/console中查看详情

关闭服务命令:

1
2
docker exec grid stop
docker stop grid

在JAVA代码中,可以通过远程的docker容器启动浏览器进行测试

1
webDriver = new RemoteWebDriver(new URL("http://localhost:4444/wd/hub"), browser);

如何在GitHub Page使用HEXO搭建博客

发表于 2018-09-21 | 分类于 JavaScript |

如何在使用GitHub Page搭建HEXO博客

项目地址

准备工作

  1. 安装Git
  2. 安装Node.js
  3. 注册Github账号
  4. 创建新的仓库 名称必须为 用户名.github.io
    此处输入图片的描述
  5. 添加本地电脑的SSH认证,如图所示
    此处输入图片的描述
    在本地命令工具中输入
    ssh-keygen -t rsa -C "Github的注册邮箱地址"
    一路回车之后会得到两个文件:id_rsa和id_rsa.pub,然后
    用带格式的编辑器(比方说notepad++或者sublime)打开id_rsa.pub,复制里面的所有内容,然粘贴到KEY里面。

安装HEXO

  1. 打开命令行工具
    npm install -g hexo-cli
  2. 安装 Hexo 完成后,分步执行(即输入代码之后敲回车)下列命令,Hexo 将会在指定文件夹中新建所需要的文件。
    hexo init <folder>
    在文件夹内执行
    npm install
  3. 成功后,博客项目成功初始化

配置NEXT主题

  1. 安装NEXT主题

mkdir themes/next
curl -s https://api.github.com/repos/iissnan/hexo-theme-next/releases/latest | grep tarball_url | cut -d '"' -f 4 | wget -i - -O- | tar -zx -C themes/next --strip-components=1
修改站点配置文件_config.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
title: 一只病猫
subtitle: 静坐常思己过,闲谈莫论人非
description: 学习、生活、闲谈、足球
author: 强壮的病猫
language: zh-Hans
timezone: Asia/Shanghai
theme: next
url: https://dinghuang.github.io/
deploy:
type: git
repo: https://github.com/dinghuang/dinghuang.github.io.git
branch: master
#开启swiftype搜索,后面的id在swiftype官网申请,[具体操作][3]
swiftype_key: HcRPHRrBuwozvgUoLNyX
#要放到仓库的静态资源都放在source文件夹中,.md会被转换成HTML,所以这里要忽略
skip_render: README.md

  1. 配置NEXT主题
    设置主题配置文件./themes/next/_config.yml,可以参考本项目的配置。
    NEXT强大之处在于继承了很多第三方服务插件,不过类似评论搜索功能的插件被墙了,外网是可以用的。具体参考
  2. 设置标签分类
    参考链接,在文章开头,引入:
    1
    2
    3
    4
    5
    title: 设计模式
    date: 2018-07-18 09:43:00
    tags:
    - JAVA
    categories: JAVA

更多配置请参考官方文档

提交

  1. 输入命令
    npm install hexo-deployer-git --save
  2. 创建文章
    hexo new "你想要的文章标题填在这个双引号里"
  3. 文章会生成在
    ./source/_posts/设计模式.md
    进行修改后
    hexo clean ; hexo genarate
  4. 然后输入
    hexo deploy
    此时文章已经成功部署。
1234
强壮的病猫

强壮的病猫

学习、生活、闲谈、足球

33 日志
14 分类
14 标签
GitHub E-Mail Google
© 2017 — 2023 强壮的病猫