Hike News
Hike News

Kubernetes集群配置优化

一、Docker配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "10"
},
"bip": "169.254.123.1/24",
"oom-score-adjust": -1000,
"registry-mirrors": ["https://registry.docker-cn.com", "https://docker.mirrors.ustc.edu.cn"],
"storage-driver": "overlay2",
"storage-opts":["overlay2.override_kernel_check=true"],
"data-root": "/var/lib/docker",
"live-restore": true
}

data-root

设置存储持久数据和资源配置的目录,默认值/var/lib/docker17.05.0以下版本参数为:graph

如果只有一个系统盘可以不用指定该参数,如果有数据盘并且数据盘不是/var,则需要指定数据目录。

registry-mirrors

镜像仓库地址,用于镜像加速。

直接连接国外dockerHub拉取镜像速度太慢,一般都会配置国内的镜像地址。

bip

设置docker0网桥地址,默认是172.17.0.1/16

默认网段有可能和现有服务器的网段冲突,建议设置成169.254.123.1/24

live-restore

当docker daemon停止时,让容器继续运行,默认false关闭,设为true打开。

默认情况下重启或者关闭docker daemon时,容器都会停止,通过设置live-restore让容器继续运行,在修改docker配置或者升级docker的时候很有用。(如果daemon重启之后使用了不同的bip或不同的data-root,则live restore无法正常工作)

log-opts

docker日志设置,默认为空。

不设置最大值的话日志文件可能会撑爆硬盘。

exec-opts

设置docker cgroup驱动。

需要注意docker的Cgroups和kubelet的Cgroups配置是否一致。

二、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
--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf 
--kubeconfig=/etc/kubernetes/kubelet.conf
--pod-manifest-path=/etc/kubernetes/manifests
--allow-privileged=true --network-plugin=cni
--cni-conf-dir=/etc/cni/net.d
--cni-bin-dir=/opt/cni/bin
--cluster-dns=192.168.176.10
--pod-infra-container-image=registry-vpc.cn-shanghai.aliyuncs.com/acs/pause-amd64:3.1
--enable-controller-attach-detach=false
--enable-load-reader
--cluster-domain=cluster.local
--cloud-provider=external
--hostname-override=$(NODE_NAME)
--provider-id=cn-shanghai.i-uf657oy682jirk1oad62
--authorization-mode=Webhook
--client-ca-file=/etc/kubernetes/pki/ca.crt
--system-reserved=memory=300Mi
--kube-reserved=memory=400Mi
--eviction-hard=imagefs.available<15%,memory.available<300Mi,nodefs.available<10%,nodefs.inodesFree<5%
--cgroup-driver=systemd
--anonymous-auth=false
--rotate-certificates=true
--cert-dir=/var/lib/kubelet/pki
--root-dir=/var/lib/kubelet

pod-infra-container-image

基础镜像,默认使用的k8s.gcr.io/pause-amd64:3.1需要翻墙,可以使用国内镜像或者自己上传的镜像。

root-dir

设置存储持久数据和资源配置的目录,默认值/var/lib/kubelet

用法同Docker的data-root参数。

cgroup-driver

设置kubelet Cgroups驱动。

需要注意docker的Cgroups和kubelet的Cgroups配置是否一致。

system-reserved

给系统保留的资源大小,可以设置CPU、内存、硬盘。

kube-reserved

给kubernetes保留的资源大小,可以设置CPU、内存、硬盘。

eviction-hard

驱逐条件,当节点资源小于设置的驱逐条件时,kubelet开始驱逐Pod。

三、Kube-proxy

1
2
3
4
- --proxy-mode=ipvs
- --kubeconfig=/var/lib/kube-proxy/kubeconfig.conf
- --cluster-cidr=172.17.0.0/18
- --hostname-override=$(NODE_NAME)

proxy-mode

ipvs可以大大提高性能。

参考资料:

Windows电脑本地开发时连接Apollo配置中心启动慢

表现:

启动时中间停顿了10秒左右

start

原因:

电脑上网卡太多

ip

解决方法:

  1. 禁用多余网卡

  2. 在启动参数指定本机IP:-Dhost.ip=10.208.202.251

    10.208.202.251改成自己的IP地址

原理:

指定本机IP后就会跳过查找有效IP这一步

具体代码

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
package com.ctrip.framework.foundation.internals;

import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;

public enum NetworkInterfaceManager {
INSTANCE;

private InetAddress m_local;

private InetAddress m_localHost;

private NetworkInterfaceManager() {
load();
}

public InetAddress findValidateIp(List<InetAddress> addresses) {
InetAddress local = null;
int maxWeight = -1;
for (InetAddress address : addresses) {
if (address instanceof Inet4Address) {
int weight = 0;

if (address.isSiteLocalAddress()) {
weight += 8;
}

if (address.isLinkLocalAddress()) {
weight += 4;
}

if (address.isLoopbackAddress()) {
weight += 2;
}

// has host name
// TODO fix performance issue when calling getHostName
if (!Objects.equals(address.getHostName(), address.getHostAddress())) {
weight += 1;
}

if (weight > maxWeight) {
maxWeight = weight;
local = address;
}
}
}
return local;
}

public String getLocalHostAddress() {
return m_local.getHostAddress();
}

public String getLocalHostName() {
try {
if (null == m_localHost) {
m_localHost = InetAddress.getLocalHost();
}
return m_localHost.getHostName();
} catch (UnknownHostException e) {
return m_local.getHostName();
}
}

private String getProperty(String name) {
String value = null;

value = System.getProperty(name);

if (value == null) {
value = System.getenv(name);
}

return value;
}

private void load() {
String ip = getProperty("host.ip");//除了启动参数也可以在环境变量中指定host.ip

if (ip != null) {//如果指定了host.ip就直接拿指定IP的网卡信息
try {
m_local = InetAddress.getByName(ip);
return;
} catch (Exception e) {
System.err.println(e);
// ignore
}
}

try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
List<NetworkInterface> nis = interfaces == null ? Collections.<NetworkInterface>emptyList() : Collections.list(interfaces);
List<InetAddress> addresses = new ArrayList<InetAddress>();
InetAddress local = null;

try {
for (NetworkInterface ni : nis) {
if (ni.isUp() && !ni.isLoopback()) {
addresses.addAll(Collections.list(ni.getInetAddresses()));
}
}
local = findValidateIp(addresses);
} catch (Exception e) {
// ignore
}
if (local != null) {
m_local = local;
return;
}
} catch (SocketException e) {
// ignore it
}

m_local = InetAddress.getLoopbackAddress();
}
}

注:

  • 官方说还可以通过改host的方式实现,但是我在win10电脑上没有成功。
  • 1.4修复了该问题

参考链接:https://github.com/ctripcorp/apollo/wiki/%E9%83%A8%E7%BD%B2&%E5%BC%80%E5%8F%91%E9%81%87%E5%88%B0%E7%9A%84%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98#6-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%A4%9A%E5%9D%97%E7%BD%91%E5%8D%A1%E9%80%A0%E6%88%90%E8%8E%B7%E5%8F%96ip%E4%B8%8D%E5%87%86%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3

https://github.com/ctripcorp/apollo/issues/1457

Kubernetes介绍

有了Docker之后,单个应用进程的使用管理变的更简单了,但是如果想要管理一个集群的不同应用,处理应用之间的关系,

比如:一个Web应用与数据库之间的访问关系,负载均衡和后端服务之间的代理关系,门户应用与授权组件的调用关系,

再比如说一个服务单位的不同功能之间的关系,如一个Web应用与日志搜集组件之间的文件交换关系,

在传统虚拟机环境对这种关系的处理方法都比较“粗颗粒”,一股脑的部署在同一台虚拟机中,需要手动维护很多跟应用协作的守护进程,用来处理它的日志搜集、灾难恢复、数据备份等辅助工作。

使用容器和编排技术以后,那些原来挤在同一个虚拟机里的各个应用、组件、守护进程,都可以被分别做成镜像,然后运行在一个个专属的容器中。

它们之间互不干涉,拥有各自的资源配额,可以被调度在整个集群里的任何一台机器上。

如果只是用来封装微服务、拉取用户镜像、调度单容器,用Docker Swarm就足够了,而且方便有效,但是如果需要路由网关、水平扩展、弹性伸缩、监控、备份、灾难恢复等一系列运维能力,就需要使用kubernetes来解决了。

Kubernetes介绍

kubernetes

Kubernetes是Google开源的容器编排调度引擎,它的目标不仅仅是一个编排系统,而是提供一个规范,可以用来描述集群的架构,定义服务的最终状态,Kubernetes可以将系统自动得到和维持在这个状态。

更直白的说,Kubernetes用户可以通过编写一个yaml或者json格式的配置文件,也可以通过工具/代码生成或直接请求Kubernetes API创建应用,该配置文件中包含了用户想要应用程序保持的状态,不论整个Kubernetes集群中的个别主机发生什么问题,都不会影响应用程序的状态,你还可以通过改变该配置文件或请求Kubernetes API来改变应用程序的状态。

配置文件里面怎么定义的,整个系统就是怎么运行的。

kubernetes 的马斯洛需求

image

Kubernetes架构图

img

Kubernets属于主从的分布式集群架构,包含Master和Node:

  1. Master作为控制节点,调度管理整个系统,包含以下组件:

    • API Server作为kubernetes系统的入口,封装了核心对象的增删改查操作,以RESTFul接口方式提供给外部客户和内部组件调用。它维护的REST对象将持久化到etcd(一个分布式强一致性的key/value存储)。
    • Scheduler:负责集群的资源调度,为新建的pod分配机器。这部分工作分出来变成一个组件,意味着可以很方便地替换成其他的调度器。
    • Controller Manager:负责执行各种控制器,目前有两类:
      (1)Endpoint Controller:定期关联service和pod(关联信息由endpoint对象维护),保证service到pod的映射总是最新的。
      (2)Replication Controller:定期关联replicationController和pod,保证replicationController定义的复制数量与实际运行pod的数量总是一致的。
  2. Node是运行节点,运行业务容器,包含以下组件:

    • Kubelet:负责管控docker容器,如启动/停止、监控运行状态等。它会定期从etcd获取分配到本机的pod,并根据pod信息启动或停止相应的容器。同时,它也会接收apiserver的HTTP请求,汇报pod的运行状态。
    • Kube Proxy:负责为pod提供代理。它会定期从etcd获取所有的service,并根据service信息创建代理。当某个客户pod要访问其他pod时,访问请求会经过本机proxy做转发。

    Kubernets使用Etcd作为存储和通信中间件,实现Master和Node的一致性,这是目前比较常见的做法,典型的SOA架构,解耦Master和Node。

基本概念

img

  • Pod

    Pod是Kubernetes的基本操作单元,把相关的一个或多个容器构成一个Pod,通常Pod里的容器运行相同的应用。Pod包含的容器运行在同一个Node(Host)上,看作一个统一管理单元,共享相同的volumes和network namespace/IP和Port空间。

  • Replication Controller

    Replication Controller确保任何时候Kubernetes集群中有指定数量的pod副本(replicas)在运行, 如果少于指定数量的pod副本(replicas),Replication Controller会启动新的Container,反之会杀死多余的以保证数量不变。

    Replication Controller使用预先定义的pod模板创建pods,一旦创建成功,pod 模板和创建的pods没有任何关联,可以修改pod 模板而不会对已创建pods有任何影响,也可以直接更新通过Replication Controller创建的pods。

    对于利用pod 模板创建的pods,Replication Controller根据label selector来关联,通过修改pods的label可以删除对应的pods。

  • Service

    Service也是Kubernetes的基本操作单元,是真实应用服务的抽象,每一个服务后面都有很多对应的容器来支持,通过Proxy的port和服务selector决定服务请求传递给后端提供服务的容器,对外表现为一个单一访问接口,外部不需要了解后端如何运行,这给扩展或维护后端带来很大的好处。

  • Label

    Label是用于区分Pod、Service、Replication Controller的key/value键值对,Pod、Service、 Replication Controller可以有多个label,但是每个label的key只能对应一个value。Labels是Service和Replication Controller运行的基础,为了将访问Service的请求转发给后端提供服务的多个容器,正是通过标识容器的labels来选择正确的容器。同样,Replication Controller也使用labels来管理通过pod 模板创建的一组容器,这样Replication Controller可以更加容易,方便地管理多个容器,无论有多少容器。

Pod的意义

Kubernetes 使用 Pod 来管理容器,每个 Pod 可以包含一个或多个紧密关联的容器。

Pod 是一组紧密关联的容器集合,它们共享 PID、IPC、Network 和 UTS namespace,是 Kubernetes 调度的基本单位。

Pod 内的多个容器共享网络和文件系统,可以通过进程间通信和文件共享这种简单高效的方式组合完成服务。

img

Pod的意义

容器都是单进程运行的,不是因为容器只能运行一个进程,而是容器没有管理多个进程的能力。

容器里PID=1的进程就是应用本身,其他的进程都是这个PID=1进程的子进程。如果启动了第二个进程,那这第二个进程异常退出的时候,外部并不能感知,也就管理不了。

在一个真正的操作系统里,进程并不是“孤苦伶仃”地独自运行的,应用之间可能有着密切的协作关系,必须部署在同一台机器上。

这些密切关系包括但不限于:互相之间会发生直接的文件交换,使用localhost或者Socket文件进行本地通信,会发生非常频繁的远程调用,共享某些Linux Namespace。

Pod的实现原理

首先,Pod只是一个逻辑概念,是一组共享了某些资源的容器。

Kubernetes真正处理的,还是宿主机操作系统上Linux容器的Namespace和Cgroups,而并不存在一个所谓的Pod的边界或者隔离环境。

Kubernetes里,Pod的实现需要使用一个中间容器,叫做Infra容器,在Pod中,Infra容器永远都是第一个被创建的容器。

其他用户自定义的容器,通过Join Network Namespace的方式与Infra容器关联在一起。

img

如图所示,这个Pod里除了两个用户容器,还有一个Infra容器。

这个Infra容器占用极少的资源,使用了一个非常特殊的镜像:k8s.gcr.io/pause,这个镜像永远处于“暂停”状态,100-200KB左右大小。

Infra容器生成NetWork Namespace后,用户容器加入Infra容器的Network Namespace中,用户容器和Infra容器在宿主机上的Namespace文件是一样的。

这意味着,对Pod中的容器A和容器B来说:

  • 它们可以直接使用localhost进行通信;
  • 他们看到的网络设备和Infra容器的一样;
  • 一个Pod只有一个IP地址,也就是容器A、容器B、Infra容器的ip地址一致;
  • 所有的网络资源被改Pod中的所有容器共享;
  • Pod的生命周期只跟Infra容器一致,与容器A和B无关。

对于同一个Pod里面的所有用户容器来说,它们的进出流量,都是通过Infra容器完成的。

Pod的本质,实际上是在扮演传统基础设施“虚拟机”的角色;容器则是这个虚拟机里运行的用户进程。

使用apollo配置中心生成雪花算法的workerId

前言

在分布式系统中,有一些需要使用全局唯一ID的场景,这时候雪花算法(snowflake)就是一种不错的解决方式。

但是雪花算法需要用到一个workerId,同一个应用部署多个实例的时候workerId不能相同。

使用环境变量或者启动参数来指定workerId的方式虽然在一定程度上解决了workerId的问题,但是给容器环境下的自动化部署或者动态扩容带来了新的挑战。

Apollo配置中心介绍

Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。

GitHub地址:https://github.com/ctripcorp/apollo
Apollo架构图

Apollo分为Client、Config Service、Admin Service、Portal四个部分:

  • Client是一个jar包集成在业务应用里,通过Meta Server从Config Service获取配置信息;
  • Config Service提供配置的读取、推送等功能,服务对象是Apollo客户端(Client);
  • Admin Service提供配置的修改、发布等功能,服务对象是Apollo管理界面(Portal);
  • Portal通过Meta Server连接Admin Service修改、发布配置信息。

ConfigDB存储配置信息,由Config Service和Admin Service使用;

PortalDB存储权限和审计信息,由Portal使用。

改造Config Service生成workerId

原理介绍

Client端通过/configs/{appId}/{clusterName}/{namespace:.+}接口拉取全量配置信息,对应方法是com.ctrip.framework.apollo.configservice.controller.ConfigController#queryConfig。方法的返回值ApolloConfig有5个属性:appIdclusternamespaceNameconfigurationsreleaseKey,只要把生成的workerId放到configurations这个Map类型的属性中,Client端就能直接通过Key值拿到workerId来使用。

具体实现

一、数据库改动

ConfigDB库的instance表存储了使用配置的应用实例。

在Instance表新加一个NodeId字段用来存储生成的workerId,并且将AppId和NodeId设置为唯一索引:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE `instance` (
`Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id',
`AppId` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'AppID',
`ClusterName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'ClusterName',
`DataCenter` varchar(64) NOT NULL DEFAULT 'default' COMMENT 'Data Center Name',
`Ip` varchar(32) NOT NULL DEFAULT '' COMMENT 'instance ip',
`NodeId` int(5) DEFAULT NULL COMMENT '节点id',
`DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`DataChange_LastTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间',
PRIMARY KEY (`Id`),
UNIQUE KEY `IX_UNIQUE_KEY` (`AppId`,`ClusterName`,`Ip`,`DataCenter`),
UNIQUE KEY `uk_appId_nodeId` (`AppId`,`NodeId`) USING BTREE,
KEY `IX_IP` (`Ip`),
KEY `IX_DataChange_LastTime` (`DataChange_LastTime`)
) ENGINE=InnoDB AUTO_INCREMENT=6779 DEFAULT CHARSET=utf8mb4 COMMENT='使用配置的应用实例';

二、代码改动

原来Instance表的记录是在获取配置时调用com.ctrip.framework.apollo.configservice.util.InstanceConfigAuditUtil#audit异步添加的,这里需要改成获取配置时同步添加实例信息并且放到ApolloConfigconfigurations属性中。

1.com.ctrip.framework.apollo.configservice.controller.ConfigController

queryConfig方法返回前获取实例信息并返回

1
2
3
4
5
6
Instance instance = instanceService.findInstance(appId, clusterName, dataCenter, clientIp);
Integer nodeId = instance.getNodeId();
if (nodeId == null) {
nodeId = -1;
}
apolloConfig.addConfig("apollo.node.id", String.valueOf(nodeId));

2.com.ctrip.framework.apollo.biz.service.InstanceService

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
@Value("${apollo.node.minId}")//workerId最小值,0-1023之间
private Integer minId;

@Value("${apollo.node.maxId}")//workerId最大值,0-1023之间
private Integer maxId;

private static final Integer DEFAULT_NODE_ID = null;

public Instance findInstance(String appId, String clusterName, String dataCenter, String ip) {
Instance instance = instanceRepository.findByAppIdAndClusterNameAndDataCenterAndIp(appId, clusterName,
dataCenter, ip);
if (instance == null) {
instance = createInstance(appId, clusterName, dataCenter, ip);
}
if (instance.getNodeId() == null) {
updateInstance(instance);
}
return instance;
}

private Instance createInstance(String appId, String clusterName, String dataCenter, String ip) {
Instance instance = new Instance();
instance.setAppId(appId);
instance.setClusterName(clusterName);
instance.setDataCenter(dataCenter);
instance.setIp(ip);
instance.setNodeId(generateNodeId(appId));
try {
instance = createInstance(instance);
} catch (DataIntegrityViolationException e) {
//使用数据库的唯一约束来保证workerId的唯一性
logger.error("createInstance.error,instance={}", instance);
instance = findInstance(appId, clusterName, dataCenter, ip);
if (instance == null) {
instance = createInstance(appId, clusterName, dataCenter, ip);
}
}
return instance;
}

private void updateInstance(Instance instance) {
try {
instance.setNodeId(generateNodeId(instance.getAppId()));
instanceRepository.save(instance);
} catch (DataIntegrityViolationException e) {
logger.error("updateInstance.error,instance={}", instance);
updateInstance(instance);
}
}

private Integer generateNodeId(String appId) {
//生成规则是当前appId最大workerId + 1
//如果workerId已经到最大值了就拿最早的workerId(这里假设一个应用不会同时有1023(maxId)个实例在运行)
Integer maxNodeId = instanceRepository.maxNodeIdByAppId(appId);
if (maxNodeId == null || minId > maxNodeId) {
return minId;
}

if (maxNodeId < maxId) {
return maxNodeId + 1;
}

return getEarliestNodeId(appId);
}

private Integer getEarliestNodeId(String appId) {
InstanceConfig instanceConfig = instanceConfigRepository.findTopByConfigAppIdOrderByReleaseDeliveryTime(appId);
if (instanceConfig == null) {
return DEFAULT_NODE_ID;
}
Instance instance = instanceRepository.findOne(instanceConfig.getInstanceId());
if (instance == null) {
return DEFAULT_NODE_ID;
}
Integer nodeId = instance.getNodeId();
instance.setNodeId(null);
instanceRepository.save(instance);
return nodeId;
}

三、使用方式

和其他配置的使用方式一样,直接用@Value("${apollo.node.id}")

这种方式生成的workerId可以直接使用在只需要一个workerId参数的雪花算法,另一种需要datacenterId和workerId两个参数的雪花算法在使用时需要做一下转换:

1
2
datacenterId=apollo.node.id >> 5
workerId=apolo.node.id & 31

取高五位的值作为datacenterId,取低五位的值作为workerId。

  • 一个workerId参数的雪花算法,workerId最大1023,二进制为1111111111;
  • datacenterId和workerId两个参数的雪花算法,datacenterId最大31,二进制为11111,workerId最大31,二进制为11111,datacenterId作为二进制的高5位,workerId作为二进制的低5位,组合起来1111111111,十进制为1023。

修改Kubernetes集群Pod容器的内核参数

容器的本质是一个进程,共享Node的内核。原以为修改了Node的内核参数容器中也会改,但实际上并不是这样,容器的内核参数可以和Node不同。

Docker Daemon

1
docker run -it --rm --sysctl net.core.somaxconn=65535 busybox cat /proc/sys/net/core/somaxconn

多个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
docker run -itd --restart=always --net=host \
--name=centos01 --hostname=centos01 \
--sysctl kernel.msgmnb=13107200 \
--sysctl kernel.msgmni=256 \
--sysctl kernel.msgmax=65536 \
--sysctl kernel.shmmax=69719476736 \
--sysctl kernel.sem='500 256000 250 1024' \
-v /mnt:/update \
centos /bin/bash


docker exec centos01 sysctl -a |grep -E \
'kernel.msgmnb|kernel.msgmni|kernel.msgmax|kernel.shmmax|kernel.sem'

Kubernetes

Kubernetes Sysctls

  1. 增加Kubelet启动参数

    1
    kubelet --allowed-unsafe-sysctls=net.*
  2. 重新加载配置

    1
    systemctl daemon-reload
  3. 重启kubelet

    1
    systemctl restart kubelet
  4. 配置Pod的securityContext参数(注意是pod不是container)

1
2
3
4
securityContext:
sysctls:
- name: net.ipv4.tcp_keepalive_time
value: "180"

Kubernetes允许配置的内核参数如下:

1
2
3
4
5
kernel.shm*,
kernel.msg*,
kernel.sem,
fs.mqueue.*,
net.*.

使用Kubernetes Sysctls推荐指定几台Node,利用污点或者节点亲和性把需要修改内核参数的pod调度到指定节点,而不是修改所有Node。

Kubernetes Init Container

使用init container不需要修改kubelet参数

1
2
3
4
5
6
7
8
9
initContainers:
- command:
- sysctl
- -w
- net.ipv4.tcp_keepalive_time=180
image: busybox:1.27
name: init-sysctl
securityContext:
privileged: true

参考资料

使用同一个deployment实现金丝雀(灰度)发布

一、发布前

发布前,nginx运行三个实例(pod),副本(replicaset)版本编号855b564c8d,此时运行状态如下:

1
2
3
nginx-855b564c8d-5qhk7                        1/1       Running            0          12h       172.20.199.135   10.22.19.14   <none>
nginx-855b564c8d-hclp9 1/1 Running 0 12h 172.20.235.210 10.22.19.5 <none>
nginx-855b564c8d-n7kn8 1/1 Running 0 12h 172.20.35.53 10.22.19.17 <none>

二、发布中

步骤一:发布

此时进行发布,新副本版本编号77b6c7b585,此时发布状态如下:

1
2
3
4
nginx-77b6c7b585-vzc9g                         0/1       Running            0          21s       172.20.42.156    10.22.19.15   <none>
nginx-855b564c8d-5qhk7 1/1 Running 0 12h 172.20.199.135 10.22.19.14 <none>
nginx-855b564c8d-hclp9 1/1 Running 0 12h 172.20.235.210 10.22.19.5 <none>
nginx-855b564c8d-n7kn8 1/1 Running 0 12h 172.20.35.53 10.22.19.17 <none>

步骤二:暂停

执行暂停命令kubectl rollout pause deployment -n qa nginx

等待第一个实例运行成功,此时状态如下:

1
2
3
4
nginx-77b6c7b585-vzc9g                         1/1       Running            0          1m        172.20.42.156    10.22.19.15   <none>
nginx-855b564c8d-5qhk7 1/1 Running 0 12h 172.20.199.135 10.22.19.14 <none>
nginx-855b564c8d-hclp9 1/1 Running 0 12h 172.20.235.210 10.22.19.5 <none>
nginx-855b564c8d-n7kn8 1/1 Running 0 12h 172.20.35.53 10.22.19.17 <none>

此时为暂停发布状态,三个旧版本实例正常运行,一个新版本实例运行成功,流量将分摊到这四个实例上。

步骤三:恢复

执行恢复命令kubectl rollout resume deployment -n qa nginx

此时状态如下:

1
2
3
4
nginx-77b6c7b585-4bt6h                         0/1       Running            0          33s       172.20.190.56    10.22.19.6    <none>
nginx-77b6c7b585-g4wnx 1/1 Running 0 1m 172.20.42.172 10.22.19.15 <none>
nginx-855b564c8d-5qhk7 1/1 Running 0 12h 172.20.199.135 10.22.19.14 <none>
nginx-855b564c8d-n7kn8 1/1 Running 0 12h 172.20.35.53 10.22.19.17 <none>

恢复后会执行正常的滚动升级操作,即新增一个新副本实例,删除一个旧副本实例。
滚动升级时,新旧版本同时存在,总实例最大数量不会超过目标数量的25%,最小数量不会低于目标数量的75%。

三、发布完成

实例的副本版本编号都变成77b6c7b585,说明全部升级成功,此时状态如下:

1
2
3
nginx-77b6c7b585-4bt6h                       1/1       Running            0          36m       172.20.190.56    10.22.19.6    <none>
nginx-77b6c7b585-g4wnx 1/1 Running 0 36m 172.20.42.172 10.22.19.15 <none>
nginx-77b6c7b585-kcsqm 1/1 Running 0 2m 172.20.235.253 10.22.19.5 <none>

四、回滚

发布中发现功能与预期不符,需要取消发布并回滚至上个发布版本,此时可以执行回滚操作。
执行回滚命令kubectl rollout undo deployment -n qa nginx
回滚操作在发布中,发布后都可以执行。

发布中回滚

当暂停发布进行灰度验证时,发现功能不符合预期,需要取消发布。
暂停时灰度验证状态:

1
2
3
4
nginx-77b6c7b585-vzc9g                         1/1       Running            0          2m        172.20.42.156    10.22.19.15   <none>
nginx-855b564c8d-5qhk7 1/1 Running 0 12h 172.20.199.135 10.22.19.14 <none>
nginx-855b564c8d-hclp9 1/1 Running 0 12h 172.20.235.210 10.22.19.5 <none>
nginx-855b564c8d-n7kn8 1/1 Running 0 12h 172.20.35.53 10.22.19.17 <none>

回滚后状态:

1
2
3
nginx-855b564c8d-5qhk7                        1/1       Running            0          12h       172.20.199.135   10.22.19.14   <none>
nginx-855b564c8d-jqbjk 1/1 Running 0 2m 172.20.235.215 10.22.19.5 <none>
nginx-855b564c8d-n7kn8 1/1 Running 0 12h 172.20.35.53 10.22.19.17 <none>

副本编号变成855b564c8d,说明回滚成功,恢复到新版本。

发布完成后回滚

全部升级成功后,发现功能异常,回滚到之前版本。
回滚前状态:

1
2
3
nginx-77b6c7b585-4bt6h                        1/1       Running            0          36m       172.20.190.56    10.22.19.6    <none>
nginx-77b6c7b585-g4wnx 1/1 Running 0 36m 172.20.42.172 10.22.19.15 <none>
nginx-77b6c7b585-kcsqm 1/1 Running 0 2m 172.20.235.253 10.22.19.5 <none>

回滚后状态:

1
2
3
nginx-855b564c8d-5qhk7                      1/1       Running            0          6m       172.20.199.135   10.22.19.14   <none>
nginx-855b564c8d-jqbjk 1/1 Running 0 6m 172.20.235.215 10.22.19.5 <none>
nginx-855b564c8d-n7kn8 1/1 Running 0 6m 172.20.35.53 10.22.19.17 <none>

滚动升级状态

目标实例数3个,旧实例数3个,总实例数3个:

1
2
3
nginx-855b564c8d-5qhk7                       1/1       Running            0          12h       172.20.199.135   10.22.19.14   <none>
nginx-855b564c8d-hclp9 1/1 Running 0 12h 172.20.235.210 10.22.19.5 <none>
nginx-855b564c8d-n7kn8 1/1 Running 0 12h 172.20.35.53 10.22.19.17 <none>

目标实例数3个,旧实例数3个,新实例数1个,总实例数4个:

1
2
3
4
nginx-77b6c7b585-vzc9g                        1/1       Running            0          1m        172.20.42.156    10.22.19.15   <none>
nginx-855b564c8d-5qhk7 1/1 Running 0 12h 172.20.199.135 10.22.19.14 <none>
nginx-855b564c8d-hclp9 1/1 Running 0 12h 172.20.235.210 10.22.19.5 <none>
nginx-855b564c8d-n7kn8 1/1 Running 0 12h 172.20.35.53 10.22.19.17 <none>

目标实例数3个,旧实例数2个,新实例数2个,总实例数4个:

1
2
3
4
nginx-77b6c7b585-pmw2f                       1/1       Running            0          15s       172.20.190.51    10.22.19.6    <none>
nginx-77b6c7b585-vzc9g 1/1 Running 0 3m 172.20.42.156 10.22.19.15 <none>
nginx-855b564c8d-5qhk7 1/1 Running 0 12h 172.20.199.135 10.22.19.14 <none>
nginx-855b564c8d-n7kn8 1/1 Running 0 12h 172.20.35.53 10.22.19.17 <none>

目标实例数3个,旧实例数1个,新实例数3个,总实例数4个

1
2
3
4
nginx-77b6c7b585-4bt6h                        1/1       Running            0          35m       172.20.190.56    10.22.19.6    <none>
nginx-77b6c7b585-g4wnx 1/1 Running 0 35m 172.20.42.172 10.22.19.15 <none>
nginx-77b6c7b585-kcsqm 1/1 Running 0 1m 172.20.235.253 10.22.19.5 <none>
nginx-855b564c8d-5qhk7 1/1 Running 0 13h 172.20.199.135 10.22.19.14 <none>

目标实例数3个,新实例数3个,总实例数3个

1
2
3
nginx-77b6c7b585-4bt6h                        1/1       Running            0          36m       172.20.190.56    10.22.19.6    <none>
nginx-77b6c7b585-g4wnx 1/1 Running 0 36m 172.20.42.172 10.22.19.15 <none>
nginx-77b6c7b585-kcsqm 1/1 Running 0 2m 172.20.235.253 10.22.19.5 <none>

配置Jenkins打包Vue项目

一、下载NodeJS插件

进入插件管理

在可选插件里面安装NodeJS

等待安装完成

二、配置NodeJS插件

进入全局工具配置

新增NodeJS

三、配置Job

配置执行shell

shell命令:

1
2
3
4
5
6
7
# 打包
node -v
npm -v
npm install chromedriver --chromedriver_cdnurl=http://cdn.npm.taobao.org/dist/chromedriver
npm install
rm -rf dist
npm run build

cnpm:

1
2
3
4
5
6
7
8
9
# 打包
node -v
npm -v
npm install -g cnpm --registry=https://registry.npm.taobao.org
#cnpm install rimraf -g
#rimraf node_modules
cnpm install
rm -rf dist
npm run build

升级Vue:

1
2
3
4
5
6
7
8
9
10
11
# 打包
node -v
npm -v
npm install chromedriver --chromedriver_cdnurl=http://cdn.npm.taobao.org/dist/chromedriver
npm install -g @vue/cli
cnpm install -s vue-template-compiler
vue -V
rm -rf node_modules
cnpm install
rm -rf dist
npm run build

常用的IntelliJ IDEA 插件推荐

一、Free Mybatis plugin

https://plugins.jetbrains.com/plugin/8321-free-mybatis-plugin/

1.把mybatis的xml文件和代码关联上,可以在mapper接口直接跳转到对应的SQL语句。

mybatis

2.如果没有对应的SQL语句,可以快捷补全(当然具体SQL语句还是要自己写)。

sql

二、Maven Helper

https://plugins.jetbrains.com/plugin/7179-maven-helper/

简单、方便的查看maven依赖和冲突

maven helper

maven helper

三、GenerateAllSetter

https://plugins.jetbrains.com/plugin/9360-generateallsetter/

一键调用一个对象的所有的set方法

generateAllSetter

四、RestfulToolkit

https://plugins.jetbrains.com/plugin/10292-restfultoolkit/

通过URL查询对应的方法,可以根据请求方法过滤,另外支持简单的HTTP调用。

restfulToolkit

五、Key Promoter X

https://plugins.jetbrains.com/plugin/9792-key-promoter-x/

帮助我们快速的掌握 IntelliJ IDEA的快捷键。

六、GsonFormat

https://plugins.jetbrains.com/plugin/7654-gsonformat/

快速方便的把json字符串转换为实体类(快捷键ALT+S)。

七、String Manipulation

https://plugins.jetbrains.com/plugin/2162-string-manipulation/

快捷方便的把字符串在驼峰、大小写等格式之间转换(快捷键Alt+M)。

Screenshot 2

八、Alibaba Java Coding Guidelines

https://plugins.jetbrains.com/plugin/10046-alibaba-java-coding-guidelines/

扫描代码是否符合规范

九、Lombok

https://plugins.jetbrains.com/plugin/6317-lombok/

简化java bean的代码量,自动生成get set toString EqualsAndHashCode 构造器。

需要引入依赖包

1
2
3
4
5
> <dependency>  
> <groupId>org.projectlombok</groupId>
> <artifactId>lombok</artifactId>
> </dependency>
>

十、.ignore

https://plugins.jetbrains.com/plugin/7495--ignore/

使用协作或者打包工具时,方便的处理要忽略的文件和文件夹。

Screenshot 1

十一、Vue.js

https://plugins.jetbrains.com/plugin/9442-vue-js/

用IDEA开发Vue的插件。

十二、element

https://plugins.jetbrains.com/plugin/10524-element/

饿了么开源前端框架element的插件。

element

Docker的本质

Docker容器其实是一种沙盒技术,能够像一个集装箱一样,把应用“装”起来。

这样应用与应用之间,就因为有了边界而不会相互干扰

被装进集装箱的应用,也可以被方便地搬来搬去

容器的本质,是由Namespace、Cgroups和rootfs三种技术构建出来的进程的隔离环境。

Docke容器技术的核心功能,是通过约束和修改进程的动态表现,从而为其创造出一个“边界”。

Namespace技术是用来修改进程视图的主要方法,Cgroups技术是用来制造约束的主要手段,rootfs技术是用来限制进程文件系统的主要方式。

Namespace:

正常情况下,每当我们在宿主机上运行一个程序的时候,操作系统都会给它分配一个进程编号,比如PID=100。这是进程的唯一标识,就像员工的工号一样。

而通过Docker把程序运行在容器中的时候,利用Namespace技术给进程施一个“障眼法”,让它看不到其他的进程,误以为只有它一个进程。

这种机制对被隔离的应用的进程空间做了限制,使其只能看到重新计算过的进程编号,比如PID=1,但事实上它在宿主机的操作系统里,还是原来的100号进程。

除了PID Namespace,Linux系统还提供了Mount、UTS、IPC、Network和User这些Namespace,用来对各种不同的进程上下文进行“障眼法”操作。

比如用Mount Namespace让被隔离的进程只能看到当前Namespace里的挂载点信息;Network Namespace让被隔离的进程只能看到当前Namespace的网络设备和配置。

所以,实际上Docker容器只是指定了这个进程所需要启用的一组Namespace参数,从而让进程只能看到当前Namespace所限定的资源、文件、设备、状态和配置等,对宿主机和其他不相关的程序完全看不到。

所以也可以说,容器其实就是一种特殊的进程而已。

虚拟机和Docker容器比较

img

在理解Docker的时候,通常会用这张图来比较虚拟机和Docker。

左边画出了虚拟机的工作原理:其中Hypervisor是虚拟机最主要的部分,它通过硬件虚拟化功能,模拟出运行一个操作系统需要的各种硬件,比如CPU、内存、I/O设备等等。然后它在这些虚拟的硬件上安装了一个新的操作系统,即Guest OS。

这样,用户的应用进程就可以运行在这个虚拟机的机器中,它能够看到的自然也只有Guest OS的文件和目录,以及这个机器里的虚拟设备。

右边用一个名为Docker Engine的软件替换了Hypervisor,把虚拟机的概念套在了容器上,实际上这个说法是不严谨的,跟真正存在的虚拟机不同,实际上并没有Docker Engine这一层。

Docker帮助用户启动的还是原来的应用进程,只不过在创建这些进程时给它们加上了各种各样的Namespace参数。

这样,这些进程就会觉得自己是各自PID Namespace里的第1号进程,只能看到各自Mount Namespace里挂载的目录和文件,只能访问各自Network Namespace里的网络设备,就好像运行在一个个独立的容器里。

img

Docker的架构其实这样画更合适

如果用虚拟化技术作为应用沙盒,就必须要由Hypervisor来负责创建虚拟机,这个虚拟机是真实存在的,并且它里面必须运行一个完整的Guest OS才能执行用户的应用进程。这就不可避免的带来的额外的资源消耗和占用。

根据实验,一个运行着CentOS的KVM虚拟机启动后,在不做优化的情况下,虚拟机自己就需要占用100~·200内存。此外,用户应用运行在虚拟机里面,对宿主机操作系统的调用就不可避免地进过虚拟化软件的拦截和处理,这本身又是一层性能损失。

使用Docker容器化后的用户应用,依然还是一个宿主机上的普通进程,这就意味着因为虚拟化而带来的性能损耗都是不存在的;

另一方面,使用Namespace作为隔离手段的容器并不需要单独的Guest OS,这使得容器额外的资源占用几乎可以忽略不计。

所以“敏捷”和“高性能”是容器相较于虚拟化最大的优势。

当然有利就有弊,Docker容器相较于虚拟化最大的弊端就是隔离的不彻底。

因为容器只是运行在宿主机上的一种特殊的进程,所以多个容器之间使用的还是同一个宿主机的操作系统内核。

另外,很多资源和对象不能被Namespace化,比如:时间。

Cgroups:

虽然容器内的进程在Namespace的作用下只能看到容器里的情况,但是在宿主机上,它作为第100号进程,与其它所有的进程之间依然是平等的竞争关系。

这就意味着,虽然这个进程表面上被隔离起来了,但它所能够使用到的资源(CPU、内存),却是可以随时被宿主机的其他进程(或者其他容器)占用。

当然这个进程也可能把所有的资源吃光,而Linux Cgroups就是Linux内核中用来为进程设置资源限制的一个重要功能。

跟Namespace的情况类似,Cgroups对资源的限制能力也有很多不完善的地方,比如/proc文件系统的问题。

/proc目录存储的是记录当前内核运行状态的一系列特殊文件,用户可以通过访问这些文件,查看系统以及当前正在运行的进程的信息,比如CPU使用情况,内存占用率等。

当我们在容器内执行top命令的时候,会发现它显示的信息是宿主机的CPU和内存,而不是当前容器的数据。

rootfs:

在Linux操作系统里,有一个名为chroot的命令,作用是帮助我们“change root file”,即改变进程的根目录到指定的位置。

而对于被chroot的进程来说,它并不知道自己的根目录已经被修改了。Mount Namespace就是基于chroot的不断改良被发明出来的。

为了让容器的根目录更加真实,一般会在这个容器的根目录下挂载一个完整操作系统的文件系统,比如Ubuntu 16.04的ISO。

这样,在容器启动之后,我们在容器里通过执行“ls /”查看根目录下的内容,就是Ubuntu 16.04的所有目录和文件。

这个挂载在容器根目录上,用来为容器进程提供隔离后执行环境的文件系统,就是容器镜像,专业名字rootfs(根文件系统)。

一个常见的rootfs,或者说容器镜像,会包含比如bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys test tmp usr var等的目录和文件。

进入容器后执行的/bin/bash,就是/bin目录下的可执行文件,与宿主机的/bin/bash完全不同。

rootfs只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。

所以说rootfs只包括了操作系统的“躯壳”,并没有包括操作系统的“灵魂”,同一台机器上的所有容器,都是共享宿主机操作系统的内核。

这意味着,容器进程需要配置内核参数、加载额外的内核模块,以及跟内核进行直接交互的时候,需要注意这些操作和依赖的对象,都是宿主机操作系统的内核,对该机器上的所有容器生效。

不过也真是由于rootfs,容器才有了一个被反复宣传至今的重要特性:一致性。

由于rootfs里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。

有了容器镜像“打包操作系统”的能力,这个最基础的依赖环境也变成了应用沙盒的一部分,这就赋予了容器的所谓一致性。

无论在本地、云端,还是在任何机器上,用户只需要解压打包好的容器镜像,那么这个应用运行所需要的完整的执行环境就被重现出来了。

这种深入到操作系统级别的运行环境一致性,打通了应用在本地开发和远端执行环境之间难以逾越的鸿沟。

另外,Docker在镜像的设计中,引入了层(layer)的概念,也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量的rootfs。

这个设计用到了一种叫做联合文件系统(Union File System)的能力,也叫UnionFS,最主要的功能是将多个不同位置的目录联合挂载到同一个目录下。

img

如图所示,容器中的rootfs分为只读层、Init层、可读写层。

只读层:包含了操作系统的一部分。

Init层:夹在只读层和可读写层中间,是Docker项目单独生成的一个内部层,用来存在/etc/hosts、/etc/resolv.conf等信息。

可读写层:用来存放修改rootfs后产生的增量,无论增、删、改,都发生在这里。

使用docker commit和push指令保存并上传被修改过的容器的时候,只读层和Init层不会有任何变化,变化的只是可读写层。

Dockerfile

实际操作中,我们不会使用docker commit命令来把一个运行中的docker容器提交成为一个docker镜像。

使用 docker commit 意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。

另外结合之前的分层,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。

这样每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到。

把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决,这个脚本就是 Dockerfile。

Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

每一个 RUN 的行为,就和手动docker commit建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。

Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。